@digo-labs/ws 0.1.10 → 0.2.1

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).
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
@@ -7,21 +7,35 @@ export declare class WebsocketClient {
7
7
  private reconnectInterval;
8
8
  private maxReconnectInterval;
9
9
  private reconnectDelay;
10
+ private getToken;
10
11
  private socket;
11
12
  private alive;
13
+ private isIdentified;
14
+ private refreshTimer;
12
15
  private listeners;
13
16
  private subscribedChannels;
17
+ private deniedChannels;
18
+ private pending;
14
19
  private openHandlers;
20
+ private readyHandlers;
15
21
  private closeHandlers;
16
22
  private errorHandlers;
17
23
  constructor(properties: WebsocketConnectionConfig);
18
24
  private open;
25
+ private identify;
26
+ private handleIdentified;
27
+ private stopRefresh;
28
+ private scheduleReconnect;
29
+ private sendRaw;
19
30
  private sendToHub;
20
31
  send(channel: string, data?: unknown): void;
21
32
  setState(channel: string, data?: unknown): void;
22
33
  subscribe(channel: string, listener: WebsocketListener): void;
23
34
  unsubscribe(channel: string, listener?: WebsocketListener): void;
35
+ isLive(channel: string): boolean;
24
36
  onOpen(handler: () => void): void;
37
+ onReady(handler: () => void): void;
38
+ offReady(handler: () => void): void;
25
39
  onClose(handler: () => void): void;
26
40
  onError(handler: (error: Event) => void): void;
27
41
  close(): void;
package/dist/client.js CHANGED
@@ -1,6 +1,6 @@
1
- import { WS_URL as e, WebsocketClientEvent as t } from "@digo-labs/common";
1
+ import { WS_URL as e, WebSocketHubEvent as t, WebsocketClientEvent as n } from "@digo-labs/common";
2
2
  //#region src/client.ts
3
- var n = class {
3
+ var r = 4e3, i = 4401, a = 6e5, o = class {
4
4
  url;
5
5
  app;
6
6
  id;
@@ -8,80 +8,134 @@ var n = class {
8
8
  reconnectInterval;
9
9
  maxReconnectInterval;
10
10
  reconnectDelay;
11
+ getToken;
11
12
  socket = null;
12
13
  alive = !0;
14
+ isIdentified = !1;
15
+ refreshTimer = null;
13
16
  listeners = /* @__PURE__ */ new Map();
14
17
  subscribedChannels = /* @__PURE__ */ new Set();
18
+ deniedChannels = /* @__PURE__ */ new Set();
19
+ pending = [];
15
20
  openHandlers = [];
21
+ readyHandlers = [];
16
22
  closeHandlers = [];
17
23
  errorHandlers = [];
18
24
  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();
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();
20
26
  }
21
27
  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());
28
+ this.socket = new WebSocket(this.url), this.isIdentified = !1, this.socket.onopen = () => {
29
+ this.reconnectDelay = this.reconnectInterval, this.identify(), this.openHandlers.forEach((e) => e());
31
30
  }, this.socket.onmessage = (e) => {
32
- let t;
31
+ let n;
33
32
  try {
34
- t = JSON.parse(e.data);
33
+ n = JSON.parse(e.data);
35
34
  } catch {
36
35
  return;
37
36
  }
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);
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;
45
44
  }
45
+ n.channel && this.listeners.get(n.channel)?.forEach((e) => e(n.data));
46
+ }, 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);
46
48
  }, this.socket.onerror = (e) => {
47
49
  this.errorHandlers.forEach((t) => t(e));
48
50
  };
49
51
  }
50
- sendToHub(e) {
52
+ async identify() {
53
+ if (!this.getToken) {
54
+ this.sendRaw({
55
+ type: n.IDENTIFY,
56
+ id: this.id,
57
+ app: this.app
58
+ }), this.handleIdentified();
59
+ return;
60
+ }
61
+ let e = await this.getToken().catch(() => null);
62
+ if (!e) {
63
+ this.socket?.close();
64
+ return;
65
+ }
66
+ this.sendRaw({
67
+ type: n.IDENTIFY,
68
+ id: this.id,
69
+ app: this.app,
70
+ token: e
71
+ });
72
+ }
73
+ handleIdentified() {
74
+ this.isIdentified || (this.isIdentified = !0, this.deniedChannels.clear(), this.subscribedChannels.size > 0 && this.sendRaw({
75
+ type: n.SUBSCRIBE,
76
+ 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()));
78
+ }
79
+ stopRefresh() {
80
+ this.refreshTimer && clearInterval(this.refreshTimer), this.refreshTimer = null;
81
+ }
82
+ scheduleReconnect(e) {
83
+ let t = (e ? this.maxReconnectInterval : this.reconnectDelay) * (.5 + Math.random());
84
+ setTimeout(() => {
85
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectInterval), this.open();
86
+ }, t);
87
+ }
88
+ sendRaw(e) {
51
89
  this.socket?.readyState === WebSocket.OPEN && this.socket.send(JSON.stringify(e));
52
90
  }
53
- send(e, n) {
91
+ 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);
97
+ }
98
+ send(e, t) {
54
99
  this.sendToHub({
55
- type: t.MESSAGE,
100
+ type: n.MESSAGE,
56
101
  channel: e,
57
- data: n
102
+ data: t
58
103
  });
59
104
  }
60
- setState(e, n) {
105
+ setState(e, t) {
61
106
  this.sendToHub({
62
- type: t.STATE,
107
+ type: n.STATE,
63
108
  channel: e,
64
- data: n
109
+ data: t
65
110
  });
66
111
  }
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,
112
+ subscribe(e, t) {
113
+ 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,
70
115
  channels: [e]
71
116
  }));
72
117
  }
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,
118
+ unsubscribe(e, t) {
119
+ 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,
76
121
  channels: [e]
77
- }))) : (this.listeners.delete(e), this.subscribedChannels.delete(e), this.sendToHub({
78
- type: t.UNSUBSCRIBE,
122
+ }))) : (this.listeners.delete(e), this.subscribedChannels.delete(e), this.deniedChannels.delete(e), this.sendToHub({
123
+ type: n.UNSUBSCRIBE,
79
124
  channels: [e]
80
125
  }));
81
126
  }
127
+ isLive(e) {
128
+ return this.isIdentified && this.subscribedChannels.has(e) && !this.deniedChannels.has(e);
129
+ }
82
130
  onOpen(e) {
83
131
  this.openHandlers.push(e);
84
132
  }
133
+ onReady(e) {
134
+ this.readyHandlers.push(e);
135
+ }
136
+ offReady(e) {
137
+ this.readyHandlers = this.readyHandlers.filter((t) => t !== e);
138
+ }
85
139
  onClose(e) {
86
140
  this.closeHandlers.push(e);
87
141
  }
@@ -89,10 +143,10 @@ var n = class {
89
143
  this.errorHandlers.push(e);
90
144
  }
91
145
  close() {
92
- this.alive = !1, this.socket?.close();
146
+ this.alive = !1, this.isIdentified = !1, this.stopRefresh(), this.socket?.close();
93
147
  }
94
148
  };
95
149
  //#endregion
96
- export { n as WebsocketClient };
150
+ export { o as WebsocketClient };
97
151
 
98
152
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
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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"}
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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"}
package/dist/server.d.ts CHANGED
@@ -2,6 +2,7 @@ import { WebsocketsHubConfig } from '@digo-labs/common';
2
2
  export declare class WebsocketHub {
3
3
  private httpServer;
4
4
  private wss;
5
+ private config;
5
6
  private clients;
6
7
  private state;
7
8
  private logs;
@@ -12,9 +13,21 @@ export declare class WebsocketHub {
12
13
  constructor(config: WebsocketsHubConfig);
13
14
  private heartbeat;
14
15
  private handleConnection;
16
+ private parseMessage;
15
17
  private handleMessage;
16
- private addLog;
18
+ private identify;
19
+ private identifyWithToken;
20
+ private canSubscribe;
21
+ private canPublish;
22
+ private channelKey;
23
+ private namespacedKey;
24
+ private storeState;
25
+ private readState;
17
26
  private broadcast;
27
+ private deny;
28
+ private isOverRate;
29
+ private rawSize;
30
+ private addLog;
18
31
  private broadcastStatus;
19
32
  private sendStatus;
20
33
  onMessage(handler: (channel: string, data: unknown) => void): void;