Repository navigation
Add River for JavaScript and TypeScript - #1443
Merged
Merged
Conversation
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.
`@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
marked this pull request as ready for review
October 5, 2026 21:59
Contributor
|
Rebased and fixed an intermittent test. |
brandur
approved these changes
Oct 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
riverqueue0.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 asjs/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 blamekeeps working.This supersedes riverqueue/riverqueue-js#38. After this merges, riverqueue-js will be archived.
The first two commits bring the package into River:
js/, sogit logandgit blamefollow the sources back to their origin. Its tags aren't imported, because itsv0.1.0would collide with River's Go release tags. The older riverqueue-js commits that appear in the log come in through this merge.0.50.0-alpha.1to match River, and each one'srepositorypoints at River with itsdirectory.js/go.modkeepsjs/out of the Go module zip that the proxy serves forgithub.com/riverqueue/river. It also keeps Go tooling out of the workspace and itsnode_modules.js/LICENSE..github/workflows/js.yaml, filtered to run only whenjs/, River's migrations or SQL, or the workflow changes.maketargets such asbuild/js,lint/js,test/js, anddoc/jscall the workspace's scripts.generate/js-migrationsandverify/js-migrationsjoin thegenerateandverifytargets, 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-adapterfor a separate conformance project.What the rest of the PR adds:
riverqueue).defineJob. Their args are validated by any Standard Schema library on insert and again before work.bigint, timestamps areTemporal.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,AbortSignalcancellation, 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'sParseStandard.Temporal.@riverqueue/driver-pgruns the full runtime on node-postgres.@riverqueue/driver-prismais insert-only. The new@riverqueue/driver-sqliteruns the full runtime onnode:sqlite. Drivers are opaque handles, and River's operations are reachable only throughriverqueue/unstable-driver, which semver doesn't cover.@riverqueue/migratebundles 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.@riverqueue/clihasmigrate-*commands,bench, and acodemod-0.1upgrade codemod.@riverqueue/worker-threadsruns CPU-bound handlers in worker threads.@riverqueue/testhas test helpers.This is a breaking change for 0.1 users.
js/docs/migrating-from-0.1.mdlists every API change.riverqueue codemod-0.1rewrites the mechanical ones and leavesTODO(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:
{ 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.PgDriverandPrismaDriverdetect YugabyteDB. Unique inserts then use ariver:unique_noncein place ofxmax. Unlessyb_enable_listen_notifyis on, clients send no notifications and poll as ifpollOnlywere set. The migrator skips advisory locks on YugabyteDB, like Go's.Here's a suggested review order:
import riverqueue-js history into js/,wire the JavaScript workspace into Riverjs/go.modstub, and moving the workflow into River's.github.build for Node 26…throughadd completion batching and background task supervisionrebuild the client as a complete River runtimesrc/driver.ts/unstable-driver.ts, and the rewrittenPgDriverandPrismaDriverdepend on each other, and the 0.1 surface and its tests are replaced in place. I'd suggest readingsrc/index.ts,job-definition.ts,client.ts, thenruntime/andservices.ts, thendriver/pg/src.test job definitions…throughtest the pilot seam's operations…add cron schedules…,add @riverqueue/migrate…test the PostgreSQL driver…,test the Prisma driver…add a SQLite driver…,test SQLite against Go's riversqlite…riversqlite.add @riverqueue/worker-threads…,add @riverqueue/test…,add the riverqueue command line…,add a riverqueue 0.1 upgrade codemodadd runnable worker examples,document the runtime…check packed packages…,keep API reports…,catch unused exports…,run every JavaScript gate in CIjs.yamlworkflow, and themaketargets. The generated.api.mdreports are worth skimming as a summary of the public API.