@mcp-b/do-runtime 0.3.4 → 0.3.5

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.
@@ -504,7 +504,6 @@ function requireRange(actorId, column, value, max) {
504
504
  /** ← `ensureInitialized` (`alarm-scheduler.c++:50-60`). */
505
505
  function ensureInitialized(db) {
506
506
  hasCurrentSqliteTable(db, "_cf_ALARM", STMT.createTable);
507
- db.run("PRAGMA journal_mode=WAL");
508
507
  db.run(STMT.createTable);
509
508
  }
510
509
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"alarm-scheduler.js","names":["#timer","#random","#getActor","#projectWake","#db","#alarms","#tasks","#loadAlarmsFromDb","#projectNextWake","#scheduleAlarm","#replace","#projection","#taskFailure","#recoverInterruptedDelivery","#jitterMsForDelay","#makeAlarmTask","#checkTimestamp","#runAlarm","#runAlarmGuarded","#taskFailed","#addTask","#abandon"],"sources":["../../src/server/alarm-scheduler.ts"],"sourcesContent":["/**\n * ← workerd `src/workerd/server/alarm-scheduler.{h,c++}`\n *\n * Delivery and retry: the `_cf_ALARM` table, the watchdog arming, the queued\n * alarm, and the retry ladder. Measured on real workerd: an alarm re-armed for\n * `Date.now()` from inside a running handler does NOT re-enter — delivery is\n * serialised (`enter:1, exit:1, enter:2, exit:2`). That property is load\n * bearing; `_cf_executingScheduleRowId` upstream is safe only because of it\n * (§2.3). Here it falls out of the queued alarm: a `setAlarm` that arrives\n * while a handler runs is stored on the entry and started only after the run\n * finishes (`alarm-scheduler.c++:116-124`, `:220-227`).\n *\n * **This is runtime-internal, and it is what a host puts behind\n * `ActorPorts.alarms`.** Upstream wires it the same way: `ActorSqliteHooks`\n * (`server.c++:3199-3219`) is a three-line adapter whose `scheduleRun` is\n * `setAlarm`/`deleteAlarm` on the scheduler, and the scheduler is built once per\n * namespace (`server.c++:2325-2350`) rather than once per actor. `hooks(actorId)`\n * below is that adapter, so a host composes the two instead of writing its own\n * ladder.\n *\n * **One deliberate divergence, and it is the table's shape.** Upstream keeps the\n * retry ladder in memory and reloads every alarm with its counters at zero,\n * which is right for a process that lives for hours and is a regression on a\n * service worker Chrome evicts after seconds — see `_cf_ALARM` below and the\n * README's divergence table. Everything else here is upstream's, line for line.\n *\n * Spec: §1.8, §2.6, decisions 6, 11 and 16 in\n * docs/decisions.md.\n */\n\nimport type { AlarmOutlet } from \"../io/actor-sqlite\";\nimport type { Timer } from \"../io/io-context\";\nimport {\n getInt64,\n getText,\n hasCurrentSqliteTable,\n isNull,\n type SqlDatabase,\n SqliteDatabase,\n} from \"../util/sqlite\";\n\n// =======================================================================================\n// Constants\n\n/**\n * ← `WorkerInterface::ALARM_RETRY_START_SECONDS` (`io/worker-interface.h:130`),\n * re-declared as `AlarmScheduler::RETRY_START_SECONDS` (`alarm-scheduler.h:42`).\n *\n * \"not a duration so we can left shift it\" — upstream's own comment, and the\n * reason the ladder below is a shift rather than a table.\n */\nexport const ALARM_RETRY_START_SECONDS = 2;\n\n/**\n * ← `WorkerInterface::ALARM_RETRY_MAX_TRIES` (`io/worker-interface.h:131`) /\n * `AlarmScheduler::RETRY_MAX_TRIES` (`alarm-scheduler.h:45`).\n *\n * \"Max number of 'valid' retry attempts, i.e the worker returned an error.\"\n * It bounds `countedRetry`, NOT `backoff`: a run of failures that do not count\n * against the limit is retried forever, and its delay is bounded by\n * `RETRY_BACKOFF_MAX` instead.\n */\nexport const ALARM_RETRY_MAX_TRIES = 6;\n\n/**\n * ← `AlarmScheduler::RETRY_BACKOFF_MAX` (`alarm-scheduler.h:50`).\n *\n * \"Bound for exponential backoff when RETRY_MAX_TRIES is exceeded due to\n * internal errors. 2 << 9 is 1024 seconds, about 17 minutes. Total time spent in\n * retries once the backoff limit is reached is over 30 minutes.\"\n */\nexport const RETRY_BACKOFF_MAX = 9;\n\n/**\n * ← `AlarmScheduler::RETRY_JITTER_FACTOR` (`alarm-scheduler.h:54`).\n *\n * \"How much jitter should be applied to retry times to avoid bundled retries\n * overloading some common dependency between a set of failed alarms.\"\n */\nexport const RETRY_JITTER_FACTOR = 0.25;\n\n/**\n * ← `(AlarmScheduler::RETRY_START_SECONDS << backoff) * kj::SECONDS`\n * (`alarm-scheduler.c++:270`), before jitter.\n *\n * It refuses a backoff outside `[0, RETRY_BACKOFF_MAX]` rather than shifting it,\n * because JS's `<<` is a 32-bit operator: an unclamped counter would wrap to a\n * zero or negative delay — a hot retry loop — instead of saturating. The ladder\n * clamps immediately above its own call, exactly where upstream does; this is\n * what makes that clamp load bearing rather than decorative.\n */\nexport function alarmRetryDelayMs(backoff: number): number {\n if (!Number.isInteger(backoff) || backoff < 0 || backoff > RETRY_BACKOFF_MAX) {\n throw new Error(`Alarm retry backoff ${backoff} is outside [0, ${RETRY_BACKOFF_MAX}].`);\n }\n return (ALARM_RETRY_START_SECONDS << backoff) * 1_000;\n}\n\n/**\n * ← `_cf_ALARM` (`alarm-scheduler.c++:54-59`), plus the two prepared statements\n * (`alarm-scheduler.h:117-123`).\n *\n * Prepared statements are not part of the backend seam, so they survive as SQL\n * text under upstream's own member names — the treatment `util/sqlite-kv.ts`'s\n * `STMT` already records.\n *\n * `scheduled_time` holds **milliseconds** where upstream holds nanoseconds\n * (`:72`, `:102`). Same reason as `_cf_METADATA`'s alarm column: a JS number\n * runs out of integer precision 104 days into the epoch at nanosecond scale, so\n * storing what upstream stores would silently round every alarm.\n *\n * **Five columns upstream does not have, and they are this section's\n * divergence.** Upstream stores `(actor_id, scheduled_time)` and keeps the whole\n * retry ladder — `backoff`, `countedRetry`, `previousRetryCountedAgainstLimit`\n * and the fact that a delivery is in flight — in the `ScheduledAlarm` struct,\n * because a workerd process lives for hours and `loadAlarmsFromDb` runs once.\n * An MV3 service worker is evicted after seconds, so a scheduler rebuilt per\n * worker lifetime never accumulates any of them: `countedRetry` cannot reach\n * `ALARM_RETRY_MAX_TRIES`, so `#abandon` is unreachable and a permanently\n * failing alarm is never given up on, and `backoff` never leaves its first rung,\n * so that alarm wakes the browser every two seconds forever. Persisting the four\n * makes the ladder a property of the alarm rather than of the process that\n * happened to be running when it failed. See the README's divergence table.\n *\n * `retry_time` is the wake, which upstream never needs to store: its retries\n * live in the timer and the row's `scheduled_time` — which stays the alarm's own\n * time, because that is the identity `deliverAlarm` and `abandonAlarm` are told\n * — is by then in the past. Reloading without it re-arms every pending retry for\n * immediately, so the counters would climb while the delay was ignored.\n *\n * `running` is written before a delivery and cleared by whatever the delivery\n * turns into. A row still marked running at load is a delivery no result ever\n * came back from, which on this substrate is the ordinary case rather than a\n * crash: Chrome evicted the worker mid-alarm. Without it that alarm is\n * redelivered on every restart with no counter moved and no delay applied, which\n * is the hot loop above in its worst form, because nothing about it is bounded.\n */\nconst STMT = {\n createTable: `\n CREATE TABLE IF NOT EXISTS _cf_ALARM (\n actor_id TEXT PRIMARY KEY,\n scheduled_time INTEGER,\n retry_time INTEGER,\n backoff INTEGER NOT NULL,\n counted_retry INTEGER NOT NULL,\n previous_retry_counted INTEGER NOT NULL,\n running INTEGER NOT NULL\n ) WITHOUT ROWID\n `,\n loadAlarms: `\n SELECT actor_id, scheduled_time, retry_time, backoff, counted_retry,\n previous_retry_counted, running\n FROM _cf_ALARM\n `,\n // A new alarm time is new work, so it carries a fresh retry budget — which is\n // upstream's behaviour too, since every path that changes an entry's scheduled\n // time replaces the whole `ScheduledAlarm` and zeroes its counters with it.\n // `running` is deliberately NOT in the SET list: a delivery that is in flight\n // is still in flight, and clearing the mark here would erase the only evidence\n // that it never finished.\n setAlarm: `\n INSERT INTO _cf_ALARM VALUES(?, ?, NULL, 0, 0, 0, 0)\n ON CONFLICT DO UPDATE SET\n scheduled_time = excluded.scheduled_time,\n retry_time = NULL,\n backoff = 0,\n counted_retry = 0,\n previous_retry_counted = 0\n `,\n markRunning: `\n UPDATE _cf_ALARM SET running = 1 WHERE actor_id = ?\n `,\n // One statement, because the retry state and the end of the delivery have to\n // land together: a restart between them would resume the alarm with a stale\n // wake and a ladder one rung behind.\n saveRetry: `\n UPDATE _cf_ALARM\n SET retry_time = ?, backoff = ?, counted_retry = ?,\n previous_retry_counted = ?, running = 0\n WHERE actor_id = ?\n `,\n clearRunning: `\n UPDATE _cf_ALARM SET running = 0 WHERE actor_id = ?\n `,\n deleteAlarm: `\n DELETE FROM _cf_ALARM WHERE actor_id = ?\n `,\n deleteAll: `\n DELETE FROM _cf_ALARM\n `,\n} as const;\n\n// =======================================================================================\n// The result a delivery reports back\n\n/**\n * ← `EventOutcome` (`io/outcome.capnp`), restricted to the values the alarm path\n * can produce.\n *\n * The whole enum is a metrics type with no port — divergence 154 records that\n * for `waitUntilStatus()` — but `runAlarm` reads one bit of it\n * (`alarm-scheduler.c++:162`, `result.outcome != EventOutcome::OK`), so the\n * values `ServiceWorkerGlobalScope::runAlarm` actually returns are named here\n * and the rest are not.\n */\nexport type EventOutcome =\n | \"ok\"\n | \"canceled\"\n | \"script-not-found\"\n | \"exception\"\n | \"exceeded-cpu\"\n | \"unknown\";\n\n/**\n * ← `WorkerInterface::AlarmResult` (`io/worker-interface.h:71-81`).\n *\n * Upstream defaults all three fields; here every one is required, because the\n * producer is `server/actor-container.ts` rather than a capnp wire default and a\n * silently-defaulted `retryCountsAgainstLimit` is the difference between an\n * alarm that survives a broken actor and one that is abandoned.\n */\nexport type AlarmResult = {\n readonly retry: boolean;\n readonly retryCountsAgainstLimit: boolean;\n readonly outcome: EventOutcome;\n readonly errorDescription?: string;\n};\n\n/**\n * ← the `WorkerInterface` `GetActorFn` hands back\n * (`alarm-scheduler.c++:160`, `:244`), restricted to its two alarm members.\n *\n * `WorkerInterface` itself has no port — it is capnp dispatch, and divergence\n * 176 records it collapsing into the stub the transport returns — so the seam is\n * the two methods the scheduler calls. `ActorContainer` satisfies it\n * structurally; `deliverAlarm` is upstream's `runAlarm` under the name Section\n * 6b already gave it.\n */\nexport interface AlarmTarget {\n deliverAlarm(scheduledTime: number, retryCount: number): Promise<AlarmResult>;\n /**\n * ← `WorkerInterface::abandonAlarm` (`io/worker-interface.h:114`): \"Returns the\n * actor's stored alarm time if it differs from scheduledTime (i.e. the user\n * set a new alarm), or null if the alarm was cleared or no alarm was stored.\"\n */\n abandonAlarm(scheduledTime: number): Promise<number | null>;\n}\n\n/** ← `AlarmScheduler::GetActorFn` (`alarm-scheduler.h:56`). */\nexport type GetActorFn = (actorId: string) => AlarmTarget;\n\nexport type AlarmSchedulerOptions = {\n /**\n * ← the `const kj::Clock&` and the `kj::Timer&` upstream takes separately\n * (`alarm-scheduler.h:58-59`). One object here because `Timer.now()` is\n * already wall-clock milliseconds — `IoContext::now()` reads the same one —\n * so nothing distinguishes the two. `checkTimestamp`'s re-check loop stays,\n * because a JS timer really can fire a fraction of a millisecond early\n * relative to the clock it is compared against.\n */\n timer: Timer;\n /**\n * The database `_cf_ALARM` lives in — upstream's `metadata.sqlite`, one per\n * namespace beside the per-actor files (`server.c++:2336-2346`).\n *\n * Already open, where upstream's constructor opens it from a vfs and a path:\n * `SqlDatabaseProvider.open` is asynchronous and a constructor cannot await,\n * which is the same reason `createActorContainer` is a promise.\n */\n db: SqlDatabase;\n getActor: GetActorFn;\n /**\n * Browser hosts can mirror the earliest durable wake onto a platform watchdog\n * such as `chrome.alarms`. Workerd needs no such seam because its process owns\n * the scheduler timer.\n */\n projectWake?: (scheduledTime: number | null) => Promise<void> | void;\n /**\n * ← `std::default_random_engine`, seeded from the monotonic clock\n * (`alarm-scheduler.c++:20-27`). A test seam on a runtime-internal class, not\n * a substrate port: the jitter is the one part of the ladder that is\n * deliberately not a function of its inputs.\n */\n random?: () => number;\n};\n\n// =======================================================================================\n// The scheduler\n\n/** ← `AlarmScheduler::AlarmStatus` (`alarm-scheduler.h:72`). */\ntype AlarmStatus = \"WAITING\" | \"STARTED\" | \"FINISHED\";\n\n/** ← `AlarmScheduler::ScheduledAlarm` (`alarm-scheduler.h:80-99`). */\ntype ScheduledAlarm = {\n readonly actorId: string;\n readonly scheduledTime: number;\n /** The timer's actual wake, including persisted retry delay; null while running. */\n wakeTime: number | null;\n /**\n * ← `kj::Promise<void> task`. It exists upstream so the entry OWNS the task and\n * destroying the entry cancels it; JS has no such destruction, so the two\n * halves of that are explicit here — `cancel` stops the pending wake, and every\n * resumption re-reads the map to see whether it is still the live entry.\n */\n task: Promise<void> | undefined;\n /** The half of kj's cancel-by-drop that stops the timer. Divergence 147's shape. */\n readonly cancel: AbortController;\n /** Once started, an alarm can have a single alarm queued behind it. */\n queuedAlarm: number | null;\n status: AlarmStatus;\n previousRetryCountedAgainstLimit: boolean;\n /**\n * Counter for calculating backoff -- separate from retry, so we can reset\n * backoff without losing the total count of retry attempts\n */\n backoff: number;\n /** Counter for retry attempts that apply to the retry limit. */\n countedRetry: number;\n};\n\n/** ← `AlarmScheduler::RetryInfo` (`alarm-scheduler.h:103-106`). */\ntype RetryInfo = {\n readonly retry: boolean;\n readonly retryCountsAgainstLimit: boolean;\n};\n\n/**\n * The half of a `ScheduledAlarm` that outlives the process holding it: one row\n * of `_cf_ALARM` past `scheduled_time`, validated.\n */\ntype PersistedAlarm = {\n /** When the next attempt is due, or null while the alarm waits for its own time. */\n readonly retryTime: number | null;\n readonly backoff: number;\n readonly countedRetry: number;\n readonly previousRetryCountedAgainstLimit: boolean;\n /** A delivery started and nothing recorded how it ended. */\n readonly running: boolean;\n};\n\n/**\n * Allows scheduling alarm executions at specific times, returning a promise\n * representing the completion of the alarm event.\n */\nexport class AlarmScheduler {\n readonly #timer: Timer;\n readonly #random: () => number;\n readonly #getActor: GetActorFn;\n readonly #projectWake: ((scheduledTime: number | null) => Promise<void> | void) | undefined;\n readonly #db: SqliteDatabase;\n /** ← `kj::HashMap<ActorKey, ScheduledAlarm> alarms`, whose key is one string. */\n readonly #alarms = new Map<string, ScheduledAlarm>();\n /** ← `kj::TaskSet tasks`, which holds a task that has outlived its entry. */\n readonly #tasks = new Set<Promise<void>>();\n #taskFailure: { readonly exception: unknown } | undefined;\n #projection: Promise<void> = Promise.resolve();\n\n constructor(options: AlarmSchedulerOptions) {\n this.#timer = options.timer;\n this.#random = options.random ?? Math.random;\n this.#getActor = options.getActor;\n this.#projectWake = options.projectWake;\n this.#db = new SqliteDatabase(options.db);\n ensureInitialized(this.#db);\n this.#loadAlarmsFromDb();\n this.#projectNextWake();\n }\n\n /**\n * ← `getAlarm` (`alarm-scheduler.c++:84-99`), including its TODO: \"Might be\n * able to simplify AlarmScheduler somewhat, now that ActorSqlite no longer\n * relies on it for getAlarm()?\"\n */\n getAlarm(actorId: string): number | null {\n const alarm = this.#alarms.get(actorId);\n if (alarm === undefined) {\n // We currently retain the entire set of queued alarms in memory, no need to hit sqlite\n return null;\n }\n if (alarm.status === \"STARTED\") {\n // getAlarm() when the alarm handler is running should return null, unless an alarm is queued;\n return alarm.queuedAlarm;\n }\n return alarm.scheduledTime;\n }\n\n /**\n * ← `setAlarm` (`alarm-scheduler.c++:101-127`).\n *\n * The `boolean` is upstream's `query.changeCount() > 0`, and it is constant\n * true against SQLite's semantics: an `INSERT … ON CONFLICT DO UPDATE` always\n * reports one changed row, even when the value is unchanged. No caller reads it\n * — `ActorSqliteHooks::scheduleRun` discards it — so it is kept as upstream's\n * shape rather than as a signal.\n */\n setAlarm(actorId: string, scheduledTime: number): boolean {\n const query = this.#db.run(STMT.setAlarm, actorId, scheduledTime);\n\n const entry = this.#alarms.get(actorId);\n if (entry === undefined) {\n this.#alarms.set(actorId, this.#scheduleAlarm(this.#timer.now(), actorId, scheduledTime));\n } else if (entry.status !== \"WAITING\") {\n // We queue any new alarm after the existing alarm even if the new alarm has the same scheduled\n // time, as receiving a notification directly maps to a write for that time in the actor.\n entry.queuedAlarm = scheduledTime;\n } else {\n this.#replace(entry, this.#scheduleAlarm(this.#timer.now(), actorId, scheduledTime));\n }\n\n this.#projectNextWake();\n\n return query.rowsWritten > 0;\n }\n\n /** ← `deleteAll` (`alarm-scheduler.c++:129-134`). */\n deleteAll(): void {\n // Cancel all in-memory alarm tasks. Upstream's `alarms.clear()` destroys every task with its\n // entry; here the abort is that destruction's timer half, and a task that has already passed\n // its wake finds itself unmapped and returns.\n for (const entry of this.#alarms.values()) entry.cancel.abort();\n this.#alarms.clear();\n // Wipe the persistent store.\n this.#db.run(STMT.deleteAll);\n this.#projectNextWake();\n }\n\n /** ← `deleteAlarm` (`alarm-scheduler.c++:136-156`). */\n deleteAlarm(actorId: string): boolean {\n const query = this.#db.run(STMT.deleteAlarm, actorId);\n\n const entry = this.#alarms.get(actorId);\n if (entry !== undefined) {\n const queued = entry.queuedAlarm;\n if (queued !== null) {\n if (entry.status === \"STARTED\") {\n // If we are currently running an alarm, we want to delete the queued instead of current.\n entry.queuedAlarm = null;\n } else {\n this.#replace(entry, this.#scheduleAlarm(this.#timer.now(), actorId, queued));\n }\n } else if (entry.status !== \"STARTED\") {\n // We can't remove running alarms.\n entry.cancel.abort();\n this.#alarms.delete(actorId);\n }\n }\n\n this.#projectNextWake();\n\n return query.rowsWritten > 0;\n }\n\n /**\n * ← `ActorSqliteHooks` (`server.c++:3199-3219`), which is the whole of how an\n * actor's storage engine reaches a scheduler: one adapter per actor, holding\n * that actor's key, turning a time into `setAlarm` or `deleteAlarm`.\n */\n hooks(actorId: string): AlarmOutlet {\n return {\n // Deliberately not `async`: `AlarmOutlet.scheduleRun` may throw synchronously and\n // `ActorSqlite` relies on it, because a scheduling failure has to reach the caller before\n // the local database commits. `priorTask` is ignored for upstream's reason — \"We ignore the\n // priorTask in workerd because everything should run synchronously.\"\n scheduleRun: (scheduledTime: number | null, _priorTask: Promise<void>): Promise<void> => {\n if (scheduledTime !== null) this.setAlarm(actorId, scheduledTime);\n else this.deleteAlarm(actorId);\n return this.#projection;\n },\n };\n }\n\n /**\n * ← `taskFailed`'s `KJ_LOG(WARNING, e)` (`alarm-scheduler.c++:289-291`), and\n * the two other log sites in `makeAlarmTask` that report a failure and carry\n * on (`:202`, `:285`).\n *\n * This package has no logger, so the exception is kept instead of written —\n * the same treatment divergence 154 records for `waitUntilStatus()`, and for\n * the same reason: a background failure that is neither logged nor readable is\n * one nothing can notice.\n */\n taskFailure(): unknown {\n return this.#taskFailure?.exception;\n }\n\n // -----------------------------------------------------------------\n\n /**\n * ← `loadAlarmsFromDb` (`alarm-scheduler.c++:62-82`), plus the retry state\n * upstream has no columns for.\n *\n * This is the whole of the divergence's read side. Upstream's loop rebuilds\n * every entry with zeroed counters, which is what makes a per-worker-lifetime\n * scheduler forget; here the counters come off the row, and a row that says a\n * delivery was in flight is recovered before its entry is built.\n */\n #loadAlarmsFromDb(): void {\n const now = this.#timer.now();\n\n // TODO(someday): don't maintain the entire alarm set in memory -- right now for the usecase of\n // local development, doing so is sufficient.\n for (const row of this.#db.run(STMT.loadAlarms).rawRows) {\n const actorId = getText(row, 0);\n const scheduledTime = getInt64(row, 1);\n const persisted = readPersistedAlarm(actorId, row);\n const resumed = persisted.running\n ? this.#recoverInterruptedDelivery(actorId, now, persisted)\n : persisted;\n this.#alarms.set(actorId, this.#scheduleAlarm(now, actorId, scheduledTime, resumed));\n }\n }\n\n /**\n * A delivery that started and never reported an outcome, turned into an\n * **uncounted** retry: `backoff` climbs so the next attempt is further away,\n * and `countedRetry` does not move, so no number of them can abandon the\n * alarm.\n *\n * That split is decision 6's ranking applied to the one failure this substrate\n * produces routinely. Chrome evicting the worker mid-alarm is an\n * infrastructure failure, which is exactly the category upstream retries\n * forever and bounds by `RETRY_BACKOFF_MAX` rather than by\n * `ALARM_RETRY_MAX_TRIES` — \"a default of user error would abandon precisely\n * the alarms that most need keeping\". So a handler that reliably kills its\n * worker settles at one attempt every 1024 seconds and is never dropped. The\n * The browser host preserves that classification by reconstructing this\n * scheduler over the same namespace database. A row left `running` is the\n * durable evidence of the interrupted delivery; the Chrome-side watchdog\n * merely recreates the worker so this recovery path can read it.\n */\n #recoverInterruptedDelivery(\n actorId: string,\n now: number,\n persisted: PersistedAlarm,\n ): PersistedAlarm {\n const backoff = Math.min(RETRY_BACKOFF_MAX, persisted.backoff);\n let delay = alarmRetryDelayMs(backoff);\n delay += this.#jitterMsForDelay(delay);\n\n const resumed: PersistedAlarm = {\n retryTime: now + delay,\n backoff: backoff + 1,\n countedRetry: persisted.countedRetry,\n // Uncounted, which is what makes the next counted failure reset the\n // backoff — upstream's own rule for a user error arriving after an\n // internal one (`alarm-scheduler.c++:257-265`).\n previousRetryCountedAgainstLimit: false,\n running: false,\n };\n this.#db.run(\n STMT.saveRetry,\n resumed.retryTime,\n resumed.backoff,\n resumed.countedRetry,\n 0,\n actorId,\n );\n return resumed;\n }\n\n /** ← `scheduleAlarm` (`alarm-scheduler.c++:166-171`). */\n #scheduleAlarm(\n now: number,\n actorId: string,\n scheduledTime: number,\n resumed?: PersistedAlarm,\n ): ScheduledAlarm {\n // The entry exists before its task, where upstream's task exists before the entry that owns it:\n // the task has to be able to ask whether it is still the live entry, which is what stands in\n // for kj cancelling it when the entry it lives on is destroyed. Nothing observes the ordering,\n // because `makeAlarmTask` awaits its wake before touching anything.\n const entry: ScheduledAlarm = {\n actorId,\n scheduledTime,\n wakeTime: null,\n task: undefined,\n cancel: new AbortController(),\n queuedAlarm: null,\n status: \"WAITING\",\n previousRetryCountedAgainstLimit: resumed?.previousRetryCountedAgainstLimit ?? false,\n backoff: resumed?.backoff ?? 0,\n countedRetry: resumed?.countedRetry ?? 0,\n };\n // A pending retry can only DELAY the wake, never pull it before the alarm's own time. The two\n // disagree when a delivery was interrupted and the actor had already asked for a later alarm:\n // the row then holds a future `scheduled_time` and a retry due seconds from now, and honouring\n // the retry would fire the newer alarm early.\n const wake = Math.max(scheduledTime, resumed?.retryTime ?? scheduledTime);\n entry.wakeTime = wake;\n entry.task = this.#makeAlarmTask(wake - now, entry, scheduledTime);\n return entry;\n }\n\n /** ← `entry.value = scheduleAlarm(...)`, whose assignment destroys the old task. */\n #replace(previous: ScheduledAlarm, next: ScheduledAlarm): void {\n previous.cancel.abort();\n this.#alarms.set(next.actorId, next);\n }\n\n /** ← `checkTimestamp` (`alarm-scheduler.c++:173-185`), as a loop rather than tail recursion. */\n async #checkTimestamp(delay: number, scheduledTime: number, signal: AbortSignal): Promise<void> {\n let remaining = delay;\n for (;;) {\n await this.#timer.afterDelay(remaining, signal);\n\n // Since we are waiting on timer.afterDelay, it's possible that timer.now() was behind\n // the real time by a few ms, leading to premature alarm() execution. This checks it the current\n // time is >= than scheduledTime to ensure we run alarms only on or after their scheduled time.\n const now = this.#timer.now();\n if (now >= scheduledTime) return;\n // If it's not yet time to trigger the alarm, we shall wait a while longer until we can\n // trigger it. This repeats until it's time for the alarm to run.\n remaining = scheduledTime - now;\n }\n }\n\n /** ← `runAlarm` (`alarm-scheduler.c++:158-164`). */\n async #runAlarm(actorId: string, scheduledTime: number, retryCount: number): Promise<RetryInfo> {\n const result = await this.#getActor(actorId).deliverAlarm(scheduledTime, retryCount);\n return {\n retry: result.outcome !== \"ok\" && result.retry,\n retryCountsAgainstLimit: result.retryCountsAgainstLimit,\n };\n }\n\n /** ← the try/catch lambda around `runAlarm` (`alarm-scheduler.c++:197-211`). */\n async #runAlarmGuarded(\n actorId: string,\n scheduledTime: number,\n retryCount: number,\n ): Promise<RetryInfo> {\n try {\n return await this.#runAlarm(actorId, scheduledTime, retryCount);\n } catch (exception) {\n this.#taskFailed(exception);\n return {\n retry: true,\n // An exception here is \"weird\", they should normally be turned into AlarmResult statuses in\n // the sandbox for any user-caused error. Let's not count this retry attempt against the\n // limit.\n retryCountsAgainstLimit: false,\n };\n }\n }\n\n /** ← `makeAlarmTask` (`alarm-scheduler.c++:187-287`). */\n async #makeAlarmTask(delay: number, entry: ScheduledAlarm, scheduledTime: number): Promise<void> {\n const actorId = entry.actorId;\n await this.#checkTimestamp(delay, scheduledTime, entry.cancel.signal);\n\n // ← `KJ_ASSERT_NONNULL(alarms.findEntry(actorRef))` (`:192`). Upstream can assert here because\n // dropping the entry destroyed this task before it could resume; this is that cancellation.\n if (this.#alarms.get(actorId) !== entry) return;\n\n // Before the delivery, so that a worker that dies during it leaves the mark behind. A failure\n // to write it refuses the delivery rather than running one nothing can notice the end of: the\n // row is untouched, so the alarm is still due and a later scheduler picks it up unchanged. The\n // entry is left WAITING with no task, which a `setAlarm` re-arms; a metadata database this\n // scheduler cannot write is already failing every `setAlarm` too.\n try {\n this.#db.run(STMT.markRunning, actorId);\n } catch (exception) {\n this.#taskFailed(exception);\n return;\n }\n\n entry.status = \"STARTED\";\n entry.wakeTime = null;\n this.#projectNextWake();\n const retryCount = entry.countedRetry;\n\n const retryInfo = await this.#runAlarmGuarded(actorId, scheduledTime, retryCount);\n\n try {\n // ← `:214`'s second `KJ_ASSERT_NONNULL`, which upstream reaches by way of its outer catch when\n // `deleteAll()` cleared the map during the run.\n if (this.#alarms.get(actorId) !== entry) return;\n\n // We can't overwrite our entry before moving ourselves out of it, as a promise cannot\n // delete itself.\n const task = entry.task;\n if (task === undefined) throw new Error(\"An alarm task ran before it was recorded.\");\n this.#addTask(task);\n entry.task = undefined;\n\n // If an alarm is queued, there's no point in retrying the current one -- proceed\n // to running the queued alarm instead.\n const queued = entry.queuedAlarm;\n if (queued !== null) {\n // The delivery is over, and the row already describes the queued alarm — `setAlarm` wrote\n // its time and zeroed the retry state when it arrived — so the mark is all that is left to\n // clear.\n this.#db.run(STMT.clearRunning, actorId);\n // creating a new alarm and overwriting the old one will reset\n // `status` to WAITING and `queuedAlarm` to null\n this.#replace(entry, this.#scheduleAlarm(this.#timer.now(), actorId, queued));\n this.#projectNextWake();\n return;\n }\n\n // When we reach this block of code and alarm has either succeeded or failed and may (or may\n // not) retry. Setting the status of an alarm as FINISHED here, will allow deletion of alarms\n // between retries. If there's a retry, `makeAlarmTask` is called, setting status as RUNNING\n // again.\n entry.status = \"FINISHED\";\n\n if (retryInfo.retry) {\n // recreate the task, running after a delay determined using the retry factor\n if (entry.countedRetry >= ALARM_RETRY_MAX_TRIES) {\n await this.#abandon(entry, scheduledTime);\n return;\n }\n if (retryInfo.retryCountsAgainstLimit) {\n entry.countedRetry += 1;\n\n if (!entry.previousRetryCountedAgainstLimit) {\n // The last retry didn't count against the limit, indicating it was due to some internal\n // error. However, this retry does, meaning it's due to an error in user code,\n // most likely a different error. We should reset the retry counter used for\n // calculating backoff, so user-caused retries don't have an unnecessarily high backoff\n // time if they come after internal-caused retries.\n\n entry.backoff = 0;\n }\n }\n entry.previousRetryCountedAgainstLimit = retryInfo.retryCountsAgainstLimit;\n\n entry.backoff = Math.min(RETRY_BACKOFF_MAX, entry.backoff);\n let retryDelay = alarmRetryDelayMs(entry.backoff);\n\n retryDelay += this.#jitterMsForDelay(retryDelay);\n\n entry.backoff += 1;\n // Persisted before the task is armed, and it also clears `running`, so the two facts a\n // restart needs — that this delivery ended, and where the ladder now stands — are one\n // write. If it throws, the outer catch records it and the mark stays set, which a later\n // scheduler reads as an interrupted delivery: the alarm keeps its counters and is retried,\n // rather than being armed here in memory the process is about to lose.\n const retryTime = this.#timer.now() + retryDelay;\n this.#db.run(\n STMT.saveRetry,\n retryTime,\n entry.backoff,\n entry.countedRetry,\n entry.previousRetryCountedAgainstLimit ? 1 : 0,\n actorId,\n );\n\n entry.wakeTime = retryTime;\n entry.task = this.#makeAlarmTask(retryDelay, entry, scheduledTime);\n this.#projectNextWake();\n } else {\n if (entry.queuedAlarm !== null) {\n throw new Error(\"An alarm that will not retry still has an alarm queued behind it.\");\n }\n this.deleteAlarm(actorId);\n }\n } catch (exception) {\n // ← `KJ_LOG(ERROR, \"Failed to run alarm and was unable to schedule a retry\", exception)`.\n this.#taskFailed(exception);\n }\n }\n\n /**\n * ← the `countedRetry >= RETRY_MAX_TRIES` block (`alarm-scheduler.c++:237-253`).\n *\n * Its comment, verbatim, because the second half is the whole point: \"Notify\n * the actor to clear its in-memory alarm state so getAlarm() reflects the\n * deletion. We ignore the returned remaining time — the workerd-local alarm\n * scheduler already has visibility into the actor's alarm state via its SQLite\n * hooks. If the notification fails, we keep the alarm in the scheduler so it is\n * not silently lost.\"\n *\n * **Divergence: the returned time is not ignored** (upstream's\n * `.ignoreResult()`, `:244`). Upstream is right that the newer alarm normally\n * arrives on its own — `ActorSqlite` reports it through `scheduleRun`, and it\n * lands in `queuedAlarm`, which `deleteAlarm` below then reschedules for. But\n * that is a race, not an invariant: `abandonAlarm` reads the actor's committed\n * metadata, and there is a window in which the actor's alarm is newer than\n * anything the scheduler has been told about. In that window upstream's\n * unconditional `deleteAlarm` removes both the row and the entry, and the alarm\n * only comes back if a later commit happens to re-announce it. Re-registering\n * what `abandonAlarm` reports closes the window, is a no-op whenever the\n * queued alarm already covered it, and takes the side that preserves the alarm.\n */\n async #abandon(entry: ScheduledAlarm, scheduledTime: number): Promise<void> {\n const actorId = entry.actorId;\n let newerAlarm: number | null;\n try {\n newerAlarm = await this.#getActor(actorId).abandonAlarm(scheduledTime);\n } catch (exception) {\n this.#taskFailed(exception);\n return;\n }\n this.deleteAlarm(actorId);\n if (newerAlarm !== null && !this.#alarms.has(actorId)) {\n this.setAlarm(actorId, newerAlarm);\n }\n }\n\n /**\n * ← `maxJitterMsForDelay` (`alarm-scheduler.c++:13-16`) drawn through\n * `std::uniform_int_distribution<>(0, max)` (`:272-273`), whose range is\n * inclusive at both ends.\n */\n #jitterMsForDelay(delayMs: number): number {\n const max = Math.floor(RETRY_JITTER_FACTOR * delayMs);\n return Math.min(max, Math.floor(this.#random() * (max + 1)));\n }\n\n /** The earliest wake a browser watchdog must keep alive across process death. */\n #projectNextWake(): void {\n if (this.#projectWake === undefined) return;\n let earliest: number | null = null;\n for (const entry of this.#alarms.values()) {\n const wake = entry.status === \"STARTED\" ? entry.queuedAlarm : entry.wakeTime;\n if (wake !== null && (earliest === null || wake < earliest)) earliest = wake;\n }\n this.#projection = Promise.resolve(this.#projectWake(earliest));\n void this.#projection.catch((exception: unknown) => this.#taskFailed(exception));\n }\n\n /** ← `tasks.add`, whose failures reach `taskFailed`. */\n #addTask(task: Promise<void>): void {\n const tracked = task.then(\n () => {\n this.#tasks.delete(tracked);\n },\n (exception: unknown) => {\n this.#tasks.delete(tracked);\n this.#taskFailed(exception);\n },\n );\n this.#tasks.add(tracked);\n }\n\n /** ← `taskFailed` (`alarm-scheduler.c++:289-291`). */\n #taskFailed(exception: unknown): void {\n this.#taskFailure ??= { exception };\n }\n}\n\n/**\n * Reads one `_cf_ALARM` row's retry state, refusing anything this scheduler\n * could not have written.\n *\n * No upstream twin — upstream reads two columns and neither can be out of range.\n * It fails closed rather than clamping because every value here decides how long\n * an alarm waits or whether it is given up on, and a repaired counter is a\n * silent behaviour change on the one path that has nobody watching it. There is\n * no reader for any older shape: a database written before these columns existed\n * fails at the SELECT, which is what pre-production means here.\n *\n * The ranges are the ladder's own. `backoff` reaches `RETRY_BACKOFF_MAX + 1`\n * because the clamp is applied before the shift and the increment after it\n * (`alarm-scheduler.c++:269-275`), and `counted_retry` reaches\n * `ALARM_RETRY_MAX_TRIES` because the limit is checked before the increment\n * (`:237`).\n */\nfunction readPersistedAlarm(actorId: string, row: readonly unknown[]): PersistedAlarm {\n const retryTime = isNull(row, 2) ? null : getInt64(row, 2);\n const backoff = requireRange(actorId, \"backoff\", getInt64(row, 3), RETRY_BACKOFF_MAX + 1);\n const countedRetry = requireRange(\n actorId,\n \"counted_retry\",\n getInt64(row, 4),\n ALARM_RETRY_MAX_TRIES,\n );\n const previous = requireRange(actorId, \"previous_retry_counted\", getInt64(row, 5), 1);\n const running = requireRange(actorId, \"running\", getInt64(row, 6), 1);\n return {\n retryTime,\n backoff,\n countedRetry,\n previousRetryCountedAgainstLimit: previous === 1,\n running: running === 1,\n };\n}\n\nfunction requireRange(actorId: string, column: string, value: number, max: number): number {\n if (value < 0 || value > max) {\n throw new Error(\n `Alarm ${column} ${value} for actor ${actorId} is outside [0, ${max}]; ` +\n `_cf_ALARM holds a state this scheduler cannot have written.`,\n );\n }\n return value;\n}\n\n/** ← `ensureInitialized` (`alarm-scheduler.c++:50-60`). */\nfunction ensureInitialized(db: SqliteDatabase): void {\n hasCurrentSqliteTable(db, \"_cf_ALARM\", STMT.createTable);\n // TODO(sqlite): Do this automatically at a lower layer?\n db.run(\"PRAGMA journal_mode=WAL\");\n\n db.run(STMT.createTable);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"alarm-scheduler.js","names":["#timer","#random","#getActor","#projectWake","#db","#alarms","#tasks","#loadAlarmsFromDb","#projectNextWake","#scheduleAlarm","#replace","#projection","#taskFailure","#recoverInterruptedDelivery","#jitterMsForDelay","#makeAlarmTask","#checkTimestamp","#runAlarm","#runAlarmGuarded","#taskFailed","#addTask","#abandon"],"sources":["../../src/server/alarm-scheduler.ts"],"sourcesContent":["/**\n * ← workerd `src/workerd/server/alarm-scheduler.{h,c++}`\n *\n * Delivery and retry: the `_cf_ALARM` table, the watchdog arming, the queued\n * alarm, and the retry ladder. Measured on real workerd: an alarm re-armed for\n * `Date.now()` from inside a running handler does NOT re-enter — delivery is\n * serialised (`enter:1, exit:1, enter:2, exit:2`). That property is load\n * bearing; `_cf_executingScheduleRowId` upstream is safe only because of it\n * (§2.3). Here it falls out of the queued alarm: a `setAlarm` that arrives\n * while a handler runs is stored on the entry and started only after the run\n * finishes (`alarm-scheduler.c++:116-124`, `:220-227`).\n *\n * **This is runtime-internal, and it is what a host puts behind\n * `ActorPorts.alarms`.** Upstream wires it the same way: `ActorSqliteHooks`\n * (`server.c++:3199-3219`) is a three-line adapter whose `scheduleRun` is\n * `setAlarm`/`deleteAlarm` on the scheduler, and the scheduler is built once per\n * namespace (`server.c++:2325-2350`) rather than once per actor. `hooks(actorId)`\n * below is that adapter, so a host composes the two instead of writing its own\n * ladder.\n *\n * **One deliberate divergence, and it is the table's shape.** Upstream keeps the\n * retry ladder in memory and reloads every alarm with its counters at zero,\n * which is right for a process that lives for hours and is a regression on a\n * service worker Chrome evicts after seconds — see `_cf_ALARM` below and the\n * README's divergence table. Everything else here is upstream's, line for line.\n *\n * Spec: §1.8, §2.6, decisions 6, 11 and 16 in\n * docs/decisions.md.\n */\n\nimport type { AlarmOutlet } from \"../io/actor-sqlite\";\nimport type { Timer } from \"../io/io-context\";\nimport {\n getInt64,\n getText,\n hasCurrentSqliteTable,\n isNull,\n type SqlDatabase,\n SqliteDatabase,\n} from \"../util/sqlite\";\n\n// =======================================================================================\n// Constants\n\n/**\n * ← `WorkerInterface::ALARM_RETRY_START_SECONDS` (`io/worker-interface.h:130`),\n * re-declared as `AlarmScheduler::RETRY_START_SECONDS` (`alarm-scheduler.h:42`).\n *\n * \"not a duration so we can left shift it\" — upstream's own comment, and the\n * reason the ladder below is a shift rather than a table.\n */\nexport const ALARM_RETRY_START_SECONDS = 2;\n\n/**\n * ← `WorkerInterface::ALARM_RETRY_MAX_TRIES` (`io/worker-interface.h:131`) /\n * `AlarmScheduler::RETRY_MAX_TRIES` (`alarm-scheduler.h:45`).\n *\n * \"Max number of 'valid' retry attempts, i.e the worker returned an error.\"\n * It bounds `countedRetry`, NOT `backoff`: a run of failures that do not count\n * against the limit is retried forever, and its delay is bounded by\n * `RETRY_BACKOFF_MAX` instead.\n */\nexport const ALARM_RETRY_MAX_TRIES = 6;\n\n/**\n * ← `AlarmScheduler::RETRY_BACKOFF_MAX` (`alarm-scheduler.h:50`).\n *\n * \"Bound for exponential backoff when RETRY_MAX_TRIES is exceeded due to\n * internal errors. 2 << 9 is 1024 seconds, about 17 minutes. Total time spent in\n * retries once the backoff limit is reached is over 30 minutes.\"\n */\nexport const RETRY_BACKOFF_MAX = 9;\n\n/**\n * ← `AlarmScheduler::RETRY_JITTER_FACTOR` (`alarm-scheduler.h:54`).\n *\n * \"How much jitter should be applied to retry times to avoid bundled retries\n * overloading some common dependency between a set of failed alarms.\"\n */\nexport const RETRY_JITTER_FACTOR = 0.25;\n\n/**\n * ← `(AlarmScheduler::RETRY_START_SECONDS << backoff) * kj::SECONDS`\n * (`alarm-scheduler.c++:270`), before jitter.\n *\n * It refuses a backoff outside `[0, RETRY_BACKOFF_MAX]` rather than shifting it,\n * because JS's `<<` is a 32-bit operator: an unclamped counter would wrap to a\n * zero or negative delay — a hot retry loop — instead of saturating. The ladder\n * clamps immediately above its own call, exactly where upstream does; this is\n * what makes that clamp load bearing rather than decorative.\n */\nexport function alarmRetryDelayMs(backoff: number): number {\n if (!Number.isInteger(backoff) || backoff < 0 || backoff > RETRY_BACKOFF_MAX) {\n throw new Error(`Alarm retry backoff ${backoff} is outside [0, ${RETRY_BACKOFF_MAX}].`);\n }\n return (ALARM_RETRY_START_SECONDS << backoff) * 1_000;\n}\n\n/**\n * ← `_cf_ALARM` (`alarm-scheduler.c++:54-59`), plus the two prepared statements\n * (`alarm-scheduler.h:117-123`).\n *\n * Prepared statements are not part of the backend seam, so they survive as SQL\n * text under upstream's own member names — the treatment `util/sqlite-kv.ts`'s\n * `STMT` already records.\n *\n * `scheduled_time` holds **milliseconds** where upstream holds nanoseconds\n * (`:72`, `:102`). Same reason as `_cf_METADATA`'s alarm column: a JS number\n * runs out of integer precision 104 days into the epoch at nanosecond scale, so\n * storing what upstream stores would silently round every alarm.\n *\n * **Five columns upstream does not have, and they are this section's\n * divergence.** Upstream stores `(actor_id, scheduled_time)` and keeps the whole\n * retry ladder — `backoff`, `countedRetry`, `previousRetryCountedAgainstLimit`\n * and the fact that a delivery is in flight — in the `ScheduledAlarm` struct,\n * because a workerd process lives for hours and `loadAlarmsFromDb` runs once.\n * An MV3 service worker is evicted after seconds, so a scheduler rebuilt per\n * worker lifetime never accumulates any of them: `countedRetry` cannot reach\n * `ALARM_RETRY_MAX_TRIES`, so `#abandon` is unreachable and a permanently\n * failing alarm is never given up on, and `backoff` never leaves its first rung,\n * so that alarm wakes the browser every two seconds forever. Persisting the four\n * makes the ladder a property of the alarm rather than of the process that\n * happened to be running when it failed. See the README's divergence table.\n *\n * `retry_time` is the wake, which upstream never needs to store: its retries\n * live in the timer and the row's `scheduled_time` — which stays the alarm's own\n * time, because that is the identity `deliverAlarm` and `abandonAlarm` are told\n * — is by then in the past. Reloading without it re-arms every pending retry for\n * immediately, so the counters would climb while the delay was ignored.\n *\n * `running` is written before a delivery and cleared by whatever the delivery\n * turns into. A row still marked running at load is a delivery no result ever\n * came back from, which on this substrate is the ordinary case rather than a\n * crash: Chrome evicted the worker mid-alarm. Without it that alarm is\n * redelivered on every restart with no counter moved and no delay applied, which\n * is the hot loop above in its worst form, because nothing about it is bounded.\n */\nconst STMT = {\n createTable: `\n CREATE TABLE IF NOT EXISTS _cf_ALARM (\n actor_id TEXT PRIMARY KEY,\n scheduled_time INTEGER,\n retry_time INTEGER,\n backoff INTEGER NOT NULL,\n counted_retry INTEGER NOT NULL,\n previous_retry_counted INTEGER NOT NULL,\n running INTEGER NOT NULL\n ) WITHOUT ROWID\n `,\n loadAlarms: `\n SELECT actor_id, scheduled_time, retry_time, backoff, counted_retry,\n previous_retry_counted, running\n FROM _cf_ALARM\n `,\n // A new alarm time is new work, so it carries a fresh retry budget — which is\n // upstream's behaviour too, since every path that changes an entry's scheduled\n // time replaces the whole `ScheduledAlarm` and zeroes its counters with it.\n // `running` is deliberately NOT in the SET list: a delivery that is in flight\n // is still in flight, and clearing the mark here would erase the only evidence\n // that it never finished.\n setAlarm: `\n INSERT INTO _cf_ALARM VALUES(?, ?, NULL, 0, 0, 0, 0)\n ON CONFLICT DO UPDATE SET\n scheduled_time = excluded.scheduled_time,\n retry_time = NULL,\n backoff = 0,\n counted_retry = 0,\n previous_retry_counted = 0\n `,\n markRunning: `\n UPDATE _cf_ALARM SET running = 1 WHERE actor_id = ?\n `,\n // One statement, because the retry state and the end of the delivery have to\n // land together: a restart between them would resume the alarm with a stale\n // wake and a ladder one rung behind.\n saveRetry: `\n UPDATE _cf_ALARM\n SET retry_time = ?, backoff = ?, counted_retry = ?,\n previous_retry_counted = ?, running = 0\n WHERE actor_id = ?\n `,\n clearRunning: `\n UPDATE _cf_ALARM SET running = 0 WHERE actor_id = ?\n `,\n deleteAlarm: `\n DELETE FROM _cf_ALARM WHERE actor_id = ?\n `,\n deleteAll: `\n DELETE FROM _cf_ALARM\n `,\n} as const;\n\n// =======================================================================================\n// The result a delivery reports back\n\n/**\n * ← `EventOutcome` (`io/outcome.capnp`), restricted to the values the alarm path\n * can produce.\n *\n * The whole enum is a metrics type with no port — divergence 154 records that\n * for `waitUntilStatus()` — but `runAlarm` reads one bit of it\n * (`alarm-scheduler.c++:162`, `result.outcome != EventOutcome::OK`), so the\n * values `ServiceWorkerGlobalScope::runAlarm` actually returns are named here\n * and the rest are not.\n */\nexport type EventOutcome =\n | \"ok\"\n | \"canceled\"\n | \"script-not-found\"\n | \"exception\"\n | \"exceeded-cpu\"\n | \"unknown\";\n\n/**\n * ← `WorkerInterface::AlarmResult` (`io/worker-interface.h:71-81`).\n *\n * Upstream defaults all three fields; here every one is required, because the\n * producer is `server/actor-container.ts` rather than a capnp wire default and a\n * silently-defaulted `retryCountsAgainstLimit` is the difference between an\n * alarm that survives a broken actor and one that is abandoned.\n */\nexport type AlarmResult = {\n readonly retry: boolean;\n readonly retryCountsAgainstLimit: boolean;\n readonly outcome: EventOutcome;\n readonly errorDescription?: string;\n};\n\n/**\n * ← the `WorkerInterface` `GetActorFn` hands back\n * (`alarm-scheduler.c++:160`, `:244`), restricted to its two alarm members.\n *\n * `WorkerInterface` itself has no port — it is capnp dispatch, and divergence\n * 176 records it collapsing into the stub the transport returns — so the seam is\n * the two methods the scheduler calls. `ActorContainer` satisfies it\n * structurally; `deliverAlarm` is upstream's `runAlarm` under the name Section\n * 6b already gave it.\n */\nexport interface AlarmTarget {\n deliverAlarm(scheduledTime: number, retryCount: number): Promise<AlarmResult>;\n /**\n * ← `WorkerInterface::abandonAlarm` (`io/worker-interface.h:114`): \"Returns the\n * actor's stored alarm time if it differs from scheduledTime (i.e. the user\n * set a new alarm), or null if the alarm was cleared or no alarm was stored.\"\n */\n abandonAlarm(scheduledTime: number): Promise<number | null>;\n}\n\n/** ← `AlarmScheduler::GetActorFn` (`alarm-scheduler.h:56`). */\nexport type GetActorFn = (actorId: string) => AlarmTarget;\n\nexport type AlarmSchedulerOptions = {\n /**\n * ← the `const kj::Clock&` and the `kj::Timer&` upstream takes separately\n * (`alarm-scheduler.h:58-59`). One object here because `Timer.now()` is\n * already wall-clock milliseconds — `IoContext::now()` reads the same one —\n * so nothing distinguishes the two. `checkTimestamp`'s re-check loop stays,\n * because a JS timer really can fire a fraction of a millisecond early\n * relative to the clock it is compared against.\n */\n timer: Timer;\n /**\n * The database `_cf_ALARM` lives in — upstream's `metadata.sqlite`, one per\n * namespace beside the per-actor files (`server.c++:2336-2346`).\n *\n * Already open, where upstream's constructor opens it from a vfs and a path:\n * `SqlDatabaseProvider.open` is asynchronous and a constructor cannot await,\n * which is the same reason `createActorContainer` is a promise.\n */\n db: SqlDatabase;\n getActor: GetActorFn;\n /**\n * Browser hosts can mirror the earliest durable wake onto a platform watchdog\n * such as `chrome.alarms`. Workerd needs no such seam because its process owns\n * the scheduler timer.\n */\n projectWake?: (scheduledTime: number | null) => Promise<void> | void;\n /**\n * ← `std::default_random_engine`, seeded from the monotonic clock\n * (`alarm-scheduler.c++:20-27`). A test seam on a runtime-internal class, not\n * a substrate port: the jitter is the one part of the ladder that is\n * deliberately not a function of its inputs.\n */\n random?: () => number;\n};\n\n// =======================================================================================\n// The scheduler\n\n/** ← `AlarmScheduler::AlarmStatus` (`alarm-scheduler.h:72`). */\ntype AlarmStatus = \"WAITING\" | \"STARTED\" | \"FINISHED\";\n\n/** ← `AlarmScheduler::ScheduledAlarm` (`alarm-scheduler.h:80-99`). */\ntype ScheduledAlarm = {\n readonly actorId: string;\n readonly scheduledTime: number;\n /** The timer's actual wake, including persisted retry delay; null while running. */\n wakeTime: number | null;\n /**\n * ← `kj::Promise<void> task`. It exists upstream so the entry OWNS the task and\n * destroying the entry cancels it; JS has no such destruction, so the two\n * halves of that are explicit here — `cancel` stops the pending wake, and every\n * resumption re-reads the map to see whether it is still the live entry.\n */\n task: Promise<void> | undefined;\n /** The half of kj's cancel-by-drop that stops the timer. Divergence 147's shape. */\n readonly cancel: AbortController;\n /** Once started, an alarm can have a single alarm queued behind it. */\n queuedAlarm: number | null;\n status: AlarmStatus;\n previousRetryCountedAgainstLimit: boolean;\n /**\n * Counter for calculating backoff -- separate from retry, so we can reset\n * backoff without losing the total count of retry attempts\n */\n backoff: number;\n /** Counter for retry attempts that apply to the retry limit. */\n countedRetry: number;\n};\n\n/** ← `AlarmScheduler::RetryInfo` (`alarm-scheduler.h:103-106`). */\ntype RetryInfo = {\n readonly retry: boolean;\n readonly retryCountsAgainstLimit: boolean;\n};\n\n/**\n * The half of a `ScheduledAlarm` that outlives the process holding it: one row\n * of `_cf_ALARM` past `scheduled_time`, validated.\n */\ntype PersistedAlarm = {\n /** When the next attempt is due, or null while the alarm waits for its own time. */\n readonly retryTime: number | null;\n readonly backoff: number;\n readonly countedRetry: number;\n readonly previousRetryCountedAgainstLimit: boolean;\n /** A delivery started and nothing recorded how it ended. */\n readonly running: boolean;\n};\n\n/**\n * Allows scheduling alarm executions at specific times, returning a promise\n * representing the completion of the alarm event.\n */\nexport class AlarmScheduler {\n readonly #timer: Timer;\n readonly #random: () => number;\n readonly #getActor: GetActorFn;\n readonly #projectWake: ((scheduledTime: number | null) => Promise<void> | void) | undefined;\n readonly #db: SqliteDatabase;\n /** ← `kj::HashMap<ActorKey, ScheduledAlarm> alarms`, whose key is one string. */\n readonly #alarms = new Map<string, ScheduledAlarm>();\n /** ← `kj::TaskSet tasks`, which holds a task that has outlived its entry. */\n readonly #tasks = new Set<Promise<void>>();\n #taskFailure: { readonly exception: unknown } | undefined;\n #projection: Promise<void> = Promise.resolve();\n\n constructor(options: AlarmSchedulerOptions) {\n this.#timer = options.timer;\n this.#random = options.random ?? Math.random;\n this.#getActor = options.getActor;\n this.#projectWake = options.projectWake;\n this.#db = new SqliteDatabase(options.db);\n ensureInitialized(this.#db);\n this.#loadAlarmsFromDb();\n this.#projectNextWake();\n }\n\n /**\n * ← `getAlarm` (`alarm-scheduler.c++:84-99`), including its TODO: \"Might be\n * able to simplify AlarmScheduler somewhat, now that ActorSqlite no longer\n * relies on it for getAlarm()?\"\n */\n getAlarm(actorId: string): number | null {\n const alarm = this.#alarms.get(actorId);\n if (alarm === undefined) {\n // We currently retain the entire set of queued alarms in memory, no need to hit sqlite\n return null;\n }\n if (alarm.status === \"STARTED\") {\n // getAlarm() when the alarm handler is running should return null, unless an alarm is queued;\n return alarm.queuedAlarm;\n }\n return alarm.scheduledTime;\n }\n\n /**\n * ← `setAlarm` (`alarm-scheduler.c++:101-127`).\n *\n * The `boolean` is upstream's `query.changeCount() > 0`, and it is constant\n * true against SQLite's semantics: an `INSERT … ON CONFLICT DO UPDATE` always\n * reports one changed row, even when the value is unchanged. No caller reads it\n * — `ActorSqliteHooks::scheduleRun` discards it — so it is kept as upstream's\n * shape rather than as a signal.\n */\n setAlarm(actorId: string, scheduledTime: number): boolean {\n const query = this.#db.run(STMT.setAlarm, actorId, scheduledTime);\n\n const entry = this.#alarms.get(actorId);\n if (entry === undefined) {\n this.#alarms.set(actorId, this.#scheduleAlarm(this.#timer.now(), actorId, scheduledTime));\n } else if (entry.status !== \"WAITING\") {\n // We queue any new alarm after the existing alarm even if the new alarm has the same scheduled\n // time, as receiving a notification directly maps to a write for that time in the actor.\n entry.queuedAlarm = scheduledTime;\n } else {\n this.#replace(entry, this.#scheduleAlarm(this.#timer.now(), actorId, scheduledTime));\n }\n\n this.#projectNextWake();\n\n return query.rowsWritten > 0;\n }\n\n /** ← `deleteAll` (`alarm-scheduler.c++:129-134`). */\n deleteAll(): void {\n // Cancel all in-memory alarm tasks. Upstream's `alarms.clear()` destroys every task with its\n // entry; here the abort is that destruction's timer half, and a task that has already passed\n // its wake finds itself unmapped and returns.\n for (const entry of this.#alarms.values()) entry.cancel.abort();\n this.#alarms.clear();\n // Wipe the persistent store.\n this.#db.run(STMT.deleteAll);\n this.#projectNextWake();\n }\n\n /** ← `deleteAlarm` (`alarm-scheduler.c++:136-156`). */\n deleteAlarm(actorId: string): boolean {\n const query = this.#db.run(STMT.deleteAlarm, actorId);\n\n const entry = this.#alarms.get(actorId);\n if (entry !== undefined) {\n const queued = entry.queuedAlarm;\n if (queued !== null) {\n if (entry.status === \"STARTED\") {\n // If we are currently running an alarm, we want to delete the queued instead of current.\n entry.queuedAlarm = null;\n } else {\n this.#replace(entry, this.#scheduleAlarm(this.#timer.now(), actorId, queued));\n }\n } else if (entry.status !== \"STARTED\") {\n // We can't remove running alarms.\n entry.cancel.abort();\n this.#alarms.delete(actorId);\n }\n }\n\n this.#projectNextWake();\n\n return query.rowsWritten > 0;\n }\n\n /**\n * ← `ActorSqliteHooks` (`server.c++:3199-3219`), which is the whole of how an\n * actor's storage engine reaches a scheduler: one adapter per actor, holding\n * that actor's key, turning a time into `setAlarm` or `deleteAlarm`.\n */\n hooks(actorId: string): AlarmOutlet {\n return {\n // Deliberately not `async`: `AlarmOutlet.scheduleRun` may throw synchronously and\n // `ActorSqlite` relies on it, because a scheduling failure has to reach the caller before\n // the local database commits. `priorTask` is ignored for upstream's reason — \"We ignore the\n // priorTask in workerd because everything should run synchronously.\"\n scheduleRun: (scheduledTime: number | null, _priorTask: Promise<void>): Promise<void> => {\n if (scheduledTime !== null) this.setAlarm(actorId, scheduledTime);\n else this.deleteAlarm(actorId);\n return this.#projection;\n },\n };\n }\n\n /**\n * ← `taskFailed`'s `KJ_LOG(WARNING, e)` (`alarm-scheduler.c++:289-291`), and\n * the two other log sites in `makeAlarmTask` that report a failure and carry\n * on (`:202`, `:285`).\n *\n * This package has no logger, so the exception is kept instead of written —\n * the same treatment divergence 154 records for `waitUntilStatus()`, and for\n * the same reason: a background failure that is neither logged nor readable is\n * one nothing can notice.\n */\n taskFailure(): unknown {\n return this.#taskFailure?.exception;\n }\n\n // -----------------------------------------------------------------\n\n /**\n * ← `loadAlarmsFromDb` (`alarm-scheduler.c++:62-82`), plus the retry state\n * upstream has no columns for.\n *\n * This is the whole of the divergence's read side. Upstream's loop rebuilds\n * every entry with zeroed counters, which is what makes a per-worker-lifetime\n * scheduler forget; here the counters come off the row, and a row that says a\n * delivery was in flight is recovered before its entry is built.\n */\n #loadAlarmsFromDb(): void {\n const now = this.#timer.now();\n\n // TODO(someday): don't maintain the entire alarm set in memory -- right now for the usecase of\n // local development, doing so is sufficient.\n for (const row of this.#db.run(STMT.loadAlarms).rawRows) {\n const actorId = getText(row, 0);\n const scheduledTime = getInt64(row, 1);\n const persisted = readPersistedAlarm(actorId, row);\n const resumed = persisted.running\n ? this.#recoverInterruptedDelivery(actorId, now, persisted)\n : persisted;\n this.#alarms.set(actorId, this.#scheduleAlarm(now, actorId, scheduledTime, resumed));\n }\n }\n\n /**\n * A delivery that started and never reported an outcome, turned into an\n * **uncounted** retry: `backoff` climbs so the next attempt is further away,\n * and `countedRetry` does not move, so no number of them can abandon the\n * alarm.\n *\n * That split is decision 6's ranking applied to the one failure this substrate\n * produces routinely. Chrome evicting the worker mid-alarm is an\n * infrastructure failure, which is exactly the category upstream retries\n * forever and bounds by `RETRY_BACKOFF_MAX` rather than by\n * `ALARM_RETRY_MAX_TRIES` — \"a default of user error would abandon precisely\n * the alarms that most need keeping\". So a handler that reliably kills its\n * worker settles at one attempt every 1024 seconds and is never dropped. The\n * The browser host preserves that classification by reconstructing this\n * scheduler over the same namespace database. A row left `running` is the\n * durable evidence of the interrupted delivery; the Chrome-side watchdog\n * merely recreates the worker so this recovery path can read it.\n */\n #recoverInterruptedDelivery(\n actorId: string,\n now: number,\n persisted: PersistedAlarm,\n ): PersistedAlarm {\n const backoff = Math.min(RETRY_BACKOFF_MAX, persisted.backoff);\n let delay = alarmRetryDelayMs(backoff);\n delay += this.#jitterMsForDelay(delay);\n\n const resumed: PersistedAlarm = {\n retryTime: now + delay,\n backoff: backoff + 1,\n countedRetry: persisted.countedRetry,\n // Uncounted, which is what makes the next counted failure reset the\n // backoff — upstream's own rule for a user error arriving after an\n // internal one (`alarm-scheduler.c++:257-265`).\n previousRetryCountedAgainstLimit: false,\n running: false,\n };\n this.#db.run(\n STMT.saveRetry,\n resumed.retryTime,\n resumed.backoff,\n resumed.countedRetry,\n 0,\n actorId,\n );\n return resumed;\n }\n\n /** ← `scheduleAlarm` (`alarm-scheduler.c++:166-171`). */\n #scheduleAlarm(\n now: number,\n actorId: string,\n scheduledTime: number,\n resumed?: PersistedAlarm,\n ): ScheduledAlarm {\n // The entry exists before its task, where upstream's task exists before the entry that owns it:\n // the task has to be able to ask whether it is still the live entry, which is what stands in\n // for kj cancelling it when the entry it lives on is destroyed. Nothing observes the ordering,\n // because `makeAlarmTask` awaits its wake before touching anything.\n const entry: ScheduledAlarm = {\n actorId,\n scheduledTime,\n wakeTime: null,\n task: undefined,\n cancel: new AbortController(),\n queuedAlarm: null,\n status: \"WAITING\",\n previousRetryCountedAgainstLimit: resumed?.previousRetryCountedAgainstLimit ?? false,\n backoff: resumed?.backoff ?? 0,\n countedRetry: resumed?.countedRetry ?? 0,\n };\n // A pending retry can only DELAY the wake, never pull it before the alarm's own time. The two\n // disagree when a delivery was interrupted and the actor had already asked for a later alarm:\n // the row then holds a future `scheduled_time` and a retry due seconds from now, and honouring\n // the retry would fire the newer alarm early.\n const wake = Math.max(scheduledTime, resumed?.retryTime ?? scheduledTime);\n entry.wakeTime = wake;\n entry.task = this.#makeAlarmTask(wake - now, entry, scheduledTime);\n return entry;\n }\n\n /** ← `entry.value = scheduleAlarm(...)`, whose assignment destroys the old task. */\n #replace(previous: ScheduledAlarm, next: ScheduledAlarm): void {\n previous.cancel.abort();\n this.#alarms.set(next.actorId, next);\n }\n\n /** ← `checkTimestamp` (`alarm-scheduler.c++:173-185`), as a loop rather than tail recursion. */\n async #checkTimestamp(delay: number, scheduledTime: number, signal: AbortSignal): Promise<void> {\n let remaining = delay;\n for (;;) {\n await this.#timer.afterDelay(remaining, signal);\n\n // Since we are waiting on timer.afterDelay, it's possible that timer.now() was behind\n // the real time by a few ms, leading to premature alarm() execution. This checks it the current\n // time is >= than scheduledTime to ensure we run alarms only on or after their scheduled time.\n const now = this.#timer.now();\n if (now >= scheduledTime) return;\n // If it's not yet time to trigger the alarm, we shall wait a while longer until we can\n // trigger it. This repeats until it's time for the alarm to run.\n remaining = scheduledTime - now;\n }\n }\n\n /** ← `runAlarm` (`alarm-scheduler.c++:158-164`). */\n async #runAlarm(actorId: string, scheduledTime: number, retryCount: number): Promise<RetryInfo> {\n const result = await this.#getActor(actorId).deliverAlarm(scheduledTime, retryCount);\n return {\n retry: result.outcome !== \"ok\" && result.retry,\n retryCountsAgainstLimit: result.retryCountsAgainstLimit,\n };\n }\n\n /** ← the try/catch lambda around `runAlarm` (`alarm-scheduler.c++:197-211`). */\n async #runAlarmGuarded(\n actorId: string,\n scheduledTime: number,\n retryCount: number,\n ): Promise<RetryInfo> {\n try {\n return await this.#runAlarm(actorId, scheduledTime, retryCount);\n } catch (exception) {\n this.#taskFailed(exception);\n return {\n retry: true,\n // An exception here is \"weird\", they should normally be turned into AlarmResult statuses in\n // the sandbox for any user-caused error. Let's not count this retry attempt against the\n // limit.\n retryCountsAgainstLimit: false,\n };\n }\n }\n\n /** ← `makeAlarmTask` (`alarm-scheduler.c++:187-287`). */\n async #makeAlarmTask(delay: number, entry: ScheduledAlarm, scheduledTime: number): Promise<void> {\n const actorId = entry.actorId;\n await this.#checkTimestamp(delay, scheduledTime, entry.cancel.signal);\n\n // ← `KJ_ASSERT_NONNULL(alarms.findEntry(actorRef))` (`:192`). Upstream can assert here because\n // dropping the entry destroyed this task before it could resume; this is that cancellation.\n if (this.#alarms.get(actorId) !== entry) return;\n\n // Before the delivery, so that a worker that dies during it leaves the mark behind. A failure\n // to write it refuses the delivery rather than running one nothing can notice the end of: the\n // row is untouched, so the alarm is still due and a later scheduler picks it up unchanged. The\n // entry is left WAITING with no task, which a `setAlarm` re-arms; a metadata database this\n // scheduler cannot write is already failing every `setAlarm` too.\n try {\n this.#db.run(STMT.markRunning, actorId);\n } catch (exception) {\n this.#taskFailed(exception);\n return;\n }\n\n entry.status = \"STARTED\";\n entry.wakeTime = null;\n this.#projectNextWake();\n const retryCount = entry.countedRetry;\n\n const retryInfo = await this.#runAlarmGuarded(actorId, scheduledTime, retryCount);\n\n try {\n // ← `:214`'s second `KJ_ASSERT_NONNULL`, which upstream reaches by way of its outer catch when\n // `deleteAll()` cleared the map during the run.\n if (this.#alarms.get(actorId) !== entry) return;\n\n // We can't overwrite our entry before moving ourselves out of it, as a promise cannot\n // delete itself.\n const task = entry.task;\n if (task === undefined) throw new Error(\"An alarm task ran before it was recorded.\");\n this.#addTask(task);\n entry.task = undefined;\n\n // If an alarm is queued, there's no point in retrying the current one -- proceed\n // to running the queued alarm instead.\n const queued = entry.queuedAlarm;\n if (queued !== null) {\n // The delivery is over, and the row already describes the queued alarm — `setAlarm` wrote\n // its time and zeroed the retry state when it arrived — so the mark is all that is left to\n // clear.\n this.#db.run(STMT.clearRunning, actorId);\n // creating a new alarm and overwriting the old one will reset\n // `status` to WAITING and `queuedAlarm` to null\n this.#replace(entry, this.#scheduleAlarm(this.#timer.now(), actorId, queued));\n this.#projectNextWake();\n return;\n }\n\n // When we reach this block of code and alarm has either succeeded or failed and may (or may\n // not) retry. Setting the status of an alarm as FINISHED here, will allow deletion of alarms\n // between retries. If there's a retry, `makeAlarmTask` is called, setting status as RUNNING\n // again.\n entry.status = \"FINISHED\";\n\n if (retryInfo.retry) {\n // recreate the task, running after a delay determined using the retry factor\n if (entry.countedRetry >= ALARM_RETRY_MAX_TRIES) {\n await this.#abandon(entry, scheduledTime);\n return;\n }\n if (retryInfo.retryCountsAgainstLimit) {\n entry.countedRetry += 1;\n\n if (!entry.previousRetryCountedAgainstLimit) {\n // The last retry didn't count against the limit, indicating it was due to some internal\n // error. However, this retry does, meaning it's due to an error in user code,\n // most likely a different error. We should reset the retry counter used for\n // calculating backoff, so user-caused retries don't have an unnecessarily high backoff\n // time if they come after internal-caused retries.\n\n entry.backoff = 0;\n }\n }\n entry.previousRetryCountedAgainstLimit = retryInfo.retryCountsAgainstLimit;\n\n entry.backoff = Math.min(RETRY_BACKOFF_MAX, entry.backoff);\n let retryDelay = alarmRetryDelayMs(entry.backoff);\n\n retryDelay += this.#jitterMsForDelay(retryDelay);\n\n entry.backoff += 1;\n // Persisted before the task is armed, and it also clears `running`, so the two facts a\n // restart needs — that this delivery ended, and where the ladder now stands — are one\n // write. If it throws, the outer catch records it and the mark stays set, which a later\n // scheduler reads as an interrupted delivery: the alarm keeps its counters and is retried,\n // rather than being armed here in memory the process is about to lose.\n const retryTime = this.#timer.now() + retryDelay;\n this.#db.run(\n STMT.saveRetry,\n retryTime,\n entry.backoff,\n entry.countedRetry,\n entry.previousRetryCountedAgainstLimit ? 1 : 0,\n actorId,\n );\n\n entry.wakeTime = retryTime;\n entry.task = this.#makeAlarmTask(retryDelay, entry, scheduledTime);\n this.#projectNextWake();\n } else {\n if (entry.queuedAlarm !== null) {\n throw new Error(\"An alarm that will not retry still has an alarm queued behind it.\");\n }\n this.deleteAlarm(actorId);\n }\n } catch (exception) {\n // ← `KJ_LOG(ERROR, \"Failed to run alarm and was unable to schedule a retry\", exception)`.\n this.#taskFailed(exception);\n }\n }\n\n /**\n * ← the `countedRetry >= RETRY_MAX_TRIES` block (`alarm-scheduler.c++:237-253`).\n *\n * Its comment, verbatim, because the second half is the whole point: \"Notify\n * the actor to clear its in-memory alarm state so getAlarm() reflects the\n * deletion. We ignore the returned remaining time — the workerd-local alarm\n * scheduler already has visibility into the actor's alarm state via its SQLite\n * hooks. If the notification fails, we keep the alarm in the scheduler so it is\n * not silently lost.\"\n *\n * **Divergence: the returned time is not ignored** (upstream's\n * `.ignoreResult()`, `:244`). Upstream is right that the newer alarm normally\n * arrives on its own — `ActorSqlite` reports it through `scheduleRun`, and it\n * lands in `queuedAlarm`, which `deleteAlarm` below then reschedules for. But\n * that is a race, not an invariant: `abandonAlarm` reads the actor's committed\n * metadata, and there is a window in which the actor's alarm is newer than\n * anything the scheduler has been told about. In that window upstream's\n * unconditional `deleteAlarm` removes both the row and the entry, and the alarm\n * only comes back if a later commit happens to re-announce it. Re-registering\n * what `abandonAlarm` reports closes the window, is a no-op whenever the\n * queued alarm already covered it, and takes the side that preserves the alarm.\n */\n async #abandon(entry: ScheduledAlarm, scheduledTime: number): Promise<void> {\n const actorId = entry.actorId;\n let newerAlarm: number | null;\n try {\n newerAlarm = await this.#getActor(actorId).abandonAlarm(scheduledTime);\n } catch (exception) {\n this.#taskFailed(exception);\n return;\n }\n this.deleteAlarm(actorId);\n if (newerAlarm !== null && !this.#alarms.has(actorId)) {\n this.setAlarm(actorId, newerAlarm);\n }\n }\n\n /**\n * ← `maxJitterMsForDelay` (`alarm-scheduler.c++:13-16`) drawn through\n * `std::uniform_int_distribution<>(0, max)` (`:272-273`), whose range is\n * inclusive at both ends.\n */\n #jitterMsForDelay(delayMs: number): number {\n const max = Math.floor(RETRY_JITTER_FACTOR * delayMs);\n return Math.min(max, Math.floor(this.#random() * (max + 1)));\n }\n\n /** The earliest wake a browser watchdog must keep alive across process death. */\n #projectNextWake(): void {\n if (this.#projectWake === undefined) return;\n let earliest: number | null = null;\n for (const entry of this.#alarms.values()) {\n const wake = entry.status === \"STARTED\" ? entry.queuedAlarm : entry.wakeTime;\n if (wake !== null && (earliest === null || wake < earliest)) earliest = wake;\n }\n this.#projection = Promise.resolve(this.#projectWake(earliest));\n void this.#projection.catch((exception: unknown) => this.#taskFailed(exception));\n }\n\n /** ← `tasks.add`, whose failures reach `taskFailed`. */\n #addTask(task: Promise<void>): void {\n const tracked = task.then(\n () => {\n this.#tasks.delete(tracked);\n },\n (exception: unknown) => {\n this.#tasks.delete(tracked);\n this.#taskFailed(exception);\n },\n );\n this.#tasks.add(tracked);\n }\n\n /** ← `taskFailed` (`alarm-scheduler.c++:289-291`). */\n #taskFailed(exception: unknown): void {\n this.#taskFailure ??= { exception };\n }\n}\n\n/**\n * Reads one `_cf_ALARM` row's retry state, refusing anything this scheduler\n * could not have written.\n *\n * No upstream twin — upstream reads two columns and neither can be out of range.\n * It fails closed rather than clamping because every value here decides how long\n * an alarm waits or whether it is given up on, and a repaired counter is a\n * silent behaviour change on the one path that has nobody watching it. There is\n * no reader for any older shape: a database written before these columns existed\n * fails at the SELECT, which is what pre-production means here.\n *\n * The ranges are the ladder's own. `backoff` reaches `RETRY_BACKOFF_MAX + 1`\n * because the clamp is applied before the shift and the increment after it\n * (`alarm-scheduler.c++:269-275`), and `counted_retry` reaches\n * `ALARM_RETRY_MAX_TRIES` because the limit is checked before the increment\n * (`:237`).\n */\nfunction readPersistedAlarm(actorId: string, row: readonly unknown[]): PersistedAlarm {\n const retryTime = isNull(row, 2) ? null : getInt64(row, 2);\n const backoff = requireRange(actorId, \"backoff\", getInt64(row, 3), RETRY_BACKOFF_MAX + 1);\n const countedRetry = requireRange(\n actorId,\n \"counted_retry\",\n getInt64(row, 4),\n ALARM_RETRY_MAX_TRIES,\n );\n const previous = requireRange(actorId, \"previous_retry_counted\", getInt64(row, 5), 1);\n const running = requireRange(actorId, \"running\", getInt64(row, 6), 1);\n return {\n retryTime,\n backoff,\n countedRetry,\n previousRetryCountedAgainstLimit: previous === 1,\n running: running === 1,\n };\n}\n\nfunction requireRange(actorId: string, column: string, value: number, max: number): number {\n if (value < 0 || value > max) {\n throw new Error(\n `Alarm ${column} ${value} for actor ${actorId} is outside [0, ${max}]; ` +\n `_cf_ALARM holds a state this scheduler cannot have written.`,\n );\n }\n return value;\n}\n\n/** ← `ensureInitialized` (`alarm-scheduler.c++:50-60`). */\nfunction ensureInitialized(db: SqliteDatabase): void {\n hasCurrentSqliteTable(db, \"_cf_ALARM\", STMT.createTable);\n db.run(STMT.createTable);\n}\n"],"mappings":"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"}
@@ -8,8 +8,8 @@
8
8
  * **`DurableObjectStorage` satisfies workers-types with no cast (§2.4).** That
9
9
  * was checked rather than asserted, and two shapes here exist only because it
10
10
  * has to: `sql.Cursor` and `sql.Statement` must be constructible with no
11
- * arguments (see `sql.ts`), and `storage.kv` is required, which is why
12
- * `api/sync-kv.ts` exists at all. The narrowings that remain are all one thing —
11
+ * arguments (see `sql.ts`), and `storage.kv` is required. The narrowings that
12
+ * remain are all one thing —
13
13
  * `get<T>` returns the caller's claim about the shape of a value SQLite handed
14
14
  * back as bytes, which no check can confirm and which upstream states the same
15
15
  * way, as a `jsg::JsRef<jsg::JsValue>` behind a `JSG_TS_OVERRIDE`'d
@@ -19,8 +19,7 @@
19
19
  * That is upstream's: a `JSG_REQUIRE` inside a method returning `jsg::Promise`
20
20
  * throws into the isolate before the promise exists, so `put(k, undefined)`
21
21
  * throws rather than rejecting. The same goes for a value that will not decode,
22
- * because §1.4 makes the SQLite path take `transformCacheResult`'s value arm and
23
- * run the decoder synchronously.
22
+ * because §1.4 makes the SQLite path run the decoder before `Promise.resolve`.
24
23
  *
25
24
  * **What the input gate does and does not do here.** Every entry point calls
26
25
  * `requireInputLock` — see its comment in `io/io-context.ts`, which is the one
@@ -32,13 +31,10 @@
32
31
  * section.
33
32
  *
34
33
  * **Decision 2's branch has one reachable site**, and it is not where upstream's
35
- * is. `transformCacheResult` branches on `allowConcurrency` because upstream's
36
- * `ActorCacheOps` returns `kj::OneOf<T, kj::Promise<T>>`; §1.4 measures that the
37
- * SQLite arm is always the immediate one, so Section 4 collapsed the `OneOf` and
38
- * the branch has nothing to select between. `transformMaybeBackpressure` keeps
39
- * it, because `DeleteAllResults.backpressure` is still a promise in
40
- * `io/actor-cache.ts`. Both helpers are kept under upstream's names so the
41
- * question "where did `allowConcurrency` go" is answered by reading them.
34
+ * is. §1.4 measures that SQLite cache operations are immediate, so their
35
+ * `kj::OneOf<T, kj::Promise<T>>` branch has nothing to select between.
36
+ * `transformMaybeBackpressure` keeps the branch because
37
+ * `DeleteAllResults.backpressure` is still a promise in `io/actor-cache.ts`.
42
38
  *
43
39
  * Not ported, because the substrate has no equivalent: Hibernatable WebSockets,
44
40
  * which is the whole reason `DurableObjectState`'s eight WebSocket methods are
@@ -60,7 +56,6 @@ import type { SqliteKv } from "../util/sqlite-kv.js";
60
56
  import type { SqliteDatabase } from "../util/sqlite.js";
61
57
  import type { ActorScopeBindings } from "./global-scope.js";
62
58
  import { SqlStorage } from "./sql.js";
63
- import { SyncKvStorage } from "./sync-kv.js";
64
59
  /**
65
60
  * ← `MAX_FACET_NAME_LENGTH` / `MAX_FACET_TREE_DEPTH`
66
61
  * (`actor-state.c++:943,947`), in the anonymous namespace beside the facet code
@@ -93,66 +88,16 @@ export declare const HIBERNATION_UNIMPLEMENTED_MESSAGE: string;
93
88
  * which every object satisfies, so nothing refuses it before the method body.
94
89
  */
95
90
  export declare const FACET_CLASS_UNSUPPORTED_MESSAGE: string;
96
- /**
97
- * ← `serializeV8Value`. The wire bytes differ because V8's serializer is not
98
- * available in browsers; the public structured-clone value semantics do not.
99
- * The short header keeps the new representation unambiguous while old JSON rows
100
- * remain readable.
101
- */
102
- export declare function serializeValue(_key: string, value: unknown): Uint8Array;
103
- /**
104
- * ← `deserializeV8Value`.
105
- *
106
- * Upstream logs "the key (to help find the data in the database if it hasn't
107
- * been deleted), the length of the value, and the first three bytes of the value
108
- * (which is just the v8-internal version header and the tag that indicates the
109
- * type of the value, but not its contents)". Our four-byte header carries only
110
- * a marker and version for the same reason.
111
- */
112
- export declare function deserializeValue(key: string, buffer: Uint8Array): unknown;
113
- /** ← `DurableObjectStorageOperations::CompiledListOptions`. */
114
- export type CompiledListOptions = {
115
- readonly start: string;
116
- readonly end: string | undefined;
117
- readonly reverse: boolean;
118
- readonly limit: number | undefined;
119
- };
120
- /**
121
- * ← `DurableObjectStorageOperations::compileListOptions`
122
- * (`actor-state.c++:314-417`). Returns undefined if the list operation would
123
- * provably return no results. Public because `SyncKvStorage` reuses it, exactly
124
- * as upstream's comment says it must.
125
- *
126
- * Two translations. `startAfter` gains ONE null character where upstream's
127
- * `kj::String` gains two, because the second of upstream's is the terminator and
128
- * a JS string has none. And every comparison here is on UTF-16 code units where
129
- * upstream's is on UTF-8 bytes, while the range the database actually applies is
130
- * SQLite's `BINARY` collation over UTF-8 — the two orders agree for every key
131
- * outside the astral planes, and a key that mixes astral characters with a
132
- * prefix can land on the wrong side of a clamp this function computes.
133
- */
134
- export declare function compileListOptions(options: DurableObjectListOptions | undefined): CompiledListOptions | undefined;
135
91
  /**
136
92
  * ← `DurableObjectStorageOperations`. "Common implementation of
137
93
  * DurableObjectStorage and DurableObjectTransaction. This class is designed to
138
94
  * be used as a mixin."
139
95
  */
140
- export declare abstract class DurableObjectStorageOperations {
96
+ declare abstract class DurableObjectStorageOperations {
141
97
  #private;
142
98
  protected readonly ctx: IoContext;
143
99
  constructor(ctx: IoContext);
144
100
  protected abstract getCache(op: string): ActorCacheOps;
145
- /** Whether to skip caching and allow concurrency on all operations. */
146
- protected useDirectIo(): boolean;
147
- /**
148
- * ← `configureOptions`. Both subclasses answer `useDirectIo()` false, so this
149
- * is the identity today; it is upstream's hook and the only place the two
150
- * flags are forced on.
151
- */
152
- protected configureOptions<T extends {
153
- allowConcurrency?: boolean;
154
- noCache?: boolean;
155
- }>(options: T): T;
156
101
  get<T = unknown>(key: string, options?: DurableObjectGetOptions): Promise<T | undefined>;
157
102
  get<T = unknown>(keys: string[], options?: DurableObjectGetOptions): Promise<Map<string, T>>;
158
103
  getAlarm(maybeOptions?: DurableObjectGetAlarmOptions): Promise<number | null>;
@@ -187,13 +132,11 @@ export declare class DurableObjectStorage extends DurableObjectStorageOperations
187
132
  getActorCacheInterface(): StorageCache;
188
133
  /** ← `DurableObjectStorage::getSqliteDb`. Always SQLite-backed here; see the header. */
189
134
  getSqliteDb(): SqliteDatabase;
190
- /** ← `DurableObjectStorage::getSqliteKv`. */
191
- getSqliteKv(): SqliteKv;
192
135
  protected getCache(): ActorCacheOps;
193
136
  /** ← `JSG_LAZY_INSTANCE_PROPERTY(sql, getSql)`. */
194
137
  get sql(): SqlStorage;
195
138
  /** ← `JSG_LAZY_INSTANCE_PROPERTY(kv, getKv)`. */
196
- get kv(): SyncKvStorage;
139
+ get kv(): globalThis.SyncKvStorage;
197
140
  /**
198
141
  * ← `DurableObjectStorage::deleteAll`.
199
142
  *
@@ -255,7 +198,7 @@ export declare class DurableObjectStorage extends DurableObjectStorageOperations
255
198
  getPrimary(): undefined;
256
199
  isReplica(): boolean;
257
200
  }
258
- export declare class DurableObjectTransaction extends DurableObjectStorageOperations implements globalThis.DurableObjectTransaction {
201
+ declare class DurableObjectTransaction extends DurableObjectStorageOperations implements globalThis.DurableObjectTransaction {
259
202
  #private;
260
203
  constructor(ctx: IoContext, cacheTxn: ActorCacheTransaction);
261
204
  protected getCache(op: string): ActorCacheOps;
@@ -394,3 +337,4 @@ export declare class DurableObjectState implements globalThis.DurableObjectState
394
337
  getHibernatableWebSocketEventTimeout(): never;
395
338
  getTags(_ws: WebSocket): never;
396
339
  }
340
+ export {};
@@ -246,19 +246,3 @@ export declare class LoopbackColoLocalActorNamespace extends ColoLocalActorNames
246
246
  */
247
247
  export type LoopbackColoLocalActorNamespaceValue = Omit<LoopbackColoLocalActorNamespace, "call"> & ((options?: LoopbackDurableObjectClassOptions) => DurableObjectClass);
248
248
  export declare function asLoopbackColoLocalActorNamespace(namespace: LoopbackColoLocalActorNamespace): LoopbackColoLocalActorNamespaceValue;
249
- /** `Value` must be assignable to `Declared`; declaring the constraint is the check. */
250
- type Assignable<Value extends Declared, Declared> = Value;
251
- /**
252
- * Checked rather than claimed: every value this module produces satisfies the
253
- * shape `@cloudflare/workers-types` 4.20260702.1 declares for it. The last two
254
- * are checked against `Cloudflare.Exports`' own description of a `ctx.exports`
255
- * entry — `LoopbackForExport<T>` intersected with the namespace — because the
256
- * two named interfaces there are call-signature-less, per the note above.
257
- */
258
- export type PinnedLoopbackTypes = [
259
- Assignable<LoopbackServiceStubValue, globalThis.LoopbackServiceStub>,
260
- Assignable<LoopbackDurableObjectClassValue, globalThis.LoopbackDurableObjectClass>,
261
- Assignable<LoopbackDurableObjectNamespaceValue, globalThis.LoopbackDurableObjectClass & globalThis.DurableObjectNamespace>,
262
- Assignable<LoopbackColoLocalActorNamespaceValue, globalThis.LoopbackDurableObjectClass & globalThis.ColoLocalActorNamespace>
263
- ];
264
- export {};
@@ -26,7 +26,7 @@
26
26
  * "wrap it in awaitIo" would be wrong three ways:
27
27
  *
28
28
  * - **Timers** capture the critical section at the ARMING call and re-enter
29
- * through `ctx.run(callback, cs)` when they fire. Not `awaitIo`, deliberately
29
+ * through `ctx.run(callback, { input: cs })` when they fire. Not `awaitIo`, deliberately
30
30
  * — see `TimeoutManager` in `io/io-context.ts` for upstream's own reason.
31
31
  * - **`fetch`** is `awaitIo` (`http.c++` has ten of them and zero
32
32
  * `awaitIoWithInputLock`), preceded by an output-gate wait so nothing departs
@@ -334,21 +334,3 @@ export declare class WorkerLoader implements globalThis.WorkerLoader {
334
334
  */
335
335
  load(code: WorkerCode): WorkerStub;
336
336
  }
337
- /** `Value` must be assignable to `Declared`; declaring the constraint is the check. */
338
- type Assignable<Value extends Declared, Declared> = Value;
339
- /**
340
- * §2.4's no-cast rule: these reach a consumer as `env` bindings typed by
341
- * `@cloudflare/workers-types`, so the type system checks the surface rather than a
342
- * cast doing it. The three struct rows point the other way, because a struct is an
343
- * argument: what has to hold is that every code a consumer can write against the
344
- * pinned type is one this file accepts. `Module` is a strict superset by the two
345
- * byte fields, for the reason its own comment gives.
346
- */
347
- export type PinnedWorkerLoaderTypes = [
348
- Assignable<WorkerLoader, globalThis.WorkerLoader>,
349
- Assignable<WorkerStub, globalThis.WorkerStub>,
350
- Assignable<globalThis.WorkerLoaderWorkerCode, WorkerCode>,
351
- Assignable<globalThis.WorkerLoaderModule, Module>,
352
- Assignable<globalThis.WorkerStubEntrypointOptions, EntrypointOptions>
353
- ];
354
- export {};
@@ -15,7 +15,7 @@ type TransformedAwait<T> = {
15
15
  /** Re-enter the actor that owns this transformed await; fail open outside actors. */
16
16
  export declare function __gate<T>(value: T): T | Promise<Awaited<T>>;
17
17
  /** Capture an actor await without publishing its context before the continuation runs. */
18
- export declare function __gateAwait<T>(value: T): T | Promise<TransformedAwait<Awaited<T>>>;
18
+ export declare function __gateAwait<T>(value: T, developmentSource?: string): T | Promise<TransformedAwait<Awaited<T>>>;
19
19
  /** Restore the captured actor at the first instruction after a transformed await. */
20
20
  export declare function __resumeAwait<T>(value: T | TransformedAwait<T>): T;
21
21
  /** Gate every operation used by `for await`, including early return and throw. */
@@ -25,7 +25,8 @@
25
25
  export type { SqlDatabase, SqlDatabaseProvider, SqlDatabaseSnapshot, SqlDatabaseSnapshotProvider, SqlResult, SqlValue, } from "./util/sqlite.js";
26
26
  export type { ReadOptions, WriteOptions } from "./io/actor-cache.js";
27
27
  export type { AlarmOutlet } from "./io/actor-sqlite.js";
28
- export type { Timer } from "./io/io-context.js";
28
+ export { BrokenActorError, type Timer } from "./io/io-context.js";
29
+ export { CanceledError } from "./io/io-gate.js";
29
30
  /**
30
31
  * The Worker Loader (§1.11, decision 15). Exported where the `api/` classes are
31
32
  * not, and for the reason `AlarmScheduler` is: this one is a **binding**, so a
@@ -35,7 +35,7 @@
35
35
  * Upstream is the same: `getCriticalSection()` (`io-context.c++:362`) does
36
36
  * not touch `currentInputLock`, and `:1214` is the only place that clears
37
37
  * it. The difference between the two forms is entirely on the far side —
38
- * `awaitIo` re-enters through `run(func, criticalSection)` and queues for a
38
+ * `awaitIo` re-enters through `run(func, { input: criticalSection })` and queues for a
39
39
  * fresh lock, `awaitIoWithInputLock` re-enters holding the ref it took.
40
40
  * 4. Removal from the stack is by identity, not by popping, because entries do
41
41
  * overlap — three deep in the unit tests. One invocation can have several
@@ -97,10 +97,10 @@
97
97
  * makes impossible; hang detection and `registerPendingEvent`, which need the
98
98
  * isolate's own idea of pending work; and the thread-local
99
99
  * `IoContext::current()` static, whose lock-resolving half the invocation stack
100
- * replaces — its *identity* half is `currentSlice` below, narrowed to the
101
- * synchronous slice, with one consumer and no resolver. `EventOutcome` and
102
- * `RequestObserver` are metrics types with no port, so `waitUntilStatus()`
103
- * returns the first exception instead.
100
+ * replaces — its *identity* half is `currentSlice` below for synchronous code
101
+ * and the await transform's captured continuation for post-await code. Neither
102
+ * identity resolves a lock. `EventOutcome` and `RequestObserver` are metrics
103
+ * types with no port, so `waitUntilStatus()` returns the first exception instead.
104
104
  */
105
105
  import { CriticalSection, type InputGate, Lock, type OutputGate } from "./io-gate.js";
106
106
  /**
@@ -147,6 +147,11 @@ export interface Actor {
147
147
  export declare const BLOCK_CONCURRENCY_WHILE_TIMEOUT_MS = 30000;
148
148
  /** Copied verbatim: users and upstream tests match on it. */
149
149
  export declare const BLOCK_CONCURRENCY_WHILE_TIMEOUT_MESSAGE: string;
150
+ /** A critical-section failure reset the actor before its caller could continue. */
151
+ export declare class BrokenActorError extends Error {
152
+ readonly name = "BrokenActorError";
153
+ constructor(cause: unknown);
154
+ }
150
155
  /**
151
156
  * THE check every storage entry point in `api/` makes before touching the
152
157
  * database, and the only place this package throws for a missing input lock.
@@ -267,6 +272,10 @@ export declare function captureGateStack(): string | undefined;
267
272
  export declare function atCheckpointEnd(run: () => void): void;
268
273
  /** ← `IoContext::tryCurrent()` (`io-context.c++:1416-1422`), over the narrowed scope above. */
269
274
  export declare function tryCurrentSlice(): IoContext | undefined;
275
+ /** The actor continuation restored by the await transform, if one is running. */
276
+ export declare function tryCurrentContinuation(): IoContext | undefined;
277
+ /** The exact actor calling now, across both a synchronous slice and a transformed continuation. */
278
+ export declare function tryCurrentIoContext(): IoContext | undefined;
270
279
  /**
271
280
  * ← `jsg::isExceptionFromInputGateBroken` (`jsg/exception.c++:168-172`):
272
281
  * "annotateBroken() produces 'broken.inputGateBroken; {message}', optionally
@@ -320,6 +329,8 @@ export declare class IoContext {
320
329
  * actor's?
321
330
  */
322
331
  isCurrentSlice(): boolean;
332
+ /** Publish this transformed continuation through the same checkpoint queue as its lock. */
333
+ restoreContinuation(): void;
323
334
  /**
324
335
  * Record that user code just engaged this context's gate — an `awaitIo`, an
325
336
  * `entry` dispatch, a re-entry callback firing. No upstream analogue, because
@@ -425,9 +436,13 @@ export declare class IoContext {
425
436
  * ← the two `IoContext::run()` overloads: given a CriticalSection it waits on that, given
426
437
  * an already-held Lock it runs under it, and given neither it takes a fresh lock from the
427
438
  * gate. The third case is what a new external event does, and it is the reason inheritance
428
- * cannot be read from gate state — see `makeReentryCallback`.
439
+ * cannot be read from gate state — see `makeReentryCallback`. `signal` cancels only the wait
440
+ * for admission; once a lock is acquired the slice runs normally.
429
441
  */
430
- run<T>(func: (lock: Lock) => T | PromiseLike<T>, ilOrCs?: Lock | CriticalSection): Promise<T>;
442
+ run<T>(func: (lock: Lock) => T | PromiseLike<T>, options?: {
443
+ readonly input?: Lock | CriticalSection | undefined;
444
+ readonly signal?: AbortSignal | undefined;
445
+ }): Promise<T>;
431
446
  /**
432
447
  * Make a function which, when called, re-enters this IoContext to run some code.
433
448
  *
@@ -446,7 +461,7 @@ export declare class IoContext {
446
461
  *
447
462
  * It does not route through `io-gate.ts`'s `makeReentryCallback`, which is the same idea
448
463
  * expressed at the gate. Upstream's `IoContext::makeReentryCallback` is literally
449
- * `ctx.run(func, cs)`, and going through the gate helper instead would take a lock this
464
+ * `ctx.run(func, { input: cs })`, and going through the gate helper instead would take a lock this
450
465
  * file then has to make current a second time. The gate copy stays: it is the shape a
451
466
  * consumer holding only a gate needs, and Section 1's tests cover it.
452
467
  */
@@ -455,7 +470,7 @@ export declare class IoContext {
455
470
  * Waits for some background I/O to complete, then executes `func` on the result.
456
471
  *
457
472
  * The input lock is NOT held across the wait: the resumption re-enters through
458
- * `run(func, criticalSection)` and takes a fresh lock, so it queues behind whatever
473
+ * `run(func, { input: criticalSection })` and takes a fresh lock, so it queues behind whatever
459
474
  * arrived in the meantime. This is what makes a Durable Object awaiting another Durable
460
475
  * Object fully re-entrant (§1.3).
461
476
  *
@@ -485,8 +500,8 @@ export declare class IoContext {
485
500
  *
486
501
  * Three behaviours live here rather than in `io-gate.ts`, which has no timer, and rather
487
502
  * than in `api/actor-state.ts`, whose own `blockConcurrencyWhile` is a one-line forward:
488
- * the 30-second deadline, the brokenness annotation, and the fact that on failure the
489
- * returned promise is never settled at all.
503
+ * the 30-second deadline, the brokenness annotation, and the typed rejection returned to
504
+ * the same-realm caller after the actor is broken.
490
505
  */
491
506
  blockConcurrencyWhile<T>(callback: (lock: Lock) => T | PromiseLike<T>): Promise<T>;
492
507
  }
@@ -224,7 +224,7 @@ export declare class CriticalSection extends InputGate {
224
224
  }
225
225
  /**
226
226
  * ← the gate half of `IoContext::makeReentryCallback()` (`io-context.h:1507`), which is
227
- * `ctx.run(func, cs)` with the critical section captured here rather than looked up later.
227
+ * `ctx.run(func, { input: cs })` with the critical section captured here rather than looked up later.
228
228
  *
229
229
  * Upstream, on why the critical section travels with the callback at all:
230
230
  *