River for TypeScript: Emit jobs from Node to Go

Brandur Leach

River’s had insert-only clients for Ruby and Python for a while, each of which inserts jobs that’ll be worked by a main Go installation with which they share a database.

Today we’re adding TypeScript, which does the same for the JS/TS ecosystem. Like the main River package, it supports single and bulk inserts, unique jobs, scheduled jobs, transactional enqueueing, and a host of River’s other standard features.

The TypeScript client can’t claim the same disproportionate performance gain as when sending work over from Ruby and Python, but it could be useful in primarily TypeScript environments for breaking off select features to leverage Go’s low memory overhead and sharp build toolchain. TypeScript’s so ubiquitous these days that it’s probably already a part of any stack that contains Go.

Multi-driver support

The package comes with two drivers: node-postgres (pg) and Prisma. Each wraps up specific internals and abstracts them away behind a generalized River insertion API.

// node-postgres
import { Pool } from 'pg';
import { Client } from 'riverqueue';
import { PgDriver } from '@riverqueue/driver-pg';
const pool = new Pool({ connectionString: 'postgres://...' });
const client = new Client(new PgDriver(pool));
// Prisma
import { PrismaClient } from '@prisma/client';
import { Client } from 'riverqueue';
import { PrismaDriver } from '@riverqueue/driver-prisma';
const prisma = new PrismaClient();
const client = new Client(new PrismaDriver(prisma));

After initialization, client.insert and client.insertMany work identically regardless of driver:

const result = await client.insert(new SortArgs(['whale', 'tiger', 'bear']), {
queue: 'high_priority',
});

Transaction support follows each driver’s idioms — a pool client for node-postgres (where by design, transactions are started and committed manually with BEGIN/COMMIT), and $transaction for Prisma passed as an explicit { tx } option to insert functions:

await prisma.$transaction(async (tx) => {
await client.insert(new SortArgs(['whale']), { tx });
await client.insert(new SortArgs(['tiger']), { tx });
});

Minimize dependencies

The project is structured as a monorepo, with the core client and each driver published as separate packages:

PackageDescription
riverqueueCore client, types, and insertion logic
@riverqueue/driver-pgDriver for node-postgres
@riverqueue/driver-prismaDriver for Prisma

A Prisma project doesn’t pull in pg as a transitive dependency, and vice versa — install what you use, and nothing extra. This keeps install size small, avoids peer dependency conflicts, and avoids bloat in case more drivers are added down the road.

Terminal window
# Only install what you need
pnpm add riverqueue @riverqueue/driver-pg pg

We’re not foremost TypeScript developers, so we’re curious to hear more feedback from people who are. If you have strong feelings on our API or design and how it could be potentially improved, let us know.

See also the TypeScript docs for full API detail, or the project on GitHub.