@pipedog/asyncio 0.1.0 → 0.2.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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/loop.d.ts +26 -0
- package/dist/loop.js +179 -0
- package/dist/primitives.d.ts +24 -13
- package/dist/primitives.js +439 -185
- package/dist/task.d.ts +48 -19
- package/dist/task.js +245 -152
- package/package.json +1 -1
package/dist/task.d.ts
CHANGED
|
@@ -19,48 +19,77 @@ export declare function isCancelledError(e: unknown): e is CancelledError;
|
|
|
19
19
|
export declare function rethrowIfCancelled(e: unknown): void;
|
|
20
20
|
/** The task the caller is running in, if any (asyncio.current_task). */
|
|
21
21
|
export declare function currentTask(): Task<unknown> | undefined;
|
|
22
|
-
type
|
|
22
|
+
type FutureState = "pending" | "fulfilled" | "rejected" | "cancelled";
|
|
23
|
+
/**
|
|
24
|
+
* asyncio.Future: a result that becomes available later. Awaiting it (it is
|
|
25
|
+
* thenable) suspends the current task until it is done, unless it already is.
|
|
26
|
+
* Done callbacks run on the ready queue, never synchronously.
|
|
27
|
+
*/
|
|
28
|
+
export declare class Future<T = unknown> implements PromiseLike<T> {
|
|
29
|
+
#private;
|
|
30
|
+
done(): boolean;
|
|
31
|
+
cancelled(): boolean;
|
|
32
|
+
result(): T;
|
|
33
|
+
exception(): unknown;
|
|
34
|
+
setResult(value: T): void;
|
|
35
|
+
setException(error: unknown): void;
|
|
36
|
+
/** Cancel the future. Returns false if it is already done. */
|
|
37
|
+
cancel(message?: string): boolean;
|
|
38
|
+
addDoneCallback(callback: (future: this) => void): void;
|
|
39
|
+
removeDoneCallback(callback: (future: this) => void): number;
|
|
40
|
+
then<R1 = T, R2 = never>(onfulfilled?: ((value: T) => R1 | PromiseLike<R1>) | null, onrejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null): Promise<R1 | R2>;
|
|
41
|
+
/** @internal The CancelledError a cancelled future raises. */
|
|
42
|
+
_makeCancelledError(): CancelledError;
|
|
43
|
+
/** @internal Mark the future done and schedule its callbacks. */
|
|
44
|
+
_setDone(state: Exclude<FutureState, "pending">, value: T | undefined, error: unknown): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Await `future` from the current task (Future.__await__ plus the task's
|
|
48
|
+
* wakeup). A done future returns at once, without suspending. Otherwise the
|
|
49
|
+
* task resumes on the ready queue once the future is done, and cancelling
|
|
50
|
+
* the task meanwhile cancels the future.
|
|
51
|
+
*/
|
|
52
|
+
export declare function awaitFuture<T>(future: Future<T>): Promise<T>;
|
|
23
53
|
export type TaskFn<T> = () => Promise<T> | T;
|
|
24
54
|
/**
|
|
25
55
|
* A unit of concurrent work, like asyncio.Task. Created with `createTask()`.
|
|
26
56
|
* Awaiting a Task (it is thenable) is a cancellation point; cancelling the
|
|
27
57
|
* awaiting task also cancels the awaited one, as in asyncio.
|
|
28
58
|
*/
|
|
29
|
-
export declare class Task<T>
|
|
59
|
+
export declare class Task<T> extends Future<T> {
|
|
30
60
|
#private;
|
|
61
|
+
/** @internal The future this task is suspended on (Task._fut_waiter). */
|
|
62
|
+
_futWaiter: Future<unknown> | null;
|
|
31
63
|
constructor(fn: TaskFn<T>, name?: string);
|
|
32
64
|
get name(): string;
|
|
33
65
|
/** asyncio Task.get_name() */
|
|
34
66
|
getName(): string;
|
|
35
67
|
setName(name: string): void;
|
|
36
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Aborted when cancellation is requested. Pass it to fetch() and friends.
|
|
70
|
+
* Once every request is withdrawn with `uncancel()`, a fresh signal is used.
|
|
71
|
+
*/
|
|
37
72
|
get signal(): AbortSignal;
|
|
38
|
-
done(): boolean;
|
|
39
|
-
cancelled(): boolean;
|
|
40
73
|
/** Number of pending cancellation requests (asyncio Task.cancelling()). */
|
|
41
74
|
cancelling(): number;
|
|
42
75
|
uncancel(): number;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
removeDoneCallback(callback: (task: Task<T>) => void): number;
|
|
47
|
-
/**
|
|
48
|
-
* Request cancellation. Returns false if the task is already done.
|
|
49
|
-
*/
|
|
76
|
+
setResult(_value: T): void;
|
|
77
|
+
setException(_error: unknown): void;
|
|
78
|
+
/** Request cancellation. Returns false if the task is already done. */
|
|
50
79
|
cancel(message?: string): boolean;
|
|
51
80
|
/**
|
|
52
|
-
* Throw CancelledError if a cancellation request is pending.
|
|
53
|
-
*
|
|
81
|
+
* Throw CancelledError if a cancellation request is pending. Primitives
|
|
82
|
+
* call this on their fast paths, so a task that resumes from a foreign
|
|
83
|
+
* await after being cancelled stops at the next one.
|
|
54
84
|
*/
|
|
55
85
|
checkpoint(): void;
|
|
56
|
-
/** @internal Register the rejecter of the await this task is suspended on. */
|
|
57
|
-
_addWaiter(waiter: Waiter): void;
|
|
58
|
-
/** @internal */
|
|
59
|
-
_removeWaiter(waiter: Waiter): void;
|
|
60
86
|
/** A promise that settles with the task, without being a cancellation point. */
|
|
61
87
|
get promise(): Promise<T>;
|
|
62
|
-
then<R1 = T, R2 = never>(onfulfilled?: ((value: T) => R1 | PromiseLike<R1>) | null, onrejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null): Promise<R1 | R2>;
|
|
63
88
|
toString(): string;
|
|
89
|
+
/** @internal Consume a pending cancellation request, returning its error. */
|
|
90
|
+
_takeCancelRequest(): CancelledError | undefined;
|
|
91
|
+
/** @internal The task suspends on `future` (Task.__step yielding a future). */
|
|
92
|
+
_block(future: Future<unknown>): void;
|
|
64
93
|
}
|
|
65
94
|
/**
|
|
66
95
|
* Suspend the current task until `register` resolves or rejects, making the
|
package/dist/task.js
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
// Copyright (c) 2026, pipedog contributors
|
|
2
2
|
// SPDX-License-Identifier: BSD-2-Clause
|
|
3
3
|
//
|
|
4
|
-
// asyncio-style tasks with cooperative cancellation.
|
|
4
|
+
// asyncio-style futures and tasks with cooperative cancellation.
|
|
5
|
+
//
|
|
6
|
+
// Scheduling follows asyncio (see loop.ts): a task runs until it awaits a
|
|
7
|
+
// future that is not done. Completing a future queues its waiters' wakeups on
|
|
8
|
+
// the ready queue instead of resuming them, so every fast path (awaiting a
|
|
9
|
+
// done future, `get()` on a non-empty queue, `put()` on an unbounded one,
|
|
10
|
+
// `wait()` on a set event) completes without letting another task run.
|
|
5
11
|
//
|
|
6
12
|
// Python delivers `CancelledError` *into* a coroutine at the await it is
|
|
7
13
|
// suspended on. JavaScript cannot interrupt an arbitrary await, so we emulate
|
|
8
|
-
// it
|
|
9
|
-
// Task, waitFor, gather...) is a
|
|
10
|
-
// cancelled:
|
|
14
|
+
// it. Every await on a Future (and so on every primitive in this package:
|
|
15
|
+
// sleep, Event.wait, Queue.get, awaiting a Task, waitFor, gather...) is a
|
|
16
|
+
// "cancellation point". When a task is cancelled:
|
|
11
17
|
//
|
|
12
|
-
// - if it is suspended on a
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
18
|
+
// - if it is suspended on a future, that future is cancelled and the task
|
|
19
|
+
// resumes (on the ready queue, as in asyncio) with CancelledError; if it
|
|
20
|
+
// awaits another task, that task is cancelled and this one resumes when it
|
|
21
|
+
// finishes;
|
|
22
|
+
// - otherwise the request stays pending: a task already woken up gets the
|
|
23
|
+
// CancelledError when it resumes, and a task inside a foreign await gets
|
|
24
|
+
// it at the next cancellation point it reaches;
|
|
16
25
|
// - `task.signal` is aborted, so foreign awaits (fetch, WebSocket, SDKs) can
|
|
17
26
|
// be made cancellable by passing the signal or wrapping them in
|
|
18
27
|
// `abortable()`.
|
|
@@ -22,7 +31,7 @@
|
|
|
22
31
|
/// <reference path="./async-hooks.d.ts" />
|
|
23
32
|
// (The reference keeps the declaration with this file: packages that compile
|
|
24
33
|
// it from source through the workspace exports don't include async-hooks.d.ts.)
|
|
25
|
-
import {
|
|
34
|
+
import { callSoon, taskStorage } from "./loop.js";
|
|
26
35
|
export class CancelledError extends Error {
|
|
27
36
|
name = "CancelledError";
|
|
28
37
|
constructor(message = "Task cancelled") {
|
|
@@ -51,10 +60,131 @@ export function rethrowIfCancelled(e) {
|
|
|
51
60
|
if (e instanceof CancelledError)
|
|
52
61
|
throw e;
|
|
53
62
|
}
|
|
54
|
-
const storage = new AsyncLocalStorage();
|
|
55
63
|
/** The task the caller is running in, if any (asyncio.current_task). */
|
|
56
64
|
export function currentTask() {
|
|
57
|
-
return
|
|
65
|
+
return taskStorage.getStore();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* asyncio.Future: a result that becomes available later. Awaiting it (it is
|
|
69
|
+
* thenable) suspends the current task until it is done, unless it already is.
|
|
70
|
+
* Done callbacks run on the ready queue, never synchronously.
|
|
71
|
+
*/
|
|
72
|
+
export class Future {
|
|
73
|
+
#state = "pending";
|
|
74
|
+
#result;
|
|
75
|
+
#error;
|
|
76
|
+
#cancelMessage;
|
|
77
|
+
#callbacks = [];
|
|
78
|
+
done() {
|
|
79
|
+
return this.#state !== "pending";
|
|
80
|
+
}
|
|
81
|
+
cancelled() {
|
|
82
|
+
return this.#state === "cancelled";
|
|
83
|
+
}
|
|
84
|
+
result() {
|
|
85
|
+
if (this.#state === "pending")
|
|
86
|
+
throw new InvalidStateError("Result is not set.");
|
|
87
|
+
if (this.#state === "fulfilled")
|
|
88
|
+
return this.#result;
|
|
89
|
+
if (this.#state === "cancelled")
|
|
90
|
+
throw this._makeCancelledError();
|
|
91
|
+
throw this.#error;
|
|
92
|
+
}
|
|
93
|
+
exception() {
|
|
94
|
+
if (this.#state === "pending")
|
|
95
|
+
throw new InvalidStateError("Exception is not set.");
|
|
96
|
+
if (this.#state === "cancelled")
|
|
97
|
+
throw this._makeCancelledError();
|
|
98
|
+
return this.#state === "rejected" ? this.#error : undefined;
|
|
99
|
+
}
|
|
100
|
+
setResult(value) {
|
|
101
|
+
if (this.#state !== "pending")
|
|
102
|
+
throw new InvalidStateError(`invalid state: ${this.#state}`);
|
|
103
|
+
this._setDone("fulfilled", value, undefined);
|
|
104
|
+
}
|
|
105
|
+
setException(error) {
|
|
106
|
+
if (this.#state !== "pending")
|
|
107
|
+
throw new InvalidStateError(`invalid state: ${this.#state}`);
|
|
108
|
+
this._setDone("rejected", undefined, error);
|
|
109
|
+
}
|
|
110
|
+
/** Cancel the future. Returns false if it is already done. */
|
|
111
|
+
cancel(message) {
|
|
112
|
+
if (this.#state !== "pending")
|
|
113
|
+
return false;
|
|
114
|
+
this.#cancelMessage = message;
|
|
115
|
+
this._setDone("cancelled", undefined, undefined);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
addDoneCallback(callback) {
|
|
119
|
+
if (this.#state !== "pending") {
|
|
120
|
+
callSoon(() => callback(this));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
this.#callbacks.push(callback);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
removeDoneCallback(callback) {
|
|
127
|
+
const before = this.#callbacks.length;
|
|
128
|
+
this.#callbacks = this.#callbacks.filter((c) => c !== callback);
|
|
129
|
+
return before - this.#callbacks.length;
|
|
130
|
+
}
|
|
131
|
+
// biome-ignore lint/suspicious/noThenProperty: Futures are awaitable by design, like asyncio.
|
|
132
|
+
then(onfulfilled, onrejected) {
|
|
133
|
+
return awaitFuture(this).then(onfulfilled, onrejected);
|
|
134
|
+
}
|
|
135
|
+
/** @internal The CancelledError a cancelled future raises. */
|
|
136
|
+
_makeCancelledError() {
|
|
137
|
+
if (this.#error instanceof CancelledError)
|
|
138
|
+
return this.#error;
|
|
139
|
+
return new CancelledError(this.#cancelMessage);
|
|
140
|
+
}
|
|
141
|
+
/** @internal Mark the future done and schedule its callbacks. */
|
|
142
|
+
_setDone(state, value, error) {
|
|
143
|
+
this.#state = state;
|
|
144
|
+
this.#result = value;
|
|
145
|
+
this.#error = error;
|
|
146
|
+
const callbacks = this.#callbacks;
|
|
147
|
+
this.#callbacks = [];
|
|
148
|
+
for (const callback of callbacks)
|
|
149
|
+
callSoon(() => callback(this));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Await `future` from the current task (Future.__await__ plus the task's
|
|
154
|
+
* wakeup). A done future returns at once, without suspending. Otherwise the
|
|
155
|
+
* task resumes on the ready queue once the future is done, and cancelling
|
|
156
|
+
* the task meanwhile cancels the future.
|
|
157
|
+
*/
|
|
158
|
+
export function awaitFuture(future) {
|
|
159
|
+
if (future.done()) {
|
|
160
|
+
try {
|
|
161
|
+
return Promise.resolve(future.result());
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
return Promise.reject(e);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const task = currentTask();
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
future.addDoneCallback(() => {
|
|
170
|
+
if (task) {
|
|
171
|
+
task._futWaiter = null;
|
|
172
|
+
// A cancellation requested after the wakeup was queued wins (Task.__step).
|
|
173
|
+
const cancelled = task._takeCancelRequest();
|
|
174
|
+
if (cancelled) {
|
|
175
|
+
reject(cancelled);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
resolve(future.result());
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
reject(e);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
task?._block(future);
|
|
187
|
+
});
|
|
58
188
|
}
|
|
59
189
|
let taskCounter = 0;
|
|
60
190
|
/**
|
|
@@ -62,28 +192,44 @@ let taskCounter = 0;
|
|
|
62
192
|
* Awaiting a Task (it is thenable) is a cancellation point; cancelling the
|
|
63
193
|
* awaiting task also cancels the awaited one, as in asyncio.
|
|
64
194
|
*/
|
|
65
|
-
export class Task {
|
|
195
|
+
export class Task extends Future {
|
|
66
196
|
#controller = new AbortController();
|
|
67
|
-
#promise;
|
|
68
|
-
#waiters = new Set();
|
|
69
|
-
#doneCallbacks = [];
|
|
70
197
|
#name;
|
|
71
|
-
#state = "pending";
|
|
72
|
-
#result;
|
|
73
|
-
#error;
|
|
74
198
|
#mustCancel = false;
|
|
75
199
|
#cancelMessage;
|
|
76
200
|
#cancelRequests = 0;
|
|
201
|
+
#promise;
|
|
202
|
+
/** @internal The future this task is suspended on (Task._fut_waiter). */
|
|
203
|
+
_futWaiter = null;
|
|
77
204
|
constructor(fn, name) {
|
|
205
|
+
super();
|
|
78
206
|
this.#name = name ?? `Task-${++taskCounter}`;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
207
|
+
let start;
|
|
208
|
+
const started = new Promise((resolve, reject) => {
|
|
209
|
+
start = { resolve, reject };
|
|
210
|
+
});
|
|
211
|
+
// Like asyncio, a task never starts running synchronously in the caller:
|
|
212
|
+
// its first step is scheduled on the ready queue.
|
|
213
|
+
callSoon(() => {
|
|
214
|
+
const cancelled = this._takeCancelRequest();
|
|
215
|
+
if (cancelled)
|
|
216
|
+
start.reject(cancelled);
|
|
217
|
+
else
|
|
218
|
+
start.resolve();
|
|
85
219
|
});
|
|
86
|
-
|
|
220
|
+
taskStorage
|
|
221
|
+
.run(this, async () => {
|
|
222
|
+
await started;
|
|
223
|
+
return await fn();
|
|
224
|
+
})
|
|
225
|
+
.then((value) => {
|
|
226
|
+
// Cancelled right before returning: the task ends cancelled.
|
|
227
|
+
const cancelled = this._takeCancelRequest();
|
|
228
|
+
if (cancelled)
|
|
229
|
+
this._setDone("cancelled", undefined, cancelled);
|
|
230
|
+
else
|
|
231
|
+
this._setDone("fulfilled", value, undefined);
|
|
232
|
+
}, (error) => this._setDone(error instanceof CancelledError ? "cancelled" : "rejected", undefined, error));
|
|
87
233
|
}
|
|
88
234
|
get name() {
|
|
89
235
|
return this.#name;
|
|
@@ -95,16 +241,13 @@ export class Task {
|
|
|
95
241
|
setName(name) {
|
|
96
242
|
this.#name = name;
|
|
97
243
|
}
|
|
98
|
-
/**
|
|
244
|
+
/**
|
|
245
|
+
* Aborted when cancellation is requested. Pass it to fetch() and friends.
|
|
246
|
+
* Once every request is withdrawn with `uncancel()`, a fresh signal is used.
|
|
247
|
+
*/
|
|
99
248
|
get signal() {
|
|
100
249
|
return this.#controller.signal;
|
|
101
250
|
}
|
|
102
|
-
done() {
|
|
103
|
-
return this.#state !== "pending";
|
|
104
|
-
}
|
|
105
|
-
cancelled() {
|
|
106
|
-
return this.#state === "cancelled";
|
|
107
|
-
}
|
|
108
251
|
/** Number of pending cancellation requests (asyncio Task.cancelling()). */
|
|
109
252
|
cancelling() {
|
|
110
253
|
return this.#cancelRequests;
|
|
@@ -112,115 +255,78 @@ export class Task {
|
|
|
112
255
|
uncancel() {
|
|
113
256
|
if (this.#cancelRequests > 0)
|
|
114
257
|
this.#cancelRequests--;
|
|
115
|
-
if (this.#cancelRequests === 0)
|
|
258
|
+
if (this.#cancelRequests === 0) {
|
|
116
259
|
this.#mustCancel = false;
|
|
260
|
+
if (this.#controller.signal.aborted)
|
|
261
|
+
this.#controller = new AbortController();
|
|
262
|
+
}
|
|
117
263
|
return this.#cancelRequests;
|
|
118
264
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
throw new InvalidStateError("Result is not set.");
|
|
122
|
-
if (this.#state === "fulfilled")
|
|
123
|
-
return this.#result;
|
|
124
|
-
throw this.#error;
|
|
125
|
-
}
|
|
126
|
-
exception() {
|
|
127
|
-
if (this.#state === "pending")
|
|
128
|
-
throw new InvalidStateError("Exception is not set.");
|
|
129
|
-
if (this.#state === "cancelled")
|
|
130
|
-
throw this.#error;
|
|
131
|
-
return this.#state === "rejected" ? this.#error : undefined;
|
|
265
|
+
setResult(_value) {
|
|
266
|
+
throw new InvalidStateError("Task does not support setResult()");
|
|
132
267
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
queueMicrotask(() => callback(this));
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
this.#doneCallbacks.push(callback);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
removeDoneCallback(callback) {
|
|
142
|
-
const before = this.#doneCallbacks.length;
|
|
143
|
-
for (let i = this.#doneCallbacks.length - 1; i >= 0; i--) {
|
|
144
|
-
if (this.#doneCallbacks[i] === callback)
|
|
145
|
-
this.#doneCallbacks.splice(i, 1);
|
|
146
|
-
}
|
|
147
|
-
return before - this.#doneCallbacks.length;
|
|
268
|
+
setException(_error) {
|
|
269
|
+
throw new InvalidStateError("Task does not support setException()");
|
|
148
270
|
}
|
|
149
|
-
/**
|
|
150
|
-
* Request cancellation. Returns false if the task is already done.
|
|
151
|
-
*/
|
|
271
|
+
/** Request cancellation. Returns false if the task is already done. */
|
|
152
272
|
cancel(message) {
|
|
153
273
|
if (this.done())
|
|
154
274
|
return false;
|
|
155
275
|
this.#cancelRequests++;
|
|
156
|
-
this.#cancelMessage = message;
|
|
157
276
|
if (!this.#controller.signal.aborted) {
|
|
158
277
|
this.#controller.abort(new CancelledError(message));
|
|
159
278
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
this.#mustCancel = true;
|
|
169
|
-
}
|
|
279
|
+
// Cancel what the task is suspended on; it resumes with the outcome.
|
|
280
|
+
if (this._futWaiter?.cancel(message))
|
|
281
|
+
return true;
|
|
282
|
+
// Running, woken up but not resumed yet, or inside a foreign await.
|
|
283
|
+
this.#mustCancel = true;
|
|
284
|
+
this.#cancelMessage = message;
|
|
170
285
|
return true;
|
|
171
286
|
}
|
|
172
287
|
/**
|
|
173
|
-
* Throw CancelledError if a cancellation request is pending.
|
|
174
|
-
*
|
|
288
|
+
* Throw CancelledError if a cancellation request is pending. Primitives
|
|
289
|
+
* call this on their fast paths, so a task that resumes from a foreign
|
|
290
|
+
* await after being cancelled stops at the next one.
|
|
175
291
|
*/
|
|
176
292
|
checkpoint() {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
throw
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
/** @internal Register the rejecter of the await this task is suspended on. */
|
|
183
|
-
_addWaiter(waiter) {
|
|
184
|
-
this.#waiters.add(waiter);
|
|
185
|
-
}
|
|
186
|
-
/** @internal */
|
|
187
|
-
_removeWaiter(waiter) {
|
|
188
|
-
this.#waiters.delete(waiter);
|
|
293
|
+
const cancelled = this._takeCancelRequest();
|
|
294
|
+
if (cancelled)
|
|
295
|
+
throw cancelled;
|
|
189
296
|
}
|
|
190
297
|
/** A promise that settles with the task, without being a cancellation point. */
|
|
191
298
|
get promise() {
|
|
299
|
+
if (!this.#promise) {
|
|
300
|
+
this.#promise = new Promise((resolve, reject) => {
|
|
301
|
+
this.addDoneCallback(() => {
|
|
302
|
+
try {
|
|
303
|
+
resolve(this.result());
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
reject(e);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
// Errors are observed through the task; never an unhandled rejection.
|
|
311
|
+
this.#promise.catch(() => { });
|
|
312
|
+
}
|
|
192
313
|
return this.#promise;
|
|
193
314
|
}
|
|
194
|
-
// biome-ignore lint/suspicious/noThenProperty: Tasks are awaitable by design, like asyncio.
|
|
195
|
-
then(onfulfilled, onrejected) {
|
|
196
|
-
const awaited = suspend((resolve, reject) => {
|
|
197
|
-
this.#promise.then(resolve, reject);
|
|
198
|
-
return () => { };
|
|
199
|
-
}, () => {
|
|
200
|
-
// asyncio: cancelling a task that awaits another task cancels the latter.
|
|
201
|
-
this.cancel();
|
|
202
|
-
});
|
|
203
|
-
return awaited.then(onfulfilled, onrejected);
|
|
204
|
-
}
|
|
205
315
|
toString() {
|
|
206
316
|
return this.#name;
|
|
207
317
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
this.#
|
|
211
|
-
|
|
212
|
-
this.#
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
throw e;
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
}
|
|
318
|
+
/** @internal Consume a pending cancellation request, returning its error. */
|
|
319
|
+
_takeCancelRequest() {
|
|
320
|
+
if (!this.#mustCancel)
|
|
321
|
+
return undefined;
|
|
322
|
+
this.#mustCancel = false;
|
|
323
|
+
return new CancelledError(this.#cancelMessage);
|
|
324
|
+
}
|
|
325
|
+
/** @internal The task suspends on `future` (Task.__step yielding a future). */
|
|
326
|
+
_block(future) {
|
|
327
|
+
this._futWaiter = future;
|
|
328
|
+
if (this.#mustCancel && future.cancel(this.#cancelMessage))
|
|
329
|
+
this.#mustCancel = false;
|
|
224
330
|
}
|
|
225
331
|
}
|
|
226
332
|
/**
|
|
@@ -229,47 +335,29 @@ export class Task {
|
|
|
229
335
|
* registration (clear a timer, remove a listener...) when the wait is
|
|
230
336
|
* cancelled. `onCancel` runs after the disposer, e.g. to cancel a child task.
|
|
231
337
|
*/
|
|
232
|
-
export function suspend(register, onCancel) {
|
|
233
|
-
const
|
|
338
|
+
export async function suspend(register, onCancel) {
|
|
339
|
+
const future = new Future();
|
|
340
|
+
const dispose = register((value) => {
|
|
341
|
+
if (!future.done())
|
|
342
|
+
future.setResult(value);
|
|
343
|
+
}, (error) => {
|
|
344
|
+
if (!future.done())
|
|
345
|
+
future.setException(error);
|
|
346
|
+
});
|
|
234
347
|
try {
|
|
235
|
-
|
|
348
|
+
return await awaitFuture(future);
|
|
236
349
|
}
|
|
237
350
|
catch (e) {
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
return new Promise((resolve, reject) => {
|
|
241
|
-
let settled = false;
|
|
242
|
-
let dispose = () => { };
|
|
243
|
-
const waiter = (error) => {
|
|
244
|
-
if (settled)
|
|
245
|
-
return;
|
|
246
|
-
settled = true;
|
|
351
|
+
if (future.cancelled()) {
|
|
247
352
|
dispose();
|
|
248
353
|
onCancel?.();
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
dispose = register((value) => {
|
|
253
|
-
if (settled)
|
|
254
|
-
return;
|
|
255
|
-
settled = true;
|
|
256
|
-
task?._removeWaiter(waiter);
|
|
257
|
-
resolve(value);
|
|
258
|
-
}, (error) => {
|
|
259
|
-
if (settled)
|
|
260
|
-
return;
|
|
261
|
-
settled = true;
|
|
262
|
-
task?._removeWaiter(waiter);
|
|
263
|
-
reject(error);
|
|
264
|
-
});
|
|
265
|
-
});
|
|
354
|
+
}
|
|
355
|
+
throw e;
|
|
356
|
+
}
|
|
266
357
|
}
|
|
267
358
|
/** Create and schedule a task (asyncio.create_task). */
|
|
268
359
|
export function createTask(fn, name) {
|
|
269
|
-
|
|
270
|
-
// Errors are observed through the task; never surface as unhandled rejections.
|
|
271
|
-
task.promise.catch(() => { });
|
|
272
|
-
return task;
|
|
360
|
+
return new Task(fn, name);
|
|
273
361
|
}
|
|
274
362
|
/** Throw if the current task has a pending cancellation (an explicit `await asyncio.sleep(0)`). */
|
|
275
363
|
export function checkCancelled() {
|
|
@@ -281,10 +369,15 @@ export function checkCancelled() {
|
|
|
281
369
|
* The foreign operation keeps running unless it was also given `task.signal`.
|
|
282
370
|
*/
|
|
283
371
|
export function abortable(promise) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
372
|
+
const future = new Future();
|
|
373
|
+
promise.then((value) => {
|
|
374
|
+
if (!future.done())
|
|
375
|
+
future.setResult(value);
|
|
376
|
+
}, (error) => {
|
|
377
|
+
if (!future.done())
|
|
378
|
+
future.setException(error);
|
|
287
379
|
});
|
|
380
|
+
return awaitFuture(future);
|
|
288
381
|
}
|
|
289
382
|
/** The current task's abort signal, or a never-aborting one outside tasks. */
|
|
290
383
|
export function currentSignal() {
|
package/package.json
CHANGED