/** * `createD1PrewarmClaimStore` — the cross-isolate half of * `createSandboxPrewarmer`'s single-flight, implemented once. * * WHY THIS SHIPS HERE. `SandboxPrewarmerOptions.claim` is REQUIRED and has no * default, which is correct — the unsafe behaviour must be said out loud. But * "required with no implementation" is how five products end up hand-rolling * five subtly different atomic leases, and a lease that is subtly wrong is * indistinguishable from one that works until two tabs leak a box. Every * agent-app product deploys to Cloudflare Workers with a D1 binding, so the * store is the same code in all of them: it is mechanism, not domain, and it * belongs beside the prewarmer. * * The measured stake (staging-sandbox, 2026-07-28): two concurrent * `POST /v1/sandboxes` with an identical name BOTH returned HTTP 201 and left * two running boxes. The platform does not dedupe by name, so nothing below * this line is defensive programming — an unclaimed race genuinely doubles * spend. * * ── ATOMICITY IS THE WHOLE POINT ─────────────────────────────────────────── * `acquire` is ONE statement. A `SELECT` followed by an `INSERT` is exactly the * race this exists to close: both isolates read "free", both write, both warm. * The upsert's `DO UPDATE ... WHERE expires_at <= now` means an unexpired claim * makes the conflict path a no-op, and `RETURNING` then yields no row — so the * loser learns it lost from the same statement that would have made it the * winner. `RETURNING` is used rather than `meta.changes` because a no-op upsert * reporting `changes: 0` is a SQLite detail, whereas "no row came back" is the * statement telling you directly. * * ── STRUCTURAL, NOT A DEPENDENCY ─────────────────────────────────────────── * The binding is taken as the narrow shape actually used (`prepare().bind()`, * `.first()`, `.run()`), per the package's structural-over-hard-dep rule. No * `@cloudflare/workers-types` import, so `/sandbox` stays importable in a plain * node test. A real `D1Database` satisfies it. * * ── THE TABLE IS THE PRODUCT'S MIGRATION, NOT A LAZY CREATE ──────────────── * The store never runs DDL. A `CREATE TABLE IF NOT EXISTS` on every project * open costs a round trip on the exact path this feature exists to keep fast, * and it hides schema drift instead of failing on it. `PREWARM_CLAIM_TABLE_DDL` * is exported so a product pastes it into a real migration. If the table is * missing, `acquire` throws — and the prewarmer treats a throwing claim as a * failed warm: it records the failure, emits `onEvent({type:'failed'})`, and * degrades to the lazy path. Fail-closed and loud, never a silent double-warm. */ /** The columns and statements this store uses, and nothing else. A real * `D1Database` structurally satisfies it. */ export interface PrewarmClaimD1Like { prepare(query: string): { bind(...values: unknown[]): { first>(): Promise; run(): Promise; }; }; } export interface D1PrewarmClaimStoreOptions { /** Defaults to `sandbox_prewarm_claims`. Must be a bare SQL identifier — it * is interpolated, because a table name cannot be a bound parameter. */ table?: string; /** Clock seam for tests. */ now?(): number; } export declare const DEFAULT_PREWARM_CLAIM_TABLE = "sandbox_prewarm_claims"; /** Paste into a migration. `expires_at` is epoch MILLISECONDS, matching * `Date.now()`, so no unit conversion sits between the lease and its clock. */ export declare const PREWARM_CLAIM_TABLE_DDL = "CREATE TABLE IF NOT EXISTS sandbox_prewarm_claims (\n key TEXT PRIMARY KEY,\n expires_at INTEGER NOT NULL\n)"; /** * A `PrewarmClaimStore` backed by one D1 table. * * ```ts * const prewarmer = createSandboxPrewarmer(shell, { * claim: createD1PrewarmClaimStore(env.DB), * mode: 'create-or-resume', * }) * ``` */ export declare function createD1PrewarmClaimStore(db: PrewarmClaimD1Like, options?: D1PrewarmClaimStoreOptions): { acquire(key: string, ttlSeconds: number): Promise; release(key: string): Promise; isHeld(key: string): Promise; };