@digo-labs/ws 0.2.1 → 0.2.2
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 +1 -1
- 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 +4 -0
- package/dist/server.js +39 -36
- package/dist/server.js.map +1 -1
- package/dist/start-hub.cjs +20193 -35382
- 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';
|
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,8 @@ export declare class WebsocketHub {
|
|
|
25
27
|
private readState;
|
|
26
28
|
private broadcast;
|
|
27
29
|
private deny;
|
|
30
|
+
private isExpired;
|
|
31
|
+
private isRenewal;
|
|
28
32
|
private isOverRate;
|
|
29
33
|
private rawSize;
|
|
30
34
|
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 = "/", b = class {
|
|
6
6
|
httpServer;
|
|
7
7
|
wss;
|
|
8
8
|
config;
|
|
@@ -15,14 +15,10 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
15
15
|
statusDirty = !1;
|
|
16
16
|
constructor(e) {
|
|
17
17
|
this.config = e, this.httpServer = c.createServer((e, t) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}));
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
t.writeHead(404), t.end();
|
|
18
|
+
e.method === "GET" && e.url === "/health" ? (t.writeHead(200, { "Content-Type": "application/json" }), t.end(JSON.stringify({
|
|
19
|
+
ok: !0,
|
|
20
|
+
clients: this.clients.size
|
|
21
|
+
}))) : (t.writeHead(404), t.end());
|
|
26
22
|
}), this.wss = new u({
|
|
27
23
|
server: this.httpServer,
|
|
28
24
|
maxPayload: d
|
|
@@ -31,11 +27,7 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
31
27
|
heartbeat() {
|
|
32
28
|
let e = !1;
|
|
33
29
|
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();
|
|
30
|
+
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
31
|
});
|
|
40
32
|
let t = Date.now();
|
|
41
33
|
this.state.forEach((e, n) => {
|
|
@@ -58,7 +50,7 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
58
50
|
}), e.on("message", (n) => {
|
|
59
51
|
if (t.token && (this.rawSize(n) > f || this.isOverRate(t))) return;
|
|
60
52
|
let r = this.parseMessage(n);
|
|
61
|
-
r && o(this.handleMessage(t, e, r));
|
|
53
|
+
r && (this.isExpired(t) && !this.isRenewal(r) ? e.close(v) : o(this.handleMessage(t, e, r)));
|
|
62
54
|
}), e.on("close", () => {
|
|
63
55
|
this.clients.delete(t), this.addLog(t, a.DISCONNECT, ""), this.broadcastStatus();
|
|
64
56
|
});
|
|
@@ -80,6 +72,10 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
80
72
|
await this.identifyWithToken(e, n, o.id, o.token);
|
|
81
73
|
break;
|
|
82
74
|
}
|
|
75
|
+
if (e.token) {
|
|
76
|
+
this.deny(n);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
83
79
|
this.identify(e, o.id, o.app);
|
|
84
80
|
break;
|
|
85
81
|
case i.SUBSCRIBE: {
|
|
@@ -136,9 +132,12 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
136
132
|
}
|
|
137
133
|
}
|
|
138
134
|
identify(e, t, n) {
|
|
139
|
-
let r = [...this.clients].find((r) => r.id === t && r.app === n &&
|
|
135
|
+
let r = [...this.clients].find((r) => r.id === t && r.app === n && r !== e && this.isSameCaller(r, e));
|
|
140
136
|
r && (r.ws.close(g), this.clients.delete(r)), e.id = t, e.app = n, this.addLog(e, a.CONNECT, ""), this.broadcastStatus();
|
|
141
137
|
}
|
|
138
|
+
isSameCaller(e, t) {
|
|
139
|
+
return !e.token || !t.token ? !e.token && !t.token : e.token.subject === t.token.subject && e.token.tier !== "public" && t.token.tier !== "public";
|
|
140
|
+
}
|
|
142
141
|
async identifyWithToken(e, n, i, a) {
|
|
143
142
|
let o = this.config.tokenSecret ? await t.verify(a, this.config.tokenSecret) : null;
|
|
144
143
|
if (!o || e.token && e.token.app !== o.app) {
|
|
@@ -146,25 +145,31 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
146
145
|
return;
|
|
147
146
|
}
|
|
148
147
|
let s = !!e.token;
|
|
149
|
-
e.token = o, s
|
|
148
|
+
e.token = o, s ? this.pruneChannels(e, n) : this.identify(e, i, o.app);
|
|
150
149
|
let c = { type: r.IDENTIFIED };
|
|
151
150
|
n.send(JSON.stringify(c));
|
|
152
151
|
}
|
|
152
|
+
pruneChannels(e, t) {
|
|
153
|
+
let n = this.channelKey(e, "");
|
|
154
|
+
[...e.channels].map((e) => e.slice(n.length)).filter((t) => !this.canSubscribe(e, t)).forEach((n) => {
|
|
155
|
+
e.channels.delete(this.channelKey(e, n)), this.deny(t, n);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
153
158
|
canSubscribe(e, n) {
|
|
154
|
-
return e.token
|
|
159
|
+
return !e.token || t.matchesAny(e.token.subscribe, n);
|
|
155
160
|
}
|
|
156
161
|
canPublish(e, n) {
|
|
157
|
-
return e.token
|
|
162
|
+
return !e.token || t.matchesAny(e.token.publish, n);
|
|
158
163
|
}
|
|
159
164
|
channelKey(e, t) {
|
|
160
165
|
return e.token ? this.namespacedKey(e.token.app, t) : t;
|
|
161
166
|
}
|
|
162
167
|
namespacedKey(t, n) {
|
|
163
|
-
return `${e}${t}${
|
|
168
|
+
return `${e}${t}${y}${n}`;
|
|
164
169
|
}
|
|
165
170
|
storeState(t, n, r) {
|
|
166
171
|
if (t.startsWith(e) && !this.state.has(t)) {
|
|
167
|
-
let e = t.slice(0, t.indexOf(
|
|
172
|
+
let e = t.slice(0, t.indexOf(y) + 1);
|
|
168
173
|
if ([...this.state.keys()].filter((t) => t.startsWith(e)).length >= h) return !1;
|
|
169
174
|
}
|
|
170
175
|
return this.state.set(t, {
|
|
@@ -175,17 +180,14 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
175
180
|
readState(e) {
|
|
176
181
|
let t = this.state.get(e);
|
|
177
182
|
if (t) {
|
|
178
|
-
if (t.expiresAt !== null && t.expiresAt <= Date.now())
|
|
179
|
-
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
return t.data;
|
|
183
|
+
if (t.expiresAt !== null && t.expiresAt <= Date.now()) this.state.delete(e);
|
|
184
|
+
else return t.data;
|
|
183
185
|
}
|
|
184
186
|
}
|
|
185
187
|
broadcast(e, t, n) {
|
|
186
188
|
let r = JSON.stringify(n);
|
|
187
189
|
this.clients.forEach((n) => {
|
|
188
|
-
n !== e && n.channels.has(t) && n.ws.readyState === l.OPEN && n.ws.send(r);
|
|
190
|
+
n !== e && n.channels.has(t) && n.ws.readyState === l.OPEN && (this.isExpired(n) ? n.ws.close(v) : n.ws.send(r));
|
|
189
191
|
});
|
|
190
192
|
}
|
|
191
193
|
deny(e, t) {
|
|
@@ -195,6 +197,12 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
195
197
|
};
|
|
196
198
|
e.readyState === l.OPEN && e.send(JSON.stringify(n));
|
|
197
199
|
}
|
|
200
|
+
isExpired(e) {
|
|
201
|
+
return !!e.token && e.token.exp * 1e3 <= Date.now();
|
|
202
|
+
}
|
|
203
|
+
isRenewal(e) {
|
|
204
|
+
return e.type === i.IDENTIFY && !!e.token;
|
|
205
|
+
}
|
|
198
206
|
isOverRate(e) {
|
|
199
207
|
let t = Date.now();
|
|
200
208
|
return t - e.windowStart >= p && (e.windowStart = t, e.messages = 0), e.messages++, e.messages > m;
|
|
@@ -213,13 +221,9 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
213
221
|
}), this.logs.length > 808 && this.logs.shift());
|
|
214
222
|
}
|
|
215
223
|
broadcastStatus() {
|
|
216
|
-
|
|
217
|
-
this.statusDirty = !0;
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
this.sendStatus(), this.statusTimer = setTimeout(() => {
|
|
224
|
+
this.statusTimer ? this.statusDirty = !0 : (this.sendStatus(), this.statusTimer = setTimeout(() => {
|
|
221
225
|
this.statusTimer = null, this.statusDirty && (this.statusDirty = !1, this.broadcastStatus());
|
|
222
|
-
}, 200);
|
|
226
|
+
}, 200));
|
|
223
227
|
}
|
|
224
228
|
sendStatus() {
|
|
225
229
|
let t = [...this.clients].filter((e) => e.channels.has(n) && e.ws.readyState === l.OPEN);
|
|
@@ -256,7 +260,6 @@ var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401,
|
|
|
256
260
|
}), this.wss.close(), this.httpServer.close();
|
|
257
261
|
}
|
|
258
262
|
};
|
|
259
|
-
|
|
260
|
-
export { y as WebsocketHub };
|
|
263
|
+
export { b as WebsocketHub };
|
|
261
264
|
|
|
262
265
|
//# 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","NAMESPACE_SEPARATOR","WebsocketHub","config","req","res","WebSocketServer","socket","terminated","client","now","stored","key","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 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 (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 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 if (this.isExpired(client) && !this.isRenewal(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 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 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.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) {\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 isExpired(client: ConnectedClient): boolean {\n return !!client.token && client.token.exp * 1000 <= Date.now();\n }\n\n private isRenewal(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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"}
|