@restatedev/restate-sdk-gen 1.15.0-rc.1 → 1.15.0-rc.2

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/README.md CHANGED
@@ -72,7 +72,8 @@ Imported directly from `@restatedev/restate-sdk-gen`:
72
72
  - **`sleep(duration)`** — journaled timer.
73
73
  - **`awakeable<T>()`** — journaled awakeable; returns `{ id, promise: Future<T> }`.
74
74
  - **`channel<T>()`** — single-shot in-memory `Channel<T>`.
75
- - **`state<T>()` / `sharedState<T>()`** — typed key-value store.
75
+ - **`state<T>()` / `sharedState<T>()`** — typed key-value store (durable, survives across invocations).
76
+ - **`contextLocal<T>(default?)`** — ambient, in-memory storage scoped to the current invocation and shared by every fiber under it. Set once near the top, read anywhere downstream without threading a parameter. Not durable — for state that must outlive the invocation, use `state()`.
76
77
  - **`serviceClient` / `objectClient` / `workflowClient`** (+ `*SendClient`) — typed RPC into other Restate services.
77
78
  - **`genericCall` / `genericSend`** — untyped RPC.
78
79
  - **`cancel(invocationId)`** — cancel another invocation.
@@ -83,7 +84,8 @@ Imported directly from `@restatedev/restate-sdk-gen`:
83
84
  - **`all(futures)`** — wait for every future, return their values in order. Heterogeneous-tuple typed (mirrors `Promise.all`).
84
85
  - **`race(futures)`** — return the first to settle; the losing routines are abandoned once the main operation settles (under the default `onMainExit: "abandon"`), so their results are discarded. Under `onMainExit: "join"` the losers keep running and a loser parked on a never-settling source keeps the handler alive (see Cancellation).
85
86
  - **`select({ tag1: future1, tag2: future2, ... })`** — Tokio/Go-style. Returns `{ tag, future }` of the winning branch; switch on `tag` and unwrap `future`.
86
- - **`spawn(op)`** — register an `Operation` as a new routine; returns a `Future<T>` for its result. Under the default `onMainExit: "abandon"`, a spawned routine still running when the main operation settles is abandoned — fire-and-forget spawns are **not** guaranteed to complete. To ensure completion, `yield*` the returned future before returning, or run with `{ onMainExit: "join" }`.
87
+ - **`spawn(op)`** — register an `Operation` as a new routine; returns a `Task<T>` (a `Future<T>` plus `interrupt(err?)`). Under the default `onMainExit: "abandon"`, a spawned routine still running when the main operation settles is abandoned — fire-and-forget spawns are **not** guaranteed to complete. To ensure completion, `yield*` the returned future before returning, or run with `{ onMainExit: "join" }`.
88
+ - **`task.interrupt(err?)`** — throw `err` into a spawned routine at its next yield point (verbatim; a default `InterruptedError` if omitted) and abort its in-flight `run` I/O. **Cascades down the spawn subtree** — every routine the task spawned (transitively) is interrupted too, with the same error, so interrupting a parent winds down the whole tree it rooted; routines spawned elsewhere are untouched. The routine's own try/catch may catch and recover (interrupt is swallowable). Under the default `onMainExit: "abandon"`, interrupt-then-return abandons the routine before its cleanup runs — interrupt then `yield*` the task ("interrupt-then-join") to drive its `catch`/`finally`.
87
89
 
88
90
  Combinators have a fast path: when every input Future is journal-backed, they collapse to a single `RestatePromise.all/race`. Otherwise they fall back to a synthesized fiber. Same semantics either way.
89
91
 
package/dist/index.cjs CHANGED
@@ -172,11 +172,12 @@ const any = (futures) => currentOps().any(futures);
172
172
  */
173
173
  const allSettled = (futures) => currentOps().allSettled(futures);
174
174
  /**
175
- * Register `op` as a fresh routine and return a `Future<T>` for its
176
- * eventual outcome. Eager — the child is already in flight by the time
177
- * `spawn` returns. A spawned routine still running when the main
178
- * operation settles is abandoned by default (see `OnMainExit`). See
179
- * `RestateOperations.spawn` for full semantics.
175
+ * Register `op` as a fresh routine and return a `Task<T>` for its
176
+ * eventual outcome — a `Future<T>` plus `interrupt(err?)` to throw into
177
+ * the routine at its next yield point. Eager — the child is already in
178
+ * flight by the time `spawn` returns. A spawned routine still running
179
+ * when the main operation settles is abandoned by default (see
180
+ * `OnMainExit`). See `RestateOperations.spawn` for full semantics.
180
181
  */
181
182
  const spawn = (op) => currentOps().spawn(op);
182
183
 
@@ -216,6 +217,43 @@ function* select(branches) {
216
217
  };
217
218
  }
218
219
 
220
+ //#endregion
221
+ //#region src/context.ts
222
+ function contextLocal(defaultValue) {
223
+ const key = Symbol("restate.contextLocal");
224
+ return {
225
+ get: () => getCurrent().getLocal(key, defaultValue),
226
+ set: (value) => getCurrent().setLocal(key, value)
227
+ };
228
+ }
229
+
230
+ //#endregion
231
+ //#region src/task.ts
232
+ /**
233
+ * The default error thrown by `interrupt()` when no explicit error is
234
+ * given. A plain `Error` (not a `TerminalError`) — interrupt imposes no
235
+ * blast radius of its own; if you want an uncaught interrupt to fail the
236
+ * invocation terminally, pass a `TerminalError` to `interrupt(err)`.
237
+ */
238
+ var InterruptedError = class extends Error {
239
+ constructor(message = "Task interrupted") {
240
+ super(message);
241
+ this.name = "InterruptedError";
242
+ }
243
+ };
244
+ /**
245
+ * Augment a spawned routine's Future with `interrupt`, producing a Task.
246
+ * Takes a plain `(err) => void` callback (the fiber's `interrupt`) rather
247
+ * than the Fiber itself, so this module stays free of scheduler/fiber
248
+ * imports. `Object.assign` mutates and returns the same Future object —
249
+ * its `[Symbol.iterator]` and symbol-keyed backing are untouched, so the
250
+ * Task remains fully yieldable and composable.
251
+ */
252
+ function makeTask(future, interruptFiber) {
253
+ const interrupt = (err) => interruptFiber(err === void 0 ? new InterruptedError() : err);
254
+ return Object.assign(future, { interrupt });
255
+ }
256
+
219
257
  //#endregion
220
258
  //#region src/future.ts
221
259
  const futureBacking = Symbol("restateFutureBacking");
@@ -243,6 +281,29 @@ function isJournalBacked(f) {
243
281
  return getBacking(f).kind === "journal";
244
282
  }
245
283
 
284
+ //#endregion
285
+ //#region src/abort.ts
286
+ /**
287
+ * Create a controller whose signal is a child of `parent`: pre-aborted
288
+ * if the parent has already fired, otherwise aborted the moment it does.
289
+ *
290
+ * The subscription uses `{ once, signal: c.signal }` so it self-detaches
291
+ * on either exit — when the parent fires (`once`) or when this controller
292
+ * is aborted by some other path (`signal`). A controller that is retired
293
+ * and replaced therefore drops its listener immediately, so at most one
294
+ * listener per child lives on the parent at a time — no accumulation
295
+ * across cancel/recover or interrupt/recover cycles.
296
+ */
297
+ function linkAbortController(parent) {
298
+ const c = new AbortController();
299
+ if (parent.aborted) c.abort(parent.reason);
300
+ else parent.addEventListener("abort", () => c.abort(parent.reason), {
301
+ once: true,
302
+ signal: c.signal
303
+ });
304
+ return c;
305
+ }
306
+
246
307
  //#endregion
247
308
  //#region src/fiber.ts
248
309
  var Fiber = class {
@@ -253,9 +314,45 @@ var Fiber = class {
253
314
  resume: null
254
315
  };
255
316
  waiters = [];
256
- constructor(op, sched) {
317
+ /**
318
+ * Monotonic park-episode id, bumped on every `wake` (invariant I2).
319
+ * Consumed by `epochGuarded` (waiter validity) and `wokenDuringStep`
320
+ * (self-interrupt detection). Generalizes the old per-AwaitRace `won`
321
+ * flag into a persistent, per-fiber guard.
322
+ */
323
+ epoch = 0;
324
+ /**
325
+ * Whether the generator has been started (`it.next()` called at least
326
+ * once). `it.throw()` on a not-yet-started generator propagates the
327
+ * error out *without* running any body code, bypassing the routine's
328
+ * own try/catch/finally — so an interrupt delivered before the first
329
+ * advance must prime the generator first (see `stepIterator`).
330
+ */
331
+ started = false;
332
+ /**
333
+ * Lazily-created AbortController for this fiber's `run` closures. Born
334
+ * a child of the scheduler's current signal (so invocation cancellation
335
+ * / attempt-end abort it) and aborted directly by `interrupt`. Recreated
336
+ * when aborted, so cleanup `run`s after a swallowed interrupt see a
337
+ * fresh, unaborted signal. Null until the fiber first needs a run signal.
338
+ */
339
+ runController = null;
340
+ /**
341
+ * Spawn-tree links. `parent` is the fiber that was advancing when this
342
+ * one was created; `children` are the fibers it spawned (including the
343
+ * synth fibers behind combinator fallbacks). `interrupt` cascades down
344
+ * `children`. On `finish`, a fiber's still-live children are re-homed
345
+ * onto its `parent` (not dropped) so the subtree stays connected to a
346
+ * live ancestor and a later cascade still reaches them — `parent` is
347
+ * therefore reassignable as ancestors complete.
348
+ */
349
+ parent;
350
+ children = /* @__PURE__ */ new Set();
351
+ constructor(op, sched, parent = null) {
257
352
  this.it = op[Symbol.iterator]();
258
353
  this.sched = sched;
354
+ this.parent = parent;
355
+ parent?.children.add(this);
259
356
  }
260
357
  isDone() {
261
358
  return this.state.kind === "done";
@@ -295,9 +392,13 @@ var Fiber = class {
295
392
  * notifies the scheduler. May be called from any state except done
296
393
  * (waking a done fiber is a programming error and is ignored
297
394
  * defensively).
395
+ *
396
+ * Bumps the park epoch so any waiters left registered on the targets
397
+ * of the park we're leaving become stale no-ops (see `epoch`).
298
398
  */
299
399
  wake(resume) {
300
400
  if (this.state.kind === "done") return;
401
+ this.epoch++;
301
402
  this.state = {
302
403
  kind: "ready",
303
404
  resume
@@ -305,6 +406,72 @@ var Fiber = class {
305
406
  this.sched.markReady(this);
306
407
  }
307
408
  /**
409
+ * Wrap a handler as a waiter that fires only while the fiber is still
410
+ * in the park episode that registered it (see `epoch`, invariant I2).
411
+ * A waiter left on a target after the fiber moved on — a re-park after
412
+ * interrupt, or a losing race source — becomes a no-op. This is the
413
+ * single mechanism behind both the AwaitRace "first source wins" dedup
414
+ * and the cross-episode stale-waiter guard.
415
+ */
416
+ epochGuarded(handler) {
417
+ const at = this.epoch;
418
+ return (s) => {
419
+ if (this.epoch !== at) return;
420
+ handler(s);
421
+ };
422
+ }
423
+ /**
424
+ * Whether a re-entrant `wake` fired during the step that began at
425
+ * `epochBefore` — i.e. the fiber interrupted *itself* while advancing
426
+ * (the only wake that can target the currently-advancing fiber). It
427
+ * leaves a fresh resume in `this.state` to deliver at this yield rather
428
+ * than parking on the op the body just produced.
429
+ */
430
+ wokenDuringStep(epochBefore) {
431
+ return this.epoch !== epochBefore && this.state.kind === "ready";
432
+ }
433
+ /**
434
+ * The AbortSignal handed to this fiber's `run` closures. Lazily
435
+ * created as a child of the scheduler's current signal, so invocation
436
+ * cancellation and attempt-end abort it like before; additionally,
437
+ * `interrupt` aborts it directly to stop just this fiber's in-flight
438
+ * I/O. Recreated once aborted, so a `run` issued after a swallowed
439
+ * interrupt (or after cancellation recovery) gets a fresh signal.
440
+ */
441
+ runSignal() {
442
+ if (this.runController === null || this.runController.signal.aborted) this.runController = linkAbortController(this.sched.abortSignal);
443
+ return this.runController.signal;
444
+ }
445
+ /**
446
+ * Throw `err` into this fiber at its next yield point, and abort its
447
+ * in-flight `run` I/O.
448
+ *
449
+ * Two effects: (1) abort the per-fiber run signal so an in-flight
450
+ * `run(fetch(url, { signal }))` stops promptly — harmless if the fiber
451
+ * has no live controller; the next `runSignal()` makes a fresh one for
452
+ * post-interrupt cleanup. (2) wake the fiber with a failure resume so
453
+ * `stepIterator` delivers `it.throw(err)` at the next advance — the
454
+ * fiber's own try/catch may catch and recover (interrupt is
455
+ * swallowable / non-sticky). A done fiber is left untouched (`wake`
456
+ * no-ops). Same machinery as the cancellation fan-out, scoped to one
457
+ * fiber.
458
+ *
459
+ * Self-interrupt (a fiber interrupting its own task while advancing)
460
+ * is uniform: the re-entrant `wake` fires during the fiber's own
461
+ * step, and `advance` detects the epoch bump and delivers the throw at
462
+ * the fiber's next yield rather than parking (see `advance`). If the
463
+ * body returns before reaching another yield, the self-interrupt is
464
+ * moot — there is no yield point to throw at.
465
+ */
466
+ interrupt(err) {
467
+ this.runController?.abort(err);
468
+ this.wake({
469
+ ok: false,
470
+ e: err
471
+ });
472
+ for (const child of this.children) child.interrupt(err);
473
+ }
474
+ /**
308
475
  * Drive the fiber's iterator until it parks (yields a primitive
309
476
  * whose dispatch ends with the fiber waiting on a source) or
310
477
  * finishes (returns or throws).
@@ -318,9 +485,10 @@ var Fiber = class {
318
485
  try {
319
486
  let resume = this.state.resume;
320
487
  while (true) {
488
+ const epochBefore = this.epoch;
321
489
  let next;
322
490
  try {
323
- next = stepIterator(this.it, resume);
491
+ next = stepIterator(this.it, resume, this.started);
324
492
  } catch (e) {
325
493
  this.finish({
326
494
  ok: false,
@@ -328,6 +496,7 @@ var Fiber = class {
328
496
  });
329
497
  return;
330
498
  }
499
+ this.started = true;
331
500
  if (next.done) {
332
501
  this.finish({
333
502
  ok: true,
@@ -335,6 +504,10 @@ var Fiber = class {
335
504
  });
336
505
  return;
337
506
  }
507
+ if (this.wokenDuringStep(epochBefore)) {
508
+ resume = this.state.resume;
509
+ continue;
510
+ }
338
511
  const node = next.value[opTag];
339
512
  let outcome;
340
513
  switch (node._tag) {
@@ -371,7 +544,7 @@ var Fiber = class {
371
544
  };
372
545
  return null;
373
546
  }
374
- const settled = backing.target.awaitCompletion((s) => this.wake(s));
547
+ const settled = backing.target.awaitCompletion(this.epochGuarded((s) => this.wake(s)));
375
548
  if (settled !== null) return settled;
376
549
  this.state = {
377
550
  kind: "parked",
@@ -385,11 +558,13 @@ var Fiber = class {
385
558
  * (wrapped as Settled) on short-circuit, or null if parked.
386
559
  *
387
560
  * On the parked path, every source registers a one-shot fire
388
- * callback that wakes the fiber with `{index, settled}`. The `won`
389
- * flag guards against duplicate wakes when multiple sources settle
390
- * in the same tick. Local sources (fibers, channels) park on the
391
- * target's waiter list; journal sources race in the main loop's
392
- * race promise.
561
+ * callback that wakes the fiber with `{index, settled}`. The epoch
562
+ * guard ensures only the first source to settle wakes the fiber
563
+ * (`wake` bumps the epoch, so any later same-tick fire sees a stale
564
+ * epoch and no-ops) and that a waiter surviving an interrupt-and-
565
+ * re-park can't fire onto the moved-on fiber. Local sources (fibers,
566
+ * channels) park on the target's waiter list; journal sources race in
567
+ * the main loop's race promise.
393
568
  */
394
569
  parkOnAwaitRace(futures) {
395
570
  for (let i = 0; i < futures.length; i++) {
@@ -402,22 +577,17 @@ var Fiber = class {
402
577
  }
403
578
  };
404
579
  }
405
- let won = false;
406
580
  const promises = [];
407
581
  for (let i = 0; i < futures.length; i++) {
408
582
  const idx = i;
409
583
  const b = getBacking(futures[i]);
410
- const fireOnce = (settled) => {
411
- if (won) return;
412
- won = true;
413
- this.wake({
414
- ok: true,
415
- v: {
416
- index: idx,
417
- settled
418
- }
419
- });
420
- };
584
+ const fireOnce = this.epochGuarded((settled) => this.wake({
585
+ ok: true,
586
+ v: {
587
+ index: idx,
588
+ settled
589
+ }
590
+ }));
421
591
  if (b.kind === "local") b.target.awaitCompletion(fireOnce);
422
592
  else promises.push({
423
593
  promise: b.promise,
@@ -439,6 +609,13 @@ var Fiber = class {
439
609
  kind: "done",
440
610
  settled
441
611
  };
612
+ const gp = this.parent;
613
+ for (const child of this.children) {
614
+ child.parent = gp;
615
+ gp?.children.add(child);
616
+ }
617
+ this.children.clear();
618
+ gp?.children.delete(this);
442
619
  const waiters = this.waiters;
443
620
  this.waiters = [];
444
621
  for (const w of waiters) w(settled);
@@ -451,10 +628,23 @@ var Fiber = class {
451
628
  * very first step; `{ok: true, v}` resumes with a value; `{ok: false,
452
629
  * e}` throws into the iterator (or, if the iterator has no `throw`
453
630
  * method, rethrows so the fiber fails).
631
+ *
632
+ * `started` is whether the generator has run at least once. Throwing
633
+ * into a *not-yet-started* generator propagates the error straight out
634
+ * without executing any body code (its try/catch/finally never engage).
635
+ * That happens when a spawned routine is interrupted before its first
636
+ * advance. To honor the "delivered at the next yield point" /
637
+ * swallowable contract, we prime the generator with one `next()` first,
638
+ * so the throw lands at the body's first yield. If the body returns
639
+ * before reaching a yield, there is no yield point and the throw is moot.
454
640
  */
455
- function stepIterator(it, resume) {
641
+ function stepIterator(it, resume, started) {
456
642
  if (resume === null) return it.next(void 0);
457
643
  if (resume.ok) return it.next(resume.v);
644
+ if (!started) {
645
+ const first = it.next(void 0);
646
+ if (first.done) return first;
647
+ }
458
648
  if (it.throw) return it.throw(resume.e);
459
649
  throw resume.e;
460
650
  }
@@ -524,6 +714,13 @@ var Scheduler = class {
524
714
  * production `execute()` always constructs a fresh scheduler.
525
715
  */
526
716
  main = null;
717
+ /**
718
+ * The fiber currently being advanced by `drainReady`, or null between
719
+ * advances. `run` reads this (via `currentRunSignal`) to hand a `run`
720
+ * closure the *current fiber's* signal rather than a single global one,
721
+ * so a targeted `interrupt` aborts only that fiber's in-flight I/O.
722
+ */
723
+ advancingFiber = null;
527
724
  abortController;
528
725
  /**
529
726
  * Parent AbortSignal supplied at construction (production: the SDK's
@@ -546,6 +743,16 @@ var Scheduler = class {
546
743
  * keep this module independent of `restate-operations.ts`.
547
744
  */
548
745
  contextSlot;
746
+ /**
747
+ * Context-local storage: an ambient key/value bag scoped to this
748
+ * scheduler — i.e. to this one `execute()` call, since production
749
+ * builds a fresh scheduler per invocation (like `fibers` / `ready` /
750
+ * `abortController`, this is per-run state, not shared across runs).
751
+ * Every fiber under this scheduler reaches the same bag (global-per-
752
+ * invocation; see `context.ts`). Keyed by the opaque symbol a
753
+ * `contextLocal()` handle owns. In-memory only — never journaled.
754
+ */
755
+ localStore = /* @__PURE__ */ new Map();
549
756
  constructor(lib, parentSignal, options) {
550
757
  this.lib = lib;
551
758
  this.onMainExit = options?.onMainExit ?? "abandon";
@@ -580,13 +787,18 @@ var Scheduler = class {
580
787
  * attempt itself is over — a parent abort is sticky.
581
788
  */
582
789
  newAbortController() {
583
- const c = new AbortController();
584
- if (this.parentSignal.aborted) c.abort(this.parentSignal.reason);
585
- else this.parentSignal.addEventListener("abort", () => c.abort(this.parentSignal.reason), {
586
- once: true,
587
- signal: c.signal
588
- });
589
- return c;
790
+ return linkAbortController(this.parentSignal);
791
+ }
792
+ /**
793
+ * The AbortSignal for the currently-advancing fiber's `run` closures —
794
+ * scoped per fiber so a targeted `interrupt(task)` aborts only that
795
+ * task's in-flight I/O, while invocation cancellation / attempt-end
796
+ * still cascade in (the fiber signal is a child of `abortSignal`).
797
+ * Falls back to the scheduler signal when no fiber is advancing (a
798
+ * defensive case — `run` is always called from inside a fiber).
799
+ */
800
+ currentRunSignal() {
801
+ return this.advancingFiber !== null ? this.advancingFiber.runSignal() : this.abortSignal;
590
802
  }
591
803
  markReady(f) {
592
804
  this.ready.push(f);
@@ -595,7 +807,7 @@ var Scheduler = class {
595
807
  this.fibers.delete(f);
596
808
  }
597
809
  createFiber(op) {
598
- const f = new Fiber(op, this);
810
+ const f = new Fiber(op, this, this.advancingFiber);
599
811
  this.fibers.add(f);
600
812
  this.ready.push(f);
601
813
  return f;
@@ -607,24 +819,44 @@ var Scheduler = class {
607
819
  });
608
820
  }
609
821
  /**
610
- * Register an Operation as a fresh fiber and return a Future that
611
- * resolves with its eventual value. Eager: by the time this returns,
612
- * the fiber is queued ready and will be advanced on the next drain.
822
+ * Register an Operation as a fresh fiber, returning both its
823
+ * local-backed Future and the underlying Fiber. Eager: by the time
824
+ * this returns, the fiber is queued ready and will be advanced on the
825
+ * next drain.
613
826
  *
614
827
  * Lifetime is bounded by the main fiber under the default
615
828
  * `"abandon"` policy: a spawned fiber still running when the main
616
829
  * fiber settles is abandoned (see {@link OnMainExit}). Under
617
830
  * `"join"` it keeps the scheduler alive until it finishes.
618
831
  *
619
- * Used by combinator fallbacks (race, allSettled, …), by
620
- * `RestateOperations.spawn`, and via the slot interface by the free
621
- * `spawn` function.
832
+ * Internal: combinator fallbacks (race, allSettled, …) use this to get
833
+ * a plain Future. The public `spawn` wraps the same pair into a `Task`.
834
+ */
835
+ spawnRoutine(op) {
836
+ const fiber = this.createFiber(op);
837
+ return {
838
+ future: makeFuture({
839
+ kind: "local",
840
+ target: fiber
841
+ }),
842
+ fiber
843
+ };
844
+ }
845
+ /** Spawn returning just the Future — for combinator fallbacks, which
846
+ * never expose `interrupt` on their synthesized fibers. */
847
+ spawnFuture(op) {
848
+ return this.spawnRoutine(op).future;
849
+ }
850
+ /**
851
+ * Register an Operation as a fresh fiber and return a `Task<U>` — a
852
+ * `Future<U>` for its eventual value, plus `interrupt(err?)` to throw
853
+ * into it at its next yield point. Eager: by the time this returns the
854
+ * fiber is queued ready. Reached by `RestateOperations.spawn` and the
855
+ * free `spawn` function.
622
856
  */
623
857
  spawn(op) {
624
- return makeFuture({
625
- kind: "local",
626
- target: this.createFiber(op)
627
- });
858
+ const { future, fiber } = this.spawnRoutine(op);
859
+ return makeTask(future, (err) => fiber.interrupt(err));
628
860
  }
629
861
  /**
630
862
  * Construct a single-shot in-memory channel. Send must be called from
@@ -635,6 +867,19 @@ var Scheduler = class {
635
867
  return makeChannel();
636
868
  }
637
869
  /**
870
+ * Read a context-local value, or `fallback` if `key` was never set in
871
+ * this invocation. `has`-checked so an explicit `setLocal(key,
872
+ * undefined)` reads back as `undefined` rather than falling through to
873
+ * the handle's default.
874
+ */
875
+ getLocal(key, fallback) {
876
+ return this.localStore.has(key) ? this.localStore.get(key) : fallback;
877
+ }
878
+ /** Write a context-local value for the rest of this invocation. */
879
+ setLocal(key, value) {
880
+ this.localStore.set(key, value);
881
+ }
882
+ /**
638
883
  * Combinator over Futures. Fast path when every input is journal-
639
884
  * backed: use the lib's all/race for a single combinator entry.
640
885
  * Otherwise, fall back to a synthesized fiber that yields each in
@@ -651,7 +896,7 @@ var Scheduler = class {
651
896
  const promises = fs.map((f) => f[futureBacking].promise);
652
897
  return this.makeJournalFuture(this.lib.all(promises));
653
898
  }
654
- return this.spawn(gen(function* () {
899
+ return this.spawnFuture(gen(function* () {
655
900
  const out = new Array(fs.length);
656
901
  for (let i = 0; i < fs.length; i++) out[i] = yield* fs[i];
657
902
  return out;
@@ -659,7 +904,7 @@ var Scheduler = class {
659
904
  }
660
905
  race(futures) {
661
906
  const fs = futures;
662
- return this.spawn(gen(function* () {
907
+ return this.spawnFuture(gen(function* () {
663
908
  const result = yield* awaitRace(fs);
664
909
  if (result.settled.ok) return result.settled.v;
665
910
  throw result.settled.e;
@@ -686,7 +931,7 @@ var Scheduler = class {
686
931
  const promises = fs.map((f) => f[futureBacking].promise);
687
932
  return this.makeJournalFuture(this.lib.any(promises));
688
933
  }
689
- return this.spawn(gen(function* () {
934
+ return this.spawnFuture(gen(function* () {
690
935
  const errors = new Array(fs.length);
691
936
  const remaining = /* @__PURE__ */ new Set();
692
937
  for (let i = 0; i < fs.length; i++) remaining.add(i);
@@ -720,7 +965,7 @@ var Scheduler = class {
720
965
  const promises = fs.map((f) => f[futureBacking].promise);
721
966
  return this.makeJournalFuture(this.lib.allSettled(promises));
722
967
  }
723
- return this.spawn(gen(function* () {
968
+ return this.spawnFuture(gen(function* () {
724
969
  const out = new Array(fs.length);
725
970
  for (let i = 0; i < fs.length; i++) try {
726
971
  out[i] = {
@@ -748,7 +993,16 @@ var Scheduler = class {
748
993
  return this.onMainExit === "abandon" && this.main !== null && this.main.isDone();
749
994
  }
750
995
  drainReady() {
751
- while (this.ready.length > 0 && !this.mainExited()) this.ready.shift().advance();
996
+ while (this.ready.length > 0 && !this.mainExited()) {
997
+ const f = this.ready.shift();
998
+ const prev = this.advancingFiber;
999
+ this.advancingFiber = f;
1000
+ try {
1001
+ f.advance();
1002
+ } finally {
1003
+ this.advancingFiber = prev;
1004
+ }
1005
+ }
752
1006
  }
753
1007
  /**
754
1008
  * Run an operation to completion. Drain the ready queue, then loop:
@@ -1032,6 +1286,12 @@ var RestateOperations = class {
1032
1286
  isProcessing() {
1033
1287
  return this.ctx.isProcessing();
1034
1288
  }
1289
+ getLocal(key, fallback) {
1290
+ return this.sched.getLocal(key, fallback);
1291
+ }
1292
+ setLocal(key, value) {
1293
+ this.sched.setLocal(key, value);
1294
+ }
1035
1295
  /**
1036
1296
  * Run a side-effecting closure as a journal entry.
1037
1297
  *
@@ -1073,7 +1333,7 @@ var RestateOperations = class {
1073
1333
  */
1074
1334
  run(action, opts) {
1075
1335
  const name = resolveRunName(action, opts);
1076
- const wrapped = wrapActionForCancellation(this.sched.abortSignal, action);
1336
+ const wrapped = wrapActionForCancellation(this.sched.currentRunSignal(), action);
1077
1337
  return this.sched.makeJournalFuture(adapt(this.ctx.run(name, wrapped, toSdkRunOptions(opts))));
1078
1338
  }
1079
1339
  sleep(duration, name) {
@@ -1172,6 +1432,9 @@ var RestateOperations = class {
1172
1432
  * A spawned routine still running when the main operation settles is
1173
1433
  * abandoned by default (its handler returns without it); pass
1174
1434
  * `{ onMainExit: "join" }` to `execute` to wait for it instead.
1435
+ *
1436
+ * Returns a `Task<T>` — a `Future<T>` plus `interrupt(err?)` to throw
1437
+ * into the routine at its next yield point.
1175
1438
  */
1176
1439
  spawn(op) {
1177
1440
  return this.sched.spawn(op);
@@ -1611,6 +1874,7 @@ function isProcessing() {
1611
1874
  }
1612
1875
 
1613
1876
  //#endregion
1877
+ exports.InterruptedError = InterruptedError;
1614
1878
  exports.all = all;
1615
1879
  exports.allSettled = allSettled;
1616
1880
  exports.any = any;
@@ -1626,6 +1890,7 @@ Object.defineProperty(exports, 'clients', {
1626
1890
  return ingress_exports;
1627
1891
  }
1628
1892
  });
1893
+ exports.contextLocal = contextLocal;
1629
1894
  exports.date = date;
1630
1895
  exports.execute = execute;
1631
1896
  exports.gen = gen;