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.
- package/dist/index.d.ts +2 -43
- package/dist/index.js +1 -38
- package/dist/index.js.map +1 -1
- package/dist/utils/Lib.d.ts +0 -12
- package/dist/utils/Lib.js +0 -65
- package/dist/utils/Lib.js.map +1 -1
- package/dist/utils/index.d.ts +0 -3
- package/dist/utils/index.js +0 -3
- package/dist/utils/index.js.map +1 -1
- package/dist/websocket.shared.types.d.ts +25 -0
- package/dist/websocket.shared.types.js +4 -0
- package/dist/websocket.shared.types.js.map +1 -0
- package/package.json +12 -66
- package/src/__tests__/singleton.test.ts +0 -143
- package/src/index.ts +2 -83
- package/src/utils/Lib.ts +0 -77
- package/src/utils/index.ts +0 -3
- package/src/websocket.shared.types.ts +27 -0
- package/dist/application.d.ts +0 -18
- package/dist/application.js +0 -60
- package/dist/application.js.map +0 -1
- package/dist/client/api/base.api.d.ts +0 -63
- package/dist/client/api/base.api.js +0 -61
- package/dist/client/api/base.api.js.map +0 -1
- package/dist/client/api/index.d.ts +0 -2
- package/dist/client/api/index.js +0 -5
- package/dist/client/api/index.js.map +0 -1
- package/dist/client/rxjs/index.d.ts +0 -1
- package/dist/client/rxjs/index.js +0 -4
- package/dist/client/rxjs/index.js.map +0 -1
- package/dist/client/rxjs/useRxjs.d.ts +0 -17
- package/dist/client/rxjs/useRxjs.js +0 -87
- package/dist/client/rxjs/useRxjs.js.map +0 -1
- package/dist/client/vite/plugins/index.d.ts +0 -2
- package/dist/client/vite/plugins/index.js +0 -5
- package/dist/client/vite/plugins/index.js.map +0 -1
- package/dist/client/vite/plugins/topsydeUtilsVitePlugin.d.ts +0 -9
- package/dist/client/vite/plugins/topsydeUtilsVitePlugin.js +0 -74
- package/dist/client/vite/plugins/topsydeUtilsVitePlugin.js.map +0 -1
- package/dist/external/index.d.ts +0 -1
- package/dist/external/index.js +0 -4
- package/dist/external/index.js.map +0 -1
- package/dist/external/re-exports.d.ts +0 -16
- package/dist/external/re-exports.js +0 -24
- package/dist/external/re-exports.js.map +0 -1
- package/dist/server/base/base.database.d.ts +0 -10
- package/dist/server/base/base.database.js +0 -23
- package/dist/server/base/base.database.js.map +0 -1
- package/dist/server/base/index.d.ts +0 -2
- package/dist/server/base/index.js +0 -5
- package/dist/server/base/index.js.map +0 -1
- package/dist/server/bun/index.d.ts +0 -3
- package/dist/server/bun/index.js +0 -6
- package/dist/server/bun/index.js.map +0 -1
- package/dist/server/bun/router/controller-discovery.d.ts +0 -13
- package/dist/server/bun/router/controller-discovery.js +0 -83
- package/dist/server/bun/router/controller-discovery.js.map +0 -1
- package/dist/server/bun/router/index.d.ts +0 -6
- package/dist/server/bun/router/index.js +0 -9
- package/dist/server/bun/router/index.js.map +0 -1
- package/dist/server/bun/router/router.d.ts +0 -12
- package/dist/server/bun/router/router.internal.d.ts +0 -15
- package/dist/server/bun/router/router.internal.js +0 -51
- package/dist/server/bun/router/router.internal.js.map +0 -1
- package/dist/server/bun/router/router.js +0 -38
- package/dist/server/bun/router/router.js.map +0 -1
- package/dist/server/bun/router/routes.d.ts +0 -5
- package/dist/server/bun/router/routes.js +0 -2
- package/dist/server/bun/router/routes.js.map +0 -1
- package/dist/server/bun/websocket/Channel.d.ts +0 -68
- package/dist/server/bun/websocket/Channel.js +0 -263
- package/dist/server/bun/websocket/Channel.js.map +0 -1
- package/dist/server/bun/websocket/Client.d.ts +0 -87
- package/dist/server/bun/websocket/Client.js +0 -193
- package/dist/server/bun/websocket/Client.js.map +0 -1
- package/dist/server/bun/websocket/Message.d.ts +0 -10
- package/dist/server/bun/websocket/Message.js +0 -103
- package/dist/server/bun/websocket/Message.js.map +0 -1
- package/dist/server/bun/websocket/Websocket.d.ts +0 -171
- package/dist/server/bun/websocket/Websocket.js +0 -336
- package/dist/server/bun/websocket/Websocket.js.map +0 -1
- package/dist/server/bun/websocket/index.d.ts +0 -11
- package/dist/server/bun/websocket/index.js +0 -14
- package/dist/server/bun/websocket/index.js.map +0 -1
- package/dist/server/bun/websocket/websocket.enums.d.ts +0 -27
- package/dist/server/bun/websocket/websocket.enums.js +0 -31
- package/dist/server/bun/websocket/websocket.enums.js.map +0 -1
- package/dist/server/bun/websocket/websocket.guards.d.ts +0 -3
- package/dist/server/bun/websocket/websocket.guards.js +0 -17
- package/dist/server/bun/websocket/websocket.guards.js.map +0 -1
- package/dist/server/bun/websocket/websocket.types.d.ts +0 -235
- package/dist/server/bun/websocket/websocket.types.js +0 -2
- package/dist/server/bun/websocket/websocket.types.js.map +0 -1
- package/dist/server/controller.d.ts +0 -62
- package/dist/server/controller.js +0 -55
- package/dist/server/controller.js.map +0 -1
- package/dist/server/index.d.ts +0 -4
- package/dist/server/index.js +0 -7
- package/dist/server/index.js.map +0 -1
- package/dist/server/service.d.ts +0 -5
- package/dist/server/service.js +0 -38
- package/dist/server/service.js.map +0 -1
- package/dist/utils/BaseDto.d.ts +0 -33
- package/dist/utils/BaseDto.js +0 -69
- package/dist/utils/BaseDto.js.map +0 -1
- package/dist/utils/BaseEntity.d.ts +0 -31
- package/dist/utils/BaseEntity.js +0 -37
- package/dist/utils/BaseEntity.js.map +0 -1
- package/dist/utils/dto_validators/IsNumberOrRangeConstraint.d.ts +0 -9
- package/dist/utils/dto_validators/IsNumberOrRangeConstraint.js +0 -85
- package/dist/utils/dto_validators/IsNumberOrRangeConstraint.js.map +0 -1
- package/dist/utils/dto_validators/index.d.ts +0 -1
- package/dist/utils/dto_validators/index.js +0 -4
- package/dist/utils/dto_validators/index.js.map +0 -1
- package/src/__tests__/app.test.ts +0 -206
- package/src/application.ts +0 -73
- package/src/client/api/base.api.ts +0 -111
- package/src/client/api/index.ts +0 -5
- package/src/client/rxjs/index.ts +0 -4
- package/src/client/rxjs/useRxjs.ts +0 -113
- package/src/client/vite/plugins/index.ts +0 -5
- package/src/client/vite/plugins/topsydeUtilsVitePlugin.ts +0 -80
- package/src/external/index.ts +0 -4
- package/src/external/re-exports.ts +0 -54
- package/src/server/base/base.database.ts +0 -31
- package/src/server/base/index.ts +0 -5
- package/src/server/bun/index.ts +0 -6
- package/src/server/bun/router/controller-discovery.ts +0 -94
- package/src/server/bun/router/index.ts +0 -9
- package/src/server/bun/router/router.internal.ts +0 -64
- package/src/server/bun/router/router.ts +0 -51
- package/src/server/bun/router/routes.ts +0 -7
- package/src/server/bun/websocket/Channel.ts +0 -310
- package/src/server/bun/websocket/Client.ts +0 -243
- package/src/server/bun/websocket/ISSUES.md +0 -1175
- package/src/server/bun/websocket/Message.ts +0 -120
- package/src/server/bun/websocket/Websocket.ts +0 -402
- package/src/server/bun/websocket/index.ts +0 -14
- package/src/server/bun/websocket/websocket.enums.ts +0 -29
- package/src/server/bun/websocket/websocket.guards.ts +0 -22
- package/src/server/bun/websocket/websocket.types.ts +0 -252
- package/src/server/controller.ts +0 -121
- package/src/server/index.ts +0 -7
- package/src/server/service.ts +0 -36
- package/src/utils/BaseDto.ts +0 -77
- package/src/utils/BaseEntity.ts +0 -49
- package/src/utils/dto_validators/IsNumberOrRangeConstraint.ts +0 -32
- 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
|
-
}
|