@cocreate/server 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/src/index.js +194 -23
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
# [1.6.0](https://github.com/CoCreate-app/CoCreate-server/compare/v1.5.1...v1.6.0) (2026-07-18)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add ingress state management with cordon, shedding, and draining events ([b52a198](https://github.com/CoCreate-app/CoCreate-server/commit/b52a1986c566fe9a73c8e694542be69d808ae3eb))
|
|
7
|
+
* implement graceful shutdown process with selective shedding and draining for improved resource management ([5c367e0](https://github.com/CoCreate-app/CoCreate-server/commit/5c367e0284f587965bd5b4248b1f1f97a4b51b2f))
|
|
8
|
+
|
|
1
9
|
## [1.5.1](https://github.com/CoCreate-app/CoCreate-server/compare/v1.5.0...v1.5.1) (2026-07-17)
|
|
2
10
|
|
|
3
11
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocreate/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "A dynamic SSL certificate management and generation tool for proxies like NGINX, with a fallback to Node.js SSL termination. It seamlessly integrates HTTP, HTTPS, and ACME protocols to ensure secure, encrypted connections.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ssl-certificate-management",
|
package/src/index.js
CHANGED
|
@@ -50,6 +50,11 @@ const sharedMap = new Map();
|
|
|
50
50
|
const pendingPromises = new Map();
|
|
51
51
|
const activeWorkers = new Map();
|
|
52
52
|
|
|
53
|
+
// Track overall system state during shutdowns to prevent concurrent signal re-evaluation
|
|
54
|
+
let isPrimaryShuttingDown = false;
|
|
55
|
+
let isMeshClosed = false; // Dual-lock flag to guarantee DB prunes finish before process exits
|
|
56
|
+
let primaryShutdownTimeout = null;
|
|
57
|
+
|
|
53
58
|
export class CoCreateServer extends EventEmitter {
|
|
54
59
|
constructor() {
|
|
55
60
|
super();
|
|
@@ -71,15 +76,76 @@ export class CoCreateServer extends EventEmitter {
|
|
|
71
76
|
this.ip = null;
|
|
72
77
|
this.infrastructure = null;
|
|
73
78
|
|
|
74
|
-
//
|
|
79
|
+
// Ingress state management flags
|
|
80
|
+
this.isCordon = false;
|
|
81
|
+
this.isShedding = false;
|
|
82
|
+
this.isDraining = false;
|
|
83
|
+
|
|
84
|
+
this.on('cordon', (payload) => {
|
|
85
|
+
const active = payload?.active !== false;
|
|
86
|
+
|
|
87
|
+
// Core Logic: Telemetry or callers cannot uncordon the node if shedding or draining is active
|
|
88
|
+
if (!active && (this.isShedding || this.isDraining)) {
|
|
89
|
+
console.warn(`[@cocreate/server] Blocked un-cordon request. Overriding active states: isShedding=${this.isShedding}, isDraining=${this.isDraining}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.isCordon = active;
|
|
94
|
+
const processLabel = this.isPrimary ? `Primary (${process.pid})` : `Worker ${process.pid} (ID: ${this.workerId})`;
|
|
95
|
+
console.log(`[@cocreate/server] ${processLabel} isCordon updated to: ${this.isCordon}`);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
this.on('shed', (payload) => {
|
|
99
|
+
this.isShedding = payload?.active !== false;
|
|
100
|
+
|
|
101
|
+
// Shedding state activation automatically triggers a network cordon
|
|
102
|
+
if (this.isShedding) {
|
|
103
|
+
this.isCordon = true;
|
|
104
|
+
|
|
105
|
+
// Selective Shedding Integration: Target specified organizations inside the socket server
|
|
106
|
+
if (payload?.organizations && this.wsManager && typeof this.wsManager.destroyOrganizations === 'function') {
|
|
107
|
+
const orgList = Array.isArray(payload.organizations) ? payload.organizations : [payload.organizations];
|
|
108
|
+
console.log(`[@cocreate/server] Shedding event triggered. Selective eviction of org IDs:`, orgList);
|
|
109
|
+
this.wsManager.destroyOrganizations(orgList);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const processLabel = this.isPrimary ? `Primary (${process.pid})` : `Worker ${process.pid} (ID: ${this.workerId})`;
|
|
113
|
+
console.log(`[@cocreate/server] ${processLabel} isShedding updated to: ${this.isShedding} (isCordon: ${this.isCordon})`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
this.on('drain', (payload) => {
|
|
117
|
+
this.isDraining = payload?.active !== false;
|
|
118
|
+
|
|
119
|
+
// Draining state activation automatically triggers a network cordon
|
|
120
|
+
if (this.isDraining) {
|
|
121
|
+
this.isCordon = true;
|
|
122
|
+
|
|
123
|
+
// Complete Worker Drain Integration: Instantly purge all sockets and loaded maps
|
|
124
|
+
if (this.wsManager && typeof this.wsManager.destroyAll === 'function') {
|
|
125
|
+
console.log(`[@cocreate/server] Worker-level drain event triggered. Purging all tenant configurations.`);
|
|
126
|
+
this.wsManager.destroyAll();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const processLabel = this.isPrimary ? `Primary (${process.pid})` : `Worker ${process.pid} (ID: ${this.workerId})`;
|
|
130
|
+
console.log(`[@cocreate/server] ${processLabel} isDraining updated to: ${this.isDraining} (isCordon: ${this.isCordon})`);
|
|
131
|
+
});
|
|
132
|
+
|
|
75
133
|
this.http = Http.createServer((req, res) => {
|
|
76
134
|
const requestUrl = req.url || '/';
|
|
77
135
|
// 1. Allow ACME challenges to pass through over unencrypted HTTP
|
|
78
136
|
if (requestUrl.startsWith('/.well-known/acme-challenge/')) {
|
|
79
137
|
return this.handleEdgeRequest(req, res);
|
|
80
138
|
}
|
|
81
|
-
|
|
139
|
+
|
|
82
140
|
const host = req.headers.host ? req.headers.host.split(':')[0] : 'localhost';
|
|
141
|
+
|
|
142
|
+
// 2. Intercept with a 307 Temporary Redirect if cordon is active
|
|
143
|
+
if (this.isCordon) {
|
|
144
|
+
res.writeHead(307, { "Location": `https://${host}${requestUrl}` });
|
|
145
|
+
return res.end();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 3. Force a 301 redirect to HTTPS for all other traffic
|
|
83
149
|
res.writeHead(301, { "Location": `https://${host}${requestUrl}` });
|
|
84
150
|
res.end();
|
|
85
151
|
});
|
|
@@ -110,7 +176,6 @@ export class CoCreateServer extends EventEmitter {
|
|
|
110
176
|
}
|
|
111
177
|
|
|
112
178
|
// 2. Compile target worker list dynamically using a flat ternary condition.
|
|
113
|
-
// This eliminates duplicate loops, nested blocks, and repeated try/catch statements.
|
|
114
179
|
const targetIds = payload.broadcast === true
|
|
115
180
|
? Array.from(this.workers.keys())
|
|
116
181
|
: (Array.isArray(payload.workers) ? payload.workers : []);
|
|
@@ -118,7 +183,7 @@ export class CoCreateServer extends EventEmitter {
|
|
|
118
183
|
// 3. Dispatch to all compiled destination targets in a single unified loop.
|
|
119
184
|
for (const wId of targetIds) {
|
|
120
185
|
const worker = this.workers.get(wId);
|
|
121
|
-
if (worker?.process && typeof worker.process.send === 'function') {
|
|
186
|
+
if (worker?.process && typeof worker.process.send === 'function' && worker.process.isConnected()) {
|
|
122
187
|
try {
|
|
123
188
|
worker.process.send({ ipcPayload: true, payload });
|
|
124
189
|
} catch (err) {
|
|
@@ -172,22 +237,17 @@ export class CoCreateServer extends EventEmitter {
|
|
|
172
237
|
if (configuredWorkers !== false && configuredWorkers !== 'false') {
|
|
173
238
|
const parsedWorkers = parseInt(configuredWorkers, 10);
|
|
174
239
|
if (parsedWorkers === -1) {
|
|
175
|
-
// Use total CPU cores minus 1 (minimum of 1) to reserve one core for the master process
|
|
176
240
|
this.totalWorkers = Math.max(1, os.cpus().length - 1);
|
|
177
241
|
} else {
|
|
178
242
|
this.totalWorkers = parsedWorkers || os.cpus().length;
|
|
179
243
|
}
|
|
180
244
|
}
|
|
181
245
|
|
|
182
|
-
// Explicitly set workerId = 1 on the single Primary process if workers configuration specifies no clustering (<= 1)
|
|
183
246
|
if (this.isPrimary && this.totalWorkers <= 1) {
|
|
184
247
|
this.workerId = 1;
|
|
185
248
|
}
|
|
186
249
|
|
|
187
|
-
// DB interface is booted first so that ServerMesh can cleanly read nodes and write startup heartbeats
|
|
188
250
|
this.crud = await CrudServer.init(this);
|
|
189
|
-
|
|
190
|
-
// Assign the ServerMesh globally. This guarantees both Workers and Primary can safely call "this.mesh.send"
|
|
191
251
|
this.mesh = ServerMesh;
|
|
192
252
|
this.autoscaler = ServerAutoscaler;
|
|
193
253
|
|
|
@@ -199,6 +259,9 @@ export class CoCreateServer extends EventEmitter {
|
|
|
199
259
|
await this.mesh.init(this);
|
|
200
260
|
await this.autoscaler.init(this);
|
|
201
261
|
|
|
262
|
+
// Setup Primary process Graceful Signal Listeners
|
|
263
|
+
setupPrimaryShutdownHooks(this);
|
|
264
|
+
|
|
202
265
|
if (this.totalWorkers > 1) {
|
|
203
266
|
console.log(`[@cocreate/server] [PRIMARY] Orchestrator running with PID: ${process.pid}`);
|
|
204
267
|
for (let i = 0; i < this.totalWorkers; i++) {
|
|
@@ -209,6 +272,12 @@ export class CoCreateServer extends EventEmitter {
|
|
|
209
272
|
SERVER_IP: this.ip,
|
|
210
273
|
SERVER_INFRASTRUCTURE: JSON.stringify(this.infrastructure)
|
|
211
274
|
});
|
|
275
|
+
|
|
276
|
+
// Protect against unhandled 'error' events on the cluster worker wrapper
|
|
277
|
+
worker.on('error', (err) => {
|
|
278
|
+
console.error(`[@cocreate/server] [PRIMARY] Error on Worker ${currentWorkerId}:`, err.message);
|
|
279
|
+
});
|
|
280
|
+
|
|
212
281
|
activeWorkers.set(currentWorkerId, {
|
|
213
282
|
id: currentWorkerId,
|
|
214
283
|
pid: worker.process.pid,
|
|
@@ -219,6 +288,7 @@ export class CoCreateServer extends EventEmitter {
|
|
|
219
288
|
// Supervise and automatically reboot dead or crashed worker processes
|
|
220
289
|
cluster.on('exit', (worker, code, signal) => {
|
|
221
290
|
console.log(`[@cocreate/server] [PRIMARY] Worker ${worker.process.pid} exited (Code: ${code}, Signal: ${signal})`);
|
|
291
|
+
|
|
222
292
|
let deadWorkerId = null;
|
|
223
293
|
for (let [id, w] of activeWorkers.entries()) {
|
|
224
294
|
if (w.pid === worker.process.pid) {
|
|
@@ -227,6 +297,22 @@ export class CoCreateServer extends EventEmitter {
|
|
|
227
297
|
break;
|
|
228
298
|
}
|
|
229
299
|
}
|
|
300
|
+
|
|
301
|
+
// SAFETY: If Primary is shutting down or draining, DO NOT fork a replacement worker process!
|
|
302
|
+
if (isPrimaryShuttingDown || this.isDraining) {
|
|
303
|
+
console.log(`[@cocreate/server] [PRIMARY] Worker ${worker.process.pid} removed. Draining active workers: ${activeWorkers.size} remaining.`);
|
|
304
|
+
|
|
305
|
+
// Dual-Lock Guard: Only exit Master if workers are dead AND mesh has completed the MongoDB prune
|
|
306
|
+
if (activeWorkers.size === 0 && isMeshClosed) {
|
|
307
|
+
console.log("[@cocreate/server] [PRIMARY] All workers successfully drained and Mesh closed. Terminating master process cleanly.");
|
|
308
|
+
if (primaryShutdownTimeout) {
|
|
309
|
+
clearTimeout(primaryShutdownTimeout);
|
|
310
|
+
}
|
|
311
|
+
process.exit(0);
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
230
316
|
const targetId = deadWorkerId || (activeWorkers.size + 1);
|
|
231
317
|
|
|
232
318
|
const newWorker = cluster.fork({
|
|
@@ -235,6 +321,11 @@ export class CoCreateServer extends EventEmitter {
|
|
|
235
321
|
SERVER_IP: this.ip,
|
|
236
322
|
SERVER_INFRASTRUCTURE: JSON.stringify(this.infrastructure)
|
|
237
323
|
});
|
|
324
|
+
|
|
325
|
+
newWorker.on('error', (err) => {
|
|
326
|
+
console.error(`[@cocreate/server] [PRIMARY] Error on replacement Worker ${targetId}:`, err.message);
|
|
327
|
+
});
|
|
328
|
+
|
|
238
329
|
activeWorkers.set(targetId, {
|
|
239
330
|
id: targetId,
|
|
240
331
|
pid: newWorker.process.pid,
|
|
@@ -242,7 +333,6 @@ export class CoCreateServer extends EventEmitter {
|
|
|
242
333
|
});
|
|
243
334
|
});
|
|
244
335
|
|
|
245
|
-
// In clustered mode, only the Primary exits early. The workers handle the core traffic.
|
|
246
336
|
if (this.totalWorkers > 1) {
|
|
247
337
|
return this;
|
|
248
338
|
}
|
|
@@ -260,23 +350,19 @@ export class CoCreateServer extends EventEmitter {
|
|
|
260
350
|
this.infrastructure = await resolveInfrastructure(this.id);
|
|
261
351
|
}
|
|
262
352
|
|
|
263
|
-
// Initialize HTTPS & Ephemeral certificate via ACME
|
|
264
353
|
this.certificate = Acme;
|
|
265
354
|
this.https = Https.createServer({
|
|
266
355
|
SNICallback: (domain, cb) => this.certificate.sniCallback(domain, cb)
|
|
267
356
|
}, (req, res) => this.handleEdgeRequest(req, res));
|
|
268
357
|
await this.certificate.init(this);
|
|
269
358
|
|
|
270
|
-
// Standard core services
|
|
271
359
|
this.wsManager = new SocketServer(this);
|
|
272
360
|
this.api = await ApiServer.init(this);
|
|
273
361
|
this.files = FileServer.init(this);
|
|
274
362
|
this.loader = await LazyLoader.init(this);
|
|
275
363
|
|
|
276
|
-
// Initialize performance telemetry
|
|
277
364
|
this.telemetry = await ServerTelemetry.init(this);
|
|
278
365
|
|
|
279
|
-
// Bind directly to standard web ports (Requires setcap or root privileges)
|
|
280
366
|
const HTTP_PORT = process.env.HTTP_PORT || 80;
|
|
281
367
|
const HTTPS_PORT = process.env.HTTPS_PORT || 443;
|
|
282
368
|
const BIND_IP = process.env.BIND_IP || '0.0.0.0';
|
|
@@ -300,6 +386,14 @@ export class CoCreateServer extends EventEmitter {
|
|
|
300
386
|
async handleEdgeRequest(req, res) {
|
|
301
387
|
try {
|
|
302
388
|
const requestUrl = req.url || '/';
|
|
389
|
+
|
|
390
|
+
// Check if cordon is active (exclude critical ACME challenges from loop-breaking)
|
|
391
|
+
if (this.isCordon && !requestUrl.startsWith('/.well-known/acme-challenge/')) {
|
|
392
|
+
const host = req.headers.host ? req.headers.host.split(':')[0] : 'localhost';
|
|
393
|
+
res.writeHead(307, { "Location": `https://${host}${requestUrl}` });
|
|
394
|
+
return res.end();
|
|
395
|
+
}
|
|
396
|
+
|
|
303
397
|
if (requestUrl.startsWith("/webhooks/")) {
|
|
304
398
|
if (this.loader) return this.loader.request(req, res);
|
|
305
399
|
}
|
|
@@ -341,10 +435,82 @@ function workerStorageManager(serverCtx, method, key, value) {
|
|
|
341
435
|
});
|
|
342
436
|
}
|
|
343
437
|
|
|
438
|
+
function setupPrimaryShutdownHooks(serverCtx) {
|
|
439
|
+
const handleSignal = async (signal) => {
|
|
440
|
+
if (isPrimaryShuttingDown) return;
|
|
441
|
+
isPrimaryShuttingDown = true;
|
|
442
|
+
serverCtx.isCordon = true;
|
|
443
|
+
serverCtx.isDraining = true;
|
|
444
|
+
|
|
445
|
+
console.log(`\n[@cocreate/server] [PRIMARY] Intercepted process termination signal: ${signal}. Commencing cluster-wide graceful shutdown...`);
|
|
446
|
+
|
|
447
|
+
// STEP 1: Close Mesh FIRST. Wipes the server record profile from the database while CRUD connection pools are completely active.
|
|
448
|
+
if (serverCtx.mesh && typeof serverCtx.mesh.close === 'function') {
|
|
449
|
+
try {
|
|
450
|
+
console.log("[@cocreate/server] [PRIMARY] Step 1: Initiating Server Mesh close & server record database removal...");
|
|
451
|
+
await serverCtx.mesh.close();
|
|
452
|
+
console.log("[@cocreate/server] [PRIMARY] Success: Server Mesh closed and server record cleanly wiped from database.");
|
|
453
|
+
} catch (err) {
|
|
454
|
+
console.error("[@cocreate/server] [PRIMARY] Failed to close Server Mesh or wipe server profile cleanly:", err);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
isMeshClosed = true;
|
|
458
|
+
|
|
459
|
+
// Dual-Lock Resolution: If all workers finished exiting while the database prune was running,
|
|
460
|
+
// terminate the Master process cleanly right now instead of waiting for the safety timeout.
|
|
461
|
+
if (activeWorkers.size === 0) {
|
|
462
|
+
console.log("[@cocreate/server] [PRIMARY] All workers successfully drained and Mesh closed. Terminating master process cleanly.");
|
|
463
|
+
if (primaryShutdownTimeout) {
|
|
464
|
+
clearTimeout(primaryShutdownTimeout);
|
|
465
|
+
}
|
|
466
|
+
process.exit(0);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// STEP 2: Now that the database profile is clean, tell all worker processes over IPC to begin local graceful drains.
|
|
470
|
+
console.log("[@cocreate/server] [PRIMARY] Step 2: Informing worker processes to start local connection drains...");
|
|
471
|
+
for (const [wId, worker] of activeWorkers.entries()) {
|
|
472
|
+
const workerInstance = worker.process;
|
|
473
|
+
if (workerInstance && typeof workerInstance.send === 'function' && workerInstance.isConnected()) {
|
|
474
|
+
console.log(`[@cocreate/server] [PRIMARY] Sending 'shutdown' instruction to Worker ${wId} (PID: ${worker.pid})`);
|
|
475
|
+
try {
|
|
476
|
+
workerInstance.send({ type: 'shutdown' });
|
|
477
|
+
} catch (err) {
|
|
478
|
+
console.error(`[@cocreate/server] Failed sending shutdown IPC instruction to worker ${wId}:`, err);
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
console.log(`[@cocreate/server] [PRIMARY] Skipping disconnected Worker ${wId} (PID: ${worker.pid})`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// STEP 3: Set Cooperative Grace Fallback Timer (5 Seconds)
|
|
486
|
+
// If workers finish early, the exit event triggers process exit immediately. If they hang, we force-kill.
|
|
487
|
+
primaryShutdownTimeout = setTimeout(() => {
|
|
488
|
+
console.warn("[@cocreate/server] [PRIMARY] Safety grace window expired! Force-terminating remaining processes...");
|
|
489
|
+
|
|
490
|
+
for (const [wId, worker] of activeWorkers.entries()) {
|
|
491
|
+
if (worker?.process) {
|
|
492
|
+
try {
|
|
493
|
+
console.log(`[@cocreate/server] [PRIMARY] Force-killing hanging Worker ${wId} (PID: ${worker.pid}).`);
|
|
494
|
+
worker.process.kill('SIGKILL');
|
|
495
|
+
} catch (err) {
|
|
496
|
+
console.error(`[@cocreate/server] Error force-killing Worker ${wId}:`, err);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
console.log("[@cocreate/server] [PRIMARY] All worker processes forcefully reaped. Terminating Master process.");
|
|
502
|
+
process.exit(0);
|
|
503
|
+
}, 5000);
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
process.on('SIGTERM', () => handleSignal('SIGTERM'));
|
|
507
|
+
process.on('SIGINT', () => handleSignal('SIGINT'));
|
|
508
|
+
process.on('SIGBREAK', () => handleSignal('SIGBREAK'));
|
|
509
|
+
}
|
|
510
|
+
|
|
344
511
|
function setupIPC(serverCtx) {
|
|
345
512
|
if (serverCtx.isPrimary) {
|
|
346
513
|
cluster.on('message', (worker, message) => {
|
|
347
|
-
// 1. Check for standard storage operations
|
|
348
514
|
if (message && message.id === serverCtx.id) {
|
|
349
515
|
try {
|
|
350
516
|
message.value = executeLocalWorkerStorageOperation(message.method, message.key, message.value);
|
|
@@ -354,11 +520,9 @@ function setupIPC(serverCtx) {
|
|
|
354
520
|
worker.send(message);
|
|
355
521
|
}
|
|
356
522
|
}
|
|
357
|
-
// 2. Routing logic for CoCreate Unified IPC Payloads
|
|
358
523
|
else if (message && message.ipcPayload && message.payload) {
|
|
359
524
|
const { payload } = message;
|
|
360
525
|
|
|
361
|
-
// Track sending worker PID to worker mapping
|
|
362
526
|
let senderWorkerId = null;
|
|
363
527
|
for (const [wId, w] of serverCtx.workers.entries()) {
|
|
364
528
|
if (w.pid === worker.process.pid) {
|
|
@@ -367,17 +531,25 @@ function setupIPC(serverCtx) {
|
|
|
367
531
|
}
|
|
368
532
|
}
|
|
369
533
|
|
|
370
|
-
// Expose sender metadata to routing logic
|
|
371
534
|
payload.senderWorkerId = senderWorkerId;
|
|
372
|
-
|
|
373
|
-
// Pass it off to the primary orchestrator's central send method
|
|
374
535
|
serverCtx.send(payload);
|
|
375
536
|
}
|
|
376
537
|
});
|
|
377
538
|
} else {
|
|
378
539
|
process.on('message', (message) => {
|
|
379
|
-
//
|
|
380
|
-
if (message && message.
|
|
540
|
+
// Intercept Master-directed shutdown instruction over the worker's IPC channel
|
|
541
|
+
if (message && message.type === 'shutdown') {
|
|
542
|
+
console.log(`[@cocreate/server] [Worker ${process.pid}] Received master coordinated shutdown signal. Commencing local drain...`);
|
|
543
|
+
|
|
544
|
+
// Immediately cordon worker traffic and trigger the internal SocketServer destroyAll logic
|
|
545
|
+
serverCtx.isCordon = true;
|
|
546
|
+
serverCtx.isDraining = true;
|
|
547
|
+
|
|
548
|
+
if (serverCtx.wsManager && typeof serverCtx.wsManager.destroyAll === 'function') {
|
|
549
|
+
serverCtx.wsManager.destroyAll();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
else if (message && message.id === serverCtx.id) {
|
|
381
553
|
const pending = pendingPromises.get(message.messageId);
|
|
382
554
|
if (pending) {
|
|
383
555
|
pendingPromises.delete(message.messageId);
|
|
@@ -388,7 +560,6 @@ function setupIPC(serverCtx) {
|
|
|
388
560
|
}
|
|
389
561
|
}
|
|
390
562
|
}
|
|
391
|
-
// 2. Incoming Unified IPC payload from Master -> Worker Process
|
|
392
563
|
else if (message && message.ipcPayload && message.payload) {
|
|
393
564
|
serverCtx.emit(message.payload.method, message.payload);
|
|
394
565
|
}
|