- Resolve OQ1: WS server accepts raw WebSocket instances via addConnection/removeConnection (framework-agnostic, not coupled to Hono/Express/Bun/Deno) - Resolve OQ2: Backpressure handled by disconnecting slow consumers at configurable threshold (default 1MB), with onBackpressure callback for observability - Resolve OQ3: Topic-based fan-out with subscription tracking instead of broadcast-all; spokes send __subscribe/__unsubscribe control events; direct messaging via 'direct:' topic pattern Add ADR-003 for subscription control protocol decision. Update all fan-out adapters (WS server, Iroh hub) and spoke adapters (WS client, Iroh spoke) with subscription tracking/forwarding. Fix routing key ambiguity (full topic string, not event type alone). Add error handling, composition, and reserved type sections. Clarify Worker as symmetric-only.
3.5 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-05-08 |
Iroh Spoke Event Target
Import: @alkdev/pubsub/event-target-iroh-spoke
Peer dep: @rayhanadev/iroh (optional, NAPI-RS native addon)
Status: Not yet implemented. Needs R&D on binding stability and Deno/NAPI compatibility.
P2P QUIC event target for the spoke (client) side. The spoke initiates the connection and opens the bidirectional stream.
createIrohSpokeEventTarget
async function createIrohSpokeEventTarget<TEvent extends TypedEvent>(
args: CreateIrohSpokeEventTargetArgs,
): Promise<TypedEventTarget<TEvent>>;
CreateIrohSpokeEventTargetArgs
| Field | Type | Required | Description |
|---|---|---|---|
endpoint |
Endpoint |
Yes | iroh endpoint (created with Endpoint.create()) |
hubNodeId |
string | NodeId |
Yes | The hub's public key (Ed25519) |
alpn |
string |
No | Application-layer protocol. Default: "alkpubsub/1" |
Protocol
Single bidirectional QUIC stream per connection. Length-prefixed JSON messages:
[4 bytes: length N][N bytes: JSON payload]
The JSON payload is the EventEnvelope:
{ "type": "call.responded", "id": "uuid-123", "payload": { "output": 42 } }
Connection Flow
- Spoke creates
Endpoint - Spoke calls
endpoint.connect(hubNodeId, alpn)→Connection - Spoke calls
connection.openBi()→SendStream+RecvStream - Spoke wraps streams in
IrohSpokeEventTarget - On disconnect:
RecvStream.readExact()throws, spoke must reconnect
Subscription Forwarding
Same pattern as WebSocket Client: when addEventListener is called for the first listener on a topic, the spoke sends a __subscribe control event to the hub. When removeEventListener removes the last listener for a topic, the spoke sends __unsubscribe. Reference counting ensures __subscribe is sent only on the first addEventListener for a topic, and __unsubscribe only when the last removeEventListener for that topic is called. This enables the hub's topic-based fan-out (see ADR-003).
Control events use the existing EventEnvelope format:
{ "type": "__subscribe", "id": "", "payload": { "topic": "message.sent:conv-123" } }
{ "type": "__unsubscribe", "id": "", "payload": { "topic": "message.sent:conv-123" } }
Key Properties
- NAT traversal — spoke dials hub by
NodeId, no public IP needed - Cryptographic identity —
Connection.remoteNodeId()verifies the hub - Bidirectional —
dispatchEventwrites toSendStream,addEventListenerreads fromRecvStream - Per-connection — one event target per QUIC connection
- Subscription forwarding —
addEventListener/removeEventListenersend__subscribe/__unsubscribecontrol events to the hub (see ADR-003)
R&D Needed
- Binding stability —
@rayhanadev/irohhas one author and no tests. API surface is small (10 methods) but needs validation. - NAPI under Deno — NAPI-RS
.nodebinaries need testing under Deno 2.x. - Stream multiplexing — multiple
openBi()streams on one connection vs. single stream with multiplexed events. Single stream + JSON framing is simpler. - Reconnection —
RecvStream.readExact()throws on connection close. Need to propagate this to listeners and support reconnect.
See ../iroh-transport.md for full protocol details, identity, and comparison with WebSocket.