@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 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.
@@ -0,0 +1,3 @@
1
+ export * from "./primitives.ts";
2
+ export * from "./task.ts";
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // Copyright (c) 2026, pipedog contributors
2
+ // SPDX-License-Identifier: BSD-2-Clause
3
+ export * from "./primitives.js";
4
+ export * from "./task.js";
5
+ //# sourceMappingURL=index.js.map
@@ -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.0.0",
4
- "description": "Placeholder for @pipedog/asyncio. Releases are published from GitHub Actions; see https://github.com/polvallverdu/pipedog/tree/main/packages/asyncio#readme",
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
  }
package/README.md DELETED
@@ -1,5 +0,0 @@
1
- # @pipedog/asyncio
2
-
3
- Placeholder version. Install a released version instead: `npm install @pipedog/asyncio@latest`.
4
-
5
- Part of [pipedog](https://github.com/polvallverdu/pipedog).