hivecast-sdk/packages/federation/ARCHITECTURE-NOTES.md

257 lines
7.9 KiB
Markdown
Raw Permalink Normal View History

# 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.