@softure-ai/jobs 0.0.0-stage → 0.1.0
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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +224 -2
- package/dist/contract.d.ts +48 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -0
- package/dist/handlers.d.ts +60 -0
- package/dist/handlers.d.ts.map +1 -0
- package/dist/handlers.js +29 -0
- package/dist/handlers.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/messages/en.d.ts +12 -0
- package/dist/messages/en.d.ts.map +1 -0
- package/dist/messages/en.js +12 -0
- package/dist/messages/en.js.map +1 -0
- package/dist/messages/index.d.ts +17 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +4 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/options.d.ts +23 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +56 -0
- package/dist/options.js.map +1 -0
- package/dist/schema.d.ts +265 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +22 -0
- package/dist/schema.js.map +1 -0
- package/dist/server/admin.d.ts +15 -0
- package/dist/server/admin.d.ts.map +1 -0
- package/dist/server/admin.js +56 -0
- package/dist/server/admin.js.map +1 -0
- package/dist/server/backoff.d.ts +6 -0
- package/dist/server/backoff.d.ts.map +1 -0
- package/dist/server/backoff.js +16 -0
- package/dist/server/backoff.js.map +1 -0
- package/dist/server/claim.d.ts +42 -0
- package/dist/server/claim.d.ts.map +1 -0
- package/dist/server/claim.js +109 -0
- package/dist/server/claim.js.map +1 -0
- package/dist/server/context.d.ts +5 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +2 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/enqueue.d.ts +38 -0
- package/dist/server/enqueue.d.ts.map +1 -0
- package/dist/server/enqueue.js +86 -0
- package/dist/server/enqueue.js.map +1 -0
- package/dist/server/handler-check.d.ts +4 -0
- package/dist/server/handler-check.d.ts.map +1 -0
- package/dist/server/handler-check.js +23 -0
- package/dist/server/handler-check.js.map +1 -0
- package/dist/server/health.d.ts +3 -0
- package/dist/server/health.d.ts.map +1 -0
- package/dist/server/health.js +11 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/index.d.ts +10 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +11 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/job-row.d.ts +6 -0
- package/dist/server/job-row.d.ts.map +1 -0
- package/dist/server/job-row.js +28 -0
- package/dist/server/job-row.js.map +1 -0
- package/dist/server/logger.d.ts +11 -0
- package/dist/server/logger.d.ts.map +1 -0
- package/dist/server/logger.js +27 -0
- package/dist/server/logger.js.map +1 -0
- package/dist/server/maintenance.d.ts +22 -0
- package/dist/server/maintenance.d.ts.map +1 -0
- package/dist/server/maintenance.js +51 -0
- package/dist/server/maintenance.js.map +1 -0
- package/dist/server/options.d.ts +7 -0
- package/dist/server/options.d.ts.map +1 -0
- package/dist/server/options.js +22 -0
- package/dist/server/options.js.map +1 -0
- package/dist/server/process.d.ts +16 -0
- package/dist/server/process.d.ts.map +1 -0
- package/dist/server/process.js +88 -0
- package/dist/server/process.js.map +1 -0
- package/dist/server/run-due.d.ts +28 -0
- package/dist/server/run-due.d.ts.map +1 -0
- package/dist/server/run-due.js +40 -0
- package/dist/server/run-due.js.map +1 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +10 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/worker/index.d.ts +3 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +5 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/process.d.ts +26 -0
- package/dist/worker/process.d.ts.map +1 -0
- package/dist/worker/process.js +79 -0
- package/dist/worker/process.js.map +1 -0
- package/dist/worker/run-job-worker.d.ts +29 -0
- package/dist/worker/run-job-worker.d.ts.map +1 -0
- package/dist/worker/run-job-worker.js +149 -0
- package/dist/worker/run-job-worker.js.map +1 -0
- package/migrations/0001_create_jobs.sql +39 -0
- package/module.json +12 -0
- package/package.json +60 -4
- package/src/contract.ts +54 -0
- package/src/handlers.ts +81 -0
- package/src/index.ts +45 -0
- package/src/messages/en.ts +11 -0
- package/src/messages/index.ts +6 -0
- package/src/options.ts +64 -0
- package/src/schema.ts +25 -0
- package/src/server/admin.ts +57 -0
- package/src/server/backoff.ts +22 -0
- package/src/server/claim.ts +148 -0
- package/src/server/context.ts +5 -0
- package/src/server/enqueue.ts +117 -0
- package/src/server/handler-check.ts +27 -0
- package/src/server/health.ts +12 -0
- package/src/server/index.ts +19 -0
- package/src/server/job-row.ts +33 -0
- package/src/server/logger.ts +34 -0
- package/src/server/maintenance.ts +67 -0
- package/src/server/options.ts +24 -0
- package/src/server/process.ts +112 -0
- package/src/server/run-due.ts +64 -0
- package/src/testing/index.ts +15 -0
- package/src/worker/index.ts +4 -0
- package/src/worker/process.ts +90 -0
- package/src/worker/run-job-worker.ts +183 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Newest first. Each version lists what changed for an app that uses `@softure-ai/jobs`. When an app has run a version in
|
|
4
|
+
production, the version gets a line `verified in: <app>@<commit>` ([docs/05](../../docs/05-adoption-playbook.md),
|
|
5
|
+
"Definition of done").
|
|
6
|
+
|
|
7
|
+
## 0.1.0
|
|
8
|
+
|
|
9
|
+
- First version (#471): background jobs in Postgres (`jobs.jobs`). `jobs({ queues })` in `softure.config.ts`;
|
|
10
|
+
`enqueueJob` inside the caller's transaction with an optional `dedupKey`; `cancelJob`, `retryJob`, `getJob`,
|
|
11
|
+
`getQueueStats`, `pruneJobs`; handlers with a zod payload schema (`defineJobHandler`, `failJob`, `retryJobLater`);
|
|
12
|
+
`runJobWorker` and `runJobWorkerProcess` (`@softure-ai/jobs/worker`): claim with `FOR UPDATE SKIP LOCKED` under a
|
|
13
|
+
lease, a heartbeat for long jobs, fenced completion, retry with exponential backoff and jitter, a terminal `failed`
|
|
14
|
+
state, pruning, and release of unfinished jobs on SIGTERM; `runDueJobs` (`@softure-ai/jobs/testing`) runs due jobs
|
|
15
|
+
in-process for tests and cron-driven apps.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SOFTURE
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,225 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @softure-ai/jobs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Background jobs in Postgres: an app enqueues work in the same transaction as its own write, and a worker process
|
|
4
|
+
runs it with leases, retries and a terminal failure state. Built for the case an adopting app solved by hand (a job
|
|
5
|
+
table of its own and a `scripts/worker.ts` loop): a slow or unreliable call after a request, such as an AI call per
|
|
6
|
+
received message.
|
|
7
|
+
|
|
8
|
+
## 1. What it provides
|
|
9
|
+
|
|
10
|
+
A job queue in one table (`jobs.jobs`) with enqueue inside the caller's transaction and dedup keys, claims with
|
|
11
|
+
`FOR UPDATE SKIP LOCKED` under a lease, fenced completion, retry with exponential backoff, a worker process with
|
|
12
|
+
graceful shutdown, and an in-process runner for tests and cron-driven apps.
|
|
13
|
+
|
|
14
|
+
## 2. Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @softure-ai/jobs @softure-ai/core @softure-ai/db drizzle-orm zod
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The worker runs as its own process. With `@softure-ai/deploy`, `softure-deploy init` sees `@softure-ai/jobs` in
|
|
21
|
+
`package.json` and adds `scripts/worker.ts`, its bundle in the image and a `worker` service next to the app (deploy
|
|
22
|
+
README, "Job worker").
|
|
23
|
+
|
|
24
|
+
## 3. Configuration
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { defineSoftureConfig } from "@softure-ai/core";
|
|
28
|
+
import { jobs } from "@softure-ai/jobs";
|
|
29
|
+
|
|
30
|
+
export default defineSoftureConfig({
|
|
31
|
+
// database, locale, timezone, appOrigin …
|
|
32
|
+
modules: [
|
|
33
|
+
jobs({
|
|
34
|
+
queues: {
|
|
35
|
+
"ai.reply": { maxAttempts: 5, backoff: { baseSeconds: 10, maxSeconds: 600 }, leaseSeconds: 120, concurrency: 4 },
|
|
36
|
+
"mail.digest": {},
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Option | Default | Meaning |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| `queues.<name>.maxAttempts` | `5` | attempts before a job is `failed`; the first run counts |
|
|
46
|
+
| `queues.<name>.backoff` | `{ baseSeconds: 10, maxSeconds: 3600 }` | delay after attempt n is `min(max, base * 2^(n-1))`, shortened by up to 20 % at random |
|
|
47
|
+
| `queues.<name>.leaseSeconds` | `300` | how long a claimed job is reserved; the worker extends it while the handler runs, so it bounds how long a crashed worker's job waits, not how long a handler may take |
|
|
48
|
+
| `queues.<name>.concurrency` | `1` | jobs of the queue one worker process runs at once |
|
|
49
|
+
| `pollIntervalMs` | `1000` | how often an idle worker looks for due jobs |
|
|
50
|
+
| `shutdownGraceMs` | `20000` | how long a stopping worker waits for running handlers before releasing their jobs |
|
|
51
|
+
| `retention` | `{ succeededDays: 7, failedDays: 30, cancelledDays: 7 }` | how long finished jobs are kept; the worker prunes once an hour |
|
|
52
|
+
|
|
53
|
+
A queue name is lowercase letters, digits, `_`, `.` and `-`. A queue the config does not define is refused by
|
|
54
|
+
`enqueueJob` and by the worker: a typo is a bug, not a new queue.
|
|
55
|
+
|
|
56
|
+
### Handlers
|
|
57
|
+
|
|
58
|
+
One handler per queue, with the zod schema of its payload. The payload read back from the table is parsed with it
|
|
59
|
+
before the handler runs; a payload that does not match fails the job with `jobs.invalid_payload` and is not retried.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// src/jobs/handlers.ts
|
|
63
|
+
import { ok } from "@softure-ai/core";
|
|
64
|
+
import { defineJobHandler, failJob, retryJobLater, type JobHandlers } from "@softure-ai/jobs";
|
|
65
|
+
import { z } from "zod";
|
|
66
|
+
|
|
67
|
+
export const handlers: JobHandlers = {
|
|
68
|
+
"ai.reply": defineJobHandler({
|
|
69
|
+
payload: z.object({ messageId: z.string() }),
|
|
70
|
+
run: async ({ messageId }, { db, signal, job }) => {
|
|
71
|
+
const message = await findMessage(db, messageId);
|
|
72
|
+
if (message === null) return failJob("app.message_gone"); // failed at once, no retry
|
|
73
|
+
const reply = await callModel(message, { signal });
|
|
74
|
+
if (reply.status === 429) return retryJobLater("app.rate_limited", reply.retryAfterSeconds);
|
|
75
|
+
if (!reply.ok) return reply; // an Err: retried with the queue's backoff
|
|
76
|
+
await saveReply(db, messageId, reply.value);
|
|
77
|
+
return ok();
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- `ok()`: done, the job is `succeeded`.
|
|
84
|
+
- An `Err` (any `Result` failure): retried after the backoff delay, until `maxAttempts`; then `failed`.
|
|
85
|
+
- `failJob(code)`: `failed` at once.
|
|
86
|
+
- `retryJobLater(code, seconds)`: retried after `seconds` instead of the backoff (a provider's `Retry-After`); still
|
|
87
|
+
counts as an attempt.
|
|
88
|
+
- A throw: retried like an `Err` with the code `jobs.handler_threw`, and logged with the error.
|
|
89
|
+
|
|
90
|
+
**Handlers run at least once.** A worker can die after the side effect and before recording it, a lease can end, a
|
|
91
|
+
stopping worker releases unfinished jobs: the job then runs again. Make handlers idempotent (check whether the reply
|
|
92
|
+
was already saved, pass an idempotency key to the provider).
|
|
93
|
+
|
|
94
|
+
`context.signal` aborts when the job is taken from the handler: the worker's grace period ended on shutdown, or its
|
|
95
|
+
lease could not be extended. Pass it to `fetch` and other cancellable calls; anything the handler returns afterwards
|
|
96
|
+
is not recorded. `context.db` is the worker's pool, not a transaction; `context.job.attempts` is this attempt's number.
|
|
97
|
+
|
|
98
|
+
### Enqueueing
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { enqueueJob } from "@softure-ai/jobs/server";
|
|
102
|
+
|
|
103
|
+
await db.transaction(async (tx) => {
|
|
104
|
+
const [message] = await tx.insert(messages).values(input).returning();
|
|
105
|
+
await enqueueJob({ db: tx, clock, config }, { queue: "ai.reply", payload: { messageId: message.id }, dedupKey: message.id });
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| Field | Meaning |
|
|
110
|
+
| --- | --- |
|
|
111
|
+
| `queue` | a configured queue |
|
|
112
|
+
| `payload` | JSON for the handler, `{}` when there is none; stored as `jsonb`, so a `Date` comes back as a string. Ids, not personal data |
|
|
113
|
+
| `runAt` / `delaySeconds` | when the job is due; default now; one or the other |
|
|
114
|
+
| `dedupKey` | while a pending or running job of the queue has this key, the call returns that job (`isDuplicate: true`) instead of writing a second one; a finished job frees the key |
|
|
115
|
+
| `maxAttempts` | this job's own limit instead of the queue's |
|
|
116
|
+
|
|
117
|
+
Inside the caller's transaction the job exists exactly when the caller's change commits; a rolled-back transaction
|
|
118
|
+
leaves no job behind.
|
|
119
|
+
|
|
120
|
+
### Running the worker
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// scripts/worker.ts
|
|
124
|
+
import { runJobWorkerProcess } from "@softure-ai/jobs/worker";
|
|
125
|
+
import config from "../softure.config";
|
|
126
|
+
import { handlers } from "../src/jobs/handlers";
|
|
127
|
+
|
|
128
|
+
await runJobWorkerProcess({ config, handlers });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`runJobWorkerProcess` opens its own pool (`database.url`, or the app's `database.handle`) with one connection per job
|
|
132
|
+
that can run at once plus one, runs the worker until SIGTERM or SIGINT, closes the pool and sets `process.exitCode`
|
|
133
|
+
(0 after a clean stop, 1 when it could not start). Pass `logger: createLogger("jobs")` from
|
|
134
|
+
`@softure-ai/observability` to send its lines to OpenTelemetry; by default they go to the console. `queues: [...]`
|
|
135
|
+
runs only some queues (one worker per queue group).
|
|
136
|
+
|
|
137
|
+
The worker claims due jobs of every queue it has a handler for, up to each queue's `concurrency`, oldest due first.
|
|
138
|
+
A claim counts as an attempt and takes a lease, which a heartbeat extends while the handler runs. A worker that dies
|
|
139
|
+
leaves its jobs `running` until their lease ends; the next claim then takes them over (or fails them with
|
|
140
|
+
`jobs.lease_expired` when no attempt is left). Every write after a claim is fenced by a claim token, so the worker
|
|
141
|
+
whose lease was taken over cannot record an outcome over the newer attempt. Several workers can run side by side:
|
|
142
|
+
`SKIP LOCKED` hands each job to one of them.
|
|
143
|
+
|
|
144
|
+
On SIGTERM the worker stops claiming, waits up to `shutdownGraceMs` for running handlers, then aborts the rest and
|
|
145
|
+
releases their jobs (pending again, due now, the attempt given back). Database errors while claiming or recording are
|
|
146
|
+
logged and retried on the next round; a database restart does not end the worker.
|
|
147
|
+
|
|
148
|
+
`runJobWorker({ db, config, handlers, signal })` is the loop without the process part, for an app that runs it inside
|
|
149
|
+
a process of its own.
|
|
150
|
+
|
|
151
|
+
### Without a worker
|
|
152
|
+
|
|
153
|
+
`runDueJobs(ctx, { handlers })` (from `@softure-ai/jobs/server` or `/testing`) runs every due job once, in this process,
|
|
154
|
+
one after another, and returns `{ succeeded, retried, failed, stale }`. A cron can drain a queue with it; call
|
|
155
|
+
`pruneJobs(ctx)` from the same cron.
|
|
156
|
+
|
|
157
|
+
### Steering and overview
|
|
158
|
+
|
|
159
|
+
From `@softure-ai/jobs/server`, each on `ctx = { db, clock, config }`:
|
|
160
|
+
|
|
161
|
+
| Function | Result |
|
|
162
|
+
| --- | --- |
|
|
163
|
+
| `getJob(ctx, id)` | the `Job` or `null` |
|
|
164
|
+
| `cancelJob(ctx, id)` | a pending job becomes `cancelled`; `jobs.not_cancellable` for a running or finished one, `jobs.not_found` |
|
|
165
|
+
| `retryJob(ctx, id)` | a failed or cancelled job runs again from scratch (attempts 0, due now); `jobs.not_retryable`, `jobs.dedup_key_taken` while a live job has its key, `jobs.not_found` |
|
|
166
|
+
| `getQueueStats(ctx)` | per queue: counts per status, the due backlog and the `run_at` of its oldest due job |
|
|
167
|
+
| `pruneJobs(ctx)` | deletes finished jobs past their retention; returns the count |
|
|
168
|
+
|
|
169
|
+
`getQueueStats` suits an app health check of its own, `ops({ checks: { "app.jobs": … } })`, e.g. unavailable when the
|
|
170
|
+
oldest due job of a queue waited more than ten minutes (no worker is running). An ops script (`@softure-ai/ops/scripts`)
|
|
171
|
+
can wrap `retryJob` and `cancelJob` for the operator.
|
|
172
|
+
|
|
173
|
+
## 4. Mounting
|
|
174
|
+
|
|
175
|
+
Nothing to mount: the module has no routes or pages. The worker is a process (§3, "Running the worker").
|
|
176
|
+
|
|
177
|
+
## 5. Migrations and tables
|
|
178
|
+
|
|
179
|
+
Schema `jobs`, one table, `migrations/0001_create_jobs.sql`:
|
|
180
|
+
|
|
181
|
+
`jobs.jobs`: `id` (bigint identity; a string in the API), `queue`, `payload` (jsonb), `status` (`pending`, `running`,
|
|
182
|
+
`succeeded`, `failed`, `cancelled`), `attempts`, `max_attempts`, `run_at`, `locked_until`, `locked_by` (the worker,
|
|
183
|
+
`<hostname>:<pid>`), `claim_token`, `dedup_key`, `last_error` (an error code, never a message), `created_at`,
|
|
184
|
+
`updated_at`, `finished_at`. Checks keep the lease columns set exactly while `running` and `finished_at` set exactly
|
|
185
|
+
for finished jobs; a partial unique index allows one live job per queue and dedup key.
|
|
186
|
+
|
|
187
|
+
The app role needs `SELECT, INSERT, UPDATE, DELETE` on the table, which the `@softure-ai/ops` roles recipe grants on
|
|
188
|
+
every schema the migrator creates.
|
|
189
|
+
|
|
190
|
+
## 6. Environment variables
|
|
191
|
+
|
|
192
|
+
None of its own. The worker reads the app's config, so it gets the app's `DATABASE_URL` and whatever its handlers need.
|
|
193
|
+
|
|
194
|
+
## 7. Switches
|
|
195
|
+
|
|
196
|
+
None.
|
|
197
|
+
|
|
198
|
+
## 8. Appearance
|
|
199
|
+
|
|
200
|
+
No UI.
|
|
201
|
+
|
|
202
|
+
## 9. Copy
|
|
203
|
+
|
|
204
|
+
`errors.*` in `src/messages/en.ts`, one key per error code (`invalid_payload`, `handler_threw`, `lease_expired`,
|
|
205
|
+
`not_found`, `not_cancellable`, `not_retryable`, `dedup_key_taken`), for an admin screen that shows a job's state.
|
|
206
|
+
|
|
207
|
+
## 10. Hooks
|
|
208
|
+
|
|
209
|
+
The handlers (§3) are the app's code. `logger` (`info`, `warn`, `error`; `@softure-ai/observability`'s `Logger` fits)
|
|
210
|
+
receives the worker's lines: started, stopped, a thrown handler with its error, a lost lease, a release, pruning.
|
|
211
|
+
|
|
212
|
+
## 11. GDPR
|
|
213
|
+
|
|
214
|
+
No export or deletion contributor: jobs are transient and pruned (§3, `retention`). Keep personal data out of
|
|
215
|
+
payloads (ids only); then a deleted account leaves nothing personal in `jobs.jobs`. `last_error` holds codes, and the
|
|
216
|
+
logger gets payload paths, never values, for a payload that does not match its schema.
|
|
217
|
+
|
|
218
|
+
## 12. Limitations
|
|
219
|
+
|
|
220
|
+
- Polling, no `LISTEN/NOTIFY`: a job waits up to `pollIntervalMs` when the worker is idle.
|
|
221
|
+
- No cron schedules or recurring jobs: a cron (or a handler that enqueues its next run) does that.
|
|
222
|
+
- No priorities within a queue and no rate limits per queue beyond `concurrency`; split work into queues instead.
|
|
223
|
+
- At-least-once delivery (§3, "Handlers run at least once").
|
|
224
|
+
- PGlite has one connection, so tests run claims one after another; concurrent claiming is Postgres behaviour
|
|
225
|
+
(`SKIP LOCKED`) and is not exercised in unit tests.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Err, ErrorCode } from "@softure-ai/core";
|
|
2
|
+
export type JobsErrorCode = "jobs.invalid_payload" | "jobs.handler_threw" | "jobs.lease_expired" | "jobs.not_found" | "jobs.not_cancellable" | "jobs.not_retryable" | "jobs.dedup_key_taken";
|
|
3
|
+
export declare const JOB_STATUSES: readonly ["pending", "running", "succeeded", "failed", "cancelled"];
|
|
4
|
+
export type JobStatus = (typeof JOB_STATUSES)[number];
|
|
5
|
+
/** One job as the module's functions return it. */
|
|
6
|
+
export interface Job {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly queue: string;
|
|
9
|
+
readonly payload: unknown;
|
|
10
|
+
readonly status: JobStatus;
|
|
11
|
+
/** Claims so far; the running attempt's number while `running`. */
|
|
12
|
+
readonly attempts: number;
|
|
13
|
+
readonly maxAttempts: number;
|
|
14
|
+
/** When a pending job is due. */
|
|
15
|
+
readonly runAt: Date;
|
|
16
|
+
readonly lockedUntil: Date | null;
|
|
17
|
+
readonly lockedBy: string | null;
|
|
18
|
+
readonly dedupKey: string | null;
|
|
19
|
+
/** The error code of the last failed attempt. */
|
|
20
|
+
readonly lastError: string | null;
|
|
21
|
+
readonly createdAt: Date;
|
|
22
|
+
readonly updatedAt: Date;
|
|
23
|
+
readonly finishedAt: Date | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A handler's failure. Retried with the queue's backoff unless `retry` is false or the attempts are used up;
|
|
27
|
+
* `retryAfterSeconds` replaces the backoff delay for this attempt (a provider's `Retry-After`).
|
|
28
|
+
*/
|
|
29
|
+
export type JobFailure = Err<ErrorCode> & {
|
|
30
|
+
readonly retry?: boolean;
|
|
31
|
+
readonly retryAfterSeconds?: number;
|
|
32
|
+
};
|
|
33
|
+
/** What happened to a job after one attempt. */
|
|
34
|
+
export type JobOutcome = {
|
|
35
|
+
readonly kind: "succeeded";
|
|
36
|
+
} | {
|
|
37
|
+
readonly kind: "retried";
|
|
38
|
+
readonly error: string;
|
|
39
|
+
readonly runAt: Date;
|
|
40
|
+
} | {
|
|
41
|
+
readonly kind: "failed";
|
|
42
|
+
readonly error: string;
|
|
43
|
+
}
|
|
44
|
+
/** The job was taken over (lease ended) or released before the attempt finished; nothing was recorded. */
|
|
45
|
+
| {
|
|
46
|
+
readonly kind: "stale";
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAEvD,MAAM,MAAM,aAAa,GACrB,sBAAsB,GACtB,oBAAoB,GACpB,oBAAoB,GACpB,gBAAgB,GAChB,sBAAsB,GACtB,oBAAoB,GACpB,sBAAsB,CAAC;AAE3B,eAAO,MAAM,YAAY,qEAAsE,CAAC;AAEhG,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtD,mDAAmD;AACnD,MAAM,WAAW,GAAG;IAClB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,iCAAiC;IACjC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,iDAAiD;IACjD,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;CAClC;AAED;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,GAAG,CAAC,SAAS,CAAC,GAAG;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC,CAAC;AAEF,gDAAgD;AAChD,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAA;CAAE,GAC1E;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AACrD,0GAA0G;GACxG;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC"}
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAaA,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,CAAU,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Clock, ErrorCode, Ok, SoftureConfig } from "@softure-ai/core";
|
|
2
|
+
import type { Queryable } from "@softure-ai/db";
|
|
3
|
+
import type { z } from "zod";
|
|
4
|
+
import type { Job, JobFailure } from "./contract.js";
|
|
5
|
+
/** What a handler receives besides its payload. */
|
|
6
|
+
export interface JobHandlerContext {
|
|
7
|
+
/** The worker's database: not a transaction; open one with `db.transaction` when the work needs it. */
|
|
8
|
+
readonly db: Queryable;
|
|
9
|
+
readonly clock: Clock;
|
|
10
|
+
readonly config: SoftureConfig;
|
|
11
|
+
/** The job as claimed: `attempts` is this attempt's number, starting at 1. */
|
|
12
|
+
readonly job: Job;
|
|
13
|
+
/**
|
|
14
|
+
* Aborted when the job is taken away from this handler: the worker stops and its grace period ended, or the lease
|
|
15
|
+
* could not be extended. Pass it to `fetch` and other cancellable calls; whatever the handler returns afterwards is
|
|
16
|
+
* not recorded.
|
|
17
|
+
*/
|
|
18
|
+
readonly signal: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* What a handler returns: `ok()` when the work is done; an `Err` to try again with the queue's backoff (until the
|
|
22
|
+
* attempts are used up); `failJob(code)` to fail for good; `retryJobLater(code, seconds)` to try again after a delay
|
|
23
|
+
* the handler chooses. A throw is retried like an `Err` with the code `jobs.handler_threw` and logged.
|
|
24
|
+
*/
|
|
25
|
+
export type JobRunResult = Ok<unknown> | JobFailure;
|
|
26
|
+
export interface JobHandler<TPayload = unknown> {
|
|
27
|
+
/** The schema of the payload; a payload that does not match fails the job with `jobs.invalid_payload`, untried. */
|
|
28
|
+
readonly payload: z.ZodType<TPayload>;
|
|
29
|
+
readonly run: (payload: TPayload, context: JobHandlerContext) => Promise<JobRunResult>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A handler of any payload type, as a handler map holds it. `run` takes `never` so every `JobHandler<T>` fits; the
|
|
33
|
+
* runner passes it the payload its own schema parsed.
|
|
34
|
+
*/
|
|
35
|
+
export interface AnyJobHandler {
|
|
36
|
+
readonly payload: z.ZodType;
|
|
37
|
+
readonly run: (payload: never, context: JobHandlerContext) => Promise<JobRunResult>;
|
|
38
|
+
}
|
|
39
|
+
/** Handlers by queue name. Every queue a worker runs needs one; a handler for an unknown queue is refused. */
|
|
40
|
+
export type JobHandlers = Readonly<Record<string, AnyJobHandler>>;
|
|
41
|
+
/**
|
|
42
|
+
* Defines a handler with its payload type inferred from the schema:
|
|
43
|
+
*
|
|
44
|
+
* export const replyHandler = defineJobHandler({
|
|
45
|
+
* payload: z.object({ messageId: z.string() }),
|
|
46
|
+
* run: async ({ messageId }, { db }) => { ...; return ok(); },
|
|
47
|
+
* });
|
|
48
|
+
*/
|
|
49
|
+
export declare function defineJobHandler<TSchema extends z.ZodType>(handler: {
|
|
50
|
+
readonly payload: TSchema;
|
|
51
|
+
readonly run: (payload: z.output<TSchema>, context: JobHandlerContext) => Promise<JobRunResult>;
|
|
52
|
+
}): JobHandler<z.output<TSchema>>;
|
|
53
|
+
/** A failure that is not retried: the job is `failed` at once with this code. */
|
|
54
|
+
export declare function failJob<E extends ErrorCode>(error: E): JobFailure;
|
|
55
|
+
/**
|
|
56
|
+
* A failure retried after `seconds` instead of the queue's backoff (a provider's `Retry-After`). Still counts as an
|
|
57
|
+
* attempt: after the last one the job is `failed`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function retryJobLater<E extends ErrorCode>(error: E, seconds: number): JobFailure;
|
|
60
|
+
//# sourceMappingURL=handlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../src/handlers.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAErD,mDAAmD;AACnD,MAAM,WAAW,iBAAiB;IAChC,uGAAuG;IACvG,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,8EAA8E;IAC9E,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAClB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,UAAU,CAAC;AAEpD,MAAM,WAAW,UAAU,CAAC,QAAQ,GAAG,OAAO;IAC5C,mHAAmH;IACnH,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACtC,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;CACxF;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;CACrF;AAED,8GAA8G;AAC9G,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;AAElE;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE;IACnE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;CACjG,GAAG,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAKhC;AAED,iFAAiF;AACjF,wBAAgB,OAAO,CAAC,CAAC,SAAS,SAAS,EAAE,KAAK,EAAE,CAAC,GAAG,UAAU,CAEjE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,CAKxF"}
|
package/dist/handlers.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines a handler with its payload type inferred from the schema:
|
|
3
|
+
*
|
|
4
|
+
* export const replyHandler = defineJobHandler({
|
|
5
|
+
* payload: z.object({ messageId: z.string() }),
|
|
6
|
+
* run: async ({ messageId }, { db }) => { ...; return ok(); },
|
|
7
|
+
* });
|
|
8
|
+
*/
|
|
9
|
+
export function defineJobHandler(handler) {
|
|
10
|
+
if (typeof handler.run !== "function") {
|
|
11
|
+
throw new Error("defineJobHandler: run must be a function");
|
|
12
|
+
}
|
|
13
|
+
return handler;
|
|
14
|
+
}
|
|
15
|
+
/** A failure that is not retried: the job is `failed` at once with this code. */
|
|
16
|
+
export function failJob(error) {
|
|
17
|
+
return { ok: false, error, retry: false };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A failure retried after `seconds` instead of the queue's backoff (a provider's `Retry-After`). Still counts as an
|
|
21
|
+
* attempt: after the last one the job is `failed`.
|
|
22
|
+
*/
|
|
23
|
+
export function retryJobLater(error, seconds) {
|
|
24
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
25
|
+
throw new RangeError(`retryJobLater: seconds must be a finite number >= 0, got ${String(seconds)}`);
|
|
26
|
+
}
|
|
27
|
+
return { ok: false, error, retryAfterSeconds: seconds };
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=handlers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handlers.js","sourceRoot":"","sources":["../src/handlers.ts"],"names":[],"mappings":"AAgDA;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAA4B,OAG3D;IACC,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,UAAU,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,OAAwC,CAAC;AAClD,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,OAAO,CAAsB,KAAQ;IACnD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC5C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAsB,KAAQ,EAAE,OAAe;IAC1E,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,UAAU,CAAC,4DAA4D,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACtG,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC;AAC1D,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare const MODULE_ID = "jobs";
|
|
2
|
+
/**
|
|
3
|
+
* Enables background jobs in `softure.config.ts`: `jobs({ queues: { "ai.reply": { maxAttempts: 5 } } })`. Jobs run
|
|
4
|
+
* in a worker process (`runJobWorkerProcess` from `@softure-ai/jobs/worker`).
|
|
5
|
+
*/
|
|
6
|
+
export declare const jobs: import("@softure-ai/core").ModuleFactory<{}, {
|
|
7
|
+
errors: {
|
|
8
|
+
invalid_payload: string;
|
|
9
|
+
handler_threw: string;
|
|
10
|
+
lease_expired: string;
|
|
11
|
+
not_found: string;
|
|
12
|
+
not_cancellable: string;
|
|
13
|
+
not_retryable: string;
|
|
14
|
+
dedup_key_taken: string;
|
|
15
|
+
};
|
|
16
|
+
}, import("zod").ZodObject<{
|
|
17
|
+
queues: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodObject<{
|
|
18
|
+
maxAttempts: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
19
|
+
backoff: import("zod").ZodDefault<import("zod").ZodObject<{
|
|
20
|
+
baseSeconds: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
21
|
+
maxSeconds: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
22
|
+
}, import("zod/v4/core").$strict>>;
|
|
23
|
+
leaseSeconds: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
24
|
+
concurrency: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
25
|
+
}, import("zod/v4/core").$strict>>;
|
|
26
|
+
pollIntervalMs: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
27
|
+
shutdownGraceMs: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
28
|
+
retention: import("zod").ZodDefault<import("zod").ZodObject<{
|
|
29
|
+
succeededDays: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
30
|
+
failedDays: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
31
|
+
cancelledDays: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
32
|
+
}, import("zod/v4/core").$strict>>;
|
|
33
|
+
}, import("zod/v4/core").$strict>>;
|
|
34
|
+
export { JOB_STATUSES, type Job, type JobFailure, type JobOutcome, type JobsErrorCode, type JobStatus, } from "./contract.js";
|
|
35
|
+
export { jobsMessages, type JobsMessages } from "./messages/index.js";
|
|
36
|
+
export { jobsOptionsSchema, type JobQueueOptions, type JobsOptions, type JobsOptionsInput } from "./options.js";
|
|
37
|
+
export { defineJobHandler, failJob, retryJobLater, type JobHandler, type JobHandlerContext, type JobHandlers } from "./handlers.js";
|
|
38
|
+
export { jobs as jobsTable } from "./schema.js";
|
|
39
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,SAAS,SAAS,CAAC;AAEhC;;;GAGG;AACH,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAiBf,CAAC;AAEH,OAAO,EACL,YAAY,EACZ,KAAK,GAAG,EACR,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,SAAS,GACf,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChH,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AACpI,OAAO,EAAE,IAAI,IAAI,SAAS,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Public API of @softure-ai/jobs: the module factory for softure.config.ts, the handler helpers and the types.
|
|
2
|
+
// Database work is in `@softure-ai/jobs/server`, the worker in `@softure-ai/jobs/worker`, the in-process runner for
|
|
3
|
+
// tests in `@softure-ai/jobs/testing`.
|
|
4
|
+
import { defineModule, resolveMigrationsDir } from "@softure-ai/core";
|
|
5
|
+
import { jobsMessages } from "./messages/index.js";
|
|
6
|
+
import { jobsOptionsSchema } from "./options.js";
|
|
7
|
+
import { checkJobsTable } from "./server/health.js";
|
|
8
|
+
export const MODULE_ID = "jobs";
|
|
9
|
+
/**
|
|
10
|
+
* Enables background jobs in `softure.config.ts`: `jobs({ queues: { "ai.reply": { maxAttempts: 5 } } })`. Jobs run
|
|
11
|
+
* in a worker process (`runJobWorkerProcess` from `@softure-ai/jobs/worker`).
|
|
12
|
+
*/
|
|
13
|
+
export const jobs = defineModule({
|
|
14
|
+
manifest: {
|
|
15
|
+
id: MODULE_ID,
|
|
16
|
+
version: "0.1.0",
|
|
17
|
+
dependsOn: {},
|
|
18
|
+
dbSchema: "jobs",
|
|
19
|
+
tables: ["jobs"],
|
|
20
|
+
env: [],
|
|
21
|
+
switches: [],
|
|
22
|
+
routes: {},
|
|
23
|
+
mount: [],
|
|
24
|
+
privacy: { exports: false, deletes: false },
|
|
25
|
+
},
|
|
26
|
+
messages: jobsMessages,
|
|
27
|
+
options: jobsOptionsSchema,
|
|
28
|
+
migrations: { dir: resolveMigrationsDir(import.meta.url, "../migrations/") },
|
|
29
|
+
health: checkJobsTable,
|
|
30
|
+
});
|
|
31
|
+
export { JOB_STATUSES, } from "./contract.js";
|
|
32
|
+
export { jobsMessages } from "./messages/index.js";
|
|
33
|
+
export { jobsOptionsSchema } from "./options.js";
|
|
34
|
+
export { defineJobHandler, failJob, retryJobLater } from "./handlers.js";
|
|
35
|
+
export { jobs as jobsTable } from "./schema.js";
|
|
36
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+GAA+G;AAC/G,oHAAoH;AACpH,uCAAuC;AACvC,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC;AAEhC;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,YAAY,CAAC;IAC/B,QAAQ,EAAE;QACR,EAAE,EAAE,SAAS;QACb,OAAO,EAAE,OAAO;QAChB,SAAS,EAAE,EAAE;QACb,QAAQ,EAAE,MAAM;QAChB,MAAM,EAAE,CAAC,MAAM,CAAC;QAChB,GAAG,EAAE,EAAE;QACP,QAAQ,EAAE,EAAE;QACZ,MAAM,EAAE,EAAE;QACV,KAAK,EAAE,EAAE;QACT,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE;KAC5C;IACD,QAAQ,EAAE,YAAY;IACtB,OAAO,EAAE,iBAAiB;IAC1B,UAAU,EAAE,EAAE,GAAG,EAAE,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,EAAE;IAC5E,MAAM,EAAE,cAAc;CACvB,CAAC,CAAC;AAEH,OAAO,EACL,YAAY,GAMb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAqB,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAiE,MAAM,cAAc,CAAC;AAChH,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,aAAa,EAA6D,MAAM,eAAe,CAAC;AACpI,OAAO,EAAE,IAAI,IAAI,SAAS,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const en: {
|
|
2
|
+
errors: {
|
|
3
|
+
invalid_payload: string;
|
|
4
|
+
handler_threw: string;
|
|
5
|
+
lease_expired: string;
|
|
6
|
+
not_found: string;
|
|
7
|
+
not_cancellable: string;
|
|
8
|
+
not_retryable: string;
|
|
9
|
+
dedup_key_taken: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=en.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/messages/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE;;;;;;;;;;CAUd,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const en = {
|
|
2
|
+
errors: {
|
|
3
|
+
invalid_payload: "The job's data is not valid.",
|
|
4
|
+
handler_threw: "The job failed unexpectedly. It will be tried again.",
|
|
5
|
+
lease_expired: "The job did not finish in time.",
|
|
6
|
+
not_found: "The job does not exist.",
|
|
7
|
+
not_cancellable: "Only a job that has not started can be cancelled.",
|
|
8
|
+
not_retryable: "Only a failed or cancelled job can be run again.",
|
|
9
|
+
dedup_key_taken: "Another job for the same work is already waiting or running.",
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=en.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"en.js","sourceRoot":"","sources":["../../src/messages/en.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,EAAE,GAAG;IAChB,MAAM,EAAE;QACN,eAAe,EAAE,8BAA8B;QAC/C,aAAa,EAAE,sDAAsD;QACrE,aAAa,EAAE,iCAAiC;QAChD,SAAS,EAAE,yBAAyB;QACpC,eAAe,EAAE,mDAAmD;QACpE,aAAa,EAAE,kDAAkD;QACjE,eAAe,EAAE,8DAA8D;KAChF;CACF,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { en } from "./en.js";
|
|
2
|
+
/** The built-in English dictionary; an app passes partial overrides or its own translation per locale. */
|
|
3
|
+
export declare const jobsMessages: {
|
|
4
|
+
en: {
|
|
5
|
+
errors: {
|
|
6
|
+
invalid_payload: string;
|
|
7
|
+
handler_threw: string;
|
|
8
|
+
lease_expired: string;
|
|
9
|
+
not_found: string;
|
|
10
|
+
not_cancellable: string;
|
|
11
|
+
not_retryable: string;
|
|
12
|
+
dedup_key_taken: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export type JobsMessages = typeof en;
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/messages/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,0GAA0G;AAC1G,eAAO,MAAM,YAAY;;;;;;;;;;;;CAAS,CAAC;AAEnC,MAAM,MAAM,YAAY,GAAG,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/messages/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,0GAA0G;AAC1G,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,EAAE,EAAE,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const jobsOptionsSchema: z.ZodObject<{
|
|
3
|
+
queues: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
4
|
+
maxAttempts: z.ZodDefault<z.ZodNumber>;
|
|
5
|
+
backoff: z.ZodDefault<z.ZodObject<{
|
|
6
|
+
baseSeconds: z.ZodDefault<z.ZodNumber>;
|
|
7
|
+
maxSeconds: z.ZodDefault<z.ZodNumber>;
|
|
8
|
+
}, z.core.$strict>>;
|
|
9
|
+
leaseSeconds: z.ZodDefault<z.ZodNumber>;
|
|
10
|
+
concurrency: z.ZodDefault<z.ZodNumber>;
|
|
11
|
+
}, z.core.$strict>>;
|
|
12
|
+
pollIntervalMs: z.ZodDefault<z.ZodNumber>;
|
|
13
|
+
shutdownGraceMs: z.ZodDefault<z.ZodNumber>;
|
|
14
|
+
retention: z.ZodDefault<z.ZodObject<{
|
|
15
|
+
succeededDays: z.ZodDefault<z.ZodNumber>;
|
|
16
|
+
failedDays: z.ZodDefault<z.ZodNumber>;
|
|
17
|
+
cancelledDays: z.ZodDefault<z.ZodNumber>;
|
|
18
|
+
}, z.core.$strict>>;
|
|
19
|
+
}, z.core.$strict>;
|
|
20
|
+
export type JobsOptionsInput = z.input<typeof jobsOptionsSchema>;
|
|
21
|
+
export type JobsOptions = z.output<typeof jobsOptionsSchema>;
|
|
22
|
+
export type JobQueueOptions = JobsOptions["queues"][string];
|
|
23
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAyCxB,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;kBAiB5B,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AACjE,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC7D,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC"}
|