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:
@@ -626,6 +626,100 @@ describe("createWebSocketClientEventTarget", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("close()", () => {
|
||||
it("sends __unsubscribe for all active subscriptions", () => {
|
||||
const ws = createMockWebSocket();
|
||||
const eventTarget = createWebSocketClientEventTarget<TestEvent>(ws as any);
|
||||
|
||||
const listener1 = vi.fn();
|
||||
const listener2 = vi.fn();
|
||||
eventTarget.addEventListener("topic:a", listener1);
|
||||
eventTarget.addEventListener("topic:b", listener2);
|
||||
|
||||
(ws.send as ReturnType<typeof vi.fn>).mockClear();
|
||||
|
||||
eventTarget.close();
|
||||
|
||||
const sent = ws.sent.map((s: string) => JSON.parse(s));
|
||||
const unsubscribes = sent.filter((e: any) => e.type === "__unsubscribe");
|
||||
expect(unsubscribes).toHaveLength(2);
|
||||
const topics = unsubscribes.map((e: any) => e.payload.topic);
|
||||
expect(topics).toContain("topic:a");
|
||||
expect(topics).toContain("topic:b");
|
||||
});
|
||||
|
||||
it("restores original onmessage handler", () => {
|
||||
const ws = createMockWebSocket();
|
||||
const originalOnmessage = vi.fn();
|
||||
ws.onmessage = originalOnmessage;
|
||||
|
||||
const eventTarget = createWebSocketClientEventTarget<TestEvent>(ws as any);
|
||||
expect(ws.onmessage).not.toBe(originalOnmessage);
|
||||
|
||||
eventTarget.close();
|
||||
|
||||
expect(ws.onmessage).toBe(originalOnmessage);
|
||||
});
|
||||
|
||||
it("does not deliver messages after close", () => {
|
||||
const ws = createMockWebSocket();
|
||||
const eventTarget = createWebSocketClientEventTarget<TestEvent>(ws as any);
|
||||
|
||||
const listener = vi.fn();
|
||||
eventTarget.addEventListener("topic:a", listener);
|
||||
|
||||
eventTarget.close();
|
||||
|
||||
const envelope: EventEnvelope = { type: "topic", id: "a", payload: "hello" };
|
||||
ws.simulateMessage(JSON.stringify(envelope));
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not send __subscribe after close", () => {
|
||||
const ws = createMockWebSocket();
|
||||
const eventTarget = createWebSocketClientEventTarget<TestEvent>(ws as any);
|
||||
eventTarget.close();
|
||||
|
||||
const listener = vi.fn();
|
||||
eventTarget.addEventListener("topic:a", listener);
|
||||
|
||||
expect(ws.send).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("__subscribe"),
|
||||
);
|
||||
});
|
||||
|
||||
it("dispatchEvent returns true but does not send after close", () => {
|
||||
const ws = createMockWebSocket();
|
||||
const eventTarget = createWebSocketClientEventTarget<TestEvent>(ws as any);
|
||||
eventTarget.close();
|
||||
|
||||
(ws.send as ReturnType<typeof vi.fn>).mockClear();
|
||||
|
||||
const event = new CustomEvent("test:event", {
|
||||
detail: { type: "test", id: "event", payload: null },
|
||||
}) as TestEvent;
|
||||
|
||||
const result = eventTarget.dispatchEvent(event);
|
||||
expect(result).toBe(true);
|
||||
expect(ws.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("is idempotent", () => {
|
||||
const ws = createMockWebSocket();
|
||||
const eventTarget = createWebSocketClientEventTarget<TestEvent>(ws as any);
|
||||
|
||||
const listener = vi.fn();
|
||||
eventTarget.addEventListener("topic:a", listener);
|
||||
|
||||
eventTarget.close();
|
||||
eventTarget.close();
|
||||
|
||||
const sentCalls = ws.sent.filter((s: string) => JSON.parse(s).type === "__unsubscribe");
|
||||
expect(sentCalls).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dispatchEvent (send path) edge cases", () => {
|
||||
it("sends envelope with null payload", () => {
|
||||
const ws = createMockWebSocket();
|
||||
|
||||
Reference in New Issue
Block a user