Streaming: refactor, add unsubscribeAll method
This commit is contained in:
parent
3ffad1df29
commit
ec5e0ed330
|
@ -40,7 +40,7 @@ const streamingController: AppController = (c) => {
|
||||||
|
|
||||||
socket.addEventListener('close', () => {
|
socket.addEventListener('close', () => {
|
||||||
console.log('websocket: connection closed');
|
console.log('websocket: connection closed');
|
||||||
ws.unsubscribe(conn, { name: stream! });
|
ws.unsubscribeAll(socket);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|
|
@ -9,8 +9,6 @@ function getSignStream(c: AppContext): WebSocket | undefined {
|
||||||
const pubkey = c.get('pubkey');
|
const pubkey = c.get('pubkey');
|
||||||
const session = c.get('session');
|
const session = c.get('session');
|
||||||
|
|
||||||
console.log(`nostr:${pubkey}:${session}`);
|
|
||||||
|
|
||||||
if (pubkey && session) {
|
if (pubkey && session) {
|
||||||
const [socket] = ws.getSockets(`nostr:${pubkey}:${session}`);
|
const [socket] = ws.getSockets(`nostr:${pubkey}:${session}`);
|
||||||
return socket;
|
return socket;
|
||||||
|
|
129
src/stream.ts
129
src/stream.ts
|
@ -1,49 +1,118 @@
|
||||||
|
/** Internal key for event subscriptions. */
|
||||||
type Topic = string;
|
type Topic = string;
|
||||||
|
|
||||||
|
/** Only the necessary metadata needed from the request. */
|
||||||
interface StreamConn {
|
interface StreamConn {
|
||||||
|
/** Hex pubkey parsed from the `Sec-Websocket-Protocol` header. */
|
||||||
pubkey?: string;
|
pubkey?: string;
|
||||||
|
/** Base62 session UUID parsed from the `Sec-Websocket-Protocol` header. */
|
||||||
session?: string;
|
session?: string;
|
||||||
|
/** The WebSocket stream. */
|
||||||
socket: WebSocket;
|
socket: WebSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Requested streaming channel, eg `user`, `notifications`. Some channels like `hashtag` have additional params. */
|
||||||
// TODO: Make this a discriminated union (needed for hashtags).
|
// TODO: Make this a discriminated union (needed for hashtags).
|
||||||
interface Stream {
|
interface Stream {
|
||||||
|
/** Name of the channel, eg `user`. */
|
||||||
name: string;
|
name: string;
|
||||||
|
/** Additional query params, eg `tag`. */
|
||||||
params?: Record<string, string>;
|
params?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sockets = new Map<Topic, Set<WebSocket>>();
|
/** Class to organize WebSocket connections by topic. */
|
||||||
|
class WebSocketConnections {
|
||||||
|
/** Set of WebSockets by topic. */
|
||||||
|
#sockets = new Map<Topic, Set<WebSocket>>();
|
||||||
|
/** Set of topics by WebSocket. We need to track this so we can unsubscribe properly. */
|
||||||
|
#topics = new WeakMap<WebSocket, Set<Topic>>();
|
||||||
|
|
||||||
function addSocket(socket: WebSocket, topic: Topic): void {
|
/** Add the WebSocket to the streaming channel. */
|
||||||
let subscribers = sockets.get(topic);
|
subscribe(conn: StreamConn, stream: Stream): void {
|
||||||
if (!subscribers) {
|
const topic = getTopic(conn, stream);
|
||||||
subscribers = new Set<WebSocket>();
|
|
||||||
sockets.set(topic, subscribers);
|
|
||||||
}
|
|
||||||
subscribers.add(socket);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeSocket(socket: WebSocket, topic: Topic): void {
|
if (topic) {
|
||||||
const subscribers = sockets.get(topic);
|
this.#addSocket(conn.socket, topic);
|
||||||
if (subscribers) {
|
this.#addTopic(conn.socket, topic);
|
||||||
subscribers.delete(socket);
|
|
||||||
if (subscribers.size === 0) {
|
|
||||||
sockets.delete(topic);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function subscribe(conn: StreamConn, stream: Stream): void {
|
/** Remove the WebSocket from the streaming channel. */
|
||||||
const topic = getTopic(conn, stream);
|
unsubscribe(conn: StreamConn, stream: Stream): void {
|
||||||
if (topic) {
|
const topic = getTopic(conn, stream);
|
||||||
addSocket(conn.socket, topic);
|
|
||||||
|
if (topic) {
|
||||||
|
this.#removeSocket(conn.socket, topic);
|
||||||
|
this.#removeTopic(conn.socket, topic);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function unsubscribe(conn: StreamConn, stream: Stream): void {
|
/** Remove the WebSocket from all its streaming channels. */
|
||||||
const topic = getTopic(conn, stream);
|
unsubscribeAll(socket: WebSocket): void {
|
||||||
if (topic) {
|
const topics = this.#topics.get(socket);
|
||||||
removeSocket(conn.socket, topic);
|
|
||||||
|
if (topics) {
|
||||||
|
for (const topic of topics) {
|
||||||
|
this.#removeSocket(socket, topic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#topics.delete(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get WebSockets for the given topic. */
|
||||||
|
getSockets(topic: Topic): Set<WebSocket> {
|
||||||
|
return this.#sockets.get(topic) ?? new Set<WebSocket>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a WebSocket to a topics set in the state. */
|
||||||
|
#addSocket(socket: WebSocket, topic: Topic): void {
|
||||||
|
let subscribers = this.#sockets.get(topic);
|
||||||
|
|
||||||
|
if (!subscribers) {
|
||||||
|
subscribers = new Set<WebSocket>();
|
||||||
|
this.#sockets.set(topic, subscribers);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribers.add(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove a WebSocket from a topics set in the state. */
|
||||||
|
#removeSocket(socket: WebSocket, topic: Topic): void {
|
||||||
|
const subscribers = this.#sockets.get(topic);
|
||||||
|
|
||||||
|
if (subscribers) {
|
||||||
|
subscribers.delete(socket);
|
||||||
|
|
||||||
|
if (subscribers.size === 0) {
|
||||||
|
this.#sockets.delete(topic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a topic to a WebSocket set in the state. */
|
||||||
|
#addTopic(socket: WebSocket, topic: Topic): void {
|
||||||
|
let topics = this.#topics.get(socket);
|
||||||
|
|
||||||
|
if (!topics) {
|
||||||
|
topics = new Set<Topic>();
|
||||||
|
this.#topics.set(socket, topics);
|
||||||
|
}
|
||||||
|
|
||||||
|
topics.add(topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove a topic from a WebSocket set in the state. */
|
||||||
|
#removeTopic(socket: WebSocket, topic: Topic): void {
|
||||||
|
const topics = this.#topics.get(socket);
|
||||||
|
|
||||||
|
if (topics) {
|
||||||
|
topics.delete(topic);
|
||||||
|
|
||||||
|
if (topics.size === 0) {
|
||||||
|
this.#topics.delete(socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,14 +136,6 @@ function getTopic(conn: StreamConn, stream: Stream): Topic | undefined {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSockets(topic: Topic): Set<WebSocket> {
|
const ws = new WebSocketConnections();
|
||||||
return sockets.get(topic) ?? new Set<WebSocket>();
|
|
||||||
}
|
|
||||||
|
|
||||||
const ws = {
|
|
||||||
subscribe,
|
|
||||||
unsubscribe,
|
|
||||||
getSockets,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ws;
|
export default ws;
|
||||||
|
|
Loading…
Reference in New Issue