@pipedog/asyncio 0.0.0 → 0.1.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/LICENSE +25 -0
- package/NOTICE +9 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -0
- package/dist/primitives.d.ts +127 -0
- package/dist/primitives.js +492 -0
- package/dist/task.d.ts +85 -0
- package/dist/task.js +293 -0
- package/package.json +29 -3
- package/README.md +0 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024–2026, Daily
|
|
4
|
+
Copyright (c) 2026, pipedog contributors
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
17
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
18
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
20
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
23
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
24
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
25
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pipedog is an unofficial TypeScript port of Pipecat (https://github.com/pipecat-ai/pipecat),
|
|
2
|
+
a framework for voice and multimodal conversational AI created by Daily.
|
|
3
|
+
|
|
4
|
+
It is not affiliated with or endorsed by Daily. Pipecat is distributed under
|
|
5
|
+
the BSD 2-Clause License, Copyright (c) 2024–2026, Daily; that copyright notice
|
|
6
|
+
and license are retained in LICENSE, and every ported source file carries a
|
|
7
|
+
header naming the upstream file it was derived from.
|
|
8
|
+
|
|
9
|
+
The upstream commit each file was ported from is recorded in porting/manifest.json.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Task, type TaskFn } from "./task.ts";
|
|
2
|
+
/** Monotonic time in seconds (loop.time()). */
|
|
3
|
+
export declare function monotonic(): number;
|
|
4
|
+
/** Monotonic time in nanoseconds (time.monotonic_ns()). */
|
|
5
|
+
export declare function monotonicNs(): number;
|
|
6
|
+
/**
|
|
7
|
+
* Wall-clock time in seconds (time.time()).
|
|
8
|
+
*
|
|
9
|
+
* Sub-millisecond, like Python's: `Date.now()` alone would give successive
|
|
10
|
+
* readings the same value, so intervals between frames pushed back to back
|
|
11
|
+
* would measure as zero. Falls back to `Date.now()` where the runtime has no
|
|
12
|
+
* time origin.
|
|
13
|
+
*/
|
|
14
|
+
export declare function time(): number;
|
|
15
|
+
/** asyncio.sleep(seconds). `sleep(0)` yields to other tasks. */
|
|
16
|
+
export declare function sleep(seconds: number): Promise<void>;
|
|
17
|
+
/** asyncio.Event */
|
|
18
|
+
export declare class Event {
|
|
19
|
+
#private;
|
|
20
|
+
isSet(): boolean;
|
|
21
|
+
set(): void;
|
|
22
|
+
clear(): void;
|
|
23
|
+
wait(): Promise<true>;
|
|
24
|
+
}
|
|
25
|
+
export declare class QueueEmpty extends Error {
|
|
26
|
+
name: string;
|
|
27
|
+
}
|
|
28
|
+
export declare class QueueFull extends Error {
|
|
29
|
+
name: string;
|
|
30
|
+
}
|
|
31
|
+
export declare class QueueShutDown extends Error {
|
|
32
|
+
name: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* asyncio.Queue. Subclasses override `_init/_put/_get/_size/_items` to change
|
|
36
|
+
* ordering, as Python's PriorityQueue and LifoQueue do.
|
|
37
|
+
*/
|
|
38
|
+
export declare class Queue<T> {
|
|
39
|
+
#private;
|
|
40
|
+
readonly maxsize: number;
|
|
41
|
+
constructor(maxsize?: number);
|
|
42
|
+
protected _items: T[];
|
|
43
|
+
protected _init(): void;
|
|
44
|
+
protected _put(item: T): void;
|
|
45
|
+
protected _get(): T;
|
|
46
|
+
protected _size(): number;
|
|
47
|
+
qsize(): number;
|
|
48
|
+
empty(): boolean;
|
|
49
|
+
full(): boolean;
|
|
50
|
+
put(item: T): Promise<void>;
|
|
51
|
+
putNowait(item: T): void;
|
|
52
|
+
get(): Promise<T>;
|
|
53
|
+
getNowait(): T;
|
|
54
|
+
taskDone(): void;
|
|
55
|
+
join(): Promise<void>;
|
|
56
|
+
shutdown(immediate?: boolean): void;
|
|
57
|
+
}
|
|
58
|
+
/** asyncio.LifoQueue */
|
|
59
|
+
export declare class LifoQueue<T> extends Queue<T> {
|
|
60
|
+
protected _get(): T;
|
|
61
|
+
}
|
|
62
|
+
/** Default ordering for PriorityQueue: compares numbers, strings and tuples (arrays) like Python. */
|
|
63
|
+
export declare function pythonCompare(a: unknown, b: unknown): number;
|
|
64
|
+
/** heapq.heappush */
|
|
65
|
+
export declare function heapPush<T>(heap: T[], item: T, compare?: (a: T, b: T) => number): void;
|
|
66
|
+
/** heapq.heappop */
|
|
67
|
+
export declare function heapPop<T>(heap: T[], compare?: (a: T, b: T) => number): T;
|
|
68
|
+
/** asyncio.PriorityQueue backed by a binary heap. Smallest item first. */
|
|
69
|
+
export declare class PriorityQueue<T> extends Queue<T> {
|
|
70
|
+
protected readonly _compare: (a: T, b: T) => number;
|
|
71
|
+
constructor(maxsize?: number, compare?: (a: T, b: T) => number);
|
|
72
|
+
protected _put(item: T): void;
|
|
73
|
+
protected _get(): T;
|
|
74
|
+
}
|
|
75
|
+
/** asyncio.Lock */
|
|
76
|
+
export declare class Lock {
|
|
77
|
+
#private;
|
|
78
|
+
locked(): boolean;
|
|
79
|
+
acquire(): Promise<true>;
|
|
80
|
+
release(): void;
|
|
81
|
+
/** `async with lock:` → `await lock.run(async () => {...})` */
|
|
82
|
+
run<R>(fn: () => Promise<R> | R): Promise<R>;
|
|
83
|
+
}
|
|
84
|
+
/** asyncio.Semaphore */
|
|
85
|
+
export declare class Semaphore {
|
|
86
|
+
#private;
|
|
87
|
+
constructor(value?: number);
|
|
88
|
+
locked(): boolean;
|
|
89
|
+
acquire(): Promise<true>;
|
|
90
|
+
release(): void;
|
|
91
|
+
run<R>(fn: () => Promise<R> | R): Promise<R>;
|
|
92
|
+
}
|
|
93
|
+
export type Awaitable<T> = TaskFn<T> | Task<T> | PromiseLike<T>;
|
|
94
|
+
/**
|
|
95
|
+
* asyncio.wait_for(aw, timeout). Pass a function (a "coroutine") so it can
|
|
96
|
+
* run as a child task that is cancelled on timeout. `timeout` null waits forever.
|
|
97
|
+
*/
|
|
98
|
+
export declare function waitFor<T>(aw: Awaitable<T>, timeout: number | null | undefined): Promise<T>;
|
|
99
|
+
/**
|
|
100
|
+
* asyncio.gather(*aws). Functions run as child tasks; if the caller is
|
|
101
|
+
* cancelled, all children are cancelled.
|
|
102
|
+
*/
|
|
103
|
+
export declare function gather<T extends readonly unknown[]>(...aws: {
|
|
104
|
+
[K in keyof T]: Awaitable<T[K]>;
|
|
105
|
+
}): Promise<T>;
|
|
106
|
+
/** asyncio.gather(*aws, return_exceptions=True) */
|
|
107
|
+
export declare function gatherSettled(...aws: Awaitable<unknown>[]): Promise<unknown[]>;
|
|
108
|
+
export declare const FIRST_COMPLETED = "FIRST_COMPLETED";
|
|
109
|
+
export declare const FIRST_EXCEPTION = "FIRST_EXCEPTION";
|
|
110
|
+
export declare const ALL_COMPLETED = "ALL_COMPLETED";
|
|
111
|
+
export type ReturnWhen = typeof FIRST_COMPLETED | typeof FIRST_EXCEPTION | typeof ALL_COMPLETED;
|
|
112
|
+
/**
|
|
113
|
+
* asyncio.wait(tasks, timeout, return_when). Does not cancel anything on
|
|
114
|
+
* timeout. Returns [done, pending].
|
|
115
|
+
*/
|
|
116
|
+
export declare function wait<T>(tasks: Iterable<Task<T>>, options?: {
|
|
117
|
+
timeout?: number | null;
|
|
118
|
+
returnWhen?: ReturnWhen;
|
|
119
|
+
}): Promise<[Set<Task<T>>, Set<Task<T>>]>;
|
|
120
|
+
/**
|
|
121
|
+
* asyncio.timeout(seconds) as a scope: runs `fn` and cancels it on timeout,
|
|
122
|
+
* raising TimeoutError.
|
|
123
|
+
*/
|
|
124
|
+
export declare function timeout<T>(seconds: number | null, fn: TaskFn<T>): Promise<T>;
|
|
125
|
+
/** asyncio.shield(aw): the caller can be cancelled without cancelling `aw`. */
|
|
126
|
+
export declare function shield<T>(aw: Awaitable<T>): Promise<T>;
|
|
127
|
+
//# sourceMappingURL=primitives.d.ts.map
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// Copyright (c) 2026, pipedog contributors
|
|
2
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
3
|
+
//
|
|
4
|
+
// asyncio synchronization primitives and helpers. Every wait here is a
|
|
5
|
+
// cancellation point (see task.ts). Durations are in seconds, as in Python.
|
|
6
|
+
import { CancelledError, createTask, currentTask, suspend, Task, TimeoutError, } from "./task.js";
|
|
7
|
+
/** Monotonic time in seconds (loop.time()). */
|
|
8
|
+
export function monotonic() {
|
|
9
|
+
return performance.now() / 1000;
|
|
10
|
+
}
|
|
11
|
+
/** Monotonic time in nanoseconds (time.monotonic_ns()). */
|
|
12
|
+
export function monotonicNs() {
|
|
13
|
+
return Math.round(performance.now() * 1_000_000);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Wall-clock time in seconds (time.time()).
|
|
17
|
+
*
|
|
18
|
+
* Sub-millisecond, like Python's: `Date.now()` alone would give successive
|
|
19
|
+
* readings the same value, so intervals between frames pushed back to back
|
|
20
|
+
* would measure as zero. Falls back to `Date.now()` where the runtime has no
|
|
21
|
+
* time origin.
|
|
22
|
+
*/
|
|
23
|
+
export function time() {
|
|
24
|
+
const origin = performance.timeOrigin;
|
|
25
|
+
return origin ? (origin + performance.now()) / 1000 : Date.now() / 1000;
|
|
26
|
+
}
|
|
27
|
+
/** asyncio.sleep(seconds). `sleep(0)` yields to other tasks. */
|
|
28
|
+
export function sleep(seconds) {
|
|
29
|
+
return suspend((resolve) => {
|
|
30
|
+
const timer = setTimeout(resolve, Math.max(0, seconds * 1000));
|
|
31
|
+
return () => clearTimeout(timer);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** asyncio.Event */
|
|
35
|
+
export class Event {
|
|
36
|
+
#set = false;
|
|
37
|
+
#waiters = new Set();
|
|
38
|
+
isSet() {
|
|
39
|
+
return this.#set;
|
|
40
|
+
}
|
|
41
|
+
set() {
|
|
42
|
+
if (this.#set)
|
|
43
|
+
return;
|
|
44
|
+
this.#set = true;
|
|
45
|
+
const waiters = [...this.#waiters];
|
|
46
|
+
this.#waiters.clear();
|
|
47
|
+
for (const waiter of waiters)
|
|
48
|
+
waiter();
|
|
49
|
+
}
|
|
50
|
+
clear() {
|
|
51
|
+
this.#set = false;
|
|
52
|
+
}
|
|
53
|
+
wait() {
|
|
54
|
+
if (this.#set) {
|
|
55
|
+
try {
|
|
56
|
+
currentTask()?.checkpoint();
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
return Promise.reject(e);
|
|
60
|
+
}
|
|
61
|
+
return Promise.resolve(true);
|
|
62
|
+
}
|
|
63
|
+
return suspend((resolve) => {
|
|
64
|
+
const waiter = () => resolve(true);
|
|
65
|
+
this.#waiters.add(waiter);
|
|
66
|
+
return () => this.#waiters.delete(waiter);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export class QueueEmpty extends Error {
|
|
71
|
+
name = "QueueEmpty";
|
|
72
|
+
}
|
|
73
|
+
export class QueueFull extends Error {
|
|
74
|
+
name = "QueueFull";
|
|
75
|
+
}
|
|
76
|
+
export class QueueShutDown extends Error {
|
|
77
|
+
name = "QueueShutDown";
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* asyncio.Queue. Subclasses override `_init/_put/_get/_size/_items` to change
|
|
81
|
+
* ordering, as Python's PriorityQueue and LifoQueue do.
|
|
82
|
+
*/
|
|
83
|
+
export class Queue {
|
|
84
|
+
maxsize;
|
|
85
|
+
#getters = [];
|
|
86
|
+
#putters = [];
|
|
87
|
+
#unfinished = 0;
|
|
88
|
+
#finished = new Event();
|
|
89
|
+
#shutdown = false;
|
|
90
|
+
constructor(maxsize = 0) {
|
|
91
|
+
this.maxsize = maxsize;
|
|
92
|
+
this._init();
|
|
93
|
+
this.#finished.set();
|
|
94
|
+
}
|
|
95
|
+
// Storage hooks.
|
|
96
|
+
_items = [];
|
|
97
|
+
_init() {
|
|
98
|
+
this._items = [];
|
|
99
|
+
}
|
|
100
|
+
_put(item) {
|
|
101
|
+
this._items.push(item);
|
|
102
|
+
}
|
|
103
|
+
_get() {
|
|
104
|
+
return this._items.shift();
|
|
105
|
+
}
|
|
106
|
+
_size() {
|
|
107
|
+
return this._items.length;
|
|
108
|
+
}
|
|
109
|
+
qsize() {
|
|
110
|
+
return this._size();
|
|
111
|
+
}
|
|
112
|
+
empty() {
|
|
113
|
+
return this._size() === 0;
|
|
114
|
+
}
|
|
115
|
+
full() {
|
|
116
|
+
return this.maxsize > 0 && this._size() >= this.maxsize;
|
|
117
|
+
}
|
|
118
|
+
async put(item) {
|
|
119
|
+
// A cancellation point even when it doesn't block: after an interruption,
|
|
120
|
+
// a task resuming from a foreign await must not push stale items.
|
|
121
|
+
currentTask()?.checkpoint();
|
|
122
|
+
while (this.full()) {
|
|
123
|
+
if (this.#shutdown)
|
|
124
|
+
throw new QueueShutDown();
|
|
125
|
+
await suspend((resolve) => {
|
|
126
|
+
this.#putters.push(resolve);
|
|
127
|
+
return () => {
|
|
128
|
+
const i = this.#putters.indexOf(resolve);
|
|
129
|
+
if (i >= 0)
|
|
130
|
+
this.#putters.splice(i, 1);
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
this.putNowait(item);
|
|
135
|
+
}
|
|
136
|
+
putNowait(item) {
|
|
137
|
+
if (this.#shutdown)
|
|
138
|
+
throw new QueueShutDown();
|
|
139
|
+
if (this.full())
|
|
140
|
+
throw new QueueFull();
|
|
141
|
+
this._put(item);
|
|
142
|
+
this.#unfinished++;
|
|
143
|
+
this.#finished.clear();
|
|
144
|
+
this.#wakeGetter();
|
|
145
|
+
}
|
|
146
|
+
async get() {
|
|
147
|
+
if (!this.empty() && this.#getters.length === 0) {
|
|
148
|
+
currentTask()?.checkpoint();
|
|
149
|
+
return this.getNowait();
|
|
150
|
+
}
|
|
151
|
+
if (this.#shutdown && this.empty())
|
|
152
|
+
throw new QueueShutDown();
|
|
153
|
+
return suspend((resolve, reject) => {
|
|
154
|
+
const getter = { resolve, reject };
|
|
155
|
+
this.#getters.push(getter);
|
|
156
|
+
// Item may already be available if getters were queued before us.
|
|
157
|
+
this.#wakeGetter();
|
|
158
|
+
return () => {
|
|
159
|
+
const i = this.#getters.indexOf(getter);
|
|
160
|
+
if (i >= 0)
|
|
161
|
+
this.#getters.splice(i, 1);
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
getNowait() {
|
|
166
|
+
if (this.empty()) {
|
|
167
|
+
if (this.#shutdown)
|
|
168
|
+
throw new QueueShutDown();
|
|
169
|
+
throw new QueueEmpty();
|
|
170
|
+
}
|
|
171
|
+
const item = this._get();
|
|
172
|
+
this.#wakePutter();
|
|
173
|
+
return item;
|
|
174
|
+
}
|
|
175
|
+
taskDone() {
|
|
176
|
+
if (this.#unfinished <= 0)
|
|
177
|
+
throw new Error("task_done() called too many times");
|
|
178
|
+
this.#unfinished--;
|
|
179
|
+
if (this.#unfinished === 0)
|
|
180
|
+
this.#finished.set();
|
|
181
|
+
}
|
|
182
|
+
async join() {
|
|
183
|
+
if (this.#unfinished > 0)
|
|
184
|
+
await this.#finished.wait();
|
|
185
|
+
}
|
|
186
|
+
shutdown(immediate = false) {
|
|
187
|
+
this.#shutdown = true;
|
|
188
|
+
if (immediate) {
|
|
189
|
+
while (!this.empty()) {
|
|
190
|
+
this._get();
|
|
191
|
+
if (this.#unfinished > 0)
|
|
192
|
+
this.#unfinished--;
|
|
193
|
+
}
|
|
194
|
+
if (this.#unfinished === 0)
|
|
195
|
+
this.#finished.set();
|
|
196
|
+
}
|
|
197
|
+
for (const getter of this.#getters.splice(0))
|
|
198
|
+
getter.reject(new QueueShutDown());
|
|
199
|
+
for (const putter of this.#putters.splice(0))
|
|
200
|
+
putter();
|
|
201
|
+
}
|
|
202
|
+
#wakeGetter() {
|
|
203
|
+
while (this.#getters.length > 0 && !this.empty()) {
|
|
204
|
+
const getter = this.#getters.shift();
|
|
205
|
+
const item = this._get();
|
|
206
|
+
getter.resolve(item);
|
|
207
|
+
this.#wakePutter();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
#wakePutter() {
|
|
211
|
+
const putter = this.#putters.shift();
|
|
212
|
+
putter?.();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** asyncio.LifoQueue */
|
|
216
|
+
export class LifoQueue extends Queue {
|
|
217
|
+
_get() {
|
|
218
|
+
return this._items.pop();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** Default ordering for PriorityQueue: compares numbers, strings and tuples (arrays) like Python. */
|
|
222
|
+
export function pythonCompare(a, b) {
|
|
223
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
224
|
+
const n = Math.min(a.length, b.length);
|
|
225
|
+
for (let i = 0; i < n; i++) {
|
|
226
|
+
const c = pythonCompare(a[i], b[i]);
|
|
227
|
+
if (c !== 0)
|
|
228
|
+
return c;
|
|
229
|
+
}
|
|
230
|
+
return a.length - b.length;
|
|
231
|
+
}
|
|
232
|
+
if (typeof a === "number" && typeof b === "number")
|
|
233
|
+
return a - b;
|
|
234
|
+
if (typeof a === "string" && typeof b === "string")
|
|
235
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
236
|
+
if (typeof a === "bigint" && typeof b === "bigint")
|
|
237
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
238
|
+
throw new TypeError(`'<' not supported between ${typeof a} and ${typeof b}`);
|
|
239
|
+
}
|
|
240
|
+
/** heapq.heappush */
|
|
241
|
+
export function heapPush(heap, item, compare = pythonCompare) {
|
|
242
|
+
heap.push(item);
|
|
243
|
+
let i = heap.length - 1;
|
|
244
|
+
while (i > 0) {
|
|
245
|
+
const parent = (i - 1) >> 1;
|
|
246
|
+
if (compare(heap[i], heap[parent]) >= 0)
|
|
247
|
+
break;
|
|
248
|
+
[heap[i], heap[parent]] = [heap[parent], heap[i]];
|
|
249
|
+
i = parent;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** heapq.heappop */
|
|
253
|
+
export function heapPop(heap, compare = pythonCompare) {
|
|
254
|
+
const top = heap[0];
|
|
255
|
+
const last = heap.pop();
|
|
256
|
+
if (heap.length > 0) {
|
|
257
|
+
heap[0] = last;
|
|
258
|
+
let i = 0;
|
|
259
|
+
for (;;) {
|
|
260
|
+
const l = 2 * i + 1;
|
|
261
|
+
const r = l + 1;
|
|
262
|
+
let m = i;
|
|
263
|
+
if (l < heap.length && compare(heap[l], heap[m]) < 0)
|
|
264
|
+
m = l;
|
|
265
|
+
if (r < heap.length && compare(heap[r], heap[m]) < 0)
|
|
266
|
+
m = r;
|
|
267
|
+
if (m === i)
|
|
268
|
+
break;
|
|
269
|
+
[heap[i], heap[m]] = [heap[m], heap[i]];
|
|
270
|
+
i = m;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return top;
|
|
274
|
+
}
|
|
275
|
+
/** asyncio.PriorityQueue backed by a binary heap. Smallest item first. */
|
|
276
|
+
export class PriorityQueue extends Queue {
|
|
277
|
+
_compare;
|
|
278
|
+
constructor(maxsize = 0, compare = pythonCompare) {
|
|
279
|
+
super(maxsize);
|
|
280
|
+
this._compare = compare;
|
|
281
|
+
}
|
|
282
|
+
_put(item) {
|
|
283
|
+
heapPush(this._items, item, this._compare);
|
|
284
|
+
}
|
|
285
|
+
_get() {
|
|
286
|
+
return heapPop(this._items, this._compare);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** asyncio.Lock */
|
|
290
|
+
export class Lock {
|
|
291
|
+
#locked = false;
|
|
292
|
+
#waiters = [];
|
|
293
|
+
locked() {
|
|
294
|
+
return this.#locked;
|
|
295
|
+
}
|
|
296
|
+
async acquire() {
|
|
297
|
+
if (!this.#locked && this.#waiters.length === 0) {
|
|
298
|
+
this.#locked = true;
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
await suspend((resolve) => {
|
|
302
|
+
this.#waiters.push(resolve);
|
|
303
|
+
return () => {
|
|
304
|
+
const i = this.#waiters.indexOf(resolve);
|
|
305
|
+
if (i >= 0)
|
|
306
|
+
this.#waiters.splice(i, 1);
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
this.#locked = true;
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
release() {
|
|
313
|
+
if (!this.#locked)
|
|
314
|
+
throw new Error("Lock is not acquired.");
|
|
315
|
+
this.#locked = false;
|
|
316
|
+
const next = this.#waiters.shift();
|
|
317
|
+
if (next) {
|
|
318
|
+
// Hand over directly so no one can barge in.
|
|
319
|
+
this.#locked = true;
|
|
320
|
+
next();
|
|
321
|
+
// `acquire` sets locked again after resuming; keep it consistent.
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** `async with lock:` → `await lock.run(async () => {...})` */
|
|
325
|
+
async run(fn) {
|
|
326
|
+
await this.acquire();
|
|
327
|
+
try {
|
|
328
|
+
return await fn();
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
this.release();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/** asyncio.Semaphore */
|
|
336
|
+
export class Semaphore {
|
|
337
|
+
#value;
|
|
338
|
+
#waiters = [];
|
|
339
|
+
constructor(value = 1) {
|
|
340
|
+
this.#value = value;
|
|
341
|
+
}
|
|
342
|
+
locked() {
|
|
343
|
+
return this.#value === 0;
|
|
344
|
+
}
|
|
345
|
+
async acquire() {
|
|
346
|
+
if (this.#value > 0 && this.#waiters.length === 0) {
|
|
347
|
+
this.#value--;
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
await suspend((resolve) => {
|
|
351
|
+
this.#waiters.push(resolve);
|
|
352
|
+
return () => {
|
|
353
|
+
const i = this.#waiters.indexOf(resolve);
|
|
354
|
+
if (i >= 0)
|
|
355
|
+
this.#waiters.splice(i, 1);
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
release() {
|
|
361
|
+
const next = this.#waiters.shift();
|
|
362
|
+
if (next)
|
|
363
|
+
next();
|
|
364
|
+
else
|
|
365
|
+
this.#value++;
|
|
366
|
+
}
|
|
367
|
+
async run(fn) {
|
|
368
|
+
await this.acquire();
|
|
369
|
+
try {
|
|
370
|
+
return await fn();
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
this.release();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function toTask(aw, name) {
|
|
378
|
+
if (aw instanceof Task)
|
|
379
|
+
return aw;
|
|
380
|
+
if (typeof aw === "function")
|
|
381
|
+
return createTask(aw, name);
|
|
382
|
+
return createTask(() => aw, name);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* asyncio.wait_for(aw, timeout). Pass a function (a "coroutine") so it can
|
|
386
|
+
* run as a child task that is cancelled on timeout. `timeout` null waits forever.
|
|
387
|
+
*/
|
|
388
|
+
export async function waitFor(aw, timeout) {
|
|
389
|
+
const child = toTask(aw, "wait_for");
|
|
390
|
+
if (timeout === null || timeout === undefined)
|
|
391
|
+
return await child;
|
|
392
|
+
let timedOut = false;
|
|
393
|
+
const timer = setTimeout(() => {
|
|
394
|
+
timedOut = true;
|
|
395
|
+
child.cancel();
|
|
396
|
+
}, Math.max(0, timeout * 1000));
|
|
397
|
+
try {
|
|
398
|
+
return await child;
|
|
399
|
+
}
|
|
400
|
+
catch (e) {
|
|
401
|
+
if (timedOut && e instanceof CancelledError && !(currentTask()?.cancelling() ?? 0)) {
|
|
402
|
+
throw new TimeoutError();
|
|
403
|
+
}
|
|
404
|
+
throw e;
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
clearTimeout(timer);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* asyncio.gather(*aws). Functions run as child tasks; if the caller is
|
|
412
|
+
* cancelled, all children are cancelled.
|
|
413
|
+
*/
|
|
414
|
+
export async function gather(...aws) {
|
|
415
|
+
const tasks = aws.map((aw) => toTask(aw));
|
|
416
|
+
return (await suspend((resolve, reject) => {
|
|
417
|
+
Promise.all(tasks.map((t) => t.promise)).then(resolve, reject);
|
|
418
|
+
return () => { };
|
|
419
|
+
}, () => {
|
|
420
|
+
for (const t of tasks)
|
|
421
|
+
t.cancel();
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
/** asyncio.gather(*aws, return_exceptions=True) */
|
|
425
|
+
export async function gatherSettled(...aws) {
|
|
426
|
+
const tasks = aws.map((aw) => toTask(aw));
|
|
427
|
+
return await suspend((resolve) => {
|
|
428
|
+
Promise.allSettled(tasks.map((t) => t.promise)).then((results) => resolve(results.map((r) => (r.status === "fulfilled" ? r.value : r.reason))));
|
|
429
|
+
return () => { };
|
|
430
|
+
}, () => {
|
|
431
|
+
for (const t of tasks)
|
|
432
|
+
t.cancel();
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
export const FIRST_COMPLETED = "FIRST_COMPLETED";
|
|
436
|
+
export const FIRST_EXCEPTION = "FIRST_EXCEPTION";
|
|
437
|
+
export const ALL_COMPLETED = "ALL_COMPLETED";
|
|
438
|
+
/**
|
|
439
|
+
* asyncio.wait(tasks, timeout, return_when). Does not cancel anything on
|
|
440
|
+
* timeout. Returns [done, pending].
|
|
441
|
+
*/
|
|
442
|
+
export function wait(tasks, options = {}) {
|
|
443
|
+
const all = [...tasks];
|
|
444
|
+
const returnWhen = options.returnWhen ?? ALL_COMPLETED;
|
|
445
|
+
const result = () => [
|
|
446
|
+
new Set(all.filter((t) => t.done())),
|
|
447
|
+
new Set(all.filter((t) => !t.done())),
|
|
448
|
+
];
|
|
449
|
+
return suspend((resolve) => {
|
|
450
|
+
let timer;
|
|
451
|
+
const check = () => {
|
|
452
|
+
const done = all.filter((t) => t.done());
|
|
453
|
+
const finished = done.length === all.length ||
|
|
454
|
+
(returnWhen === FIRST_COMPLETED && done.length > 0) ||
|
|
455
|
+
(returnWhen === FIRST_EXCEPTION &&
|
|
456
|
+
done.some((t) => !t.cancelled() && t.exception() !== undefined));
|
|
457
|
+
if (finished) {
|
|
458
|
+
if (timer)
|
|
459
|
+
clearTimeout(timer);
|
|
460
|
+
resolve(result());
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
for (const t of all)
|
|
464
|
+
t.addDoneCallback(check);
|
|
465
|
+
if (options.timeout !== undefined && options.timeout !== null) {
|
|
466
|
+
timer = setTimeout(() => resolve(result()), options.timeout * 1000);
|
|
467
|
+
}
|
|
468
|
+
check();
|
|
469
|
+
return () => {
|
|
470
|
+
if (timer)
|
|
471
|
+
clearTimeout(timer);
|
|
472
|
+
for (const t of all)
|
|
473
|
+
t.removeDoneCallback(check);
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* asyncio.timeout(seconds) as a scope: runs `fn` and cancels it on timeout,
|
|
479
|
+
* raising TimeoutError.
|
|
480
|
+
*/
|
|
481
|
+
export function timeout(seconds, fn) {
|
|
482
|
+
return waitFor(fn, seconds);
|
|
483
|
+
}
|
|
484
|
+
/** asyncio.shield(aw): the caller can be cancelled without cancelling `aw`. */
|
|
485
|
+
export function shield(aw) {
|
|
486
|
+
const task = toTask(aw);
|
|
487
|
+
return suspend((resolve, reject) => {
|
|
488
|
+
task.promise.then(resolve, reject);
|
|
489
|
+
return () => { };
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
//# sourceMappingURL=primitives.js.map
|
package/dist/task.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export declare class CancelledError extends Error {
|
|
2
|
+
name: string;
|
|
3
|
+
constructor(message?: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class TimeoutError extends Error {
|
|
6
|
+
name: string;
|
|
7
|
+
constructor(message?: string);
|
|
8
|
+
}
|
|
9
|
+
export declare class InvalidStateError extends Error {
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
/** Whether `e` is a cancellation. Use it to re-throw in broad `catch` blocks. */
|
|
13
|
+
export declare function isCancelledError(e: unknown): e is CancelledError;
|
|
14
|
+
/**
|
|
15
|
+
* Re-throw `e` if it is a cancellation. Python's `except Exception` does not
|
|
16
|
+
* catch `CancelledError` (it is a BaseException); JavaScript's `catch` catches
|
|
17
|
+
* everything, so ported `except Exception` blocks start with this call.
|
|
18
|
+
*/
|
|
19
|
+
export declare function rethrowIfCancelled(e: unknown): void;
|
|
20
|
+
/** The task the caller is running in, if any (asyncio.current_task). */
|
|
21
|
+
export declare function currentTask(): Task<unknown> | undefined;
|
|
22
|
+
type Waiter = (error: CancelledError) => void;
|
|
23
|
+
export type TaskFn<T> = () => Promise<T> | T;
|
|
24
|
+
/**
|
|
25
|
+
* A unit of concurrent work, like asyncio.Task. Created with `createTask()`.
|
|
26
|
+
* Awaiting a Task (it is thenable) is a cancellation point; cancelling the
|
|
27
|
+
* awaiting task also cancels the awaited one, as in asyncio.
|
|
28
|
+
*/
|
|
29
|
+
export declare class Task<T> implements PromiseLike<T> {
|
|
30
|
+
#private;
|
|
31
|
+
constructor(fn: TaskFn<T>, name?: string);
|
|
32
|
+
get name(): string;
|
|
33
|
+
/** asyncio Task.get_name() */
|
|
34
|
+
getName(): string;
|
|
35
|
+
setName(name: string): void;
|
|
36
|
+
/** Aborted when cancellation is requested. Pass it to fetch() and friends. */
|
|
37
|
+
get signal(): AbortSignal;
|
|
38
|
+
done(): boolean;
|
|
39
|
+
cancelled(): boolean;
|
|
40
|
+
/** Number of pending cancellation requests (asyncio Task.cancelling()). */
|
|
41
|
+
cancelling(): number;
|
|
42
|
+
uncancel(): number;
|
|
43
|
+
result(): T;
|
|
44
|
+
exception(): unknown;
|
|
45
|
+
addDoneCallback(callback: (task: Task<T>) => void): void;
|
|
46
|
+
removeDoneCallback(callback: (task: Task<T>) => void): number;
|
|
47
|
+
/**
|
|
48
|
+
* Request cancellation. Returns false if the task is already done.
|
|
49
|
+
*/
|
|
50
|
+
cancel(message?: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Throw CancelledError if a cancellation request is pending. Every
|
|
53
|
+
* primitive calls this before suspending.
|
|
54
|
+
*/
|
|
55
|
+
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
|
+
/** A promise that settles with the task, without being a cancellation point. */
|
|
61
|
+
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
|
+
toString(): string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Suspend the current task until `register` resolves or rejects, making the
|
|
67
|
+
* wait a cancellation point. `register` returns a disposer that undoes the
|
|
68
|
+
* registration (clear a timer, remove a listener...) when the wait is
|
|
69
|
+
* cancelled. `onCancel` runs after the disposer, e.g. to cancel a child task.
|
|
70
|
+
*/
|
|
71
|
+
export declare function suspend<T>(register: (resolve: (value: T) => void, reject: (error: unknown) => void) => () => void, onCancel?: () => void): Promise<T>;
|
|
72
|
+
/** Create and schedule a task (asyncio.create_task). */
|
|
73
|
+
export declare function createTask<T>(fn: TaskFn<T>, name?: string): Task<T>;
|
|
74
|
+
/** Throw if the current task has a pending cancellation (an explicit `await asyncio.sleep(0)`). */
|
|
75
|
+
export declare function checkCancelled(): void;
|
|
76
|
+
/**
|
|
77
|
+
* Await a foreign promise (fetch, SDK call...) as a cancellation point: if the
|
|
78
|
+
* current task is cancelled while waiting, this rejects with CancelledError.
|
|
79
|
+
* The foreign operation keeps running unless it was also given `task.signal`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function abortable<T>(promise: PromiseLike<T>): Promise<T>;
|
|
82
|
+
/** The current task's abort signal, or a never-aborting one outside tasks. */
|
|
83
|
+
export declare function currentSignal(): AbortSignal;
|
|
84
|
+
export {};
|
|
85
|
+
//# sourceMappingURL=task.d.ts.map
|
package/dist/task.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// Copyright (c) 2026, pipedog contributors
|
|
2
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
3
|
+
//
|
|
4
|
+
// asyncio-style tasks with cooperative cancellation.
|
|
5
|
+
//
|
|
6
|
+
// Python delivers `CancelledError` *into* a coroutine at the await it is
|
|
7
|
+
// suspended on. JavaScript cannot interrupt an arbitrary await, so we emulate
|
|
8
|
+
// it: every primitive in this package (sleep, Event.wait, Queue.get, awaiting a
|
|
9
|
+
// Task, waitFor, gather...) is a "cancellation point". When a task is
|
|
10
|
+
// cancelled:
|
|
11
|
+
//
|
|
12
|
+
// - if it is suspended on a cancellation point, that await rejects with
|
|
13
|
+
// CancelledError right away (the request is consumed, as in asyncio);
|
|
14
|
+
// - otherwise the request stays pending and the next cancellation point the
|
|
15
|
+
// task reaches throws it;
|
|
16
|
+
// - `task.signal` is aborted, so foreign awaits (fetch, WebSocket, SDKs) can
|
|
17
|
+
// be made cancellable by passing the signal or wrapping them in
|
|
18
|
+
// `abortable()`.
|
|
19
|
+
//
|
|
20
|
+
// The current task is tracked with AsyncLocalStorage, which Bun, Node, Deno
|
|
21
|
+
// and Cloudflare Workers (nodejs_compat) all provide.
|
|
22
|
+
/// <reference path="./async-hooks.d.ts" />
|
|
23
|
+
// (The reference keeps the declaration with this file: packages that compile
|
|
24
|
+
// it from source through the workspace exports don't include async-hooks.d.ts.)
|
|
25
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
26
|
+
export class CancelledError extends Error {
|
|
27
|
+
name = "CancelledError";
|
|
28
|
+
constructor(message = "Task cancelled") {
|
|
29
|
+
super(message);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class TimeoutError extends Error {
|
|
33
|
+
name = "TimeoutError";
|
|
34
|
+
constructor(message = "Timed out") {
|
|
35
|
+
super(message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export class InvalidStateError extends Error {
|
|
39
|
+
name = "InvalidStateError";
|
|
40
|
+
}
|
|
41
|
+
/** Whether `e` is a cancellation. Use it to re-throw in broad `catch` blocks. */
|
|
42
|
+
export function isCancelledError(e) {
|
|
43
|
+
return e instanceof CancelledError;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Re-throw `e` if it is a cancellation. Python's `except Exception` does not
|
|
47
|
+
* catch `CancelledError` (it is a BaseException); JavaScript's `catch` catches
|
|
48
|
+
* everything, so ported `except Exception` blocks start with this call.
|
|
49
|
+
*/
|
|
50
|
+
export function rethrowIfCancelled(e) {
|
|
51
|
+
if (e instanceof CancelledError)
|
|
52
|
+
throw e;
|
|
53
|
+
}
|
|
54
|
+
const storage = new AsyncLocalStorage();
|
|
55
|
+
/** The task the caller is running in, if any (asyncio.current_task). */
|
|
56
|
+
export function currentTask() {
|
|
57
|
+
return storage.getStore();
|
|
58
|
+
}
|
|
59
|
+
let taskCounter = 0;
|
|
60
|
+
/**
|
|
61
|
+
* A unit of concurrent work, like asyncio.Task. Created with `createTask()`.
|
|
62
|
+
* Awaiting a Task (it is thenable) is a cancellation point; cancelling the
|
|
63
|
+
* awaiting task also cancels the awaited one, as in asyncio.
|
|
64
|
+
*/
|
|
65
|
+
export class Task {
|
|
66
|
+
#controller = new AbortController();
|
|
67
|
+
#promise;
|
|
68
|
+
#waiters = new Set();
|
|
69
|
+
#doneCallbacks = [];
|
|
70
|
+
#name;
|
|
71
|
+
#state = "pending";
|
|
72
|
+
#result;
|
|
73
|
+
#error;
|
|
74
|
+
#mustCancel = false;
|
|
75
|
+
#cancelMessage;
|
|
76
|
+
#cancelRequests = 0;
|
|
77
|
+
constructor(fn, name) {
|
|
78
|
+
this.#name = name ?? `Task-${++taskCounter}`;
|
|
79
|
+
this.#promise = storage.run(this, async () => {
|
|
80
|
+
// Like asyncio, a task never starts running synchronously in the
|
|
81
|
+
// caller: it starts once the caller yields.
|
|
82
|
+
await Promise.resolve();
|
|
83
|
+
this.checkpoint();
|
|
84
|
+
return await fn();
|
|
85
|
+
});
|
|
86
|
+
this.#promise.then((value) => this.#settle("fulfilled", value, undefined), (error) => this.#settle(error instanceof CancelledError ? "cancelled" : "rejected", undefined, error));
|
|
87
|
+
}
|
|
88
|
+
get name() {
|
|
89
|
+
return this.#name;
|
|
90
|
+
}
|
|
91
|
+
/** asyncio Task.get_name() */
|
|
92
|
+
getName() {
|
|
93
|
+
return this.#name;
|
|
94
|
+
}
|
|
95
|
+
setName(name) {
|
|
96
|
+
this.#name = name;
|
|
97
|
+
}
|
|
98
|
+
/** Aborted when cancellation is requested. Pass it to fetch() and friends. */
|
|
99
|
+
get signal() {
|
|
100
|
+
return this.#controller.signal;
|
|
101
|
+
}
|
|
102
|
+
done() {
|
|
103
|
+
return this.#state !== "pending";
|
|
104
|
+
}
|
|
105
|
+
cancelled() {
|
|
106
|
+
return this.#state === "cancelled";
|
|
107
|
+
}
|
|
108
|
+
/** Number of pending cancellation requests (asyncio Task.cancelling()). */
|
|
109
|
+
cancelling() {
|
|
110
|
+
return this.#cancelRequests;
|
|
111
|
+
}
|
|
112
|
+
uncancel() {
|
|
113
|
+
if (this.#cancelRequests > 0)
|
|
114
|
+
this.#cancelRequests--;
|
|
115
|
+
if (this.#cancelRequests === 0)
|
|
116
|
+
this.#mustCancel = false;
|
|
117
|
+
return this.#cancelRequests;
|
|
118
|
+
}
|
|
119
|
+
result() {
|
|
120
|
+
if (this.#state === "pending")
|
|
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;
|
|
132
|
+
}
|
|
133
|
+
addDoneCallback(callback) {
|
|
134
|
+
if (this.done()) {
|
|
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;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Request cancellation. Returns false if the task is already done.
|
|
151
|
+
*/
|
|
152
|
+
cancel(message) {
|
|
153
|
+
if (this.done())
|
|
154
|
+
return false;
|
|
155
|
+
this.#cancelRequests++;
|
|
156
|
+
this.#cancelMessage = message;
|
|
157
|
+
if (!this.#controller.signal.aborted) {
|
|
158
|
+
this.#controller.abort(new CancelledError(message));
|
|
159
|
+
}
|
|
160
|
+
if (this.#waiters.size > 0) {
|
|
161
|
+
// Deliver to the await the task is suspended on; consumed.
|
|
162
|
+
const waiters = [...this.#waiters];
|
|
163
|
+
this.#waiters.clear();
|
|
164
|
+
for (const waiter of waiters)
|
|
165
|
+
waiter(new CancelledError(message));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
this.#mustCancel = true;
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Throw CancelledError if a cancellation request is pending. Every
|
|
174
|
+
* primitive calls this before suspending.
|
|
175
|
+
*/
|
|
176
|
+
checkpoint() {
|
|
177
|
+
if (this.#mustCancel) {
|
|
178
|
+
this.#mustCancel = false;
|
|
179
|
+
throw new CancelledError(this.#cancelMessage);
|
|
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);
|
|
189
|
+
}
|
|
190
|
+
/** A promise that settles with the task, without being a cancellation point. */
|
|
191
|
+
get promise() {
|
|
192
|
+
return this.#promise;
|
|
193
|
+
}
|
|
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
|
+
toString() {
|
|
206
|
+
return this.#name;
|
|
207
|
+
}
|
|
208
|
+
#settle(state, value, error) {
|
|
209
|
+
this.#state = state;
|
|
210
|
+
this.#result = value;
|
|
211
|
+
this.#error = error;
|
|
212
|
+
this.#waiters.clear();
|
|
213
|
+
const callbacks = this.#doneCallbacks.splice(0);
|
|
214
|
+
for (const callback of callbacks) {
|
|
215
|
+
try {
|
|
216
|
+
callback(this);
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
queueMicrotask(() => {
|
|
220
|
+
throw e;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Suspend the current task until `register` resolves or rejects, making the
|
|
228
|
+
* wait a cancellation point. `register` returns a disposer that undoes the
|
|
229
|
+
* registration (clear a timer, remove a listener...) when the wait is
|
|
230
|
+
* cancelled. `onCancel` runs after the disposer, e.g. to cancel a child task.
|
|
231
|
+
*/
|
|
232
|
+
export function suspend(register, onCancel) {
|
|
233
|
+
const task = currentTask();
|
|
234
|
+
try {
|
|
235
|
+
task?.checkpoint();
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
return Promise.reject(e);
|
|
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;
|
|
247
|
+
dispose();
|
|
248
|
+
onCancel?.();
|
|
249
|
+
reject(error);
|
|
250
|
+
};
|
|
251
|
+
task?._addWaiter(waiter);
|
|
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
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/** Create and schedule a task (asyncio.create_task). */
|
|
268
|
+
export function createTask(fn, name) {
|
|
269
|
+
const task = new Task(fn, name);
|
|
270
|
+
// Errors are observed through the task; never surface as unhandled rejections.
|
|
271
|
+
task.promise.catch(() => { });
|
|
272
|
+
return task;
|
|
273
|
+
}
|
|
274
|
+
/** Throw if the current task has a pending cancellation (an explicit `await asyncio.sleep(0)`). */
|
|
275
|
+
export function checkCancelled() {
|
|
276
|
+
currentTask()?.checkpoint();
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Await a foreign promise (fetch, SDK call...) as a cancellation point: if the
|
|
280
|
+
* current task is cancelled while waiting, this rejects with CancelledError.
|
|
281
|
+
* The foreign operation keeps running unless it was also given `task.signal`.
|
|
282
|
+
*/
|
|
283
|
+
export function abortable(promise) {
|
|
284
|
+
return suspend((resolve, reject) => {
|
|
285
|
+
promise.then(resolve, reject);
|
|
286
|
+
return () => { };
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/** The current task's abort signal, or a never-aborting one outside tasks. */
|
|
290
|
+
export function currentSignal() {
|
|
291
|
+
return currentTask()?.signal ?? new AbortController().signal;
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=task.js.map
|
package/package.json
CHANGED
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipedog/asyncio",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "asyncio-style tasks, cancellation, queues and events for JavaScript runtimes (Bun, Node, Workers).",
|
|
5
5
|
"license": "BSD-2-Clause",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/polvallverdu/pipedog.git",
|
|
9
9
|
"directory": "packages/asyncio"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://github.com/polvallverdu/pipedog/tree/main/packages/asyncio#readme"
|
|
11
|
+
"homepage": "https://github.com/polvallverdu/pipedog/tree/main/packages/asyncio#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/polvallverdu/pipedog/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"NOTICE"
|
|
27
|
+
],
|
|
28
|
+
"pipecat": {
|
|
29
|
+
"runtimes": [
|
|
30
|
+
"bun",
|
|
31
|
+
"node",
|
|
32
|
+
"deno",
|
|
33
|
+
"workerd"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"main": "./dist/index.js",
|
|
37
|
+
"types": "./dist/index.d.ts"
|
|
12
38
|
}
|