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.
Files changed (94) hide show
  1. package/dist/index.d.ts +2 -31
  2. package/dist/index.js +1 -27
  3. package/dist/index.js.map +1 -1
  4. package/dist/utils/Lib.d.ts +0 -12
  5. package/dist/utils/Lib.js +0 -65
  6. package/dist/utils/Lib.js.map +1 -1
  7. package/dist/websocket.shared.types.d.ts +25 -0
  8. package/dist/websocket.shared.types.js +4 -0
  9. package/dist/websocket.shared.types.js.map +1 -0
  10. package/package.json +1 -22
  11. package/src/index.ts +2 -51
  12. package/src/utils/Lib.ts +0 -77
  13. package/src/websocket.shared.types.ts +27 -0
  14. package/dist/application.d.ts +0 -18
  15. package/dist/application.js +0 -60
  16. package/dist/application.js.map +0 -1
  17. package/dist/server/base/base.database.d.ts +0 -10
  18. package/dist/server/base/base.database.js +0 -23
  19. package/dist/server/base/base.database.js.map +0 -1
  20. package/dist/server/base/index.d.ts +0 -2
  21. package/dist/server/base/index.js +0 -5
  22. package/dist/server/base/index.js.map +0 -1
  23. package/dist/server/bun/index.d.ts +0 -3
  24. package/dist/server/bun/index.js +0 -6
  25. package/dist/server/bun/index.js.map +0 -1
  26. package/dist/server/bun/router/controller-discovery.d.ts +0 -13
  27. package/dist/server/bun/router/controller-discovery.js +0 -83
  28. package/dist/server/bun/router/controller-discovery.js.map +0 -1
  29. package/dist/server/bun/router/index.d.ts +0 -6
  30. package/dist/server/bun/router/index.js +0 -9
  31. package/dist/server/bun/router/index.js.map +0 -1
  32. package/dist/server/bun/router/router.d.ts +0 -12
  33. package/dist/server/bun/router/router.internal.d.ts +0 -15
  34. package/dist/server/bun/router/router.internal.js +0 -51
  35. package/dist/server/bun/router/router.internal.js.map +0 -1
  36. package/dist/server/bun/router/router.js +0 -38
  37. package/dist/server/bun/router/router.js.map +0 -1
  38. package/dist/server/bun/router/routes.d.ts +0 -5
  39. package/dist/server/bun/router/routes.js +0 -2
  40. package/dist/server/bun/router/routes.js.map +0 -1
  41. package/dist/server/bun/websocket/Channel.d.ts +0 -68
  42. package/dist/server/bun/websocket/Channel.js +0 -263
  43. package/dist/server/bun/websocket/Channel.js.map +0 -1
  44. package/dist/server/bun/websocket/Client.d.ts +0 -87
  45. package/dist/server/bun/websocket/Client.js +0 -193
  46. package/dist/server/bun/websocket/Client.js.map +0 -1
  47. package/dist/server/bun/websocket/Message.d.ts +0 -10
  48. package/dist/server/bun/websocket/Message.js +0 -103
  49. package/dist/server/bun/websocket/Message.js.map +0 -1
  50. package/dist/server/bun/websocket/Websocket.d.ts +0 -171
  51. package/dist/server/bun/websocket/Websocket.js +0 -336
  52. package/dist/server/bun/websocket/Websocket.js.map +0 -1
  53. package/dist/server/bun/websocket/index.d.ts +0 -11
  54. package/dist/server/bun/websocket/index.js +0 -14
  55. package/dist/server/bun/websocket/index.js.map +0 -1
  56. package/dist/server/bun/websocket/websocket.enums.d.ts +0 -27
  57. package/dist/server/bun/websocket/websocket.enums.js +0 -31
  58. package/dist/server/bun/websocket/websocket.enums.js.map +0 -1
  59. package/dist/server/bun/websocket/websocket.guards.d.ts +0 -3
  60. package/dist/server/bun/websocket/websocket.guards.js +0 -17
  61. package/dist/server/bun/websocket/websocket.guards.js.map +0 -1
  62. package/dist/server/bun/websocket/websocket.types.d.ts +0 -235
  63. package/dist/server/bun/websocket/websocket.types.js +0 -2
  64. package/dist/server/bun/websocket/websocket.types.js.map +0 -1
  65. package/dist/server/controller.d.ts +0 -62
  66. package/dist/server/controller.js +0 -55
  67. package/dist/server/controller.js.map +0 -1
  68. package/dist/server/index.d.ts +0 -4
  69. package/dist/server/index.js +0 -7
  70. package/dist/server/index.js.map +0 -1
  71. package/dist/server/service.d.ts +0 -5
  72. package/dist/server/service.js +0 -38
  73. package/dist/server/service.js.map +0 -1
  74. package/src/application.ts +0 -73
  75. package/src/server/base/base.database.ts +0 -31
  76. package/src/server/base/index.ts +0 -5
  77. package/src/server/bun/index.ts +0 -6
  78. package/src/server/bun/router/controller-discovery.ts +0 -94
  79. package/src/server/bun/router/index.ts +0 -9
  80. package/src/server/bun/router/router.internal.ts +0 -64
  81. package/src/server/bun/router/router.ts +0 -51
  82. package/src/server/bun/router/routes.ts +0 -7
  83. package/src/server/bun/websocket/Channel.ts +0 -310
  84. package/src/server/bun/websocket/Client.ts +0 -243
  85. package/src/server/bun/websocket/ISSUES.md +0 -1175
  86. package/src/server/bun/websocket/Message.ts +0 -120
  87. package/src/server/bun/websocket/Websocket.ts +0 -402
  88. package/src/server/bun/websocket/index.ts +0 -14
  89. package/src/server/bun/websocket/websocket.enums.ts +0 -29
  90. package/src/server/bun/websocket/websocket.guards.ts +0 -22
  91. package/src/server/bun/websocket/websocket.types.ts +0 -252
  92. package/src/server/controller.ts +0 -121
  93. package/src/server/index.ts +0 -7
  94. 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,7 +0,0 @@
1
- import Controller from "../../controller";
2
-
3
- export type Routes = {
4
- [key: string]: new () => Controller;
5
- };
6
-
7
- export const routes: Routes = {};
@@ -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
- }