feat(ws-client): implement WebSocket client event target adapter
This commit is contained in:
100
src/event-target-websocket-client.ts
Normal file
100
src/event-target-websocket-client.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { TypedEventTarget, TypedEvent, EventEnvelope } from "./types.js";
|
||||
|
||||
export function createWebSocketClientEventTarget<TEvent extends TypedEvent>(
|
||||
ws: WebSocket,
|
||||
): TypedEventTarget<TEvent> {
|
||||
const callbacksForTopic = new Map<string, Set<EventListener>>();
|
||||
|
||||
ws.onmessage = (event: MessageEvent) => {
|
||||
let envelope: EventEnvelope;
|
||||
try {
|
||||
envelope = JSON.parse(event.data as string) as EventEnvelope;
|
||||
} catch {
|
||||
console.warn(
|
||||
`Failed to parse WebSocket message: ${event.data}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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 TEvent;
|
||||
|
||||
for (const callback of callbacks) {
|
||||
callback(customEvent);
|
||||
}
|
||||
};
|
||||
|
||||
function addCallback(topic: string, callback: EventListener) {
|
||||
let callbacks = callbacksForTopic.get(topic);
|
||||
const isFirst = callbacks === undefined;
|
||||
if (isFirst) {
|
||||
callbacks = new Set();
|
||||
callbacksForTopic.set(topic, callbacks);
|
||||
}
|
||||
callbacks!.add(callback);
|
||||
|
||||
if (isFirst) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "__subscribe",
|
||||
id: "",
|
||||
payload: { topic },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function removeCallback(topic: string, callback: EventListener) {
|
||||
const callbacks = callbacksForTopic.get(topic);
|
||||
if (callbacks === undefined) {
|
||||
return;
|
||||
}
|
||||
const existed = callbacks.delete(callback);
|
||||
if (!existed) {
|
||||
return;
|
||||
}
|
||||
if (callbacks.size > 0) {
|
||||
return;
|
||||
}
|
||||
callbacksForTopic.delete(topic);
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "__unsubscribe",
|
||||
id: "",
|
||||
payload: { topic },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
addEventListener(topic, callbackOrOptions: EventListenerOrEventListenerObject) {
|
||||
if (callbackOrOptions != null) {
|
||||
const callback =
|
||||
"handleEvent" in callbackOrOptions ? callbackOrOptions.handleEvent : callbackOrOptions;
|
||||
addCallback(topic, callback);
|
||||
}
|
||||
},
|
||||
dispatchEvent(event: TEvent) {
|
||||
ws.send(JSON.stringify(event.detail));
|
||||
return true;
|
||||
},
|
||||
removeEventListener(topic, callbackOrOptions: EventListenerOrEventListenerObject) {
|
||||
if (callbackOrOptions != null) {
|
||||
const callback =
|
||||
"handleEvent" in callbackOrOptions ? callbackOrOptions.handleEvent : callbackOrOptions;
|
||||
removeCallback(topic, callback);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -2,4 +2,5 @@ export { createPubSub, type PubSub, type PubSubConfig, type PubSubEvent, type Pu
|
||||
export { type EventEnvelope, type TypedEvent, type TypedEventTarget, type TypedEventListener, type TypedEventListenerObject, type TypedEventListenerOrEventListenerObject } from "./types.js";
|
||||
export { filter, map, pipe, take, reduce, toArray, batch, dedupe, window, flat, groupBy, chain, join } from "./operators.js";
|
||||
export { Repeater, RepeaterOverflowError, type Push, type Stop, type RepeaterExecutor, type RepeaterBuffer } from "./repeater.js";
|
||||
export { createRedisEventTarget, type CreateRedisEventTargetArgs } from "./event-target-redis.js";
|
||||
export { createRedisEventTarget, type CreateRedisEventTargetArgs } from "./event-target-redis.js";
|
||||
export { createWebSocketClientEventTarget } from "./event-target-websocket-client.js";
|
||||
Reference in New Issue
Block a user