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:
@@ -26,6 +26,11 @@ function createMockRedis() {
|
||||
}
|
||||
return {} as any;
|
||||
}),
|
||||
off: vi.fn((event: string, callback: (channel: string, message: string) => void) => {
|
||||
if (event === "message" && messageListener === callback) {
|
||||
messageListener = null;
|
||||
}
|
||||
}),
|
||||
publications,
|
||||
subscriptions,
|
||||
unsubscriptions,
|
||||
@@ -595,4 +600,102 @@ describe("createRedisEventTarget", () => {
|
||||
expect(subscribeClient.unsubscribe).toHaveBeenCalledWith("obj:test2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("close()", () => {
|
||||
it("unsubscribes from all active channels", () => {
|
||||
const publishClient = createMockRedis();
|
||||
const subscribeClient = createMockRedis();
|
||||
const eventTarget = createRedisEventTarget<TestEvent>({
|
||||
publishClient: publishClient as any,
|
||||
subscribeClient: subscribeClient as any,
|
||||
});
|
||||
|
||||
const listener1 = vi.fn();
|
||||
const listener2 = vi.fn();
|
||||
eventTarget.addEventListener("topic:a", listener1);
|
||||
eventTarget.addEventListener("topic:b", listener2);
|
||||
|
||||
eventTarget.close();
|
||||
|
||||
expect(subscribeClient.unsubscribe).toHaveBeenCalledWith("topic:a");
|
||||
expect(subscribeClient.unsubscribe).toHaveBeenCalledWith("topic:b");
|
||||
});
|
||||
|
||||
it("removes the message listener from subscribeClient", () => {
|
||||
const publishClient = createMockRedis();
|
||||
const subscribeClient = createMockRedis();
|
||||
const eventTarget = createRedisEventTarget<TestEvent>({
|
||||
publishClient: publishClient as any,
|
||||
subscribeClient: subscribeClient as any,
|
||||
});
|
||||
|
||||
eventTarget.close();
|
||||
|
||||
expect(subscribeClient.off).toHaveBeenCalledWith("message", expect.any(Function));
|
||||
});
|
||||
|
||||
it("does not receive messages after close", () => {
|
||||
const publishClient = createMockRedis();
|
||||
const subscribeClient = createMockRedis();
|
||||
const eventTarget = createRedisEventTarget<TestEvent>({
|
||||
publishClient: publishClient as any,
|
||||
subscribeClient: subscribeClient as any,
|
||||
});
|
||||
|
||||
const listener = vi.fn();
|
||||
eventTarget.addEventListener("topic:a", listener);
|
||||
|
||||
eventTarget.close();
|
||||
|
||||
const envelope: EventEnvelope = { type: "topic", id: "a", payload: "hello" };
|
||||
subscribeClient.simulateMessage("topic:a", JSON.stringify(envelope));
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("is idempotent", () => {
|
||||
const publishClient = createMockRedis();
|
||||
const subscribeClient = createMockRedis();
|
||||
const eventTarget = createRedisEventTarget<TestEvent>({
|
||||
publishClient: publishClient as any,
|
||||
subscribeClient: subscribeClient as any,
|
||||
});
|
||||
|
||||
const listener = vi.fn();
|
||||
eventTarget.addEventListener("topic:a", listener);
|
||||
|
||||
eventTarget.close();
|
||||
eventTarget.close();
|
||||
|
||||
expect(subscribeClient.unsubscribe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handles close with no subscriptions", () => {
|
||||
const publishClient = createMockRedis();
|
||||
const subscribeClient = createMockRedis();
|
||||
const eventTarget = createRedisEventTarget<TestEvent>({
|
||||
publishClient: publishClient as any,
|
||||
subscribeClient: subscribeClient as any,
|
||||
});
|
||||
|
||||
expect(() => eventTarget.close()).not.toThrow();
|
||||
});
|
||||
|
||||
it("unsubscribes from prefixed channels correctly", () => {
|
||||
const publishClient = createMockRedis();
|
||||
const subscribeClient = createMockRedis();
|
||||
const eventTarget = createRedisEventTarget<TestEvent>({
|
||||
publishClient: publishClient as any,
|
||||
subscribeClient: subscribeClient as any,
|
||||
prefix: "alk:events:",
|
||||
});
|
||||
|
||||
const listener = vi.fn();
|
||||
eventTarget.addEventListener("topic:a", listener);
|
||||
|
||||
eventTarget.close();
|
||||
|
||||
expect(subscribeClient.unsubscribe).toHaveBeenCalledWith("alk:events:topic:a");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user