@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,24 @@
1
+ import { Token } from '../token.js';
2
+ import { Scope } from '../scope.js';
3
+ import { ScopedResources } from '../scopedResource.js';
4
+ export class StandardScope extends Scope {
5
+ #resources;
6
+ #token;
7
+ #onError;
8
+ get token() {
9
+ return this.#token;
10
+ }
11
+ get resources() {
12
+ return this.#resources;
13
+ }
14
+ _onError(value) {
15
+ return this.#onError ? this.#onError(value) : super._onError(value);
16
+ }
17
+ constructor(params) {
18
+ super();
19
+ this.#token = params.token ?? Token.static;
20
+ this.#resources = (params.token?.isCancelled ? undefined : params.resources) ?? ScopedResources.empty;
21
+ this.#onError = params.onError;
22
+ }
23
+ }
24
+ //# sourceMappingURL=standard.js.map
@@ -0,0 +1,4 @@
1
+ export { Scope, STATIC_SCOPE } from './scope/base.ts';
2
+ export { StandardScope } from './scope/standard.ts';
3
+ export type { CommonScopeOptions, ScopeContext, ScopeRunOptions, ToScope } from './scope/base.ts';
4
+ //# sourceMappingURL=scope.d.ts.map
package/dist/scope.js ADDED
@@ -0,0 +1,3 @@
1
+ export { Scope, STATIC_SCOPE } from './scope/base.js';
2
+ export { StandardScope } from './scope/standard.js';
3
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1,59 @@
1
+ import { Scope } from './scope.ts';
2
+ import type { Token } from './token.ts';
3
+ import type { Awaitable, Falsy } from './types.ts';
4
+ export declare class ResourceError extends Error {
5
+ resource: ResourceReadKey | undefined;
6
+ constructor(resource?: ResourceReadKey);
7
+ }
8
+ interface Covariant<out T> {
9
+ _covariant: Covariant<T>;
10
+ }
11
+ interface Contravariant<in T> {
12
+ _contravariant: Contravariant<T>;
13
+ }
14
+ declare const _covariant: unique symbol;
15
+ declare const _contravariant: unique symbol;
16
+ export declare abstract class ResourceKey<in TIn extends TOut, out TOut = TIn> {
17
+ #private;
18
+ private _base;
19
+ [_covariant]: Covariant<TOut>;
20
+ [_contravariant]: Contravariant<TIn>;
21
+ static create<T>(this: void, name?: string | ((...args: never) => T) | (abstract new (...args: never) => T)): ResourceKey<T>;
22
+ get optional(): ResourceReadKey<TOut | undefined>;
23
+ static optional<K extends ResourceKey.Structured<T>, T = ResourceKey.Resolved<K>>(key: K): ResourceReadKey<T | undefined>;
24
+ abstract toString(): string;
25
+ }
26
+ export declare namespace ResourceKey {
27
+ type Object<T> = {
28
+ readonly [K in keyof T]: Structured<T[K]>;
29
+ };
30
+ type Structured<T> = ResourceReadKey<T> | Object<T> | (Falsy & (undefined extends T ? unknown : never));
31
+ type Resolved<K extends Structured<unknown>> = K extends Falsy ? undefined : K extends ResourceReadKey<infer T> ? T : K extends {
32
+ [k: string]: Structured<unknown>;
33
+ } ? {
34
+ [P in keyof K]: Resolved<K[P]>;
35
+ } : unknown;
36
+ }
37
+ export interface ResourceReadKey<out T = unknown> extends ResourceKey<never, T> {
38
+ }
39
+ export interface ResourceWriteKey<in T> extends ResourceKey<T, unknown> {
40
+ }
41
+ export declare class ScopedResources {
42
+ #private;
43
+ private constructor();
44
+ get<K extends ResourceKey.Structured<T>, T = ResourceKey.Resolved<K>>(key: K): T;
45
+ tryGet<K extends ResourceKey.Structured<T>, T = ResourceKey.Resolved<K>>(key: K): T | undefined;
46
+ get isEmpty(): boolean;
47
+ static readonly empty: ScopedResources;
48
+ static builder(this: void, token?: Token): ScopedResources.Builder;
49
+ static combine(this: void, collections: Iterable<ScopedResources | Falsy>): ScopedResources;
50
+ }
51
+ export declare namespace ScopedResources {
52
+ interface Builder {
53
+ inherit(parent: ScopedResources | Scope): this;
54
+ put<T>(key: ResourceWriteKey<T>, value: Awaitable<T>): this;
55
+ finish(): ScopedResources;
56
+ }
57
+ }
58
+ export {};
59
+ //# sourceMappingURL=scopedResource.d.ts.map
@@ -0,0 +1,242 @@
1
+ import { joinPromises } from './join.js';
2
+ import { Scope } from './scope.js';
3
+ import { isAsyncDisposable, isDisposable } from './utils.js';
4
+ export class ResourceError extends Error {
5
+ static {
6
+ this.prototype.name = this.name;
7
+ }
8
+ resource;
9
+ constructor(resource) {
10
+ super();
11
+ this.resource = resource;
12
+ }
13
+ }
14
+ export class ResourceKey {
15
+ static create(name) {
16
+ if (name === undefined) {
17
+ const { stack } = new Error();
18
+ name = stack?.match(/\n\s*at\s+([^\s]+)/)?.[1];
19
+ }
20
+ else if (typeof name === 'function') {
21
+ ({ name } = name);
22
+ }
23
+ return new StandardResourceKey(name || 'unknown');
24
+ }
25
+ #optional;
26
+ get optional() {
27
+ return (this.#optional ??= new OptionalResourceKey(this));
28
+ }
29
+ static optional(key) {
30
+ if (key instanceof ResourceKey)
31
+ return key.optional;
32
+ return new OptionalResourceKey(key);
33
+ }
34
+ }
35
+ class CustomResourceKey extends ResourceKey {
36
+ }
37
+ class StandardResourceKey extends ResourceKey {
38
+ name;
39
+ constructor(name) {
40
+ super();
41
+ this.name = name;
42
+ }
43
+ toString() {
44
+ return `ResourceKey(${this.name})`;
45
+ }
46
+ }
47
+ function formatKey(key, indent = '') {
48
+ if (!key)
49
+ return '∅';
50
+ if (key instanceof ResourceKey)
51
+ return String(key);
52
+ const parts = ['{\n'];
53
+ for (const [k, v] of Object.entries(key)) {
54
+ parts.push(indent, k, ': ', formatKey(v, indent + ' '), ',\n');
55
+ }
56
+ parts.push('}');
57
+ return parts.join('');
58
+ }
59
+ class OptionalResourceKey extends CustomResourceKey {
60
+ #inner;
61
+ constructor(inner) {
62
+ super();
63
+ this.#inner = inner;
64
+ }
65
+ _resolve(res) {
66
+ return [res.tryGet(this.#inner)];
67
+ }
68
+ toString() {
69
+ return `optional(${formatKey(this.#inner)})`;
70
+ }
71
+ get optional() {
72
+ return this;
73
+ }
74
+ }
75
+ export class ScopedResources {
76
+ #parents;
77
+ #items;
78
+ constructor() { }
79
+ #get(key) {
80
+ if (!key)
81
+ return [undefined];
82
+ if (key instanceof CustomResourceKey) {
83
+ return key._resolve(this);
84
+ }
85
+ if (key instanceof ResourceKey) {
86
+ if (this.#items?.has(key))
87
+ return [this.#items?.get(key)];
88
+ if (this.#parents) {
89
+ for (const parent of this.#parents) {
90
+ const out = parent.#get(key);
91
+ if (out)
92
+ return out;
93
+ }
94
+ }
95
+ return new ResourceError(key);
96
+ }
97
+ const out = {};
98
+ for (const [prop, value] of Object.entries(key)) {
99
+ const resolved = this.#get(value);
100
+ if (resolved instanceof Error)
101
+ return resolved;
102
+ out[prop] = resolved[0];
103
+ }
104
+ return [out];
105
+ }
106
+ get(key) {
107
+ const out = this.#get(key);
108
+ if (out instanceof Error)
109
+ throw out;
110
+ return out[0];
111
+ }
112
+ tryGet(key) {
113
+ const out = this.#get(key);
114
+ if (out instanceof Error)
115
+ return undefined;
116
+ return out[0];
117
+ }
118
+ #isEmpty;
119
+ get isEmpty() {
120
+ if (this.#isEmpty)
121
+ return true;
122
+ if (this.#items?.size)
123
+ return false;
124
+ if (this.#parents) {
125
+ for (const parent of this.#parents) {
126
+ if (!parent.isEmpty)
127
+ return false;
128
+ this.#parents.delete(parent);
129
+ }
130
+ }
131
+ return (this.#isEmpty = true);
132
+ }
133
+ #flatten(dest, visited) {
134
+ if (visited.has(this))
135
+ return;
136
+ visited.add(this);
137
+ if (this.#items?.size) {
138
+ dest.add(this);
139
+ const newParents = new Set();
140
+ const newVisited = new Set([this]);
141
+ const parents = this.#parents;
142
+ if (!parents)
143
+ return;
144
+ this.#parents = newParents;
145
+ for (const parent of parents) {
146
+ parent.#flatten(newParents, newVisited);
147
+ }
148
+ return;
149
+ }
150
+ if (!this.#parents)
151
+ return;
152
+ for (const parent of this.#parents) {
153
+ parent.#flatten(dest, visited);
154
+ }
155
+ }
156
+ static empty = Object.freeze(new this());
157
+ static builder(token) {
158
+ return new ScopedResources.#Builder(token);
159
+ }
160
+ #disposing;
161
+ #dispose() {
162
+ return (this.#disposing ??= (async () => {
163
+ const items = this.#items;
164
+ if (!items)
165
+ return;
166
+ this.#items = undefined;
167
+ this.#parents?.clear();
168
+ await joinPromises(items.values(), item => {
169
+ if (isDisposable(item)) {
170
+ item[Symbol.dispose]();
171
+ return;
172
+ }
173
+ if (isAsyncDisposable(item))
174
+ return item[Symbol.asyncDispose]();
175
+ });
176
+ items.clear();
177
+ })());
178
+ }
179
+ static #Builder = class Builder {
180
+ #inherit = [];
181
+ #put = new Map();
182
+ #token;
183
+ constructor(token) {
184
+ if (token?.isCancelled) {
185
+ this.#inherit = this.#put = undefined;
186
+ return;
187
+ }
188
+ this.#token = token?.isDefused ? undefined : token;
189
+ }
190
+ inherit(parent) {
191
+ if (!parent)
192
+ return this;
193
+ this.#inherit?.push(parent instanceof Scope ? parent.resources : parent);
194
+ return this;
195
+ }
196
+ put(key, value) {
197
+ this.#put?.set(key, value);
198
+ return this;
199
+ }
200
+ finish() {
201
+ // Reverse the `inherit` list so that more recently-added entries take precedence.
202
+ const inherit = this.#inherit?.reverse();
203
+ const put = this.#put;
204
+ const token = this.#token;
205
+ this.#inherit = this.#put = this.#token = undefined;
206
+ let parents = undefined;
207
+ if (inherit?.length) {
208
+ const visited = new Set();
209
+ const dest = new Set();
210
+ inherit.forEach(r => r.#flatten(dest, visited));
211
+ parents = dest.size ? dest : undefined;
212
+ }
213
+ if (!put?.size && (parents?.size ?? 0 <= 1)) {
214
+ const parent = parents?.values().next().value;
215
+ if (!parent)
216
+ return ScopedResources.empty;
217
+ if (!token)
218
+ return parent;
219
+ }
220
+ const resources = new ScopedResources();
221
+ resources.#items = put?.size ? put : undefined;
222
+ resources.#parents = parents;
223
+ token?.add(() => resources.#dispose());
224
+ return resources;
225
+ }
226
+ };
227
+ static combine(collections) {
228
+ const set = new Set();
229
+ const visited = new Set();
230
+ for (const item of collections) {
231
+ if (!item)
232
+ continue;
233
+ item.#flatten(set, visited);
234
+ }
235
+ if (set.size > 1) {
236
+ const resources = new ScopedResources();
237
+ resources.#parents = set;
238
+ }
239
+ return set.values().next().value ?? ScopedResources.empty;
240
+ }
241
+ }
242
+ //# sourceMappingURL=scopedResource.js.map
@@ -0,0 +1,3 @@
1
+ export declare const cancellableAdded: unique symbol;
2
+ export declare const cancellableRemoved: unique symbol;
3
+ //# sourceMappingURL=symbols.d.ts.map
@@ -0,0 +1,3 @@
1
+ export const cancellableAdded = Symbol.for('async-scope.cancellable-added');
2
+ export const cancellableRemoved = Symbol.for('async-scope.cancellable-removed');
3
+ //# sourceMappingURL=symbols.js.map
@@ -0,0 +1,13 @@
1
+ import type { CancellableOptions } from './cancel.ts';
2
+ interface Clock {
3
+ setTimeout: typeof globalThis.setTimeout;
4
+ setInterval: typeof globalThis.setInterval;
5
+ clearTimeout: typeof globalThis.clearTimeout;
6
+ clearInterval: typeof globalThis.clearInterval;
7
+ }
8
+ interface TimerOptions extends CancellableOptions {
9
+ clock?: Clock | undefined;
10
+ }
11
+ export declare function delay(ms: number, options?: TimerOptions): Promise<void>;
12
+ export {};
13
+ //# sourceMappingURL=timer.d.ts.map
package/dist/timer.js ADDED
@@ -0,0 +1,28 @@
1
+ import { Token } from './token.js';
2
+ const GlobalClock = {
3
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
4
+ setTimeout: (f, ...args) => globalThis.setTimeout(f, ...args),
5
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
6
+ setInterval: (f, ...args) => globalThis.setInterval(f, ...args),
7
+ clearTimeout: id => globalThis.clearTimeout(id),
8
+ clearInterval: id => globalThis.clearInterval(id),
9
+ };
10
+ export function delay(ms, options) {
11
+ const token = Token.from(options);
12
+ if (token.isCancelled)
13
+ return Promise.reject(token.error);
14
+ const { setTimeout, clearTimeout } = options?.clock ?? GlobalClock;
15
+ if (token.isDefused)
16
+ return new Promise(resolve => setTimeout(resolve, ms));
17
+ return new Promise((resolve, reject) => {
18
+ const sub = token.add(e => {
19
+ clearTimeout(id);
20
+ reject(e);
21
+ });
22
+ const id = setTimeout(() => {
23
+ sub?.dispose();
24
+ resolve();
25
+ }, ms);
26
+ });
27
+ }
28
+ //# sourceMappingURL=timer.js.map
@@ -0,0 +1,135 @@
1
+ import type { CancellableOrDisposable, CancellableLike, CancellationListener, Cancellable } from '../cancel.ts';
2
+ import type { Awaitable, Falsy } from '../types.ts';
3
+ import { Subscription, type SubscriptionLifecycle } from '../events/sub.js';
4
+ import type { ToScope } from '../scope.ts';
5
+ export declare abstract class Token {
6
+ #private;
7
+ /**
8
+ * The reason for the cancellation if this token has already been cancelled, otherwise `undefined`.
9
+ *
10
+ * @see {@link Token#isCancelled}
11
+ */
12
+ abstract get error(): Error | undefined;
13
+ /** If `true`, this token has been _defused_, meaning it is guaranteed never to be cancelled. */
14
+ get isDefused(): boolean;
15
+ /**
16
+ * If `true`, this token has been _cancelled_, and trigger any additional listeners.
17
+ *
18
+ * This is equivalent to `token.error !== undefined`
19
+ *
20
+ * @see {@link Token#error}
21
+ */
22
+ get isCancelled(): boolean;
23
+ /**
24
+ * Attempts to add a single listener to this token.
25
+ * This is called by provided methods like {@link add} and {@link use}.
26
+ *
27
+ * @param key - an arbitrary object used as an identifier for the listener.
28
+ * @param listener - An object that may include any of the methods of a {@link Cancellable},
29
+ * {@link disposable}, or {@link AsyncDisposable}.
30
+ * @returns
31
+ * - A {@link Subscription} if either the listener was safely added or the token is defused.
32
+ * - `undefined` if the listener could not be safely added. This is typically due to the token being
33
+ * cancelled and indicates any future attempts will also fail.
34
+ */
35
+ protected abstract addOne(listener: CancellableOrDisposable): Subscription | undefined;
36
+ /**
37
+ * Adds listeners
38
+ *
39
+ * @param listeners - listeners that should be added to this token.
40
+ * Each parameter may be:
41
+ * - A {@link CancellableLike}:
42
+ * - A {@link Cancellable}
43
+ * - A {@link Disposable}
44
+ * - An {@link AsyncDisposable}
45
+ * - A falsy value, which will be ignored
46
+ * - A (possibly nested) array of any of the above.
47
+ * - A function that receives an {@link Error} and optionally returns a promise.
48
+ *
49
+ * @returns
50
+ * - 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.
51
+ * - `undefined` if no listener could be safely added. This is typically due to the token being cancelled
52
+ * and indicates any future attempts will also fail.
53
+ */
54
+ add(...listeners: (CancellableLike | CancellationListener)[]): Subscription | undefined;
55
+ use<X extends CancellableLike>(target: X): X;
56
+ /**
57
+ * Attempts to add a listener to this token.
58
+ * Returns the target if successfully added, or `undefined` if not added.
59
+ */
60
+ tryUse<X extends CancellableLike>(listener: X): X | undefined;
61
+ /**
62
+ * An {@link AbortSignal} representing this token.
63
+ * When this token is cancelled, the returned signal will be aborted.
64
+ * When this token is defused, a new signal is generated on each access.
65
+ */
66
+ get signal(): AbortSignal;
67
+ throwIfCancelled(): void;
68
+ static createController(this: void, options?: Token.CreateParams): TokenController;
69
+ static create(this: void, options: Token.CreateParams): Token;
70
+ /** A token that will never be cancelled. */
71
+ static get static(): Token;
72
+ /** @returns a token that has already been cancelled. */
73
+ static cancelled(reason?: unknown): Token;
74
+ /**
75
+ *
76
+ * @returns A {@link Token} that will be cancelled when any of the given tokens are
77
+ * cancelled.
78
+ *
79
+ * If present, the first token encountered that has already been called will be returned.
80
+ */
81
+ static combine(src: Iterable<Token | Falsy>): Token;
82
+ /** @returns a {@link Token} that is cancelled when `signal` is aborted. */
83
+ static fromAbortSignal(this: void, signal: AbortSignal, options?: Token.FromAbortSignalParams): Token;
84
+ static from(this: void, src: ToScope): Token;
85
+ }
86
+ export declare namespace Token {
87
+ interface Callbacks {
88
+ /**
89
+ * Called when the token is defused either explicitly or due to the controller getting garbage
90
+ * collected.
91
+ */
92
+ onDefuse?: ((this: void) => void) | undefined;
93
+ /**
94
+ * Called on {@link CancellationController.cancel}. If a promise-like is returned, no listeners will be
95
+ * notified of the cancellation until after the promise-like resolves.
96
+ *
97
+ * If this callback throws or rejects, the {@link CancellationController.cancel} promise will also
98
+ * reject after all cancellation listeners and the {@link onBeforeCancel} promise is settled
99
+ * if present.
100
+ */
101
+ onBeforeCancel?: ((this: void, error: Error) => Awaitable<void>) | undefined;
102
+ /**
103
+ * Called after all promises returned by cancellation listeners have either resolved or rejected.
104
+ *
105
+ * If this callback exists and returns a promise-like, the {@link CancellationController.cancel} promise
106
+ * will not resolve until after this promise-like is settled.
107
+ *
108
+ * If this callback throws or rejects, the {@link CancellationController.cancel} promise will
109
+ * also reject.
110
+ */
111
+ onAfterCancel?: ((this: void, error: Error) => Awaitable<void>) | undefined;
112
+ }
113
+ interface CreateParams extends SubscriptionLifecycle<[ctrl: TokenController], [ctrl: TokenController]>, Callbacks {
114
+ }
115
+ interface FromAbortSignalParams extends Callbacks {
116
+ /**
117
+ * Called asynchronously after the signal is aborted if any of the cancellation listeners or callbacks
118
+ * either throw or reject.
119
+ *
120
+ * If this is not provided, any errors thrown during cancellation will be unhandled rejections.
121
+ */
122
+ onError?: ((reason: unknown) => void) | undefined;
123
+ }
124
+ }
125
+ /**
126
+ * A {@link Token} that will never be cancelled.
127
+ * @see {@link Token.static}.
128
+ */
129
+ export declare const STATIC_TOKEN: Token;
130
+ export interface TokenController extends Cancellable, AsyncDisposable {
131
+ readonly token: Token;
132
+ readonly cancel: (this: void, reason?: unknown) => Promise<void>;
133
+ readonly defuse: (this: void) => void;
134
+ }
135
+ //# sourceMappingURL=base.d.ts.map