Simplify to transport-only: remove call protocol, add EventEnvelope, expand stream operators
- Remove src/call.ts (PendingRequestMap, CallEventSchema, CallError) — call protocol belongs in @alkdev/operations
- Add EventEnvelope type ({ type, id, payload }) as the cross-platform serialization contract
- Simplify createPubSub: replace PubSubPublishArgsByKey tuple model with PubSubEventMap; publish(type, id, payload) and subscribe(type, id) use explicit id for topic scoping
- Update Redis adapter to serialize/deserialize full EventEnvelope
- Expand operators: add take, reduce, toArray, batch, dedupe, window, flat, groupBy, chain, join
- Remove @alkdev/typebox runtime dependency (was only used by call.ts)
- Remove ./call sub-path export from package.json and tsup config
- Update all architecture docs to reflect transport-only scope, add Worker adapter, remove call protocol references
- Remove docs/architecture/call-protocol.md
- Update AGENTS.md with new source layout and transport-only principle
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-04-30
|
||||
last_updated: 2026-05-01
|
||||
---
|
||||
|
||||
# Event Target Adapters
|
||||
|
||||
In-process, Redis, and WebSocket event targets. All implement `TypedEventTarget<TEvent>`.
|
||||
In-process, Redis, WebSocket, and Worker event targets. All implement `TypedEventTarget<TEvent>`.
|
||||
|
||||
## Interface Contract
|
||||
|
||||
@@ -13,10 +13,12 @@ Every adapter must implement:
|
||||
|
||||
| Method | Behavior |
|
||||
|--------|----------|
|
||||
| `addEventListener(type, callback)` | Register listener for event type. Callback receives `CustomEvent` with typed `detail`. |
|
||||
| `addEventListener(type, callback)` | Register listener for event type. Callback receives `CustomEvent` with typed `detail` (an `EventEnvelope`). |
|
||||
| `dispatchEvent(event)` | Send/dispatch event. Returns `boolean` (always `true` for non-cancelable events). |
|
||||
| `removeEventListener(type, callback)` | Unregister listener. Clean up underlying subscription when no listeners remain for a topic. |
|
||||
|
||||
All adapters use the `EventEnvelope` format (`{ type, id, payload }`) as the serialization contract. Adapters that cross process boundaries (Redis, WebSocket, Iroh) serialize/deserialize the full envelope as JSON.
|
||||
|
||||
## In-Process (Default)
|
||||
|
||||
No adapter needed. `createPubSub` uses `new EventTarget()` by default. This works for single-process deployments where all pubsub participants share the same memory.
|
||||
@@ -49,10 +51,13 @@ function createRedisEventTarget<TEvent extends TypedEvent>(
|
||||
- `dispatchEvent` → `publishClient.publish(event.type, serializer.stringify(event.detail))`
|
||||
- `addEventListener` → `subscribeClient.subscribe(topic)`, track callbacks per topic
|
||||
- `removeEventListener` → remove callback; if no callbacks remain for topic, `subscribeClient.unsubscribe(topic)`
|
||||
- On message: deserializes with `serializer.parse`, reconstructs `CustomEvent(channel, { detail: envelope })`
|
||||
|
||||
The `detail` of the `CustomEvent` dispatched to local listeners is the full `EventEnvelope` object (`{ type, id, payload }`).
|
||||
|
||||
### Channel Naming
|
||||
|
||||
Currently uses raw event type as Redis channel name (e.g., `session.status:proj_123`). Architecture recommends `alk:events:{eventType}` prefix but this is not yet implemented. Should be configurable: `createRedisEventTarget({ ..., prefix: "alk:events:" })`.
|
||||
Currently uses the topic string directly as the Redis channel name (e.g., `session.status:proj_123`). Architecture recommends `alk:events:{eventType}` prefix but this is not yet implemented. Should be configurable: `createRedisEventTarget({ ..., prefix: "alk:events:" })`.
|
||||
|
||||
### Limitations (Current)
|
||||
|
||||
@@ -62,7 +67,7 @@ Currently uses raw event type as Redis channel name (e.g., `session.status:proj_
|
||||
|
||||
### Test Coverage
|
||||
|
||||
5 tests in alkhub (publish path only, mocked ioredis). No tests for subscription-receive path, unsubscribe cleanup, or error handling.
|
||||
No tests yet (test directory is empty). Previous alkhub had 5 Redis tests (publish path only, mocked ioredis).
|
||||
|
||||
## WebSocket
|
||||
|
||||
@@ -77,16 +82,17 @@ class WebSocketEventTarget implements TypedEventTarget<any> {
|
||||
|
||||
constructor(private ws: WebSocket) {
|
||||
ws.onmessage = (msg) => {
|
||||
const { type, payload } = JSON.parse(msg.data as string)
|
||||
const event = new CustomEvent(type, { detail: payload })
|
||||
for (const listener of this.listeners.get(type) ?? []) {
|
||||
const envelope = JSON.parse(msg.data as string) // { type, id, payload }
|
||||
const topic = `${envelope.type}:${envelope.id}`
|
||||
const event = new CustomEvent(topic, { detail: envelope })
|
||||
for (const listener of this.listeners.get(topic) ?? []) {
|
||||
listener(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchEvent(event: CustomEvent): boolean {
|
||||
this.ws.send(JSON.stringify({ type: event.type, payload: event.detail }))
|
||||
this.ws.send(JSON.stringify(event.detail)) // sends { type, id, payload }
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -101,11 +107,49 @@ class WebSocketEventTarget implements TypedEventTarget<any> {
|
||||
- **Per-connection** — hub creates one per spoke connection
|
||||
- **JSON framing** — WebSocket provides native message boundaries (no length-prefix needed)
|
||||
- **No native deps** — works in browsers and Node
|
||||
- **Envelope serialization** — sends/receives the full `EventEnvelope` JSON (`{ type, id, payload }`)
|
||||
|
||||
### Gap: Reconnection
|
||||
|
||||
WebSocket connections drop. On reconnect, the spoke must re-register with the hub (same `hub.register` flow). The `WebSocketEventTarget` itself is per-connection — a new connection means a new event target instance. Reconnection logic belongs to the spoke lifecycle, not the event target.
|
||||
|
||||
### Gap: Hub-Side Architecture
|
||||
## Worker
|
||||
|
||||
The hub needs per-connection event target + `PendingRequestMap` creation on accept, cleanup on disconnect. This is a hub architectural concern, not a pubsub concern. See `@alkdev/alkhub_ts/docs/architecture/spoke-runner.md`.
|
||||
**Import**: `@alkdev/pubsub/event-target-worker` (not yet implemented)
|
||||
**Peer dep**: none (Web Worker API is standard)
|
||||
|
||||
### Design
|
||||
|
||||
A `WorkerEventTarget` implementing `TypedEventTarget` over `postMessage`/`onmessage`. This enables `createPubSub` to work across Web Worker boundaries.
|
||||
|
||||
The worker message protocol uses the `EventEnvelope` format:
|
||||
|
||||
```json
|
||||
{ "type": "call.responded", "id": "uuid-123", "payload": { "output": 42 } }
|
||||
```
|
||||
|
||||
### Two-Sided Design
|
||||
|
||||
- **Main thread** (`WorkerPoolManager` side): dispatches typed messages to workers via `worker.postMessage()`, receives responses via `worker.onmessage`
|
||||
- **Worker thread**: dispatches to main thread via `parentPort.postMessage()`, receives from main thread via `globalThis.onmessage`
|
||||
|
||||
Both sides wrap `postMessage`/`onmessage` to implement the `TypedEventTarget` interface:
|
||||
|
||||
```ts
|
||||
// Main thread side
|
||||
const workerEventTarget = createWorkerEventTarget(worker);
|
||||
|
||||
// Worker thread side
|
||||
const mainEventTarget = createMainThreadEventTarget();
|
||||
```
|
||||
|
||||
### Key Properties
|
||||
|
||||
- **Bidirectional** — both sides can publish and subscribe
|
||||
- **Per-worker** — each worker gets its own event target
|
||||
- **Structured clone** — Web Workers use structured clone algorithm for serialization, but JSON-serializable `EventEnvelope` ensures cross-platform compatibility
|
||||
- **No native deps** — works in any environment with Web Worker support
|
||||
|
||||
### Relationship to Taskgraph / Operations
|
||||
|
||||
The worker event target enables distributed operation execution. Workers can subscribe to `call.requested` events and publish `call.responded` events through the event target, allowing `@alkdev/operations` to dispatch work to worker threads via the same pubsub transport.
|
||||
Reference in New Issue
Block a user