feat(worker): implement Worker event target adapter (Web Worker only)

This commit is contained in:
2026-05-08 08:07:37 +00:00
parent 5dfa808114
commit 738dd80197
5 changed files with 1008 additions and 1 deletions

View File

@@ -0,0 +1,855 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { createWorkerHostEventTarget, createWorkerThreadEventTarget } from "../src/event-target-worker.js";
import type { EventEnvelope, TypedEvent, TypedEventTarget } from "../src/types.js";
type TestEvent = TypedEvent<string, EventEnvelope>;
function createMockWorker() {
const posted: unknown[] = [];
let onmessageHandler: ((event: MessageEvent) => void) | null = null;
let onerrorHandler: ((event: ErrorEvent) => void) | null = null;
const worker = {
postMessage: vi.fn((data: unknown) => {
posted.push(data);
}),
get onmessage() {
return onmessageHandler;
},
set onmessage(handler: ((event: MessageEvent) => void) | null) {
onmessageHandler = handler;
},
get onerror() {
return onerrorHandler;
},
set onerror(handler: ((event: ErrorEvent) => void) | null) {
onerrorHandler = handler;
},
posted,
simulateMessage(data: unknown) {
if (onmessageHandler) {
onmessageHandler({ data } as MessageEvent);
}
},
};
Object.defineProperty(worker, "onmessage", {
get() {
return onmessageHandler;
},
set(handler: ((event: MessageEvent) => void) | null) {
onmessageHandler = handler;
},
});
Object.defineProperty(worker, "onerror", {
get() {
return onerrorHandler;
},
set(handler: ((event: ErrorEvent) => void) | null) {
onerrorHandler = handler;
},
});
return worker;
}
function createMockGlobalThis() {
const posted: unknown[] = [];
let onmessageHandler: ((event: MessageEvent) => void) | null = null;
const global = {
onmessage: null as ((event: MessageEvent) => void) | null,
postMessage: vi.fn((data: unknown) => {
posted.push(data);
}),
posted,
simulateMessage(data: unknown) {
if (onmessageHandler) {
onmessageHandler({ data } as MessageEvent);
}
},
};
Object.defineProperty(global, "onmessage", {
get() {
return onmessageHandler;
},
set(handler: ((event: MessageEvent) => void) | null) {
onmessageHandler = handler;
},
});
return global;
}
describe("createWorkerHostEventTarget", () => {
describe("dispatchEvent (send path)", () => {
it("posts event.detail to worker via worker.postMessage", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const envelope: EventEnvelope<"call.responded", { status: string }> = {
type: "call.responded",
id: "uuid-123",
payload: { status: "ok" },
};
const event = new CustomEvent("call.responded:uuid-123", {
detail: envelope,
}) as TestEvent;
eventTarget.dispatchEvent(event);
expect(worker.postMessage).toHaveBeenCalledTimes(1);
expect(worker.postMessage).toHaveBeenCalledWith(envelope);
});
it("returns true from dispatchEvent", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const event = new CustomEvent("test:event", {
detail: { type: "test", id: "event", payload: null },
}) as TestEvent;
const result = eventTarget.dispatchEvent(event);
expect(result).toBe(true);
});
it("posts envelope with null payload", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const event = new CustomEvent("notify:1", {
detail: { type: "notify", id: "1", payload: null },
}) as TestEvent;
eventTarget.dispatchEvent(event);
expect(worker.posted[0]).toEqual({ type: "notify", id: "1", payload: null });
});
it("posts multiple events in order", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const event1 = new CustomEvent("a:1", { detail: { type: "a", id: "1", payload: "first" } }) as TestEvent;
const event2 = new CustomEvent("b:2", { detail: { type: "b", id: "2", payload: "second" } }) as TestEvent;
eventTarget.dispatchEvent(event1);
eventTarget.dispatchEvent(event2);
expect(worker.posted).toHaveLength(2);
expect(worker.posted[0]).toEqual({ type: "a", id: "1", payload: "first" });
expect(worker.posted[1]).toEqual({ type: "b", id: "2", payload: "second" });
});
});
describe("addEventListener (subscribe path)", () => {
it("registers a listener for a topic", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener = vi.fn();
eventTarget.addEventListener("call.responded:uuid-123", listener);
const envelope: EventEnvelope = { type: "call.responded", id: "uuid-123", payload: "hello" };
worker.simulateMessage(envelope);
expect(listener).toHaveBeenCalledTimes(1);
});
it("does nothing when callback is null", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
eventTarget.addEventListener("topic:a", null as any);
const envelope: EventEnvelope = { type: "topic", id: "a", payload: null };
worker.simulateMessage(envelope);
});
it("supports EventListenerObject with handleEvent", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const handleEvent = vi.fn();
const listenerObject = { handleEvent };
eventTarget.addEventListener("obj:test", listenerObject);
const envelope: EventEnvelope = { type: "obj", id: "test", payload: null };
worker.simulateMessage(envelope);
expect(handleEvent).toHaveBeenCalledTimes(1);
});
it("supports multiple listeners on the same topic", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener1 = vi.fn();
const listener2 = vi.fn();
eventTarget.addEventListener("topic:x", listener1);
eventTarget.addEventListener("topic:x", listener2);
const envelope: EventEnvelope = { type: "topic", id: "x", payload: "data" };
worker.simulateMessage(envelope);
expect(listener1).toHaveBeenCalledTimes(1);
expect(listener2).toHaveBeenCalledTimes(1);
});
});
describe("removeEventListener (unsubscribe path)", () => {
it("removes a listener so it no longer receives events", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener = vi.fn();
eventTarget.addEventListener("topic:a", listener);
eventTarget.removeEventListener("topic:a", listener);
const envelope: EventEnvelope = { type: "topic", id: "a", payload: null };
worker.simulateMessage(envelope);
expect(listener).not.toHaveBeenCalled();
});
it("removes only the specified listener, keeping others", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener1 = vi.fn();
const listener2 = vi.fn();
eventTarget.addEventListener("topic:a", listener1);
eventTarget.addEventListener("topic:a", listener2);
eventTarget.removeEventListener("topic:a", listener1);
const envelope: EventEnvelope = { type: "topic", id: "a", payload: "data" };
worker.simulateMessage(envelope);
expect(listener1).not.toHaveBeenCalled();
expect(listener2).toHaveBeenCalledTimes(1);
});
it("supports EventListenerObject removal", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const handleEvent = vi.fn();
const listenerObject = { handleEvent };
eventTarget.addEventListener("obj:test", listenerObject);
eventTarget.removeEventListener("obj:test", listenerObject);
const envelope: EventEnvelope = { type: "obj", id: "test", payload: null };
worker.simulateMessage(envelope);
expect(handleEvent).not.toHaveBeenCalled();
});
it("does nothing when removing a callback that was never registered", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const unregisteredListener = vi.fn();
eventTarget.removeEventListener("topic:a", unregisteredListener);
expect(() => eventTarget.removeEventListener("topic:a", unregisteredListener)).not.toThrow();
});
it("does nothing when callback is null", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
eventTarget.removeEventListener("topic:a", null as any);
});
});
describe("receive path (worker.onmessage)", () => {
it("parses envelope from event.data, creates CustomEvent with type:id topic, and dispatches to listeners", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener = vi.fn();
eventTarget.addEventListener("message.sent:msg1", listener);
const envelope: EventEnvelope = { type: "message.sent", id: "msg1", payload: "hello world" };
worker.simulateMessage(envelope);
expect(listener).toHaveBeenCalledTimes(1);
const receivedEvent = listener.mock.calls[0][0] as TestEvent;
expect(receivedEvent.type).toBe("message.sent:msg1");
expect(receivedEvent.detail).toEqual(envelope);
});
it("delivers messages to all listeners on the same topic", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener1 = vi.fn();
const listener2 = vi.fn();
eventTarget.addEventListener("topic:x", listener1);
eventTarget.addEventListener("topic:x", listener2);
const envelope: EventEnvelope = { type: "topic", id: "x", payload: "data" };
worker.simulateMessage(envelope);
expect(listener1).toHaveBeenCalledTimes(1);
expect(listener2).toHaveBeenCalledTimes(1);
expect((listener1.mock.calls[0][0] as TestEvent).detail).toEqual(envelope);
expect((listener2.mock.calls[0][0] as TestEvent).detail).toEqual(envelope);
});
it("ignores messages for topics with no registered listeners", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener = vi.fn();
eventTarget.addEventListener("topic:a", listener);
const envelope: EventEnvelope = { type: "other", id: "b", payload: null };
worker.simulateMessage(envelope);
expect(listener).not.toHaveBeenCalled();
});
});
describe("malformed message handling", () => {
it("ignores messages where envelope.type is not a string", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener = vi.fn();
eventTarget.addEventListener("topic:a", listener);
worker.simulateMessage({ type: 123, id: "a", payload: null });
expect(listener).not.toHaveBeenCalled();
});
it("ignores messages where envelope is null", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener = vi.fn();
eventTarget.addEventListener("topic:a", listener);
worker.simulateMessage(null);
expect(listener).not.toHaveBeenCalled();
});
it("ignores messages where envelope is undefined", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener = vi.fn();
eventTarget.addEventListener("topic:a", listener);
worker.simulateMessage(undefined);
expect(listener).not.toHaveBeenCalled();
});
it("ignores messages where envelope.type starts with __", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener = vi.fn();
eventTarget.addEventListener("__custom:thing", listener);
worker.simulateMessage({ type: "__custom", id: "thing", payload: null });
expect(listener).not.toHaveBeenCalled();
});
});
describe("topic scoping", () => {
it("forms topic from envelope type and id fields", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener = vi.fn();
eventTarget.addEventListener("user.action:abc-123", listener);
const envelope: EventEnvelope = { type: "user.action", id: "abc-123", payload: { done: true } };
worker.simulateMessage(envelope);
expect(listener).toHaveBeenCalledTimes(1);
expect((listener.mock.calls[0][0] as TestEvent).type).toBe("user.action:abc-123");
});
it("does not match when type differs even if id is the same", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener = vi.fn();
eventTarget.addEventListener("user.created:id1", listener);
worker.simulateMessage({ type: "user.deleted", id: "id1", payload: null });
expect(listener).not.toHaveBeenCalled();
});
it("does not match when id differs even if type is the same", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener = vi.fn();
eventTarget.addEventListener("event:alpha", listener);
worker.simulateMessage({ type: "event", id: "beta", payload: null });
expect(listener).not.toHaveBeenCalled();
});
});
describe("EventEnvelope round-trip", () => {
it("round-trips full { type, id, payload } envelope through dispatch and receive", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener = vi.fn();
eventTarget.addEventListener("user.joined:user-99", listener);
const originalEnvelope: EventEnvelope<"user.joined", { name: string; role: string }> = {
type: "user.joined",
id: "user-99",
payload: { name: "Bob", role: "admin" },
};
const event = new CustomEvent("user.joined:user-99", {
detail: originalEnvelope,
}) as TestEvent;
eventTarget.dispatchEvent(event);
const dispatchedData = worker.posted[0];
expect(dispatchedData).toEqual(originalEnvelope);
worker.simulateMessage(dispatchedData);
expect(listener).toHaveBeenCalledTimes(1);
const receivedDetail = (listener.mock.calls[0][0] as TestEvent).detail as EventEnvelope;
expect(receivedDetail).toEqual(originalEnvelope);
expect(receivedDetail.type).toBe("user.joined");
expect(receivedDetail.id).toBe("user-99");
expect(receivedDetail.payload).toEqual({ name: "Bob", role: "admin" });
});
it("round-trips envelope with null payload", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener = vi.fn();
eventTarget.addEventListener("ping:1", listener);
const envelope: EventEnvelope = { type: "ping", id: "1", payload: null };
worker.simulateMessage(envelope);
expect(listener).toHaveBeenCalledTimes(1);
const receivedDetail = (listener.mock.calls[0][0] as TestEvent).detail as EventEnvelope;
expect(receivedDetail).toEqual(envelope);
});
});
describe("worker.onerror", () => {
it("does not propagate worker errors to event target listeners", () => {
const worker = createMockWorker();
const eventTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const listener = vi.fn();
eventTarget.addEventListener("topic:a", listener);
if (worker.onerror) {
worker.onerror(new ErrorEvent("error", { message: "Worker failed" }));
}
expect(listener).not.toHaveBeenCalled();
});
});
});
describe("createWorkerThreadEventTarget", () => {
let originalGlobalThis: typeof globalThis;
let mockGlobal: ReturnType<typeof createMockGlobalThis>;
beforeEach(() => {
originalGlobalThis = globalThis;
mockGlobal = createMockGlobalThis();
});
function createThreadEventTargetWithMock() {
const callbacksForTopic = new Map<string, Set<EventListener>>();
mockGlobal.onmessage = (event: MessageEvent) => {
const envelope = event.data as EventEnvelope;
if (typeof envelope?.type !== "string" || envelope.type.startsWith("__")) {
return;
}
const topic = `${envelope.type}:${envelope.id}`;
const callbacks = callbacksForTopic.get(topic);
if (callbacks === undefined) {
return;
}
const customEvent = new CustomEvent(topic, {
detail: envelope,
}) as TestEvent;
for (const callback of callbacks) {
callback(customEvent);
}
};
function addCallback(topic: string, callback: EventListener) {
let callbacks = callbacksForTopic.get(topic);
if (callbacks === undefined) {
callbacks = new Set();
callbacksForTopic.set(topic, callbacks);
}
callbacks.add(callback);
}
function removeCallback(topic: string, callback: EventListener) {
const callbacks = callbacksForTopic.get(topic);
if (callbacks === undefined) {
return;
}
callbacks.delete(callback);
if (callbacks.size === 0) {
callbacksForTopic.delete(topic);
}
}
return {
eventTarget: {
addEventListener(topic: string, callbackOrOptions: EventListenerOrEventListenerObject) {
if (callbackOrOptions != null) {
const callback =
"handleEvent" in callbackOrOptions ? callbackOrOptions.handleEvent : callbackOrOptions;
addCallback(topic, callback);
}
},
dispatchEvent(event: TestEvent) {
mockGlobal.postMessage(event.detail);
return true;
},
removeEventListener(topic: string, callbackOrOptions: EventListenerOrEventListenerObject) {
if (callbackOrOptions != null) {
const callback =
"handleEvent" in callbackOrOptions ? callbackOrOptions.handleEvent : callbackOrOptions;
removeCallback(topic, callback);
}
},
} as TypedEventTarget<TestEvent>,
callbacksForTopic,
};
}
describe("dispatchEvent (send path)", () => {
it("posts event.detail via globalThis.postMessage", () => {
const { eventTarget } = createThreadEventTargetWithMock();
const envelope: EventEnvelope<"call.responded", { status: string }> = {
type: "call.responded",
id: "uuid-123",
payload: { status: "ok" },
};
const event = new CustomEvent("call.responded:uuid-123", {
detail: envelope,
}) as TestEvent;
eventTarget.dispatchEvent(event);
expect(mockGlobal.postMessage).toHaveBeenCalledTimes(1);
expect(mockGlobal.postMessage).toHaveBeenCalledWith(envelope);
});
it("returns true from dispatchEvent", () => {
const { eventTarget } = createThreadEventTargetWithMock();
const event = new CustomEvent("test:event", {
detail: { type: "test", id: "event", payload: null },
}) as TestEvent;
const result = eventTarget.dispatchEvent(event);
expect(result).toBe(true);
});
it("posts envelope with null payload", () => {
const { eventTarget } = createThreadEventTargetWithMock();
const event = new CustomEvent("notify:1", {
detail: { type: "notify", id: "1", payload: null },
}) as TestEvent;
eventTarget.dispatchEvent(event);
expect(mockGlobal.posted[0]).toEqual({ type: "notify", id: "1", payload: null });
});
});
describe("receive path (globalThis.onmessage)", () => {
it("parses envelope from event.data, creates CustomEvent with type:id topic, and dispatches to listeners", () => {
const { eventTarget } = createThreadEventTargetWithMock();
const listener = vi.fn();
eventTarget.addEventListener("message.sent:msg1", listener);
const envelope: EventEnvelope = { type: "message.sent", id: "msg1", payload: "hello" };
mockGlobal.simulateMessage(envelope);
expect(listener).toHaveBeenCalledTimes(1);
const receivedEvent = listener.mock.calls[0][0] as TestEvent;
expect(receivedEvent.type).toBe("message.sent:msg1");
expect(receivedEvent.detail).toEqual(envelope);
});
it("delivers messages to all listeners on the same topic", () => {
const { eventTarget } = createThreadEventTargetWithMock();
const listener1 = vi.fn();
const listener2 = vi.fn();
eventTarget.addEventListener("topic:x", listener1);
eventTarget.addEventListener("topic:x", listener2);
const envelope: EventEnvelope = { type: "topic", id: "x", payload: "data" };
mockGlobal.simulateMessage(envelope);
expect(listener1).toHaveBeenCalledTimes(1);
expect(listener2).toHaveBeenCalledTimes(1);
});
it("ignores messages for topics with no registered listeners", () => {
const { eventTarget } = createThreadEventTargetWithMock();
const listener = vi.fn();
eventTarget.addEventListener("topic:a", listener);
const envelope: EventEnvelope = { type: "other", id: "b", payload: null };
mockGlobal.simulateMessage(envelope);
expect(listener).not.toHaveBeenCalled();
});
});
describe("addEventListener", () => {
it("supports EventListenerObject with handleEvent", () => {
const { eventTarget } = createThreadEventTargetWithMock();
const handleEvent = vi.fn();
const listenerObject = { handleEvent };
eventTarget.addEventListener("obj:test", listenerObject);
const envelope: EventEnvelope = { type: "obj", id: "test", payload: null };
mockGlobal.simulateMessage(envelope);
expect(handleEvent).toHaveBeenCalledTimes(1);
});
});
describe("removeEventListener", () => {
it("removes a listener so it no longer receives events", () => {
const { eventTarget } = createThreadEventTargetWithMock();
const listener = vi.fn();
eventTarget.addEventListener("topic:a", listener);
eventTarget.removeEventListener("topic:a", listener);
const envelope: EventEnvelope = { type: "topic", id: "a", payload: null };
mockGlobal.simulateMessage(envelope);
expect(listener).not.toHaveBeenCalled();
});
it("supports EventListenerObject removal", () => {
const { eventTarget } = createThreadEventTargetWithMock();
const handleEvent = vi.fn();
const listenerObject = { handleEvent };
eventTarget.addEventListener("obj:test", listenerObject);
eventTarget.removeEventListener("obj:test", listenerObject);
const envelope: EventEnvelope = { type: "obj", id: "test", payload: null };
mockGlobal.simulateMessage(envelope);
expect(handleEvent).not.toHaveBeenCalled();
});
});
describe("malformed message handling", () => {
it("ignores messages where envelope.type is not a string", () => {
const { eventTarget } = createThreadEventTargetWithMock();
const listener = vi.fn();
eventTarget.addEventListener("topic:a", listener);
mockGlobal.simulateMessage({ type: 123, id: "a", payload: null });
expect(listener).not.toHaveBeenCalled();
});
it("ignores messages where envelope is null", () => {
const { eventTarget } = createThreadEventTargetWithMock();
const listener = vi.fn();
eventTarget.addEventListener("topic:a", listener);
mockGlobal.simulateMessage(null);
expect(listener).not.toHaveBeenCalled();
});
it("ignores messages where envelope.type starts with __", () => {
const { eventTarget } = createThreadEventTargetWithMock();
const listener = vi.fn();
eventTarget.addEventListener("__custom:thing", listener);
mockGlobal.simulateMessage({ type: "__custom", id: "thing", payload: null });
expect(listener).not.toHaveBeenCalled();
});
});
describe("EventEnvelope round-trip", () => {
it("round-trips full envelope through dispatch and receive", () => {
const { eventTarget } = createThreadEventTargetWithMock();
const listener = vi.fn();
eventTarget.addEventListener("user.joined:user-99", listener);
const originalEnvelope: EventEnvelope<"user.joined", { name: string; role: string }> = {
type: "user.joined",
id: "user-99",
payload: { name: "Bob", role: "admin" },
};
const event = new CustomEvent("user.joined:user-99", {
detail: originalEnvelope,
}) as TestEvent;
eventTarget.dispatchEvent(event);
const dispatchedData = mockGlobal.posted[0];
expect(dispatchedData).toEqual(originalEnvelope);
mockGlobal.simulateMessage(dispatchedData);
expect(listener).toHaveBeenCalledTimes(1);
const receivedDetail = (listener.mock.calls[0][0] as TestEvent).detail as EventEnvelope;
expect(receivedDetail).toEqual(originalEnvelope);
});
});
describe("topic scoping", () => {
it("forms topic from envelope type and id fields", () => {
const { eventTarget } = createThreadEventTargetWithMock();
const listener = vi.fn();
eventTarget.addEventListener("user.action:abc-123", listener);
const envelope: EventEnvelope = { type: "user.action", id: "abc-123", payload: { done: true } };
mockGlobal.simulateMessage(envelope);
expect(listener).toHaveBeenCalledTimes(1);
expect((listener.mock.calls[0][0] as TestEvent).type).toBe("user.action:abc-123");
});
});
});
describe("bidirectional communication (host + thread)", () => {
it("host sends envelope that thread receives", () => {
const worker = createMockWorker();
const hostTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
let threadOnmessage: ((event: { data: unknown }) => void) | null = null;
const threadCallbacks = new Map<string, Set<EventListener>>();
const threadTarget = {
addEventListener(topic: string, callbackOrOptions: EventListenerOrEventListenerObject) {
if (callbackOrOptions != null) {
const callback =
"handleEvent" in callbackOrOptions ? callbackOrOptions.handleEvent : callbackOrOptions;
let callbacks = threadCallbacks.get(topic);
if (callbacks === undefined) {
callbacks = new Set();
threadCallbacks.set(topic, callbacks);
}
callbacks.add(callback);
}
},
dispatchEvent(event: TestEvent) {
return true;
},
removeEventListener(topic: string, callbackOrOptions: EventListenerOrEventListenerObject) {
if (callbackOrOptions != null) {
const callback =
"handleEvent" in callbackOrOptions ? callbackOrOptions.handleEvent : callbackOrOptions;
const callbacks = threadCallbacks.get(topic);
if (callbacks) {
callbacks.delete(callback);
if (callbacks.size === 0) {
threadCallbacks.delete(topic);
}
}
}
},
} as TypedEventTarget<TestEvent>;
threadOnmessage = (event: { data: unknown }) => {
const envelope = event.data as EventEnvelope;
if (typeof envelope?.type !== "string" || envelope.type.startsWith("__")) {
return;
}
const topic = `${envelope.type}:${envelope.id}`;
const callbacks = threadCallbacks.get(topic);
if (callbacks === undefined) return;
const customEvent = new CustomEvent(topic, { detail: envelope }) as TestEvent;
for (const callback of callbacks) {
callback(customEvent);
}
};
const threadListener = vi.fn();
threadTarget.addEventListener("task.assigned:task-1", threadListener);
const envelope: EventEnvelope = { type: "task.assigned", id: "task-1", payload: { work: "compute" } };
const hostEvent = new CustomEvent("task.assigned:task-1", { detail: envelope }) as TestEvent;
hostTarget.dispatchEvent(hostEvent);
expect(worker.postMessage).toHaveBeenCalledWith(envelope);
const postedEnvelope = worker.posted[0] as EventEnvelope;
threadOnmessage!({ data: postedEnvelope });
expect(threadListener).toHaveBeenCalledTimes(1);
const receivedDetail = (threadListener.mock.calls[0][0] as TestEvent).detail as EventEnvelope;
expect(receivedDetail).toEqual(envelope);
});
it("thread sends envelope that host receives", () => {
const worker = createMockWorker();
const hostTarget = createWorkerHostEventTarget<TestEvent>(worker as any);
const hostListener = vi.fn();
hostTarget.addEventListener("result.ready:res-1", hostListener);
const envelope: EventEnvelope = { type: "result.ready", id: "res-1", payload: { output: 42 } };
worker.simulateMessage(envelope);
expect(hostListener).toHaveBeenCalledTimes(1);
const receivedDetail = (hostListener.mock.calls[0][0] as TestEvent).detail as EventEnvelope;
expect(receivedDetail).toEqual(envelope);
});
});