@sproutboat/runtime 0.45.0 → 0.46.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.
@@ -3043,6 +3043,12 @@ if (globalThis.setTimeout == null) globalThis.setTimeout = setTimeout;
3043
3043
  if (globalThis.clearTimeout == null) globalThis.clearTimeout = clearTimeout;
3044
3044
  if (globalThis.setInterval == null) globalThis.setInterval = setInterval;
3045
3045
  if (globalThis.clearInterval == null) globalThis.clearInterval = clearInterval;
3046
+ // The runtime's timers, for the prelude's own use. Once a Durable Object is built at the eviction date, the four globals become
3047
+ // wrappers that pin the object that armed a timer (__sbDOWrapTimers); a parked timer or a poll of ours must never pin anything.
3048
+ const __sbSetTimeout = globalThis.setTimeout;
3049
+ const __sbClearTimeout = globalThis.clearTimeout;
3050
+ const __sbSetInterval = globalThis.setInterval;
3051
+ const __sbClearInterval = globalThis.clearInterval;
3046
3052
 
3047
3053
  // Prepended to every handler by tools/compile.ts, before Porffor's native-fetch
3048
3054
  // bundled handler. Porffor's runtime/fetch-globals.js (checked through alpha-10)
@@ -3895,7 +3901,7 @@ if (typeof globalThis.EventTarget !== "function") {
3895
3901
  }
3896
3902
  static timeout(ms) {
3897
3903
  const signal = new AbortSignal();
3898
- setTimeout(() => signal.__sbAbort(namedError("TimeoutError", "The operation timed out")), ms);
3904
+ __sbSetTimeout(() => signal.__sbAbort(namedError("TimeoutError", "The operation timed out")), ms);
3899
3905
  return signal;
3900
3906
  }
3901
3907
  static any(signals) {
@@ -11249,27 +11255,104 @@ function __sbMakeR2(name) {
11249
11255
  };
11250
11256
  }
11251
11257
 
11252
- // Asset rules use the same parsed manifest in both transports. Matching mirrors
11253
- // @sproutboat/assets; parity tests cover the portable native implementation.
11254
- function __sbAssetRedirect(rule, path) {
11255
- const star = rule.from.indexOf("*");
11256
- if (star !== -1) {
11257
- const prefix = rule.from.slice(0, star);
11258
- if (!path.startsWith(prefix)) return null;
11259
- return rule.to.split(":splat").join(path.slice(prefix.length));
11258
+ // Asset rules use the same parsed manifest in both transports. Redirect matching mirrors
11259
+ // @sproutboat/assets (Cloudflare's `_redirects` rules: the whole path must match, so a trailing
11260
+ // slash is significant; `:name` is one or more characters up to the next "/"; one `*` is
11261
+ // `:splat`; a static rule beats a dynamic one); parity tests cover the portable implementation.
11262
+ function __sbRuleAlpha(code) {
11263
+ return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
11264
+ }
11265
+ function __sbRuleWord(code) {
11266
+ return __sbRuleAlpha(code) || (code >= 48 && code <= 57) || code === 95;
11267
+ }
11268
+ function __sbRuleIsStatic(from) {
11269
+ for (let i = 0; i < from.length; i++) {
11270
+ const code = from.charCodeAt(i);
11271
+ if (code === 42) return false;
11272
+ if (code === 58 && i + 1 < from.length && __sbRuleAlpha(from.charCodeAt(i + 1))) return false;
11260
11273
  }
11261
- const from = rule.from.split("/").filter(Boolean);
11262
- const parts = path.split("/").filter(Boolean);
11263
- if (from.length !== parts.length) return null;
11264
- const params = {};
11274
+ return true;
11275
+ }
11276
+ // Tokens are [kind, text-or-name] pairs: 0 literal text, 1 :name, 2 splat. Null: the rule can never match.
11277
+ function __sbRuleTokens(from) {
11278
+ const tokens = [];
11279
+ const names = [];
11280
+ let text = "";
11265
11281
  for (let i = 0; i < from.length; i++) {
11266
- if (from[i].startsWith(":")) params[from[i].slice(1)] = parts[i];
11267
- else if (from[i] !== parts[i]) return null;
11282
+ const code = from.charCodeAt(i);
11283
+ if (code === 42) {
11284
+ if (text !== "") tokens.push([0, text]);
11285
+ text = "";
11286
+ if (names.indexOf("splat") !== -1) return null;
11287
+ names.push("splat");
11288
+ tokens.push([2, "splat"]);
11289
+ } else if (code === 58 && i + 1 < from.length && __sbRuleAlpha(from.charCodeAt(i + 1))) {
11290
+ let end = i + 2;
11291
+ while (end < from.length && __sbRuleWord(from.charCodeAt(end))) end++;
11292
+ const name = from.slice(i + 1, end);
11293
+ if (names.indexOf(name) !== -1) return null;
11294
+ names.push(name);
11295
+ if (text !== "") tokens.push([0, text]);
11296
+ text = "";
11297
+ tokens.push([1, name]);
11298
+ i = end - 1;
11299
+ } else {
11300
+ text += from.charAt(i);
11301
+ }
11302
+ }
11303
+ if (text !== "") tokens.push([0, text]);
11304
+ return tokens;
11305
+ }
11306
+ function __sbRuleMatch(tokens, t, path, p, names, values) {
11307
+ if (t === tokens.length) return p === path.length;
11308
+ const kind = tokens[t][0];
11309
+ const word = tokens[t][1];
11310
+ if (kind === 0) {
11311
+ if (path.slice(p, p + word.length) !== word) return false;
11312
+ return __sbRuleMatch(tokens, t + 1, path, p + word.length, names, values);
11313
+ }
11314
+ let limit = path.length;
11315
+ if (kind === 1) {
11316
+ for (let i = p; i < path.length; i++) {
11317
+ if (path.charCodeAt(i) === 47) {
11318
+ limit = i;
11319
+ break;
11320
+ }
11321
+ }
11322
+ }
11323
+ const least = kind === 1 ? p + 1 : p;
11324
+ for (let q = limit; q >= least; q--) {
11325
+ names.push(word);
11326
+ values.push(path.slice(p, q));
11327
+ if (__sbRuleMatch(tokens, t + 1, path, q, names, values)) return true;
11328
+ names.pop();
11329
+ values.pop();
11268
11330
  }
11331
+ return false;
11332
+ }
11333
+ function __sbAssetRedirect(rule, path) {
11334
+ if (__sbRuleIsStatic(rule.from)) return rule.from === path ? rule.to : null;
11335
+ const tokens = __sbRuleTokens(rule.from);
11336
+ if (tokens === null) return null;
11337
+ const names = [];
11338
+ const values = [];
11339
+ if (!__sbRuleMatch(tokens, 0, path, 0, names, values)) return null;
11269
11340
  let to = rule.to;
11270
- for (const name in params) to = to.split(":" + name).join(params[name]);
11341
+ for (let i = 0; i < names.length; i++) to = to.split(":" + names[i]).join(values[i]);
11271
11342
  return to;
11272
11343
  }
11344
+ // The first static rule that matches, else the first dynamic one: { status, location } or null.
11345
+ function __sbFindAssetRedirect(rules, path) {
11346
+ for (let i = 0; i < rules.length; i++) {
11347
+ if (__sbRuleIsStatic(rules[i].from) && rules[i].from === path) return { status: rules[i].status, location: rules[i].to };
11348
+ }
11349
+ for (let i = 0; i < rules.length; i++) {
11350
+ if (__sbRuleIsStatic(rules[i].from)) continue;
11351
+ const location = __sbAssetRedirect(rules[i], path);
11352
+ if (location !== null) return { status: rules[i].status, location };
11353
+ }
11354
+ return null;
11355
+ }
11273
11356
 
11274
11357
  function __sbAssetHeaderMatches(pattern, path) {
11275
11358
  const from = pattern.split("/").filter(Boolean);
@@ -11284,11 +11367,8 @@ function __sbAssetHeaderMatches(pattern, path) {
11284
11367
  function __sbAssetResponse(path, r) {
11285
11368
  if (r.location != null) return new Response(null, { status: r.status, headers: { location: r.location } });
11286
11369
  const rules = r.rules || {};
11287
- const redirects = rules.redirects || [];
11288
- for (let i = 0; i < redirects.length; i++) {
11289
- const to = __sbAssetRedirect(redirects[i], path);
11290
- if (to !== null) return new Response(null, { status: redirects[i].status, headers: { location: to } });
11291
- }
11370
+ const redirect = __sbFindAssetRedirect(rules.redirects || [], path);
11371
+ if (redirect !== null) return new Response(null, { status: redirect.status, headers: { location: redirect.location } });
11292
11372
  const headers = new Headers();
11293
11373
  if (r.type) headers.set("content-type", r.type);
11294
11374
  if (r.found) headers.set("etag", '"' + r.hash + '"');
@@ -12202,7 +12282,7 @@ async function __sbPumpRequestBody(id, source, ended) {
12202
12282
  if (bytes.length === 0) continue;
12203
12283
  let backlog = Number(globalThis.__sbAsyncBody(id, "c", __sbToBytes(bytes)));
12204
12284
  while (backlog > 4 * 1024 * 1024 && !ended()) {
12205
- await new Promise((resolve) => setTimeout(resolve, 2));
12285
+ await new Promise((resolve) => __sbSetTimeout(resolve, 2));
12206
12286
  backlog = Number(globalThis.__sbAsyncBody(id, "q", ""));
12207
12287
  }
12208
12288
  }
@@ -12222,10 +12302,10 @@ function __sbUpstreamStreamed(op, msg, label, signal) {
12222
12302
  let done = false;
12223
12303
  const finish = () => {
12224
12304
  done = true;
12225
- clearInterval(id);
12305
+ __sbClearInterval(id);
12226
12306
  if (onAbort) signal.removeEventListener("abort", onAbort);
12227
12307
  };
12228
- const id = setInterval(() => {
12308
+ const id = __sbSetInterval(() => {
12229
12309
  if (done) return;
12230
12310
  const raw = __sbHttpAsyncResult(id);
12231
12311
  if (controller === null) {
@@ -12361,7 +12441,7 @@ function __sbUpstream(op, msg, label, signal, always) {
12361
12441
  const settled = () => {
12362
12442
  if (onAbort) signal.removeEventListener("abort", onAbort);
12363
12443
  };
12364
- const id = setTimeout(() => {
12444
+ const id = __sbSetTimeout(() => {
12365
12445
  settled();
12366
12446
  try {
12367
12447
  resolve(__sbUpstreamResponse(__sbFetchReply(__sbHttpAsyncResult(id))));
@@ -12372,7 +12452,7 @@ function __sbUpstream(op, msg, label, signal, always) {
12372
12452
  if (signal) {
12373
12453
  onAbort = () => {
12374
12454
  settled();
12375
- clearTimeout(id);
12455
+ __sbClearTimeout(id);
12376
12456
  __sbCancelAsync(id);
12377
12457
  reject(signal.reason);
12378
12458
  };
@@ -12383,7 +12463,7 @@ function __sbUpstream(op, msg, label, signal, always) {
12383
12463
  globalThis.__sbFetchUpstream(op, msg);
12384
12464
  } catch (e) {
12385
12465
  settled();
12386
- clearTimeout(id);
12466
+ __sbClearTimeout(id);
12387
12467
  reject(e);
12388
12468
  }
12389
12469
  });
@@ -12396,7 +12476,7 @@ function __sbUpstream(op, msg, label, signal, always) {
12396
12476
  globalThis.__sbInstallFetch = function () {
12397
12477
  __sbIngressStreamingSetup();
12398
12478
  __sbInstallWebSockets();
12399
- globalThis.fetch = function (input, init) {
12479
+ const plainFetch = function (input, init) {
12400
12480
  let parts;
12401
12481
  try {
12402
12482
  parts = __sbFetchParts(input, init);
@@ -12418,6 +12498,29 @@ globalThis.__sbInstallFetch = function () {
12418
12498
  if (stream instanceof ReadableStream) return __sbCollectBody(stream).then((bytes) => send(__sbRequestBody(bytes)));
12419
12499
  return send(__sbPartsBody(parts));
12420
12500
  };
12501
+ // Inside a Durable Object's event an unawaited fetch keeps its object in memory until it settles.
12502
+ globalThis.fetch = function (input, init) {
12503
+ const rec = __sbDOL !== null ? __sbDOFrameRec() : undefined;
12504
+ if (rec === undefined || rec.dead) return plainFetch(input, init);
12505
+ __sbDOPin(rec);
12506
+ let p;
12507
+ try {
12508
+ p = plainFetch(input, init);
12509
+ } catch (error) {
12510
+ __sbDOUnpin(rec);
12511
+ throw error;
12512
+ }
12513
+ return p.then(
12514
+ (r) => {
12515
+ __sbDOUnpin(rec);
12516
+ return r;
12517
+ },
12518
+ (error) => {
12519
+ __sbDOUnpin(rec);
12520
+ throw error;
12521
+ },
12522
+ );
12523
+ };
12421
12524
  };
12422
12525
 
12423
12526
  // ---------------------------------------------------------------------------
@@ -12665,6 +12768,7 @@ function __sbDOCallback(fn, args) {
12665
12768
 
12666
12769
  // One RPC call: method `method` with `args`, or the getter `method` when args is null.
12667
12770
  function __sbDOInvoke(cls, id, method, args) {
12771
+ if (__sbDOEvictOn()) return __sbDOInvokeLive(cls, id, method, args);
12668
12772
  let copied = null;
12669
12773
  let inst;
12670
12774
  try {
@@ -12691,7 +12795,38 @@ function __sbDOInvoke(cls, id, method, args) {
12691
12795
  );
12692
12796
  }
12693
12797
 
12798
+ // RPC into an object that may be evicted (compatibility date 2026-11-21): the event enters through __sbDORun.
12799
+ function __sbDOInvokeLive(cls, id, method, args) {
12800
+ let copied = null;
12801
+ try {
12802
+ if (args !== null) copied = __sbDOCopy(args);
12803
+ } catch (error) {
12804
+ return Promise.reject(error);
12805
+ }
12806
+ return __sbDORun(
12807
+ cls,
12808
+ id.toString(),
12809
+ id,
12810
+ (inst) => {
12811
+ const d = __sbDOFind(__sbDOClasses[cls], method);
12812
+ if (args === null) return d.get.call(inst);
12813
+ if (!d || !__sbIsFn(d.value)) throw new TypeError('The RPC receiver does not implement the method "' + method + '".');
12814
+ return d.value.apply(inst, copied);
12815
+ },
12816
+ false,
12817
+ ).then(__sbDOCopy);
12818
+ }
12819
+
12694
12820
  function __sbDOFetchV2(className, id, input, init) {
12821
+ if (__sbDOEvictOn()) {
12822
+ let request;
12823
+ try {
12824
+ request = new Request(input, init);
12825
+ } catch (error) {
12826
+ return Promise.reject(error);
12827
+ }
12828
+ return __sbDORun(className, id.toString(), id, (inst) => __sbDOFetchEvent(inst, request), false);
12829
+ }
12695
12830
  let req;
12696
12831
  let inst;
12697
12832
  try {
@@ -12816,6 +12951,691 @@ function __sbGetDOInstance(cls, idText, idObj) {
12816
12951
  return inst;
12817
12952
  }
12818
12953
 
12954
+ // ---------------------------------------------------------------------------
12955
+ // Durable Object lifecycle (design 07 stage 1c, plan 018), from compatibility date 2026-11-21: an idle object
12956
+ // leaves memory and is constructed again by its next event, so one app can hold one object per customer.
12957
+ //
12958
+ // A record per object in a worker (not the bare instance): { cls, id, key, inst, state, gate, holder, tasks, busy,
12959
+ // pins, last, dead, prev, next, ... }. `busy` counts the events that entered through __sbDORun and have not
12960
+ // finished, their waitUntil drain included; `pins` counts things that outlive an event and cannot be rebuilt (a pending
12961
+ // timer, an unawaited fetch, a streamed response); open plain sockets are checked when idleness is. The records are one
12962
+ // least-recently-idle list (`head` the stalest). An object is evicted only when __sbDOIsIdle says so, in one synchronous
12963
+ // turn (__sbDOEvict): nothing in it awaits. Before the date nothing here runs and every object stays resident.
12964
+ //
12965
+ // Settings (environment; sproutd sets them from `limits ID do-idle-ms|objects|do-wait-ms`):
12966
+ // SB_DO_IDLE_MS how long an object sits idle before it leaves, 100 to 600000 (default 10000; 0 turns idle eviction off)
12967
+ // SB_DO_MAX most objects in memory in this worker (default 16 per MiB of SB_MEMORY_LIMIT, 64 to 4096; 4096 with no limit)
12968
+ // SB_DO_WAIT_MS how long an object's first event waits for a slot at the cap before it fails alone (default 5000)
12969
+ // SB_DO_QUEUE_MAX most events queued behind one object's input gate (default 1024)
12970
+ const __SB_DO_EVICT_DATE = "2026-11-21";
12971
+ const __sbDOCtxKey = {};
12972
+ let __sbDOL = null;
12973
+
12974
+ function __sbDOEvictOn() {
12975
+ return __sbCompatDate() >= __SB_DO_EVICT_DATE && __sbDOGated() && __sbDOv3();
12976
+ }
12977
+
12978
+ function __sbDOEnvNum(name, fallback, min, max) {
12979
+ const text = __sbEnv(name);
12980
+ if (text === "") return fallback;
12981
+ const n = Number(text);
12982
+ return n >= min && n <= max ? Math.trunc(n) : fallback;
12983
+ }
12984
+
12985
+ function __sbDOLife() {
12986
+ if (__sbDOL !== null) return __sbDOL;
12987
+ let idle = __sbDOEnvNum("SB_DO_IDLE_MS", 10000, 100, 600000);
12988
+ if (__sbEnv("SB_DO_IDLE_MS") === "0") idle = 0;
12989
+ const limit = Number(__sbEnv("SB_MEMORY_LIMIT"));
12990
+ const byLimit = limit > 0 ? Math.max(64, Math.min(4096, Math.floor(limit / 1048576) * 16)) : 4096;
12991
+ __sbDOL = {
12992
+ idle,
12993
+ cap: __sbDOEnvNum("SB_DO_MAX", byLimit, 1, 100000),
12994
+ waitMs: __sbDOEnvNum("SB_DO_WAIT_MS", 5000, 10, 600000),
12995
+ queueMax: __sbDOEnvNum("SB_DO_QUEUE_MAX", 1024, 1, 1000000),
12996
+ recs: {},
12997
+ head: null,
12998
+ tail: null,
12999
+ count: 0,
13000
+ waiters: [],
13001
+ sweepArmed: false,
13002
+ hostU: -1,
13003
+ restoreParked: false,
13004
+ restoreTries: {},
13005
+ noteAt: 0,
13006
+ noteText: "",
13007
+ pinned: 0,
13008
+ evictedIdle: 0,
13009
+ evictedCap: 0,
13010
+ evictedAbort: 0,
13011
+ waits: 0,
13012
+ waitMsMax: 0,
13013
+ overloaded: 0,
13014
+ zombie: 0,
13015
+ wakeUsLast: 0,
13016
+ wakeUsMax: 0,
13017
+ };
13018
+ __sbDOWrapTimers();
13019
+ return __sbDOL;
13020
+ }
13021
+
13022
+ function __sbDOEvictedError() {
13023
+ return new Error("This Durable Object instance was evicted from memory.");
13024
+ }
13025
+
13026
+ function __sbDOOverloaded(why) {
13027
+ const error = new Error("Durable Object is overloaded: " + why);
13028
+ error.overloaded = true;
13029
+ return error;
13030
+ }
13031
+
13032
+ // The record of the object whose event is running now (the frame the event entered carries it), or undefined.
13033
+ function __sbDOFrameRec() {
13034
+ const frame = __sbCtxGet();
13035
+ if (!frame) return undefined;
13036
+ const at = __sbFrameIndex(frame, __sbDOCtxKey);
13037
+ return at === -1 ? undefined : frame.vals[at];
13038
+ }
13039
+
13040
+ // ---- the list: head is the stalest record, tail the freshest ----
13041
+ function __sbDOLink(L, rec) {
13042
+ rec.prev = L.tail;
13043
+ rec.next = null;
13044
+ if (L.tail !== null) L.tail.next = rec;
13045
+ else L.head = rec;
13046
+ L.tail = rec;
13047
+ L.count++;
13048
+ }
13049
+ function __sbDOUnlink(L, rec) {
13050
+ if (rec.prev !== null) rec.prev.next = rec.next;
13051
+ else L.head = rec.next;
13052
+ if (rec.next !== null) rec.next.prev = rec.prev;
13053
+ else L.tail = rec.prev;
13054
+ rec.prev = null;
13055
+ rec.next = null;
13056
+ L.count--;
13057
+ }
13058
+ // Becomes idle now: the freshest record.
13059
+ function __sbDOFresh(L, rec, now) {
13060
+ rec.last = now === undefined ? Date.now() : now;
13061
+ if (L.tail === rec) return;
13062
+ __sbDOUnlink(L, rec);
13063
+ __sbDOLink(L, rec);
13064
+ }
13065
+
13066
+ // ---- idleness ----
13067
+ function __sbDOSocketsFree(L, rec) {
13068
+ const plain = rec.plain;
13069
+ if (plain.length > 0) {
13070
+ for (let i = plain.length - 1; i >= 0; i--) if (plain[i]._state === __SB_WS_CLOSED) plain.splice(i, 1);
13071
+ if (plain.length > 0) return false;
13072
+ }
13073
+ const tcp = rec.tcp;
13074
+ if (tcp.length > 0) {
13075
+ for (let i = tcp.length - 1; i >= 0; i--) if (tcp[i].id === 0) tcp.splice(i, 1);
13076
+ if (tcp.length > 0) return false;
13077
+ }
13078
+ const sockets = rec.holder.sockets;
13079
+ for (let i = 0; i < sockets.length; i++) {
13080
+ const server = sockets[i];
13081
+ if (server._state === __SB_WS_CLOSED) continue;
13082
+ const client = server._peer;
13083
+ // A pair whose two ends are both in the worker, or no host that takes a socket back: nothing outside holds it.
13084
+ if (client === null || client._host === 0) return false;
13085
+ if (L.hostU < 0) L.hostU = __sbWsOp("U", 0, "", 0) === "1" ? 1 : 0;
13086
+ if (L.hostU !== 1) return false;
13087
+ // An event that left the bridge and exists only in this worker (the bridge keeps the rest of the queue).
13088
+ if (client._inbox.length > 0 || server._inbox.length > 0 || client._flushing || server._flushing) return false;
13089
+ }
13090
+ return true;
13091
+ }
13092
+ function __sbDOIsIdle(L, rec) {
13093
+ const gate = rec.gate;
13094
+ return (
13095
+ rec.busy === 0 &&
13096
+ rec.pins === 0 &&
13097
+ gate.locks === 0 &&
13098
+ gate.waiting.length === 0 &&
13099
+ !gate.txnBusy &&
13100
+ gate.txnQueue.length === 0 &&
13101
+ __sbDOSocketsFree(L, rec)
13102
+ );
13103
+ }
13104
+
13105
+ function __sbDOPin(rec) {
13106
+ rec.pins++;
13107
+ if (rec.pins === 1) {
13108
+ rec.pinSince = Date.now();
13109
+ __sbDOL.pinned++;
13110
+ }
13111
+ }
13112
+ function __sbDOUnpin(rec) {
13113
+ if (rec.pins === 0) return;
13114
+ rec.pins--;
13115
+ if (rec.pins !== 0) return;
13116
+ const L = __sbDOL;
13117
+ if (rec.dead) return;
13118
+ L.pinned--;
13119
+ rec.pinLogged = false;
13120
+ if (rec.busy === 0) {
13121
+ __sbDOFresh(L, rec);
13122
+ __sbDOServe(L);
13123
+ __sbDOArmSweep(L);
13124
+ }
13125
+ }
13126
+
13127
+ // ---- eviction: one synchronous turn, nothing awaits ----
13128
+ // The reason says why: "idle", "cap" (a slot was needed), "abort" (state.abort()), "bcw" (blockConcurrencyWhile failed).
13129
+ // `force` skips the idleness check and closes the object's sockets (abort and reset); otherwise sockets go back to the host.
13130
+ function __sbDOEvict(rec, reason, force) {
13131
+ if (rec.dead) return false;
13132
+ const L = __sbDOL;
13133
+ if (!force && !__sbDOIsIdle(L, rec)) return false;
13134
+ if (rec.registered) {
13135
+ delete L.recs[rec.key];
13136
+ __sbDOUnlink(L, rec);
13137
+ rec.registered = false;
13138
+ if (rec.pins > 0) L.pinned--;
13139
+ }
13140
+ // Revoke: every state method of this instance now throws the evicted error, so a leftover closure cannot write as a second instance.
13141
+ rec.dead = true;
13142
+ __sbDOReleaseSockets(L, rec, force);
13143
+ if (rec.usedSql) {
13144
+ try {
13145
+ __sbRpc("do.sql.close", { cls: rec.cls, file: rec.sqlFile });
13146
+ } catch (error) {
13147
+ __sbLogUncaught("do.sql.close", error);
13148
+ }
13149
+ }
13150
+ const holder = rec.holder;
13151
+ if (holder !== null) {
13152
+ holder.inst = null;
13153
+ holder.rec = null;
13154
+ holder.sockets = [];
13155
+ }
13156
+ rec.inst = null;
13157
+ rec.state = null;
13158
+ rec.plain = [];
13159
+ rec.tcp = [];
13160
+ if (reason === "idle") L.evictedIdle++;
13161
+ else if (reason === "cap") L.evictedCap++;
13162
+ else L.evictedAbort++;
13163
+ return true;
13164
+ }
13165
+
13166
+ // The object's hibernatable sockets go back to the host (op g: unclaimed, their queued events kept), or are closed for a reset.
13167
+ function __sbDOReleaseSockets(L, rec, force) {
13168
+ const holder = rec.holder;
13169
+ if (holder === null) return;
13170
+ const sockets = holder.sockets;
13171
+ for (let i = 0; i < sockets.length; i++) {
13172
+ const server = sockets[i];
13173
+ const client = server._peer;
13174
+ if (client !== null && client._pumpTimer !== undefined) {
13175
+ __sbClearTimeout(client._pumpTimer);
13176
+ client._pumpTimer = undefined;
13177
+ }
13178
+ if (server._state === __SB_WS_CLOSED) continue;
13179
+ if (force) {
13180
+ try {
13181
+ server.close(1011, "Durable Object reset");
13182
+ } catch {
13183
+ /* already closing */
13184
+ }
13185
+ continue;
13186
+ }
13187
+ if (client !== null && client._host > 0) {
13188
+ const hostId = client._host;
13189
+ client._host = 0;
13190
+ server._state = __SB_WS_CLOSED;
13191
+ if (!L.restoreParked) {
13192
+ L.restoreParked = true;
13193
+ __sbWsRestorePark();
13194
+ }
13195
+ __sbWsOp("g", hostId, "", 0);
13196
+ }
13197
+ }
13198
+ }
13199
+
13200
+ // state.abort() and a failed blockConcurrencyWhile(): the object is forgotten (storage kept) and this instance is revoked.
13201
+ function __sbDOReset(rec, reason, error) {
13202
+ if (rec.dead) return;
13203
+ const L = __sbDOL;
13204
+ rec.deadError = error === undefined ? null : error;
13205
+ __sbDOEvict(rec, reason, true);
13206
+ // Events waiting at its gate now fail (each finds the record dead).
13207
+ rec.gate.locks = 0;
13208
+ __sbDOPump(rec.gate);
13209
+ __sbDOServe(L);
13210
+ }
13211
+
13212
+ function __sbDOStalest(L) {
13213
+ for (let rec = L.head; rec !== null; rec = rec.next) if (__sbDOIsIdle(L, rec)) return rec;
13214
+ return null;
13215
+ }
13216
+
13217
+ // The sweep: idle objects older than the timeout leave, from the stale end of the list. Bounded twice: `limit` records are looked at and at
13218
+ // most `most` leave, because an eviction closes a SQLite handle (a checkpoint and its fsyncs, about a millisecond) and the whole sweep is one
13219
+ // synchronous turn: 500 objects going at once would stop every request for half a second or more (measured: p99 1.9 s in an alarm storm).
13220
+ // What is left goes on the next tick; the cap evicts one object at a time when a slot is needed in the meantime.
13221
+ function __sbDOSweep(L, limit, most) {
13222
+ if (L.idle === 0) return;
13223
+ const now = Date.now();
13224
+ let seen = 0;
13225
+ let left = most;
13226
+ for (let rec = L.head; rec !== null && seen < limit && left > 0; ) {
13227
+ const next = rec.next;
13228
+ seen++;
13229
+ if (rec.busy === 0) {
13230
+ if (now - rec.last < L.idle) break;
13231
+ if (rec.pins > 0) {
13232
+ if (!rec.pinLogged && now - rec.pinSince > 60000) {
13233
+ rec.pinLogged = true;
13234
+ console.error("Durable Object " + rec.cls + " has been held in memory for over 60 s by a timer, a connection or a streamed response; it cannot be evicted while that lives");
13235
+ }
13236
+ } else if (__sbDOEvict(rec, "idle", false)) left--;
13237
+ }
13238
+ rec = next;
13239
+ }
13240
+ }
13241
+
13242
+ // The sweep timer is parked at the host (op D): the host fires it when its deadline passes, and it is not background work, so
13243
+ // an app whose objects are all evicted is still reaped on time. A host without the op (an older sproutd) gets no timer: objects
13244
+ // then leave when the next event arrives. A standalone binary has no reaper, so a plain timer does it.
13245
+ function __sbDOArmSweep(L) {
13246
+ if (L.sweepArmed || L.idle === 0 || L.count === 0) return;
13247
+ let delay = L.idle;
13248
+ if (L.head !== null) delay = Math.max(50, Math.min(L.idle, L.head.last + L.idle - Date.now()));
13249
+ const timer = __sbSetTimeout(__sbDOSweepFire, 2147483647);
13250
+ const parked = __sbWsOp("D", Math.trunc(delay), "", timer) === "0";
13251
+ if (parked) {
13252
+ L.sweepArmed = true;
13253
+ return;
13254
+ }
13255
+ __sbClearTimeout(timer);
13256
+ if (!__sbHosted()) {
13257
+ L.sweepArmed = true;
13258
+ __sbSetTimeout(__sbDOSweepFire, delay);
13259
+ }
13260
+ }
13261
+ function __sbDOSweepFire() {
13262
+ const L = __sbDOL;
13263
+ if (L === null) return;
13264
+ L.sweepArmed = false;
13265
+ const before = L.evictedIdle;
13266
+ __sbDOSweep(L, 100000, 24);
13267
+ // Objects left: SQLite hands the pages of the databases still open back too (the quiet collection follows this timer).
13268
+ if (L.evictedIdle !== before) {
13269
+ try {
13270
+ __sbRpc("do.release", {});
13271
+ } catch (error) {
13272
+ __sbLogUncaught("do.release", error);
13273
+ }
13274
+ }
13275
+ __sbDOServe(L);
13276
+ __sbDOArmSweep(L);
13277
+ __sbDONote(L, true);
13278
+ }
13279
+
13280
+ // ---- acquire: the record of an object, constructing it when it has none ----
13281
+ function __sbDOResolveId(cls, idText, idObj) {
13282
+ const stateId = idObj || (idText.indexOf("name:") === 0 ? __sbDONamedId(cls, idText.slice(5)) : __sbDOMakeId(idText, undefined));
13283
+ return stateId;
13284
+ }
13285
+
13286
+ // Returns the record, or a promise of one when the cap is full and nothing is idle.
13287
+ function __sbDOAcquire(cls, idText, idObj) {
13288
+ const L = __sbDOLife();
13289
+ // The common case: the caller's id text is the object's canonical id, so the record is one lookup away.
13290
+ const quick = L.recs[cls + " " + idText];
13291
+ if (quick !== undefined) return quick;
13292
+ if (!__sbDOClasses[cls]) throw new Error("no such Durable Object class: " + cls);
13293
+ const stateId = __sbDOResolveId(cls, idText, idObj);
13294
+ const id = stateId.toString();
13295
+ const key = cls + " " + id;
13296
+ const rec = L.recs[key];
13297
+ if (rec !== undefined) return rec;
13298
+ __sbDOSweep(L, 8, 4);
13299
+ if (L.count >= L.cap) {
13300
+ const victim = __sbDOStalest(L);
13301
+ if (victim === null) return __sbDOWait(L, cls, id, stateId, key);
13302
+ __sbDOEvict(victim, "cap", false);
13303
+ }
13304
+ return __sbDOBuild(L, cls, id, stateId, key);
13305
+ }
13306
+
13307
+ function __sbDOWait(L, cls, id, stateId, key) {
13308
+ L.waits++;
13309
+ return new Promise((resolve, reject) => {
13310
+ const w = { cls, id, stateId, key, resolve, reject, timer: 0, done: false, t0: Date.now() };
13311
+ w.timer = __sbSetTimeout(() => {
13312
+ if (w.done) return;
13313
+ w.done = true;
13314
+ const at = L.waiters.indexOf(w);
13315
+ if (at >= 0) L.waiters.splice(at, 1);
13316
+ L.overloaded++;
13317
+ reject(__sbDOOverloaded("too many objects are awake in this app"));
13318
+ }, L.waitMs);
13319
+ L.waiters.push(w);
13320
+ });
13321
+ }
13322
+
13323
+ // A slot or an idle object freed: serve the waiting first events in order.
13324
+ function __sbDOServe(L) {
13325
+ while (L.waiters.length > 0) {
13326
+ const w = L.waiters[0];
13327
+ let rec = L.recs[w.key];
13328
+ if (rec === undefined) {
13329
+ if (L.count >= L.cap) {
13330
+ const victim = __sbDOStalest(L);
13331
+ if (victim === null) return;
13332
+ __sbDOEvict(victim, "cap", false);
13333
+ }
13334
+ }
13335
+ L.waiters.shift();
13336
+ w.done = true;
13337
+ __sbClearTimeout(w.timer);
13338
+ const waited = Date.now() - w.t0;
13339
+ if (waited > L.waitMsMax) L.waitMsMax = waited;
13340
+ try {
13341
+ if (rec === undefined) rec = __sbDOBuild(L, w.cls, w.id, w.stateId, w.key);
13342
+ w.resolve(rec);
13343
+ } catch (error) {
13344
+ w.reject(error);
13345
+ }
13346
+ }
13347
+ }
13348
+
13349
+ // A clock finer than Date.now() for the wake counter (microseconds); the performance object is built on its first read.
13350
+ function __sbDONow() {
13351
+ const p = globalThis.performance;
13352
+ return p && __sbIsFn(p.now) ? p.now() : Date.now();
13353
+ }
13354
+
13355
+ function __sbDOBuild(L, cls, id, stateId, key) {
13356
+ const Ctor = __sbDOClasses[cls];
13357
+ const t0 = __sbDONow();
13358
+ if (stateId.name !== undefined) __sbDOAdopt(cls, "name:" + stateId.name, id);
13359
+ const tasks = [];
13360
+ const gate = __sbDOMakeGate();
13361
+ const rec = {
13362
+ cls,
13363
+ id,
13364
+ key,
13365
+ inst: null,
13366
+ state: null,
13367
+ gate,
13368
+ holder: null,
13369
+ tasks,
13370
+ busy: 0,
13371
+ pins: 0,
13372
+ pinSince: 0,
13373
+ pinLogged: false,
13374
+ last: Date.now(),
13375
+ usedSql: false,
13376
+ sqlFile: "",
13377
+ dead: false,
13378
+ registered: false,
13379
+ prev: null,
13380
+ next: null,
13381
+ frame: null,
13382
+ plain: [],
13383
+ tcp: [],
13384
+ deadError: null,
13385
+ };
13386
+ rec.frame = __sbFrameWith(undefined, __sbDOCtxKey, rec);
13387
+ const state = {
13388
+ id: stateId,
13389
+ storage: __sbDOAsyncStorage(cls, id, gate, rec),
13390
+ blockConcurrencyWhile(fn) {
13391
+ if (!__sbIsFn(fn))
13392
+ throw new TypeError("Failed to execute 'blockConcurrencyWhile' on 'DurableObjectState': parameter 1 is not of type 'function'.");
13393
+ if (rec.dead) throw __sbDOEvictedError();
13394
+ // No event enters until fn settles; a throw or 30 s without settling resets the object, as in workerd.
13395
+ gate.locks++;
13396
+ let settled = false;
13397
+ const done = () => {
13398
+ settled = true;
13399
+ __sbClearTimeout(timer);
13400
+ if (!rec.dead) {
13401
+ gate.locks--;
13402
+ __sbDOPump(gate);
13403
+ }
13404
+ };
13405
+ const timer = __sbSetTimeout(() => {
13406
+ if (settled) return;
13407
+ settled = true;
13408
+ console.error("blockConcurrencyWhile() did not settle in 30 seconds: the Durable Object " + cls + " was reset");
13409
+ __sbDOReset(rec, "bcw", new Error("Durable Object reset because blockConcurrencyWhile() did not settle in 30 seconds."));
13410
+ }, 30000);
13411
+ let r;
13412
+ try {
13413
+ r = fn();
13414
+ } catch (error) {
13415
+ done();
13416
+ __sbDOReset(rec, "bcw", new Error("Durable Object reset because blockConcurrencyWhile() threw: " + String((error && error.message) || error)));
13417
+ return Promise.reject(error);
13418
+ }
13419
+ return Promise.resolve(r).then(
13420
+ (v) => {
13421
+ done();
13422
+ return v;
13423
+ },
13424
+ (error) => {
13425
+ done();
13426
+ __sbDOReset(rec, "bcw", new Error("Durable Object reset because blockConcurrencyWhile() failed: " + String((error && error.message) || error)));
13427
+ throw error;
13428
+ },
13429
+ );
13430
+ },
13431
+ waitUntil(p) {
13432
+ if (rec.dead) throw __sbDOEvictedError();
13433
+ if (p && __sbIsFn(p.then)) tasks.push(p);
13434
+ },
13435
+ };
13436
+ state.props = {};
13437
+ state.abort = (reason) => {
13438
+ __sbDOReset(rec, "abort");
13439
+ throw new Error(reason == null ? "Durable Object was aborted." : String(reason));
13440
+ };
13441
+ const holder = { inst: null, rec, auto: null, cls, id, restored: [], sockets: [] };
13442
+ rec.holder = holder;
13443
+ rec.state = state;
13444
+ if (__sbWsEnabled()) __sbDOWebSocketState(state, holder);
13445
+ let inst;
13446
+ try {
13447
+ inst = new Ctor(state, __sbEnvValue());
13448
+ } catch (error) {
13449
+ rec.dead = true;
13450
+ throw error;
13451
+ }
13452
+ // blockConcurrencyWhile() reset the object while its constructor was still running (a synchronous throw).
13453
+ if (rec.dead) throw new Error("The Durable Object " + cls + " was reset while it was starting.");
13454
+ holder.inst = inst;
13455
+ inst.__sbTasks = tasks;
13456
+ inst.__sbGate = gate;
13457
+ rec.inst = inst;
13458
+ inst.__sbRec = rec;
13459
+ L.recs[key] = rec;
13460
+ rec.registered = true;
13461
+ __sbDOLink(L, rec);
13462
+ const wake = Math.round((__sbDONow() - t0) * 1000);
13463
+ L.wakeUsLast = wake;
13464
+ if (wake > L.wakeUsMax) L.wakeUsMax = wake;
13465
+ // Sockets restored from hibernation start delivering now that the object exists.
13466
+ const restored = holder.restored;
13467
+ holder.restored = [];
13468
+ for (let i = 0; i < restored.length; i++) __sbWsPump(restored[i]);
13469
+ __sbDOArmSweep(L);
13470
+ return rec;
13471
+ }
13472
+
13473
+ // ---- run an event: the one entry for RPC, stub.fetch, alarms and socket events ----
13474
+ // `run(inst)` is the event; it enters the object's gate and runs in a frame that names the object. The returned promise settles with
13475
+ // the event's value. The object counts as busy until the event and the drain of its waitUntil tasks have settled (`wait`: the
13476
+ // returned promise waits for that drain too).
13477
+ function __sbDORun(cls, idText, idObj, run, wait) {
13478
+ let acquired;
13479
+ try {
13480
+ acquired = __sbDOAcquire(cls, idText, idObj);
13481
+ } catch (error) {
13482
+ return Promise.reject(error);
13483
+ }
13484
+ if (__sbIsFn(acquired.then)) return acquired.then((rec) => __sbDOStart(rec, run, wait));
13485
+ return __sbDOStart(acquired, run, wait);
13486
+ }
13487
+
13488
+ function __sbDOStart(rec, run, wait) {
13489
+ const L = __sbDOL;
13490
+ if (rec.dead) return Promise.reject(rec.deadError !== null ? rec.deadError : __sbDOEvictedError());
13491
+ const gate = rec.gate;
13492
+ if (gate.waiting.length >= L.queueMax) {
13493
+ L.overloaded++;
13494
+ return Promise.reject(__sbDOOverloaded("too many events are queued for this object"));
13495
+ }
13496
+ rec.busy++;
13497
+ // The input gate, as __sbDOEnter has it: an open gate runs the event now, a shut one queues it behind the others.
13498
+ if (gate.locks === 0 && gate.waiting.length === 0) return __sbDOEvent(L, rec, run, wait);
13499
+ return new Promise((resolve) => {
13500
+ gate.waiting.push(() => {
13501
+ resolve(__sbDOEvent(L, rec, run, wait));
13502
+ });
13503
+ });
13504
+ }
13505
+
13506
+ // One event, in the object's frame; busy until it and the drain of its waitUntil tasks have settled. One coroutine and nothing else
13507
+ // per event: this path is paid by every call into every object.
13508
+ async function __sbDOEvent(L, rec, run, wait) {
13509
+ try {
13510
+ if (rec.dead) throw rec.deadError !== null ? rec.deadError : __sbDOEvictedError();
13511
+ return await __sbRunInFrame(rec.frame, run, [rec.inst]);
13512
+ } finally {
13513
+ const drained = __sbDOSettle(L, rec, wait);
13514
+ if (drained !== undefined) await drained;
13515
+ }
13516
+ }
13517
+
13518
+ // The event is over: drain its waitUntil tasks (it stays busy until they settle), then it may become idle.
13519
+ function __sbDOSettle(L, rec, wait) {
13520
+ if (rec.tasks.length > 0) {
13521
+ const finish = () => __sbDOIdle(L, rec);
13522
+ const drained = __sbDrainWaitUntil(rec.tasks);
13523
+ drained.then(finish, finish);
13524
+ return wait ? drained.then(undefined, () => {}) : undefined;
13525
+ }
13526
+ __sbDOIdle(L, rec);
13527
+ return undefined;
13528
+ }
13529
+ function __sbDOIdle(L, rec) {
13530
+ rec.busy--;
13531
+ if (rec.busy !== 0 || rec.dead) return;
13532
+ const now = Date.now();
13533
+ if (rec.pins === 0) __sbDOFresh(L, rec, now);
13534
+ if (L.waiters.length > 0) __sbDOServe(L);
13535
+ if (!L.sweepArmed) __sbDOArmSweep(L);
13536
+ if (now - L.noteAt >= 1000) __sbDONote(L, true);
13537
+ }
13538
+
13539
+ // ---- pins: a timer, an unawaited fetch and a streamed response keep their object in memory ----
13540
+ // The timer globals become wrappers once the first object is built. The prelude's own timers use __sbSetTimeout and the rest,
13541
+ // which are the runtime's, so a parked timer of ours never pins anything.
13542
+ function __sbDOWrapTimers() {
13543
+ const armed = new Map();
13544
+ const arm = (original, repeat) =>
13545
+ function (callback, delay, ...args) {
13546
+ const rec = __sbDOFrameRec();
13547
+ if (rec === undefined || rec.dead || !__sbIsFn(callback)) return original(callback, delay, ...args);
13548
+ const slot = { id: 0, rec };
13549
+ __sbDOPin(rec);
13550
+ const fired = repeat
13551
+ ? callback
13552
+ : function (...fireArgs) {
13553
+ armed.delete(slot.id);
13554
+ __sbDOUnpin(slot.rec);
13555
+ return callback.apply(undefined, fireArgs);
13556
+ };
13557
+ slot.id = Number(original(fired, delay, ...args));
13558
+ armed.set(slot.id, slot);
13559
+ return slot.id;
13560
+ };
13561
+ const clear = (original) =>
13562
+ function (id) {
13563
+ const key = Number(id);
13564
+ const slot = armed.get(key);
13565
+ if (slot !== undefined) {
13566
+ armed.delete(key);
13567
+ __sbDOUnpin(slot.rec);
13568
+ }
13569
+ return original(id);
13570
+ };
13571
+ globalThis.setTimeout = arm(__sbSetTimeout, false);
13572
+ globalThis.setInterval = arm(__sbSetInterval, true);
13573
+ globalThis.clearTimeout = clear(__sbClearTimeout);
13574
+ globalThis.clearInterval = clear(__sbClearInterval);
13575
+ }
13576
+
13577
+ // ---- counters: cheap, one host note a second at most, only when changed ----
13578
+ function __sbDOStats(L) {
13579
+ let sqlOpen = 0;
13580
+ try {
13581
+ sqlOpen = Number(__sbRpc("do.stats", {}).stats.doOpen) || 0;
13582
+ } catch {
13583
+ sqlOpen = 0;
13584
+ }
13585
+ return (
13586
+ "objects_awake=" + L.count +
13587
+ " objects_pinned=" + L.pinned +
13588
+ " do_sql_open=" + sqlOpen +
13589
+ " evicted_idle=" + L.evictedIdle +
13590
+ " evicted_cap=" + L.evictedCap +
13591
+ " evicted_abort=" + L.evictedAbort +
13592
+ " waits=" + L.waits +
13593
+ " wait_ms_max=" + L.waitMsMax +
13594
+ " overloaded=" + L.overloaded +
13595
+ " zombie_calls=" + L.zombie +
13596
+ " wake_us=" + L.wakeUsLast + "," + L.wakeUsMax
13597
+ );
13598
+ }
13599
+ function __sbDONote(L, force) {
13600
+ const now = Date.now();
13601
+ if (!force && now - L.noteAt < 1000) return;
13602
+ L.noteAt = now;
13603
+ const text = __sbDOStats(L);
13604
+ if (text === L.noteText) return;
13605
+ L.noteText = text;
13606
+ if (__sbHosted()) __sbHostNote("do " + text);
13607
+ if (__sbEnv("SB_DO_STATS") === "1") console.error("do " + text);
13608
+ }
13609
+
13610
+ // A revoked state method: the evicted (or reset) object's instance cannot touch storage any more.
13611
+ function __sbDOZombie(rec) {
13612
+ __sbDOL.zombie++;
13613
+ return rec.deadError !== null ? rec.deadError : __sbDOEvictedError();
13614
+ }
13615
+ function __sbDOFetchEvent(inst, req) {
13616
+ return __sbFetchWithDrain(inst.fetch(req), [], false, true);
13617
+ }
13618
+ function __sbWsRestorePark() {
13619
+ const timer = __sbSetTimeout(__sbWsRestoreLoop, 2147483647);
13620
+ __sbWsOp("R", 0, "", timer);
13621
+ }
13622
+ function __sbWsRestoreFailed(id, error) {
13623
+ __sbLogUncaught("websocket", error);
13624
+ if (!(error && error.overloaded === true)) return;
13625
+ const L = __sbDOL;
13626
+ const tries = (L.restoreTries[id] || 0) + 1;
13627
+ L.restoreTries[id] = tries;
13628
+ if (tries >= 3) {
13629
+ delete L.restoreTries[id];
13630
+ // Three waits for a slot and none came: try again later, said to the client.
13631
+ __sbWsOp("c", id, "", 1013);
13632
+ __sbWsOp("r", id, "", 0);
13633
+ return;
13634
+ }
13635
+ // Back to the host's restore list with its queued events: the next slot serves it.
13636
+ __sbWsOp("g", id, "", 0);
13637
+ }
13638
+
12819
13639
  function __sbDOStorage(cls, id) {
12820
13640
  return {
12821
13641
  get(key) {
@@ -12913,7 +13733,7 @@ function __sbDOPump(gate) {
12913
13733
  // Shut the gate until the next timer turn (see above).
12914
13734
  function __sbDOHold(gate) {
12915
13735
  gate.locks++;
12916
- setTimeout(function () {
13736
+ __sbSetTimeout(function () {
12917
13737
  gate.locks--;
12918
13738
  __sbDOPump(gate);
12919
13739
  }, 0);
@@ -12949,6 +13769,12 @@ function __sbDOEnter(gate, run) {
12949
13769
  // (kept on the instance, as __sbAlarmAt and __sbAlarmFails)
12950
13770
  function __sbDOAlarmCall(inst, cls, id, at, attempts) {
12951
13771
  if (__sbCompatDate() < "2026-11-04") return inst.alarm();
13772
+ // From the eviction date the count is the delivery row's (or the host's), never the instance's: the instance can be gone
13773
+ // between two runs of the same alarm.
13774
+ if (__sbDOEvictOn()) {
13775
+ const retries = attempts != null && Number(attempts) > 0 ? Number(attempts) : 0;
13776
+ return __sbDOAlarmRun(inst, { retryCount: retries, isRetry: retries > 0 });
13777
+ }
12952
13778
  const when = Number(at) || 0;
12953
13779
  if (inst.__sbAlarmAt !== when) {
12954
13780
  inst.__sbAlarmAt = when;
@@ -13259,7 +14085,8 @@ function __sbDOStrictStorage(cls, id) {
13259
14085
  setAlarm(when) {
13260
14086
  const at = when instanceof Date ? when.getTime() : when;
13261
14087
  if (__sbDOv3()) {
13262
- const inst = __sbDOInstances[cls + " " + id];
14088
+ const live = __sbDOL !== null ? __sbDOL.recs[cls + " " + id] : undefined;
14089
+ const inst = live !== undefined ? live.inst : __sbDOInstances[cls + " " + id];
13263
14090
  if (inst && !__sbIsFn(inst.alarm))
13264
14091
  throw new TypeError("Your Durable Object class must have an alarm() handler in order to call setAlarm()");
13265
14092
  if (when instanceof Date || Object(when) instanceof Number) {
@@ -13301,9 +14128,11 @@ function __sbDOStrictStorage(cls, id) {
13301
14128
  }
13302
14129
 
13303
14130
  // The 2026-10-28 storage: every method returns a Promise; reads shut the input gate.
13304
- function __sbDOAsyncStorage(cls, id, gate) {
14131
+ function __sbDOAsyncStorage(cls, id, gate, rec) {
13305
14132
  const s = __sbDOStrictStorage(cls, id);
14133
+ // rec is the record of an object that may be evicted (2026-11-21): once it is dead every storage call fails with the evicted error.
13306
14134
  const settle = (f) => {
14135
+ if (rec !== undefined && rec.dead) return Promise.reject(__sbDOZombie(rec));
13307
14136
  try {
13308
14137
  return Promise.resolve(f());
13309
14138
  } catch (error) {
@@ -13311,9 +14140,13 @@ function __sbDOAsyncStorage(cls, id, gate) {
13311
14140
  }
13312
14141
  };
13313
14142
  const read = (f) => {
14143
+ if (rec !== undefined && rec.dead) return Promise.reject(__sbDOZombie(rec));
13314
14144
  __sbDOHold(gate);
13315
14145
  return settle(f);
13316
14146
  };
14147
+ const alive = () => {
14148
+ if (rec !== undefined && rec.dead) throw __sbDOZombie(rec);
14149
+ };
13317
14150
  const storage = {
13318
14151
  get: (key) => read(() => s.get(key)),
13319
14152
  list: (options) => read(() => s.list(options)),
@@ -13324,10 +14157,13 @@ function __sbDOAsyncStorage(cls, id, gate) {
13324
14157
  setAlarm: (when) => settle(() => s.setAlarm(when)),
13325
14158
  deleteAlarm: () => settle(() => s.deleteAlarm()),
13326
14159
  sync: () => Promise.resolve(),
13327
- transaction: (fn) => __sbDOTransaction(cls, id, s, gate, fn),
14160
+ transaction: (fn) => {
14161
+ if (rec !== undefined && rec.dead) return Promise.reject(__sbDOZombie(rec));
14162
+ return __sbDOTransaction(cls, id, s, gate, fn);
14163
+ },
13328
14164
  };
13329
14165
  if (__sbDOv2()) {
13330
- const sql = __sbDOSql(cls, id);
14166
+ const sql = __sbDOSql(cls, id, rec);
13331
14167
  storage.sql = sql;
13332
14168
  storage.transactionSync = (fn) => sql.__sbTransaction(fn);
13333
14169
  if (__sbDOv3()) {
@@ -13339,12 +14175,22 @@ function __sbDOAsyncStorage(cls, id, gate) {
13339
14175
  });
13340
14176
  // The synchronous key-value API, over the same keys as get() and put().
13341
14177
  storage.kv = {
13342
- get: (key) => s.get(key),
14178
+ get: (key) => {
14179
+ alive();
14180
+ return s.get(key);
14181
+ },
13343
14182
  put: (key, value) => {
14183
+ alive();
13344
14184
  s.put(key, value);
13345
14185
  },
13346
- delete: (key) => s.delete(key),
13347
- list: (options) => s.list(options).entries(),
14186
+ delete: (key) => {
14187
+ alive();
14188
+ return s.delete(key);
14189
+ },
14190
+ list: (options) => {
14191
+ alive();
14192
+ return s.list(options).entries();
14193
+ },
13348
14194
  };
13349
14195
  // Point-in-time recovery needs the storage engine's log, which this one does not keep.
13350
14196
  const pitr = () => Promise.reject(new Error("Point-in-time recovery is not supported by this Durable Object storage."));
@@ -13519,9 +14365,14 @@ function __sbDOSqlPolicy(text) {
13519
14365
  }
13520
14366
  }
13521
14367
 
13522
- function __sbDOSql(cls, id) {
14368
+ function __sbDOSql(cls, id, rec) {
13523
14369
  const file = __sbDOIsHex64(id) ? id.toLowerCase() : __sbDOSha("sbdo-file:" + id);
14370
+ if (rec !== undefined) rec.sqlFile = file;
13524
14371
  const run = (sql, params) => {
14372
+ if (rec !== undefined) {
14373
+ if (rec.dead) throw __sbDOZombie(rec);
14374
+ rec.usedSql = true;
14375
+ }
13525
14376
  try {
13526
14377
  return __sbRpc("do.sql", { cls, file, sql, params });
13527
14378
  } catch (err) {
@@ -14074,10 +14925,10 @@ async function __sbDrainWaitUntil(tasks, logRec) {
14074
14925
  // busy for the full 25 s (sproutd will not reap a worker with timers left, B22).
14075
14926
  let timer;
14076
14927
  const cap = new Promise((resolve) => {
14077
- timer = setTimeout(resolve, __SB_WAITUNTIL_TIMEOUT_MS);
14928
+ timer = __sbSetTimeout(resolve, __SB_WAITUNTIL_TIMEOUT_MS);
14078
14929
  });
14079
14930
  await Promise.race([__sbDrainAll(pending, logRec), cap]);
14080
- clearTimeout(timer);
14931
+ __sbClearTimeout(timer);
14081
14932
  }
14082
14933
 
14083
14934
  async function __sbDrainAll(pending, logRec) {
@@ -14199,7 +15050,7 @@ const __SB_STREAM_WINDOW = 1024 * 1024;
14199
15050
  // Resolves when the host says the stream's unsent bytes fell (or the client went away).
14200
15051
  function __sbStreamDrained(id) {
14201
15052
  return new Promise((resolve) => {
14202
- const timer = setTimeout(resolve, 2147483647);
15053
+ const timer = __sbSetTimeout(resolve, 2147483647);
14203
15054
  __sbStreamOp("k", id, timer);
14204
15055
  });
14205
15056
  }
@@ -14210,6 +15061,14 @@ function __sbStreamResponse(response) {
14210
15061
  response.headers._immutable = false;
14211
15062
  response.headers.set("x-sb-stream", id);
14212
15063
  response.body = "";
15064
+ // A streamed response of a Durable Object keeps its object busy until the stream ends (this runs in the object's frame).
15065
+ if (__sbDOL !== null) {
15066
+ const owner = __sbDOFrameRec();
15067
+ if (owner !== undefined && !owner.dead) {
15068
+ __sbDOPin(owner);
15069
+ response._sbDOPin = owner;
15070
+ }
15071
+ }
14213
15072
  (async () => {
14214
15073
  const reader = stream.getReader();
14215
15074
  try {
@@ -14244,6 +15103,7 @@ function __sbStreamResponse(response) {
14244
15103
  __sbLogUncaught("fetch", error);
14245
15104
  }
14246
15105
  __sbStreamOp("e", id, "");
15106
+ if (response._sbDOPin !== undefined) __sbDOUnpin(response._sbDOPin);
14247
15107
  })();
14248
15108
  return response;
14249
15109
  }
@@ -14386,6 +15246,10 @@ class __SbWebSocket {
14386
15246
  if (this._hib !== null) throw new TypeError("Can't accept() a WebSocket that was accepted with acceptWebSocket().");
14387
15247
  if (this._accepted) return;
14388
15248
  this._accepted = true;
15249
+ if (__sbDOL !== null) {
15250
+ const rec = __sbDOFrameRec();
15251
+ if (rec !== undefined && !rec.dead) rec.plain.push(this);
15252
+ }
14389
15253
  __sbWsSchedule(this);
14390
15254
  }
14391
15255
  send(message) {
@@ -14557,6 +15421,16 @@ function __sbWsDispatch(ws, event) {
14557
15421
 
14558
15422
  // A hibernatable socket's event runs the object's handler through its input gate.
14559
15423
  function __sbWsHibernated(ws, name, args) {
15424
+ const rec = ws._hib.holder.rec;
15425
+ if (rec !== undefined && rec !== null) {
15426
+ if (rec.dead || rec.inst === null || !__sbIsFn(rec.inst[name])) return;
15427
+ __sbDOStart(
15428
+ rec,
15429
+ (inst) => (name === "webSocketClose" ? inst.webSocketClose(args[0], args[1], args[2], args[3]) : inst.webSocketMessage(args[0], args[1])),
15430
+ false,
15431
+ ).then(undefined, (error) => __sbLogUncaught("websocket", error));
15432
+ return;
15433
+ }
14560
15434
  const inst = ws._hib.holder.inst;
14561
15435
  if (inst == null || !__sbIsFn(inst[name])) return;
14562
15436
  const run = () => {
@@ -14577,12 +15451,14 @@ function __sbWsHibernated(ws, name, args) {
14577
15451
  // state from 2026-10-30. `holder.inst` is the object once it exists.
14578
15452
  function __sbDOWebSocketState(state, holder) {
14579
15453
  const sockets = [];
15454
+ holder.sockets = sockets;
14580
15455
  const live = () => {
14581
15456
  for (let i = sockets.length - 1; i >= 0; i--) if (sockets[i]._state === __SB_WS_CLOSED) sockets.splice(i, 1);
14582
15457
  return sockets;
14583
15458
  };
14584
15459
  __sbWsHibRestore(holder, sockets);
14585
15460
  state.acceptWebSocket = function (ws, tags) {
15461
+ if (holder.rec !== undefined && holder.rec !== null && holder.rec.dead) throw __sbDOZombie(holder.rec);
14586
15462
  if (!(ws instanceof __SbWebSocket)) throw new TypeError("acceptWebSocket needs a WebSocket");
14587
15463
  if (ws._accepted || ws._hib !== null || ws._host !== 0) throw new TypeError("This WebSocket was already accepted.");
14588
15464
  const list = [];
@@ -14657,7 +15533,8 @@ function __sbWsPump(ws) {
14657
15533
  for (;;) {
14658
15534
  const head = __sbWsOp("p", id, "", 0);
14659
15535
  if (head === "") {
14660
- const timer = setTimeout(() => __sbWsPump(ws), 2147483647);
15536
+ const timer = __sbSetTimeout(() => __sbWsPump(ws), 2147483647);
15537
+ ws._pumpTimer = timer;
14661
15538
  // From 2026-11-01 a parked socket is not background work (z): its app may be reaped.
14662
15539
  __sbWsOp(__sbWsHibOn() ? "z" : "w", id, "", timer);
14663
15540
  return;
@@ -14746,9 +15623,9 @@ function __sbTcpPump(st) {
14746
15623
  if (head === "") {
14747
15624
  if (st.id === 0) return;
14748
15625
  if (__sbWsCanWake()) {
14749
- st.timer = setTimeout(() => __sbTcpPump(st), 2147483647);
15626
+ st.timer = __sbSetTimeout(() => __sbTcpPump(st), 2147483647);
14750
15627
  __sbWsOp("w", st.id, "", st.timer);
14751
- } else st.timer = setTimeout(() => __sbTcpPump(st), 10);
15628
+ } else st.timer = __sbSetTimeout(() => __sbTcpPump(st), 10);
14752
15629
  return;
14753
15630
  }
14754
15631
  const parts = head.split(" ");
@@ -14813,7 +15690,7 @@ function __sbTcpRelease(st) {
14813
15690
  const id = st.id;
14814
15691
  if (id === 0) return;
14815
15692
  st.id = 0;
14816
- if (st.timer !== undefined) clearTimeout(st.timer);
15693
+ if (st.timer !== undefined) __sbClearTimeout(st.timer);
14817
15694
  __sbWsOp("r", id, "", 0);
14818
15695
  __sbTcpOpen--;
14819
15696
  }
@@ -14961,7 +15838,7 @@ function __sbTcpFacade(st, mode, upgrade) {
14961
15838
  // the old socket ends once the upgrade is under way (workerd resolves its `closed`)
14962
15839
  const oldResolve = st.closedResolve;
14963
15840
  const next = __sbTcpFacade(st, "on", upgrade);
14964
- setTimeout(() => oldResolve(undefined), 0);
15841
+ __sbSetTimeout(() => oldResolve(undefined), 0);
14965
15842
  return next;
14966
15843
  },
14967
15844
  };
@@ -14993,7 +15870,7 @@ function __sbConnect(address, options) {
14993
15870
  if (target.port === 25) {
14994
15871
  st.failed = new Error("proxy request failed, cannot connect to the specified address");
14995
15872
  st.eof = true;
14996
- setTimeout(() => __sbTcpSettle(st), 0);
15873
+ __sbSetTimeout(() => __sbTcpSettle(st), 0);
14997
15874
  return socket;
14998
15875
  }
14999
15876
  const spec = target.host + "\n" + target.port + "\n" + (mode === "on" ? "1" : mode === "starttls" ? "2" : "0") + "\n" + target.host;
@@ -15001,11 +15878,15 @@ function __sbConnect(address, options) {
15001
15878
  if (!(id > 0)) {
15002
15879
  st.failed = new Error(__sbTcpOpen >= 16 ? "Too many open connections" : "TCP sockets are not available on this host");
15003
15880
  st.eof = true;
15004
- setTimeout(() => __sbTcpSettle(st), 0);
15881
+ __sbSetTimeout(() => __sbTcpSettle(st), 0);
15005
15882
  return socket;
15006
15883
  }
15007
15884
  st.id = id;
15008
15885
  __sbTcpOpen++;
15886
+ if (__sbDOL !== null) {
15887
+ const rec = __sbDOFrameRec();
15888
+ if (rec !== undefined && !rec.dead) rec.tcp.push(st);
15889
+ }
15009
15890
  __sbTcpPump(st);
15010
15891
  return socket;
15011
15892
  }
@@ -15082,10 +15963,14 @@ function __sbWsConnectOutbound(ws, input, protocols) {
15082
15963
  ws._accepted = true;
15083
15964
  ws._state = 0;
15084
15965
  ws.url = target.url;
15966
+ if (__sbDOL !== null) {
15967
+ const rec = __sbDOFrameRec();
15968
+ if (rec !== undefined && !rec.dead) rec.plain.push(ws);
15969
+ }
15085
15970
  const id = __sbWsOpen(target, list, null);
15086
15971
  if (!(id > 0)) {
15087
15972
  ws._state = __SB_WS_CLOSED;
15088
- setTimeout(() => {
15973
+ __sbSetTimeout(() => {
15089
15974
  __sbWsFire(ws, { type: "error", target: ws });
15090
15975
  __sbWsFire(ws, { type: "close", code: 1006, reason: "", wasClean: false, target: ws });
15091
15976
  }, 0);
@@ -15104,9 +15989,9 @@ function __sbWsPumpOut(ws) {
15104
15989
  const head = __sbWsOp("p", id, "", 0);
15105
15990
  if (head === "") {
15106
15991
  if (__sbWsCanWake()) {
15107
- const timer = setTimeout(() => __sbWsPumpOut(ws), 2147483647);
15992
+ const timer = __sbSetTimeout(() => __sbWsPumpOut(ws), 2147483647);
15108
15993
  __sbWsOp("w", id, "", timer);
15109
- } else setTimeout(() => __sbWsPumpOut(ws), 15);
15994
+ } else __sbSetTimeout(() => __sbWsPumpOut(ws), 15);
15110
15995
  return;
15111
15996
  }
15112
15997
  const parts = head.split(" ");
@@ -15212,7 +16097,7 @@ function __sbWsOutClose(ws, code, text) {
15212
16097
  const pending = ws._pending;
15213
16098
  ws._pending = null;
15214
16099
  if (pending) pending.reject(new TypeError("WebSocket connection was closed before it opened"));
15215
- else setTimeout(() => {
16100
+ else __sbSetTimeout(() => {
15216
16101
  __sbWsFire(ws, { type: "error", target: ws });
15217
16102
  __sbWsFire(ws, { type: "close", code: 1006, reason: "", wasClean: false, target: ws });
15218
16103
  }, 0);
@@ -15221,7 +16106,7 @@ function __sbWsOutClose(ws, code, text) {
15221
16106
  ws._state = __SB_WS_CLOSING;
15222
16107
  __sbWsOp("c", id, text, code === undefined ? 1000 : code);
15223
16108
  // A peer that never answers the close is dropped after a while.
15224
- setTimeout(() => {
16109
+ __sbSetTimeout(() => {
15225
16110
  if (ws._host !== id) return;
15226
16111
  ws._host = 0;
15227
16112
  __sbWsOp("r", id, "", 0);
@@ -15414,8 +16299,7 @@ function __sbWsRestoreLoop() {
15414
16299
  __sbLogUncaught("websocket", error);
15415
16300
  }
15416
16301
  }
15417
- const timer = setTimeout(__sbWsRestoreLoop, 2147483647);
15418
- __sbWsOp("R", 0, "", timer);
16302
+ __sbWsRestorePark();
15419
16303
  }
15420
16304
  function __sbWsRestoreSocket(id) {
15421
16305
  const r = __sbRpc("do.storage.get", { cls: "__sbws", id: "index", key: "s" + id });
@@ -15434,6 +16318,10 @@ function __sbWsRestoreSocket(id) {
15434
16318
  return;
15435
16319
  }
15436
16320
  // Constructing the object restores its sockets (__sbWsHibRestore) and starts their pumps.
16321
+ if (__sbDOEvictOn()) {
16322
+ __sbDORun(target[0], target[1], undefined, () => undefined, false).then(undefined, (error) => __sbWsRestoreFailed(id, error));
16323
+ return;
16324
+ }
15437
16325
  __sbGetDOInstance(target[0], target[1]);
15438
16326
  }
15439
16327
 
@@ -15869,7 +16757,7 @@ function __sbIngressStream(request, id) {
15869
16757
  const state = Number(__sbReqBodyOp("p", id, ""));
15870
16758
  if (state === 0) {
15871
16759
  // Nothing yet: the server fires this timer when bytes, the end or an error arrive.
15872
- const timer = setTimeout(() => step(controller, resolve), 2147483647);
16760
+ const timer = __sbSetTimeout(() => step(controller, resolve), 2147483647);
15873
16761
  __sbReqBodyOp("w", id, timer);
15874
16762
  return;
15875
16763
  }
@@ -16015,7 +16903,7 @@ function __sbR2TransferFailed(status, text) {
16015
16903
  // Resolves when the streamed request body `body` has more (or ended or failed): the host fires the parked timer.
16016
16904
  function __sbR2BodyWait(body) {
16017
16905
  return new Promise((resolve) => {
16018
- const timer = setTimeout(resolve, 2147483647);
16906
+ const timer = __sbSetTimeout(resolve, 2147483647);
16019
16907
  __sbReqBodyOp("w", body, timer);
16020
16908
  });
16021
16909
  }
@@ -16267,6 +17155,22 @@ globalThis.__sbEntry = function (handlers, request) {
16267
17155
  const body = __sbReadJson(request);
16268
17156
  const __acls = String(body.cls || "");
16269
17157
  const __aid = String(body.id || "");
17158
+ if (__sbDOEvictOn()) {
17159
+ const none = {};
17160
+ return __sbDORun(
17161
+ __acls,
17162
+ __aid,
17163
+ undefined,
17164
+ (instance) => (__sbIsFn(instance.alarm) ? __sbDOAlarmCall(instance, __acls, __aid, body.scheduledTime, body.attempts) : none),
17165
+ true,
17166
+ ).then(
17167
+ (value) => (value === none ? new Response("no alarm handler", { status: 404 }) : new Response(null, { status: 204 })),
17168
+ (error) => {
17169
+ __sbLogUncaught("alarm", error);
17170
+ return error && error.overloaded === true ? new Response("alarm deferred: too many objects are awake", { status: 503 }) : new Response("alarm failed", { status: 500 });
17171
+ },
17172
+ );
17173
+ }
16270
17174
  const inst = __sbGetDOInstance(__acls, __aid);
16271
17175
  if (!__sbIsFn(inst.alarm)) return new Response("no alarm handler", { status: 404 });
16272
17176
  const __retries = __sbCompatDate() >= "2026-11-04";