Concepts
Core concepts
Section titled “Core 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 anoutcomeofcompleted,failed,cancelled,outcome_unknownorinterrupted, and an optionalfailureReason.
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 theAbortSignal.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.
Shared D1 and PostgreSQL storage
Section titled “Shared D1 and PostgreSQL storage”@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.