@youngspe/async-scope 0.1.0-dev.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.
@@ -0,0 +1,252 @@
1
+ import { CancellationError, toError } from '../error.js';
2
+ import * as Symbols from '../symbols.js';
3
+ import { isArray, isIterable } from '../utils.js';
4
+ import { Subscription } from '../events/sub.js';
5
+ import { CancelEvent } from '../token.js';
6
+ export class Token {
7
+ /** If `true`, this token has been _defused_, meaning it is guaranteed never to be cancelled. */
8
+ get isDefused() {
9
+ return false;
10
+ }
11
+ /**
12
+ * If `true`, this token has been _cancelled_, and trigger any additional listeners.
13
+ *
14
+ * This is equivalent to `token.error !== undefined`
15
+ *
16
+ * @see {@link Token#error}
17
+ */
18
+ get isCancelled() {
19
+ return this.error !== undefined;
20
+ }
21
+ /**
22
+ * Adds listeners
23
+ *
24
+ * @param listeners - listeners that should be added to this token.
25
+ * Each parameter may be:
26
+ * - A {@link CancellableLike}:
27
+ * - A {@link Cancellable}
28
+ * - A {@link Disposable}
29
+ * - An {@link AsyncDisposable}
30
+ * - A falsy value, which will be ignored
31
+ * - A (possibly nested) array of any of the above.
32
+ * - A function that receives an {@link Error} and optionally returns a promise.
33
+ *
34
+ * @returns
35
+ * - A {@link Subscription} that may be used to pause, resume, or remove the provide listeners if either the listeners were safely added or the token is defused.
36
+ * - `undefined` if no listener could be safely added. This is typically due to the token being cancelled
37
+ * and indicates any future attempts will also fail.
38
+ */
39
+ add(...listeners) {
40
+ if (this.isCancelled)
41
+ return undefined;
42
+ if (this.isDefused)
43
+ return Subscription.noop;
44
+ const subs = [];
45
+ for (let listener of listeners) {
46
+ if (!listener)
47
+ continue;
48
+ if (isArray(listener)) {
49
+ this.add(...listener);
50
+ continue;
51
+ }
52
+ if (typeof listener === 'function') {
53
+ listener = { cancel: listener };
54
+ }
55
+ const sub = this.addOne(listener);
56
+ if (!sub)
57
+ return undefined;
58
+ subs.push(sub);
59
+ }
60
+ return Subscription.collect(subs);
61
+ }
62
+ use(target) {
63
+ const { error } = this;
64
+ if (error)
65
+ throw error;
66
+ this.add(target);
67
+ return target;
68
+ }
69
+ /**
70
+ * Attempts to add a listener to this token.
71
+ * Returns the target if successfully added, or `undefined` if not added.
72
+ */
73
+ tryUse(listener) {
74
+ return this.add(listener) ? listener : undefined;
75
+ }
76
+ #signal;
77
+ /**
78
+ * An {@link AbortSignal} representing this token.
79
+ * When this token is cancelled, the returned signal will be aborted.
80
+ * When this token is defused, a new signal is generated on each access.
81
+ */
82
+ get signal() {
83
+ if (this.#signal)
84
+ return this.#signal;
85
+ if (this.isDefused) {
86
+ // When this is defused, generate a new AbortSignal every time so listeners that get added don't
87
+ // stick around.
88
+ return AbortSignal.any([]);
89
+ }
90
+ const { error } = this;
91
+ if (error)
92
+ return (this.#signal = AbortSignal.abort(error));
93
+ const ac = new AbortController();
94
+ const listener = {
95
+ cancel: ac.abort.bind(ac),
96
+ [Symbols.cancellableRemoved]: () => {
97
+ // Discard the AbortSignal on defuse so we don't keep the event listeners.
98
+ this.#signal = undefined;
99
+ },
100
+ };
101
+ this.addOne(listener);
102
+ return (this.#signal = ac.signal);
103
+ }
104
+ throwIfCancelled() {
105
+ const { error } = this;
106
+ if (error)
107
+ throw error;
108
+ }
109
+ static createController(options) {
110
+ return CancelEvent.createController(options);
111
+ }
112
+ static create(options) {
113
+ return Token.createController(options).token;
114
+ }
115
+ /** A token that will never be cancelled. */
116
+ static get static() {
117
+ return STATIC_TOKEN;
118
+ }
119
+ /** @returns a token that has already been cancelled. */
120
+ static cancelled(reason = new CancellationError()) {
121
+ return new CancelledToken(toError(reason));
122
+ }
123
+ /**
124
+ *
125
+ * @returns A {@link Token} that will be cancelled when any of the given tokens are
126
+ * cancelled.
127
+ *
128
+ * If present, the first token encountered that has already been called will be returned.
129
+ */
130
+ static combine(src) {
131
+ let last = undefined;
132
+ const tokens = new Set();
133
+ for (const item of src) {
134
+ if (!item)
135
+ continue;
136
+ if (item.isCancelled)
137
+ return item;
138
+ last = item;
139
+ if (item.isDefused)
140
+ continue;
141
+ tokens.add(item);
142
+ }
143
+ if (tokens.size > 1) {
144
+ return Token.create({
145
+ init: ctrl => {
146
+ const sub = Subscription.collect(Array.from(tokens, t => t.add(ctrl)));
147
+ return {
148
+ resume: () => {
149
+ sub.resume();
150
+ return { pause: () => sub.pause() };
151
+ },
152
+ close: () => {
153
+ sub.dispose();
154
+ },
155
+ };
156
+ },
157
+ });
158
+ }
159
+ const [token] = tokens;
160
+ return token ?? last ?? STATIC_TOKEN;
161
+ }
162
+ /** @returns a {@link Token} that is cancelled when `signal` is aborted. */
163
+ static fromAbortSignal(signal, options) {
164
+ if (signal.aborted)
165
+ return new CancelledToken(toError(signal.reason));
166
+ const { onError, ...callbacks } = options ?? {};
167
+ return Token.create({
168
+ ...callbacks,
169
+ init: ({ cancel }) => {
170
+ const onAbort = () => void cancel(signal.reason).catch(onError);
171
+ signal.addEventListener('abort', onAbort, { once: true });
172
+ return {
173
+ close: () => {
174
+ signal.removeEventListener('abort', onAbort);
175
+ },
176
+ };
177
+ },
178
+ });
179
+ }
180
+ static from(src) {
181
+ const tokens = new Set();
182
+ const signals = new Set();
183
+ const flatten = (src) => {
184
+ if (!src)
185
+ return undefined;
186
+ if (src instanceof AbortSignal) {
187
+ if (src.aborted)
188
+ return Token.cancelled(src.reason);
189
+ signals.add(src);
190
+ return undefined;
191
+ }
192
+ if (src instanceof Token) {
193
+ if (src.isCancelled)
194
+ return src;
195
+ if (!src.isDefused) {
196
+ tokens.add(src);
197
+ }
198
+ return undefined;
199
+ }
200
+ if (isIterable(src)) {
201
+ for (const item of src) {
202
+ const out = flatten(item);
203
+ if (out)
204
+ return out;
205
+ }
206
+ return undefined;
207
+ }
208
+ return flatten(src.scope) || flatten(src.token) || flatten(src.signal);
209
+ };
210
+ const cancelled = flatten(src);
211
+ if (cancelled)
212
+ return cancelled;
213
+ let [signal] = signals;
214
+ if (signals.size > 1) {
215
+ signal = AbortSignal.any(Array.from(signals));
216
+ }
217
+ if (signal) {
218
+ tokens.add(Token.fromAbortSignal(signal));
219
+ }
220
+ return Token.combine(tokens);
221
+ }
222
+ }
223
+ /** Singleton class for {@link STATIC_TOKEN}. */
224
+ class StaticToken extends Token {
225
+ get error() {
226
+ return undefined;
227
+ }
228
+ get isDefused() {
229
+ return true;
230
+ }
231
+ addOne() {
232
+ return Subscription.noop;
233
+ }
234
+ }
235
+ /**
236
+ * A {@link Token} that will never be cancelled.
237
+ * @see {@link Token.static}.
238
+ */
239
+ export const STATIC_TOKEN = new StaticToken();
240
+ /**
241
+ * A {@link Token} that is already cancelled.
242
+ * @see {@link Token.cancelled}
243
+ */
244
+ class CancelledToken extends Token {
245
+ error = new Error();
246
+ constructor(error) {
247
+ super();
248
+ this.error = error;
249
+ }
250
+ addOne() { }
251
+ }
252
+ //# sourceMappingURL=base.js.map
@@ -0,0 +1,4 @@
1
+ export { Token, STATIC_TOKEN } from './token/base.ts';
2
+ export { CancelEvent } from './events/cancel.ts';
3
+ export type { TokenController } from './token/base.ts';
4
+ //# sourceMappingURL=token.d.ts.map
package/dist/token.js ADDED
@@ -0,0 +1,3 @@
1
+ export { Token, STATIC_TOKEN } from './token/base.js';
2
+ export { CancelEvent } from './events/cancel.js';
3
+ //# sourceMappingURL=token.js.map
@@ -0,0 +1,117 @@
1
+ /**
2
+ * A value that yields `T` when awaited. Useful for functions that take either the value itself or a
3
+ * promise that yields it.
4
+ *
5
+ * @template T The type of the value returned by `value` when awaited.
6
+ *
7
+ * @example
8
+ * function asyncMultiply(lhs: Awaitable<number>, rhs: Awaitable<number>) {
9
+ * const [l, r] = await Promise.all([lhs, rhs]);
10
+ * return l * r;
11
+ * }
12
+ *
13
+ */
14
+ export type Awaitable<T> = T | Promise<T> | PromiseLike<T>;
15
+ /**
16
+ * A value that's treated like `false` in conditional operations like `if` or `&&`
17
+ *
18
+ * More permissive APIs may accept falsy values and treat them as empty or undefined.
19
+ *
20
+ * @example
21
+ * interface Node { value: number }
22
+ *
23
+ * const nodes: Node[] = [];
24
+ *
25
+ * function addNode(node: Node | Falsy) {
26
+ * if (node) {
27
+ * nodes.push(node);
28
+ * }
29
+ * }
30
+ *
31
+ * // Generate some nodes:
32
+ * const exampleNodes = [-2,1,4,-6,-3,-2,5].map(value => ({ value }));
33
+ *
34
+ * // Add only the nodes with a positive value.
35
+ * // When the value is negative, `node.value > 0 && node` will evaluate to `false`.
36
+ * exampleNodes.forEach(node => addNode(node.value > 0 && node));
37
+ *
38
+ */
39
+ export type Falsy = false | null | undefined | 0 | 0n | '';
40
+ /** If `--exactOptionalProperties` is enabled, evaluates to `Then`. Otherwise `Else`. */
41
+ export type IfExactOptionalPropertiesEnabled<Then, Else> = {
42
+ x: undefined;
43
+ } extends {
44
+ x?: never;
45
+ } ? Else : Then;
46
+ /** Evalutes to `Then` if `T` is `never`, otherwise `Else`. */
47
+ type IfNever<T, Then, Else> = (T extends any ? Else : never) | (Then & ([T] extends [never] ? unknown : never));
48
+ /**
49
+ * Evaluates to `never` if `T` is `never`, otherwise `U`.
50
+ *
51
+ * @example
52
+ *
53
+ * // This type may or may not contain an error value of type `E`:
54
+ * type State<E> = Ready | Failed<E>;
55
+ * type Ready = { status: 'ready', value: number }
56
+ * // When `E` is `never`, `Failed<E>` is also never thanks to `OrNever<E>`:
57
+ * type Failed<E> = { status: 'failed'; error: E } & OrNever<E>;
58
+ *
59
+ * // This means when `E` is `never`, the type `State<never>` evaluates to
60
+ * // `Ready` to correctly reflect that it can never be in the `Failed` state.
61
+ *
62
+ * // Given a function with this signature:
63
+ * function createState<E>(): State<E> {
64
+ * return { value: 0 }
65
+ * }
66
+ *
67
+ * // The following operation is valid because we have specified there will
68
+ * // never be an error:
69
+ * const { value } = createState<never>();
70
+ */
71
+ export type OrNever<T, U = unknown> = IfNever<T, never, U>;
72
+ /** Evaluates to `T`, unless `T` is never, in which case it evaluates to `Else`. */
73
+ export type UnlessNeverElse<T, Else> = [T] extends [never] ? Else : T;
74
+ type _OptionalUndefinedParams<A extends any[], R extends any[]> = {
75
+ [K in keyof R]: undefined;
76
+ } extends R ? A extends [...infer L, ...R] ? [
77
+ ...L,
78
+ ...Partial<R>
79
+ ] : A : R extends [any, ...infer Rest] ? _OptionalUndefinedParams<A, Rest> : A;
80
+ export type OptionalUndefinedParams<A extends any[]> = _OptionalUndefinedParams<A, A>;
81
+ type SimplifyObject<T> = {
82
+ [K in keyof T]: T[K];
83
+ };
84
+ /**
85
+ * Makes properties of `T` that may be `undefined` optional.
86
+ * Useful for a params/options object for a function when a value might not be required depending
87
+ * on the type parameters.
88
+ *
89
+ * @example
90
+ *
91
+ * interface MyParams<T, U> {
92
+ * items: T[]
93
+ * // This property may be undefined if a `T` is already a valid `U`, but
94
+ * // as-is you'll still need to include `transform: undefined` in your options.
95
+ * transform: ((value: T) => U) | (T extends U ? undefined : never);
96
+ * }
97
+ *
98
+ * function myMap<T, U = T>({
99
+ * items,
100
+ * transform,
101
+ * }: OptionalUndefinedProps<MyParams<T, U>>): U[] {
102
+ * return transform ? items.map(transform) : items as (T & U)[]
103
+ * }
104
+ *
105
+ * const strings = myMap({
106
+ * items: [1, 2, 3],
107
+ * transform: String,
108
+ * }); // ['1', '2', '3']
109
+ *
110
+ * // `transform` is not required because `T` and `U` are both `number`.
111
+ * const unchanged = myMap({ items: [1, 2, 3] }); // [1, 2, 3]
112
+ */
113
+ export type OptionalUndefinedProps<T> = SimplifyObject<Partial<T> & {
114
+ [K in keyof T as undefined extends T[K] ? never : K]: T[K];
115
+ }>;
116
+ export {};
117
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,51 @@
1
+ import type { Awaitable, UnlessNeverElse } from './types.ts';
2
+ /**
3
+ * Gets the base {@link Iterable} types for each constituent of union type `I` that is an iterable.
4
+ * Discards all non-iterable constituents.
5
+ *
6
+ * This is a helper for {@linkcode AsIterable}.
7
+ */
8
+ type ExtractIterable<I> = I extends Iterable<infer X, infer Y, infer Z> ? Iterable<X, Y, Z> : never;
9
+ /**
10
+ * If `I` or any of its union constituents extend {@link Iterable}, evaluates to the base iterable
11
+ * type of `I` or its constituents.
12
+ * Otherwise, evaluates to {@linkcode Iterable|Iterable<unknown, unknown, any>}.
13
+ *
14
+ * This is used in the predicate type of {@linkcode isIterable} to narrow a type down to
15
+ * {@link Iterable} while preserving the most likely `yield`, `return`, and `next` types.
16
+ */
17
+ type AsIterable<I> = UnlessNeverElse<ExtractIterable<I>, Iterable<unknown, unknown, any>>;
18
+ export declare function isIterable<T>(value: T | AsIterable<T> | null | undefined): value is AsIterable<T>;
19
+ export declare function isPromiseLike<T>(value: Awaitable<T> | null | undefined): value is PromiseLike<T>;
20
+ /** @returns `true` if `value` has a {@linkcode Symbol.dispose} method. */
21
+ export declare function isDisposable(value: unknown): value is Disposable;
22
+ /** @returns `true` if `value` has a {@linkcode Symbol.asyncDispose} method. */
23
+ export declare function isAsyncDisposable(value: unknown): value is AsyncDisposable;
24
+ /**
25
+ * Gets the base {@link Array} types for each constituent of union type `I` that is an array.
26
+ * Discards all non-array constituents.
27
+ *
28
+ * This is a helper for {@linkcode AsArray}.
29
+ */
30
+ type ExtractArray<I> = I extends Array<infer X> ? X[] : I extends ReadonlyArray<infer X> ? readonly X[] : never;
31
+ /**
32
+ * If `I` or any of its union constituents extend {@link Array} or {@link ReadonlyArray}, evaluates
33
+ * to the base array type of `I` or its constituents.
34
+ * Otherwise, evaluates to `unknown[]`.
35
+ *
36
+ * This is used in the predicate type of {@linkcode isArray} to narrow a type down to {@link Array}
37
+ * while preserving the most likely element type.
38
+ */
39
+ type AsArray<I> = UnlessNeverElse<ExtractArray<I>, unknown[]>;
40
+ interface IsArrayFunction {
41
+ <I>(value: I | AsArray<I> | null | undefined): value is AsArray<I>;
42
+ }
43
+ /**
44
+ * @returns `true` if {@link value} is an array.
45
+ *
46
+ * @remarks
47
+ * This is an alias for {@link Array.isArray} but the types work out better for readonly arrays.
48
+ */
49
+ export declare const isArray: IsArrayFunction;
50
+ export {};
51
+ //# sourceMappingURL=utils.d.ts.map
package/dist/utils.js ADDED
@@ -0,0 +1,20 @@
1
+ export function isIterable(value) {
2
+ return typeof value?.[Symbol.iterator] === 'function';
3
+ }
4
+ export function isPromiseLike(value) {
5
+ return typeof value?.then === 'function';
6
+ }
7
+ export function isDisposable(value) {
8
+ return typeof value?.[Symbol.dispose] === 'function';
9
+ }
10
+ export function isAsyncDisposable(value) {
11
+ return typeof value?.[Symbol.asyncDispose] === 'function';
12
+ }
13
+ /**
14
+ * @returns `true` if {@link value} is an array.
15
+ *
16
+ * @remarks
17
+ * This is an alias for {@link Array.isArray} but the types work out better for readonly arrays.
18
+ */
19
+ export const isArray = Array.isArray.bind(Array);
20
+ //# sourceMappingURL=utils.js.map
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@youngspe/async-scope",
3
+ "version": "0.1.0-dev.0",
4
+ "main": "./dist/index.js",
5
+ "exports": {
6
+ ".": "./dist/index.js",
7
+ "./events": "./dist/events.js"
8
+ },
9
+ "type": "module",
10
+ "devDependencies": {
11
+ "knip": "^6.3.0"
12
+ },
13
+ "files": [
14
+ "./dist",
15
+ "!**/*.map",
16
+ "!**/*.test.*"
17
+ ],
18
+ "scripts": {
19
+ "test": "tsgo -b tsconfig.test.json --noCheck && node --expose-gc --enable-source-maps --test \"./dist/**/*.test.js\""
20
+ }
21
+ }