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.
- package/.github/workflows/npmpkg.ci.yml +1 -0
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +1 -0
- package/.vscode/settings.json +10 -5
- package/CHANGELOG.md +223 -2
- package/CLI-HELP.md +36 -7
- package/README.md +38 -9
- package/bin/build.js +27 -11
- package/bin/deploy.js +20 -21
- package/bin/file.js +32 -13
- package/bin/index.js +2 -1
- package/bin/vs.js +1 -1
- package/bump.config.js +26 -0
- package/conf.js +20 -4
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
- package/manifests/kind-config-dev.yaml +8 -0
- package/manifests/mongodb/pv-pvc.yaml +44 -8
- package/manifests/mongodb/statefulset.yaml +55 -68
- package/package.json +40 -25
- package/scripts/k3s-node-setup.sh +30 -11
- package/scripts/nat-iptables.sh +103 -18
- package/src/api/core/core.router.js +19 -14
- package/src/api/core/core.service.js +5 -5
- package/src/api/default/default.router.js +22 -18
- package/src/api/default/default.service.js +5 -5
- package/src/api/document/document.router.js +28 -23
- package/src/api/document/document.service.js +100 -23
- package/src/api/file/file.router.js +19 -13
- package/src/api/file/file.service.js +9 -7
- package/src/api/test/test.router.js +17 -12
- package/src/api/types.js +24 -0
- package/src/api/user/guest.service.js +5 -4
- package/src/api/user/user.router.js +297 -288
- package/src/api/user/user.service.js +100 -35
- package/src/cli/baremetal.js +20 -11
- package/src/cli/cluster.js +243 -55
- package/src/cli/db.js +106 -62
- package/src/cli/deploy.js +297 -154
- package/src/cli/fs.js +19 -3
- package/src/cli/index.js +37 -9
- package/src/cli/ipfs.js +4 -6
- package/src/cli/kubectl.js +4 -1
- package/src/cli/lxd.js +217 -135
- package/src/cli/release.js +289 -131
- package/src/cli/repository.js +91 -34
- package/src/cli/run.js +297 -56
- package/src/cli/test.js +9 -3
- package/src/client/Default.index.js +9 -3
- package/src/client/components/core/Auth.js +19 -5
- package/src/client/components/core/Docs.js +6 -34
- package/src/client/components/core/FileExplorer.js +6 -6
- package/src/client/components/core/Modal.js +65 -2
- package/src/client/components/core/PanelForm.js +56 -52
- package/src/client/components/core/Recover.js +4 -4
- package/src/client/components/core/Worker.js +170 -350
- package/src/client/services/default/default.management.js +20 -25
- package/src/client/services/user/guest.service.js +10 -3
- package/src/client/sw/core.sw.js +174 -112
- package/src/db/DataBaseProvider.js +120 -20
- package/src/db/mongo/MongoBootstrap.js +587 -0
- package/src/db/mongo/MongooseDB.js +126 -22
- package/src/index.js +1 -1
- package/src/runtime/express/Express.js +2 -2
- package/src/runtime/wp/Wp.js +8 -5
- package/src/server/auth.js +2 -2
- package/src/server/client-build-docs.js +1 -1
- package/src/server/client-build.js +94 -129
- package/src/server/conf.js +20 -65
- package/src/server/data-query.js +32 -20
- package/src/server/dns.js +22 -0
- package/src/server/process.js +180 -19
- package/src/server/runtime.js +1 -1
- package/src/server/start.js +26 -7
- package/src/server/valkey.js +9 -2
- package/src/ws/IoInterface.js +16 -16
- package/src/ws/core/channels/core.ws.chat.js +11 -11
- package/src/ws/core/channels/core.ws.mailer.js +29 -29
- package/src/ws/core/channels/core.ws.stream.js +19 -19
- package/src/ws/core/core.ws.connection.js +8 -8
- package/src/ws/core/core.ws.server.js +6 -5
- package/src/ws/default/channels/default.ws.main.js +10 -10
- package/src/ws/default/default.ws.connection.js +4 -4
- package/src/ws/default/default.ws.server.js +4 -3
- package/typedoc.json +10 -1
- package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
- package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
- /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
- /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
- /package/src/client/ssr/{pages → views}/Test.js +0 -0
package/src/server/start.js
CHANGED
|
@@ -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 = {
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/server/valkey.js
CHANGED
|
@@ -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
|
-
|
|
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 });
|
package/src/ws/IoInterface.js
CHANGED
|
@@ -46,9 +46,9 @@ class IoChannel {
|
|
|
46
46
|
constructor(IoInterface) {
|
|
47
47
|
this.#IoInterface = {
|
|
48
48
|
channel: '',
|
|
49
|
-
connection: async (socket = {}, client = {},
|
|
50
|
-
controller: async (socket = {}, client = {}, payload = {},
|
|
51
|
-
disconnect: async (socket = {}, client = {}, reason = '',
|
|
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}
|
|
71
|
+
* @param {string} hostKeyContext - Unique identifier for the WebSocket management context.
|
|
72
72
|
* @returns {Promise<void>}
|
|
73
73
|
*/
|
|
74
|
-
async connection(socket,
|
|
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,
|
|
79
|
-
await this.#IoInterface.connection(socket, this.client,
|
|
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,
|
|
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}
|
|
92
|
+
* @param {string} hostKeyContext - Unique identifier for the WebSocket management context.
|
|
93
93
|
* @returns {Promise<void>}
|
|
94
94
|
*/
|
|
95
|
-
async controller(socket, args,
|
|
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,
|
|
104
|
+
await this.#IoInterface.controller(socket, this.client, payload, hostKeyContext, args);
|
|
105
105
|
} catch (error) {
|
|
106
|
-
logger.error(error, { channel: this.channel,
|
|
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}
|
|
115
|
+
* @param {string} hostKeyContext - Unique identifier for the WebSocket management context.
|
|
116
116
|
* @returns {Promise<void>}
|
|
117
117
|
*/
|
|
118
|
-
async disconnect(socket, reason,
|
|
118
|
+
async disconnect(socket, reason, hostKeyContext) {
|
|
119
119
|
try {
|
|
120
|
-
await this.#IoInterface.disconnect(socket, this.client, reason,
|
|
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,
|
|
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
|
|
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,
|
|
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}
|
|
42
|
+
* @param {string} hostKeyContext - Unique server context ID.
|
|
43
43
|
*/
|
|
44
|
-
static init(
|
|
45
|
-
this.#state[
|
|
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}
|
|
51
|
+
* @param {string} hostKeyContext
|
|
52
52
|
*/
|
|
53
|
-
static connection(socket,
|
|
54
|
-
return this.#io.connection(socket,
|
|
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}
|
|
61
|
+
* @param {string} hostKeyContext
|
|
62
62
|
*/
|
|
63
|
-
static disconnect(socket, reason,
|
|
64
|
-
return this.#io.disconnect(socket, reason,
|
|
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 `[
|
|
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: `[
|
|
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,
|
|
24
|
+
controller(socket, client, payload, hostKeyContext) {
|
|
25
25
|
switch (payload.status) {
|
|
26
26
|
case 'register-user':
|
|
27
|
-
CoreWsMailerChannel.setUser(
|
|
27
|
+
CoreWsMailerChannel.setUser(hostKeyContext, socket.id, payload.user);
|
|
28
28
|
break;
|
|
29
29
|
case 'unregister-user':
|
|
30
|
-
CoreWsMailerChannel.removeSocket(
|
|
30
|
+
CoreWsMailerChannel.removeSocket(hostKeyContext, socket.id);
|
|
31
31
|
break;
|
|
32
32
|
default:
|
|
33
33
|
break;
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
|
-
disconnect(socket, client, reason,
|
|
37
|
-
CoreWsMailerChannel.removeSocket(
|
|
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}
|
|
53
|
+
* @param {string} hostKeyContext - Unique server context ID (`${host}${path}`).
|
|
54
54
|
*/
|
|
55
|
-
static init(
|
|
56
|
-
this.#data[
|
|
57
|
-
this.#userIndex[
|
|
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}
|
|
63
|
+
* @param {string} hostKeyContext
|
|
64
64
|
*/
|
|
65
|
-
static connection(socket,
|
|
66
|
-
return this.#io.connection(socket,
|
|
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}
|
|
73
|
+
* @param {string} hostKeyContext
|
|
74
74
|
*/
|
|
75
|
-
static disconnect(socket, reason,
|
|
76
|
-
return this.#io.disconnect(socket, reason,
|
|
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}
|
|
81
|
+
* @param {string} hostKeyContext
|
|
82
82
|
* @param {string} socketId
|
|
83
83
|
* @param {Object} user - User data with `_id` property.
|
|
84
84
|
*/
|
|
85
|
-
static setUser(
|
|
86
|
-
this.#data[
|
|
85
|
+
static setUser(hostKeyContext, socketId, user) {
|
|
86
|
+
this.#data[hostKeyContext][socketId] = { model: { user } };
|
|
87
87
|
if (user?._id) {
|
|
88
|
-
this.#userIndex[
|
|
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}
|
|
94
|
+
* @param {string} hostKeyContext
|
|
95
95
|
* @param {string} socketId
|
|
96
96
|
*/
|
|
97
|
-
static removeSocket(
|
|
98
|
-
const entry = this.#data[
|
|
97
|
+
static removeSocket(hostKeyContext, socketId) {
|
|
98
|
+
const entry = this.#data[hostKeyContext]?.[socketId];
|
|
99
99
|
if (entry?.model?.user?._id) {
|
|
100
|
-
delete this.#userIndex[
|
|
100
|
+
delete this.#userIndex[hostKeyContext][entry.model.user._id.toString()];
|
|
101
101
|
}
|
|
102
|
-
delete this.#data[
|
|
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}
|
|
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(
|
|
112
|
-
return this.#userIndex[
|
|
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 `[
|
|
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,
|
|
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[
|
|
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[
|
|
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,
|
|
38
|
-
CoreWsStreamChannel.#state[
|
|
37
|
+
connection(socket, client, hostKeyContext) {
|
|
38
|
+
CoreWsStreamChannel.#state[hostKeyContext][socket.id] = [];
|
|
39
39
|
},
|
|
40
|
-
disconnect(socket, client, reason,
|
|
41
|
-
const entry = CoreWsStreamChannel.#state[
|
|
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[
|
|
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[
|
|
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}
|
|
64
|
+
* @param {string} hostKeyContext - Unique server context ID (`${host}${path}`).
|
|
65
65
|
*/
|
|
66
|
-
static init(
|
|
67
|
-
this.#state[
|
|
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}
|
|
73
|
+
* @param {string} hostKeyContext
|
|
74
74
|
*/
|
|
75
|
-
static connection(socket,
|
|
76
|
-
return this.#io.connection(socket,
|
|
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}
|
|
83
|
+
* @param {string} hostKeyContext
|
|
84
84
|
*/
|
|
85
|
-
static disconnect(socket, reason,
|
|
86
|
-
return this.#io.disconnect(socket, reason,
|
|
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}
|
|
22
|
+
* @param {string} hostKeyContext
|
|
23
23
|
*/
|
|
24
|
-
static handle(socket,
|
|
24
|
+
static handle(socket, hostKeyContext) {
|
|
25
25
|
logger.info(`New connection established. Socket ID: ${socket.id}`);
|
|
26
26
|
|
|
27
|
-
CoreWsChatChannel.connection(socket,
|
|
28
|
-
CoreWsMailerChannel.connection(socket,
|
|
29
|
-
CoreWsStreamChannel.connection(socket,
|
|
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,
|
|
35
|
-
CoreWsMailerChannel.disconnect(socket, reason,
|
|
36
|
-
CoreWsStreamChannel.disconnect(socket, reason,
|
|
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
|
|
31
|
+
const hostKeyContext = resolveHostKeyContext({ host, path });
|
|
31
32
|
|
|
32
|
-
CoreWsChatChannel.init(
|
|
33
|
-
CoreWsMailerChannel.init(
|
|
34
|
-
CoreWsStreamChannel.init(
|
|
33
|
+
CoreWsChatChannel.init(hostKeyContext);
|
|
34
|
+
CoreWsMailerChannel.init(hostKeyContext);
|
|
35
|
+
CoreWsStreamChannel.init(hostKeyContext);
|
|
35
36
|
|
|
36
|
-
return IoServer.create(httpServer, options, (socket) => CoreWsConnectionHandler.handle(socket,
|
|
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
|
|
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}
|
|
31
|
+
* @param {string} hostKeyContext
|
|
32
32
|
*/
|
|
33
|
-
static init(
|
|
34
|
-
this.#state[
|
|
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}
|
|
40
|
+
* @param {string} hostKeyContext
|
|
41
41
|
*/
|
|
42
|
-
static connection(socket,
|
|
43
|
-
return this.#io.connection(socket,
|
|
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}
|
|
50
|
+
* @param {string} hostKeyContext
|
|
51
51
|
*/
|
|
52
|
-
static disconnect(socket, reason,
|
|
53
|
-
return this.#io.disconnect(socket, reason,
|
|
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}
|
|
20
|
+
* @param {string} hostKeyContext
|
|
21
21
|
*/
|
|
22
|
-
static handle(socket,
|
|
22
|
+
static handle(socket, hostKeyContext) {
|
|
23
23
|
logger.info(`DefaultWsConnection ${socket.id}`);
|
|
24
24
|
|
|
25
|
-
DefaultWsMainChannel.connection(socket,
|
|
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,
|
|
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
|
|
28
|
+
const hostKeyContext = resolveHostKeyContext({ host, path });
|
|
28
29
|
|
|
29
|
-
DefaultWsMainChannel.init(
|
|
30
|
+
DefaultWsMainChannel.init(hostKeyContext);
|
|
30
31
|
|
|
31
|
-
return IoServer.create(httpServer, options, (socket) => DefaultWsConnectionHandler.handle(socket,
|
|
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": [
|
|
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/",
|