CRITICAL: createPubSub.publish() was dispatching CustomEvent with
just the event type (e.g. 'call.responded') instead of the composite
topic string ('call.responded:uuid-123'). This broke all adapters
that rely on topic-scoped dispatch — Redis subscribe/publish
channels didn't match, and WS server fan-out routing would fail.
Fixed to dispatch with the full type:id composite.
Other fixes:
- Add __ prefix runtime guard in publish() (reserved for control)
- Add Redis barrel re-export to src/index.ts (ADR-002 compliance)
- Clarify WS server: adapter's onclose calls removeConnection
internally; user doesn't need to
- WS client: document null callback no-op, removeEventListener
edge cases (unregistered callback, null callback)
- WS server: document dispatchEvent always returns true
- Redis spec: document in-flight message edge case after unsubscribe
- Worker adapter: rename createMainThreadEventTarget to
createWorkerThreadEventTarget, createWorkerEventTarget to
createWorkerHostEventTarget (fix inverted naming)
- api-surface.md: add PubSub.publish() section documenting the
type:id composite and __ guard
134 lines
5.0 KiB
Markdown
134 lines
5.0 KiB
Markdown
---
|
|
status: draft
|
|
last_updated: 2026-05-08
|
|
---
|
|
|
|
# Worker Event Target
|
|
|
|
**Import**: `@alkdev/pubsub/event-target-worker`
|
|
**Peer dep**: none (Web Worker / Node worker_threads are standard)
|
|
**Status**: Not yet implemented. Needs R&D on Node vs Web Worker API differences.
|
|
|
|
Enables `createPubSub` to work across Worker boundaries. Two factory functions: one for the main thread side, one for the worker thread side.
|
|
|
|
## API
|
|
|
|
```ts
|
|
// Main thread side — wraps a Worker instance
|
|
function createWorkerHostEventTarget<TEvent extends TypedEvent>(
|
|
worker: Worker,
|
|
): TypedEventTarget<TEvent>;
|
|
|
|
// Worker thread side — wraps parent message port
|
|
function createWorkerThreadEventTarget<TEvent extends TypedEvent>(): TypedEventTarget<TEvent>;
|
|
```
|
|
|
|
The naming convention: `Host` is the side that owns the `Worker` object (typically the main thread). `Thread` is the side that runs inside the worker (accessing `self.onmessage` / `parentPort`).
|
|
|
|
## Protocol
|
|
|
|
Worker messages use the `EventEnvelope` format over `postMessage`:
|
|
|
|
```json
|
|
{ "type": "call.responded", "id": "uuid-123", "payload": { "output": 42 } }
|
|
```
|
|
|
|
### Host → Worker Thread
|
|
|
|
```ts
|
|
// Host side (createWorkerHostEventTarget)
|
|
dispatchEvent(event) {
|
|
this.worker.postMessage(event.detail);
|
|
// event.detail is the EventEnvelope
|
|
return true;
|
|
}
|
|
```
|
|
|
|
### Worker Thread → Host
|
|
|
|
```ts
|
|
// Worker thread side (createWorkerThreadEventTarget)
|
|
dispatchEvent(event) {
|
|
globalThis.postMessage(event.detail);
|
|
return true;
|
|
}
|
|
```
|
|
|
|
### Receiving on Host Side
|
|
|
|
```ts
|
|
// Host side (createWorkerHostEventTarget)
|
|
this.worker.onmessage = (msg) => {
|
|
const envelope = msg.data;
|
|
const topic = `${envelope.type}:${envelope.id}`;
|
|
const event = new CustomEvent(topic, { detail: envelope });
|
|
// dispatch to listeners
|
|
};
|
|
|
|
### Receiving on Worker Thread Side
|
|
|
|
```ts
|
|
// Worker thread side (createWorkerThreadEventTarget)
|
|
globalThis.onmessage = (msg) => {
|
|
const envelope = msg.data;
|
|
const topic = `${envelope.type}:${envelope.id}`;
|
|
const event = new CustomEvent(topic, { detail: envelope });
|
|
// dispatch to listeners
|
|
};
|
|
```
|
|
|
|
## Key Properties
|
|
|
|
- **Bidirectional** — both sides can publish and subscribe
|
|
- **Per-worker** — each worker gets its own event target on the main thread side
|
|
- **Structured clone** — Web Workers use structured clone for serialization, but the JSON-serializable `EventEnvelope` ensures cross-platform compatibility
|
|
- **No native deps** — works in any environment with Worker support
|
|
|
|
## Open Questions / R&D Needed
|
|
|
|
### Node vs Web Worker API
|
|
|
|
The APIs differ significantly:
|
|
|
|
| Feature | Web Worker | Node `worker_threads` |
|
|
|---------|-----------|----------------------|
|
|
| Create | `new Worker(url)` | `new Worker(path)` |
|
|
| Send | `worker.postMessage(msg)` | `worker.postMessage(msg)` |
|
|
| Receive | `worker.onmessage` | `worker.on('message')` |
|
|
| Worker send | `self.postMessage(msg)` | `parentPort.postMessage(msg)` |
|
|
| Worker receive | `self.onmessage` | `parentPort.on('message')` |
|
|
| Transfer | `postMessage(msg, [transfer])` | `postMessage(msg, [transferList])` |
|
|
| `MessagePort` | No built-in | Yes — `MessageChannel` for direct ports |
|
|
|
|
Options:
|
|
1. **Two adapters** — `event-target-web-worker` and `event-target-node-worker`
|
|
2. **One adapter with runtime detection** — detect environment and use appropriate API
|
|
3. **One adapter abstracting both** — wrap the differences behind a common interface
|
|
|
|
Recommendation: Start with a single adapter that targets Web Workers (browser + Deno + Bun all support this API). Add Node `worker_threads` support later if needed, potentially with a `MessagePort`-based approach for direct channels.
|
|
|
|
### Worker Pool Pattern
|
|
|
|
The original sandbox implementation used a worker pool pattern. A `WorkerPoolManager` would:
|
|
1. Maintain a pool of workers
|
|
2. Assign tasks to available workers
|
|
3. Collect results and fan out to subscribers
|
|
|
|
This is **not** part of the `WorkerEventTarget` — it's a downstream concern for `@alkdev/operations`. The event target just wraps a single `postMessage`/`onmessage` channel. Pool management belongs higher.
|
|
|
|
### Transferable Objects
|
|
|
|
Web Workers support `Transferable` objects (ArrayBuffers, etc.) for zero-copy transfer. The current `EventEnvelope` is JSON, which gets structured-cloned. If large payloads need zero-copy transfer, the envelope could support a `Transferable` field, but this adds complexity and is not needed for the initial implementation.
|
|
|
|
## Relationship to Downstream
|
|
|
|
Workers can subscribe to events and publish results through the event target, allowing `@alkdev/operations` to dispatch work to worker threads via the same pubsub transport. The correlation (`id` field in the envelope) connects request to response.
|
|
|
|
## Test Plan
|
|
|
|
1. **Main → Worker send** — dispatchEvent from main posts message to worker
|
|
2. **Worker → Main send** — dispatchEvent from worker posts message to main
|
|
3. **Bidirectional** — both sides can subscribe and publish
|
|
4. **Topic scoping** — type:id topics correctly formed
|
|
5. **Envelope round-trip** — full envelope survives serialization
|
|
6. **Worker termination** — cleanup when worker exits |