topsyde-utils 1.0.205 → 1.0.207

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.
Files changed (31) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.js +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/server/bun/websocket/Channel.d.ts +25 -3
  5. package/dist/server/bun/websocket/Channel.js +80 -26
  6. package/dist/server/bun/websocket/Channel.js.map +1 -1
  7. package/dist/server/bun/websocket/Client.d.ts +34 -1
  8. package/dist/server/bun/websocket/Client.js +95 -18
  9. package/dist/server/bun/websocket/Client.js.map +1 -1
  10. package/dist/server/bun/websocket/Message.d.ts +6 -10
  11. package/dist/server/bun/websocket/Message.js +31 -32
  12. package/dist/server/bun/websocket/Message.js.map +1 -1
  13. package/dist/server/bun/websocket/Websocket.d.ts +34 -4
  14. package/dist/server/bun/websocket/Websocket.js +70 -12
  15. package/dist/server/bun/websocket/Websocket.js.map +1 -1
  16. package/dist/server/bun/websocket/websocket.enums.d.ts +6 -0
  17. package/dist/server/bun/websocket/websocket.enums.js +7 -0
  18. package/dist/server/bun/websocket/websocket.enums.js.map +1 -1
  19. package/dist/server/bun/websocket/websocket.types.d.ts +60 -3
  20. package/dist/server/bun/websocket/websocket.types.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/__tests__/app.test.ts +1 -1
  23. package/src/__tests__/singleton.test.ts +6 -4
  24. package/src/index.ts +3 -1
  25. package/src/server/bun/websocket/Channel.ts +89 -36
  26. package/src/server/bun/websocket/Client.ts +109 -19
  27. package/src/server/bun/websocket/ISSUES.md +1175 -0
  28. package/src/server/bun/websocket/Message.ts +36 -49
  29. package/src/server/bun/websocket/Websocket.ts +71 -12
  30. package/src/server/bun/websocket/websocket.enums.ts +7 -0
  31. package/src/server/bun/websocket/websocket.types.ts +58 -3
@@ -1,24 +1,23 @@
1
1
  import Guards from "../../../utils/Guards";
2
- import {
3
- WebsocketStructuredMessage,
4
- WebsocketMessage,
5
- WebsocketMessageOptions,
6
- I_WebsocketChannel,
7
- I_WebsocketClient,
8
- WebsocketEntityData,
9
- } from "./websocket.types";
2
+ import { WebsocketStructuredMessage, WebsocketMessage, WebsocketMessageOptions, I_WebsocketClient, WebsocketEntityData } from "./websocket.types";
10
3
 
11
4
  export default class Message {
12
- private messageTemplate: WebsocketStructuredMessage<any>;
13
-
14
- constructor() {
15
- this.messageTemplate = { type: "", content: {}, channel: "", timestamp: "" };
16
- }
17
-
18
- public create(message: WebsocketMessage, options?: WebsocketMessageOptions): WebsocketStructuredMessage {
19
- // Clone the template (faster than creating new objects)
20
- const output = Object.assign({}, this.messageTemplate);
21
- // Set the dynamic properties in a single pass
5
+ // Shared template for all messages
6
+ private static readonly MESSAGE_TEMPLATE: WebsocketStructuredMessage<any> = {
7
+ type: "",
8
+ content: {},
9
+ channel: "",
10
+ timestamp: "",
11
+ };
12
+
13
+ // Private constructor to prevent instantiation
14
+ private constructor() {}
15
+
16
+ public static Create(message: WebsocketMessage, options?: WebsocketMessageOptions): WebsocketStructuredMessage {
17
+ // Clone the template
18
+ const output = Object.assign({}, Message.MESSAGE_TEMPLATE);
19
+
20
+ // Set the dynamic properties
22
21
  output.type = message.type;
23
22
  output.channel = message.channel || options?.channel || "N/A";
24
23
 
@@ -31,7 +30,7 @@ export default class Message {
31
30
  output.content = {};
32
31
  }
33
32
 
34
- // Process options in a single pass if provided
33
+ // Process options if provided
35
34
  if (options) {
36
35
  // Add data if provided
37
36
  if (options.data !== undefined) {
@@ -43,18 +42,20 @@ export default class Message {
43
42
  output.content.data = options.data;
44
43
  }
45
44
  }
45
+
46
46
  // Add client information if provided
47
47
  if (options.client && Guards.IsObject(options.client) && Guards.IsString(options.client.id, true)) {
48
48
  output.client = {
49
49
  id: options.client.id,
50
- name: options.client.name,
50
+ name: options.client.name || "Unknown",
51
51
  };
52
- } /* else {
53
- delete output.client;
54
- } */
52
+ }
55
53
 
56
54
  // Include channel metadata if requested
57
- if (options.includeMetadata !== false) output.metadata = options.metadata;
55
+ // Include channel metadata if provided as an object
56
+ if (options.metadata && Guards.IsObject(options.metadata) && !Guards.IsArray(options.metadata)) {
57
+ output.metadata = options.metadata;
58
+ }
58
59
 
59
60
  // Add timestamp if requested (default: true)
60
61
  if (options.includeTimestamp !== false) {
@@ -81,8 +82,7 @@ export default class Message {
81
82
 
82
83
  // Apply custom transformation if provided
83
84
  if (options.transform) {
84
- const transformed = options.transform(output);
85
- return transformed;
85
+ return options.transform(output);
86
86
  }
87
87
  } else {
88
88
  output.timestamp = new Date().toISOString();
@@ -91,19 +91,21 @@ export default class Message {
91
91
  return output;
92
92
  }
93
93
 
94
- public createWhisper(message: Omit<WebsocketMessage, "type">, options?: WebsocketMessageOptions) {
95
- return this.create({ ...message, content: message.content, channel: message.channel, type: "whisper" }, options);
94
+ public static CreateWhisper(message: Omit<WebsocketMessage, "type">, options?: WebsocketMessageOptions): WebsocketStructuredMessage {
95
+ return Message.Create({ ...message, content: message.content, channel: message.channel, type: "whisper" }, options);
96
+ }
97
+
98
+ public static Serialize<T = string>(message: WebsocketStructuredMessage, transform?: (message: WebsocketStructuredMessage) => T): string | T {
99
+ return transform ? transform(message) : JSON.stringify(message);
96
100
  }
97
101
 
98
- public send(target: I_WebsocketClient, message: WebsocketStructuredMessage): void;
99
- public send(target: I_WebsocketClient, message: WebsocketMessage, options?: WebsocketMessageOptions): void;
100
- public send(target: I_WebsocketClient, message: WebsocketStructuredMessage | WebsocketMessage, options?: WebsocketMessageOptions): void {
101
- target.send(this.create(message, options));
102
+ public static Send(target: I_WebsocketClient, message: WebsocketMessage, options?: WebsocketMessageOptions): void {
103
+ target.send(Message.Create(message, options));
102
104
  }
103
105
 
104
- public alert(target: I_WebsocketClient, reason: string, client?: WebsocketEntityData) {
106
+ public static Alert(target: I_WebsocketClient, reason: string, client?: WebsocketEntityData): void {
105
107
  target.send(
106
- this.create(
108
+ Message.Create(
107
109
  {
108
110
  content: {
109
111
  message: reason,
@@ -115,19 +117,4 @@ export default class Message {
115
117
  ),
116
118
  );
117
119
  }
118
-
119
- public serialize<T = string>(message: WebsocketStructuredMessage, transform?: (message: WebsocketStructuredMessage) => T) {
120
- return transform ? transform(message) : JSON.stringify(message);
121
- }
122
-
123
- public static Serialize<T = string>(message: WebsocketStructuredMessage, transform: (message: WebsocketStructuredMessage) => T): T;
124
- public static Serialize<T = string>(message: WebsocketStructuredMessage, transform?: (message: WebsocketStructuredMessage) => T): string | T;
125
- public static Serialize<T = string>(message: WebsocketStructuredMessage, transform?: (message: WebsocketStructuredMessage) => T): string | T {
126
- return transform ? transform(message) : JSON.stringify(message);
127
- }
128
-
129
- public static Create(message: WebsocketMessage, options?: WebsocketMessageOptions): WebsocketStructuredMessage{
130
- const msg = new Message();
131
- return msg.create(message, options);
132
- }
133
120
  }
@@ -28,6 +28,25 @@ export interface I_WebsocketConstructor {
28
28
  options?: WebsocketConstructorOptions;
29
29
  }
30
30
 
31
+ /**
32
+ * Websocket - Singleton managing clients, channels, and message routing
33
+ *
34
+ * ## API Design: Static vs Instance
35
+ * - **Static methods**: Use in application code (e.g., `Websocket.Broadcast()`, `Websocket.GetClient()`)
36
+ * - **Instance methods**: Use when extending the class (e.g., `protected createClient()`)
37
+ *
38
+ * Static methods are facades that call the singleton instance internally.
39
+ *
40
+ * @example
41
+ * // Application code - use static methods
42
+ * Websocket.Broadcast("lobby", { type: "chat", content: { message: "Hi!" } });
43
+ *
44
+ * // Extension - override instance methods
45
+ * MyWebsocket extends Websocket:
46
+ * protected createClient(entity) {
47
+ * return new MyCustomClient(entity);
48
+ * }
49
+ */
31
50
  export default class Websocket extends Singleton {
32
51
  protected _channels: WebsocketChannel;
33
52
  protected _clients: Map<string, I_WebsocketClient> = new Map();
@@ -111,7 +130,7 @@ export default class Websocket extends Singleton {
111
130
  if (this._ws_interface_handlers.message) return this._ws_interface_handlers.message(ws, message);
112
131
 
113
132
  ws.send("This is the message from the server: " + message);
114
- Websocket.BraodcastAll({ type: "client.message.received", content: { message } });
133
+ Websocket.BroadCastAll({ type: "client.message.received", content: { message } });
115
134
  };
116
135
 
117
136
  private clientConnected = (ws: ServerWebSocket<WebsocketEntityData>) => {
@@ -124,7 +143,14 @@ export default class Websocket extends Singleton {
124
143
  this._clients.set(client.id, client);
125
144
 
126
145
  client.send({ type: E_WebsocketMessageType.CLIENT_CONNECTED, content: { message: "Welcome to the server", client: client.whoami() } });
127
- global.addMember(client);
146
+
147
+ // Client handles its own joining logic with rollback support
148
+ if (!client.joinChannel(global)) {
149
+ Lib.Warn(`Failed to add client ${client.id} to global channel`);
150
+ }
151
+
152
+ // Mark as fully connected
153
+ client.markConnected();
128
154
 
129
155
  if (this._ws_interface_handlers.open) this._ws_interface_handlers.open(ws);
130
156
  };
@@ -132,15 +158,23 @@ export default class Websocket extends Singleton {
132
158
  private clientDisconnected = (ws: ServerWebSocket<WebsocketEntityData>, code: number, reason: string) => {
133
159
  if (this._options.debug) Lib.Log("Client disconnected", ws.data);
134
160
 
135
- if (this._ws_interface_handlers.close) this._ws_interface_handlers.close(ws, code, reason);
136
-
137
161
  const client = this._clients.get(ws.data.id);
138
162
  if (!client) return;
139
163
 
164
+ // Mark as disconnecting
165
+ client.markDisconnecting();
166
+
167
+ if (this._ws_interface_handlers.close) this._ws_interface_handlers.close(ws, code, reason);
168
+
169
+ // Remove from all channels
140
170
  this._channels.forEach((channel) => {
141
171
  channel.removeMember(client);
142
172
  });
143
173
 
174
+ // Mark as disconnected
175
+ client.markDisconnected();
176
+
177
+ // Remove from registry
144
178
  this._clients.delete(ws.data.id);
145
179
  };
146
180
 
@@ -196,7 +230,7 @@ export default class Websocket extends Singleton {
196
230
  * @param message - The message
197
231
  * @param args - The arguments
198
232
  */
199
- public static BraodcastAll(message: WebsocketStructuredMessage, ...args: any[]) {
233
+ public static BroadCastAll(message: WebsocketStructuredMessage, ...args: any[]) {
200
234
  const ws = this.GetInstance<Websocket>();
201
235
  ws._channels.forEach((channel) => channel.broadcast(message, ...args));
202
236
  }
@@ -297,14 +331,39 @@ export default class Websocket extends Singleton {
297
331
  }
298
332
 
299
333
  /**
300
- * Generate a message
301
- * @returns The generated message
334
+ * Get all connected clients (excluding connecting/disconnecting)
335
+ * @returns Array of connected clients
302
336
  */
303
- public static GenerateMessage(): WebsocketStructuredMessage {
304
- const msg: WebsocketStructuredMessage = {
305
- type: "",
306
- content: {},
337
+ public static GetConnectedClients(): I_WebsocketClient[] {
338
+ const ws = this.GetInstance<Websocket>();
339
+ return Array.from(ws._clients.values()).filter(
340
+ client => client.state === "connected"
341
+ );
342
+ }
343
+
344
+ /**
345
+ * Get client statistics by state
346
+ * @returns Object with counts by state
347
+ */
348
+ public static GetClientStats() {
349
+ const ws = this.GetInstance<Websocket>();
350
+ const stats = {
351
+ total: ws._clients.size,
352
+ connecting: 0,
353
+ connected: 0,
354
+ disconnecting: 0,
355
+ disconnected: 0,
307
356
  };
308
- return msg;
357
+
358
+ for (const client of ws._clients.values()) {
359
+ switch (client.state) {
360
+ case "connecting": stats.connecting++; break;
361
+ case "connected": stats.connected++; break;
362
+ case "disconnecting": stats.disconnecting++; break;
363
+ case "disconnected": stats.disconnected++; break;
364
+ }
365
+ }
366
+
367
+ return stats;
309
368
  }
310
369
  }
@@ -20,3 +20,10 @@ export enum E_WebsocketMessagePriority {
20
20
  MEDIUM = 1,
21
21
  HIGH = 2,
22
22
  }
23
+
24
+ export enum E_ClientState {
25
+ CONNECTING = "connecting",
26
+ CONNECTED = "connected",
27
+ DISCONNECTING = "disconnecting",
28
+ DISCONNECTED = "disconnected",
29
+ }
@@ -1,6 +1,7 @@
1
1
  import { ServerWebSocket, WebSocketHandler } from "bun";
2
2
  import Channel from "./Channel";
3
3
  import Websocket from "./Websocket";
4
+ import { E_ClientState } from "./websocket.enums";
4
5
 
5
6
  export type BunWebsocketMessage = string | Buffer<ArrayBufferLike>;
6
7
 
@@ -102,7 +103,45 @@ export type WebsocketMessage<T extends Record<string, any> = Record<string, any>
102
103
  [key: string]: any;
103
104
  };
104
105
 
105
- export type WebsocketStructuredMessage<T extends Record<string, any> = Record<string, any>> = WebsocketMessage<T> & WebsocketMessageOptions;
106
+ /**
107
+ * Message structure sent over the wire to clients.
108
+ * This is the actual WebSocket payload format - transport options are NOT included.
109
+ */
110
+ export type WebsocketStructuredMessage<T extends Record<string, any> = Record<string, any>> = {
111
+ /** Message type identifier for client-side routing */
112
+ type: string;
113
+
114
+ /** Message payload */
115
+ content: T;
116
+
117
+ /** Channel ID where message originated */
118
+ channel?: string;
119
+
120
+ /** ISO timestamp when message was created */
121
+ timestamp?: string;
122
+
123
+ /** Client information (who sent this) */
124
+ client?: WebsocketEntityData;
125
+
126
+ /** Channel metadata (if included) */
127
+ metadata?: Record<string, string>;
128
+
129
+ /** Message priority for client-side processing */
130
+ priority?: number;
131
+
132
+ /** Expiration timestamp (milliseconds since epoch) */
133
+ expiresAt?: number;
134
+
135
+ /** Any additional custom fields */
136
+ [key: string]: any;
137
+ };
138
+
139
+ /**
140
+ * @deprecated This type incorrectly mixed transport options with wire format.
141
+ * Use WebsocketStructuredMessage for wire format and WebsocketMessageOptions for options.
142
+ * This will be removed in a future version.
143
+ */
144
+ export type deprecated_WebsocketStructuredMessage<T extends Record<string, any> = Record<string, any>> = WebsocketMessage<T> & WebsocketMessageOptions;
106
145
 
107
146
  export type WebsocketEntityId = string;
108
147
  export type WebsocketEntityName = string;
@@ -114,15 +153,21 @@ export interface I_WebsocketEntity extends WebsocketEntityData {
114
153
 
115
154
  export interface I_WebsocketClient extends I_WebsocketEntity {
116
155
  channels: WebsocketChannel<I_WebsocketChannel>;
156
+ state: E_ClientState;
117
157
  send(message: string, options?: WebsocketMessageOptions): void;
118
158
  send(message: WebsocketStructuredMessage): void;
119
159
  subscribe(channel: string): any;
120
- joinChannel(channel: I_WebsocketChannel, send?: boolean): void;
160
+ joinChannel(channel: I_WebsocketChannel, send?: boolean): boolean;
121
161
  leaveChannel(channel: I_WebsocketChannel, send?: boolean): void;
122
162
  joinChannels(channels: I_WebsocketChannel[], send?: boolean): void;
123
163
  leaveChannels(channels?: I_WebsocketChannel[], send?: boolean): void;
124
164
  unsubscribe(channel: string): any;
125
165
  whoami(): WebsocketEntityData;
166
+ canReceiveMessages(): boolean;
167
+ markConnected(): void;
168
+ markDisconnecting(): void;
169
+ markDisconnected(): void;
170
+ getConnectionInfo(): { id: string; name: string; state: E_ClientState; connectedAt?: Date; disconnectedAt?: Date; uptime: number; channelCount: number };
126
171
  }
127
172
 
128
173
  export interface I_WebsocketChannelEntity<T extends Websocket = Websocket> extends WebsocketEntityData {
@@ -133,6 +178,15 @@ export interface I_WebsocketChannelEntity<T extends Websocket = Websocket> exten
133
178
  export type BroadcastOptions = WebsocketMessageOptions & {
134
179
  debug?: boolean;
135
180
  };
181
+
182
+ // Result type for addMember operations
183
+ export type AddMemberResult = { success: true; client: I_WebsocketClient } | { success: false; reason: "full" | "already_member" | "error"; error?: Error };
184
+
185
+ // Options for addMember operations
186
+ export type AddMemberOptions = {
187
+ /** Whether to notify client when channel is full (default: false) */
188
+ notify_when_full?: boolean;
189
+ };
136
190
  export interface I_WebsocketChannel<T extends Websocket = Websocket> extends I_WebsocketChannelEntity<T> {
137
191
  limit: number;
138
192
  members: Map<string, I_WebsocketClient>;
@@ -140,7 +194,8 @@ export interface I_WebsocketChannel<T extends Websocket = Websocket> extends I_W
140
194
  createdAt: Date;
141
195
  broadcast(message: WebsocketStructuredMessage | string, options?: BroadcastOptions): void;
142
196
  hasMember(client: I_WebsocketEntity | string): boolean;
143
- addMember(entity: I_WebsocketClient): I_WebsocketClient | false;
197
+ addMember(entity: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult;
198
+ removeMemberInternal(entity: I_WebsocketClient): void;
144
199
  removeMember(entity: I_WebsocketEntity): I_WebsocketClient | false;
145
200
  getMember(client: I_WebsocketEntity | string): I_WebsocketClient | undefined;
146
201
  getMembers(clients?: string[] | I_WebsocketEntity[]): I_WebsocketClient[];