@mcp-b/do-runtime 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { a as getInt64, c as isNull, o as getText, r as SqliteDatabase, s as hasCurrentSqliteTable } from "../chunks/sqlite-DFg92Tgt.js";
1
+ import { a as SqliteDatabase, c as getText, l as hasCurrentSqliteTable, s as getInt64, t as ensureRuntimeStorageVersion, u as isNull } from "../chunks/sqlite-migrations-DsWmLP_B.js";
2
2
  //#region src/server/alarm-scheduler.ts
3
3
  /**
4
4
  * ← `WorkerInterface::ALARM_RETRY_START_SECONDS` (`io/worker-interface.h:130`),
@@ -152,6 +152,7 @@ var AlarmScheduler = class {
152
152
  this.#random = options.random ?? Math.random;
153
153
  this.#getActor = options.getActor;
154
154
  this.#projectWake = options.projectWake;
155
+ ensureRuntimeStorageVersion(options.db, "alarms");
155
156
  this.#db = new SqliteDatabase(options.db);
156
157
  ensureInitialized(this.#db);
157
158
  this.#loadAlarmsFromDb();
@@ -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 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\";\nimport { ensureRuntimeStorageVersion } from \"../util/sqlite-migrations\";\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 ensureRuntimeStorageVersion(options.db, \"alarms\");\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":";;;;;;;;;AAoDA,IAAa,4BAA4B;;;;;;;;;;AAWzC,IAAa,wBAAwB;;;;;;;;AASrC,IAAa,oBAAoB;;;;;;;AAQjC,IAAa,sBAAsB;;;;;;;;;;;AAYnC,SAAgB,kBAAkB,SAAyB;CACzD,IAAI,CAAC,OAAO,UAAU,OAAO,KAAK,UAAU,KAAK,UAAA,GAC/C,MAAM,IAAI,MAAM,uBAAuB,QAAQ,oBAAuC;CAExF,QAAA,KAAqC,WAAW;AAClD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,IAAM,OAAO;CACX,aAAa;;;;;;;;;;;CAWb,YAAY;;;;;CAWZ,UAAU;;;;;;;;;CASV,aAAa;;;CAMb,WAAW;;;;;;CAMX,cAAc;;;CAGd,aAAa;;;CAGb,WAAW;;;AAGb;;;;;AA0JA,IAAa,iBAAb,MAA4B;CAC1B;CACA;CACA;CACA;CACA;;CAEA,0BAAmB,IAAI,IAA4B;;CAEnD,yBAAkB,IAAI,IAAmB;CACzC;CACA,cAA6B,QAAQ,QAAQ;CAE7C,YAAY,SAAgC;EAC1C,KAAKA,SAAS,QAAQ;EACtB,KAAKC,UAAU,QAAQ,UAAU,KAAK;EACtC,KAAKC,YAAY,QAAQ;EACzB,KAAKC,eAAe,QAAQ;EAC5B,4BAA4B,QAAQ,IAAI,QAAQ;EAChD,KAAKC,MAAM,IAAI,eAAe,QAAQ,EAAE;EACxC,kBAAkB,KAAKA,GAAG;EAC1B,KAAKG,kBAAkB;EACvB,KAAKC,iBAAiB;CACxB;;;;;;CAOA,SAAS,SAAgC;EACvC,MAAM,QAAQ,KAAKH,QAAQ,IAAI,OAAO;EACtC,IAAI,UAAU,KAAA,GAEZ,OAAO;EAET,IAAI,MAAM,WAAW,WAEnB,OAAO,MAAM;EAEf,OAAO,MAAM;CACf;;;;;;;;;;CAWA,SAAS,SAAiB,eAAgC;EACxD,MAAM,QAAQ,KAAKD,IAAI,IAAI,KAAK,UAAU,SAAS,aAAa;EAEhE,MAAM,QAAQ,KAAKC,QAAQ,IAAI,OAAO;EACtC,IAAI,UAAU,KAAA,GACZ,KAAKA,QAAQ,IAAI,SAAS,KAAKI,eAAe,KAAKT,OAAO,IAAI,GAAG,SAAS,aAAa,CAAC;OACnF,IAAI,MAAM,WAAW,WAG1B,MAAM,cAAc;OAEpB,KAAKU,SAAS,OAAO,KAAKD,eAAe,KAAKT,OAAO,IAAI,GAAG,SAAS,aAAa,CAAC;EAGrF,KAAKQ,iBAAiB;EAEtB,OAAO,MAAM,cAAc;CAC7B;;CAGA,YAAkB;EAIhB,KAAK,MAAM,SAAS,KAAKH,QAAQ,OAAO,GAAG,MAAM,OAAO,MAAM;EAC9D,KAAKA,QAAQ,MAAM;EAEnB,KAAKD,IAAI,IAAI,KAAK,SAAS;EAC3B,KAAKI,iBAAiB;CACxB;;CAGA,YAAY,SAA0B;EACpC,MAAM,QAAQ,KAAKJ,IAAI,IAAI,KAAK,aAAa,OAAO;EAEpD,MAAM,QAAQ,KAAKC,QAAQ,IAAI,OAAO;EACtC,IAAI,UAAU,KAAA,GAAW;GACvB,MAAM,SAAS,MAAM;GACrB,IAAI,WAAW,MAAM;IACnB,IAAI,MAAM,WAAW,WAEnB,MAAM,cAAc;SAEpB,KAAKK,SAAS,OAAO,KAAKD,eAAe,KAAKT,OAAO,IAAI,GAAG,SAAS,MAAM,CAAC;GAEhF,OAAO,IAAI,MAAM,WAAW,WAAW;IAErC,MAAM,OAAO,MAAM;IACnB,KAAKK,QAAQ,OAAO,OAAO;GAC7B;EACF;EAEA,KAAKG,iBAAiB;EAEtB,OAAO,MAAM,cAAc;CAC7B;;;;;;CAOA,MAAM,SAA8B;EAClC,OAAO,EAKL,cAAc,eAA8B,eAA6C;GACvF,IAAI,kBAAkB,MAAM,KAAK,SAAS,SAAS,aAAa;QAC3D,KAAK,YAAY,OAAO;GAC7B,OAAO,KAAKG;EACd,EACF;CACF;;;;;;;;;;;CAYA,cAAuB;EACrB,OAAO,KAAKC,cAAc;CAC5B;;;;;;;;;;CAaA,oBAA0B;EACxB,MAAM,MAAM,KAAKZ,OAAO,IAAI;EAI5B,KAAK,MAAM,OAAO,KAAKI,IAAI,IAAI,KAAK,UAAU,CAAC,CAAC,SAAS;GACvD,MAAM,UAAU,QAAQ,KAAK,CAAC;GAC9B,MAAM,gBAAgB,SAAS,KAAK,CAAC;GACrC,MAAM,YAAY,mBAAmB,SAAS,GAAG;GACjD,MAAM,UAAU,UAAU,UACtB,KAAKS,4BAA4B,SAAS,KAAK,SAAS,IACxD;GACJ,KAAKR,QAAQ,IAAI,SAAS,KAAKI,eAAe,KAAK,SAAS,eAAe,OAAO,CAAC;EACrF;CACF;;;;;;;;;;;;;;;;;;;CAoBA,4BACE,SACA,KACA,WACgB;EAChB,MAAM,UAAU,KAAK,IAAA,GAAuB,UAAU,OAAO;EAC7D,IAAI,QAAQ,kBAAkB,OAAO;EACrC,SAAS,KAAKK,kBAAkB,KAAK;EAErC,MAAM,UAA0B;GAC9B,WAAW,MAAM;GACjB,SAAS,UAAU;GACnB,cAAc,UAAU;GAIxB,kCAAkC;GAClC,SAAS;EACX;EACA,KAAKV,IAAI,IACP,KAAK,WACL,QAAQ,WACR,QAAQ,SACR,QAAQ,cACR,GACA,OACF;EACA,OAAO;CACT;;CAGA,eACE,KACA,SACA,eACA,SACgB;EAKhB,MAAM,QAAwB;GAC5B;GACA;GACA,UAAU;GACV,MAAM,KAAA;GACN,QAAQ,IAAI,gBAAgB;GAC5B,aAAa;GACb,QAAQ;GACR,kCAAkC,SAAS,oCAAoC;GAC/E,SAAS,SAAS,WAAW;GAC7B,cAAc,SAAS,gBAAgB;EACzC;EAKA,MAAM,OAAO,KAAK,IAAI,eAAe,SAAS,aAAa,aAAa;EACxE,MAAM,WAAW;EACjB,MAAM,OAAO,KAAKW,eAAe,OAAO,KAAK,OAAO,aAAa;EACjE,OAAO;CACT;;CAGA,SAAS,UAA0B,MAA4B;EAC7D,SAAS,OAAO,MAAM;EACtB,KAAKV,QAAQ,IAAI,KAAK,SAAS,IAAI;CACrC;;CAGA,MAAMW,gBAAgB,OAAe,eAAuB,QAAoC;EAC9F,IAAI,YAAY;EAChB,SAAS;GACP,MAAM,KAAKhB,OAAO,WAAW,WAAW,MAAM;GAK9C,MAAM,MAAM,KAAKA,OAAO,IAAI;GAC5B,IAAI,OAAO,eAAe;GAG1B,YAAY,gBAAgB;EAC9B;CACF;;CAGA,MAAMiB,UAAU,SAAiB,eAAuB,YAAwC;EAC9F,MAAM,SAAS,MAAM,KAAKf,UAAU,OAAO,CAAC,CAAC,aAAa,eAAe,UAAU;EACnF,OAAO;GACL,OAAO,OAAO,YAAY,QAAQ,OAAO;GACzC,yBAAyB,OAAO;EAClC;CACF;;CAGA,MAAMgB,iBACJ,SACA,eACA,YACoB;EACpB,IAAI;GACF,OAAO,MAAM,KAAKD,UAAU,SAAS,eAAe,UAAU;EAChE,SAAS,WAAW;GAClB,KAAKE,YAAY,SAAS;GAC1B,OAAO;IACL,OAAO;IAIP,yBAAyB;GAC3B;EACF;CACF;;CAGA,MAAMJ,eAAe,OAAe,OAAuB,eAAsC;EAC/F,MAAM,UAAU,MAAM;EACtB,MAAM,KAAKC,gBAAgB,OAAO,eAAe,MAAM,OAAO,MAAM;EAIpE,IAAI,KAAKX,QAAQ,IAAI,OAAO,MAAM,OAAO;EAOzC,IAAI;GACF,KAAKD,IAAI,IAAI,KAAK,aAAa,OAAO;EACxC,SAAS,WAAW;GAClB,KAAKe,YAAY,SAAS;GAC1B;EACF;EAEA,MAAM,SAAS;EACf,MAAM,WAAW;EACjB,KAAKX,iBAAiB;EACtB,MAAM,aAAa,MAAM;EAEzB,MAAM,YAAY,MAAM,KAAKU,iBAAiB,SAAS,eAAe,UAAU;EAEhF,IAAI;GAGF,IAAI,KAAKb,QAAQ,IAAI,OAAO,MAAM,OAAO;GAIzC,MAAM,OAAO,MAAM;GACnB,IAAI,SAAS,KAAA,GAAW,MAAM,IAAI,MAAM,2CAA2C;GACnF,KAAKe,SAAS,IAAI;GAClB,MAAM,OAAO,KAAA;GAIb,MAAM,SAAS,MAAM;GACrB,IAAI,WAAW,MAAM;IAInB,KAAKhB,IAAI,IAAI,KAAK,cAAc,OAAO;IAGvC,KAAKM,SAAS,OAAO,KAAKD,eAAe,KAAKT,OAAO,IAAI,GAAG,SAAS,MAAM,CAAC;IAC5E,KAAKQ,iBAAiB;IACtB;GACF;GAMA,MAAM,SAAS;GAEf,IAAI,UAAU,OAAO;IAEnB,IAAI,MAAM,gBAAA,GAAuC;KAC/C,MAAM,KAAKa,SAAS,OAAO,aAAa;KACxC;IACF;IACA,IAAI,UAAU,yBAAyB;KACrC,MAAM,gBAAgB;KAEtB,IAAI,CAAC,MAAM,kCAOT,MAAM,UAAU;IAEpB;IACA,MAAM,mCAAmC,UAAU;IAEnD,MAAM,UAAU,KAAK,IAAA,GAAuB,MAAM,OAAO;IACzD,IAAI,aAAa,kBAAkB,MAAM,OAAO;IAEhD,cAAc,KAAKP,kBAAkB,UAAU;IAE/C,MAAM,WAAW;IAMjB,MAAM,YAAY,KAAKd,OAAO,IAAI,IAAI;IACtC,KAAKI,IAAI,IACP,KAAK,WACL,WACA,MAAM,SACN,MAAM,cACN,MAAM,mCAAmC,IAAI,GAC7C,OACF;IAEA,MAAM,WAAW;IACjB,MAAM,OAAO,KAAKW,eAAe,YAAY,OAAO,aAAa;IACjE,KAAKP,iBAAiB;GACxB,OAAO;IACL,IAAI,MAAM,gBAAgB,MACxB,MAAM,IAAI,MAAM,mEAAmE;IAErF,KAAK,YAAY,OAAO;GAC1B;EACF,SAAS,WAAW;GAElB,KAAKW,YAAY,SAAS;EAC5B;CACF;;;;;;;;;;;;;;;;;;;;;;;CAwBA,MAAME,SAAS,OAAuB,eAAsC;EAC1E,MAAM,UAAU,MAAM;EACtB,IAAI;EACJ,IAAI;GACF,aAAa,MAAM,KAAKnB,UAAU,OAAO,CAAC,CAAC,aAAa,aAAa;EACvE,SAAS,WAAW;GAClB,KAAKiB,YAAY,SAAS;GAC1B;EACF;EACA,KAAK,YAAY,OAAO;EACxB,IAAI,eAAe,QAAQ,CAAC,KAAKd,QAAQ,IAAI,OAAO,GAClD,KAAK,SAAS,SAAS,UAAU;CAErC;;;;;;CAOA,kBAAkB,SAAyB;EACzC,MAAM,MAAM,KAAK,MAAM,sBAAsB,OAAO;EACpD,OAAO,KAAK,IAAI,KAAK,KAAK,MAAM,KAAKJ,QAAQ,KAAK,MAAM,EAAE,CAAC;CAC7D;;CAGA,mBAAyB;EACvB,IAAI,KAAKE,iBAAiB,KAAA,GAAW;EACrC,IAAI,WAA0B;EAC9B,KAAK,MAAM,SAAS,KAAKE,QAAQ,OAAO,GAAG;GACzC,MAAM,OAAO,MAAM,WAAW,YAAY,MAAM,cAAc,MAAM;GACpE,IAAI,SAAS,SAAS,aAAa,QAAQ,OAAO,WAAW,WAAW;EAC1E;EACA,KAAKM,cAAc,QAAQ,QAAQ,KAAKR,aAAa,QAAQ,CAAC;EAC9D,KAAUQ,YAAY,OAAO,cAAuB,KAAKQ,YAAY,SAAS,CAAC;CACjF;;CAGA,SAAS,MAA2B;EAClC,MAAM,UAAU,KAAK,WACb;GACJ,KAAKb,OAAO,OAAO,OAAO;EAC5B,IACC,cAAuB;GACtB,KAAKA,OAAO,OAAO,OAAO;GAC1B,KAAKa,YAAY,SAAS;EAC5B,CACF;EACA,KAAKb,OAAO,IAAI,OAAO;CACzB;;CAGA,YAAY,WAA0B;EACpC,KAAKM,iBAAiB,EAAE,UAAU;CACpC;AACF;;;;;;;;;;;;;;;;;;AAmBA,SAAS,mBAAmB,SAAiB,KAAyC;CACpF,MAAM,YAAY,OAAO,KAAK,CAAC,IAAI,OAAO,SAAS,KAAK,CAAC;CACzD,MAAM,UAAU,aAAa,SAAS,WAAW,SAAS,KAAK,CAAC,GAAG,EAAqB;CACxF,MAAM,eAAe,aACnB,SACA,iBACA,SAAS,KAAK,CAAC,GAAA,CAEjB;CACA,MAAM,WAAW,aAAa,SAAS,0BAA0B,SAAS,KAAK,CAAC,GAAG,CAAC;CACpF,MAAM,UAAU,aAAa,SAAS,WAAW,SAAS,KAAK,CAAC,GAAG,CAAC;CACpE,OAAO;EACL;EACA;EACA;EACA,kCAAkC,aAAa;EAC/C,SAAS,YAAY;CACvB;AACF;AAEA,SAAS,aAAa,SAAiB,QAAgB,OAAe,KAAqB;CACzF,IAAI,QAAQ,KAAK,QAAQ,KACvB,MAAM,IAAI,MACR,SAAS,OAAO,GAAG,MAAM,aAAa,QAAQ,kBAAkB,IAAI,+DAEtE;CAEF,OAAO;AACT;;AAGA,SAAS,kBAAkB,IAA0B;CACnD,sBAAsB,IAAI,aAAa,KAAK,WAAW;CACvD,GAAG,IAAI,KAAK,WAAW;AACzB"}
@@ -95,6 +95,11 @@ export declare const SqlStorageRegulator: {
95
95
  /** "Bill for queries executed from JavaScript." Nothing reads it — `SqliteObserver` has no port. */
96
96
  shouldAddQueryStats(): boolean;
97
97
  };
98
+ /**
99
+ * ← the message a `SQLITE_DENY` from the authorizer surfaces to JavaScript,
100
+ * byte-identical so a caller matching on it ports unchanged.
101
+ */
102
+ export declare const SQL_NOT_AUTHORIZED_MESSAGE = "not authorized: SQLITE_AUTH";
98
103
  /**
99
104
  * ← the `jsg::Ref<DurableObjectStorage>` `SqlStorage` holds, narrowed to the one
100
105
  * member it reaches through (`SqlStorage::getDb`). `DurableObjectStorage`
@@ -124,8 +129,12 @@ type CursorState = {
124
129
  */
125
130
  export declare class Cursor<T extends SqlRow = SqlRow> implements SqlStorageCursor<T> {
126
131
  #private;
127
- readonly columnNames: string[];
128
132
  constructor(state?: CursorState);
133
+ /**
134
+ * ← `JSG_READONLY_PROTOTYPE_PROPERTY(columnNames)` (`sql.h:210`): a
135
+ * prototype accessor, not an own field, so a cursor JSON-stringifies to `{}`.
136
+ */
137
+ get columnNames(): string[];
129
138
  /** ← `Cursor::next`, whose `RowIterator::Next` is this exact shape. */
130
139
  next(): {
131
140
  done?: false;
@@ -138,9 +147,12 @@ export declare class Cursor<T extends SqlRow = SqlRow> implements SqlStorageCurs
138
147
  toArray(): T[];
139
148
  /** ← `Cursor::one`. Both messages are upstream's, verbatim. */
140
149
  one(): T;
141
- /** ← `Cursor::raw`, which shares this cursor's position rather than restarting. */
150
+ /**
151
+ * ← `Cursor::raw`, which shares this cursor's position rather than
152
+ * restarting. The iterator's shape is `RawIterator`'s whole doc comment.
153
+ */
142
154
  raw<U extends SqlStorageValue[]>(): IterableIterator<U>;
143
- /** ← `JSG_ITERABLE(rows)`. */
155
+ /** ← `JSG_ITERABLE(rows)`, yielding through the same shared position. */
144
156
  [Symbol.iterator](): IterableIterator<T>;
145
157
  get rowsRead(): number;
146
158
  /** ← `Cursor::getRowsWritten`, which is `SqlResult.rowsWritten` here. */
@@ -7,10 +7,14 @@ type Outcome<T> = {
7
7
  readonly exception: unknown;
8
8
  };
9
9
  declare const TRANSFORMED_AWAIT: unique symbol;
10
+ type PublicationReservation = {
11
+ readonly context: IoContext;
12
+ };
10
13
  type TransformedAwait<T> = {
11
14
  readonly [TRANSFORMED_AWAIT]: true;
12
15
  readonly context: IoContext;
13
16
  readonly outcome: Outcome<T>;
17
+ readonly reservation: PublicationReservation;
14
18
  };
15
19
  /** Re-enter the actor that owns this transformed await; fail open outside actors. */
16
20
  export declare function __gate<T>(value: T): T | Promise<Awaited<T>>;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Runtime storage versioning. Package-original — workerd has no counterpart,
3
+ * because Cloudflare upgrades workerd and its storage together. This package is
4
+ * an npm dependency over data it does not control: OPFS files in a browser and
5
+ * SQLite files on disk outlive any release, so the release that changes a
6
+ * `_cf_` shape has to bring existing files forward itself.
7
+ *
8
+ * The mechanism is the Cloudflare Agents SDK's `_ensureSchema` pattern moved
9
+ * one layer down: a stored schema version, a cumulative list of forward-only
10
+ * idempotent migration steps, and a fast path that skips everything when the
11
+ * stored version is current. Where an Agent runs this in its constructor
12
+ * (before any event is delivered), the runtime runs it at database open —
13
+ * `createActorContainer()` and `AlarmScheduler`'s constructor — which precedes
14
+ * every event by construction, so no `blockConcurrencyWhile` is involved.
15
+ *
16
+ * The version lives in `PRAGMA user_version`: per database file, carried by
17
+ * snapshots, transactional. Application SQL cannot reach it — `SqlStorage`
18
+ * ports workerd's pragma allowlist (`util/sqlite.c++:539-563`), which does not
19
+ * include `user_version` — so the value is runtime-owned by the same rule that
20
+ * reserves `_cf_` names.
21
+ *
22
+ * Rules for a step, when the first real one is written:
23
+ *
24
+ * - **Forward-only.** Never edit or reorder a shipped step; add the next one
25
+ * and bump `RUNTIME_STORAGE_VERSION`.
26
+ * - **Runtime tables only, guarded on existence.** Every database file holds
27
+ * a different subset of runtime tables (`_cf_KV`/`_cf_METADATA` in an actor
28
+ * database, the facet index and deletion receipts in the root's facet
29
+ * database, `_cf_ALARM` in a scheduler's), creation is lazy, and a database
30
+ * handed to `AlarmScheduler` is host-opened and may hold host tables beside
31
+ * the runtime's — a step must no-op where its table is absent and must
32
+ * never touch a table the runtime does not own.
33
+ * - **Idempotent against the current shape too** (`IF NOT EXISTS`, tolerate
34
+ * "duplicate column"): `deleteAll()` resets a file to version 0 while this
35
+ * release recreates its tables at the current shape, so a later chain run
36
+ * can meet already-current tables.
37
+ * - **No transaction control.** The chain and the stamp are one transaction;
38
+ * a step that issues `BEGIN`/`COMMIT`/`ROLLBACK`/`SAVEPOINT` is refused by
39
+ * name below rather than silently splitting it.
40
+ *
41
+ * Spec: decision 19 in docs/decisions.md.
42
+ */
43
+ import { type SqlDatabase, type SqlDatabaseSnapshot } from "./sqlite.js";
44
+ /**
45
+ * The shape of every runtime-owned table in this release. Bump together with
46
+ * the step that upgrades the previous shape.
47
+ */
48
+ export declare const RUNTIME_STORAGE_VERSION = 1;
49
+ export type RuntimeMigration = (db: SqlDatabase) => void;
50
+ /**
51
+ * Bring one just-opened runtime database to `RUNTIME_STORAGE_VERSION`. Called
52
+ * by every seam that opens a runtime database, before anything reads it, with
53
+ * the database's own name so a refusal says which file it is about. The last
54
+ * two parameters exist for the tests in this module's test file; every real
55
+ * caller takes the shipped defaults.
56
+ *
57
+ * A version newer than this release refuses — the analogue of
58
+ * `hasCurrentSqliteTable`'s refusal, with the one remedy named. A version 0
59
+ * database is from before versioning existed (the same shape as version 1) or
60
+ * a fresh file; both enter the chain at 1. Pending steps and the stamp commit
61
+ * as one transaction, so a failed step leaves the file exactly as it was and
62
+ * the container placement fails with the step's error.
63
+ */
64
+ /**
65
+ * Refuse a snapshot image stamped by a newer release at the import seam, where
66
+ * the operation that brought the file in is the one that fails — instead of at
67
+ * the next placement, far from the cause. `user_version` sits at byte 60 of
68
+ * the SQLite header, big-endian (https://www.sqlite.org/fileformat2.html);
69
+ * callers validate the header shape first (`requireValidSqlDatabaseSnapshot`).
70
+ */
71
+ export declare function requireImportableRuntimeStorage(snapshot: SqlDatabaseSnapshot, current?: number): void;
72
+ export declare function ensureRuntimeStorageVersion(db: SqlDatabase, name: string, current?: number, migrations?: readonly RuntimeMigration[]): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-b/do-runtime",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "description": "Cloudflare's Durable Object runtime (workerd), ported to TypeScript: actors with input/output gates, SQLite storage, facets and alarms, running in the browser and in Node.",
5
5
  "keywords": [
6
6
  "actors",
@@ -93,6 +93,8 @@
93
93
  "@types/node": "^26.1.2",
94
94
  "@types/ungap__structured-clone": "1.2.0",
95
95
  "@vitest/browser-playwright": "4.1.10",
96
+ "@vitest/coverage-v8": "4.1.10",
97
+ "drizzle-orm": "^0.45.2",
96
98
  "playwright": "1.61.1",
97
99
  "publint": "0.3.23",
98
100
  "typescript": "^5.9.3",
@@ -1 +0,0 @@
1
- {"version":3,"file":"sqlite-DFg92Tgt.js","names":["#backend","#resetListeners","#onWriteCallback","#onCriticalErrorCallback","#exec","#execStatement","#checkForAutoRollback","#applyChange","#inTransaction","#savepoints","#rollbackCallbacks","#criticalError","#runRollbackCallbacksDownTo"],"sources":["../../src/util/sqlite.ts"],"sourcesContent":["/**\n * ← workerd `src/workerd/util/sqlite.{h,c++}`\n *\n * The SQL backend port. Upstream's seam is the same one: `server.c++` opens\n * `<actor-id>.<facetId>.sqlite` and hands `ActorSqlite` a `SqliteDatabase`.\n *\n * Almost none of upstream's 3,768 lines are ours. `sqlite.{h,c++}` is workerd's\n * binding to the SQLite C API — statement caching, the VFS, regulators, the\n * authorizer, the memory-metering allocator. Underneath us that role is played\n * by `node:sqlite` and sqlite-wasm, which is the storage-backend adaptation the\n * design record sanctions: `io-gate.h` knows nothing about SQLite, `ActorSqlite`\n * calls into it, and that seam is upstream's rather than ours.\n *\n * So this file is two things stacked:\n *\n * 1. `SqlDatabase` / `SqlDatabaseProvider` — the backend seam. `backends/`\n * implements it, and nothing above `util/` ever sees a driver type.\n * 2. `SqliteDatabase` — the small part of upstream's class that is genuinely\n * ours, because the layers above call into it and a stateless exec interface\n * cannot express it: `onRollback`, the transaction/savepoint stack it needs,\n * `reset()` and its `ResetListener` notification, all three of which\n * `sqlite-kv.c++` and `sqlite-metadata.c++` reach for; plus `onWrite`,\n * `notifyWrite` and `onCriticalError`, which only `io/actor-sqlite.ts`\n * reaches for. That last trio lives here rather than one directory up\n * because it lives here upstream (`sqlite.h:240`, `:248`, `:267`): a\n * callback slot is not actor knowledge, and a reader who finds `onWrite` in\n * `sqlite.h` has to find it in this file too. Every consumer takes a\n * `SqliteDatabase`, exactly as upstream's take a `SqliteDatabase&`.\n *\n * `transactionSync` is NOT here — it lives in `io/actor-sqlite.ts` as\n * SAVEPOINT/RELEASE with a depth counter, exactly as upstream has it. Today\n * both browser and Node adapters duplicate `BEGIN IMMEDIATE`, which is why a\n * nested call is a live SQLite error (§2.4). Moving it inward fixes that.\n *\n * Not ported, because the substrate has no equivalent: the `Regulator` /\n * authorizer machinery (there is no untrusted-SQL path in `util/`, and\n * `api/sql.ts` owns that question); `SqliteObserver` row-count billing, whose\n * counters are libsql `STMTSTATUS` extensions neither backend exposes;\n * `sqlite-metering.{h,c++}`, which swaps SQLite's process-wide allocator to\n * meter per-database memory — a C-API facility with no JS analogue; and the\n * point-in-time-recovery APIs, a named substrate boundary in the package README.\n *\n * Spec: §1.4, §2.4 in docs/decisions.md.\n */\n\n/** The four values SQLite itself accepts after public JSG-style conversion. */\nexport type SqlValue = string | number | null | Uint8Array;\n\nexport type SqlResult = {\n readonly columnNames: readonly string[];\n readonly rawRows: readonly (readonly unknown[])[];\n /** Rows changed by this statement, including DML with `RETURNING`. */\n readonly rowsWritten: number;\n};\n\n/** ← `SqliteDatabase::IngestResult`. */\nexport type SqlIngestResult = {\n readonly remainder: string;\n readonly rowsRead: number;\n readonly rowsWritten: number;\n readonly statementCount: number;\n};\n\n/**\n * One SQLite-compiled statement from the front of a SQL string.\n *\n * `sql` is the exact prefix SQLite consumed, including trigger bodies. Keeping\n * that boundary on the backend is what prevents JavaScript from inventing a\n * second, subtly different SQL grammar.\n */\nexport interface SqlDatabaseStatement {\n readonly sql: string;\n readonly parameterCount: number;\n execute(params: readonly SqlValue[]): SqlResult;\n close(): void;\n}\n\nexport const SQL_WRONG_BINDINGS_MESSAGE = \"Wrong number of parameter bindings for SQL query.\";\n\n/** ← `SQLITE_LIMIT_LENGTH`, raised from 2.2 MB to 4 MiB in workerd 2026-08-20. */\nexport const SQLITE_LENGTH_LIMIT = 4 * 1024 * 1024;\n\nexport const SQLITE_TOOBIG_MESSAGE = \"string or blob too big: SQLITE_TOOBIG\";\n\nconst textEncoder = new TextEncoder();\n\n/** The part of `sqlite3_limit(SQLITE_LIMIT_LENGTH)` visible at the JS binding seam. */\nexport function requireSqliteLength(value: unknown): void {\n const length =\n typeof value === \"string\"\n ? textEncoder.encode(value).byteLength\n : value instanceof Uint8Array\n ? value.byteLength\n : 0;\n if (length > SQLITE_LENGTH_LIMIT) throw new Error(SQLITE_TOOBIG_MESSAGE);\n}\n\nexport const SQL_PRELUDE_BINDINGS_MESSAGE =\n \"When executing multiple SQL statements in a single call, only the last statement can have \" +\n \"parameters.\";\n\n/**\n * One open database. Synchronous exec, matching every substrate we have: in a\n * SQLite-backed Durable Object reads return a value rather than a promise\n * (§1.4), which is what makes the input gate cheap.\n */\nexport interface SqlDatabase {\n /** Compile exactly the first statement, using SQLite's own statement boundary. */\n prepare(sql: string): SqlDatabaseStatement;\n exec(sql: string, params: readonly SqlValue[]): SqlResult;\n readonly databaseSize: number;\n /**\n * ← `sqlite3_get_autocommit(db) == 0`, which is how upstream's\n * `handleCriticalError` learns that SQLite rolled a transaction back on its\n * own (`sqlite.c++:669-691`).\n *\n * SQLite auto-rolls-back on `SQLITE_FULL`, `SQLITE_IOERR`, `SQLITE_NOMEM` and\n * `SQLITE_INTERRUPT`. Nothing announces it, so without this the savepoint\n * stack above would keep believing a transaction is open and the rollback\n * callbacks would never fire — a stale cache with nothing thrown, which is\n * the one failure this layer must never produce.\n */\n readonly inTransaction: boolean;\n /**\n * ← `SqliteDatabase::reset()` — \"delete the underlying database file and\n * create a new one in its place\", which is how upstream implements\n * `deleteAll()`.\n *\n * On the backend rather than above it because only the backend knows how to\n * recreate its own file, and because the alternative — enumerating and\n * dropping every table — is the fragile dance today's `storage.ts` performs,\n * complete with an FTS5 shadow-table ordering hazard its comment documents.\n * The `SqlDatabase` reference stays valid across the call; what changes is\n * the file behind it.\n */\n reset(): void;\n close(): void;\n}\n\n/**\n * Opens databases within ONE actor's storage scope. The package derives the\n * names (`\"root\"`, `` `facet-${facetId}` ``); the host maps them onto files.\n * OPFS layout knowledge stays with the host — this package never reaches for\n * `navigator.storage`.\n */\nexport interface SqlDatabaseProvider {\n open(name: string): Promise<SqlDatabase>;\n}\n\n/** A portable, host-owned image of every SQLite database in one actor storage scope. */\nexport type SqlDatabaseSnapshot = {\n readonly version: 1;\n readonly databases: readonly {\n readonly name: string;\n readonly image: Uint8Array;\n }[];\n};\n\n/** Local backup/restore. This is deliberately not Cloudflare's time-indexed PITR service. */\nexport interface SqlDatabaseSnapshotProvider extends SqlDatabaseProvider {\n /** Close every database opened through this provider before snapshot or placement teardown. */\n close(): void;\n exportSnapshot(): Promise<SqlDatabaseSnapshot>;\n importSnapshot(snapshot: SqlDatabaseSnapshot): Promise<void>;\n}\n\nconst SAFE_DATABASE_NAME = /^[A-Za-z0-9_-]+$/;\nconst SQLITE_HEADER = \"SQLite format 3\";\n\nexport function requireSafeDatabaseName(name: string): void {\n if (!SAFE_DATABASE_NAME.test(name)) {\n throw new Error(`Database name is not a safe file name: ${name}`);\n }\n}\n\n/** Validate the complete snapshot before a backend replaces any files. */\nexport function requireValidSqlDatabaseSnapshot(snapshot: SqlDatabaseSnapshot): void {\n const candidate: unknown = snapshot;\n if (\n candidate === null ||\n typeof candidate !== \"object\" ||\n !(\"version\" in candidate) ||\n candidate.version !== 1 ||\n !(\"databases\" in candidate) ||\n !Array.isArray(candidate.databases)\n ) {\n throw new Error(\"Unsupported SQLite snapshot format.\");\n }\n const names = new Set<string>();\n for (const database of candidate.databases) {\n if (\n database === null ||\n typeof database !== \"object\" ||\n !(\"name\" in database) ||\n typeof database.name !== \"string\" ||\n !(\"image\" in database)\n ) {\n throw new Error(\"SQLite snapshot contains an invalid database entry.\");\n }\n const { name, image } = database;\n requireSafeDatabaseName(name);\n if (names.has(name)) {\n throw new Error(`SQLite snapshot contains duplicate database name: ${name}`);\n }\n names.add(name);\n if (\n !(image instanceof Uint8Array) ||\n image.byteLength < 512 ||\n image.byteLength % 512 !== 0 ||\n [...SQLITE_HEADER].some((character, index) => image[index] !== character.charCodeAt(0))\n ) {\n throw new Error(`Snapshot entry ${name} is not a valid SQLite database image.`);\n }\n }\n}\n\n/**\n * ← `SqliteDatabase::QueryOptions`. The C++ regulator pointer is narrowed to\n * a callback over the exact SQL source SQLite compiled; `api/sql.ts` owns the\n * policy because this backend seam owns no public-API knowledge.\n *\n * `allowUnconfirmed`'s only destination is `onWrite(bool allowUnconfirmed)`,\n * which fires *before* the statement executes so the automatic transaction\n * opens first — see `isWrite` below for how a statement is known to be a write\n * without the compiled plan upstream reads it from.\n */\nexport type QueryOptions = {\n allowUnconfirmed?: boolean;\n /** The public SQL regulator, run once against each SQLite-compiled statement. */\n regulate?: (sql: string) => void;\n};\n\n/**\n * ← the state `SqliteDatabase::onCriticalError` reports and\n * `observedCriticalError()` latches.\n *\n * Raised when SQLite has rolled back an open transaction on its own. Upstream\n * hands this to `ActorSqlite`, which treats it as fatal; §1.6 is why — a\n * storage failure this severe destroys the object rather than being survived.\n * Until `io/actor-sqlite.ts` wires it to `onBroken`, latching it and refusing\n * every subsequent statement is what keeps a caller from reading through a\n * cache that is knowingly wrong.\n */\nexport class SqliteCriticalError extends Error {\n override readonly name = \"SqliteCriticalError\";\n}\n\n/**\n * ← `SqliteDatabase::ResetListener`.\n *\n * Upstream's is a base class whose constructor registers and whose destructor\n * unregisters. JS has neither, so registration is the explicit\n * `db.addResetListener(this)` call — the same translation Section 1 applied to\n * every kj destructor.\n */\nexport interface ResetListener {\n /** Called before the database is actually reset. */\n beforeSqliteReset(): void;\n}\n\n/** ← `SqliteDatabase::Query::isNull(uint column)`. */\nexport function isNull(row: readonly unknown[], column: number): boolean {\n return row[column] === null || row[column] === undefined;\n}\n\n/** ← `SqliteDatabase::Query::getBlob(uint column)`. Fails closed on any other column type. */\nexport function getBlob(row: readonly unknown[], column: number): Uint8Array {\n const value = row[column];\n if (value instanceof Uint8Array) return value;\n throw new Error(`Expected a BLOB in column ${column}, got ${describe(value)}.`);\n}\n\n/** ← `SqliteDatabase::Query::getText(uint column)`. Fails closed on any other column type. */\nexport function getText(row: readonly unknown[], column: number): string {\n const value = row[column];\n if (typeof value === \"string\") return value;\n throw new Error(`Expected TEXT in column ${column}, got ${describe(value)}.`);\n}\n\n/**\n * ← `SqliteDatabase::Query::getInt64(uint column)`.\n *\n * Narrowed to a safe integer rather than upstream's `int64_t`: a JS number\n * cannot carry the full range, and a silently-rounded row id or alarm time is\n * exactly the kind of corruption this layer must not produce.\n */\nexport function getInt64(row: readonly unknown[], column: number): number {\n const value = row[column];\n if (typeof value === \"number\" && Number.isSafeInteger(value)) return value;\n if (typeof value === \"bigint\") {\n const narrowed = Number(value);\n if (Number.isSafeInteger(narrowed) && BigInt(narrowed) === value) return narrowed;\n }\n throw new Error(`Expected a safe integer in column ${column}, got ${describe(value)}.`);\n}\n\nfunction describe(value: unknown): string {\n if (value === null) return \"NULL\";\n if (value instanceof Uint8Array) return \"a BLOB\";\n return `${typeof value} ${String(value)}`;\n}\n\ntype Savepoint = {\n name: string;\n /** Size of `rollbackCallbacks` when this savepoint was created. */\n rollbackCallbackIndex: number;\n};\n\n/**\n * ← `SqliteDatabase`, restricted to the members `sqlite-kv` and\n * `sqlite-metadata` actually call.\n *\n * The one piece of real machinery here is the transaction/savepoint stack that\n * `onRollback()` needs. Upstream learns of a `BEGIN` / `SAVEPOINT` / `COMMIT` /\n * `RELEASE` / `ROLLBACK` from the SQLite authorizer while the statement is\n * being compiled (`prepareSql` fills a `ParseContext::stateChange`); we have no\n * authorizer, so the statement text is the only source. `applyChange` below is\n * a line-for-line port of upstream's; only where the `StateChange` comes from\n * differs.\n */\nexport class SqliteDatabase {\n readonly #backend: SqlDatabase;\n readonly #resetListeners = new Set<ResetListener>();\n\n /** Callbacks registered with onRollback that haven't been committed nor rolled back yet. */\n #rollbackCallbacks: (() => void)[] = [];\n /** Savepoints that haven't been committed nor rolled back yet. */\n #savepoints: Savepoint[] = [];\n /** True if in a BEGIN TRANSACTION transaction. */\n #inTransaction = false;\n /** ← `criticalErrorOccurred`, holding the exception rather than a bool. */\n #criticalError: SqliteCriticalError | undefined;\n /** ← `onWriteCallback`. */\n #onWriteCallback: ((allowUnconfirmed: boolean) => void) | undefined;\n /** ← `onCriticalErrorCallback`. */\n #onCriticalErrorCallback: ((exception: SqliteCriticalError) => void) | undefined;\n\n constructor(backend: SqlDatabase) {\n this.#backend = backend;\n }\n\n /**\n * Invokes the given callback whenever a query begins which may write to the\n * database. The callback is called just before executing the query.\n *\n * Durable Objects uses this to automatically begin a transaction and close the\n * output gate.\n *\n * Note that the write callback is NOT called before (or at any point during) a\n * `reset()`. Use the `ResetListener` mechanism for that case.\n */\n onWrite(callback: (allowUnconfirmed: boolean) => void): void {\n this.#onWriteCallback = callback;\n }\n\n /**\n * Invokes the given callback when a \"critical error\" causes an automatic\n * rollback during a transaction.\n *\n * See: https://www.sqlite.org/lang_transaction.html#response_to_errors_within_a_transaction\n *\n * Upstream passes `(errorMessage, maybeException)` and lets the caller build\n * the exception; `#checkForAutoRollback` has already built one by the time it\n * can tell a rollback happened, so the callback receives that.\n */\n onCriticalError(callback: (exception: SqliteCriticalError) => void): void {\n this.#onCriticalErrorCallback = callback;\n }\n\n /**\n * Invoke the onWrite() callback.\n *\n * \"This is useful when the caller is about to execute a statement which SQLite\n * considers read-only, but needs to be considered a write for our purposes. In\n * particular, we use the onWrite callback to start automatic transactions, and\n * we use the SAVEPOINT statement to implement explicit transactions. For\n * synchronous transactions, the explicit transaction needs to be nested inside\n * the automatic transaction, so we need to force an auto-transaction to start\n * before the SAVEPOINT.\"\n */\n notifyWrite(allowUnconfirmed = false): void {\n this.#onWriteCallback?.(allowUnconfirmed);\n }\n\n /** ← `SqliteDatabase::run`, in both its bare and its `QueryOptions` form. */\n run(sql: string, ...bindings: SqlValue[]): SqlResult;\n run(options: QueryOptions, sql: string, ...bindings: SqlValue[]): SqlResult;\n run(first: string | QueryOptions, ...rest: SqlValue[]): SqlResult {\n if (typeof first === \"string\") return this.#exec(first, rest, false);\n\n const [sql, ...bindings] = rest;\n if (typeof sql !== \"string\") throw new Error(\"run(options, sql, ...) takes a SQL string.\");\n return this.#exec(sql, bindings, first.allowUnconfirmed ?? false, first.regulate);\n }\n\n /** ← `SqliteDatabase::ingestSql`: execute complete statements, retain the partial tail. */\n ingest(sql: string, regulate?: (sql: string) => void): SqlIngestResult {\n this.assertUsable();\n let remainder = sql;\n let rowsRead = 0;\n let rowsWritten = 0;\n let statementCount = 0;\n\n while (hasSqlStatement(remainder)) {\n let statement: SqlDatabaseStatement;\n try {\n statement = this.#backend.prepare(remainder);\n } catch (error) {\n if (error instanceof Error && /incomplete input/i.test(error.message)) break;\n throw error;\n }\n try {\n // sqlite3_complete_length(), which upstream uses, requires the terminating semicolon.\n if (!statement.sql.trimEnd().endsWith(\";\")) break;\n regulate?.(statement.sql);\n if (statement.parameterCount !== 0) throw new Error(SQL_WRONG_BINDINGS_MESSAGE);\n const result = this.#execStatement(statement, [], false);\n rowsRead += result.rawRows.length;\n rowsWritten += result.rowsWritten;\n statementCount += 1;\n remainder = remainder.slice(statement.sql.length);\n } finally {\n statement.close();\n }\n }\n\n return { remainder, rowsRead, rowsWritten, statementCount };\n }\n\n #exec(\n sql: string,\n bindings: readonly SqlValue[],\n allowUnconfirmed: boolean,\n regulate?: (sql: string) => void,\n ): SqlResult {\n this.assertUsable();\n\n let remaining = sql;\n let result: SqlResult | undefined;\n while (hasSqlStatement(remaining)) {\n const statement = this.#backend.prepare(remaining);\n const tail = remaining.slice(statement.sql.length);\n const isFinal = !hasSqlStatement(tail);\n try {\n regulate?.(statement.sql);\n if (!isFinal && statement.parameterCount !== 0) {\n throw new Error(SQL_PRELUDE_BINDINGS_MESSAGE);\n }\n if (isFinal && statement.parameterCount !== bindings.length) {\n throw new Error(SQL_WRONG_BINDINGS_MESSAGE);\n }\n result = this.#execStatement(\n statement,\n isFinal ? bindings : [],\n isFinal ? allowUnconfirmed : false,\n );\n } finally {\n statement.close();\n }\n remaining = tail;\n }\n if (result === undefined) throw new Error(\"Expected at least one SQL statement.\");\n return result;\n }\n\n /** Execute one statement from `run()`'s batch; bindings and results belong to the last one. */\n #execStatement(\n statement: SqlDatabaseStatement,\n bindings: readonly SqlValue[],\n allowUnconfirmed: boolean,\n ): SqlResult {\n const { sql } = statement;\n const change = classify(sql);\n\n // Before the statement runs, as upstream's `Query::checkRequirements` does: the callback opens\n // the transaction this statement is about to write into, and it is also allowed to refuse the\n // statement outright when whatever owns the transaction is already broken.\n if (isWrite(sql)) this.notifyWrite(allowUnconfirmed);\n\n let result: SqlResult;\n try {\n result = statement.execute(bindings);\n } catch (error) {\n this.#checkForAutoRollback(error);\n throw error;\n }\n // Upstream applies the effect on the statement's first step, i.e. after it\n // has actually run, so a statement that throws changes nothing.\n this.#applyChange(change);\n return result;\n }\n\n /**\n * ← `SqliteDatabase::handleCriticalError`, which reaches the same conclusion\n * from the error code plus `sqlite3_get_autocommit`. We do not see the error\n * code — the backend has already turned it into a JS exception — so the\n * disagreement between our stack and the backend's is the whole signal, and\n * it is enough: we only ask after a statement has failed, and the only thing\n * that closes a transaction without going through `run()` is SQLite itself.\n *\n * The callbacks are DISCARDED rather than invoked, which looks wrong for two\n * lines and is not. Invoking them is only correct when a rollback actually\n * happened, and the branch above is the sole case where that is knowable; in\n * the ordinary case — a constraint violation, which does not roll anything\n * back — firing them would restore a cache the database has moved past, which\n * is corruption in the other direction. Upstream does not fire them here\n * either. It makes the actor fatal instead, and so does this: the caches are\n * knowingly stale, so the database is finished rather than repaired.\n */\n #checkForAutoRollback(cause: unknown): void {\n if (!this.#inTransaction && this.#savepoints.length === 0) return;\n if (this.#backend.inTransaction) return;\n\n this.#inTransaction = false;\n this.#savepoints = [];\n this.#rollbackCallbacks = [];\n const critical = new SqliteCriticalError(\n \"SQLite rolled back the open transaction in response to a critical error, so every \" +\n \"in-memory view of this database is now stale and it can no longer be used.\",\n { cause },\n );\n this.#criticalError = critical;\n this.#onCriticalErrorCallback?.(critical);\n throw critical;\n }\n\n /**\n * ← `SqliteDatabase::observedCriticalError()`. The named state\n * `io/actor-sqlite.ts` wires to `onBroken`, so it does not have to re-derive\n * the condition from an exception it caught.\n */\n observedCriticalError(): SqliteCriticalError | undefined {\n return this.#criticalError;\n }\n\n /**\n * The guard for any read this package serves from a cache rather than from a\n * statement. Those are the only paths a latched critical error would not\n * already stop, and they are exactly the paths whose answer is wrong once\n * SQLite has rolled back underneath them.\n */\n assertUsable(): void {\n if (this.#criticalError !== undefined) throw this.#criticalError;\n }\n\n get databaseSize(): number {\n return this.#backend.databaseSize;\n }\n\n /**\n * ← `SqliteDatabase::onRollback`.\n *\n * \"Register a callback which shall be called if the current transaction is\n * rolled back. If the current transaction commits, then the callback is\n * discarded without invoking it. [...] When a rollback occurs, callbacks are\n * invoked in the reverse of the order in which they were registered.\"\n *\n * With nothing open there is nothing that can roll back, so the callback is\n * dropped — upstream's `if (inTransaction || !savepoints.empty())`.\n */\n onRollback(callback: () => void): void {\n if (this.#inTransaction || this.#savepoints.length > 0) {\n this.#rollbackCallbacks.push(callback);\n }\n }\n\n addResetListener(listener: ResetListener): void {\n this.#resetListeners.add(listener);\n }\n\n removeResetListener(listener: ResetListener): void {\n this.#resetListeners.delete(listener);\n }\n\n /** ← `SqliteDatabase::reset()`. */\n reset(): void {\n // Refused for the same reason `run()` is: the listeners below read their own\n // state on the way out, and after a critical error that state is stale.\n this.assertUsable();\n // \"If transactions are open during reset(), whatever had the transaction\n // open is going to get confused at best, or lose data at worst.\"\n if (this.#inTransaction || this.#savepoints.length > 0) {\n throw new Error(\"can't reset() a database during a transaction\");\n }\n for (const listener of this.#resetListeners) {\n listener.beforeSqliteReset();\n }\n this.#backend.reset();\n }\n\n close(): void {\n this.#backend.close();\n }\n\n /** ← `SqliteDatabase::applyChange`, ported statement for statement. */\n #applyChange(change: StateChange): void {\n switch (change.kind) {\n case \"none\":\n break;\n\n case \"begin\":\n if (change.savepointName !== null) {\n this.#savepoints.push({\n name: change.savepointName,\n rollbackCallbackIndex: this.#rollbackCallbacks.length,\n });\n } else {\n assert(\n this.#savepoints.length === 0,\n \"BEGIN TRANSACTION should have failed when savepoints are present?\",\n );\n assert(\n !this.#inTransaction,\n \"BEGIN TRANSACTION should have failed when already in a transaction?\",\n );\n assert(\n this.#rollbackCallbacks.length === 0,\n \"we shouldn't have been keeping rollback callbacks with no transaction open!\",\n );\n this.#inTransaction = true;\n }\n break;\n\n case \"commit\":\n if (change.savepointName !== null) {\n // Per https://www.sqlite.org/lang_savepoint.html, releasing a savepoint also releases\n // all later savepoints.\n for (;;) {\n const savepoint = this.#savepoints.pop();\n assert(savepoint !== undefined, \"released a savepoint that didn't exist?\");\n if (savepoint.name === change.savepointName) break;\n }\n } else {\n assert(this.#inTransaction, \"COMMIT TRANSACTION without BEGIN TRANSACTION?\");\n // Since BEGIN TRANSACTION cannot be nested within a savepoint, this must have released\n // all savepoints implicitly.\n this.#savepoints = [];\n this.#inTransaction = false;\n }\n if (this.#savepoints.length === 0 && !this.#inTransaction) {\n this.#rollbackCallbacks = [];\n }\n break;\n\n case \"rollback\":\n if (change.savepointName !== null) {\n for (;;) {\n const savepoint = this.#savepoints[this.#savepoints.length - 1];\n assert(savepoint !== undefined, \"released a savepoint that didn't exist?\");\n if (savepoint.name === change.savepointName) {\n this.#runRollbackCallbacksDownTo(savepoint.rollbackCallbackIndex);\n // Rolling back to a savepoint does not release it, so it stays on the stack and\n // must be released separately.\n break;\n }\n this.#savepoints.pop();\n }\n } else {\n assert(this.#inTransaction, \"ROLLBACK TRANSACTION without BEGIN TRANSACTION?\");\n this.#savepoints = [];\n this.#inTransaction = false;\n this.#runRollbackCallbacksDownTo(0);\n }\n break;\n }\n }\n\n #runRollbackCallbacksDownTo(index: number): void {\n assert(this.#rollbackCallbacks.length >= index, \"rollback callback stack shrank?\");\n while (this.#rollbackCallbacks.length > index) {\n // Upstream pops first and then invokes, so a callback that throws does not leave itself on\n // the stack to be invoked a second time by the next rollback.\n const callback = this.#rollbackCallbacks.pop();\n assert(callback !== undefined, \"rollback callback stack shrank?\");\n callback();\n }\n }\n}\n\ntype SqliteSchemaDatabase = Pick<SqlDatabase, \"exec\"> | Pick<SqliteDatabase, \"run\">;\n\n/**\n * Returns whether a runtime-owned table exists, and refuses any present shape\n * other than the one this release writes.\n */\nexport function hasCurrentSqliteTable(\n db: SqliteSchemaDatabase,\n name: string,\n createSql: string,\n): boolean {\n // SQLite identifiers are ASCII case-insensitive, so schema validation must\n // find the same object that CREATE TABLE IF NOT EXISTS would collide with.\n const query = \"SELECT type, sql FROM sqlite_master WHERE name = ? COLLATE NOCASE\";\n const rows = \"run\" in db ? db.run(query, name).rawRows : db.exec(query, [name]).rawRows;\n const row = rows[0];\n if (row === undefined) return false;\n if (\n rows.length !== 1 ||\n row[0] !== \"table\" ||\n typeof row[1] !== \"string\" ||\n normalizeSchemaSql(row[1]) !== normalizeSchemaSql(createSql)\n ) {\n throw new Error(\n `Incompatible @mcp-b/do-runtime storage schema for table \"${name}\". ` +\n \"This release accepts only the current schema and does not migrate stored runtime data.\",\n );\n }\n return true;\n}\n\nfunction normalizeSchemaSql(sql: string): string {\n return sql\n .replace(/\\bIF\\s+NOT\\s+EXISTS\\b/giu, \"\")\n .replace(/\\s+/gu, \" \")\n .trim()\n .replace(/;$/u, \"\")\n .toLowerCase();\n}\n\nfunction assert(condition: boolean, message: string): asserts condition {\n if (!condition) throw new Error(message);\n}\n\n/** ← `SqliteDatabase::StateChange`. */\ntype StateChange =\n | { kind: \"none\" }\n | { kind: \"begin\"; savepointName: string | null }\n | { kind: \"commit\"; savepointName: string | null }\n | { kind: \"rollback\"; savepointName: string | null };\n\nconst NO_CHANGE: StateChange = { kind: \"none\" };\n\n/** SQLite savepoint names compare case-insensitively, so the stack stores them folded. */\nfunction savepointName(raw: string): string {\n const unquoted =\n (raw.startsWith('\"') && raw.endsWith('\"')) ||\n (raw.startsWith(\"'\") && raw.endsWith(\"'\")) ||\n (raw.startsWith(\"`\") && raw.endsWith(\"`\"))\n ? raw.slice(1, -1)\n : raw.startsWith(\"[\") && raw.endsWith(\"]\")\n ? raw.slice(1, -1)\n : raw;\n return unquoted.toLowerCase();\n}\n\nconst NAME = String.raw`(\"[^\"]*\"|'[^']*'|\\`[^\\`]*\\`|\\[[^\\]]*\\]|[A-Za-z_][A-Za-z0-9_$]*)`;\nconst BEGIN = new RegExp(\n String.raw`^BEGIN(\\s+(DEFERRED|IMMEDIATE|EXCLUSIVE))?(\\s+TRANSACTION)?$`,\n \"i\",\n);\nconst SAVEPOINT = new RegExp(String.raw`^SAVEPOINT\\s+${NAME}$`, \"i\");\nconst COMMIT = new RegExp(String.raw`^(COMMIT|END)(\\s+TRANSACTION)?$`, \"i\");\nconst RELEASE = new RegExp(String.raw`^RELEASE(\\s+SAVEPOINT)?\\s+${NAME}$`, \"i\");\nconst ROLLBACK = new RegExp(String.raw`^ROLLBACK(\\s+TRANSACTION)?$`, \"i\");\nconst ROLLBACK_TO = new RegExp(\n String.raw`^ROLLBACK(\\s+TRANSACTION)?\\s+TO(\\s+SAVEPOINT)?\\s+${NAME}$`,\n \"i\",\n);\nconst TRANSACTION_KEYWORD = /^(BEGIN|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\\b/i;\n\n/** Every group referenced below is mandatory in its pattern, so an absent one is a broken pattern. */\nfunction group(match: RegExpExecArray, index: number): string {\n const value = match[index];\n if (value === undefined) throw new Error(`SQL pattern group ${index} did not match: ${match[0]}`);\n return value;\n}\n\n/**\n * Derives upstream's `StateChange` from the statement text.\n *\n * A statement that opens with a transaction keyword but does not match one of\n * the forms below throws rather than being classified as `NoChange`, since\n * guessing in that direction is what loses a rollback callback. `run()` asks\n * the backend to compile one native statement at a time and applies this state\n * change after each executes, matching workerd's `prepareMulti()` prelude.\n */\nfunction classify(sql: string): StateChange {\n const statement = stripLeadingTrivia(sql).trim().replace(/;$/, \"\").trimEnd();\n\n const rollbackTo = ROLLBACK_TO.exec(statement);\n if (rollbackTo !== null) {\n return { kind: \"rollback\", savepointName: savepointName(group(rollbackTo, 3)) };\n }\n if (ROLLBACK.test(statement)) return { kind: \"rollback\", savepointName: null };\n\n const savepoint = SAVEPOINT.exec(statement);\n if (savepoint !== null) {\n return { kind: \"begin\", savepointName: savepointName(group(savepoint, 1)) };\n }\n if (BEGIN.test(statement)) return { kind: \"begin\", savepointName: null };\n\n const release = RELEASE.exec(statement);\n if (release !== null) {\n return { kind: \"commit\", savepointName: savepointName(group(release, 2)) };\n }\n if (COMMIT.test(statement)) return { kind: \"commit\", savepointName: null };\n\n if (TRANSACTION_KEYWORD.test(statement)) {\n throw new Error(`Unrecognized transaction-control statement: ${statement}`);\n }\n return NO_CHANGE;\n}\n\n/**\n * ← `!sqlite3_stmt_readonly(statement)`, the test upstream's `onWrite` gate is\n * written against (`sqlite.c++:1562-1568`).\n *\n * It is NOT the authorizer — the authorizer never sees this question, and the\n * distinction is load bearing: `sqlite3_stmt_readonly()` reports BEGIN, COMMIT,\n * ROLLBACK, SAVEPOINT and RELEASE as read-only, which is why `notifyWrite()`\n * exists at all and why an automatic transaction's own `BEGIN` does not recurse\n * into the callback that issued it.\n *\n * Neither backend exposes the compiled statement, so the text is the only\n * source, and it fails closed the way `classify` does: **a statement is a write\n * unless it provably is not.** The complete read set is `SELECT` and `EXPLAIN`\n * plus the five transaction-control forms. `WITH`, `PRAGMA` and anything\n * unrecognised are writes, which costs a read-only CTE a transaction and an\n * output-gate lock it does not need, and cannot cost atomicity — which is the\n * only error this classification is allowed to make.\n */\nfunction isWrite(sql: string): boolean {\n return !NON_WRITE_KEYWORDS.has(leadingKeyword(sql));\n}\n\nconst NON_WRITE_KEYWORDS = new Set([\n \"SELECT\",\n \"EXPLAIN\",\n \"BEGIN\",\n \"COMMIT\",\n \"END\",\n \"ROLLBACK\",\n \"SAVEPOINT\",\n \"RELEASE\",\n]);\n\n/** The first bare word, skipping whitespace and both comment forms. */\nfunction leadingKeyword(sql: string): string {\n return (/^[A-Za-z]+/.exec(stripLeadingTrivia(sql))?.[0] ?? \"\").toUpperCase();\n}\n\nfunction hasSqlStatement(sql: string): boolean {\n return stripLeadingTrivia(sql).trim().length > 0;\n}\n\nfunction stripLeadingTrivia(statement: string): string {\n let index = 0;\n for (;;) {\n while (index < statement.length && /\\s/.test(statement.charAt(index))) index += 1;\n if (statement[index] === \"-\" && statement[index + 1] === \"-\") {\n const newline = statement.indexOf(\"\\n\", index);\n index = newline === -1 ? statement.length : newline + 1;\n continue;\n }\n if (statement[index] === \"/\" && statement[index + 1] === \"*\") {\n const close = statement.indexOf(\"*/\", index + 2);\n index = close === -1 ? statement.length : close + 2;\n continue;\n }\n return statement.slice(index);\n }\n}\n"],"mappings":"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"}