@digo-labs/ws 0.1.7 → 0.1.8
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 +11 -0
- package/dist/client.js +97 -79
- package/dist/client.js.map +1 -1
- package/dist/server.js +149 -141
- package/dist/server.js.map +1 -1
- package/dist/start-hub.cjs +1199 -455
- package/dist/start-hub.cjs.map +1 -1
- package/dist/start-hub.css +6 -3
- package/dist/start-hub.css.map +1 -1
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @digo-labs/ws
|
|
2
|
+
|
|
3
|
+
The low-level websocket client (`WebsocketClient`), server primitives (`@digo-labs/ws/server`), and the `digo-ws-hub` CLI for local development. Most apps use the typed wrapper in [`@digo-labs/services`](https://design.digo-labs.com/docs/guide/package-services) instead.
|
|
4
|
+
|
|
5
|
+
**📖 Full documentation:** https://design.digo-labs.com/docs/guide/package-ws
|
|
6
|
+
|
|
7
|
+
The Digo design-system guide is the single source of truth for this package — its full API, examples, and patterns live there. This README stays thin on purpose and points to it.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @digo-labs/ws
|
|
11
|
+
```
|
package/dist/client.js
CHANGED
|
@@ -1,80 +1,98 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
var
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
1
|
+
import { WS_URL as e, WebsocketClientEvent as t } from "@digo-labs/common";
|
|
2
|
+
//#region src/client.ts
|
|
3
|
+
var n = class {
|
|
4
|
+
url;
|
|
5
|
+
app;
|
|
6
|
+
id;
|
|
7
|
+
reconnectEnabled;
|
|
8
|
+
reconnectInterval;
|
|
9
|
+
maxReconnectInterval;
|
|
10
|
+
reconnectDelay;
|
|
11
|
+
socket = null;
|
|
12
|
+
alive = !0;
|
|
13
|
+
listeners = /* @__PURE__ */ new Map();
|
|
14
|
+
subscribedChannels = /* @__PURE__ */ new Set();
|
|
15
|
+
openHandlers = [];
|
|
16
|
+
closeHandlers = [];
|
|
17
|
+
errorHandlers = [];
|
|
18
|
+
constructor(t) {
|
|
19
|
+
this.url = t.url ?? e, this.app = t.app ?? "unknown", this.id = t.id ?? crypto.randomUUID(), this.reconnectEnabled = t.reconnect ?? !0, this.reconnectInterval = t.reconnectInterval ?? 1e3, this.maxReconnectInterval = t.maxReconnectInterval ?? 1e4, this.reconnectDelay = this.reconnectInterval, this.open();
|
|
20
|
+
}
|
|
21
|
+
open() {
|
|
22
|
+
this.socket = new WebSocket(this.url), this.socket.onopen = () => {
|
|
23
|
+
this.reconnectDelay = this.reconnectInterval, this.sendToHub({
|
|
24
|
+
type: t.IDENTIFY,
|
|
25
|
+
id: this.id,
|
|
26
|
+
app: this.app
|
|
27
|
+
}), this.subscribedChannels.size > 0 && this.sendToHub({
|
|
28
|
+
type: t.SUBSCRIBE,
|
|
29
|
+
channels: [...this.subscribedChannels]
|
|
30
|
+
}), this.openHandlers.forEach((e) => e());
|
|
31
|
+
}, this.socket.onmessage = (e) => {
|
|
32
|
+
let t;
|
|
33
|
+
try {
|
|
34
|
+
t = JSON.parse(e.data);
|
|
35
|
+
} catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.listeners.get(t.channel)?.forEach((e) => e(t.data));
|
|
39
|
+
}, this.socket.onclose = (e) => {
|
|
40
|
+
if (this.closeHandlers.forEach((e) => e()), e.code !== 4e3 && this.alive && this.reconnectEnabled) {
|
|
41
|
+
let e = this.reconnectDelay * (.5 + Math.random());
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectInterval), this.open();
|
|
44
|
+
}, e);
|
|
45
|
+
}
|
|
46
|
+
}, this.socket.onerror = (e) => {
|
|
47
|
+
this.errorHandlers.forEach((t) => t(e));
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
sendToHub(e) {
|
|
51
|
+
this.socket?.readyState === WebSocket.OPEN && this.socket.send(JSON.stringify(e));
|
|
52
|
+
}
|
|
53
|
+
send(e, n) {
|
|
54
|
+
this.sendToHub({
|
|
55
|
+
type: t.MESSAGE,
|
|
56
|
+
channel: e,
|
|
57
|
+
data: n
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
setState(e, n) {
|
|
61
|
+
this.sendToHub({
|
|
62
|
+
type: t.STATE,
|
|
63
|
+
channel: e,
|
|
64
|
+
data: n
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
subscribe(e, n) {
|
|
68
|
+
this.listeners.has(e) || this.listeners.set(e, /* @__PURE__ */ new Set()), this.listeners.get(e).add(n), this.subscribedChannels.has(e) || (this.subscribedChannels.add(e), this.sendToHub({
|
|
69
|
+
type: t.SUBSCRIBE,
|
|
70
|
+
channels: [e]
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
unsubscribe(e, n) {
|
|
74
|
+
n ? (this.listeners.get(e)?.delete(n), this.listeners.get(e)?.size === 0 && (this.listeners.delete(e), this.subscribedChannels.delete(e), this.sendToHub({
|
|
75
|
+
type: t.UNSUBSCRIBE,
|
|
76
|
+
channels: [e]
|
|
77
|
+
}))) : (this.listeners.delete(e), this.subscribedChannels.delete(e), this.sendToHub({
|
|
78
|
+
type: t.UNSUBSCRIBE,
|
|
79
|
+
channels: [e]
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
onOpen(e) {
|
|
83
|
+
this.openHandlers.push(e);
|
|
84
|
+
}
|
|
85
|
+
onClose(e) {
|
|
86
|
+
this.closeHandlers.push(e);
|
|
87
|
+
}
|
|
88
|
+
onError(e) {
|
|
89
|
+
this.errorHandlers.push(e);
|
|
90
|
+
}
|
|
91
|
+
close() {
|
|
92
|
+
this.alive = !1, this.socket?.close();
|
|
93
|
+
}
|
|
79
94
|
};
|
|
80
|
-
//#
|
|
95
|
+
//#endregion
|
|
96
|
+
export { n as WebsocketClient };
|
|
97
|
+
|
|
98
|
+
//# sourceMappingURL=client.js.map
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sources":["../src/client.ts"],"sourcesContent":["import { WebsocketConnectionConfig, WebsocketClientEvent, WebsocketClientMessage, WebsocketHubMessage, WebsocketListener, WS_URL } from '@digo-labs/common';\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\n private socket: WebSocket | null = null;\n private alive = true;\n private listeners = new Map<string, Set<WebsocketListener>>();\n private subscribedChannels = new Set<string>();\n private openHandlers: (() => 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;\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\n this.open();\n }\n\n private open(): void {\n this.socket = new WebSocket(this.url);\n\n this.socket.onopen = () => {\n this.reconnectDelay = this.reconnectInterval;\n\n this.sendToHub({ type: WebsocketClientEvent.IDENTIFY, id: this.id, app: this.app });\n\n if (this.subscribedChannels.size > 0) {\n this.sendToHub({ type: WebsocketClientEvent.SUBSCRIBE, channels: [...this.subscribedChannels] });\n }\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 const channelListeners = this.listeners.get(message.channel);\n\n channelListeners?.forEach(listener => listener(message.data));\n };\n\n this.socket.onclose = (event) => {\n this.closeHandlers.forEach(h => h());\n\n if (event.code === 4000) return;\n\n if (this.alive && this.reconnectEnabled) {\n const jitteredDelay = 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\n this.socket.onerror = (error) => {\n this.errorHandlers.forEach(h => h(error));\n };\n }\n\n private sendToHub(message: WebsocketClientMessage): void {\n if (this.socket?.readyState === WebSocket.OPEN) {\n this.socket.send(JSON.stringify(message));\n }\n }\n\n public send(channel: string, data?: unknown): void {\n this.sendToHub({ type: WebsocketClientEvent.MESSAGE, channel, data });\n }\n\n public setState(channel: string, data?: unknown): void {\n this.sendToHub({ type: WebsocketClientEvent.STATE, channel, 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.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.sendToHub({ type: WebsocketClientEvent.UNSUBSCRIBE, channels: [channel] });\n }\n } else {\n this.listeners.delete(channel);\n this.subscribedChannels.delete(channel);\n this.sendToHub({ type: WebsocketClientEvent.UNSUBSCRIBE, channels: [channel] });\n }\n }\n\n public onOpen(handler: () => void): void { this.openHandlers.push(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.socket?.close();\n }\n}\n"],"
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["import { WebsocketConnectionConfig, WebsocketClientEvent, WebsocketClientMessage, WebsocketHubMessage, WebsocketListener, WS_URL } from '@digo-labs/common';\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\n private socket: WebSocket | null = null;\n private alive = true;\n private listeners = new Map<string, Set<WebsocketListener>>();\n private subscribedChannels = new Set<string>();\n private openHandlers: (() => 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\n this.open();\n }\n\n private open(): void {\n this.socket = new WebSocket(this.url);\n\n this.socket.onopen = () => {\n this.reconnectDelay = this.reconnectInterval;\n\n this.sendToHub({ type: WebsocketClientEvent.IDENTIFY, id: this.id, app: this.app });\n\n if (this.subscribedChannels.size > 0) {\n this.sendToHub({ type: WebsocketClientEvent.SUBSCRIBE, channels: [...this.subscribedChannels] });\n }\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 const channelListeners = this.listeners.get(message.channel);\n\n channelListeners?.forEach(listener => listener(message.data));\n };\n\n this.socket.onclose = (event) => {\n this.closeHandlers.forEach(h => h());\n\n if (event.code === 4000) return;\n\n if (this.alive && this.reconnectEnabled) {\n const jitteredDelay = 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\n this.socket.onerror = (error) => {\n this.errorHandlers.forEach(h => h(error));\n };\n }\n\n private sendToHub(message: WebsocketClientMessage): void {\n if (this.socket?.readyState === WebSocket.OPEN) {\n this.socket.send(JSON.stringify(message));\n }\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.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.sendToHub({ type: WebsocketClientEvent.UNSUBSCRIBE, channels: [channel] });\n }\n } else {\n this.listeners.delete(channel);\n this.subscribedChannels.delete(channel);\n this.sendToHub({ type: WebsocketClientEvent.UNSUBSCRIBE, channels: [channel] });\n }\n }\n\n public onOpen(handler: () => void): void { this.openHandlers.push(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.socket?.close();\n }\n}\n"],"mappings":"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"}
|
package/dist/server.js
CHANGED
|
@@ -1,142 +1,150 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
1
|
+
import { WEBSOCKET_HUB_CHANNEL as e, WebSocketHubEvent as t, WebsocketClientEvent as n, WebsocketLogEvent as r } from "@digo-labs/common";
|
|
2
|
+
import i from "node:http";
|
|
3
|
+
import { WebSocket as a, WebSocketServer as o } from "ws";
|
|
4
|
+
//#region src/server.ts
|
|
5
|
+
var s = class {
|
|
6
|
+
httpServer;
|
|
7
|
+
wss;
|
|
8
|
+
clients = /* @__PURE__ */ new Set();
|
|
9
|
+
state = /* @__PURE__ */ new Map();
|
|
10
|
+
logs = [];
|
|
11
|
+
messageHandlers = [];
|
|
12
|
+
heartbeatInterval;
|
|
13
|
+
constructor(e) {
|
|
14
|
+
this.httpServer = i.createServer((e, t) => {
|
|
15
|
+
if (e.method === "GET" && e.url === "/health") {
|
|
16
|
+
t.writeHead(200, { "Content-Type": "application/json" }), t.end(JSON.stringify({
|
|
17
|
+
ok: !0,
|
|
18
|
+
clients: this.clients.size
|
|
19
|
+
}));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
t.writeHead(404), t.end();
|
|
23
|
+
}), this.wss = new o({ server: this.httpServer }), this.heartbeatInterval = setInterval(() => this.heartbeat(), 3e4), this.wss.on("close", () => clearInterval(this.heartbeatInterval)), this.wss.on("connection", (e) => this.handleConnection(e)), this.httpServer.listen(e.port);
|
|
24
|
+
}
|
|
25
|
+
heartbeat() {
|
|
26
|
+
let e = !1;
|
|
27
|
+
this.clients.forEach((t) => {
|
|
28
|
+
if (!t.alive) {
|
|
29
|
+
t.ws.terminate(), this.clients.delete(t), e = !0;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
t.alive = !1, t.ws.ping();
|
|
33
|
+
}), e && this.broadcastStatus();
|
|
34
|
+
}
|
|
35
|
+
handleConnection(e) {
|
|
36
|
+
let t = {
|
|
37
|
+
id: "unknown",
|
|
38
|
+
app: "unknown",
|
|
39
|
+
ws: e,
|
|
40
|
+
channels: /* @__PURE__ */ new Set(),
|
|
41
|
+
alive: !0
|
|
42
|
+
};
|
|
43
|
+
this.clients.add(t), e.on("error", () => {}), e.on("pong", () => {
|
|
44
|
+
t.alive = !0;
|
|
45
|
+
}), e.on("message", (n) => {
|
|
46
|
+
let r;
|
|
47
|
+
try {
|
|
48
|
+
r = JSON.parse(n.toString());
|
|
49
|
+
} catch {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.handleMessage(t, e, r);
|
|
53
|
+
}), e.on("close", () => {
|
|
54
|
+
this.clients.delete(t), this.addLog(t.id, r.DISCONNECT, ""), this.broadcastStatus();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
handleMessage(e, i, a) {
|
|
58
|
+
switch (a.type) {
|
|
59
|
+
case n.IDENTIFY: {
|
|
60
|
+
let t = [...this.clients].find((t) => t.id === a.id && t !== e);
|
|
61
|
+
t && (t.ws.close(4e3), this.clients.delete(t)), e.id = a.id, e.app = a.app, this.addLog(a.id, r.CONNECT, ""), this.broadcastStatus();
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case n.SUBSCRIBE:
|
|
65
|
+
a.channels.forEach((n) => {
|
|
66
|
+
if (e.channels.add(n), this.state.has(n)) {
|
|
67
|
+
let e = {
|
|
68
|
+
type: t.STATE,
|
|
69
|
+
channel: n,
|
|
70
|
+
data: this.state.get(n)
|
|
71
|
+
};
|
|
72
|
+
i.send(JSON.stringify(e));
|
|
73
|
+
}
|
|
74
|
+
}), this.addLog(e.id, r.SUBSCRIBE, a.channels.join(", ")), this.broadcastStatus();
|
|
75
|
+
break;
|
|
76
|
+
case n.UNSUBSCRIBE:
|
|
77
|
+
a.channels.forEach((t) => e.channels.delete(t)), this.addLog(e.id, r.UNSUBSCRIBE, a.channels.join(", ")), this.broadcastStatus();
|
|
78
|
+
break;
|
|
79
|
+
case n.MESSAGE: {
|
|
80
|
+
let n = {
|
|
81
|
+
type: t.MESSAGE,
|
|
82
|
+
channel: a.channel,
|
|
83
|
+
data: a.data
|
|
84
|
+
};
|
|
85
|
+
this.broadcast(e, a.channel, n), this.messageHandlers.forEach((e) => e(a.channel, a.data)), this.addLog(e.id, r.MESSAGE, a.channel, a.data), this.broadcastStatus();
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case n.STATE: {
|
|
89
|
+
this.state.set(a.channel, a.data);
|
|
90
|
+
let n = {
|
|
91
|
+
type: t.STATE,
|
|
92
|
+
channel: a.channel,
|
|
93
|
+
data: a.data
|
|
94
|
+
};
|
|
95
|
+
this.broadcast(e, a.channel, n), this.messageHandlers.forEach((e) => e(a.channel, a.data)), this.addLog(e.id, r.STATE, a.channel, a.data), this.broadcastStatus();
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
addLog(e, t, n, r) {
|
|
101
|
+
this.logs.push({
|
|
102
|
+
timestamp: Date.now(),
|
|
103
|
+
sender: e,
|
|
104
|
+
type: t,
|
|
105
|
+
channel: n,
|
|
106
|
+
data: r
|
|
107
|
+
}), this.logs.length > 808 && this.logs.shift();
|
|
108
|
+
}
|
|
109
|
+
broadcast(e, t, n) {
|
|
110
|
+
let r = JSON.stringify(n);
|
|
111
|
+
this.clients.forEach((n) => {
|
|
112
|
+
n !== e && n.channels.has(t) && n.ws.readyState === a.OPEN && n.ws.send(r);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
broadcastStatus() {
|
|
116
|
+
let n = {
|
|
117
|
+
clients: [...this.clients].map((e) => ({
|
|
118
|
+
id: e.id,
|
|
119
|
+
app: e.app,
|
|
120
|
+
channels: [...e.channels]
|
|
121
|
+
})),
|
|
122
|
+
state: Object.fromEntries([...this.state].filter(([t]) => t !== e)),
|
|
123
|
+
logs: this.logs
|
|
124
|
+
};
|
|
125
|
+
this.state.set(e, n);
|
|
126
|
+
let r = {
|
|
127
|
+
type: t.STATE,
|
|
128
|
+
channel: e,
|
|
129
|
+
data: n
|
|
130
|
+
}, i = JSON.stringify(r);
|
|
131
|
+
this.clients.forEach((t) => {
|
|
132
|
+
t.channels.has(e) && t.ws.readyState === a.OPEN && t.ws.send(i);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
onMessage(e) {
|
|
136
|
+
this.messageHandlers.push(e);
|
|
137
|
+
}
|
|
138
|
+
getState(e) {
|
|
139
|
+
return this.state.get(e);
|
|
140
|
+
}
|
|
141
|
+
close() {
|
|
142
|
+
clearInterval(this.heartbeatInterval), this.clients.forEach((e) => {
|
|
143
|
+
e.ws.readyState === a.OPEN && e.ws.close(1001, "Hub shutting down");
|
|
144
|
+
}), this.wss.close(), this.httpServer.close();
|
|
145
|
+
}
|
|
141
146
|
};
|
|
142
|
-
//#
|
|
147
|
+
//#endregion
|
|
148
|
+
export { s as WebsocketHub };
|
|
149
|
+
|
|
150
|
+
//# sourceMappingURL=server.js.map
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sources":["../src/server.ts"],"sourcesContent":["import http from 'node:http';\nimport { WebSocketServer, WebSocket } from 'ws';\n\nimport { WebsocketClientMessage, WebsocketClientEvent, WebSocketHubEvent, WebsocketHubMessage, WebsocketHubLogEntry, WebsocketLogEvent, WebsocketHubStatus, WebsocketsHubConfig, WEBSOCKET_HUB_CHANNEL } from '@digo-labs/common';\n\ninterface ConnectedClient {\n id: string;\n app: string;\n ws: WebSocket;\n channels: Set<string>;\n alive: boolean;\n}\n\nexport class WebsocketHub {\n private httpServer: http.Server;\n private wss: WebSocketServer;\n\n private clients = new Set<ConnectedClient>();\n private state = new Map<string, unknown>();\n\n private logs: WebsocketHubLogEntry[] = [];\n private messageHandlers: ((channel: string, data: unknown) => void)[] = [];\n private heartbeatInterval: ReturnType<typeof setInterval>;\n\n constructor(config: WebsocketsHubConfig) {\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 });\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 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 };\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 let message: WebsocketClientMessage;\n\n try {\n message = JSON.parse(raw.toString());\n } catch { return; }\n\n this.handleMessage(client, socket, message);\n });\n\n socket.on('close', () => {\n this.clients.delete(client);\n\n this.addLog(client.id, WebsocketLogEvent.DISCONNECT, '');\n\n this.broadcastStatus();\n });\n }\n\n private handleMessage(client: ConnectedClient, socket: WebSocket, message: WebsocketClientMessage): void {\n switch (message.type) {\n case WebsocketClientEvent.IDENTIFY: {\n const existing = [...this.clients].find(c => c.id === message.id && c !== client);\n\n if (existing) {\n existing.ws.close(4000);\n\n this.clients.delete(existing);\n }\n\n client.id = message.id;\n client.app = message.app;\n\n this.addLog(message.id, WebsocketLogEvent.CONNECT, '');\n\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.SUBSCRIBE: {\n message.channels.forEach((channel) => {\n client.channels.add(channel);\n\n if (this.state.has(channel)) {\n const stateMessage: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: channel,\n data: this.state.get(channel),\n };\n\n socket.send(JSON.stringify(stateMessage));\n }\n });\n\n this.addLog(client.id, WebsocketLogEvent.SUBSCRIBE, message.channels.join(', '));\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.UNSUBSCRIBE: {\n message.channels.forEach(channel => client.channels.delete(channel));\n\n this.addLog(client.id, WebsocketLogEvent.UNSUBSCRIBE, message.channels.join(', '));\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.MESSAGE: {\n const outgoing: WebsocketHubMessage = {\n type: WebSocketHubEvent.MESSAGE,\n channel: message.channel,\n data: message.data,\n };\n\n this.broadcast(client, message.channel, outgoing);\n\n this.messageHandlers.forEach(h => h(message.channel, message.data));\n\n this.addLog(client.id, WebsocketLogEvent.MESSAGE, message.channel, message.data);\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.STATE: {\n this.state.set(message.channel, message.data);\n\n const outgoing: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: message.channel,\n data: message.data,\n };\n\n this.broadcast(client, message.channel, outgoing);\n\n this.messageHandlers.forEach(h => h(message.channel, message.data));\n\n this.addLog(client.id, WebsocketLogEvent.STATE, message.channel, message.data);\n\n this.broadcastStatus();\n break;\n }\n }\n }\n\n private addLog(sender: string, type: WebsocketLogEvent, channel: string, data?: unknown): void {\n this.logs.push({ timestamp: Date.now(), sender, type, channel, data });\n\n if (this.logs.length > 808) this.logs.shift();\n }\n\n private broadcast(sender: ConnectedClient, channel: string, message: WebsocketHubMessage): void {\n const payload = JSON.stringify(message);\n\n this.clients.forEach((client) => {\n if (client !== sender && client.channels.has(channel) && client.ws.readyState === WebSocket.OPEN) {\n client.ws.send(payload);\n }\n });\n }\n\n private broadcastStatus(): void {\n const status: WebsocketHubStatus = {\n clients: [...this.clients].map(client => ({ id: client.id, app: client.app, channels: [...client.channels] })),\n state: Object.fromEntries([...this.state].filter(([k]) => k !== WEBSOCKET_HUB_CHANNEL)),\n logs: this.logs,\n };\n\n this.state.set(WEBSOCKET_HUB_CHANNEL, status);\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 this.clients.forEach((client) => {\n if (client.channels.has(WEBSOCKET_HUB_CHANNEL) && client.ws.readyState === WebSocket.OPEN) {\n client.ws.send(payload);\n }\n });\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.state.get(channel); }\n\n public close(): void {\n clearInterval(this.heartbeatInterval);\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"],"names":["WebsocketHub","config","__publicField","http","req","res","WebSocketServer","socket","terminated","client","raw","message","WebsocketLogEvent","WebsocketClientEvent","existing","c","channel","stateMessage","WebSocketHubEvent","outgoing","h","sender","type","data","payload","WebSocket","status","k","WEBSOCKET_HUB_CHANNEL","handler"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["import http from 'node:http';\nimport { WebSocketServer, WebSocket } from 'ws';\n\nimport { WebsocketClientMessage, WebsocketClientEvent, WebSocketHubEvent, WebsocketHubMessage, WebsocketHubLogEntry, WebsocketLogEvent, WebsocketHubStatus, WebsocketsHubConfig, WEBSOCKET_HUB_CHANNEL } from '@digo-labs/common';\n\ninterface ConnectedClient {\n id: string;\n app: string;\n ws: WebSocket;\n channels: Set<string>;\n alive: boolean;\n}\n\nexport class WebsocketHub {\n private httpServer: http.Server;\n private wss: WebSocketServer;\n\n private clients = new Set<ConnectedClient>();\n private state = new Map<string, unknown>();\n\n private logs: WebsocketHubLogEntry[] = [];\n private messageHandlers: ((channel: string, data: unknown) => void)[] = [];\n private heartbeatInterval: ReturnType<typeof setInterval>;\n\n constructor(config: WebsocketsHubConfig) {\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 });\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 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 };\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 let message: WebsocketClientMessage;\n\n try {\n message = JSON.parse(raw.toString());\n } catch { return; }\n\n this.handleMessage(client, socket, message);\n });\n\n socket.on('close', () => {\n this.clients.delete(client);\n\n this.addLog(client.id, WebsocketLogEvent.DISCONNECT, '');\n\n this.broadcastStatus();\n });\n }\n\n private handleMessage(client: ConnectedClient, socket: WebSocket, message: WebsocketClientMessage): void {\n switch (message.type) {\n case WebsocketClientEvent.IDENTIFY: {\n const existing = [...this.clients].find(c => c.id === message.id && c !== client);\n\n if (existing) {\n existing.ws.close(4000);\n\n this.clients.delete(existing);\n }\n\n client.id = message.id;\n client.app = message.app;\n\n this.addLog(message.id, WebsocketLogEvent.CONNECT, '');\n\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.SUBSCRIBE: {\n message.channels.forEach((channel) => {\n client.channels.add(channel);\n\n if (this.state.has(channel)) {\n const stateMessage: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: channel,\n data: this.state.get(channel),\n };\n\n socket.send(JSON.stringify(stateMessage));\n }\n });\n\n this.addLog(client.id, WebsocketLogEvent.SUBSCRIBE, message.channels.join(', '));\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.UNSUBSCRIBE: {\n message.channels.forEach(channel => client.channels.delete(channel));\n\n this.addLog(client.id, WebsocketLogEvent.UNSUBSCRIBE, message.channels.join(', '));\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.MESSAGE: {\n const outgoing: WebsocketHubMessage = {\n type: WebSocketHubEvent.MESSAGE,\n channel: message.channel,\n data: message.data,\n };\n\n this.broadcast(client, message.channel, outgoing);\n\n this.messageHandlers.forEach(h => h(message.channel, message.data));\n\n this.addLog(client.id, WebsocketLogEvent.MESSAGE, message.channel, message.data);\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.STATE: {\n this.state.set(message.channel, message.data);\n\n const outgoing: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: message.channel,\n data: message.data,\n };\n\n this.broadcast(client, message.channel, outgoing);\n\n this.messageHandlers.forEach(h => h(message.channel, message.data));\n\n this.addLog(client.id, WebsocketLogEvent.STATE, message.channel, message.data);\n\n this.broadcastStatus();\n break;\n }\n }\n }\n\n private addLog(sender: string, type: WebsocketLogEvent, channel: string, data?: unknown): void {\n this.logs.push({ timestamp: Date.now(), sender: sender, type: type, channel: channel, data: data });\n\n if (this.logs.length > 808) this.logs.shift();\n }\n\n private broadcast(sender: ConnectedClient, channel: string, message: WebsocketHubMessage): void {\n const payload = JSON.stringify(message);\n\n this.clients.forEach((client) => {\n if (client !== sender && client.channels.has(channel) && client.ws.readyState === WebSocket.OPEN) {\n client.ws.send(payload);\n }\n });\n }\n\n private broadcastStatus(): void {\n const status: WebsocketHubStatus = {\n clients: [...this.clients].map(client => ({ id: client.id, app: client.app, channels: [...client.channels] })),\n state: Object.fromEntries([...this.state].filter(([k]) => k !== WEBSOCKET_HUB_CHANNEL)),\n logs: this.logs,\n };\n\n this.state.set(WEBSOCKET_HUB_CHANNEL, status);\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 this.clients.forEach((client) => {\n if (client.channels.has(WEBSOCKET_HUB_CHANNEL) && client.ws.readyState === WebSocket.OPEN) {\n client.ws.send(payload);\n }\n });\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.state.get(channel); }\n\n public close(): void {\n clearInterval(this.heartbeatInterval);\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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"}
|