@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/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/loop.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
/** @internal The task the code is running in (see task.ts). */
|
|
3
|
+
export declare const taskStorage: AsyncLocalStorage<unknown>;
|
|
4
|
+
/** A callback scheduled with `callSoon()` / `callLater()` (asyncio.Handle). */
|
|
5
|
+
export declare class Handle {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(callback: () => void);
|
|
8
|
+
/** Cancel the callback. Does nothing if it already ran. */
|
|
9
|
+
cancel(): void;
|
|
10
|
+
cancelled(): boolean;
|
|
11
|
+
/** @internal */
|
|
12
|
+
_setTimer(timer: ReturnType<typeof setTimeout>): void;
|
|
13
|
+
/** @internal */
|
|
14
|
+
_clearTimer(): void;
|
|
15
|
+
/** @internal */
|
|
16
|
+
_run(): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Schedule `callback` to run on a later turn of the loop, after the code
|
|
20
|
+
* running now (and every callback scheduled before it) has run up to its
|
|
21
|
+
* next suspension (loop.call_soon). Runs with no current task.
|
|
22
|
+
*/
|
|
23
|
+
export declare function callSoon(callback: () => void): Handle;
|
|
24
|
+
/** Schedule `callback` after `delay` seconds (loop.call_later). */
|
|
25
|
+
export declare function callLater(delay: number, callback: () => void): Handle;
|
|
26
|
+
//# sourceMappingURL=loop.d.ts.map
|
package/dist/loop.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Copyright (c) 2026, pipedog contributors
|
|
2
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
3
|
+
//
|
|
4
|
+
// The ready queue: asyncio's event loop scheduling, for JavaScript.
|
|
5
|
+
//
|
|
6
|
+
// In asyncio a task runs until it genuinely suspends (awaits a future that is
|
|
7
|
+
// not done). Completing a future does not resume its waiters: their wakeups
|
|
8
|
+
// are queued with `loop.call_soon()` and run later, one after another, in FIFO
|
|
9
|
+
// order. So `await queue.get()` on a non-empty queue, `await queue.put()` on
|
|
10
|
+
// an unbounded one or awaiting a coroutine that never suspends all complete
|
|
11
|
+
// without letting any other task run.
|
|
12
|
+
//
|
|
13
|
+
// JavaScript resumes an awaiting function one microtask after its promise
|
|
14
|
+
// settles, so naive promise-based primitives interleave tasks at every
|
|
15
|
+
// `await`. Here, every wakeup (a future's done callbacks, a task's first step,
|
|
16
|
+
// `sleep(0)`, timers) is a `callSoon()` callback instead, and callbacks run
|
|
17
|
+
// one per macrotask: the microtask queue (the resumed task's run up to its next
|
|
18
|
+
// real suspension, plus anything it set off) drains completely before the
|
|
19
|
+
// next callback runs. Pending callbacks keep the process alive, like a running
|
|
20
|
+
// loop.
|
|
21
|
+
//
|
|
22
|
+
// The macrotask source is picked per runtime: a MessageChannel on Node
|
|
23
|
+
// (setImmediate there goes through a full libuv loop iteration per callback),
|
|
24
|
+
// setImmediate on Bun and Deno, a body read on Cloudflare Workers (its
|
|
25
|
+
// setImmediate and timers wait about a millisecond; MessageChannel messages
|
|
26
|
+
// are delivered without draining microtasks in between) and MessageChannel or
|
|
27
|
+
// setTimeout elsewhere.
|
|
28
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
29
|
+
/** @internal The task the code is running in (see task.ts). */
|
|
30
|
+
export const taskStorage = new AsyncLocalStorage();
|
|
31
|
+
/** A callback scheduled with `callSoon()` / `callLater()` (asyncio.Handle). */
|
|
32
|
+
export class Handle {
|
|
33
|
+
#callback;
|
|
34
|
+
#timer;
|
|
35
|
+
constructor(callback) {
|
|
36
|
+
this.#callback = callback;
|
|
37
|
+
}
|
|
38
|
+
/** Cancel the callback. Does nothing if it already ran. */
|
|
39
|
+
cancel() {
|
|
40
|
+
this.#callback = null;
|
|
41
|
+
if (this.#timer !== undefined) {
|
|
42
|
+
clearTimeout(this.#timer);
|
|
43
|
+
this.#timer = undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
cancelled() {
|
|
47
|
+
return this.#callback === null;
|
|
48
|
+
}
|
|
49
|
+
/** @internal */
|
|
50
|
+
_setTimer(timer) {
|
|
51
|
+
this.#timer = timer;
|
|
52
|
+
}
|
|
53
|
+
/** @internal */
|
|
54
|
+
_clearTimer() {
|
|
55
|
+
this.#timer = undefined;
|
|
56
|
+
}
|
|
57
|
+
/** @internal */
|
|
58
|
+
_run() {
|
|
59
|
+
const callback = this.#callback;
|
|
60
|
+
if (callback === null)
|
|
61
|
+
return;
|
|
62
|
+
this.#callback = null;
|
|
63
|
+
callback();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// FIFO of ready handles: an array consumed from `head`, compacted lazily.
|
|
67
|
+
let ready = [];
|
|
68
|
+
let head = 0;
|
|
69
|
+
let trigger;
|
|
70
|
+
let armed = false;
|
|
71
|
+
/**
|
|
72
|
+
* Schedule `callback` to run on a later turn of the loop, after the code
|
|
73
|
+
* running now (and every callback scheduled before it) has run up to its
|
|
74
|
+
* next suspension (loop.call_soon). Runs with no current task.
|
|
75
|
+
*/
|
|
76
|
+
export function callSoon(callback) {
|
|
77
|
+
const handle = new Handle(callback);
|
|
78
|
+
enqueue(handle);
|
|
79
|
+
return handle;
|
|
80
|
+
}
|
|
81
|
+
/** Schedule `callback` after `delay` seconds (loop.call_later). */
|
|
82
|
+
export function callLater(delay, callback) {
|
|
83
|
+
const handle = new Handle(callback);
|
|
84
|
+
// As in asyncio, a due timer only moves its callback to the ready queue.
|
|
85
|
+
handle._setTimer(setTimeout(() => {
|
|
86
|
+
handle._clearTimer();
|
|
87
|
+
if (!handle.cancelled())
|
|
88
|
+
enqueue(handle);
|
|
89
|
+
}, Math.max(0, delay * 1000)));
|
|
90
|
+
return handle;
|
|
91
|
+
}
|
|
92
|
+
function enqueue(handle) {
|
|
93
|
+
ready.push(handle);
|
|
94
|
+
if (!armed) {
|
|
95
|
+
armed = true;
|
|
96
|
+
trigger ??= makeTrigger(runOne);
|
|
97
|
+
trigger.arm();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** One loop turn: run the oldest ready callback, then let microtasks drain. */
|
|
101
|
+
function runOne() {
|
|
102
|
+
armed = false;
|
|
103
|
+
const handle = ready[head];
|
|
104
|
+
if (handle === undefined) {
|
|
105
|
+
trigger?.idle();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
ready[head] = undefined;
|
|
109
|
+
head++;
|
|
110
|
+
if (head === ready.length) {
|
|
111
|
+
ready = [];
|
|
112
|
+
head = 0;
|
|
113
|
+
}
|
|
114
|
+
else if (head >= 1024 && head * 2 >= ready.length) {
|
|
115
|
+
ready = ready.slice(head);
|
|
116
|
+
head = 0;
|
|
117
|
+
}
|
|
118
|
+
if (head < ready.length) {
|
|
119
|
+
armed = true;
|
|
120
|
+
trigger.arm();
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
trigger.idle();
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
// Callbacks run outside any task, as in asyncio (current_task() is None).
|
|
127
|
+
taskStorage.run(undefined, () => handle._run());
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
// asyncio logs callback errors and keeps the loop going; surface it the
|
|
131
|
+
// way JavaScript reports an uncaught error, without stopping the queue.
|
|
132
|
+
queueMicrotask(() => {
|
|
133
|
+
throw e;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function makeTrigger(run) {
|
|
138
|
+
const g = globalThis;
|
|
139
|
+
const noop = () => { };
|
|
140
|
+
if (g.navigator?.userAgent === "Cloudflare-Workers" && typeof g.Response === "function") {
|
|
141
|
+
// A body read settles from the runtime's event loop, after microtasks.
|
|
142
|
+
const Response = g.Response;
|
|
143
|
+
return { arm: () => void new Response("").arrayBuffer().then(run), idle: noop };
|
|
144
|
+
}
|
|
145
|
+
const isNode = typeof g.process?.versions?.node === "string" && !g.Bun && !g.Deno;
|
|
146
|
+
if (isNode && typeof g.MessageChannel === "function") {
|
|
147
|
+
const { port1, port2 } = new g.MessageChannel();
|
|
148
|
+
port1.onmessage = run;
|
|
149
|
+
// Only pending callbacks keep the process alive.
|
|
150
|
+
port1.unref?.();
|
|
151
|
+
let referenced = false;
|
|
152
|
+
return {
|
|
153
|
+
arm: () => {
|
|
154
|
+
if (!referenced) {
|
|
155
|
+
referenced = true;
|
|
156
|
+
port1.ref?.();
|
|
157
|
+
}
|
|
158
|
+
port2.postMessage(null);
|
|
159
|
+
},
|
|
160
|
+
idle: () => {
|
|
161
|
+
if (referenced) {
|
|
162
|
+
referenced = false;
|
|
163
|
+
port1.unref?.();
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (typeof g.setImmediate === "function") {
|
|
169
|
+
const setImmediate = g.setImmediate;
|
|
170
|
+
return { arm: () => void setImmediate(run), idle: noop };
|
|
171
|
+
}
|
|
172
|
+
if (typeof g.MessageChannel === "function") {
|
|
173
|
+
const { port1, port2 } = new g.MessageChannel();
|
|
174
|
+
port1.onmessage = run;
|
|
175
|
+
return { arm: () => port2.postMessage(null), idle: noop };
|
|
176
|
+
}
|
|
177
|
+
return { arm: () => void setTimeout(run, 0), idle: noop };
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=loop.js.map
|
package/dist/primitives.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Future, type TaskFn } from "./task.ts";
|
|
2
2
|
/** Monotonic time in seconds (loop.time()). */
|
|
3
3
|
export declare function monotonic(): number;
|
|
4
4
|
/** Monotonic time in nanoseconds (time.monotonic_ns()). */
|
|
@@ -12,7 +12,10 @@ export declare function monotonicNs(): number;
|
|
|
12
12
|
* time origin.
|
|
13
13
|
*/
|
|
14
14
|
export declare function time(): number;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* asyncio.sleep(seconds). `sleep(0)` yields once: the task goes to the back
|
|
17
|
+
* of the ready queue, so everything already scheduled runs first.
|
|
18
|
+
*/
|
|
16
19
|
export declare function sleep(seconds: number): Promise<void>;
|
|
17
20
|
/** asyncio.Event */
|
|
18
21
|
export declare class Event {
|
|
@@ -32,7 +35,7 @@ export declare class QueueShutDown extends Error {
|
|
|
32
35
|
name: string;
|
|
33
36
|
}
|
|
34
37
|
/**
|
|
35
|
-
* asyncio.Queue. Subclasses override `_init/_put/_get/_size
|
|
38
|
+
* asyncio.Queue. Subclasses override `_init/_put/_get/_size` to change
|
|
36
39
|
* ordering, as Python's PriorityQueue and LifoQueue do.
|
|
37
40
|
*/
|
|
38
41
|
export declare class Queue<T> {
|
|
@@ -40,10 +43,13 @@ export declare class Queue<T> {
|
|
|
40
43
|
readonly maxsize: number;
|
|
41
44
|
constructor(maxsize?: number);
|
|
42
45
|
protected _items: T[];
|
|
46
|
+
protected _head: number;
|
|
43
47
|
protected _init(): void;
|
|
44
48
|
protected _put(item: T): void;
|
|
45
49
|
protected _get(): T;
|
|
46
50
|
protected _size(): number;
|
|
51
|
+
/** The queued items in storage order (for subclasses that inspect them). */
|
|
52
|
+
protected _queued(): readonly T[];
|
|
47
53
|
qsize(): number;
|
|
48
54
|
empty(): boolean;
|
|
49
55
|
full(): boolean;
|
|
@@ -85,17 +91,25 @@ export declare class Lock {
|
|
|
85
91
|
export declare class Semaphore {
|
|
86
92
|
#private;
|
|
87
93
|
constructor(value?: number);
|
|
94
|
+
/** Due to state, or FIFO rules (must allow others to run first). */
|
|
88
95
|
locked(): boolean;
|
|
89
96
|
acquire(): Promise<true>;
|
|
90
97
|
release(): void;
|
|
91
98
|
run<R>(fn: () => Promise<R> | R): Promise<R>;
|
|
92
99
|
}
|
|
93
|
-
|
|
100
|
+
/** Something to await: a function (a "coroutine"), a Task or Future, or a foreign promise. */
|
|
101
|
+
export type Awaitable<T> = TaskFn<T> | Future<T> | PromiseLike<T>;
|
|
94
102
|
/**
|
|
95
|
-
* asyncio.wait_for(aw, timeout).
|
|
96
|
-
*
|
|
103
|
+
* asyncio.wait_for(aw, timeout). A function (a "coroutine") or a foreign
|
|
104
|
+
* promise is awaited in the current task, a Task is awaited (and cancelled on
|
|
105
|
+
* timeout). `timeout` null waits forever.
|
|
97
106
|
*/
|
|
98
107
|
export declare function waitFor<T>(aw: Awaitable<T>, timeout: number | null | undefined): Promise<T>;
|
|
108
|
+
/**
|
|
109
|
+
* asyncio.timeout(seconds) as a scope: runs `fn` and cancels it on timeout,
|
|
110
|
+
* raising TimeoutError.
|
|
111
|
+
*/
|
|
112
|
+
export declare function timeout<T>(seconds: number | null, fn: TaskFn<T>): Promise<T>;
|
|
99
113
|
/**
|
|
100
114
|
* asyncio.gather(*aws). Functions run as child tasks; if the caller is
|
|
101
115
|
* cancelled, all children are cancelled.
|
|
@@ -113,15 +127,12 @@ export type ReturnWhen = typeof FIRST_COMPLETED | typeof FIRST_EXCEPTION | typeo
|
|
|
113
127
|
* asyncio.wait(tasks, timeout, return_when). Does not cancel anything on
|
|
114
128
|
* timeout. Returns [done, pending].
|
|
115
129
|
*/
|
|
116
|
-
export declare function wait<
|
|
130
|
+
export declare function wait<F extends Future<any>>(tasks: Iterable<F>, options?: {
|
|
117
131
|
timeout?: number | null;
|
|
118
132
|
returnWhen?: ReturnWhen;
|
|
119
|
-
}): Promise<[Set<
|
|
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>;
|
|
133
|
+
}): Promise<[Set<F>, Set<F>]>;
|
|
125
134
|
/** asyncio.shield(aw): the caller can be cancelled without cancelling `aw`. */
|
|
126
135
|
export declare function shield<T>(aw: Awaitable<T>): Promise<T>;
|
|
136
|
+
/** Whether `value` is a Future or Task (asyncio.isfuture). */
|
|
137
|
+
export declare function isFuture(value: unknown): value is Future<unknown>;
|
|
127
138
|
//# sourceMappingURL=primitives.d.ts.map
|