Skip to content

Commit 5784e38

Browse files
authored
Merge branch 'master' into corelate-wip-to-worker
2 parents a18c48e + 77ec0ea commit 5784e38

28 files changed

Lines changed: 4872 additions & 1584 deletions

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,48 @@ A HTTP proxy is available in the [`@pg-boss/proxy`](https://www.npmjs.com/packag
7070

7171
See the [proxy documentation](https://github.com/timgit/pg-boss/blob/master/packages/proxy/README.md) for full configuration and deployment options.
7272

73+
## ORM Transaction Adapters
74+
75+
pg-boss ships adapters for running operations inside ORM-managed transactions. Each adapter wraps the ORM's transaction object as an `IDatabase` you can pass via the `db` option on `send()`, `insert()`, `fetch()`, `complete()`, and other methods.
76+
77+
### Knex / Kysely / Prisma
78+
79+
```ts
80+
import { fromKnex, fromKysely, fromPrisma } from 'pg-boss'
81+
```
82+
83+
```ts
84+
// Knex
85+
await knex.transaction(async (trx) => {
86+
await boss.send('my-queue', data, { db: fromKnex(trx) })
87+
})
88+
89+
// Kysely
90+
await db.transaction().execute(async (trx) => {
91+
await boss.send('my-queue', data, { db: fromKysely(trx) })
92+
})
93+
94+
// Prisma (v7+ with @prisma/adapter-pg)
95+
await prisma.$transaction(async (tx) => {
96+
await boss.send('my-queue', data, { db: fromPrisma(tx) })
97+
})
98+
```
99+
100+
### Drizzle
101+
102+
The Drizzle adapter accepts the `sql` tagged-template function from `drizzle-orm` as a second argument so it can construct parameterised queries without a runtime dependency on `drizzle-orm`.
103+
104+
```ts
105+
import { fromDrizzle } from 'pg-boss'
106+
import { sql } from 'drizzle-orm'
107+
```
108+
109+
```ts
110+
await db.transaction(async (tx) => {
111+
await boss.send('my-queue', data, { db: fromDrizzle(tx, sql) })
112+
})
113+
```
114+
73115
## Requirements
74116
* Node 22.12 or higher for CommonJS's require(esm)
75117
* PostgreSQL 13 or higher

docs/_sidebar.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313
* * [PubSub](./api/pubsub.md)
1414
* * [Workers](./api/workers.md)
1515
* * [Testing](./api/testing.md)
16+
* * [Adapters](./api/adapters.md)
1617
* * [Utils](./api/utils.md)
1718
* [SQL](sql.md)

docs/api/adapters.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# ORM Transaction Adapters
2+
3+
pg-boss operations such as `send()`, `insert()`, `fetch()`, and `complete()` accept a `db` option that lets you run them inside an existing database transaction. This is how you ensure that job creation (or completion) is atomic with your application's own writes — if the transaction rolls back, so does the job.
4+
5+
Each adapter wraps the ORM's transaction object as a pg-boss `Db` (the `executeSql` interface), so pg-boss can execute its own SQL within your transaction.
6+
7+
```ts
8+
interface Db {
9+
executeSql(text: string, values: any[]): Promise<{ rows: any[] }>;
10+
}
11+
```
12+
13+
## Knex
14+
15+
```ts
16+
import { fromKnex } from 'pg-boss'
17+
18+
await knex.transaction(async (trx) => {
19+
// your application writes ...
20+
await trx('orders').insert({ item: 'widget', qty: 1 })
21+
22+
// schedule a pg-boss job in the same transaction
23+
await boss.send('order-processing', { item: 'widget' }, { db: fromKnex(trx) })
24+
})
25+
```
26+
27+
## Kysely
28+
29+
```ts
30+
import { fromKysely } from 'pg-boss'
31+
32+
await db.transaction().execute(async (trx) => {
33+
await trx.insertInto('orders').values({ item: 'widget', qty: 1 }).execute()
34+
35+
await boss.send('order-processing', { item: 'widget' }, { db: fromKysely(trx) })
36+
})
37+
```
38+
39+
## Drizzle
40+
41+
The Drizzle adapter requires the `sql` tagged-template function from `drizzle-orm` as a second argument. This allows pg-boss to construct parameterised queries through Drizzle's public API without adding `drizzle-orm` as a runtime dependency.
42+
43+
```ts
44+
import { fromDrizzle } from 'pg-boss'
45+
import { sql } from 'drizzle-orm'
46+
47+
await db.transaction(async (tx) => {
48+
await tx.insert(orders).values({ item: 'widget', qty: 1 })
49+
50+
await boss.send('order-processing', { item: 'widget' }, { db: fromDrizzle(tx, sql) })
51+
})
52+
```
53+
54+
## Prisma
55+
56+
Requires Prisma v7+ with `@prisma/adapter-pg`.
57+
58+
```ts
59+
import { fromPrisma } from 'pg-boss'
60+
61+
await prisma.$transaction(async (tx) => {
62+
await tx.order.create({ data: { item: 'widget', qty: 1 } })
63+
64+
await boss.send('order-processing', { item: 'widget' }, { db: fromPrisma(tx) })
65+
})
66+
```
67+
68+
## Rollback behaviour
69+
70+
When the ORM transaction is rolled back (either explicitly or by throwing an error), all pg-boss operations executed through the adapter are rolled back as well. This is the primary reason to use these adapters — to guarantee atomicity between your application writes and job scheduling.

docs/api/jobs.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ All retry, expiration, and retention options can also be set on the queue and wi
8080
}
8181
```
8282

83+
pg-boss ships with built-in adapters for popular ORMs. See [ORM Transaction Adapters](./api/adapters.md) for details.
84+
8385
**Deferred jobs**
8486

8587
* **startAfter** int, string, or Date

docs/api/workers.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
Adds a new polling worker for a queue and executes the provided callback function when jobs are found. Each call to work() will add a new worker and resolve a unqiue worker id.
66

7-
Workers can be stopped via `offWork()` all at once by queue name or individually by using the worker id. Worker activity may be monitored by listening to the `wip` event.
7+
Workers can be stopped via `offWork()` all at once by queue name or individually by using the worker id. Worker activity may be monitored by listening to the `wip` event or by polling [`getWipData()`](#getwipdataoptions).
88

99
The default options for `work()` is 1 job every 2 seconds.
1010

@@ -13,7 +13,7 @@ The default options for `work()` is 1 job every 2 seconds.
1313
**Arguments**
1414
- `name`: string, *required*
1515
- `options`: object
16-
- `handler`: function(jobs), *required*
16+
- `handler`: function(jobs): Promise<any>, *required*
1717

1818
**Options**
1919

@@ -165,7 +165,7 @@ In this setup:
165165

166166
**Handler function**
167167

168-
`handler` should return a promise (Usually this is an `async` function). If an unhandled error occurs in a handler, `fail()` will automatically be called for the jobs, storing the error in the `output` property, making the job or jobs available for retry.
168+
`handler` should return a promise (Usually this is an `async` function). If the `handler` returns a value or an object, it will be stored in the `output` property. If an unhandled error occurs in a handler, `fail()` will automatically be called for the jobs, storing the error in the `output` property, making the job or jobs available for retry.
169169

170170
The jobs argument is an array of jobs with the following properties.
171171

@@ -217,6 +217,33 @@ await boss.work('process-video', async ([ job ]) => {
217217
})
218218
```
219219

220+
### `getWipData(options)`
221+
222+
Returns a snapshot of all workers in this instance of pg-boss with state `created`, `active`, or `stopping`. This is the same data payload emitted by the `wip` event, but available on-demand without waiting for a job transition.
223+
224+
Use this for continuous monitoring of worker utilization — for example, driving metrics or autoscaling signals when jobs are long-running and the `wip` event may not fire frequently enough.
225+
226+
**Arguments**
227+
- `options`: object *(optional)*
228+
229+
**Options**
230+
231+
* **includeInternal**, bool, *(default=false)*
232+
233+
If true, includes workers for pg-boss internal queues (e.g., scheduling).
234+
235+
**Returns**: `WipData[]`
236+
237+
```js
238+
// Poll worker utilization every 2 seconds for metrics
239+
setInterval(() => {
240+
const workers = boss.getWipData()
241+
const working = workers.filter(w => w.state === 'active' && w.count > 0).length
242+
const idle = workers.filter(w => w.state === 'active' && w.count === 0).length
243+
console.log(`working: ${working}, idle: ${idle}`)
244+
}, 2000)
245+
```
246+
220247
### `notifyWorker(id)`
221248

222249
Notifies a worker by id to bypass the job polling interval (see `pollingIntervalSeconds`) for this iteration in the loop.

0 commit comments

Comments
 (0)