drupflare/worker - v1.0.0
    Preparing search index...

    Class SitePhpDurableObject

    One site: the interpreter AND the database, in the same isolate.

    This is the arrangement the whole project has been pointing at and had never executed. ctx.storage.sql is synchronous only from inside the Durable Object, and PHP's PDO is blocking, so the driver can only work if PHP runs HERE. Until this class existed the PHP half was tested against a PDO stand-in and the JS half against synthetic statements, and the two had never met.

    Hierarchy

    • SiteDurableObject
      • SitePhpDurableObject
    Index
    _migrator?: SqlMigrator
    alarmFirings?: number
    alarmRearms?: number
    authSpend?: AuthSpend

    Today's authenticated spend, or undefined when nothing has been charged in this lifetime.

    In memory only, and read back from ctx.storage on each charge rather than trusted across one: an eviction between two authenticated renders must not reset the day's budget, and the durable record is the only thing that survives it.

    bootDiag: string[]
    bootMs: number | null
    bumpCoalesced?: boolean
    bumps?: number
    consecutiveFillFailures?: number

    consecutive failing batches, for the capped backoff; reset by any batch that progressed

    ctx: DurableObjectState
    doRequestsSinceFlush?: number

    Durable Object invocations since the last flush, counted in memory.

    Same shape as rowsSinceFlush and for the same reason: persisting per invocation would cost a row per invocation, so the meter would inflate the OTHER meter it sits beside. Folded into a per-UTC-day total on the alarm, where one row is already being written.

    doRing: RingBuffer = ...
    env: SiteEnv
    gate: Gate
    heapRestore: Payload | null

    what the last heap restore attempt did, or null when it was never attempted

    heapRestoreCursor: HeapRestoreCursor | null

    Where a chunked restore has got to, or null when there is nothing in flight.

    In memory only, which is the safety argument. The cursor indexes into a LIVE heap, so its correct lifetime is exactly that heap's lifetime. Persisting it to ctx.storage would let a cursor survive an eviction that destroyed the heap it describes, and the next firing would resume at chunk 7 of a heap that is back to zeros -- a restore that reports complete over a heap that is 6 chunks of nothing. Dying with the isolate makes a resumed-onto-nothing restore unrepresentable; the cost is repeating at most one object's worth of memcpy after an eviction, which is idempotent because every chunk writes fixed bytes at fixed offsets.

    httpTablesReady?: boolean
    lastAlarmAt?: number
    lastAlarmClass?: AlarmClass
    lastAlarmOutcome?: unknown
    lastBootInclusiveMs?: number

    wall time of the last fill that ALSO booted; kept for diagnostics, never used as an estimate

    lastCron?: Payload
    lastCronAt?: number
    lastFindings?: Finding[]

    what the last supervised alarm found, for /__health

    lastFleetError?: string
    lastGc?: Payload
    lastGcAt?: number
    lastHttpDrain?: Payload
    lastHttpDrainAt?: number
    lastKeepWarm?: number
    lastMirrorDrain?: Payload
    lastMirrorDrainAt?: number
    lastPageMirrorDrain?: Payload
    lastPageMirrorDrainAt?: number
    lastRenderMs?: number
    lastUpdb?: Payload
    lastUpdbAt?: number
    lastWindowFills?: number
    logs?: Payload[]
    mails?: { bytes: number; subject: unknown; to: unknown }[]
    memoryRing: RingBuffer = ...

    Trend rings for the three signals a slope is claimed about.

    Held here rather than inside src/ops/supervisor.ts, which is not allowed to own state: a tripwire keeping its own history would keep a poisoned observation alive across the very recycle the layer exists to survive. In-memory on purpose -- an eviction should forget the trend rather than resume a stale one.

    migrated: boolean
    migrateFailures?: number
    mountInfo: SiteMountInfo | null
    out: string[]
    pageBytes: Map<string, number[]> = ...

    per-path render sizes, previous renders only; the baseline renderSizeAnomaly compares against

    pageHits: Map<string, number> = ...

    per-path serve counts, in memory only; the R2 page mirror publishes the busiest first

    pagesFilledByAlarms?: number
    php: PhpInstance | null
    phpLaneEntries?: number
    pinnedHandles?: Set<object>

    Strong references to every vrzno handle value this interpreter has handed PHP.

    In memory only: it exists to stop the handle table's WeakRefs dying under a heap that still holds their integers, and it is meaningless once that heap is gone. See pinHandles().

    queryCount: number
    renderClockUnmeasurable?: boolean
    rowsRead?: number
    rowsRing: RingBuffer = ...
    rowsSinceFlush?: number

    writes accumulated since the last alarm folded them into the daily total

    rowsWritten?: number

    accumulated across the object's life; undefined until the first statement

    serveTablesReady?: boolean
    sql: SqlStorage
    sqlTraceSeq?: number

    statement counter for PW_SQL_TRACE, so the tail can be read as a sequence

    storageLaneServes?: number
    suppressBump?: boolean
    txnCount?: number
    txnSpeculative?: number
    txnStatements?: number
    windowClosedAt?: number
    windowFills?: number
    windowOpenedAt?: number
    windowsOpened?: number
    writeTally?: WriteTally

    per-table rows-written tally; only allocated when /__writes turns it on

    • Alarm handler. Touches the database so the isolate stays resident and the page cache stays populated, then re-arms.

      Deliberately cheap: a keep-warm that renders a page would burn CPU on every tick for no user.

      Returns Promise<any>

    • Arms the fill alarm without disturbing one that is already sooner.

      Extracted because bumpGeneration() is synchronous and cannot await getAlarm(); overwriting an existing alarm here would push a pending fill chain out. Fire-and-forget is safe: the worst case is an alarm at +1 ms that finds nothing to do.

      Returns void

    • Parameters

      • reason: string = 'manual'
      • arm: { arm?: boolean } = {}

        false requeues without waking the chain, leaving the caller to arm it. A long-running write needs that: armFillAlarm() schedules at +1 ms and does not await the write, so a fill starts while the caller still has seconds of work in the same event.

      Returns {
          droppedFromRequeue: number;
          generation: number;
          purgedPages: number;
          reason: string;
          requeued: number;
      }

    • Flips one byte of a stored chunk and leaves its digest alone, so the next restore must refuse.

      This exists to make the refusal EXECUTABLE IN PRODUCTION rather than only in a test. The distinction is not pedantic here: LAZY_MOUNT was covered by tests and unreachable in the deployed worker for its entire life, and the failure this guards against -- a chunk that lands with the wrong bytes -- produces a heap of the right length that renders something subtly wrong with no error at all. The only convincing evidence that the guard works is watching a deployed object refuse a chunk it was actually asked to apply.

      Writes the DIGEST-BEARING side of the pair untouched: corrupting both would restore cleanly and prove nothing.

      Parameters

      • seq: number

      Returns Payload

    • Parameters

      • table: string
      • Optionalwhere: string

      Returns number | null

    • today's DO invocations, without flushing

      Parameters

      • nowMs: number = ...

      Returns number

    • today's rows written, without flushing -- for a read that must not write

      Parameters

      • nowMs: number = ...

      Returns number

    • Fetches everything PHP deferred, in JS, where awaiting is legal.

      Runs between PHP invocations rather than inside one. Bounded per call because a queue full of slow hosts would otherwise occupy the object indefinitely.

      Parameters

      • limit: number = 5

      Returns Promise<{ drained: Payload[]; remaining: number }>

    • The fetch cache and the deferred queue; durable, so a drain survives eviction.

      KEYED BY METHOD+URL+BODY, NOT BY URL. Both tables were url TEXT PRIMARY KEY, which is a live correctness bug rather than a POST-only gap: two deferred fetches to the same endpoint are one row, so the second overwrites the first and a caller can be handed a response fetched for somebody else. For a captcha verification that is one visitor receiving another's verdict.

      The key is the exact tuple, LENGTH-PREFIXED, and never a hash. A non-cryptographic hash is forgeable and this key decides which cached response a request reads; a cryptographic one cannot be used because crypto.subtle.digest is async and the key is derived inside the synchronous call PHP makes. See deferredKey.

      Returns void

    • Builds the interpreter, installs the bridge, mounts the tree. Once.

      Lazy rather than in the constructor so boot cost is attributable to a route and measurable on its own. /request-boot already proved a static build can be instantiated inside a request handler (bootMs 36) -- no runtime codegen, so workerd permits it.

      Parameters

      • opts: { skipRestore?: boolean } = {}

      Returns Promise<PhpInstance>

    • What the next render on this instance is expected to cost, in ms.

      The budget has to be a prediction, because a render cannot be interrupted once it starts. php._run() is one synchronous call into wasm: while it executes nothing else in that thread runs, so no setTimeout, no AbortSignal and no Promise.race can preempt it. Measured -- a Worker racing a 1 ms timer against a stub.fetch() that rendered for 119 ms lost the race, because the timer could not fire until the wasm call returned.

      So the decision is taken before the render starts, from what this instance has already observed. The last render is the best predictor available: it was produced by the same kernel state the next one will meet.

      Pessimistic before any evidence exists. A Durable Object hibernates after roughly 10 s of inactivity and discards its in-memory state, which includes this.php, the mounted tree and the booted kernel -- so a cold instance is the common case in production, not the exception, and it must not gamble a visitor's request on a 3,754 ms boot.

      Returns number

    • Every Drupal statement, plus the automatic invalidation trigger.

      Suppressed during /__migrate: replaying the packed site inserts the packed cachetags rows, which is setup rather than a content change.

      Coalesced, because one content save invalidates many tags and each tag is its own merge('cachetags'). Once a bump has happened there is nothing left to invalidate, so further writes are ignored until a page is cached again -- fillOne() clears the flag. Without this a node save would bump the generation dozens of times and re-run DELETE on an already-empty table for each.

      Parameters

      • sql: string
      • Optionalparams: SqlBindings

      Returns ExecSqlResult

    • Atomic replay of a buffered Drupal transaction.

      Drupal's Connection::beginTransaction()/commit()/rollBack() is a BEGIN-COMMIT api; ctx.storage.transactionSync() is callback-scoped. They do not compose, and issuing BEGIN as SQL throws outright ("please use the state.storage.transaction() ... APIs instead"). So the PHP driver withholds writes and hands the whole list here to be replayed inside one transactionSync.

      commit: false is the speculative path: replay, run one read so the caller can see its own uncommitted write, then abort. Throwing from inside the callback is what makes transactionSync roll back, so the throw is deliberate and its message is not an error.

      Parameters

      • req: TxnRequest

      Returns ExecTxnResult

    • TWO LANES, and which one answers is the throughput story.

      The storage lane serves a cached page out of cfw_page with pure ctx.storage.sql and never enters the gate, so a HIT is answered WHILE a render is in flight instead of queueing behind it. That matters most for the thing it was built for: a sliced render holds the PHP lane for its whole sliced lifetime, and without this split every HIT in that window would wait, putting the single-object ceiling back through the side door.

      Three conditions make it safe, and each one is a real constraint rather than a formality:

      1. It runs NO DDL. ensureServeTables() issues CREATE TABLE IF NOT EXISTS, and DDL while the PHP lane holds an open transaction replay dirties sqlite_master and turns every later read in that transaction into a speculative replay -- the O(W x R) cost that once wedged the local runtime hard enough to take unrelated sites down with it. So the fast lane declines until some gated request has created the tables.
      2. It never awaits. One SELECT, one response. Nothing can interleave inside it, so it cannot observe a half-applied write.
      3. It never touches PHP.

      lane=gate forces the gated path, which is what makes the split testable: the same race with the fast lane disabled must show the HIT waiting.

      Parameters

      • request: Request

      Returns Promise<Response>

    • Parameters

      • targetPath: string | null = null
      • bins: string[] = ...
      • destruct: string | boolean = false
      • request: RenderRequest = {}

      Returns Promise<FillOutcome>

    • Folds this firing's Durable Object invocations into a per-UTC-day total.

      Counted because the DO-request quota is one of the two daily ceilings the whole architecture is scored against, and the limits page reported "nothing measures this yet" for it -- an unmeasured meter beside a measured one reads as the healthy one.

      IT COUNTS WHAT REACHED THIS OBJECT, which is the honest scope: a request answered by the edge cache never enters the isolate, so this is the DO meter and explicitly not the Worker-request meter. Conflating them would report a confident wrong number for the serving ceiling.

      Parameters

      • nowMs: number = ...

      Returns number

    • Folds the writes accumulated since the last flush into a per-UTC-day total.

      Flushed on the alarm, never per write, and that is a correctness requirement rather than an optimisation: persisting the counter costs a row write, so a per-write flush would DOUBLE the number it is measuring. Once per firing it is one row against a batch of them, and that row is itself counted, so the meter includes its own cost.

      Keyed by UTC date because the limit is daily and the object is not. A Durable Object is evicted whenever Cloudflare likes, so an in-memory lifetime counter reports a fraction of the day and reads as healthy; the date key means an eviction loses at most the writes since the last alarm rather than the whole day.

      Parameters

      • nowMs: number = ...

      Returns number

      the running total for today, after folding in whatever had accumulated.

    • The site generation: one integer that every edge cache key carries.

      Drupal's cache tags cannot purge a URL-keyed edge cache -- tag purge is an Enterprise feature -- so invalidation is done by making every previously cached URL unreachable instead of by deleting anything. Appending this to the key means one integer write invalidates the whole site, everywhere, for free.

      Returns number

    • Routing, deliberately OUTSIDE the gate.

      A subclass has to be able to fall through to these routes from inside its own gated handler. Calling a gated fetch() from within gate.run() self-deadlocks: the inner run() awaits the release of the outer link, which only resolves once the outer callback returns. ctx.blockConcurrencyWhile() forbids nesting outright for the same reason. So fetch() gates once and every route body lives here.

      Parameters

      • request: Request
      • url: URL

      Returns Promise<Response>

    • Returns Promise<boolean>

    • The emscripten heap, as bytes.

      Neither member is typed by php-wasm, and which one exists depends on the build: HEAPU8 is the view emscripten maintains, and wasmMemory is the fallback for a build that does not export it. Returns null when neither is present, because a snapshot of the wrong object would be a plausible-looking heap of the wrong bytes.

      Parameters

      • binary: SiteBinary

      Returns Uint8Array<ArrayBufferLike> | null

    • Parameters

      • url: string
      • method: string = 'GET'
      • body: string = ''

      Returns { body: string; headers: Payload; status: number } | null

    • Wall-clock budget a MISS may spend rendering before it gives up on the visitor and hands the path to the alarm chain.

      2 s covers the whole measured first-render range -- 195 ms on minimal, 909 ms on standard, 1,636 ms observed here on a loaded machine -- and excludes the 3,754 ms cold boot, which cannot be waited out. It bounds the visitor's patience, not a billed resource: wall time is not charged against the CPU budget (4 ms of Worker CPU against 827 ms of wall, measured).

      budget on the query string overrides so the fallback is testable, and 0 disables inline rendering entirely, which restores the always-202 shape.

      Paid defaults to 10 s rather than 2 s, which only matters once an interpreter exists: a cold object refuses on !this.php before this number is ever consulted. See bootInline.

      Parameters

      • url: URL

      Returns number

    • Installs the synchronous SQL entry point on the PHP Module so vrzno can reach it, wrapped in the codec.

      The codec matters here specifically: node IDs, file sizes and timestamps all cross this boundary, PHP is 32-bit, and a SQLite INTEGER can exceed 2^31. Handing back a raw JS number silently wraps it -- that is the bug class the codec closes, and a database driver is where it would bite hardest.

      Both entry points run inside withMask(): they are JS frames under the PHP stack, and a slice interrupt that fires there cannot suspend (see src/mask.js). The decode/encode codec calls sit inside the same window, so the codec needs no mask of its own.

      Parameters

      • module: Record<string, unknown>

      Returns Record<string, unknown>

    • The host half of drupal/drupflare/.

      Every one of these is synchronous, which is the design constraint rather than a shortcut. Host::call() in PHP does $reply = $invoke($json) and reads the result immediately; PHP cannot await, so a host function that returned a Promise would hand PHP an object it can only stringify. So the capabilities that need the network are split in two:

      • cfwQueueFetch records the request and returns at once (no suspension).
      • cfwHttpCacheGet / cfwFetch answer from what a previous drain already fetched (no suspension).
      • the actual fetch() happens in drainHttpQueue(), in JS, between PHP runs.

      That is exactly the cached -> deferred -> sync layering CfwDeferredHttp documents, with the sync tier absent until a JSPI build exists. A miss is reported as a miss rather than faked.

      Parameters

      • binary: SiteBinary

      Returns SiteBinary

    • The rolling median body size for one path, over PREVIOUS renders only.

      Reading cfw_page instead would compare the render against the row it just wrote -- a ratio of exactly 1.00 every time, so renderSizeAnomaly could not fire at all. The 90,038-byte admin page served to an anonymous visitor is the incident that check exists for, and it is detectable only against the path's own history.

      In memory and capped, like pageHits: an eviction should forget the baseline rather than compare a fresh render against a stale one, and history in SQL would cost a row per render on the meter that binds.

      Parameters

      • path: string

      Returns number | null

    • Parameters

      • key: string
      • fallback: string | null = null

      Returns string | null

    • Parameters

      • key: string
      • value: unknown

      Returns void

    • The /migrate body for the JS engine.

      Shaped to answer both plans from one route. all=1 (or a paid plan) replays every chunk in this invocation, which is what a 30 s CPU budget wants. The default replays chunksPerInvocation() and arms an alarm to carry the rest, which is what a 10 ms budget requires -- so a free-plan deploy self-migrates over ~15 alarm firings without an operator poking the route 15 times.

      Parameters

      • url: URL | null

      Returns Promise<Payload>

    • The cursor when a migration is started but unfinished, else null.

      Null for a site that never started one, because that is every deploy predating the chunked engine and those must keep serving. "Never started" and "half done" are genuinely different states and conflating them would take the whole existing fleet offline.

      Returns MigrateCursor | null

    • Advances the migration by one invocation's worth of chunks, or returns null when there is nothing to do.

      Returns null in two distinct cases that must not be conflated: no migration has ever been started (so this object is not mid-flight and an alarm should get on with filling), and the migration is finished. A site with no manifest at all also returns null rather than throwing, because that is the shape of every pre-existing deploy and an alarm that throws stops re-arming.

      Returns Promise<Payload | null>

    • The JS-side migrator, lazily built.

      Lazy because the manifest is an asset fetch and most invocations never migrate; cached on the instance because a warm object re-reading it per chunk would pay 15 pointless subrequests against the 50-per-invocation cap.

      Returns SqlMigrator

    • The R2 bucket to offload files to, or null when there is none.

      NULL IS A SUPPORTED STATE, not a misconfiguration, and that is the whole reason this is a method rather than a direct this.env.FILES read at the call site. The Durable Object is where the file durably lives (src/db/file-store.ts stores bytes in DO SQL precisely so an eviction cannot lose them); R2 is an offload that buys serving-ceiling headroom by answering requests without a Worker invocation. A site with no bucket bound is fully functional and simply pays a Worker request per file, which is the free-tier default.

      Typed structurally so the drain is drivable over a stand-in -- the cases worth testing are a put that throws and a binding that is absent, and neither needs a real bucket.

      Returns MirrorBucket | null

    • Whether this site has never been provisioned, so a page request has nothing to render from.

      NO CURSOR AT ALL is a different state from a half-finished one, and it was the state a fresh deploy sat in forever: migrateStepIfPending() returns null without a cursor, so the alarm chain never started, and /serve answered warming on every request for the life of the object. Provisioning happened only if somebody called /migrate by hand -- which is a DIAGNOSTIC route, so on the canonical config there was no way to do it at all.

      Returns boolean

    • records one render's size AFTER the median for it was taken, so a render never sets its own baseline

      Parameters

      • path: string
      • bytes: number

      Returns void

    • Date.now() is not available at DO global scope in some contexts and the value must also survive the 32-bit PHP boundary, so it is read here and encoded by the codec on the way out rather than passed raw.

      Returns number

    • What the object can see about itself at the end of an alarm.

      Every field is a scalar the caller already had or a ring capped at 8, so this cannot become a full-table scan on a path that runs every firing. countOrNull() is the one exception and it is why semaphoreRows is omitted on an unmigrated site rather than reported as 0: a table that does not exist is not an invariant that holds.

      Parameters

      • outcomes: (Payload | null)[]

      Returns Observation

    • The pack generation this object migrated from, or null.

      A snapshot is only valid for the pack it was booted against: a new pack means a different tree, so a heap restored across that boundary holds interned paths into files that moved. The migrate cursor already records the generation, so there is no second source of truth.

      Returns string | null

    • One stored page, as an HTTP response.

      x-cfw-generation is on every serve response: it is how the Worker learns the current generation without spending a Durable Object request to ask for it.

      Parameters

      • row: PageRow
      • tier: "HIT" | "RENDER"
      • serveMs: number
      • extra: Record<string, string> = {}

      Returns Response

    • Keeps every vrzno handle alive for the interpreter's whole life.

      MEASURED, and it is why capture alone was not enough. Module.targets.byInteger is php-wasm's WeakerMap -- a Map of WeakRefs with a FinalizationRegistry -- and its iterator DELETES any entry whose referent has been collected. So a handle PHP acquired during the kernel boot and did not call again was gone from the table by snapshot time, while the integer was still sitting in the heap. On the edge that read as misses: [2] from /heap?op=trace: the restored heap asked for handle 2 and the table held only handle 1.

      A snapshot cannot fix that after the fact, so the pin goes in before any PHP runs. The strong set is per-interpreter and per-object, and a booted kernel mints a handful of handles, so this trades a few retained JS objects for a handle table that still describes the heap.

      add is defined non-writable on the index, which is why the whole object is swapped rather than the method patched; the glue re-reads Module.targets at every call site.

      Parameters

      • binary: SiteBinary

      Returns number

    • Loads the CI-rendered pages straight into the serving table.

      Default on for free, off for paid, overridable both ways. Pre-filling changes what a MISS means -- a prefilled path is a HIT on its first ever request -- so the switch is explicit per plan rather than implied. See prefillDefault(). Rendered on native PHP, where a warm page costs 5.45 ms against 46 ms of edge cpuTime, by scripts/drupal/prefill-cache.php. An absent prefill.json is normal: a site that skipped the CI step just starts cold.

      Extracted from the /__migrate route, which was a production bug rather than untidiness. Living in the route handler meant only a request-driven migration ever prefilled. A migration that completes on the ALARM chain is the default -- migrationSelfDrives() arms it from the first call -- and is the only path a deployed site takes, so a real deploy finished migrating with cfw_page empty and answered 503 on its front page until somebody happened to request a render. Both callers now share this.

      Parameters

      • asked: string | null = null

        the ?prefill= override: '1' forces on, '0' forces off, null defers to the plan

      Returns Promise<Payload>

    • Whether a page request has asked this site to provision itself.

      Durable, because the request that asks and the alarm that acts are different invocations and an eviction sits between them. One row in cfw_meta, the table the serve path already writes.

      Returns boolean

    • Parameters

      • url: string
      • method: string = 'GET'
      • body: string = ''

      Returns void

    • Writes this site's row into the fleet inventory, when there is anything worth writing.

      Silent when no D1 binding exists, which is the free-tier default and not an error: a single site does not need an inventory, and the whole point of the predicate is that the steady state costs one row per site per day.

      Returns Promise<void>

    • Records that a visitor wants this site, and wakes the alarm chain to build it.

      Returns Promise<void>

    • Applies the next slice of an in-flight restore. Driven by alarm(), never by a request.

      The heap is read back out of the LIVE binary each firing rather than held in a field, because the cursor is only meaningful for the heap that is actually mounted right now -- if the isolate were replaced between firings, this.php would be null and there would be nothing to resume onto. That case cannot arise silently: the cursor is in-memory, so it died with the isolate, and the next boot starts a fresh restore at chunk 0.

      Returns Promise<Payload | null>

    • runs a PHP fragment and returns everything it wrote

      Parameters

      • code: string

      Returns Promise<string>

    • runs a fragment and parses the JSON object it printed

      Parameters

      • code: string

      Returns Promise<Payload>

    • Keeps the interpreter warm.

      Boot is per-DO-lifetime, not per-request: 3,754 ms of CPU on the edge, of which roughly 1 s of the residual has no identified lever. So the strategy cannot be "make boot fast", it has to be "boot rarely". An alarm is the only way a DO wakes itself, so it is the cold-start strategy rather than an optimisation.

      The interval is a floor, not a guarantee: Cloudflare may still evict, and alarms are best-effort. It reduces cold starts, it does not remove them.

      Parameters

      • intervalMs: number = 240000

      Returns Promise<
          | { existingAlarm?: undefined; intervalMs: number; scheduled: boolean }
          | { existingAlarm: number; intervalMs?: undefined; scheduled: boolean },
      >

    • A cached page, or null to say "not mine".

      Synchronous by construction: an async here would introduce the await that condition 2 forbids. The serve counter is incremented only on the answering path so a request is still counted exactly once whichever lane takes it.

      Parameters

      • url: URL

      Returns Response | null

    • Whether this alarm firing should spend rows on garbage collection.

      Two gates, and both are about the shared meter rather than caution. Page fills and GC both spend rows written (100k/day), so GC never runs while pages are waiting -- a visitor's MISS outranks reclaiming disk. And it is interval-gated because the measured steady state is 0 rows written: running it on every firing would spend statements to discover there is nothing to do.

      Returns boolean

    • Writes this object's booted heap into its own SQLite.

      Post-boot, pre-render: a rendered heap is a request-contaminated heap, and the uid-1 cache-poisoning bug is what that contamination looks like when it goes wrong.

      The fd table travels WITH the bytes. The open descriptor table is the load-bearing part of a restore -- not inode alignment, which was tested and falsified -- so a snapshot whose descriptors were reconstructed from some other instance is not a snapshot.

      So does the vrzno handle table: the heap stores Module.targets ids as bare integers, so a render through a restored heap died with TypeError: target is not a function on a deployed worker. See HandleRecord.

      Parameters

      • opts: { chunkBytes?: number } = {}

      Returns Promise<Payload>

    • Counts a failure against the queue head when the fill THREW rather than reported.

      The three-strikes rule lives inside fillOne(), which means it only runs when the render comes back with an error to record. A JS-level throw -- an uncatchable one out of the wasm import, say -- skips it entirely, and the row it should have struck is the row the alarm re-arm reads to decide whether to fire again in 1 ms. That combination is a spin, and it was measured as one.

      Parameters

      • error: string

      Returns number | null

      the attempts now recorded, or null when the queue was empty

    • Runs the host tripwires and moves the repair ladder.

      ON THE ALARM AND NOWHERE ELSE. Two reasons, and neither is style: recordFinding() is a row write and rows written is the meter that binds the regeneration ceiling, so a per-request tripwire pass would spend the budget it exists to watch; and a waiting visitor outranks bookkeeping, which is the same rule that puts GC and cron after the fills.

      The state is persisted only when it CHANGES. A healthy site writes zero rows here, so the whole layer is free until something is actually wrong.

      Parameters

      • outcomes: (Payload | null)[]

      Returns Finding[]

    • Restores a stored heap into this instance, or explains why it refused.

      Called AFTER the mount and after the bridge and capabilities are installed, because those populate the JS side that the heap's vrzno handles index into. Restoring before them would leave the interpreter holding indices into an array that does not exist yet.

      The fd table is asserted BEFORE the memcpy and the refusal is loud. Dropping /dev/urandom's descriptor alone throws RandomException; dropping the three sqlite descriptors gives a locking-protocol error after an 80-120 second stall, which on the edge is a hung request rather than an error. A refusal costs one boot; proceeding costs a hang.

      Parameters

      • binary: SiteBinary
      • opts: { maxChunks?: number } = {}

      Returns Promise<Payload>

    • Whether a database-update run is in progress and owes the alarm chain work.

      One indexed read of one row, so an ordinary alarm on a site that has never run updb pays essentially nothing for this check.

      Returns boolean

    • Advances a database-update run by one beat.

      updbStep() owns no transport, no alarm and no env, exactly like cronStep(), so the dependency bag is assembled here. It refuses on a cold interpreter rather than booting one, because boot is 4,019 ms of indivisible CPU and a beat that starts cold cannot fit any per-invocation budget.

      Returns Promise<{ updb: Payload }>