topsyde-utils 1.3.1 → 2.0.0
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 -31
- package/dist/index.js +1 -27
- 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/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 +1 -22
- package/src/index.ts +2 -51
- package/src/utils/Lib.ts +0 -77
- 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/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/src/application.ts +0 -73
- 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
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { join } from 'path';
|
|
2
|
-
import { readdirSync, statSync } from 'fs';
|
|
3
|
-
import { Lib } from '../../../utils';
|
|
4
|
-
import { Routes } from './routes';
|
|
5
|
-
|
|
6
|
-
const fallbackRoutes: Routes = {};
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Dynamically discovers and loads controllers from the components directory
|
|
10
|
-
*/
|
|
11
|
-
export class ControllerDiscovery {
|
|
12
|
-
public static async DiscoverRoutes(componentPaths: string[]) {
|
|
13
|
-
try {
|
|
14
|
-
const allDiscoveredRoutes: Routes = {};
|
|
15
|
-
|
|
16
|
-
// Discover controllers in all specified component paths
|
|
17
|
-
for (const path of componentPaths) {
|
|
18
|
-
const discoveredRoutes = await ControllerDiscovery.Find(path);
|
|
19
|
-
|
|
20
|
-
// Merge discovered routes
|
|
21
|
-
Object.assign(allDiscoveredRoutes, discoveredRoutes);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Use discovered routes if any were found, otherwise use fallback
|
|
25
|
-
if (Object.keys(allDiscoveredRoutes).length > 0) {
|
|
26
|
-
Lib.Log(`Using auto-discovered routes from paths: ${componentPaths.join(', ')}`);
|
|
27
|
-
return allDiscoveredRoutes;
|
|
28
|
-
} else {
|
|
29
|
-
Lib.Log('No routes discovered, using fallback routes');
|
|
30
|
-
return fallbackRoutes;
|
|
31
|
-
}
|
|
32
|
-
} catch (error) {
|
|
33
|
-
// If auto-discovery fails, use fallback routes
|
|
34
|
-
Lib.Warn('Controller auto-discovery failed, using fallback routes:', error);
|
|
35
|
-
return fallbackRoutes;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Discovers controllers in the specified directory
|
|
41
|
-
* @param componentsPath Optional custom path to components directory (relative to project root)
|
|
42
|
-
* @returns Routes object with discovered controllers
|
|
43
|
-
*/
|
|
44
|
-
public static async Find(componentsPath?: string): Promise<Routes> {
|
|
45
|
-
const routes: Routes = {};
|
|
46
|
-
|
|
47
|
-
// Get project root - use process.cwd() to get the root of the project using this library
|
|
48
|
-
const projectRoot = process.cwd();
|
|
49
|
-
|
|
50
|
-
// Use provided path or default to components directory
|
|
51
|
-
const componentsDir = componentsPath
|
|
52
|
-
? join(projectRoot, componentsPath) // From project root
|
|
53
|
-
: join(projectRoot, 'src', 'components'); // Default location
|
|
54
|
-
|
|
55
|
-
Lib.Log(`Looking for controllers in: ${componentsDir}`);
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
// Get all component directories
|
|
59
|
-
const componentFolders = readdirSync(componentsDir).filter(folder =>
|
|
60
|
-
statSync(join(componentsDir, folder)).isDirectory()
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
// Process each component folder
|
|
64
|
-
for (const componentName of componentFolders) {
|
|
65
|
-
const controllerPath = join(componentsDir, componentName, `${componentName}.controller.ts`);
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
// Check if controller file exists
|
|
69
|
-
const controllerFile = Bun.file(controllerPath);
|
|
70
|
-
if (await controllerFile.exists()) {
|
|
71
|
-
// Import the controller
|
|
72
|
-
try {
|
|
73
|
-
const module = await import(controllerPath);
|
|
74
|
-
const Controller = module.default;
|
|
75
|
-
|
|
76
|
-
if (Controller && typeof Controller === 'function') {
|
|
77
|
-
routes[componentName] = Controller;
|
|
78
|
-
Lib.Log(`Registered controller: ${componentName}`);
|
|
79
|
-
}
|
|
80
|
-
} catch (err) {
|
|
81
|
-
Lib.Warn(`Failed to import controller for ${componentName}:`, err);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
} catch (err) {
|
|
85
|
-
Lib.Warn(`Error processing component ${componentName}:`, err);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
} catch (err) {
|
|
89
|
-
Lib.Warn(`Error discovering controllers in ${componentsDir}:`, err);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return routes;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// This file is auto-generated by scripts/generate-indexes.ts
|
|
2
|
-
// Do not edit this file directly
|
|
3
|
-
|
|
4
|
-
export * from './controller-discovery';
|
|
5
|
-
export * from './routes';
|
|
6
|
-
export * from './router';
|
|
7
|
-
export * from './router.internal';
|
|
8
|
-
export { default as Router } from './router';
|
|
9
|
-
export { default as Router_Internal } from './router.internal';
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { Throwable } from "../../..";
|
|
2
|
-
import { ERROR_CODE } from "../../../errors";
|
|
3
|
-
import Controller from "../../controller";
|
|
4
|
-
import { Debug } from "../../../utils/Lib";
|
|
5
|
-
import { Routes } from "./routes";
|
|
6
|
-
|
|
7
|
-
class Router_Internal {
|
|
8
|
-
private registry = new Map<string, Controller>();
|
|
9
|
-
private routes: Routes;
|
|
10
|
-
|
|
11
|
-
constructor(routes?: Routes) {
|
|
12
|
-
this.routes = routes ?? {};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async post<T>(req: Request): Promise<T> {
|
|
16
|
-
return await this.handleRequest(req);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async get<T>(req: Request): Promise<T> {
|
|
20
|
-
return await this.handleRequest(req);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
private async handleRequest<T>(request: Request): Promise<T> {
|
|
24
|
-
const path = this.getPath(request);
|
|
25
|
-
const output = await this.resolve(path).call<T>(request);
|
|
26
|
-
return output;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
private getPath(request: Request): string {
|
|
30
|
-
return new URL(request.url).pathname;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private resolve(path: string): Controller {
|
|
34
|
-
const controllerKey = path.split("/")[1];
|
|
35
|
-
return this.register(controllerKey, () => this.controllerFactory(controllerKey));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
private register(controllerKey: string, factory: () => Controller): Controller {
|
|
39
|
-
if (!this.registry.has(controllerKey)) {
|
|
40
|
-
this.registry.set(controllerKey, factory());
|
|
41
|
-
Debug.Log(`Caching controller: /${controllerKey}`);
|
|
42
|
-
}
|
|
43
|
-
return this.registry.get(controllerKey) as Controller;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
public setRoutes(routes: Routes): void {
|
|
47
|
-
this.routes = routes;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
private controllerFactory(controllerKey: string): Controller {
|
|
51
|
-
try {
|
|
52
|
-
if (!(controllerKey in this.routes)) throw new Throwable(`${ERROR_CODE.INVALID_CONTROLLER}: ${controllerKey}`, { logError: false });
|
|
53
|
-
|
|
54
|
-
const ControllerClass = this.routes[controllerKey as keyof typeof this.routes];
|
|
55
|
-
|
|
56
|
-
return new ControllerClass();
|
|
57
|
-
} catch (err) {
|
|
58
|
-
console.error("controllerFactory", err);
|
|
59
|
-
throw err;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export default Router_Internal;
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { ERROR_CODE } from "../../../errors";
|
|
2
|
-
import Singleton from "../../../singleton";
|
|
3
|
-
import Guards from "../../../utils/Guards";
|
|
4
|
-
import Router_Internal from "./router.internal";
|
|
5
|
-
import { Routes } from "./routes";
|
|
6
|
-
|
|
7
|
-
type MethodMap<T> = {
|
|
8
|
-
[method: string]: (req: Request) => Promise<T>;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
class Router extends Singleton {
|
|
12
|
-
private internal: Router_Internal;
|
|
13
|
-
|
|
14
|
-
public constructor(routes?: Routes) {
|
|
15
|
-
super();
|
|
16
|
-
this.internal = new Router_Internal(routes);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
private setRoutes(routes: Routes): void {
|
|
20
|
-
this.internal.setRoutes(routes);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
public static async Call<T>(request: Request): Promise<T> {
|
|
24
|
-
if (Guards.IsNil(request)) throw ERROR_CODE.NO_REQUEST;
|
|
25
|
-
const methods: MethodMap<T> = this.getMethodMap();
|
|
26
|
-
const method = methods[request.method];
|
|
27
|
-
|
|
28
|
-
if (Guards.IsNil(method)) throw ERROR_CODE.INVALID_METHOD;
|
|
29
|
-
|
|
30
|
-
return await method(request);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
public static SetRoutes(routes: Routes) {
|
|
34
|
-
this.GetInstance<Router>().setRoutes(routes);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
private static getMethodMap<T>(): MethodMap<T> {
|
|
38
|
-
const router = this.GetInstance<Router>();
|
|
39
|
-
return {
|
|
40
|
-
GET: async (req) => await router.internal.get(req),
|
|
41
|
-
POST: async (req) => await router.internal.post(req),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
public static GetQueryParams(request: Request): URLSearchParams {
|
|
46
|
-
const url = new URL(request.url);
|
|
47
|
-
return url.searchParams;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export default Router;
|
|
@@ -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);
|
|
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
|
-
}
|