@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 CHANGED
@@ -1,3 +1,4 @@
1
+ export { callLater, callSoon, Handle } from "./loop.ts";
1
2
  export * from "./primitives.ts";
2
3
  export * from "./task.ts";
3
4
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) 2026, pipedog contributors
2
2
  // SPDX-License-Identifier: BSD-2-Clause
3
+ export { callLater, callSoon, Handle } from "./loop.js";
3
4
  export * from "./primitives.js";
4
5
  export * from "./task.js";
5
6
  //# sourceMappingURL=index.js.map
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
@@ -1,4 +1,4 @@
1
- import { Task, type TaskFn } from "./task.ts";
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
- /** asyncio.sleep(seconds). `sleep(0)` yields to other tasks. */
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/_items` to change
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
- export type Awaitable<T> = TaskFn<T> | Task<T> | PromiseLike<T>;
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). Pass a function (a "coroutine") so it can
96
- * run as a child task that is cancelled on timeout. `timeout` null waits forever.
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<T>(tasks: Iterable<Task<T>>, options?: {
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<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>;
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