feat(redis): add channel prefix and error handling

This commit is contained in:
2026-05-08 06:33:15 +00:00
parent 7c12b40ed2
commit 392682c7be
6 changed files with 258 additions and 28 deletions

View File

@@ -394,6 +394,169 @@ describe("createRedisEventTarget", () => {
});
});
describe("channel prefix", () => {
it("publishes to prefixed channel when prefix is set", () => {
const publishClient = createMockRedis();
const subscribeClient = createMockRedis();
const eventTarget = createRedisEventTarget<TestEvent>({
publishClient: publishClient as any,
subscribeClient: subscribeClient as any,
prefix: "alk:events:",
});
const event = new CustomEvent("call.responded:uuid-123", {
detail: { type: "call.responded", id: "uuid-123", payload: { status: "ok" } },
}) as TestEvent;
eventTarget.dispatchEvent(event);
expect(publishClient.publish).toHaveBeenCalledWith(
"alk:events:call.responded:uuid-123",
expect.any(String),
);
});
it("subscribes to prefixed channel when prefix is set", () => {
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("call.responded:uuid-123", listener);
expect(subscribeClient.subscribe).toHaveBeenCalledWith("alk:events:call.responded:uuid-123");
});
it("unsubscribes from prefixed channel when prefix is set", () => {
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("call.responded:uuid-123", listener);
eventTarget.removeEventListener("call.responded:uuid-123", listener);
expect(subscribeClient.unsubscribe).toHaveBeenCalledWith("alk:events:call.responded:uuid-123");
});
it("delivers messages on prefixed channels to listeners", () => {
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("call.responded:uuid-123", listener);
const envelope: EventEnvelope = {
type: "call.responded",
id: "uuid-123",
payload: { status: "ok" },
};
subscribeClient.simulateMessage("alk:events:call.responded:uuid-123", JSON.stringify(envelope));
expect(listener).toHaveBeenCalledTimes(1);
expect((listener.mock.calls[0][0] as TestEvent).detail).toEqual(envelope);
});
it("ignores messages on non-prefixed channels when prefix is set", () => {
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("call.responded:uuid-123", listener);
subscribeClient.simulateMessage("call.responded:uuid-123", JSON.stringify({ type: "call.responded", id: "uuid-123", payload: null }));
expect(listener).not.toHaveBeenCalled();
});
it("defaults prefix to empty string", () => {
const publishClient = createMockRedis();
const subscribeClient = createMockRedis();
const eventTarget = createRedisEventTarget<TestEvent>({
publishClient: publishClient as any,
subscribeClient: subscribeClient as any,
});
const event = new CustomEvent("test:1", {
detail: { type: "test", id: "1", payload: null },
}) as TestEvent;
eventTarget.dispatchEvent(event);
expect(publishClient.publish).toHaveBeenCalledWith("test:1", expect.any(String));
});
});
describe("error handling", () => {
it("skips messages that fail to parse and logs a warning", () => {
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);
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
subscribeClient.simulateMessage("topic:a", "not valid json{{");
expect(listener).not.toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith(
'Failed to parse message on channel "topic:a": not valid json{{',
);
warnSpy.mockRestore();
});
it("continues delivering valid messages after a parse error", () => {
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);
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
subscribeClient.simulateMessage("topic:a", "broken{{{");
expect(listener).not.toHaveBeenCalled();
const envelope: EventEnvelope = { type: "topic", id: "a", payload: "good" };
subscribeClient.simulateMessage("topic:a", JSON.stringify(envelope));
expect(listener).toHaveBeenCalledTimes(1);
expect((listener.mock.calls[0][0] as TestEvent).detail).toEqual(envelope);
warnSpy.mockRestore();
});
});
describe("EventListenerObject support", () => {
it("addEventListener accepts EventListenerObject with handleEvent", () => {
const publishClient = createMockRedis();