Full SDK workspace: core, contracts, sdk, cli, browser-host, browser-kit, federation, omega-core, oracle, self-healing, strategies, tools, emacs. Clean extraction from the development monorepo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
257 lines
7.9 KiB
Markdown
257 lines
7.9 KiB
Markdown
# Architecture Notes
|
|
|
|
> Architect's design rationale for the typescript-federation package.
|
|
> Documents accommodation of Matrix-3 integration requirements while preserving federation invariants.
|
|
|
|
---
|
|
|
|
## Design Philosophy
|
|
|
|
This package implements a classic **Anti-Corruption Layer** (Fowler) sitting between:
|
|
|
|
* **Federation / Edge** (MXENV/MXURI semantics + mailbox transport constraints), and
|
|
* **Local component runtime** (your existing component system / handler contracts).
|
|
|
|
The trick is: **make the integration seam tolerant and observable**, while keeping the core routing/security invariants rigid.
|
|
|
|
This is implemented as a "strangler fig" compatibility layer, not a rewrite.
|
|
|
|
---
|
|
|
|
## Invariants (MUST NOT VIOLATE)
|
|
|
|
The invariants live in the **federation boundary**, not in the developer ergonomics:
|
|
|
|
| Invariant | Description |
|
|
|-----------|-------------|
|
|
| **Mailbox pattern (no wildcards)** | EdgeGateway subscribes to *exact* ingress topics (`public.ingress.<realm>` and/or `mx/in/1/<realm>/00`) |
|
|
| **Semantic URI ≠ delivery topic** | MXURI parsing canonicalizes to `toRealm` + `toPath`; delivery goes to mailbox topic |
|
|
| **Anti-relay** | Block any attempt to turn EdgeGateway into an open relay; enforce `replyTo.realm == from.realm` |
|
|
| **Replay / rate limit / policy** | Centralized at ingress boundary, not on local bus |
|
|
|
|
**Core principle:** Strict at trust boundaries, tolerant inside the trust domain.
|
|
|
|
---
|
|
|
|
## Accommodations Implemented
|
|
|
|
### P0 - Integration Seam (Must-Have)
|
|
|
|
#### #7 Internal Request Format
|
|
|
|
**Invariant pressure:** None (intra-realm only, local bus).
|
|
|
|
**Pattern:** Bounded context with stable contract + extension points.
|
|
|
|
**Implementation:**
|
|
|
|
1. Documented internal wire contract:
|
|
|
|
```typescript
|
|
interface InternalRequestWire {
|
|
correlationId: string;
|
|
replyTo: string; // internal topic (string)
|
|
params: unknown; // decoded body
|
|
_mx?: { // optional (config)
|
|
fromRealm: string;
|
|
incomingProfile: 'matrix3' | 'mxenv1';
|
|
msgId?: string;
|
|
principalKey?: string;
|
|
traceId?: string;
|
|
};
|
|
envelope?: unknown; // optional (config)
|
|
}
|
|
```
|
|
|
|
2. Config switches on EdgeGateway:
|
|
|
|
| Option | Default | Purpose |
|
|
|--------|---------|---------|
|
|
| `includeInternalMeta` | `true` | Set `false` if handlers are strict-schema and choke on extra fields |
|
|
| `includeInboundEnvelope` | `false` | Set `true` if handlers want full inbound envelope visibility |
|
|
|
|
This is the **Strategy/Policy** pattern at the seam without infecting the core.
|
|
|
|
#### #8 Lenient Internal Replies
|
|
|
|
**Invariant pressure:** None (local bus only).
|
|
|
|
**Pattern:** Tolerant Reader (Postel's Law) + consumer-driven contract.
|
|
|
|
**Implementation:** `normalizeInternalReply(rawPayload, fallbackCorrelationId)`
|
|
|
|
EdgeGateway accepts **any** of these reply shapes:
|
|
|
|
| Shape | Interpretation |
|
|
|-------|----------------|
|
|
| `{ correlationId, ok, body, error? }` | Full strict reply |
|
|
| `{ ok: true, body: ... }` | Semi-strict (no correlationId) |
|
|
| `{ ok: false, error: { code, message } }` | Error reply |
|
|
| `{ result: ... }` | Inferred `ok=true`, `body=result` |
|
|
| Raw JSON value (`"hello"`, `123`, `{...}`) | Inferred `ok=true`, `body=<value>` |
|
|
| Invalid JSON string | Inferred `ok=true`, `body=<raw string>` |
|
|
|
|
This allows existing handlers to work without forced schema migration.
|
|
|
|
#### #5 Matrix-3 Compatibility Tests
|
|
|
|
**Invariant pressure:** None (proof harnessing).
|
|
|
|
**Implementation:** `tests/integration/matrix3_compat.test.ts`
|
|
|
|
Covers:
|
|
- Legacy request envelope with `ttlMs`
|
|
- Flat string `replyTo`
|
|
- Legacy replies with `ok:true/false` detection
|
|
- Legacy URI parsing (no `/-/` delimiter)
|
|
- Lenient internal reply `{ result: ... }` end-to-end
|
|
|
|
Executable evidence, not hand-wavy claims.
|
|
|
|
---
|
|
|
|
### P1 - Observability + Typing
|
|
|
|
#### #3 EdgeGateway Events
|
|
|
|
**Invariant pressure:** None (pure side-effects).
|
|
|
|
**Pattern:** Observer (GoF) / structured tracing hooks.
|
|
|
|
**Implementation:**
|
|
|
|
```typescript
|
|
interface EdgeGatewayOptions {
|
|
onInboundRequest?: (envelope: TaggedEnvelope, internalTopic: string) => void;
|
|
onInboundReply?: (envelope: TaggedEnvelope) => void;
|
|
onOutboundReply?: (replyEnvelope, ingressTopic: string) => void;
|
|
onTimeout?: (correlationId: string, toPath: string) => void;
|
|
onDrop?: (reason: string, topic: string, rawPayload: string) => void;
|
|
onSecurityBlock?: (reason: SecurityBlockReason, envelope: TaggedEnvelope) => void;
|
|
}
|
|
```
|
|
|
|
Makes integration debugging feasible (especially with `nats sub` and correlated logs).
|
|
|
|
#### #6 Export Envelope Type Aliases
|
|
|
|
```typescript
|
|
export type {
|
|
CrossRealmEnvelopeLegacy as CrossRealmEnvelope,
|
|
ReplyEnvelopeLegacy as ReplyEnvelope,
|
|
} from './envelope';
|
|
```
|
|
|
|
Low-friction TypeScript ergonomics: import from package and keep type compatibility.
|
|
|
|
#### #9 TTL Clarification
|
|
|
|
| Field | Semantics |
|
|
|-------|-----------|
|
|
| `MxEnvelopeV1.ttl` | Hop limit (routing TTL) |
|
|
| Legacy `ttlMs` | Expiry duration (milliseconds) |
|
|
|
|
The compat layer **does not coerce** `ttlMs` to `ttl` (different units, different semantics).
|
|
|
|
---
|
|
|
|
### P2 - Transport Ergonomics
|
|
|
|
#### #2 Transport Interface Meta
|
|
|
|
```typescript
|
|
interface Transport {
|
|
publish(topic: string, payload: string, meta?: TransportMeta): void;
|
|
subscribe(topic: string, handler: (topic, payload, meta?) => void): () => void;
|
|
}
|
|
|
|
interface TransportMeta {
|
|
qos?: 0 | 1 | 2;
|
|
retain?: boolean;
|
|
// extension keys allowed
|
|
}
|
|
```
|
|
|
|
**Pattern:** Ports-and-adapters. Core stays oblivious, adapters preserve delivery guarantees.
|
|
|
|
### Metrics + Config Validation
|
|
|
|
**Implementation:**
|
|
|
|
```typescript
|
|
// Fail-fast validation
|
|
const validation = EdgeGateway.validateOptions(opts);
|
|
if (!validation.valid) throw new Error(validation.errors.join('; '));
|
|
|
|
// Runtime metrics
|
|
const metrics = edge.getMetrics();
|
|
// { inboundRequests, inboundReplies, outboundReplies, timeouts, securityBlocks, pendingRequests }
|
|
|
|
edge.resetMetrics();
|
|
```
|
|
|
|
---
|
|
|
|
## What Was NOT Implemented (And Why)
|
|
|
|
### File Naming (PascalCase)
|
|
|
|
Safe but cosmetic. Cross-platform casing hazards (macOS vs Linux CI).
|
|
Do as a single mechanical commit with consistent import updates if desired.
|
|
|
|
### Raw Mode
|
|
|
|
With lenient reply normalization + `includeInboundEnvelope`, raw mode is largely redundant.
|
|
If still needed, formalize an `InternalCodec` Strategy - but that's a bigger API commitment.
|
|
|
|
### Graceful Shutdown (Drain Pending)
|
|
|
|
Lifecycle semantic decision: fail fast (send negative replies) or wait?
|
|
Should be pinned to product requirements before implementing.
|
|
|
|
---
|
|
|
|
## Integration Recipe
|
|
|
|
Minimal "no churn" bridge wiring:
|
|
|
|
```typescript
|
|
import {
|
|
EdgeGateway,
|
|
WIRE_PROFILES,
|
|
} from '@open-matrix/federation';
|
|
|
|
const edge = new EdgeGateway({
|
|
localRealm: 'matrix-3',
|
|
profile: WIRE_PROFILES.matrix3, // legacy mailbox + legacy envelope
|
|
backbone: backboneTransport, // NatsTransport instance
|
|
localBus: localTransport, // NatsTransport or InMemoryBroker
|
|
|
|
// Integration seam knobs:
|
|
includeInternalMeta: false, // if handlers are strict schema
|
|
includeInboundEnvelope: true, // if handlers want full envelope visibility
|
|
|
|
// Observability:
|
|
onInboundRequest: (env, topic) => console.log('IN', env.profile, topic),
|
|
onOutboundReply: (rep, ingressTopic) => console.log('OUT', ingressTopic),
|
|
onTimeout: (cid, toPath) => console.warn('TIMEOUT', cid, toPath),
|
|
onSecurityBlock: (reason) => console.warn('SECURITY', reason),
|
|
});
|
|
|
|
edge.start();
|
|
```
|
|
|
|
On the internal side, your component runtime just needs to:
|
|
|
|
1. Subscribe to internal topic `mx/1/matrix-3/<toPath>`
|
|
2. Read `replyTo` from the request payload
|
|
3. Publish *any* reply shape to `replyTo` (raw value is fine)
|
|
|
|
No forced schema migration.
|
|
|
|
---
|
|
|
|
## Future Enhancements (Invariant-Safe)
|
|
|
|
The next "developer happiness" increment is to formalize an `InternalCodec` Strategy (encode/decode) to plug existing internal envelope formats 1:1. But with the changes above, this is likely unnecessary - integration without churn is already achieved while keeping the federation boundary strict.
|