@mcp-b/do-runtime 0.2.2 → 0.3.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.js CHANGED
@@ -1,1447 +1,9 @@
1
+ import { a as isExceptionFromInputGateBroken, c as tryCurrentSlice, i as hasUserErrorDetail, l as InputGate, n as atCheckpointEnd, o as requireInputLock, r as captureGateStack, s as setUserErrorDetail, t as IoContext, u as OutputGate } from "./chunks/io-context-Ci3Rf6U5.js";
1
2
  import { RpcTarget as RpcTarget$1 } from "./cloudflare-workers.js";
2
3
  import { a as getInt64, c as isNull, i as getBlob, o as getText, r as SqliteDatabase, s as hasCurrentSqliteTable } from "./chunks/sqlite-DFg92Tgt.js";
3
4
  import { ALARM_RETRY_MAX_TRIES, ALARM_RETRY_START_SECONDS, AlarmScheduler, RETRY_BACKOFF_MAX, RETRY_JITTER_FACTOR, alarmRetryDelayMs } from "./server/alarm-scheduler.js";
4
5
  import { deserialize, serialize } from "@ungap/structured-clone";
5
6
  import { RpcTarget, newMessagePortRpcSession } from "capnweb";
6
- //#region src/io/io-gate.ts
7
- /**
8
- * ← workerd `src/workerd/io/io-gate.{h,c++}`
9
- *
10
- * An I/O gate allows someone to "lock" a type of I/O so that other concurrent tasks trying to
11
- * perform that type of I/O are blocked until the lock is released.
12
- *
13
- * I/O gates are used in actors to implement consistency guarantees, allowing in-memory state and
14
- * storage to be synchronized.
15
- *
16
- * Each Actor has two main gates:
17
- * - Input gate: While locked, blocks all incoming I/O events of any type from being delivered to
18
- * the actor, other than the specific event or events that hold the lock. This includes
19
- * blocking responses to subrequests, timer events, input streams, etc. Used when storage
20
- * operations are outstanding, so that awaiting a storage operation does not risk allowing
21
- * concurrent events that render the state inconsistent.
22
- * - Output gate: While locked, blocks all outgoing messages from an actor that would allow the
23
- * rest of the world to observe the actor's state. Held while writes that have been confirmed
24
- * to the application are still being flushed to disk. If the flush fails, these messages will
25
- * never be sent, so that the rest of the world cannot observe a prematurely-confirmed write.
26
- *
27
- * Three things kj gives for free and JS does not, resolved the same way at every site:
28
- *
29
- * 1. **Destructors.** `~Lock` releases and `~CriticalSection` diagnoses a dropped section as
30
- * deadlock. Both become explicit: `Lock.release()` and `CriticalSection.drop()`. A `Lock`
31
- * released twice throws rather than corrupting the refcount.
32
- * 2. **Cancel-by-drop.** Dropping a `kj::Promise` unwinds its waiter. Every such site takes an
33
- * `AbortSignal`, the convention `Timer.afterDelay` already set in `io-context.ts`. Aborting a
34
- * `wait()` rejects it with `CanceledError`; a never-settling promise would be an invisible
35
- * hang, which is what this repo's fail-closed tenet exists to prevent.
36
- * 3. **`kj::ForkedPromise` holding an exception with no branches.** JS reports that as an
37
- * unhandled rejection, so every promise this module stores keeps a no-op `catch` of its own
38
- * and hands observers a separate view.
39
- *
40
- * Error strings are copied verbatim from upstream; users and upstream tests match on them.
41
- *
42
- * Not ported: `SpanParent`/`SpanBuilder` tracing, since there is no `trace.h` here and upstream's
43
- * own tests pass `nullptr` at every call site; and the `~InputGate` assertion that no locks
44
- * outlive the gate, which guards against dangling references GC makes impossible.
45
- *
46
- * Spec: §1.1, §1.2, §1.5, decisions 3, 5 and 13 in
47
- * docs/decisions.md.
48
- */
49
- /**
50
- * Raised when a `wait()` is cancelled through its `AbortSignal`.
51
- *
52
- * kj has no equivalent, because a cancelled continuation simply never runs. It is deliberately
53
- * NOT a gate failure: a cancelled waiter leaves the gate exactly as it found it, so
54
- * `CriticalSection.wait()` rethrows this one without calling `setBroken()`.
55
- */
56
- var CanceledError = class extends Error {
57
- name = "CanceledError";
58
- };
59
- /** Mirrors `OutputGate::makeUnfulfilledException()`: one place that spells the exception. */
60
- function makeCanceledError() {
61
- return new CanceledError("input gate wait was canceled");
62
- }
63
- /**
64
- * `addEventListener("abort", ...)` never fires for a signal that has already aborted, so a
65
- * pre-aborted signal would silently hold its lock forever. Every cancellation site goes through
66
- * here, and every one of them first rejects a pre-aborted wait before touching gate state, so
67
- * "cancelled" always means "left the gate exactly as it found it".
68
- */
69
- function onAbort(signal, run) {
70
- if (signal === void 0) return;
71
- if (signal.aborted) {
72
- run();
73
- return;
74
- }
75
- signal.addEventListener("abort", run, { once: true });
76
- }
77
- /** ← `InputGate::Hooks::DEFAULT`. */
78
- var DEFAULT_INPUT_GATE_HOOKS = {
79
- inputGateLocked() {},
80
- inputGateReleased() {},
81
- inputGateWaiterAdded() {},
82
- inputGateWaiterRemoved() {}
83
- };
84
- /** ← `InputGate::Waiter`: a `kj::List` node plus the adapted promise's fulfiller. */
85
- var Waiter = class {
86
- /** Rewritten by `CriticalSection.succeeded()` when a straggler is reparented. */
87
- gate;
88
- isChildWaiter;
89
- /** ← `link.isLinked()`. */
90
- linked = true;
91
- #resolve;
92
- #reject;
93
- constructor(gate, isChildWaiter, resolve, reject) {
94
- this.gate = gate;
95
- this.isChildWaiter = isChildWaiter;
96
- this.#resolve = resolve;
97
- this.#reject = reject;
98
- gate.hooks.inputGateWaiterAdded();
99
- if (isChildWaiter) gate.waitingChildren.push(this);
100
- else gate.waiters.push(this);
101
- }
102
- unlink() {
103
- if (!this.linked) return;
104
- this.linked = false;
105
- const list = this.isChildWaiter ? this.gate.waitingChildren : this.gate.waiters;
106
- const index = list.indexOf(this);
107
- if (index < 0) throw new Error("InputGate::Waiter is linked but absent from its gate's list");
108
- list.splice(index, 1);
109
- }
110
- fulfill(lock) {
111
- this.unlink();
112
- this.gate.hooks.inputGateWaiterRemoved();
113
- this.#resolve(lock);
114
- }
115
- reject(exception) {
116
- this.unlink();
117
- this.gate.hooks.inputGateWaiterRemoved();
118
- this.#reject(exception);
119
- }
120
- };
121
- /**
122
- * An InputGate blocks incoming events from being delivered to an actor while the lock is held.
123
- *
124
- * Upstream marks the state below `private` and befriends `Lock` and `CriticalSection`.
125
- * TypeScript has no friendship, and `protected` would not let `CriticalSection` reach these
126
- * members on its *parent* gate, which `succeeded()` does. The boundary that actually holds is
127
- * the package facade in `src/index.ts`, which exports none of these types.
128
- */
129
- var InputGate = class {
130
- hooks;
131
- /**
132
- * How many instances of `Lock` currently exist? When this reaches zero, we'll release some
133
- * waiters.
134
- *
135
- * Upstream also carries a `bool isCriticalSection`, because `CriticalSection` inherits
136
- * `InputGate` privately and has to `static_cast` back. `instanceof` is the same test with no
137
- * cast and no field that can disagree with the object it describes.
138
- */
139
- lockCount = 0;
140
- waiters = [];
141
- /**
142
- * Waiters representing CriticalSections that are ready to start. These take priority over other
143
- * waiters.
144
- */
145
- waitingChildren = [];
146
- /** A fulfiller for onBroken(), or an exception if already broken. */
147
- brokenState;
148
- #brokenPromise;
149
- constructor(hooks = DEFAULT_INPUT_GATE_HOOKS) {
150
- this.hooks = hooks;
151
- const { promise, reject } = Promise.withResolvers();
152
- this.#brokenPromise = promise;
153
- this.brokenState = {
154
- kind: "fulfiller",
155
- reject
156
- };
157
- promise.catch(() => {});
158
- }
159
- /** Wait until there are no `Lock`s, then create a new one and return it. */
160
- wait(signal) {
161
- if (signal?.aborted === true) return Promise.reject(makeCanceledError());
162
- else if (this.brokenState.kind === "exception") return Promise.reject(this.brokenState.exception);
163
- else if (this.lockCount === 0) return Promise.resolve(new Lock(this));
164
- else return this.newWaiterPromise(false, signal);
165
- }
166
- /**
167
- * Rejects if and when calls to `wait()` become broken due to a failed critical section. The
168
- * actor should be shut down in this case. This promise never resolves, only rejects.
169
- */
170
- onBroken() {
171
- if (this.brokenState.kind === "exception") return Promise.reject(this.brokenState.exception);
172
- else return this.#brokenPromise;
173
- }
174
- /** ← `kj::newAdaptedPromise<Lock, Waiter>(gate, isChildWaiter, span)`. */
175
- newWaiterPromise(isChildWaiter, signal) {
176
- const { promise, resolve, reject } = Promise.withResolvers();
177
- const waiter = new Waiter(this, isChildWaiter, resolve, reject);
178
- onAbort(signal, () => {
179
- if (!waiter.linked) return;
180
- waiter.reject(makeCanceledError());
181
- });
182
- return promise;
183
- }
184
- releaseLock() {
185
- if (this instanceof CriticalSection && this.state === "REPARENTED") {
186
- if (this.waitingChildren.length !== 0 || this.waiters.length !== 0 || this.lockCount !== 0) throw new Error("releasing a lock on a reparented CriticalSection that still holds state");
187
- this.parentAsInputGate().releaseLock();
188
- return;
189
- }
190
- if (this.lockCount === 0) throw new Error("InputGate::releaseLock() with no locks outstanding");
191
- this.lockCount--;
192
- if (this.lockCount === 0) {
193
- this.hooks.inputGateReleased();
194
- const child = this.waitingChildren[0];
195
- if (child !== void 0) child.fulfill(new Lock(this));
196
- else {
197
- const waiter = this.waiters[0];
198
- if (waiter !== void 0) waiter.fulfill(new Lock(this));
199
- }
200
- }
201
- }
202
- /** Called when a critical section fails. All future waiters will throw this exception. */
203
- setBroken(exception) {
204
- for (const waiter of [...this.waitingChildren]) waiter.reject(exception);
205
- for (const waiter of [...this.waiters]) waiter.reject(exception);
206
- if (this.brokenState.kind === "fulfiller") this.brokenState.reject(exception);
207
- this.brokenState = {
208
- kind: "exception",
209
- exception
210
- };
211
- }
212
- };
213
- /** ← `InputGate::Lock`. A lock that blocks all new events from being delivered while it exists. */
214
- var Lock = class Lock {
215
- /** ← "Becomes null on move." Here it becomes undefined on `release()`. */
216
- #gate;
217
- constructor(gate) {
218
- this.#gate = gate;
219
- let gateToLock = gate;
220
- if (gate instanceof CriticalSection && gate.state === "REPARENTED") gateToLock = gate.parentAsInputGate();
221
- if (++gateToLock.lockCount === 1) gateToLock.hooks.inputGateLocked();
222
- }
223
- /** ← `~Lock`. */
224
- release() {
225
- const gate = this.#gate;
226
- if (gate === void 0) throw new Error("InputGate::Lock was released twice");
227
- this.#gate = void 0;
228
- gate.releaseLock();
229
- }
230
- /**
231
- * Increments the lock's refcount, returning a duplicate `Lock`. All `Lock`s must be released
232
- * before the gate is unlocked.
233
- */
234
- addRef() {
235
- return new Lock(this.#requireGate());
236
- }
237
- /**
238
- * Start a new critical section from this lock. After `wait()` has been called on the returned
239
- * critical section for the first time, no further Locks will be handed out by
240
- * InputGate::wait() until the CriticalSection has been dropped.
241
- *
242
- * CriticalSections can be nested. If this Lock is itself part of a CriticalSection, the new
243
- * CriticalSection will be nested within it and the outer CriticalSection's wait() won't
244
- * produce a Lock again until the inner CriticalSection is dropped.
245
- */
246
- startCriticalSection() {
247
- return new CriticalSection(this.#requireGate());
248
- }
249
- /** If this lock was taken in a CriticalSection, return it. */
250
- getCriticalSection() {
251
- const gate = this.#requireGate();
252
- return gate instanceof CriticalSection ? gate : void 0;
253
- }
254
- isFor(otherGate) {
255
- if (otherGate instanceof CriticalSection) throw new Error("InputGate::Lock::isFor() takes the root gate, not a CriticalSection");
256
- let ptr = this.#requireGate();
257
- while (ptr instanceof CriticalSection) ptr = ptr.parentAsInputGate();
258
- return ptr === otherGate;
259
- }
260
- /** ← `operator==`. */
261
- equals(other) {
262
- return this.#requireGate() === other.#requireGate();
263
- }
264
- #requireGate() {
265
- const gate = this.#gate;
266
- if (gate === void 0) throw new Error("InputGate::Lock was used after release");
267
- return gate;
268
- }
269
- };
270
- /**
271
- * A CriticalSection is a procedure that must not be interrupted by anything "external".
272
- * While a CriticalSection is running, all events that were not initiated by the
273
- * CriticalSection itself will be blocked from being delivered.
274
- *
275
- * The difference between a Lock and a CriticalSection is that a critical section may succeed
276
- * or fail. A failed critical section permanently breaks the input gate. Locks, on the other
277
- * hand, are simply released when dropped.
278
- *
279
- * A CriticalSection itself holds a Lock, which blocks the "parent scope" from continuing
280
- * execution until the critical section is done. Meanwhile, the code running inside the critical
281
- * section obtains nested Locks. These nested locks control concurrency of the operations
282
- * initiated within the critical section in the same way that input locks normally do at the
283
- * top-level scope. E.g., if a critical section initiates a storage read and a fetch() at the
284
- * same time, the fetch() is prevented from returning until after the storage read has returned.
285
- */
286
- var CriticalSection = class CriticalSection extends InputGate {
287
- state = "NOT_STARTED";
288
- /**
289
- * Points to the parent scope, which may be another CriticalSection in the case of nesting.
290
- * ← `kj::OneOf<InputGate*, kj::Own<CriticalSection>>`; the two arms are one `instanceof` apart.
291
- */
292
- #parent;
293
- /**
294
- * A lock in the parent scope. `parentLock` becomes non-null after the first lock is obtained,
295
- * and becomes null again when succeeded() is called.
296
- */
297
- #parentLock;
298
- constructor(parent) {
299
- super();
300
- this.#parent = parent;
301
- }
302
- /**
303
- * Wait for a nested lock in order to continue this CriticalSection.
304
- *
305
- * The first call to wait() begins the CriticalSection. After that wait completes, until the
306
- * CriticalSection is done and dropped, no other locks will be allowed on this InputGate, except
307
- * locks requested by calling wait() on this CriticalSection -- or one of its children.
308
- *
309
- * Everything before the first `await` runs in the caller's synchronous slice, which is what
310
- * lets the NOT_STARTED path take its parent lock before any other event can queue behind it.
311
- */
312
- async wait(signal) {
313
- if (signal?.aborted === true) throw makeCanceledError();
314
- for (;;) switch (this.state) {
315
- case "NOT_STARTED": {
316
- this.state = "INITIAL_WAIT";
317
- const target = this.parentAsInputGate();
318
- if (target.brokenState.kind === "exception") {
319
- const exception = target.brokenState.exception;
320
- this.setBroken(exception);
321
- throw exception;
322
- }
323
- if (target.lockCount === 0) {
324
- this.state = "RUNNING";
325
- this.#parentLock = new Lock(target);
326
- continue;
327
- } else {
328
- let lock;
329
- try {
330
- lock = await target.newWaiterPromise(true, signal);
331
- } catch (exception) {
332
- if (exception instanceof CanceledError) throw exception;
333
- this.state = "RUNNING";
334
- this.setBroken(exception);
335
- throw exception;
336
- }
337
- this.state = "RUNNING";
338
- this.#parentLock = lock;
339
- continue;
340
- }
341
- }
342
- case "INITIAL_WAIT": throw new Error("CriticalSection::wait() should be called once initially");
343
- case "RUNNING": return await super.wait(signal);
344
- case "REPARENTED": return await this.#parent.wait(signal);
345
- }
346
- }
347
- /**
348
- * Call when the critical section has completed successfully. If this is not called before the
349
- * CriticalSection is dropped, then failed() is called implicitly.
350
- *
351
- * Returns the input lock that was held on the parent critical section. This can be used to
352
- * continue execution in the parent before any other input arrives.
353
- */
354
- succeeded() {
355
- if (this.state !== "RUNNING") throw new Error("CriticalSection::succeeded() requires a running critical section");
356
- const parentGate = this.parentAsInputGate();
357
- for (const waiter of this.waitingChildren) waiter.gate = parentGate;
358
- parentGate.waitingChildren.push(...this.waitingChildren);
359
- this.waitingChildren.length = 0;
360
- for (const waiter of this.waiters) waiter.gate = parentGate;
361
- parentGate.waiters.push(...this.waiters);
362
- this.waiters.length = 0;
363
- parentGate.lockCount += this.lockCount;
364
- this.lockCount = 0;
365
- this.state = "REPARENTED";
366
- const result = this.#parentLock;
367
- if (result === void 0) throw new Error("CriticalSection::succeeded() with no parent lock");
368
- this.#parentLock = void 0;
369
- return result;
370
- }
371
- /**
372
- * Call to indicate the CriticalSection has failed with the given exception. This immediately
373
- * breaks the InputGate.
374
- */
375
- failed(exception) {
376
- if (this.brokenState.kind === "exception") return;
377
- this.setBroken(exception);
378
- if (this.#parent instanceof CriticalSection) this.#parent.failed(exception);
379
- else this.#parent.setBroken(exception);
380
- }
381
- /** ← `~CriticalSection`. */
382
- drop() {
383
- switch (this.state) {
384
- case "NOT_STARTED": break;
385
- case "INITIAL_WAIT": break;
386
- case "RUNNING": this.failed(/* @__PURE__ */ new Error("jsg.Error: A critical section within this Durable Object awaited a Promise that apparently will never complete. This could happen in particular if a critical section awaits a task that was initiated outside of the critical section. Since a critical section blocks all other tasks from completing, this leads to deadlock."));
387
- }
388
- const parentLock = this.#parentLock;
389
- if (parentLock !== void 0) {
390
- this.#parentLock = void 0;
391
- parentLock.release();
392
- }
393
- }
394
- /** Return a reference for the parent scope, skipping any reparented CriticalSections */
395
- parentAsInputGate() {
396
- let parent = this.#parent;
397
- for (;;) {
398
- if (!(parent instanceof CriticalSection)) return parent;
399
- if (parent.state !== "REPARENTED") return parent;
400
- parent = parent.#parent;
401
- }
402
- }
403
- };
404
- /** ← `OutputGate::Hooks::DEFAULT`. */
405
- var DEFAULT_OUTPUT_GATE_HOOKS = {
406
- /** ← `kj::NEVER_DONE`. */
407
- makeTimeoutPromise: () => new Promise(() => {}),
408
- outputGateLocked() {},
409
- outputGateReleased() {},
410
- outputGateWaiterAdded() {},
411
- outputGateWaiterRemoved() {}
412
- };
413
- /** ← `OutputGate::makeUnfulfilledException()`. */
414
- function makeUnfulfilledException() {
415
- return /* @__PURE__ */ new Error("output lock was canceled before completion");
416
- }
417
- /**
418
- * An OutputGate blocks outgoing messages from an Actor until writes which they might depend on
419
- * are confirmed.
420
- *
421
- * A promise chain, not a counter (§1.1): each `lockWhile` joins a new link onto the chain and
422
- * re-forks it, so a `wait()` is bound to exactly the locks outstanding when it was taken and is
423
- * unaffected by any later `lockWhile`.
424
- */
425
- var OutputGate = class {
426
- #hooks;
427
- #pastLocksPromise = Promise.resolve();
428
- /** A fulfiller for onBroken(), or an exception if already broken. */
429
- #brokenState = { kind: "none" };
430
- constructor(hooks = DEFAULT_OUTPUT_GATE_HOOKS) {
431
- this.#hooks = hooks;
432
- }
433
- /**
434
- * Block all future `wait()` calls until `promise` completes. Returns a wrapper around
435
- * `promise`. If `promise` rejects, the exception will propagate to all future `wait()`s. If the
436
- * returned promise is canceled before completion, all future `wait()`s will also throw.
437
- */
438
- lockWhile(promise, signal) {
439
- const fulfiller = this.#lock();
440
- const raced = Promise.race([promise, this.#hooks.makeTimeoutPromise()]);
441
- this.#hooks.outputGateLocked();
442
- return new Promise((resolve, reject) => {
443
- onAbort(signal, () => {
444
- if (!fulfiller.isWaiting()) return;
445
- this.#hooks.outputGateReleased();
446
- const exception = makeUnfulfilledException();
447
- this.#setBroken(exception);
448
- fulfiller.reject(exception);
449
- reject(exception);
450
- });
451
- raced.then((value) => {
452
- if (!fulfiller.isWaiting()) return;
453
- fulfiller.fulfill();
454
- this.#hooks.outputGateReleased();
455
- resolve(value);
456
- }, (exception) => {
457
- if (!fulfiller.isWaiting()) return;
458
- this.#setBroken(exception);
459
- fulfiller.reject(exception);
460
- this.#hooks.outputGateReleased();
461
- reject(exception);
462
- });
463
- });
464
- }
465
- /**
466
- * Wait until all preceding locks are released. The wait will not be affected by any future
467
- * call to `lockWhile()`.
468
- */
469
- wait() {
470
- this.#hooks.outputGateWaiterAdded();
471
- return this.#pastLocksPromise.then(() => {
472
- this.#hooks.outputGateWaiterRemoved();
473
- }, (exception) => {
474
- this.#hooks.outputGateWaiterRemoved();
475
- throw exception;
476
- });
477
- }
478
- /**
479
- * Rejects if and when calls to `wait()` become broken due to a failed lockWhile(). The actor
480
- * should be shut down in this case. This promise never resolves, only rejects.
481
- *
482
- * This method can only be called once.
483
- */
484
- onBroken() {
485
- if (this.#brokenState.kind === "fulfiller") throw new Error("onBroken() can only be called once");
486
- if (this.#brokenState.kind === "exception") return Promise.reject(this.#brokenState.exception);
487
- else {
488
- const { promise, reject } = Promise.withResolvers();
489
- this.#brokenState = {
490
- kind: "fulfiller",
491
- reject
492
- };
493
- promise.catch(() => {});
494
- return promise;
495
- }
496
- }
497
- isBroken() {
498
- return this.#brokenState.kind === "exception";
499
- }
500
- #lock() {
501
- const { promise, resolve, reject } = Promise.withResolvers();
502
- this.#setPastLocks(Promise.allSettled([this.#pastLocksPromise, promise]).then((results) => {
503
- for (const result of results) if (result.status === "rejected") throw result.reason;
504
- }));
505
- let waiting = true;
506
- return {
507
- isWaiting: () => waiting,
508
- fulfill: () => {
509
- waiting = false;
510
- resolve();
511
- },
512
- reject: (exception) => {
513
- waiting = false;
514
- reject(exception);
515
- }
516
- };
517
- }
518
- #setPastLocks(promise) {
519
- this.#pastLocksPromise = promise;
520
- promise.catch(() => {});
521
- }
522
- #setBroken(exception) {
523
- if (this.#brokenState.kind === "fulfiller") this.#brokenState.reject(exception);
524
- this.#brokenState = {
525
- kind: "exception",
526
- exception
527
- };
528
- }
529
- };
530
- //#endregion
531
- //#region src/io/io-context.ts
532
- /**
533
- * ← workerd `src/workerd/io/io-context.{h,c++}`
534
- *
535
- * IoContext: the one door, plus the two await forms.
536
- *
537
- * This is the file with no true upstream correspondence for its *enforcement*.
538
- * Workerd acquires the input gate at isolate entry, so acquisition is
539
- * structural. We have no isolate hook, so a lock is taken at our own dispatch
540
- * boundary instead. See "The enforcement point is the one thing we cannot port"
541
- * in the design record.
542
- *
543
- * What this file does, stated before anything about how it got here:
544
- *
545
- * 1. `#currentInputLocks` is a STACK of the locks held by the slices that are
546
- * running. It is upstream's single `kj::Maybe<InputGate::Lock>
547
- * currentInputLock` member (`io-context.h:993`) with the `Maybe` widened,
548
- * and a "frame" is an entry in it. A frame carries nothing else: the only
549
- * two things ever read from one are the lock (`getInputLock`) and the
550
- * critical section that lock belongs to (`getCriticalSection`), and the
551
- * lock answers both.
552
- * 2. A lock is released, and leaves the stack, at the END OF THE MICROTASK
553
- * CHECKPOINT of the slice holding it — not when that slice's synchronous
554
- * body returns. That is upstream's own boundary, not a relaxation of it:
555
- * `runImpl`'s inner `KJ_DEFER` calls `js.runMicrotasks()`
556
- * (`io-context.c++:1262`) and `runInContextScope`'s outer one then clears
557
- * `currentInputLock` (`:1214`), inner scope first, so the whole checkpoint
558
- * drains under the lock. `currentInputLock` holds the `Lock` by value, so
559
- * clearing it is the release. `atCheckpointEnd` below is that point.
560
- * 3. NEITHER await form releases anything at the moment of the await.
561
- * `awaitIo` reads `getCriticalSection()` and `awaitIoWithInputLock` takes
562
- * an `addRef`; both then return a promise and let the slice end normally.
563
- * So an invocation that calls `awaitIo` keeps the gate until its own
564
- * checkpoint-end exit and the gate opens there — which is still before any
565
- * real I/O can complete, so "a bare timer releases the input gate" holds.
566
- * Upstream is the same: `getCriticalSection()` (`io-context.c++:362`) does
567
- * not touch `currentInputLock`, and `:1214` is the only place that clears
568
- * it. The difference between the two forms is entirely on the far side —
569
- * `awaitIo` re-enters through `run(func, criticalSection)` and queues for a
570
- * fresh lock, `awaitIoWithInputLock` re-enters holding the ref it took.
571
- * 4. Removal from the stack is by identity, not by popping, because entries do
572
- * overlap — three deep in the unit tests. One invocation can have several
573
- * held awaits outstanding (`Promise.all` over two storage reads), and a
574
- * section's last body slice overlaps the slice that resolves it.
575
- *
576
- * Two mechanics were validated against real workerd before this scaffolding
577
- * landed, and both are easy to get wrong:
578
- *
579
- * 1. The ambient "which lock am I under" must be a STACK of invocation frames,
580
- * not a single slot, so that `current` always names the running invocation.
581
- * The prototype that found this stated it as "`awaitIo` splices its frame
582
- * out and re-pushes the SAME frame on resume". That is not what happens
583
- * here and the difference is worth knowing: nothing splices at the point of
584
- * the await, because a lock already leaves the stack when its slice ends,
585
- * which is the same event that releases it (`#exit` does both). A
586
- * resumption then pushes a fresh lock. The prototype needed the splice
587
- * because its door held one lock for a whole invocation; the identity that
588
- * has to survive an await here is the critical section, and that is
589
- * captured at the call rather than looked up later.
590
- * 2. That ambient is safe here for a reason that does NOT generalise to the
591
- * §2.3 ambient-field hazard: the gate guarantees pushes and pops nest
592
- * properly in time, because only one holder chain is inside at once.
593
- * `_cf_currentSubAgentBridge` has no such guarantee, which is why it is a
594
- * live bug and this is not.
595
- *
596
- * Consequence: no async context is required. Do not add a dependency on
597
- * decision 8 here without re-running the conformance gate suite first.
598
- *
599
- * The invariant a future simplifier has to re-check: a single slot would pass
600
- * every test in `io-context.test.ts`, and that was established by trying it,
601
- * not by argument. `current()` is only read from inside a slice, every slice
602
- * pushes its own lock last, and no second slice can begin while an earlier
603
- * frame is still waiting for its checkpoint-end exit — that frame is holding
604
- * the gate. So the top of a stack and the last write to a slot always agree.
605
- * The stack is still what is here, because a slot would be holding a stale
606
- * value at every one of the overlaps in (4), and that is unobservable for
607
- * exactly as long as the invariant holds. Tightening (2) back to the end of the
608
- * synchronous body breaks it immediately, which is the regime the mechanic was
609
- * found in.
610
- *
611
- * Spec: §1.2, §1.3, §1.5, §1.6, §1.7.1, §1.9, decisions 1, 2, 4 and 13 in
612
- * docs/decisions.md.
613
- *
614
- * `TimeoutManager` is the only part of this file a consumer's own code reaches
615
- * directly: every host-provided async primitive a
616
- * Durable Object can await has to route through here, or the continuation after
617
- * it resumes with an empty invocation stack. `TimeoutManager` below is the
618
- * timer half; `api/global-scope.ts` and `api/web-socket.ts` are the rest.
619
- *
620
- * Not ported, because the substrate has no equivalent to port onto: isolate and
621
- * async locks (`Worker::Lock`, `jsg::Lock`, `takeAsyncLock`) and everything
622
- * that exists to enter or leave an isolate — which is precisely the thing this
623
- * file substitutes for; the limit enforcer and `afterLimitTimeout` (the
624
- * deadline takes the `Timer` port instead); trace spans, already a recorded
625
- * divergence documented in §1.12; subrequest channels and HTTP, which are
626
- * `api/http.ts`'s gating over the substrate's own `fetch`;
627
- * `IoOwn`/`IoPtr`/`DeleteQueue`, which guard cross-context dereferences that GC
628
- * makes impossible; hang detection and `registerPendingEvent`, which need the
629
- * isolate's own idea of pending work; and the thread-local
630
- * `IoContext::current()` static, whose lock-resolving half the invocation stack
631
- * replaces — its *identity* half is `currentSlice` below, narrowed to the
632
- * synchronous slice, with one consumer and no resolver. `EventOutcome` and
633
- * `RequestObserver` are metrics types with no port, so `waitUntilStatus()`
634
- * returns the first exception instead.
635
- */
636
- /** ← `static constexpr int64_t max = 3153600000000; // Milliseconds in 100 years`. */
637
- var MAX_TIMEOUT_MS = 31536e8;
638
- /** ← `afterLimitTimeout(30 * kj::SECONDS)` in `IoContext::blockConcurrencyWhile`. */
639
- var BLOCK_CONCURRENCY_WHILE_TIMEOUT_MS = 3e4;
640
- /** Copied verbatim: users and upstream tests match on it. */
641
- var BLOCK_CONCURRENCY_WHILE_TIMEOUT_MESSAGE = "A call to blockConcurrencyWhile() in a Durable Object waited for too long. The call was canceled and the Durable Object was reset.";
642
- /** ← `jsg::annotateBroken(msg, "broken.inputGateBroken")`. */
643
- var INPUT_GATE_BROKEN_PREFIX = "broken.inputGateBroken; ";
644
- /**
645
- * THE check every storage entry point in `api/` makes before touching the
646
- * database, and the only place this package throws for a missing input lock.
647
- *
648
- * Upstream never faces the question. `IoContext::current()` is a thread-local
649
- * read with a `KJ_REQUIRE` behind it, and it cannot fail for a storage call
650
- * because isolate entry is the only way into an actor and it always took a
651
- * lock. We have no isolate hook (see this file's header), so a continuation
652
- * that resumed from a raw `setTimeout`, a raw `fetch`, or any other promise the
653
- * runtime does not own comes back with an empty invocation stack, and its next
654
- * storage call reaches `ActorSqlite` — which is synchronous, touches no gate,
655
- * and would happily serve it outside any transaction boundary.
656
- *
657
- * So this throws, matching the assert. It is deliberately ONE function called
658
- * from every entry point rather than a check written at each of them: the
659
- * policy is a design decision that belongs to the design record, and when it
660
- * changes it has to change in one place rather than across a surface the
661
- * vendored consumer reaches from hundreds of call sites. There is no lenient
662
- * mode, no flag, and no implicit acquire — a lost invocation is loud, and a
663
- * lost transaction boundary is not.
664
- */
665
- function requireInputLock(ctx, op) {
666
- if (ctx.hasCurrent()) return;
667
- throw new Error(`${op}: no input lock available in this context${ctx.describeLostLock()}`);
668
- }
669
- /**
670
- * A stack for `noteGateUse`, captured where user frames are still on the stack.
671
- *
672
- * The invocation stack's own push and pop are scheduler moments — `#runImpl`
673
- * runs from a gate resumption and `#exit` from a `MessageChannel` callback — so
674
- * a trace taken there names only runtime internals. The moments that still see
675
- * the caller are the synchronous entries into the gate machinery: an `awaitIo`
676
- * call, an `entry` dispatch, a callback's registration. V8's zero-cost async
677
- * traces extend those with the awaiting chain, which is usually the frame the
678
- * reader actually wants.
679
- *
680
- * The first slice drops the `Error` header (absent on SpiderMonkey) and the two
681
- * runtime frames: this helper and the gate entry point that called it.
682
- */
683
- function captureGateStack() {
684
- const stack = (/* @__PURE__ */ new Error()).stack;
685
- if (stack === void 0) return void 0;
686
- const frames = stack.split("\n");
687
- const trimmed = frames.slice(frames[0]?.startsWith("Error") ? 3 : 2).join("\n");
688
- return trimmed === "" ? void 0 : trimmed;
689
- }
690
- /**
691
- * The end of the microtask checkpoint — the moment `runImpl`'s `KJ_DEFER` fires.
692
- *
693
- * Upstream drains the whole checkpoint INSIDE the isolate run: the defer calls
694
- * `js.runMicrotasks()` and only the outer defer in `runInContextScope` then
695
- * clears `currentInputLock`. So a continuation that awaits nothing but
696
- * already-resolved promises stays under the same lock, and a continuation that
697
- * waits on real I/O does not. In JS the only observable end of a microtask drain
698
- * is the next macrotask, so that is where a lock leaves the invocation stack.
699
- *
700
- * Measured, because the obvious alternatives are wrong in ways nothing catches:
701
- * releasing synchronously when the invoked function returns, or on the next
702
- * microtask, both hand the lock back BEFORE the code awaiting the I/O resumes as
703
- * soon as one promise sits between the two — one `async` wrapper, a `.then`, a
704
- * `Promise.all` — and `actor-state.ts` is exactly such a wrapper. The gate would
705
- * then open in the middle of a storage await with no test failing, which is the
706
- * silent loss of atomicity §1.7.1 names.
707
- *
708
- * `MessageChannel` and not `setTimeout`, decided by benchmark rather than by
709
- * argument, because the two are three orders of magnitude apart on the shapes
710
- * that wait for a release. A chain of these schedules each hand-off from inside
711
- * the previous one's callback, so a `setTimeout` chain's nesting level climbs
712
- * past five and stays there, where browsers clamp it to 4ms. Median per hand-off
713
- * over 50 chained hops:
714
- *
715
- * | | setTimeout | MessageChannel |
716
- * | node | 1.273 ms | 0.018 ms |
717
- * | chromium page | 4.96 ms | 0.024 ms |
718
- * | chromium Worker | 5.542 ms | 0.022 ms |
719
- *
720
- * Only two shapes pay it: an `awaitIo` chain, which has to re-acquire the lock
721
- * its slice gave up, and a queue of events waiting on the holder. A chain of
722
- * held storage awaits does not — the lock passes from `addRef` to `addRef` and
723
- * the release is off the critical path. So 50 sequential facet RPCs cost 277ms
724
- * of pure clamp in a Worker under `setTimeout` and 1.1ms under this.
725
- *
726
- * One channel per BATCH, and a batch is an explicit array: `atCheckpointEnd`
727
- * pushes onto `pendingCheckpointEnds` and only the push that finds it empty opens
728
- * a channel. Ordering then comes from the array, which the language guarantees,
729
- * rather than from delivery order across separate channels, which no
730
- * specification does. That mattered because the storage engine below depends on
731
- * a commit scheduled inside a slice running before the release scheduled at the
732
- * end of that slice; separate channels do deliver in post order in Node,
733
- * measured across 200 of them including one scheduled from inside another's
734
- * callback, but a browser that chose otherwise would open the gate onto a
735
- * transaction a previous event left open, and nothing would say so.
736
- *
737
- * A callback scheduled DURING a drain lands in the next batch, which is the
738
- * semantics to want: one hand-off is one checkpoint end, and a slice that begins
739
- * inside this drain gets its own. The drain runs every callback even if one
740
- * throws, and rethrows the first exception afterwards, because abandoning the
741
- * rest of a batch is how a gate wedges with nothing to see.
742
- *
743
- * A long-lived shared port was rejected for the reason it always is: it has to be
744
- * closed on abort and `unref`'d so it cannot hold a test runner's event loop
745
- * open, and an `unref`'d port can drop a release at exit — a wedged gate. A
746
- * channel that lives for exactly one message cannot. Batching gets most of what
747
- * a shared port was worth anyway: a slice that schedules a commit and a release
748
- * now allocates one channel where it used to allocate two.
749
- *
750
- * **Exported because `kj::evalLater()` is this same point.** `ActorSqlite` opens
751
- * its implicit transaction on the first write and commits it "on the next turn of
752
- * the event loop" (`actor-sqlite.c++:352-357`); upstream's next turn is after the
753
- * isolate run, which is after `js.runMicrotasks()`, which is after
754
- * `currentInputLock` is cleared. Upstream's two boundaries are one boundary, and
755
- * they stay one here only if the commit rides the same primitive as the release.
756
- * Two consequences the storage engine depends on, both properties of this
757
- * function rather than of `ActorSqlite`:
758
- *
759
- * 1. **Everything that holds the input lock across an await is a microtask
760
- * chain.** `awaitIoWithInputLock` resumes through `#awaitIoImpl`'s `.then`
761
- * into `run(func, lock)`, which never waits on the gate. So a whole run of
762
- * held storage awaits finishes before the next hand-off and its writes are
763
- * one transaction (§1.7.1 row 1).
764
- * 2. **Everything that releases it needs at least one hand-off.** `awaitIo`
765
- * resumes through `gate.wait()`, which cannot resolve until `#exit` runs
766
- * here. So a timer or outbound await puts the commit between the two writes
767
- * (§1.7.1 row 2).
768
- *
769
- */
770
- function atCheckpointEnd(run) {
771
- pendingCheckpointEnds.push(run);
772
- if (pendingCheckpointEnds.length > 1) return;
773
- const channel = new MessageChannel();
774
- channel.port1.onmessage = () => {
775
- channel.port1.close();
776
- channel.port2.close();
777
- const batch = pendingCheckpointEnds.splice(0);
778
- let failure;
779
- for (const callback of batch) try {
780
- callback();
781
- } catch (exception) {
782
- failure ??= { exception };
783
- }
784
- if (failure !== void 0) throw failure.exception;
785
- };
786
- channel.port2.postMessage(0);
787
- }
788
- /** The current batch. Its order is the hand-off order every consumer below relies on. */
789
- var pendingCheckpointEnds = [];
790
- /**
791
- * ← `static thread_local IoContext* threadLocalRequest` (`io-context.c++:25`).
792
- *
793
- * **This is NOT an async context and must not become one.** It is set on entry
794
- * to a slice's SYNCHRONOUS body and restored the instant that body returns —
795
- * which for an `async` function is its first `await`. It propagates through
796
- * exactly nothing. The package's "no async context is required" property
797
- * (README, Part 4 mechanic 1) is undisturbed: nothing resolves a lock through
798
- * this, and deleting it would change no gate behaviour.
799
- *
800
- * Upstream's scope is wider and cannot be matched. `runInContextScope` saves the
801
- * previous context, installs itself, and restores at the end of the isolate run
802
- * — which drains the whole microtask checkpoint synchronously, so upstream's
803
- * `current()` covers a slice's continuations too. JS cannot drain a checkpoint
804
- * synchronously, so covering continuations here would mean holding the value
805
- * until `atCheckpointEnd`, and by then a SECOND actor's body can have run and
806
- * left. That is not a hazard upstream has: two actors' slices genuinely overlap
807
- * in this window (§1.10 gives every facet its own gates, so nothing serialises
808
- * a parent against its child), and the value would be wrong with nothing to say
809
- * so.
810
- *
811
- * So the port keeps only the half that is exact, and the one consumer is a
812
- * tripwire that refuses on mismatch and stays quiet on `undefined` — never a
813
- * resolver. See `requireOwnSlice` in `api/global-scope.ts`.
814
- */
815
- var currentSlice;
816
- /** ← `IoContext::tryCurrent()` (`io-context.c++:1416-1422`), over the narrowed scope above. */
817
- function tryCurrentSlice() {
818
- return currentSlice;
819
- }
820
- /**
821
- * ← the `SuppressIoContextScope` constructor's `threadLocalRequest = this` half
822
- * (`io-context.c++:1208`), as a function rather than an assignment in `#runImpl`.
823
- *
824
- * A function because `currentSlice = this` reads to a linter as a `this` alias — the
825
- * ES5 `var self = this` habit — when it is the opposite: publishing the running
826
- * context to a module scope, which is exactly what the C++ does to a thread local.
827
- */
828
- function enterSlice(context) {
829
- currentSlice = context;
830
- }
831
- /** ← `promiseForExceptionOrT()`: merge the rejection into the value so it survives the hop. */
832
- function promiseForExceptionOrT(promise) {
833
- return promise.then((value) => ({
834
- ok: true,
835
- value
836
- }), (exception) => ({
837
- ok: false,
838
- exception
839
- }));
840
- }
841
- /** ← `IdentityFunc<T>`. */
842
- function identity(value) {
843
- return value;
844
- }
845
- /**
846
- * ← the `if (!msg.startsWith("broken."))` guard in `blockConcurrencyWhile`'s error
847
- * handler. Upstream rewrites the exception's description in place, so this does
848
- * too. `jsg`'s exception tunnelling — the `jsg.Error:` / `remote.` prefixes
849
- * `annotateBroken()` also produces — has no port here, so only the brokenness tag
850
- * it exists to carry survives.
851
- */
852
- function annotateInputGateBroken(exception) {
853
- if (!(exception instanceof Error)) return;
854
- if (exception.message.startsWith("broken.") || exception.message.startsWith("remote.broken.")) return;
855
- exception.message = INPUT_GATE_BROKEN_PREFIX + exception.message;
856
- }
857
- /**
858
- * ← `jsg::isExceptionFromInputGateBroken` (`jsg/exception.c++:168-172`):
859
- * "annotateBroken() produces 'broken.inputGateBroken; {message}', optionally
860
- * prefixed with 'remote.' when crossing RPC boundaries. Strip the remote prefix
861
- * first, then check the tag."
862
- *
863
- * Its writer is `annotateInputGateBroken` directly above, which is why the two
864
- * live together rather than the reader moving to its consumer: `jsg/` has no
865
- * module here, and a prefix known in two places is a prefix that drifts.
866
- */
867
- function isExceptionFromInputGateBroken(exception) {
868
- if (!(exception instanceof Error)) return false;
869
- let description = exception.message;
870
- while (description.startsWith(REMOTE_EXCEPTION_PREFIX)) description = description.slice(REMOTE_EXCEPTION_PREFIX.length);
871
- return description.startsWith(INPUT_GATE_BROKEN_PREFIX);
872
- }
873
- /** ← `ERROR_REMOTE_PREFIX` (`jsg/exception.h`). */
874
- var REMOTE_EXCEPTION_PREFIX = "remote.";
875
- /**
876
- * ← `jsg::EXCEPTION_IS_USER_ERROR` (`jsg/exception.h:160`), a
877
- * `kj::Exception::DetailTypeId` attached to an arbitrary exception rather than a
878
- * type of exception.
879
- *
880
- * A symbol-keyed property is the closest JS has: it rides any thrown object,
881
- * survives a rethrow, and cannot collide with anything an application writes.
882
- * `Symbol.for` rather than `Symbol()` so the detail is still legible after the
883
- * exception crosses a realm — the mistake decision 18 records capnweb making.
884
- */
885
- var EXCEPTION_IS_USER_ERROR = Symbol.for("workerd.exceptionIsUserError");
886
- /** ← `error.setDetail(jsg::EXCEPTION_IS_USER_ERROR, kj::heapArray<byte>(0))`. */
887
- function setUserErrorDetail(exception) {
888
- if (typeof exception !== "object" || exception === null) return;
889
- Object.defineProperty(exception, EXCEPTION_IS_USER_ERROR, {
890
- value: true,
891
- enumerable: false,
892
- configurable: true
893
- });
894
- }
895
- /** ← `e.getDetail(jsg::EXCEPTION_IS_USER_ERROR) != kj::none`. */
896
- function hasUserErrorDetail(exception) {
897
- if (typeof exception !== "object" || exception === null) return false;
898
- return exception[EXCEPTION_IS_USER_ERROR] === true;
899
- }
900
- /**
901
- * ← `IoContext::TimeoutManagerImpl` (`io-context.c++:40-140`, `:742-880`).
902
- *
903
- * **The one mechanic worth reading twice: a timer is NOT `awaitIo`.** Upstream
904
- * says why in a comment on the very line (`io-context.c++:756-758`): "the manual
905
- * use of run() here (including carrying over the critical section) is kind of
906
- * ugly, but using awaitIo() doesn't work here because we need the ability to
907
- * cancel the timer, so we don't want to addTask() it, which awaitIo() does
908
- * implicitly." So the shape is `cs = ctx.getCriticalSection()` captured at the
909
- * call, then `ctx.run(callback, cs)` when it fires. The captured section is what
910
- * makes a timer armed inside `blockConcurrencyWhile` run INSIDE that section
911
- * rather than queueing on the root gate behind it.
912
- *
913
- * **Substrate divergence: one `Timer.afterDelay` per timeout, where upstream
914
- * keeps a sorted `timeoutTimes` map and a single `timerTask` for the nearest.**
915
- * That structure exists because `kj::TimerChannel::atTime` supports one pending
916
- * wait, so upstream has to multiplex; `Timer.afterDelay` takes as many
917
- * concurrent waits as are asked for. The observable properties it produced —
918
- * timers fire in deadline order, ties broken by arming order — are the ones both
919
- * lane timers already have, since both are `setTimeout` underneath. Nothing else
920
- * in `resetTimerTask` is observable, so nothing else is ported.
921
- *
922
- * Not ported: `TimeoutId::Generator` and its cross-`ServiceWorkerGlobalScope`
923
- * assertion (`io-context.c++:52-60`), which exists to catch an IoContext being
924
- * current for a different V8 context — a confusion with no shape here, since a
925
- * timeout id is minted by the manager that owns it; `registerPendingEvent`,
926
- * which needs the isolate's own idea of pending work; and `getNextTimeout`,
927
- * whose only caller is the limit enforcer.
928
- */
929
- var TimeoutManager = class {
930
- #timer;
931
- #timeouts = /* @__PURE__ */ new Map();
932
- #nextId = 1;
933
- constructor(timer) {
934
- this.#timer = timer;
935
- }
936
- /** ← `TimeoutManagerImpl::setTimeout` (`io-context.c++:51-67`). */
937
- setTimeout(ctx, params) {
938
- const id = this.#nextId++;
939
- const state = {
940
- params,
941
- isCanceled: false,
942
- armed: void 0
943
- };
944
- this.#timeouts.set(id, state);
945
- this.#arm(ctx, id, state);
946
- return id;
947
- }
948
- /** ← `TimeoutManagerImpl::clearTimeout` (`io-context.c++:874-883`). */
949
- clearTimeout(id) {
950
- const state = this.#timeouts.get(id);
951
- if (state === void 0) return;
952
- this.#cancel(id, state);
953
- }
954
- /** ← `TimeoutManagerImpl::getTimeoutCount` (`io-context.c++:71-73`). */
955
- getTimeoutCount() {
956
- return this.#timeouts.size;
957
- }
958
- /** ← `TimeoutManagerImpl::cancelAll` (`io-context.c++:83-87`). */
959
- cancelAll() {
960
- for (const [id, state] of [...this.#timeouts]) this.#cancel(id, state);
961
- }
962
- /** ← `TimeoutState::cancel` — clear the flag, drop the callback reference, disarm. */
963
- #cancel(id, state) {
964
- state.isCanceled = true;
965
- state.params.callback = void 0;
966
- state.armed?.abort();
967
- state.armed = void 0;
968
- this.#timeouts.delete(id);
969
- }
970
- /** ← `TimeoutManagerImpl::setTimeoutImpl` (`io-context.c++:742-853`). */
971
- #arm(ctx, id, state) {
972
- const criticalSection = ctx.getCriticalSection();
973
- const wake = new AbortController();
974
- state.armed = wake;
975
- const fired = this.#timer.afterDelay(state.params.msDelay, wake.signal).then(async () => {
976
- if (state.isCanceled) return;
977
- state.armed = void 0;
978
- await this.#fire(ctx, id, state, criticalSection);
979
- }, (exception) => {
980
- if (!state.isCanceled) throw exception;
981
- });
982
- ctx.addWaitUntil(fired);
983
- }
984
- /** ← the body of the `.then` at `io-context.c++:759-816`, which is one `context.run`. */
985
- async #fire(ctx, id, state, criticalSection) {
986
- await ctx.run(() => {
987
- if (state.isCanceled) return;
988
- const callback = state.params.callback;
989
- if (callback === void 0) return;
990
- if (!state.params.repeat) {
991
- state.params.callback = void 0;
992
- this.#timeouts.delete(id);
993
- }
994
- try {
995
- callback();
996
- } finally {
997
- if (state.params.repeat && !state.isCanceled) this.#arm(ctx, id, state);
998
- }
999
- }, criticalSection);
1000
- }
1001
- };
1002
- /**
1003
- * A set of background promises, with the one behaviour `kj::TaskSet` adds over an
1004
- * array: a failing task reports to `taskFailed` instead of becoming an unhandled
1005
- * rejection, and the set can be waited on until empty.
1006
- */
1007
- var TaskSet$1 = class {
1008
- #tasks = /* @__PURE__ */ new Set();
1009
- #taskFailed;
1010
- constructor(taskFailed) {
1011
- this.#taskFailed = taskFailed;
1012
- }
1013
- add(promise) {
1014
- const task = promise.then(() => {
1015
- this.#tasks.delete(task);
1016
- }, (exception) => {
1017
- this.#tasks.delete(task);
1018
- this.#taskFailed(exception);
1019
- });
1020
- this.#tasks.add(task);
1021
- }
1022
- /** ← `kj::TaskSet::onEmpty()`. Re-checks, since a task can add another. */
1023
- async onEmpty() {
1024
- while (this.#tasks.size > 0) await Promise.all([...this.#tasks]);
1025
- }
1026
- };
1027
- var IoContext = class {
1028
- #actor;
1029
- #timer;
1030
- /**
1031
- * ← `kj::Maybe<InputGate::Lock> currentInputLock`, made a stack.
1032
- *
1033
- * The top entry is the lock of the slice that is running right now. Entries are
1034
- * removed by identity rather than popped, so `#exit` never depends on the order
1035
- * the overlapping slices happen to leave in.
1036
- */
1037
- #currentInputLocks = [];
1038
- /**
1039
- * Where this context's gate was last deliberately engaged, for
1040
- * `describeLostLock`. One slot, overwritten on every engagement — the gate
1041
- * serialises slices, so the latest note is the best available ancestor of
1042
- * whatever continuation is running lockless now.
1043
- */
1044
- #lastGateUse;
1045
- #abortException;
1046
- #abortPromise;
1047
- #rejectAbort;
1048
- /**
1049
- * Two sets, as upstream: `abortWhen()` always uses `tasks` so that a monitor
1050
- * which never completes cannot hold up a drain, while `addTask()` in an actor
1051
- * is a waitUntil task.
1052
- */
1053
- #tasks;
1054
- #waitUntilTasks;
1055
- #addTaskCounter = 0;
1056
- #waitUntilStatus;
1057
- /** ← `kj::Own<TimeoutManager> timeoutManager` (`io-context.h:1043`). */
1058
- #timeouts;
1059
- constructor(actor, timer) {
1060
- this.#actor = actor;
1061
- this.#timer = timer;
1062
- this.#timeouts = new TimeoutManager(timer);
1063
- const { promise, reject } = Promise.withResolvers();
1064
- this.#abortPromise = promise;
1065
- this.#rejectAbort = reject;
1066
- promise.catch(() => {});
1067
- this.#tasks = new TaskSet$1((exception) => this.#taskFailed(exception));
1068
- this.#waitUntilTasks = new TaskSet$1((exception) => this.#taskFailed(exception));
1069
- this.abortWhen(actor.getInputGate().onBroken());
1070
- this.abortWhen(actor.getOutputGate().onBroken());
1071
- }
1072
- /**
1073
- * Get the current input lock. Throws an exception if no input lock is held (e.g. because
1074
- * this is not an actor request).
1075
- *
1076
- * ← `KJ_ASSERT_NONNULL(currentInputLock, ...).addRef()`. The `addRef` IS the §1.2
1077
- * distinction: it is the only way to hold the gate past the end of this slice.
1078
- */
1079
- getInputLock() {
1080
- return this.#requireCurrent().addRef();
1081
- }
1082
- /** Get the current CriticalSection, if there is one, or returns null if not. */
1083
- getCriticalSection() {
1084
- return this.#currentInputLocks.at(-1)?.getCriticalSection();
1085
- }
1086
- /** Is a gated slice running? The question `IoContext::hasCurrent()` answers upstream. */
1087
- hasCurrent() {
1088
- return this.#currentInputLocks.length > 0;
1089
- }
1090
- /**
1091
- * ← `IoContext::isCurrent()` (`io-context.c++:1428-1430`), over the narrowed
1092
- * scope `currentSlice` documents: true only while a synchronous body of THIS
1093
- * context is on the JS stack.
1094
- *
1095
- * Distinct from `hasCurrent()` above, which asks whether this context holds a
1096
- * lock at all — true throughout an outstanding held await, and true for a
1097
- * parent whose slice is awaiting a facet while the facet's body runs. This one
1098
- * is the question a shared global has to answer: is the code calling me this
1099
- * actor's?
1100
- */
1101
- isCurrentSlice() {
1102
- return currentSlice === this;
1103
- }
1104
- /**
1105
- * Record that user code just engaged this context's gate — an `awaitIo`, an
1106
- * `entry` dispatch, a re-entry callback firing. No upstream analogue, because
1107
- * upstream cannot lose the lock; here a continuation that awaits a promise
1108
- * the runtime does not own comes back lockless, the throw lands at the next
1109
- * storage call three layers later, and the gap between "where the code last
1110
- * verifiably ran gated" and the throw site is exactly where the foreign await
1111
- * hides. This is that first coordinate. Always on: the capture rides calls
1112
- * that already allocate promise machinery, and a stack costs microseconds
1113
- * against the diagnosis it replaces.
1114
- */
1115
- noteGateUse(what, stack) {
1116
- this.#lastGateUse = {
1117
- what,
1118
- stack,
1119
- at: this.now()
1120
- };
1121
- }
1122
- /**
1123
- * The suffix `requireInputLock` appends when the invocation stack is empty:
1124
- * where this context's gate was last engaged, and how long before the throw.
1125
- *
1126
- * "Last engaged" is the honest claim, not "this continuation's ancestor" —
1127
- * once the offending chain went lockless the gate reopened, so another slice
1128
- * may have run in between and be the note this reports. In practice the loss
1129
- * is discovered within the same event storm and the note is the parent; when
1130
- * it is not, an engagement of this actor moments earlier is still the right
1131
- * neighbourhood to search.
1132
- */
1133
- describeLostLock() {
1134
- const use = this.#lastGateUse;
1135
- if (use === void 0) return " (this context has never held its gate: the call arrived from outside any actor invocation)";
1136
- const age = Math.round(this.now() - use.at);
1137
- const stackSuffix = use.stack === void 0 ? "" : `, at:\n${use.stack}`;
1138
- return ` (an await after the last gated point resumed from a promise the runtime does not own; the gate was last engaged by ${use.what} ${age}ms before this call${stackSuffix})`;
1139
- }
1140
- /**
1141
- * ← `IoContext::getActorOrThrow()`. Upstream's throws when the request is not
1142
- * an actor request; there is no such request here, so it is a plain accessor.
1143
- */
1144
- getActorOrThrow() {
1145
- return this.#actor;
1146
- }
1147
- /** ← `IoContext::now()` (`io-context.h:703`), which reads the same timer. */
1148
- now() {
1149
- return this.#timer.now();
1150
- }
1151
- /**
1152
- * ← `IoContext::setTimeoutImpl` (`io-context.c++:885-899`), clamp included.
1153
- *
1154
- * The generator parameter is gone with `TimeoutId::Generator` — see
1155
- * `TimeoutManager`'s header — so the signature is upstream's minus that one
1156
- * argument.
1157
- */
1158
- setTimeoutImpl(repeat, callback, msDelay) {
1159
- const delay = msDelay <= 0 || Number.isNaN(msDelay) ? 0 : msDelay >= MAX_TIMEOUT_MS ? MAX_TIMEOUT_MS : Math.trunc(msDelay);
1160
- return this.#timeouts.setTimeout(this, {
1161
- repeat,
1162
- msDelay: delay,
1163
- callback
1164
- });
1165
- }
1166
- /** ← `IoContext::clearTimeoutImpl` (`io-context.c++:901-903`). */
1167
- clearTimeoutImpl(id) {
1168
- this.#timeouts.clearTimeout(id);
1169
- }
1170
- /** ← `IoContext::getTimeoutCount` (`io-context.c++:905-907`). */
1171
- getTimeoutCount() {
1172
- return this.#timeouts.getTimeoutCount();
1173
- }
1174
- /**
1175
- * Wait until all outstanding output locks have been unlocked. Does not wait for future
1176
- * output locks, even if they are created before past locks are unlocked.
1177
- */
1178
- waitForOutputLocks() {
1179
- return this.#actor.getOutputGate().wait();
1180
- }
1181
- /**
1182
- * Check if the output gate is currently broken. This indicates that there was a problem
1183
- * with committing storage writes.
1184
- */
1185
- isOutputGateBroken() {
1186
- return this.#actor.getOutputGate().isBroken();
1187
- }
1188
- /** Lock output until the given promise completes. */
1189
- lockOutputWhile(promise, signal) {
1190
- return this.#actor.getOutputGate().lockWhile(promise, signal);
1191
- }
1192
- /**
1193
- * Rejects if and when the context should be aborted, e.g. because a gate broke. This
1194
- * promise never resolves, only rejects.
1195
- */
1196
- onAbort() {
1197
- return this.#abortPromise;
1198
- }
1199
- /** Force context abort now. */
1200
- abort(exception) {
1201
- if (this.#abortException !== void 0) return;
1202
- this.#abortException = { exception };
1203
- this.#actor.shutdownActorCache(exception);
1204
- this.#timeouts.cancelAll();
1205
- this.#rejectAbort(exception);
1206
- }
1207
- /**
1208
- * Await the given promise and, if it throws, call `abort()` with the exception. The promise
1209
- * given here should just be a monitoring promise, it should not represent any sort of
1210
- * background work beyond monitoring.
1211
- */
1212
- abortWhen(promise) {
1213
- if (this.#abortException === void 0) this.#tasks.add(promise.then(() => {}, (exception) => {
1214
- this.abort(exception);
1215
- }));
1216
- }
1217
- /**
1218
- * Arrange for the given promise to execute as part of this request.
1219
- *
1220
- * "In Actors, we treat all tasks as wait-until tasks, because it's perfectly legit to start
1221
- * a task under one request and then expect some other request to handle it later." Every
1222
- * context here is an actor context, so that branch is the only branch.
1223
- */
1224
- addTask(promise) {
1225
- ++this.#addTaskCounter;
1226
- this.addWaitUntil(promise);
1227
- }
1228
- /**
1229
- * Indicates that the script has requested that it stay active until the given promise
1230
- * resolves. `drainWaitUntil()` waits until all such promises have completed. Touches
1231
- * neither gate (§1.9).
1232
- */
1233
- addWaitUntil(promise) {
1234
- this.#waitUntilTasks.add(promise);
1235
- }
1236
- /** Returns the number of times addTask() has been called (even if the tasks have completed). */
1237
- taskCount() {
1238
- return this.#addTaskCounter;
1239
- }
1240
- /**
1241
- * The first exception a background task failed with, if any.
1242
- *
1243
- * ← `waitUntilStatus()`, which returns an `EventOutcome` derived from the exception by
1244
- * `RequestObserver`. There is no observer here, so the exception itself is the status —
1245
- * and keeping it is what stops a failed background task from being swallowed, since
1246
- * upstream's other half of `taskFailed()` is a log this package has no port for.
1247
- */
1248
- waitUntilStatus() {
1249
- return this.#waitUntilStatus?.exception;
1250
- }
1251
- /**
1252
- * ← `IncomingRequest::drain()`, actor branch. "For actors, all promises are canceled on
1253
- * actor shutdown, not on a fixed timeout, because work doesn't necessarily happen on a
1254
- * per-request basis in actors."
1255
- */
1256
- async drainWaitUntil() {
1257
- await Promise.race([this.#waitUntilTasks.onEmpty(), this.#abortPromise.catch(() => {})]);
1258
- }
1259
- /**
1260
- * Run the given callback within this context, holding an input lock.
1261
- *
1262
- * ← the two `IoContext::run()` overloads: given a CriticalSection it waits on that, given
1263
- * an already-held Lock it runs under it, and given neither it takes a fresh lock from the
1264
- * gate. The third case is what a new external event does, and it is the reason inheritance
1265
- * cannot be read from gate state — see `makeReentryCallback`.
1266
- */
1267
- async run(func, ilOrCs) {
1268
- const aborted = this.#abortException;
1269
- if (aborted !== void 0) throw aborted.exception;
1270
- let lock;
1271
- if (ilOrCs === void 0) lock = await this.#actor.getInputGate().wait();
1272
- else if (ilOrCs instanceof CriticalSection) lock = await ilOrCs.wait();
1273
- else lock = ilOrCs;
1274
- return await this.#runImpl(func, lock);
1275
- }
1276
- /**
1277
- * Make a function which, when called, re-enters this IoContext to run some code.
1278
- *
1279
- * Upstream, on why the critical section travels with the callback at all:
1280
- *
1281
- * > "What if the call was made within blockConcurrencyWhile()? The callback will be blocked
1282
- * > until the critical section ends, which could lead to deadlock if the critical section
1283
- * > code is waiting on it? ... The callback is allowed to run within the critical section
1284
- * > (blockConcurrencyWhile()) from which it was called."
1285
- *
1286
- * The section is read here, at the point of capture, and never on invocation: a new
1287
- * external event that inherited the running section would skip the queue and
1288
- * `blockConcurrencyWhile` would silently block nothing (Part 4, mechanic 2).
1289
- *
1290
- * The returned function can be called multiple times.
1291
- *
1292
- * It does not route through `io-gate.ts`'s `makeReentryCallback`, which is the same idea
1293
- * expressed at the gate. Upstream's `IoContext::makeReentryCallback` is literally
1294
- * `ctx.run(func, cs)`, and going through the gate helper instead would take a lock this
1295
- * file then has to make current a second time. The gate copy stays: it is the shape a
1296
- * consumer holding only a gate needs, and Section 1's tests cover it.
1297
- */
1298
- makeReentryCallback(func) {
1299
- this.#requireCurrent();
1300
- const criticalSection = this.getCriticalSection();
1301
- const registrationStack = captureGateStack();
1302
- return async (...args) => {
1303
- this.noteGateUse("a re-entry callback registered at the site below", registrationStack);
1304
- const call = this.run((lock) => func(lock, ...args), criticalSection);
1305
- this.addTask(call.then(() => {}, () => {}));
1306
- return await call;
1307
- };
1308
- }
1309
- awaitIo(promise, func = identity) {
1310
- this.noteGateUse("awaitIo", captureGateStack());
1311
- return this.#awaitIoImpl(promise, this.getCriticalSection(), func);
1312
- }
1313
- awaitIoWithInputLock(promise, func = identity) {
1314
- let inputLock;
1315
- try {
1316
- inputLock = this.getInputLock();
1317
- } catch (exception) {
1318
- return Promise.reject(exception);
1319
- }
1320
- this.noteGateUse("awaitIoWithInputLock", captureGateStack());
1321
- return this.#awaitIoImpl(promise, inputLock, func);
1322
- }
1323
- /**
1324
- * Runs `callback` within its own critical section, returning its final result. If
1325
- * `callback` throws, the input lock will break, resetting the actor.
1326
- *
1327
- * Three behaviours live here rather than in `io-gate.ts`, which has no timer, and rather
1328
- * than in `api/actor-state.ts`, whose own `blockConcurrencyWhile` is a one-line forward:
1329
- * the 30-second deadline, the brokenness annotation, and the fact that on failure the
1330
- * returned promise is never settled at all.
1331
- */
1332
- blockConcurrencyWhile(callback) {
1333
- const lock = this.getInputLock();
1334
- this.noteGateUse("blockConcurrencyWhile", captureGateStack());
1335
- const criticalSection = lock.startCriticalSection();
1336
- const { promise: result, resolve } = Promise.withResolvers();
1337
- this.addTask((async () => {
1338
- try {
1339
- const value = await this.#runCriticalSection(criticalSection, callback);
1340
- await this.#runImpl(() => {
1341
- resolve(value);
1342
- }, criticalSection.succeeded());
1343
- } catch (exception) {
1344
- annotateInputGateBroken(exception);
1345
- criticalSection.failed(exception);
1346
- throw exception;
1347
- } finally {
1348
- criticalSection.drop();
1349
- }
1350
- })());
1351
- lock.release();
1352
- return result;
1353
- }
1354
- /**
1355
- * ← `runImpl()` + `runInContextScope()`: check the lock belongs to this actor, make it the
1356
- * current one, run, and let `KJ_DEFER` clear it.
1357
- *
1358
- * The defer fires after the microtask checkpoint, not when `func` returns — see
1359
- * `atCheckpointEnd`. Everything else about the scope is isolate machinery with no port.
1360
- */
1361
- async #runImpl(func, lock) {
1362
- if (!lock.isFor(this.#actor.getInputGate())) throw new Error("IoContext::runImpl() was given a lock belonging to another actor");
1363
- this.#currentInputLocks.push(lock);
1364
- let result;
1365
- const previousSlice = currentSlice;
1366
- enterSlice(this);
1367
- try {
1368
- result = func(lock);
1369
- } finally {
1370
- enterSlice(previousSlice);
1371
- atCheckpointEnd(() => {
1372
- this.#exit(lock);
1373
- });
1374
- }
1375
- return await result;
1376
- }
1377
- /**
1378
- * ← `requireCurrent()`. Upstream asks whether this IoContext is the thread's current one;
1379
- * with no thread-local there is only one question left, and it is the one every caller of
1380
- * `requireCurrent()` actually depends on: is a gated slice running?
1381
- */
1382
- #requireCurrent() {
1383
- const lock = this.#currentInputLocks.at(-1);
1384
- if (lock === void 0) throw new Error(`no input lock available in this context${this.describeLostLock()}`);
1385
- return lock;
1386
- }
1387
- /** ← the far side of `runInContextScope`'s `KJ_DEFER`. */
1388
- #exit(lock) {
1389
- const at = this.#currentInputLocks.lastIndexOf(lock);
1390
- if (at < 0) throw new Error("IoContext invocation stack lost a lock it was holding");
1391
- this.#currentInputLocks.splice(at, 1);
1392
- lock.release();
1393
- }
1394
- /**
1395
- * ← `awaitIoImpl()`.
1396
- *
1397
- * The KJ-side rejection is merged into the value so a single continuation handles both, the
1398
- * continuation re-enters through `run(func, ilOrCs)`, and the whole thing rides `addTask()`.
1399
- * When `ilOrCs` is a Lock this is `awaitIoWithInputLock` and the gate never opened; when it
1400
- * is a CriticalSection or nothing this is `awaitIo` and the resumption queues for a fresh
1401
- * lock like any other event.
1402
- */
1403
- #awaitIoImpl(promise, ilOrCs, func) {
1404
- const { promise: result, resolve, reject } = Promise.withResolvers();
1405
- this.addTask(promiseForExceptionOrT(promise).then(async (outcome) => {
1406
- try {
1407
- await this.run(() => {
1408
- if (outcome.ok) try {
1409
- resolve(func(outcome.value));
1410
- } catch (exception) {
1411
- reject(exception);
1412
- }
1413
- else reject(outcome.exception);
1414
- }, ilOrCs);
1415
- } catch (exception) {
1416
- if (ilOrCs instanceof Lock) ilOrCs.release();
1417
- throw exception;
1418
- }
1419
- }));
1420
- return result;
1421
- }
1422
- /**
1423
- * ← the first `.then()` of `blockConcurrencyWhile`: start the section, run the callback
1424
- * under its first nested lock, and race the deadline.
1425
- */
1426
- async #runCriticalSection(criticalSection, callback) {
1427
- const inputLock = await criticalSection.wait();
1428
- return await this.#runImpl((lock) => {
1429
- const running = callback(lock);
1430
- const deadline = new AbortController();
1431
- const timeout = this.#timer.afterDelay(BLOCK_CONCURRENCY_WHILE_TIMEOUT_MS, deadline.signal).then(() => {
1432
- throw new Error(BLOCK_CONCURRENCY_WHILE_TIMEOUT_MESSAGE);
1433
- });
1434
- return Promise.race([Promise.resolve(running), timeout]).finally(() => {
1435
- deadline.abort();
1436
- });
1437
- }, inputLock);
1438
- }
1439
- /** ← `IoContext::taskFailed()`, minus the logging half, which has no port. */
1440
- #taskFailed(exception) {
1441
- if (this.#waitUntilStatus === void 0) this.#waitUntilStatus = { exception };
1442
- }
1443
- };
1444
- //#endregion
1445
7
  //#region src/api/actor.ts
1446
8
  /**
1447
9
  * ← the `[1, 2048]` bound in `ColoLocalActorNamespace::get`.
@@ -3685,6 +2247,42 @@ var BODY_CONSUMERS = [
3685
2247
  "json",
3686
2248
  "text"
3687
2249
  ];
2250
+ /** A consumed Blob is a second-order body: every read it produces needs the same gate. */
2251
+ function gateBlob(ctx, blob) {
2252
+ const arrayBuffer = blob.arrayBuffer.bind(blob);
2253
+ const bytes = blob.bytes.bind(blob);
2254
+ const slice = blob.slice.bind(blob);
2255
+ const stream = blob.stream.bind(blob);
2256
+ const text = blob.text.bind(blob);
2257
+ Object.defineProperties(blob, {
2258
+ arrayBuffer: {
2259
+ configurable: true,
2260
+ writable: true,
2261
+ value: () => ctx.awaitIo(arrayBuffer())
2262
+ },
2263
+ bytes: {
2264
+ configurable: true,
2265
+ writable: true,
2266
+ value: () => ctx.awaitIo(bytes())
2267
+ },
2268
+ slice: {
2269
+ configurable: true,
2270
+ writable: true,
2271
+ value: (start, end, contentType) => gateBlob(ctx, slice(start, end, contentType))
2272
+ },
2273
+ stream: {
2274
+ configurable: true,
2275
+ writable: true,
2276
+ value: () => gateReadableStream(ctx, stream())
2277
+ },
2278
+ text: {
2279
+ configurable: true,
2280
+ writable: true,
2281
+ value: () => ctx.awaitIo(text())
2282
+ }
2283
+ });
2284
+ return blob;
2285
+ }
3688
2286
  /**
3689
2287
  * Wrap a `Request` or `Response` so every asynchronous step of reading it
3690
2288
  * resumes inside a gated slice.
@@ -3718,7 +2316,10 @@ function gateBody(ctx, value) {
3718
2316
  return clone;
3719
2317
  }
3720
2318
  if (BODY_CONSUMERS.includes(property)) {
3721
- const consume = (...args) => ctx.awaitIo(subject[property].apply(subject, args));
2319
+ const consume = (...args) => {
2320
+ const result = subject[property].apply(subject, args);
2321
+ return ctx.awaitIo(result, (value) => property === "blob" ? gateBlob(ctx, value) : value);
2322
+ };
3722
2323
  bound.set(property, consume);
3723
2324
  return consume;
3724
2325
  }
@@ -3748,11 +2349,45 @@ function gateResponseBody(ctx, response) {
3748
2349
  */
3749
2350
  var BYOB_READER_UNGATABLE_MESSAGE = "getReader({ mode: 'byob' }): a BYOB reader cannot be gated by this runtime, because `ReadableStream.getReader` is the only seam it has and a byte stream's `read(view)` returns the caller's own buffer. Read the body through `arrayBuffer()` or a default reader.";
3750
2351
  function gateReadableStream(ctx, stream) {
2352
+ const cancel = stream.cancel.bind(stream);
3751
2353
  const getReader = stream.getReader.bind(stream);
3752
2354
  const tee = stream.tee.bind(stream);
3753
2355
  const pipeThrough = stream.pipeThrough.bind(stream);
3754
2356
  const pipeTo = stream.pipeTo.bind(stream);
2357
+ const values = async function* (options) {
2358
+ const reader = gateReader(ctx, getReader());
2359
+ let finished = false;
2360
+ try {
2361
+ for (;;) {
2362
+ let result;
2363
+ try {
2364
+ result = await reader.read();
2365
+ } catch (exception) {
2366
+ finished = true;
2367
+ throw exception;
2368
+ }
2369
+ if (result.done) {
2370
+ finished = true;
2371
+ return;
2372
+ }
2373
+ yield result.value;
2374
+ }
2375
+ } finally {
2376
+ try {
2377
+ if (!finished && options?.preventCancel !== true) await reader.cancel();
2378
+ } finally {
2379
+ reader.releaseLock();
2380
+ }
2381
+ }
2382
+ };
3755
2383
  Object.defineProperties(stream, {
2384
+ cancel: {
2385
+ configurable: true,
2386
+ writable: true,
2387
+ value(reason) {
2388
+ return ctx.awaitIo(cancel(reason));
2389
+ }
2390
+ },
3756
2391
  getReader: {
3757
2392
  configurable: true,
3758
2393
  writable: true,
@@ -3782,15 +2417,47 @@ function gateReadableStream(ctx, stream) {
3782
2417
  value(destination, options) {
3783
2418
  return ctx.awaitIo(pipeTo(destination, options));
3784
2419
  }
2420
+ },
2421
+ values: {
2422
+ configurable: true,
2423
+ writable: true,
2424
+ value: values
2425
+ },
2426
+ [Symbol.asyncIterator]: {
2427
+ configurable: true,
2428
+ writable: true,
2429
+ value: values
3785
2430
  }
3786
2431
  });
3787
2432
  return stream;
3788
2433
  }
3789
2434
  function gateReader(ctx, reader) {
2435
+ const bound = /* @__PURE__ */ new Map();
3790
2436
  return new Proxy(reader, { get(subject, property) {
3791
- if (property === "read") return () => ctx.awaitIo(subject.read());
2437
+ const cached = bound.get(property);
2438
+ if (cached !== void 0) return cached;
2439
+ if (property === "closed") {
2440
+ const closed = ctx.awaitIo(subject.closed);
2441
+ bound.set(property, closed);
2442
+ return closed;
2443
+ }
2444
+ if (property === "read") {
2445
+ const read = () => ctx.awaitIo(subject.read());
2446
+ bound.set(property, read);
2447
+ return read;
2448
+ }
2449
+ if (property === "cancel") {
2450
+ const cancel = (reason) => ctx.awaitIo(subject.cancel(reason));
2451
+ bound.set(property, cancel);
2452
+ return cancel;
2453
+ }
3792
2454
  const value = Reflect.get(subject, property, subject);
3793
- return typeof value === "function" ? value.bind(subject) : value;
2455
+ if (typeof value === "function") {
2456
+ const method = value.bind(subject);
2457
+ bound.set(property, method);
2458
+ return method;
2459
+ }
2460
+ return value;
3794
2461
  } });
3795
2462
  }
3796
2463
  //#endregion