topsyde-utils 1.3.2 → 2.0.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.
Files changed (148) hide show
  1. package/dist/index.d.ts +2 -43
  2. package/dist/index.js +1 -38
  3. package/dist/index.js.map +1 -1
  4. package/dist/utils/Lib.d.ts +0 -12
  5. package/dist/utils/Lib.js +0 -65
  6. package/dist/utils/Lib.js.map +1 -1
  7. package/dist/utils/index.d.ts +0 -3
  8. package/dist/utils/index.js +0 -3
  9. package/dist/utils/index.js.map +1 -1
  10. package/dist/websocket.shared.types.d.ts +25 -0
  11. package/dist/websocket.shared.types.js +4 -0
  12. package/dist/websocket.shared.types.js.map +1 -0
  13. package/package.json +12 -66
  14. package/src/__tests__/singleton.test.ts +0 -143
  15. package/src/index.ts +2 -83
  16. package/src/utils/Lib.ts +0 -77
  17. package/src/utils/index.ts +0 -3
  18. package/src/websocket.shared.types.ts +27 -0
  19. package/dist/application.d.ts +0 -18
  20. package/dist/application.js +0 -60
  21. package/dist/application.js.map +0 -1
  22. package/dist/client/api/base.api.d.ts +0 -63
  23. package/dist/client/api/base.api.js +0 -61
  24. package/dist/client/api/base.api.js.map +0 -1
  25. package/dist/client/api/index.d.ts +0 -2
  26. package/dist/client/api/index.js +0 -5
  27. package/dist/client/api/index.js.map +0 -1
  28. package/dist/client/rxjs/index.d.ts +0 -1
  29. package/dist/client/rxjs/index.js +0 -4
  30. package/dist/client/rxjs/index.js.map +0 -1
  31. package/dist/client/rxjs/useRxjs.d.ts +0 -17
  32. package/dist/client/rxjs/useRxjs.js +0 -87
  33. package/dist/client/rxjs/useRxjs.js.map +0 -1
  34. package/dist/client/vite/plugins/index.d.ts +0 -2
  35. package/dist/client/vite/plugins/index.js +0 -5
  36. package/dist/client/vite/plugins/index.js.map +0 -1
  37. package/dist/client/vite/plugins/topsydeUtilsVitePlugin.d.ts +0 -9
  38. package/dist/client/vite/plugins/topsydeUtilsVitePlugin.js +0 -74
  39. package/dist/client/vite/plugins/topsydeUtilsVitePlugin.js.map +0 -1
  40. package/dist/external/index.d.ts +0 -1
  41. package/dist/external/index.js +0 -4
  42. package/dist/external/index.js.map +0 -1
  43. package/dist/external/re-exports.d.ts +0 -16
  44. package/dist/external/re-exports.js +0 -24
  45. package/dist/external/re-exports.js.map +0 -1
  46. package/dist/server/base/base.database.d.ts +0 -10
  47. package/dist/server/base/base.database.js +0 -23
  48. package/dist/server/base/base.database.js.map +0 -1
  49. package/dist/server/base/index.d.ts +0 -2
  50. package/dist/server/base/index.js +0 -5
  51. package/dist/server/base/index.js.map +0 -1
  52. package/dist/server/bun/index.d.ts +0 -3
  53. package/dist/server/bun/index.js +0 -6
  54. package/dist/server/bun/index.js.map +0 -1
  55. package/dist/server/bun/router/controller-discovery.d.ts +0 -13
  56. package/dist/server/bun/router/controller-discovery.js +0 -83
  57. package/dist/server/bun/router/controller-discovery.js.map +0 -1
  58. package/dist/server/bun/router/index.d.ts +0 -6
  59. package/dist/server/bun/router/index.js +0 -9
  60. package/dist/server/bun/router/index.js.map +0 -1
  61. package/dist/server/bun/router/router.d.ts +0 -12
  62. package/dist/server/bun/router/router.internal.d.ts +0 -15
  63. package/dist/server/bun/router/router.internal.js +0 -51
  64. package/dist/server/bun/router/router.internal.js.map +0 -1
  65. package/dist/server/bun/router/router.js +0 -38
  66. package/dist/server/bun/router/router.js.map +0 -1
  67. package/dist/server/bun/router/routes.d.ts +0 -5
  68. package/dist/server/bun/router/routes.js +0 -2
  69. package/dist/server/bun/router/routes.js.map +0 -1
  70. package/dist/server/bun/websocket/Channel.d.ts +0 -68
  71. package/dist/server/bun/websocket/Channel.js +0 -263
  72. package/dist/server/bun/websocket/Channel.js.map +0 -1
  73. package/dist/server/bun/websocket/Client.d.ts +0 -87
  74. package/dist/server/bun/websocket/Client.js +0 -193
  75. package/dist/server/bun/websocket/Client.js.map +0 -1
  76. package/dist/server/bun/websocket/Message.d.ts +0 -10
  77. package/dist/server/bun/websocket/Message.js +0 -103
  78. package/dist/server/bun/websocket/Message.js.map +0 -1
  79. package/dist/server/bun/websocket/Websocket.d.ts +0 -171
  80. package/dist/server/bun/websocket/Websocket.js +0 -336
  81. package/dist/server/bun/websocket/Websocket.js.map +0 -1
  82. package/dist/server/bun/websocket/index.d.ts +0 -11
  83. package/dist/server/bun/websocket/index.js +0 -14
  84. package/dist/server/bun/websocket/index.js.map +0 -1
  85. package/dist/server/bun/websocket/websocket.enums.d.ts +0 -27
  86. package/dist/server/bun/websocket/websocket.enums.js +0 -31
  87. package/dist/server/bun/websocket/websocket.enums.js.map +0 -1
  88. package/dist/server/bun/websocket/websocket.guards.d.ts +0 -3
  89. package/dist/server/bun/websocket/websocket.guards.js +0 -17
  90. package/dist/server/bun/websocket/websocket.guards.js.map +0 -1
  91. package/dist/server/bun/websocket/websocket.types.d.ts +0 -235
  92. package/dist/server/bun/websocket/websocket.types.js +0 -2
  93. package/dist/server/bun/websocket/websocket.types.js.map +0 -1
  94. package/dist/server/controller.d.ts +0 -62
  95. package/dist/server/controller.js +0 -55
  96. package/dist/server/controller.js.map +0 -1
  97. package/dist/server/index.d.ts +0 -4
  98. package/dist/server/index.js +0 -7
  99. package/dist/server/index.js.map +0 -1
  100. package/dist/server/service.d.ts +0 -5
  101. package/dist/server/service.js +0 -38
  102. package/dist/server/service.js.map +0 -1
  103. package/dist/utils/BaseDto.d.ts +0 -33
  104. package/dist/utils/BaseDto.js +0 -69
  105. package/dist/utils/BaseDto.js.map +0 -1
  106. package/dist/utils/BaseEntity.d.ts +0 -31
  107. package/dist/utils/BaseEntity.js +0 -37
  108. package/dist/utils/BaseEntity.js.map +0 -1
  109. package/dist/utils/dto_validators/IsNumberOrRangeConstraint.d.ts +0 -9
  110. package/dist/utils/dto_validators/IsNumberOrRangeConstraint.js +0 -85
  111. package/dist/utils/dto_validators/IsNumberOrRangeConstraint.js.map +0 -1
  112. package/dist/utils/dto_validators/index.d.ts +0 -1
  113. package/dist/utils/dto_validators/index.js +0 -4
  114. package/dist/utils/dto_validators/index.js.map +0 -1
  115. package/src/__tests__/app.test.ts +0 -206
  116. package/src/application.ts +0 -73
  117. package/src/client/api/base.api.ts +0 -111
  118. package/src/client/api/index.ts +0 -5
  119. package/src/client/rxjs/index.ts +0 -4
  120. package/src/client/rxjs/useRxjs.ts +0 -113
  121. package/src/client/vite/plugins/index.ts +0 -5
  122. package/src/client/vite/plugins/topsydeUtilsVitePlugin.ts +0 -80
  123. package/src/external/index.ts +0 -4
  124. package/src/external/re-exports.ts +0 -54
  125. package/src/server/base/base.database.ts +0 -31
  126. package/src/server/base/index.ts +0 -5
  127. package/src/server/bun/index.ts +0 -6
  128. package/src/server/bun/router/controller-discovery.ts +0 -94
  129. package/src/server/bun/router/index.ts +0 -9
  130. package/src/server/bun/router/router.internal.ts +0 -64
  131. package/src/server/bun/router/router.ts +0 -51
  132. package/src/server/bun/router/routes.ts +0 -7
  133. package/src/server/bun/websocket/Channel.ts +0 -310
  134. package/src/server/bun/websocket/Client.ts +0 -243
  135. package/src/server/bun/websocket/ISSUES.md +0 -1175
  136. package/src/server/bun/websocket/Message.ts +0 -120
  137. package/src/server/bun/websocket/Websocket.ts +0 -402
  138. package/src/server/bun/websocket/index.ts +0 -14
  139. package/src/server/bun/websocket/websocket.enums.ts +0 -29
  140. package/src/server/bun/websocket/websocket.guards.ts +0 -22
  141. package/src/server/bun/websocket/websocket.types.ts +0 -252
  142. package/src/server/controller.ts +0 -121
  143. package/src/server/index.ts +0 -7
  144. package/src/server/service.ts +0 -36
  145. package/src/utils/BaseDto.ts +0 -77
  146. package/src/utils/BaseEntity.ts +0 -49
  147. package/src/utils/dto_validators/IsNumberOrRangeConstraint.ts +0 -32
  148. package/src/utils/dto_validators/index.ts +0 -4
@@ -1,310 +0,0 @@
1
- import { Guards, Lib } from "../../../utils";
2
- import Message from "./Message";
3
- import Websocket from "./Websocket";
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";
15
- import { E_WebsocketMessageType } from "./websocket.enums";
16
-
17
- /**
18
- * Channel - Pub/sub topic for WebSocket clients
19
- *
20
- * ## Membership Contract
21
- * - `addMember()` validates capacity and adds to `members` map
22
- * - Client drives join via `joinChannel()` which subscribes and handles rollback
23
- * - If subscription fails, membership is automatically rolled back
24
- * - Member count never exceeds `limit`
25
- *
26
- * @example
27
- * const channel = new Channel("game-1", "Game Room", ws, 10);
28
- * const result = channel.addMember(client);
29
- * if (result.success) {
30
- * channel.broadcast({ type: "player.joined", content: { player: client.whoami() } });
31
- * }
32
- */
33
- export default class Channel<T extends Websocket = Websocket> implements I_WebsocketChannel<T> {
34
- public createdAt: Date = new Date();
35
- public id: string;
36
- public name: string;
37
- public limit: number;
38
- public members: Map<string, I_WebsocketClient>;
39
- public metadata: Record<string, string>;
40
- public ws: T;
41
-
42
- constructor(id: string, name: string, ws: T, limit?: number, members?: Map<string, I_WebsocketClient>, metadata?: Record<string, string>) {
43
- this.id = id;
44
- this.name = name;
45
- this.limit = limit ?? 5;
46
- this.members = members ?? new Map();
47
- this.metadata = metadata ?? {};
48
- this.ws = ws;
49
- }
50
-
51
- public broadcast(message: WebsocketMessage | string, options?: BroadcastOptions) {
52
- if (Guards.IsString(message)) {
53
- const msg: WebsocketMessage = {
54
- type: "message",
55
- content: { message },
56
- };
57
- message = msg;
58
- }
59
-
60
- const output = Message.Create(message, { ...options, channel: this.id });
61
-
62
- // Include channel metadata if requested
63
- if (options?.includeMetadata) {
64
- output.metadata = options.includeMetadata === true ? this.getMetadata() : this.getFilteredMetadata(options.includeMetadata);
65
- }
66
-
67
- const serializedMessage = Message.Serialize(output);
68
-
69
- // If we need to exclude clients, send individually to prevent excluded clients from receiving
70
- if (options?.excludeClients && options.excludeClients.length > 0) {
71
- const excludeSet = new Set(options.excludeClients); // O(1) lookup
72
-
73
- for (const [clientId, client] of this.members) {
74
- if (!excludeSet.has(clientId)) {
75
- try {
76
- client.ws.send(serializedMessage);
77
- } catch (error) {
78
- Lib.Warn(`Failed to send to client ${clientId}:`, error);
79
- }
80
- }
81
- }
82
- return;
83
- }
84
-
85
- // Otherwise use pub/sub for everyone
86
- this.ws.server.publish(this.id, serializedMessage);
87
- }
88
-
89
- // Helper method for filtered metadata
90
- private getFilteredMetadata(keys: string[]) {
91
- const metadata = this.getMetadata();
92
- const filtered: Record<string, string> = {};
93
-
94
- for (const key of keys) {
95
- if (metadata[key] !== undefined) {
96
- filtered[key] = metadata[key];
97
- }
98
- }
99
-
100
- return filtered;
101
- }
102
-
103
- public hasMember(client: I_WebsocketEntity | string) {
104
- if (typeof client === "string") return this.members.has(client);
105
- return this.members.has(client.id);
106
- }
107
-
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 {
114
- // Check if already a member
115
- if (this.members.has(client.id)) {
116
- return { success: false, reason: "already_member" };
117
- }
118
-
119
- // Check capacity
120
- if (!this.canAddMember()) {
121
- // Optionally notify client why they can't join
122
- if (options?.notify_when_full) {
123
- this.notifyChannelFull(client);
124
- }
125
- return { success: false, reason: "full" };
126
- }
127
-
128
- try {
129
- this.members.set(client.id, client);
130
- return { success: true, client };
131
- } catch (error) {
132
- // Rollback
133
- this.members.delete(client.id);
134
- return {
135
- success: false,
136
- reason: "error",
137
- error: error instanceof Error ? error : new Error(String(error)),
138
- };
139
- }
140
- }
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
-
195
- private notifyChannelFull(client: I_WebsocketClient): void {
196
- client.send({
197
- type: E_WebsocketMessageType.ERROR,
198
- content: {
199
- message: `Channel "${this.name}" is full (${this.limit} members)`,
200
- code: "CHANNEL_FULL",
201
- channel: this.id,
202
- },
203
- });
204
- }
205
-
206
- /**
207
- * Internal method to remove a member without triggering client-side cleanup.
208
- * Used for rollback operations when joinChannel fails.
209
- * @internal
210
- */
211
- public removeFromMembersMap(client: I_WebsocketClient): void {
212
- this.members.delete(client.id);
213
- }
214
-
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
222
- if (!this.members.has(entity.id)) return false;
223
- const client = this.members.get(entity.id);
224
- if (!client) return false;
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
-
245
- return client;
246
- }
247
-
248
- public getMember(client: I_WebsocketEntity | string) {
249
- if (typeof client === "string") return this.members.get(client);
250
- return this.members.get(client.id);
251
- }
252
-
253
- public getMembers(clients?: string[] | I_WebsocketEntity[]): I_WebsocketClient[] {
254
- if (!clients) return Array.from(this.members.values());
255
- return clients.map((client) => this.getMember(client)).filter((client) => client !== undefined) as I_WebsocketClient[];
256
- }
257
-
258
- public getMetadata() {
259
- return this.metadata;
260
- }
261
-
262
- public getCreatedAt() {
263
- return this.createdAt;
264
- }
265
-
266
- public getId() {
267
- return this.id;
268
- }
269
-
270
- public getName() {
271
- return this.name;
272
- }
273
-
274
- public getLimit() {
275
- return this.limit;
276
- }
277
-
278
- public getSize() {
279
- return this.members.size;
280
- }
281
-
282
- public canAddMember() {
283
- const size = this.getSize();
284
- return size < this.limit;
285
- }
286
-
287
- public delete() {
288
- //first remove all members
289
- this.members.forEach((member) => {
290
- this.removeMember(member, { notify: true });
291
- });
292
- //then clear members map
293
- this.members.clear();
294
- }
295
-
296
- public static GetChannelType(channels: WebsocketChannel<I_WebsocketChannel> | undefined) {
297
- if (!channels) return Channel;
298
- if (channels.size > 0) {
299
- const firstChannel = channels.values().next().value;
300
- if (firstChannel) {
301
- return firstChannel.constructor as typeof Channel;
302
- } else {
303
- return Channel;
304
- }
305
- } else {
306
- Lib.Warn("Channels are empty, using default channel class");
307
- return Channel;
308
- }
309
- }
310
- }
@@ -1,243 +0,0 @@
1
- import { ServerWebSocket } from "bun";
2
- import type {
3
- I_WebsocketClient,
4
- WebsocketEntityData,
5
- WebsocketChannel,
6
- WebsocketStructuredMessage,
7
- I_WebsocketEntity,
8
- I_WebsocketChannel,
9
- WebsocketMessageOptions,
10
- WebsocketMessage,
11
- } from "./websocket.types";
12
- import { E_WebsocketMessageType, E_ClientState } from "./websocket.enums";
13
- import { Guards, Lib } from "../../../utils";
14
- import Message from "./Message";
15
-
16
- /**
17
- * Client - Connected WebSocket client with channel membership
18
- *
19
- * ## Channel Membership
20
- * - Maintains own channel list and handles Bun pub/sub subscriptions
21
- * - `joinChannel()` adds to channel, subscribes, and handles rollback on failure
22
- * - Always use `channel.addMember(client)` in application code, not `client.joinChannel()` directly
23
- *
24
- * @example
25
- * // ✅ Correct
26
- * channel.addMember(client);
27
- *
28
- * // ❌ Incorrect - internal use only
29
- * client.joinChannel(channel);
30
- */
31
- export default class Client implements I_WebsocketClient {
32
- private _id: string;
33
- private _name: string;
34
- private _ws: ServerWebSocket<WebsocketEntityData>;
35
- private _channels: WebsocketChannel<I_WebsocketChannel>;
36
- private _state: E_ClientState;
37
- private _connectedAt?: Date;
38
- private _disconnectedAt?: Date;
39
-
40
- private set ws(value: ServerWebSocket<WebsocketEntityData>) {
41
- this._ws = value;
42
- }
43
-
44
- public get ws(): ServerWebSocket<WebsocketEntityData> {
45
- return this._ws;
46
- }
47
-
48
- private set id(value: string) {
49
- this._id = value;
50
- }
51
-
52
- public get id(): string {
53
- return this._id;
54
- }
55
-
56
- public get name(): string {
57
- return this._name;
58
- }
59
-
60
- private set name(value: string) {
61
- this._name = value;
62
- }
63
-
64
- private set channels(value: WebsocketChannel<I_WebsocketChannel>) {
65
- this._channels = value;
66
- }
67
-
68
- public get channels(): WebsocketChannel<I_WebsocketChannel> {
69
- return this._channels;
70
- }
71
-
72
- public get state(): E_ClientState {
73
- return this._state;
74
- }
75
-
76
- constructor(entity: I_WebsocketEntity) {
77
- this._id = entity.id;
78
- this._name = entity.name;
79
- this._ws = entity.ws;
80
- this._channels = new Map();
81
- this._state = E_ClientState.CONNECTING;
82
- }
83
-
84
- public canReceiveMessages(): boolean {
85
- return this._state === E_ClientState.CONNECTED || this._state === E_ClientState.DISCONNECTING;
86
- }
87
-
88
- public markConnected(): void {
89
- this._state = E_ClientState.CONNECTED;
90
- this._connectedAt = new Date();
91
- }
92
-
93
- public markDisconnecting(): void {
94
- this._state = E_ClientState.DISCONNECTING;
95
- }
96
-
97
- public markDisconnected(): void {
98
- this._state = E_ClientState.DISCONNECTED;
99
- this._disconnectedAt = new Date();
100
- }
101
-
102
- public getConnectionInfo() {
103
- return {
104
- id: this.id,
105
- name: this.name,
106
- state: this._state,
107
- connectedAt: this._connectedAt,
108
- disconnectedAt: this._disconnectedAt,
109
- uptime: this._connectedAt ? Date.now() - this._connectedAt.getTime() : 0,
110
- channelCount: this._channels.size,
111
- };
112
- }
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
- */
136
- public joinChannel(channel: I_WebsocketChannel, send: boolean = true): { success: boolean; reason: string } {
137
- const channel_id = channel.getId();
138
-
139
- // Check if already joined
140
- if (this.channels.has(channel_id)) {
141
- return { success: false, reason: "already_member" };
142
- }
143
-
144
- // Delegate to channel (which now handles full coordination)
145
- const result = channel.addMember(this, { notify: send });
146
-
147
- if (!result.success) {
148
- return { success: false, reason: result.reason };
149
- }
150
-
151
- return { success: true, reason: "" };
152
- }
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
- */
158
- public leaveChannel(channel: I_WebsocketChannel, send: boolean = true) {
159
- const channel_id = channel.getId();
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 });
168
- }
169
-
170
- public joinChannels(channels: I_WebsocketChannel[], send: boolean = true) {
171
- channels.forEach((channel) => {
172
- this.joinChannel(channel, false);
173
- });
174
- if (send) this.send({ type: E_WebsocketMessageType.CLIENT_JOIN_CHANNELS, content: { channels }, client: this.whoami() });
175
- }
176
-
177
- public leaveChannels(channels?: I_WebsocketChannel[], send: boolean = true) {
178
- if (!channels) channels = Array.from(this.channels.values());
179
- channels.forEach((channel) => {
180
- this.leaveChannel(channel, false);
181
- });
182
- if (send) this.send({ type: E_WebsocketMessageType.CLIENT_LEAVE_CHANNELS, content: { channels }, client: this.whoami() });
183
- }
184
-
185
- public whoami(): { id: string; name: string } {
186
- return { id: this.id, name: this.name };
187
- }
188
-
189
- public send(message: string, options?: WebsocketMessageOptions): void;
190
- public send(message: WebsocketStructuredMessage): void;
191
- public send(message: WebsocketStructuredMessage | string, options?: WebsocketMessageOptions): void {
192
- // Check state before sending
193
- if (!this.canReceiveMessages()) {
194
- Lib.Warn(`Cannot send to client ${this.id} in state ${this._state}`);
195
- return;
196
- }
197
-
198
- try {
199
- if (Guards.IsString(message)) {
200
- const msg: WebsocketMessage = {
201
- type: "message",
202
- content: { message },
203
- };
204
- message = Message.Create(msg, options);
205
- }
206
- this.ws.send(JSON.stringify({ client: this.whoami(), ...message }));
207
- } catch (error) {
208
- Lib.Warn(`Failed to send message to client ${this.id}:`, error);
209
- if (error instanceof Error && error.message.includes("closed")) {
210
- this.markDisconnected();
211
- }
212
- }
213
- }
214
-
215
- public subscribe(channel: string): void {
216
- this.ws.subscribe(channel);
217
- }
218
-
219
- public unsubscribe(channel: string): void {
220
- this.ws.unsubscribe(channel);
221
- }
222
-
223
- public static GetClientType(clients: Map<string, I_WebsocketClient> | undefined): typeof Client {
224
- if (!clients) return Client;
225
- if (clients.size > 0) {
226
- const firstClient = clients.values().next().value;
227
- if (firstClient) {
228
- return firstClient.constructor as typeof Client;
229
- }
230
- }
231
-
232
- // Fallback to default Client class
233
- Lib.Warn("Clients map is empty, using default client class");
234
- return Client;
235
- }
236
-
237
- public static System() {
238
- return <WebsocketEntityData>{
239
- id: "system",
240
- name: "System",
241
- };
242
- }
243
- }