@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.
@@ -1,9 +1,13 @@
1
1
  // Copyright (c) 2026, pipedog contributors
2
2
  // SPDX-License-Identifier: BSD-2-Clause
3
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";
4
+ // asyncio synchronization primitives and helpers, ported from CPython's
5
+ // asyncio (locks.py, queues.py, tasks.py, timeouts.py) on top of Future, so
6
+ // they wake waiters exactly as asyncio does: through the ready queue, never
7
+ // synchronously. Every wait here is a cancellation point (see task.ts).
8
+ // Durations are in seconds, as in Python.
9
+ import { callLater, callSoon } from "./loop.js";
10
+ import { abortable, awaitFuture, CancelledError, createTask, currentTask, Future, TimeoutError, } from "./task.js";
7
11
  /** Monotonic time in seconds (loop.time()). */
8
12
  export function monotonic() {
9
13
  return performance.now() / 1000;
@@ -24,47 +28,75 @@ export function time() {
24
28
  const origin = performance.timeOrigin;
25
29
  return origin ? (origin + performance.now()) / 1000 : Date.now() / 1000;
26
30
  }
27
- /** asyncio.sleep(seconds). `sleep(0)` yields to other tasks. */
31
+ function remove(items, item) {
32
+ const i = items.indexOf(item);
33
+ if (i < 0)
34
+ return false;
35
+ items.splice(i, 1);
36
+ return true;
37
+ }
38
+ /**
39
+ * asyncio.sleep(seconds). `sleep(0)` yields once: the task goes to the back
40
+ * of the ready queue, so everything already scheduled runs first.
41
+ */
28
42
  export function sleep(seconds) {
29
- return suspend((resolve) => {
30
- const timer = setTimeout(resolve, Math.max(0, seconds * 1000));
31
- return () => clearTimeout(timer);
43
+ if (!(seconds > 0))
44
+ return sleep0();
45
+ const future = new Future();
46
+ const handle = callLater(seconds, () => {
47
+ if (!future.done())
48
+ future.setResult(undefined);
49
+ });
50
+ const done = awaitFuture(future);
51
+ done.then(() => handle.cancel(), () => handle.cancel());
52
+ return done;
53
+ }
54
+ /** A bare `yield` in a task: reschedule it at the back of the ready queue. */
55
+ function sleep0() {
56
+ const task = currentTask();
57
+ return new Promise((resolve, reject) => {
58
+ callSoon(() => {
59
+ const cancelled = task?._takeCancelRequest();
60
+ if (cancelled)
61
+ reject(cancelled);
62
+ else
63
+ resolve();
64
+ });
32
65
  });
33
66
  }
34
67
  /** asyncio.Event */
35
68
  export class Event {
36
- #set = false;
37
- #waiters = new Set();
69
+ #value = false;
70
+ #waiters = [];
38
71
  isSet() {
39
- return this.#set;
72
+ return this.#value;
40
73
  }
41
74
  set() {
42
- if (this.#set)
75
+ if (this.#value)
43
76
  return;
44
- this.#set = true;
45
- const waiters = [...this.#waiters];
46
- this.#waiters.clear();
47
- for (const waiter of waiters)
48
- waiter();
77
+ this.#value = true;
78
+ for (const future of this.#waiters) {
79
+ if (!future.done())
80
+ future.setResult(true);
81
+ }
49
82
  }
50
83
  clear() {
51
- this.#set = false;
84
+ this.#value = false;
52
85
  }
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);
86
+ async wait() {
87
+ if (this.#value) {
88
+ currentTask()?.checkpoint();
89
+ return true;
90
+ }
91
+ const future = new Future();
92
+ this.#waiters.push(future);
93
+ try {
94
+ await future;
95
+ return true;
96
+ }
97
+ finally {
98
+ remove(this.#waiters, future);
62
99
  }
63
- return suspend((resolve) => {
64
- const waiter = () => resolve(true);
65
- this.#waiters.add(waiter);
66
- return () => this.#waiters.delete(waiter);
67
- });
68
100
  }
69
101
  }
70
102
  export class QueueEmpty extends Error {
@@ -77,7 +109,7 @@ export class QueueShutDown extends Error {
77
109
  name = "QueueShutDown";
78
110
  }
79
111
  /**
80
- * asyncio.Queue. Subclasses override `_init/_put/_get/_size/_items` to change
112
+ * asyncio.Queue. Subclasses override `_init/_put/_get/_size` to change
81
113
  * ordering, as Python's PriorityQueue and LifoQueue do.
82
114
  */
83
115
  export class Queue {
@@ -92,19 +124,47 @@ export class Queue {
92
124
  this._init();
93
125
  this.#finished.set();
94
126
  }
95
- // Storage hooks.
127
+ // Storage hooks. FIFO storage is `_items` from index `_head` on: a deque
128
+ // (collections.deque in Python), since Array.shift() is O(n) on V8.
96
129
  _items = [];
130
+ _head = 0;
97
131
  _init() {
98
132
  this._items = [];
133
+ this._head = 0;
99
134
  }
100
135
  _put(item) {
101
136
  this._items.push(item);
102
137
  }
103
138
  _get() {
104
- return this._items.shift();
139
+ const items = this._items;
140
+ const item = items[this._head];
141
+ items[this._head] = undefined;
142
+ this._head++;
143
+ if (this._head === items.length) {
144
+ items.length = 0;
145
+ this._head = 0;
146
+ }
147
+ else if (this._head >= 1024 && this._head * 2 >= items.length) {
148
+ items.splice(0, this._head);
149
+ this._head = 0;
150
+ }
151
+ return item;
105
152
  }
106
153
  _size() {
107
- return this._items.length;
154
+ return this._items.length - this._head;
155
+ }
156
+ /** The queued items in storage order (for subclasses that inspect them). */
157
+ _queued() {
158
+ return this._head === 0 ? this._items : this._items.slice(this._head);
159
+ }
160
+ #wakeupNext(waiters) {
161
+ while (waiters.length > 0) {
162
+ const waiter = waiters.shift();
163
+ if (!waiter.done()) {
164
+ waiter.setResult(undefined);
165
+ break;
166
+ }
167
+ }
108
168
  }
109
169
  qsize() {
110
170
  return this._size();
@@ -122,14 +182,19 @@ export class Queue {
122
182
  while (this.full()) {
123
183
  if (this.#shutdown)
124
184
  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
- });
185
+ const putter = new Future();
186
+ this.#putters.push(putter);
187
+ try {
188
+ await putter;
189
+ }
190
+ catch (e) {
191
+ putter.cancel();
192
+ remove(this.#putters, putter);
193
+ // Woken up by getNowait() but can't take the slot: pass it on.
194
+ if (!this.full() && !putter.cancelled())
195
+ this.#wakeupNext(this.#putters);
196
+ throw e;
197
+ }
133
198
  }
134
199
  this.putNowait(item);
135
200
  }
@@ -141,26 +206,29 @@ export class Queue {
141
206
  this._put(item);
142
207
  this.#unfinished++;
143
208
  this.#finished.clear();
144
- this.#wakeGetter();
209
+ this.#wakeupNext(this.#getters);
145
210
  }
146
211
  async get() {
147
- if (!this.empty() && this.#getters.length === 0) {
212
+ if (!this.empty())
148
213
  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 };
214
+ while (this.empty()) {
215
+ if (this.#shutdown && this.empty())
216
+ throw new QueueShutDown();
217
+ const getter = new Future();
155
218
  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
- });
219
+ try {
220
+ await getter;
221
+ }
222
+ catch (e) {
223
+ getter.cancel();
224
+ remove(this.#getters, getter);
225
+ // Woken up by putNowait() but can't take the item: pass it on.
226
+ if (!this.empty() && !getter.cancelled())
227
+ this.#wakeupNext(this.#getters);
228
+ throw e;
229
+ }
230
+ }
231
+ return this.getNowait();
164
232
  }
165
233
  getNowait() {
166
234
  if (this.empty()) {
@@ -169,7 +237,7 @@ export class Queue {
169
237
  throw new QueueEmpty();
170
238
  }
171
239
  const item = this._get();
172
- this.#wakePutter();
240
+ this.#wakeupNext(this.#putters);
173
241
  return item;
174
242
  }
175
243
  taskDone() {
@@ -194,22 +262,15 @@ export class Queue {
194
262
  if (this.#unfinished === 0)
195
263
  this.#finished.set();
196
264
  }
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();
265
+ // All getters need to re-check queue-empty to raise QueueShutDown.
266
+ for (const getter of this.#getters.splice(0)) {
267
+ if (!getter.done())
268
+ getter.setResult(undefined);
269
+ }
270
+ for (const putter of this.#putters.splice(0)) {
271
+ if (!putter.done())
272
+ putter.setResult(undefined);
208
273
  }
209
- }
210
- #wakePutter() {
211
- const putter = this.#putters.shift();
212
- putter?.();
213
274
  }
214
275
  }
215
276
  /** asyncio.LifoQueue */
@@ -289,23 +350,32 @@ export class PriorityQueue extends Queue {
289
350
  /** asyncio.Lock */
290
351
  export class Lock {
291
352
  #locked = false;
292
- #waiters = [];
353
+ #waiters = null;
293
354
  locked() {
294
355
  return this.#locked;
295
356
  }
296
357
  async acquire() {
297
- if (!this.#locked && this.#waiters.length === 0) {
358
+ if (!this.#locked && (this.#waiters === null || this.#waiters.every((w) => w.cancelled()))) {
298
359
  this.#locked = true;
299
360
  return true;
300
361
  }
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
- });
362
+ this.#waiters ??= [];
363
+ const future = new Future();
364
+ this.#waiters.push(future);
365
+ try {
366
+ try {
367
+ await future;
368
+ }
369
+ finally {
370
+ remove(this.#waiters, future);
371
+ }
372
+ }
373
+ catch (e) {
374
+ // If the lock is free, make sure the next waiter in line runs.
375
+ if (e instanceof CancelledError && !this.#locked)
376
+ this.#wakeUpFirst();
377
+ throw e;
378
+ }
309
379
  this.#locked = true;
310
380
  return true;
311
381
  }
@@ -313,13 +383,13 @@ export class Lock {
313
383
  if (!this.#locked)
314
384
  throw new Error("Lock is not acquired.");
315
385
  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
- }
386
+ this.#wakeUpFirst();
387
+ }
388
+ #wakeUpFirst() {
389
+ const first = this.#waiters?.[0];
390
+ // done() means that the waiter is already set to wake up.
391
+ if (first && !first.done())
392
+ first.setResult(true);
323
393
  }
324
394
  /** `async with lock:` → `await lock.run(async () => {...})` */
325
395
  async run(fn) {
@@ -335,34 +405,64 @@ export class Lock {
335
405
  /** asyncio.Semaphore */
336
406
  export class Semaphore {
337
407
  #value;
338
- #waiters = [];
408
+ #waiters = null;
339
409
  constructor(value = 1) {
410
+ if (value < 0)
411
+ throw new RangeError("Semaphore initial value must be >= 0");
340
412
  this.#value = value;
341
413
  }
414
+ /** Due to state, or FIFO rules (must allow others to run first). */
342
415
  locked() {
343
- return this.#value === 0;
416
+ return this.#value === 0 || (this.#waiters ?? []).some((w) => !w.cancelled());
344
417
  }
345
418
  async acquire() {
346
- if (this.#value > 0 && this.#waiters.length === 0) {
419
+ if (!this.locked()) {
420
+ // Maintain FIFO, wait for others to start even if value > 0.
347
421
  this.#value--;
348
422
  return true;
349
423
  }
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
- });
424
+ this.#waiters ??= [];
425
+ const future = new Future();
426
+ this.#waiters.push(future);
427
+ try {
428
+ try {
429
+ await future;
430
+ }
431
+ finally {
432
+ remove(this.#waiters, future);
433
+ }
434
+ }
435
+ catch (e) {
436
+ if (e instanceof CancelledError && future.done() && !future.cancelled()) {
437
+ // Woken up by #wakeUpNext() but not acquiring: undo its bookkeeping.
438
+ this.#value++;
439
+ }
440
+ throw e;
441
+ }
442
+ finally {
443
+ // New waiters may have arrived but had to wait due to FIFO.
444
+ while (this.#value > 0) {
445
+ if (!this.#wakeUpNext())
446
+ break;
447
+ }
448
+ }
358
449
  return true;
359
450
  }
360
451
  release() {
361
- const next = this.#waiters.shift();
362
- if (next)
363
- next();
364
- else
365
- this.#value++;
452
+ this.#value++;
453
+ this.#wakeUpNext();
454
+ }
455
+ #wakeUpNext() {
456
+ if (!this.#waiters)
457
+ return false;
458
+ for (const future of this.#waiters) {
459
+ if (!future.done()) {
460
+ this.#value--;
461
+ future.setResult(true);
462
+ return true;
463
+ }
464
+ }
465
+ return false;
366
466
  }
367
467
  async run(fn) {
368
468
  await this.acquire();
@@ -374,63 +474,197 @@ export class Semaphore {
374
474
  }
375
475
  }
376
476
  }
377
- function toTask(aw, name) {
378
- if (aw instanceof Task)
477
+ /** asyncio.ensure_future: functions and foreign promises run as tasks. */
478
+ function ensureFuture(aw, name) {
479
+ if (aw instanceof Future)
379
480
  return aw;
380
481
  if (typeof aw === "function")
381
482
  return createTask(aw, name);
382
483
  return createTask(() => aw, name);
383
484
  }
485
+ /** Await `aw` in the current task, as `await aw` would in Python. */
486
+ function awaitInline(aw) {
487
+ if (aw instanceof Future)
488
+ return awaitFuture(aw);
489
+ if (typeof aw === "function")
490
+ return aw();
491
+ // A foreign promise: make the wait cancellable.
492
+ return abortable(aw);
493
+ }
494
+ /** tasks._cancel_and_wait: cancel `future` and wait until it is done. */
495
+ async function cancelAndWait(future) {
496
+ const waiter = new Future();
497
+ const release = () => {
498
+ if (!waiter.done())
499
+ waiter.setResult(undefined);
500
+ };
501
+ future.addDoneCallback(release);
502
+ try {
503
+ future.cancel();
504
+ // Not waiting on `future` directly keeps this reliably cancellable.
505
+ await waiter;
506
+ }
507
+ finally {
508
+ future.removeDoneCallback(release);
509
+ }
510
+ }
384
511
  /**
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.
512
+ * asyncio.timeout(seconds) around `fn`, run in the current task: on expiry
513
+ * the task is cancelled and the CancelledError becomes TimeoutError
514
+ * (timeouts.Timeout). `seconds` null never expires.
387
515
  */
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));
516
+ async function withTimeout(seconds, fn) {
517
+ const task = currentTask();
518
+ if (!task) {
519
+ // asyncio.timeout() needs a task; give it one.
520
+ return await createTask(() => withTimeout(seconds, fn), "timeout");
521
+ }
522
+ let expiring = false;
523
+ const cancelling = task.cancelling();
524
+ let handle = null;
525
+ if (seconds !== null && seconds !== undefined) {
526
+ const onTimeout = () => {
527
+ task.cancel();
528
+ expiring = true;
529
+ handle = null;
530
+ };
531
+ handle = seconds <= 0 ? callSoon(onTimeout) : callLater(seconds, onTimeout);
532
+ }
533
+ let result;
397
534
  try {
398
- return await child;
535
+ result = await fn();
399
536
  }
400
537
  catch (e) {
401
- if (timedOut && e instanceof CancelledError && !(currentTask()?.cancelling() ?? 0)) {
538
+ handle?.cancel();
539
+ if (expiring && task.uncancel() <= cancelling && e instanceof CancelledError) {
402
540
  throw new TimeoutError();
403
541
  }
404
542
  throw e;
405
543
  }
406
- finally {
407
- clearTimeout(timer);
544
+ handle?.cancel();
545
+ if (expiring)
546
+ task.uncancel();
547
+ return result;
548
+ }
549
+ /**
550
+ * asyncio.wait_for(aw, timeout). A function (a "coroutine") or a foreign
551
+ * promise is awaited in the current task, a Task is awaited (and cancelled on
552
+ * timeout). `timeout` null waits forever.
553
+ */
554
+ export async function waitFor(aw, timeout) {
555
+ if (timeout !== null && timeout !== undefined && timeout <= 0) {
556
+ const future = ensureFuture(aw, "wait_for");
557
+ if (future.done())
558
+ return future.result();
559
+ await cancelAndWait(future);
560
+ try {
561
+ return future.result();
562
+ }
563
+ catch (e) {
564
+ if (e instanceof CancelledError)
565
+ throw new TimeoutError();
566
+ throw e;
567
+ }
408
568
  }
569
+ return await withTimeout(timeout, () => awaitInline(aw));
570
+ }
571
+ /**
572
+ * asyncio.timeout(seconds) as a scope: runs `fn` and cancels it on timeout,
573
+ * raising TimeoutError.
574
+ */
575
+ export function timeout(seconds, fn) {
576
+ return withTimeout(seconds, fn);
577
+ }
578
+ /** tasks._GatheringFuture: cancelling it cancels the children. */
579
+ class GatheringFuture extends Future {
580
+ #children;
581
+ cancelRequested = false;
582
+ constructor(children) {
583
+ super();
584
+ this.#children = children;
585
+ }
586
+ cancel(message) {
587
+ if (this.done())
588
+ return false;
589
+ let cancelled = false;
590
+ for (const child of this.#children) {
591
+ if (child.cancel(message))
592
+ cancelled = true;
593
+ }
594
+ // Propagate the cancellation once the children are done.
595
+ if (cancelled)
596
+ this.cancelRequested = true;
597
+ return cancelled;
598
+ }
599
+ }
600
+ function gatherFuture(aws, returnExceptions) {
601
+ if (aws.length === 0) {
602
+ const outer = new Future();
603
+ outer.setResult([]);
604
+ return outer;
605
+ }
606
+ const children = [];
607
+ const doneFutures = [];
608
+ const seen = new Map();
609
+ let nfutures = 0;
610
+ let nfinished = 0;
611
+ let outer = null;
612
+ const onDone = (future) => {
613
+ nfinished++;
614
+ if (outer === null || outer.done())
615
+ return;
616
+ if (!returnExceptions) {
617
+ if (future.cancelled()) {
618
+ outer.setException(future._makeCancelledError());
619
+ return;
620
+ }
621
+ const error = future.exception();
622
+ if (error !== undefined) {
623
+ outer.setException(error);
624
+ return;
625
+ }
626
+ }
627
+ if (nfinished === nfutures) {
628
+ const results = children.map((child) => {
629
+ if (child.cancelled())
630
+ return child._makeCancelledError();
631
+ const error = child.exception();
632
+ return error !== undefined ? error : child.result();
633
+ });
634
+ if (outer.cancelRequested)
635
+ outer.setException(future._makeCancelledError());
636
+ else
637
+ outer.setResult(results);
638
+ }
639
+ };
640
+ for (const aw of aws) {
641
+ let future = seen.get(aw);
642
+ if (!future) {
643
+ future = ensureFuture(aw);
644
+ nfutures++;
645
+ seen.set(aw, future);
646
+ if (future.done())
647
+ doneFutures.push(future);
648
+ else
649
+ future.addDoneCallback(onDone);
650
+ }
651
+ children.push(future);
652
+ }
653
+ outer = new GatheringFuture(children);
654
+ for (const future of doneFutures)
655
+ onDone(future);
656
+ return outer;
409
657
  }
410
658
  /**
411
659
  * asyncio.gather(*aws). Functions run as child tasks; if the caller is
412
660
  * cancelled, all children are cancelled.
413
661
  */
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
- }));
662
+ export function gather(...aws) {
663
+ return awaitFuture(gatherFuture(aws, false));
423
664
  }
424
665
  /** 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
- });
666
+ export function gatherSettled(...aws) {
667
+ return awaitFuture(gatherFuture(aws, true));
434
668
  }
435
669
  export const FIRST_COMPLETED = "FIRST_COMPLETED";
436
670
  export const FIRST_EXCEPTION = "FIRST_EXCEPTION";
@@ -439,54 +673,74 @@ export const ALL_COMPLETED = "ALL_COMPLETED";
439
673
  * asyncio.wait(tasks, timeout, return_when). Does not cancel anything on
440
674
  * timeout. Returns [done, pending].
441
675
  */
442
- export function wait(tasks, options = {}) {
443
- const all = [...tasks];
676
+ export async function wait(tasks, options = {}) {
677
+ const all = [...new Set(tasks)];
444
678
  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);
679
+ if (all.length > 0) {
680
+ const waiter = new Future();
681
+ let timeoutHandle = null;
465
682
  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);
683
+ timeoutHandle = callLater(options.timeout, () => {
684
+ if (!waiter.done())
685
+ waiter.setResult(undefined);
686
+ });
687
+ }
688
+ let counter = all.length;
689
+ const onCompletion = (future) => {
690
+ counter--;
691
+ if (counter <= 0 ||
692
+ returnWhen === FIRST_COMPLETED ||
693
+ (returnWhen === FIRST_EXCEPTION && !future.cancelled() && future.exception() !== undefined)) {
694
+ timeoutHandle?.cancel();
695
+ if (!waiter.done())
696
+ waiter.setResult(undefined);
697
+ }
474
698
  };
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);
699
+ for (const future of all)
700
+ future.addDoneCallback(onCompletion);
701
+ try {
702
+ await waiter;
703
+ }
704
+ finally {
705
+ timeoutHandle?.cancel();
706
+ for (const future of all)
707
+ future.removeDoneCallback(onCompletion);
708
+ }
709
+ }
710
+ const done = new Set();
711
+ const pending = new Set();
712
+ for (const future of all)
713
+ (future.done() ? done : pending).add(future);
714
+ return [done, pending];
483
715
  }
484
716
  /** asyncio.shield(aw): the caller can be cancelled without cancelling `aw`. */
485
717
  export function shield(aw) {
486
- const task = toTask(aw);
487
- return suspend((resolve, reject) => {
488
- task.promise.then(resolve, reject);
489
- return () => { };
718
+ const inner = ensureFuture(aw);
719
+ if (inner.done())
720
+ return awaitFuture(inner);
721
+ const outer = new Future();
722
+ const onInnerDone = () => {
723
+ if (outer.cancelled())
724
+ return;
725
+ if (inner.cancelled()) {
726
+ outer.cancel();
727
+ return;
728
+ }
729
+ const error = inner.exception();
730
+ if (error !== undefined)
731
+ outer.setException(error);
732
+ else
733
+ outer.setResult(inner.result());
734
+ };
735
+ inner.addDoneCallback(onInnerDone);
736
+ outer.addDoneCallback(() => {
737
+ if (!inner.done())
738
+ inner.removeDoneCallback(onInnerDone);
490
739
  });
740
+ return awaitFuture(outer);
741
+ }
742
+ /** Whether `value` is a Future or Task (asyncio.isfuture). */
743
+ export function isFuture(value) {
744
+ return value instanceof Future;
491
745
  }
492
746
  //# sourceMappingURL=primitives.js.map