--- status: draft last_updated: 2026-05-08 --- # Iroh Spoke Event Target **Import**: `@alkdev/pubsub/event-target-iroh-spoke` **Peer dep**: `@alkdev/iroh` (optional, NAPI-RS native addon — pending fork of iroh-ts) **Status**: Deferred. Pending fork of iroh-ts with Linux + WASM platform targets. P2P QUIC event target for the spoke (client) side. The spoke initiates the connection and opens the bidirectional stream. ## `createIrohSpokeEventTarget` ```ts async function createIrohSpokeEventTarget( args: CreateIrohSpokeEventTargetArgs, ): Promise>; ``` ### `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`: ```json { "type": "call.responded", "id": "uuid-123", "payload": { "output": 42 } } ``` ## Connection Flow 1. Spoke creates `Endpoint` 2. Spoke calls `endpoint.connect(hubNodeId, alpn)` → `Connection` 3. Spoke calls `connection.openBi()` → `SendStream` + `RecvStream` 4. Spoke wraps streams in `IrohSpokeEventTarget` 5. On disconnect: `RecvStream.readExact()` throws, spoke must reconnect ## Subscription Forwarding Same pattern as [WebSocket Client](websocket-client.md): 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](../decisions/003-subscription-control-protocol.md)). Control events use the existing `EventEnvelope` format: ```json { "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** — `dispatchEvent` writes to `SendStream`, `addEventListener` reads from `RecvStream` - **Per-connection** — one event target per QUIC connection - **Subscription forwarding** — `addEventListener`/`removeEventListener` send `__subscribe`/`__unsubscribe` control events to the hub (see [ADR-003](../decisions/003-subscription-control-protocol.md)) ## R&D Needed 1. **Fork of iroh-ts** — `@rayhanadev/iroh` has one author and no tests. We plan to fork as `@alkdev/iroh` with Linux (NAPI-RS) + WASM platform targets. Windows and macOS native builds are deferred — cross-compilation from Linux is possible with NAPI but tricky, and macOS licensing is dubious for CI. Initial release: Linux native + WASM fallback. Adding other platforms is a future accessibility concern. 2. **Stream multiplexing** — multiple `openBi()` streams on one connection vs. single stream with multiplexed events. Single stream + JSON framing is simpler. 3. **Reconnection** — `RecvStream.readExact()` throws on connection close. Need to propagate this to listeners and support reconnect. See [../iroh-transport.md](../iroh-transport.md) for full protocol details, identity, and comparison with WebSocket.