aveazul 0.1.2 → 0.1.4

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
@@ -1,18 +1,10 @@
1
1
  # AveAzul
2
2
 
3
- AveAzul ("Blue Bird" in Spanish) is a Promise extension library that provides Bluebird-like utility methods built on top of native Promises. The name is a Spanish play on words referencing Bluebird.
3
+ AveAzul ("Blue Bird" in Spanish) is a Promise library that extends native Promises with Bluebird-like utility methods. Built on top of native Promises, it provides a familiar API for Node.js developers who are used to working with [Bluebird](https://github.com/petkaantonov/bluebird) ([npm](https://www.npmjs.com/package/bluebird)).
4
4
 
5
- ## Requirements
5
+ This library helps migrate legacy code that uses Bluebird specific APIs to native Promises with minimal changes. It aims to provide a drop-in replacement that maintains compatibility while leveraging native Promises.
6
6
 
7
- - Node.js 16 or higher
8
-
9
- ## Features
10
-
11
- - Extends native Promise with Bluebird-like utility methods
12
- - Built on top of the efficient `xaa` library for async operations
13
- - Zero external runtime dependencies (other than `xaa`)
14
- - TypeScript-friendly
15
- - Familiar Bluebird-style API
7
+ Do you prefer Bluebird's API with native Promises? AveAzul gives you both - familiar Bluebird methods built on native Promise.
16
8
 
17
9
  ## Installation
18
10
 
@@ -23,65 +15,109 @@ npm install aveazul
23
15
  ## Usage
24
16
 
25
17
  ```javascript
26
- const AveAzul = require('aveazul');
27
-
28
- // Create a new promise
29
- const promise = new AveAzul((resolve, reject) => {
30
- setTimeout(() => resolve('result'), 1000);
31
- });
18
+ const AveAzul = require("aveazul");
32
19
 
33
- // Use Bluebird-style methods
34
- promise
35
- .tap(value => console.log('Got:', value))
36
- .delay(500)
37
- .then(value => console.log('After delay:', value));
20
+ // Basic Promise usage
21
+ const promise = new AveAzul((resolve) => resolve(42));
22
+ promise.then((value) => console.log(value)); // 42
38
23
 
39
- // Static helpers
40
- AveAzul.delay(1000, 'hello')
41
- .then(value => console.log(value));
42
-
43
- // Array operations
24
+ // Utility methods
44
25
  AveAzul.resolve([1, 2, 3])
45
- .map(x => x * 2)
46
- .filter(x => x > 4)
47
- .then(result => console.log(result)); // [6]
26
+ .map((x) => x * 2)
27
+ .filter((x) => x > 2)
28
+ .then((result) => console.log(result)); // [4, 6]
29
+
30
+ // Promisify callback-style functions
31
+ const fs = require("fs");
32
+ const readFile = AveAzul.promisify(fs.readFile);
33
+ readFile("file.txt").then((content) => console.log(content));
34
+ // Properties from the original function are preserved
35
+ console.log(readFile.length); // Original function's length property
36
+
37
+ // Promisify all methods of an object
38
+ const obj = {
39
+ method(cb) {
40
+ cb(null, "result");
41
+ },
42
+ };
43
+ AveAzul.promisifyAll(obj);
44
+ obj.methodAsync().then((result) => console.log(result)); // 'result'
45
+
46
+ // Resource management with disposer and using
47
+ const getResource = () => {
48
+ return AveAzul.resolve({
49
+ data: "important data",
50
+ close: () => console.log("Resource closed!"),
51
+ }).disposer((resource) => resource.close());
52
+ };
53
+
54
+ AveAzul.using(getResource(), (resource) => {
55
+ console.log(resource.data); // "important data"
56
+ return AveAzul.resolve("operation completed");
57
+ }).then((result) => {
58
+ console.log(result); // "operation completed"
59
+ // Resource is automatically closed here, even if an error occurred
60
+ });
48
61
  ```
49
62
 
50
63
  ## API
51
64
 
52
65
  ### Instance Methods
53
66
 
54
- - `tap(fn)` - Execute side effects in a chain
67
+ - `tap(fn)` - Execute side effects and return original value
55
68
  - `filter(fn)` - Filter array elements
56
- - `map(fn)` - Map array elements
57
- - `return(value)` - Inject a value into the chain
69
+ - `map(fn)` - Transform array elements
70
+ - `return(value)` - Inject a new value
58
71
  - `each(fn)` - Iterate over array elements
59
- - `delay(ms)` - Delay execution
60
- - `timeout(ms, message?)` - Set operation timeout
61
- - `try(fn)` - Wrap sync/async functions
62
- - `props(obj)` - Handle object properties
63
- - `reduce(fn, initialValue?)` - Reduce array
72
+ - `delay(ms)` - Delay resolution
73
+ - `timeout(ms, message?)` - Reject after specified time
74
+ - `props(obj)` - Resolve object properties
75
+ - `tapCatch(fn)` - Execute side effects on rejection
76
+ - `reduce(fn, initialValue?)` - Reduce array elements
64
77
  - `throw(reason)` - Return rejected promise
65
78
  - `catchThrow(reason)` - Catch and throw new error
66
79
  - `catchReturn(value)` - Catch and return value
67
- - `get(propertyPath)` - Get nested property
80
+ - `get(propertyPath)` - Retrieve property value
81
+ - `disposer(fn)` - Create a disposer for use with AveAzul.using() for resource cleanup
68
82
 
69
83
  ### Static Methods
70
84
 
71
- - `AveAzul.delay(ms, value?)` - Create delayed promise
72
- - `AveAzul.map(array, fn)` - Map array elements
73
- - `AveAzul.try(fn)` - Wrap function execution
74
- - `AveAzul.props(obj)` - Handle object properties
75
- - `AveAzul.defer()` - Create deferred promise
76
- - `AveAzul.promisify(fn, options?)` - Promisify callback functions
77
- - `AveAzul.each(items, fn)` - Iterate over items
78
- - `AveAzul.reduce(array, fn, initialValue?)` - Reduce array
79
- - `AveAzul.throw(reason)` - Create rejected promise
85
+ - `delay(ms, value?)` - Resolve after specified time
86
+ - `map(value, fn)` - Transform array elements
87
+ - `try(fn)` - Wrap sync/async functions
88
+ - `props(obj)` - Resolve object properties
89
+ - `defer()` - Create a deferred promise
90
+ - `promisify(fn, options?)` - Convert callback-style functions to promises (preserves original function properties)
91
+ - `each(items, fn)` - Iterate over array elements
92
+ - `reduce(array, fn, initialValue?)` - Reduce array elements
93
+ - `method(fn)` - Creates a method that returns a promise resolving to the value returned by the original function
94
+ - `throw(reason)` - Return rejected promise
95
+ - `promisifyAll(target, options?)` - Convert all methods of an object/class to promises
96
+ - `using(resources, fn)` - Manage resources with automatic cleanup
97
+
98
+ ### PromisifyAll Options
99
+
100
+ - `suffix` (default: 'Async') - Suffix to append to promisified method names
101
+ - `filter` - Filter function to determine which methods to promisify
102
+ - `promisifier` - Custom function to handle promisification
103
+ - `multiArgs` (default: false) - Whether to support multiple callback arguments
104
+
105
+ ## Development
106
+
107
+ ```bash
108
+ # Install dependencies
109
+ npm install
110
+
111
+ # Run tests
112
+ npm test
113
+ npm run test:watch
114
+ npm run test:coverage
115
+ ```
80
116
 
81
117
  ## License
82
118
 
83
- Apache 2.0
119
+ Apache-2.0
84
120
 
85
- ## Dependencies
121
+ ## Author
86
122
 
87
- - [xaa](https://github.com/jchip/xaa) - Efficient async/await helpers
123
+ Joel Chen
package/lib/aveazul.js CHANGED
@@ -1,8 +1,11 @@
1
1
  "use strict";
2
2
 
3
3
  const xaa = require("xaa");
4
- const { promisify: nodePromisify } = require("node:util");
5
-
4
+ const { promisify } = require("./promisify");
5
+ const { promisifyAll } = require("./promisify-all");
6
+ const { Disposer } = require("./disposer");
7
+ const { using } = require("./using");
8
+ const { isPromise, triggerUncaughtException } = require("./util");
6
9
  /**
7
10
  * AveAzul ("Blue Bird" in Spanish) - Extended Promise class that provides Bluebird-like utility methods
8
11
  * This implementation is inspired by and provides similar APIs to the Bluebird Promise library,
@@ -28,7 +31,7 @@ class AveAzul extends Promise {
28
31
  * @returns {Promise} Promise that resolves with the original value
29
32
  */
30
33
  tap(fn) {
31
- return this.then(value => {
34
+ return this.then((value) => {
32
35
  fn(value);
33
36
  return value;
34
37
  });
@@ -41,7 +44,7 @@ class AveAzul extends Promise {
41
44
  * @returns {Promise} Promise that resolves with the filtered array
42
45
  */
43
46
  filter(fn) {
44
- return this.then(value => xaa.filter(value, fn));
47
+ return this.then((value) => xaa.filter(value, fn));
45
48
  }
46
49
 
47
50
  /**
@@ -51,7 +54,7 @@ class AveAzul extends Promise {
51
54
  * @returns {Promise} Promise that resolves with the mapped array
52
55
  */
53
56
  map(fn) {
54
- return this.then(value => xaa.map(value, fn));
57
+ return this.then((value) => xaa.map(value, fn));
55
58
  }
56
59
 
57
60
  /**
@@ -71,7 +74,18 @@ class AveAzul extends Promise {
71
74
  * @returns {Promise} Promise that resolves when iteration is complete
72
75
  */
73
76
  each(fn) {
74
- return this.then(value => xaa.each(value, fn));
77
+ return this.then(async (value) => {
78
+ const result = [];
79
+ for (let i = 0; i < value.length; i++) {
80
+ let x = value[i];
81
+ if (isPromise(x)) {
82
+ x = await x;
83
+ }
84
+ await fn(x, i, value.length);
85
+ result.push(x);
86
+ }
87
+ return result;
88
+ });
75
89
  }
76
90
 
77
91
  /**
@@ -80,7 +94,7 @@ class AveAzul extends Promise {
80
94
  * @returns {Promise} Promise that resolves after the delay
81
95
  */
82
96
  delay(ms) {
83
- return this.then(value => xaa.delay(ms, value));
97
+ return xaa.delay(ms);
84
98
  }
85
99
 
86
100
  /**
@@ -89,43 +103,8 @@ class AveAzul extends Promise {
89
103
  * @param {string} [message] - Optional error message
90
104
  * @returns {Promise} Promise that rejects if timeout occurs
91
105
  */
92
- timeout(ms, message = "Operation timed out") {
93
- return new AveAzul((resolve, reject) => {
94
- const timer = setTimeout(() => {
95
- reject(new Error(message));
96
- }, ms);
97
-
98
- this.then(
99
- value => {
100
- clearTimeout(timer);
101
- resolve(value);
102
- },
103
- err => {
104
- clearTimeout(timer);
105
- reject(err);
106
- }
107
- );
108
- });
109
- }
110
-
111
- /**
112
- * Bluebird-style try() for wrapping sync/async functions
113
- * @param {Function} fn - Function to execute
114
- * @returns {Promise} Promise that resolves with the function's return value
115
- */
116
- try(fn) {
117
- return new AveAzul((resolve, reject) => {
118
- try {
119
- const result = fn();
120
- if (result && typeof result.then === "function") {
121
- result.then(resolve, reject);
122
- } else {
123
- resolve(result);
124
- }
125
- } catch (err) {
126
- reject(err);
127
- }
128
- });
106
+ timeout(ms, message = "operation timed out") {
107
+ return AveAzul.resolve(xaa.timeout(ms, message).run(this));
129
108
  }
130
109
 
131
110
  /**
@@ -133,34 +112,18 @@ class AveAzul extends Promise {
133
112
  * @param {Object} obj - Object with promise values
134
113
  * @returns {Promise} Promise that resolves with an object of resolved values
135
114
  */
136
- props(obj) {
137
- const keys = Object.keys(obj);
138
- const values = keys.map(k => obj[k]);
139
-
140
- return AveAzul.all(values).then(results => {
141
- const resolved = {};
142
- keys.forEach((k, i) => {
143
- resolved[k] = results[i];
115
+ props() {
116
+ return this.then((value) => {
117
+ const keys = Object.keys(value);
118
+ const values = keys.map((k) => value[k]);
119
+
120
+ return AveAzul.all(values).then((results) => {
121
+ const resolved = {};
122
+ keys.forEach((k, i) => {
123
+ resolved[k] = results[i];
124
+ });
125
+ return resolved;
144
126
  });
145
- return resolved;
146
- });
147
- }
148
-
149
- /**
150
- * Bluebird-style catchIf() with predicate matching
151
- * @param {Function|Error} predicate - Error class or predicate function
152
- * @param {Function} fn - Handler function
153
- * @returns {Promise} Promise with conditional catch handler
154
- */
155
- catchIf(predicate, fn) {
156
- return this.catch(err => {
157
- if (
158
- (typeof predicate === "function" && !predicate.prototype && predicate(err)) ||
159
- (typeof predicate === "function" && predicate.prototype && err instanceof predicate)
160
- ) {
161
- return fn(err);
162
- }
163
- throw err;
164
127
  });
165
128
  }
166
129
 
@@ -170,7 +133,7 @@ class AveAzul extends Promise {
170
133
  * @returns {Promise} Promise that maintains the rejection
171
134
  */
172
135
  tapCatch(fn) {
173
- return this.catch(err => {
136
+ return this.catch((err) => {
174
137
  fn(err);
175
138
  throw err;
176
139
  });
@@ -184,7 +147,32 @@ class AveAzul extends Promise {
184
147
  * @returns {Promise} Promise that resolves with the final reduced value
185
148
  */
186
149
  reduce(fn, initialValue) {
187
- return this.then(array => AveAzul.reduce(array, fn, initialValue));
150
+ const hasInitial = arguments.length > 1;
151
+
152
+ return this.then(async (array) => {
153
+ const len = array.length;
154
+ let value;
155
+ let idx;
156
+ if (hasInitial) {
157
+ idx = 0;
158
+ value = initialValue;
159
+ } else {
160
+ idx = 1;
161
+ value = array[0];
162
+ }
163
+
164
+ value = isPromise(value) ? await value : value;
165
+
166
+ for (; idx < len; idx++) {
167
+ let x = array[idx];
168
+ if (isPromise(x)) {
169
+ x = await x;
170
+ }
171
+ value = await fn(value, x, idx, len);
172
+ }
173
+
174
+ return value;
175
+ });
188
176
  }
189
177
 
190
178
  /**
@@ -202,7 +190,9 @@ class AveAzul extends Promise {
202
190
  * @returns {Promise} Promise that rejects with the new reason
203
191
  */
204
192
  catchThrow(reason) {
205
- return this.catch(() => AveAzul.throw(reason));
193
+ return this.catch(() => {
194
+ throw reason;
195
+ });
206
196
  }
207
197
 
208
198
  /**
@@ -216,27 +206,23 @@ class AveAzul extends Promise {
216
206
 
217
207
  /**
218
208
  * Bluebird-style get() for retrieving a property value
219
- * @param {string|number} propertyPath - Path to the property (can be nested using dot notation)
209
+ * @param {string|number} key - Key to retrieve
220
210
  * @returns {Promise} Promise that resolves with the property value
221
211
  */
222
- get(propertyPath) {
223
- return this.then(value => {
224
- if (value == null) {
225
- throw new TypeError("Cannot read property '" + propertyPath + "' of " + value);
226
- }
227
-
228
- let result = value;
229
- const props = String(propertyPath).split(".");
230
-
231
- for (const prop of props) {
232
- if (result == null) {
233
- throw new TypeError("Cannot read property '" + prop + "' of " + result);
234
- }
235
- result = result[prop];
236
- }
212
+ get(key) {
213
+ return this.then((value) => value[key]);
214
+ }
237
215
 
238
- return result;
239
- });
216
+ /**
217
+ * Bluebird-style disposer() for resource cleanup
218
+ * @param {Function} fn - Cleanup function
219
+ * @returns {Disposer} Disposer object
220
+ */
221
+ disposer(fn) {
222
+ if (typeof fn !== "function") {
223
+ throw new TypeError("Expected a function");
224
+ }
225
+ return new Disposer(fn, this);
240
226
  }
241
227
  }
242
228
 
@@ -270,18 +256,18 @@ AveAzul.map = (value, fn) => AveAzul.resolve(xaa.map(value, fn));
270
256
  * @param {Function} fn - Function to execute
271
257
  * @returns {Promise} Promise that resolves with the function's return value
272
258
  */
273
- AveAzul.try = fn => AveAzul.resolve(xaa.wrap(fn));
259
+ AveAzul.try = (fn) => AveAzul.resolve(xaa.wrap(fn));
274
260
 
275
261
  /**
276
262
  * Bluebird-style props() for object properties
277
263
  * @param {Object} obj - Object with promise values
278
264
  * @returns {Promise} Promise that resolves with an object of resolved values
279
265
  */
280
- AveAzul.props = obj => {
266
+ AveAzul.props = (obj) => {
281
267
  const keys = Object.keys(obj);
282
- const values = keys.map(k => obj[k]);
268
+ const values = keys.map((k) => obj[k]);
283
269
 
284
- return AveAzul.all(values).then(results => {
270
+ return AveAzul.all(values).then((results) => {
285
271
  const resolved = {};
286
272
  keys.forEach((k, i) => {
287
273
  resolved[k] = results[i];
@@ -295,24 +281,7 @@ AveAzul.props = obj => {
295
281
  * @returns {Object} Deferred object with promise, resolve, and reject methods
296
282
  */
297
283
  AveAzul.defer = () => {
298
- const deferred = xaa.makeDefer();
299
- deferred.promise = AveAzul.resolve(deferred.promise);
300
- return deferred;
301
- };
302
-
303
- /**
304
- * Bluebird-style promisify() for converting callback-style functions to promises
305
- * @param {Function} fn - Function to promisify
306
- * @param {Object} [options] - Options object
307
- * @param {Object} [options.context] - `this` context to bind the function to
308
- * @returns {Function} Promisified function that returns an AveAzul promise
309
- */
310
- AveAzul.promisify = (fn, options = {}) => {
311
- const promisified = nodePromisify(fn);
312
- if (options.context) {
313
- return (...args) => AveAzul.resolve(promisified.apply(options.context, args));
314
- }
315
- return (...args) => AveAzul.resolve(promisified(...args));
284
+ return xaa.makeDefer(AveAzul);
316
285
  };
317
286
 
318
287
  /**
@@ -321,7 +290,9 @@ AveAzul.promisify = (fn, options = {}) => {
321
290
  * @param {Function} fn - Iterator function to call for each item
322
291
  * @returns {Promise} Promise that resolves when iteration is complete
323
292
  */
324
- AveAzul.each = (items, fn) => AveAzul.resolve(xaa.each(items, fn));
293
+ AveAzul.each = function (items, fn) {
294
+ return AveAzul.resolve(items).each(fn);
295
+ };
325
296
 
326
297
  /**
327
298
  * Bluebird-style reduce() for array reduction
@@ -330,124 +301,83 @@ AveAzul.each = (items, fn) => AveAzul.resolve(xaa.each(items, fn));
330
301
  * @param {*} [initialValue] - Optional initial value
331
302
  * @returns {Promise} Promise that resolves with the final reduced value
332
303
  */
333
- AveAzul.reduce = (array, fn, initialValue) => {
334
- const hasInitial = arguments.length > 2;
335
- const len = array.length;
336
-
337
- return AveAzul.resolve().then(async () => {
338
- let value = hasInitial ? initialValue : array[0];
339
- const start = hasInitial ? 0 : 1;
340
-
341
- for (let i = start; i < len; i++) {
342
- value = await fn(value, array[i], i, len);
343
- }
304
+ AveAzul.reduce = function (array, ...args) {
305
+ return AveAzul.resolve(array).reduce(...args);
306
+ };
344
307
 
345
- return value;
308
+ /**
309
+ * Bluebird-style promisify() for converting callback-based functions to promises
310
+ * @param {Function} fn - Function to promisify
311
+ * @param {Object} [options] - Options object
312
+ * @returns {Function} Promisified function
313
+ */
314
+ AveAzul.promisify = (fn, options) => {
315
+ return promisify(fn, {
316
+ ...options,
317
+ Promise: AveAzul,
346
318
  });
347
319
  };
348
320
 
349
321
  /**
350
- * Bluebird-style throw() that returns a rejected promise with the given reason
351
- * @param {*} reason - Value to reject the promise with
352
- * @returns {Promise} Promise that rejects with the given reason
322
+ * Bluebird-style promisifyAll() for converting callback-based functions to promises
323
+ * @param {Object} target - Object to promisify
324
+ * @param {Object} [options] - Options object
325
+ * @returns {Object} Object with promisified methods
353
326
  */
354
- AveAzul.throw = reason => AveAzul.reject(reason);
327
+ AveAzul.promisifyAll = (target, options) => {
328
+ return promisifyAll(target, { ...options, Promise: AveAzul });
329
+ };
355
330
 
356
331
  /**
357
- * Bluebird-style promisifyAll() for converting all methods of an object or class to promise-based versions
358
- * Similar to Bluebird's Promise.promisifyAll()
359
- *
360
- * @param {Object|Function} target - The object or class to promisify
361
- * @param {Object} [options] - Configuration options
362
- * @param {string} [options.suffix='Async'] - Suffix to append to promisified method names
363
- * @param {Function} [options.filter] - Filter function to determine which methods to promisify
364
- * @param {Function} [options.promisifier] - Custom function to handle promisification
365
- * @param {boolean} [options.multiArgs=false] - Whether to support multiple callback arguments
366
- * @param {boolean} [options.excludeMain=false] - Whether to exclude promisifying the main object/class
367
- * @param {Object} [options.context] - The context (this) to use when calling methods
368
- * @returns {Object|Function} The promisified object or class
369
- * @throws {TypeError} If target is null, undefined, or not an object/function
370
- *
371
- * @example
372
- * // Promisify an object
373
- * const obj = {
374
- * method(cb) { cb(null, 'result'); }
375
- * };
376
- * AveAzul.promisifyAll(obj);
377
- * const result = await obj.methodAsync();
378
- *
379
- * @example
380
- * // Promisify a class
381
- * class MyClass {
382
- * method(cb) { cb(null, 'result'); }
383
- * }
384
- * AveAzul.promisifyAll(MyClass);
385
- * const instance = new MyClass();
386
- * const result = await instance.methodAsync();
387
- *
388
- * @example
389
- * // With custom options
390
- * const obj = {
391
- * method(cb) { cb(null, 'result1', 'result2'); }
392
- * };
393
- * AveAzul.promisifyAll(obj, {
394
- * suffix: 'Promise',
395
- * multiArgs: true,
396
- * filter: (name) => name === 'method'
397
- * });
398
- * const [result1, result2] = await obj.methodPromise();
332
+ * Bluebird-style method() for creating a method that returns a promise
333
+ * @param {Function} fn - Function to create a method for
334
+ * @returns {Function} Method function that returns a promise
399
335
  */
400
- AveAzul.promisifyAll = (target, options = {}) => {
401
- const {
402
- suffix = 'Async',
403
- filter = (name, func, targetObj, passedOptions) => {
404
- return (
405
- typeof func === 'function' &&
406
- !func.name.startsWith('_') &&
407
- !func.name.startsWith('promisify') &&
408
- !func.name.startsWith('promisifyAll')
409
- );
410
- },
411
- promisifier = (fn, context, multiArgs) => {
412
- if (multiArgs) {
413
- return (...args) => {
414
- return new AveAzul((resolve, reject) => {
415
- args.push((err, ...results) => {
416
- if (err) reject(err);
417
- else resolve(results);
418
- });
419
- fn.apply(context, args);
420
- });
421
- };
336
+ AveAzul.method = (fn) => {
337
+ return function (...args) {
338
+ return new AveAzul((resolve, reject) => {
339
+ try {
340
+ const result = fn.call(this, ...args);
341
+ resolve(result);
342
+ } catch (error) {
343
+ reject(error);
422
344
  }
423
- return AveAzul.promisify(fn, { context });
424
- },
425
- multiArgs = false,
426
- excludeMain = false,
427
- context = target
428
- } = options;
429
-
430
- if (target == null || (typeof target !== 'object' && typeof target !== 'function')) {
431
- throw new TypeError('target must be an object');
432
- }
433
-
434
- const targetObj = target.prototype || target;
435
- const keys = Object.getOwnPropertyNames(targetObj);
345
+ });
346
+ };
347
+ };
436
348
 
437
- for (const key of keys) {
438
- const func = targetObj[key];
439
- if (filter(key, func, targetObj, options)) {
440
- const promisifiedKey = key + suffix;
441
- targetObj[promisifiedKey] = promisifier(func, context, multiArgs);
442
- }
349
+ /**
350
+ * Bluebird-style using() for resource management. There is only a static version of this method.
351
+ * After the handler finish and returns, regardless of whether it resolves or rejects, the resources will be disposed.
352
+ *
353
+ * @param {Disposer|Array<Disposer>} resources - Resource disposers, either an array of disposers or a variadic argument list
354
+ * @param {Function} handler - Handler function that will receive the resources as arguments
355
+ * @returns {Promise} Promise that resolves with handler result
356
+ */
357
+ AveAzul.using = (resources, ...args) => {
358
+ if (args.length === 0) {
359
+ throw new TypeError("resrouces and handler function required");
443
360
  }
444
361
 
445
- if (!excludeMain && typeof target === 'function') {
446
- target.promisify = AveAzul.promisify;
447
- target.promisifyAll = AveAzul.promisifyAll;
362
+ if (Array.isArray(resources)) {
363
+ if (args.length > 1) {
364
+ throw new TypeError(
365
+ "only two arguments are allowed when passing an array of resources"
366
+ );
367
+ }
368
+ return using(resources, args[0], AveAzul, true);
448
369
  }
449
-
450
- return target;
370
+ const handler = args.pop();
371
+ return using([resources, ...args], handler, AveAzul, false);
451
372
  };
452
373
 
374
+ /**
375
+ * @description
376
+ * When fatal error and AveAzul needs to crash the process,
377
+ * this method is used to throw the error.
378
+ *
379
+ * @param {Error} error - The error to throw.
380
+ */
381
+ AveAzul.___throwUncaughtError = triggerUncaughtException;
382
+
453
383
  module.exports = AveAzul;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Disposer class for resource cleanup
5
+ * @private
6
+ */
7
+ class Disposer {
8
+ constructor(data, promise) {
9
+ this._data = data; // The cleanup function
10
+ this._promise = promise; // The promise that resolves to the resource
11
+ }
12
+ }
13
+
14
+ module.exports.Disposer = Disposer;
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+
3
+ const { promisify } = require("./promisify");
4
+ const {
5
+ isIdentifier,
6
+ isClass,
7
+ isConstructor,
8
+ isPromisified,
9
+ getObjectKeys,
10
+ } = require("./util");
11
+
12
+ const defaultSuffix = "Async";
13
+
14
+ const defaultFilter = function (name) {
15
+ return isIdentifier(name) && name.charAt(0) !== "_" && name !== "constructor";
16
+ };
17
+
18
+ const defaultPromisifier = (fn, _defaultPromisifier, options) => {
19
+ return promisify(fn, {
20
+ ...options,
21
+ copyProps: false,
22
+ });
23
+ };
24
+
25
+ const excludedPrototypes = [
26
+ Object.getPrototypeOf(Array),
27
+ Object.getPrototypeOf(Object),
28
+ Object.getPrototypeOf(Function),
29
+ ];
30
+
31
+ const excludedClasses = [Array, Object, Function];
32
+
33
+ // Helper function to determine if a class extends from any excluded class
34
+ function isExcludedClass(obj) {
35
+ if (excludedClasses.includes(obj)) {
36
+ return true;
37
+ }
38
+
39
+ // Check if obj extends from any excluded class using instanceof
40
+ if (typeof obj === "function" && obj.prototype) {
41
+ // Check if prototype is instance of any excluded class
42
+ for (const excludedClass of excludedClasses) {
43
+ if (obj.prototype instanceof excludedClass) {
44
+ return true;
45
+ }
46
+ }
47
+ }
48
+
49
+ return false;
50
+ }
51
+
52
+ function promisifyAll2(obj, options) {
53
+ if (isExcludedClass(obj)) {
54
+ return;
55
+ }
56
+
57
+ const allKeys = getObjectKeys(obj, excludedPrototypes);
58
+
59
+ for (const key of allKeys) {
60
+ if (key.endsWith(options.suffix)) {
61
+ throw new TypeError(
62
+ "Cannot promisify an API that has normal methods with '%s'-suffix\u000a\u000a See http://goo.gl/MqrFmX\u000a".replace(
63
+ "%s",
64
+ options.suffix
65
+ )
66
+ );
67
+ }
68
+ const value = obj[key];
69
+ const promisifiedKey = key + options.suffix;
70
+ const passesDefaultFilter =
71
+ options.filter === defaultFilter ? true : defaultFilter(key, value, obj);
72
+ if (
73
+ isConstructor(value) ||
74
+ typeof value !== "function" ||
75
+ isPromisified(value) ||
76
+ obj[promisifiedKey] ||
77
+ !options.filter(key, value, obj, passesDefaultFilter)
78
+ ) {
79
+ continue;
80
+ }
81
+ obj[promisifiedKey] = options.promisifier(value, defaultPromisifier, {
82
+ context: obj,
83
+ copyProps: false,
84
+ multiArgs: options.multiArgs,
85
+ Promise: options.Promise,
86
+ });
87
+ }
88
+ }
89
+
90
+ function promisifyAll(target, _options) {
91
+ if (typeof target !== "function" && typeof target !== "object") {
92
+ throw new TypeError(
93
+ "the target of promisifyAll must be an object or a function"
94
+ );
95
+ }
96
+
97
+ const options = {
98
+ suffix: defaultSuffix,
99
+ filter: defaultFilter,
100
+ promisifier: defaultPromisifier,
101
+ Promise: global.Promise,
102
+ ..._options,
103
+ };
104
+
105
+ const suffix = options.suffix;
106
+
107
+ if (!isIdentifier(suffix)) {
108
+ throw new RangeError(
109
+ "suffix must be a valid identifier\u000a\u000a See http://goo.gl/MqrFmX\u000a"
110
+ );
111
+ }
112
+
113
+ const allKeys = getObjectKeys(target, excludedPrototypes);
114
+
115
+ for (const key of allKeys) {
116
+ const value = target[key];
117
+ if (
118
+ value &&
119
+ key !== "constructor" &&
120
+ !key.startsWith("_") &&
121
+ isClass(value)
122
+ ) {
123
+ const proto = Object.getPrototypeOf(value);
124
+ if (!excludedPrototypes.includes(proto)) {
125
+ promisifyAll2(proto, options);
126
+ }
127
+
128
+ promisifyAll2(value, options);
129
+ }
130
+ }
131
+
132
+ promisifyAll2(target, options);
133
+ }
134
+
135
+ module.exports.promisifyAll = promisifyAll;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+
3
+ const { copyOwnProperties, isPromisified } = require("./util");
4
+
5
+ module.exports.promisify = function promisify(fn, _options) {
6
+ if (typeof fn !== "function") {
7
+ throw new TypeError("expecting a function but got " + {}.toString.call(fn));
8
+ }
9
+
10
+ if (isPromisified(fn)) {
11
+ return fn;
12
+ }
13
+
14
+ const options = {
15
+ Promise: global.Promise,
16
+ multiArgs: false,
17
+ copyProps: true,
18
+ suffix: "",
19
+ ..._options,
20
+ };
21
+
22
+ const Promise = options.Promise;
23
+ const multiArgs = !!options.multiArgs;
24
+
25
+ const promisifiedFn = (...args) => {
26
+ return new Promise((resolve, reject) => {
27
+ // add a callback to the end of the arguments to transfer the result to the promise
28
+ args.push((err, ...values) => {
29
+ if (err) {
30
+ return reject(err);
31
+ }
32
+ if (multiArgs) {
33
+ resolve(values);
34
+ } else {
35
+ resolve(values[0]);
36
+ }
37
+ });
38
+
39
+ // call the original function with the updated args
40
+ fn.call(options.context, ...args);
41
+ });
42
+ };
43
+
44
+ if (options.copyProps) {
45
+ copyOwnProperties(fn, promisifiedFn);
46
+ }
47
+
48
+ Object.defineProperty(promisifiedFn, "__isPromisified__", {
49
+ value: true,
50
+ writable: false,
51
+ enumerable: false,
52
+ configurable: true,
53
+ });
54
+
55
+ Object.defineProperty(promisifiedFn, "length", {
56
+ value: fn.length,
57
+ writable: false,
58
+ enumerable: false,
59
+ configurable: false,
60
+ });
61
+
62
+ Object.defineProperty(promisifiedFn, "name", {
63
+ value: fn.name + options.suffix,
64
+ writable: false,
65
+ enumerable: false,
66
+ configurable: false,
67
+ });
68
+
69
+ return promisifiedFn;
70
+ };
package/lib/using.js ADDED
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+
3
+ const { Disposer } = require("./disposer");
4
+ const { isPromise } = require("./util");
5
+
6
+ /**
7
+ * @description
8
+ * The using function is a utility function that allows you to acquire resources,
9
+ * process them, and then dispose of them in an error-safe manner.
10
+ *
11
+ * @param {Array} resources - An array of resources to acquire.
12
+ * @param {Function} handler - A function that will be called with the acquired resources.
13
+ * @param {Promise} Promise - The Promise implementation to use. AveAzul or Bluebird.
14
+ * @param {boolean} asArray - Whether to return the result as an array.
15
+ * @returns {Promise} A promise that resolves to the result of the handler function.
16
+ */
17
+ function using(resources, handler, Promise, asArray) {
18
+ if (typeof handler !== "function") {
19
+ throw new TypeError("handler must be a function");
20
+ }
21
+
22
+ // resources is guaranateed to be an array of disposer, promise like, or any value
23
+ // first process all resources by mapping the resources array:
24
+ // 1. if it's a disposer, get its promise and resolve its value
25
+ // 2. if it's a promise like, get its value
26
+ // 3. otherwise, return the value
27
+ // Expect Promise to be AveAzul or Bluebird that has map method
28
+ const acquisitionErrors = [];
29
+
30
+ const acquireResources = () => {
31
+ const promiseRes = resources.map((resource) => {
32
+ // if it's a promise-like, wait for its resolved value
33
+ if (isPromise(resource)) {
34
+ return { ___promise: resource };
35
+ }
36
+ return resource;
37
+ });
38
+
39
+ return Promise.map(promiseRes, async (resource) => {
40
+ if (resource instanceof Disposer) {
41
+ try {
42
+ const res = await resource._promise;
43
+ resource._result = res;
44
+ } catch (error) {
45
+ acquisitionErrors.push(error);
46
+ resource._error = error;
47
+ }
48
+ return resource;
49
+ }
50
+
51
+ // if it's a promise like, wait for its resolved value
52
+ if (resource && resource.___promise) {
53
+ try {
54
+ const res = await resource.___promise;
55
+ resource._result = res;
56
+ } catch (error) {
57
+ acquisitionErrors.push(error);
58
+ resource._error = error;
59
+ }
60
+ return resource;
61
+ }
62
+
63
+ return { _result: resource };
64
+ });
65
+ };
66
+
67
+ const disposeResources = (processedResources) => {
68
+ const errors = [];
69
+ return Promise.each(processedResources, async (resource) => {
70
+ // dispose all resources that were acquired without errors
71
+ if (!resource._error && resource.hasOwnProperty("_result")) {
72
+ if (resource instanceof Disposer) {
73
+ try {
74
+ await resource._data(resource._result);
75
+ } catch (error) {
76
+ errors.push(error);
77
+ }
78
+ }
79
+ }
80
+ }).finally(() => {
81
+ if (errors.length > 0) {
82
+ Promise.___throwUncaughtError(new Error("cleanup resources failed"));
83
+ }
84
+ });
85
+ };
86
+
87
+ return acquireResources().then((processedResources) => {
88
+ if (acquisitionErrors.length > 0) {
89
+ return disposeResources(processedResources).tap(() => {
90
+ throw acquisitionErrors[0];
91
+ });
92
+ }
93
+
94
+ // now collect all the results into an array
95
+ const results = [];
96
+ for (const resource of processedResources) {
97
+ results.push(resource._result);
98
+ }
99
+
100
+ let handlerResult;
101
+ let handlerSyncError;
102
+ try {
103
+ // now call the handler with the results
104
+ if (asArray) {
105
+ handlerResult = handler(results);
106
+ } else {
107
+ handlerResult = handler(...results);
108
+ }
109
+ } catch (error) {
110
+ // catch sync error from handler
111
+ handlerSyncError = error;
112
+ }
113
+
114
+ return disposeResources(processedResources).then(() => {
115
+ if (handlerSyncError) {
116
+ throw handlerSyncError;
117
+ }
118
+ return handlerResult;
119
+ });
120
+ });
121
+ }
122
+
123
+ module.exports.using = using;
package/lib/util.js ADDED
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Determines if a function is a class (either ES6 class or ES5 constructor function)
5
+ * This function performs several checks to identify different class patterns:
6
+ * 1. ES6 classes with the 'class' keyword
7
+ * 2. Constructor functions (ES5 classes) with prototype methods
8
+ *
9
+ * @param {*} fn - The value to check
10
+ * @returns {boolean} - True if the function is a class, false otherwise
11
+ */
12
+ function isClass(fn) {
13
+ // Early return for non-functions or null/undefined
14
+ if (!fn || typeof fn !== "function") return false;
15
+
16
+ // Method 1: Check for ES6 class syntax
17
+ // This detects class declarations and class expressions
18
+ try {
19
+ const fnStr = fn.toString();
20
+ if (fnStr.startsWith("class ") || /^class\s+/.test(fnStr)) {
21
+ return true;
22
+ }
23
+ } catch (e) {
24
+ // Ignore errors that might occur when calling toString()
25
+ // Some objects might have custom toString implementations that throw
26
+ return false;
27
+ }
28
+
29
+ // Method 2: Check for constructor functions (ES5 class pattern)
30
+ // A proper constructor has its .prototype.constructor pointing back to itself
31
+ if (fn.prototype && fn.prototype.constructor === fn) {
32
+ // Additional validation to filter out regular functions
33
+ // that happen to have the correct prototype structure
34
+
35
+ // Check if the prototype has any methods other than constructor
36
+ // This is a strong indicator of a class-like structure
37
+ const hasOwnMethods = Object.getOwnPropertyNames(fn.prototype).some(
38
+ (name) =>
39
+ name !== "constructor" && typeof fn.prototype[name] === "function"
40
+ );
41
+
42
+ // If it has prototype methods OR has static properties/methods
43
+ // Either condition suggests it's being used as a class
44
+ return hasOwnMethods || Object.getOwnPropertyNames(fn).length > 0;
45
+ }
46
+
47
+ // Not a class by any of our detection methods
48
+ return false;
49
+ }
50
+
51
+ const rident = /^[a-z$_][a-z$_0-9]*$/i;
52
+ function isIdentifier(str) {
53
+ return rident.test(str);
54
+ }
55
+
56
+ function isConstructor(func) {
57
+ if (!func) {
58
+ return false;
59
+ }
60
+ const proto = func.prototype;
61
+ return (
62
+ !!proto &&
63
+ !!proto.constructor &&
64
+ !!proto.constructor.name &&
65
+ proto.constructor.name === func.name
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Prop filtering code copied from bluebird/js/release
71
+ */
72
+ const noCopyProps = [
73
+ "arity",
74
+ "length",
75
+ "name",
76
+ "arguments",
77
+ "caller",
78
+ "callee",
79
+ "prototype",
80
+ "__isPromisified__",
81
+ ];
82
+ const noCopyPropsPattern = new RegExp("^(?:" + noCopyProps.join("|") + ")$");
83
+
84
+ function propsFilter(key) {
85
+ return !noCopyPropsPattern.test(key);
86
+ }
87
+
88
+ function copyOwnProperties(source, target, filter = propsFilter) {
89
+ const names = Object.getOwnPropertyNames(source);
90
+
91
+ for (const name of names) {
92
+ if (filter(name)) {
93
+ Object.defineProperty(
94
+ target,
95
+ name,
96
+ Object.getOwnPropertyDescriptor(source, name)
97
+ );
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Copied from bluebird/js/release/util.js
104
+ * @param {*} fn
105
+ * @returns {boolean}
106
+ */
107
+ function isPromisified(fn) {
108
+ try {
109
+ return fn.__isPromisified__ === true;
110
+ } catch (e) {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Determines if an object is a Promise instance
117
+ * @param {*} obj - The object to check
118
+ * @returns {boolean} - True if the object is a Promise instance, false otherwise
119
+ */
120
+ function isPromise(obj) {
121
+ return (
122
+ obj instanceof Promise ||
123
+ (obj != null &&
124
+ typeof obj === "object" &&
125
+ typeof obj.then === "function" &&
126
+ typeof obj.catch === "function")
127
+ );
128
+ }
129
+
130
+ const defaultExcluded = [
131
+ Object.getPrototypeOf(Array), // Array.prototype
132
+ Object.getPrototypeOf(Object), // Object.prototype
133
+ Object.getPrototypeOf(Function), // Function.prototype
134
+ ];
135
+
136
+ /**
137
+ * Gets all property keys from an object and its prototype chain, excluding standard
138
+ * prototypes like Object.prototype, Array.prototype, and Function.prototype
139
+ *
140
+ * @param {Object} target - The target object to get keys from
141
+ * @param {Array} [excludedPrototypes=[]] - An array of prototype objects to exclude keys from
142
+ * @returns {Array<string>} - Array of property keys
143
+ */
144
+ function getObjectKeys(target, excludedPrototypes = []) {
145
+ const excluded =
146
+ excludedPrototypes.length > 0 ? excludedPrototypes : defaultExcluded;
147
+
148
+ // Get own properties
149
+ const ownKeys = Object.getOwnPropertyNames(target);
150
+
151
+ // Get prototype properties, excluding those from excluded prototypes
152
+ let protoKeys = [];
153
+ let currentProto = Object.getPrototypeOf(target);
154
+
155
+ // Walk up the prototype chain until we hit null or an excluded prototype
156
+ while (currentProto && !excluded.includes(currentProto)) {
157
+ protoKeys = [...protoKeys, ...Object.getOwnPropertyNames(currentProto)];
158
+ currentProto = Object.getPrototypeOf(currentProto);
159
+ }
160
+
161
+ // Combine own properties and prototype properties
162
+ return [...protoKeys, ...ownKeys];
163
+ }
164
+
165
+ /**
166
+ * Triggers an uncaught exception in a safe way by scheduling it on the next event loop tick
167
+ * This is used for fatal errors that should crash the process
168
+ * @param {Error} error - The error to throw
169
+ */
170
+ function triggerUncaughtException(error) {
171
+ if (!(error instanceof Error)) {
172
+ error = new Error(String(error));
173
+ }
174
+
175
+ // Use setTimeout with 0ms delay to throw on the next event loop tick
176
+ // This ensures the current execution context completes first
177
+ setTimeout(() => {
178
+ throw error;
179
+ }, 0);
180
+ }
181
+
182
+ module.exports.copyOwnProperties = copyOwnProperties;
183
+ module.exports.isClass = isClass;
184
+ module.exports.isIdentifier = isIdentifier;
185
+ module.exports.isConstructor = isConstructor;
186
+ module.exports.isPromisified = isPromisified;
187
+ module.exports.isPromise = isPromise;
188
+ module.exports.triggerUncaughtException = triggerUncaughtException;
189
+ module.exports.getObjectKeys = getObjectKeys;
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "aveazul",
3
- "version": "0.1.2",
4
- "description": "Extend native Promise with bluebird like APIs for Node.js",
3
+ "version": "0.1.4",
4
+ "description": "Bluebird-like APIs in extended native Promise",
5
5
  "main": "lib/aveazul.js",
6
6
  "homepage": "https://github.com/jchip/aveazul",
7
7
  "license": "Apache-2.0",
8
8
  "scripts": {
9
9
  "test": "jest test",
10
10
  "test:watch": "jest test --watch",
11
- "test:coverage": "jest test --coverage"
11
+ "test:coverage": "jest test --coverage",
12
+ "test:bluebird": "USE_BLUEBIRD=true jest test",
13
+ "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --no-coverage",
14
+ "test:debug:bluebird": "USE_BLUEBIRD=true node --inspect-brk node_modules/.bin/jest --runInBand --no-coverage"
12
15
  },
13
16
  "author": "Joel Chen",
14
17
  "files": [
@@ -28,6 +31,8 @@
28
31
  "xaa": "^1.7.3"
29
32
  },
30
33
  "devDependencies": {
31
- "jest": "^29.7.0"
34
+ "bluebird": "^3.7.2",
35
+ "jest": "^29.7.0",
36
+ "rimraf": "^3.0.1"
32
37
  }
33
38
  }