@softure-ai/jobs 0.0.0-stage → 0.1.1

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.
Files changed (137) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +289 -2
  4. package/dist/contract.d.ts +48 -0
  5. package/dist/contract.d.ts.map +1 -0
  6. package/dist/contract.js +2 -0
  7. package/dist/contract.js.map +1 -0
  8. package/dist/handlers.d.ts +60 -0
  9. package/dist/handlers.d.ts.map +1 -0
  10. package/dist/handlers.js +29 -0
  11. package/dist/handlers.js.map +1 -0
  12. package/dist/index.d.ts +87 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +36 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/messages/en.d.ts +12 -0
  17. package/dist/messages/en.d.ts.map +1 -0
  18. package/dist/messages/en.js +12 -0
  19. package/dist/messages/en.js.map +1 -0
  20. package/dist/messages/index.d.ts +17 -0
  21. package/dist/messages/index.d.ts.map +1 -0
  22. package/dist/messages/index.js +4 -0
  23. package/dist/messages/index.js.map +1 -0
  24. package/dist/options.d.ts +76 -0
  25. package/dist/options.d.ts.map +1 -0
  26. package/dist/options.js +86 -0
  27. package/dist/options.js.map +1 -0
  28. package/dist/schema.d.ts +357 -0
  29. package/dist/schema.d.ts.map +1 -0
  30. package/dist/schema.js +29 -0
  31. package/dist/schema.js.map +1 -0
  32. package/dist/server/admin.d.ts +15 -0
  33. package/dist/server/admin.d.ts.map +1 -0
  34. package/dist/server/admin.js +56 -0
  35. package/dist/server/admin.js.map +1 -0
  36. package/dist/server/backoff.d.ts +6 -0
  37. package/dist/server/backoff.d.ts.map +1 -0
  38. package/dist/server/backoff.js +16 -0
  39. package/dist/server/backoff.js.map +1 -0
  40. package/dist/server/claim.d.ts +45 -0
  41. package/dist/server/claim.d.ts.map +1 -0
  42. package/dist/server/claim.js +119 -0
  43. package/dist/server/claim.js.map +1 -0
  44. package/dist/server/context.d.ts +5 -0
  45. package/dist/server/context.d.ts.map +1 -0
  46. package/dist/server/context.js +2 -0
  47. package/dist/server/context.js.map +1 -0
  48. package/dist/server/enqueue.d.ts +38 -0
  49. package/dist/server/enqueue.d.ts.map +1 -0
  50. package/dist/server/enqueue.js +86 -0
  51. package/dist/server/enqueue.js.map +1 -0
  52. package/dist/server/handler-check.d.ts +4 -0
  53. package/dist/server/handler-check.d.ts.map +1 -0
  54. package/dist/server/handler-check.js +23 -0
  55. package/dist/server/handler-check.js.map +1 -0
  56. package/dist/server/health.d.ts +3 -0
  57. package/dist/server/health.d.ts.map +1 -0
  58. package/dist/server/health.js +11 -0
  59. package/dist/server/health.js.map +1 -0
  60. package/dist/server/index.d.ts +11 -0
  61. package/dist/server/index.d.ts.map +1 -0
  62. package/dist/server/index.js +12 -0
  63. package/dist/server/index.js.map +1 -0
  64. package/dist/server/job-row.d.ts +6 -0
  65. package/dist/server/job-row.d.ts.map +1 -0
  66. package/dist/server/job-row.js +28 -0
  67. package/dist/server/job-row.js.map +1 -0
  68. package/dist/server/logger.d.ts +11 -0
  69. package/dist/server/logger.d.ts.map +1 -0
  70. package/dist/server/logger.js +27 -0
  71. package/dist/server/logger.js.map +1 -0
  72. package/dist/server/maintenance.d.ts +25 -0
  73. package/dist/server/maintenance.d.ts.map +1 -0
  74. package/dist/server/maintenance.js +53 -0
  75. package/dist/server/maintenance.js.map +1 -0
  76. package/dist/server/options.d.ts +7 -0
  77. package/dist/server/options.d.ts.map +1 -0
  78. package/dist/server/options.js +22 -0
  79. package/dist/server/options.js.map +1 -0
  80. package/dist/server/process.d.ts +16 -0
  81. package/dist/server/process.d.ts.map +1 -0
  82. package/dist/server/process.js +88 -0
  83. package/dist/server/process.js.map +1 -0
  84. package/dist/server/run-due.d.ts +28 -0
  85. package/dist/server/run-due.d.ts.map +1 -0
  86. package/dist/server/run-due.js +42 -0
  87. package/dist/server/run-due.js.map +1 -0
  88. package/dist/server/schedules.d.ts +45 -0
  89. package/dist/server/schedules.d.ts.map +1 -0
  90. package/dist/server/schedules.js +97 -0
  91. package/dist/server/schedules.js.map +1 -0
  92. package/dist/testing/index.d.ts +6 -0
  93. package/dist/testing/index.d.ts.map +1 -0
  94. package/dist/testing/index.js +10 -0
  95. package/dist/testing/index.js.map +1 -0
  96. package/dist/worker/index.d.ts +3 -0
  97. package/dist/worker/index.d.ts.map +1 -0
  98. package/dist/worker/index.js +5 -0
  99. package/dist/worker/index.js.map +1 -0
  100. package/dist/worker/process.d.ts +26 -0
  101. package/dist/worker/process.d.ts.map +1 -0
  102. package/dist/worker/process.js +79 -0
  103. package/dist/worker/process.js.map +1 -0
  104. package/dist/worker/run-job-worker.d.ts +29 -0
  105. package/dist/worker/run-job-worker.d.ts.map +1 -0
  106. package/dist/worker/run-job-worker.js +167 -0
  107. package/dist/worker/run-job-worker.js.map +1 -0
  108. package/migrations/0001_create_jobs.sql +39 -0
  109. package/migrations/0002_create_schedules.sql +12 -0
  110. package/module.json +12 -0
  111. package/package.json +60 -4
  112. package/src/contract.ts +54 -0
  113. package/src/handlers.ts +81 -0
  114. package/src/index.ts +52 -0
  115. package/src/messages/en.ts +11 -0
  116. package/src/messages/index.ts +6 -0
  117. package/src/options.ts +100 -0
  118. package/src/schema.ts +33 -0
  119. package/src/server/admin.ts +57 -0
  120. package/src/server/backoff.ts +22 -0
  121. package/src/server/claim.ts +156 -0
  122. package/src/server/context.ts +5 -0
  123. package/src/server/enqueue.ts +117 -0
  124. package/src/server/handler-check.ts +27 -0
  125. package/src/server/health.ts +12 -0
  126. package/src/server/index.ts +26 -0
  127. package/src/server/job-row.ts +33 -0
  128. package/src/server/logger.ts +34 -0
  129. package/src/server/maintenance.ts +71 -0
  130. package/src/server/options.ts +24 -0
  131. package/src/server/process.ts +112 -0
  132. package/src/server/run-due.ts +66 -0
  133. package/src/server/schedules.ts +121 -0
  134. package/src/testing/index.ts +15 -0
  135. package/src/worker/index.ts +4 -0
  136. package/src/worker/process.ts +90 -0
  137. package/src/worker/run-job-worker.ts +198 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
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.1
8
+
9
+ - Recurring schedules (#491): `jobs({ schedules: { "<queue>": { every: "1h", payload } } })`. Workers enqueue each
10
+ period's run once, however many run (a conditional upsert of the new table `jobs.schedules`, migration
11
+ `0002_create_schedules.sql`, in the same transaction as the job); missed periods collapse into one run; while a run
12
+ is still live the next period joins it. A schedule's queue takes the default queue settings unless `queues` names
13
+ it, and `queues` may be left out when schedules define them. `runDueJobs` enqueues due runs first;
14
+ `enqueueDueSchedules` serves apps that drain from a cron. `getQueueStats` reports each queue's `schedule`
15
+ (`everySeconds`, `lastEnqueuedAt`, `lastSucceededAt`, `isLate`). auth, mcp-access, waitlist, security and analytics
16
+ ship their prune handlers and schedules in their `./jobs` entries.
17
+
18
+ ## 0.1.0
19
+
20
+ - First version (#471): background jobs in Postgres (`jobs.jobs`). `jobs({ queues })` in `softure.config.ts`;
21
+ `enqueueJob` inside the caller's transaction with an optional `dedupKey`; `cancelJob`, `retryJob`, `getJob`,
22
+ `getQueueStats`, `pruneJobs`; handlers with a zod payload schema (`defineJobHandler`, `failJob`, `retryJobLater`);
23
+ `runJobWorker` and `runJobWorkerProcess` (`@softure-ai/jobs/worker`): claim with `FOR UPDATE SKIP LOCKED` under a
24
+ lease, a heartbeat for long jobs, fenced completion, retry with exponential backoff and jitter, a terminal `failed`
25
+ state, pruning, and release of unfinished jobs on SIGTERM; `runDueJobs` (`@softure-ai/jobs/testing`) runs due jobs
26
+ in-process for tests and cron-driven apps.
27
+
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,290 @@
1
- # Temporary Holding Version
1
+ # @softure-ai/jobs
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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, recurring schedules that run once per period however many workers run, and an in-process runner
13
+ for tests and cron-driven apps.
14
+
15
+ ## 2. Installation
16
+
17
+ ```bash
18
+ npm install @softure-ai/jobs @softure-ai/core @softure-ai/db drizzle-orm zod
19
+ ```
20
+
21
+ The worker runs as its own process. With `@softure-ai/deploy`, `softure-deploy init` sees `@softure-ai/jobs` in
22
+ `package.json` and adds `scripts/worker.ts`, its bundle in the image and a `worker` service next to the app (deploy
23
+ README, "Job worker").
24
+
25
+ ## 3. Configuration
26
+
27
+ ```ts
28
+ import { defineSoftureConfig } from "@softure-ai/core";
29
+ import { jobs } from "@softure-ai/jobs";
30
+
31
+ export default defineSoftureConfig({
32
+ // database, locale, timezone, appOrigin …
33
+ modules: [
34
+ jobs({
35
+ queues: {
36
+ "ai.reply": { maxAttempts: 5, backoff: { baseSeconds: 10, maxSeconds: 600 }, leaseSeconds: 120, concurrency: 4 },
37
+ "mail.digest": {},
38
+ },
39
+ }),
40
+ ],
41
+ });
42
+ ```
43
+
44
+ | Option | Default | Meaning |
45
+ | --- | --- | --- |
46
+ | `queues.<name>.maxAttempts` | `5` | attempts before a job is `failed`; the first run counts |
47
+ | `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 |
48
+ | `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 |
49
+ | `queues.<name>.concurrency` | `1` | jobs of the queue one worker process runs at once |
50
+ | `schedules.<queue>.every` | (required) | how often the schedule runs: `<n>m`, `<n>h` or `<n>d`, from `1m` to `7d` (§3, "Schedules") |
51
+ | `schedules.<queue>.payload` | `{}` | the payload of every scheduled run |
52
+ | `pollIntervalMs` | `1000` | how often an idle worker looks for due jobs |
53
+ | `shutdownGraceMs` | `20000` | how long a stopping worker waits for running handlers before releasing their jobs |
54
+ | `retention` | `{ succeededDays: 7, failedDays: 30, cancelledDays: 7 }` | how long finished jobs are kept; the worker prunes once an hour |
55
+
56
+ A queue name is lowercase letters, digits, `_`, `.` and `-`. A queue the config does not define is refused by
57
+ `enqueueJob` and by the worker: a typo is a bug, not a new queue. A schedule defines its queue too, with the default
58
+ settings unless `queues` names it; the config needs at least one queue or schedule.
59
+
60
+ ### Handlers
61
+
62
+ One handler per queue, with the zod schema of its payload. The payload read back from the table is parsed with it
63
+ before the handler runs; a payload that does not match fails the job with `jobs.invalid_payload` and is not retried.
64
+
65
+ ```ts
66
+ // src/jobs/handlers.ts
67
+ import { ok } from "@softure-ai/core";
68
+ import { defineJobHandler, failJob, retryJobLater, type JobHandlers } from "@softure-ai/jobs";
69
+ import { z } from "zod";
70
+
71
+ export const handlers: JobHandlers = {
72
+ "ai.reply": defineJobHandler({
73
+ payload: z.object({ messageId: z.string() }),
74
+ run: async ({ messageId }, { db, signal, job }) => {
75
+ const message = await findMessage(db, messageId);
76
+ if (message === null) return failJob("app.message_gone"); // failed at once, no retry
77
+ const reply = await callModel(message, { signal });
78
+ if (reply.status === 429) return retryJobLater("app.rate_limited", reply.retryAfterSeconds);
79
+ if (!reply.ok) return reply; // an Err: retried with the queue's backoff
80
+ await saveReply(db, messageId, reply.value);
81
+ return ok();
82
+ },
83
+ }),
84
+ };
85
+ ```
86
+
87
+ - `ok()`: done, the job is `succeeded`.
88
+ - An `Err` (any `Result` failure): retried after the backoff delay, until `maxAttempts`; then `failed`.
89
+ - `failJob(code)`: `failed` at once.
90
+ - `retryJobLater(code, seconds)`: retried after `seconds` instead of the backoff (a provider's `Retry-After`); still
91
+ counts as an attempt.
92
+ - A throw: retried like an `Err` with the code `jobs.handler_threw`, and logged with the error.
93
+
94
+ **Handlers run at least once.** A worker can die after the side effect and before recording it, a lease can end, a
95
+ stopping worker releases unfinished jobs: the job then runs again. Make handlers idempotent (check whether the reply
96
+ was already saved, pass an idempotency key to the provider).
97
+
98
+ `context.signal` aborts when the job is taken from the handler: the worker's grace period ended on shutdown, or its
99
+ lease could not be extended. Pass it to `fetch` and other cancellable calls; anything the handler returns afterwards
100
+ is not recorded. `context.db` is the worker's pool, not a transaction; `context.job.attempts` is this attempt's number.
101
+
102
+ ### Enqueueing
103
+
104
+ ```ts
105
+ import { enqueueJob } from "@softure-ai/jobs/server";
106
+
107
+ await db.transaction(async (tx) => {
108
+ const [message] = await tx.insert(messages).values(input).returning();
109
+ await enqueueJob({ db: tx, clock, config }, { queue: "ai.reply", payload: { messageId: message.id }, dedupKey: message.id });
110
+ });
111
+ ```
112
+
113
+ | Field | Meaning |
114
+ | --- | --- |
115
+ | `queue` | a configured queue |
116
+ | `payload` | JSON for the handler, `{}` when there is none; stored as `jsonb`, so a `Date` comes back as a string. Ids, not personal data |
117
+ | `runAt` / `delaySeconds` | when the job is due; default now; one or the other |
118
+ | `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 |
119
+ | `maxAttempts` | this job's own limit instead of the queue's |
120
+
121
+ Inside the caller's transaction the job exists exactly when the caller's change commits; a rolled-back transaction
122
+ leaves no job behind.
123
+
124
+ ### Running the worker
125
+
126
+ ```ts
127
+ // scripts/worker.ts
128
+ import { runJobWorkerProcess } from "@softure-ai/jobs/worker";
129
+ import config from "../softure.config";
130
+ import { handlers } from "../src/jobs/handlers";
131
+
132
+ await runJobWorkerProcess({ config, handlers });
133
+ ```
134
+
135
+ `runJobWorkerProcess` opens its own pool (`database.url`, or the app's `database.handle`) with one connection per job
136
+ that can run at once plus one, runs the worker until SIGTERM or SIGINT, closes the pool and sets `process.exitCode`
137
+ (0 after a clean stop, 1 when it could not start). Pass `logger: createLogger("jobs")` from
138
+ `@softure-ai/observability` to send its lines to OpenTelemetry; by default they go to the console. `queues: [...]`
139
+ runs only some queues (one worker per queue group).
140
+
141
+ The worker claims due jobs of every queue it has a handler for, up to each queue's `concurrency`, oldest due first.
142
+ A claim counts as an attempt and takes a lease, which a heartbeat extends while the handler runs. A worker that dies
143
+ leaves its jobs `running` until their lease ends; the next claim then takes them over (or fails them with
144
+ `jobs.lease_expired` when no attempt is left). Every write after a claim is fenced by a claim token, so the worker
145
+ whose lease was taken over cannot record an outcome over the newer attempt. Several workers can run side by side:
146
+ `SKIP LOCKED` hands each job to one of them.
147
+
148
+ On SIGTERM the worker stops claiming, waits up to `shutdownGraceMs` for running handlers, then aborts the rest and
149
+ releases their jobs (pending again, due now, the attempt given back). Database errors while claiming or recording are
150
+ logged and retried on the next round; a database restart does not end the worker.
151
+
152
+ `runJobWorker({ db, config, handlers, signal })` is the loop without the process part, for an app that runs it inside
153
+ a process of its own.
154
+
155
+ ### Schedules
156
+
157
+ A schedule runs its queue's handler once per period, instead of an app cron:
158
+
159
+ ```ts
160
+ import { authJobSchedules } from "@softure-ai/auth/jobs";
161
+ import { securityJobSchedules } from "@softure-ai/security/jobs";
162
+
163
+ jobs({
164
+ schedules: {
165
+ ...authJobSchedules, // auth.prune-sessions hourly, auth.prune-password-resets daily
166
+ ...securityJobSchedules, // security.prune-rate-limits hourly
167
+ "app.digest": { every: "1d", payload: { kind: "weekly" } },
168
+ },
169
+ });
170
+ ```
171
+
172
+ ```ts
173
+ // src/jobs/handlers.ts: the packages' handlers next to the app's own
174
+ import { authJobHandlers } from "@softure-ai/auth/jobs";
175
+ import { securityJobHandlers } from "@softure-ai/security/jobs";
176
+
177
+ export const handlers: JobHandlers = { ...authJobHandlers, ...securityJobHandlers, "app.digest": digestHandler };
178
+ ```
179
+
180
+ - Periods start on whole multiples of `every` counted from 1970-01-01 00:00 UTC: `1h` on the full hour, `1d` at
181
+ midnight UTC, `15m` at :00, :15, :30, :45. A run is due at the start of its period.
182
+ - Once per period, however many workers run: a worker takes a period with a conditional upsert of
183
+ `jobs.schedules` (the period must be later than the stored one) and enqueues the run in the same transaction, so a
184
+ missed or doubled tick cannot happen. A worker enqueues only the schedules of the queues it runs.
185
+ - Missed periods (no worker ran) collapse into one run for the current period.
186
+ - Every scheduled run has the dedup key `schedule`: while a run is still pending or running (retrying a failure), the
187
+ next period joins it instead of queueing a second run behind it.
188
+ - Scheduled runs are ordinary jobs: retries, `maxAttempts`, `getJob`, `retryJob` and pruning apply.
189
+ - A package's prune handlers live in its `./jobs` entry (`@softure-ai/auth/jobs`, `@softure-ai/mcp-access/jobs`,
190
+ `@softure-ai/waitlist/jobs`, `@softure-ai/security/jobs`, `@softure-ai/analytics/jobs`), with `@softure-ai/jobs` as an
191
+ optional peer.
192
+
193
+ `getQueueStats` reports each scheduled queue's `schedule`: `everySeconds`, `lastEnqueuedAt` (the start of the latest
194
+ period a run was enqueued for), `lastSucceededAt` (any succeeded job of the queue, a run enqueued by hand too) and
195
+ `isLate`: no success within two periods, counted from when a worker first saw the schedule, or no worker has seen it
196
+ at all. The module's health check does not fail on a late schedule (a late cleanup must not take the app out of
197
+ rotation); an app that wants an alarm adds a check of its own:
198
+
199
+ ```ts
200
+ ops({
201
+ checks: {
202
+ "app.schedules": async (context) => {
203
+ const late = (await getQueueStats(context as JobsContext)).filter((stats) => stats.schedule?.isLate === true);
204
+ return late.length === 0 ? ok() : err("app.schedules_late");
205
+ },
206
+ },
207
+ });
208
+ ```
209
+
210
+ ### Without a worker
211
+
212
+ `runDueJobs(ctx, { handlers })` (from `@softure-ai/jobs/server` or `/testing`) enqueues the due runs of the schedules
213
+ among its queues, then runs every due job once, in this process, one after another, and returns
214
+ `{ succeeded, retried, failed, stale }`. A cron can drain a queue with it; call `pruneJobs(ctx)` from the same cron.
215
+ `enqueueDueSchedules(ctx, { queues? })` only enqueues the due scheduled runs.
216
+
217
+ ### Steering and overview
218
+
219
+ From `@softure-ai/jobs/server`, each on `ctx = { db, clock, config }`:
220
+
221
+ | Function | Result |
222
+ | --- | --- |
223
+ | `getJob(ctx, id)` | the `Job` or `null` |
224
+ | `cancelJob(ctx, id)` | a pending job becomes `cancelled`; `jobs.not_cancellable` for a running or finished one, `jobs.not_found` |
225
+ | `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` |
226
+ | `getQueueStats(ctx)` | per queue: counts per status, the due backlog, the `run_at` of its oldest due job and its `schedule` (or `null`) |
227
+ | `pruneJobs(ctx)` | deletes finished jobs past their retention; returns the count |
228
+
229
+ `getQueueStats` suits an app health check of its own, `ops({ checks: { "app.jobs": … } })`, e.g. unavailable when the
230
+ oldest due job of a queue waited more than ten minutes (no worker is running). An ops script (`@softure-ai/ops/scripts`)
231
+ can wrap `retryJob` and `cancelJob` for the operator.
232
+
233
+ ## 4. Mounting
234
+
235
+ Nothing to mount: the module has no routes or pages. The worker is a process (§3, "Running the worker").
236
+
237
+ ## 5. Migrations and tables
238
+
239
+ Schema `jobs`, two tables, `migrations/0001_create_jobs.sql` and `migrations/0002_create_schedules.sql`:
240
+
241
+ `jobs.jobs`: `id` (bigint identity; a string in the API), `queue`, `payload` (jsonb), `status` (`pending`, `running`,
242
+ `succeeded`, `failed`, `cancelled`), `attempts`, `max_attempts`, `run_at`, `locked_until`, `locked_by` (the worker,
243
+ `<hostname>:<pid>`), `claim_token`, `dedup_key`, `last_error` (an error code, never a message), `created_at`,
244
+ `updated_at`, `finished_at`. Checks keep the lease columns set exactly while `running` and `finished_at` set exactly
245
+ for finished jobs; a partial unique index allows one live job per queue and dedup key.
246
+
247
+ `jobs.schedules`: `name` (the schedule's queue, primary key), `last_slot_at` (the start of the latest period a run
248
+ was enqueued for), `last_succeeded_at`, `created_at`, `updated_at`. One row per schedule a worker has seen; a row of
249
+ a schedule removed from the config stays and is ignored.
250
+
251
+ The app role needs `SELECT, INSERT, UPDATE, DELETE` on the tables, which the `@softure-ai/ops` roles recipe grants on
252
+ every schema the migrator creates.
253
+
254
+ ## 6. Environment variables
255
+
256
+ None of its own. The worker reads the app's config, so it gets the app's `DATABASE_URL` and whatever its handlers need.
257
+
258
+ ## 7. Switches
259
+
260
+ None.
261
+
262
+ ## 8. Appearance
263
+
264
+ No UI.
265
+
266
+ ## 9. Copy
267
+
268
+ `errors.*` in `src/messages/en.ts`, one key per error code (`invalid_payload`, `handler_threw`, `lease_expired`,
269
+ `not_found`, `not_cancellable`, `not_retryable`, `dedup_key_taken`), for an admin screen that shows a job's state.
270
+
271
+ ## 10. Hooks
272
+
273
+ The handlers (§3) are the app's code. `logger` (`info`, `warn`, `error`; `@softure-ai/observability`'s `Logger` fits)
274
+ receives the worker's lines: started, stopped, a scheduled run enqueued, a thrown handler with its error, a lost lease,
275
+ a release, pruning.
276
+
277
+ ## 11. GDPR
278
+
279
+ No export or deletion contributor: jobs are transient and pruned (§3, `retention`). Keep personal data out of
280
+ payloads (ids only); then a deleted account leaves nothing personal in `jobs.jobs`. `last_error` holds codes, and the
281
+ logger gets payload paths, never values, for a payload that does not match its schema.
282
+
283
+ ## 12. Limitations
284
+
285
+ - Polling, no `LISTEN/NOTIFY`: a job waits up to `pollIntervalMs` when the worker is idle.
286
+ - Schedules take a fixed period (`every`), aligned to UTC; no cron expressions, no time zones, no "at 03:00".
287
+ - No priorities within a queue and no rate limits per queue beyond `concurrency`; split work into queues instead.
288
+ - At-least-once delivery (§3, "Handlers run at least once").
289
+ - PGlite has one connection, so tests run claims one after another; concurrent claiming is Postgres behaviour
290
+ (`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"}
@@ -0,0 +1,2 @@
1
+ export const JOB_STATUSES = ["pending", "running", "succeeded", "failed", "cancelled"];
2
+ //# sourceMappingURL=contract.js.map
@@ -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"}
@@ -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"}
@@ -0,0 +1,87 @@
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").ZodPipe<import("zod").ZodObject<{
17
+ queues: import("zod").ZodDefault<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
+ schedules: import("zod").ZodDefault<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodObject<{
27
+ every: import("zod").ZodString;
28
+ payload: import("zod").ZodDefault<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
29
+ }, import("zod/v4/core").$strict>>>;
30
+ pollIntervalMs: import("zod").ZodDefault<import("zod").ZodNumber>;
31
+ shutdownGraceMs: import("zod").ZodDefault<import("zod").ZodNumber>;
32
+ retention: import("zod").ZodDefault<import("zod").ZodObject<{
33
+ succeededDays: import("zod").ZodDefault<import("zod").ZodNumber>;
34
+ failedDays: import("zod").ZodDefault<import("zod").ZodNumber>;
35
+ cancelledDays: import("zod").ZodDefault<import("zod").ZodNumber>;
36
+ }, import("zod/v4/core").$strict>>;
37
+ }, import("zod/v4/core").$strict>, import("zod").ZodTransform<{
38
+ queues: {
39
+ [x: string]: {
40
+ maxAttempts: number;
41
+ backoff: {
42
+ baseSeconds: number;
43
+ maxSeconds: number;
44
+ };
45
+ leaseSeconds: number;
46
+ concurrency: number;
47
+ };
48
+ };
49
+ schedules: Record<string, {
50
+ every: string;
51
+ payload: Record<string, unknown>;
52
+ }>;
53
+ pollIntervalMs: number;
54
+ shutdownGraceMs: number;
55
+ retention: {
56
+ succeededDays: number;
57
+ failedDays: number;
58
+ cancelledDays: number;
59
+ };
60
+ }, {
61
+ queues: Record<string, {
62
+ maxAttempts: number;
63
+ backoff: {
64
+ baseSeconds: number;
65
+ maxSeconds: number;
66
+ };
67
+ leaseSeconds: number;
68
+ concurrency: number;
69
+ }>;
70
+ schedules: Record<string, {
71
+ every: string;
72
+ payload: Record<string, unknown>;
73
+ }>;
74
+ pollIntervalMs: number;
75
+ shutdownGraceMs: number;
76
+ retention: {
77
+ succeededDays: number;
78
+ failedDays: number;
79
+ cancelledDays: number;
80
+ };
81
+ }>>>;
82
+ export { JOB_STATUSES, type Job, type JobFailure, type JobOutcome, type JobsErrorCode, type JobStatus, } from "./contract.js";
83
+ export { jobsMessages, type JobsMessages } from "./messages/index.js";
84
+ export { jobsOptionsSchema, type JobQueueOptions, type JobScheduleOptions, type JobSchedulesInput, type JobsOptions, type JobsOptionsInput, } from "./options.js";
85
+ export { defineJobHandler, failJob, retryJobLater, type JobHandler, type JobHandlerContext, type JobHandlers } from "./handlers.js";
86
+ export { jobs as jobsTable, schedules as jobSchedulesTable } from "./schema.js";
87
+ //# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAiBf,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,EACL,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAC;AACtB,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,SAAS,IAAI,iBAAiB,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.1",
17
+ dependsOn: {},
18
+ dbSchema: "jobs",
19
+ tables: ["jobs", "schedules"],
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, schedules as jobSchedulesTable } 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,EAAE,WAAW,CAAC;QAC7B,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,EACL,iBAAiB,GAMlB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,aAAa,EAA6D,MAAM,eAAe,CAAC;AACpI,OAAO,EAAE,IAAI,IAAI,SAAS,EAAE,SAAS,IAAI,iBAAiB,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