@spfn/core 0.3.0-beta.6 → 0.3.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{boss-D16fO2oG.d.ts → boss-BiDCbbAQ.d.ts} +60 -7
- package/dist/job/index.d.ts +48 -3
- package/dist/job/index.js +118 -40
- package/dist/job/index.js.map +1 -1
- package/dist/server/index.d.ts +19 -2
- package/dist/server/index.js +129 -39
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
- package/src/job/README.md +108 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spfn/core",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.8",
|
|
4
4
|
"description": "Full-stack TypeScript backend for Next.js: file-based typed routes, Drizzle entities and repositories, PostgreSQL transactions and a generated end-to-end client, in one fixed vertical slice per feature",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/job/README.md
CHANGED
|
@@ -31,13 +31,15 @@ In a normal SPFN app you do **not** call `initBoss` / `registerJobs` yourself
|
|
|
31
31
|
From `@spfn/core/job`:
|
|
32
32
|
|
|
33
33
|
- Builder: `job(name)` → `JobBuilder`
|
|
34
|
-
- Router: `defineJobRouter(jobs)`, `
|
|
34
|
+
- Router: `defineJobRouter(jobs)`, `mergeJobRouters(base, extra)`, `collectJobs(router, prefix?)`,
|
|
35
|
+
`isJobDef(v)`, `isJobRouter(v)`
|
|
36
|
+
- Queue policy: `resolveQueuePolicy(job)`
|
|
35
37
|
- pg-boss lifecycle: `initBoss(options)`, `getBoss()`, `stopBoss()`, `isBossRunning()`,
|
|
36
38
|
`shouldClearOnStart()`
|
|
37
39
|
- Registration: `registerJobs(router)` — **takes a `JobRouter`, not an array**
|
|
38
|
-
- Types: `JobDef`, `JobRouter`, `JobRouterEntry`, `JobOptions`, `
|
|
39
|
-
`JobHandler`, `CompensateHandler`, `InferJobInput`, `InferJobOutput`,
|
|
40
|
-
`BossConfig` (deprecated alias of `BossOptions`)
|
|
40
|
+
- Types: `JobDef`, `JobRouter`, `JobRouterEntry`, `JobOptions`, `JobQueuePolicy`,
|
|
41
|
+
`JobSendOptions`, `JobHandler`, `CompensateHandler`, `InferJobInput`, `InferJobOutput`,
|
|
42
|
+
`BossOptions`, `BossConfig` (deprecated alias of `BossOptions`)
|
|
41
43
|
|
|
42
44
|
`JobBuilder` methods: `.input(schema)`, `.output(schema)`, `.on(event)`, `.cron(expr)`,
|
|
43
45
|
`.runOnce()`, `.options(opts)`, `.timeout(ms)`, `.compensate(fn)`, `.handler(fn)`.
|
|
@@ -138,6 +140,10 @@ const typedJob = job('typed')
|
|
|
138
140
|
const noInput = job('simple').handler(async () => { await db.cleanup(); });
|
|
139
141
|
```
|
|
140
142
|
|
|
143
|
+
A `Type.Union([...])` input is supported: `send`, `run`, `sendBatch` and the handler are
|
|
144
|
+
typed on the whole union, so each member is accepted and a non-member is rejected — no cast
|
|
145
|
+
and no per-member overload.
|
|
146
|
+
|
|
141
147
|
### Cron — scheduled
|
|
142
148
|
|
|
143
149
|
```typescript
|
|
@@ -167,7 +173,13 @@ const initCache = job('init-cache')
|
|
|
167
173
|
.handler(async () => { await cache.warmup(); });
|
|
168
174
|
```
|
|
169
175
|
|
|
170
|
-
Enqueued with `singletonKey: 'runOnce:<name>'`
|
|
176
|
+
Enqueued with `singletonKey: 'runOnce:<name>'` on an `exclusive` queue, so concurrent
|
|
177
|
+
instances don't double-run it.
|
|
178
|
+
|
|
179
|
+
`exclusive` deduplicates against the `created`, `retry` and `active` states, so a boot while
|
|
180
|
+
the previous run is still queued or running is dropped. Once that run *completes*, the next
|
|
181
|
+
boot enqueues it again — the guarantee is **once per server start**, not once ever. For
|
|
182
|
+
once-ever semantics, keep a marker of your own in the database and return early.
|
|
171
183
|
|
|
172
184
|
### Event-driven — `.on(event)`
|
|
173
185
|
|
|
@@ -204,7 +216,8 @@ job('important-task')
|
|
|
204
216
|
retryDelay: 5000, // ms between retries (default 1000)
|
|
205
217
|
expireInSeconds: 600, // handler timeout (default 300)
|
|
206
218
|
priority: 10, // higher = first (default 0)
|
|
207
|
-
singletonKey: 'unique', // dedupe key
|
|
219
|
+
singletonKey: 'unique', // dedupe key — see Queue policy below
|
|
220
|
+
policy: 'exclusive', // what singletonKey dedupes (default: see below)
|
|
208
221
|
retentionSeconds: 86400,// keep completed jobs (default 604800 = 7d)
|
|
209
222
|
batchSize: 1, // jobs per worker poll(default 1)
|
|
210
223
|
})
|
|
@@ -223,10 +236,69 @@ job('quick').timeout(10000).handler(async () => { /* ... */ }); // expireInSecon
|
|
|
223
236
|
| `retryDelay` | number | 1000 | ms between retries |
|
|
224
237
|
| `expireInSeconds` | number | 300 | handler timeout (seconds) |
|
|
225
238
|
| `priority` | number | 0 | higher = processed first |
|
|
226
|
-
| `singletonKey` | string | — |
|
|
239
|
+
| `singletonKey` | string | — | dedup key; effect depends on [`policy`](#queue-policy-and-deduplication) |
|
|
240
|
+
| `policy` | `JobQueuePolicy` | see below | [what `singletonKey` deduplicates](#queue-policy-and-deduplication) |
|
|
227
241
|
| `retentionSeconds` | number | 604800 | completed-job retention |
|
|
228
242
|
| `batchSize` | number | 1 | jobs fetched per worker poll; `> 1` ⇒ parallel |
|
|
229
243
|
|
|
244
|
+
### Queue policy and deduplication
|
|
245
|
+
|
|
246
|
+
`singletonKey` on its own deduplicates nothing. pg-boss puts a unique index on
|
|
247
|
+
`(queue name, COALESCE(singleton_key, ''))` only for non-`standard` queues; on a `standard`
|
|
248
|
+
queue the insert's `ON CONFLICT DO NOTHING` never conflicts, so the key is stored and
|
|
249
|
+
ignored. The policy is a property of the **queue**, set from the job definition at
|
|
250
|
+
registration.
|
|
251
|
+
|
|
252
|
+
| policy | one job per `(queue, singletonKey)` among states… |
|
|
253
|
+
|---|---|
|
|
254
|
+
| `standard` | none — the key is ignored |
|
|
255
|
+
| `short` | `created` (queued, not yet picked up) |
|
|
256
|
+
| `singleton` | `active` (running) — many may queue, one runs at a time |
|
|
257
|
+
| `stately` | one per state among `created`, `retry`, `active` |
|
|
258
|
+
| `exclusive` | `created` + `retry` + `active` together — a second enqueue while one is pending or running is dropped |
|
|
259
|
+
|
|
260
|
+
**Default rule:** an explicit `.options({ policy })` always wins; otherwise a job that is
|
|
261
|
+
`.runOnce()` or sets `options.singletonKey` gets `exclusive`; everything else stays
|
|
262
|
+
`standard`. Plain jobs must stay `standard` — `exclusive` with no key deduplicates on
|
|
263
|
+
`COALESCE(singleton_key, '')` and would collapse the whole queue to one pending job.
|
|
264
|
+
|
|
265
|
+
**Deduplication is scoped by queue name.** The queue name is part of every index, so two
|
|
266
|
+
different queues never deduplicate against each other even with the same key. Event-driven
|
|
267
|
+
jobs dedupe on their `event:<name>` queue, not on `<job.name>`.
|
|
268
|
+
|
|
269
|
+
**Event-driven jobs share `event:<name>`**, so the policy resolved from one subscriber
|
|
270
|
+
applies to every subscriber of that event, and `emit()` carries no `singletonKey` — a
|
|
271
|
+
non-standard policy there deduplicates on the empty key, i.e. one un-started job per event
|
|
272
|
+
queue. Give `.on(event)` jobs an explicit `policy: 'standard'` unless collapsing pending
|
|
273
|
+
emits is exactly what you want, and don't put `options.singletonKey` on one.
|
|
274
|
+
|
|
275
|
+
**A send-time `singletonKey` needs a policy on the definition.** `.send(input, {
|
|
276
|
+
singletonKey })` cannot change the queue's policy. On a `standard` queue the send is
|
|
277
|
+
accepted and the key silently does nothing; the first such send logs
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
[Job:<name>] singletonKey ignored: queue policy is "standard"; set .options({ policy: 'exclusive' }) (or another non-standard policy) to deduplicate
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
once per job name. Put `policy` on `.options()` to make the key take effect.
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
const reindex = job('reindex')
|
|
287
|
+
.options({ policy: 'exclusive' }) // now send-time keys deduplicate
|
|
288
|
+
.handler(async () => { await search.reindex(); });
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
#### Upgrading
|
|
292
|
+
|
|
293
|
+
Queues created by earlier versions of this package exist as `standard`, and pg-boss
|
|
294
|
+
`create_queue` is `INSERT … ON CONFLICT DO NOTHING` — an existing queue keeps its policy and
|
|
295
|
+
`updateQueue` cannot change one. Registration therefore compares the wanted policy against
|
|
296
|
+
`getQueue()` and, on a mismatch, logs an error naming the queue, both policies and the
|
|
297
|
+
remedy; boot continues with a queue that does not deduplicate. The remedy is to
|
|
298
|
+
`getBoss().deleteQueue('<name>')` in a quiet window — **it drops that queue's job rows** —
|
|
299
|
+
and restart. In development, `clearOnStart` recreates the queue with the new policy
|
|
300
|
+
automatically.
|
|
301
|
+
|
|
230
302
|
---
|
|
231
303
|
|
|
232
304
|
## Sending jobs
|
|
@@ -318,6 +390,10 @@ export const jobRouter = defineJobRouter({
|
|
|
318
390
|
});
|
|
319
391
|
```
|
|
320
392
|
|
|
393
|
+
`mergeJobRouters(base, extra)` combines two routers into a new one (neither input is
|
|
394
|
+
mutated) and throws when a top-level key exists in both. `.jobs()` uses it — you rarely call
|
|
395
|
+
it directly.
|
|
396
|
+
|
|
321
397
|
---
|
|
322
398
|
|
|
323
399
|
## Server wiring
|
|
@@ -325,6 +401,26 @@ export const jobRouter = defineJobRouter({
|
|
|
325
401
|
`defineServerConfig().jobs(router, config?)` at server start: reads `env.DATABASE_URL`, calls
|
|
326
402
|
`initBoss({ connectionString: DATABASE_URL, ...config })`, then `registerJobs(router)`.
|
|
327
403
|
|
|
404
|
+
**`.jobs()` may be called once per domain router.** Repeated calls **merge**, the way
|
|
405
|
+
`.routes()` composes — they do not replace one another:
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
export default defineServerConfig()
|
|
409
|
+
.routes(appRouter)
|
|
410
|
+
.jobs(emailJobRouter, { clearOnStart: isDev }) // pg-boss config on one call only
|
|
411
|
+
.jobs(billingJobRouter)
|
|
412
|
+
.jobs(reportingJobRouter)
|
|
413
|
+
.build();
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Two collisions are possible and both fail loudly:
|
|
417
|
+
|
|
418
|
+
- a **router key** present in two calls throws at config time
|
|
419
|
+
(`jobs(): key "x" is already registered by an earlier .jobs() call`);
|
|
420
|
+
- two jobs sharing a **`job.name`** — the pg-boss queue name — throw in `registerJobs()` at
|
|
421
|
+
boot, before any queue or worker is created;
|
|
422
|
+
- passing the pg-boss `config` on more than one call throws too; there is one boss per server.
|
|
423
|
+
|
|
328
424
|
```typescript
|
|
329
425
|
export default defineServerConfig()
|
|
330
426
|
.routes(appRouter)
|
|
@@ -395,7 +491,10 @@ so self-signed certs work.
|
|
|
395
491
|
`.input()` and expect data.
|
|
396
492
|
- **Job names must be unique across the (flattened) router.** Two jobs sharing a `name` map to
|
|
397
493
|
the same pg-boss queue and collide. Nested router keys are namespaced into the *router* path
|
|
398
|
-
but the pg-boss queue is `job.name` — keep `name` strings unique.
|
|
494
|
+
but the pg-boss queue is `job.name` — keep `name` strings unique. `registerJobs()` throws on
|
|
495
|
+
a duplicate name at boot, quoting both router keys.
|
|
496
|
+
- **`singletonKey` does nothing on a `standard` queue.** Set `policy` on the definition — see
|
|
497
|
+
[Queue policy and deduplication](#queue-policy-and-deduplication).
|
|
399
498
|
- **`clearOnStart: true` deletes pending/scheduled jobs on boot.** Development only — it wipes
|
|
400
499
|
queued work (and the subscribed event queues) every restart.
|
|
401
500
|
- **`expireInSeconds` is a handler timeout, not a delay.** A handler exceeding it is treated as
|
|
@@ -493,7 +592,7 @@ it('processes an order', async () =>
|
|
|
493
592
|
```typescript
|
|
494
593
|
import type {
|
|
495
594
|
JobDef, JobRouter, JobRouterEntry,
|
|
496
|
-
JobOptions, JobSendOptions, JobHandler, CompensateHandler,
|
|
595
|
+
JobOptions, JobQueuePolicy, JobSendOptions, JobHandler, CompensateHandler,
|
|
497
596
|
InferJobInput, InferJobOutput, BossOptions,
|
|
498
597
|
} from '@spfn/core/job';
|
|
499
598
|
|