Skip to content

Concepts

Jobs. A job has a jobId, a queue, a lane, an integer priority, an opaque JSON payload and an optional recovery: { maxRecoveries }. IDs, queue names and lanes are 1 to 256 characters. Watchdog stores the lane but does not schedule by it. A job moves through three states:

  • queued: accepted and waiting.
  • running: claimed by one tick and handed to your executor.
  • settled: finished, with an outcome of completed, failed, cancelled, outcome_unknown or interrupted, and an optional failureReason.

Ingest is idempotent. ingest(job) returns queued for a new ID and duplicate for an identical replay. A replay with the same ID and different queue, lane, priority, payload or recovery policy fails.

One tick, one job. tick() picks the queued job with the highest priority, oldest first, across all queues. It claims that job, runs your executor, and settles the exact claim it took. A second tick() on the same runtime while one is in flight returns { status: "busy" } instead of running in parallel. Your executor returns one of completed, failed (with an optional short reason), cancelled or outcome_unknown. If it throws or is interrupted, Watchdog settles the job as failed with its own reason code, such as execution_failed or execution_interrupted.

Host ports. You supply four things:

  • owner: the SQLite capability shown in the quick start.
  • execute(job, signal): runs one claimed job. Honour the AbortSignal.
  • isAlive(job): answers whether a running job’s execution still exists.
  • wake.recompute(): arms your next timer or alarm after durable state changes.

Disappearance recovery. Each tick first checks the oldest running claim. If isAlive returns false, the execution is gone. A job with recovery budget left goes back to queued. Otherwise it settles as interrupted. If isAlive throws, Watchdog does not guess. The claim stays as it is, and an otherwise idle tick reports { status: "liveness_unknown", jobId, reason }.

Cancellation. cancel({ jobId, cancellationId }) records a durable cancellation. A queued job settles as cancelled on its next tick. A job the current runtime is executing is interrupted and settles as cancelled. Replaying the same cancellationId returns replayed. A different one returns conflict. Pass onlyIfQueued: true to refuse cancellation once a job has started.

Owner transactions. runtime.transactional is a synchronous projection for code already inside your own SQLite transaction. enqueueAcceptedJob and recordCancellationIntent use the same mutation as ingest and cancel, so your domain write and the job commit or roll back together. readQueueHead and readLatestSettledJob read queue state inside that transaction.

Queue projection. Pass project to Watchdog.make to receive a { queue, head, settled? } transition inside the same transaction as every ingest, claim, cancellation, settlement, recovery and purge. Use it to keep a per-queue status row in step with Watchdog. A throw from project fails the transition.

Purge. purge(queue) deletes every queued job in one queue. Running and settled jobs stay.

@fungi.computer/watchdog/d1 exports createD1Watchdog and watchdogD1Schema. @fungi.computer/watchdog/postgres exports createPostgresWatchdog and watchdogPostgresSchema. Apply the package-owned schema through your migration tool before you open a runtime. Opening a runtime never migrates a database.

These runtimes have the same ingest, read, readQueueHead, tick, cancel, purge and hasPendingWork operations. They take execute, wake and an optional now clock instead of an owner and liveness probe. Watchdog answers liveness itself from the claim’s lease.

D1 enqueue(job) returns a prepared statement for the producer’s native batch. enqueueFromSelect({ text, values }) accepts a trusted native SQLite ?NNN-bound SELECT of job_id, queue_name, lane, priority, payload_json and nullable recovery_max. Watchdog supplies its INSERT and conflict checks. This lets a job depend on facts conditionally inserted earlier in the same batch, without a JavaScript read in between. PostgreSQL enqueue(client, job) uses the caller’s checked-out transaction client. The caller owns BEGIN, COMMIT and ROLLBACK. An identical replay does nothing. A conflicting identity or cargo fails the whole native transaction.

Both adapters use the same lifecycle engine as the SQLite entrypoint. Claims stay private, with two-minute leases, ten-second renewal and bounded renewal I/O. Execution must honour its AbortSignal. Renewal and settlement reject expired or replaced claims. A failed liveness probe never declares an execution dead. Database time checks stop an in-flight renewal from resurrecting an expired claim. The optional now clock replaces database time for deterministic tests. Production hosts should use the default. A durable host wake still drives ticks and disappearance recovery after a process dies. Provider effects remain at-least-once.

purgeD1Queues and purgePostgresQueues return parameterized erasure statements for queues selected by trusted producer SQL. The caller joins them to its native batch or transaction, and Watchdog stays the only owner of job deletion SQL. These helpers remove every attempt in the selected queues and expose no claim or settlement controls. Carrier composes them with envelope erasure.