Skip to content

Commit d1fbcad

Browse files
committed
Collapsed class into the primitive
1 parent 8abab23 commit d1fbcad

File tree

2 files changed

+53
-77
lines changed

2 files changed

+53
-77
lines changed

packages/sse/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -395,9 +395,10 @@ const { data } = createSSE("https://api.example.com/events", {
395395

396396
`makeSSEWorker(target)` returns an `SSESourceFn`, the same factory interface that `createSSE` uses internally. When `createSSE` opens a connection it calls this factory instead of the default `makeSSE`, which:
397397

398-
1. Creates a `WorkerEventSource` — an `EventTarget` that posts a `connect` message to the Worker and re-dispatches `open` / `message` / `error` events received back from it.
399-
2. The Worker script (`worker-handler`) receives the `connect` message, creates a real `EventSource` there, and posts events back via `postMessage`.
400-
3. `createSSE`'s reactive machinery — signals, reconnect timer, URL tracking, `onCleanup` — runs on the main thread as normal; it just talks to a `WorkerEventSource` instead of a real `EventSource`.
398+
1. Creates a plain `EventTarget` with a `readyState` property and a `close()` method, satisfying the `SSESourceHandle` interface without needing a real `EventSource`.
399+
2. Posts a `connect` message to the Worker. The Worker script (`worker-handler`) creates a real `EventSource` there and posts `open` / `message` / `error` events back via `postMessage`.
400+
3. The message listener on the main thread forwards those events to `createSSE`'s callbacks and dispatches them on the `EventTarget` so any direct `addEventListener` calls also work.
401+
4. `createSSE`'s reactive machinery — signals, reconnect timer, URL tracking, `onCleanup` — runs on the main thread as normal; it just receives events via `postMessage` instead of directly from a real `EventSource`.
401402

402403
### Type reference
403404

packages/sse/src/worker.ts

Lines changed: 49 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -21,62 +21,6 @@ export type SSEWorkerTarget = {
2121
removeEventListener(type: "message", listener: (e: MessageEvent<SSEWorkerMessage>) => void): void;
2222
};
2323

24-
// ─── WorkerEventSource ────────────────────────────────────────────────────────
25-
26-
/**
27-
* An `EventTarget` facade that tunnels SSE events through a Worker.
28-
* Not exported — consumers use `makeSSEWorker` to obtain instances.
29-
*/
30-
class WorkerEventSource extends EventTarget {
31-
private _readyState: SSEReadyStateValue = SSEReadyState.CONNECTING;
32-
33-
get readyState(): SSEReadyStateValue {
34-
return this._readyState;
35-
}
36-
37-
private readonly _id: string;
38-
private readonly _target: SSEWorkerTarget;
39-
private readonly _listener: (e: MessageEvent<SSEWorkerMessage>) => void;
40-
41-
constructor(target: SSEWorkerTarget, url: string, options: SSEOptions) {
42-
super();
43-
44-
this._id = Math.random().toString(36).slice(2, 11);
45-
this._target = target;
46-
47-
this._listener = (e: MessageEvent<SSEWorkerMessage>) => {
48-
const msg = e.data;
49-
if (msg.id !== this._id) return;
50-
51-
if (msg.type === "open") {
52-
this._readyState = SSEReadyState.OPEN;
53-
this.dispatchEvent(new Event("open"));
54-
} else if (msg.type === "message") {
55-
this.dispatchEvent(new MessageEvent(msg.eventType, { data: msg.data }));
56-
} else if (msg.type === "error") {
57-
this._readyState = msg.readyState;
58-
this.dispatchEvent(new Event("error"));
59-
}
60-
};
61-
62-
target.addEventListener("message", this._listener);
63-
64-
target.postMessage({
65-
type: "connect",
66-
id: this._id,
67-
url,
68-
withCredentials: options.withCredentials,
69-
events: options.events ? Object.keys(options.events) : undefined,
70-
});
71-
}
72-
73-
close() {
74-
this._readyState = SSEReadyState.CLOSED;
75-
this._target.postMessage({ type: "disconnect", id: this._id });
76-
this._target.removeEventListener("message", this._listener);
77-
}
78-
}
79-
8024
// ─── makeSSEWorker ────────────────────────────────────────────────────────────
8125

8226
/**
@@ -100,28 +44,59 @@ class WorkerEventSource extends EventTarget {
10044
*/
10145
export function makeSSEWorker(target: SSEWorkerTarget): SSESourceFn {
10246
return (url: string, options: SSEOptions) => {
103-
const source = new WorkerEventSource(target, url, options);
47+
const id = Math.random().toString(36).slice(2, 11);
48+
let readyState: SSEReadyStateValue = SSEReadyState.CONNECTING;
49+
50+
// SSESourceHandle requires EventTarget & { readyState, close }.
51+
// We keep EventTarget so callers can use addEventListener directly on the source.
52+
const source = new EventTarget() as EventTarget & {
53+
readyState: SSEReadyStateValue;
54+
close(): void;
55+
};
10456

105-
if (options.onOpen) source.addEventListener("open", options.onOpen);
106-
if (options.onMessage) source.addEventListener("message", options.onMessage as EventListener);
107-
if (options.onError) source.addEventListener("error", options.onError);
108-
if (options.events) {
109-
for (const [name, handler] of Object.entries(options.events))
110-
source.addEventListener(name, handler as EventListener);
111-
}
57+
Object.defineProperty(source, "readyState", { get: () => readyState });
11258

113-
const cleanup = () => {
114-
source.close();
115-
if (options.onOpen) source.removeEventListener("open", options.onOpen);
116-
if (options.onMessage)
117-
source.removeEventListener("message", options.onMessage as EventListener);
118-
if (options.onError) source.removeEventListener("error", options.onError);
119-
if (options.events) {
120-
for (const [name, handler] of Object.entries(options.events))
121-
source.removeEventListener(name, handler as EventListener);
59+
const listener = (e: MessageEvent<SSEWorkerMessage>) => {
60+
const msg = e.data;
61+
if (msg.id !== id) return;
62+
63+
if (msg.type === "open") {
64+
readyState = SSEReadyState.OPEN;
65+
const ev = new Event("open");
66+
source.dispatchEvent(ev);
67+
options.onOpen?.(ev);
68+
} else if (msg.type === "message") {
69+
const ev = new MessageEvent(msg.eventType, { data: msg.data });
70+
source.dispatchEvent(ev);
71+
if (msg.eventType === "message") {
72+
options.onMessage?.(ev);
73+
} else {
74+
options.events?.[msg.eventType]?.(ev);
75+
}
76+
} else if (msg.type === "error") {
77+
readyState = msg.readyState;
78+
const ev = new Event("error");
79+
source.dispatchEvent(ev);
80+
options.onError?.(ev);
12281
}
12382
};
12483

125-
return [source, cleanup];
84+
source.close = () => {
85+
readyState = SSEReadyState.CLOSED;
86+
target.postMessage({ type: "disconnect", id });
87+
target.removeEventListener("message", listener);
88+
};
89+
90+
target.addEventListener("message", listener);
91+
92+
target.postMessage({
93+
type: "connect",
94+
id,
95+
url,
96+
withCredentials: options.withCredentials,
97+
events: options.events ? Object.keys(options.events) : undefined,
98+
});
99+
100+
return [source, () => source.close()];
126101
};
127102
}

0 commit comments

Comments
 (0)