@flex-development/when 2.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,9 +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,
7
- * i.e. a {@linkcode PromiseLike} object.
8
+ * Check if `value` looks like a {@linkcode Thenable}.
9
+ *
10
+ * @see {@linkcode Thenable}
8
11
  *
9
12
  * @template {any} T
10
13
  * The resolved value
@@ -13,15 +16,34 @@
13
16
  *
14
17
  * @param {unknown} value
15
18
  * The thing to check
16
- * @return {value is PromiseLike<T>}
19
+ * @return {value is Thenable<T>}
17
20
  * `true` if `value` is an object or function with a `then` method,
18
- * `false` otherwise
21
+ * and maybe-callable methods `catch` and/or `finally`, `false` otherwise
19
22
  */
20
23
  function isThenable(value) {
21
- if (!value)
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']))
22
31
  return false;
23
- if (typeof value !== 'function' && typeof value !== 'object')
32
+ // cannot be a thenable, invalid `finally` property.
33
+ if (!maybeCallable(value['finally']))
24
34
  return false;
25
- return 'then' in value && typeof value.then === 'function';
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';
26
49
  }
27
- export default isThenable;
package/dist/lib/when.mjs CHANGED
@@ -2,7 +2,8 @@
2
2
  * @file when
3
3
  * @module when/lib/when
4
4
  */
5
- import isPromise from '#lib/is-promise';
5
+ import isCatchable from '#lib/is-catchable';
6
+ import isFinalizable from '#lib/is-finalizable';
6
7
  import isThenable from '#lib/is-thenable';
7
8
  export default when;
8
9
  /**
@@ -29,40 +30,90 @@ export default when;
29
30
  * The next awaitable
30
31
  */
31
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;
32
45
  if (typeof chain === 'object') {
33
46
  fail = chain.fail;
47
+ finish = chain.finish;
34
48
  context = chain.context;
35
49
  args = chain.args ?? [];
36
50
  chain = chain.chain;
37
51
  }
38
- // no promise, call chain function immediately.
52
+ // no thenable, call chain function immediately.
39
53
  if (!isThenable(value)) {
40
54
  try {
41
- // try attaching "global" rejection handler with `catch`.
42
- return katch(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)));
43
58
  }
44
59
  catch (e) {
45
- return failure(e);
60
+ return finalize(katch(failure(e)));
46
61
  }
47
62
  }
48
- // already have a promise, chain the chain callback.
63
+ // already have a thenable, chain the chain callback.
49
64
  value = value.then(res => chain.call(context, ...args, res), failure);
50
- // try attaching "global" rejection handler with `catch`.
51
- return katch(value);
65
+ // try attaching "global" rejection handler with `catch`,
66
+ // then try running `finish`, or attaching it with `finally`.
67
+ return finalize(katch(value));
52
68
  /**
53
69
  * @this {void}
54
70
  *
55
71
  * @param {unknown} e
56
72
  * The error to handle
57
73
  * @return {unknown}
58
- * The rejection result
59
- * @throws {unknown}
74
+ * The rejection result or never, may throw `e`
60
75
  */
61
76
  function failure(e) {
62
77
  if (typeof fail !== 'function')
63
- throw e;
78
+ return thrower(e);
64
79
  return fail.call(context, e);
65
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
+ }
66
117
  /**
67
118
  * Try attaching a rejection handler with `catch`.
68
119
  *
@@ -74,8 +125,21 @@ function when(value, chain, fail, context, ...args) {
74
125
  * The `value`
75
126
  */
76
127
  function katch(value) {
77
- if (isPromise(value))
128
+ if (isCatchable(value))
78
129
  value = value.catch(failure);
79
130
  return value;
80
131
  }
132
+ /**
133
+ * @this {void}
134
+ *
135
+ * @param {unknown} e
136
+ * The error to throw
137
+ * @return {never}
138
+ * Never; throws `e`
139
+ * @throws {unknown}
140
+ */
141
+ function thrower(e) {
142
+ void fin();
143
+ throw e;
144
+ }
81
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';
@@ -0,0 +1,350 @@
1
+ /**
2
+ * @file createThenable
3
+ * @module when/testing/lib/createThenable
4
+ */
5
+ import isThenable from '#lib/is-thenable';
6
+ export default createThenable;
7
+ /**
8
+ * Create a thenable.
9
+ *
10
+ * The returned object conforms to {@linkcode Thenable} and ensures `then`
11
+ * always returns another {@linkcode Thenable}, even when adopting a foreign
12
+ * thenable.
13
+ *
14
+ * When `options` is omitted, `null`, or `undefined`, the returned thenable is
15
+ * *modern* (a thenable with `then`, `catch`, and `finally` methods).
16
+ * Pass an options object (e.g. `{}`) to start from a *bare* (`then` method
17
+ * only) thenable and selectively enable methods.
18
+ *
19
+ * @see {@linkcode CreateThenableOptions}
20
+ * @see {@linkcode Executor}
21
+ * @see {@linkcode Thenable}
22
+ *
23
+ * @template {any} T
24
+ * The resolved value
25
+ * @template {any} [Reason=Error]
26
+ * The reason for a rejection
27
+ * @template {Thenable<T>} [Result=Thenable<T>]
28
+ * The thenable
29
+ *
30
+ * @this {void}
31
+ *
32
+ * @param {Executor<T, Reason>} executor
33
+ * The initialization callback
34
+ * @param {CreateThenableOptions | null | undefined} [options]
35
+ * Options for creating a thenable
36
+ * @return {Result}
37
+ * The thenable
38
+ */
39
+ function createThenable(executor, options) {
40
+ /**
41
+ * Whether the thenable has been settled.
42
+ *
43
+ * @var {boolean} settled
44
+ */
45
+ let settled = false;
46
+ /**
47
+ * The settled state.
48
+ *
49
+ * @var {State<T, Reason> | undefined} state
50
+ */
51
+ let state;
52
+ try {
53
+ executor(ok, nok);
54
+ }
55
+ catch (e) {
56
+ nok(e);
57
+ }
58
+ // executor neither resolved nor rejected, treat as `ok(undefined)`.
59
+ if (!state)
60
+ ok(undefined);
61
+ return wrapState(state);
62
+ /**
63
+ * Adopt an awaitable into a {@linkcode Thenable}.
64
+ *
65
+ * If `x` is a thenable, the returned thenable follows its settlement.
66
+ * Otherwise, the returned thenable is immediately resolved with `x`.
67
+ *
68
+ * @template {any} X
69
+ * The resolved value
70
+ *
71
+ * @this {void}
72
+ *
73
+ * @param {Awaitable<X>} x
74
+ * The awaitable to adopt
75
+ * @return {Thenable<X>}
76
+ * A thenable representing the adopted awaitable
77
+ */
78
+ function adopt(x) {
79
+ if (isThenable(x)) {
80
+ // wrap foreign thenable so `then` still returns a thenable.
81
+ return addMethods({
82
+ /**
83
+ * @template {any} [Next=X]
84
+ * The next resolved value on success
85
+ * @template {any} [Failure=never]
86
+ * The next resolved value on failure
87
+ *
88
+ * @this {void}
89
+ *
90
+ * @param {OnFulfilled<X, Next> | null | undefined} [onfulfilled]
91
+ * The callback to execute when the thenable is resolved
92
+ * @param {OnRejected<Failure, Reason> | null | undefined} [onrejected]
93
+ * The callback to execute when the thenable is rejected
94
+ * @return {Thenable<Failure | Next>}
95
+ * The next thenable
96
+ */
97
+ then(onfulfilled, onrejected) {
98
+ return adopt(x.then(
99
+ /**
100
+ * @this {void}
101
+ *
102
+ * @param {X} value
103
+ * The resolved value
104
+ * @return {Awaitable<Next>}
105
+ * The next awaitable
106
+ */
107
+ function succ(value) {
108
+ return resolve(value, onfulfilled);
109
+ },
110
+ /**
111
+ * @this {void}
112
+ *
113
+ * @param {unknown} e
114
+ * The reason for the rejection
115
+ * @return {Awaitable<Failure>}
116
+ * The next awaitable
117
+ */
118
+ function fail(e) {
119
+ return reject(e, onrejected);
120
+ }));
121
+ }
122
+ });
123
+ }
124
+ return wrapState({ ok: true, value: x });
125
+ }
126
+ /**
127
+ * Add requested methods to a `thenable`.
128
+ *
129
+ * @template {any} U
130
+ * The resolved value
131
+ *
132
+ * @this {void}
133
+ *
134
+ * @param {Thenable<U>} thenable
135
+ * The current thenable
136
+ * @return {Thenable<U>}
137
+ * The `thenable`
138
+ */
139
+ function addMethods(thenable) {
140
+ if (!options) {
141
+ thenable.catch = katch;
142
+ thenable.finally = finalize;
143
+ return thenable;
144
+ }
145
+ if (options.catch) {
146
+ thenable.catch = katch;
147
+ }
148
+ else if ('catch' in options && options.catch !== false) {
149
+ thenable.catch = options.catch;
150
+ }
151
+ if (options.finally) {
152
+ thenable.finally = finalize;
153
+ }
154
+ else if ('finally' in options && options.finally !== false) {
155
+ thenable.finally = options.finally;
156
+ }
157
+ return thenable;
158
+ }
159
+ /**
160
+ * Attach a callback that is invoked only when `this` thenable is settled.
161
+ *
162
+ * @template {any} U
163
+ * The resolved value
164
+ *
165
+ * @this {Thenable<U>}
166
+ *
167
+ * @param {OnFinally | null | undefined} [onfinally]
168
+ * The callback to execute when the thenable is settled
169
+ * @return {Thenable<U>}
170
+ * The next thenable
171
+ */
172
+ function finalize(onfinally) {
173
+ return typeof onfinally === 'function' ? this.then(succ, fail) : this;
174
+ /**
175
+ * @this {void}
176
+ *
177
+ * @param {unknown} reason
178
+ * The reason for the rejection
179
+ * @return {never}
180
+ * Never; throws `reason`
181
+ * @throws {unknown}
182
+ */
183
+ function fail(reason) {
184
+ void onfinally();
185
+ throw reason;
186
+ }
187
+ /**
188
+ * @this {void}
189
+ *
190
+ * @param {U} value
191
+ * The resolved value
192
+ * @return {U}
193
+ * The resolved `value`
194
+ */
195
+ function succ(value) {
196
+ return void onfinally(), value;
197
+ }
198
+ }
199
+ /**
200
+ * Attach a callback only for the rejection of `this` thenable.
201
+ *
202
+ * @template {any} U
203
+ * The resolved value
204
+ * @template {any} [Failure=never]
205
+ * The resolved value on failure
206
+ *
207
+ * @this {Thenable<U>}
208
+ *
209
+ * @param {OnRejected<Failure, Reason> | null | undefined} [onrejected]
210
+ * The callback to execute when the thenable is rejected
211
+ * @return {Thenable<Failure | U>}
212
+ * The next thenable
213
+ */
214
+ function katch(onrejected) {
215
+ return this.then(null, onrejected);
216
+ }
217
+ /**
218
+ * Reject the thenable.
219
+ *
220
+ * @this {void}
221
+ *
222
+ * @param {Reason} reason
223
+ * The reason for the rejection
224
+ * @return {undefined}
225
+ */
226
+ function nok(reason) {
227
+ return void (settled || (settled = true, state = { ok: false, reason }));
228
+ }
229
+ /**
230
+ * Resolve the thenable.
231
+ *
232
+ * @this {void}
233
+ *
234
+ * @param {Awaitable<T>} value
235
+ * The awaitable
236
+ * @return {undefined}
237
+ */
238
+ function ok(value) {
239
+ return void (settled || (settled = true, state = { ok: true, value }));
240
+ }
241
+ /**
242
+ * Handle a rejection.
243
+ *
244
+ * @template {any} T
245
+ * The next resolved value
246
+ *
247
+ * @this {void}
248
+ *
249
+ * @param {unknown} reason
250
+ * The reason for the rejection
251
+ * @param {OnRejected<T, Reason> | null | undefined} [onrejected]
252
+ * The callback to execute when the thenable is rejected
253
+ * @return {Awaitable<T>}
254
+ * The next awaitable
255
+ */
256
+ function reject(reason, onrejected) {
257
+ if (typeof onrejected === 'function')
258
+ return onrejected(reason);
259
+ return wrapState({ ok: false, reason: reason });
260
+ }
261
+ /**
262
+ * Handle a resolved `value`.
263
+ *
264
+ * @template {any} T
265
+ * The resolved value
266
+ * @template {any} Next
267
+ * The next resolved value
268
+ * @this {void}
269
+ *
270
+ * @param {T} value
271
+ * The resolved value
272
+ * @return {Awaitable<Next>}
273
+ * The next awaitable
274
+ */
275
+ function resolve(value, onfulfilled) {
276
+ if (typeof onfulfilled === 'function')
277
+ return onfulfilled(value);
278
+ return value;
279
+ }
280
+ /**
281
+ * Wrap a settled state into a {@linkcode Thenable}.
282
+ *
283
+ * @template {any} S
284
+ * The resolved value
285
+ *
286
+ * @this {void}
287
+ *
288
+ * @param {State} s
289
+ * The settled state
290
+ * @return {Thenable<S>}
291
+ * A thenable representing the settled state
292
+ */
293
+ function wrapState(s) {
294
+ return addMethods({
295
+ /**
296
+ * @template {any} [Next=S]
297
+ * The next resolved value on success
298
+ * @template {any} [Failure=never]
299
+ * The next resolved value on failure
300
+ *
301
+ * @this {void}
302
+ *
303
+ * @param {OnFulfilled<S, Next> | null | undefined} [onfulfilled]
304
+ * The callback to execute when the thenable is resolved
305
+ * @param {OnRejected<Failure, Reason> | null | undefined} [onrejected]
306
+ * The callback to execute when the thenable is rejected
307
+ * @return {Thenable<Failure | Next>}
308
+ * The next thenable
309
+ */
310
+ then(onfulfilled, onrejected) {
311
+ try {
312
+ if (s.ok) {
313
+ if (isThenable(s.value))
314
+ return adopt(s.value).then(succ, fail);
315
+ return adopt(succ(s.value));
316
+ /**
317
+ * @this {void}
318
+ *
319
+ * @param {S} value
320
+ * The resolved value
321
+ * @return {Awaitable<Next>}
322
+ * The next awaitable
323
+ */
324
+ function succ(value) {
325
+ return resolve(value, onfulfilled);
326
+ }
327
+ }
328
+ // rejected.
329
+ if (typeof onrejected === 'function')
330
+ return adopt(fail(s.reason));
331
+ return wrapState({ ok: false, reason: s.reason });
332
+ }
333
+ catch (e) {
334
+ return wrapState({ ok: false, reason: e });
335
+ }
336
+ /**
337
+ * @this {void}
338
+ *
339
+ * @param {unknown} e
340
+ * The reason for the rejection
341
+ * @return {Awaitable<Failure>}
342
+ * The next awaitable
343
+ */
344
+ function fail(e) {
345
+ return reject(e, onrejected);
346
+ }
347
+ }
348
+ });
349
+ }
350
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @file Entry Point - Library
3
+ * @module when/testing/lib
4
+ */
5
+ export { default as createThenable } from '#testing/lib/create-thenable';