underpost 2.8.884 → 2.8.885

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 (43) hide show
  1. package/README.md +4 -120
  2. package/bin/deploy.js +9 -10
  3. package/bin/file.js +4 -6
  4. package/cli.md +15 -11
  5. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  6. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  7. package/package.json +1 -1
  8. package/src/cli/cluster.js +21 -0
  9. package/src/cli/cron.js +8 -0
  10. package/src/cli/db.js +63 -1
  11. package/src/cli/deploy.js +156 -3
  12. package/src/cli/env.js +43 -0
  13. package/src/cli/fs.js +94 -0
  14. package/src/cli/image.js +8 -0
  15. package/src/cli/index.js +17 -4
  16. package/src/cli/monitor.js +0 -1
  17. package/src/cli/repository.js +95 -2
  18. package/src/client/components/core/Css.js +16 -0
  19. package/src/client/components/core/Docs.js +5 -13
  20. package/src/client/components/core/Modal.js +48 -29
  21. package/src/client/components/core/Router.js +6 -3
  22. package/src/client/components/core/Worker.js +205 -118
  23. package/src/client/components/default/MenuDefault.js +1 -0
  24. package/src/client.dev.js +6 -3
  25. package/src/db/DataBaseProvider.js +65 -12
  26. package/src/db/mariadb/MariaDB.js +39 -6
  27. package/src/db/mongo/MongooseDB.js +51 -133
  28. package/src/index.js +1 -1
  29. package/src/mailer/EmailRender.js +58 -9
  30. package/src/mailer/MailerProvider.js +98 -25
  31. package/src/runtime/express/Express.js +20 -34
  32. package/src/server/auth.js +9 -28
  33. package/src/server/client-build-live.js +14 -5
  34. package/src/server/client-dev-server.js +21 -8
  35. package/src/server/conf.js +78 -25
  36. package/src/server/peer.js +2 -2
  37. package/src/server/runtime.js +0 -5
  38. package/src/server/start.js +39 -0
  39. package/src/ws/IoInterface.js +132 -39
  40. package/src/ws/IoServer.js +79 -31
  41. package/src/ws/core/core.ws.connection.js +50 -16
  42. package/src/ws/core/core.ws.emit.js +47 -8
  43. package/src/ws/core/core.ws.server.js +62 -10
@@ -8,18 +8,20 @@ const logger = loggerFactory(import.meta);
8
8
  const clientLiveBuild = async () => {
9
9
  if (fs.existsSync(`./tmp/client.build.json`)) {
10
10
  const deployId = process.argv[2];
11
-
11
+ const subConf = process.argv[3];
12
12
  let clientId = 'default';
13
13
  let host = 'default.net';
14
14
  let path = '/';
15
15
  let baseHost = `${host}${path === '/' ? '' : path}`;
16
16
  let views = Config.default.client[clientId].views;
17
+ let apiBaseHost;
18
+ let apiBaseProxyPath;
17
19
 
18
20
  if (
19
21
  deployId &&
20
22
  (fs.existsSync(`./engine-private/conf/${deployId}`) || fs.existsSync(`./engine-private/replica/${deployId}`))
21
23
  ) {
22
- loadConf(deployId);
24
+ loadConf(deployId, subConf);
23
25
  const confClient = JSON.parse(
24
26
  fs.readFileSync(
25
27
  fs.existsSync(`./engine-private/replica/${deployId}`)
@@ -32,7 +34,9 @@ const clientLiveBuild = async () => {
32
34
  );
33
35
  const confServer = JSON.parse(
34
36
  fs.readFileSync(
35
- fs.existsSync(`./engine-private/replica/${deployId}`)
37
+ fs.existsSync(`./engine-private/conf/${deployId}/conf.server.dev.${subConf}.json`)
38
+ ? `./engine-private/conf/${deployId}/conf.server.dev.${subConf}.json`
39
+ : fs.existsSync(`./engine-private/replica/${deployId}`)
36
40
  ? `./engine-private/replica/${deployId}/conf.server.json`
37
41
  : fs.existsSync(`./engine-private/conf/${deployId}/conf.server.json`)
38
42
  ? `./engine-private/conf/${deployId}/conf.server.json`
@@ -40,20 +44,25 @@ const clientLiveBuild = async () => {
40
44
  'utf8',
41
45
  ),
42
46
  );
43
- host = process.argv[3];
44
- path = process.argv[4];
47
+ host = process.argv[4];
48
+ path = process.argv[5];
45
49
  clientId = confServer[host][path].client;
46
50
  views = confClient[clientId].views;
47
51
  baseHost = `${host}${path === '/' ? '' : path}`;
52
+ apiBaseHost = confServer[host][path].apiBaseHost;
53
+ apiBaseProxyPath = confServer[host][path].apiBaseProxyPath;
48
54
  }
49
55
 
50
56
  logger.info('Live build config', {
51
57
  deployId,
58
+ subConf,
52
59
  host,
53
60
  path,
54
61
  clientId,
55
62
  baseHost,
56
63
  views: views.length,
64
+ apiBaseHost,
65
+ apiBaseProxyPath,
57
66
  });
58
67
 
59
68
  const updates = JSON.parse(fs.readFileSync(`./tmp/client.build.json`, 'utf8'));
@@ -2,17 +2,26 @@ import fs from 'fs-extra';
2
2
  import nodemon from 'nodemon';
3
3
  import { shellExec } from './process.js';
4
4
  import { loggerFactory } from './logger.js';
5
+ import { writeEnv } from './conf.js';
6
+ import dotenv from 'dotenv';
5
7
 
6
8
  const logger = loggerFactory(import.meta);
7
9
 
8
- const createClientDevServer = () => {
9
- // process.argv.slice(2).join(' ')
10
- shellExec(`env-cmd -f .env.development node bin/deploy build-full-client ${process.argv.slice(2).join(' ')}`);
10
+ const createClientDevServer = (
11
+ deployId = process.argv[2] || 'dd-default',
12
+ subConf = process.argv[3] || '',
13
+ host = process.argv[4] || 'default.net',
14
+ path = process.argv[5] || '/',
15
+ ) => {
11
16
  shellExec(
12
- `env-cmd -f .env.development node src/api ${process.argv[2]}${process.argv[5] ? ` ${process.argv[5]}` : ''}${
13
- process.argv.includes('static') ? ' static' : ''
14
- }`,
15
- { async: true },
17
+ `env-cmd -f ./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}.${subConf}-dev-client node bin/deploy build-full-client ${deployId} ${subConf}-dev-client ${host} ${path}`.trim(),
18
+ );
19
+
20
+ shellExec(
21
+ `env-cmd -f ./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}.${subConf}-dev-client node src/server ${deployId} ${subConf}-dev-client`.trim(),
22
+ {
23
+ async: true,
24
+ },
16
25
  );
17
26
 
18
27
  // https://github.com/remy/nodemon/blob/main/doc/events.md
@@ -28,7 +37,11 @@ const createClientDevServer = () => {
28
37
 
29
38
  let buildPathScope = [];
30
39
 
31
- const nodemonOptions = { script: './src/client.build', args: process.argv.slice(2), watch: 'src/client' };
40
+ const nodemonOptions = {
41
+ script: './src/client.build',
42
+ args: [`${deployId}`, `${subConf}-dev-client`, `${host}`, `${path}`],
43
+ watch: 'src/client',
44
+ };
32
45
  logger.info('nodemon option', { nodemonOptions });
33
46
  nodemon(nodemonOptions)
34
47
  .on('start', function (...args) {
@@ -25,13 +25,14 @@ const logger = loggerFactory(import.meta);
25
25
  const Config = {
26
26
  default: DefaultConf,
27
27
  build: async function (deployContext = 'dd-default', deployList, subConf) {
28
- if (typeof process.argv[2] === 'string' && process.argv[2].startsWith('dd-')) deployContext = process.argv[2];
28
+ if (process.argv[2] && typeof process.argv[2] === 'string' && process.argv[2].startsWith('dd-'))
29
+ deployContext = process.argv[2];
30
+ if (!subConf && process.argv[3] && typeof process.argv[3] === 'string') subConf = process.argv[3];
29
31
  if (!fs.existsSync(`./tmp`)) fs.mkdirSync(`./tmp`, { recursive: true });
30
32
  UnderpostRootEnv.API.set('await-deploy', new Date().toISOString());
31
- if (fs.existsSync(`./engine-private/replica/${deployContext}`))
32
- return loadConf(deployContext, process.env.NODE_ENV, subConf);
33
- else if (deployContext.startsWith('dd-')) return loadConf(deployContext, process.env.NODE_ENV, subConf);
34
- if (deployContext === 'proxy') Config.buildProxy(deployContext, deployList, subConf);
33
+ if (fs.existsSync(`./engine-private/replica/${deployContext}`)) return loadConf(deployContext, subConf);
34
+ else if (deployContext.startsWith('dd-')) return loadConf(deployContext, subConf);
35
+ if (deployContext === 'proxy') Config.buildProxy(deployList, subConf);
35
36
  },
36
37
  deployIdFactory: function (deployId = 'dd-default', options = { cluster: false }) {
37
38
  if (!deployId.startsWith('dd-')) deployId = `dd-${deployId}`;
@@ -105,7 +106,7 @@ const Config = {
105
106
  for (const confType of Object.keys(this.default))
106
107
  fs.writeFileSync(`${folder}/conf.${confType}.json`, JSON.stringify(this.default[confType], null, 4), 'utf8');
107
108
  },
108
- buildProxy: function (deployContext = 'dd-default', deployList, subConf) {
109
+ buildProxy: function (deployList = 'dd-default', subConf = '') {
109
110
  if (!deployList) deployList = process.argv[3];
110
111
  if (!subConf) subConf = process.argv[4];
111
112
  this.default.server = {};
@@ -121,7 +122,7 @@ const Config = {
121
122
  if (process.env.NODE_ENV === 'development' && fs.existsSync(confDevPath)) confPath = confDevPath;
122
123
  const serverConf = JSON.parse(fs.readFileSync(confPath, 'utf8'));
123
124
 
124
- for (const host of Object.keys(loadReplicas(serverConf, deployContext, subConf))) {
125
+ for (const host of Object.keys(loadReplicas(serverConf))) {
125
126
  if (serverConf[host]['/'])
126
127
  this.default.server[host] = {
127
128
  ...this.default.server[host],
@@ -138,13 +139,13 @@ const Config = {
138
139
  },
139
140
  };
140
141
 
141
- const loadConf = (deployId = 'dd-default', envInput, subConf) => {
142
+ const loadConf = (deployId = 'dd-default', subConf) => {
142
143
  if (deployId === 'current') {
143
144
  console.log(process.env.DEPLOY_ID);
144
145
  return;
145
146
  }
146
147
  if (deployId === 'clean') {
147
- const path = envInput ?? '.';
148
+ const path = '.';
148
149
  fs.removeSync(`${path}/.env`);
149
150
  shellExec(`git checkout ${path}/.env.production`);
150
151
  shellExec(`git checkout ${path}/.env.development`);
@@ -164,7 +165,6 @@ const loadConf = (deployId = 'dd-default', envInput, subConf) => {
164
165
  for (const typeConf of Object.keys(Config.default)) {
165
166
  let srcConf = fs.readFileSync(`${folder}/conf.${typeConf}.json`, 'utf8');
166
167
  if (process.env.NODE_ENV === 'development' && typeConf === 'server') {
167
- if (!subConf) subConf = process.argv[3];
168
168
  const devConfPath = `${folder}/conf.${typeConf}.dev${subConf ? `.${subConf}` : ''}.json`;
169
169
  if (fs.existsSync(devConfPath)) srcConf = fs.readFileSync(devConfPath, 'utf8');
170
170
  }
@@ -174,10 +174,13 @@ const loadConf = (deployId = 'dd-default', envInput, subConf) => {
174
174
  fs.writeFileSync(`./.env.production`, fs.readFileSync(`${folder}/.env.production`, 'utf8'), 'utf8');
175
175
  fs.writeFileSync(`./.env.development`, fs.readFileSync(`${folder}/.env.development`, 'utf8'), 'utf8');
176
176
  fs.writeFileSync(`./.env.test`, fs.readFileSync(`${folder}/.env.test`, 'utf8'), 'utf8');
177
- const NODE_ENV = envInput || process.env.NODE_ENV;
177
+ const NODE_ENV = process.env.NODE_ENV;
178
178
  if (NODE_ENV) {
179
- fs.writeFileSync(`./.env`, fs.readFileSync(`${folder}/.env.${NODE_ENV}`, 'utf8'), 'utf8');
180
- const env = dotenv.parse(fs.readFileSync(`${folder}/.env.${NODE_ENV}`, 'utf8'));
179
+ const subPathEnv = fs.existsSync(`${folder}/.env.${NODE_ENV}.${subConf}`)
180
+ ? `${folder}/.env.${NODE_ENV}.${subConf}`
181
+ : `${folder}/.env.${NODE_ENV}`;
182
+ fs.writeFileSync(`./.env`, fs.readFileSync(subPathEnv, 'utf8'), 'utf8');
183
+ const env = dotenv.parse(fs.readFileSync(subPathEnv, 'utf8'));
181
184
  process.env = {
182
185
  ...process.env,
183
186
  ...env,
@@ -191,22 +194,16 @@ const loadConf = (deployId = 'dd-default', envInput, subConf) => {
191
194
  return { folder, deployId };
192
195
  };
193
196
 
194
- const loadReplicas = (confServer, deployContext, subConf) => {
195
- if (!deployContext) deployContext = process.argv[2];
196
- if (!subConf) subConf = process.argv[3];
197
+ const loadReplicas = (confServer) => {
197
198
  for (const host of Object.keys(confServer)) {
198
199
  for (const path of Object.keys(confServer[host])) {
199
200
  const { replicas, singleReplica } = confServer[host][path];
200
- if (
201
- replicas &&
202
- (deployContext === 'proxy' ||
203
- !singleReplica ||
204
- (singleReplica && process.env.NODE_ENV === 'development' && !subConf))
205
- )
201
+ if (replicas && !singleReplica)
206
202
  for (const replicaPath of replicas) {
207
- confServer[host][replicaPath] = newInstance(confServer[host][path]);
208
- delete confServer[host][replicaPath].replicas;
209
- delete confServer[host][replicaPath].singleReplica;
203
+ {
204
+ confServer[host][replicaPath] = newInstance(confServer[host][path]);
205
+ delete confServer[host][replicaPath].replicas;
206
+ }
210
207
  }
211
208
  }
212
209
  }
@@ -982,6 +979,60 @@ const getInstanceContext = async (options = { singleReplica, replicas, redirect:
982
979
  return { redirectTarget };
983
980
  };
984
981
 
982
+ const buildApiConf = async (options = { deployId: '', subConf: '', host: '', path: '', origin: '' }) => {
983
+ let { deployId, subConf, host, path, origin } = options;
984
+ if (!deployId) deployId = process.argv[2].trim();
985
+ if (!subConf) subConf = process.argv[3].trim();
986
+ if (process.argv[4]) host = process.argv[4].trim();
987
+ if (process.argv[5]) path = process.argv[5].trim();
988
+ if (process.argv[6])
989
+ origin = `${process.env.NODE_ENV === 'production' ? 'https' : 'http'}://${process.argv[6].trim()}`;
990
+
991
+ if (!origin) return;
992
+ const confServer = JSON.parse(
993
+ fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.dev.${subConf}.json`, 'utf8'),
994
+ );
995
+ const envObj = dotenv.parse(
996
+ fs.readFileSync(`./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}`, 'utf8'),
997
+ );
998
+ if (host && path) {
999
+ confServer[host][path].origins = [origin];
1000
+ logger.info('Build api conf', { host, path, origin });
1001
+ } else return;
1002
+ writeEnv(`./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}.${subConf}-dev-api`, envObj);
1003
+ fs.writeFileSync(
1004
+ `./engine-private/conf/${deployId}/conf.server.dev.${subConf}-dev-api.json`,
1005
+ JSON.stringify(confServer, null, 4),
1006
+ 'utf8',
1007
+ );
1008
+ };
1009
+
1010
+ const buildClientStaticConf = async (options = { deployId: '', subConf: '', apiBaseHost: '', host: '', path: '' }) => {
1011
+ let { deployId, subConf, host, path } = options;
1012
+ if (!deployId) deployId = process.argv[2].trim();
1013
+ if (!subConf) subConf = process.argv[3].trim();
1014
+ if (!host) host = process.argv[4].trim();
1015
+ if (!path) path = process.argv[5].trim();
1016
+ const confServer = JSON.parse(
1017
+ fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.dev.${subConf}-dev-api.json`, 'utf8'),
1018
+ );
1019
+ const envObj = dotenv.parse(
1020
+ fs.readFileSync(`./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}.${subConf}-dev-api`, 'utf8'),
1021
+ );
1022
+ envObj.PORT = parseInt(envObj.PORT);
1023
+ const apiBaseHost = options?.apiBaseHost ? options.apiBaseHost : `localhost:${envObj.PORT + 1}`;
1024
+ confServer[host][path].apiBaseHost = apiBaseHost;
1025
+ confServer[host][path].apiBaseProxyPath = path;
1026
+ logger.info('Build client static conf', { host, path, apiBaseHost });
1027
+ envObj.PORT = parseInt(confServer[host][path].origins[0].split(':')[2]) - 1;
1028
+ writeEnv(`./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}.${subConf}-dev-client`, envObj);
1029
+ fs.writeFileSync(
1030
+ `./engine-private/conf/${deployId}/conf.server.dev.${subConf}-dev-client.json`,
1031
+ JSON.stringify(confServer, null, 4),
1032
+ 'utf8',
1033
+ );
1034
+ };
1035
+
985
1036
  export {
986
1037
  Cmd,
987
1038
  Config,
@@ -1015,4 +1066,6 @@ export {
1015
1066
  rebuildConfFactory,
1016
1067
  buildCliDoc,
1017
1068
  getInstanceContext,
1069
+ buildApiConf,
1070
+ buildClientStaticConf,
1018
1071
  };
@@ -48,7 +48,7 @@ const logger = loggerFactory(import.meta);
48
48
  */
49
49
  const createPeerServer = async ({ port, devPort, origins, host, path }) => {
50
50
  if (process.env.NODE_ENV === 'development' && devPort) {
51
- logger.warn(`Adding development origin: http://localhost:${devPort}`);
51
+ // logger.warn(`Adding development origin: http://localhost:${devPort}`);
52
52
  origins.push(`http://localhost:${devPort}`);
53
53
  }
54
54
 
@@ -67,7 +67,7 @@ const createPeerServer = async ({ port, devPort, origins, host, path }) => {
67
67
  };
68
68
 
69
69
  // Use the framework's factory to listen on the server, ensuring graceful startup/shutdown
70
- const peerServer = UnderpostStartUp.API.listenServerFactory(() => PeerServer(options));
70
+ const peerServer = UnderpostStartUp.API.listenServerFactory(async () => PeerServer(options));
71
71
 
72
72
  return { options, peerServer, meta: import.meta };
73
73
  };
@@ -103,10 +103,6 @@ const buildRuntime = async () => {
103
103
 
104
104
  switch (runtime) {
105
105
  case 'nodejs':
106
- // The devApiPort is used for development CORS origin calculation
107
- // It needs to account for the current port and potential peer server increment
108
- const devApiPort = currentPort + (peer ? 2 : 1);
109
-
110
106
  logger.info('Build nodejs server runtime', `${host}${path}:${port}`);
111
107
 
112
108
  const { portsUsed } = await ExpressService.createApp({
@@ -124,7 +120,6 @@ const buildRuntime = async () => {
124
120
  peer,
125
121
  valkey,
126
122
  apiBaseHost,
127
- devApiPort, // Pass the dynamically calculated dev API port
128
123
  redirectTarget,
129
124
  rootHostPath,
130
125
  confSSR,
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Manages the startup and runtime configuration of Underpost applications.
3
+ * @module src/server/start.js
4
+ * @namespace UnderpostStartUp
5
+ */
6
+
1
7
  import UnderpostDeploy from '../cli/deploy.js';
2
8
  import fs from 'fs-extra';
3
9
  import { awaitDeployMonitor } from './conf.js';
@@ -7,8 +13,17 @@ import UnderpostRootEnv from '../cli/env.js';
7
13
 
8
14
  const logger = loggerFactory(import.meta);
9
15
 
16
+ /**
17
+ * @class UnderpostStartUp
18
+ * @description Manages the startup and runtime configuration of Underpost applications.
19
+ * @memberof UnderpostStartUp
20
+ */
10
21
  class UnderpostStartUp {
11
22
  static API = {
23
+ /**
24
+ * Logs the runtime network configuration.
25
+ * @memberof UnderpostStartUp
26
+ */
12
27
  logRuntimeRouter: () => {
13
28
  const displayLog = {};
14
29
 
@@ -18,6 +33,12 @@ class UnderpostStartUp {
18
33
 
19
34
  logger.info('Runtime network', displayLog);
20
35
  },
36
+ /**
37
+ * Creates a server factory.
38
+ * @memberof UnderpostStartUp
39
+ * @param {Function} logic - The logic to execute when the server is listening.
40
+ * @returns {Object} An object with a listen method.
41
+ */
21
42
  listenServerFactory: (logic = async () => {}) => {
22
43
  return {
23
44
  listen: async (...args) => {
@@ -36,6 +57,15 @@ class UnderpostStartUp {
36
57
  },
37
58
  };
38
59
  },
60
+
61
+ /**
62
+ * Controls the listening port for a server.
63
+ * @memberof UnderpostStartUp
64
+ * @param {Object} server - The server to listen on.
65
+ * @param {number|string} port - The port number or colon for all ports.
66
+ * @param {Object} metadata - Metadata for the server.
67
+ * @returns {Promise<boolean>} A promise that resolves to true if the server is listening, false otherwise.
68
+ */
39
69
  listenPortController: async (server, port, metadata) =>
40
70
  new Promise((resolve) => {
41
71
  try {
@@ -79,6 +109,15 @@ class UnderpostStartUp {
79
109
  }
80
110
  }),
81
111
 
112
+ /**
113
+ * Starts a deployment.
114
+ * @memberof UnderpostStartUp
115
+ * @param {string} deployId - The ID of the deployment.
116
+ * @param {string} env - The environment of the deployment.
117
+ * @param {Object} options - Options for the deployment.
118
+ * @param {boolean} options.build - Whether to build the deployment.
119
+ * @param {boolean} options.run - Whether to run the deployment.
120
+ */
82
121
  async callback(deployId = 'dd-default', env = 'development', options = { build: false, run: false }) {
83
122
  if (options.build === true) await UnderpostStartUp.API.build(deployId, env);
84
123
  if (options.run === true) await UnderpostStartUp.API.run(deployId, env);
@@ -1,45 +1,138 @@
1
+ /**
2
+ * Module for creating and managing WebSocket channels.
3
+ * @module src/ws/IoInterface.js
4
+ * @namespace SocketIoInterface
5
+ */
6
+
1
7
  import { loggerFactory } from '../server/logger.js';
8
+ import { Socket } from 'socket.io';
2
9
 
3
10
  const logger = loggerFactory(import.meta);
4
11
 
5
- const IoCreateChannel = (
6
- IoInterface = {
7
- channel: '',
8
- connection: (socket = {}, client = {}, wsManagementId = '') => {},
9
- controller: (socket = {}, client = {}, args = '', wsManagementId = '') => {},
10
- disconnect: (socket = {}, client = {}, reason = '', wsManagementId = '') => {},
11
- stream: false,
12
- },
13
- ) => {
14
- return {
15
- channel: IoInterface.channel,
16
- client: {},
17
- connection: async function (socket, wsManagementId) {
18
- try {
19
- this.client[socket.id] = socket;
20
- socket.on(IoInterface.channel, (...args) => this.controller(socket, args, wsManagementId));
21
- await IoInterface.connection(socket, this.client, wsManagementId);
22
- } catch (error) {
23
- logger.error(error, { channel: IoInterface.channel, wsManagementId, stack: error.stack });
24
- }
25
- },
26
- controller: async function (socket, args, wsManagementId) {
27
- try {
28
- const payload = IoInterface.stream ? args[0] : JSON.parse(args[0]);
29
- await IoInterface.controller(socket, this.client, payload, wsManagementId, args);
30
- } catch (error) {
31
- logger.error(error, { channel: IoInterface.channel, wsManagementId, args, stack: error.stack });
32
- }
33
- },
34
- disconnect: async function (socket, reason, wsManagementId) {
35
- try {
36
- await IoInterface.disconnect(socket, this.client, reason, wsManagementId);
37
- delete this.client[socket.id];
38
- } catch (error) {
39
- logger.error(error, { channel: IoInterface.channel, wsManagementId, reason, stack: error.stack });
12
+ /**
13
+ * Defines the structure for a WebSocket channel's behavior functions.
14
+ * @typedef {Object} ChannelInterface
15
+ * @property {string} channel - The name of the channel.
16
+ * @property {function(Socket, Object.<string, Socket>, string): Promise<void>} [connection] - Handler on client connection.
17
+ * @property {function(Socket, Object.<string, Socket>, any, string, any[]): Promise<void>} [controller] - Handler for incoming channel messages.
18
+ * @property {function(Socket, Object.<string, Socket>, string, string): Promise<void>} [disconnect] - Handler on client disconnection.
19
+ * @property {boolean} [stream=false] - Whether the channel should treat the message as a raw stream (no JSON parsing).
20
+ */
21
+
22
+ /**
23
+ * @class
24
+ * @alias IoChannel
25
+ * @memberof SocketIoInterface
26
+ * @classdesc Manages the logic, client map, and event listeners for a specific WebSocket channel,
27
+ * ensuring robust message handling and lifecycle management.
28
+ */
29
+ class IoChannel {
30
+ /**
31
+ * @private
32
+ * @type {ChannelInterface}
33
+ */
34
+ #IoInterface;
35
+
36
+ /**
37
+ * Map of connected sockets for this channel, keyed by socket ID.
38
+ * @type {Object.<string, Socket>}
39
+ */
40
+ client = {};
41
+
42
+ /**
43
+ * Creates an instance of IoChannel.
44
+ * @param {ChannelInterface} IoInterface - The interface object defining the channel's behavior.
45
+ */
46
+ constructor(IoInterface) {
47
+ this.#IoInterface = {
48
+ channel: '',
49
+ connection: async (socket = {}, client = {}, wsManagementId = '') => {},
50
+ controller: async (socket = {}, client = {}, payload = {}, wsManagementId = '', args = []) => {},
51
+ disconnect: async (socket = {}, client = {}, reason = '', wsManagementId = '') => {},
52
+ stream: false,
53
+ ...IoInterface,
54
+ };
55
+ logger.debug(`Channel instance created for: ${this.channel}`);
56
+ }
57
+
58
+ /**
59
+ * Gets the name of the channel.
60
+ * @returns {string} The channel name.
61
+ */
62
+ get channel() {
63
+ return this.#IoInterface.channel;
64
+ }
65
+
66
+ /**
67
+ * Handles a new socket connection for this channel.
68
+ * Sets up the listener for the channel message.
69
+ *
70
+ * @param {Socket} socket - The Socket.IO socket object.
71
+ * @param {string} wsManagementId - Unique identifier for the WebSocket management context.
72
+ * @returns {Promise<void>}
73
+ */
74
+ async connection(socket, wsManagementId) {
75
+ try {
76
+ this.client[socket.id] = socket;
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);
80
+ logger.debug(`Socket ${socket.id} connected to channel ${this.channel}`);
81
+ } catch (error) {
82
+ logger.error(error, { channel: this.channel, wsManagementId, stack: error.stack });
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Handles incoming messages on the channel.
88
+ *
89
+ * @private
90
+ * @param {Socket} socket - The Socket.IO socket object.
91
+ * @param {any[]} args - The raw arguments received from the socket event.
92
+ * @param {string} wsManagementId - Unique identifier for the WebSocket management context.
93
+ * @returns {Promise<void>}
94
+ */
95
+ async controller(socket, args, wsManagementId) {
96
+ try {
97
+ if (!args || args.length === 0) {
98
+ logger.warn(`No arguments received for channel: ${this.channel}`, { socketId: socket.id });
99
+ return;
40
100
  }
41
- },
42
- };
43
- };
101
+ // Determine if JSON parsing is needed based on the stream flag
102
+ const payload = this.#IoInterface.stream ? args[0] : JSON.parse(args[0]);
103
+
104
+ await this.#IoInterface.controller(socket, this.client, payload, wsManagementId, args);
105
+ } catch (error) {
106
+ logger.error(error, { channel: this.channel, wsManagementId, socketId: socket.id, args, stack: error.stack });
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Handles a socket disconnection for this channel.
112
+ *
113
+ * @param {Socket} socket - The Socket.IO socket object.
114
+ * @param {string} reason - The reason for disconnection (e.g., 'client namespace disconnect').
115
+ * @param {string} wsManagementId - Unique identifier for the WebSocket management context.
116
+ * @returns {Promise<void>}
117
+ */
118
+ async disconnect(socket, reason, wsManagementId) {
119
+ try {
120
+ await this.#IoInterface.disconnect(socket, this.client, reason, wsManagementId);
121
+ delete this.client[socket.id];
122
+ logger.debug(`Socket ${socket.id} disconnected from channel ${this.channel}. Reason: ${reason}`);
123
+ } catch (error) {
124
+ logger.error(error, { channel: this.channel, wsManagementId, reason, socketId: socket.id, stack: error.stack });
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Backward compatibility function to create a new channel instance.
131
+ * @memberof SocketIoInterface
132
+ * @function IoCreateChannel
133
+ * @param {ChannelInterface} IoInterface - The interface object defining the channel's behavior.
134
+ * @returns {IoChannel} An instance of the IoChannel class.
135
+ */
136
+ const IoCreateChannel = (IoInterface) => new IoChannel(IoInterface);
44
137
 
45
- export { IoCreateChannel };
138
+ export { IoChannel, IoCreateChannel };