@rosepetal/node-red-contrib-async-function 1.0.2 → 1.0.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/README.md +8 -6
- package/nodes/async-function.html +9 -7
- package/nodes/async-function.js +320 -27
- package/nodes/lib/child-process-pool.js +33 -6
- package/nodes/lib/child-process-script.js +179 -5
- package/nodes/lib/message-serializer.js +171 -16
- package/nodes/lib/module-installer.js +48 -25
- package/nodes/lib/shared-memory-manager.js +50 -12
- package/nodes/lib/worker-pool.js +47 -10
- package/nodes/lib/worker-script.js +206 -10
- package/package.json +1 -1
|
@@ -16,6 +16,9 @@ const { AsyncMessageSerializer } = require('./message-serializer');
|
|
|
16
16
|
// Track worker state
|
|
17
17
|
let isTerminating = false;
|
|
18
18
|
let isInitialized = false;
|
|
19
|
+
let transferMode = 'shared';
|
|
20
|
+
const MSG_WRAPPER_KEY = '__rosepetal_msg';
|
|
21
|
+
const CONTEXT_WRAPPER_KEY = '__rosepetal_context';
|
|
19
22
|
|
|
20
23
|
// AsyncLocalStorage for tracking task context across async boundaries
|
|
21
24
|
const taskContext = new AsyncLocalStorage();
|
|
@@ -70,6 +73,120 @@ function configureBaseRequire(nodeRedUserDir) {
|
|
|
70
73
|
global.require = baseRequire;
|
|
71
74
|
}
|
|
72
75
|
|
|
76
|
+
function createContextProxy(initialData, updates) {
|
|
77
|
+
const data = (initialData && typeof initialData === 'object') ? initialData : {};
|
|
78
|
+
const updateMap = updates || {};
|
|
79
|
+
|
|
80
|
+
const getValue = (key, fallback) => {
|
|
81
|
+
if (Object.prototype.hasOwnProperty.call(updateMap, key)) {
|
|
82
|
+
return updateMap[key];
|
|
83
|
+
}
|
|
84
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
85
|
+
return data[key];
|
|
86
|
+
}
|
|
87
|
+
return fallback;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const setValue = (key, value) => {
|
|
91
|
+
data[key] = value;
|
|
92
|
+
updateMap[key] = value;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const api = {
|
|
96
|
+
get: (key, storeOrCb, cbMaybe) => {
|
|
97
|
+
let callback = null;
|
|
98
|
+
if (typeof storeOrCb === 'function') {
|
|
99
|
+
callback = storeOrCb;
|
|
100
|
+
} else if (typeof cbMaybe === 'function') {
|
|
101
|
+
callback = cbMaybe;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const value = Array.isArray(key)
|
|
105
|
+
? key.map((entry) => getValue(entry, undefined))
|
|
106
|
+
: getValue(key, undefined);
|
|
107
|
+
|
|
108
|
+
if (typeof callback === 'function') {
|
|
109
|
+
callback(null, value);
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
},
|
|
114
|
+
set: (key, value, cbMaybe) => {
|
|
115
|
+
let callback = typeof cbMaybe === 'function' ? cbMaybe : null;
|
|
116
|
+
let entries = [];
|
|
117
|
+
|
|
118
|
+
if (Array.isArray(key)) {
|
|
119
|
+
if (Array.isArray(value)) {
|
|
120
|
+
entries = key.map((entry, index) => [entry, value[index]]);
|
|
121
|
+
} else if (value && typeof value === 'object') {
|
|
122
|
+
entries = key.map((entry) => [entry, value[entry]]);
|
|
123
|
+
}
|
|
124
|
+
} else if (key && typeof key === 'object' && value !== null) {
|
|
125
|
+
entries = Object.entries(key);
|
|
126
|
+
if (typeof value === 'function') {
|
|
127
|
+
callback = value;
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
entries = [[key, value]];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
entries.forEach(([entryKey, entryValue]) => {
|
|
134
|
+
if (entryKey !== undefined) {
|
|
135
|
+
setValue(entryKey, entryValue);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (typeof callback === 'function') {
|
|
140
|
+
callback(null);
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return new Proxy(api, {
|
|
147
|
+
get(target, prop) {
|
|
148
|
+
if (typeof prop === 'symbol' || prop in target) {
|
|
149
|
+
return target[prop];
|
|
150
|
+
}
|
|
151
|
+
return getValue(prop, undefined);
|
|
152
|
+
},
|
|
153
|
+
set(target, prop, value) {
|
|
154
|
+
if (typeof prop === 'symbol' || prop in target) {
|
|
155
|
+
target[prop] = value;
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
setValue(prop, value);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createNodeProxy(logs) {
|
|
165
|
+
const push = (level, args) => {
|
|
166
|
+
if (!Array.isArray(logs)) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (!args || args.length === 0) {
|
|
170
|
+
logs.push({ level, message: '' });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const message = args.length === 1 ? args[0] : args.map((arg) => arg);
|
|
174
|
+
logs.push({ level, message });
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
warn: (...args) => {
|
|
179
|
+
push('warn', args);
|
|
180
|
+
},
|
|
181
|
+
error: (...args) => {
|
|
182
|
+
push('error', args);
|
|
183
|
+
},
|
|
184
|
+
log: (...args) => {
|
|
185
|
+
push('log', args);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
73
190
|
function parseModuleSpec(spec) {
|
|
74
191
|
const match = /((?:@[^/]+\/)?[^/@]+)(\/[^/@]+)?(?:@([\s\S]+))?/.exec(spec);
|
|
75
192
|
if (!match) {
|
|
@@ -139,6 +256,9 @@ async function initializeWorker(initData) {
|
|
|
139
256
|
configureBaseRequire(nodeRedUserDir);
|
|
140
257
|
|
|
141
258
|
const threshold = initData && typeof initData.shmThreshold === 'number' ? initData.shmThreshold : undefined;
|
|
259
|
+
if (initData && typeof initData.transferMode === 'string') {
|
|
260
|
+
transferMode = initData.transferMode;
|
|
261
|
+
}
|
|
142
262
|
shmManager = new SharedMemoryManager({
|
|
143
263
|
threshold,
|
|
144
264
|
trackAttachments: false,
|
|
@@ -188,30 +308,59 @@ process.on('message', async (data) => {
|
|
|
188
308
|
|
|
189
309
|
if (type === 'execute') {
|
|
190
310
|
taskContext.run({ taskId }, async () => {
|
|
311
|
+
const contextUpdates = { flow: {}, global: {}, context: {} };
|
|
312
|
+
const logs = [];
|
|
313
|
+
|
|
191
314
|
try {
|
|
192
315
|
const restoreStart = process.hrtime.bigint();
|
|
193
|
-
const
|
|
316
|
+
const restoredPayload = await serializer.restoreBuffers(msg);
|
|
194
317
|
const transferToWorkerMs = hrtimeDiffToMs(restoreStart);
|
|
195
318
|
|
|
319
|
+
let contextPayload = {};
|
|
320
|
+
let restoredMsg = restoredPayload;
|
|
321
|
+
if (restoredPayload && typeof restoredPayload === 'object' && Object.prototype.hasOwnProperty.call(restoredPayload, MSG_WRAPPER_KEY)) {
|
|
322
|
+
contextPayload = restoredPayload[CONTEXT_WRAPPER_KEY] || {};
|
|
323
|
+
restoredMsg = restoredPayload[MSG_WRAPPER_KEY];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const nodeProxy = createNodeProxy(logs);
|
|
327
|
+
const flowProxy = createContextProxy(contextPayload.flow, contextUpdates.flow);
|
|
328
|
+
const globalProxy = createContextProxy(contextPayload.global, contextUpdates.global);
|
|
329
|
+
const contextProxy = createContextProxy(contextPayload.context, contextUpdates.context);
|
|
330
|
+
|
|
196
331
|
const cacheKey = code + '|' + moduleVars.join(',');
|
|
197
332
|
let userFunction = getCachedFunction(cacheKey);
|
|
198
333
|
if (!userFunction) {
|
|
199
|
-
userFunction = new AsyncFunction('msg', ...moduleVars, code);
|
|
334
|
+
userFunction = new AsyncFunction('msg', 'node', 'flow', 'global', 'context', ...moduleVars, code);
|
|
200
335
|
setCachedFunction(cacheKey, userFunction);
|
|
201
336
|
}
|
|
202
337
|
|
|
203
338
|
const execStart = process.hrtime.bigint();
|
|
204
|
-
const rawResult = await userFunction(restoredMsg, ...moduleValues);
|
|
339
|
+
const rawResult = await userFunction(restoredMsg, nodeProxy, flowProxy, globalProxy, contextProxy, ...moduleValues);
|
|
205
340
|
const executionMs = hrtimeDiffToMs(execStart);
|
|
206
341
|
|
|
207
342
|
const encodeStart = process.hrtime.bigint();
|
|
208
|
-
const
|
|
343
|
+
const bufferCache = new WeakMap();
|
|
344
|
+
const encodedResult = await serializer.sanitizeMessage(rawResult, null, taskId, {
|
|
345
|
+
transferMode,
|
|
346
|
+
bufferCache
|
|
347
|
+
});
|
|
348
|
+
const hasUpdates = (
|
|
349
|
+
Object.keys(contextUpdates.flow).length > 0 ||
|
|
350
|
+
Object.keys(contextUpdates.global).length > 0 ||
|
|
351
|
+
Object.keys(contextUpdates.context).length > 0
|
|
352
|
+
);
|
|
353
|
+
const encodedContextUpdates = hasUpdates
|
|
354
|
+
? await serializer.sanitizeMessage(contextUpdates, null, taskId, { transferMode, bufferCache })
|
|
355
|
+
: null;
|
|
209
356
|
const transferToMainMs = hrtimeDiffToMs(encodeStart);
|
|
210
357
|
|
|
211
358
|
sendMessage({
|
|
212
359
|
type: 'result',
|
|
213
360
|
taskId,
|
|
214
361
|
result: encodedResult,
|
|
362
|
+
contextUpdates: encodedContextUpdates,
|
|
363
|
+
logs: logs.length > 0 ? logs : null,
|
|
215
364
|
performance: {
|
|
216
365
|
transferToWorkerMs,
|
|
217
366
|
executionMs,
|
|
@@ -219,7 +368,32 @@ process.on('message', async (data) => {
|
|
|
219
368
|
}
|
|
220
369
|
});
|
|
221
370
|
} catch (err) {
|
|
222
|
-
|
|
371
|
+
let encodedContextUpdates = null;
|
|
372
|
+
const hasUpdates = (
|
|
373
|
+
Object.keys(contextUpdates.flow).length > 0 ||
|
|
374
|
+
Object.keys(contextUpdates.global).length > 0 ||
|
|
375
|
+
Object.keys(contextUpdates.context).length > 0
|
|
376
|
+
);
|
|
377
|
+
if (hasUpdates) {
|
|
378
|
+
try {
|
|
379
|
+
encodedContextUpdates = await serializer.sanitizeMessage(contextUpdates, null, taskId, { transferMode });
|
|
380
|
+
} catch (_encodeErr) {
|
|
381
|
+
encodedContextUpdates = null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
386
|
+
sendMessage({
|
|
387
|
+
type: 'error',
|
|
388
|
+
taskId,
|
|
389
|
+
error: {
|
|
390
|
+
message: error.message,
|
|
391
|
+
stack: error.stack || '',
|
|
392
|
+
name: error.name || 'Error'
|
|
393
|
+
},
|
|
394
|
+
contextUpdates: encodedContextUpdates,
|
|
395
|
+
logs: logs.length > 0 ? logs : null
|
|
396
|
+
});
|
|
223
397
|
}
|
|
224
398
|
});
|
|
225
399
|
} else if (type === 'terminate') {
|
|
@@ -10,6 +10,50 @@
|
|
|
10
10
|
|
|
11
11
|
const SHARED_SENTINEL_KEY = '__rosepetal_shm_path__';
|
|
12
12
|
const SHARED_BASE64_KEY = '__rosepetal_base64__';
|
|
13
|
+
const TRANSFER_SENTINEL_KEY = '__rosepetal_transfer_ab__';
|
|
14
|
+
|
|
15
|
+
function canTransferBuffer(value) {
|
|
16
|
+
if (!Buffer.isBuffer(value)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!value.buffer || !(value.buffer instanceof ArrayBuffer)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Avoid detaching pooled/sliced buffers that share backing stores
|
|
25
|
+
if (value.byteOffset !== 0) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (value.byteLength !== value.buffer.byteLength) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createTransferDescriptor(value, transferList, transferSet) {
|
|
37
|
+
if (!transferList) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!canTransferBuffer(value)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const arrayBuffer = value.buffer;
|
|
46
|
+
if (transferSet && !transferSet.has(arrayBuffer)) {
|
|
47
|
+
transferSet.add(arrayBuffer);
|
|
48
|
+
transferList.push(arrayBuffer);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
[TRANSFER_SENTINEL_KEY]: arrayBuffer,
|
|
53
|
+
byteOffset: value.byteOffset,
|
|
54
|
+
byteLength: value.byteLength
|
|
55
|
+
};
|
|
56
|
+
}
|
|
13
57
|
|
|
14
58
|
/**
|
|
15
59
|
* Async Message Serializer Class
|
|
@@ -31,18 +75,29 @@ class AsyncMessageSerializer {
|
|
|
31
75
|
* @param {object} msg - Message object to sanitize
|
|
32
76
|
* @param {object} node - Node-RED node instance (for warnings)
|
|
33
77
|
* @param {number|string} taskId - Task identifier for shared memory tracking
|
|
78
|
+
* @param {object} options - Optional serialization options
|
|
34
79
|
* @returns {Promise<object>} Sanitized message object
|
|
35
80
|
*/
|
|
36
|
-
async sanitizeMessage(msg, node, taskId) {
|
|
81
|
+
async sanitizeMessage(msg, node, taskId, options = {}) {
|
|
37
82
|
if (!msg || typeof msg !== 'object') {
|
|
38
83
|
return msg;
|
|
39
84
|
}
|
|
40
85
|
|
|
41
86
|
const seen = new WeakMap();
|
|
42
87
|
const bufferIndex = { value: 0 }; // Mutable counter for buffer indexing
|
|
88
|
+
const transferMode = typeof options.transferMode === 'string' ? options.transferMode : 'shared';
|
|
89
|
+
const transferList = Array.isArray(options.transferList) ? options.transferList : null;
|
|
90
|
+
const transferSet = options.transferSet || (transferList ? new Set() : null);
|
|
91
|
+
const bufferCache = options.bufferCache || new WeakMap();
|
|
92
|
+
const cloneOptions = {
|
|
93
|
+
transferMode,
|
|
94
|
+
transferList,
|
|
95
|
+
transferSet,
|
|
96
|
+
bufferCache
|
|
97
|
+
};
|
|
43
98
|
|
|
44
99
|
try {
|
|
45
|
-
return await this.cloneValue(msg, seen, bufferIndex, taskId, 0);
|
|
100
|
+
return await this.cloneValue(msg, seen, bufferIndex, taskId, 0, cloneOptions);
|
|
46
101
|
} catch (err) {
|
|
47
102
|
if (node) {
|
|
48
103
|
node.warn(`Message cloning failed: ${err.message}, creating minimal message`);
|
|
@@ -58,11 +113,10 @@ class AsyncMessageSerializer {
|
|
|
58
113
|
* @param {number} depth - Current recursion depth
|
|
59
114
|
* @param {object} bufferIndex - Mutable buffer index counter
|
|
60
115
|
* @param {number|string} taskId - Task identifier
|
|
61
|
-
* @param {object}
|
|
62
|
-
* @param {string} path - Current property path (for warnings)
|
|
116
|
+
* @param {object} options - Serialization options
|
|
63
117
|
* @returns {Promise<*>} Cloned value
|
|
64
118
|
*/
|
|
65
|
-
async cloneValue(value, seen, bufferIndex, taskId, depth) {
|
|
119
|
+
async cloneValue(value, seen, bufferIndex, taskId, depth, options) {
|
|
66
120
|
// Optional depth guard (disabled by default for speed)
|
|
67
121
|
if (this.maxDepth > 0 && depth > this.maxDepth) {
|
|
68
122
|
return null;
|
|
@@ -83,14 +137,74 @@ class AsyncMessageSerializer {
|
|
|
83
137
|
return undefined;
|
|
84
138
|
}
|
|
85
139
|
|
|
86
|
-
// Buffers + typed arrays:
|
|
140
|
+
// Buffers + typed arrays: transfer, inline copy, or shared memory
|
|
87
141
|
if (Buffer.isBuffer(value)) {
|
|
88
|
-
|
|
142
|
+
if (options && options.bufferCache && options.bufferCache.has(value)) {
|
|
143
|
+
return options.bufferCache.get(value);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (options && options.transferMode === 'copy') {
|
|
147
|
+
if (options.bufferCache) {
|
|
148
|
+
options.bufferCache.set(value, value);
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (options && options.transferMode === 'transfer' && options.transferList) {
|
|
154
|
+
const transferDescriptor = createTransferDescriptor(value, options.transferList, options.transferSet);
|
|
155
|
+
if (transferDescriptor) {
|
|
156
|
+
if (options.bufferCache) {
|
|
157
|
+
options.bufferCache.set(value, transferDescriptor);
|
|
158
|
+
}
|
|
159
|
+
return transferDescriptor;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (options.bufferCache) {
|
|
163
|
+
options.bufferCache.set(value, value);
|
|
164
|
+
}
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const sharedDescriptor = await this.shmManager.writeBuffer(value, taskId, bufferIndex.value++);
|
|
169
|
+
if (options && options.bufferCache) {
|
|
170
|
+
options.bufferCache.set(value, sharedDescriptor);
|
|
171
|
+
}
|
|
172
|
+
return sharedDescriptor;
|
|
89
173
|
}
|
|
90
174
|
|
|
91
175
|
if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
|
|
92
176
|
const asBuffer = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
93
|
-
|
|
177
|
+
if (options && options.bufferCache && options.bufferCache.has(asBuffer)) {
|
|
178
|
+
return options.bufferCache.get(asBuffer);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (options && options.transferMode === 'copy') {
|
|
182
|
+
if (options.bufferCache) {
|
|
183
|
+
options.bufferCache.set(asBuffer, asBuffer);
|
|
184
|
+
}
|
|
185
|
+
return asBuffer;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (options && options.transferMode === 'transfer' && options.transferList) {
|
|
189
|
+
const transferDescriptor = createTransferDescriptor(asBuffer, options.transferList, options.transferSet);
|
|
190
|
+
if (transferDescriptor) {
|
|
191
|
+
if (options.bufferCache) {
|
|
192
|
+
options.bufferCache.set(asBuffer, transferDescriptor);
|
|
193
|
+
}
|
|
194
|
+
return transferDescriptor;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (options.bufferCache) {
|
|
198
|
+
options.bufferCache.set(asBuffer, asBuffer);
|
|
199
|
+
}
|
|
200
|
+
return asBuffer;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const sharedDescriptor = await this.shmManager.writeBuffer(asBuffer, taskId, bufferIndex.value++);
|
|
204
|
+
if (options && options.bufferCache) {
|
|
205
|
+
options.bufferCache.set(asBuffer, sharedDescriptor);
|
|
206
|
+
}
|
|
207
|
+
return sharedDescriptor;
|
|
94
208
|
}
|
|
95
209
|
|
|
96
210
|
if (type === 'object') {
|
|
@@ -104,7 +218,7 @@ class AsyncMessageSerializer {
|
|
|
104
218
|
|
|
105
219
|
if (Array.isArray(value)) {
|
|
106
220
|
for (let i = 0; i < value.length; i++) {
|
|
107
|
-
const clonedItem = await this.cloneValue(value[i], seen, bufferIndex, taskId, depth + 1);
|
|
221
|
+
const clonedItem = await this.cloneValue(value[i], seen, bufferIndex, taskId, depth + 1, options);
|
|
108
222
|
clone[i] = clonedItem === undefined ? null : clonedItem;
|
|
109
223
|
}
|
|
110
224
|
return clone;
|
|
@@ -113,7 +227,7 @@ class AsyncMessageSerializer {
|
|
|
113
227
|
const keys = Object.keys(value);
|
|
114
228
|
for (let i = 0; i < keys.length; i++) {
|
|
115
229
|
const key = keys[i];
|
|
116
|
-
const clonedVal = await this.cloneValue(value[key], seen, bufferIndex, taskId, depth + 1);
|
|
230
|
+
const clonedVal = await this.cloneValue(value[key], seen, bufferIndex, taskId, depth + 1, options);
|
|
117
231
|
if (clonedVal !== undefined) {
|
|
118
232
|
clone[key] = clonedVal;
|
|
119
233
|
}
|
|
@@ -132,10 +246,11 @@ class AsyncMessageSerializer {
|
|
|
132
246
|
*/
|
|
133
247
|
async restoreBuffers(value) {
|
|
134
248
|
const seen = new WeakMap();
|
|
135
|
-
|
|
249
|
+
const descriptorCache = new WeakMap();
|
|
250
|
+
return this.restoreValue(value, seen, descriptorCache);
|
|
136
251
|
}
|
|
137
252
|
|
|
138
|
-
async restoreValue(value, seen) {
|
|
253
|
+
async restoreValue(value, seen, descriptorCache) {
|
|
139
254
|
// Handle null/undefined
|
|
140
255
|
if (value === null || value === undefined) {
|
|
141
256
|
return value;
|
|
@@ -162,7 +277,7 @@ class AsyncMessageSerializer {
|
|
|
162
277
|
seen.set(value, result);
|
|
163
278
|
|
|
164
279
|
for (let i = 0; i < value.length; i++) {
|
|
165
|
-
result[i] = await this.restoreValue(value[i], seen);
|
|
280
|
+
result[i] = await this.restoreValue(value[i], seen, descriptorCache);
|
|
166
281
|
}
|
|
167
282
|
|
|
168
283
|
return result;
|
|
@@ -170,10 +285,42 @@ class AsyncMessageSerializer {
|
|
|
170
285
|
|
|
171
286
|
// Handle objects
|
|
172
287
|
if (type === 'object') {
|
|
288
|
+
// Transfer-list descriptor
|
|
289
|
+
if (Object.prototype.hasOwnProperty.call(value, TRANSFER_SENTINEL_KEY)) {
|
|
290
|
+
if (descriptorCache && descriptorCache.has(value)) {
|
|
291
|
+
return descriptorCache.get(value);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const arrayBuffer = value[TRANSFER_SENTINEL_KEY];
|
|
296
|
+
if (!(arrayBuffer instanceof ArrayBuffer)) {
|
|
297
|
+
return Buffer.alloc(0);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const byteOffset = Number(value.byteOffset) || 0;
|
|
301
|
+
const byteLength = Number(value.byteLength) || arrayBuffer.byteLength;
|
|
302
|
+
const buffer = Buffer.from(arrayBuffer, byteOffset, byteLength);
|
|
303
|
+
if (descriptorCache) {
|
|
304
|
+
descriptorCache.set(value, buffer);
|
|
305
|
+
}
|
|
306
|
+
return buffer;
|
|
307
|
+
} catch (_err) {
|
|
308
|
+
return Buffer.alloc(0);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
173
312
|
// Shared memory descriptor
|
|
174
313
|
if (Object.prototype.hasOwnProperty.call(value, SHARED_SENTINEL_KEY)) {
|
|
314
|
+
if (descriptorCache && descriptorCache.has(value)) {
|
|
315
|
+
return descriptorCache.get(value);
|
|
316
|
+
}
|
|
317
|
+
|
|
175
318
|
try {
|
|
176
|
-
|
|
319
|
+
const buffer = await this.shmManager.readBuffer(value, { deleteAfterRead: true });
|
|
320
|
+
if (descriptorCache) {
|
|
321
|
+
descriptorCache.set(value, buffer);
|
|
322
|
+
}
|
|
323
|
+
return buffer;
|
|
177
324
|
} catch (_err) {
|
|
178
325
|
return Buffer.alloc(0);
|
|
179
326
|
}
|
|
@@ -181,8 +328,16 @@ class AsyncMessageSerializer {
|
|
|
181
328
|
|
|
182
329
|
// Base64 fallback
|
|
183
330
|
if (Object.prototype.hasOwnProperty.call(value, SHARED_BASE64_KEY)) {
|
|
331
|
+
if (descriptorCache && descriptorCache.has(value)) {
|
|
332
|
+
return descriptorCache.get(value);
|
|
333
|
+
}
|
|
334
|
+
|
|
184
335
|
try {
|
|
185
|
-
|
|
336
|
+
const buffer = Buffer.from(value[SHARED_BASE64_KEY] || '', 'base64');
|
|
337
|
+
if (descriptorCache) {
|
|
338
|
+
descriptorCache.set(value, buffer);
|
|
339
|
+
}
|
|
340
|
+
return buffer;
|
|
186
341
|
} catch (_err) {
|
|
187
342
|
return Buffer.alloc(0);
|
|
188
343
|
}
|
|
@@ -198,7 +353,7 @@ class AsyncMessageSerializer {
|
|
|
198
353
|
const entries = Object.entries(value);
|
|
199
354
|
for (let i = 0; i < entries.length; i++) {
|
|
200
355
|
const [key, val] = entries[i];
|
|
201
|
-
result[key] = await this.restoreValue(val, seen);
|
|
356
|
+
result[key] = await this.restoreValue(val, seen, descriptorCache);
|
|
202
357
|
}
|
|
203
358
|
|
|
204
359
|
return result;
|
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
* Modules are installed in the Node-RED user directory (~/.node-red).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const {
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const os = require('os');
|
|
11
|
-
const fs = require('fs');
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
const { constants: fsConstants } = require('fs');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Get the Node-RED user directory
|
|
15
|
-
* @returns {string} Path to Node-RED user directory
|
|
16
|
+
* @returns {Promise<string>} Path to Node-RED user directory
|
|
16
17
|
*/
|
|
17
|
-
function getNodeRedUserDir() {
|
|
18
|
+
async function getNodeRedUserDir() {
|
|
18
19
|
// Check for explicit NODE_RED_HOME environment variable
|
|
19
20
|
if (process.env.NODE_RED_HOME) {
|
|
20
21
|
return process.env.NODE_RED_HOME;
|
|
@@ -24,8 +25,11 @@ function getNodeRedUserDir() {
|
|
|
24
25
|
const defaultDir = path.join(os.homedir(), '.node-red');
|
|
25
26
|
|
|
26
27
|
// Verify it exists
|
|
27
|
-
|
|
28
|
+
try {
|
|
29
|
+
await fs.access(defaultDir, fsConstants.F_OK);
|
|
28
30
|
return defaultDir;
|
|
31
|
+
} catch (_err) {
|
|
32
|
+
// Ignore access errors
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
// Fallback to home directory if .node-red doesn't exist
|
|
@@ -35,9 +39,9 @@ function getNodeRedUserDir() {
|
|
|
35
39
|
/**
|
|
36
40
|
* Install an npm module in the Node-RED user directory
|
|
37
41
|
* @param {string} moduleName - Name of the module to install
|
|
38
|
-
* @returns {boolean} True if installation succeeded
|
|
42
|
+
* @returns {Promise<boolean>} True if installation succeeded
|
|
39
43
|
*/
|
|
40
|
-
function installModule(moduleName) {
|
|
44
|
+
async function installModule(moduleName) {
|
|
41
45
|
if (!moduleName || typeof moduleName !== 'string') {
|
|
42
46
|
console.error('[async-function] Invalid module name');
|
|
43
47
|
return false;
|
|
@@ -50,30 +54,49 @@ function installModule(moduleName) {
|
|
|
50
54
|
return false;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
const userDir = getNodeRedUserDir();
|
|
57
|
+
const userDir = await getNodeRedUserDir();
|
|
54
58
|
|
|
55
59
|
try {
|
|
56
60
|
console.log(`[async-function] Installing module: ${sanitizedName} in ${userDir}`);
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
await new Promise((resolve, reject) => {
|
|
63
|
+
const child = spawn('npm', ['install', sanitizedName], {
|
|
64
|
+
cwd: userDir,
|
|
65
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
66
|
+
env: {
|
|
67
|
+
...process.env,
|
|
68
|
+
npm_config_loglevel: 'error'
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
let stderr = '';
|
|
73
|
+
if (child.stderr) {
|
|
74
|
+
child.stderr.on('data', (chunk) => {
|
|
75
|
+
stderr += chunk.toString();
|
|
76
|
+
});
|
|
66
77
|
}
|
|
67
|
-
});
|
|
68
78
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
const timeout = setTimeout(() => {
|
|
80
|
+
if (!child.killed) {
|
|
81
|
+
child.kill();
|
|
82
|
+
}
|
|
83
|
+
reject(new Error('npm install timed out'));
|
|
84
|
+
}, 120000);
|
|
85
|
+
|
|
86
|
+
child.on('error', (err) => {
|
|
87
|
+
clearTimeout(timeout);
|
|
88
|
+
reject(err);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
child.on('close', (code) => {
|
|
92
|
+
clearTimeout(timeout);
|
|
93
|
+
if (code === 0) {
|
|
94
|
+
resolve();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
reject(new Error(`npm install failed with code ${code}: ${stderr || 'Unknown error'}`));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
77
100
|
|
|
78
101
|
console.log(`[async-function] Successfully installed: ${sanitizedName}`);
|
|
79
102
|
return true;
|