@rosepetal/node-red-contrib-async-function 1.0.0 → 1.0.2
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 +4 -3
- package/nodes/async-function.html +65 -12
- package/nodes/async-function.js +132 -48
- package/nodes/lib/child-process-pool.js +610 -0
- package/nodes/lib/child-process-script.js +264 -0
- package/nodes/lib/message-serializer.js +1 -1
- package/nodes/lib/worker-pool.js +12 -0
- package/nodes/lib/worker-script.js +132 -69
- package/package.json +1 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child Process Script
|
|
3
|
+
*
|
|
4
|
+
* Executes user-provided JavaScript code in a child process.
|
|
5
|
+
* Handles message communication with the parent process.
|
|
6
|
+
* Restores buffers from shared memory before execution.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
10
|
+
const { createRequire, builtinModules } = require('module');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const url = require('url');
|
|
13
|
+
const { SharedMemoryManager } = require('./shared-memory-manager');
|
|
14
|
+
const { AsyncMessageSerializer } = require('./message-serializer');
|
|
15
|
+
|
|
16
|
+
// Track worker state
|
|
17
|
+
let isTerminating = false;
|
|
18
|
+
let isInitialized = false;
|
|
19
|
+
|
|
20
|
+
// AsyncLocalStorage for tracking task context across async boundaries
|
|
21
|
+
const taskContext = new AsyncLocalStorage();
|
|
22
|
+
|
|
23
|
+
function hrtimeDiffToMs(start) {
|
|
24
|
+
if (typeof start !== 'bigint') {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
const diff = process.hrtime.bigint() - start;
|
|
28
|
+
return Number(diff) / 1e6;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Shared memory manager and serializer are initialized after init message
|
|
32
|
+
let shmManager = null;
|
|
33
|
+
let serializer = null;
|
|
34
|
+
|
|
35
|
+
// Cache compiled user code per worker for hot-path performance
|
|
36
|
+
const AsyncFunction = (async function() {}).constructor;
|
|
37
|
+
const MAX_CACHE_SIZE = 100;
|
|
38
|
+
const compiledCodeCache = new Map();
|
|
39
|
+
|
|
40
|
+
function getCachedFunction(cacheKey) {
|
|
41
|
+
const fn = compiledCodeCache.get(cacheKey);
|
|
42
|
+
if (fn) {
|
|
43
|
+
compiledCodeCache.delete(cacheKey);
|
|
44
|
+
compiledCodeCache.set(cacheKey, fn);
|
|
45
|
+
}
|
|
46
|
+
return fn;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function setCachedFunction(cacheKey, fn) {
|
|
50
|
+
if (compiledCodeCache.size >= MAX_CACHE_SIZE) {
|
|
51
|
+
const oldestKey = compiledCodeCache.keys().next().value;
|
|
52
|
+
compiledCodeCache.delete(oldestKey);
|
|
53
|
+
}
|
|
54
|
+
compiledCodeCache.set(cacheKey, fn);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Module loading setup
|
|
58
|
+
let baseRequire = require;
|
|
59
|
+
const loadedModules = {};
|
|
60
|
+
const moduleVars = [];
|
|
61
|
+
const moduleValues = [];
|
|
62
|
+
const failedModules = [];
|
|
63
|
+
|
|
64
|
+
function configureBaseRequire(nodeRedUserDir) {
|
|
65
|
+
if (nodeRedUserDir) {
|
|
66
|
+
baseRequire = createRequire(path.join(nodeRedUserDir, 'package.json'));
|
|
67
|
+
} else {
|
|
68
|
+
baseRequire = require;
|
|
69
|
+
}
|
|
70
|
+
global.require = baseRequire;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseModuleSpec(spec) {
|
|
74
|
+
const match = /((?:@[^/]+\/)?[^/@]+)(\/[^/@]+)?(?:@([\s\S]+))?/.exec(spec);
|
|
75
|
+
if (!match) {
|
|
76
|
+
return { spec, module: spec, subpath: '', builtin: false };
|
|
77
|
+
}
|
|
78
|
+
const moduleName = match[1];
|
|
79
|
+
const subpath = match[2] || '';
|
|
80
|
+
let builtinName = moduleName;
|
|
81
|
+
if (builtinName.startsWith('node:')) {
|
|
82
|
+
builtinName = builtinName.slice(5);
|
|
83
|
+
}
|
|
84
|
+
const builtin = builtinModules.includes(builtinName);
|
|
85
|
+
return { spec, module: moduleName, subpath, builtin };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function loadModule(spec) {
|
|
89
|
+
const parsed = parseModuleSpec(spec);
|
|
90
|
+
if (parsed.builtin) {
|
|
91
|
+
return baseRequire(parsed.module + parsed.subpath);
|
|
92
|
+
}
|
|
93
|
+
const resolvedPath = baseRequire.resolve(spec);
|
|
94
|
+
const moduleUrl = url.pathToFileURL(resolvedPath);
|
|
95
|
+
const imported = await import(moduleUrl);
|
|
96
|
+
return imported.default || imported;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function loadConfiguredModules(libs) {
|
|
100
|
+
if (!Array.isArray(libs)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
for (const lib of libs) {
|
|
104
|
+
if (!lib || !lib.module || !lib.var) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
loadedModules[lib.var] = await loadModule(lib.module);
|
|
109
|
+
moduleVars.push(lib.var);
|
|
110
|
+
moduleValues.push(loadedModules[lib.var]);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(`[async-function] Failed to load module ${lib.module}: ${err.message}`);
|
|
113
|
+
failedModules.push({ module: lib.module, var: lib.var, error: err.message });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sendMessage(payload) {
|
|
119
|
+
if (typeof process.send === 'function') {
|
|
120
|
+
process.send(payload);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sendError(taskId, err) {
|
|
125
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
126
|
+
sendMessage({
|
|
127
|
+
type: 'error',
|
|
128
|
+
taskId,
|
|
129
|
+
error: {
|
|
130
|
+
message: error.message,
|
|
131
|
+
stack: error.stack || '',
|
|
132
|
+
name: error.name || 'Error'
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function initializeWorker(initData) {
|
|
138
|
+
const nodeRedUserDir = initData && initData.nodeRedUserDir ? initData.nodeRedUserDir : null;
|
|
139
|
+
configureBaseRequire(nodeRedUserDir);
|
|
140
|
+
|
|
141
|
+
const threshold = initData && typeof initData.shmThreshold === 'number' ? initData.shmThreshold : undefined;
|
|
142
|
+
shmManager = new SharedMemoryManager({
|
|
143
|
+
threshold,
|
|
144
|
+
trackAttachments: false,
|
|
145
|
+
cleanupOrphanedFiles: false
|
|
146
|
+
});
|
|
147
|
+
serializer = new AsyncMessageSerializer(shmManager);
|
|
148
|
+
|
|
149
|
+
await loadConfiguredModules(initData ? initData.libs : []);
|
|
150
|
+
isInitialized = true;
|
|
151
|
+
|
|
152
|
+
sendMessage({
|
|
153
|
+
type: 'ready',
|
|
154
|
+
failedModules: failedModules.length > 0 ? failedModules : undefined
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
process.on('message', async (data) => {
|
|
159
|
+
if (!data || typeof data !== 'object') {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (data.type === 'init') {
|
|
164
|
+
try {
|
|
165
|
+
await initializeWorker(data);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
failedModules.push({ module: '(init)', var: null, error: err.message });
|
|
168
|
+
sendMessage({
|
|
169
|
+
type: 'ready',
|
|
170
|
+
failedModules
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (isTerminating) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!isInitialized) {
|
|
181
|
+
if (data.type === 'execute') {
|
|
182
|
+
sendError(data.taskId, new Error('Worker not initialized'));
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const { type, taskId, code, msg } = data;
|
|
188
|
+
|
|
189
|
+
if (type === 'execute') {
|
|
190
|
+
taskContext.run({ taskId }, async () => {
|
|
191
|
+
try {
|
|
192
|
+
const restoreStart = process.hrtime.bigint();
|
|
193
|
+
const restoredMsg = await serializer.restoreBuffers(msg);
|
|
194
|
+
const transferToWorkerMs = hrtimeDiffToMs(restoreStart);
|
|
195
|
+
|
|
196
|
+
const cacheKey = code + '|' + moduleVars.join(',');
|
|
197
|
+
let userFunction = getCachedFunction(cacheKey);
|
|
198
|
+
if (!userFunction) {
|
|
199
|
+
userFunction = new AsyncFunction('msg', ...moduleVars, code);
|
|
200
|
+
setCachedFunction(cacheKey, userFunction);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const execStart = process.hrtime.bigint();
|
|
204
|
+
const rawResult = await userFunction(restoredMsg, ...moduleValues);
|
|
205
|
+
const executionMs = hrtimeDiffToMs(execStart);
|
|
206
|
+
|
|
207
|
+
const encodeStart = process.hrtime.bigint();
|
|
208
|
+
const encodedResult = await serializer.sanitizeMessage(rawResult, null, taskId);
|
|
209
|
+
const transferToMainMs = hrtimeDiffToMs(encodeStart);
|
|
210
|
+
|
|
211
|
+
sendMessage({
|
|
212
|
+
type: 'result',
|
|
213
|
+
taskId,
|
|
214
|
+
result: encodedResult,
|
|
215
|
+
performance: {
|
|
216
|
+
transferToWorkerMs,
|
|
217
|
+
executionMs,
|
|
218
|
+
transferToMainMs
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
} catch (err) {
|
|
222
|
+
sendError(taskId, err);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
} else if (type === 'terminate') {
|
|
226
|
+
isTerminating = true;
|
|
227
|
+
sendMessage({
|
|
228
|
+
type: 'terminated',
|
|
229
|
+
taskId
|
|
230
|
+
});
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
process.on('uncaughtException', (err) => {
|
|
236
|
+
if (!isTerminating) {
|
|
237
|
+
const store = taskContext.getStore();
|
|
238
|
+
sendMessage({
|
|
239
|
+
type: 'error',
|
|
240
|
+
taskId: store?.taskId ?? null,
|
|
241
|
+
error: {
|
|
242
|
+
message: `Uncaught exception: ${err.message}`,
|
|
243
|
+
stack: err.stack || '',
|
|
244
|
+
name: err.name || 'Error'
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
process.on('unhandledRejection', (reason) => {
|
|
251
|
+
if (!isTerminating) {
|
|
252
|
+
const store = taskContext.getStore();
|
|
253
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
254
|
+
sendMessage({
|
|
255
|
+
type: 'error',
|
|
256
|
+
taskId: store?.taskId ?? null,
|
|
257
|
+
error: {
|
|
258
|
+
message: `Unhandled rejection: ${error.message}`,
|
|
259
|
+
stack: error.stack || '',
|
|
260
|
+
name: 'UnhandledRejection'
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Async Message Serializer
|
|
3
3
|
*
|
|
4
4
|
* Fast message cloning for worker thread communication.
|
|
5
|
-
* Matches the shared-memory + msg-copy semantics used by the
|
|
5
|
+
* Matches the shared-memory + msg-copy semantics used by the legacy executor "hot mode":
|
|
6
6
|
* - Buffers (and typed arrays) can be offloaded to shared memory with descriptors
|
|
7
7
|
* - Base64 fallback for shared-memory failures
|
|
8
8
|
* - Circular references preserved via WeakMap
|
package/nodes/lib/worker-pool.js
CHANGED
|
@@ -105,6 +105,18 @@ class WorkerPool {
|
|
|
105
105
|
worker.on('message', (msg) => {
|
|
106
106
|
if (msg.type === 'ready') {
|
|
107
107
|
clearTimeout(readyTimeout);
|
|
108
|
+
if (msg.failedModules && msg.failedModules.length > 0) {
|
|
109
|
+
const detail = msg.failedModules
|
|
110
|
+
.map((failed) => `${failed.module} (${failed.var}): ${failed.error}`)
|
|
111
|
+
.join('; ');
|
|
112
|
+
const err = new Error(`Worker failed to load module(s): ${detail}`);
|
|
113
|
+
err.failedModules = msg.failedModules;
|
|
114
|
+
worker.terminate().finally(() => {
|
|
115
|
+
reject(err);
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
108
120
|
workerState.state = WorkerState.IDLE;
|
|
109
121
|
this.workers.push(workerState);
|
|
110
122
|
resolve(workerState);
|
|
@@ -7,12 +7,20 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const { parentPort, workerData } = require('worker_threads');
|
|
10
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
11
|
+
const { createRequire, builtinModules } = require('module');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const url = require('url');
|
|
10
14
|
const { SharedMemoryManager } = require('./shared-memory-manager');
|
|
11
15
|
const { AsyncMessageSerializer } = require('./message-serializer');
|
|
12
16
|
|
|
13
17
|
// Track worker state
|
|
14
18
|
let isTerminating = false;
|
|
15
19
|
|
|
20
|
+
// AsyncLocalStorage for tracking task context across async boundaries
|
|
21
|
+
// This ensures unhandled rejections can be attributed to the correct task
|
|
22
|
+
const taskContext = new AsyncLocalStorage();
|
|
23
|
+
|
|
16
24
|
function hrtimeDiffToMs(start) {
|
|
17
25
|
if (typeof start !== 'bigint') {
|
|
18
26
|
return 0;
|
|
@@ -58,26 +66,58 @@ function setCachedFunction(cacheKey, fn) {
|
|
|
58
66
|
const loadedModules = {};
|
|
59
67
|
const moduleVars = [];
|
|
60
68
|
const moduleValues = [];
|
|
69
|
+
const failedModules = []; // Track modules that failed to load
|
|
70
|
+
|
|
71
|
+
const baseRequire = (() => {
|
|
72
|
+
if (workerData && workerData.nodeRedUserDir) {
|
|
73
|
+
return createRequire(path.join(workerData.nodeRedUserDir, 'package.json'));
|
|
74
|
+
}
|
|
75
|
+
return require;
|
|
76
|
+
})();
|
|
61
77
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
if (!
|
|
67
|
-
module
|
|
78
|
+
global.require = baseRequire;
|
|
79
|
+
|
|
80
|
+
function parseModuleSpec(spec) {
|
|
81
|
+
const match = /((?:@[^/]+\/)?[^/@]+)(\/[^/@]+)?(?:@([\s\S]+))?/.exec(spec);
|
|
82
|
+
if (!match) {
|
|
83
|
+
return { spec, module: spec, subpath: '', builtin: false };
|
|
84
|
+
}
|
|
85
|
+
const moduleName = match[1];
|
|
86
|
+
const subpath = match[2] || '';
|
|
87
|
+
let builtinName = moduleName;
|
|
88
|
+
if (builtinName.startsWith('node:')) {
|
|
89
|
+
builtinName = builtinName.slice(5);
|
|
68
90
|
}
|
|
91
|
+
const builtin = builtinModules.includes(builtinName);
|
|
92
|
+
return { spec, module: moduleName, subpath, builtin };
|
|
69
93
|
}
|
|
70
94
|
|
|
71
|
-
|
|
95
|
+
async function loadModule(spec) {
|
|
96
|
+
const parsed = parseModuleSpec(spec);
|
|
97
|
+
if (parsed.builtin) {
|
|
98
|
+
return baseRequire(parsed.module + parsed.subpath);
|
|
99
|
+
}
|
|
100
|
+
const resolvedPath = baseRequire.resolve(spec);
|
|
101
|
+
const moduleUrl = url.pathToFileURL(resolvedPath);
|
|
102
|
+
const imported = await import(moduleUrl);
|
|
103
|
+
return imported.default || imported;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function loadConfiguredModules() {
|
|
107
|
+
if (!workerData || !Array.isArray(workerData.libs)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
72
110
|
for (const lib of workerData.libs) {
|
|
73
|
-
if (lib.module
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
111
|
+
if (!lib || !lib.module || !lib.var) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
loadedModules[lib.var] = await loadModule(lib.module);
|
|
116
|
+
moduleVars.push(lib.var);
|
|
117
|
+
moduleValues.push(loadedModules[lib.var]);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error(`[async-function] Failed to load module ${lib.module}: ${err.message}`);
|
|
120
|
+
failedModules.push({ module: lib.module, var: lib.var, error: err.message });
|
|
81
121
|
}
|
|
82
122
|
}
|
|
83
123
|
}
|
|
@@ -85,7 +125,13 @@ if (workerData && workerData.libs && Array.isArray(workerData.libs)) {
|
|
|
85
125
|
/**
|
|
86
126
|
* Handle incoming messages from main thread
|
|
87
127
|
*/
|
|
88
|
-
|
|
128
|
+
async function initializeWorker() {
|
|
129
|
+
await loadConfiguredModules();
|
|
130
|
+
|
|
131
|
+
if (!parentPort) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
89
135
|
parentPort.on('message', async (data) => {
|
|
90
136
|
// Ignore messages if terminating
|
|
91
137
|
if (isTerminating) {
|
|
@@ -96,55 +142,60 @@ if (parentPort) {
|
|
|
96
142
|
|
|
97
143
|
// Handle different message types
|
|
98
144
|
if (type === 'execute') {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
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
|
+
// Run task within AsyncLocalStorage context for proper error attribution
|
|
146
|
+
// This ensures unhandled rejections from fire-and-forget promises
|
|
147
|
+
// can be traced back to the correct task
|
|
148
|
+
taskContext.run({ taskId }, async () => {
|
|
149
|
+
try {
|
|
150
|
+
// Restore + execute user code
|
|
151
|
+
const restoreStart = process.hrtime.bigint();
|
|
152
|
+
const restoredMsg = await serializer.restoreBuffers(msg);
|
|
153
|
+
const transferToWorkerMs = hrtimeDiffToMs(restoreStart);
|
|
154
|
+
|
|
155
|
+
// Cache key includes code + module vars to handle different module configs
|
|
156
|
+
const cacheKey = code + '|' + moduleVars.join(',');
|
|
157
|
+
let userFunction = getCachedFunction(cacheKey);
|
|
158
|
+
if (!userFunction) {
|
|
159
|
+
// Create function with msg + all module variables as parameters
|
|
160
|
+
userFunction = new AsyncFunction('msg', ...moduleVars, code);
|
|
161
|
+
setCachedFunction(cacheKey, userFunction);
|
|
145
162
|
}
|
|
146
|
-
|
|
147
|
-
|
|
163
|
+
|
|
164
|
+
const execStart = process.hrtime.bigint();
|
|
165
|
+
// Execute with msg and all loaded module values
|
|
166
|
+
const rawResult = await userFunction(restoredMsg, ...moduleValues);
|
|
167
|
+
const executionMs = hrtimeDiffToMs(execStart);
|
|
168
|
+
|
|
169
|
+
// Offload buffers in the result (large Buffers -> shared memory descriptors)
|
|
170
|
+
const encodeStart = process.hrtime.bigint();
|
|
171
|
+
const encodedResult = await serializer.sanitizeMessage(rawResult, null, taskId);
|
|
172
|
+
const transferToMainMs = hrtimeDiffToMs(encodeStart);
|
|
173
|
+
|
|
174
|
+
// Send result back to main thread
|
|
175
|
+
parentPort.postMessage({
|
|
176
|
+
type: 'result',
|
|
177
|
+
taskId,
|
|
178
|
+
result: encodedResult,
|
|
179
|
+
performance: {
|
|
180
|
+
transferToWorkerMs,
|
|
181
|
+
executionMs,
|
|
182
|
+
transferToMainMs
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
} catch (err) {
|
|
187
|
+
// Send error back to main thread
|
|
188
|
+
parentPort.postMessage({
|
|
189
|
+
type: 'error',
|
|
190
|
+
taskId,
|
|
191
|
+
error: {
|
|
192
|
+
message: err.message,
|
|
193
|
+
stack: err.stack,
|
|
194
|
+
name: err.name
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
});
|
|
148
199
|
} else if (type === 'terminate') {
|
|
149
200
|
// Graceful termination requested
|
|
150
201
|
isTerminating = true;
|
|
@@ -156,12 +207,13 @@ if (parentPort) {
|
|
|
156
207
|
}
|
|
157
208
|
});
|
|
158
209
|
|
|
159
|
-
// Handle errors
|
|
210
|
+
// Handle errors - use AsyncLocalStorage to get taskId for proper error attribution
|
|
160
211
|
process.on('uncaughtException', (err) => {
|
|
161
212
|
if (!isTerminating) {
|
|
213
|
+
const store = taskContext.getStore();
|
|
162
214
|
parentPort.postMessage({
|
|
163
215
|
type: 'error',
|
|
164
|
-
taskId: null,
|
|
216
|
+
taskId: store?.taskId ?? null,
|
|
165
217
|
error: {
|
|
166
218
|
message: `Uncaught exception: ${err.message}`,
|
|
167
219
|
stack: err.stack,
|
|
@@ -173,9 +225,10 @@ if (parentPort) {
|
|
|
173
225
|
|
|
174
226
|
process.on('unhandledRejection', (reason, _promise) => {
|
|
175
227
|
if (!isTerminating) {
|
|
228
|
+
const store = taskContext.getStore();
|
|
176
229
|
parentPort.postMessage({
|
|
177
230
|
type: 'error',
|
|
178
|
-
taskId: null,
|
|
231
|
+
taskId: store?.taskId ?? null,
|
|
179
232
|
error: {
|
|
180
233
|
message: `Unhandled rejection: ${reason}`,
|
|
181
234
|
stack: reason?.stack || '',
|
|
@@ -185,8 +238,18 @@ if (parentPort) {
|
|
|
185
238
|
}
|
|
186
239
|
});
|
|
187
240
|
|
|
188
|
-
// Signal ready
|
|
241
|
+
// Signal ready (include any module loading failures)
|
|
189
242
|
parentPort.postMessage({
|
|
190
|
-
type: 'ready'
|
|
243
|
+
type: 'ready',
|
|
244
|
+
failedModules: failedModules.length > 0 ? failedModules : undefined
|
|
191
245
|
});
|
|
192
246
|
}
|
|
247
|
+
|
|
248
|
+
initializeWorker().catch((err) => {
|
|
249
|
+
if (parentPort) {
|
|
250
|
+
parentPort.postMessage({
|
|
251
|
+
type: 'ready',
|
|
252
|
+
failedModules: [{ module: '(init)', var: null, error: err.message }]
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
});
|
package/package.json
CHANGED