Skip to content

Add River for JavaScript and TypeScript - #1443

Merged
brandur merged 44 commits into
masterfrom
bg/js-port
Oct 5, 2026
Merged

brandur merged 44 commits into
masterfrom
bg/js-port

Conversation

@bgentry

@bgentry bgentry commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

riverqueue 0.1 can only insert jobs. A Node service can enqueue work for Go workers, but it can't run workers itself, list or cancel jobs, or take part in maintenance. It also rounds values River depends on: job IDs become JavaScript numbers and timestamps lose precision, so a 64-bit ID written by Go may not round-trip. This PR moves the JavaScript implementation into River as js/ and rebuilds it as a complete River implementation. It inserts, works, and maintains jobs in the same database as River for Go and Rust, on PostgreSQL and SQLite.

This builds on the River Rust port (#1442), now merged. Review from "import riverqueue-js history into js/" onward; the commits before it are riverqueue-js's history, imported so git blame keeps working.

This supersedes riverqueue/riverqueue-js#38. After this merges, riverqueue-js will be archived.

The first two commits bring the package into River:

  • History import. riverqueue-js's history is merged in with every path moved under js/, so git log and git blame follow the sources back to their origin. Its tags aren't imported, because its v0.1.0 would collide with River's Go release tags. The older riverqueue-js commits that appear in the log come in through this merge.
  • Package layout. All packages are versioned 0.50.0-alpha.1 to match River, and each one's repository points at River with its directory.
  • Go module. A stub js/go.mod keeps js/ out of the Go module zip that the proxy serves for github.com/riverqueue/river. It also keeps Go tooling out of the workspace and its node_modules.
  • Licensing. JavaScript keeps its LGPL-3.0-or-later license in js/LICENSE.
  • CI and make targets. CI is .github/workflows/js.yaml, filtered to run only when js/, River's migrations or SQL, or the workflow changes. make targets such as build/js, lint/js, test/js, and doc/js call the workspace's scripts. generate/js-migrations and verify/js-migrations join the generate and verify targets, and Dependabot updates the workspace from River's configuration.

Cross-language conformance isn't part of this PR. It's split out the same way #1442 split it for Rust. The cron and snooze-counter goldens the tests use are static copies, with their provenance noted. The JavaScript conformance adapter is kept on bg/js-conformance-adapter for a separate conformance project.

What the rest of the PR adds:

  • Core runtime (riverqueue).
    • Jobs are declared with defineJob. Their args are validated by any Standard Schema library on insert and again before work.
    • IDs are bigint, timestamps are Temporal.Instant, and args and metadata use an exact JSON value model, so no database value is rounded.
    • client.start() runs a supervised runtime with per-queue producers, AbortSignal cancellation, timeouts, stuck-job detection, retries, snoozes, resumable steps, transactional completion, batched completion, leader election, and Go's maintenance services.
    • cron() parses expressions exactly like robfig/cron's ParseStandard.
    • It requires Node 26 with native Temporal.
  • Drivers. @riverqueue/driver-pg runs the full runtime on node-postgres. @riverqueue/driver-prisma is insert-only. The new @riverqueue/driver-sqlite runs the full runtime on node:sqlite. Drivers are opaque handles, and River's operations are reachable only through riverqueue/unstable-driver, which semver doesn't cover.
  • Migrations. @riverqueue/migrate bundles River's PostgreSQL and SQLite migration lines. They're copied byte for byte from River's Go drivers, and a SHA-256 manifest is verified before anything runs.
  • Other packages. @riverqueue/cli has migrate-* commands, bench, and a codemod-0.1 upgrade codemod. @riverqueue/worker-threads runs CPU-bound handlers in worker threads. @riverqueue/test has test helpers.

This is a breaking change for 0.1 users. js/docs/migrating-from-0.1.md lists every API change. riverqueue codemod-0.1 rewrites the mechanical ones and leaves TODO(riverqueue-0.1) markers on anything it can't handle. A fixture pins the published 0.1 tarball and checks the codemod's output.

Some behaviors are deliberate and match Go:

  • Caller transactions. An operation given a caller's transaction as { tx } runs directly in it, with no savepoint or nested transaction, on every driver. This matches River Go after Avoid opening subtransactions when already in a transaction #1420. If an operation fails after writing, its writes stay in the caller's transaction for the caller to roll back.
  • JSON exactness. Unique key hash inputs and job list cursors are encoded byte for byte like Go, including Go's escaping and sorted top-level keys, because other implementations hash or decode those exact bytes. Other stored and notified JSON only has to be semantically equivalent to Go's. There are no APIs whose only purpose is to reproduce Go's exact bytes.
  • YugabyteDB. PgDriver and PrismaDriver detect YugabyteDB. Unique inserts then use a river:unique_nonce in place of xmax. Unless yb_enable_listen_notify is on, clients send no notifications and poll as if pollOnly were set. The migrator skips advisory locks on YugabyteDB, like Go's.

Here's a suggested review order:

Commits Notes
import riverqueue-js history into js/, wire the JavaScript workspace into River The history merge, plus repository metadata, the js/go.mod stub, and moving the workflow into River's .github.
build for Node 26… through add completion batching and background task supervision Foundations: toolchain, errors, logger and metrics, the JSON value model, durations and timers, and the completion batcher. Each one is small and standalone.
rebuild the client as a complete River runtime This is the main commit, about 25k lines. It can't be split into smaller commits that each build, because the new client, the driver seam in src/driver.ts/unstable-driver.ts, and the rewritten PgDriver and PrismaDriver depend on each other, and the 0.1 surface and its tests are replaced in place. I'd suggest reading src/index.ts, job-definition.ts, client.ts, then runtime/ and services.ts, then driver/pg/src.
test job definitions… through test the pilot seam's operations… Unit and property tests for the core, split by area.
add cron schedules…, add @riverqueue/migrate… Self-contained features.
test the PostgreSQL driver…, test the Prisma driver… Driver integration tests, which need the migrations from the previous commit.
add a SQLite driver…, test SQLite against Go's riversqlite… The SQLite driver and its parity tests with Go's riversqlite.
add @riverqueue/worker-threads…, add @riverqueue/test…, add the riverqueue command line…, add a riverqueue 0.1 upgrade codemod Separate packages.
add runnable worker examples, document the runtime… Examples, the README, guides, and the changelog.
check packed packages…, keep API reports…, catch unused exports…, run every JavaScript gate in CI Release checks, the full js.yaml workflow, and the make targets. The generated .api.md reports are worth skimming as a summary of the public API.

brandur and others added 30 commits November 8, 2023 12:47
Add insert-only River TypeScript package, similar to what we have
already for Python and Ruby. Like the other packages, it's split up into
subpackages for each supported DB package/ORM, which so far is `prisma`
and `node-postgres`.
Follows up #1 with a little forgotten code review feedback I forgot to
push. It's currently possible for multiple insertions that have the same
unique properties to be in the same batch, which will cause Postgres to
fail with an error saying that the row's already been updated once and
can't be updated again. Here, deduplicate inside the batch to handle
this case.
Prepare initial release 0.1.0 and update release instructions while
we're at it (these were prospective and hadn't actually been run
before).
)

| Package | From | To |
| --- | --- | --- |
| [eslint](https://github.com/eslint/eslint) | `10.3.0` | `10.5.0` |
| [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) | `8.20.0` | `8.22.0` |
| [prettier](https://github.com/prettier/prettier) | `3.8.3` | `3.8.4` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.59.2` | `8.62.0` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.5` | `4.1.9` |

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Vitest's optional Vite peer resolves to 8.0.10, which is affected by
Windows path-handling advisories. Because Vite is not a direct dependency,
pnpm does not advance that peer resolution independently.

Require Vite ^8.0.16 as a development dependency and regenerate the
lockfile. This establishes the fixed release as the compatibility floor,
lets routine updates select later Vite 8 releases, and updates the matching
Rolldown native artifacts.
Prisma 7.9.0 pins its internal `@prisma/dev` package to versions
that depend on vulnerable `find-my-way` 9.6.0 and `valibot` 1.2.0.
Dependabot cannot update those exact transitives independently.

Bump the Prisma CLI, client, and PostgreSQL adapter packages together
to 7.9.1. Its `@prisma/dev` 0.24.17 dependency resolves
`find-my-way` 9.7.0 and `valibot` 1.4.2 while preserving package
alignment across both Prisma workspaces.
Merge the history of riverqueue-js, the JavaScript implementation of
River published as `riverqueue@0.1.0`, `@riverqueue/driver-pg@0.1.0`,
and `@riverqueue/driver-prisma@0.1.0`, with every path moved under
`js/`, so `git log` and `git blame` follow the JavaScript sources from
their origin.

The repository's tags aren't imported: its `v0.1.0` would collide with
River's Go release tags.
The JavaScript packages now live in `js/` of the River repository.
Point each package's `repository` at River with the package's
`directory`, and its `bugs` and `homepage` at River too, and update
documentation links that pointed at the old repository.

Add a stub `go.mod` in `js/`. Go excludes nested modules from a
module's zip, so the module that the Go proxy serves for
`github.com/riverqueue/river` carries no JavaScript, and `go test
./...`, `go vet ./...`, and golangci-lint skip the workspace and its
`node_modules`.

Run the JavaScript workflow from River's `.github`, in `js/`, when the
workspace, River's migrations, or the workflow changes, migrating its
test databases with this revision's River CLI. Dependabot updates the
workspace's npm dependencies from River's configuration.
Target ES2025 with the `ESNext.Temporal` lib on Node 26, whose official
builds enable `Temporal`, and build every package with TypeScript 6. The
runtime added on top of this uses `Temporal` for durations and instants
throughout.

Exclude test files from the published build, and add a separate program
that typechecks sources and tests together via `typecheck:tests`.

Give Vitest a shared setup file that attributes escaped rejections and
uncaught exceptions to the test that caused them, fails files that leave
new event-loop handles open, and pins fast-check's seed so property tests
replay identically. `test:coverage` adds an opt-in V8 coverage report.

Pin transitive `deepmerge-ts`, `mysql2`, and `nanoid` versions to clear
current advisories in Prisma's dependency tree.
Root every error River throws deliberately at `RiverError`, whose
`code` is one of a fixed set of stable categories (`database`,
`validation`, `configuration`, `job_cancelled`, and so on). Each
subclass narrows `code` to its own category, so callers can branch with
either `instanceof` or a `switch` on `code`.

Validate queue names, job kinds, and user-specified IDs with River's
portable grammar, including Go's `|` queue separator. Lookups of an
existing queue (get, pause, resume, update) don't check the grammar, so
a name no queue can have is simply not found, like Go.

`assertRuntimeSupport` fails fast with a clear error on runtimes without
`Temporal`, instead of a later `ReferenceError` or a lossy timestamp
fallback.
Define the `Logger` River writes to with pino's argument order, an
attributes object first and then the message, so `logger: pino()` and
most structured loggers work as is. Without a configured logger, `warn`
and `error` go to `console`; `logger: false` silences them.

Publish typed runtime metrics (fetch counts and durations, dropped and
requeued completions) on the `riverqueue:metric` diagnostics channel,
which costs nothing without a subscriber.
Job args, metadata, and output cross a JSON boundary that every River
implementation reads and writes. Validate and copy values into that
domain with `toJsonValue`/`toJsonObject`, which reject accessors, class
instances, sparse arrays, cycles, non-finite numbers, unsafe integers,
and `bigint` instead of relying on `JSON.stringify`'s lossy coercions.
Non-finite numbers fail with Go's `encoding/json` wording.

Numbers that can't round-trip through a JavaScript `number`, such as a
64-bit ID written by another language, parse to an `ExactJsonNumber`
built on Node's raw-JSON primitive, so they pass back through River
unchanged.

Encode unique-key hash inputs and job list cursors the way River Go
does: Go's escaping of `<`, `>`, `&`, U+2028, and U+2029, and top-level
keys sorted bytewise and written as `sjson` assembles them, so every
implementation hashes the same bytes. Property tests cover parsing,
canonical decimals, and the unique encodings.
Every River duration option takes a `Temporal.Duration` or a duration
like such as `{ seconds: 5 }`. `toDuration` rejects bare numbers,
calendar units, and negative values with an error naming the option,
and bounds durations by Go's maximum `time.Duration`, River's protocol
limit.

Add the timing primitives the runtime waits on. Every timer is
unreferenced, so a pending delay never keeps a stopped client's process
alive, and delays longer than Node's 24.8-day timer limit are waited out
in chunks like Go's timers. `LinkedAbortSignal` links a per-operation
signal to a parent and is disposed when the operation settles, avoiding
`AbortSignal.any`, whose cleanup is quadratic in a long-lived parent's
dependents. Waits and deadlines go through a replaceable `RuntimeTimer`,
and `ManualTimer` drives one on a virtual clock so tests run
deterministically.

Encode PostgreSQL timestamps truncated to whole microseconds the way
pgx does, so every engine stores the same instant.
Batch job outcomes with River's bounded persistence policy: one query
runs while the next batch accumulates, and a second concurrent query
starts only for a full batch, which bounds database pressure and keeps
hook and event timing predictable. A batch whose persistence fails is
either requeued or dropped; a dropped completion leaves its job
`running` for the rescuer to recover.

`TaskSupervisor` owns the runtime's background tasks and turns a
detached failure into one observable runtime failure. `EventDispatcher`
delivers events to `onEvent` hooks in order but off the completion path,
so a slow hook doesn't hold worker or completion capacity until its
queue fills. `EventLoopDelayMonitor` samples event-loop delay on an
unreferenced timer for reporting.
Replace the insert-only 0.1 client with a River implementation that
inserts, works, and maintains jobs in the same database as River for Go
and Rust.

Jobs are declared with `defineJob`, whose args are validated by any
Standard Schema library or an explicit decoder on insert and again
before work. Inserts take plain `{ job, args, options }` items and
report `status: "inserted" | "duplicate"`; unique options use `unique`
with `Temporal` durations and match Go's unique keys, including
`excludeKind` rules and duplicate keys within one batch. Job IDs are
`bigint` and timestamps `Temporal.Instant`, so no database value is
rounded.

`client.start()` runs a supervised runtime: per-queue producers with
concurrency limits and dynamic queues, cooperative cancellation through
`AbortSignal`, job timeouts and stuck-job detection, work outcomes
(`complete`, `snooze`, `discard`, `cancel`), retry policies, an error
handler, resumable steps, output recording, and transactional
completion. Completions are persisted in bounded batches, and the
runtime retries transient database failures with backoff. Leader
election drives Go's maintenance services: scheduler, rescuer, job and
queue cleaners, reindexer, and periodic jobs. Hooks, work and insert
middleware, plugins, event subscriptions, and `diagnostics_channel`
publishing extend it. `client.jobs` and `client.queues` query and
control jobs and queues with keyset pagination whose job cursors use
Go's `JobListCursor` text.

Database access goes through drivers that are opaque handles; their
operations are reachable only through the `riverqueue/unstable-driver`
seam, which is not covered by semver. Rewrite `PgDriver` and
`PrismaDriver` against it: `PgDriver` implements the full runtime on
`node-postgres`, including YugabyteDB detection, while `PrismaDriver`
supports insertion. Like River for Go, an operation given a caller's
transaction runs directly in it and opens no savepoint or nested
transaction, so a failure after River's write leaves the write for the
caller to roll back. The drivers' tests are rewritten in later commits,
since their integration tests need the canonical migrations.

Enable `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, and
`verbatimModuleSyntax`, and lint with typescript-eslint's strict
type-aware rules. Every package build copies the root `LICENSE`.
Cover `defineJob` with Zod and Valibot schemas, decoders, and the
compile-time JSON checks on producer and worker types; the exact JSON
round trip of job rows; attempt error decoding with Go's
`encoding/json` leniency; transform plugin payload privacy; insert
notification limiting; PostgreSQL capability detection; and event
subscriptions.

A property test checks every unique option combination against a
reference key, including key ordering, `byArgs` path selection,
rejection of paths Go reads as array indexes, and UTC period windows.
Cover job and queue query normalization, metadata-and-output-only job
updates, bounded bulk deletion, and the keyset SQL each job list order
renders. Job list cursors decode either base64 alphabet, treat Go's zero
time as no time, and reject times Go can't encode; property tests check
that every cursor round-trips and that edited or arbitrary input fails
only with a cursor validation error.

Cover the maintenance batcher's circuit breaker, which switches a
service to reduced batches after consecutive timed-out batches like Go.
Exercise the runtime end to end: claims and fetch cooldowns, work
outcomes and retry delays, payload decoding before work, cooperative
cancellation and remote cancellation after a timeout, stuck-job
handling, hooks and event delivery off the completion path, completion
timeouts and persistence failures, notification stream recovery,
`fetchOnlyKnownKinds`, clients without leader election, graceful and
forced shutdown, and unknown-option rejection. A stress suite races
claims, cancellations, and stops, and a fault suite injects database
failures.

Cover `Workers` registration (definitions, handler factories, kind
aliases), outcome values, resumable steps, and snooze counting, which
matches River Go's executor on River's shared fixture cases.
Cover leader election and term renewal, including bidding within 50 ms
of another client's resignation and ending a term at its local deadline
while renewal hangs; the scheduler, rescuer, and cleaners with their
batch bounds, Go-compatible promotion horizons, and cancellation with
the leadership term; reindexing on its schedule; and the periodic job
enqueuer with durable records, hooks, and dropped failures.

A property test checks that the periodic registry advances like Go's
periodic job enqueuer under any order of commands.
Cover the operations a pilot can intercept through
`riverqueue/unstable-driver`: continuations that run River's own
operation at most once, replacement of stuck-job reads and rescues,
rejection of rows River can't insert, and each row's arguments from
before the client's argument transforms, which the insert interceptor
sees beside the transformed rows River stores.

Drive the runtime through a pilot's producer sessions: claims bounded
by queue capacity and limited to known kinds, fixed-rate producer
reports that never overlap, queue names reserved until their generation
stops, supervised services restarted after backoff, and peer attempts
that accept outcomes only for their own peers and retry failed peers on
their worker's retry policy, like Go. A claim that breaks its
contract stops the runtime after its running attempts finish.
`cron()` builds a periodic schedule from a cron expression parsed
exactly as River Go parses it, with robfig/cron's `ParseStandard`:
five-field expressions, descriptors such as `@daily`, `@every` with
Go's `time.ParseDuration` syntax, and `CRON_TZ=` prefixes. Next times
match Go's, including across daylight saving transitions. Expressions
without a `CRON_TZ=` prefix or `timeZone` option use the process's
local time zone, as in Go.

Tests run River's shared cron fixtures, copied with their provenance,
plus property tests that occurrences are strictly increasing whole
seconds and that each is found again from just before it.
Bundle River's PostgreSQL and SQLite `main` migration lines, versions 1
through 8, copied byte for byte from River with a manifest of SHA-256
digests that the loader verifies before running anything.
`createMigrator` takes the driver a client uses, or a bare connection
(`{ pool, schema }`, `{ client, schema }`, or `{ database }`), and plans
and applies migrations up or down with River's version bookkeeping.

Each step runs in a transaction under a lock: a transaction-scoped
advisory lock on PostgreSQL, skipped on YugabyteDB like Go's migrator,
and `BEGIN IMMEDIATE` on SQLite, retried asynchronously while another
connection holds the write lock.

`generate:migrations` copies the migrations from River's Go drivers,
and `verify:migrations` checks the committed copy and its manifest
against them.
Unit tests cover `PgDriver`'s typing and surface and the parsing of
every PostgreSQL value River reads itself, such as `bigint` IDs,
microsecond timestamps, and arrays.

Integration tests run each driver operation against PostgreSQL with
River's migrations applied, in the default and a custom schema,
including stale rescue snapshots, client stops, and clients without
leader election. Further suites cover runtime resilience (row locks
that outlast a statement timeout, transactional completion, `LISTEN`
failures, a caller-owned pool's size), the pilot seam on real
transactions, including caller transactions without savepoints and the
untransformed arguments an insert interceptor sees, a multi-client
stress test, and
PostgreSQL-compatible servers without `xmax` or `LISTEN`/`NOTIFY`,
simulated the way River Go's tests simulate YugabyteDB.
Unit tests cover the insert-only typing, exact IDs and timestamp
precision, the complete insert contract, unique conflicts, YugabyteDB
nonces without notifications, and inserts without a caller transaction,
which run in a Prisma interactive transaction with the driver's
transaction options.

Integration tests insert on a real PostgreSQL database through a
node-postgres stand-in for Prisma's raw query API, including unique
skips that keep the existing job's kind and notifications on commit,
and through a generated Prisma client with `@prisma/adapter-pg` to
check exact integers and Prisma's interactive transactions.
bgentry and others added 13 commits October 5, 2026 16:44
`@riverqueue/driver-sqlite` runs River's complete runtime on Node's
built-in `node:sqlite`, with River Go's SQLite schema and queries. Like
River for Go, River works on a private connection to the application's
database file, in WAL mode with a zero busy timeout, so application
statements never join River's transactions. While another connection
holds the write lock, River retries with an asynchronous backoff so the
event loop keeps running.

Pass an application handle with a transaction open as `{ tx }` to run
River's statements directly in it, without a savepoint, like River for
Go; validation runs before any write. `transaction()` begins one
with `BEGIN IMMEDIATE`, retrying asynchronously while the database is
busy. An insertion without `{ tx }` runs in a transaction River owns,
begun lazily at its first statement. Insert middleware and hooks run
inside it, so when that transaction is still open at the event loop's
next turn River rolls it back and fails the insertion with a
`TransactionScopeError`, rather than holding the write lock across I/O.
`SqliteDriver.memory()` and `driver.connect()` share an in-memory
database.

Tests cover the driver's operations, operation scopes,
the FIFO handle lock, notification batching, and keyset pagination
properties.
Check that the SQLite driver reads and writes rows exactly as River
Go's `riversqlite` does: rows only Go's wider decoding accepts, null
cancellation markers, update, cancellation, retry, and output JSON,
near-future retries and snoozes stored as available, and rescue guards
against stale snapshots.

Run the whole runtime on SQLite, including while another connection
holds the write lock, and drive the pilot seam through River's private
connection and application transactions: interceptors that throw
after `next()`, insert interceptors that see each row's arguments from
before argument transforms, caller transactions without savepoints,
nested operations, finalized job deletion filters, metadata
notifications, and peer claims through a graceful stop.
Run handlers in a bounded pool of Node worker threads so CPU-heavy work
can't block the event loop River claims, completes, and stops on.
`WorkerThreads` is a work executor: register a handler by module URL
and export name with `workers.addExecutor(definition,
executor.handler(definition, { module, exportName }))`, and a
`WorkerThreadModule` type rejects a missing or mistyped export at
compile time.

Args are decoded and validated in the main thread; River JSON crosses
the boundary as text, preserving exact numbers, and other decoded args
must survive structured clone unchanged. Outcomes, output, metadata, and
logs cross back as River JSON with River's size bounds, and a thrown
error fails the attempt with a bounded `WorkerThreadHandlerError`.

When an attempt is cancelled, times out, or its client stops, the
handler's signal aborts; a handler that hasn't settled after the
client's `jobStuckThreshold` has its thread terminated, failing with a
`JobAbortedError` when a stop caused the abort. A thread that crashes
or exits is discarded, and a replacement starts when a task needs it.
`createTestClient` returns an insert-only client with deterministic IDs
and a log of every insertion, so producer code typed against
`InsertClient` can be tested without a database. `requireInserted`,
`requireNotInserted`, and `requireManyInserted` assert on that log like
Go's `rivertest`, and their `InDatabase` variants check jobs a real
client inserted through `client.jobs`.

`testJob` builds a realistic running job whose args are validated by
the definition exactly as the runtime validates them, and `workOnce`
runs one handler, or the one a `Workers` registry has for the job's
kind, without a database or background runtime. The helpers use the
production API's exact `bigint`, `Temporal.Instant`, and JSON values.
Mirror River's Go CLI for migrations: `migrate-up`, `migrate-down`,
`migrate-list`, `validate`, and `migrate-get` on PostgreSQL
(`postgres://` URLs or `PG*` variables) and SQLite (`sqlite://PATH`),
with `--schema`, `--target-version`, `--max-steps`, `--dry-run`,
`--show-sql`, and Go's 10 second default `--statement-timeout`. `run()`
runs the same commands in-process, and an embedding package can add its
own migration lines, selected with `--line`, as Go's CLI allows.

`riverqueue bench` inserts and works no-op jobs on a disposable
PostgreSQL database and reports throughput with the same default
workload as River's Go and Rust benchmarks, plus peak running jobs,
pending completions, pool connections, event-loop delay, and memory. It
requires an explicit `--database-url` and confirmation, since it empties
River's tables.
`riverqueue codemod-0.1` rewrites code written for the insert-only 0.1
client to the current API with the project's own `typescript` package:
argument classes whose persisted args are their constructor parameter
properties become `defineJob` definitions, `JobArgsObject` and
`InsertManyParams` become definitions and plain batch items,
`uniqueOpts` becomes `unique` with duration values, and renamed option
types are imported under their old names. It edits only the expressions
it rewrites, is idempotent, and marks what it can't rewrite with
`TODO(riverqueue-0.1)` comments. `--check` exits 1 if anything would
change. The migration guide describes every change and what the
codemod leaves to finish by hand.

A fixture pins the published `riverqueue@0.1.0` tarball by its registry
hashes along with its documentation, a 0.1 consumer, the codemod's exact
output, and the hand-migrated result. `migration:legacy` verifies the
archive, reruns the codemod, requires every remaining compiler error to
sit under a `TODO` marker, and compiles the result with TypeScript 6 and
the next TypeScript release, guarding packed paths against machine-local
names.
Add examples alongside the existing node-postgres and Prisma inserters:

- `pg-worker` migrates PostgreSQL, works a job that snoozes once and
  then inserts a follow-up through the worker's client, and stops
  gracefully on completion or `SIGTERM`.
- `sqlite-worker` migrates an in-memory SQLite database and works a
  typed job in-process.
- `graceful-shutdown` stops a client while a handler is still running
  and shows the completion persisted before `stop` resolves.
- `hooks-metrics` feeds hooks, middleware, and events into a small
  metrics collector.
- `worker-thread-cpu` runs a CPU-heavy handler on
  `@riverqueue/worker-threads`.
- `mixed-language` inserts a versioned payload under a stable kind that
  a Go, Rust, or JavaScript worker can decode alike.
Rewrite the README and guide for the current API: requirements (Node 26
with native `Temporal`, TypeScript 6, ESM), a PostgreSQL quickstart, and
sections on defining, inserting, working, and querying jobs and on
exact values. Add guides for databases, the runtime, errors and retries,
periodic jobs, resumable jobs, observability, testing, deployment, and
development, a README for each package, and changelog entries for the
release.

`docs:snippets` typechecks every TypeScript snippet in the READMEs and
guides as its own module against the workspace sources, and `docs:api`
builds TypeDoc reference documentation for every package's entry point.
Prepare every package's metadata for publishing with provenance: ship
compiled output with its sources and declaration maps, the README, and
the license, and pack with `prepack`.

`package:check` packs each package and inspects the archive with
`publint` and `attw`, checks dependency metadata, source maps, and that
no archive carries machine-local paths. It then installs the tarballs
into fresh consumer projects: JavaScript consumers must not need type
packages, TypeScript consumers compile under `node20` and `nodenext`
with TypeScript 6 and the next release, CommonJS consumers load River
through `require(esm)`, mismatched package versions are rejected, the
examples build against the tarballs, and `node --test` suites exercise
the packed packages on SQLite and, when configured, PostgreSQL.
`license:check` limits production dependency licenses to an allow list.
Generate an `.api.md` report from the built declarations of each
published entry point, listing every exported declaration with its
TSDoc, so API and documentation changes show up in diffs.
API Extractor can't analyze these packages, since its bundled compiler
rejects the `ES2025` target and the global `Temporal` types, so the
script uses the workspace's own compiler. Generation fails on a
"forgotten export", an exported declaration that refers to a type its
entry point doesn't export, unless the name is allow-listed with a
reason.

`api:report` writes the reports and `api:check` fails when they're out
of date.
Run knip as part of `lint`, so an export, namespace member, or exported
type that nothing in the workspace uses fails the check. The
worker-threads package's thread module is loaded by URL rather than
imported, so knip is told it's an entry point.

`typecheck:next` typechecks sources and tests with the next TypeScript
release as well as TypeScript 6.
Build and typecheck with both compilers; check API reports and docs,
documentation snippets, the 0.1 migration fixture, and the migration
mirror; lint, check formatting and licenses, and audit dependencies.
Run unit tests on Node 26 and integration tests on PostgreSQL 14
through 18, and check the packed archives and build the examples
against them. The workflow runs when the workspace, River's migrations
or SQL, or the workflow itself changes, and a shared action sets up
Node.js, pnpm, and the workspace.

Add `make` targets that delegate to the workspace's scripts with
`pnpm -C js`: `build/js`, `lint/js`, `test/js`, `test/js/integration`,
`doc/js`, `check/js/dependencies`, `check/js/package`, and
`generate/js-migrations` and `verify/js-migrations`, which join the
`generate` and `verify` aggregates.

Limit Dependabot's grouped JavaScript updates to minor and patch
releases so major updates arrive separately, and have it update the
composite action too.
From Codex:

> Fixed the shared timing helper in js/src/pilot-runtime.test.ts:273. It
> stopped after 10,000 event-loop turns, which could finish before a real
> cooldown expired. It now uses bounded, time-based polling.
@brandur
brandur marked this pull request as ready for review October 5, 2026 21:59
@brandur

brandur commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Rebased and fixed an intermittent test.

@brandur
brandur merged commit 9be933c into master Oct 5, 2026
38 checks passed
@brandur
brandur deleted the bg/js-port branch October 5, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants