@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 CHANGED
@@ -31,7 +31,7 @@ Drop an **async function** node into your flow. Write your code just like you wo
31
31
 
32
32
  **Skip It For:**
33
33
  - Simple math or quick transformations (the regular function node is faster).
34
- - When you need `context`, `flow`, or `global` storage (coming in v2.0).
34
+ - Flows that require live context reads/writes during execution (context is snapshot-based).
35
35
 
36
36
  ## Node Options
37
37
 
@@ -62,7 +62,7 @@ return msg;
62
62
  ```
63
63
 
64
64
  ### Buffer Handling
65
- - **Buffers** – Any `Buffer` in `msg` is transferred through shared memory (`/dev/shm` on Linux, otherwise `os.tmpdir()`), with base64 fallback if needed.
65
+ - **Buffers** – Worker Threads use zero-copy transfer when possible. Child Process mode and non-transferable buffers fall back to shared memory (`/dev/shm` on Linux, otherwise `os.tmpdir()`), with base64 fallback if needed.
66
66
 
67
67
  ## Typical Flow
68
68
 
@@ -86,9 +86,11 @@ return msg;
86
86
  - `console` – Logging functions
87
87
  - `setTimeout`, `setInterval` – Timers
88
88
 
89
- **Not Available (Yet):**
90
- - `context`, `flow`, `global` Coming in v2.0
91
- - `node` Node instance methods
89
+ **Notes:**
90
+ - `context`, `flow`, `global` are snapshot-based: reads are from the snapshot, writes are applied after the function completes
91
+ - Snapshot includes only literal keys found in `flow.get("key")` / `global.get("key")` / `context.get("key")`
92
+ - Context store selection is not supported (default store only)
93
+ - `node.warn/error/log` are collected and forwarded to the main thread
92
94
  - Non-serializable objects (functions, symbols, etc.)
93
95
 
94
96
  ## Code Examples
@@ -170,7 +172,7 @@ The node shows you what's happening in real time:
170
172
  - Best for operations taking more than 10ms to run.
171
173
  - Each node maintains a fixed pool of workers—no startup delay or dynamic scaling overhead.
172
174
  - Workers are dedicated per-node, ensuring predictable performance.
173
- - **Binary Fast Path**: Buffers use shared memory transfer (base64 fallback), keeping messages responsive even with large payloads.
175
+ - **Binary Fast Path**: Worker threads use zero-copy transfer when possible; shared memory is the fallback.
174
176
  - Event loop never blocks, even when processing multi-MB binary data (images, files, etc.).
175
177
 
176
178
  ## Error Handling
@@ -7,7 +7,7 @@
7
7
  defaults: {
8
8
  name: { value: '' },
9
9
  func: {
10
- value: '// Write your async code here\n// The code runs in a worker thread or child process\n// Available: msg, return, async/await, require()\n// Not available: context, flow, global, node\n\nreturn msg;'
10
+ value: '// Write your async code here\n// The code runs in a worker thread or child process\n// Available: msg, return, async/await, require(), node.warn/error/log,\n// flow/global/context (snapshot-based)\n\nreturn msg;'
11
11
  },
12
12
  outputs: { value: 1, validate: RED.validators.number() },
13
13
  timeout: { value: 30000, validate: RED.validators.number() },
@@ -507,8 +507,8 @@
507
507
  <b>Tips:</b>
508
508
  <ul style="margin-top:5px; margin-bottom:0;">
509
509
  <li>Code runs in a worker thread or child process to prevent blocking</li>
510
- <li>Available: <code>msg</code>, <code>return</code>, <code>async/await</code>, <code>require()</code></li>
511
- <li>Not available: <code>context</code>, <code>flow</code>, <code>global</code>, <code>node</code></li>
510
+ <li>Available: <code>msg</code>, <code>return</code>, <code>async/await</code>, <code>require()</code>, <code>node</code>, <code>flow</code>, <code>global</code>, <code>context</code></li>
511
+ <li>Context access is snapshot-based (writes apply after execution)</li>
512
512
  <li>Use "Child Process" runtime for native modules that require main thread (e.g., <code>gl</code>)</li>
513
513
  <li>Return <code>msg</code> for single output, or <code>[msg1, msg2]</code> for multiple outputs</li>
514
514
  <li>Return <code>null</code> to stop the flow</li>
@@ -546,14 +546,14 @@
546
546
  <li><code>return</code> - Return modified message or array of messages</li>
547
547
  <li><code>async/await</code> - Use async operations</li>
548
548
  <li><code>require()</code> - Import Node.js modules</li>
549
+ <li><code>node</code> - Node helpers (<code>warn</code>, <code>error</code>, <code>log</code>)</li>
550
+ <li><code>flow</code>, <code>global</code>, <code>context</code> - Snapshot-based context access (writes apply after execution)</li>
549
551
  <li><code>console</code> - Logging functions</li>
550
552
  <li><code>setTimeout</code>, <code>setInterval</code> - Timers</li>
551
553
  </ul>
552
554
 
553
555
  <h4>Not Available</h4>
554
556
  <ul>
555
- <li><code>context</code>, <code>flow</code>, <code>global</code> - Context storage (v2.0 feature)</li>
556
- <li><code>node</code> - Node instance methods</li>
557
557
  <li>Non-serializable objects in <code>msg</code></li>
558
558
  </ul>
559
559
 
@@ -612,7 +612,7 @@ return msg;</pre>
612
612
  <dt>Queue Size</dt>
613
613
  <dd>Maximum number of messages queued when all workers are busy (10-1000). Default: 100.</dd>
614
614
  <dt>Buffers</dt>
615
- <dd>All Buffers are transferred via shared memory when possible (falls back to base64 if needed).</dd>
615
+ <dd>Worker threads use zero-copy transfer when possible; shared memory is used as a fallback.</dd>
616
616
  </dl>
617
617
 
618
618
  <h3>Status Display</h3>
@@ -639,9 +639,11 @@ return msg;</pre>
639
639
  <li>Best for CPU-intensive operations (>10ms execution time)</li>
640
640
  <li>For simple operations, use the standard function node</li>
641
641
  <li>Workers are pooled and reused for efficiency</li>
642
- <li><b>Buffer Transfer:</b> Buffers are streamed through shared memory (base64 fallback)</li>
642
+ <li><b>Buffer Transfer:</b> Worker threads use zero-copy transfer when possible; shared memory is the fallback</li>
643
643
  <li>Event loop never blocks, even when processing multi-MB binary data (images, files, etc.)</li>
644
644
  <li>Message transfer is optimized by copying only referenced <code>msg.*</code> keys when possible</li>
645
+ <li>Context snapshots include only literal keys found in <code>flow.get("key")</code> etc.</li>
646
+ <li>Context snapshots use the default context store only</li>
645
647
  <li>Timing is recorded on <code>msg.performance</code> under the node label</li>
646
648
  </ul>
647
649
 
@@ -44,7 +44,7 @@ function extractMsgKeysFromCode(code) {
44
44
  return keys;
45
45
  }
46
46
 
47
- function buildWorkerInputMsg(originalMsg, code) {
47
+ function buildWorkerInputMsgCore(originalMsg, code) {
48
48
  if (!originalMsg || typeof originalMsg !== 'object') {
49
49
  return originalMsg;
50
50
  }
@@ -84,6 +84,304 @@ function hrtimeDiffToMs(start) {
84
84
  return Number(diff) / 1e6;
85
85
  }
86
86
 
87
+ function normalizeTransferMode(mode, executionMode) {
88
+ const normalized = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
89
+ if (normalized === 'transfer' || normalized === 'shared' || normalized === 'copy') {
90
+ return normalized;
91
+ }
92
+ return executionMode === 'worker_threads' ? 'transfer' : 'shared';
93
+ }
94
+
95
+ const MSG_WRAPPER_KEY = '__rosepetal_msg';
96
+ const CONTEXT_WRAPPER_KEY = '__rosepetal_context';
97
+
98
+ function extractContextKeysFromCode(code) {
99
+ const flowKeys = new Set();
100
+ const globalKeys = new Set();
101
+ const contextKeys = new Set();
102
+
103
+ if (typeof code !== 'string' || !code.trim()) {
104
+ return { flow: flowKeys, global: globalKeys, context: contextKeys, usesContext: false };
105
+ }
106
+
107
+ const flowRegex = /flow\.(?:get|set)\(\s*['"]([^'"]+)['"]/g;
108
+ const flowBracket = /flow\[['"]([^'"]+)['"]\]/g;
109
+ const globalRegex = /global\.(?:get|set)\(\s*['"]([^'"]+)['"]/g;
110
+ const globalBracket = /global\[['"]([^'"]+)['"]\]/g;
111
+ const contextRegex = /context\.(?:get|set)\(\s*['"]([^'"]+)['"]/g;
112
+ const contextBracket = /context\[['"]([^'"]+)['"]\]/g;
113
+
114
+ let match = flowRegex.exec(code);
115
+ while (match) {
116
+ flowKeys.add(match[1]);
117
+ match = flowRegex.exec(code);
118
+ }
119
+
120
+ match = flowBracket.exec(code);
121
+ while (match) {
122
+ flowKeys.add(match[1]);
123
+ match = flowBracket.exec(code);
124
+ }
125
+
126
+ match = globalRegex.exec(code);
127
+ while (match) {
128
+ globalKeys.add(match[1]);
129
+ match = globalRegex.exec(code);
130
+ }
131
+
132
+ match = globalBracket.exec(code);
133
+ while (match) {
134
+ globalKeys.add(match[1]);
135
+ match = globalBracket.exec(code);
136
+ }
137
+
138
+ match = contextRegex.exec(code);
139
+ while (match) {
140
+ contextKeys.add(match[1]);
141
+ match = contextRegex.exec(code);
142
+ }
143
+
144
+ match = contextBracket.exec(code);
145
+ while (match) {
146
+ contextKeys.add(match[1]);
147
+ match = contextBracket.exec(code);
148
+ }
149
+
150
+ const usesContext = /\b(flow|global|context)\s*\./.test(code);
151
+ return { flow: flowKeys, global: globalKeys, context: contextKeys, usesContext };
152
+ }
153
+
154
+ async function buildContextSnapshot(node, code) {
155
+ const empty = { flow: {}, global: {}, context: {} };
156
+ if (!node || typeof node.context !== 'function') {
157
+ return { snapshot: empty, usesContext: false };
158
+ }
159
+
160
+ const keys = extractContextKeysFromCode(code);
161
+ if (!keys.usesContext) {
162
+ return { snapshot: empty, usesContext: false };
163
+ }
164
+
165
+ const ctx = node.context();
166
+ if (!ctx) {
167
+ return { snapshot: empty, usesContext: true };
168
+ }
169
+
170
+ const snapshot = { flow: {}, global: {}, context: {} };
171
+ const coerceBufferLike = (value) => {
172
+ if (!value || typeof value !== 'object') {
173
+ return value;
174
+ }
175
+ if (Buffer.isBuffer(value)) {
176
+ return value;
177
+ }
178
+ if (value.type === 'Buffer' && Array.isArray(value.data)) {
179
+ return Buffer.from(value.data);
180
+ }
181
+ if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
182
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
183
+ }
184
+ return value;
185
+ };
186
+
187
+ const getValueAsync = (scope, key) => {
188
+ if (!scope || typeof scope.get !== 'function') {
189
+ return Promise.resolve(undefined);
190
+ }
191
+ if (scope.get.length >= 2) {
192
+ return new Promise((resolve) => {
193
+ try {
194
+ scope.get(key, (err, value) => {
195
+ resolve(err ? undefined : value);
196
+ });
197
+ } catch (_err) {
198
+ resolve(undefined);
199
+ }
200
+ });
201
+ }
202
+ try {
203
+ const value = scope.get(key);
204
+ if (value && typeof value.then === 'function') {
205
+ return value.then((resolved) => resolved).catch(() => undefined);
206
+ }
207
+ return Promise.resolve(value);
208
+ } catch (_err) {
209
+ return Promise.resolve(undefined);
210
+ }
211
+ };
212
+
213
+ const readKeys = async (scope, keySet, target) => {
214
+ if (!scope || typeof scope.get !== 'function') {
215
+ return;
216
+ }
217
+ const entries = Array.from(keySet);
218
+ await Promise.all(entries.map(async (key) => {
219
+ try {
220
+ const value = await getValueAsync(scope, key);
221
+ target[key] = coerceBufferLike(value);
222
+ } catch (_err) {
223
+ // Ignore context read errors to avoid blocking execution
224
+ }
225
+ }));
226
+ };
227
+
228
+ await Promise.all([
229
+ readKeys(ctx.flow, keys.flow, snapshot.flow),
230
+ readKeys(ctx.global, keys.global, snapshot.global),
231
+ readKeys(ctx, keys.context, snapshot.context)
232
+ ]);
233
+
234
+ return { snapshot, usesContext: true };
235
+ }
236
+
237
+ function buildWorkerInputMsg(originalMsg, code, contextSnapshot, useContextWrapper) {
238
+ if (!useContextWrapper) {
239
+ return buildWorkerInputMsgCore(originalMsg, code);
240
+ }
241
+
242
+ const msgPayload = buildWorkerInputMsgCore(originalMsg, code);
243
+ return {
244
+ [MSG_WRAPPER_KEY]: msgPayload,
245
+ [CONTEXT_WRAPPER_KEY]: contextSnapshot || { flow: {}, global: {}, context: {} }
246
+ };
247
+ }
248
+
249
+ function rehydrateContextValue(value) {
250
+ if (Buffer.isBuffer(value)) {
251
+ return value;
252
+ }
253
+
254
+ if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
255
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
256
+ }
257
+
258
+ if (Array.isArray(value)) {
259
+ return value.map((item) => rehydrateContextValue(item));
260
+ }
261
+
262
+ if (value && typeof value === 'object') {
263
+ if (value.type === 'Buffer' && Array.isArray(value.data)) {
264
+ return Buffer.from(value.data);
265
+ }
266
+
267
+ const obj = Array.isArray(value) ? [] : {};
268
+ Object.keys(value).forEach((key) => {
269
+ obj[key] = rehydrateContextValue(value[key]);
270
+ });
271
+ return obj;
272
+ }
273
+
274
+ return value;
275
+ }
276
+
277
+ async function applyContextUpdates(node, updates, msg) {
278
+ if (!node || !updates || typeof updates !== 'object' || typeof node.context !== 'function') {
279
+ return;
280
+ }
281
+
282
+ const context = node.context();
283
+ if (!context) {
284
+ return;
285
+ }
286
+
287
+ const flowUpdates = updates.flow && typeof updates.flow === 'object' ? updates.flow : {};
288
+ const globalUpdates = updates.global && typeof updates.global === 'object' ? updates.global : {};
289
+ const contextUpdates = updates.context && typeof updates.context === 'object' ? updates.context : {};
290
+
291
+ const applyUpdates = async (scope, scopeLabel, scopeUpdates) => {
292
+ if (!scope || typeof scope.set !== 'function') {
293
+ return;
294
+ }
295
+ const setValueAsync = (key, value) => {
296
+ if (scope.set.length >= 3) {
297
+ return new Promise((resolve) => {
298
+ try {
299
+ scope.set(key, value, (err) => {
300
+ if (err && typeof node.warn === 'function') {
301
+ node.warn(`Failed to set ${scopeLabel} context "${key}": ${err.message || err}`, msg);
302
+ }
303
+ resolve();
304
+ });
305
+ } catch (err) {
306
+ if (typeof node.warn === 'function') {
307
+ node.warn(`Failed to set ${scopeLabel} context "${key}": ${err.message || err}`, msg);
308
+ }
309
+ resolve();
310
+ }
311
+ });
312
+ }
313
+ try {
314
+ const result = scope.set(key, value);
315
+ if (result && typeof result.then === 'function') {
316
+ return result.catch((err) => {
317
+ if (typeof node.warn === 'function') {
318
+ node.warn(`Failed to set ${scopeLabel} context "${key}": ${err.message || err}`, msg);
319
+ }
320
+ });
321
+ }
322
+ } catch (err) {
323
+ if (typeof node.warn === 'function') {
324
+ node.warn(`Failed to set ${scopeLabel} context "${key}": ${err.message || err}`, msg);
325
+ }
326
+ }
327
+ return Promise.resolve();
328
+ };
329
+ const entries = Object.keys(scopeUpdates);
330
+ await Promise.all(entries.map(async (key) => {
331
+ const value = rehydrateContextValue(scopeUpdates[key]);
332
+ await setValueAsync(key, value);
333
+ }));
334
+ };
335
+
336
+ await Promise.all([
337
+ applyUpdates(context.flow, 'flow', flowUpdates),
338
+ applyUpdates(context.global, 'global', globalUpdates),
339
+ applyUpdates(context, 'context', contextUpdates)
340
+ ]);
341
+ }
342
+
343
+ function applyWorkerLogs(node, logs, msg) {
344
+ if (!node || !Array.isArray(logs)) {
345
+ return;
346
+ }
347
+
348
+ logs.forEach((entry) => {
349
+ if (!entry) {
350
+ return;
351
+ }
352
+
353
+ const level = typeof entry === 'object' && entry.level ? String(entry.level) : 'warn';
354
+ const message = typeof entry === 'object' && entry.message !== undefined
355
+ ? entry.message
356
+ : entry;
357
+ let text = '';
358
+ if (typeof message === 'string') {
359
+ text = message;
360
+ } else {
361
+ try {
362
+ text = JSON.stringify(message);
363
+ } catch (_err) {
364
+ text = String(message);
365
+ }
366
+ }
367
+
368
+ if (level === 'error' && typeof node.error === 'function') {
369
+ node.error(text, msg);
370
+ return;
371
+ }
372
+
373
+ if (level === 'log' && typeof node.log === 'function') {
374
+ node.log(text);
375
+ return;
376
+ }
377
+
378
+ if (typeof node.warn === 'function') {
379
+ node.warn(text, msg);
380
+ }
381
+ });
382
+ }
383
+
384
+
87
385
  function applyPerformanceMetrics(node, originalMsg, targetMsg, performance) {
88
386
  if (!performance || typeof performance !== 'object') {
89
387
  return;
@@ -241,32 +539,9 @@ module.exports = function(RED) {
241
539
  }
242
540
 
243
541
  const nodeRedUserDir = resolveNodeRedUserDir(RED);
542
+ const transferMode = normalizeTransferMode(config.transferMode, node.executionMode);
244
543
 
245
- // Verify modules are resolvable WITHOUT loading them in the main thread.
246
- // Loading native modules (like 'gl') in main thread prevents them from
247
- // working in worker threads due to native module registration conflicts.
248
- if (node.libs.length > 0) {
249
- const { createRequire } = require('module');
250
- const nodeRedRequire = createRequire(path.join(nodeRedUserDir, 'package.json'));
251
-
252
- for (const lib of node.libs) {
253
- if (!lib || !lib.module || !lib.var) {
254
- continue;
255
- }
256
- try {
257
- // Only resolve the path - don't actually load the module
258
- nodeRedRequire.resolve(lib.module);
259
- } catch (err) {
260
- node.error(`Module "${lib.module}" not found. Install it with: cd ${nodeRedUserDir} && npm install ${lib.module}`);
261
- node.status({
262
- fill: 'red',
263
- shape: 'dot',
264
- text: `Module not found: ${lib.module}`
265
- });
266
- return;
267
- }
268
- }
269
- }
544
+ // Module resolution happens inside workers to avoid blocking the main thread.
270
545
 
271
546
  const startPool = () => {
272
547
  try {
@@ -276,6 +551,7 @@ module.exports = function(RED) {
276
551
  maxQueueSize: config.maxQueueSize || 100,
277
552
  taskTimeout: node.timeout,
278
553
  shmThreshold: 0,
554
+ transferMode,
279
555
  libs: node.libs,
280
556
  nodeRedUserDir
281
557
  });
@@ -324,22 +600,29 @@ module.exports = function(RED) {
324
600
  }
325
601
 
326
602
  const timing = { start: process.hrtime.bigint() };
327
- const workerMsg = buildWorkerInputMsg(msg, node.func);
603
+ const contextInfo = await buildContextSnapshot(node, node.func);
604
+ const workerMsg = buildWorkerInputMsg(msg, node.func, contextInfo.snapshot, contextInfo.usesContext);
328
605
 
329
606
  try {
330
607
  const payload = await node.pool.executeTask(node.func, workerMsg, node.timeout);
331
608
 
332
609
  let resultData;
333
610
  let performanceData = null;
611
+ let contextUpdates = null;
612
+ let logs = null;
334
613
 
335
614
  if (payload && typeof payload === 'object' && Object.prototype.hasOwnProperty.call(payload, 'result')) {
336
615
  resultData = payload.result;
337
616
  performanceData = payload.performance || null;
617
+ contextUpdates = payload.contextUpdates || null;
618
+ logs = payload.logs || null;
338
619
  } else {
339
620
  resultData = payload;
340
621
  }
341
622
 
342
623
  if (resultData === null || resultData === undefined) {
624
+ await applyContextUpdates(node, contextUpdates, msg);
625
+ applyWorkerLogs(node, logs, msg);
343
626
  done();
344
627
  return;
345
628
  }
@@ -348,6 +631,9 @@ module.exports = function(RED) {
348
631
  const mergedPerformance = Object.assign({}, performanceData || {});
349
632
  mergedPerformance.totalMs = totalMs;
350
633
 
634
+ await applyContextUpdates(node, contextUpdates, msg);
635
+ applyWorkerLogs(node, logs, msg);
636
+
351
637
  const output = mergeResult(msg, resultData);
352
638
  applyPerformanceToResult(node, msg, output, mergedPerformance);
353
639
 
@@ -362,6 +648,13 @@ module.exports = function(RED) {
362
648
  text: `Error: ${err.message.substring(0, 20)}`
363
649
  });
364
650
 
651
+ if (err && err.contextUpdates) {
652
+ await applyContextUpdates(node, err.contextUpdates, msg);
653
+ }
654
+ if (err && err.logs) {
655
+ applyWorkerLogs(node, err.logs, msg);
656
+ }
657
+
365
658
  // Log error
366
659
  node.error(`Async function error: ${err.message}`, msg);
367
660
 
@@ -17,6 +17,7 @@ const DEFAULT_CONFIG = {
17
17
  taskTimeout: 30000, // Default task timeout: 30s
18
18
  maxQueueSize: 100, // Max queued messages
19
19
  shmThreshold: 0, // Always use shared memory for Buffers
20
+ transferMode: 'shared', // shared | copy (transfer not supported for child processes)
20
21
  libs: [], // External modules to load in workers
21
22
  nodeRedUserDir: null, // Node-RED user directory for module resolution
22
23
  workerScript: path.join(__dirname, 'child-process-script.js')
@@ -166,7 +167,8 @@ class ChildProcessPool {
166
167
  type: 'init',
167
168
  libs: this.config.libs || [],
168
169
  nodeRedUserDir: this.config.nodeRedUserDir,
169
- shmThreshold: this.config.shmThreshold
170
+ shmThreshold: this.config.shmThreshold,
171
+ transferMode: this.config.transferMode
170
172
  });
171
173
  } catch (err) {
172
174
  clearTimeout(readyTimeout);
@@ -298,7 +300,9 @@ class ChildProcessPool {
298
300
  workerState.state = WorkerState.BUSY;
299
301
  workerState.taskId = taskId;
300
302
 
301
- this.serializer.sanitizeMessage(msg, null, taskId).then(sanitizedMsg => {
303
+ this.serializer.sanitizeMessage(msg, null, taskId, {
304
+ transferMode: this.config.transferMode
305
+ }).then(sanitizedMsg => {
302
306
  this.timeoutManager.startTimeout(taskId, timeout, () => {
303
307
  this.handleTimeout(workerState, taskId);
304
308
  });
@@ -329,7 +333,7 @@ class ChildProcessPool {
329
333
  * @param {object} message - Message from worker
330
334
  */
331
335
  handleWorkerMessage(workerState, message) {
332
- const { type, taskId, result, error, performance } = message || {};
336
+ const { type, taskId, result, error, performance, contextUpdates, logs } = message || {};
333
337
 
334
338
  if (type === 'result') {
335
339
  this.timeoutManager.cancelTimeout(taskId);
@@ -339,8 +343,18 @@ class ChildProcessPool {
339
343
  this.callbacks.delete(taskId);
340
344
  this.recycleWorker(workerState);
341
345
 
342
- this.serializer.restoreBuffers(result).then(restoredResult => {
343
- callback(null, { result: restoredResult, performance: performance || null });
346
+ const restoreResult = this.serializer.restoreBuffers(result);
347
+ const restoreContext = contextUpdates
348
+ ? this.serializer.restoreBuffers(contextUpdates)
349
+ : Promise.resolve(contextUpdates);
350
+
351
+ Promise.all([restoreResult, restoreContext]).then(([restoredResult, restoredContext]) => {
352
+ callback(null, {
353
+ result: restoredResult,
354
+ performance: performance || null,
355
+ contextUpdates: restoredContext || null,
356
+ logs: Array.isArray(logs) ? logs : null
357
+ });
344
358
  }).catch(restoreErr => {
345
359
  callback(restoreErr instanceof Error ? restoreErr : new Error(String(restoreErr)), null);
346
360
  });
@@ -362,7 +376,20 @@ class ChildProcessPool {
362
376
  if (error && error.name) {
363
377
  err.name = error.name;
364
378
  }
365
- callback(err, null);
379
+
380
+ if (contextUpdates) {
381
+ this.serializer.restoreBuffers(contextUpdates).then(restoredContext => {
382
+ err.contextUpdates = restoredContext;
383
+ err.logs = Array.isArray(logs) ? logs : null;
384
+ callback(err, null);
385
+ }).catch(() => {
386
+ err.logs = Array.isArray(logs) ? logs : null;
387
+ callback(err, null);
388
+ });
389
+ } else {
390
+ err.logs = Array.isArray(logs) ? logs : null;
391
+ callback(err, null);
392
+ }
366
393
  }
367
394
 
368
395
  this.recycleWorker(workerState);