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