Set up project structure, source files, and architecture docs
- Copy core source from alkhub_ts/packages/core/pubsub/ with import path fixups (typed_event_target.ts → types.ts, .ts → .js extensions) - Make PubSubPublishArgsByKey exported (was private type, needed by barrel) - Add package.json with sub-path exports and optional peer deps (ioredis) - Add tsup.config.ts with multi-entry + splitting for tree-shaking - Add tsconfig.json, vitest.config.ts, .gitignore - Add AGENTS.md with project conventions and adapter checklist - Add architecture docs following taskgraph/alkhub pattern: docs/architecture/README.md, api-surface.md, event-targets.md, iroh-transport.md, build-distribution.md - Add ADRs: 001-graphql-yoga-fork, 002-tree-shake-pattern - Copy migration research doc to docs/research/migration.md - Dual-license MIT OR Apache-2.0 (matching taskgraph)
This commit is contained in:
111
docs/architecture/event-targets.md
Normal file
111
docs/architecture/event-targets.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-04-30
|
||||
---
|
||||
|
||||
# Event Target Adapters
|
||||
|
||||
In-process, Redis, and WebSocket event targets. All implement `TypedEventTarget<TEvent>`.
|
||||
|
||||
## Interface Contract
|
||||
|
||||
Every adapter must implement:
|
||||
|
||||
| Method | Behavior |
|
||||
|--------|----------|
|
||||
| `addEventListener(type, callback)` | Register listener for event type. Callback receives `CustomEvent` with typed `detail`. |
|
||||
| `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. |
|
||||
|
||||
## 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.
|
||||
|
||||
No explicit `InProcessEventTarget` class — the web standard `EventTarget` already implements the interface. Could be formalized later if a name makes the API clearer, but `new EventTarget()` is already the standard.
|
||||
|
||||
## Redis
|
||||
|
||||
**Import**: `@alkdev/pubsub/event-target-redis`
|
||||
**Peer dep**: `ioredis@^5.0.0` (optional)
|
||||
|
||||
### `createRedisEventTarget`
|
||||
|
||||
```ts
|
||||
function createRedisEventTarget<TEvent extends TypedEvent>(
|
||||
args: CreateRedisEventTargetArgs,
|
||||
): TypedEventTarget<TEvent>;
|
||||
```
|
||||
|
||||
### `CreateRedisEventTargetArgs`
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `publishClient` | `Redis \| Cluster` | Yes | ioredis client for publishing. Can share a connection. |
|
||||
| `subscribeClient` | `Redis \| Cluster` | Yes | ioredis client for subscribing. Must be dedicated — Redis requires subscriber connections to only receive messages. |
|
||||
| `serializer` | `{ stringify, parse }` | No | Custom serializer. Defaults to `JSON`. |
|
||||
|
||||
### How It Works
|
||||
|
||||
- `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)`
|
||||
|
||||
### 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:" })`.
|
||||
|
||||
### Limitations (Current)
|
||||
|
||||
- **No error handling** — connection failures, reconnection, and message parse errors are not handled
|
||||
- **No channel prefix** — raw event types as channel names risk collision in shared Redis instances
|
||||
- **No unsubscribe cleanup on client disconnect** — if the subscribe client disconnects, registered callbacks remain in the map but will never fire
|
||||
|
||||
### Test Coverage
|
||||
|
||||
5 tests in alkhub (publish path only, mocked ioredis). No tests for subscription-receive path, unsubscribe cleanup, or error handling.
|
||||
|
||||
## WebSocket
|
||||
|
||||
**Import**: `@alkdev/pubsub/event-target-websocket` (not yet implemented)
|
||||
**Peer dep**: none (WebSocket is a web standard)
|
||||
|
||||
### Design (Spec from `spoke-runner.md`)
|
||||
|
||||
```ts
|
||||
class WebSocketEventTarget implements TypedEventTarget<any> {
|
||||
private listeners = new Map<string, Set<(event: CustomEvent) => void>>()
|
||||
|
||||
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) ?? []) {
|
||||
listener(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchEvent(event: CustomEvent): boolean {
|
||||
this.ws.send(JSON.stringify({ type: event.type, payload: event.detail }))
|
||||
return true
|
||||
}
|
||||
|
||||
addEventListener(type: string, listener: (event: CustomEvent) => void): void { ... }
|
||||
removeEventListener(type: string, listener: (event: CustomEvent) => void): void { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Key Properties
|
||||
|
||||
- **Bidirectional** — `dispatchEvent` sends over WS, `addEventListener` receives from WS
|
||||
- **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
|
||||
|
||||
### 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
|
||||
|
||||
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`.
|
||||
Reference in New Issue
Block a user