aveazul 0.1.4 → 0.1.5

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
@@ -58,6 +58,17 @@ AveAzul.using(getResource(), (resource) => {
58
58
  console.log(result); // "operation completed"
59
59
  // Resource is automatically closed here, even if an error occurred
60
60
  });
61
+
62
+ // Using spread to apply array results as arguments
63
+ AveAzul.all([getUser(1), getPosts(1), getComments(1)]).spread(
64
+ (user, posts, comments) => {
65
+ // Instead of using .then(([user, posts, comments]) => {...})
66
+ console.log(
67
+ `User ${user.name} has ${posts.length} posts and ${comments.length} comments`
68
+ );
69
+ return { user, activity: { posts, comments } };
70
+ }
71
+ );
61
72
  ```
62
73
 
63
74
  ## API
@@ -72,6 +83,7 @@ AveAzul.using(getResource(), (resource) => {
72
83
  - `delay(ms)` - Delay resolution
73
84
  - `timeout(ms, message?)` - Reject after specified time
74
85
  - `props(obj)` - Resolve object properties
86
+ - `spread(fn)` - Apply array values as arguments to function
75
87
  - `tapCatch(fn)` - Execute side effects on rejection
76
88
  - `reduce(fn, initialValue?)` - Reduce array elements
77
89
  - `throw(reason)` - Return rejected promise
@@ -88,12 +100,15 @@ AveAzul.using(getResource(), (resource) => {
88
100
  - `props(obj)` - Resolve object properties
89
101
  - `defer()` - Create a deferred promise
90
102
  - `promisify(fn, options?)` - Convert callback-style functions to promises (preserves original function properties)
103
+ - `fromNode(fn, options?)` - Convert Node-style callback functions to promise-returning functions
104
+ - `fromCallback(fn, options?)` - Alias for fromNode
91
105
  - `each(items, fn)` - Iterate over array elements
92
106
  - `reduce(array, fn, initialValue?)` - Reduce array elements
93
107
  - `method(fn)` - Creates a method that returns a promise resolving to the value returned by the original function
94
108
  - `throw(reason)` - Return rejected promise
95
109
  - `promisifyAll(target, options?)` - Convert all methods of an object/class to promises
96
110
  - `using(resources, fn)` - Manage resources with automatic cleanup
111
+ - `join(...values, handler?)` - Wait for multiple promises and pass their resolved values as separate arguments to the handler function. If no handler is provided, behaves like Promise.all
97
112
 
98
113
  ### PromisifyAll Options
99
114
 
package/lib/aveazul.js CHANGED
@@ -6,6 +6,7 @@ const { promisifyAll } = require("./promisify-all");
6
6
  const { Disposer } = require("./disposer");
7
7
  const { using } = require("./using");
8
8
  const { isPromise, triggerUncaughtException } = require("./util");
9
+
9
10
  /**
10
11
  * AveAzul ("Blue Bird" in Spanish) - Extended Promise class that provides Bluebird-like utility methods
11
12
  * This implementation is inspired by and provides similar APIs to the Bluebird Promise library,
@@ -31,8 +32,8 @@ class AveAzul extends Promise {
31
32
  * @returns {Promise} Promise that resolves with the original value
32
33
  */
33
34
  tap(fn) {
34
- return this.then((value) => {
35
- fn(value);
35
+ return this.then(async (value) => {
36
+ await fn(value);
36
37
  return value;
37
38
  });
38
39
  }
@@ -222,8 +223,36 @@ class AveAzul extends Promise {
222
223
  if (typeof fn !== "function") {
223
224
  throw new TypeError("Expected a function");
224
225
  }
226
+
225
227
  return new Disposer(fn, this);
226
228
  }
229
+
230
+ /**
231
+ * Bluebird-style spread() method for handling array arguments
232
+ * Similar to Bluebird's Promise.prototype.spread()
233
+ * @param {Function} fn - Function to apply to the array arguments
234
+ * @returns {Promise} Promise that resolves with the function's return value
235
+ */
236
+ spread(fn) {
237
+ if (typeof fn !== "function") {
238
+ return AveAzul.reject(
239
+ new TypeError("expecting a function but got " + fn)
240
+ );
241
+ }
242
+
243
+ return this.then(async (args) => {
244
+ if (Array.isArray(args)) {
245
+ for (let i = 0; i < args.length; i++) {
246
+ if (isPromise(args[i])) {
247
+ args[i] = await args[i];
248
+ }
249
+ }
250
+ return fn(...args);
251
+ } else {
252
+ return fn(args);
253
+ }
254
+ });
255
+ }
227
256
  }
228
257
 
229
258
  /**
@@ -371,6 +400,52 @@ AveAzul.using = (resources, ...args) => {
371
400
  return using([resources, ...args], handler, AveAzul, false);
372
401
  };
373
402
 
403
+ /**
404
+ * Bluebird-style join() for joining promises
405
+ *
406
+ * @param {...Promise} args - Promises to join
407
+ * @param {Function} handler - Handler function to apply to the joined results
408
+ * @returns {Promise} Promise that resolves with the handler's return value
409
+ */
410
+ AveAzul.join = function (...args) {
411
+ if (args.length > 1 && typeof args.at(-1) === "function") {
412
+ const handler = args.pop();
413
+ return AveAzul.all(args).then((results) => handler(...results));
414
+ } else {
415
+ return AveAzul.all(args);
416
+ }
417
+ };
418
+
419
+ function fromCallback(fn, options) {
420
+ return new AveAzul((resolve, reject) => {
421
+ try {
422
+ fn((err, ...args) => {
423
+ if (err) {
424
+ reject(err);
425
+ } else {
426
+ if (options && options.multiArgs) {
427
+ resolve(args);
428
+ } else {
429
+ resolve(args[0]);
430
+ }
431
+ }
432
+ });
433
+ } catch (err) {
434
+ reject(err);
435
+ }
436
+ });
437
+ }
438
+
439
+ AveAzul.fromNode = fromCallback;
440
+
441
+ /**
442
+ * Bluebird-style fromCallback() for converting callback-based functions to promises
443
+ * @param {Function} fn - Function to convert
444
+ * @param {Object} [options] - Options object
445
+ * @returns {Promise} Promise that resolves with the function's return value
446
+ */
447
+ AveAzul.fromCallback = fromCallback;
448
+
374
449
  /**
375
450
  * @description
376
451
  * When fatal error and AveAzul needs to crash the process,
@@ -381,3 +456,6 @@ AveAzul.using = (resources, ...args) => {
381
456
  AveAzul.___throwUncaughtError = triggerUncaughtException;
382
457
 
383
458
  module.exports = AveAzul;
459
+
460
+ const { setupNotImplemented } = require("./not-implemented");
461
+ setupNotImplemented(AveAzul);
package/lib/disposer.js CHANGED
@@ -5,8 +5,8 @@
5
5
  * @private
6
6
  */
7
7
  class Disposer {
8
- constructor(data, promise) {
9
- this._data = data; // The cleanup function
8
+ constructor(fn, promise) {
9
+ this._data = fn; // The cleanup function
10
10
  this._promise = promise; // The promise that resolves to the resource
11
11
  }
12
12
  }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+
3
+ function createNotImplemented(name) {
4
+ return function () {
5
+ const msg = name + " Not implemented in aveazul";
6
+ console.error(msg);
7
+ throw new Error(msg);
8
+ };
9
+ }
10
+
11
+ function createInstanceNotImplemented(AveAzul) {
12
+ const methods = [
13
+ "spread",
14
+ "error",
15
+ "bind",
16
+ "join",
17
+ "try",
18
+ "method",
19
+ "isFulfilled",
20
+ "isRejected",
21
+ "isPending",
22
+ "value",
23
+ "reason",
24
+ "props",
25
+ "any",
26
+ "some",
27
+ "map",
28
+ "reduce",
29
+ "filter",
30
+ "each",
31
+ "mapSeries",
32
+ "tap",
33
+ "tapCatch",
34
+ "catchThrow",
35
+ "catchReturn",
36
+ "get",
37
+ "throw",
38
+ "call",
39
+ ];
40
+
41
+ const proto = AveAzul.prototype;
42
+ const ret = [];
43
+ for (const method of methods) {
44
+ if (!proto[method]) {
45
+ ret.push(method);
46
+ proto[method] = createNotImplemented("instance " + method);
47
+ }
48
+ }
49
+ return ret;
50
+ }
51
+
52
+ function createStaticNotImplemented(AveAzul) {
53
+ const methods = [
54
+ "join",
55
+ "try",
56
+ "method",
57
+ "resolve",
58
+ "reject",
59
+ "all",
60
+ "props",
61
+ "any",
62
+ "some",
63
+ "map",
64
+ "filter",
65
+ "each",
66
+ "mapSeries",
67
+ "race",
68
+ "promisify",
69
+ "promisifyAll",
70
+ "fromNode",
71
+ "fromCallback",
72
+ "delay",
73
+ "coroutine",
74
+ "config",
75
+ ];
76
+ const ret = [];
77
+ for (const method of methods) {
78
+ if (!AveAzul[method]) {
79
+ ret.push(method);
80
+ AveAzul[method] = createNotImplemented("static " + method);
81
+ }
82
+ }
83
+ return ret;
84
+ }
85
+
86
+ function setupNotImplemented(AveAzul) {
87
+ const instance = createInstanceNotImplemented(AveAzul);
88
+ const staticMethods = createStaticNotImplemented(AveAzul);
89
+ AveAzul.__notImplementedInstance = instance;
90
+ AveAzul.__notImplementedStatic = staticMethods;
91
+ }
92
+
93
+ module.exports = {
94
+ createInstanceNotImplemented,
95
+ createStaticNotImplemented,
96
+ setupNotImplemented,
97
+ };
@@ -7,6 +7,7 @@ const {
7
7
  isConstructor,
8
8
  isPromisified,
9
9
  getObjectKeys,
10
+ isExcludedPrototype,
10
11
  } = require("./util");
11
12
 
12
13
  const defaultSuffix = "Async";
@@ -22,12 +23,6 @@ const defaultPromisifier = (fn, _defaultPromisifier, options) => {
22
23
  });
23
24
  };
24
25
 
25
- const excludedPrototypes = [
26
- Object.getPrototypeOf(Array),
27
- Object.getPrototypeOf(Object),
28
- Object.getPrototypeOf(Function),
29
- ];
30
-
31
26
  const excludedClasses = [Array, Object, Function];
32
27
 
33
28
  // Helper function to determine if a class extends from any excluded class
@@ -54,17 +49,9 @@ function promisifyAll2(obj, options) {
54
49
  return;
55
50
  }
56
51
 
57
- const allKeys = getObjectKeys(obj, excludedPrototypes);
52
+ const allKeys = getObjectKeys(obj);
58
53
 
59
54
  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
55
  const value = obj[key];
69
56
  const promisifiedKey = key + options.suffix;
70
57
  const passesDefaultFilter =
@@ -78,6 +65,16 @@ function promisifyAll2(obj, options) {
78
65
  ) {
79
66
  continue;
80
67
  }
68
+
69
+ if (key.endsWith(options.suffix)) {
70
+ throw new TypeError(
71
+ "Cannot promisify an API that has normal methods with '%s'-suffix\u000a\u000a See http://goo.gl/MqrFmX\u000a".replace(
72
+ "%s",
73
+ options.suffix
74
+ )
75
+ );
76
+ }
77
+
81
78
  obj[promisifiedKey] = options.promisifier(value, defaultPromisifier, {
82
79
  context: obj,
83
80
  copyProps: false,
@@ -110,7 +107,7 @@ function promisifyAll(target, _options) {
110
107
  );
111
108
  }
112
109
 
113
- const allKeys = getObjectKeys(target, excludedPrototypes);
110
+ const allKeys = getObjectKeys(target);
114
111
 
115
112
  for (const key of allKeys) {
116
113
  const value = target[key];
@@ -121,7 +118,7 @@ function promisifyAll(target, _options) {
121
118
  isClass(value)
122
119
  ) {
123
120
  const proto = Object.getPrototypeOf(value);
124
- if (!excludedPrototypes.includes(proto)) {
121
+ if (!isExcludedPrototype(proto)) {
125
122
  promisifyAll2(proto, options);
126
123
  }
127
124
 
package/lib/using.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const { Disposer } = require("./disposer");
4
4
  const { isPromise } = require("./util");
5
5
 
6
+ const SYM_FN_DISPOSE = Symbol("fnDispose");
6
7
  /**
7
8
  * @description
8
9
  * The using function is a utility function that allows you to acquire resources,
@@ -37,10 +38,15 @@ function using(resources, handler, Promise, asArray) {
37
38
  });
38
39
 
39
40
  return Promise.map(promiseRes, async (resource) => {
40
- if (resource instanceof Disposer) {
41
+ if (
42
+ resource &&
43
+ (resource instanceof Disposer ||
44
+ (resource._promise && typeof resource._data === "function"))
45
+ ) {
41
46
  try {
42
47
  const res = await resource._promise;
43
48
  resource._result = res;
49
+ resource[SYM_FN_DISPOSE] = resource._data;
44
50
  } catch (error) {
45
51
  acquisitionErrors.push(error);
46
52
  resource._error = error;
@@ -68,18 +74,18 @@ function using(resources, handler, Promise, asArray) {
68
74
  const errors = [];
69
75
  return Promise.each(processedResources, async (resource) => {
70
76
  // 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
- }
77
+ if (resource && resource[SYM_FN_DISPOSE]) {
78
+ try {
79
+ await resource[SYM_FN_DISPOSE](resource._result);
80
+ } catch (error) {
81
+ errors.push(error);
78
82
  }
79
83
  }
80
84
  }).finally(() => {
81
85
  if (errors.length > 0) {
82
- Promise.___throwUncaughtError(new Error("cleanup resources failed"));
86
+ Promise.___throwUncaughtError(
87
+ new AggregateError(errors, "cleanup resources failed", errors)
88
+ );
83
89
  }
84
90
  });
85
91
  };
@@ -97,26 +103,25 @@ function using(resources, handler, Promise, asArray) {
97
103
  results.push(resource._result);
98
104
  }
99
105
 
100
- let handlerResult;
101
- let handlerSyncError;
106
+ let handlerPromise;
107
+
102
108
  try {
103
109
  // now call the handler with the results
104
- if (asArray) {
105
- handlerResult = handler(results);
106
- } else {
107
- handlerResult = handler(...results);
108
- }
110
+ handlerPromise = Promise.resolve(
111
+ asArray ? handler(results) : handler(...results)
112
+ );
109
113
  } catch (error) {
110
114
  // catch sync error from handler
111
- handlerSyncError = error;
115
+ handlerPromise = Promise.reject(error);
112
116
  }
113
117
 
114
- return disposeResources(processedResources).then(() => {
115
- if (handlerSyncError) {
116
- throw handlerSyncError;
117
- }
118
- return handlerResult;
119
- });
118
+ return handlerPromise
119
+ .tap(() => {
120
+ return disposeResources(processedResources);
121
+ })
122
+ .tapCatch(() => {
123
+ return disposeResources(processedResources);
124
+ });
120
125
  });
121
126
  }
122
127
 
package/lib/util.js CHANGED
@@ -9,43 +9,34 @@
9
9
  * @param {*} fn - The value to check
10
10
  * @returns {boolean} - True if the function is a class, false otherwise
11
11
  */
12
+ const thisAssignmentPattern = /this\s*\.\s*\S+\s*=/;
12
13
  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
14
  try {
19
- const fnStr = fn.toString();
20
- if (fnStr.startsWith("class ") || /^class\s+/.test(fnStr)) {
21
- return true;
15
+ if (typeof fn === "function") {
16
+ const keys = Object.getOwnPropertyNames(fn.prototype);
17
+
18
+ const fnStr = fn.toString();
19
+ const es6Class = fnStr.startsWith("class ") || /^class\s+/.test(fnStr);
20
+ const hasMethods = keys.length > 1;
21
+ const hasMethodsOtherThanConstructor =
22
+ keys.length > 0 && !(keys.length === 1 && keys[0] === "constructor");
23
+ const hasThisAssignmentAndStaticMethods =
24
+ thisAssignmentPattern.test(fnStr) &&
25
+ Object.getOwnPropertyNames(fn).length > 0;
26
+
27
+ if (
28
+ es6Class ||
29
+ hasMethods ||
30
+ hasMethodsOtherThanConstructor ||
31
+ hasThisAssignmentAndStaticMethods
32
+ ) {
33
+ return true;
34
+ }
22
35
  }
36
+ return false;
23
37
  } catch (e) {
24
- // Ignore errors that might occur when calling toString()
25
- // Some objects might have custom toString implementations that throw
26
38
  return false;
27
39
  }
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
40
  }
50
41
 
51
42
  const rident = /^[a-z$_][a-z$_0-9]*$/i;
@@ -127,12 +118,25 @@ function isPromise(obj) {
127
118
  );
128
119
  }
129
120
 
121
+ // istanbul ignore next
122
+ const emptyFatArrow = () => {};
123
+ // istanbul ignore next
124
+ const emptyFunction = function () {};
125
+
130
126
  const defaultExcluded = [
131
127
  Object.getPrototypeOf(Array), // Array.prototype
132
128
  Object.getPrototypeOf(Object), // Object.prototype
133
129
  Object.getPrototypeOf(Function), // Function.prototype
130
+ Object.getPrototypeOf([]),
131
+ Object.getPrototypeOf({}),
132
+ Object.getPrototypeOf(emptyFatArrow),
133
+ Object.getPrototypeOf(emptyFunction),
134
134
  ];
135
135
 
136
+ function isExcludedPrototype(proto) {
137
+ return defaultExcluded.includes(proto);
138
+ }
139
+
136
140
  /**
137
141
  * Gets all property keys from an object and its prototype chain, excluding standard
138
142
  * prototypes like Object.prototype, Array.prototype, and Function.prototype
@@ -187,3 +191,4 @@ module.exports.isPromisified = isPromisified;
187
191
  module.exports.isPromise = isPromise;
188
192
  module.exports.triggerUncaughtException = triggerUncaughtException;
189
193
  module.exports.getObjectKeys = getObjectKeys;
194
+ module.exports.isExcludedPrototype = isExcludedPrototype;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aveazul",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Bluebird-like APIs in extended native Promise",
5
5
  "main": "lib/aveazul.js",
6
6
  "homepage": "https://github.com/jchip/aveazul",
@@ -11,7 +11,9 @@
11
11
  "test:coverage": "jest test --coverage",
12
12
  "test:bluebird": "USE_BLUEBIRD=true jest test",
13
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"
14
+ "test:debug:bluebird": "USE_BLUEBIRD=true node --inspect-brk node_modules/.bin/jest --runInBand --no-coverage",
15
+ "jest": "jest --no-coverage",
16
+ "jest:bluebird": "USE_BLUEBIRD=true jest --no-coverage"
15
17
  },
16
18
  "author": "Joel Chen",
17
19
  "files": [