@cocreate/server 1.6.1 → 1.6.3

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/src/ipc.js ADDED
@@ -0,0 +1,157 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2023 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
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.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ *
17
+ ********************************************************************************/
18
+
19
+ import cluster from 'node:cluster';
20
+
21
+ /**
22
+ * Initializes Inter-Process Communication (IPC) transport routing between
23
+ * the Master orchestrator and clustered Worker processes.
24
+ *
25
+ * @param {Object} Server - The CoCreateServer instance reference
26
+ * @param {Map} pendingPromises - Tracking map for async worker RPC promises
27
+ * @returns {Function} Context-aware send function for dispatching cluster messages
28
+ */
29
+ export function init(Server, pendingPromises) {
30
+ if (Server.isMaster) {
31
+ cluster.on('message', async (worker, message) => {
32
+ if (!message || typeof message !== 'object') return;
33
+
34
+ // 1. Resolve pending RPC promise if messageId matches
35
+ if (message.messageId && pendingPromises.has(message.messageId)) {
36
+ const promiseHandler = pendingPromises.get(message.messageId);
37
+ pendingPromises.delete(message.messageId);
38
+ if (message.success) {
39
+ promiseHandler.resolve(message.result);
40
+ } else {
41
+ promiseHandler.reject(new Error(message.error || 'IPC RPC Request Failed'));
42
+ }
43
+ return;
44
+ }
45
+
46
+ // 2. Emit event locally on Master for subscribers (e.g. log, state, storage)
47
+ if (message.method) {
48
+ Server.emit(message.method, message);
49
+ }
50
+
51
+ // 3. Relay broadcast or targeted messages to other workers
52
+ if (message.broadcast || message.worker) {
53
+ send(message);
54
+ }
55
+ });
56
+
57
+ function send(payload = {}) {
58
+ if (!payload || typeof payload !== 'object') return;
59
+ payload.id = Server.id;
60
+
61
+ const targetWorkers = [];
62
+
63
+ if (payload.worker) {
64
+ if (Array.isArray(payload.worker)) {
65
+ for (const wId of payload.worker) {
66
+ const w = Server.workers.get(wId);
67
+ if (w) targetWorkers.push(w);
68
+ }
69
+ } else {
70
+ const w = Server.workers.get(payload.worker);
71
+ if (w) targetWorkers.push(w);
72
+ }
73
+ } else {
74
+ // Broadcast to all workers by default if no specific worker specified
75
+ for (const w of Server.workers.values()) {
76
+ targetWorkers.push(w);
77
+ }
78
+ }
79
+
80
+ // Check edge listener fallback conditions for cordon/drain commands
81
+ if (payload.method === 'cordon' || payload.method === 'drain') {
82
+ const isEnabling = typeof payload.value === 'boolean' ? payload.value : (payload.value !== false);
83
+ if (isEnabling) {
84
+ const targetedIds = new Set(targetWorkers.map(w => w.id));
85
+ let activeRemaining = 0;
86
+
87
+ for (const w of Server.workers.values()) {
88
+ if (!targetedIds.has(w.id) && w.isListening && !w.isCordoned && !w.isDraining) {
89
+ activeRemaining++;
90
+ }
91
+ }
92
+
93
+ if (activeRemaining === 0) {
94
+ Server.evaluateMasterFallback();
95
+ }
96
+ }
97
+ }
98
+
99
+ for (const workerObj of targetWorkers) {
100
+ const workerProcess = workerObj.process || workerObj;
101
+ if (workerProcess && typeof workerProcess.send === 'function' && workerProcess.isConnected()) {
102
+ try {
103
+ workerProcess.send(payload);
104
+ } catch (err) {
105
+ console.error(`[@cocreate/server] [MASTER] Failed to send IPC payload to worker ${workerObj.id}:`, err.message);
106
+ }
107
+ }
108
+ }
109
+
110
+ // Fire event locally on Master process unless explicitly turned off via master: false
111
+ if (payload.master !== false && payload.method) {
112
+ Server.emit(payload.method, payload);
113
+ }
114
+ }
115
+
116
+ return send;
117
+ } else {
118
+ process.on('message', async (message) => {
119
+ if (!message || typeof message !== 'object') return;
120
+
121
+ if (message.messageId && pendingPromises.has(message.messageId)) {
122
+ const promiseHandler = pendingPromises.get(message.messageId);
123
+ pendingPromises.delete(message.messageId);
124
+
125
+ if (message.success) {
126
+ promiseHandler.resolve(message.result);
127
+ } else {
128
+ promiseHandler.reject(new Error(message.error || 'IPC RPC Call Failed'));
129
+ }
130
+ return;
131
+ }
132
+
133
+ if (message.method) {
134
+ Server.emit(message.method, message);
135
+ }
136
+ });
137
+
138
+ return function send(payload = {}) {
139
+ if (!payload || typeof payload !== 'object') return;
140
+ payload.id = Server.id;
141
+
142
+ if (typeof process.send === 'function' && process.connected) {
143
+ try {
144
+ process.send(payload);
145
+ } catch (err) {
146
+ console.error(`[@cocreate/server] [WORKER ${process.pid}] Failed to dispatch IPC message to master:`, err.message);
147
+ }
148
+ } else {
149
+ if (payload.method) {
150
+ Server.emit(payload.method, payload);
151
+ }
152
+ }
153
+ };
154
+ }
155
+ }
156
+
157
+ export default { init };
package/src/log.js ADDED
@@ -0,0 +1,207 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2023 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
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.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ *
17
+ ********************************************************************************/
18
+
19
+ import util from 'node:util';
20
+
21
+ /**
22
+ * LogManager captures console logs and manual server log dispatches across
23
+ * cluster workers, aggregates them on the master orchestrator, and periodically
24
+ * flushes a capped ring buffer of logs to the server object in the database.
25
+ */
26
+ export class LogManager {
27
+ constructor(Server) {
28
+ this.server = Server;
29
+ this.logQueue = [];
30
+ this.recentLogs = [];
31
+ this.maxLogs = 100; // Capped ring buffer size on server document
32
+ this.flushIntervalMs = 1500; // Time-window flush in milliseconds
33
+ this.maxBatchSize = 20; // Trigger immediate flush if queue exceeds size
34
+ this.isLoggingInternal = false; // Recursion guard against infinite loops
35
+ this.flushTimer = null;
36
+
37
+ // Preserve native console methods for uninhibited terminal stdout/stderr
38
+ this.originalConsole = {
39
+ log: console.log.bind(console),
40
+ info: console.info.bind(console),
41
+ warn: console.warn.bind(console),
42
+ error: console.error.bind(console)
43
+ };
44
+
45
+ this.bindMethods();
46
+ this.setupIPCListeners();
47
+ this.patchConsole();
48
+ this.setupFlushTimer();
49
+ }
50
+
51
+ bindMethods() {
52
+ this.server.log = this.log.bind(this);
53
+ this.server.flushLogs = this.flushLogs.bind(this);
54
+ this.server.handleIncomingWorkerLog = this.handleIncomingWorkerLog.bind(this);
55
+ }
56
+
57
+ setupIPCListeners() {
58
+ // Listen for 'server.log' events on master process dispatched across IPC
59
+ if (this.server.isMaster) {
60
+ this.server.on('server.log', (message) => {
61
+ if (message && message.log) {
62
+ this.handleIncomingWorkerLog(message.log);
63
+ }
64
+ });
65
+ }
66
+ }
67
+
68
+ patchConsole() {
69
+ const self = this;
70
+
71
+ const createConsoleProxy = (level, originalFn) => {
72
+ return function (...args) {
73
+ // Always execute original console method to print to standard stdout/stderr
74
+ originalFn.apply(console, args);
75
+
76
+ // Skip log capture if internal logging recursion flag is active
77
+ if (self.isLoggingInternal) return;
78
+
79
+ try {
80
+ const message = args.map(arg => {
81
+ if (typeof arg === 'string') return arg;
82
+ if (arg instanceof Error) return arg.stack || arg.message;
83
+ return util.inspect(arg, { depth: 3, colors: false });
84
+ }).join(' ');
85
+
86
+ self.log(level, message);
87
+ } catch (err) {
88
+ // Fail gracefully to ensure native console execution is never disrupted
89
+ }
90
+ };
91
+ };
92
+
93
+ console.log = createConsoleProxy('info', this.originalConsole.log);
94
+ console.info = createConsoleProxy('info', this.originalConsole.info);
95
+ console.warn = createConsoleProxy('warn', this.originalConsole.warn);
96
+ console.error = createConsoleProxy('error', this.originalConsole.error);
97
+ }
98
+
99
+ log(level = 'info', message = '', metadata = null) {
100
+ if (this.isLoggingInternal) return;
101
+
102
+ const sourceWorker = this.server.isMaster ? 'master' : (this.server.workerId || 1);
103
+
104
+ const logEntry = {
105
+ timestamp: Date.now(),
106
+ workerId: sourceWorker,
107
+ level,
108
+ message,
109
+ ...(metadata && typeof metadata === 'object' ? { metadata } : {})
110
+ };
111
+
112
+ if (!this.server.isMaster) {
113
+ // Worker Process: Relay log entry to Master Orchestrator over IPC
114
+ if (typeof this.server.send === 'function') {
115
+ this.server.send({
116
+ method: 'server.log',
117
+ log: logEntry
118
+ });
119
+ } else if (process.send && process.connected) {
120
+ try {
121
+ process.send({
122
+ method: 'server.log',
123
+ log: logEntry
124
+ });
125
+ } catch (err) {
126
+ // Fallback to original console error if IPC fails
127
+ this.originalConsole.error('[@cocreate/server] [WORKER LOG IPC ERROR]', err.message);
128
+ }
129
+ }
130
+ } else {
131
+ // Master Process: Append directly to local log queue
132
+ this.enqueueLog(logEntry);
133
+ }
134
+ }
135
+
136
+ handleIncomingWorkerLog(logEntry) {
137
+ if (!this.server.isMaster || !logEntry) return;
138
+ this.enqueueLog(logEntry);
139
+ }
140
+
141
+ enqueueLog(logEntry) {
142
+ this.logQueue.push(logEntry);
143
+
144
+ // Immediately flush if queue hits batch limit or if an error level log is emitted
145
+ if (this.logQueue.length >= this.maxBatchSize || logEntry.level === 'error') {
146
+ this.flushLogs();
147
+ }
148
+ }
149
+
150
+ setupFlushTimer() {
151
+ if (!this.server.isMaster) return;
152
+
153
+ this.flushTimer = setInterval(() => {
154
+ this.flushLogs();
155
+ }, this.flushIntervalMs);
156
+
157
+ // Ensure flush timer doesn't keep node process alive during shutdown
158
+ if (this.flushTimer.unref) {
159
+ this.flushTimer.unref();
160
+ }
161
+ }
162
+
163
+ async flushLogs() {
164
+ if (!this.server.isMaster || this.logQueue.length === 0 || this.isLoggingInternal) return;
165
+
166
+ this.isLoggingInternal = true;
167
+
168
+ try {
169
+ const pendingLogs = [...this.logQueue];
170
+ this.logQueue = [];
171
+
172
+ // Maintain capped ring buffer of recent logs on Master
173
+ this.recentLogs = [...this.recentLogs, ...pendingLogs].slice(-this.maxLogs);
174
+
175
+ // Persist updated log ring buffer to the server object in DB
176
+ if (this.server.crud && typeof this.server.getServerObject === 'function') {
177
+ const payload = this.server.getServerObject({
178
+ logs: this.recentLogs
179
+ });
180
+
181
+ await this.server.crud.send(payload);
182
+ }
183
+ } catch (err) {
184
+ this.originalConsole.error('[@cocreate/server] [LOG FLUSH ERROR]', err.message);
185
+ } finally {
186
+ this.isLoggingInternal = false;
187
+ }
188
+ }
189
+
190
+ destroy() {
191
+ if (this.flushTimer) {
192
+ clearInterval(this.flushTimer);
193
+ this.flushTimer = null;
194
+ }
195
+
196
+ console.log = this.originalConsole.log;
197
+ console.info = this.originalConsole.info;
198
+ console.warn = this.originalConsole.warn;
199
+ console.error = this.originalConsole.error;
200
+ }
201
+ }
202
+
203
+ export default {
204
+ init(Server) {
205
+ return new LogManager(Server);
206
+ }
207
+ };