fix: add close() lifecycle methods to all adapters, fix WS client handler preservation, add Worker thread context guard

- Add close() to Redis, WS Client, WS Server, Worker Host, Worker Thread adapters
  for graceful teardown (cleanup subscriptions, restore handlers, clear maps)
- WS Client now saves/restores original onmessage (consistent with WS Server)
- WS Client dispatchEvent/addEventListener/removeEventListener are no-ops after close()
- WS Server close() removes all connections and clears local listeners
- Redis close() unsubscribes all channels and removes message listener
- Worker Host/Thread close() restore original onmessage and clear callbacks
- Worker Thread throws clear error if globalThis.postMessage is unavailable
- Add double-call guard to WS Server removeConnection
- Export new adapter interface types (RedisEventTarget, WebSocketClientEventTarget, etc.)
- Add sideEffects: false to package.json for tree-shaking
- Update architecture docs: lifecycle section, close() contract, adapter status updates
- 22 new tests covering close(), handler restoration, idempotency, and context guard
This commit is contained in:
2026-05-08 16:19:16 +00:00
parent 96ec2456e1
commit a12c52b407
13 changed files with 483 additions and 24 deletions

View File

@@ -90,7 +90,7 @@ The `Repeater` automatically cleans up its `addEventListener` when the consumer
|--------|--------|-------------|
| `EventEnvelope<TType, TPayload>` | `types.ts` | Cross-platform envelope: `{ type, id, payload }`. JSON-serializable. |
| `TypedEvent<TType, TDetail>` | `types.ts` | Event with typed `type` and `detail`. Omits `CustomEvent`'s untyped fields. |
| `TypedEventTarget<TEvent>` | `types.ts` | Extends `EventTarget` with typed `addEventListener`, `dispatchEvent`, `removeEventListener`. |
| `TypedEventTarget<TEvent>` | `types.ts` | Extends `EventTarget` with typed `addEventListener`, `dispatchEvent`, `removeEventListener`. All adapters' `dispatchEvent` returns `true` (events are non-cancelable). |
| `TypedEventListener<TEvent>` | `types.ts` | `(evt: TEvent) => void` |
| `TypedEventListenerObject<TEvent>` | `types.ts` | `{ handleEvent(object: TEvent): void }` |
| `TypedEventListenerOrEventListenerObject<TEvent>` | `types.ts` | Union of the above |
@@ -99,6 +99,27 @@ The `Repeater` automatically cleans up its `addEventListener` when the consumer
| `PubSubEvent<TEventMap, TType>` | `create_pubsub.ts` | Derived `TypedEvent` for a specific event type, with `detail` as `EventEnvelope<TType, TPayload>` |
| `PubSubEventTarget<TEventMap>` | `create_pubsub.ts` | `TypedEventTarget<PubSubEvent<...>>` |
## Adapter Lifecycle
All transport adapters provide a `close()` method for graceful teardown. After `close()`:
- The adapter is unusable (no-op for `addEventListener`, `removeEventListener`, `dispatchEvent`)
- All subscriptions are cleaned up (Redis channels unsubscribed, `__unsubscribe` sent for WebSocket topics, callbacks cleared)
- Intercepted handlers are restored to their originals
- The underlying transport (Redis connection, WebSocket, Worker) is **not** destroyed — the caller owns it
`close()` is idempotent. Calling it multiple times is safe.
Adapter return types reflect this:
| Adapter | Return type |
|---------|-------------|
| Redis | `RedisEventTarget<TEvent>` (extends `TypedEventTarget<TEvent>`, adds `close()`) |
| WebSocket Client | `WebSocketClientEventTarget<TEvent>` (extends `TypedEventTarget<TEvent>`, adds `close()`) |
| WebSocket Server | `WebSocketServerEventTarget<TEvent>` (extends `TypedEventTarget<TEvent>`, adds `addConnection`, `removeConnection`, `close()`) |
| Worker Host | `WorkerHostEventTarget<TEvent>` (extends `TypedEventTarget<TEvent>`, adds `close()`) |
| Worker Thread | `WorkerThreadEventTarget<TEvent>` (extends `TypedEventTarget<TEvent>`, adds `close()`) |
## Operators
All operators work with any `AsyncIterable`. Operators that return `Repeater` provide backpressure-aware push semantics.