@digo-labs/ws 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/client.d.ts +1 -0
- package/dist/client.js +27 -40
- package/dist/client.js.map +1 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.js +60 -43
- package/dist/server.js.map +1 -1
- package/dist/start-hub.cjs +8035 -23196
- package/dist/start-hub.cjs.map +1 -1
- package/dist/start-hub.css +60 -27
- package/dist/start-hub.css.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ npm install @digo-labs/ws
|
|
|
10
10
|
|
|
11
11
|
## Client
|
|
12
12
|
|
|
13
|
-
`new WebsocketClient(config)` connects straight away. Every config field is optional: `url` (default `WS_URL` from `@digo-labs/common`, `wss://ws.digo-labs.com`), `app` (default `'unknown'`), `id` (default a random UUID), `reconnect` (default `true`), `reconnectInterval` / `maxReconnectInterval` (default `1000` / `10000` ms, doubled with jitter) and `getToken` (an async function returning a realtime token; without it the client identifies anonymously).
|
|
13
|
+
`new WebsocketClient(config)` connects straight away. Every config field is optional: `url` (default `WS_URL` from `@digo-labs/common`, `wss://ws.digo-labs.com`), `app` (default `'unknown'`), `id` (default a random UUID), `reconnect` (default `true`), `reconnectInterval` / `maxReconnectInterval` (default `1000` / `10000` ms, doubled with jitter) and `getToken` (an async function returning a realtime token; without it the client identifies anonymously). The client calls `getToken` again halfway through each token's life; the hub closes a connection whose token expired with code `4408`, and the client reconnects.
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
16
|
import { WebsocketClient } from '@digo-labs/ws';
|
|
@@ -42,7 +42,7 @@ hub.getState('scores');
|
|
|
42
42
|
hub.close();
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
It also answers `GET /health` with `{ ok, clients }`. `tokenSecret` lets it verify the tokens clients send through `getToken`.
|
|
45
|
+
It also answers `GET /health` with `{ ok, clients }`. `tokenSecret` lets it verify the tokens clients send through `getToken`. With `requireToken: true` it closes with code `4401` every client that doesn't identify with a valid token within 5 seconds, or sends anything else first; it won't start without a `tokenSecret`. A tokenless `WebsocketClient` keeps reconnecting at its slowest interval, so after turning it on, clients that reconnect every few seconds and close with `4401` are tokenless clients, not an outage.
|
|
46
46
|
|
|
47
47
|
## Running the hub locally
|
|
48
48
|
|
|
@@ -51,4 +51,4 @@ npx digo-ws-hub # port 9900
|
|
|
51
51
|
npx digo-ws-hub 9901 # another port
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
The CLI reads `WS_TOKEN_SECRET` from the environment and shuts down cleanly on Ctrl+C. Inside the core monorepo, `npm run ws` (or `npm run dev`) starts the same hub from source.
|
|
54
|
+
The CLI reads `WS_TOKEN_SECRET` and `WS_REQUIRE_TOKEN` (`1` turns on `requireToken`, `0` or unset leaves it off, anything else stops the CLI) from the environment and shuts down cleanly on Ctrl+C. Inside the core monorepo, `npm run ws` (or `npm run dev`) starts the same hub from source.
|
package/dist/client.d.ts
CHANGED
package/dist/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
var
|
|
1
|
+
import { REALTIME_TOKEN_SECONDS as e, RealtimeHelpers as t, WS_URL as n, WebSocketHubEvent as r, WebsocketClientEvent as i } from "@digo-labs/common";
|
|
2
|
+
// src/client.ts
|
|
3
|
+
var a = 4e3, o = 4401, s = .5, c = 5e3, l = e * 1e3 * s, u = class {
|
|
4
4
|
url;
|
|
5
5
|
app;
|
|
6
6
|
id;
|
|
@@ -21,30 +21,22 @@ var r = 4e3, i = 4401, a = 6e5, o = class {
|
|
|
21
21
|
readyHandlers = [];
|
|
22
22
|
closeHandlers = [];
|
|
23
23
|
errorHandlers = [];
|
|
24
|
-
constructor(
|
|
25
|
-
this.url =
|
|
24
|
+
constructor(e) {
|
|
25
|
+
this.url = e.url ?? n, this.app = e.app ?? "unknown", this.id = e.id ?? crypto.randomUUID(), this.reconnectEnabled = e.reconnect ?? !0, this.reconnectInterval = e.reconnectInterval ?? 1e3, this.maxReconnectInterval = e.maxReconnectInterval ?? 1e4, this.reconnectDelay = this.reconnectInterval, this.getToken = e.getToken ?? null, this.open();
|
|
26
26
|
}
|
|
27
27
|
open() {
|
|
28
28
|
this.socket = new WebSocket(this.url), this.isIdentified = !1, this.socket.onopen = () => {
|
|
29
29
|
this.reconnectDelay = this.reconnectInterval, this.identify(), this.openHandlers.forEach((e) => e());
|
|
30
30
|
}, this.socket.onmessage = (e) => {
|
|
31
|
-
let
|
|
31
|
+
let t;
|
|
32
32
|
try {
|
|
33
|
-
|
|
33
|
+
t = JSON.parse(e.data);
|
|
34
34
|
} catch {
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
this.handleIdentified();
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
if (n.type === t.DENIED) {
|
|
42
|
-
n.channel && this.deniedChannels.add(n.channel);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
n.channel && this.listeners.get(n.channel)?.forEach((e) => e(n.data));
|
|
37
|
+
t.type === r.IDENTIFIED ? this.handleIdentified() : t.type === r.DENIED ? t.channel && this.deniedChannels.add(t.channel) : t.channel && this.listeners.get(t.channel)?.forEach((e) => e(t.data));
|
|
46
38
|
}, this.socket.onclose = (e) => {
|
|
47
|
-
this.isIdentified = !1, this.pending = [], this.stopRefresh(), this.closeHandlers.forEach((e) => e()), e.code !==
|
|
39
|
+
this.isIdentified = !1, this.pending = [], this.stopRefresh(), this.closeHandlers.forEach((e) => e()), e.code !== a && this.alive && this.reconnectEnabled && this.scheduleReconnect(e.code === o);
|
|
48
40
|
}, this.socket.onerror = (e) => {
|
|
49
41
|
this.errorHandlers.forEach((t) => t(e));
|
|
50
42
|
};
|
|
@@ -52,32 +44,32 @@ var r = 4e3, i = 4401, a = 6e5, o = class {
|
|
|
52
44
|
async identify() {
|
|
53
45
|
if (!this.getToken) {
|
|
54
46
|
this.sendRaw({
|
|
55
|
-
type:
|
|
47
|
+
type: i.IDENTIFY,
|
|
56
48
|
id: this.id,
|
|
57
49
|
app: this.app
|
|
58
50
|
}), this.handleIdentified();
|
|
59
51
|
return;
|
|
60
52
|
}
|
|
61
53
|
let e = await this.getToken().catch(() => null);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
this.sendRaw({
|
|
67
|
-
type: n.IDENTIFY,
|
|
54
|
+
this.alive && this.socket?.readyState === WebSocket.OPEN && (e ? (this.sendRaw({
|
|
55
|
+
type: i.IDENTIFY,
|
|
68
56
|
id: this.id,
|
|
69
57
|
app: this.app,
|
|
70
58
|
token: e
|
|
71
|
-
});
|
|
59
|
+
}), this.scheduleRefresh(e)) : this.socket.close());
|
|
60
|
+
}
|
|
61
|
+
scheduleRefresh(e) {
|
|
62
|
+
let n = t.readLifetime(e), r = n ? Math.max(n * s, c) : l;
|
|
63
|
+
this.stopRefresh(), this.refreshTimer = setTimeout(() => void this.identify(), r);
|
|
72
64
|
}
|
|
73
65
|
handleIdentified() {
|
|
74
66
|
this.isIdentified || (this.isIdentified = !0, this.deniedChannels.clear(), this.subscribedChannels.size > 0 && this.sendRaw({
|
|
75
|
-
type:
|
|
67
|
+
type: i.SUBSCRIBE,
|
|
76
68
|
channels: [...this.subscribedChannels]
|
|
77
|
-
}), this.pending.splice(0).forEach((e) => this.sendRaw(e)), this.
|
|
69
|
+
}), this.pending.splice(0).forEach((e) => this.sendRaw(e)), this.readyHandlers.forEach((e) => e()));
|
|
78
70
|
}
|
|
79
71
|
stopRefresh() {
|
|
80
|
-
this.refreshTimer &&
|
|
72
|
+
this.refreshTimer && clearTimeout(this.refreshTimer), this.refreshTimer = null;
|
|
81
73
|
}
|
|
82
74
|
scheduleReconnect(e) {
|
|
83
75
|
let t = (e ? this.maxReconnectInterval : this.reconnectDelay) * (.5 + Math.random());
|
|
@@ -89,38 +81,34 @@ var r = 4e3, i = 4401, a = 6e5, o = class {
|
|
|
89
81
|
this.socket?.readyState === WebSocket.OPEN && this.socket.send(JSON.stringify(e));
|
|
90
82
|
}
|
|
91
83
|
sendToHub(e) {
|
|
92
|
-
|
|
93
|
-
this.sendRaw(e);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
(e.type === n.MESSAGE || e.type === n.STATE) && this.socket?.readyState === WebSocket.OPEN && this.pending.push(e);
|
|
84
|
+
this.isIdentified ? this.sendRaw(e) : (e.type === i.MESSAGE || e.type === i.STATE) && this.socket?.readyState === WebSocket.OPEN && this.pending.push(e);
|
|
97
85
|
}
|
|
98
86
|
send(e, t) {
|
|
99
87
|
this.sendToHub({
|
|
100
|
-
type:
|
|
88
|
+
type: i.MESSAGE,
|
|
101
89
|
channel: e,
|
|
102
90
|
data: t
|
|
103
91
|
});
|
|
104
92
|
}
|
|
105
93
|
setState(e, t) {
|
|
106
94
|
this.sendToHub({
|
|
107
|
-
type:
|
|
95
|
+
type: i.STATE,
|
|
108
96
|
channel: e,
|
|
109
97
|
data: t
|
|
110
98
|
});
|
|
111
99
|
}
|
|
112
100
|
subscribe(e, t) {
|
|
113
101
|
this.listeners.has(e) || this.listeners.set(e, /* @__PURE__ */ new Set()), this.listeners.get(e).add(t), this.subscribedChannels.has(e) || (this.subscribedChannels.add(e), this.deniedChannels.delete(e), this.sendToHub({
|
|
114
|
-
type:
|
|
102
|
+
type: i.SUBSCRIBE,
|
|
115
103
|
channels: [e]
|
|
116
104
|
}));
|
|
117
105
|
}
|
|
118
106
|
unsubscribe(e, t) {
|
|
119
107
|
t ? (this.listeners.get(e)?.delete(t), this.listeners.get(e)?.size === 0 && (this.listeners.delete(e), this.subscribedChannels.delete(e), this.deniedChannels.delete(e), this.sendToHub({
|
|
120
|
-
type:
|
|
108
|
+
type: i.UNSUBSCRIBE,
|
|
121
109
|
channels: [e]
|
|
122
110
|
}))) : (this.listeners.delete(e), this.subscribedChannels.delete(e), this.deniedChannels.delete(e), this.sendToHub({
|
|
123
|
-
type:
|
|
111
|
+
type: i.UNSUBSCRIBE,
|
|
124
112
|
channels: [e]
|
|
125
113
|
}));
|
|
126
114
|
}
|
|
@@ -146,7 +134,6 @@ var r = 4e3, i = 4401, a = 6e5, o = class {
|
|
|
146
134
|
this.alive = !1, this.isIdentified = !1, this.stopRefresh(), this.socket?.close();
|
|
147
135
|
}
|
|
148
136
|
};
|
|
149
|
-
|
|
150
|
-
export { o as WebsocketClient };
|
|
137
|
+
export { u as WebsocketClient };
|
|
151
138
|
|
|
152
139
|
//# sourceMappingURL=client.js.map
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["import { WebsocketConnectionConfig, WebsocketClientEvent, WebsocketClientMessage, WebSocketHubEvent, WebsocketHubMessage, WebsocketListener, WS_URL } from '@digo-labs/common';\n\nconst KICKED_CODE = 4000;\nconst DENIED_CODE = 4401;\nconst REFRESH_INTERVAL = 600_000;\n\nexport class WebsocketClient {\n private url: string;\n private app: string;\n private id: string;\n private reconnectEnabled: boolean;\n private reconnectInterval: number;\n private maxReconnectInterval: number;\n private reconnectDelay: number;\n private getToken: (() => Promise<string | null>) | null;\n\n private socket: WebSocket | null = null;\n private alive = true;\n private isIdentified = false;\n private refreshTimer: ReturnType<typeof setInterval> | null = null;\n private listeners = new Map<string, Set<WebsocketListener>>();\n private subscribedChannels = new Set<string>();\n private deniedChannels = new Set<string>();\n private pending: WebsocketClientMessage[] = [];\n private openHandlers: (() => void)[] = [];\n private readyHandlers: (() => void)[] = [];\n private closeHandlers: (() => void)[] = [];\n private errorHandlers: ((error: Event) => void)[] = [];\n\n constructor(properties: WebsocketConnectionConfig) {\n this.url = properties.url ?? WS_URL;\n this.app = properties.app ?? 'unknown';\n this.id = properties.id ?? crypto.randomUUID();\n this.reconnectEnabled = properties.reconnect ?? true;\n this.reconnectInterval = properties.reconnectInterval ?? 1000;\n this.maxReconnectInterval = properties.maxReconnectInterval ?? 10000;\n this.reconnectDelay = this.reconnectInterval;\n this.getToken = properties.getToken ?? null;\n\n this.open();\n }\n\n private open(): void {\n this.socket = new WebSocket(this.url);\n this.isIdentified = false;\n\n this.socket.onopen = () => {\n this.reconnectDelay = this.reconnectInterval;\n\n void this.identify();\n\n this.openHandlers.forEach(h => h());\n };\n\n this.socket.onmessage = (event) => {\n let message: WebsocketHubMessage;\n\n try {\n message = JSON.parse(event.data as string);\n } catch { return; }\n\n if (message.type === WebSocketHubEvent.IDENTIFIED) {\n this.handleIdentified();\n\n return;\n }\n\n if (message.type === WebSocketHubEvent.DENIED) {\n if (message.channel) this.deniedChannels.add(message.channel);\n\n return;\n }\n\n if (!message.channel) return;\n\n this.listeners.get(message.channel)?.forEach(listener => listener(message.data));\n };\n\n this.socket.onclose = (event) => {\n this.isIdentified = false;\n this.pending = [];\n\n this.stopRefresh();\n\n this.closeHandlers.forEach(h => h());\n\n if (event.code === KICKED_CODE) return;\n\n if (this.alive && this.reconnectEnabled) this.scheduleReconnect(event.code === DENIED_CODE);\n };\n\n this.socket.onerror = (error) => {\n this.errorHandlers.forEach(h => h(error));\n };\n }\n\n private async identify(): Promise<void> {\n if (!this.getToken) {\n this.sendRaw({ type: WebsocketClientEvent.IDENTIFY, id: this.id, app: this.app });\n this.handleIdentified();\n\n return;\n }\n\n const token = await this.getToken().catch(() => null);\n\n if (!token) {\n this.socket?.close();\n\n return;\n }\n\n this.sendRaw({ type: WebsocketClientEvent.IDENTIFY, id: this.id, app: this.app, token: token });\n }\n\n private handleIdentified(): void {\n if (this.isIdentified) return;\n\n this.isIdentified = true;\n this.deniedChannels.clear();\n\n if (this.subscribedChannels.size > 0) this.sendRaw({ type: WebsocketClientEvent.SUBSCRIBE, channels: [...this.subscribedChannels] });\n\n this.pending.splice(0).forEach(message => this.sendRaw(message));\n\n if (this.getToken && !this.refreshTimer) {\n this.refreshTimer = setInterval(() => void this.identify(), REFRESH_INTERVAL);\n }\n\n this.readyHandlers.forEach(h => h());\n }\n\n private stopRefresh(): void {\n if (this.refreshTimer) clearInterval(this.refreshTimer);\n\n this.refreshTimer = null;\n }\n\n private scheduleReconnect(isSlow: boolean): void {\n const jitteredDelay = (isSlow ? this.maxReconnectInterval : this.reconnectDelay) * (0.5 + Math.random());\n\n setTimeout(() => {\n this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectInterval);\n this.open();\n }, jitteredDelay);\n }\n\n private sendRaw(message: WebsocketClientMessage): void {\n if (this.socket?.readyState === WebSocket.OPEN) {\n this.socket.send(JSON.stringify(message));\n }\n }\n\n private sendToHub(message: WebsocketClientMessage): void {\n if (this.isIdentified) {\n this.sendRaw(message);\n\n return;\n }\n\n const isHeld = message.type === WebsocketClientEvent.MESSAGE || message.type === WebsocketClientEvent.STATE;\n\n if (isHeld && this.socket?.readyState === WebSocket.OPEN) this.pending.push(message);\n }\n\n public send(channel: string, data?: unknown): void {\n this.sendToHub({ type: WebsocketClientEvent.MESSAGE, channel: channel, data: data });\n }\n\n public setState(channel: string, data?: unknown): void {\n this.sendToHub({ type: WebsocketClientEvent.STATE, channel: channel, data: data });\n }\n\n public subscribe(channel: string, listener: WebsocketListener): void {\n if (!this.listeners.has(channel)) this.listeners.set(channel, new Set());\n\n this.listeners.get(channel)!.add(listener);\n\n if (!this.subscribedChannels.has(channel)) {\n this.subscribedChannels.add(channel);\n this.deniedChannels.delete(channel);\n this.sendToHub({ type: WebsocketClientEvent.SUBSCRIBE, channels: [channel] });\n }\n }\n\n public unsubscribe(channel: string, listener?: WebsocketListener): void {\n if (listener) {\n this.listeners.get(channel)?.delete(listener);\n\n if (this.listeners.get(channel)?.size === 0) {\n this.listeners.delete(channel);\n this.subscribedChannels.delete(channel);\n this.deniedChannels.delete(channel);\n this.sendToHub({ type: WebsocketClientEvent.UNSUBSCRIBE, channels: [channel] });\n }\n } else {\n this.listeners.delete(channel);\n this.subscribedChannels.delete(channel);\n this.deniedChannels.delete(channel);\n this.sendToHub({ type: WebsocketClientEvent.UNSUBSCRIBE, channels: [channel] });\n }\n }\n\n public isLive(channel: string): boolean {\n return this.isIdentified && this.subscribedChannels.has(channel) && !this.deniedChannels.has(channel);\n }\n\n public onOpen(handler: () => void): void { this.openHandlers.push(handler); }\n\n public onReady(handler: () => void): void { this.readyHandlers.push(handler); }\n\n public offReady(handler: () => void): void { this.readyHandlers = this.readyHandlers.filter(h => h !== handler); }\n\n public onClose(handler: () => void): void { this.closeHandlers.push(handler); }\n\n public onError(handler: (error: Event) => void): void { this.errorHandlers.push(handler); }\n\n public close(): void {\n this.alive = false;\n this.isIdentified = false;\n\n this.stopRefresh();\n\n this.socket?.close();\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["KICKED_CODE","DENIED_CODE","REFRESH_SHARE","MIN_REFRESH","DEFAULT_REFRESH","REALTIME_TOKEN_SECONDS","WebsocketClient","properties","WS_URL","h","message","event","WebSocketHubEvent","listener","error","WebsocketClientEvent","token","lifetime","RealtimeHelpers","wait","jitteredDelay","isSlow","channel","handler"],"sources":["../src/client.ts"],"sourcesContent":["import { REALTIME_TOKEN_SECONDS, RealtimeHelpers, WebsocketConnectionConfig, WebsocketClientEvent, WebsocketClientMessage, WebSocketHubEvent, WebsocketHubMessage, WebsocketListener, WS_URL } from '@digo-labs/common';\n\nconst KICKED_CODE = 4000;\nconst DENIED_CODE = 4401;\nconst REFRESH_SHARE = 0.5;\nconst MIN_REFRESH = 5_000;\nconst DEFAULT_REFRESH = REALTIME_TOKEN_SECONDS * 1000 * REFRESH_SHARE;\n\nexport class WebsocketClient {\n private url: string;\n private app: string;\n private id: string;\n private reconnectEnabled: boolean;\n private reconnectInterval: number;\n private maxReconnectInterval: number;\n private reconnectDelay: number;\n private getToken: (() => Promise<string | null>) | null;\n\n private socket: WebSocket | null = null;\n private alive = true;\n private isIdentified = false;\n private refreshTimer: ReturnType<typeof setTimeout> | null = null;\n private listeners = new Map<string, Set<WebsocketListener>>();\n private subscribedChannels = new Set<string>();\n private deniedChannels = new Set<string>();\n private pending: WebsocketClientMessage[] = [];\n private openHandlers: (() => void)[] = [];\n private readyHandlers: (() => void)[] = [];\n private closeHandlers: (() => void)[] = [];\n private errorHandlers: ((error: Event) => void)[] = [];\n\n constructor(properties: WebsocketConnectionConfig) {\n this.url = properties.url ?? WS_URL;\n this.app = properties.app ?? 'unknown';\n this.id = properties.id ?? crypto.randomUUID();\n this.reconnectEnabled = properties.reconnect ?? true;\n this.reconnectInterval = properties.reconnectInterval ?? 1000;\n this.maxReconnectInterval = properties.maxReconnectInterval ?? 10000;\n this.reconnectDelay = this.reconnectInterval;\n this.getToken = properties.getToken ?? null;\n\n this.open();\n }\n\n private open(): void {\n this.socket = new WebSocket(this.url);\n this.isIdentified = false;\n\n this.socket.onopen = () => {\n this.reconnectDelay = this.reconnectInterval;\n\n void this.identify();\n\n this.openHandlers.forEach(h => h());\n };\n\n this.socket.onmessage = (event) => {\n let message: WebsocketHubMessage;\n\n try {\n message = JSON.parse(event.data as string);\n } catch { return; }\n\n if (message.type === WebSocketHubEvent.IDENTIFIED) {\n this.handleIdentified();\n\n return;\n }\n\n if (message.type === WebSocketHubEvent.DENIED) {\n if (message.channel) this.deniedChannels.add(message.channel);\n\n return;\n }\n\n if (!message.channel) return;\n\n this.listeners.get(message.channel)?.forEach(listener => listener(message.data));\n };\n\n this.socket.onclose = (event) => {\n this.isIdentified = false;\n this.pending = [];\n\n this.stopRefresh();\n\n this.closeHandlers.forEach(h => h());\n\n if (event.code === KICKED_CODE) return;\n\n if (this.alive && this.reconnectEnabled) this.scheduleReconnect(event.code === DENIED_CODE);\n };\n\n this.socket.onerror = (error) => {\n this.errorHandlers.forEach(h => h(error));\n };\n }\n\n private async identify(): Promise<void> {\n if (!this.getToken) {\n this.sendRaw({ type: WebsocketClientEvent.IDENTIFY, id: this.id, app: this.app });\n this.handleIdentified();\n\n return;\n }\n\n const token = await this.getToken().catch(() => null);\n\n if (!this.alive || this.socket?.readyState !== WebSocket.OPEN) return;\n\n if (!token) {\n this.socket.close();\n\n return;\n }\n\n this.sendRaw({ type: WebsocketClientEvent.IDENTIFY, id: this.id, app: this.app, token: token });\n this.scheduleRefresh(token);\n }\n\n private scheduleRefresh(token: string): void {\n const lifetime = RealtimeHelpers.readLifetime(token);\n const wait = lifetime ? Math.max(lifetime * REFRESH_SHARE, MIN_REFRESH) : DEFAULT_REFRESH;\n\n this.stopRefresh();\n\n this.refreshTimer = setTimeout(() => void this.identify(), wait);\n }\n\n private handleIdentified(): void {\n if (this.isIdentified) return;\n\n this.isIdentified = true;\n this.deniedChannels.clear();\n\n if (this.subscribedChannels.size > 0) this.sendRaw({ type: WebsocketClientEvent.SUBSCRIBE, channels: [...this.subscribedChannels] });\n\n this.pending.splice(0).forEach(message => this.sendRaw(message));\n\n this.readyHandlers.forEach(h => h());\n }\n\n private stopRefresh(): void {\n if (this.refreshTimer) clearTimeout(this.refreshTimer);\n\n this.refreshTimer = null;\n }\n\n private scheduleReconnect(isSlow: boolean): void {\n const jitteredDelay = (isSlow ? this.maxReconnectInterval : this.reconnectDelay) * (0.5 + Math.random());\n\n setTimeout(() => {\n this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectInterval);\n this.open();\n }, jitteredDelay);\n }\n\n private sendRaw(message: WebsocketClientMessage): void {\n if (this.socket?.readyState === WebSocket.OPEN) {\n this.socket.send(JSON.stringify(message));\n }\n }\n\n private sendToHub(message: WebsocketClientMessage): void {\n if (this.isIdentified) {\n this.sendRaw(message);\n\n return;\n }\n\n const isHeld = message.type === WebsocketClientEvent.MESSAGE || message.type === WebsocketClientEvent.STATE;\n\n if (isHeld && this.socket?.readyState === WebSocket.OPEN) this.pending.push(message);\n }\n\n public send(channel: string, data?: unknown): void {\n this.sendToHub({ type: WebsocketClientEvent.MESSAGE, channel: channel, data: data });\n }\n\n public setState(channel: string, data?: unknown): void {\n this.sendToHub({ type: WebsocketClientEvent.STATE, channel: channel, data: data });\n }\n\n public subscribe(channel: string, listener: WebsocketListener): void {\n if (!this.listeners.has(channel)) this.listeners.set(channel, new Set());\n\n this.listeners.get(channel)!.add(listener);\n\n if (!this.subscribedChannels.has(channel)) {\n this.subscribedChannels.add(channel);\n this.deniedChannels.delete(channel);\n this.sendToHub({ type: WebsocketClientEvent.SUBSCRIBE, channels: [channel] });\n }\n }\n\n public unsubscribe(channel: string, listener?: WebsocketListener): void {\n if (listener) {\n this.listeners.get(channel)?.delete(listener);\n\n if (this.listeners.get(channel)?.size === 0) {\n this.listeners.delete(channel);\n this.subscribedChannels.delete(channel);\n this.deniedChannels.delete(channel);\n this.sendToHub({ type: WebsocketClientEvent.UNSUBSCRIBE, channels: [channel] });\n }\n } else {\n this.listeners.delete(channel);\n this.subscribedChannels.delete(channel);\n this.deniedChannels.delete(channel);\n this.sendToHub({ type: WebsocketClientEvent.UNSUBSCRIBE, channels: [channel] });\n }\n }\n\n public isLive(channel: string): boolean {\n return this.isIdentified && this.subscribedChannels.has(channel) && !this.deniedChannels.has(channel);\n }\n\n public onOpen(handler: () => void): void { this.openHandlers.push(handler); }\n\n public onReady(handler: () => void): void { this.readyHandlers.push(handler); }\n\n public offReady(handler: () => void): void { this.readyHandlers = this.readyHandlers.filter(h => h !== handler); }\n\n public onClose(handler: () => void): void { this.closeHandlers.push(handler); }\n\n public onError(handler: (error: Event) => void): void { this.errorHandlers.push(handler); }\n\n public close(): void {\n this.alive = false;\n this.isIdentified = false;\n\n this.stopRefresh();\n\n this.socket?.close();\n }\n}\n"],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -16,7 +16,9 @@ export declare class WebsocketHub {
|
|
|
16
16
|
private parseMessage;
|
|
17
17
|
private handleMessage;
|
|
18
18
|
private identify;
|
|
19
|
+
private isSameCaller;
|
|
19
20
|
private identifyWithToken;
|
|
21
|
+
private pruneChannels;
|
|
20
22
|
private canSubscribe;
|
|
21
23
|
private canPublish;
|
|
22
24
|
private channelKey;
|
|
@@ -25,6 +27,10 @@ export declare class WebsocketHub {
|
|
|
25
27
|
private readState;
|
|
26
28
|
private broadcast;
|
|
27
29
|
private deny;
|
|
30
|
+
private refuse;
|
|
31
|
+
private refuseUnidentified;
|
|
32
|
+
private isExpired;
|
|
33
|
+
private carriesToken;
|
|
28
34
|
private isOverRate;
|
|
29
35
|
private rawSize;
|
|
30
36
|
private addLog;
|
package/dist/server.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { CHANNEL_NAMESPACE_PREFIX as e, RealtimeHelpers as t, WEBSOCKET_HUB_CHANNEL as n, WebSocketHubEvent as r, WebsocketClientEvent as i, WebsocketLogEvent as a, tryCatch as o, websocketClientMessageSchema as s } from "@digo-labs/common";
|
|
2
2
|
import c from "node:http";
|
|
3
3
|
import { WebSocket as l, WebSocketServer as u } from "ws";
|
|
4
|
-
|
|
5
|
-
var d =
|
|
4
|
+
// src/server.ts
|
|
5
|
+
var d = 1048576, f = 65536, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401, v = 4408, y = 5e3, b = "/", x = class {
|
|
6
6
|
httpServer;
|
|
7
7
|
wss;
|
|
8
8
|
config;
|
|
@@ -14,15 +14,12 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
14
14
|
statusTimer = null;
|
|
15
15
|
statusDirty = !1;
|
|
16
16
|
constructor(e) {
|
|
17
|
+
if (e.requireToken && !e.tokenSecret) throw Error("requireToken needs a tokenSecret, or the hub refuses every client");
|
|
17
18
|
this.config = e, this.httpServer = c.createServer((e, t) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}));
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
t.writeHead(404), t.end();
|
|
19
|
+
e.method === "GET" && e.url === "/health" ? (t.writeHead(200, { "Content-Type": "application/json" }), t.end(JSON.stringify({
|
|
20
|
+
ok: !0,
|
|
21
|
+
clients: this.clients.size
|
|
22
|
+
}))) : (t.writeHead(404), t.end());
|
|
26
23
|
}), this.wss = new u({
|
|
27
24
|
server: this.httpServer,
|
|
28
25
|
maxPayload: d
|
|
@@ -31,11 +28,7 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
31
28
|
heartbeat() {
|
|
32
29
|
let e = !1;
|
|
33
30
|
this.clients.forEach((t) => {
|
|
34
|
-
|
|
35
|
-
t.ws.terminate(), this.clients.delete(t), e = !0;
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
t.alive = !1, t.ws.ping();
|
|
31
|
+
this.isExpired(t) ? t.ws.close(v) : t.alive ? (t.alive = !1, t.ws.ping()) : (t.ws.terminate(), this.clients.delete(t), e = !0);
|
|
39
32
|
});
|
|
40
33
|
let t = Date.now();
|
|
41
34
|
this.state.forEach((e, n) => {
|
|
@@ -50,17 +43,21 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
50
43
|
channels: /* @__PURE__ */ new Set(),
|
|
51
44
|
alive: !0,
|
|
52
45
|
token: null,
|
|
46
|
+
identifying: null,
|
|
53
47
|
windowStart: Date.now(),
|
|
54
48
|
messages: 0
|
|
55
49
|
};
|
|
56
|
-
this.clients.add(t)
|
|
50
|
+
this.clients.add(t);
|
|
51
|
+
let n = this.config.requireToken ? setTimeout(() => this.refuseUnidentified(t, e), y) : null;
|
|
52
|
+
e.on("error", () => {}), e.on("pong", () => {
|
|
57
53
|
t.alive = !0;
|
|
58
54
|
}), e.on("message", (n) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
let r = !t.token && !!this.config.requireToken;
|
|
56
|
+
if ((t.token || r) && (this.rawSize(n) > f || this.isOverRate(t))) return;
|
|
57
|
+
let i = this.parseMessage(n);
|
|
58
|
+
i ? this.isExpired(t) && !this.carriesToken(i) ? e.close(v) : o(this.handleMessage(t, e, i)) : r && this.refuse(e);
|
|
62
59
|
}), e.on("close", () => {
|
|
63
|
-
this.clients.delete(t), this.addLog(t, a.DISCONNECT, ""), this.broadcastStatus();
|
|
60
|
+
n && clearTimeout(n), this.clients.delete(t), this.addLog(t, a.DISCONNECT, ""), this.broadcastStatus();
|
|
64
61
|
});
|
|
65
62
|
}
|
|
66
63
|
parseMessage(e) {
|
|
@@ -74,10 +71,17 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
74
71
|
return n.success ? n.data : null;
|
|
75
72
|
}
|
|
76
73
|
async handleMessage(e, n, o) {
|
|
77
|
-
|
|
74
|
+
if (e.identifying && await e.identifying, this.config.requireToken && !e.token && !this.carriesToken(o)) this.refuse(n);
|
|
75
|
+
else switch (o.type) {
|
|
78
76
|
case i.IDENTIFY:
|
|
79
77
|
if (o.token) {
|
|
80
|
-
|
|
78
|
+
e.identifying = this.identifyWithToken(e, n, o.id, o.token).finally(() => {
|
|
79
|
+
e.identifying = null;
|
|
80
|
+
}), await e.identifying;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
if (e.token) {
|
|
84
|
+
this.deny(n);
|
|
81
85
|
break;
|
|
82
86
|
}
|
|
83
87
|
this.identify(e, o.id, o.app);
|
|
@@ -136,35 +140,44 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
136
140
|
}
|
|
137
141
|
}
|
|
138
142
|
identify(e, t, n) {
|
|
139
|
-
let r = [...this.clients].find((r) => r.id === t && r.app === n &&
|
|
143
|
+
let r = [...this.clients].find((r) => r.id === t && r.app === n && r !== e && this.isSameCaller(r, e));
|
|
140
144
|
r && (r.ws.close(g), this.clients.delete(r)), e.id = t, e.app = n, this.addLog(e, a.CONNECT, ""), this.broadcastStatus();
|
|
141
145
|
}
|
|
146
|
+
isSameCaller(e, t) {
|
|
147
|
+
return !e.token || !t.token ? !e.token && !t.token : e.token.subject === t.token.subject && e.token.tier !== "public" && t.token.tier !== "public";
|
|
148
|
+
}
|
|
142
149
|
async identifyWithToken(e, n, i, a) {
|
|
143
150
|
let o = this.config.tokenSecret ? await t.verify(a, this.config.tokenSecret) : null;
|
|
144
151
|
if (!o || e.token && e.token.app !== o.app) {
|
|
145
|
-
this.
|
|
152
|
+
this.refuse(n);
|
|
146
153
|
return;
|
|
147
154
|
}
|
|
148
155
|
let s = !!e.token;
|
|
149
|
-
e.token = o, s
|
|
156
|
+
e.token = o, s ? this.pruneChannels(e, n) : this.identify(e, i, o.app);
|
|
150
157
|
let c = { type: r.IDENTIFIED };
|
|
151
158
|
n.send(JSON.stringify(c));
|
|
152
159
|
}
|
|
160
|
+
pruneChannels(e, t) {
|
|
161
|
+
let n = this.channelKey(e, "");
|
|
162
|
+
[...e.channels].map((e) => e.slice(n.length)).filter((t) => !this.canSubscribe(e, t)).forEach((n) => {
|
|
163
|
+
e.channels.delete(this.channelKey(e, n)), this.deny(t, n);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
153
166
|
canSubscribe(e, n) {
|
|
154
|
-
return e.token
|
|
167
|
+
return !e.token || t.matchesAny(e.token.subscribe, n);
|
|
155
168
|
}
|
|
156
169
|
canPublish(e, n) {
|
|
157
|
-
return e.token
|
|
170
|
+
return !e.token || t.matchesAny(e.token.publish, n);
|
|
158
171
|
}
|
|
159
172
|
channelKey(e, t) {
|
|
160
173
|
return e.token ? this.namespacedKey(e.token.app, t) : t;
|
|
161
174
|
}
|
|
162
175
|
namespacedKey(t, n) {
|
|
163
|
-
return `${e}${t}${
|
|
176
|
+
return `${e}${t}${b}${n}`;
|
|
164
177
|
}
|
|
165
178
|
storeState(t, n, r) {
|
|
166
179
|
if (t.startsWith(e) && !this.state.has(t)) {
|
|
167
|
-
let e = t.slice(0, t.indexOf(
|
|
180
|
+
let e = t.slice(0, t.indexOf(b) + 1);
|
|
168
181
|
if ([...this.state.keys()].filter((t) => t.startsWith(e)).length >= h) return !1;
|
|
169
182
|
}
|
|
170
183
|
return this.state.set(t, {
|
|
@@ -175,17 +188,14 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
175
188
|
readState(e) {
|
|
176
189
|
let t = this.state.get(e);
|
|
177
190
|
if (t) {
|
|
178
|
-
if (t.expiresAt !== null && t.expiresAt <= Date.now())
|
|
179
|
-
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
return t.data;
|
|
191
|
+
if (t.expiresAt !== null && t.expiresAt <= Date.now()) this.state.delete(e);
|
|
192
|
+
else return t.data;
|
|
183
193
|
}
|
|
184
194
|
}
|
|
185
195
|
broadcast(e, t, n) {
|
|
186
196
|
let r = JSON.stringify(n);
|
|
187
197
|
this.clients.forEach((n) => {
|
|
188
|
-
n !== e && n.channels.has(t) && n.ws.readyState === l.OPEN && n.ws.send(r);
|
|
198
|
+
n !== e && n.channels.has(t) && n.ws.readyState === l.OPEN && (this.isExpired(n) ? n.ws.close(v) : n.ws.send(r));
|
|
189
199
|
});
|
|
190
200
|
}
|
|
191
201
|
deny(e, t) {
|
|
@@ -195,6 +205,18 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
195
205
|
};
|
|
196
206
|
e.readyState === l.OPEN && e.send(JSON.stringify(n));
|
|
197
207
|
}
|
|
208
|
+
refuse(e) {
|
|
209
|
+
this.deny(e), e.close(_);
|
|
210
|
+
}
|
|
211
|
+
refuseUnidentified(e, t) {
|
|
212
|
+
e.token || this.refuse(t);
|
|
213
|
+
}
|
|
214
|
+
isExpired(e) {
|
|
215
|
+
return !!e.token && e.token.exp * 1e3 <= Date.now();
|
|
216
|
+
}
|
|
217
|
+
carriesToken(e) {
|
|
218
|
+
return e.type === i.IDENTIFY && !!e.token;
|
|
219
|
+
}
|
|
198
220
|
isOverRate(e) {
|
|
199
221
|
let t = Date.now();
|
|
200
222
|
return t - e.windowStart >= p && (e.windowStart = t, e.messages = 0), e.messages++, e.messages > m;
|
|
@@ -213,13 +235,9 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
213
235
|
}), this.logs.length > 808 && this.logs.shift());
|
|
214
236
|
}
|
|
215
237
|
broadcastStatus() {
|
|
216
|
-
|
|
217
|
-
this.statusDirty = !0;
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
this.sendStatus(), this.statusTimer = setTimeout(() => {
|
|
238
|
+
this.statusTimer ? this.statusDirty = !0 : (this.sendStatus(), this.statusTimer = setTimeout(() => {
|
|
221
239
|
this.statusTimer = null, this.statusDirty && (this.statusDirty = !1, this.broadcastStatus());
|
|
222
|
-
}, 200);
|
|
240
|
+
}, 200));
|
|
223
241
|
}
|
|
224
242
|
sendStatus() {
|
|
225
243
|
let t = [...this.clients].filter((e) => e.channels.has(n) && e.ws.readyState === l.OPEN);
|
|
@@ -256,7 +274,6 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
256
274
|
}), this.wss.close(), this.httpServer.close();
|
|
257
275
|
}
|
|
258
276
|
};
|
|
259
|
-
|
|
260
|
-
export { y as WebsocketHub };
|
|
277
|
+
export { x as WebsocketHub };
|
|
261
278
|
|
|
262
279
|
//# sourceMappingURL=server.js.map
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["import http from 'node:http';\nimport { RawData, WebSocketServer, WebSocket } from 'ws';\n\nimport { CHANNEL_NAMESPACE_PREFIX, RealtimeHelpers, RealtimeTokenPayload, tryCatch, WebsocketClientMessage, WebsocketClientEvent, WebSocketHubEvent, WebsocketHubMessage, WebsocketHubLogEntry, WebsocketLogEvent, WebsocketHubStatus, WebsocketsHubConfig, WEBSOCKET_HUB_CHANNEL, websocketClientMessageSchema } from '@digo-labs/common';\n\nconst MAX_PAYLOAD_BYTES = 1024 * 1024;\nconst MAX_TOKENED_BYTES = 64 * 1024;\nconst RATE_WINDOW = 1_000;\nconst RATE_LIMIT = 20;\nconst MAX_STATES_PER_APP = 1_000;\nconst KICKED_CODE = 4000;\nconst DENIED_CODE = 4401;\nconst NAMESPACE_SEPARATOR = '/';\n\ninterface ConnectedClient {\n id: string;\n app: string;\n ws: WebSocket;\n channels: Set<string>;\n alive: boolean;\n token: RealtimeTokenPayload | null;\n windowStart: number;\n messages: number;\n}\n\ninterface StoredState {\n data: unknown;\n expiresAt: number | null;\n}\n\nexport class WebsocketHub {\n private httpServer: http.Server;\n private wss: WebSocketServer;\n private config: WebsocketsHubConfig;\n\n private clients = new Set<ConnectedClient>();\n private state = new Map<string, StoredState>();\n\n private logs: WebsocketHubLogEntry[] = [];\n private messageHandlers: ((channel: string, data: unknown) => void)[] = [];\n private heartbeatInterval: ReturnType<typeof setInterval>;\n private statusTimer: ReturnType<typeof setTimeout> | null = null;\n private statusDirty = false;\n\n constructor(config: WebsocketsHubConfig) {\n this.config = config;\n\n this.httpServer = http.createServer((req, res) => {\n if (req.method === 'GET' && req.url === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ ok: true, clients: this.clients.size }));\n return;\n }\n\n res.writeHead(404);\n res.end();\n });\n\n this.wss = new WebSocketServer({ server: this.httpServer, maxPayload: MAX_PAYLOAD_BYTES });\n\n this.heartbeatInterval = setInterval(() => this.heartbeat(), 30000);\n\n this.wss.on('close', () => clearInterval(this.heartbeatInterval));\n this.wss.on('connection', socket => this.handleConnection(socket));\n\n this.httpServer.listen(config.port);\n }\n\n private heartbeat(): void {\n let terminated = false;\n\n this.clients.forEach((client) => {\n if (!client.alive) {\n client.ws.terminate();\n\n this.clients.delete(client);\n\n terminated = true;\n\n return;\n }\n\n client.alive = false;\n\n client.ws.ping();\n });\n\n const now = Date.now();\n\n this.state.forEach((stored, key) => {\n if (stored.expiresAt !== null && stored.expiresAt <= now) this.state.delete(key);\n });\n\n if (terminated) this.broadcastStatus();\n }\n\n private handleConnection(socket: WebSocket): void {\n const client: ConnectedClient = {\n id: 'unknown',\n app: 'unknown',\n ws: socket,\n channels: new Set(),\n alive: true,\n token: null,\n windowStart: Date.now(),\n messages: 0,\n };\n\n this.clients.add(client);\n\n socket.on('error', () => {});\n\n socket.on('pong', () => {\n client.alive = true;\n });\n\n socket.on('message', (raw) => {\n if (client.token && (this.rawSize(raw) > MAX_TOKENED_BYTES || this.isOverRate(client))) return;\n\n const message = this.parseMessage(raw);\n\n if (!message) return;\n\n void tryCatch(this.handleMessage(client, socket, message));\n });\n\n socket.on('close', () => {\n this.clients.delete(client);\n\n this.addLog(client, WebsocketLogEvent.DISCONNECT, '');\n\n this.broadcastStatus();\n });\n }\n\n private parseMessage(raw: RawData): WebsocketClientMessage | null {\n let data: unknown;\n\n try {\n data = JSON.parse(raw.toString());\n } catch { return null; }\n\n const result = websocketClientMessageSchema.safeParse(data);\n\n return result.success ? result.data : null;\n }\n\n private async handleMessage(client: ConnectedClient, socket: WebSocket, message: WebsocketClientMessage): Promise<void> {\n switch (message.type) {\n case WebsocketClientEvent.IDENTIFY: {\n if (message.token) {\n await this.identifyWithToken(client, socket, message.id, message.token);\n\n break;\n }\n\n this.identify(client, message.id, message.app);\n break;\n }\n\n case WebsocketClientEvent.SUBSCRIBE: {\n const allowed = message.channels.filter(channel => this.canSubscribe(client, channel));\n\n allowed.forEach((channel) => {\n const key = this.channelKey(client, channel);\n\n client.channels.add(key);\n\n const stored = this.readState(key);\n\n if (stored !== undefined) {\n const stateMessage: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: channel,\n data: stored,\n };\n\n socket.send(JSON.stringify(stateMessage));\n }\n });\n\n message.channels.filter(channel => !allowed.includes(channel)).forEach(channel => this.deny(socket, channel));\n\n this.addLog(client, WebsocketLogEvent.SUBSCRIBE, allowed.join(', '));\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.UNSUBSCRIBE: {\n message.channels.forEach(channel => client.channels.delete(this.channelKey(client, channel)));\n\n this.addLog(client, WebsocketLogEvent.UNSUBSCRIBE, message.channels.join(', '));\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.MESSAGE: {\n if (!this.canPublish(client, message.channel)) {\n this.deny(socket, message.channel);\n\n break;\n }\n\n const outgoing: WebsocketHubMessage = {\n type: WebSocketHubEvent.MESSAGE,\n channel: message.channel,\n data: message.data,\n };\n\n this.broadcast(client, this.channelKey(client, message.channel), outgoing);\n\n this.messageHandlers.forEach(h => h(message.channel, message.data));\n\n this.addLog(client, WebsocketLogEvent.MESSAGE, message.channel, message.data);\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.STATE: {\n if (!this.canPublish(client, message.channel)) {\n this.deny(socket, message.channel);\n\n break;\n }\n\n const key = this.channelKey(client, message.channel);\n\n if (!this.storeState(key, message.data, client.token ? RealtimeHelpers.getRetainSeconds(client.token, message.channel) : null)) {\n this.deny(socket, message.channel);\n\n break;\n }\n\n const outgoing: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: message.channel,\n data: message.data,\n };\n\n this.broadcast(client, key, outgoing);\n\n this.messageHandlers.forEach(h => h(message.channel, message.data));\n\n this.addLog(client, WebsocketLogEvent.STATE, message.channel, message.data);\n\n this.broadcastStatus();\n break;\n }\n }\n }\n\n private identify(client: ConnectedClient, id: string, app: string): void {\n const existing = [...this.clients].find(c => c.id === id && c.app === app && !c.token === !client.token && c !== client);\n\n if (existing) {\n existing.ws.close(KICKED_CODE);\n\n this.clients.delete(existing);\n }\n\n client.id = id;\n client.app = app;\n\n this.addLog(client, WebsocketLogEvent.CONNECT, '');\n\n this.broadcastStatus();\n }\n\n private async identifyWithToken(client: ConnectedClient, socket: WebSocket, id: string, token: string): Promise<void> {\n const payload = this.config.tokenSecret ? await RealtimeHelpers.verify(token, this.config.tokenSecret) : null;\n\n if (!payload || (client.token && client.token.app !== payload.app)) {\n this.deny(socket);\n\n socket.close(DENIED_CODE);\n\n return;\n }\n\n const isRefresh = !!client.token;\n\n client.token = payload;\n\n if (!isRefresh) this.identify(client, id, payload.app);\n\n const identified: WebsocketHubMessage = { type: WebSocketHubEvent.IDENTIFIED };\n\n socket.send(JSON.stringify(identified));\n }\n\n private canSubscribe(client: ConnectedClient, channel: string): boolean {\n if (!client.token) return true;\n\n return RealtimeHelpers.matchesAny(client.token.subscribe, channel);\n }\n\n private canPublish(client: ConnectedClient, channel: string): boolean {\n if (!client.token) return true;\n\n return RealtimeHelpers.matchesAny(client.token.publish, channel);\n }\n\n private channelKey(client: ConnectedClient, channel: string): string {\n return client.token ? this.namespacedKey(client.token.app, channel) : channel;\n }\n\n private namespacedKey(app: string, channel: string): string {\n return `${CHANNEL_NAMESPACE_PREFIX}${app}${NAMESPACE_SEPARATOR}${channel}`;\n }\n\n private storeState(key: string, data: unknown, retainSeconds: number | null): boolean {\n if (key.startsWith(CHANNEL_NAMESPACE_PREFIX) && !this.state.has(key)) {\n const namespace = key.slice(0, key.indexOf(NAMESPACE_SEPARATOR) + 1);\n const count = [...this.state.keys()].filter(stored => stored.startsWith(namespace)).length;\n\n if (count >= MAX_STATES_PER_APP) return false;\n }\n\n this.state.set(key, { data: data, expiresAt: retainSeconds === null ? null : Date.now() + retainSeconds * 1000 });\n\n return true;\n }\n\n private readState(key: string): unknown {\n const stored = this.state.get(key);\n\n if (!stored) return undefined;\n\n if (stored.expiresAt !== null && stored.expiresAt <= Date.now()) {\n this.state.delete(key);\n\n return undefined;\n }\n\n return stored.data;\n }\n\n private broadcast(sender: ConnectedClient, key: string, message: WebsocketHubMessage): void {\n const payload = JSON.stringify(message);\n\n this.clients.forEach((client) => {\n if (client !== sender && client.channels.has(key) && client.ws.readyState === WebSocket.OPEN) {\n client.ws.send(payload);\n }\n });\n }\n\n private deny(socket: WebSocket, channel?: string): void {\n const denied: WebsocketHubMessage = { type: WebSocketHubEvent.DENIED, channel: channel };\n\n if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(denied));\n }\n\n private isOverRate(client: ConnectedClient): boolean {\n const now = Date.now();\n\n if (now - client.windowStart >= RATE_WINDOW) {\n client.windowStart = now;\n client.messages = 0;\n }\n\n client.messages++;\n\n return client.messages > RATE_LIMIT;\n }\n\n private rawSize(raw: RawData): number {\n if (Array.isArray(raw)) return raw.reduce((sum, chunk) => sum + chunk.length, 0);\n\n return raw instanceof ArrayBuffer ? raw.byteLength : raw.length;\n }\n\n private addLog(client: ConnectedClient, type: WebsocketLogEvent, channel: string, data?: unknown): void {\n if (client.token) return;\n\n this.logs.push({ timestamp: Date.now(), sender: client.id, app: client.app, type: type, channel: channel, data: data });\n\n if (this.logs.length > 808) this.logs.shift();\n }\n\n private broadcastStatus(): void {\n if (this.statusTimer) {\n this.statusDirty = true;\n\n return;\n }\n\n this.sendStatus();\n\n this.statusTimer = setTimeout(() => {\n this.statusTimer = null;\n\n if (!this.statusDirty) return;\n\n this.statusDirty = false;\n\n this.broadcastStatus();\n }, 200);\n }\n\n private sendStatus(): void {\n const watchers = [...this.clients].filter(client => client.channels.has(WEBSOCKET_HUB_CHANNEL) && client.ws.readyState === WebSocket.OPEN);\n\n if (!watchers.length) return;\n\n const legacyClients = [...this.clients].filter(client => !client.token);\n const legacyStates = [...this.state].filter(([key]) => key !== WEBSOCKET_HUB_CHANNEL && !key.startsWith(CHANNEL_NAMESPACE_PREFIX));\n\n const status: WebsocketHubStatus = {\n clients: legacyClients.map(client => ({ id: client.id, app: client.app, channels: [...client.channels] })),\n state: Object.fromEntries(legacyStates.map(([key, stored]) => [key, stored.data])),\n logs: this.logs,\n };\n\n this.state.set(WEBSOCKET_HUB_CHANNEL, { data: status, expiresAt: null });\n\n const message: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: WEBSOCKET_HUB_CHANNEL,\n data: status,\n };\n\n const payload = JSON.stringify(message);\n\n watchers.forEach(client => client.ws.send(payload));\n }\n\n public onMessage(handler: (channel: string, data: unknown) => void): void { this.messageHandlers.push(handler); }\n\n public getState(channel: string): unknown { return this.readState(channel); }\n\n public close(): void {\n clearInterval(this.heartbeatInterval);\n\n if (this.statusTimer) clearTimeout(this.statusTimer);\n\n this.clients.forEach((client) => {\n if (client.ws.readyState === WebSocket.OPEN) {\n client.ws.close(1001, 'Hub shutting down');\n }\n });\n\n this.wss.close();\n this.httpServer.close();\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.js","names":["http","MAX_PAYLOAD_BYTES","MAX_TOKENED_BYTES","RATE_WINDOW","RATE_LIMIT","MAX_STATES_PER_APP","KICKED_CODE","DENIED_CODE","EXPIRED_CODE","IDENTIFY_DEADLINE","NAMESPACE_SEPARATOR","WebsocketHub","config","req","res","WebSocketServer","socket","terminated","client","now","stored","key","deadline","isUnidentifiedOnStrictHub","raw","message","tryCatch","WebsocketLogEvent","data","result","websocketClientMessageSchema","WebsocketClientEvent","allowed","channel","stateMessage","WebSocketHubEvent","outgoing","h","RealtimeHelpers","existing","c","id","app","first","second","payload","token","isRefresh","identified","prefix","CHANNEL_NAMESPACE_PREFIX","namespace","retainSeconds","sender","WebSocket","denied","sum","chunk","watchers","WEBSOCKET_HUB_CHANNEL","legacyClients","legacyStates","status","handler"],"sources":["../src/server.ts"],"sourcesContent":["import http from 'node:http';\nimport { RawData, WebSocketServer, WebSocket } from 'ws';\n\nimport { CHANNEL_NAMESPACE_PREFIX, RealtimeHelpers, RealtimeTokenPayload, tryCatch, WebsocketClientMessage, WebsocketClientEvent, WebSocketHubEvent, WebsocketHubMessage, WebsocketHubLogEntry, WebsocketLogEvent, WebsocketHubStatus, WebsocketsHubConfig, WEBSOCKET_HUB_CHANNEL, websocketClientMessageSchema } from '@digo-labs/common';\n\nconst MAX_PAYLOAD_BYTES = 1024 * 1024;\nconst MAX_TOKENED_BYTES = 64 * 1024;\nconst RATE_WINDOW = 1_000;\nconst RATE_LIMIT = 20;\nconst MAX_STATES_PER_APP = 1_000;\nconst KICKED_CODE = 4000;\nconst DENIED_CODE = 4401;\nconst EXPIRED_CODE = 4408;\nconst IDENTIFY_DEADLINE = 5_000;\nconst NAMESPACE_SEPARATOR = '/';\n\ninterface ConnectedClient {\n id: string;\n app: string;\n ws: WebSocket;\n channels: Set<string>;\n alive: boolean;\n token: RealtimeTokenPayload | null;\n identifying: Promise<void> | null;\n windowStart: number;\n messages: number;\n}\n\ninterface StoredState {\n data: unknown;\n expiresAt: number | null;\n}\n\nexport class WebsocketHub {\n private httpServer: http.Server;\n private wss: WebSocketServer;\n private config: WebsocketsHubConfig;\n\n private clients = new Set<ConnectedClient>();\n private state = new Map<string, StoredState>();\n\n private logs: WebsocketHubLogEntry[] = [];\n private messageHandlers: ((channel: string, data: unknown) => void)[] = [];\n private heartbeatInterval: ReturnType<typeof setInterval>;\n private statusTimer: ReturnType<typeof setTimeout> | null = null;\n private statusDirty = false;\n\n constructor(config: WebsocketsHubConfig) {\n if (config.requireToken && !config.tokenSecret) throw new Error('requireToken needs a tokenSecret, or the hub refuses every client');\n\n this.config = config;\n\n this.httpServer = http.createServer((req, res) => {\n if (req.method === 'GET' && req.url === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ ok: true, clients: this.clients.size }));\n return;\n }\n\n res.writeHead(404);\n res.end();\n });\n\n this.wss = new WebSocketServer({ server: this.httpServer, maxPayload: MAX_PAYLOAD_BYTES });\n\n this.heartbeatInterval = setInterval(() => this.heartbeat(), 30000);\n\n this.wss.on('close', () => clearInterval(this.heartbeatInterval));\n this.wss.on('connection', socket => this.handleConnection(socket));\n\n this.httpServer.listen(config.port);\n }\n\n private heartbeat(): void {\n let terminated = false;\n\n this.clients.forEach((client) => {\n if (this.isExpired(client)) {\n client.ws.close(EXPIRED_CODE);\n\n return;\n }\n\n if (!client.alive) {\n client.ws.terminate();\n\n this.clients.delete(client);\n\n terminated = true;\n\n return;\n }\n\n client.alive = false;\n\n client.ws.ping();\n });\n\n const now = Date.now();\n\n this.state.forEach((stored, key) => {\n if (stored.expiresAt !== null && stored.expiresAt <= now) this.state.delete(key);\n });\n\n if (terminated) this.broadcastStatus();\n }\n\n private handleConnection(socket: WebSocket): void {\n const client: ConnectedClient = {\n id: 'unknown',\n app: 'unknown',\n ws: socket,\n channels: new Set(),\n alive: true,\n token: null,\n identifying: null,\n windowStart: Date.now(),\n messages: 0,\n };\n\n this.clients.add(client);\n\n const deadline = this.config.requireToken ? setTimeout(() => this.refuseUnidentified(client, socket), IDENTIFY_DEADLINE) : null;\n\n socket.on('error', () => {});\n\n socket.on('pong', () => {\n client.alive = true;\n });\n\n socket.on('message', (raw) => {\n const isUnidentifiedOnStrictHub = !client.token && !!this.config.requireToken;\n\n if ((client.token || isUnidentifiedOnStrictHub) && (this.rawSize(raw) > MAX_TOKENED_BYTES || this.isOverRate(client))) return;\n\n const message = this.parseMessage(raw);\n\n if (!message) {\n if (isUnidentifiedOnStrictHub) this.refuse(socket);\n\n return;\n }\n\n if (this.isExpired(client) && !this.carriesToken(message)) {\n socket.close(EXPIRED_CODE);\n\n return;\n }\n\n void tryCatch(this.handleMessage(client, socket, message));\n });\n\n socket.on('close', () => {\n if (deadline) clearTimeout(deadline);\n\n this.clients.delete(client);\n\n this.addLog(client, WebsocketLogEvent.DISCONNECT, '');\n\n this.broadcastStatus();\n });\n }\n\n private parseMessage(raw: RawData): WebsocketClientMessage | null {\n let data: unknown;\n\n try {\n data = JSON.parse(raw.toString());\n } catch { return null; }\n\n const result = websocketClientMessageSchema.safeParse(data);\n\n return result.success ? result.data : null;\n }\n\n private async handleMessage(client: ConnectedClient, socket: WebSocket, message: WebsocketClientMessage): Promise<void> {\n if (client.identifying) await client.identifying;\n\n if (this.config.requireToken && !client.token && !this.carriesToken(message)) {\n this.refuse(socket);\n\n return;\n }\n\n switch (message.type) {\n case WebsocketClientEvent.IDENTIFY: {\n if (message.token) {\n client.identifying = this.identifyWithToken(client, socket, message.id, message.token).finally(() => {\n client.identifying = null;\n });\n\n await client.identifying;\n\n break;\n }\n\n if (client.token) {\n this.deny(socket);\n\n break;\n }\n\n this.identify(client, message.id, message.app);\n break;\n }\n\n case WebsocketClientEvent.SUBSCRIBE: {\n const allowed = message.channels.filter(channel => this.canSubscribe(client, channel));\n\n allowed.forEach((channel) => {\n const key = this.channelKey(client, channel);\n\n client.channels.add(key);\n\n const stored = this.readState(key);\n\n if (stored !== undefined) {\n const stateMessage: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: channel,\n data: stored,\n };\n\n socket.send(JSON.stringify(stateMessage));\n }\n });\n\n message.channels.filter(channel => !allowed.includes(channel)).forEach(channel => this.deny(socket, channel));\n\n this.addLog(client, WebsocketLogEvent.SUBSCRIBE, allowed.join(', '));\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.UNSUBSCRIBE: {\n message.channels.forEach(channel => client.channels.delete(this.channelKey(client, channel)));\n\n this.addLog(client, WebsocketLogEvent.UNSUBSCRIBE, message.channels.join(', '));\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.MESSAGE: {\n if (!this.canPublish(client, message.channel)) {\n this.deny(socket, message.channel);\n\n break;\n }\n\n const outgoing: WebsocketHubMessage = {\n type: WebSocketHubEvent.MESSAGE,\n channel: message.channel,\n data: message.data,\n };\n\n this.broadcast(client, this.channelKey(client, message.channel), outgoing);\n\n this.messageHandlers.forEach(h => h(message.channel, message.data));\n\n this.addLog(client, WebsocketLogEvent.MESSAGE, message.channel, message.data);\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.STATE: {\n if (!this.canPublish(client, message.channel)) {\n this.deny(socket, message.channel);\n\n break;\n }\n\n const key = this.channelKey(client, message.channel);\n\n if (!this.storeState(key, message.data, client.token ? RealtimeHelpers.getRetainSeconds(client.token, message.channel) : null)) {\n this.deny(socket, message.channel);\n\n break;\n }\n\n const outgoing: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: message.channel,\n data: message.data,\n };\n\n this.broadcast(client, key, outgoing);\n\n this.messageHandlers.forEach(h => h(message.channel, message.data));\n\n this.addLog(client, WebsocketLogEvent.STATE, message.channel, message.data);\n\n this.broadcastStatus();\n break;\n }\n }\n }\n\n private identify(client: ConnectedClient, id: string, app: string): void {\n const existing = [...this.clients].find(c => c.id === id && c.app === app && c !== client && this.isSameCaller(c, client));\n\n if (existing) {\n existing.ws.close(KICKED_CODE);\n\n this.clients.delete(existing);\n }\n\n client.id = id;\n client.app = app;\n\n this.addLog(client, WebsocketLogEvent.CONNECT, '');\n\n this.broadcastStatus();\n }\n\n private isSameCaller(first: ConnectedClient, second: ConnectedClient): boolean {\n if (!first.token || !second.token) return !first.token && !second.token;\n\n return first.token.subject === second.token.subject && first.token.tier !== 'public' && second.token.tier !== 'public';\n }\n\n private async identifyWithToken(client: ConnectedClient, socket: WebSocket, id: string, token: string): Promise<void> {\n const payload = this.config.tokenSecret ? await RealtimeHelpers.verify(token, this.config.tokenSecret) : null;\n\n if (!payload || (client.token && client.token.app !== payload.app)) {\n this.refuse(socket);\n\n return;\n }\n\n const isRefresh = !!client.token;\n\n client.token = payload;\n\n if (isRefresh) {\n this.pruneChannels(client, socket);\n } else {\n this.identify(client, id, payload.app);\n }\n\n const identified: WebsocketHubMessage = { type: WebSocketHubEvent.IDENTIFIED };\n\n socket.send(JSON.stringify(identified));\n }\n\n private pruneChannels(client: ConnectedClient, socket: WebSocket): void {\n const prefix = this.channelKey(client, '');\n\n [...client.channels].map(key => key.slice(prefix.length)).filter(channel => !this.canSubscribe(client, channel)).forEach((channel) => {\n client.channels.delete(this.channelKey(client, channel));\n\n this.deny(socket, channel);\n });\n }\n\n private canSubscribe(client: ConnectedClient, channel: string): boolean {\n if (!client.token) return true;\n\n return RealtimeHelpers.matchesAny(client.token.subscribe, channel);\n }\n\n private canPublish(client: ConnectedClient, channel: string): boolean {\n if (!client.token) return true;\n\n return RealtimeHelpers.matchesAny(client.token.publish, channel);\n }\n\n private channelKey(client: ConnectedClient, channel: string): string {\n return client.token ? this.namespacedKey(client.token.app, channel) : channel;\n }\n\n private namespacedKey(app: string, channel: string): string {\n return `${CHANNEL_NAMESPACE_PREFIX}${app}${NAMESPACE_SEPARATOR}${channel}`;\n }\n\n private storeState(key: string, data: unknown, retainSeconds: number | null): boolean {\n if (key.startsWith(CHANNEL_NAMESPACE_PREFIX) && !this.state.has(key)) {\n const namespace = key.slice(0, key.indexOf(NAMESPACE_SEPARATOR) + 1);\n const count = [...this.state.keys()].filter(stored => stored.startsWith(namespace)).length;\n\n if (count >= MAX_STATES_PER_APP) return false;\n }\n\n this.state.set(key, { data: data, expiresAt: retainSeconds === null ? null : Date.now() + retainSeconds * 1000 });\n\n return true;\n }\n\n private readState(key: string): unknown {\n const stored = this.state.get(key);\n\n if (!stored) return undefined;\n\n if (stored.expiresAt !== null && stored.expiresAt <= Date.now()) {\n this.state.delete(key);\n\n return undefined;\n }\n\n return stored.data;\n }\n\n private broadcast(sender: ConnectedClient, key: string, message: WebsocketHubMessage): void {\n const payload = JSON.stringify(message);\n\n this.clients.forEach((client) => {\n if (client === sender || !client.channels.has(key) || client.ws.readyState !== WebSocket.OPEN) return;\n\n if (this.isExpired(client)) {\n client.ws.close(EXPIRED_CODE);\n\n return;\n }\n\n client.ws.send(payload);\n });\n }\n\n private deny(socket: WebSocket, channel?: string): void {\n const denied: WebsocketHubMessage = { type: WebSocketHubEvent.DENIED, channel: channel };\n\n if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(denied));\n }\n\n private refuse(socket: WebSocket): void {\n this.deny(socket);\n\n socket.close(DENIED_CODE);\n }\n\n private refuseUnidentified(client: ConnectedClient, socket: WebSocket): void {\n if (!client.token) this.refuse(socket);\n }\n\n private isExpired(client: ConnectedClient): boolean {\n return !!client.token && client.token.exp * 1000 <= Date.now();\n }\n\n private carriesToken(message: WebsocketClientMessage): boolean {\n return message.type === WebsocketClientEvent.IDENTIFY && !!message.token;\n }\n\n private isOverRate(client: ConnectedClient): boolean {\n const now = Date.now();\n\n if (now - client.windowStart >= RATE_WINDOW) {\n client.windowStart = now;\n client.messages = 0;\n }\n\n client.messages++;\n\n return client.messages > RATE_LIMIT;\n }\n\n private rawSize(raw: RawData): number {\n if (Array.isArray(raw)) return raw.reduce((sum, chunk) => sum + chunk.length, 0);\n\n return raw instanceof ArrayBuffer ? raw.byteLength : raw.length;\n }\n\n private addLog(client: ConnectedClient, type: WebsocketLogEvent, channel: string, data?: unknown): void {\n if (client.token) return;\n\n this.logs.push({ timestamp: Date.now(), sender: client.id, app: client.app, type: type, channel: channel, data: data });\n\n if (this.logs.length > 808) this.logs.shift();\n }\n\n private broadcastStatus(): void {\n if (this.statusTimer) {\n this.statusDirty = true;\n\n return;\n }\n\n this.sendStatus();\n\n this.statusTimer = setTimeout(() => {\n this.statusTimer = null;\n\n if (!this.statusDirty) return;\n\n this.statusDirty = false;\n\n this.broadcastStatus();\n }, 200);\n }\n\n private sendStatus(): void {\n const watchers = [...this.clients].filter(client => client.channels.has(WEBSOCKET_HUB_CHANNEL) && client.ws.readyState === WebSocket.OPEN);\n\n if (!watchers.length) return;\n\n const legacyClients = [...this.clients].filter(client => !client.token);\n const legacyStates = [...this.state].filter(([key]) => key !== WEBSOCKET_HUB_CHANNEL && !key.startsWith(CHANNEL_NAMESPACE_PREFIX));\n\n const status: WebsocketHubStatus = {\n clients: legacyClients.map(client => ({ id: client.id, app: client.app, channels: [...client.channels] })),\n state: Object.fromEntries(legacyStates.map(([key, stored]) => [key, stored.data])),\n logs: this.logs,\n };\n\n this.state.set(WEBSOCKET_HUB_CHANNEL, { data: status, expiresAt: null });\n\n const message: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: WEBSOCKET_HUB_CHANNEL,\n data: status,\n };\n\n const payload = JSON.stringify(message);\n\n watchers.forEach(client => client.ws.send(payload));\n }\n\n public onMessage(handler: (channel: string, data: unknown) => void): void { this.messageHandlers.push(handler); }\n\n public getState(channel: string): unknown { return this.readState(channel); }\n\n public close(): void {\n clearInterval(this.heartbeatInterval);\n\n if (this.statusTimer) clearTimeout(this.statusTimer);\n\n this.clients.forEach((client) => {\n if (client.ws.readyState === WebSocket.OPEN) {\n client.ws.close(1001, 'Hub shutting down');\n }\n });\n\n this.wss.close();\n this.httpServer.close();\n }\n}\n"],"mappings":"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"}
|