@digo-labs/ws 0.2.0 → 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 CHANGED
@@ -1,11 +1,54 @@
1
1
  # @digo-labs/ws
2
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.
3
+ The low-level websocket client (`WebsocketClient`), the hub server (`@digo-labs/ws/server`), and the `digo-ws-hub` CLI for running a hub locally. Most apps use the typed `Websockets` wrapper from [`@digo-labs/services`](https://design.digo-labs.com/docs/guide/package-services) instead, which builds a `WebsocketClient` for you and adds the app name and auth token.
4
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.
5
+ **How to read this.** This guide ships with the package, so it matches the version you have installed; the site describes the latest release. The types in `node_modules/@digo-labs/ws/dist/*.d.ts` are exact and win if they disagree with it. Deeper pages: https://design.digo-labs.com/docs/guide/package-ws.
8
6
 
9
7
  ```bash
10
8
  npm install @digo-labs/ws
11
9
  ```
10
+
11
+ ## Client
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). 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
+
15
+ ```ts
16
+ import { WebsocketClient } from '@digo-labs/ws';
17
+
18
+ const client = new WebsocketClient({ url: 'ws://localhost:9900', app: 'my-app' });
19
+
20
+ client.subscribe('scores', data => console.log(data));
21
+ client.send('scores', { team: 'red', points: 3 }); // one-off message to subscribers
22
+ client.setState('scores', { red: 3, blue: 1 }); // stored by the hub, sent to anyone who subscribes later
23
+ client.onReady(() => console.log('identified'));
24
+ ```
25
+
26
+ - `send` and `setState` called while the socket is open but before the hub confirms the client are queued and flushed once it does. Subscriptions are re-sent after every reconnect.
27
+ - `isLive(channel)` is `true` once the client is identified, subscribed and not denied on that channel.
28
+ - `unsubscribe(channel, listener?)` removes one listener, or the whole channel when no listener is given.
29
+ - Lifecycle: `onOpen`, `onReady` / `offReady`, `onClose`, `onError`. `close()` stops reconnecting for good.
30
+
31
+ ## Server
32
+
33
+ `@digo-labs/ws/server` exports `WebsocketHub`, the hub itself (Node only):
34
+
35
+ ```ts
36
+ import { WebsocketHub } from '@digo-labs/ws/server';
37
+
38
+ const hub = new WebsocketHub({ port: 9900, tokenSecret: process.env.WS_TOKEN_SECRET });
39
+
40
+ hub.onMessage((channel, data) => console.log(channel, data));
41
+ hub.getState('scores');
42
+ hub.close();
43
+ ```
44
+
45
+ It also answers `GET /health` with `{ ok, clients }`. `tokenSecret` lets it verify the tokens clients send through `getToken`.
46
+
47
+ ## Running the hub locally
48
+
49
+ ```bash
50
+ npx digo-ws-hub # port 9900
51
+ npx digo-ws-hub 9901 # another port
52
+ ```
53
+
54
+ The CLI reads `WS_TOKEN_SECRET` from the environment and shuts down cleanly on Ctrl+C. Inside the core monorepo, `npm run ws` (or `npm run dev`) starts the same hub from source.
package/dist/client.d.ts CHANGED
@@ -23,6 +23,7 @@ export declare class WebsocketClient {
23
23
  constructor(properties: WebsocketConnectionConfig);
24
24
  private open;
25
25
  private identify;
26
+ private scheduleRefresh;
26
27
  private handleIdentified;
27
28
  private stopRefresh;
28
29
  private scheduleReconnect;
package/dist/client.js CHANGED
@@ -1,6 +1,6 @@
1
- import { WS_URL as e, WebSocketHubEvent as t, WebsocketClientEvent as n } from "@digo-labs/common";
2
- //#region src/client.ts
3
- var r = 4e3, i = 4401, a = 6e5, o = class {
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(t) {
25
- 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.getToken = t.getToken ?? null, this.open();
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 n;
31
+ let t;
32
32
  try {
33
- n = JSON.parse(e.data);
33
+ t = JSON.parse(e.data);
34
34
  } catch {
35
35
  return;
36
36
  }
37
- if (n.type === t.IDENTIFIED) {
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 !== r && this.alive && this.reconnectEnabled && this.scheduleReconnect(e.code === i);
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: n.IDENTIFY,
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
- if (!e) {
63
- this.socket?.close();
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: n.SUBSCRIBE,
67
+ type: i.SUBSCRIBE,
76
68
  channels: [...this.subscribedChannels]
77
- }), this.pending.splice(0).forEach((e) => this.sendRaw(e)), this.getToken && !this.refreshTimer && (this.refreshTimer = setInterval(() => void this.identify(), a)), this.readyHandlers.forEach((e) => e()));
69
+ }), this.pending.splice(0).forEach((e) => this.sendRaw(e)), this.readyHandlers.forEach((e) => e()));
78
70
  }
79
71
  stopRefresh() {
80
- this.refreshTimer && clearInterval(this.refreshTimer), this.refreshTimer = null;
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
- if (this.isIdentified) {
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: n.MESSAGE,
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: n.STATE,
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: n.SUBSCRIBE,
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: n.UNSUBSCRIBE,
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: n.UNSUBSCRIBE,
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
- //#endregion
150
- export { o as WebsocketClient };
137
+ export { u as WebsocketClient };
151
138
 
152
139
  //# sourceMappingURL=client.js.map
@@ -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
- //#region src/server.ts
5
- var d = 1024 * 1024, f = 64 * 1024, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401, v = "/", y = class {
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
- if (e.method === "GET" && e.url === "/health") {
19
- t.writeHead(200, { "Content-Type": "application/json" }), t.end(JSON.stringify({
20
- ok: !0,
21
- clients: this.clients.size
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
- if (!t.alive) {
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 && !r.token == !e.token && r !== e);
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 || this.identify(e, i, o.app);
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 ? t.matchesAny(e.token.subscribe, n) : !0;
159
+ return !e.token || t.matchesAny(e.token.subscribe, n);
155
160
  }
156
161
  canPublish(e, n) {
157
- return e.token ? t.matchesAny(e.token.publish, n) : !0;
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}${v}${n}`;
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(v) + 1);
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
- this.state.delete(e);
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
- if (this.statusTimer) {
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
- //#endregion
260
- export { y as WebsocketHub };
263
+ export { b as WebsocketHub };
261
264
 
262
265
  //# sourceMappingURL=server.js.map
@@ -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":";;;;AAKA,IAAMC,IAAsB,SACtBC,IAAsB,OACtBC,IAAsB,KACtBC,IAAsB,IACtBC,IAAsB,KACtBC,IAAsB,KACtBC,IAAsB,MACtBC,IAAsB,MACtBC,IAAsB,KAkBfC,IAAb,MAA0B;CACxB;CACA;CACA;CAEA,0BAAkB,IAAI,IAAqB;CAC3C,wBAAkB,IAAI,IAAyB;CAE/C,OAAoD,CAAC;CACrD,kBAA0E,CAAC;CAC3E;CACA,cAAkE;CAClE,cAAsB;CAEtB,YAAY,GAA6B;EAqBvC,AApBA,KAAK,SAASC,GAEd,KAAK,aAAaX,EAAK,cAAc,GAAK,MAAQ;GAChD,AAAIY,EAAI,WAAW,SAASA,EAAI,QAAQ,aACtCC,EAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC,GACzDA,EAAI,IAAI,KAAK,UAAU;IAAE,IAAI;IAAM,SAAS,KAAK,QAAQ;GAAK,CAAC,CAAC,MAIlEA,EAAI,UAAU,GAAG,GACjBA,EAAI,IAAI;EACV,CAAC,GAED,KAAK,MAAM,IAAIC,EAAgB;GAAE,QAAQ,KAAK;GAAY,YAAYb;EAAkB,CAAC,GAEzF,KAAK,oBAAoB,kBAAkB,KAAK,UAAU,GAAG,GAAK,GAElE,KAAK,IAAI,GAAG,eAAoB,cAAc,KAAK,iBAAiB,CAAC,GACrE,KAAK,IAAI,GAAG,eAAc,MAAU,KAAK,iBAAiBc,CAAM,CAAC,GAEjE,KAAK,WAAW,OAAOJ,EAAO,IAAI;CACpC;CAEA,YAA0B;EACxB,IAAIK,IAAa;EAEjB,KAAK,QAAQ,SAAS,MAAW;GAC/B,AAAI,KAAK,UAAUC,CAAM,IACvBA,EAAO,GAAG,MAAMT,CAAY,IAKzBS,EAAO,SAUZ,EAAO,QAAQ,IAEfA,EAAO,GAAG,KAAK,MAXbA,EAAO,GAAG,UAAU,GAEpB,KAAK,QAAQ,OAAOA,CAAM,GAE1B,IAAa;EAQjB,CAAC;EAED,IAAMC,IAAM,KAAK,IAAI;EAMrB,AAJA,KAAK,MAAM,SAAS,GAAQ,MAAQ;GAClC,AAAIC,EAAO,cAAc,QAAQA,EAAO,aAAaD,KAAK,KAAK,MAAM,OAAOE,CAAG;EACjF,CAAC,GAEGJ,KAAY,KAAK,gBAAgB;CACvC;CAEA,iBAAyB,GAAyB;EAChD,IAAMC,IAA0B;GAC9B,IAAa;GACb,KAAa;GACb,IAAaF;GACb,0BAAa,IAAI,IAAI;GACrB,OAAa;GACb,OAAa;GACb,aAAa,KAAK,IAAI;GACtB,UAAa;EACf;EA0BA,AAxBA,KAAK,QAAQ,IAAIE,CAAM,GAEvBF,EAAO,GAAG,eAAe,CAAC,CAAC,GAE3BA,EAAO,GAAG,cAAc;GACtB,EAAO,QAAQ;EACjB,CAAC,GAEDA,EAAO,GAAG,YAAY,MAAQ;GAC5B,IAAIE,EAAO,UAAU,KAAK,QAAQI,CAAG,IAAInB,KAAqB,KAAK,WAAWe,CAAM,IAAI;GAExF,IAAMK,IAAU,KAAK,aAAaD,CAAG;GAErC,AAAKC,MAED,KAAK,UAAUL,CAAM,KAAK,CAAC,KAAK,UAAUK,CAAO,IACnDP,EAAO,MAAMP,CAAY,IAK3Be,EAAc,KAAK,cAAcN,GAAQF,GAAQO,CAAO,CAAC;EAC3D,CAAC,GAEDP,EAAO,GAAG,eAAe;GAKvB,AAJA,KAAK,QAAQ,OAAOE,CAAM,GAE1B,KAAK,OAAOA,GAAQO,EAAkB,YAAY,EAAE,GAEpD,KAAK,gBAAgB;EACvB,CAAC;CACH;CAEA,aAAqB,GAA6C;EAChE,IAAIC;EAEJ,IAAI;GACF,IAAO,KAAK,MAAMJ,EAAI,SAAS,CAAC;EAClC,QAAQ;GAAE,OAAO;EAAM;EAEvB,IAAMK,IAASC,EAA6B,UAAUF,CAAI;EAE1D,OAAOC,EAAO,UAAUA,EAAO,OAAO;CACxC;CAEA,MAAc,cAAc,GAAyB,GAAmB,GAAgD;EACtH,QAAQJ,EAAQ,MAAhB;GACE,KAAKM,EAAqB;IACxB,IAAIN,EAAQ,OAAO;KACjB,MAAM,KAAK,kBAAkBL,GAAQF,GAAQO,EAAQ,IAAIA,EAAQ,KAAK;KAEtE;IACF;IAEA,IAAIL,EAAO,OAAO;KAChB,KAAK,KAAKF,CAAM;KAEhB;IACF;IAEA,KAAK,SAASE,GAAQK,EAAQ,IAAIA,EAAQ,GAAG;IAC7C;GAGF,KAAKM,EAAqB,WAAW;IACnC,IAAMC,IAAUP,EAAQ,SAAS,QAAO,MAAW,KAAK,aAAaL,GAAQa,CAAO,CAAC;IAuBrF,AArBAD,EAAQ,SAAS,MAAY;KAC3B,IAAMT,IAAM,KAAK,WAAWH,GAAQa,CAAO;KAE3C,EAAO,SAAS,IAAIV,CAAG;KAEvB,IAAMD,IAAS,KAAK,UAAUC,CAAG;KAEjC,IAAID,MAAW,KAAA,GAAW;MACxB,IAAMY,IAAoC;OACxC,MAASC,EAAkB;OAClB;OACT,MAASb;MACX;MAEA,EAAO,KAAK,KAAK,UAAUY,CAAY,CAAC;KAC1C;IACF,CAAC,GAEDT,EAAQ,SAAS,QAAO,MAAW,CAACO,EAAQ,SAASC,CAAO,CAAC,CAAC,CAAC,SAAQ,MAAW,KAAK,KAAKf,GAAQe,CAAO,CAAC,GAE5G,KAAK,OAAOb,GAAQO,EAAkB,WAAWK,EAAQ,KAAK,IAAI,CAAC,GACnE,KAAK,gBAAgB;IACrB;GACF;GAEA,KAAKD,EAAqB;IAIxB,AAHAN,EAAQ,SAAS,SAAQ,MAAWL,EAAO,SAAS,OAAO,KAAK,WAAWA,GAAQa,CAAO,CAAC,CAAC,GAE5F,KAAK,OAAOb,GAAQO,EAAkB,aAAaF,EAAQ,SAAS,KAAK,IAAI,CAAC,GAC9E,KAAK,gBAAgB;IACrB;GAGF,KAAKM,EAAqB,SAAS;IACjC,IAAI,CAAC,KAAK,WAAWX,GAAQK,EAAQ,OAAO,GAAG;KAC7C,KAAK,KAAKP,GAAQO,EAAQ,OAAO;KAEjC;IACF;IAEA,IAAMW,IAAgC;KACpC,MAASD,EAAkB;KAC3B,SAASV,EAAQ;KACjB,MAASA,EAAQ;IACnB;IAOA,AALA,KAAK,UAAUL,GAAQ,KAAK,WAAWA,GAAQK,EAAQ,OAAO,GAAGW,CAAQ,GAEzE,KAAK,gBAAgB,SAAQ,MAAKC,EAAEZ,EAAQ,SAASA,EAAQ,IAAI,CAAC,GAElE,KAAK,OAAOL,GAAQO,EAAkB,SAASF,EAAQ,SAASA,EAAQ,IAAI,GAC5E,KAAK,gBAAgB;IACrB;GACF;GAEA,KAAKM,EAAqB,OAAO;IAC/B,IAAI,CAAC,KAAK,WAAWX,GAAQK,EAAQ,OAAO,GAAG;KAC7C,KAAK,KAAKP,GAAQO,EAAQ,OAAO;KAEjC;IACF;IAEA,IAAMF,IAAM,KAAK,WAAWH,GAAQK,EAAQ,OAAO;IAEnD,IAAI,CAAC,KAAK,WAAWF,GAAKE,EAAQ,MAAML,EAAO,QAAQkB,EAAgB,iBAAiBlB,EAAO,OAAOK,EAAQ,OAAO,IAAI,IAAI,GAAG;KAC9H,KAAK,KAAKP,GAAQO,EAAQ,OAAO;KAEjC;IACF;IAEA,IAAMW,IAAgC;KACpC,MAASD,EAAkB;KAC3B,SAASV,EAAQ;KACjB,MAASA,EAAQ;IACnB;IAQA,AANA,KAAK,UAAUL,GAAQG,GAAKa,CAAQ,GAEpC,KAAK,gBAAgB,SAAQ,MAAKC,EAAEZ,EAAQ,SAASA,EAAQ,IAAI,CAAC,GAElE,KAAK,OAAOL,GAAQO,EAAkB,OAAOF,EAAQ,SAASA,EAAQ,IAAI,GAE1E,KAAK,gBAAgB;IACrB;GACF;EACF;CACF;CAEA,SAAiB,GAAyB,GAAY,GAAmB;EACvE,IAAMc,IAAW,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,MAAK,MAAKC,EAAE,OAAOC,KAAMD,EAAE,QAAQE,KAAOF,MAAMpB,KAAU,KAAK,aAAaoB,GAAGpB,CAAM,CAAC;EAazH,AAXImB,MACFA,EAAS,GAAG,MAAM9B,CAAW,GAE7B,KAAK,QAAQ,OAAO8B,CAAQ,IAG9B,EAAO,KAAME,GACb,EAAO,MAAMC,GAEb,KAAK,OAAOtB,GAAQO,EAAkB,SAAS,EAAE,GAEjD,KAAK,gBAAgB;CACvB;CAEA,aAAqB,GAAwB,GAAkC;EAG7E,OAFI,CAACgB,EAAM,SAAS,CAACC,EAAO,QAAc,CAACD,EAAM,SAAS,CAACC,EAAO,QAE3DD,EAAM,MAAM,YAAYC,EAAO,MAAM,WAAWD,EAAM,MAAM,SAAS,YAAYC,EAAO,MAAM,SAAS;CAChH;CAEA,MAAc,kBAAkB,GAAyB,GAAmB,GAAY,GAA8B;EACpH,IAAMC,IAAU,KAAK,OAAO,cAAc,MAAMP,EAAgB,OAAOQ,GAAO,KAAK,OAAO,WAAW,IAAI;EAEzG,IAAI,CAACD,KAAYzB,EAAO,SAASA,EAAO,MAAM,QAAQyB,EAAQ,KAAM;GAGlE,AAFA,KAAK,KAAK3B,CAAM,GAEhBA,EAAO,MAAMR,CAAW;GAExB;EACF;EAEA,IAAMqC,IAAY,CAAC,CAAC3B,EAAO;EAI3B,AAFA,EAAO,QAAQyB,GAEXE,IACF,KAAK,cAAc3B,GAAQF,CAAM,IAEjC,KAAK,SAASE,GAAQqB,GAAII,EAAQ,GAAG;EAGvC,IAAMG,IAAkC,EAAE,MAAMb,EAAkB,WAAW;EAE7E,EAAO,KAAK,KAAK,UAAUa,CAAU,CAAC;CACxC;CAEA,cAAsB,GAAyB,GAAyB;EACtE,IAAMC,IAAS,KAAK,WAAW7B,GAAQ,EAAE;EAEzC,CAAC,GAAGA,EAAO,QAAQ,CAAC,CAAC,KAAI,MAAOG,EAAI,MAAM0B,EAAO,MAAM,CAAC,CAAC,CAAC,QAAO,MAAW,CAAC,KAAK,aAAa7B,GAAQa,CAAO,CAAC,CAAC,CAAC,SAAS,MAAY;GAGpI,AAFAb,EAAO,SAAS,OAAO,KAAK,WAAWA,GAAQa,CAAO,CAAC,GAEvD,KAAK,KAAKf,GAAQe,CAAO;EAC3B,CAAC;CACH;CAEA,aAAqB,GAAyB,GAA0B;EAGtE,OAFA,CAAKb,EAAO,SAELkB,EAAgB,WAAWlB,EAAO,MAAM,WAAWa,CAAO;CACnE;CAEA,WAAmB,GAAyB,GAA0B;EAGpE,OAFA,CAAKb,EAAO,SAELkB,EAAgB,WAAWlB,EAAO,MAAM,SAASa,CAAO;CACjE;CAEA,WAAmB,GAAyB,GAAyB;EACnE,OAAOb,EAAO,QAAQ,KAAK,cAAcA,EAAO,MAAM,KAAKa,CAAO,IAAIA;CACxE;CAEA,cAAsB,GAAa,GAAyB;EAC1D,OAAO,GAAGiB,IAA2BR,IAAM9B,IAAsBqB;CACnE;CAEA,WAAmB,GAAa,GAAe,GAAuC;EACpF,IAAIV,EAAI,WAAW2B,CAAwB,KAAK,CAAC,KAAK,MAAM,IAAI3B,CAAG,GAAG;GACpE,IAAM4B,IAAY5B,EAAI,MAAM,GAAGA,EAAI,QAAQX,CAAmB,IAAI,CAAC;GAGnE,IAFkB,CAAC,GAAG,KAAK,MAAM,KAAK,CAAC,CAAC,CAAC,QAAO,MAAUU,EAAO,WAAW6B,CAAS,CAAC,CAAC,CAAC,UAE3E3C,GAAoB,OAAO;EAC1C;EAIA,OAFA,KAAK,MAAM,IAAIe,GAAK;GAAQ;GAAM,WAAW6B,MAAkB,OAAO,OAAO,KAAK,IAAI,IAAIA,IAAgB;EAAK,CAAC,GAEzG;CACT;CAEA,UAAkB,GAAsB;EACtC,IAAM9B,IAAS,KAAK,MAAM,IAAIC,CAAG;EAEjC,IAAKD,GAEL;OAAIA,EAAO,cAAc,QAAQA,EAAO,aAAa,KAAK,IAAI,GAC5D,KAAK,MAAM,OAAOC,CAAG;QAKvB,OAAOD,EAAO;EAFd;CAGF;CAEA,UAAkB,GAAyB,GAAa,GAAoC;EAC1F,IAAMuB,IAAU,KAAK,UAAUpB,CAAO;EAEtC,KAAK,QAAQ,SAAS,MAAW;GAC/B,AAAIL,MAAWiC,KAAWjC,EAAO,SAAS,IAAIG,CAAG,KAAKH,EAAO,GAAG,eAAekC,EAAU,SAErF,KAAK,UAAUlC,CAAM,IACvBA,EAAO,GAAG,MAAMT,CAAY,IAK9BS,EAAO,GAAG,KAAKyB,CAAO;EACxB,CAAC;CACH;CAEA,KAAa,GAAmB,GAAwB;EACtD,IAAMU,IAA8B;GAAE,MAAMpB,EAAkB;GAAiB;EAAQ;EAEvF,AAAIjB,EAAO,eAAeoC,EAAU,QAAMpC,EAAO,KAAK,KAAK,UAAUqC,CAAM,CAAC;CAC9E;CAEA,UAAkB,GAAkC;EAClD,OAAO,CAAC,CAACnC,EAAO,SAASA,EAAO,MAAM,MAAM,OAAQ,KAAK,IAAI;CAC/D;CAEA,UAAkB,GAA0C;EAC1D,OAAOK,EAAQ,SAASM,EAAqB,YAAY,CAAC,CAACN,EAAQ;CACrE;CAEA,WAAmB,GAAkC;EACnD,IAAMJ,IAAM,KAAK,IAAI;EASrB,OAPIA,IAAMD,EAAO,eAAed,MAC9B,EAAO,cAAce,GACrB,EAAO,WAAc,IAGvB,EAAO,YAEAD,EAAO,WAAWb;CAC3B;CAEA,QAAgB,GAAsB;EAGpC,OAFI,MAAM,QAAQiB,CAAG,IAAUA,EAAI,QAAQ,GAAK,MAAUgC,IAAMC,EAAM,QAAQ,CAAC,IAExEjC,aAAe,cAAcA,EAAI,aAAaA,EAAI;CAC3D;CAEA,OAAe,GAAyB,GAAyB,GAAiB,GAAsB;EACtG,AAAIJ,EAAO,UAEX,KAAK,KAAK,KAAK;GAAE,WAAW,KAAK,IAAI;GAAG,QAAQA,EAAO;GAAI,KAAKA,EAAO;GAAW;GAAe;GAAe;EAAK,CAAC,GAElH,KAAK,KAAK,SAAS,OAAK,KAAK,KAAK,MAAM;CAC9C;CAEA,kBAAgC;EAC9B,AAAI,KAAK,cACP,KAAK,cAAc,MAKrB,KAAK,WAAW,GAEhB,KAAK,cAAc,iBAAiB;GAGlC,AAFA,KAAK,cAAc,MAEd,KAAK,gBAEV,KAAK,cAAc,IAEnB,KAAK,gBAAgB;EACvB,GAAG,GAAG;CACR;CAEA,aAA2B;EACzB,IAAMsC,IAAW,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,QAAO,MAAUtC,EAAO,SAAS,IAAIuC,CAAqB,KAAKvC,EAAO,GAAG,eAAekC,EAAU,IAAI;EAEzI,IAAI,CAACI,EAAS,QAAQ;EAEtB,IAAME,IAAgB,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,QAAO,MAAU,CAACxC,EAAO,KAAK,GAChEyC,IAAgB,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC,QAAQ,CAACtC,OAASA,MAAQoC,KAAyB,CAACpC,EAAI,WAAW2B,CAAwB,CAAC,GAE5HY,IAA6B;GACjC,SAASF,EAAc,KAAI,OAAW;IAAE,IAAIxC,EAAO;IAAI,KAAKA,EAAO;IAAK,UAAU,CAAC,GAAGA,EAAO,QAAQ;GAAE,EAAE;GACzG,OAAS,OAAO,YAAYyC,EAAa,KAAK,CAACtC,GAAKD,OAAY,CAACC,GAAKD,EAAO,IAAI,CAAC,CAAC;GACnF,MAAS,KAAK;EAChB;EAEA,KAAK,MAAM,IAAIqC,GAAuB;GAAE,MAAMG;GAAQ,WAAW;EAAK,CAAC;EAEvE,IAAMrC,IAA+B;GACnC,MAASU,EAAkB;GAC3B,SAASwB;GACT,MAASG;EACX,GAEMjB,IAAU,KAAK,UAAUpB,CAAO;EAEtC,EAAS,SAAQ,MAAUL,EAAO,GAAG,KAAKyB,CAAO,CAAC;CACpD;CAEA,UAAiB,GAAyD;EAAE,KAAK,gBAAgB,KAAKkB,CAAO;CAAG;CAEhH,SAAgB,GAA0B;EAAE,OAAO,KAAK,UAAU9B,CAAO;CAAG;CAE5E,QAAqB;EAYnB,AAXA,cAAc,KAAK,iBAAiB,GAEhC,KAAK,eAAa,aAAa,KAAK,WAAW,GAEnD,KAAK,QAAQ,SAAS,MAAW;GAC/B,AAAIb,EAAO,GAAG,eAAekC,EAAU,QACrClC,EAAO,GAAG,MAAM,MAAM,mBAAmB;EAE7C,CAAC,GAED,KAAK,IAAI,MAAM,GACf,KAAK,WAAW,MAAM;CACxB;AACF"}