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
60 lines
2.9 KiB
Markdown
60 lines
2.9 KiB
Markdown
---
|
|
status: draft
|
|
last_updated: 2026-05-08
|
|
---
|
|
|
|
# Redis Event Target
|
|
|
|
**Import**: `@alkdev/pubsub/event-target-redis`
|
|
**Peer dep**: `ioredis@^5.0.0` (optional)
|
|
**Status**: Implemented. Needs tests.
|
|
|
|
Adapted from `@graphql-yoga/redis-event-target` (MIT).
|
|
|
|
## `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)`
|
|
- 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 the topic string directly as the Redis channel name (e.g., `call.responded:uuid-123`). Should support a configurable prefix: `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
|
|
- **In-flight messages after unsubscribe** — if `removeEventListener` triggers an `unsubscribe` while a Redis message is in flight, the message may arrive after the callback is removed. This is harmless (the callback set is empty, so the message is a no-op) but worth noting for implementers
|
|
|
|
## Test Coverage
|
|
|
|
No tests yet. Need:
|
|
|
|
1. **Publish path** — dispatchEvent sends to Redis with correct channel and serialized envelope
|
|
2. **Subscribe path** — addEventListener subscribes to Redis, onMessage dispatches to local listeners
|
|
3. **Unsubscribe** — removeEventListener unsubscribes from Redis when no listeners remain for a topic
|
|
4. **Topic scoping** — type:id topics are correctly formed
|
|
5. **Envelope serialization** — full `{ type, id, payload }` round-trips through JSON
|
|
6. **Multiple listeners** — multiple listeners on same topic, single Redis subscribe
|
|
7. **Error propagation** — what happens on connection failure |