topsyde-utils 1.2.3 → 1.3.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/dist/index.d.ts CHANGED
@@ -62,5 +62,5 @@ export { ControllerResponse, ControllerAction, ControllerMap, ControllerOptions
62
62
  export { Routes } from "./server/bun/router/routes";
63
63
  export { WebsocketConstructorOptions, I_WebsocketConstructor } from "./server/bun/websocket/Websocket";
64
64
  export { E_WebsocketMessageType, E_WebsocketMessagePriority, E_ClientState } from "./server/bun/websocket/websocket.enums";
65
- 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";
65
+ export { BunWebsocketMessage, WebsocketChannel, WebsocketClients, WebsocketMessageOptions, WebsocketMessage, WebsocketStructuredMessage, WebsocketEntityId, WebsocketEntityName, WebsocketEntityData, I_WebsocketEntity, I_WebsocketClient, I_WebsocketChannelEntity, BroadcastOptions, AddMemberResult, AddMemberOptions, RemoveMemberOptions, I_WebsocketChannel, WebsocketInterfaceHandlers, I_WebsocketInterface, } from "./server/bun/websocket/websocket.types";
66
66
  export { E_SUBJET_TYPE, I_RxjsPayload, RxjsNamespaces, AsyncSubject, BehaviorSubject, ReplaySubject, Subject, Subscription } from "./utils/Rxjs";
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/api/base.api\";\nexport * from \"./client/rxjs/useRxjs\";\nexport * from \"./client/vite/plugins/topsydeUtilsVitePlugin\";\nexport * from \"./external/re-exports\";\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 BaseAPI } from \"./client/api/base.api\";\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 {\n\tExpose,\n\tValidationError,\n\tIsArray,\n\tIsBoolean,\n\tIsDate,\n\tIsEnum,\n\tIsNumber,\n\tIsObject,\n\tIsOptional,\n\tIsString,\n\tValidate,\n\tIsAlphanumeric,\n\tIsBooleanString,\n\tIsDateString,\n\tIsDecimal,\n\tIsDefined,\n\tIsNumberString,\n\tIsNotEmpty,\n\tIsNotEmptyObject,\n} from \"./external/re-exports\";\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
+ {"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/api/base.api\";\nexport * from \"./client/rxjs/useRxjs\";\nexport * from \"./client/vite/plugins/topsydeUtilsVitePlugin\";\nexport * from \"./external/re-exports\";\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 BaseAPI } from \"./client/api/base.api\";\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 {\n\tExpose,\n\tValidationError,\n\tIsArray,\n\tIsBoolean,\n\tIsDate,\n\tIsEnum,\n\tIsNumber,\n\tIsObject,\n\tIsOptional,\n\tIsString,\n\tValidate,\n\tIsAlphanumeric,\n\tIsBooleanString,\n\tIsDateString,\n\tIsDecimal,\n\tIsDefined,\n\tIsNumberString,\n\tIsNotEmpty,\n\tIsNotEmptyObject,\n} from \"./external/re-exports\";\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\tRemoveMemberOptions,\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,5 @@
1
1
  import Websocket from "./Websocket";
2
- import type { BroadcastOptions, I_WebsocketChannel, I_WebsocketClient, I_WebsocketEntity, WebsocketChannel, WebsocketMessage, AddMemberResult, AddMemberOptions } from "./websocket.types";
2
+ import type { BroadcastOptions, I_WebsocketChannel, I_WebsocketClient, I_WebsocketEntity, WebsocketChannel, WebsocketMessage, AddMemberResult, AddMemberOptions, RemoveMemberOptions } from "./websocket.types";
3
3
  /**
4
4
  * Channel - Pub/sub topic for WebSocket clients
5
5
  *
@@ -28,15 +28,32 @@ export default class Channel<T extends Websocket = Websocket> implements I_Webso
28
28
  broadcast(message: WebsocketMessage | string, options?: BroadcastOptions): void;
29
29
  private getFilteredMetadata;
30
30
  hasMember(client: I_WebsocketEntity | string): boolean;
31
+ /**
32
+ * ATOMIC: Add member to channel (membership only, no side effects)
33
+ * Internal method used for rollback-safe operations
34
+ * @internal
35
+ */
36
+ private addToMembersMap;
37
+ /**
38
+ * Add a client to this channel with full coordination
39
+ * Handles: membership + WebSocket subscription + client-side tracking + optional notification
40
+ * This ensures two-way coordination between channel and client
41
+ */
31
42
  addMember(client: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult;
43
+ addMembers(clients: I_WebsocketClient[], options?: AddMemberOptions): AddMemberResult[];
32
44
  private notifyChannelFull;
33
45
  /**
34
46
  * Internal method to remove a member without triggering client-side cleanup.
35
47
  * Used for rollback operations when joinChannel fails.
36
48
  * @internal
37
49
  */
38
- removeMemberInternal(client: I_WebsocketClient): void;
39
- removeMember(entity: I_WebsocketEntity): false | I_WebsocketClient;
50
+ removeFromMembersMap(client: I_WebsocketClient): void;
51
+ /**
52
+ * Remove a client from this channel with full coordination
53
+ * Handles: membership removal + WebSocket unsubscription + client-side tracking removal + optional notification
54
+ * This ensures two-way coordination between channel and client
55
+ */
56
+ removeMember(entity: I_WebsocketEntity, options?: RemoveMemberOptions): false | I_WebsocketClient;
40
57
  getMember(client: I_WebsocketEntity | string): I_WebsocketClient | undefined;
41
58
  getMembers(clients?: string[] | I_WebsocketEntity[]): I_WebsocketClient[];
42
59
  getMetadata(): Record<string, string>;
@@ -46,5 +63,6 @@ export default class Channel<T extends Websocket = Websocket> implements I_Webso
46
63
  getLimit(): number;
47
64
  getSize(): number;
48
65
  canAddMember(): boolean;
66
+ delete(): void;
49
67
  static GetChannelType(channels: WebsocketChannel<I_WebsocketChannel> | undefined): typeof Channel;
50
68
  }
@@ -75,18 +75,23 @@ export default class Channel {
75
75
  return this.members.has(client);
76
76
  return this.members.has(client.id);
77
77
  }
78
- addMember(client, options) {
78
+ /**
79
+ * ATOMIC: Add member to channel (membership only, no side effects)
80
+ * Internal method used for rollback-safe operations
81
+ * @internal
82
+ */
83
+ addToMembersMap(client, options) {
79
84
  // Check if already a member
80
85
  if (this.members.has(client.id)) {
81
- return { success: false, reason: 'already_member' };
86
+ return { success: false, reason: "already_member" };
82
87
  }
83
- // Check capacity (atomic check)
84
- if (this.members.size >= this.limit) {
88
+ // Check capacity
89
+ if (!this.canAddMember()) {
85
90
  // Optionally notify client why they can't join
86
91
  if (options?.notify_when_full) {
87
92
  this.notifyChannelFull(client);
88
93
  }
89
- return { success: false, reason: 'full' };
94
+ return { success: false, reason: "full" };
90
95
  }
91
96
  try {
92
97
  this.members.set(client.id, client);
@@ -97,19 +102,67 @@ export default class Channel {
97
102
  this.members.delete(client.id);
98
103
  return {
99
104
  success: false,
100
- reason: 'error',
101
- error: error instanceof Error ? error : new Error(String(error))
105
+ reason: "error",
106
+ error: error instanceof Error ? error : new Error(String(error)),
102
107
  };
103
108
  }
104
109
  }
110
+ /**
111
+ * Add a client to this channel with full coordination
112
+ * Handles: membership + WebSocket subscription + client-side tracking + optional notification
113
+ * This ensures two-way coordination between channel and client
114
+ */
115
+ addMember(client, options) {
116
+ // 1. Atomic membership add
117
+ const result = this.addToMembersMap(client, options);
118
+ if (!result.success) {
119
+ return result;
120
+ }
121
+ try {
122
+ // 2. Subscribe client's WebSocket to channel pub/sub topic
123
+ // CRITICAL: Without this, client won't receive channel.broadcast() messages
124
+ client.subscribe(this.id);
125
+ // 3. Track channel on client side (client's channels map)
126
+ client.trackChannel(this);
127
+ // 4. Optional welcome notification
128
+ if (options?.notify) {
129
+ client.send({
130
+ type: E_WebsocketMessageType.CLIENT_JOIN_CHANNEL,
131
+ content: { message: "Welcome to the channel" },
132
+ channel: this.id,
133
+ client: client.whoami(),
134
+ });
135
+ }
136
+ return result;
137
+ }
138
+ catch (error) {
139
+ // Rollback on failure: remove membership + unsubscribe + untrack
140
+ this.removeFromMembersMap(client);
141
+ client.unsubscribe(this.id);
142
+ client.untrackChannel(this);
143
+ throw error;
144
+ }
145
+ }
146
+ addMembers(clients, options) {
147
+ const results = [];
148
+ for (const client of clients) {
149
+ const result = this.addMember(client, options);
150
+ results.push(result);
151
+ if (!result.success) {
152
+ // Stop adding further members on failure
153
+ break;
154
+ }
155
+ }
156
+ return results;
157
+ }
105
158
  notifyChannelFull(client) {
106
159
  client.send({
107
160
  type: E_WebsocketMessageType.ERROR,
108
161
  content: {
109
162
  message: `Channel "${this.name}" is full (${this.limit} members)`,
110
- code: 'CHANNEL_FULL',
111
- channel: this.id
112
- }
163
+ code: "CHANNEL_FULL",
164
+ channel: this.id,
165
+ },
113
166
  });
114
167
  }
115
168
  /**
@@ -117,17 +170,36 @@ export default class Channel {
117
170
  * Used for rollback operations when joinChannel fails.
118
171
  * @internal
119
172
  */
120
- removeMemberInternal(client) {
173
+ removeFromMembersMap(client) {
121
174
  this.members.delete(client.id);
122
175
  }
123
- removeMember(entity) {
176
+ /**
177
+ * Remove a client from this channel with full coordination
178
+ * Handles: membership removal + WebSocket unsubscription + client-side tracking removal + optional notification
179
+ * This ensures two-way coordination between channel and client
180
+ */
181
+ removeMember(entity, options) {
182
+ // 1. Check if member exists
124
183
  if (!this.members.has(entity.id))
125
184
  return false;
126
185
  const client = this.members.get(entity.id);
127
186
  if (!client)
128
187
  return false;
129
- client.leaveChannel(this);
130
- this.members.delete(entity.id);
188
+ // 2. Remove from channel members (atomic operation)
189
+ this.removeFromMembersMap(client);
190
+ // 3. Unsubscribe client's WebSocket from channel pub/sub topic
191
+ client.unsubscribe(this.id);
192
+ // 4. Untrack channel on client side (remove from client's channels map)
193
+ client.untrackChannel(this);
194
+ // 5. Optional goodbye notification
195
+ if (options?.notify) {
196
+ client.send({
197
+ type: E_WebsocketMessageType.CLIENT_LEAVE_CHANNEL,
198
+ content: { message: "You left the channel" },
199
+ channel: this.id,
200
+ client: client.whoami(),
201
+ });
202
+ }
131
203
  return client;
132
204
  }
133
205
  getMember(client) {
@@ -162,6 +234,14 @@ export default class Channel {
162
234
  const size = this.getSize();
163
235
  return size < this.limit;
164
236
  }
237
+ delete() {
238
+ //first remove all members
239
+ this.members.forEach((member) => {
240
+ this.removeMember(member);
241
+ });
242
+ //then clear members map
243
+ this.members.clear();
244
+ }
165
245
  static GetChannelType(channels) {
166
246
  if (!channels)
167
247
  return Channel;
@@ -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 { 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
+ {"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\tAddMemberResult,\n\tAddMemberOptions,\n\tRemoveMemberOptions,\n} 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\t/**\n\t * ATOMIC: Add member to channel (membership only, no side effects)\n\t * Internal method used for rollback-safe operations\n\t * @internal\n\t */\n\tprivate addToMembersMap(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\n\t\tif (!this.canAddMember()) {\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\t/**\n\t * Add a client to this channel with full coordination\n\t * Handles: membership + WebSocket subscription + client-side tracking + optional notification\n\t * This ensures two-way coordination between channel and client\n\t */\n\tpublic addMember(client: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult {\n\t\t// 1. Atomic membership add\n\t\tconst result = this.addToMembersMap(client, options);\n\t\tif (!result.success) {\n\t\t\treturn result;\n\t\t}\n\n\t\ttry {\n\t\t\t// 2. Subscribe client's WebSocket to channel pub/sub topic\n\t\t\t// CRITICAL: Without this, client won't receive channel.broadcast() messages\n\t\t\tclient.subscribe(this.id);\n\n\t\t\t// 3. Track channel on client side (client's channels map)\n\t\t\tclient.trackChannel(this);\n\n\t\t\t// 4. Optional welcome notification\n\t\t\tif (options?.notify) {\n\t\t\t\tclient.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: this.id,\n\t\t\t\t\tclient: client.whoami(),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn result;\n\t\t} catch (error) {\n\t\t\t// Rollback on failure: remove membership + unsubscribe + untrack\n\t\t\tthis.removeFromMembersMap(client);\n\t\t\tclient.unsubscribe(this.id);\n\t\t\tclient.untrackChannel(this);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tpublic addMembers(clients: I_WebsocketClient[], options?: AddMemberOptions): AddMemberResult[] {\n\t\tconst results: AddMemberResult[] = [];\n\t\tfor (const client of clients) {\n\t\t\tconst result = this.addMember(client, options);\n\t\t\tresults.push(result);\n\t\t\tif (!result.success) {\n\t\t\t\t// Stop adding further members on failure\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn results;\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 removeFromMembersMap(client: I_WebsocketClient): void {\n\t\tthis.members.delete(client.id);\n\t}\n\n\t/**\n\t * Remove a client from this channel with full coordination\n\t * Handles: membership removal + WebSocket unsubscription + client-side tracking removal + optional notification\n\t * This ensures two-way coordination between channel and client\n\t */\n\tpublic removeMember(entity: I_WebsocketEntity, options?: RemoveMemberOptions) {\n\t\t// 1. Check if member exists\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\n\t\t// 2. Remove from channel members (atomic operation)\n\t\tthis.removeFromMembersMap(client);\n\n\t\t// 3. Unsubscribe client's WebSocket from channel pub/sub topic\n\t\tclient.unsubscribe(this.id);\n\n\t\t// 4. Untrack channel on client side (remove from client's channels map)\n\t\tclient.untrackChannel(this);\n\n\t\t// 5. Optional goodbye notification\n\t\tif (options?.notify) {\n\t\t\tclient.send({\n\t\t\t\ttype: E_WebsocketMessageType.CLIENT_LEAVE_CHANNEL,\n\t\t\t\tcontent: { message: \"You left the channel\" },\n\t\t\t\tchannel: this.id,\n\t\t\t\tclient: client.whoami(),\n\t\t\t});\n\t\t}\n\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 delete() {\n\t\t//first remove all members\n\t\tthis.members.forEach((member) => {\n\t\t\tthis.removeMember(member);\n\t\t});\n\t\t//then clear members map\n\t\tthis.members.clear();\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"]}
@@ -47,10 +47,30 @@ export default class Client implements I_WebsocketClient {
47
47
  uptime: number;
48
48
  channelCount: number;
49
49
  };
50
+ /**
51
+ * HELPER: Track channel on client side (for channel.addMember coordination)
52
+ * Allows channel to update client's internal channel map
53
+ * @internal Used by channel.addMember()
54
+ */
55
+ trackChannel(channel: I_WebsocketChannel): void;
56
+ /**
57
+ * HELPER: Untrack channel on client side (for channel.addMember rollback)
58
+ * Allows channel to remove from client's internal channel map during rollback
59
+ * @internal Used by channel.addMember() error handling
60
+ */
61
+ untrackChannel(channel: I_WebsocketChannel): void;
62
+ /**
63
+ * Join a channel (thin wrapper that delegates to channel.addMember)
64
+ * channel.addMember() handles all coordination: membership + subscription + tracking + notification
65
+ */
50
66
  joinChannel(channel: I_WebsocketChannel, send?: boolean): {
51
67
  success: boolean;
52
68
  reason: string;
53
69
  };
70
+ /**
71
+ * Leave a channel (thin wrapper that delegates to channel.removeMember)
72
+ * channel.removeMember() handles all coordination: membership removal + unsubscription + tracking removal + notification
73
+ */
54
74
  leaveChannel(channel: I_WebsocketChannel, send?: boolean): void;
55
75
  joinChannels(channels: I_WebsocketChannel[], send?: boolean): void;
56
76
  leaveChannels(channels?: I_WebsocketChannel[], send?: boolean): void;
@@ -76,50 +76,51 @@ export default class Client {
76
76
  channelCount: this._channels.size,
77
77
  };
78
78
  }
79
+ /**
80
+ * HELPER: Track channel on client side (for channel.addMember coordination)
81
+ * Allows channel to update client's internal channel map
82
+ * @internal Used by channel.addMember()
83
+ */
84
+ trackChannel(channel) {
85
+ this.channels.set(channel.getId(), channel);
86
+ }
87
+ /**
88
+ * HELPER: Untrack channel on client side (for channel.addMember rollback)
89
+ * Allows channel to remove from client's internal channel map during rollback
90
+ * @internal Used by channel.addMember() error handling
91
+ */
92
+ untrackChannel(channel) {
93
+ this.channels.delete(channel.getId());
94
+ }
95
+ /**
96
+ * Join a channel (thin wrapper that delegates to channel.addMember)
97
+ * channel.addMember() handles all coordination: membership + subscription + tracking + notification
98
+ */
79
99
  joinChannel(channel, send = true) {
80
100
  const channel_id = channel.getId();
81
101
  // Check if already joined
82
102
  if (this.channels.has(channel_id)) {
83
103
  return { success: false, reason: "already_member" };
84
104
  }
85
- // Try to add to channel first
86
- const result = channel.addMember(this);
105
+ // Delegate to channel (which now handles full coordination)
106
+ const result = channel.addMember(this, { notify: send });
87
107
  if (!result.success) {
88
- return { success: false, reason: result.reason }; // 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 { success: true, reason: "" };
104
- }
105
- catch (error) {
106
- // Rollback channel membership on failure
107
- channel.removeMemberInternal(this);
108
- this.channels.delete(channel_id);
109
- throw error;
108
+ return { success: false, reason: result.reason };
110
109
  }
110
+ return { success: true, reason: "" };
111
111
  }
112
+ /**
113
+ * Leave a channel (thin wrapper that delegates to channel.removeMember)
114
+ * channel.removeMember() handles all coordination: membership removal + unsubscription + tracking removal + notification
115
+ */
112
116
  leaveChannel(channel, send = true) {
113
117
  const channel_id = channel.getId();
114
- this.channels.delete(channel_id);
115
- this.unsubscribe(channel_id);
116
- if (send)
117
- this.send({
118
- type: E_WebsocketMessageType.CLIENT_LEAVE_CHANNEL,
119
- content: { message: "(" + channel_id + ") Left the channel" },
120
- channel: channel_id,
121
- client: this.whoami(),
122
- });
118
+ // Check if we're in the channel
119
+ if (!this.channels.has(channel_id)) {
120
+ return;
121
+ }
122
+ // Delegate to channel (which now handles full coordination)
123
+ channel.removeMember(this, { notify: send });
123
124
  }
124
125
  joinChannels(channels, send = true) {
125
126
  channels.forEach((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, 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 || this._state === E_ClientState.DISCONNECTING;\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): { success: boolean; reason: string } {\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 { success: false, reason: \"already_member\" };\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 { success: false, reason: result.reason }; // 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 { success: true, reason: \"\" };\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: \"(\" + channel_id + \") 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
+ {"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 || this._state === E_ClientState.DISCONNECTING;\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\t/**\n\t * HELPER: Track channel on client side (for channel.addMember coordination)\n\t * Allows channel to update client's internal channel map\n\t * @internal Used by channel.addMember()\n\t */\n\tpublic trackChannel(channel: I_WebsocketChannel): void {\n\t\tthis.channels.set(channel.getId(), channel);\n\t}\n\n\t/**\n\t * HELPER: Untrack channel on client side (for channel.addMember rollback)\n\t * Allows channel to remove from client's internal channel map during rollback\n\t * @internal Used by channel.addMember() error handling\n\t */\n\tpublic untrackChannel(channel: I_WebsocketChannel): void {\n\t\tthis.channels.delete(channel.getId());\n\t}\n\n\t/**\n\t * Join a channel (thin wrapper that delegates to channel.addMember)\n\t * channel.addMember() handles all coordination: membership + subscription + tracking + notification\n\t */\n\tpublic joinChannel(channel: I_WebsocketChannel, send: boolean = true): { success: boolean; reason: string } {\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 { success: false, reason: \"already_member\" };\n\t\t}\n\n\t\t// Delegate to channel (which now handles full coordination)\n\t\tconst result = channel.addMember(this, { notify: send });\n\n\t\tif (!result.success) {\n\t\t\treturn { success: false, reason: result.reason };\n\t\t}\n\n\t\treturn { success: true, reason: \"\" };\n\t}\n\n\t/**\n\t * Leave a channel (thin wrapper that delegates to channel.removeMember)\n\t * channel.removeMember() handles all coordination: membership removal + unsubscription + tracking removal + notification\n\t */\n\tpublic leaveChannel(channel: I_WebsocketChannel, send: boolean = true) {\n\t\tconst channel_id = channel.getId();\n\n\t\t// Check if we're in the channel\n\t\tif (!this.channels.has(channel_id)) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Delegate to channel (which now handles full coordination)\n\t\tchannel.removeMember(this, { notify: send });\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"]}
@@ -141,6 +141,10 @@ export default class Websocket extends Singleton {
141
141
  * @param id - The id of the channel
142
142
  */
143
143
  removeChannel(id) {
144
+ const channel = this._channels.get(id);
145
+ if (!channel)
146
+ return;
147
+ channel.delete();
144
148
  this._channels.delete(id);
145
149
  }
146
150
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"Websocket.js","sourceRoot":"","sources":["../../../../src/server/bun/websocket/Websocket.ts"],"names":[],"mappings":"AACA,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,OAAO,MAAM,WAAW,CAAC;AAChC,OAAO,MAAM,MAAM,UAAU,CAAC;AAW9B,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAe3D;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,OAAO,OAAO,SAAU,SAAQ,SAAS;IAW/C,YAAsB,OAAgC;QACrD,KAAK,EAAE,CAAC;QAVC,aAAQ,GAAmC,IAAI,GAAG,EAAE,CAAC;QAOrD,YAAO,GAAG,CAAC,CAAC;QAqEd,0BAAqB,GAAG,CAAC,EAAwC,EAAE,OAA4B,EAAE,EAAE;YAC1G,IAAI,CAAC;gBACJ,IAAI,SAAS,CAAC,SAAS,CAAC,EAAE,EAAE,OAAO,CAAC;oBAAE,OAAO;gBAE7C,IAAI,IAAI,CAAC,sBAAsB,CAAC,OAAO;oBAAE,OAAO,IAAI,CAAC,sBAAsB,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;gBAEjG,EAAE,CAAC,IAAI,CAAC,uCAAuC,GAAG,OAAO,CAAC,CAAC;gBAC3D,SAAS,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;YACnF,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;gBAC9D,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,iDAAiD,GAAG,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;YACtH,CAAC;QACF,CAAC,CAAC;QAEM,oBAAe,GAAG,CAAC,EAAwC,EAAE,EAAE;YACtE,IAAI,CAAC;gBACJ,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK;oBAAE,GAAG,CAAC,GAAG,CAAC,0BAA0B,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;gBAEtE,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBAC5C,IAAI,CAAC,MAAM;oBAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;gBAEzD,MAAM,MAAM,GAAG,SAAS,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;gBACtF,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;gBACrC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACf,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO;oBAAE,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;gBAEnF,0BAA0B;gBAC1B,MAAM,CAAC,aAAa,EAAE,CAAC;gBAEvB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,sBAAsB,CAAC,gBAAgB,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,uBAAuB,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;gBAEvI,6DAA6D;gBAC7D,MAAM,UAAU,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;gBAC9C,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;oBACzB,MAAM,IAAI,KAAK,CAAC,iCAAiC,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;gBACxE,CAAC;gBAED,IAAI,IAAI,CAAC,sBAAsB,CAAC,IAAI;oBAAE,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC5E,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;gBAC9D,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,iDAAiD,GAAG,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;YACtH,CAAC;QACF,CAAC,CAAC;QAEM,uBAAkB,GAAG,CAAC,EAAwC,EAAE,IAAY,EAAE,MAAc,EAAE,EAAE;YACvG,IAAI,CAAC;gBACJ,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK;oBAAE,GAAG,CAAC,GAAG,CAAC,qBAAqB,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;gBAEjE,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBAC7C,IAAI,CAAC,MAAM;oBAAE,OAAO;gBAEpB,wBAAwB;gBACxB,MAAM,CAAC,iBAAiB,EAAE,CAAC;gBAE3B,IAAI,IAAI,CAAC,sBAAsB,CAAC,KAAK;oBAAE,IAAI,CAAC,sBAAsB,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;gBAE3F,2BAA2B;gBAC3B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;oBAClC,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;gBAC9B,CAAC,CAAC,CAAC;gBAEH,uBAAuB;gBACvB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBACjC,uBAAuB;gBACvB,MAAM,CAAC,gBAAgB,EAAE,CAAC;YAC3B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;gBAC9D,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,8CAA8C,GAAG,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;YACnH,CAAC;QACF,CAAC,CAAC;QAEM,oBAAe,GAAG,CAAC,EAAwC,EAAE,OAA4B,EAAE,EAAE;YACpG,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;gBACxB,MAAM,IAAI,GAA+B,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;gBACxF,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;gBAC9B,OAAO,IAAI,CAAC;YACb,CAAC;YACD,OAAO,KAAK,CAAC;QACd,CAAC,CAAC;QA/ID,IAAI,CAAC,aAAa,GAAG,OAAO,EAAE,YAAY,CAAC;QAC3C,IAAI,CAAC,SAAS,GAAG,OAAO,EAAE,QAAQ,IAAI,IAAI,GAAG,EAAmB,CAAC;QACjE,IAAI,CAAC,YAAY,GAAG,OAAO,EAAE,WAAW,IAAI,MAAM,CAAC;QACnD,IAAI,CAAC,aAAa,GAAG,OAAO,EAAE,YAAY,IAAI,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACxF,IAAI,CAAC,QAAQ,GAAG,OAAO,EAAE,OAAO,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACrD,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,oBAAoB,IAAI,IAAI,CAAC,CAAC;QACnF,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IACjG,CAAC;IAED,IAAc,MAAM,CAAC,KAAa;QACjC,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,IAAW,MAAM;QAChB,OAAO,IAAI,CAAC,OAAO,CAAC;IACrB,CAAC;IAEM,GAAG,CAAC,MAAc;QACxB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,OAAO,CAAC,KAAK,EAAE,CAAC;QAChB,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACzC,CAAC;IAED;;;;;;OAMG;IACI,aAAa,CAAC,EAAU,EAAE,IAAY,EAAE,KAAc;QAC5D,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAY,CAAC;QACrE,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,aAAa,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QAC9D,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QAChC,OAAO,OAAO,CAAC;IAChB,CAAC;IAED;;;OAGG;IACI,aAAa,CAAC,EAAU;QAC9B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC3B,CAAC;IAED;;;;;;OAMG;IACI,MAAM,CAAC,aAAa,CAAC,EAAU,EAAE,IAAY,EAAE,KAAc;QACnE,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QACzC,OAAO,EAAE,CAAC,aAAa,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IAC1C,CAAC;IAEM,QAAQ;QACd,OAAO;YACN,IAAI,EAAE,IAAI,CAAC,eAAe;YAC1B,OAAO,EAAE,IAAI,CAAC,qBAAqB;YACnC,KAAK,EAAE,IAAI,CAAC,kBAAkB;SAC9B,CAAC;IACH,CAAC;IAkFS,YAAY,CAAC,MAAyB;QAC/C,OAAO,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IAED;;;;;OAKG;IACI,MAAM,CAAC,SAAS,CAAC,EAAwC,EAAE,OAA4B;QAC7F,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QAC3C,OAAO,IAAI,CAAC,eAAe,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IAC1C,CAAC;IAED;;;OAGG;IACI,MAAM,CAAC,MAAM;QACnB,OAAO,IAAI,CAAC,WAAW,EAAa,CAAC,MAAM,CAAC;IAC7C,CAAC;IAED;;;;;OAKG;IACI,MAAM,CAAC,SAAS,CAAC,OAAe,EAAE,OAAmC,EAAE,GAAG,IAAW;QAC3F,6CAA6C;QAC7C,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QACzC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC7C,CAAC;QACD,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC/D,CAAC;IAED;;;;OAIG;IACI,MAAM,CAAC,YAAY,CAAC,OAAmC,EAAE,GAAG,IAAW;QAC7E,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QACzC,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;IACxE,CAAC;IAED;;;;OAIG;IACI,MAAM,CAAC,IAAI,CAAC,OAAe,EAAE,MAAyB;QAC5D,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QACzC,MAAM,MAAM,GAAG,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACI,MAAM,CAAC,KAAK,CAAC,OAAe,EAAE,MAAyB;QAC7D,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QACzC,MAAM,MAAM,GAAG,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IACjD,CAAC;IAUM,MAAM,CAAC,SAAS,CAAC,EAAU,EAAE,eAAwB,IAAI;QAC/D,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QACzC,MAAM,MAAM,GAAG,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACnC,IAAI,CAAC,MAAM,IAAI,YAAY;YAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,EAAE,YAAY,CAAC,CAAC;QAC/E,OAAO,MAAM,CAAC;IACf,CAAC;IAED;;;;OAIG;IACI,MAAM,CAAC,UAAU,CAAC,EAAU;QAClC,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QACzC,OAAO,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC7B,CAAC;IAED;;;OAGG;IACI,MAAM,CAAC,WAAW;QACxB,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QACzC,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1C,CAAC;IAED;;;OAGG;IACI,MAAM,CAAC,UAAU;QACvB,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QACzC,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,CAAC;IAEM,MAAM,CAAC,SAAS;QACtB,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QACzC,OAAO,EAAE,CAAC,OAAO,CAAC;IACnB,CAAC;IAED;;;OAGG;IACI,MAAM,CAAC,cAAc;QAC3B,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QACzC,OAAO,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC;IACzB,CAAC;IAED;;;OAGG;IACI,MAAM,CAAC,eAAe;QAC5B,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QACzC,OAAO,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC;IAC1B,CAAC;IAED;;;;OAIG;IACI,MAAM,CAAC,YAAY,CAAC,MAAyB;QACnD,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QACzC,OAAO,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAED;;;OAGG;IACI,MAAM,CAAC,mBAAmB;QAChC,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QACzC,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,WAAW,CAAC,CAAC;IAC1F,CAAC;IAED;;;OAGG;IACI,MAAM,CAAC,cAAc;QAC3B,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAa,CAAC;QACzC,MAAM,KAAK,GAAG;YACb,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI;YACvB,UAAU,EAAE,CAAC;YACb,SAAS,EAAE,CAAC;YACZ,aAAa,EAAE,CAAC;YAChB,YAAY,EAAE,CAAC;SACf,CAAC;QAEF,KAAK,MAAM,MAAM,IAAI,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC3C,QAAQ,MAAM,CAAC,KAAK,EAAE,CAAC;gBACtB,KAAK,YAAY;oBAChB,KAAK,CAAC,UAAU,EAAE,CAAC;oBACnB,MAAM;gBACP,KAAK,WAAW;oBACf,KAAK,CAAC,SAAS,EAAE,CAAC;oBAClB,MAAM;gBACP,KAAK,eAAe;oBACnB,KAAK,CAAC,aAAa,EAAE,CAAC;oBACtB,MAAM;gBACP,KAAK,cAAc;oBAClB,KAAK,CAAC,YAAY,EAAE,CAAC;oBACrB,MAAM;YACR,CAAC;QACF,CAAC;QAED,OAAO,KAAK,CAAC;IACd,CAAC;CACD","sourcesContent":["import { Server, ServerWebSocket, WebSocketHandler } from \"bun\";\nimport Singleton from \"../../../singleton\";\nimport { Lib } from \"../../../utils\";\nimport { Console } from \"../../../utils/Console\";\nimport Channel from \"./Channel\";\nimport Client from \"./Client\";\nimport type {\n\tI_WebsocketChannel,\n\tI_WebsocketClient,\n\tI_WebsocketEntity,\n\tI_WebsocketInterface,\n\tWebsocketChannel,\n\tWebsocketEntityData,\n\tBunWebsocketMessage,\n\tWebsocketStructuredMessage,\n} from \"./websocket.types\";\nimport { E_WebsocketMessageType } from \"./websocket.enums\";\n\nexport type WebsocketConstructorOptions = {\n\tdebug?: boolean;\n\tglobal_channel_limit?: number;\n};\n\nexport interface I_WebsocketConstructor {\n\tws_interface?: I_WebsocketInterface;\n\tchannels?: WebsocketChannel;\n\tclientClass?: typeof Client;\n\tchannelClass?: typeof Channel;\n\toptions?: WebsocketConstructorOptions;\n}\n\n/**\n * Websocket - Singleton managing clients, channels, and message routing\n *\n * ## API Design: Static vs Instance\n * - **Static methods**: Use in application code (e.g., `Websocket.Broadcast()`, `Websocket.GetClient()`)\n * - **Instance methods**: Use when extending the class (e.g., `protected createClient()`)\n *\n * Static methods are facades that call the singleton instance internally.\n *\n * @example\n * // Application code - use static methods\n * Websocket.Broadcast(\"lobby\", { type: \"chat\", content: { message: \"Hi!\" } });\n *\n * // Extension - override instance methods\n * MyWebsocket extends Websocket:\n * protected createClient(entity) {\n * return new MyCustomClient(entity);\n * }\n */\nexport default class Websocket extends Singleton {\n\tprotected _channels: WebsocketChannel;\n\tprotected _clients: Map<string, I_WebsocketClient> = new Map();\n\tprotected _server!: Server;\n\tprotected _channelClass: typeof Channel;\n\tprotected _clientClass: typeof Client;\n\tprotected _ws_interface?: I_WebsocketInterface;\n\tprotected _options: WebsocketConstructorOptions;\n\tprotected _ws_interface_handlers: Partial<WebSocketHandler<WebsocketEntityData>>;\n\tprotected _lastId = 1;\n\n\tprotected constructor(options?: I_WebsocketConstructor) {\n\t\tsuper();\n\t\tthis._ws_interface = options?.ws_interface;\n\t\tthis._channels = options?.channels ?? new Map<string, Channel>();\n\t\tthis._clientClass = options?.clientClass ?? Client;\n\t\tthis._channelClass = options?.channelClass ?? Channel.GetChannelType(options?.channels);\n\t\tthis._options = options?.options ?? { debug: false };\n\t\tthis.createChannel(\"global\", \"Global\", this._options.global_channel_limit ?? 1000);\n\t\tthis._ws_interface_handlers = this._ws_interface?.handlers(this._channels, this._clients) ?? {};\n\t}\n\n\tprotected set server(value: Server) {\n\t\tthis._server = value;\n\t}\n\n\tpublic get server(): Server {\n\t\treturn this._server;\n\t}\n\n\tpublic set(server: Server) {\n\t\tthis.server = server;\n\t\tConsole.blank();\n\t\tConsole.success(\"Websocket server set\");\n\t}\n\n\t/**\n\t * Create a new channel\n\t * @param id - The id of the channel\n\t * @param name - The name of the channel\n\t * @param limit - The limit of the channel\n\t * @returns The created channel\n\t */\n\tpublic createChannel(id: string, name: string, limit?: number): Channel {\n\t\tif (this._channels.has(id)) return this._channels.get(id) as Channel;\n\t\tconst channel = new this._channelClass(id, name, this, limit);\n\t\tthis._channels.set(id, channel);\n\t\treturn channel;\n\t}\n\n\t/**\n\t * Remove a channel\n\t * @param id - The id of the channel\n\t */\n\tpublic removeChannel(id: string) {\n\t\tthis._channels.delete(id);\n\t}\n\n\t/**\n\t * Create a new channel\n\t * @param id - The id of the channel\n\t * @param name - The name of the channel\n\t * @param limit - The limit of the channel\n\t * @returns The created channel\n\t */\n\tpublic static CreateChannel(id: string, name: string, limit?: number) {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn ws.createChannel(id, name, limit);\n\t}\n\n\tpublic handlers(): WebSocketHandler<WebsocketEntityData> {\n\t\treturn {\n\t\t\topen: this.clientConnected,\n\t\t\tmessage: this.clientMessageReceived,\n\t\t\tclose: this.clientDisconnected,\n\t\t};\n\t}\n\n\tprivate clientMessageReceived = (ws: ServerWebSocket<WebsocketEntityData>, message: BunWebsocketMessage) => {\n\t\ttry {\n\t\t\tif (Websocket.Heartbeat(ws, message)) return;\n\n\t\t\tif (this._ws_interface_handlers.message) return this._ws_interface_handlers.message(ws, message);\n\n\t\t\tws.send(\"This is the message from the server: \" + message);\n\t\t\tWebsocket.BroadCastAll({ type: \"client.message.received\", content: { message } });\n\t\t} catch (error) {\n\t\t\tconsole.error(error instanceof Error ? error.message : error);\n\t\t\tws.close(1011, \"Internal server error during message handling: \" + (error instanceof Error ? error.message : error));\n\t\t}\n\t};\n\n\tprivate clientConnected = (ws: ServerWebSocket<WebsocketEntityData>) => {\n\t\ttry {\n\t\t\tif (this._options.debug) Lib.Log(\"[debug] Client connected\", ws.data);\n\n\t\t\tconst global = this._channels.get(\"global\");\n\t\t\tif (!global) throw new Error(\"Global channel not found\");\n\n\t\t\tconst client = Websocket.CreateClient({ id: ws.data.id, ws: ws, name: ws.data.name });\n\t\t\tthis._clients.set(client.id, client);\n\t\t\tthis._lastId++;\n\t\t\tif ((Number(client.id) || 0) >= this._lastId) this._lastId = Number(client.id) + 1;\n\n\t\t\t// Mark as fully connected\n\t\t\tclient.markConnected();\n\n\t\t\tclient.send({ type: E_WebsocketMessageType.CLIENT_CONNECTED, content: { message: \"Welcome to the server\", client: client.whoami() } });\n\n\t\t\t// Client handles its own joining logic with rollback support\n\t\t\tconst joinResult = client.joinChannel(global);\n\t\t\tif (!joinResult.success) {\n\t\t\t\tthrow new Error(\"Failed to join global channel: \" + joinResult.reason);\n\t\t\t}\n\n\t\t\tif (this._ws_interface_handlers.open) this._ws_interface_handlers.open(ws);\n\t\t} catch (error) {\n\t\t\tconsole.error(error instanceof Error ? error.message : error);\n\t\t\tws.close(1011, \"Internal server error during connection setup: \" + (error instanceof Error ? error.message : error));\n\t\t}\n\t};\n\n\tprivate clientDisconnected = (ws: ServerWebSocket<WebsocketEntityData>, code: number, reason: string) => {\n\t\ttry {\n\t\t\tif (this._options.debug) Lib.Log(\"Client disconnected\", ws.data);\n\n\t\t\tconst client = this._clients.get(ws.data.id);\n\t\t\tif (!client) return;\n\n\t\t\t// Mark as disconnecting\n\t\t\tclient.markDisconnecting();\n\n\t\t\tif (this._ws_interface_handlers.close) this._ws_interface_handlers.close(ws, code, reason);\n\n\t\t\t// Remove from all channels\n\t\t\tthis._channels.forEach((channel) => {\n\t\t\t\tchannel.removeMember(client);\n\t\t\t});\n\n\t\t\t// Remove from registry\n\t\t\tthis._clients.delete(ws.data.id);\n\t\t\t// Mark as disconnected\n\t\t\tclient.markDisconnected();\n\t\t} catch (error) {\n\t\t\tconsole.error(error instanceof Error ? error.message : error);\n\t\t\tws.close(1011, \"Internal server error during disconnection: \" + (error instanceof Error ? error.message : error));\n\t\t}\n\t};\n\n\tprivate handleHeartbeat = (ws: ServerWebSocket<WebsocketEntityData>, message: BunWebsocketMessage) => {\n\t\tif (message === \"ping\") {\n\t\t\tconst pong: WebsocketStructuredMessage = { type: \"pong\", content: { message: \"pong\" } };\n\t\t\tws.send(JSON.stringify(pong));\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\n\tprotected createClient(entity: I_WebsocketEntity): I_WebsocketClient {\n\t\treturn new this._clientClass(entity);\n\t}\n\n\t/**\n\t * Handle the heartbeat\n\t * @param ws - The websocket\n\t * @param message - The message\n\t * @returns True if the heartbeat was handled, false otherwise\n\t */\n\tpublic static Heartbeat(ws: ServerWebSocket<WebsocketEntityData>, message: BunWebsocketMessage) {\n\t\tconst self = this.GetInstance<Websocket>();\n\t\treturn self.handleHeartbeat(ws, message);\n\t}\n\n\t/**\n\t * Get the server\n\t * @returns The server\n\t */\n\tpublic static Server() {\n\t\treturn this.GetInstance<Websocket>().server;\n\t}\n\n\t/**\n\t * Broadcast a message to a channel\n\t * @param channel - The channel\n\t * @param message - The message\n\t * @param args - The arguments\n\t */\n\tpublic static Broadcast(channel: string, message: WebsocketStructuredMessage, ...args: any[]) {\n\t\t// Get the server from the singleton instance\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\tif (!ws.server) {\n\t\t\tthrow new Error(\"Websocket server not set\");\n\t\t}\n\t\tws.server.publish(channel, JSON.stringify({ message, args }));\n\t}\n\n\t/**\n\t * Broadcast a message to all channels\n\t * @param message - The message\n\t * @param args - The arguments\n\t */\n\tpublic static BroadCastAll(message: WebsocketStructuredMessage, ...args: any[]) {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\tws._channels.forEach((channel) => channel.broadcast(message, ...args));\n\t}\n\n\t/**\n\t * Join a channel\n\t * @param channel - The channel\n\t * @param entity - The entity\n\t */\n\tpublic static Join(channel: string, entity: I_WebsocketEntity) {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\tconst client = ws._clients.get(entity.id);\n\t\tif (!client) return;\n\t\tws._channels.get(channel)?.addMember(client);\n\t}\n\n\t/**\n\t * Leave a channel\n\t * @param channel - The channel\n\t * @param entity - The entity\n\t */\n\tpublic static Leave(channel: string, entity: I_WebsocketEntity) {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\tconst client = ws._clients.get(entity.id);\n\t\tif (!client) return;\n\t\tws._channels.get(channel)?.removeMember(client);\n\t}\n\n\t/**\n\t * Get a client\n\t * @param id - The id of the client\n\t * @param throw_if_nil - Whether to throw an error if the client is not found\n\t * @returns The client\n\t */\n\tpublic static GetClient(id: string, throw_if_nil?: true): I_WebsocketClient;\n\tpublic static GetClient(id: string, throw_if_nil?: false): I_WebsocketClient | undefined;\n\tpublic static GetClient(id: string, throw_if_nil: boolean = true): I_WebsocketClient | undefined {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\tconst client = ws._clients.get(id);\n\t\tif (!client && throw_if_nil) throw new Error(`Client with id ${id} not found`);\n\t\treturn client;\n\t}\n\n\t/**\n\t * Get a channel\n\t * @param id - The id of the channel\n\t * @returns The channel\n\t */\n\tpublic static GetChannel(id: string) {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn ws._channels.get(id);\n\t}\n\n\t/**\n\t * Get all channels\n\t * @returns The channels\n\t */\n\tpublic static GetChannels() {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn Array.from(ws._channels.values());\n\t}\n\n\t/**\n\t * Get all clients\n\t * @returns The clients\n\t */\n\tpublic static GetClients() {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn Array.from(ws._clients.values());\n\t}\n\n\tpublic static GetLastId() {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn ws._lastId;\n\t}\n\n\t/**\n\t * Get the number of clients\n\t * @returns The number of clients\n\t */\n\tpublic static GetClientCount() {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn ws._clients.size;\n\t}\n\n\t/**\n\t * Get the number of channels\n\t * @returns The number of channels\n\t */\n\tpublic static GetChannelCount() {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn ws._channels.size;\n\t}\n\n\t/**\n\t * Create a client\n\t * @param entity - The entity\n\t * @returns The created client\n\t */\n\tpublic static CreateClient(entity: I_WebsocketEntity): I_WebsocketClient {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn ws.createClient(entity);\n\t}\n\n\t/**\n\t * Get all connected clients (excluding connecting/disconnecting)\n\t * @returns Array of connected clients\n\t */\n\tpublic static GetConnectedClients(): I_WebsocketClient[] {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn Array.from(ws._clients.values()).filter((client) => client.state === \"connected\");\n\t}\n\n\t/**\n\t * Get client statistics by state\n\t * @returns Object with counts by state\n\t */\n\tpublic static GetClientStats() {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\tconst stats = {\n\t\t\ttotal: ws._clients.size,\n\t\t\tconnecting: 0,\n\t\t\tconnected: 0,\n\t\t\tdisconnecting: 0,\n\t\t\tdisconnected: 0,\n\t\t};\n\n\t\tfor (const client of ws._clients.values()) {\n\t\t\tswitch (client.state) {\n\t\t\t\tcase \"connecting\":\n\t\t\t\t\tstats.connecting++;\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"connected\":\n\t\t\t\t\tstats.connected++;\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"disconnecting\":\n\t\t\t\t\tstats.disconnecting++;\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"disconnected\":\n\t\t\t\t\tstats.disconnected++;\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\treturn stats;\n\t}\n}\n"]}
1
+ {"version":3,"file":"Websocket.js","sourceRoot":"","sources":["../../../../src/server/bun/websocket/Websocket.ts"],"names":[],"mappings":"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","sourcesContent":["import { Server, ServerWebSocket, WebSocketHandler } from \"bun\";\nimport Singleton from \"../../../singleton\";\nimport { Lib } from \"../../../utils\";\nimport { Console } from \"../../../utils/Console\";\nimport Channel from \"./Channel\";\nimport Client from \"./Client\";\nimport type {\n\tI_WebsocketChannel,\n\tI_WebsocketClient,\n\tI_WebsocketEntity,\n\tI_WebsocketInterface,\n\tWebsocketChannel,\n\tWebsocketEntityData,\n\tBunWebsocketMessage,\n\tWebsocketStructuredMessage,\n} from \"./websocket.types\";\nimport { E_WebsocketMessageType } from \"./websocket.enums\";\n\nexport type WebsocketConstructorOptions = {\n\tdebug?: boolean;\n\tglobal_channel_limit?: number;\n};\n\nexport interface I_WebsocketConstructor {\n\tws_interface?: I_WebsocketInterface;\n\tchannels?: WebsocketChannel;\n\tclientClass?: typeof Client;\n\tchannelClass?: typeof Channel;\n\toptions?: WebsocketConstructorOptions;\n}\n\n/**\n * Websocket - Singleton managing clients, channels, and message routing\n *\n * ## API Design: Static vs Instance\n * - **Static methods**: Use in application code (e.g., `Websocket.Broadcast()`, `Websocket.GetClient()`)\n * - **Instance methods**: Use when extending the class (e.g., `protected createClient()`)\n *\n * Static methods are facades that call the singleton instance internally.\n *\n * @example\n * // Application code - use static methods\n * Websocket.Broadcast(\"lobby\", { type: \"chat\", content: { message: \"Hi!\" } });\n *\n * // Extension - override instance methods\n * MyWebsocket extends Websocket:\n * protected createClient(entity) {\n * return new MyCustomClient(entity);\n * }\n */\nexport default class Websocket extends Singleton {\n\tprotected _channels: WebsocketChannel;\n\tprotected _clients: Map<string, I_WebsocketClient> = new Map();\n\tprotected _server!: Server;\n\tprotected _channelClass: typeof Channel;\n\tprotected _clientClass: typeof Client;\n\tprotected _ws_interface?: I_WebsocketInterface;\n\tprotected _options: WebsocketConstructorOptions;\n\tprotected _ws_interface_handlers: Partial<WebSocketHandler<WebsocketEntityData>>;\n\tprotected _lastId = 1;\n\n\tprotected constructor(options?: I_WebsocketConstructor) {\n\t\tsuper();\n\t\tthis._ws_interface = options?.ws_interface;\n\t\tthis._channels = options?.channels ?? new Map<string, Channel>();\n\t\tthis._clientClass = options?.clientClass ?? Client;\n\t\tthis._channelClass = options?.channelClass ?? Channel.GetChannelType(options?.channels);\n\t\tthis._options = options?.options ?? { debug: false };\n\t\tthis.createChannel(\"global\", \"Global\", this._options.global_channel_limit ?? 1000);\n\t\tthis._ws_interface_handlers = this._ws_interface?.handlers(this._channels, this._clients) ?? {};\n\t}\n\n\tprotected set server(value: Server) {\n\t\tthis._server = value;\n\t}\n\n\tpublic get server(): Server {\n\t\treturn this._server;\n\t}\n\n\tpublic set(server: Server) {\n\t\tthis.server = server;\n\t\tConsole.blank();\n\t\tConsole.success(\"Websocket server set\");\n\t}\n\n\t/**\n\t * Create a new channel\n\t * @param id - The id of the channel\n\t * @param name - The name of the channel\n\t * @param limit - The limit of the channel\n\t * @returns The created channel\n\t */\n\tpublic createChannel(id: string, name: string, limit?: number): Channel {\n\t\tif (this._channels.has(id)) return this._channels.get(id) as Channel;\n\t\tconst channel = new this._channelClass(id, name, this, limit);\n\t\tthis._channels.set(id, channel);\n\t\treturn channel;\n\t}\n\n\t/**\n\t * Remove a channel\n\t * @param id - The id of the channel\n\t */\n\tpublic removeChannel(id: string) {\n\t\tconst channel = this._channels.get(id);\n\t\tif (!channel) return;\n\t\tchannel.delete();\n\t\tthis._channels.delete(id);\n\t}\n\n\t/**\n\t * Create a new channel\n\t * @param id - The id of the channel\n\t * @param name - The name of the channel\n\t * @param limit - The limit of the channel\n\t * @returns The created channel\n\t */\n\tpublic static CreateChannel(id: string, name: string, limit?: number) {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn ws.createChannel(id, name, limit);\n\t}\n\n\tpublic handlers(): WebSocketHandler<WebsocketEntityData> {\n\t\treturn {\n\t\t\topen: this.clientConnected,\n\t\t\tmessage: this.clientMessageReceived,\n\t\t\tclose: this.clientDisconnected,\n\t\t};\n\t}\n\n\tprivate clientMessageReceived = (ws: ServerWebSocket<WebsocketEntityData>, message: BunWebsocketMessage) => {\n\t\ttry {\n\t\t\tif (Websocket.Heartbeat(ws, message)) return;\n\n\t\t\tif (this._ws_interface_handlers.message) return this._ws_interface_handlers.message(ws, message);\n\n\t\t\tws.send(\"This is the message from the server: \" + message);\n\t\t\tWebsocket.BroadCastAll({ type: \"client.message.received\", content: { message } });\n\t\t} catch (error) {\n\t\t\tconsole.error(error instanceof Error ? error.message : error);\n\t\t\tws.close(1011, \"Internal server error during message handling: \" + (error instanceof Error ? error.message : error));\n\t\t}\n\t};\n\n\tprivate clientConnected = (ws: ServerWebSocket<WebsocketEntityData>) => {\n\t\ttry {\n\t\t\tif (this._options.debug) Lib.Log(\"[debug] Client connected\", ws.data);\n\n\t\t\tconst global = this._channels.get(\"global\");\n\t\t\tif (!global) throw new Error(\"Global channel not found\");\n\n\t\t\tconst client = Websocket.CreateClient({ id: ws.data.id, ws: ws, name: ws.data.name });\n\t\t\tthis._clients.set(client.id, client);\n\t\t\tthis._lastId++;\n\t\t\tif ((Number(client.id) || 0) >= this._lastId) this._lastId = Number(client.id) + 1;\n\n\t\t\t// Mark as fully connected\n\t\t\tclient.markConnected();\n\n\t\t\tclient.send({ type: E_WebsocketMessageType.CLIENT_CONNECTED, content: { message: \"Welcome to the server\", client: client.whoami() } });\n\n\t\t\t// Client handles its own joining logic with rollback support\n\t\t\tconst joinResult = client.joinChannel(global);\n\t\t\tif (!joinResult.success) {\n\t\t\t\tthrow new Error(\"Failed to join global channel: \" + joinResult.reason);\n\t\t\t}\n\n\t\t\tif (this._ws_interface_handlers.open) this._ws_interface_handlers.open(ws);\n\t\t} catch (error) {\n\t\t\tconsole.error(error instanceof Error ? error.message : error);\n\t\t\tws.close(1011, \"Internal server error during connection setup: \" + (error instanceof Error ? error.message : error));\n\t\t}\n\t};\n\n\tprivate clientDisconnected = (ws: ServerWebSocket<WebsocketEntityData>, code: number, reason: string) => {\n\t\ttry {\n\t\t\tif (this._options.debug) Lib.Log(\"Client disconnected\", ws.data);\n\n\t\t\tconst client = this._clients.get(ws.data.id);\n\t\t\tif (!client) return;\n\n\t\t\t// Mark as disconnecting\n\t\t\tclient.markDisconnecting();\n\n\t\t\tif (this._ws_interface_handlers.close) this._ws_interface_handlers.close(ws, code, reason);\n\n\t\t\t// Remove from all channels\n\t\t\tthis._channels.forEach((channel) => {\n\t\t\t\tchannel.removeMember(client);\n\t\t\t});\n\n\t\t\t// Remove from registry\n\t\t\tthis._clients.delete(ws.data.id);\n\t\t\t// Mark as disconnected\n\t\t\tclient.markDisconnected();\n\t\t} catch (error) {\n\t\t\tconsole.error(error instanceof Error ? error.message : error);\n\t\t\tws.close(1011, \"Internal server error during disconnection: \" + (error instanceof Error ? error.message : error));\n\t\t}\n\t};\n\n\tprivate handleHeartbeat = (ws: ServerWebSocket<WebsocketEntityData>, message: BunWebsocketMessage) => {\n\t\tif (message === \"ping\") {\n\t\t\tconst pong: WebsocketStructuredMessage = { type: \"pong\", content: { message: \"pong\" } };\n\t\t\tws.send(JSON.stringify(pong));\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\n\tprotected createClient(entity: I_WebsocketEntity): I_WebsocketClient {\n\t\treturn new this._clientClass(entity);\n\t}\n\n\t/**\n\t * Handle the heartbeat\n\t * @param ws - The websocket\n\t * @param message - The message\n\t * @returns True if the heartbeat was handled, false otherwise\n\t */\n\tpublic static Heartbeat(ws: ServerWebSocket<WebsocketEntityData>, message: BunWebsocketMessage) {\n\t\tconst self = this.GetInstance<Websocket>();\n\t\treturn self.handleHeartbeat(ws, message);\n\t}\n\n\t/**\n\t * Get the server\n\t * @returns The server\n\t */\n\tpublic static Server() {\n\t\treturn this.GetInstance<Websocket>().server;\n\t}\n\n\t/**\n\t * Broadcast a message to a channel\n\t * @param channel - The channel\n\t * @param message - The message\n\t * @param args - The arguments\n\t */\n\tpublic static Broadcast(channel: string, message: WebsocketStructuredMessage, ...args: any[]) {\n\t\t// Get the server from the singleton instance\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\tif (!ws.server) {\n\t\t\tthrow new Error(\"Websocket server not set\");\n\t\t}\n\t\tws.server.publish(channel, JSON.stringify({ message, args }));\n\t}\n\n\t/**\n\t * Broadcast a message to all channels\n\t * @param message - The message\n\t * @param args - The arguments\n\t */\n\tpublic static BroadCastAll(message: WebsocketStructuredMessage, ...args: any[]) {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\tws._channels.forEach((channel) => channel.broadcast(message, ...args));\n\t}\n\n\t/**\n\t * Join a channel\n\t * @param channel - The channel\n\t * @param entity - The entity\n\t */\n\tpublic static Join(channel: string, entity: I_WebsocketEntity) {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\tconst client = ws._clients.get(entity.id);\n\t\tif (!client) return;\n\t\tws._channels.get(channel)?.addMember(client);\n\t}\n\n\t/**\n\t * Leave a channel\n\t * @param channel - The channel\n\t * @param entity - The entity\n\t */\n\tpublic static Leave(channel: string, entity: I_WebsocketEntity) {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\tconst client = ws._clients.get(entity.id);\n\t\tif (!client) return;\n\t\tws._channels.get(channel)?.removeMember(client);\n\t}\n\n\t/**\n\t * Get a client\n\t * @param id - The id of the client\n\t * @param throw_if_nil - Whether to throw an error if the client is not found\n\t * @returns The client\n\t */\n\tpublic static GetClient(id: string, throw_if_nil?: true): I_WebsocketClient;\n\tpublic static GetClient(id: string, throw_if_nil?: false): I_WebsocketClient | undefined;\n\tpublic static GetClient(id: string, throw_if_nil: boolean = true): I_WebsocketClient | undefined {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\tconst client = ws._clients.get(id);\n\t\tif (!client && throw_if_nil) throw new Error(`Client with id ${id} not found`);\n\t\treturn client;\n\t}\n\n\t/**\n\t * Get a channel\n\t * @param id - The id of the channel\n\t * @returns The channel\n\t */\n\tpublic static GetChannel(id: string) {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn ws._channels.get(id);\n\t}\n\n\t/**\n\t * Get all channels\n\t * @returns The channels\n\t */\n\tpublic static GetChannels() {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn Array.from(ws._channels.values());\n\t}\n\n\t/**\n\t * Get all clients\n\t * @returns The clients\n\t */\n\tpublic static GetClients() {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn Array.from(ws._clients.values());\n\t}\n\n\tpublic static GetLastId() {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn ws._lastId;\n\t}\n\n\t/**\n\t * Get the number of clients\n\t * @returns The number of clients\n\t */\n\tpublic static GetClientCount() {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn ws._clients.size;\n\t}\n\n\t/**\n\t * Get the number of channels\n\t * @returns The number of channels\n\t */\n\tpublic static GetChannelCount() {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn ws._channels.size;\n\t}\n\n\t/**\n\t * Create a client\n\t * @param entity - The entity\n\t * @returns The created client\n\t */\n\tpublic static CreateClient(entity: I_WebsocketEntity): I_WebsocketClient {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn ws.createClient(entity);\n\t}\n\n\t/**\n\t * Get all connected clients (excluding connecting/disconnecting)\n\t * @returns Array of connected clients\n\t */\n\tpublic static GetConnectedClients(): I_WebsocketClient[] {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\treturn Array.from(ws._clients.values()).filter((client) => client.state === \"connected\");\n\t}\n\n\t/**\n\t * Get client statistics by state\n\t * @returns Object with counts by state\n\t */\n\tpublic static GetClientStats() {\n\t\tconst ws = this.GetInstance<Websocket>();\n\t\tconst stats = {\n\t\t\ttotal: ws._clients.size,\n\t\t\tconnecting: 0,\n\t\t\tconnected: 0,\n\t\t\tdisconnecting: 0,\n\t\t\tdisconnected: 0,\n\t\t};\n\n\t\tfor (const client of ws._clients.values()) {\n\t\t\tswitch (client.state) {\n\t\t\t\tcase \"connecting\":\n\t\t\t\t\tstats.connecting++;\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"connected\":\n\t\t\t\t\tstats.connected++;\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"disconnecting\":\n\t\t\t\t\tstats.disconnecting++;\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"disconnected\":\n\t\t\t\t\tstats.disconnected++;\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\treturn stats;\n\t}\n}\n"]}
@@ -134,6 +134,8 @@ export interface I_WebsocketClient extends I_WebsocketEntity {
134
134
  send(message: string, options?: WebsocketMessageOptions): void;
135
135
  send(message: WebsocketStructuredMessage): void;
136
136
  subscribe(channel: string): any;
137
+ trackChannel(channel: I_WebsocketChannel): void;
138
+ untrackChannel(channel: I_WebsocketChannel): void;
137
139
  joinChannel(channel: I_WebsocketChannel, send?: boolean): {
138
140
  success: boolean;
139
141
  reason: string;
@@ -174,6 +176,12 @@ export type AddMemberResult = {
174
176
  export type AddMemberOptions = {
175
177
  /** Whether to notify client when channel is full (default: false) */
176
178
  notify_when_full?: boolean;
179
+ /** Whether to send welcome notification to client on successful join (default: false) */
180
+ notify?: boolean;
181
+ };
182
+ export type RemoveMemberOptions = {
183
+ /** Whether to send goodbye notification to client on successful leave (default: false) */
184
+ notify?: boolean;
177
185
  };
178
186
  export interface I_WebsocketChannel<T extends Websocket = Websocket> extends I_WebsocketChannelEntity<T> {
179
187
  limit: number;
@@ -183,8 +191,8 @@ export interface I_WebsocketChannel<T extends Websocket = Websocket> extends I_W
183
191
  broadcast(message: WebsocketStructuredMessage | string, options?: BroadcastOptions): void;
184
192
  hasMember(client: I_WebsocketEntity | string): boolean;
185
193
  addMember(entity: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult;
186
- removeMemberInternal(entity: I_WebsocketClient): void;
187
- removeMember(entity: I_WebsocketEntity): I_WebsocketClient | false;
194
+ removeFromMembersMap(entity: I_WebsocketClient): void;
195
+ removeMember(entity: I_WebsocketEntity, options?: RemoveMemberOptions): I_WebsocketClient | false;
188
196
  getMember(client: I_WebsocketEntity | string): I_WebsocketClient | undefined;
189
197
  getMembers(clients?: string[] | I_WebsocketEntity[]): I_WebsocketClient[];
190
198
  getMetadata(): Record<string, string>;
@@ -1 +1 @@
1
- {"version":3,"file":"websocket.types.js","sourceRoot":"","sources":["../../../../src/server/bun/websocket/websocket.types.ts"],"names":[],"mappings":"","sourcesContent":["import { ServerWebSocket, WebSocketHandler } from \"bun\";\nimport Channel from \"./Channel\";\nimport Websocket from \"./Websocket\";\nimport { E_ClientState } from \"./websocket.enums\";\n\nexport type BunWebsocketMessage = string | Buffer<ArrayBufferLike>;\n\nexport type WebsocketChannel<T extends I_WebsocketChannel = Channel> = Map<string, T>;\nexport type WebsocketClients = Map<string, I_WebsocketClient>;\nexport type WebsocketMessageOptions = {\n\t/**\n\t * Additional data to include in the message content\n\t * If an object is provided, it will be merged with the content\n\t * If a primitive value is provided, it will be added as content.data\n\t */\n\tdata?: any;\n\n\t/**\n\t * Client information to include in the message\n\t * Will be added as content.client\n\t */\n\tclient?: Partial<WebsocketEntityData> & {\n\t\t[key: string]: any;\n\t};\n\n\t/**\n\t * Channel metadata to include in the message\n\t * If true, all metadata will be included\n\t * If an array of strings, only the specified keys will be included\n\t */\n\tincludeMetadata?: boolean | string[];\n\n\t/**\n\t * Client IDs to exclude from receiving the broadcast\n\t * Useful for sending messages to all clients except the sender\n\t */\n\texcludeClients?: string[];\n\n\t/**\n\t * Channel to include in the message\n\t * Defaults to the channel of the message\n\t */\n\tchannel?: string;\n\n\t/**\n\t * Whether to include timestamp in the message\n\t * Defaults to true\n\t */\n\tincludeTimestamp?: boolean;\n\n\t/**\n\t * Custom fields to add to the root of the message\n\t * These will be merged with the message object\n\t */\n\tcustomFields?: Record<string, any>;\n\n\t/**\n\t * Transform function to modify the final message before sending\n\t * This is applied after all other processing\n\t */\n\ttransform?: (message: any) => any;\n\n\t/**\n\t * Priority of the message (higher numbers = higher priority)\n\t * Can be used by clients to determine processing order\n\t */\n\tpriority?: number;\n\n\t/**\n\t * Message expiration time in milliseconds since epoch\n\t * Can be used by clients to ignore outdated messages\n\t */\n\texpiresAt?: number;\n\n\t/**\n\t * Metadata to include in the message\n\t * If an array of strings, only the specified keys will be included\n\t */\n\tmetadata?: boolean | string[] | Record<string, string>;\n};\n\nexport type WebsocketMessage<T extends Record<string, any> = Record<string, any>> = {\n\t/**\n\t * Message type identifier used for client-side routing\n\t */\n\ttype: string;\n\t/**\n\t * Message content - can be any data structure\n\t * If a string is provided, it will be wrapped in {message: string}\n\t */\n\tcontent: T;\n\t/**\n\t * Channel ID\n\t */\n\tchannel?: string;\n\t/**\n\t * Timestamp of the message\n\t */\n\ttimestamp?: string;\n\t/**\n\t * Any additional custom fields\n\t */\n\t[key: string]: any;\n};\n\n/**\n * Message structure sent over the wire to clients.\n * This is the actual WebSocket payload format - transport options are NOT included.\n */\nexport type WebsocketStructuredMessage<T extends Record<string, any> = Record<string, any>> = {\n\t/** Message type identifier for client-side routing */\n\ttype: string;\n\n\t/** Message payload */\n\tcontent: T;\n\n\t/** Channel ID where message originated */\n\tchannel?: string;\n\n\t/** ISO timestamp when message was created */\n\ttimestamp?: string;\n\n\t/** Client information (who sent this) */\n\tclient?: WebsocketEntityData;\n\n\t/** Channel metadata (if included) */\n\tmetadata?: Record<string, string>;\n\n\t/** Message priority for client-side processing */\n\tpriority?: number;\n\n\t/** Expiration timestamp (milliseconds since epoch) */\n\texpiresAt?: number;\n\n\t/** Any additional custom fields */\n\t[key: string]: any;\n};\n\n/**\n * @deprecated This type incorrectly mixed transport options with wire format.\n * Use WebsocketStructuredMessage for wire format and WebsocketMessageOptions for options.\n * This will be removed in a future version.\n */\nexport type deprecated_WebsocketStructuredMessage<T extends Record<string, any> = Record<string, any>> = WebsocketMessage<T> & WebsocketMessageOptions;\n\nexport type WebsocketEntityId = string;\nexport type WebsocketEntityName = string;\nexport type WebsocketEntityData = { id: WebsocketEntityId; name: WebsocketEntityName };\n\nexport interface I_WebsocketEntity extends WebsocketEntityData {\n\tws: ServerWebSocket<WebsocketEntityData>;\n}\n\nexport interface I_WebsocketClient extends I_WebsocketEntity {\n\tchannels: WebsocketChannel<I_WebsocketChannel>;\n\tstate: E_ClientState;\n\tsend(message: string, options?: WebsocketMessageOptions): void;\n\tsend(message: WebsocketStructuredMessage): void;\n\tsubscribe(channel: string): any;\n\tjoinChannel(channel: I_WebsocketChannel, send?: boolean): { success: boolean; reason: string };\n\tleaveChannel(channel: I_WebsocketChannel, send?: boolean): void;\n\tjoinChannels(channels: I_WebsocketChannel[], send?: boolean): void;\n\tleaveChannels(channels?: I_WebsocketChannel[], send?: boolean): void;\n\tunsubscribe(channel: string): any;\n\twhoami(): WebsocketEntityData;\n\tcanReceiveMessages(): boolean;\n\tmarkConnected(): void;\n\tmarkDisconnecting(): void;\n\tmarkDisconnected(): void;\n\tgetConnectionInfo(): { id: string; name: string; state: E_ClientState; connectedAt?: Date; disconnectedAt?: Date; uptime: number; channelCount: number };\n}\n\nexport interface I_WebsocketChannelEntity<T extends Websocket = Websocket> extends WebsocketEntityData {\n\tws: T;\n}\n\n// New types for the broadcast method\nexport type BroadcastOptions = WebsocketMessageOptions & {\n\tdebug?: boolean;\n};\n\n// Result type for addMember operations\nexport type AddMemberResult = { success: true; client: I_WebsocketClient } | { success: false; reason: \"full\" | \"already_member\" | \"error\"; error?: Error };\n\n// Options for addMember operations\nexport type AddMemberOptions = {\n\t/** Whether to notify client when channel is full (default: false) */\n\tnotify_when_full?: boolean;\n};\nexport interface I_WebsocketChannel<T extends Websocket = Websocket> extends I_WebsocketChannelEntity<T> {\n\tlimit: number;\n\tmembers: Map<string, I_WebsocketClient>;\n\tmetadata: Record<string, string>;\n\tcreatedAt: Date;\n\tbroadcast(message: WebsocketStructuredMessage | string, options?: BroadcastOptions): void;\n\thasMember(client: I_WebsocketEntity | string): boolean;\n\taddMember(entity: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult;\n\tremoveMemberInternal(entity: I_WebsocketClient): void;\n\tremoveMember(entity: I_WebsocketEntity): I_WebsocketClient | false;\n\tgetMember(client: I_WebsocketEntity | string): I_WebsocketClient | undefined;\n\tgetMembers(clients?: string[] | I_WebsocketEntity[]): I_WebsocketClient[];\n\tgetMetadata(): Record<string, string>;\n\tgetCreatedAt(): Date;\n\tgetId(): string;\n\tgetSize(): number;\n\tgetLimit(): number;\n\tgetName(): string;\n\tcanAddMember(): boolean;\n}\n\n/**\n * Interface for implementing custom WebSocket behavior.\n *\n * @interface I_WebsocketInterface\n *\n * @property {Function} setup - Initializes the WebSocket handler with channels and clients\n *\n * The interface supports three optional handler methods:\n *\n * - `message`: Custom message handler that replaces the default handler\n * - `open`: Connection handler that runs after the default open handler\n * - `close`: Disconnection handler that runs before the default close handler\n */\nexport type WebsocketInterfaceHandlers = Partial<WebSocketHandler<WebsocketEntityData>>;\n\n/**\n * Interface for implementing custom WebSocket behavior.\n *\n * @interface I_WebsocketInterface\n *\n * @property {Function} setup - Initializes the WebSocket handler with channels and clients\n *\n * The interface supports three optional handler methods:\n *\n * - `message`: Custom message handler that replaces the default handler\n * - `open`: Connection handler that runs after the default open handler\n * - `close`: Disconnection handler that runs before the default close handler\n */\nexport interface I_WebsocketInterface {\n\thandlers: (channels: WebsocketChannel, clients: WebsocketClients) => WebsocketInterfaceHandlers;\n}\n"]}
1
+ {"version":3,"file":"websocket.types.js","sourceRoot":"","sources":["../../../../src/server/bun/websocket/websocket.types.ts"],"names":[],"mappings":"","sourcesContent":["import { ServerWebSocket, WebSocketHandler } from \"bun\";\nimport Channel from \"./Channel\";\nimport Websocket from \"./Websocket\";\nimport { E_ClientState } from \"./websocket.enums\";\n\nexport type BunWebsocketMessage = string | Buffer<ArrayBufferLike>;\n\nexport type WebsocketChannel<T extends I_WebsocketChannel = Channel> = Map<string, T>;\nexport type WebsocketClients = Map<string, I_WebsocketClient>;\nexport type WebsocketMessageOptions = {\n\t/**\n\t * Additional data to include in the message content\n\t * If an object is provided, it will be merged with the content\n\t * If a primitive value is provided, it will be added as content.data\n\t */\n\tdata?: any;\n\n\t/**\n\t * Client information to include in the message\n\t * Will be added as content.client\n\t */\n\tclient?: Partial<WebsocketEntityData> & {\n\t\t[key: string]: any;\n\t};\n\n\t/**\n\t * Channel metadata to include in the message\n\t * If true, all metadata will be included\n\t * If an array of strings, only the specified keys will be included\n\t */\n\tincludeMetadata?: boolean | string[];\n\n\t/**\n\t * Client IDs to exclude from receiving the broadcast\n\t * Useful for sending messages to all clients except the sender\n\t */\n\texcludeClients?: string[];\n\n\t/**\n\t * Channel to include in the message\n\t * Defaults to the channel of the message\n\t */\n\tchannel?: string;\n\n\t/**\n\t * Whether to include timestamp in the message\n\t * Defaults to true\n\t */\n\tincludeTimestamp?: boolean;\n\n\t/**\n\t * Custom fields to add to the root of the message\n\t * These will be merged with the message object\n\t */\n\tcustomFields?: Record<string, any>;\n\n\t/**\n\t * Transform function to modify the final message before sending\n\t * This is applied after all other processing\n\t */\n\ttransform?: (message: any) => any;\n\n\t/**\n\t * Priority of the message (higher numbers = higher priority)\n\t * Can be used by clients to determine processing order\n\t */\n\tpriority?: number;\n\n\t/**\n\t * Message expiration time in milliseconds since epoch\n\t * Can be used by clients to ignore outdated messages\n\t */\n\texpiresAt?: number;\n\n\t/**\n\t * Metadata to include in the message\n\t * If an array of strings, only the specified keys will be included\n\t */\n\tmetadata?: boolean | string[] | Record<string, string>;\n};\n\nexport type WebsocketMessage<T extends Record<string, any> = Record<string, any>> = {\n\t/**\n\t * Message type identifier used for client-side routing\n\t */\n\ttype: string;\n\t/**\n\t * Message content - can be any data structure\n\t * If a string is provided, it will be wrapped in {message: string}\n\t */\n\tcontent: T;\n\t/**\n\t * Channel ID\n\t */\n\tchannel?: string;\n\t/**\n\t * Timestamp of the message\n\t */\n\ttimestamp?: string;\n\t/**\n\t * Any additional custom fields\n\t */\n\t[key: string]: any;\n};\n\n/**\n * Message structure sent over the wire to clients.\n * This is the actual WebSocket payload format - transport options are NOT included.\n */\nexport type WebsocketStructuredMessage<T extends Record<string, any> = Record<string, any>> = {\n\t/** Message type identifier for client-side routing */\n\ttype: string;\n\n\t/** Message payload */\n\tcontent: T;\n\n\t/** Channel ID where message originated */\n\tchannel?: string;\n\n\t/** ISO timestamp when message was created */\n\ttimestamp?: string;\n\n\t/** Client information (who sent this) */\n\tclient?: WebsocketEntityData;\n\n\t/** Channel metadata (if included) */\n\tmetadata?: Record<string, string>;\n\n\t/** Message priority for client-side processing */\n\tpriority?: number;\n\n\t/** Expiration timestamp (milliseconds since epoch) */\n\texpiresAt?: number;\n\n\t/** Any additional custom fields */\n\t[key: string]: any;\n};\n\n/**\n * @deprecated This type incorrectly mixed transport options with wire format.\n * Use WebsocketStructuredMessage for wire format and WebsocketMessageOptions for options.\n * This will be removed in a future version.\n */\nexport type deprecated_WebsocketStructuredMessage<T extends Record<string, any> = Record<string, any>> = WebsocketMessage<T> & WebsocketMessageOptions;\n\nexport type WebsocketEntityId = string;\nexport type WebsocketEntityName = string;\nexport type WebsocketEntityData = { id: WebsocketEntityId; name: WebsocketEntityName };\n\nexport interface I_WebsocketEntity extends WebsocketEntityData {\n\tws: ServerWebSocket<WebsocketEntityData>;\n}\n\nexport interface I_WebsocketClient extends I_WebsocketEntity {\n\tchannels: WebsocketChannel<I_WebsocketChannel>;\n\tstate: E_ClientState;\n\tsend(message: string, options?: WebsocketMessageOptions): void;\n\tsend(message: WebsocketStructuredMessage): void;\n\tsubscribe(channel: string): any;\n\ttrackChannel(channel: I_WebsocketChannel): void;\n\tuntrackChannel(channel: I_WebsocketChannel): void;\n\tjoinChannel(channel: I_WebsocketChannel, send?: boolean): { success: boolean; reason: string };\n\tleaveChannel(channel: I_WebsocketChannel, send?: boolean): void;\n\tjoinChannels(channels: I_WebsocketChannel[], send?: boolean): void;\n\tleaveChannels(channels?: I_WebsocketChannel[], send?: boolean): void;\n\tunsubscribe(channel: string): any;\n\twhoami(): WebsocketEntityData;\n\tcanReceiveMessages(): boolean;\n\tmarkConnected(): void;\n\tmarkDisconnecting(): void;\n\tmarkDisconnected(): void;\n\tgetConnectionInfo(): { id: string; name: string; state: E_ClientState; connectedAt?: Date; disconnectedAt?: Date; uptime: number; channelCount: number };\n}\n\nexport interface I_WebsocketChannelEntity<T extends Websocket = Websocket> extends WebsocketEntityData {\n\tws: T;\n}\n\n// New types for the broadcast method\nexport type BroadcastOptions = WebsocketMessageOptions & {\n\tdebug?: boolean;\n};\n\n// Result type for addMember operations\nexport type AddMemberResult = { success: true; client: I_WebsocketClient } | { success: false; reason: \"full\" | \"already_member\" | \"error\"; error?: Error };\n\n// Options for addMember operations\nexport type AddMemberOptions = {\n\t/** Whether to notify client when channel is full (default: false) */\n\tnotify_when_full?: boolean;\n\t/** Whether to send welcome notification to client on successful join (default: false) */\n\tnotify?: boolean;\n};\n\n// Options for removeMember operations\nexport type RemoveMemberOptions = {\n\t/** Whether to send goodbye notification to client on successful leave (default: false) */\n\tnotify?: boolean;\n};\n\nexport interface I_WebsocketChannel<T extends Websocket = Websocket> extends I_WebsocketChannelEntity<T> {\n\tlimit: number;\n\tmembers: Map<string, I_WebsocketClient>;\n\tmetadata: Record<string, string>;\n\tcreatedAt: Date;\n\tbroadcast(message: WebsocketStructuredMessage | string, options?: BroadcastOptions): void;\n\thasMember(client: I_WebsocketEntity | string): boolean;\n\taddMember(entity: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult;\n\tremoveFromMembersMap(entity: I_WebsocketClient): void;\n\tremoveMember(entity: I_WebsocketEntity, options?: RemoveMemberOptions): I_WebsocketClient | false;\n\tgetMember(client: I_WebsocketEntity | string): I_WebsocketClient | undefined;\n\tgetMembers(clients?: string[] | I_WebsocketEntity[]): I_WebsocketClient[];\n\tgetMetadata(): Record<string, string>;\n\tgetCreatedAt(): Date;\n\tgetId(): string;\n\tgetSize(): number;\n\tgetLimit(): number;\n\tgetName(): string;\n\tcanAddMember(): boolean;\n}\n\n/**\n * Interface for implementing custom WebSocket behavior.\n *\n * @interface I_WebsocketInterface\n *\n * @property {Function} setup - Initializes the WebSocket handler with channels and clients\n *\n * The interface supports three optional handler methods:\n *\n * - `message`: Custom message handler that replaces the default handler\n * - `open`: Connection handler that runs after the default open handler\n * - `close`: Disconnection handler that runs before the default close handler\n */\nexport type WebsocketInterfaceHandlers = Partial<WebSocketHandler<WebsocketEntityData>>;\n\n/**\n * Interface for implementing custom WebSocket behavior.\n *\n * @interface I_WebsocketInterface\n *\n * @property {Function} setup - Initializes the WebSocket handler with channels and clients\n *\n * The interface supports three optional handler methods:\n *\n * - `message`: Custom message handler that replaces the default handler\n * - `open`: Connection handler that runs after the default open handler\n * - `close`: Disconnection handler that runs before the default close handler\n */\nexport interface I_WebsocketInterface {\n\thandlers: (channels: WebsocketChannel, clients: WebsocketClients) => WebsocketInterfaceHandlers;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "topsyde-utils",
3
- "version": "1.2.3",
3
+ "version": "1.3.1",
4
4
  "description": "A bundle of TypeScript utility classes and functions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -106,6 +106,7 @@ export {
106
106
  BroadcastOptions,
107
107
  AddMemberResult,
108
108
  AddMemberOptions,
109
+ RemoveMemberOptions,
109
110
  I_WebsocketChannel,
110
111
  WebsocketInterfaceHandlers,
111
112
  I_WebsocketInterface,
@@ -1,7 +1,17 @@
1
1
  import { Guards, Lib } from "../../../utils";
2
2
  import Message from "./Message";
3
3
  import Websocket from "./Websocket";
4
- import type { BroadcastOptions, I_WebsocketChannel, I_WebsocketClient, I_WebsocketEntity, WebsocketChannel, WebsocketMessage, AddMemberResult, AddMemberOptions } from "./websocket.types";
4
+ import type {
5
+ BroadcastOptions,
6
+ I_WebsocketChannel,
7
+ I_WebsocketClient,
8
+ I_WebsocketEntity,
9
+ WebsocketChannel,
10
+ WebsocketMessage,
11
+ AddMemberResult,
12
+ AddMemberOptions,
13
+ RemoveMemberOptions,
14
+ } from "./websocket.types";
5
15
  import { E_WebsocketMessageType } from "./websocket.enums";
6
16
 
7
17
  /**
@@ -95,19 +105,24 @@ export default class Channel<T extends Websocket = Websocket> implements I_Webso
95
105
  return this.members.has(client.id);
96
106
  }
97
107
 
98
- public addMember(client: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult {
108
+ /**
109
+ * ATOMIC: Add member to channel (membership only, no side effects)
110
+ * Internal method used for rollback-safe operations
111
+ * @internal
112
+ */
113
+ private addToMembersMap(client: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult {
99
114
  // Check if already a member
100
115
  if (this.members.has(client.id)) {
101
- return { success: false, reason: 'already_member' };
116
+ return { success: false, reason: "already_member" };
102
117
  }
103
118
 
104
- // Check capacity (atomic check)
105
- if (this.members.size >= this.limit) {
119
+ // Check capacity
120
+ if (!this.canAddMember()) {
106
121
  // Optionally notify client why they can't join
107
122
  if (options?.notify_when_full) {
108
123
  this.notifyChannelFull(client);
109
124
  }
110
- return { success: false, reason: 'full' };
125
+ return { success: false, reason: "full" };
111
126
  }
112
127
 
113
128
  try {
@@ -118,20 +133,73 @@ export default class Channel<T extends Websocket = Websocket> implements I_Webso
118
133
  this.members.delete(client.id);
119
134
  return {
120
135
  success: false,
121
- reason: 'error',
122
- error: error instanceof Error ? error : new Error(String(error))
136
+ reason: "error",
137
+ error: error instanceof Error ? error : new Error(String(error)),
123
138
  };
124
139
  }
125
140
  }
126
141
 
142
+ /**
143
+ * Add a client to this channel with full coordination
144
+ * Handles: membership + WebSocket subscription + client-side tracking + optional notification
145
+ * This ensures two-way coordination between channel and client
146
+ */
147
+ public addMember(client: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult {
148
+ // 1. Atomic membership add
149
+ const result = this.addToMembersMap(client, options);
150
+ if (!result.success) {
151
+ return result;
152
+ }
153
+
154
+ try {
155
+ // 2. Subscribe client's WebSocket to channel pub/sub topic
156
+ // CRITICAL: Without this, client won't receive channel.broadcast() messages
157
+ client.subscribe(this.id);
158
+
159
+ // 3. Track channel on client side (client's channels map)
160
+ client.trackChannel(this);
161
+
162
+ // 4. Optional welcome notification
163
+ if (options?.notify) {
164
+ client.send({
165
+ type: E_WebsocketMessageType.CLIENT_JOIN_CHANNEL,
166
+ content: { message: "Welcome to the channel" },
167
+ channel: this.id,
168
+ client: client.whoami(),
169
+ });
170
+ }
171
+
172
+ return result;
173
+ } catch (error) {
174
+ // Rollback on failure: remove membership + unsubscribe + untrack
175
+ this.removeFromMembersMap(client);
176
+ client.unsubscribe(this.id);
177
+ client.untrackChannel(this);
178
+ throw error;
179
+ }
180
+ }
181
+
182
+ public addMembers(clients: I_WebsocketClient[], options?: AddMemberOptions): AddMemberResult[] {
183
+ const results: AddMemberResult[] = [];
184
+ for (const client of clients) {
185
+ const result = this.addMember(client, options);
186
+ results.push(result);
187
+ if (!result.success) {
188
+ // Stop adding further members on failure
189
+ break;
190
+ }
191
+ }
192
+ return results;
193
+ }
194
+
127
195
  private notifyChannelFull(client: I_WebsocketClient): void {
128
196
  client.send({
129
197
  type: E_WebsocketMessageType.ERROR,
130
198
  content: {
131
199
  message: `Channel "${this.name}" is full (${this.limit} members)`,
132
- code: 'CHANNEL_FULL',
133
- channel: this.id
134
- }
200
+ code: "CHANNEL_FULL",
201
+ channel: this.id,
202
+ },
135
203
  });
136
204
  }
137
205
 
@@ -140,16 +208,40 @@ export default class Channel<T extends Websocket = Websocket> implements I_Webso
140
208
  * Used for rollback operations when joinChannel fails.
141
209
  * @internal
142
210
  */
143
- public removeMemberInternal(client: I_WebsocketClient): void {
211
+ public removeFromMembersMap(client: I_WebsocketClient): void {
144
212
  this.members.delete(client.id);
145
213
  }
146
214
 
147
- public removeMember(entity: I_WebsocketEntity) {
215
+ /**
216
+ * Remove a client from this channel with full coordination
217
+ * Handles: membership removal + WebSocket unsubscription + client-side tracking removal + optional notification
218
+ * This ensures two-way coordination between channel and client
219
+ */
220
+ public removeMember(entity: I_WebsocketEntity, options?: RemoveMemberOptions) {
221
+ // 1. Check if member exists
148
222
  if (!this.members.has(entity.id)) return false;
149
223
  const client = this.members.get(entity.id);
150
224
  if (!client) return false;
151
- client.leaveChannel(this);
152
- this.members.delete(entity.id);
225
+
226
+ // 2. Remove from channel members (atomic operation)
227
+ this.removeFromMembersMap(client);
228
+
229
+ // 3. Unsubscribe client's WebSocket from channel pub/sub topic
230
+ client.unsubscribe(this.id);
231
+
232
+ // 4. Untrack channel on client side (remove from client's channels map)
233
+ client.untrackChannel(this);
234
+
235
+ // 5. Optional goodbye notification
236
+ if (options?.notify) {
237
+ client.send({
238
+ type: E_WebsocketMessageType.CLIENT_LEAVE_CHANNEL,
239
+ content: { message: "You left the channel" },
240
+ channel: this.id,
241
+ client: client.whoami(),
242
+ });
243
+ }
244
+
153
245
  return client;
154
246
  }
155
247
 
@@ -192,6 +284,15 @@ export default class Channel<T extends Websocket = Websocket> implements I_Webso
192
284
  return size < this.limit;
193
285
  }
194
286
 
287
+ public delete() {
288
+ //first remove all members
289
+ this.members.forEach((member) => {
290
+ this.removeMember(member);
291
+ });
292
+ //then clear members map
293
+ this.members.clear();
294
+ }
295
+
195
296
  public static GetChannelType(channels: WebsocketChannel<I_WebsocketChannel> | undefined) {
196
297
  if (!channels) return Channel;
197
298
  if (channels.size > 0) {
@@ -111,6 +111,28 @@ export default class Client implements I_WebsocketClient {
111
111
  };
112
112
  }
113
113
 
114
+ /**
115
+ * HELPER: Track channel on client side (for channel.addMember coordination)
116
+ * Allows channel to update client's internal channel map
117
+ * @internal Used by channel.addMember()
118
+ */
119
+ public trackChannel(channel: I_WebsocketChannel): void {
120
+ this.channels.set(channel.getId(), channel);
121
+ }
122
+
123
+ /**
124
+ * HELPER: Untrack channel on client side (for channel.addMember rollback)
125
+ * Allows channel to remove from client's internal channel map during rollback
126
+ * @internal Used by channel.addMember() error handling
127
+ */
128
+ public untrackChannel(channel: I_WebsocketChannel): void {
129
+ this.channels.delete(channel.getId());
130
+ }
131
+
132
+ /**
133
+ * Join a channel (thin wrapper that delegates to channel.addMember)
134
+ * channel.addMember() handles all coordination: membership + subscription + tracking + notification
135
+ */
114
136
  public joinChannel(channel: I_WebsocketChannel, send: boolean = true): { success: boolean; reason: string } {
115
137
  const channel_id = channel.getId();
116
138
 
@@ -119,47 +141,30 @@ export default class Client implements I_WebsocketClient {
119
141
  return { success: false, reason: "already_member" };
120
142
  }
121
143
 
122
- // Try to add to channel first
123
- const result = channel.addMember(this);
144
+ // Delegate to channel (which now handles full coordination)
145
+ const result = channel.addMember(this, { notify: send });
146
+
124
147
  if (!result.success) {
125
- return { success: false, reason: result.reason }; // Channel full, already member, or other issue
148
+ return { success: false, reason: result.reason };
126
149
  }
127
150
 
128
- try {
129
- // Subscribe to channel's pub/sub topic
130
- this.subscribe(channel_id);
131
- this.channels.set(channel_id, channel);
132
-
133
- // Send join notification
134
- if (send) {
135
- this.send({
136
- type: E_WebsocketMessageType.CLIENT_JOIN_CHANNEL,
137
- content: { message: "Welcome to the channel" },
138
- channel: channel_id,
139
- client: this.whoami(),
140
- });
141
- }
142
-
143
- return { success: true, reason: "" };
144
- } catch (error) {
145
- // Rollback channel membership on failure
146
- channel.removeMemberInternal(this);
147
- this.channels.delete(channel_id);
148
- throw error;
149
- }
151
+ return { success: true, reason: "" };
150
152
  }
151
153
 
154
+ /**
155
+ * Leave a channel (thin wrapper that delegates to channel.removeMember)
156
+ * channel.removeMember() handles all coordination: membership removal + unsubscription + tracking removal + notification
157
+ */
152
158
  public leaveChannel(channel: I_WebsocketChannel, send: boolean = true) {
153
159
  const channel_id = channel.getId();
154
- this.channels.delete(channel_id);
155
- this.unsubscribe(channel_id);
156
- if (send)
157
- this.send({
158
- type: E_WebsocketMessageType.CLIENT_LEAVE_CHANNEL,
159
- content: { message: "(" + channel_id + ") Left the channel" },
160
- channel: channel_id,
161
- client: this.whoami(),
162
- });
160
+
161
+ // Check if we're in the channel
162
+ if (!this.channels.has(channel_id)) {
163
+ return;
164
+ }
165
+
166
+ // Delegate to channel (which now handles full coordination)
167
+ channel.removeMember(this, { notify: send });
163
168
  }
164
169
 
165
170
  public joinChannels(channels: I_WebsocketChannel[], send: boolean = true) {
@@ -103,6 +103,9 @@ export default class Websocket extends Singleton {
103
103
  * @param id - The id of the channel
104
104
  */
105
105
  public removeChannel(id: string) {
106
+ const channel = this._channels.get(id);
107
+ if (!channel) return;
108
+ channel.delete();
106
109
  this._channels.delete(id);
107
110
  }
108
111
 
@@ -157,6 +157,8 @@ export interface I_WebsocketClient extends I_WebsocketEntity {
157
157
  send(message: string, options?: WebsocketMessageOptions): void;
158
158
  send(message: WebsocketStructuredMessage): void;
159
159
  subscribe(channel: string): any;
160
+ trackChannel(channel: I_WebsocketChannel): void;
161
+ untrackChannel(channel: I_WebsocketChannel): void;
160
162
  joinChannel(channel: I_WebsocketChannel, send?: boolean): { success: boolean; reason: string };
161
163
  leaveChannel(channel: I_WebsocketChannel, send?: boolean): void;
162
164
  joinChannels(channels: I_WebsocketChannel[], send?: boolean): void;
@@ -186,7 +188,16 @@ export type AddMemberResult = { success: true; client: I_WebsocketClient } | { s
186
188
  export type AddMemberOptions = {
187
189
  /** Whether to notify client when channel is full (default: false) */
188
190
  notify_when_full?: boolean;
191
+ /** Whether to send welcome notification to client on successful join (default: false) */
192
+ notify?: boolean;
189
193
  };
194
+
195
+ // Options for removeMember operations
196
+ export type RemoveMemberOptions = {
197
+ /** Whether to send goodbye notification to client on successful leave (default: false) */
198
+ notify?: boolean;
199
+ };
200
+
190
201
  export interface I_WebsocketChannel<T extends Websocket = Websocket> extends I_WebsocketChannelEntity<T> {
191
202
  limit: number;
192
203
  members: Map<string, I_WebsocketClient>;
@@ -195,8 +206,8 @@ export interface I_WebsocketChannel<T extends Websocket = Websocket> extends I_W
195
206
  broadcast(message: WebsocketStructuredMessage | string, options?: BroadcastOptions): void;
196
207
  hasMember(client: I_WebsocketEntity | string): boolean;
197
208
  addMember(entity: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult;
198
- removeMemberInternal(entity: I_WebsocketClient): void;
199
- removeMember(entity: I_WebsocketEntity): I_WebsocketClient | false;
209
+ removeFromMembersMap(entity: I_WebsocketClient): void;
210
+ removeMember(entity: I_WebsocketEntity, options?: RemoveMemberOptions): I_WebsocketClient | false;
200
211
  getMember(client: I_WebsocketEntity | string): I_WebsocketClient | undefined;
201
212
  getMembers(clients?: string[] | I_WebsocketEntity[]): I_WebsocketClient[];
202
213
  getMetadata(): Record<string, string>;