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