@cocreate/server-mesh 1.2.0 → 1.2.2
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/automated.yml +1 -1
- package/CHANGELOG.md +7 -0
- package/README.md +326 -36
- package/package.json +14 -10
- package/release.config.js +1 -1
- package/src/inbound.js +226 -0
- package/src/index.js +333 -631
- package/src/outbound.js +222 -0
package/src/index.js
CHANGED
|
@@ -6,727 +6,429 @@
|
|
|
6
6
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
7
7
|
* (at your option) any later version.
|
|
8
8
|
*
|
|
9
|
-
* This program is distributed in
|
|
10
|
-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
-
* GNU Affero General Public License for more details.
|
|
9
|
+
* This program is distributed in commercials or open source under GNU AGPLv3.
|
|
13
10
|
*
|
|
14
11
|
* You should have received a copy of the GNU Affero General Public License
|
|
15
12
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
13
|
*
|
|
17
14
|
********************************************************************************/
|
|
18
15
|
|
|
19
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
20
|
-
import https from 'node:https';
|
|
21
|
-
import fs from 'node:fs';
|
|
22
|
-
import path from 'node:path';
|
|
23
|
-
import os from 'node:os';
|
|
24
|
-
import cluster from 'node:cluster';
|
|
25
|
-
import { execSync } from 'node:child_process';
|
|
26
16
|
import { EventEmitter } from 'node:events';
|
|
27
17
|
import crypto from 'node:crypto';
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const retryTimers = new Map(); // peerIp -> Timeout ID
|
|
31
|
-
const pruneTimers = new Map(); // peerIp -> Timeout ID
|
|
32
|
-
const orgWorkers = new Map(); // organization_id -> Set(workerId)
|
|
33
|
-
const orgServers = new Map(); // organization_id -> Set(WebSocket)
|
|
34
|
-
|
|
35
|
-
let isGracefulExitRunning = false;
|
|
36
|
-
let wsPingInterval = null;
|
|
37
|
-
|
|
38
|
-
let serverCtx = null;
|
|
39
|
-
const MESH_PORT = process.env.MESH_PORT || 8090;
|
|
40
|
-
let meshWss = null;
|
|
41
|
-
let secureMeshServer = null;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Safely loops and sends JSON payloads to one or multiple sockets.
|
|
45
|
-
* Wraps individual operations in independent try-catch layers to avoid process instability.
|
|
46
|
-
*
|
|
47
|
-
* @param {WebSocket|WebSocket[]} sockets - Target WebSocket(s)
|
|
48
|
-
* @param {Object} data - Payload content to dispatch
|
|
49
|
-
*/
|
|
50
|
-
function socketSend(sockets, data) {
|
|
51
|
-
const targetSockets = Array.isArray(sockets) ? sockets : [sockets];
|
|
52
|
-
const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
|
|
53
|
-
|
|
54
|
-
for (const socket of targetSockets) {
|
|
55
|
-
try {
|
|
56
|
-
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
57
|
-
socket.send(dataStr);
|
|
58
|
-
}
|
|
59
|
-
} catch (err) {
|
|
60
|
-
console.error(`[@cocreate/server-mesh] Error executing socketSend transaction:`, err.message);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
18
|
+
import { ServerMeshInbound } from './inbound.js';
|
|
19
|
+
import { ServerMeshOutbound } from './outbound.js';
|
|
64
20
|
|
|
65
21
|
/**
|
|
66
|
-
* ServerMesh
|
|
67
|
-
*
|
|
22
|
+
* ServerMesh orchestrates multi-tenant routing, unified IPC event forwarding,
|
|
23
|
+
* shared active connection pools, and coordinates ServerMeshInbound and ServerMeshOutbound.
|
|
68
24
|
*/
|
|
69
|
-
class ServerMesh extends EventEmitter {
|
|
25
|
+
export class ServerMesh extends EventEmitter {
|
|
70
26
|
constructor() {
|
|
71
27
|
super();
|
|
28
|
+
this.server = null;
|
|
29
|
+
|
|
30
|
+
// Sub-module transport managers
|
|
31
|
+
this.inboundManager = new ServerMeshInbound(this);
|
|
32
|
+
this.outboundManager = new ServerMeshOutbound(this);
|
|
33
|
+
|
|
34
|
+
// Shared Connection & Multi-tenant Routing Tables
|
|
35
|
+
this.activeConnections = new Map(); // serverId / IP -> Network Socket
|
|
36
|
+
this.orgWorkers = new Map(); // organization_id -> Set(workerId)
|
|
37
|
+
this.orgServers = new Map(); // organization_id -> Set(Network Socket)
|
|
38
|
+
|
|
39
|
+
this.isClosed = false;
|
|
40
|
+
this.handleServerEvent = this.handleServerEvent.bind(this);
|
|
72
41
|
}
|
|
73
42
|
|
|
74
43
|
/**
|
|
75
|
-
*
|
|
76
|
-
* Automatically appends a unique ID (uid) to guarantee end-to-end tracking.
|
|
44
|
+
* Initializes the ServerMesh orchestrator with the CoCreateServer instance context.
|
|
77
45
|
*
|
|
78
|
-
* @param {Object}
|
|
46
|
+
* @param {Object} server - CoCreateServer instance
|
|
47
|
+
* @returns {Promise<ServerMesh>}
|
|
79
48
|
*/
|
|
80
|
-
|
|
81
|
-
if (!
|
|
82
|
-
|
|
83
|
-
return;
|
|
49
|
+
async init(server) {
|
|
50
|
+
if (!server) {
|
|
51
|
+
throw new Error('[@cocreate/server-mesh] Failed to initialize: server instance is required.');
|
|
84
52
|
}
|
|
85
53
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
54
|
+
this.server = server;
|
|
55
|
+
this.server.mesh = this; // Attach mesh instance directly to server context
|
|
56
|
+
this.isClosed = false;
|
|
90
57
|
|
|
91
|
-
|
|
58
|
+
// Register IPC listeners on server context
|
|
59
|
+
this.setupServerListeners();
|
|
92
60
|
|
|
93
|
-
|
|
94
|
-
// Worker Thread: Forward to Master process via native child IPC to relay
|
|
95
|
-
if (typeof process.send === 'function') {
|
|
96
|
-
process.send({
|
|
97
|
-
method: 'mesh.outbound_mesh_relay',
|
|
98
|
-
payload: payload
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
} else {
|
|
102
|
-
// Master Process: Determine target sockets dynamically based on payload directives
|
|
103
|
-
const targets = [];
|
|
61
|
+
const isPrimary = this.server.isPrimary ?? true;
|
|
104
62
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// Multicast/Unicast Routing: Handle polymorphic servers filter (single string or array)
|
|
110
|
-
const serversList = Array.isArray(payload.servers)
|
|
111
|
-
? payload.servers
|
|
112
|
-
: [payload.servers];
|
|
63
|
+
if (!isPrimary) {
|
|
64
|
+
console.log(`[@cocreate/server-mesh] Initialized mesh layer in worker process context`);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
113
67
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
targets.push(socket);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
68
|
+
// Initialize inbound and outbound sub-modules for Primary node
|
|
69
|
+
await this.inboundManager.init(this.server);
|
|
70
|
+
await this.outboundManager.init(this.server);
|
|
121
71
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// Dispatch safely using standard wrapper loop
|
|
126
|
-
socketSend(uniqueSockets, payload);
|
|
127
|
-
}
|
|
72
|
+
console.log(`[@cocreate/server-mesh] Initialized server mesh layer for node ${this.server.id || 'unassigned'}`);
|
|
73
|
+
return this;
|
|
128
74
|
}
|
|
129
75
|
|
|
130
76
|
/**
|
|
131
|
-
*
|
|
132
|
-
* @returns {Promise<void>} Resolves when all connections have dropped and port binds are freed
|
|
77
|
+
* Binds IPC message and broadcasting event handlers to this.server.
|
|
133
78
|
*/
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const serverMeshInstance = new ServerMesh();
|
|
140
|
-
|
|
141
|
-
// Listener A: Handle peer registration of an organization subscription
|
|
142
|
-
serverMeshInstance.on('mesh.orgAdded', (message, ws) => {
|
|
143
|
-
if (!ws) return;
|
|
144
|
-
const organization_id = message.organization_id;
|
|
145
|
-
const ip = ws.remoteAddress || ws._socket?.remoteAddress;
|
|
146
|
-
const peerId = message.senderId || ip;
|
|
147
|
-
|
|
148
|
-
if (!orgServers.has(organization_id)) {
|
|
149
|
-
orgServers.set(organization_id, new Set());
|
|
150
|
-
}
|
|
151
|
-
orgServers.get(organization_id).add(ws);
|
|
152
|
-
console.log(`[@cocreate/server-mesh] Peer ${peerId} registered subscription for org "${organization_id}"`);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// Listener B: Handle peer de-registration of an organization subscription
|
|
156
|
-
serverMeshInstance.on('mesh.orgDeleted', (message, ws) => {
|
|
157
|
-
if (!ws) return;
|
|
158
|
-
const organization_id = message.organization_id;
|
|
159
|
-
const ip = ws.remoteAddress || ws._socket?.remoteAddress;
|
|
160
|
-
const peerId = message.senderId || ip;
|
|
161
|
-
|
|
162
|
-
const sockets = orgServers.get(organization_id);
|
|
163
|
-
if (sockets) {
|
|
164
|
-
sockets.delete(ws);
|
|
165
|
-
if (sockets.size === 0) {
|
|
166
|
-
orgServers.delete(organization_id);
|
|
167
|
-
}
|
|
168
|
-
console.log(`[@cocreate/server-mesh] Peer ${peerId} de-registered subscription from org "${organization_id}"`);
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Listener C: Handle incoming real-time network unicast transaction data
|
|
173
|
-
serverMeshInstance.on('mesh.orgData', (message) => {
|
|
174
|
-
const { organization_id, data } = message;
|
|
175
|
-
|
|
176
|
-
// Local Unicast: Relay directly to local child worker threads hosting this tenant
|
|
177
|
-
const activeWorkerIds = orgWorkers.get(organization_id);
|
|
178
|
-
if (activeWorkerIds && activeWorkerIds.size > 0 && serverCtx) {
|
|
179
|
-
try {
|
|
180
|
-
serverCtx.send({
|
|
181
|
-
method: 'mesh.inbound',
|
|
182
|
-
workers: Array.from(activeWorkerIds),
|
|
183
|
-
master: false,
|
|
184
|
-
data: typeof data === 'string' ? data : JSON.stringify(data)
|
|
185
|
-
});
|
|
186
|
-
} catch (err) {
|
|
187
|
-
console.error(`[@cocreate/server-mesh] Failed to relay incoming orgData to workers of org "${organization_id}":`, err.message);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// Listener D: Handle cluster coordination to cancel DB prune schedules on peer consensus
|
|
193
|
-
serverMeshInstance.on('mesh.peer_pruned', (message) => {
|
|
194
|
-
if (message.deadIp) {
|
|
195
|
-
const activeTimerId = pruneTimers.get(message.deadIp);
|
|
196
|
-
if (activeTimerId) {
|
|
197
|
-
clearTimeout(activeTimerId);
|
|
198
|
-
pruneTimers.delete(message.deadIp);
|
|
199
|
-
console.log(`[@cocreate/server-mesh] Cancelled DB prune for ${message.deadIp} due to peer consensus.`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
});
|
|
79
|
+
setupServerListeners() {
|
|
80
|
+
if (!this.server || typeof this.server.on !== 'function') return;
|
|
203
81
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
serverCtx.send({
|
|
209
|
-
method: 'mesh.inbound',
|
|
210
|
-
broadcast: true,
|
|
211
|
-
master: false,
|
|
212
|
-
data: JSON.stringify(message)
|
|
213
|
-
});
|
|
214
|
-
} catch (err) {
|
|
215
|
-
console.error("[@cocreate/server-mesh] Failed to relay raw fallback message to workers:", err.message);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
});
|
|
82
|
+
// Main unified IPC entry point
|
|
83
|
+
this.server.on('mesh', (payload) => {
|
|
84
|
+
this.handleIPCPayload(payload);
|
|
85
|
+
});
|
|
219
86
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (!result) return [];
|
|
225
|
-
if (Array.isArray(result)) return result;
|
|
226
|
-
if (Array.isArray(result.object)) return result.object;
|
|
227
|
-
if (Array.isArray(result.data)) return result.data;
|
|
228
|
-
return [];
|
|
229
|
-
}
|
|
87
|
+
// Direct mesh dispatch listener
|
|
88
|
+
this.server.on('mesh.send', (payload) => {
|
|
89
|
+
this.send(payload);
|
|
90
|
+
});
|
|
230
91
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
*/
|
|
234
|
-
function generateTLSCredentials() {
|
|
235
|
-
const tmpDir = os.tmpdir();
|
|
236
|
-
const keyPath = path.join(tmpDir, `mesh-key-${Date.now()}-${crypto.randomBytes(4).toString('hex')}.pem`);
|
|
237
|
-
const certPath = path.join(tmpDir, `mesh-cert-${Date.now()}-${crypto.randomBytes(4).toString('hex')}.pem`);
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
execSync(`openssl req -x509 -newkey rsa:2048 -keyout "${keyPath}" -out "${certPath}" -days 1 -nodes -subj "/CN=localhost"`, { stdio: 'ignore' });
|
|
241
|
-
const key = fs.readFileSync(keyPath);
|
|
242
|
-
const cert = fs.readFileSync(certPath);
|
|
243
|
-
|
|
244
|
-
// Cleanup generated temporary certificate files
|
|
245
|
-
try { fs.unlinkSync(keyPath); } catch {}
|
|
246
|
-
try { fs.unlinkSync(certPath); } catch {}
|
|
247
|
-
|
|
248
|
-
return { key, cert };
|
|
249
|
-
} catch (err) {
|
|
250
|
-
console.error('[@cocreate/server-mesh] Failed to generate ephemeral TLS credentials:', err.message);
|
|
251
|
-
return null;
|
|
92
|
+
// Register protocol routing logic
|
|
93
|
+
this.setupProtocolListeners();
|
|
252
94
|
}
|
|
253
|
-
}
|
|
254
95
|
|
|
255
|
-
/**
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Handles inbound IPC payloads from server.on('mesh').
|
|
98
|
+
*
|
|
99
|
+
* @param {Object} payload - IPC message containing 'method' property
|
|
100
|
+
*/
|
|
101
|
+
handleIPCPayload(payload) {
|
|
102
|
+
if (!payload || typeof payload !== 'object' || !payload.method) return;
|
|
103
|
+
|
|
104
|
+
const { method } = payload;
|
|
105
|
+
|
|
106
|
+
if (method === 'mesh.send' || method === 'mesh.outbound_mesh_relay') {
|
|
107
|
+
const dataToTransmit = payload.payload ? payload.payload : payload;
|
|
108
|
+
this.send(dataToTransmit);
|
|
109
|
+
}
|
|
110
|
+
else if (method === 'mesh.registerOrg' || method === 'mesh.orgAdded') {
|
|
111
|
+
const { organization_id, workerId } = payload;
|
|
112
|
+
let isNewOrg = false;
|
|
113
|
+
|
|
114
|
+
if (!this.orgWorkers.has(organization_id)) {
|
|
115
|
+
this.orgWorkers.set(organization_id, new Set());
|
|
116
|
+
isNewOrg = true;
|
|
264
117
|
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Boots the socket mesh and configures routing tunnels.
|
|
271
|
-
*
|
|
272
|
-
* @param {Object} server - CoCreateServer instance context
|
|
273
|
-
*/
|
|
274
|
-
export async function init(server) {
|
|
275
|
-
serverCtx = server;
|
|
276
|
-
|
|
277
|
-
const isPrimary = serverCtx.isPrimary;
|
|
278
118
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
// Monitor process IPC from Primary
|
|
282
|
-
process.on('message', (message) => {
|
|
283
|
-
if (message && message.method) {
|
|
284
|
-
serverMeshInstance.emit(message.method, message);
|
|
119
|
+
if (workerId) {
|
|
120
|
+
this.orgWorkers.get(organization_id).add(workerId);
|
|
285
121
|
}
|
|
286
|
-
});
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// --- MASTER TUNNEL & UNICAST ROUTING PIPELINE ---
|
|
291
|
-
// Handle incoming IPC registrations from child workers
|
|
292
|
-
cluster.on('message', (workerRef, message) => {
|
|
293
|
-
if (message && message.method) {
|
|
294
|
-
if (message.method === 'mesh.registerOrg') {
|
|
295
|
-
const { organization_id, workerId } = message;
|
|
296
|
-
let isNewOrg = false;
|
|
297
|
-
|
|
298
|
-
if (!orgWorkers.has(organization_id)) {
|
|
299
|
-
orgWorkers.set(organization_id, new Set());
|
|
300
|
-
isNewOrg = true;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
orgWorkers.get(organization_id).add(workerId);
|
|
304
|
-
console.log(`[@cocreate/server-mesh] [LOCAL-MAP] Tenant "${organization_id}" registered on Worker ${workerId}`);
|
|
305
122
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
123
|
+
if (isNewOrg) {
|
|
124
|
+
this.send({
|
|
125
|
+
method: 'mesh.orgAdded',
|
|
126
|
+
organization_id,
|
|
127
|
+
senderId: this.server.id
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if (method === 'mesh.deregisterOrg' || method === 'mesh.orgDeleted') {
|
|
132
|
+
const { organization_id, workerId } = payload;
|
|
133
|
+
const workersSet = this.orgWorkers.get(organization_id);
|
|
134
|
+
|
|
135
|
+
if (workersSet) {
|
|
136
|
+
if (workerId) workersSet.delete(workerId);
|
|
137
|
+
if (!workerId || workersSet.size === 0) {
|
|
138
|
+
this.orgWorkers.delete(organization_id);
|
|
139
|
+
this.send({
|
|
140
|
+
method: 'mesh.orgDeleted',
|
|
309
141
|
organization_id,
|
|
310
|
-
senderId:
|
|
142
|
+
senderId: this.server.id
|
|
311
143
|
});
|
|
312
144
|
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else if (method === 'mesh.orgData') {
|
|
148
|
+
const { organization_id, data, senderWorkerId } = payload;
|
|
149
|
+
|
|
150
|
+
// Relay locally to sibling workers hosting tenant
|
|
151
|
+
const activeWorkerIds = this.orgWorkers.get(organization_id);
|
|
152
|
+
if (activeWorkerIds && activeWorkerIds.size > 0 && this.server) {
|
|
153
|
+
const siblingWorkerIds = senderWorkerId
|
|
154
|
+
? Array.from(activeWorkerIds).filter(wId => wId !== senderWorkerId)
|
|
155
|
+
: Array.from(activeWorkerIds);
|
|
156
|
+
|
|
157
|
+
if (siblingWorkerIds.length > 0) {
|
|
158
|
+
try {
|
|
159
|
+
this.server.send({
|
|
160
|
+
method: 'mesh.inbound',
|
|
161
|
+
workers: siblingWorkerIds,
|
|
162
|
+
master: false,
|
|
163
|
+
data: typeof data === 'string' ? data : JSON.stringify(data)
|
|
327
164
|
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error("[@cocreate/server-mesh] Failed to relay local orgData:", err.message);
|
|
328
167
|
}
|
|
329
168
|
}
|
|
330
|
-
}
|
|
331
|
-
else if (message.method === 'mesh.orgData') {
|
|
332
|
-
const { organization_id, data, senderWorkerId } = message;
|
|
333
|
-
|
|
334
|
-
// Local Unicast: Relay directly to child worker cores hosting the tenant (ignoring sender)
|
|
335
|
-
const activeWorkerIds = orgWorkers.get(organization_id);
|
|
336
|
-
if (activeWorkerIds && activeWorkerIds.size > 0) {
|
|
337
|
-
const siblingWorkerIds = Array.from(activeWorkerIds).filter(wId => wId !== senderWorkerId);
|
|
338
|
-
if (siblingWorkerIds.length > 0) {
|
|
339
|
-
try {
|
|
340
|
-
serverCtx.send({
|
|
341
|
-
method: 'mesh.inbound',
|
|
342
|
-
workers: siblingWorkerIds,
|
|
343
|
-
master: false,
|
|
344
|
-
data: typeof data === 'string' ? data : JSON.stringify(data)
|
|
345
|
-
});
|
|
346
|
-
} catch (err) {
|
|
347
|
-
console.error("[@cocreate/server-mesh] Failed to propagate orgData to local siblings:", err.message);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
169
|
+
}
|
|
351
170
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
171
|
+
// Relay to network peer sockets subscribed to tenant
|
|
172
|
+
const targetSockets = this.orgServers.get(organization_id);
|
|
173
|
+
if (targetSockets && targetSockets.size > 0) {
|
|
174
|
+
this.socketSend(Array.from(targetSockets), {
|
|
175
|
+
method: 'mesh.orgData',
|
|
176
|
+
organization_id,
|
|
177
|
+
data
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
const targetMethod = method.replace(/^mesh\./, '');
|
|
183
|
+
const systemEvents = ['restart', 'reboot', 'shutdown', 'cordon', 'shed', 'drain'];
|
|
184
|
+
|
|
185
|
+
if (systemEvents.includes(targetMethod)) {
|
|
186
|
+
const actionPayload = { ...payload, method: targetMethod };
|
|
187
|
+
if (typeof this.server.send === 'function') {
|
|
188
|
+
this.server.send(actionPayload);
|
|
367
189
|
}
|
|
190
|
+
this.emit(targetMethod, actionPayload);
|
|
191
|
+
} else {
|
|
192
|
+
this.emit(method, payload);
|
|
368
193
|
}
|
|
369
|
-
|
|
370
|
-
// Mirror all incoming process IPC commands to local listeners
|
|
371
|
-
serverMeshInstance.emit(message.method, message, workerRef);
|
|
372
194
|
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Registers protocol handlers for multi-tenant subscriptions and consensus events.
|
|
199
|
+
*/
|
|
200
|
+
setupProtocolListeners() {
|
|
201
|
+
this.on('mesh.orgAdded', (message, ws) => {
|
|
202
|
+
if (!ws) return;
|
|
203
|
+
const organization_id = message.organization_id;
|
|
204
|
+
const ip = ws.remoteAddress || ws._socket?.remoteAddress;
|
|
205
|
+
const peerId = message.senderId || ip;
|
|
206
|
+
|
|
207
|
+
if (!this.orgServers.has(organization_id)) {
|
|
208
|
+
this.orgServers.set(organization_id, new Set());
|
|
385
209
|
}
|
|
210
|
+
this.orgServers.get(organization_id).add(ws);
|
|
211
|
+
console.log(`[@cocreate/server-mesh] Peer ${peerId} registered subscription for org "${organization_id}"`);
|
|
386
212
|
});
|
|
387
213
|
|
|
388
|
-
|
|
389
|
-
|
|
214
|
+
this.on('mesh.orgDeleted', (message, ws) => {
|
|
215
|
+
if (!ws) return;
|
|
216
|
+
const organization_id = message.organization_id;
|
|
217
|
+
const ip = ws.remoteAddress || ws._socket?.remoteAddress;
|
|
218
|
+
const peerId = message.senderId || ip;
|
|
219
|
+
|
|
220
|
+
const sockets = this.orgServers.get(organization_id);
|
|
221
|
+
if (sockets) {
|
|
222
|
+
sockets.delete(ws);
|
|
223
|
+
if (sockets.size === 0) {
|
|
224
|
+
this.orgServers.delete(organization_id);
|
|
225
|
+
}
|
|
226
|
+
console.log(`[@cocreate/server-mesh] Peer ${peerId} de-registered subscription from org "${organization_id}"`);
|
|
227
|
+
}
|
|
390
228
|
});
|
|
391
|
-
} else {
|
|
392
|
-
throw new Error("Unable to boot secure mesh: TLS credential generation failed.");
|
|
393
|
-
}
|
|
394
229
|
|
|
395
|
-
|
|
230
|
+
this.on('mesh.orgData', (message) => {
|
|
231
|
+
const { organization_id, data } = message;
|
|
396
232
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
233
|
+
const activeWorkerIds = this.orgWorkers.get(organization_id);
|
|
234
|
+
if (activeWorkerIds && activeWorkerIds.size > 0 && this.server) {
|
|
235
|
+
try {
|
|
236
|
+
this.server.send({
|
|
237
|
+
method: 'mesh.inbound',
|
|
238
|
+
workers: Array.from(activeWorkerIds),
|
|
239
|
+
master: false,
|
|
240
|
+
data: typeof data === 'string' ? data : JSON.stringify(data)
|
|
241
|
+
});
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error(`[@cocreate/server-mesh] Failed to relay incoming orgData for org "${organization_id}":`, err.message);
|
|
244
|
+
}
|
|
408
245
|
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const identifier = remoteServerId || ip;
|
|
412
|
-
console.log(`[@cocreate/server-mesh] Secure peer connection established: ${identifier}`);
|
|
413
|
-
|
|
414
|
-
ws.isAlive = true;
|
|
415
|
-
ws.on('pong', () => { ws.isAlive = true; });
|
|
246
|
+
});
|
|
416
247
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
248
|
+
this.on('mesh.peer_pruned', (message) => {
|
|
249
|
+
if (message.deadIp && this.outboundManager) {
|
|
250
|
+
const activeTimerId = this.outboundManager.pruneTimers.get(message.deadIp);
|
|
251
|
+
if (activeTimerId) {
|
|
252
|
+
clearTimeout(activeTimerId);
|
|
253
|
+
this.outboundManager.pruneTimers.delete(message.deadIp);
|
|
254
|
+
console.log(`[@cocreate/server-mesh] Cancelled prune schedule for ${message.deadIp} on peer consensus.`);
|
|
423
255
|
}
|
|
424
|
-
} catch (err) {
|
|
425
|
-
// Safely catch parsing failures
|
|
426
256
|
}
|
|
427
257
|
});
|
|
428
258
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
259
|
+
this.on('mesh.raw_message', (message) => {
|
|
260
|
+
if (this.server) {
|
|
261
|
+
try {
|
|
262
|
+
this.server.send({
|
|
263
|
+
method: 'mesh.inbound',
|
|
264
|
+
broadcast: true,
|
|
265
|
+
master: false,
|
|
266
|
+
data: JSON.stringify(message)
|
|
267
|
+
});
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.error("[@cocreate/server-mesh] Failed to relay raw fallback message:", err.message);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
438
272
|
});
|
|
439
|
-
|
|
440
|
-
activeConnections.set(identifier, ws);
|
|
441
|
-
if (remoteServerId) activeConnections.set(ip, ws);
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
try {
|
|
445
|
-
await getServers();
|
|
446
|
-
} catch (err) {
|
|
447
|
-
console.error("[@cocreate/server-mesh] Peer discovery reconciliation cycle failed on start:", err.message);
|
|
448
273
|
}
|
|
449
|
-
|
|
450
|
-
wsPingInterval = setInterval(pingActiveWebsockets, 30000);
|
|
451
|
-
}
|
|
452
274
|
|
|
453
|
-
/**
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Parses incoming network socket payloads and dispatches events.
|
|
277
|
+
*
|
|
278
|
+
* @param {string|Buffer} rawData
|
|
279
|
+
* @param {Object} ws - Active network socket reference
|
|
280
|
+
*/
|
|
281
|
+
handleIncomingMessage(rawData, ws) {
|
|
282
|
+
let message;
|
|
460
283
|
try {
|
|
461
|
-
|
|
462
|
-
method: 'object.delete',
|
|
463
|
-
array: 'servers',
|
|
464
|
-
$filter: {
|
|
465
|
-
query: { 'infrastructure.instanceId': instanceId },
|
|
466
|
-
limit: 0
|
|
467
|
-
},
|
|
468
|
-
organization_id: serverCtx.organization_id,
|
|
469
|
-
host: serverCtx.host
|
|
470
|
-
});
|
|
284
|
+
message = JSON.parse(rawData.toString());
|
|
471
285
|
} catch (err) {
|
|
472
|
-
|
|
286
|
+
return;
|
|
473
287
|
}
|
|
474
|
-
}
|
|
475
288
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
289
|
+
if (!message || typeof message !== 'object') return;
|
|
290
|
+
|
|
291
|
+
if (message.server_id && message.server_id === this.server?.id) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const method = message.method;
|
|
296
|
+
if (!method) return;
|
|
297
|
+
|
|
298
|
+
const targetMethod = method.replace(/^mesh\./, '');
|
|
299
|
+
const systemEvents = ['restart', 'reboot', 'shutdown', 'cordon', 'shed', 'drain'];
|
|
300
|
+
|
|
301
|
+
if (systemEvents.includes(targetMethod)) {
|
|
302
|
+
const ipcPayload = { ...message, method: targetMethod };
|
|
303
|
+
|
|
304
|
+
if (this.server && typeof this.server.send === 'function') {
|
|
305
|
+
this.server.send(ipcPayload);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.emit(targetMethod, ipcPayload, ws);
|
|
494
309
|
} else {
|
|
495
|
-
|
|
310
|
+
this.emit(method, message, ws);
|
|
496
311
|
}
|
|
497
|
-
} catch (err) {
|
|
498
|
-
console.error("[@cocreate/server-mesh] Startup registration network error:", err.message);
|
|
499
312
|
}
|
|
500
|
-
}
|
|
501
313
|
|
|
502
|
-
/**
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
return ws.terminate();
|
|
314
|
+
/**
|
|
315
|
+
* Transmits payload across active network socket tunnels or targeted servers.
|
|
316
|
+
*
|
|
317
|
+
* @param {Object} payload
|
|
318
|
+
*/
|
|
319
|
+
send(payload) {
|
|
320
|
+
if (!payload || typeof payload !== 'object' || !payload.method) {
|
|
321
|
+
console.error("[@cocreate/server-mesh] Cannot transmit mesh payload without a defined 'method' key.");
|
|
322
|
+
return;
|
|
512
323
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
} catch (err) {
|
|
517
|
-
console.error("[@cocreate/server-mesh] Error during websocket ping:", err.message);
|
|
324
|
+
|
|
325
|
+
if (!payload.uuid) {
|
|
326
|
+
payload.uuid = crypto.randomUUID();
|
|
518
327
|
}
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
328
|
|
|
522
|
-
|
|
523
|
-
* Cleanly handles node evictions and releases network bindings.
|
|
524
|
-
* Rewritten to support fully coordinated async invocation from Primary Server controller without process.exit
|
|
525
|
-
* @returns {Promise<void>} Resolves when the HTTPS mesh server port is successfully released.
|
|
526
|
-
*/
|
|
527
|
-
async function closeMesh() {
|
|
528
|
-
if (isGracefulExitRunning) return;
|
|
529
|
-
isGracefulExitRunning = true;
|
|
530
|
-
|
|
531
|
-
console.log(`[@cocreate/server-mesh] Coordinated secure mesh shutdown triggered. Evicting state...`);
|
|
329
|
+
const isPrimary = this.server?.isPrimary ?? true;
|
|
532
330
|
|
|
533
|
-
|
|
331
|
+
if (!isPrimary) {
|
|
332
|
+
if (typeof this.server?.send === 'function') {
|
|
333
|
+
this.server.send({
|
|
334
|
+
method: 'mesh.outbound_mesh_relay',
|
|
335
|
+
payload: payload
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
const targets = [];
|
|
534
340
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
341
|
+
if (payload.broadcast === true || !payload.servers) {
|
|
342
|
+
targets.push(...this.activeConnections.values());
|
|
343
|
+
} else {
|
|
344
|
+
const serversList = Array.isArray(payload.servers) ? payload.servers : [payload.servers];
|
|
345
|
+
for (const id of serversList) {
|
|
346
|
+
const socket = this.activeConnections.get(id);
|
|
347
|
+
if (socket) {
|
|
348
|
+
targets.push(socket);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const uniqueSockets = Array.from(new Set(targets));
|
|
354
|
+
this.socketSend(uniqueSockets, payload);
|
|
547
355
|
}
|
|
548
356
|
}
|
|
549
357
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Safely serializes and sends payload data across network sockets without direct library coupling.
|
|
360
|
+
*
|
|
361
|
+
* @param {Object|Object[]} sockets - Target socket reference(s)
|
|
362
|
+
* @param {Object} data
|
|
363
|
+
*/
|
|
364
|
+
socketSend(sockets, data) {
|
|
365
|
+
const targetSockets = Array.isArray(sockets) ? sockets : [sockets];
|
|
366
|
+
const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
|
|
554
367
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
368
|
+
for (const socket of targetSockets) {
|
|
369
|
+
try {
|
|
370
|
+
// 1 corresponds to OPEN state in WebSocket standard protocol implementations
|
|
371
|
+
if (socket && socket.readyState === 1) {
|
|
372
|
+
socket.send(dataStr);
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
console.error(`[@cocreate/server-mesh] Error executing socketSend transaction:`, err.message);
|
|
376
|
+
}
|
|
559
377
|
}
|
|
560
378
|
}
|
|
561
|
-
activeConnections.clear();
|
|
562
|
-
|
|
563
|
-
if (meshWss) {
|
|
564
|
-
try { meshWss.close(); } catch {}
|
|
565
|
-
}
|
|
566
379
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Safely removes a closed socket connection from tenant routing maps.
|
|
382
|
+
*
|
|
383
|
+
* @param {Object} ws
|
|
384
|
+
*/
|
|
385
|
+
removeSocketFromAllOrgServers(ws) {
|
|
386
|
+
for (const [orgId, sockets] of this.orgServers.entries()) {
|
|
387
|
+
if (sockets.has(ws)) {
|
|
388
|
+
sockets.delete(ws);
|
|
389
|
+
if (sockets.size === 0) {
|
|
390
|
+
this.orgServers.delete(orgId);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
575
393
|
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
394
|
+
}
|
|
578
395
|
|
|
579
|
-
/**
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
// Register profile once on boot
|
|
586
|
-
await registerServer();
|
|
587
|
-
|
|
588
|
-
const readPayload = {
|
|
589
|
-
method: 'object.read',
|
|
590
|
-
array: 'servers',
|
|
591
|
-
$filter: {
|
|
592
|
-
query: { _id: { $lt: serverCtx.id } },
|
|
593
|
-
sort: { _id: 1 },
|
|
594
|
-
limit: 0
|
|
595
|
-
},
|
|
596
|
-
organization_id: serverCtx.organization_id,
|
|
597
|
-
host: serverCtx.host
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
let readResult;
|
|
601
|
-
try {
|
|
602
|
-
readResult = await serverCtx.crud.send(readPayload);
|
|
603
|
-
} catch (err) {
|
|
604
|
-
console.error("[@cocreate/server-mesh] Failed to fetch active peer server records:", err.message);
|
|
605
|
-
return;
|
|
396
|
+
/**
|
|
397
|
+
* Internal server event handler callback.
|
|
398
|
+
*/
|
|
399
|
+
handleServerEvent(payload) {
|
|
400
|
+
this.send(payload);
|
|
606
401
|
}
|
|
607
402
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if (
|
|
615
|
-
|
|
616
|
-
console.log(`[@cocreate/server-mesh] Initiating secure connection stream to target: ${targetIp}`);
|
|
617
|
-
|
|
618
|
-
let retryCount = 0;
|
|
619
|
-
const maxRetries = 10;
|
|
620
|
-
|
|
621
|
-
const connectToPeer = () => {
|
|
622
|
-
if (isGracefulExitRunning) return;
|
|
623
|
-
|
|
624
|
-
const targetUrl = `wss://${targetIp}:${MESH_PORT}/${serverCtx.organization_id}`;
|
|
625
|
-
const clientSocket = new WebSocket(targetUrl, [
|
|
626
|
-
encodeURIComponent(JSON.stringify({
|
|
627
|
-
type: 'peer',
|
|
628
|
-
ip: serverCtx.ip,
|
|
629
|
-
id: serverCtx.id,
|
|
630
|
-
token: process.env.CLUSTER_SECRET,
|
|
631
|
-
sniHost: serverCtx.host
|
|
632
|
-
}))
|
|
633
|
-
], {
|
|
634
|
-
servername: serverCtx.host,
|
|
635
|
-
rejectUnauthorized: false
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
clientSocket.isAlive = true;
|
|
639
|
-
clientSocket.on('pong', () => { clientSocket.isAlive = true; });
|
|
640
|
-
clientSocket.on('ping', () => { clientSocket.pong(); });
|
|
641
|
-
|
|
642
|
-
clientSocket.on('open', () => {
|
|
643
|
-
console.log(`[@cocreate/server-mesh] Secure link complete to peer: ${targetIp}`);
|
|
644
|
-
retryCount = 0;
|
|
645
|
-
|
|
646
|
-
if (retryTimers.has(targetIp)) clearTimeout(retryTimers.get(targetIp));
|
|
647
|
-
if (pruneTimers.has(targetIp)) clearTimeout(pruneTimers.get(targetIp));
|
|
648
|
-
|
|
649
|
-
retryTimers.delete(targetIp);
|
|
650
|
-
pruneTimers.delete(targetIp);
|
|
651
|
-
|
|
652
|
-
clientSocket.isPeer = true;
|
|
653
|
-
clientSocket.isPlatformTunnel = true;
|
|
654
|
-
|
|
655
|
-
activeConnections.set(targetPeer._id, clientSocket);
|
|
656
|
-
activeConnections.set(targetIp, clientSocket);
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
clientSocket.on('close', () => {
|
|
660
|
-
activeConnections.delete(targetPeer._id);
|
|
661
|
-
activeConnections.delete(targetIp);
|
|
662
|
-
retryTimers.delete(targetIp);
|
|
663
|
-
removeSocketFromAllOrgServers(clientSocket);
|
|
664
|
-
|
|
665
|
-
if (isGracefulExitRunning) return;
|
|
666
|
-
|
|
667
|
-
if (retryCount < maxRetries) {
|
|
668
|
-
retryCount++;
|
|
669
|
-
const delay = 5000 + Math.floor(Math.random() * 2000);
|
|
670
|
-
console.warn(`[@cocreate/server-mesh] Peer disconnected. Retrying in ${delay}ms...`);
|
|
671
|
-
const timerId = setTimeout(connectToPeer, delay);
|
|
672
|
-
retryTimers.set(targetIp, timerId);
|
|
673
|
-
} else {
|
|
674
|
-
const staggerDelay = peers.length * 1000;
|
|
675
|
-
console.warn(`[@cocreate/server-mesh] Peer dead. Scheduling db prune in ${staggerDelay}ms...`);
|
|
676
|
-
|
|
677
|
-
const pruneTimerId = setTimeout(async () => {
|
|
678
|
-
console.error(`[@cocreate/server-mesh] Pruning unresponsive node: ${targetIp}`);
|
|
679
|
-
|
|
680
|
-
try {
|
|
681
|
-
await serverCtx.crud.send({
|
|
682
|
-
method: 'object.delete',
|
|
683
|
-
array: 'servers',
|
|
684
|
-
object: { _id: targetPeer._id },
|
|
685
|
-
organization_id: serverCtx.organization_id,
|
|
686
|
-
host: serverCtx.host
|
|
687
|
-
});
|
|
688
|
-
} catch (err) {
|
|
689
|
-
console.error(`[@cocreate/server-mesh] Failed to prune node ${targetIp} from database:`, err.message);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Symmetrical Consensus Broadcast: Emits with standard uuid tracking
|
|
693
|
-
serverMeshInstance.send({
|
|
694
|
-
method: 'mesh.peer_pruned',
|
|
695
|
-
deadIp: targetIp,
|
|
696
|
-
isForwarded: true,
|
|
697
|
-
senderId: serverCtx.id
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
pruneTimers.delete(targetIp);
|
|
701
|
-
}, staggerDelay);
|
|
702
|
-
|
|
703
|
-
pruneTimers.set(targetIp, pruneTimerId);
|
|
704
|
-
}
|
|
705
|
-
});
|
|
403
|
+
/**
|
|
404
|
+
* Initiates coordinated mesh shutdown, closing server ports and outbound timers.
|
|
405
|
+
*
|
|
406
|
+
* @returns {Promise<void>}
|
|
407
|
+
*/
|
|
408
|
+
async close() {
|
|
409
|
+
if (this.isClosed) return;
|
|
410
|
+
this.isClosed = true;
|
|
706
411
|
|
|
707
|
-
|
|
708
|
-
clientSocket.on('message', (rawData) => {
|
|
709
|
-
try {
|
|
710
|
-
const message = JSON.parse(rawData.toString());
|
|
711
|
-
if (message && message.method) {
|
|
712
|
-
serverMeshInstance.emit(message.method, message, clientSocket);
|
|
713
|
-
}
|
|
714
|
-
} catch (err) {
|
|
715
|
-
// Safely catch parsing failures
|
|
716
|
-
}
|
|
717
|
-
});
|
|
412
|
+
console.log(`[@cocreate/server-mesh] Coordinated secure mesh shutdown triggered.`);
|
|
718
413
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
414
|
+
if (this.outboundManager) {
|
|
415
|
+
this.outboundManager.close();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const uniqueSockets = new Set(this.activeConnections.values());
|
|
419
|
+
for (const ws of uniqueSockets) {
|
|
420
|
+
if (ws && ws.readyState === 1) {
|
|
421
|
+
try { ws.close(); } catch {}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
this.activeConnections.clear();
|
|
723
425
|
|
|
724
|
-
|
|
426
|
+
if (this.inboundManager) {
|
|
427
|
+
await this.inboundManager.close();
|
|
428
|
+
}
|
|
725
429
|
}
|
|
726
430
|
}
|
|
727
431
|
|
|
728
|
-
//
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
// Default export matches standard functional entry contract
|
|
732
|
-
export default serverMeshInstance;
|
|
432
|
+
// Export singleton instance as default export and named export
|
|
433
|
+
export const serverMesh = new ServerMesh();
|
|
434
|
+
export default serverMesh;
|