@rosepetal/node-red-contrib-async-function 1.0.0 → 1.0.1

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.
@@ -107,6 +107,14 @@ class WorkerPool {
107
107
  clearTimeout(readyTimeout);
108
108
  workerState.state = WorkerState.IDLE;
109
109
  this.workers.push(workerState);
110
+
111
+ // Log module loading failures so users can diagnose issues
112
+ if (msg.failedModules && msg.failedModules.length > 0) {
113
+ for (const failed of msg.failedModules) {
114
+ console.warn(`[async-function] Worker failed to load module "${failed.module}" (${failed.var}): ${failed.error}`);
115
+ }
116
+ }
117
+
110
118
  resolve(workerState);
111
119
  }
112
120
  });
@@ -7,12 +7,17 @@
7
7
  */
8
8
 
9
9
  const { parentPort, workerData } = require('worker_threads');
10
+ const { AsyncLocalStorage } = require('async_hooks');
10
11
  const { SharedMemoryManager } = require('./shared-memory-manager');
11
12
  const { AsyncMessageSerializer } = require('./message-serializer');
12
13
 
13
14
  // Track worker state
14
15
  let isTerminating = false;
15
16
 
17
+ // AsyncLocalStorage for tracking task context across async boundaries
18
+ // This ensures unhandled rejections can be attributed to the correct task
19
+ const taskContext = new AsyncLocalStorage();
20
+
16
21
  function hrtimeDiffToMs(start) {
17
22
  if (typeof start !== 'bigint') {
18
23
  return 0;
@@ -58,6 +63,7 @@ function setCachedFunction(cacheKey, fn) {
58
63
  const loadedModules = {};
59
64
  const moduleVars = [];
60
65
  const moduleValues = [];
66
+ const failedModules = []; // Track modules that failed to load
61
67
 
62
68
  // Add Node-RED user directory to module search path for external modules
63
69
  if (workerData && workerData.nodeRedUserDir) {
@@ -77,6 +83,7 @@ if (workerData && workerData.libs && Array.isArray(workerData.libs)) {
77
83
  moduleValues.push(loadedModules[lib.var]);
78
84
  } catch (err) {
79
85
  console.error(`[async-function] Failed to load module ${lib.module}: ${err.message}`);
86
+ failedModules.push({ module: lib.module, var: lib.var, error: err.message });
80
87
  }
81
88
  }
82
89
  }
@@ -96,55 +103,60 @@ if (parentPort) {
96
103
 
97
104
  // Handle different message types
98
105
  if (type === 'execute') {
99
- try {
100
- // Restore + execute user code
101
- const restoreStart = process.hrtime.bigint();
102
- const restoredMsg = await serializer.restoreBuffers(msg);
103
- const transferToPythonMs = hrtimeDiffToMs(restoreStart);
104
-
105
- // Cache key includes code + module vars to handle different module configs
106
- const cacheKey = code + '|' + moduleVars.join(',');
107
- let userFunction = getCachedFunction(cacheKey);
108
- if (!userFunction) {
109
- // Create function with msg + all module variables as parameters
110
- userFunction = new AsyncFunction('msg', ...moduleVars, code);
111
- setCachedFunction(cacheKey, userFunction);
112
- }
113
-
114
- const execStart = process.hrtime.bigint();
115
- // Execute with msg and all loaded module values
116
- const rawResult = await userFunction(restoredMsg, ...moduleValues);
117
- const executionMs = hrtimeDiffToMs(execStart);
118
-
119
- // Offload buffers in the result (large Buffers -> shared memory descriptors)
120
- const encodeStart = process.hrtime.bigint();
121
- const encodedResult = await serializer.sanitizeMessage(rawResult, null, taskId);
122
- const transferToJsMs = hrtimeDiffToMs(encodeStart);
123
-
124
- // Send result back to main thread
125
- parentPort.postMessage({
126
- type: 'result',
127
- taskId,
128
- result: encodedResult,
129
- performance: {
130
- transfer_to_python_ms: transferToPythonMs,
131
- execution_ms: executionMs,
132
- transfer_to_js_ms: transferToJsMs
106
+ // Run task within AsyncLocalStorage context for proper error attribution
107
+ // This ensures unhandled rejections from fire-and-forget promises
108
+ // can be traced back to the correct task
109
+ taskContext.run({ taskId }, async () => {
110
+ try {
111
+ // Restore + execute user code
112
+ const restoreStart = process.hrtime.bigint();
113
+ const restoredMsg = await serializer.restoreBuffers(msg);
114
+ const transferToPythonMs = hrtimeDiffToMs(restoreStart);
115
+
116
+ // Cache key includes code + module vars to handle different module configs
117
+ const cacheKey = code + '|' + moduleVars.join(',');
118
+ let userFunction = getCachedFunction(cacheKey);
119
+ if (!userFunction) {
120
+ // Create function with msg + all module variables as parameters
121
+ userFunction = new AsyncFunction('msg', ...moduleVars, code);
122
+ setCachedFunction(cacheKey, userFunction);
133
123
  }
134
- });
135
124
 
136
- } catch (err) {
137
- // Send error back to main thread
138
- parentPort.postMessage({
139
- type: 'error',
140
- taskId,
141
- error: {
142
- message: err.message,
143
- stack: err.stack,
144
- name: err.name
145
- }
146
- });
147
- }
125
+ const execStart = process.hrtime.bigint();
126
+ // Execute with msg and all loaded module values
127
+ const rawResult = await userFunction(restoredMsg, ...moduleValues);
128
+ const executionMs = hrtimeDiffToMs(execStart);
129
+
130
+ // Offload buffers in the result (large Buffers -> shared memory descriptors)
131
+ const encodeStart = process.hrtime.bigint();
132
+ const encodedResult = await serializer.sanitizeMessage(rawResult, null, taskId);
133
+ const transferToJsMs = hrtimeDiffToMs(encodeStart);
134
+
135
+ // Send result back to main thread
136
+ parentPort.postMessage({
137
+ type: 'result',
138
+ taskId,
139
+ result: encodedResult,
140
+ performance: {
141
+ transfer_to_python_ms: transferToPythonMs,
142
+ execution_ms: executionMs,
143
+ transfer_to_js_ms: transferToJsMs
144
+ }
145
+ });
146
+
147
+ } catch (err) {
148
+ // Send error back to main thread
149
+ parentPort.postMessage({
150
+ type: 'error',
151
+ taskId,
152
+ error: {
153
+ message: err.message,
154
+ stack: err.stack,
155
+ name: err.name
156
+ }
157
+ });
158
+ }
159
+ });
148
160
  } else if (type === 'terminate') {
149
161
  // Graceful termination requested
150
162
  isTerminating = true;
@@ -156,12 +168,13 @@ if (parentPort) {
156
168
  }
157
169
  });
158
170
 
159
- // Handle errors
171
+ // Handle errors - use AsyncLocalStorage to get taskId for proper error attribution
160
172
  process.on('uncaughtException', (err) => {
161
173
  if (!isTerminating) {
174
+ const store = taskContext.getStore();
162
175
  parentPort.postMessage({
163
176
  type: 'error',
164
- taskId: null,
177
+ taskId: store?.taskId ?? null,
165
178
  error: {
166
179
  message: `Uncaught exception: ${err.message}`,
167
180
  stack: err.stack,
@@ -173,9 +186,10 @@ if (parentPort) {
173
186
 
174
187
  process.on('unhandledRejection', (reason, _promise) => {
175
188
  if (!isTerminating) {
189
+ const store = taskContext.getStore();
176
190
  parentPort.postMessage({
177
191
  type: 'error',
178
- taskId: null,
192
+ taskId: store?.taskId ?? null,
179
193
  error: {
180
194
  message: `Unhandled rejection: ${reason}`,
181
195
  stack: reason?.stack || '',
@@ -185,8 +199,9 @@ if (parentPort) {
185
199
  }
186
200
  });
187
201
 
188
- // Signal ready
202
+ // Signal ready (include any module loading failures)
189
203
  parentPort.postMessage({
190
- type: 'ready'
204
+ type: 'ready',
205
+ failedModules: failedModules.length > 0 ? failedModules : undefined
191
206
  });
192
207
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rosepetal/node-red-contrib-async-function",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A Node-RED function node that runs code in worker threads to keep your flows responsive",
5
5
  "repository": {
6
6
  "type": "git",