@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.
@@ -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 restoredMsg = await serializer.restoreBuffers(msg);
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 encodedResult = await serializer.sanitizeMessage(rawResult, null, taskId);
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
- sendError(taskId, err);
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} node - Node-RED node instance
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: offload to shared memory when above threshold
140
+ // Buffers + typed arrays: transfer, inline copy, or shared memory
87
141
  if (Buffer.isBuffer(value)) {
88
- return await this.shmManager.writeBuffer(value, taskId, bufferIndex.value++);
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
- return await this.shmManager.writeBuffer(asBuffer, taskId, bufferIndex.value++);
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
- return this.restoreValue(value, seen);
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
- return await this.shmManager.readBuffer(value, { deleteAfterRead: true });
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
- return Buffer.from(value[SHARED_BASE64_KEY] || '', 'base64');
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 { spawnSync } = require('child_process');
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
- if (fs.existsSync(defaultDir)) {
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
- // Use spawnSync with array arguments to prevent command injection
59
- const result = spawnSync('npm', ['install', sanitizedName], {
60
- cwd: userDir,
61
- stdio: 'pipe',
62
- timeout: 120000, // 2 minute timeout
63
- env: {
64
- ...process.env,
65
- npm_config_loglevel: 'error'
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
- if (result.error) {
70
- throw result.error;
71
- }
72
-
73
- if (result.status !== 0) {
74
- const stderr = result.stderr ? result.stderr.toString() : 'Unknown error';
75
- throw new Error(`npm install failed with code ${result.status}: ${stderr}`);
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;