underpost 3.2.8 → 3.2.10

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 (92) hide show
  1. package/.github/workflows/npmpkg.ci.yml +1 -0
  2. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  3. package/.github/workflows/release.cd.yml +1 -0
  4. package/.vscode/settings.json +10 -5
  5. package/CHANGELOG.md +223 -2
  6. package/CLI-HELP.md +36 -7
  7. package/README.md +38 -9
  8. package/bin/build.js +27 -11
  9. package/bin/deploy.js +20 -21
  10. package/bin/file.js +32 -13
  11. package/bin/index.js +2 -1
  12. package/bin/vs.js +1 -1
  13. package/bump.config.js +26 -0
  14. package/conf.js +20 -4
  15. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  16. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
  17. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  18. package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
  19. package/manifests/kind-config-dev.yaml +8 -0
  20. package/manifests/mongodb/pv-pvc.yaml +44 -8
  21. package/manifests/mongodb/statefulset.yaml +55 -68
  22. package/package.json +40 -25
  23. package/scripts/k3s-node-setup.sh +30 -11
  24. package/scripts/nat-iptables.sh +103 -18
  25. package/src/api/core/core.router.js +19 -14
  26. package/src/api/core/core.service.js +5 -5
  27. package/src/api/default/default.router.js +22 -18
  28. package/src/api/default/default.service.js +5 -5
  29. package/src/api/document/document.router.js +28 -23
  30. package/src/api/document/document.service.js +100 -23
  31. package/src/api/file/file.router.js +19 -13
  32. package/src/api/file/file.service.js +9 -7
  33. package/src/api/test/test.router.js +17 -12
  34. package/src/api/types.js +24 -0
  35. package/src/api/user/guest.service.js +5 -4
  36. package/src/api/user/user.router.js +297 -288
  37. package/src/api/user/user.service.js +100 -35
  38. package/src/cli/baremetal.js +20 -11
  39. package/src/cli/cluster.js +243 -55
  40. package/src/cli/db.js +106 -62
  41. package/src/cli/deploy.js +297 -154
  42. package/src/cli/fs.js +19 -3
  43. package/src/cli/index.js +37 -9
  44. package/src/cli/ipfs.js +4 -6
  45. package/src/cli/kubectl.js +4 -1
  46. package/src/cli/lxd.js +217 -135
  47. package/src/cli/release.js +289 -131
  48. package/src/cli/repository.js +91 -34
  49. package/src/cli/run.js +297 -56
  50. package/src/cli/test.js +9 -3
  51. package/src/client/Default.index.js +9 -3
  52. package/src/client/components/core/Auth.js +19 -5
  53. package/src/client/components/core/Docs.js +6 -34
  54. package/src/client/components/core/FileExplorer.js +6 -6
  55. package/src/client/components/core/Modal.js +65 -2
  56. package/src/client/components/core/PanelForm.js +56 -52
  57. package/src/client/components/core/Recover.js +4 -4
  58. package/src/client/components/core/Worker.js +170 -350
  59. package/src/client/services/default/default.management.js +20 -25
  60. package/src/client/services/user/guest.service.js +10 -3
  61. package/src/client/sw/core.sw.js +174 -112
  62. package/src/db/DataBaseProvider.js +120 -20
  63. package/src/db/mongo/MongoBootstrap.js +587 -0
  64. package/src/db/mongo/MongooseDB.js +126 -22
  65. package/src/index.js +1 -1
  66. package/src/runtime/express/Express.js +2 -2
  67. package/src/runtime/wp/Wp.js +8 -5
  68. package/src/server/auth.js +2 -2
  69. package/src/server/client-build-docs.js +1 -1
  70. package/src/server/client-build.js +94 -129
  71. package/src/server/conf.js +20 -65
  72. package/src/server/data-query.js +32 -20
  73. package/src/server/dns.js +22 -0
  74. package/src/server/process.js +180 -19
  75. package/src/server/runtime.js +1 -1
  76. package/src/server/start.js +26 -7
  77. package/src/server/valkey.js +9 -2
  78. package/src/ws/IoInterface.js +16 -16
  79. package/src/ws/core/channels/core.ws.chat.js +11 -11
  80. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  81. package/src/ws/core/channels/core.ws.stream.js +19 -19
  82. package/src/ws/core/core.ws.connection.js +8 -8
  83. package/src/ws/core/core.ws.server.js +6 -5
  84. package/src/ws/default/channels/default.ws.main.js +10 -10
  85. package/src/ws/default/default.ws.connection.js +4 -4
  86. package/src/ws/default/default.ws.server.js +4 -3
  87. package/typedoc.json +10 -1
  88. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  89. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  90. /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
  91. /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
  92. /package/src/client/ssr/{pages → views}/Test.js +0 -0
@@ -52,7 +52,7 @@ class UnderpostStartUp {
52
52
  * @param {Function} logic - The logic to execute when the server is listening.
53
53
  * @returns {Object} An object with a listen method.
54
54
  */
55
- listenServerFactory: (logic = async () => {}) => {
55
+ listenServerFactory: (logic = async () => { }) => {
56
56
  return {
57
57
  listen: async (...args) => {
58
58
  const msDelta = 1000;
@@ -133,11 +133,19 @@ class UnderpostStartUp {
133
133
  * @param {boolean} options.underpostQuicklyInstall - Whether to use underpost quickly install.
134
134
  * @param {boolean} options.skipPullBase - Whether to skip pulling the base code.
135
135
  * @param {boolean} options.skipFullBuild - Whether to skip building the full client bundle.
136
+ * @param {boolean} options.pullBundle - When true, download pre-built client bundle from Cloudinary via pull-bundle before starting.
136
137
  */
137
138
  async callback(
138
139
  deployId = 'dd-default',
139
140
  env = 'development',
140
- options = { build: false, run: false, underpostQuicklyInstall: false, skipPullBase: false, skipFullBuild: false },
141
+ options = {
142
+ build: false,
143
+ run: false,
144
+ underpostQuicklyInstall: false,
145
+ skipPullBase: false,
146
+ skipFullBuild: false,
147
+ pullBundle: false,
148
+ },
141
149
  ) {
142
150
  Underpost.env.set('container-status', `${deployId}-${env}-build-deployment`);
143
151
  if (options.build === true) await Underpost.start.build(deployId, env, options);
@@ -152,12 +160,14 @@ class UnderpostStartUp {
152
160
  * @param {boolean} options.skipPullBase - Whether to skip pulling the base code and use the current workspace code directly.
153
161
  * @param {boolean} options.underpostQuicklyInstall - Whether to use underpost quickly install.
154
162
  * @param {boolean} options.skipFullBuild - Whether to skip building the full client bundle.
163
+ * @param {boolean} options.pullBundle - When true, download pre-built client bundle from Cloudinary via pull-bundle (must be pushed first with push-bundle).
164
+ * This flag is independent of skipFullBuild: it can be combined with skipFullBuild or used alone.
155
165
  * @memberof UnderpostStartUp
156
166
  */
157
167
  async build(
158
168
  deployId = 'dd-default',
159
169
  env = 'development',
160
- options = { underpostQuicklyInstall: false, skipPullBase: false, skipFullBuild: false },
170
+ options = { underpostQuicklyInstall: false, skipPullBase: false, skipFullBuild: false, pullBundle: false },
161
171
  ) {
162
172
  const buildBasePath = `/home/dd`;
163
173
  const repoName = `engine-${deployId.split('-')[1]}`;
@@ -176,7 +186,8 @@ class UnderpostStartUp {
176
186
  for (const itcScript of itcScripts)
177
187
  if (itcScript.match(deployId)) shellExec(`node ./engine-private/itc-scripts/${itcScript}`);
178
188
  }
179
- if (!options.skipFullBuild) shellExec(`node bin client ${deployId}`);
189
+ if (options.pullBundle === true) shellExec(`node bin run pull-bundle --deploy-id ${deployId}`);
190
+ else if (!options.skipFullBuild) shellExec(`node bin client ${deployId}`);
180
191
  },
181
192
  /**
182
193
  * Runs a deployment.
@@ -187,20 +198,28 @@ class UnderpostStartUp {
187
198
  */
188
199
  async run(deployId = 'dd-default', env = 'development', options = {}) {
189
200
  const runCmd = env === 'production' ? 'run prod:container' : 'run dev:container';
201
+ const makeDeployCallback = (cmd) => (code, out, msg) => {
202
+ if (code !== 0) {
203
+ logger.error(`Deployment process exited with code ${code}`, { cmd, msg });
204
+ Underpost.env.set('container-status', 'error');
205
+ }
206
+ };
190
207
  if (fs.existsSync(`./engine-private/replica`)) {
191
208
  const replicas = await fs.readdir(`./engine-private/replica`);
192
209
  for (const replica of replicas) {
193
210
  if (!replica.match(deployId)) continue;
194
211
  shellExec(`node bin env ${replica} ${env}`);
195
- shellExec(`npm ${runCmd} ${replica}`, { async: true });
212
+ const replicaCmd = `npm ${runCmd} ${replica}`;
213
+ shellExec(replicaCmd, { async: true, callback: makeDeployCallback(replicaCmd) });
196
214
  await awaitDeployMonitor(true);
197
215
  }
198
216
  }
199
217
  shellExec(`node bin env ${deployId} ${env}`);
200
- shellExec(`npm ${runCmd} ${deployId}`, { async: true });
218
+ const deployCmd = `npm ${runCmd} ${deployId}`;
219
+ shellExec(deployCmd, { async: true, callback: makeDeployCallback(deployCmd) });
201
220
  await awaitDeployMonitor(true);
202
221
  if (env === 'production' && Underpost.env.isInsideContainer()) Underpost.secret.globalSecretClean();
203
- Underpost.env.set('container-status', `${deployId}-${env}-running-deployment`);
222
+ if (Underpost.env.get('container-status') !== 'error') Underpost.env.set('container-status', `${deployId}-${env}-running-deployment`);
204
223
  },
205
224
  };
206
225
  }
@@ -22,7 +22,7 @@ const logger = loggerFactory(import.meta);
22
22
  /** @type {Record<string, import('iovalkey').default>} */
23
23
  const ValkeyInstances = {};
24
24
 
25
- /** @type {Record<string, 'connected' | 'error'>} */
25
+ /** @type {Record<string, 'connected' | 'reconnecting' | 'error'>} */
26
26
  const ValkeyStatus = {};
27
27
 
28
28
  /**
@@ -57,7 +57,10 @@ const createValkeyConnection = async (instance = {}, connectionOptions = {}) =>
57
57
  const client = new Valkey({
58
58
  port: connectionOptions.port ?? undefined,
59
59
  host: connectionOptions.host ?? undefined,
60
- retryStrategy: (attempt) => (attempt === 1 ? undefined : 1000),
60
+ // Retry indefinitely with capped exponential backoff (1 s 30 s)
61
+ retryStrategy: (attempt) => Math.min(attempt * 1000, 30000),
62
+ // Fail commands immediately when not connected; do NOT queue them
63
+ maxRetriesPerRequest: 0,
61
64
  });
62
65
 
63
66
  client.on('ready', () => {
@@ -68,6 +71,10 @@ const createValkeyConnection = async (instance = {}, connectionOptions = {}) =>
68
71
  ValkeyStatus[key] = 'error';
69
72
  logger.error('Valkey error', { err: err?.message, instance });
70
73
  });
74
+ client.on('reconnecting', () => {
75
+ ValkeyStatus[key] = 'reconnecting';
76
+ logger.warn('Valkey reconnecting...', { instance });
77
+ });
71
78
  client.on('end', () => {
72
79
  ValkeyStatus[key] = 'error';
73
80
  logger.warn('Valkey connection ended', { instance });
@@ -46,9 +46,9 @@ class IoChannel {
46
46
  constructor(IoInterface) {
47
47
  this.#IoInterface = {
48
48
  channel: '',
49
- connection: async (socket = {}, client = {}, wsManagementId = '') => {},
50
- controller: async (socket = {}, client = {}, payload = {}, wsManagementId = '', args = []) => {},
51
- disconnect: async (socket = {}, client = {}, reason = '', wsManagementId = '') => {},
49
+ connection: async (socket = {}, client = {}, hostKeyContext = '') => { },
50
+ controller: async (socket = {}, client = {}, payload = {}, hostKeyContext = '', args = []) => { },
51
+ disconnect: async (socket = {}, client = {}, reason = '', hostKeyContext = '') => { },
52
52
  stream: false,
53
53
  ...IoInterface,
54
54
  };
@@ -68,18 +68,18 @@ class IoChannel {
68
68
  * Sets up the listener for the channel message.
69
69
  *
70
70
  * @param {Socket} socket - The Socket.IO socket object.
71
- * @param {string} wsManagementId - Unique identifier for the WebSocket management context.
71
+ * @param {string} hostKeyContext - Unique identifier for the WebSocket management context.
72
72
  * @returns {Promise<void>}
73
73
  */
74
- async connection(socket, wsManagementId) {
74
+ async connection(socket, hostKeyContext) {
75
75
  try {
76
76
  this.client[socket.id] = socket;
77
77
  // Use bind/arrow function to maintain 'this' context for the controller
78
- socket.on(this.channel, (...args) => this.controller(socket, args, wsManagementId));
79
- await this.#IoInterface.connection(socket, this.client, wsManagementId);
78
+ socket.on(this.channel, (...args) => this.controller(socket, args, hostKeyContext));
79
+ await this.#IoInterface.connection(socket, this.client, hostKeyContext);
80
80
  logger.debug(`Socket ${socket.id} connected to channel ${this.channel}`);
81
81
  } catch (error) {
82
- logger.error(error, { channel: this.channel, wsManagementId, stack: error.stack });
82
+ logger.error(error, { channel: this.channel, hostKeyContext, stack: error.stack });
83
83
  }
84
84
  }
85
85
 
@@ -89,10 +89,10 @@ class IoChannel {
89
89
  * @method
90
90
  * @param {Socket} socket - The Socket.IO socket object.
91
91
  * @param {any[]} args - The raw arguments received from the socket event.
92
- * @param {string} wsManagementId - Unique identifier for the WebSocket management context.
92
+ * @param {string} hostKeyContext - Unique identifier for the WebSocket management context.
93
93
  * @returns {Promise<void>}
94
94
  */
95
- async controller(socket, args, wsManagementId) {
95
+ async controller(socket, args, hostKeyContext) {
96
96
  try {
97
97
  if (!args || args.length === 0) {
98
98
  logger.warn(`No arguments received for channel: ${this.channel}`, { socketId: socket.id });
@@ -101,9 +101,9 @@ class IoChannel {
101
101
  // Determine if JSON parsing is needed based on the stream flag
102
102
  const payload = this.#IoInterface.stream ? args[0] : JSON.parse(args[0]);
103
103
 
104
- await this.#IoInterface.controller(socket, this.client, payload, wsManagementId, args);
104
+ await this.#IoInterface.controller(socket, this.client, payload, hostKeyContext, args);
105
105
  } catch (error) {
106
- logger.error(error, { channel: this.channel, wsManagementId, socketId: socket.id, args, stack: error.stack });
106
+ logger.error(error, { channel: this.channel, hostKeyContext, socketId: socket.id, args, stack: error.stack });
107
107
  }
108
108
  }
109
109
 
@@ -112,16 +112,16 @@ class IoChannel {
112
112
  *
113
113
  * @param {Socket} socket - The Socket.IO socket object.
114
114
  * @param {string} reason - The reason for disconnection (e.g., 'client namespace disconnect').
115
- * @param {string} wsManagementId - Unique identifier for the WebSocket management context.
115
+ * @param {string} hostKeyContext - Unique identifier for the WebSocket management context.
116
116
  * @returns {Promise<void>}
117
117
  */
118
- async disconnect(socket, reason, wsManagementId) {
118
+ async disconnect(socket, reason, hostKeyContext) {
119
119
  try {
120
- await this.#IoInterface.disconnect(socket, this.client, reason, wsManagementId);
120
+ await this.#IoInterface.disconnect(socket, this.client, reason, hostKeyContext);
121
121
  delete this.client[socket.id];
122
122
  logger.debug(`Socket ${socket.id} disconnected from channel ${this.channel}. Reason: ${reason}`);
123
123
  } catch (error) {
124
- logger.error(error, { channel: this.channel, wsManagementId, reason, socketId: socket.id, stack: error.stack });
124
+ logger.error(error, { channel: this.channel, hostKeyContext, reason, socketId: socket.id, stack: error.stack });
125
125
  }
126
126
  }
127
127
  }
@@ -12,13 +12,13 @@ import { CoreWsEmitter } from '../core.ws.emit.js';
12
12
  * Broadcasts incoming messages to all other connected sockets.
13
13
  */
14
14
  class CoreWsChatChannel {
15
- /** @type {Object.<string, Object>} Per-instance state keyed by wsManagementId. */
15
+ /** @type {Object.<string, Object>} Per-instance state keyed by hostKeyContext. */
16
16
  static #state = {};
17
17
 
18
18
  /** @type {IoChannel} */
19
19
  static #io = new IoChannel({
20
20
  channel: 'chat',
21
- controller(socket, client, payload, wsManagementId) {
21
+ controller(socket, client, payload, hostKeyContext) {
22
22
  for (const socketId of Object.keys(client)) {
23
23
  if (socketId !== socket.id) {
24
24
  CoreWsEmitter.emit('chat', client[socketId], { id: socket.id, ...payload });
@@ -39,29 +39,29 @@ class CoreWsChatChannel {
39
39
 
40
40
  /**
41
41
  * Initializes state for a server instance.
42
- * @param {string} wsManagementId - Unique server context ID.
42
+ * @param {string} hostKeyContext - Unique server context ID.
43
43
  */
44
- static init(wsManagementId) {
45
- this.#state[wsManagementId] = {};
44
+ static init(hostKeyContext) {
45
+ this.#state[hostKeyContext] = {};
46
46
  }
47
47
 
48
48
  /**
49
49
  * Registers a socket connection.
50
50
  * @param {import('socket.io').Socket} socket
51
- * @param {string} wsManagementId
51
+ * @param {string} hostKeyContext
52
52
  */
53
- static connection(socket, wsManagementId) {
54
- return this.#io.connection(socket, wsManagementId);
53
+ static connection(socket, hostKeyContext) {
54
+ return this.#io.connection(socket, hostKeyContext);
55
55
  }
56
56
 
57
57
  /**
58
58
  * Handles socket disconnection.
59
59
  * @param {import('socket.io').Socket} socket
60
60
  * @param {string} reason
61
- * @param {string} wsManagementId
61
+ * @param {string} hostKeyContext
62
62
  */
63
- static disconnect(socket, reason, wsManagementId) {
64
- return this.#io.disconnect(socket, reason, wsManagementId);
63
+ static disconnect(socket, reason, hostKeyContext) {
64
+ return this.#io.disconnect(socket, reason, hostKeyContext);
65
65
  }
66
66
  }
67
67
 
@@ -12,29 +12,29 @@ import { IoChannel } from '../../IoInterface.js';
12
12
  * Handles register/unregister messages and cleanup on disconnect.
13
13
  */
14
14
  class CoreWsMailerChannel {
15
- /** @type {Object.<string, Object.<string, { model: { user: Object } }>>} Socket data keyed by `[wsManagementId][socketId]`. */
15
+ /** @type {Object.<string, Object.<string, { model: { user: Object } }>>} Socket data keyed by `[hostKeyContext][socketId]`. */
16
16
  static #data = {};
17
17
 
18
- /** @type {Object.<string, Object.<string, string>>} Reverse index: `[wsManagementId][userId]` → socketId. */
18
+ /** @type {Object.<string, Object.<string, string>>} Reverse index: `[hostKeyContext][userId]` → socketId. */
19
19
  static #userIndex = {};
20
20
 
21
21
  /** @type {IoChannel} */
22
22
  static #io = new IoChannel({
23
23
  channel: 'mailer',
24
- controller(socket, client, payload, wsManagementId) {
24
+ controller(socket, client, payload, hostKeyContext) {
25
25
  switch (payload.status) {
26
26
  case 'register-user':
27
- CoreWsMailerChannel.setUser(wsManagementId, socket.id, payload.user);
27
+ CoreWsMailerChannel.setUser(hostKeyContext, socket.id, payload.user);
28
28
  break;
29
29
  case 'unregister-user':
30
- CoreWsMailerChannel.removeSocket(wsManagementId, socket.id);
30
+ CoreWsMailerChannel.removeSocket(hostKeyContext, socket.id);
31
31
  break;
32
32
  default:
33
33
  break;
34
34
  }
35
35
  },
36
- disconnect(socket, client, reason, wsManagementId) {
37
- CoreWsMailerChannel.removeSocket(wsManagementId, socket.id);
36
+ disconnect(socket, client, reason, hostKeyContext) {
37
+ CoreWsMailerChannel.removeSocket(hostKeyContext, socket.id);
38
38
  },
39
39
  });
40
40
 
@@ -50,66 +50,66 @@ class CoreWsMailerChannel {
50
50
 
51
51
  /**
52
52
  * Initializes state for a server instance.
53
- * @param {string} wsManagementId - Unique server context ID (`${host}${path}`).
53
+ * @param {string} hostKeyContext - Unique server context ID (`${host}${path}`).
54
54
  */
55
- static init(wsManagementId) {
56
- this.#data[wsManagementId] = {};
57
- this.#userIndex[wsManagementId] = {};
55
+ static init(hostKeyContext) {
56
+ this.#data[hostKeyContext] = {};
57
+ this.#userIndex[hostKeyContext] = {};
58
58
  }
59
59
 
60
60
  /**
61
61
  * Registers a socket connection.
62
62
  * @param {import('socket.io').Socket} socket
63
- * @param {string} wsManagementId
63
+ * @param {string} hostKeyContext
64
64
  */
65
- static connection(socket, wsManagementId) {
66
- return this.#io.connection(socket, wsManagementId);
65
+ static connection(socket, hostKeyContext) {
66
+ return this.#io.connection(socket, hostKeyContext);
67
67
  }
68
68
 
69
69
  /**
70
70
  * Handles socket disconnection.
71
71
  * @param {import('socket.io').Socket} socket
72
72
  * @param {string} reason
73
- * @param {string} wsManagementId
73
+ * @param {string} hostKeyContext
74
74
  */
75
- static disconnect(socket, reason, wsManagementId) {
76
- return this.#io.disconnect(socket, reason, wsManagementId);
75
+ static disconnect(socket, reason, hostKeyContext) {
76
+ return this.#io.disconnect(socket, reason, hostKeyContext);
77
77
  }
78
78
 
79
79
  /**
80
80
  * Registers a user↔socket mapping.
81
- * @param {string} wsManagementId
81
+ * @param {string} hostKeyContext
82
82
  * @param {string} socketId
83
83
  * @param {Object} user - User data with `_id` property.
84
84
  */
85
- static setUser(wsManagementId, socketId, user) {
86
- this.#data[wsManagementId][socketId] = { model: { user } };
85
+ static setUser(hostKeyContext, socketId, user) {
86
+ this.#data[hostKeyContext][socketId] = { model: { user } };
87
87
  if (user?._id) {
88
- this.#userIndex[wsManagementId][user._id.toString()] = socketId;
88
+ this.#userIndex[hostKeyContext][user._id.toString()] = socketId;
89
89
  }
90
90
  }
91
91
 
92
92
  /**
93
93
  * Removes a socket entry and its reverse user index.
94
- * @param {string} wsManagementId
94
+ * @param {string} hostKeyContext
95
95
  * @param {string} socketId
96
96
  */
97
- static removeSocket(wsManagementId, socketId) {
98
- const entry = this.#data[wsManagementId]?.[socketId];
97
+ static removeSocket(hostKeyContext, socketId) {
98
+ const entry = this.#data[hostKeyContext]?.[socketId];
99
99
  if (entry?.model?.user?._id) {
100
- delete this.#userIndex[wsManagementId][entry.model.user._id.toString()];
100
+ delete this.#userIndex[hostKeyContext][entry.model.user._id.toString()];
101
101
  }
102
- delete this.#data[wsManagementId]?.[socketId];
102
+ delete this.#data[hostKeyContext]?.[socketId];
103
103
  }
104
104
 
105
105
  /**
106
106
  * Finds the socket ID for a user (O(1) reverse index lookup).
107
- * @param {string} wsManagementId
107
+ * @param {string} hostKeyContext
108
108
  * @param {string} userId - The user `_id`.
109
109
  * @returns {string|undefined} Socket ID, or `undefined` if not connected.
110
110
  */
111
- static getUserWsId(wsManagementId, userId) {
112
- return this.#userIndex[wsManagementId]?.[userId];
111
+ static getUserWsId(hostKeyContext, userId) {
112
+ return this.#userIndex[hostKeyContext]?.[userId];
113
113
  }
114
114
  }
115
115
 
@@ -11,41 +11,41 @@ import { IoChannel } from '../../IoInterface.js';
11
11
  * and broadcast connection/disconnection events to other room members.
12
12
  */
13
13
  class CoreWsStreamChannel {
14
- /** @type {Object.<string, Object.<string, Array>>} Per-socket room/user args keyed by `[wsManagementId][socketId]`. */
14
+ /** @type {Object.<string, Object.<string, Array>>} Per-socket room/user args keyed by `[hostKeyContext][socketId]`. */
15
15
  static #state = {};
16
16
 
17
17
  /** @type {IoChannel} */
18
18
  static #io = new IoChannel({
19
19
  channel: 'stream',
20
20
  stream: true,
21
- controller(socket, client, payload, wsManagementId, args) {
21
+ controller(socket, client, payload, hostKeyContext, args) {
22
22
  const [roomId, userId] = args;
23
23
 
24
24
  // Collect existing users in the room before registering the new one
25
25
  const existingUsers = [];
26
- for (const entry of Object.values(CoreWsStreamChannel.#state[wsManagementId])) {
26
+ for (const entry of Object.values(CoreWsStreamChannel.#state[hostKeyContext])) {
27
27
  if (entry[0] === roomId && entry[1]) existingUsers.push(entry[1]);
28
28
  }
29
29
 
30
- CoreWsStreamChannel.#state[wsManagementId][socket.id] = args;
30
+ CoreWsStreamChannel.#state[hostKeyContext][socket.id] = args;
31
31
  socket.join(roomId);
32
32
  socket.to(roomId).emit('stream-user-connected', userId);
33
33
 
34
34
  // Tell the joining user about everyone already in the room
35
35
  if (existingUsers.length > 0) socket.emit('stream-existing-users', existingUsers);
36
36
  },
37
- connection(socket, client, wsManagementId) {
38
- CoreWsStreamChannel.#state[wsManagementId][socket.id] = [];
37
+ connection(socket, client, hostKeyContext) {
38
+ CoreWsStreamChannel.#state[hostKeyContext][socket.id] = [];
39
39
  },
40
- disconnect(socket, client, reason, wsManagementId) {
41
- const entry = CoreWsStreamChannel.#state[wsManagementId]?.[socket.id];
40
+ disconnect(socket, client, reason, hostKeyContext) {
41
+ const entry = CoreWsStreamChannel.#state[hostKeyContext]?.[socket.id];
42
42
  if (!entry || entry.length === 0) {
43
- if (CoreWsStreamChannel.#state[wsManagementId]) delete CoreWsStreamChannel.#state[wsManagementId][socket.id];
43
+ if (CoreWsStreamChannel.#state[hostKeyContext]) delete CoreWsStreamChannel.#state[hostKeyContext][socket.id];
44
44
  return;
45
45
  }
46
46
  const [roomId, userId] = entry;
47
47
  socket.to(roomId).emit('stream-user-disconnected', userId);
48
- delete CoreWsStreamChannel.#state[wsManagementId][socket.id];
48
+ delete CoreWsStreamChannel.#state[hostKeyContext][socket.id];
49
49
  },
50
50
  });
51
51
 
@@ -61,29 +61,29 @@ class CoreWsStreamChannel {
61
61
 
62
62
  /**
63
63
  * Initializes state for a server instance.
64
- * @param {string} wsManagementId - Unique server context ID (`${host}${path}`).
64
+ * @param {string} hostKeyContext - Unique server context ID (`${host}${path}`).
65
65
  */
66
- static init(wsManagementId) {
67
- this.#state[wsManagementId] = {};
66
+ static init(hostKeyContext) {
67
+ this.#state[hostKeyContext] = {};
68
68
  }
69
69
 
70
70
  /**
71
71
  * Registers a socket connection.
72
72
  * @param {import('socket.io').Socket} socket
73
- * @param {string} wsManagementId
73
+ * @param {string} hostKeyContext
74
74
  */
75
- static connection(socket, wsManagementId) {
76
- return this.#io.connection(socket, wsManagementId);
75
+ static connection(socket, hostKeyContext) {
76
+ return this.#io.connection(socket, hostKeyContext);
77
77
  }
78
78
 
79
79
  /**
80
80
  * Handles socket disconnection.
81
81
  * @param {import('socket.io').Socket} socket
82
82
  * @param {string} reason
83
- * @param {string} wsManagementId
83
+ * @param {string} hostKeyContext
84
84
  */
85
- static disconnect(socket, reason, wsManagementId) {
86
- return this.#io.disconnect(socket, reason, wsManagementId);
85
+ static disconnect(socket, reason, hostKeyContext) {
86
+ return this.#io.disconnect(socket, reason, hostKeyContext);
87
87
  }
88
88
  }
89
89
 
@@ -19,21 +19,21 @@ class CoreWsConnectionHandler {
19
19
  /**
20
20
  * Handles a new WebSocket connection.
21
21
  * @param {import('socket.io').Socket} socket
22
- * @param {string} wsManagementId
22
+ * @param {string} hostKeyContext
23
23
  */
24
- static handle(socket, wsManagementId) {
24
+ static handle(socket, hostKeyContext) {
25
25
  logger.info(`New connection established. Socket ID: ${socket.id}`);
26
26
 
27
- CoreWsChatChannel.connection(socket, wsManagementId);
28
- CoreWsMailerChannel.connection(socket, wsManagementId);
29
- CoreWsStreamChannel.connection(socket, wsManagementId);
27
+ CoreWsChatChannel.connection(socket, hostKeyContext);
28
+ CoreWsMailerChannel.connection(socket, hostKeyContext);
29
+ CoreWsStreamChannel.connection(socket, hostKeyContext);
30
30
 
31
31
  socket.on('disconnect', (reason) => {
32
32
  logger.info(`Connection disconnected. Socket ID: ${socket.id}, reason: ${reason}`);
33
33
 
34
- CoreWsChatChannel.disconnect(socket, reason, wsManagementId);
35
- CoreWsMailerChannel.disconnect(socket, reason, wsManagementId);
36
- CoreWsStreamChannel.disconnect(socket, reason, wsManagementId);
34
+ CoreWsChatChannel.disconnect(socket, reason, hostKeyContext);
35
+ CoreWsMailerChannel.disconnect(socket, reason, hostKeyContext);
36
+ CoreWsStreamChannel.disconnect(socket, reason, hostKeyContext);
37
37
  });
38
38
  }
39
39
  }
@@ -10,6 +10,7 @@ import { CoreWsConnectionHandler } from './core.ws.connection.js';
10
10
  import { CoreWsChatChannel } from './channels/core.ws.chat.js';
11
11
  import { CoreWsMailerChannel } from './channels/core.ws.mailer.js';
12
12
  import { CoreWsStreamChannel } from './channels/core.ws.stream.js';
13
+ import { resolveHostKeyContext } from '../../server/conf.js';
13
14
 
14
15
  /**
15
16
  * @class CoreWsServer
@@ -27,13 +28,13 @@ class CoreWsServer {
27
28
  */
28
29
  static create(httpServer, options) {
29
30
  const { host, path } = options;
30
- const wsManagementId = `${host}${path}`;
31
+ const hostKeyContext = resolveHostKeyContext({ host, path });
31
32
 
32
- CoreWsChatChannel.init(wsManagementId);
33
- CoreWsMailerChannel.init(wsManagementId);
34
- CoreWsStreamChannel.init(wsManagementId);
33
+ CoreWsChatChannel.init(hostKeyContext);
34
+ CoreWsMailerChannel.init(hostKeyContext);
35
+ CoreWsStreamChannel.init(hostKeyContext);
35
36
 
36
- return IoServer.create(httpServer, options, (socket) => CoreWsConnectionHandler.handle(socket, wsManagementId));
37
+ return IoServer.create(httpServer, options, (socket) => CoreWsConnectionHandler.handle(socket, hostKeyContext));
37
38
  }
38
39
  }
39
40
 
@@ -10,7 +10,7 @@ import { IoChannel } from '../../IoInterface.js';
10
10
  * @classdesc Provides a no-op main channel for the default WebSocket server.
11
11
  */
12
12
  class DefaultWsMainChannel {
13
- /** @type {Object.<string, Object>} Per-instance state keyed by wsManagementId. */
13
+ /** @type {Object.<string, Object>} Per-instance state keyed by hostKeyContext. */
14
14
  static #state = {};
15
15
 
16
16
  /** @type {IoChannel} */
@@ -28,29 +28,29 @@ class DefaultWsMainChannel {
28
28
 
29
29
  /**
30
30
  * Initializes state for a server instance.
31
- * @param {string} wsManagementId
31
+ * @param {string} hostKeyContext
32
32
  */
33
- static init(wsManagementId) {
34
- this.#state[wsManagementId] = {};
33
+ static init(hostKeyContext) {
34
+ this.#state[hostKeyContext] = {};
35
35
  }
36
36
 
37
37
  /**
38
38
  * Registers a socket connection.
39
39
  * @param {import('socket.io').Socket} socket
40
- * @param {string} wsManagementId
40
+ * @param {string} hostKeyContext
41
41
  */
42
- static connection(socket, wsManagementId) {
43
- return this.#io.connection(socket, wsManagementId);
42
+ static connection(socket, hostKeyContext) {
43
+ return this.#io.connection(socket, hostKeyContext);
44
44
  }
45
45
 
46
46
  /**
47
47
  * Handles socket disconnection.
48
48
  * @param {import('socket.io').Socket} socket
49
49
  * @param {string} reason
50
- * @param {string} wsManagementId
50
+ * @param {string} hostKeyContext
51
51
  */
52
- static disconnect(socket, reason, wsManagementId) {
53
- return this.#io.disconnect(socket, reason, wsManagementId);
52
+ static disconnect(socket, reason, hostKeyContext) {
53
+ return this.#io.disconnect(socket, reason, hostKeyContext);
54
54
  }
55
55
  }
56
56
 
@@ -17,17 +17,17 @@ class DefaultWsConnectionHandler {
17
17
  /**
18
18
  * Handles a new WebSocket connection.
19
19
  * @param {import('socket.io').Socket} socket
20
- * @param {string} wsManagementId
20
+ * @param {string} hostKeyContext
21
21
  */
22
- static handle(socket, wsManagementId) {
22
+ static handle(socket, hostKeyContext) {
23
23
  logger.info(`DefaultWsConnection ${socket.id}`);
24
24
 
25
- DefaultWsMainChannel.connection(socket, wsManagementId);
25
+ DefaultWsMainChannel.connection(socket, hostKeyContext);
26
26
 
27
27
  socket.on('disconnect', (reason) => {
28
28
  logger.info(`DefaultWsConnection ${socket.id} due to reason: ${reason}`);
29
29
 
30
- DefaultWsMainChannel.disconnect(socket, reason, wsManagementId);
30
+ DefaultWsMainChannel.disconnect(socket, reason, hostKeyContext);
31
31
  });
32
32
  }
33
33
  }
@@ -8,6 +8,7 @@
8
8
  import { IoServer } from '../IoServer.js';
9
9
  import { DefaultWsConnectionHandler } from './default.ws.connection.js';
10
10
  import { DefaultWsMainChannel } from './channels/default.ws.main.js';
11
+ import { resolveHostKeyContext } from '../../server/conf.js';
11
12
 
12
13
  /**
13
14
  * @class DefaultWsServer
@@ -24,11 +25,11 @@ class DefaultWsServer {
24
25
  */
25
26
  static create(httpServer, options) {
26
27
  const { host, path } = options;
27
- const wsManagementId = `${host}${path}`;
28
+ const hostKeyContext = resolveHostKeyContext({ host, path });
28
29
 
29
- DefaultWsMainChannel.init(wsManagementId);
30
+ DefaultWsMainChannel.init(hostKeyContext);
30
31
 
31
- return IoServer.create(httpServer, options, (socket) => DefaultWsConnectionHandler.handle(socket, wsManagementId));
32
+ return IoServer.create(httpServer, options, (socket) => DefaultWsConnectionHandler.handle(socket, hostKeyContext));
32
33
  }
33
34
  }
34
35
 
package/typedoc.json CHANGED
@@ -1,6 +1,15 @@
1
1
  {
2
2
  "name": "Nexodev - ERP, CRM Development & Cloud DevOps Services",
3
- "entryPoints": ["./src/server", "./src/api", "./src/db", "./src/ws", "./src/grpc", "./src/mailer", "./src/runtime"],
3
+ "entryPoints": [
4
+ "./src/server",
5
+ "./src/api",
6
+ "./src/cli",
7
+ "./src/db",
8
+ "./src/ws",
9
+ "./src/grpc",
10
+ "./src/mailer",
11
+ "./src/runtime"
12
+ ],
4
13
  "entryPointStrategy": "expand",
5
14
  "exclude": ["**/node_modules/**", "**/docs/**", "**/client/**"],
6
15
  "out": "./public/www.nexodev.org/docs/",