@flex-development/when 1.0.0 → 3.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.
@@ -2,5 +2,9 @@
2
2
  * @file Entry Point - Library
3
3
  * @module when/lib
4
4
  */
5
- export { default as isPromiseLike, default as isThenable } from '#lib/is-thenable';
5
+ export { default as isCatchable } from '#lib/is-catchable';
6
+ export { default as isFinalizable } from '#lib/is-finalizable';
7
+ export { default as isPromise } from '#lib/is-promise';
8
+ export { default as isPromiseLike } from '#lib/is-promise-like';
9
+ export { default as isThenable } from '#lib/is-thenable';
6
10
  export { default as when } from '#lib/when';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @file isCatchable
3
+ * @module when/lib/isCatchable
4
+ */
5
+ import isThenable from '#lib/is-thenable';
6
+ /**
7
+ * Check if `value` looks like a `Thenable` that can be caught.
8
+ *
9
+ * @see {@linkcode Catchable}
10
+ *
11
+ * @template {any} T
12
+ * The resolved value
13
+ *
14
+ * @this {void}
15
+ *
16
+ * @param {unknown} value
17
+ * The thing to check
18
+ * @return {value is Catchable<T>}
19
+ * `true` if `value` is a thenable with a `catch` method, `false` otherwise
20
+ */
21
+ function isCatchable(value) {
22
+ return isThenable(value) && typeof value.catch === 'function';
23
+ }
24
+ export default isCatchable;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @file isFinalizable
3
+ * @module when/lib/isFinalizable
4
+ */
5
+ import isThenable from '#lib/is-thenable';
6
+ /**
7
+ * Check if `value` looks like a thenable that can be finalized.
8
+ *
9
+ * @see {@linkcode Finalizable}
10
+ *
11
+ * @template {any} T
12
+ * The resolved value
13
+ *
14
+ * @this {void}
15
+ *
16
+ * @param {unknown} value
17
+ * The thing to check
18
+ * @return {value is Finalizable<T>}
19
+ * `true` if `value` is a thenable with a `finally` method, `false` otherwise
20
+ */
21
+ function isFinalizable(value) {
22
+ return isThenable(value) && typeof value.finally === 'function';
23
+ }
24
+ export default isFinalizable;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @file isPromiseLike
3
+ * @module when/lib/isPromiseLike
4
+ */
5
+ /**
6
+ * Check if `value` looks like a {@linkcode PromiseLike} structure.
7
+ *
8
+ * @template {any} T
9
+ * The resolved value
10
+ *
11
+ * @this {void}
12
+ *
13
+ * @param {unknown} value
14
+ * The thing to check
15
+ * @return {value is PromiseLike<T>}
16
+ * `true` if `value` is an object or function with a `then` method,
17
+ * `false` otherwise
18
+ */
19
+ function isPromiseLike(value) {
20
+ if (typeof value !== 'function' && typeof value !== 'object')
21
+ return false;
22
+ return !!value && 'then' in value && typeof value.then === 'function';
23
+ }
24
+ export default isPromiseLike;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @file isPromise
3
+ * @module when/lib/isPromise
4
+ */
5
+ import isCatchable from '#lib/is-catchable';
6
+ import isFinalizable from '#lib/is-finalizable';
7
+ import isThenable from '#lib/is-thenable';
8
+ /**
9
+ * Check if `value` looks like a {@linkcode Promise}.
10
+ *
11
+ * > 👉 **Note**: This function intentionally performs structural checks
12
+ * > instead of brand checks.
13
+ * > It does not rely on `instanceof Promise` or constructors, making it
14
+ * > compatible with cross-realm promises and custom thenables.
15
+ *
16
+ * @see {@linkcode isThenable}
17
+ *
18
+ * @template {any} T
19
+ * The resolved value
20
+ *
21
+ * @this {void}
22
+ *
23
+ * @param {unknown} value
24
+ * The thing to check
25
+ * @param {boolean | null | undefined} [finalizable=true]
26
+ * Whether a `finally` method is required.\
27
+ * When `false`, only `then` and `catch` are checked
28
+ * @return {value is Promise<T>}
29
+ * `true` if `value` is a thenable with a `catch` method,
30
+ * and `finally` method (if requested), `false` otherwise
31
+ */
32
+ function isPromise(value, finalizable) {
33
+ if (!(isThenable(value) && isCatchable(value)))
34
+ return false;
35
+ return finalizable ?? true ? isFinalizable(value) : true;
36
+ }
37
+ export default isPromise;
@@ -2,8 +2,12 @@
2
2
  * @file isThenable
3
3
  * @module when/lib/isThenable
4
4
  */
5
+ import hasThen from '#lib/is-promise-like';
6
+ export default isThenable;
5
7
  /**
6
- * Check if `value` looks like a thenable.
8
+ * Check if `value` looks like a {@linkcode Thenable}.
9
+ *
10
+ * @see {@linkcode Thenable}
7
11
  *
8
12
  * @template {any} T
9
13
  * The resolved value
@@ -12,14 +16,34 @@
12
16
  *
13
17
  * @param {unknown} value
14
18
  * The thing to check
15
- * @return {value is PromiseLike<T>}
16
- * `true` if `value` is a thenable, `false` otherwise
19
+ * @return {value is Thenable<T>}
20
+ * `true` if `value` is an object or function with a `then` method,
21
+ * and maybe-callable methods `catch` and/or `finally`, `false` otherwise
17
22
  */
18
23
  function isThenable(value) {
19
- return (!Array.isArray(value) &&
20
- typeof value === 'object' &&
21
- value !== null &&
22
- 'then' in value &&
23
- typeof value.then === 'function');
24
+ if (!hasThen(value))
25
+ return false; // no `then` method, cannot be a thenable.
26
+ // a thenable without a `catch` or `finally` method.
27
+ if (!('catch' in value) && !('finally' in value))
28
+ return true;
29
+ // cannot be a thenable, invalid `catch` property.
30
+ if (!maybeCallable(value['catch']))
31
+ return false;
32
+ // cannot be a thenable, invalid `finally` property.
33
+ if (!maybeCallable(value['finally']))
34
+ return false;
35
+ return true; // thenable.
36
+ }
37
+ /**
38
+ * @internal
39
+ *
40
+ * @this {void}
41
+ *
42
+ * @param {unknown} value
43
+ * The thing to check
44
+ * @return {((...args: any[]) => any) | null | undefined}
45
+ * `true` if `value` is a function, `null`, or `undefined`, `false` otherwise
46
+ */
47
+ function maybeCallable(value) {
48
+ return value == null || typeof value === 'function';
24
49
  }
25
- export default isThenable;
package/dist/lib/when.mjs CHANGED
@@ -2,61 +2,144 @@
2
2
  * @file when
3
3
  * @module when/lib/when
4
4
  */
5
+ import isCatchable from '#lib/is-catchable';
6
+ import isFinalizable from '#lib/is-finalizable';
5
7
  import isThenable from '#lib/is-thenable';
6
8
  export default when;
7
9
  /**
8
10
  * Chain a callback, calling the function after `value` is resolved,
9
- * or immediately if `value` is not thenable.
11
+ * or immediately if `value` is not a thenable.
10
12
  *
11
13
  * @see {@linkcode Chain}
12
14
  * @see {@linkcode Options}
13
- * @see {@linkcode Reject}
15
+ * @see {@linkcode Fail}
14
16
  *
15
17
  * @this {void}
16
18
  *
17
19
  * @param {unknown} value
18
- * The promise or the resolved value
20
+ * The current awaitable
19
21
  * @param {Chain<any, unknown> | Options} chain
20
22
  * The chain callback or options for chaining
21
- * @param {Reject | null | undefined} [reject]
22
- * The callback to fire when a promise is rejected or an error is thrown
23
+ * @param {Fail | null | undefined} [fail]
24
+ * The callback to fire when a failure occurs
23
25
  * @param {unknown} [context]
24
- * The `this` context of the chain and error callbacks
26
+ * The `this` context of the chain and `fail` callbacks
25
27
  * @param {unknown[]} args
26
28
  * The arguments to pass to the chain callback
27
29
  * @return {unknown}
28
- * The next promise or value
30
+ * The next awaitable
29
31
  */
30
- function when(value, chain, reject, context, ...args) {
32
+ function when(value, chain, fail, context, ...args) {
33
+ /**
34
+ * The post-processing hook.
35
+ *
36
+ * @var {Finish | null | undefined}
37
+ */
38
+ let finish;
39
+ /**
40
+ * Whether the post-processing hook ran.
41
+ *
42
+ * @var {boolean}
43
+ */
44
+ let finished = false;
31
45
  if (typeof chain === 'object') {
32
- reject = chain.reject;
46
+ fail = chain.fail;
47
+ finish = chain.finish;
33
48
  context = chain.context;
34
49
  args = chain.args ?? [];
35
50
  chain = chain.chain;
36
51
  }
37
- // no promise, call chain function immediately.
52
+ // no thenable, call chain function immediately.
38
53
  if (!isThenable(value)) {
39
54
  try {
40
- return chain.call(context, ...args, value);
55
+ // try attaching "global" rejection handler with `catch`,
56
+ // then try running `finish`, or attaching it with `finally`.
57
+ return finalize(katch(chain.call(context, ...args, value)));
41
58
  }
42
59
  catch (e) {
43
- return fail(e);
60
+ return finalize(katch(failure(e)));
44
61
  }
45
62
  }
46
- // already have a promise, chain callback.
47
- return value.then(resolved => chain.call(context, ...args, resolved), fail);
63
+ // already have a thenable, chain the chain callback.
64
+ value = value.then(res => chain.call(context, ...args, res), failure);
65
+ // try attaching "global" rejection handler with `catch`,
66
+ // then try running `finish`, or attaching it with `finally`.
67
+ return finalize(katch(value));
48
68
  /**
49
69
  * @this {void}
50
70
  *
51
71
  * @param {unknown} e
52
72
  * The error to handle
53
73
  * @return {unknown}
54
- * The rejection result
74
+ * The rejection result or never, may throw `e`
75
+ */
76
+ function failure(e) {
77
+ if (typeof fail !== 'function')
78
+ return thrower(e);
79
+ return fail.call(context, e);
80
+ }
81
+ /**
82
+ * @this {void}
83
+ *
84
+ * @return {undefined}
85
+ */
86
+ function fin() {
87
+ return void (finished || (finished = true, finish?.call(context)));
88
+ }
89
+ /**
90
+ * @this {void}
91
+ *
92
+ * @param {unknown} value
93
+ * The awaitable
94
+ * @return {unknown}
95
+ * The `value`
96
+ */
97
+ function finalize(value) {
98
+ if (typeof finish === 'function') {
99
+ if (isFinalizable(value))
100
+ return value.finally(fin);
101
+ if (isThenable(value))
102
+ return value.then(identity, thrower);
103
+ }
104
+ return identity(value);
105
+ /**
106
+ * @this {void}
107
+ *
108
+ * @param {unknown} value
109
+ * The resolved value
110
+ * @return {unknown}
111
+ * The `value`
112
+ */
113
+ function identity(value) {
114
+ return fin(), value;
115
+ }
116
+ }
117
+ /**
118
+ * Try attaching a rejection handler with `catch`.
119
+ *
120
+ * @this {void}
121
+ *
122
+ * @param {unknown} value
123
+ * The awaitable
124
+ * @return {unknown}
125
+ * The `value`
126
+ */
127
+ function katch(value) {
128
+ if (isCatchable(value))
129
+ value = value.catch(failure);
130
+ return value;
131
+ }
132
+ /**
133
+ * @this {void}
134
+ *
135
+ * @param {unknown} e
136
+ * The error to throw
137
+ * @return {never}
138
+ * Never; throws `e`
55
139
  * @throws {unknown}
56
140
  */
57
- function fail(e) {
58
- if (typeof reject !== 'function')
59
- throw e;
60
- return reject.call(context, e);
141
+ function thrower(e) {
142
+ void fin();
143
+ throw e;
61
144
  }
62
145
  }
@@ -0,0 +1,149 @@
1
+ import type { Thenable, Awaitable } from '@flex-development/when';
2
+
3
+ /**
4
+ * @file Interfaces - CreateThenableOptions
5
+ * @module when/testing/interfaces/CreateThenableOptions
6
+ */
7
+ /**
8
+ * Options for creating a thenable.
9
+ */
10
+ interface CreateThenableOptions {
11
+ /**
12
+ * Control whether returned thenables implement a `catch` method.
13
+ *
14
+ * When an options object is omitted, `null`, or `undefined`,
15
+ * the method will be implemented.
16
+ *
17
+ * When an options object is provided, `catch` is only implemented
18
+ * if `options.catch` is `true`.
19
+ *
20
+ * If `options.catch` is `null` or `undefined`, the thenable's `catch`
21
+ * property will have the same value.\
22
+ * Pass `false` to disable the method implementation.
23
+ */
24
+ catch?: boolean | null | undefined;
25
+ /**
26
+ * Control whether returned thenables implement a `finally` method.
27
+ *
28
+ * When an options object is omitted, `null`, or `undefined`,
29
+ * the method will be implemented.
30
+ *
31
+ * When an options object is provided, `finally` is only implemented
32
+ * if `options.finally` is `true`.
33
+ *
34
+ * If `options.finally` is `null` or `undefined`, the thenable's `finally`
35
+ * property will have the same value.\
36
+ * Pass `false` to disable the method implementation.
37
+ */
38
+ finally?: boolean | null | undefined;
39
+ }
40
+
41
+ /**
42
+ * @file createThenable
43
+ * @module when/testing/lib/createThenable
44
+ */
45
+
46
+ /**
47
+ * Create a thenable.
48
+ *
49
+ * The returned object conforms to {@linkcode Thenable} and ensures `then`
50
+ * always returns another {@linkcode Thenable}, even when adopting a foreign
51
+ * thenable.
52
+ *
53
+ * When `options` is omitted, `null`, or `undefined`, the returned thenable is
54
+ * *modern* (a thenable with `then`, `catch`, and `finally` methods).
55
+ * Pass an options object (e.g. `{}`) to start from a *bare* (`then` method
56
+ * only) thenable and selectively enable methods.
57
+ *
58
+ * @see {@linkcode CreateThenableOptions}
59
+ * @see {@linkcode Executor}
60
+ * @see {@linkcode Thenable}
61
+ *
62
+ * @template {any} T
63
+ * The resolved value
64
+ * @template {any} [Reason=Error]
65
+ * The reason for a rejection
66
+ * @template {Thenable<T>} [Result=Thenable<T>]
67
+ * The thenable
68
+ *
69
+ * @this {void}
70
+ *
71
+ * @param {Executor<T, Reason>} executor
72
+ * The initialization callback
73
+ * @param {CreateThenableOptions | null | undefined} [options]
74
+ * Options for creating a thenable
75
+ * @return {Result}
76
+ * The thenable
77
+ */
78
+ declare function createThenable<T, Reason = Error, Result extends Thenable<T> = Thenable<T>>(this: void, executor: Executor<T, Reason>, options?: CreateThenableOptions | null | undefined): Result;
79
+
80
+ /**
81
+ * @file Type Aliases - Executor
82
+ * @module when/testing/types/Executor
83
+ */
84
+
85
+ /**
86
+ * The callback used to initialize a thenable.
87
+ *
88
+ * @see {@linkcode Awaitable}
89
+ * @see {@linkcode Resolve}
90
+ * @see {@linkcode Reject}
91
+ *
92
+ * @template {any} [T=any]
93
+ * The resolved value
94
+ * @template {any} [Reason=Error]
95
+ * The reason for a rejection
96
+ *
97
+ * @this {void}
98
+ *
99
+ * @param {Resolve<T>} resolve
100
+ * The callback used to resolve the thenable with a value
101
+ * or the result of another awaitable
102
+ * @param {Reject<Reason>} reject
103
+ * The callback used to reject the thenable with a provided reason or error
104
+ * @return {undefined | void}
105
+ */
106
+ type Executor<T = any, Reason = Error> = (this: void, resolve: Resolve<T>, reject: Reject<Reason>) => undefined | void;
107
+
108
+ /**
109
+ * @file Type Aliases - Reject
110
+ * @module when/testing/types/Reject
111
+ */
112
+ /**
113
+ * The callback used to reject a thenable with a provided reason or error.
114
+ *
115
+ * @template {any} [Reason=Error]
116
+ * The reason for the rejection
117
+ *
118
+ * @this {void}
119
+ *
120
+ * @param {Reason} reason
121
+ * The reason for the rejection
122
+ * @return {undefined}
123
+ */
124
+ type Reject<Reason = Error> = (this: void, reason: Reason) => undefined;
125
+
126
+ /**
127
+ * @file Type Aliases - Resolve
128
+ * @module when/testing/types/Resolve
129
+ */
130
+
131
+ /**
132
+ * The callback used to resolve a thenable with a value
133
+ * or the result of another awaitable.
134
+ *
135
+ * @see {@linkcode Awaitable}
136
+ *
137
+ * @template {any} [T=any]
138
+ * The resolved value
139
+ *
140
+ * @this {void}
141
+ *
142
+ * @param {Awaitable<T>} value
143
+ * The awaitable
144
+ * @return {undefined}
145
+ */
146
+ type Resolve<T = any> = (this: void, value: Awaitable<T>) => undefined;
147
+
148
+ export { createThenable };
149
+ export type { CreateThenableOptions, Executor, Reject, Resolve };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @file Package Entry Point
3
+ * @module when/testing
4
+ */
5
+ export * from '#testing/lib/index';