@rosepetal/node-red-contrib-async-function 1.0.0
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/LICENSE +13 -0
- package/README.md +213 -0
- package/assets/example.png +0 -0
- package/nodes/async-function.html +600 -0
- package/nodes/async-function.js +351 -0
- package/nodes/lib/message-serializer.js +407 -0
- package/nodes/lib/module-installer.js +105 -0
- package/nodes/lib/shared-memory-manager.js +311 -0
- package/nodes/lib/timeout-manager.js +139 -0
- package/nodes/lib/worker-pool.js +533 -0
- package/nodes/lib/worker-script.js +192 -0
- package/package.json +41 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async Message Serializer
|
|
3
|
+
*
|
|
4
|
+
* Fast message cloning for worker thread communication.
|
|
5
|
+
* Matches the shared-memory + msg-copy semantics used by the python executor "hot mode":
|
|
6
|
+
* - Buffers (and typed arrays) can be offloaded to shared memory with descriptors
|
|
7
|
+
* - Base64 fallback for shared-memory failures
|
|
8
|
+
* - Circular references preserved via WeakMap
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const SHARED_SENTINEL_KEY = '__rosepetal_shm_path__';
|
|
12
|
+
const SHARED_BASE64_KEY = '__rosepetal_base64__';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Async Message Serializer Class
|
|
16
|
+
* Handles message cloning with shared memory offloading for large buffers
|
|
17
|
+
*/
|
|
18
|
+
class AsyncMessageSerializer {
|
|
19
|
+
/**
|
|
20
|
+
* Create an async message serializer
|
|
21
|
+
* @param {SharedMemoryManager} sharedMemoryManager - Shared memory manager instance
|
|
22
|
+
* @param {object} options - Configuration options
|
|
23
|
+
*/
|
|
24
|
+
constructor(sharedMemoryManager, options = {}) {
|
|
25
|
+
this.shmManager = sharedMemoryManager;
|
|
26
|
+
this.maxDepth = options.maxDepth || 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Main entry point - sanitize message for worker thread communication
|
|
31
|
+
* @param {object} msg - Message object to sanitize
|
|
32
|
+
* @param {object} node - Node-RED node instance (for warnings)
|
|
33
|
+
* @param {number|string} taskId - Task identifier for shared memory tracking
|
|
34
|
+
* @returns {Promise<object>} Sanitized message object
|
|
35
|
+
*/
|
|
36
|
+
async sanitizeMessage(msg, node, taskId) {
|
|
37
|
+
if (!msg || typeof msg !== 'object') {
|
|
38
|
+
return msg;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const seen = new WeakMap();
|
|
42
|
+
const bufferIndex = { value: 0 }; // Mutable counter for buffer indexing
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
return await this.cloneValue(msg, seen, bufferIndex, taskId, 0);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (node) {
|
|
48
|
+
node.warn(`Message cloning failed: ${err.message}, creating minimal message`);
|
|
49
|
+
}
|
|
50
|
+
return this.createMinimalMessage(msg);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Recursively clone a value with async operation and shared memory offloading
|
|
56
|
+
* @param {*} value - Value to clone
|
|
57
|
+
* @param {WeakMap} seen - Circular reference tracker
|
|
58
|
+
* @param {number} depth - Current recursion depth
|
|
59
|
+
* @param {object} bufferIndex - Mutable buffer index counter
|
|
60
|
+
* @param {number|string} taskId - Task identifier
|
|
61
|
+
* @param {object} node - Node-RED node instance
|
|
62
|
+
* @param {string} path - Current property path (for warnings)
|
|
63
|
+
* @returns {Promise<*>} Cloned value
|
|
64
|
+
*/
|
|
65
|
+
async cloneValue(value, seen, bufferIndex, taskId, depth) {
|
|
66
|
+
// Optional depth guard (disabled by default for speed)
|
|
67
|
+
if (this.maxDepth > 0 && depth > this.maxDepth) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (value === null || value === undefined) {
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Handle primitives
|
|
76
|
+
const type = typeof value;
|
|
77
|
+
if (type === 'string' || type === 'number' || type === 'boolean' || type === 'bigint') {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Drop non-cloneable types
|
|
82
|
+
if (type === 'function' || type === 'symbol') {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Buffers + typed arrays: offload to shared memory when above threshold
|
|
87
|
+
if (Buffer.isBuffer(value)) {
|
|
88
|
+
return await this.shmManager.writeBuffer(value, taskId, bufferIndex.value++);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
|
|
92
|
+
const asBuffer = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
93
|
+
return await this.shmManager.writeBuffer(asBuffer, taskId, bufferIndex.value++);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (type === 'object') {
|
|
97
|
+
// Preserve circular references
|
|
98
|
+
if (seen.has(value)) {
|
|
99
|
+
return seen.get(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const clone = Array.isArray(value) ? [] : {};
|
|
103
|
+
seen.set(value, clone);
|
|
104
|
+
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
for (let i = 0; i < value.length; i++) {
|
|
107
|
+
const clonedItem = await this.cloneValue(value[i], seen, bufferIndex, taskId, depth + 1);
|
|
108
|
+
clone[i] = clonedItem === undefined ? null : clonedItem;
|
|
109
|
+
}
|
|
110
|
+
return clone;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const keys = Object.keys(value);
|
|
114
|
+
for (let i = 0; i < keys.length; i++) {
|
|
115
|
+
const key = keys[i];
|
|
116
|
+
const clonedVal = await this.cloneValue(value[key], seen, bufferIndex, taskId, depth + 1);
|
|
117
|
+
if (clonedVal !== undefined) {
|
|
118
|
+
clone[key] = clonedVal;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return clone;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Restore buffers from shared memory descriptors (worker-side)
|
|
130
|
+
* @param {*} value - Value to restore (may contain descriptors)
|
|
131
|
+
* @returns {Promise<*>} Value with buffers restored
|
|
132
|
+
*/
|
|
133
|
+
async restoreBuffers(value) {
|
|
134
|
+
const seen = new WeakMap();
|
|
135
|
+
return this.restoreValue(value, seen);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async restoreValue(value, seen) {
|
|
139
|
+
// Handle null/undefined
|
|
140
|
+
if (value === null || value === undefined) {
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Handle primitives
|
|
145
|
+
const type = typeof value;
|
|
146
|
+
if (type === 'string' || type === 'number' || type === 'boolean' || type === 'bigint') {
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Handle buffers (already restored)
|
|
151
|
+
if (Buffer.isBuffer(value)) {
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle arrays
|
|
156
|
+
if (Array.isArray(value)) {
|
|
157
|
+
if (seen.has(value)) {
|
|
158
|
+
return seen.get(value);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const result = new Array(value.length);
|
|
162
|
+
seen.set(value, result);
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < value.length; i++) {
|
|
165
|
+
result[i] = await this.restoreValue(value[i], seen);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Handle objects
|
|
172
|
+
if (type === 'object') {
|
|
173
|
+
// Shared memory descriptor
|
|
174
|
+
if (Object.prototype.hasOwnProperty.call(value, SHARED_SENTINEL_KEY)) {
|
|
175
|
+
try {
|
|
176
|
+
return await this.shmManager.readBuffer(value, { deleteAfterRead: true });
|
|
177
|
+
} catch (_err) {
|
|
178
|
+
return Buffer.alloc(0);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Base64 fallback
|
|
183
|
+
if (Object.prototype.hasOwnProperty.call(value, SHARED_BASE64_KEY)) {
|
|
184
|
+
try {
|
|
185
|
+
return Buffer.from(value[SHARED_BASE64_KEY] || '', 'base64');
|
|
186
|
+
} catch (_err) {
|
|
187
|
+
return Buffer.alloc(0);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (seen.has(value)) {
|
|
192
|
+
return seen.get(value);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const result = {};
|
|
196
|
+
seen.set(value, result);
|
|
197
|
+
|
|
198
|
+
const entries = Object.entries(value);
|
|
199
|
+
for (let i = 0; i < entries.length; i++) {
|
|
200
|
+
const [key, val] = entries[i];
|
|
201
|
+
result[key] = await this.restoreValue(val, seen);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create a minimal message for error cases
|
|
212
|
+
* Preserves only essential properties
|
|
213
|
+
* @param {object} msg - Original message
|
|
214
|
+
* @returns {object} Minimal message
|
|
215
|
+
*/
|
|
216
|
+
createMinimalMessage(msg) {
|
|
217
|
+
return {
|
|
218
|
+
_msgid: msg._msgid || '',
|
|
219
|
+
topic: msg.topic || '',
|
|
220
|
+
payload: msg.payload !== undefined ? msg.payload : null
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Legacy synchronous functions for backward compatibility
|
|
227
|
+
* (used in tests or cases where SharedMemoryManager is not available)
|
|
228
|
+
*/
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Check if a value is serializable for worker thread communication
|
|
232
|
+
* @param {*} value - Value to check
|
|
233
|
+
* @returns {boolean} True if serializable
|
|
234
|
+
*/
|
|
235
|
+
function isSerializable(value) {
|
|
236
|
+
if (value === null || value === undefined) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const type = typeof value;
|
|
241
|
+
|
|
242
|
+
// Primitive types are serializable
|
|
243
|
+
if (type === 'string' || type === 'number' || type === 'boolean') {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Functions are not serializable
|
|
248
|
+
if (type === 'function') {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Symbols are not serializable
|
|
253
|
+
if (type === 'symbol') {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Try to clone the value
|
|
258
|
+
try {
|
|
259
|
+
// Use structuredClone if available (Node.js 17+)
|
|
260
|
+
if (typeof structuredClone !== 'undefined') {
|
|
261
|
+
structuredClone(value);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Fallback: try JSON serialization
|
|
266
|
+
JSON.stringify(value);
|
|
267
|
+
return true;
|
|
268
|
+
} catch (err) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Legacy synchronous sanitize function (for backward compatibility)
|
|
275
|
+
* NOTE: This is kept for tests but should not be used in production
|
|
276
|
+
* Use AsyncMessageSerializer instead
|
|
277
|
+
*
|
|
278
|
+
* @param {object} msg - Message object to sanitize
|
|
279
|
+
* @param {object} node - Node-RED node instance (for warnings)
|
|
280
|
+
* @returns {object} Sanitized message object
|
|
281
|
+
*/
|
|
282
|
+
function sanitizeMessage(msg, node) {
|
|
283
|
+
if (!msg || typeof msg !== 'object') {
|
|
284
|
+
return msg;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
// Try structuredClone first (Node.js 17+)
|
|
289
|
+
if (typeof structuredClone !== 'undefined') {
|
|
290
|
+
return structuredClone(msg);
|
|
291
|
+
}
|
|
292
|
+
} catch (err) {
|
|
293
|
+
// If structuredClone fails, fall through to manual filtering
|
|
294
|
+
if (node) {
|
|
295
|
+
node.warn('Message contains non-serializable data, filtering...');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Manual filtering for older Node.js versions or complex cases
|
|
300
|
+
const cloned = {};
|
|
301
|
+
const seen = new WeakSet();
|
|
302
|
+
|
|
303
|
+
function cloneValue(value, path) {
|
|
304
|
+
// Handle null/undefined
|
|
305
|
+
if (value === null || value === undefined) {
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Handle primitives
|
|
310
|
+
const type = typeof value;
|
|
311
|
+
if (type === 'string' || type === 'number' || type === 'boolean') {
|
|
312
|
+
return value;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Skip functions
|
|
316
|
+
if (type === 'function') {
|
|
317
|
+
if (node) {
|
|
318
|
+
node.warn(`Property '${path}' is a function and will be omitted`);
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Skip symbols
|
|
324
|
+
if (type === 'symbol') {
|
|
325
|
+
if (node) {
|
|
326
|
+
node.warn(`Property '${path}' is a symbol and will be omitted`);
|
|
327
|
+
}
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Handle dates
|
|
332
|
+
if (value instanceof Date) {
|
|
333
|
+
return new Date(value);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Handle buffers
|
|
337
|
+
if (Buffer.isBuffer(value)) {
|
|
338
|
+
return Buffer.from(value);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Handle arrays
|
|
342
|
+
if (Array.isArray(value)) {
|
|
343
|
+
return value.map((item, index) => cloneValue(item, `${path}[${index}]`));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Handle objects
|
|
347
|
+
if (type === 'object') {
|
|
348
|
+
// Check for circular reference
|
|
349
|
+
if (seen.has(value)) {
|
|
350
|
+
if (node) {
|
|
351
|
+
node.warn(`Circular reference detected at '${path}'`);
|
|
352
|
+
}
|
|
353
|
+
return '[Circular]';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
seen.add(value);
|
|
357
|
+
|
|
358
|
+
const result = {};
|
|
359
|
+
for (const [key, val] of Object.entries(value)) {
|
|
360
|
+
const clonedVal = cloneValue(val, path ? `${path}.${key}` : key);
|
|
361
|
+
if (clonedVal !== undefined) {
|
|
362
|
+
result[key] = clonedVal;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Unknown type
|
|
370
|
+
if (node) {
|
|
371
|
+
node.warn(`Property '${path}' has unknown type '${type}' and will be omitted`);
|
|
372
|
+
}
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Clone each property
|
|
377
|
+
for (const [key, value] of Object.entries(msg)) {
|
|
378
|
+
const clonedValue = cloneValue(value, key);
|
|
379
|
+
if (clonedValue !== undefined) {
|
|
380
|
+
cloned[key] = clonedValue;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return cloned;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Create a minimal message for error cases
|
|
389
|
+
* Preserves only essential properties
|
|
390
|
+
*
|
|
391
|
+
* @param {object} msg - Original message
|
|
392
|
+
* @returns {object} Minimal message
|
|
393
|
+
*/
|
|
394
|
+
function createMinimalMessage(msg) {
|
|
395
|
+
return {
|
|
396
|
+
_msgid: msg._msgid || '',
|
|
397
|
+
topic: msg.topic || '',
|
|
398
|
+
payload: msg.payload !== undefined ? msg.payload : null
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
module.exports = {
|
|
403
|
+
AsyncMessageSerializer,
|
|
404
|
+
isSerializable,
|
|
405
|
+
sanitizeMessage,
|
|
406
|
+
createMinimalMessage
|
|
407
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module Installer
|
|
3
|
+
*
|
|
4
|
+
* Handles automatic installation of npm modules for the async-function node.
|
|
5
|
+
* Modules are installed in the Node-RED user directory (~/.node-red).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the Node-RED user directory
|
|
15
|
+
* @returns {string} Path to Node-RED user directory
|
|
16
|
+
*/
|
|
17
|
+
function getNodeRedUserDir() {
|
|
18
|
+
// Check for explicit NODE_RED_HOME environment variable
|
|
19
|
+
if (process.env.NODE_RED_HOME) {
|
|
20
|
+
return process.env.NODE_RED_HOME;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Default to ~/.node-red
|
|
24
|
+
const defaultDir = path.join(os.homedir(), '.node-red');
|
|
25
|
+
|
|
26
|
+
// Verify it exists
|
|
27
|
+
if (fs.existsSync(defaultDir)) {
|
|
28
|
+
return defaultDir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Fallback to home directory if .node-red doesn't exist
|
|
32
|
+
return os.homedir();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Install an npm module in the Node-RED user directory
|
|
37
|
+
* @param {string} moduleName - Name of the module to install
|
|
38
|
+
* @returns {boolean} True if installation succeeded
|
|
39
|
+
*/
|
|
40
|
+
function installModule(moduleName) {
|
|
41
|
+
if (!moduleName || typeof moduleName !== 'string') {
|
|
42
|
+
console.error('[async-function] Invalid module name');
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Sanitize module name to prevent command injection
|
|
47
|
+
const sanitizedName = moduleName.trim();
|
|
48
|
+
if (!/^(@[\w-]+\/)?[\w.-]+(@[\w.-]+)?$/.test(sanitizedName)) {
|
|
49
|
+
console.error(`[async-function] Invalid module name format: ${sanitizedName}`);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const userDir = getNodeRedUserDir();
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
console.log(`[async-function] Installing module: ${sanitizedName} in ${userDir}`);
|
|
57
|
+
|
|
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'
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
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
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`[async-function] Successfully installed: ${sanitizedName}`);
|
|
79
|
+
return true;
|
|
80
|
+
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error(`[async-function] Failed to install ${sanitizedName}: ${err.message}`);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if a module is available
|
|
89
|
+
* @param {string} moduleName - Name of the module to check
|
|
90
|
+
* @returns {boolean} True if module can be resolved
|
|
91
|
+
*/
|
|
92
|
+
function isModuleAvailable(moduleName) {
|
|
93
|
+
try {
|
|
94
|
+
require.resolve(moduleName);
|
|
95
|
+
return true;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
installModule,
|
|
103
|
+
isModuleAvailable,
|
|
104
|
+
getNodeRedUserDir
|
|
105
|
+
};
|