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
package/dist/index.d.ts CHANGED
@@ -57,6 +57,6 @@ export { RxjsDataType, NamespaceActions, MultiNamespaceActions } from "./client/
57
57
  export { ControllerResponse, ControllerAction, ControllerMap, ControllerOptions } from "./server/controller";
58
58
  export { Routes } from "./server/bun/router/routes";
59
59
  export { WebsocketConstructorOptions, I_WebsocketConstructor } from "./server/bun/websocket/Websocket";
60
- export { E_WebsocketMessageType, E_WebsocketMessagePriority } from "./server/bun/websocket/websocket.enums";
61
- export { BunWebsocketMessage, WebsocketChannel, WebsocketClients, WebsocketMessageOptions, WebsocketMessage, WebsocketStructuredMessage, WebsocketEntityId, WebsocketEntityName, WebsocketEntityData, I_WebsocketEntity, I_WebsocketClient, I_WebsocketChannelEntity, BroadcastOptions, I_WebsocketChannel, WebsocketInterfaceHandlers, I_WebsocketInterface, } from "./server/bun/websocket/websocket.types";
60
+ export { E_WebsocketMessageType, E_WebsocketMessagePriority, E_ClientState } from "./server/bun/websocket/websocket.enums";
61
+ export { BunWebsocketMessage, WebsocketChannel, WebsocketClients, WebsocketMessageOptions, WebsocketMessage, WebsocketStructuredMessage, WebsocketEntityId, WebsocketEntityName, WebsocketEntityData, I_WebsocketEntity, I_WebsocketClient, I_WebsocketChannelEntity, BroadcastOptions, AddMemberResult, AddMemberOptions, I_WebsocketChannel, WebsocketInterfaceHandlers, I_WebsocketInterface, } from "./server/bun/websocket/websocket.types";
62
62
  export { E_SUBJET_TYPE, I_RxjsPayload, RxjsNamespaces, AsyncSubject, BehaviorSubject, ReplaySubject, Subject, Subscription } from "./utils/Rxjs";
package/dist/index.js CHANGED
@@ -56,6 +56,6 @@ export { ERROR_CODE, HTTP_ERROR_CODE, WS_ERROR_CODE } from "./errors.js";
56
56
  export { RESPONSE_INIT, HEADERS_INIT, RESPONSE_METHOD_OPTIONS } from "./application.js";
57
57
  export { DEFAULT_FALSE_RESPONSE, LOG_COLORS, LOG_ICONS } from "./consts.js";
58
58
  export { E_IS, E_ENVIRONMENTS } from "./enums.js";
59
- export { E_WebsocketMessageType, E_WebsocketMessagePriority } from "./server/bun/websocket/websocket.enums.js";
59
+ export { E_WebsocketMessageType, E_WebsocketMessagePriority, E_ClientState } from "./server/bun/websocket/websocket.enums.js";
60
60
  export { E_SUBJET_TYPE, AsyncSubject, BehaviorSubject, ReplaySubject, Subject, Subscription } from "./utils/Rxjs.js";
61
61
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["// This file is auto-generated by scripts/generate-indexes.ts\n// Do not edit this file directly\n\n// Export all modules\nexport * from \"./errors\";\nexport * from \"./singleton\";\nexport * from \"./initializable\";\nexport * from \"./application\";\nexport * from \"./consts\";\nexport * from \"./types\";\nexport * from \"./throwable\";\nexport * from \"./enums\";\nexport * from \"./client/rxjs/useRxjs\";\nexport * from \"./client/vite/plugins/topsydeUtilsVitePlugin\";\nexport * from \"./server/controller\";\nexport * from \"./server/service\";\nexport * from \"./server/base/base.database\";\nexport * from \"./server/bun/router/controller-discovery\";\nexport * from \"./server/bun/router/routes\";\nexport * from \"./server/bun/router/router\";\nexport * from \"./server/bun/router/router.internal\";\nexport * from \"./server/bun/websocket/Websocket\";\nexport * from \"./server/bun/websocket/websocket.guards\";\nexport * from \"./server/bun/websocket/Message\";\nexport * from \"./server/bun/websocket/Channel\";\nexport * from \"./server/bun/websocket/Client\";\nexport * from \"./server/bun/websocket/websocket.enums\";\nexport * from \"./server/bun/websocket/websocket.types\";\nexport * from \"./utils/Rxjs\";\nexport * from \"./utils/Lib\";\nexport * from \"./utils/Guards\";\nexport * from \"./utils/BaseEntity\";\nexport * from \"./utils/Console\";\nexport * from \"./utils/BaseDto\";\nexport * from \"./utils/dto_validators/IsNumberOrRangeConstraint\";\n\n// Export default classes\nexport { default as Singleton } from \"./singleton\";\nexport { default as Initializable } from \"./initializable\";\nexport { default as Application } from \"./application\";\nexport { default as Throwable } from \"./throwable\";\nexport { default as TopsydeUtilsVitePlugin } from \"./client/vite/plugins/topsydeUtilsVitePlugin\";\nexport { default as Controller } from \"./server/controller\";\nexport { default as Service } from \"./server/service\";\nexport { default as Database } from \"./server/base/base.database\";\nexport { default as Router } from \"./server/bun/router/router\";\nexport { default as Router_Internal } from \"./server/bun/router/router.internal\";\nexport { default as Websocket } from \"./server/bun/websocket/Websocket\";\nexport { default as Message } from \"./server/bun/websocket/Message\";\nexport { default as Channel } from \"./server/bun/websocket/Channel\";\nexport { default as Client } from \"./server/bun/websocket/Client\";\nexport { default as Lib } from \"./utils/Lib\";\nexport { default as Guards } from \"./utils/Guards\";\nexport { default as BaseEntity } from \"./utils/BaseEntity\";\nexport { default as Console } from \"./utils/Console\";\n\n// Re-export specific items for backward compatibility\nexport { ERROR_CODE, HTTP_ERROR_CODE, WS_ERROR_CODE } from \"./errors\";\nexport { InitializableOptions, InitializableEvent } from \"./initializable\";\nexport { RESPONSE_INIT, HEADERS_INIT, RESPONSE_METHOD_OPTIONS } from \"./application\";\nexport { DEFAULT_FALSE_RESPONSE, LOG_COLORS, LOG_ICONS } from \"./consts\";\nexport { ClassConstructor, NonNullableType, ObjectKeys, KVObj, I_ApplicationResponse } from \"./types\";\nexport { E_IS, E_ENVIRONMENTS } from \"./enums\";\nexport { RxjsDataType, NamespaceActions, MultiNamespaceActions } from \"./client/rxjs/useRxjs\";\nexport { ControllerResponse, ControllerAction, ControllerMap, ControllerOptions } from \"./server/controller\";\nexport { Routes } from \"./server/bun/router/routes\";\nexport { WebsocketConstructorOptions, I_WebsocketConstructor } from \"./server/bun/websocket/Websocket\";\nexport { E_WebsocketMessageType, E_WebsocketMessagePriority } from \"./server/bun/websocket/websocket.enums\";\nexport {\n\tBunWebsocketMessage,\n\tWebsocketChannel,\n\tWebsocketClients,\n\tWebsocketMessageOptions,\n\tWebsocketMessage,\n\tWebsocketStructuredMessage,\n\tWebsocketEntityId,\n\tWebsocketEntityName,\n\tWebsocketEntityData,\n\tI_WebsocketEntity,\n\tI_WebsocketClient,\n\tI_WebsocketChannelEntity,\n\tBroadcastOptions,\n\tI_WebsocketChannel,\n\tWebsocketInterfaceHandlers,\n\tI_WebsocketInterface,\n} from \"./server/bun/websocket/websocket.types\";\nexport { E_SUBJET_TYPE, I_RxjsPayload, RxjsNamespaces, AsyncSubject, BehaviorSubject, ReplaySubject, Subject, Subscription } from \"./utils/Rxjs\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["// This file is auto-generated by scripts/generate-indexes.ts\n// Do not edit this file directly\n\n// Export all modules\nexport * from \"./errors\";\nexport * from \"./singleton\";\nexport * from \"./initializable\";\nexport * from \"./application\";\nexport * from \"./consts\";\nexport * from \"./types\";\nexport * from \"./throwable\";\nexport * from \"./enums\";\nexport * from \"./client/rxjs/useRxjs\";\nexport * from \"./client/vite/plugins/topsydeUtilsVitePlugin\";\nexport * from \"./server/controller\";\nexport * from \"./server/service\";\nexport * from \"./server/base/base.database\";\nexport * from \"./server/bun/router/controller-discovery\";\nexport * from \"./server/bun/router/routes\";\nexport * from \"./server/bun/router/router\";\nexport * from \"./server/bun/router/router.internal\";\nexport * from \"./server/bun/websocket/Websocket\";\nexport * from \"./server/bun/websocket/websocket.guards\";\nexport * from \"./server/bun/websocket/Message\";\nexport * from \"./server/bun/websocket/Channel\";\nexport * from \"./server/bun/websocket/Client\";\nexport * from \"./server/bun/websocket/websocket.enums\";\nexport * from \"./server/bun/websocket/websocket.types\";\nexport * from \"./utils/Rxjs\";\nexport * from \"./utils/Lib\";\nexport * from \"./utils/Guards\";\nexport * from \"./utils/BaseEntity\";\nexport * from \"./utils/Console\";\nexport * from \"./utils/BaseDto\";\nexport * from \"./utils/dto_validators/IsNumberOrRangeConstraint\";\n\n// Export default classes\nexport { default as Singleton } from \"./singleton\";\nexport { default as Initializable } from \"./initializable\";\nexport { default as Application } from \"./application\";\nexport { default as Throwable } from \"./throwable\";\nexport { default as TopsydeUtilsVitePlugin } from \"./client/vite/plugins/topsydeUtilsVitePlugin\";\nexport { default as Controller } from \"./server/controller\";\nexport { default as Service } from \"./server/service\";\nexport { default as Database } from \"./server/base/base.database\";\nexport { default as Router } from \"./server/bun/router/router\";\nexport { default as Router_Internal } from \"./server/bun/router/router.internal\";\nexport { default as Websocket } from \"./server/bun/websocket/Websocket\";\nexport { default as Message } from \"./server/bun/websocket/Message\";\nexport { default as Channel } from \"./server/bun/websocket/Channel\";\nexport { default as Client } from \"./server/bun/websocket/Client\";\nexport { default as Lib } from \"./utils/Lib\";\nexport { default as Guards } from \"./utils/Guards\";\nexport { default as BaseEntity } from \"./utils/BaseEntity\";\nexport { default as Console } from \"./utils/Console\";\n\n// Re-export specific items for backward compatibility\nexport { ERROR_CODE, HTTP_ERROR_CODE, WS_ERROR_CODE } from \"./errors\";\nexport { InitializableOptions, InitializableEvent } from \"./initializable\";\nexport { RESPONSE_INIT, HEADERS_INIT, RESPONSE_METHOD_OPTIONS } from \"./application\";\nexport { DEFAULT_FALSE_RESPONSE, LOG_COLORS, LOG_ICONS } from \"./consts\";\nexport { ClassConstructor, NonNullableType, ObjectKeys, KVObj, I_ApplicationResponse } from \"./types\";\nexport { E_IS, E_ENVIRONMENTS } from \"./enums\";\nexport { RxjsDataType, NamespaceActions, MultiNamespaceActions } from \"./client/rxjs/useRxjs\";\nexport { ControllerResponse, ControllerAction, ControllerMap, ControllerOptions } from \"./server/controller\";\nexport { Routes } from \"./server/bun/router/routes\";\nexport { WebsocketConstructorOptions, I_WebsocketConstructor } from \"./server/bun/websocket/Websocket\";\nexport { E_WebsocketMessageType, E_WebsocketMessagePriority, E_ClientState } from \"./server/bun/websocket/websocket.enums\";\nexport {\n\tBunWebsocketMessage,\n\tWebsocketChannel,\n\tWebsocketClients,\n\tWebsocketMessageOptions,\n\tWebsocketMessage,\n\tWebsocketStructuredMessage,\n\tWebsocketEntityId,\n\tWebsocketEntityName,\n\tWebsocketEntityData,\n\tI_WebsocketEntity,\n\tI_WebsocketClient,\n\tI_WebsocketChannelEntity,\n\tBroadcastOptions,\n\tAddMemberResult,\n\tAddMemberOptions,\n\tI_WebsocketChannel,\n\tWebsocketInterfaceHandlers,\n\tI_WebsocketInterface,\n} from \"./server/bun/websocket/websocket.types\";\nexport { E_SUBJET_TYPE, I_RxjsPayload, RxjsNamespaces, AsyncSubject, BehaviorSubject, ReplaySubject, Subject, Subscription } from \"./utils/Rxjs\";\n"]}
@@ -1,5 +1,21 @@
1
1
  import Websocket from "./Websocket";
2
- import type { BroadcastOptions, I_WebsocketChannel, I_WebsocketClient, I_WebsocketEntity, WebsocketChannel, WebsocketMessage } from "./websocket.types";
2
+ import type { BroadcastOptions, I_WebsocketChannel, I_WebsocketClient, I_WebsocketEntity, WebsocketChannel, WebsocketMessage, AddMemberResult, AddMemberOptions } from "./websocket.types";
3
+ /**
4
+ * Channel - Pub/sub topic for WebSocket clients
5
+ *
6
+ * ## Membership Contract
7
+ * - `addMember()` validates capacity and adds to `members` map
8
+ * - Client drives join via `joinChannel()` which subscribes and handles rollback
9
+ * - If subscription fails, membership is automatically rolled back
10
+ * - Member count never exceeds `limit`
11
+ *
12
+ * @example
13
+ * const channel = new Channel("game-1", "Game Room", ws, 10);
14
+ * const result = channel.addMember(client);
15
+ * if (result.success) {
16
+ * channel.broadcast({ type: "player.joined", content: { player: client.whoami() } });
17
+ * }
18
+ */
3
19
  export default class Channel<T extends Websocket = Websocket> implements I_WebsocketChannel<T> {
4
20
  createdAt: Date;
5
21
  id: string;
@@ -8,12 +24,18 @@ export default class Channel<T extends Websocket = Websocket> implements I_Webso
8
24
  members: Map<string, I_WebsocketClient>;
9
25
  metadata: Record<string, string>;
10
26
  ws: T;
11
- private message;
12
27
  constructor(id: string, name: string, ws: T, limit?: number, members?: Map<string, I_WebsocketClient>, metadata?: Record<string, string>);
13
28
  broadcast(message: WebsocketMessage | string, options?: BroadcastOptions): void;
14
29
  private getFilteredMetadata;
15
30
  hasMember(client: I_WebsocketEntity | string): boolean;
16
- addMember(client: I_WebsocketClient): false | I_WebsocketClient;
31
+ addMember(client: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult;
32
+ private notifyChannelFull;
33
+ /**
34
+ * Internal method to remove a member without triggering client-side cleanup.
35
+ * Used for rollback operations when joinChannel fails.
36
+ * @internal
37
+ */
38
+ removeMemberInternal(client: I_WebsocketClient): void;
17
39
  removeMember(entity: I_WebsocketEntity): false | I_WebsocketClient;
18
40
  getMember(client: I_WebsocketEntity | string): I_WebsocketClient | undefined;
19
41
  getMembers(clients?: string[] | I_WebsocketEntity[]): I_WebsocketClient[];
@@ -1,5 +1,22 @@
1
1
  import { Guards, Lib } from "../../../utils/index.js";
2
2
  import Message from "./Message.js";
3
+ import { E_WebsocketMessageType } from "./websocket.enums.js";
4
+ /**
5
+ * Channel - Pub/sub topic for WebSocket clients
6
+ *
7
+ * ## Membership Contract
8
+ * - `addMember()` validates capacity and adds to `members` map
9
+ * - Client drives join via `joinChannel()` which subscribes and handles rollback
10
+ * - If subscription fails, membership is automatically rolled back
11
+ * - Member count never exceeds `limit`
12
+ *
13
+ * @example
14
+ * const channel = new Channel("game-1", "Game Room", ws, 10);
15
+ * const result = channel.addMember(client);
16
+ * if (result.success) {
17
+ * channel.broadcast({ type: "player.joined", content: { player: client.whoami() } });
18
+ * }
19
+ */
3
20
  export default class Channel {
4
21
  constructor(id, name, ws, limit, members, metadata) {
5
22
  this.createdAt = new Date();
@@ -9,7 +26,6 @@ export default class Channel {
9
26
  this.members = members ?? new Map();
10
27
  this.metadata = metadata ?? {};
11
28
  this.ws = ws;
12
- this.message = new Message();
13
29
  }
14
30
  broadcast(message, options) {
15
31
  if (Guards.IsString(message)) {
@@ -19,29 +35,29 @@ export default class Channel {
19
35
  };
20
36
  message = msg;
21
37
  }
22
- const output = this.message.create(message, { ...options, channel: this.name });
23
- if (options) {
24
- // Include channel metadata if requested
25
- if (options.includeMetadata) {
26
- output.metadata = options.includeMetadata === true ? this.getMetadata() : this.getFilteredMetadata(options.includeMetadata);
27
- }
28
- // Handle excluded clients if needed
29
- if (options.excludeClients && options.excludeClients.length > 0) {
30
- // For large channels with many excluded clients, it might be more efficient
31
- // to send directly to each client instead of using channel publish
32
- if (this.members.size > 10 && options.excludeClients.length > this.members.size / 3) {
33
- const serializedMessage = this.message.serialize(output);
34
- for (const [clientId, client] of this.members) {
35
- if (!options.excludeClients.includes(clientId)) {
36
- client.ws.send(serializedMessage);
37
- }
38
+ const output = Message.Create(message, { ...options, channel: this.id });
39
+ // Include channel metadata if requested
40
+ if (options?.includeMetadata) {
41
+ output.metadata = options.includeMetadata === true ? this.getMetadata() : this.getFilteredMetadata(options.includeMetadata);
42
+ }
43
+ const serializedMessage = Message.Serialize(output);
44
+ // If we need to exclude clients, send individually to prevent excluded clients from receiving
45
+ if (options?.excludeClients && options.excludeClients.length > 0) {
46
+ const excludeSet = new Set(options.excludeClients); // O(1) lookup
47
+ for (const [clientId, client] of this.members) {
48
+ if (!excludeSet.has(clientId)) {
49
+ try {
50
+ client.ws.send(serializedMessage);
51
+ }
52
+ catch (error) {
53
+ Lib.Warn(`Failed to send to client ${clientId}:`, error);
38
54
  }
39
- return;
40
55
  }
41
56
  }
57
+ return;
42
58
  }
43
- // Publish to the channel
44
- this.ws.server.publish(this.id, this.message.serialize(output));
59
+ // Otherwise use pub/sub for everyone
60
+ this.ws.server.publish(this.id, serializedMessage);
45
61
  }
46
62
  // Helper method for filtered metadata
47
63
  getFilteredMetadata(keys) {
@@ -59,12 +75,50 @@ export default class Channel {
59
75
  return this.members.has(client);
60
76
  return this.members.has(client.id);
61
77
  }
62
- addMember(client) {
63
- if (!this.canAddMember())
64
- return false;
65
- this.members.set(client.id, client);
66
- client.joinChannel(this);
67
- return client;
78
+ addMember(client, options) {
79
+ // Check if already a member
80
+ if (this.members.has(client.id)) {
81
+ return { success: false, reason: 'already_member' };
82
+ }
83
+ // Check capacity (atomic check)
84
+ if (this.members.size >= this.limit) {
85
+ // Optionally notify client why they can't join
86
+ if (options?.notify_when_full) {
87
+ this.notifyChannelFull(client);
88
+ }
89
+ return { success: false, reason: 'full' };
90
+ }
91
+ try {
92
+ this.members.set(client.id, client);
93
+ return { success: true, client };
94
+ }
95
+ catch (error) {
96
+ // Rollback
97
+ this.members.delete(client.id);
98
+ return {
99
+ success: false,
100
+ reason: 'error',
101
+ error: error instanceof Error ? error : new Error(String(error))
102
+ };
103
+ }
104
+ }
105
+ notifyChannelFull(client) {
106
+ client.send({
107
+ type: E_WebsocketMessageType.ERROR,
108
+ content: {
109
+ message: `Channel "${this.name}" is full (${this.limit} members)`,
110
+ code: 'CHANNEL_FULL',
111
+ channel: this.id
112
+ }
113
+ });
114
+ }
115
+ /**
116
+ * Internal method to remove a member without triggering client-side cleanup.
117
+ * Used for rollback operations when joinChannel fails.
118
+ * @internal
119
+ */
120
+ removeMemberInternal(client) {
121
+ this.members.delete(client.id);
68
122
  }
69
123
  removeMember(entity) {
70
124
  if (!this.members.has(entity.id))
@@ -1 +1 @@
1
- {"version":3,"file":"Channel.js","sourceRoot":"","sources":["../../../../src/server/bun/websocket/Channel.ts"],"names":[],"mappings":"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","sourcesContent":["import { Guards, Lib } from \"../../../utils\";\nimport Message from \"./Message\";\nimport Websocket from \"./Websocket\";\nimport type {\n\tBroadcastOptions,\n\tI_WebsocketChannel,\n\tI_WebsocketClient,\n\tI_WebsocketEntity,\n\tWebsocketChannel,\n\tWebsocketMessage\n} from \"./websocket.types\";\n\nexport default class Channel<T extends Websocket = Websocket> implements I_WebsocketChannel<T> {\n\tpublic createdAt: Date = new Date();\n\tpublic id: string;\n\tpublic name: string;\n\tpublic limit: number;\n\tpublic members: Map<string, I_WebsocketClient>;\n\tpublic metadata: Record<string, string>;\n\tpublic ws: T;\n\tprivate message: Message;\n\n\tconstructor(id: string, name: string, ws: T, limit?: number, members?: Map<string, I_WebsocketClient>, metadata?: Record<string, string>) {\n\t\tthis.id = id;\n\t\tthis.name = name;\n\t\tthis.limit = limit ?? 5;\n\t\tthis.members = members ?? new Map();\n\t\tthis.metadata = metadata ?? {};\n\t\tthis.ws = ws;\n\t\tthis.message = new Message();\n\t\t\n\t}\n\n\tpublic broadcast(message: WebsocketMessage | string, options?: BroadcastOptions) {\n\t\tif (Guards.IsString(message)) {\n\t\t\tconst msg: WebsocketMessage = {\n\t\t\t\ttype: \"message\",\n\t\t\t\tcontent: { message },\n\t\t\t};\n\t\t\tmessage = msg;\n\t\t}\n\t\tconst output = this.message.create(message, { ...options, channel: this.name });\n\t\tif (options) {\n\t\t\t// Include channel metadata if requested\n\t\t\tif (options.includeMetadata) {\n\t\t\t\toutput.metadata = options.includeMetadata === true ? this.getMetadata() : this.getFilteredMetadata(options.includeMetadata);\n\t\t\t}\n\n\t\t\t// Handle excluded clients if needed\n\t\t\tif (options.excludeClients && options.excludeClients.length > 0) {\n\t\t\t\t// For large channels with many excluded clients, it might be more efficient\n\t\t\t\t// to send directly to each client instead of using channel publish\n\t\t\t\tif (this.members.size > 10 && options.excludeClients.length > this.members.size / 3) {\n\t\t\t\t\tconst serializedMessage = this.message.serialize(output);\n\t\t\t\t\tfor (const [clientId, client] of this.members) {\n\t\t\t\t\t\tif (!options.excludeClients.includes(clientId)) {\n\t\t\t\t\t\t\tclient.ws.send(serializedMessage);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Publish to the channel\n\t\tthis.ws.server.publish(this.id, this.message.serialize(output));\n\t}\n\n\t// Helper method for filtered metadata\n\tprivate getFilteredMetadata(keys: string[]) {\n\t\tconst metadata = this.getMetadata();\n\t\tconst filtered: Record<string, string> = {};\n\n\t\tfor (const key of keys) {\n\t\t\tif (metadata[key] !== undefined) {\n\t\t\t\tfiltered[key] = metadata[key];\n\t\t\t}\n\t\t}\n\n\t\treturn filtered;\n\t}\n\n\tpublic hasMember(client: I_WebsocketEntity | string) {\n\t\tif (typeof client === \"string\") return this.members.has(client);\n\t\treturn this.members.has(client.id);\n\t}\n\n\tpublic addMember(client: I_WebsocketClient) {\n\t\tif (!this.canAddMember()) return false;\n\t\tthis.members.set(client.id, client);\n\t\tclient.joinChannel(this);\n\t\treturn client;\n\t}\n\n\tpublic removeMember(entity: I_WebsocketEntity) {\n\t\tif (!this.members.has(entity.id)) return false;\n\t\tconst client = this.members.get(entity.id);\n\t\tif (!client) return false;\n\t\tclient.leaveChannel(this);\n\t\tthis.members.delete(entity.id);\n\t\treturn client;\n\t}\n\n\tpublic getMember(client: I_WebsocketEntity | string) {\n\t\tif (typeof client === \"string\") return this.members.get(client);\n\t\treturn this.members.get(client.id);\n\t}\n\n\tpublic getMembers(clients?: string[] | I_WebsocketEntity[]): I_WebsocketClient[] {\n\t\tif (!clients) return Array.from(this.members.values());\n\t\treturn clients.map((client) => this.getMember(client)).filter((client) => client !== undefined) as I_WebsocketClient[];\n\t}\n\n\tpublic getMetadata() {\n\t\treturn this.metadata;\n\t}\n\n\tpublic getCreatedAt() {\n\t\treturn this.createdAt;\n\t}\n\n\tpublic getId() {\n\t\treturn this.id;\n\t}\n\n\tpublic getName() {\n\t\treturn this.name;\n\t}\n\n\tpublic getLimit() {\n\t\treturn this.limit;\n\t}\n\n\tpublic getSize() {\n\t\treturn this.members.size;\n\t}\n\n\tpublic canAddMember() {\n\t\tconst size = this.getSize();\n\t\treturn size < this.limit;\n\t}\n\n\tpublic static GetChannelType(channels: WebsocketChannel<I_WebsocketChannel> | undefined) {\n\t\tif (!channels) return Channel;\n\t\tif (channels.size > 0) {\n\t\t\tconst firstChannel = channels.values().next().value;\n\t\t\tif (firstChannel) {\n\t\t\t\treturn firstChannel.constructor as typeof Channel;\n\t\t\t} else {\n\t\t\t\treturn Channel;\n\t\t\t}\n\t\t} else {\n\t\t\tLib.Warn(\"Channels are empty, using default channel class\");\n\t\t\treturn Channel;\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"Channel.js","sourceRoot":"","sources":["../../../../src/server/bun/websocket/Channel.ts"],"names":[],"mappings":"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","sourcesContent":["import { Guards, Lib } from \"../../../utils\";\nimport Message from \"./Message\";\nimport Websocket from \"./Websocket\";\nimport type { BroadcastOptions, I_WebsocketChannel, I_WebsocketClient, I_WebsocketEntity, WebsocketChannel, WebsocketMessage, AddMemberResult, AddMemberOptions } from \"./websocket.types\";\nimport { E_WebsocketMessageType } from \"./websocket.enums\";\n\n/**\n * Channel - Pub/sub topic for WebSocket clients\n *\n * ## Membership Contract\n * - `addMember()` validates capacity and adds to `members` map\n * - Client drives join via `joinChannel()` which subscribes and handles rollback\n * - If subscription fails, membership is automatically rolled back\n * - Member count never exceeds `limit`\n *\n * @example\n * const channel = new Channel(\"game-1\", \"Game Room\", ws, 10);\n * const result = channel.addMember(client);\n * if (result.success) {\n * channel.broadcast({ type: \"player.joined\", content: { player: client.whoami() } });\n * }\n */\nexport default class Channel<T extends Websocket = Websocket> implements I_WebsocketChannel<T> {\n\tpublic createdAt: Date = new Date();\n\tpublic id: string;\n\tpublic name: string;\n\tpublic limit: number;\n\tpublic members: Map<string, I_WebsocketClient>;\n\tpublic metadata: Record<string, string>;\n\tpublic ws: T;\n\n\tconstructor(id: string, name: string, ws: T, limit?: number, members?: Map<string, I_WebsocketClient>, metadata?: Record<string, string>) {\n\t\tthis.id = id;\n\t\tthis.name = name;\n\t\tthis.limit = limit ?? 5;\n\t\tthis.members = members ?? new Map();\n\t\tthis.metadata = metadata ?? {};\n\t\tthis.ws = ws;\n\t}\n\n\tpublic broadcast(message: WebsocketMessage | string, options?: BroadcastOptions) {\n\t\tif (Guards.IsString(message)) {\n\t\t\tconst msg: WebsocketMessage = {\n\t\t\t\ttype: \"message\",\n\t\t\t\tcontent: { message },\n\t\t\t};\n\t\t\tmessage = msg;\n\t\t}\n\n\t\tconst output = Message.Create(message, { ...options, channel: this.id });\n\n\t\t// Include channel metadata if requested\n\t\tif (options?.includeMetadata) {\n\t\t\toutput.metadata = options.includeMetadata === true ? this.getMetadata() : this.getFilteredMetadata(options.includeMetadata);\n\t\t}\n\n\t\tconst serializedMessage = Message.Serialize(output);\n\n\t\t// If we need to exclude clients, send individually to prevent excluded clients from receiving\n\t\tif (options?.excludeClients && options.excludeClients.length > 0) {\n\t\t\tconst excludeSet = new Set(options.excludeClients); // O(1) lookup\n\n\t\t\tfor (const [clientId, client] of this.members) {\n\t\t\t\tif (!excludeSet.has(clientId)) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tclient.ws.send(serializedMessage);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tLib.Warn(`Failed to send to client ${clientId}:`, error);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Otherwise use pub/sub for everyone\n\t\tthis.ws.server.publish(this.id, serializedMessage);\n\t}\n\n\t// Helper method for filtered metadata\n\tprivate getFilteredMetadata(keys: string[]) {\n\t\tconst metadata = this.getMetadata();\n\t\tconst filtered: Record<string, string> = {};\n\n\t\tfor (const key of keys) {\n\t\t\tif (metadata[key] !== undefined) {\n\t\t\t\tfiltered[key] = metadata[key];\n\t\t\t}\n\t\t}\n\n\t\treturn filtered;\n\t}\n\n\tpublic hasMember(client: I_WebsocketEntity | string) {\n\t\tif (typeof client === \"string\") return this.members.has(client);\n\t\treturn this.members.has(client.id);\n\t}\n\n\tpublic addMember(client: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult {\n\t\t// Check if already a member\n\t\tif (this.members.has(client.id)) {\n\t\t\treturn { success: false, reason: 'already_member' };\n\t\t}\n\n\t\t// Check capacity (atomic check)\n\t\tif (this.members.size >= this.limit) {\n\t\t\t// Optionally notify client why they can't join\n\t\t\tif (options?.notify_when_full) {\n\t\t\t\tthis.notifyChannelFull(client);\n\t\t\t}\n\t\t\treturn { success: false, reason: 'full' };\n\t\t}\n\n\t\ttry {\n\t\t\tthis.members.set(client.id, client);\n\t\t\treturn { success: true, client };\n\t\t} catch (error) {\n\t\t\t// Rollback\n\t\t\tthis.members.delete(client.id);\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\treason: 'error',\n\t\t\t\terror: error instanceof Error ? error : new Error(String(error))\n\t\t\t};\n\t\t}\n\t}\n\n\tprivate notifyChannelFull(client: I_WebsocketClient): void {\n\t\tclient.send({\n\t\t\ttype: E_WebsocketMessageType.ERROR,\n\t\t\tcontent: {\n\t\t\t\tmessage: `Channel \"${this.name}\" is full (${this.limit} members)`,\n\t\t\t\tcode: 'CHANNEL_FULL',\n\t\t\t\tchannel: this.id\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Internal method to remove a member without triggering client-side cleanup.\n\t * Used for rollback operations when joinChannel fails.\n\t * @internal\n\t */\n\tpublic removeMemberInternal(client: I_WebsocketClient): void {\n\t\tthis.members.delete(client.id);\n\t}\n\n\tpublic removeMember(entity: I_WebsocketEntity) {\n\t\tif (!this.members.has(entity.id)) return false;\n\t\tconst client = this.members.get(entity.id);\n\t\tif (!client) return false;\n\t\tclient.leaveChannel(this);\n\t\tthis.members.delete(entity.id);\n\t\treturn client;\n\t}\n\n\tpublic getMember(client: I_WebsocketEntity | string) {\n\t\tif (typeof client === \"string\") return this.members.get(client);\n\t\treturn this.members.get(client.id);\n\t}\n\n\tpublic getMembers(clients?: string[] | I_WebsocketEntity[]): I_WebsocketClient[] {\n\t\tif (!clients) return Array.from(this.members.values());\n\t\treturn clients.map((client) => this.getMember(client)).filter((client) => client !== undefined) as I_WebsocketClient[];\n\t}\n\n\tpublic getMetadata() {\n\t\treturn this.metadata;\n\t}\n\n\tpublic getCreatedAt() {\n\t\treturn this.createdAt;\n\t}\n\n\tpublic getId() {\n\t\treturn this.id;\n\t}\n\n\tpublic getName() {\n\t\treturn this.name;\n\t}\n\n\tpublic getLimit() {\n\t\treturn this.limit;\n\t}\n\n\tpublic getSize() {\n\t\treturn this.members.size;\n\t}\n\n\tpublic canAddMember() {\n\t\tconst size = this.getSize();\n\t\treturn size < this.limit;\n\t}\n\n\tpublic static GetChannelType(channels: WebsocketChannel<I_WebsocketChannel> | undefined) {\n\t\tif (!channels) return Channel;\n\t\tif (channels.size > 0) {\n\t\t\tconst firstChannel = channels.values().next().value;\n\t\t\tif (firstChannel) {\n\t\t\t\treturn firstChannel.constructor as typeof Channel;\n\t\t\t} else {\n\t\t\t\treturn Channel;\n\t\t\t}\n\t\t} else {\n\t\t\tLib.Warn(\"Channels are empty, using default channel class\");\n\t\t\treturn Channel;\n\t\t}\n\t}\n}\n"]}
@@ -1,10 +1,29 @@
1
1
  import { ServerWebSocket } from "bun";
2
2
  import type { I_WebsocketClient, WebsocketEntityData, WebsocketChannel, WebsocketStructuredMessage, I_WebsocketEntity, I_WebsocketChannel, WebsocketMessageOptions } from "./websocket.types";
3
+ import { E_ClientState } from "./websocket.enums";
4
+ /**
5
+ * Client - Connected WebSocket client with channel membership
6
+ *
7
+ * ## Channel Membership
8
+ * - Maintains own channel list and handles Bun pub/sub subscriptions
9
+ * - `joinChannel()` adds to channel, subscribes, and handles rollback on failure
10
+ * - Always use `channel.addMember(client)` in application code, not `client.joinChannel()` directly
11
+ *
12
+ * @example
13
+ * // ✅ Correct
14
+ * channel.addMember(client);
15
+ *
16
+ * // ❌ Incorrect - internal use only
17
+ * client.joinChannel(channel);
18
+ */
3
19
  export default class Client implements I_WebsocketClient {
4
20
  private _id;
5
21
  private _name;
6
22
  private _ws;
7
23
  private _channels;
24
+ private _state;
25
+ private _connectedAt?;
26
+ private _disconnectedAt?;
8
27
  private set ws(value);
9
28
  get ws(): ServerWebSocket<WebsocketEntityData>;
10
29
  private set id(value);
@@ -13,8 +32,22 @@ export default class Client implements I_WebsocketClient {
13
32
  private set name(value);
14
33
  private set channels(value);
15
34
  get channels(): WebsocketChannel<I_WebsocketChannel>;
35
+ get state(): E_ClientState;
16
36
  constructor(entity: I_WebsocketEntity);
17
- joinChannel(channel: I_WebsocketChannel, send?: boolean): void;
37
+ canReceiveMessages(): boolean;
38
+ markConnected(): void;
39
+ markDisconnecting(): void;
40
+ markDisconnected(): void;
41
+ getConnectionInfo(): {
42
+ id: string;
43
+ name: string;
44
+ state: E_ClientState;
45
+ connectedAt: Date | undefined;
46
+ disconnectedAt: Date | undefined;
47
+ uptime: number;
48
+ channelCount: number;
49
+ };
50
+ joinChannel(channel: I_WebsocketChannel, send?: boolean): boolean;
18
51
  leaveChannel(channel: I_WebsocketChannel, send?: boolean): void;
19
52
  joinChannels(channels: I_WebsocketChannel[], send?: boolean): void;
20
53
  leaveChannels(channels?: I_WebsocketChannel[], send?: boolean): void;
@@ -1,6 +1,21 @@
1
- import { E_WebsocketMessageType } from "./websocket.enums.js";
1
+ import { E_WebsocketMessageType, E_ClientState } from "./websocket.enums.js";
2
2
  import { Guards, Lib } from "../../../utils/index.js";
3
3
  import Message from "./Message.js";
4
+ /**
5
+ * Client - Connected WebSocket client with channel membership
6
+ *
7
+ * ## Channel Membership
8
+ * - Maintains own channel list and handles Bun pub/sub subscriptions
9
+ * - `joinChannel()` adds to channel, subscribes, and handles rollback on failure
10
+ * - Always use `channel.addMember(client)` in application code, not `client.joinChannel()` directly
11
+ *
12
+ * @example
13
+ * // ✅ Correct
14
+ * channel.addMember(client);
15
+ *
16
+ * // ❌ Incorrect - internal use only
17
+ * client.joinChannel(channel);
18
+ */
4
19
  export default class Client {
5
20
  set ws(value) {
6
21
  this._ws = value;
@@ -26,24 +41,73 @@ export default class Client {
26
41
  get channels() {
27
42
  return this._channels;
28
43
  }
44
+ get state() {
45
+ return this._state;
46
+ }
29
47
  constructor(entity) {
30
48
  this._id = entity.id;
31
49
  this._name = entity.name;
32
50
  this._ws = entity.ws;
33
- this.ws = entity.ws;
34
51
  this._channels = new Map();
52
+ this._state = E_ClientState.CONNECTING;
53
+ }
54
+ canReceiveMessages() {
55
+ return this._state === E_ClientState.CONNECTED;
56
+ }
57
+ markConnected() {
58
+ this._state = E_ClientState.CONNECTED;
59
+ this._connectedAt = new Date();
60
+ }
61
+ markDisconnecting() {
62
+ this._state = E_ClientState.DISCONNECTING;
63
+ }
64
+ markDisconnected() {
65
+ this._state = E_ClientState.DISCONNECTED;
66
+ this._disconnectedAt = new Date();
67
+ }
68
+ getConnectionInfo() {
69
+ return {
70
+ id: this.id,
71
+ name: this.name,
72
+ state: this._state,
73
+ connectedAt: this._connectedAt,
74
+ disconnectedAt: this._disconnectedAt,
75
+ uptime: this._connectedAt ? Date.now() - this._connectedAt.getTime() : 0,
76
+ channelCount: this._channels.size,
77
+ };
35
78
  }
36
79
  joinChannel(channel, send = true) {
37
80
  const channel_id = channel.getId();
38
- this.subscribe(channel_id);
39
- this.channels.set(channel_id, channel);
40
- if (send)
41
- this.send({
42
- type: E_WebsocketMessageType.CLIENT_JOIN_CHANNEL,
43
- content: { message: "Welcome to the channel" },
44
- channel: channel_id,
45
- client: this.whoami(),
46
- });
81
+ // Check if already joined
82
+ if (this.channels.has(channel_id)) {
83
+ return false;
84
+ }
85
+ // Try to add to channel first
86
+ const result = channel.addMember(this);
87
+ if (!result.success) {
88
+ return false; // Channel full, already member, or other issue
89
+ }
90
+ try {
91
+ // Subscribe to channel's pub/sub topic
92
+ this.subscribe(channel_id);
93
+ this.channels.set(channel_id, channel);
94
+ // Send join notification
95
+ if (send) {
96
+ this.send({
97
+ type: E_WebsocketMessageType.CLIENT_JOIN_CHANNEL,
98
+ content: { message: "Welcome to the channel" },
99
+ channel: channel_id,
100
+ client: this.whoami(),
101
+ });
102
+ }
103
+ return true;
104
+ }
105
+ catch (error) {
106
+ // Rollback channel membership on failure
107
+ channel.removeMemberInternal(this);
108
+ this.channels.delete(channel_id);
109
+ throw error;
110
+ }
47
111
  }
48
112
  leaveChannel(channel, send = true) {
49
113
  const channel_id = channel.getId();
@@ -77,14 +141,27 @@ export default class Client {
77
141
  return { id: this.id, name: this.name };
78
142
  }
79
143
  send(message, options) {
80
- if (Guards.IsString(message)) {
81
- const msg = {
82
- type: "message",
83
- content: { message },
84
- };
85
- message = Message.Create(msg, options);
144
+ // Check state before sending
145
+ if (!this.canReceiveMessages()) {
146
+ Lib.Warn(`Cannot send to client ${this.id} in state ${this._state}`);
147
+ return;
148
+ }
149
+ try {
150
+ if (Guards.IsString(message)) {
151
+ const msg = {
152
+ type: "message",
153
+ content: { message },
154
+ };
155
+ message = Message.Create(msg, options);
156
+ }
157
+ this.ws.send(JSON.stringify({ client: this.whoami(), ...message }));
158
+ }
159
+ catch (error) {
160
+ Lib.Warn(`Failed to send message to client ${this.id}:`, error);
161
+ if (error instanceof Error && error.message.includes("closed")) {
162
+ this.markDisconnected();
163
+ }
86
164
  }
87
- this.ws.send(JSON.stringify({ client: this.whoami(), ...message }));
88
165
  }
89
166
  subscribe(channel) {
90
167
  this.ws.subscribe(channel);
@@ -1 +1 @@
1
- {"version":3,"file":"Client.js","sourceRoot":"","sources":["../../../../src/server/bun/websocket/Client.ts"],"names":[],"mappings":"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","sourcesContent":["import { ServerWebSocket } from \"bun\";\nimport type {\n\tI_WebsocketClient,\n\tWebsocketEntityData,\n\tWebsocketChannel,\n\tWebsocketStructuredMessage,\n\tI_WebsocketEntity,\n\tI_WebsocketChannel,\n\tWebsocketMessageOptions,\n\tWebsocketMessage,\n} from \"./websocket.types\";\nimport { E_WebsocketMessageType } from \"./websocket.enums\";\nimport { Guards, Lib } from \"../../../utils\";\nimport Message from \"./Message\";\n\nexport default class Client implements I_WebsocketClient {\n\tprivate _id: string;\n\tprivate _name: string;\n\tprivate _ws: ServerWebSocket<WebsocketEntityData>;\n\tprivate _channels: WebsocketChannel<I_WebsocketChannel>;\n\n\tprivate set ws(value: ServerWebSocket<WebsocketEntityData>) {\n\t\tthis._ws = value;\n\t}\n\n\tpublic get ws(): ServerWebSocket<WebsocketEntityData> {\n\t\treturn this._ws;\n\t}\n\n\tprivate set id(value: string) {\n\t\tthis._id = value;\n\t}\n\n\tpublic get id(): string {\n\t\treturn this._id;\n\t}\n\n\tpublic get name(): string {\n\t\treturn this._name;\n\t}\n\n\tprivate set name(value: string) {\n\t\tthis._name = value;\n\t}\n\n\tprivate set channels(value: WebsocketChannel<I_WebsocketChannel>) {\n\t\tthis._channels = value;\n\t}\n\n\tpublic get channels(): WebsocketChannel<I_WebsocketChannel> {\n\t\treturn this._channels;\n\t}\n\n\tconstructor(entity: I_WebsocketEntity) {\n\t\tthis._id = entity.id;\n\t\tthis._name = entity.name;\n\t\tthis._ws = entity.ws;\n\t\tthis.ws = entity.ws;\n\t\tthis._channels = new Map();\n\t}\n\n\tpublic joinChannel(channel: I_WebsocketChannel, send: boolean = true) {\n\t\tconst channel_id = channel.getId();\n\t\tthis.subscribe(channel_id);\n\t\tthis.channels.set(channel_id, channel);\n\t\tif (send)\n\t\t\tthis.send({\n\t\t\t\ttype: E_WebsocketMessageType.CLIENT_JOIN_CHANNEL,\n\t\t\t\tcontent: { message: \"Welcome to the channel\" },\n\t\t\t\tchannel: channel_id,\n\t\t\t\tclient: this.whoami(),\n\t\t\t});\n\t}\n\n\tpublic leaveChannel(channel: I_WebsocketChannel, send: boolean = true) {\n\t\tconst channel_id = channel.getId();\n\t\tthis.channels.delete(channel_id);\n\t\tthis.unsubscribe(channel_id);\n\t\tif (send)\n\t\t\tthis.send({\n\t\t\t\ttype: E_WebsocketMessageType.CLIENT_LEAVE_CHANNEL,\n\t\t\t\tcontent: { message: \"Left the channel\" },\n\t\t\t\tchannel: channel_id,\n\t\t\t\tclient: this.whoami(),\n\t\t\t});\n\t}\n\n\tpublic joinChannels(channels: I_WebsocketChannel[], send: boolean = true) {\n\t\tchannels.forEach((channel) => {\n\t\t\tthis.joinChannel(channel, false);\n\t\t});\n\t\tif (send) this.send({ type: E_WebsocketMessageType.CLIENT_JOIN_CHANNELS, content: { channels }, client: this.whoami() });\n\t}\n\n\tpublic leaveChannels(channels?: I_WebsocketChannel[], send: boolean = true) {\n\t\tif (!channels) channels = Array.from(this.channels.values());\n\t\tchannels.forEach((channel) => {\n\t\t\tthis.leaveChannel(channel, false);\n\t\t});\n\t\tif (send) this.send({ type: E_WebsocketMessageType.CLIENT_LEAVE_CHANNELS, content: { channels }, client: this.whoami() });\n\t}\n\n\tpublic whoami(): { id: string; name: string } {\n\t\treturn { id: this.id, name: this.name };\n\t}\n\n\tpublic send(message: string, options?: WebsocketMessageOptions): void;\n\tpublic send(message: WebsocketStructuredMessage): void;\n\tpublic send(message: WebsocketStructuredMessage | string, options?: WebsocketMessageOptions): void {\n\t\tif (Guards.IsString(message)) {\n\t\t\tconst msg: WebsocketMessage = {\n\t\t\t\ttype: \"message\",\n\t\t\t\tcontent: { message },\n\t\t\t};\n\t\t\tmessage = Message.Create(msg, options);\n\t\t}\n\t\tthis.ws.send(JSON.stringify({ client: this.whoami(), ...message }));\n\t}\n\n\tpublic subscribe(channel: string): void {\n\t\tthis.ws.subscribe(channel);\n\t}\n\n\tpublic unsubscribe(channel: string): void {\n\t\tthis.ws.unsubscribe(channel);\n\t}\n\n\tpublic static GetClientType(clients: Map<string, I_WebsocketClient> | undefined): typeof Client {\n\t\tif (!clients) return Client;\n\t\tif (clients.size > 0) {\n\t\t\tconst firstClient = clients.values().next().value;\n\t\t\tif (firstClient) {\n\t\t\t\treturn firstClient.constructor as typeof Client;\n\t\t\t}\n\t\t}\n\n\t\t// Fallback to default Client class\n\t\tLib.Warn(\"Clients map is empty, using default client class\");\n\t\treturn Client;\n\t}\n\n\tpublic static System() {\n\t\treturn <WebsocketEntityData>{\n\t\t\tid: \"system\",\n\t\t\tname: \"System\",\n\t\t};\n\t}\n}\n"]}
1
+ {"version":3,"file":"Client.js","sourceRoot":"","sources":["../../../../src/server/bun/websocket/Client.ts"],"names":[],"mappings":"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","sourcesContent":["import { ServerWebSocket } from \"bun\";\nimport type {\n\tI_WebsocketClient,\n\tWebsocketEntityData,\n\tWebsocketChannel,\n\tWebsocketStructuredMessage,\n\tI_WebsocketEntity,\n\tI_WebsocketChannel,\n\tWebsocketMessageOptions,\n\tWebsocketMessage,\n} from \"./websocket.types\";\nimport { E_WebsocketMessageType, E_ClientState } from \"./websocket.enums\";\nimport { Guards, Lib } from \"../../../utils\";\nimport Message from \"./Message\";\n\n/**\n * Client - Connected WebSocket client with channel membership\n *\n * ## Channel Membership\n * - Maintains own channel list and handles Bun pub/sub subscriptions\n * - `joinChannel()` adds to channel, subscribes, and handles rollback on failure\n * - Always use `channel.addMember(client)` in application code, not `client.joinChannel()` directly\n *\n * @example\n * // ✅ Correct\n * channel.addMember(client);\n *\n * // ❌ Incorrect - internal use only\n * client.joinChannel(channel);\n */\nexport default class Client implements I_WebsocketClient {\n\tprivate _id: string;\n\tprivate _name: string;\n\tprivate _ws: ServerWebSocket<WebsocketEntityData>;\n\tprivate _channels: WebsocketChannel<I_WebsocketChannel>;\n\tprivate _state: E_ClientState;\n\tprivate _connectedAt?: Date;\n\tprivate _disconnectedAt?: Date;\n\n\tprivate set ws(value: ServerWebSocket<WebsocketEntityData>) {\n\t\tthis._ws = value;\n\t}\n\n\tpublic get ws(): ServerWebSocket<WebsocketEntityData> {\n\t\treturn this._ws;\n\t}\n\n\tprivate set id(value: string) {\n\t\tthis._id = value;\n\t}\n\n\tpublic get id(): string {\n\t\treturn this._id;\n\t}\n\n\tpublic get name(): string {\n\t\treturn this._name;\n\t}\n\n\tprivate set name(value: string) {\n\t\tthis._name = value;\n\t}\n\n\tprivate set channels(value: WebsocketChannel<I_WebsocketChannel>) {\n\t\tthis._channels = value;\n\t}\n\n\tpublic get channels(): WebsocketChannel<I_WebsocketChannel> {\n\t\treturn this._channels;\n\t}\n\n\tpublic get state(): E_ClientState {\n\t\treturn this._state;\n\t}\n\n\tconstructor(entity: I_WebsocketEntity) {\n\t\tthis._id = entity.id;\n\t\tthis._name = entity.name;\n\t\tthis._ws = entity.ws;\n\t\tthis._channels = new Map();\n\t\tthis._state = E_ClientState.CONNECTING;\n\t}\n\n\tpublic canReceiveMessages(): boolean {\n\t\treturn this._state === E_ClientState.CONNECTED;\n\t}\n\n\tpublic markConnected(): void {\n\t\tthis._state = E_ClientState.CONNECTED;\n\t\tthis._connectedAt = new Date();\n\t}\n\n\tpublic markDisconnecting(): void {\n\t\tthis._state = E_ClientState.DISCONNECTING;\n\t}\n\n\tpublic markDisconnected(): void {\n\t\tthis._state = E_ClientState.DISCONNECTED;\n\t\tthis._disconnectedAt = new Date();\n\t}\n\n\tpublic getConnectionInfo() {\n\t\treturn {\n\t\t\tid: this.id,\n\t\t\tname: this.name,\n\t\t\tstate: this._state,\n\t\t\tconnectedAt: this._connectedAt,\n\t\t\tdisconnectedAt: this._disconnectedAt,\n\t\t\tuptime: this._connectedAt ? Date.now() - this._connectedAt.getTime() : 0,\n\t\t\tchannelCount: this._channels.size,\n\t\t};\n\t}\n\n\tpublic joinChannel(channel: I_WebsocketChannel, send: boolean = true): boolean {\n\t\tconst channel_id = channel.getId();\n\n\t\t// Check if already joined\n\t\tif (this.channels.has(channel_id)) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Try to add to channel first\n\t\tconst result = channel.addMember(this);\n\t\tif (!result.success) {\n\t\t\treturn false; // Channel full, already member, or other issue\n\t\t}\n\n\t\ttry {\n\t\t\t// Subscribe to channel's pub/sub topic\n\t\t\tthis.subscribe(channel_id);\n\t\t\tthis.channels.set(channel_id, channel);\n\n\t\t\t// Send join notification\n\t\t\tif (send) {\n\t\t\t\tthis.send({\n\t\t\t\t\ttype: E_WebsocketMessageType.CLIENT_JOIN_CHANNEL,\n\t\t\t\t\tcontent: { message: \"Welcome to the channel\" },\n\t\t\t\t\tchannel: channel_id,\n\t\t\t\t\tclient: this.whoami(),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\t// Rollback channel membership on failure\n\t\t\tchannel.removeMemberInternal(this);\n\t\t\tthis.channels.delete(channel_id);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tpublic leaveChannel(channel: I_WebsocketChannel, send: boolean = true) {\n\t\tconst channel_id = channel.getId();\n\t\tthis.channels.delete(channel_id);\n\t\tthis.unsubscribe(channel_id);\n\t\tif (send)\n\t\t\tthis.send({\n\t\t\t\ttype: E_WebsocketMessageType.CLIENT_LEAVE_CHANNEL,\n\t\t\t\tcontent: { message: \"Left the channel\" },\n\t\t\t\tchannel: channel_id,\n\t\t\t\tclient: this.whoami(),\n\t\t\t});\n\t}\n\n\tpublic joinChannels(channels: I_WebsocketChannel[], send: boolean = true) {\n\t\tchannels.forEach((channel) => {\n\t\t\tthis.joinChannel(channel, false);\n\t\t});\n\t\tif (send) this.send({ type: E_WebsocketMessageType.CLIENT_JOIN_CHANNELS, content: { channels }, client: this.whoami() });\n\t}\n\n\tpublic leaveChannels(channels?: I_WebsocketChannel[], send: boolean = true) {\n\t\tif (!channels) channels = Array.from(this.channels.values());\n\t\tchannels.forEach((channel) => {\n\t\t\tthis.leaveChannel(channel, false);\n\t\t});\n\t\tif (send) this.send({ type: E_WebsocketMessageType.CLIENT_LEAVE_CHANNELS, content: { channels }, client: this.whoami() });\n\t}\n\n\tpublic whoami(): { id: string; name: string } {\n\t\treturn { id: this.id, name: this.name };\n\t}\n\n\tpublic send(message: string, options?: WebsocketMessageOptions): void;\n\tpublic send(message: WebsocketStructuredMessage): void;\n\tpublic send(message: WebsocketStructuredMessage | string, options?: WebsocketMessageOptions): void {\n\t\t// Check state before sending\n\t\tif (!this.canReceiveMessages()) {\n\t\t\tLib.Warn(`Cannot send to client ${this.id} in state ${this._state}`);\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tif (Guards.IsString(message)) {\n\t\t\t\tconst msg: WebsocketMessage = {\n\t\t\t\t\ttype: \"message\",\n\t\t\t\t\tcontent: { message },\n\t\t\t\t};\n\t\t\t\tmessage = Message.Create(msg, options);\n\t\t\t}\n\t\t\tthis.ws.send(JSON.stringify({ client: this.whoami(), ...message }));\n\t\t} catch (error) {\n\t\t\tLib.Warn(`Failed to send message to client ${this.id}:`, error);\n\t\t\tif (error instanceof Error && error.message.includes(\"closed\")) {\n\t\t\t\tthis.markDisconnected();\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic subscribe(channel: string): void {\n\t\tthis.ws.subscribe(channel);\n\t}\n\n\tpublic unsubscribe(channel: string): void {\n\t\tthis.ws.unsubscribe(channel);\n\t}\n\n\tpublic static GetClientType(clients: Map<string, I_WebsocketClient> | undefined): typeof Client {\n\t\tif (!clients) return Client;\n\t\tif (clients.size > 0) {\n\t\t\tconst firstClient = clients.values().next().value;\n\t\t\tif (firstClient) {\n\t\t\t\treturn firstClient.constructor as typeof Client;\n\t\t\t}\n\t\t}\n\n\t\t// Fallback to default Client class\n\t\tLib.Warn(\"Clients map is empty, using default client class\");\n\t\treturn Client;\n\t}\n\n\tpublic static System() {\n\t\treturn <WebsocketEntityData>{\n\t\t\tid: \"system\",\n\t\t\tname: \"System\",\n\t\t};\n\t}\n}\n"]}
@@ -1,14 +1,10 @@
1
1
  import { WebsocketStructuredMessage, WebsocketMessage, WebsocketMessageOptions, I_WebsocketClient, WebsocketEntityData } from "./websocket.types";
2
2
  export default class Message {
3
- private messageTemplate;
4
- constructor();
5
- create(message: WebsocketMessage, options?: WebsocketMessageOptions): WebsocketStructuredMessage;
6
- createWhisper(message: Omit<WebsocketMessage, "type">, options?: WebsocketMessageOptions): WebsocketStructuredMessage;
7
- send(target: I_WebsocketClient, message: WebsocketStructuredMessage): void;
8
- send(target: I_WebsocketClient, message: WebsocketMessage, options?: WebsocketMessageOptions): void;
9
- alert(target: I_WebsocketClient, reason: string, client?: WebsocketEntityData): void;
10
- serialize<T = string>(message: WebsocketStructuredMessage, transform?: (message: WebsocketStructuredMessage) => T): string | T;
11
- static Serialize<T = string>(message: WebsocketStructuredMessage, transform: (message: WebsocketStructuredMessage) => T): T;
12
- static Serialize<T = string>(message: WebsocketStructuredMessage, transform?: (message: WebsocketStructuredMessage) => T): string | T;
3
+ private static readonly MESSAGE_TEMPLATE;
4
+ private constructor();
13
5
  static Create(message: WebsocketMessage, options?: WebsocketMessageOptions): WebsocketStructuredMessage;
6
+ static CreateWhisper(message: Omit<WebsocketMessage, "type">, options?: WebsocketMessageOptions): WebsocketStructuredMessage;
7
+ static Serialize<T = string>(message: WebsocketStructuredMessage, transform?: (message: WebsocketStructuredMessage) => T): string | T;
8
+ static Send(target: I_WebsocketClient, message: WebsocketMessage, options?: WebsocketMessageOptions): void;
9
+ static Alert(target: I_WebsocketClient, reason: string, client?: WebsocketEntityData): void;
14
10
  }