@owlmeans/queue 0.1.18-rc.22 → 0.1.18-rc.24
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/README.md +60 -12
- package/agent-meta/manifest.json +9 -2
- package/agent-meta/skills/queue/SKILL.md +54 -11
- package/agent-meta/skills/scheduled-jobs/SKILL.md +149 -0
- package/build/config.d.ts +29 -1
- package/build/config.d.ts.map +1 -1
- package/build/config.js +101 -1
- package/build/config.js.map +1 -1
- package/build/errors.d.ts +9 -0
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +13 -0
- package/build/errors.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/protocol.d.ts.map +1 -1
- package/build/protocol.js +4 -7
- package/build/protocol.js.map +1 -1
- package/build/route.d.ts +15 -0
- package/build/route.d.ts.map +1 -0
- package/build/route.js +32 -0
- package/build/route.js.map +1 -0
- package/build/transport.d.ts.map +1 -1
- package/build/transport.js +8 -11
- package/build/transport.js.map +1 -1
- package/build/types.d.ts +55 -0
- package/build/types.d.ts.map +1 -1
- package/build/worker.d.ts.map +1 -1
- package/build/worker.js +5 -4
- package/build/worker.js.map +1 -1
- package/package.json +8 -8
- package/src/config.ts +117 -2
- package/src/errors.ts +15 -0
- package/src/index.ts +1 -0
- package/src/protocol.ts +4 -7
- package/src/route.ts +48 -0
- package/src/transport.ts +8 -11
- package/src/types.ts +56 -0
- package/src/worker.ts +5 -4
- package/tests/protocol.spec.ts +2 -2
- package/tests/schedule.spec.ts +100 -0
package/README.md
CHANGED
|
@@ -6,8 +6,8 @@ than about five seconds, accepted work must survive a process restart, or the wo
|
|
|
6
6
|
against a third party (payment capture, provider sync, bulk mail, report generation, model
|
|
7
7
|
pipelines). A request that finishes in under about two seconds and makes no external call should
|
|
8
8
|
not queue: do it inline. Check for a smaller fix first, such as an index, a batch write or a cached
|
|
9
|
-
aggregate.
|
|
10
|
-
|
|
9
|
+
aggregate. Recurring work is a declared schedule: the broker produces a job on an interval or a
|
|
10
|
+
cron pattern and a registered processor runs it.
|
|
11
11
|
|
|
12
12
|
This package is contracts only and carries no broker code. A driver implements them
|
|
13
13
|
(`@owlmeans/redis-queue` does, over BullMQ). Depend on this package from a shared contract package,
|
|
@@ -16,7 +16,7 @@ and on the driver only where the application wires itself up.
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
bun add @owlmeans/queue@^0.1.18-rc.
|
|
19
|
+
bun add @owlmeans/queue@^0.1.18-rc.24
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Concepts
|
|
@@ -26,8 +26,8 @@ bun add @owlmeans/queue@^0.1.18-rc.22
|
|
|
26
26
|
address, not a deployment.
|
|
27
27
|
- **Listen**: `listenQueues(cfg, ...names)` in one process's own config says which queues *that
|
|
28
28
|
process* consumes. A process that listens to nothing is a producer only.
|
|
29
|
-
- **Queued protocol**: an immutable entrypoint protocol whose route uses `job()`
|
|
30
|
-
|
|
29
|
+
- **Queued protocol**: an immutable backend entrypoint protocol whose route uses `job()` from this
|
|
30
|
+
package (`QUEUE_PROTOCOL`). Callers use `ctx.entrypoint(protocol).call(...)` unchanged,
|
|
31
31
|
and the QUEUE transport carries the call through the broker.
|
|
32
32
|
- **Job record**: `ctx.jobs(queue)` returns a `QueueResource`, the ordinary `Resource<JobRecord>`
|
|
33
33
|
contract plus `wait`, `flow`, `counts` and `close`. `create` enqueues, `list` inspects and `take`
|
|
@@ -36,6 +36,9 @@ bun add @owlmeans/queue@^0.1.18-rc.22
|
|
|
36
36
|
a `JobContext`. It must be safe to run twice and must `touch()` in long loops.
|
|
37
37
|
- **Single-flight id**: a `JobOptions.id` derived from what the job is about (`develop:<storyId>`).
|
|
38
38
|
A duplicate enqueue then returns the existing job instead of a second one.
|
|
39
|
+
- **Schedule**: `declareSchedule(cfg, { id, queue, name, every | pattern, … })` in the shared config.
|
|
40
|
+
Every process that listens to the queue reconciles the broker with the declared schedules when its
|
|
41
|
+
worker starts; each run is an ordinary job, and `JobContext.scheduled` names the schedule.
|
|
39
42
|
|
|
40
43
|
## Usage
|
|
41
44
|
|
|
@@ -77,7 +80,8 @@ appendRedisQueue(context, { hooks: billingQueueHooks(context) })
|
|
|
77
80
|
|
|
78
81
|
```ts
|
|
79
82
|
import { contract, protocol, typed } from '@owlmeans/entrypoint'
|
|
80
|
-
import {
|
|
83
|
+
import { job } from '@owlmeans/queue'
|
|
84
|
+
import { backend, route } from '@owlmeans/route'
|
|
81
85
|
|
|
82
86
|
const aliases = { base: 'app:report', build: 'app:report:build' } as const
|
|
83
87
|
const reportBase = protocol(route(aliases.base, '/reports', backend({ service: APP_WORKER })), contract())
|
|
@@ -155,6 +159,41 @@ await context.jobs(APP_OPS).flow({
|
|
|
155
159
|
})
|
|
156
160
|
```
|
|
157
161
|
|
|
162
|
+
### Recurring jobs
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { declareQueue, declareSchedule } from '@owlmeans/queue'
|
|
166
|
+
|
|
167
|
+
// Shared config: the queue first, then its schedules.
|
|
168
|
+
declareQueue(cfg, APP_MAINTENANCE, ['app:maintenance:reconcile', 'app:maintenance:purge'], {
|
|
169
|
+
worker: { concurrency: 1, lockDuration: 60_000 }
|
|
170
|
+
})
|
|
171
|
+
declareSchedule(cfg, {
|
|
172
|
+
id: 'app-nightly-reconcile', queue: APP_MAINTENANCE, name: 'app:maintenance:reconcile',
|
|
173
|
+
pattern: '17 3 * * *', tz: 'UTC', data: {},
|
|
174
|
+
})
|
|
175
|
+
declareSchedule(cfg, {
|
|
176
|
+
id: 'app-purge', queue: APP_MAINTENANCE, name: 'app:maintenance:purge', every: 15 * 60_000,
|
|
177
|
+
})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The process that `listenQueues(cfg, APP_MAINTENANCE)` registers the processors; nothing else is
|
|
181
|
+
wired. A scheduled run carries no caller and no credentials, so it goes to a processor, never to a
|
|
182
|
+
guarded entrypoint:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
worker.process(APP_MAINTENANCE, 'app:maintenance:reconcile', async job => {
|
|
186
|
+
for await (const page of organizations()) {
|
|
187
|
+
await job.touch()
|
|
188
|
+
await reconcile(page) // safe to run twice: a restart re-runs the whole sweep
|
|
189
|
+
}
|
|
190
|
+
return { scheduled: job.scheduled } // 'app-nightly-reconcile'
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Renaming a schedule id removes the old scheduler and creates a new one; deleting a declaration
|
|
195
|
+
removes its scheduler at the next worker start.
|
|
196
|
+
|
|
158
197
|
### Jobs are records
|
|
159
198
|
|
|
160
199
|
```ts
|
|
@@ -197,6 +236,9 @@ not the job, so read the job back to learn what became of it.
|
|
|
197
236
|
| `declareQueue(cfg, name, jobs, opts?)` | function | Declare a queue and the job names it accepts; re-declaring replaces |
|
|
198
237
|
| `listenQueues(cfg, ...names)` | function | Name the queues this process consumes |
|
|
199
238
|
| `queueOf(cfg, name)`, `queueOfJob(cfg, job)`, `isListening(cfg, name)` | function | Read declarations back; `queueOf` throws `UnknownQueue` |
|
|
239
|
+
| `declareSchedule(cfg, schedule)` | function | Declare a recurring job after checking it; re-declaring an id replaces |
|
|
240
|
+
| `schedulesOf(cfg, queue?)` | function | The declared schedules, of one queue when named |
|
|
241
|
+
| `assertSchedule(cfg, schedule)`, `assertSchedules(cfg)` | function | Check one schedule, or every declared one plus duplicate ids |
|
|
200
242
|
| `enqueueProtocol(ctx, protocol, request, options?)` | function | Enqueue a QUEUE protocol with `JobOptions`, typed by the protocol |
|
|
201
243
|
| `waitForProtocol(ctx, protocol, job, { timeout }?)` | function | Wait for that job and unwrap the typed reply |
|
|
202
244
|
| `queueJobOf(request)` | function | `QueueJobMeta` of the broker job behind a handler's request, or `null` |
|
|
@@ -204,11 +246,11 @@ not the job, so read the job back to learn what became of it.
|
|
|
204
246
|
| `queueWorkerMiddleware(alias?)` | function | Start the worker at the Ready stage, only in a process that listens |
|
|
205
247
|
| `handleJob(ctx, job)`, `entrypointProcessor(ctx)`, `servedJobs(ctx)` | function | The bridge a driver dispatches entrypoint jobs through |
|
|
206
248
|
| `requestOf(envelope)`, `assertFresh(envelope, ttl?)` | function | Rebuild a request from an envelope; enforce `envelopeTtl` |
|
|
207
|
-
| `QueueConfig`, `QueueDeclaration`, `QueueWorkerOptions`, `JobOptions` | type | Configuration shapes |
|
|
249
|
+
| `QueueConfig`, `QueueDeclaration`, `QueueWorkerOptions`, `JobOptions`, `ScheduleDeclaration` | type | Configuration shapes |
|
|
208
250
|
| `QueueResource<D, R>` | type | A queue as a resource, plus `queue`, `wait`, `flow`, `counts`, `close` |
|
|
209
251
|
| `JobRecord<D, R>`, `JobEvent<R>`, `FlowSpec<D>` | type | A job as a record, a lifecycle event, a graph node |
|
|
210
252
|
| `QueueWorkerService` | type | `process`, `start`, `stop`, `listening`, `hooks` |
|
|
211
|
-
| `JobContext<D>`, `JobProcessor<D, R>` | type | What a processor receives; the processor signature |
|
|
253
|
+
| `JobContext<D>`, `JobProcessor<D, R>` | type | What a processor receives (`scheduled` names the schedule of a scheduled run); the processor signature |
|
|
212
254
|
| `QueueHooks` | type | `wrapHandler`, `onJobResult`, `onJobStalled`, `onJobDead` |
|
|
213
255
|
| `QueueJobMeta`, `JobEnvelope`, `JobReply<T>` | type | Broker identity for a handler, the call envelope, the reply shape |
|
|
214
256
|
| `QueueAppend`, `QueueDriver` | type | The `ctx.jobs(queue?)` mixin a driver installs; what a driver supplies |
|
|
@@ -216,7 +258,7 @@ not the job, so read the job back to learn what became of it.
|
|
|
216
258
|
| `JobState`, `JobEventType` | enum | Job lifecycle states and event types |
|
|
217
259
|
| `isSettled(state?)` | function | Whether a state is final |
|
|
218
260
|
| `DEFAULT_ALIAS`, `DEFAULT_JOB_TIMEOUT`, `DEFAULT_ATTEMPTS` | const | `'queue'`, `60_000` ms, `1` |
|
|
219
|
-
| `QueueError`, `QueueTimeout`, `UnknownJob`, `UnknownJobName`, `UnknownQueue`, `QueueNotListening`, `JobNotServed`, `EnvelopeExpired` | class | Error family |
|
|
261
|
+
| `QueueError`, `QueueTimeout`, `UnknownJob`, `UnknownJobName`, `UnknownQueue`, `QueueNotListening`, `JobNotServed`, `EnvelopeExpired`, `ScheduleMisdeclared` | class | Error family |
|
|
220
262
|
|
|
221
263
|
## Common pitfalls
|
|
222
264
|
|
|
@@ -233,13 +275,19 @@ not the job, so read the job back to learn what became of it.
|
|
|
233
275
|
- **Long loops without `touch()`**, or processors that are not safe to run twice.
|
|
234
276
|
- **Throwing from a child for a domain failure.** Return `{ ok: false, error }` instead.
|
|
235
277
|
- **Equal child ids across queues in one flow.** `children()` is keyed by bare id and collapses them.
|
|
236
|
-
- **A self-re-enqueueing job as a cron.**
|
|
278
|
+
- **A self-re-enqueueing job as a cron.** Declare a schedule. `delay` defers a single job once.
|
|
279
|
+
- **A schedule on a queue nothing consumes.** Only a listening worker creates the scheduler, and the
|
|
280
|
+
broker produces each next run when the previous one starts.
|
|
281
|
+
- **Loading different schedule lists in two processes that listen to one queue.** Each start removes
|
|
282
|
+
the schedules its own list does not name.
|
|
283
|
+
- **Declaring a schedule before its queue.** `declareSchedule` checks the queue and the job name and
|
|
284
|
+
throws `UnknownQueue` / `UnknownJobName`.
|
|
237
285
|
- **Calling `ctx.jobs()` with no name once a second queue exists.** It throws `UnknownQueue`.
|
|
238
286
|
|
|
239
287
|
## Related packages
|
|
240
288
|
|
|
241
289
|
- [`@owlmeans/redis-queue`](../redis-queue): the BullMQ driver, and where queue integration tests live
|
|
242
|
-
- [`@owlmeans/route`](../route):
|
|
290
|
+
- [`@owlmeans/route`](../route): transport-neutral route declarations used by the queue-owned `job()` builder
|
|
243
291
|
- [`@owlmeans/entrypoint`](../entrypoint): protocol declarations and the transport seam
|
|
244
292
|
- [`@owlmeans/resource`](../resource): the `Resource` contract jobs are read through
|
|
245
293
|
- [`@owlmeans/server-job`](../server-job) / [`@owlmeans/client-job`](../client-job): listing,
|
|
@@ -253,7 +301,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
253
301
|
your project's skill store (`.agents/skills/`):
|
|
254
302
|
|
|
255
303
|
```sh
|
|
256
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
304
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.30
|
|
257
305
|
```
|
|
258
306
|
|
|
259
307
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/queue",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.24",
|
|
5
|
+
"generatedAt": "2026-09-19T13:42:56.226Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
|
+
{
|
|
9
|
+
"kind": "skill",
|
|
10
|
+
"name": "scheduled-jobs",
|
|
11
|
+
"category": "multi-package",
|
|
12
|
+
"file": "skills/scheduled-jobs/SKILL.md",
|
|
13
|
+
"canonicalPath": ".agents/skills/scheduled-jobs/SKILL.md"
|
|
14
|
+
},
|
|
8
15
|
{
|
|
9
16
|
"kind": "skill",
|
|
10
17
|
"name": "queue",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: queue
|
|
3
|
-
description: How to use @owlmeans/queue — protocol-object enqueue/wait, job queues as resources, QUEUE declarations, producer/consumer split, single-flight job ids, lifecycle hooks, and processor rules. Auto-invoked when importing queue types
|
|
3
|
+
description: How to use @owlmeans/queue — protocol-object enqueue/wait, job queues as resources, QUEUE declarations, producer/consumer split, single-flight job ids, recurring schedules, lifecycle hooks, and processor rules. Auto-invoked when importing queue types, declaring a job protocol, or declaring a schedule.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
@@ -8,19 +8,20 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/queue
|
|
9
9
|
|
|
10
10
|
**Layer:** Infra
|
|
11
|
-
**Install:** `"@owlmeans/queue": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/queue": "^0.1.18-rc.24"` in `dependencies`
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
them. Depend on this one from a shared
|
|
13
|
+
Backend transport contracts only. It carries no broker code — a driver package
|
|
14
|
+
(`@owlmeans/redis-queue`) implements them. Depend on this one from a shared backend package;
|
|
15
|
+
generic common and frontend packages must not import it. Depend on the driver only where the
|
|
15
16
|
application wires itself up.
|
|
16
17
|
|
|
17
18
|
## The one idea
|
|
18
19
|
|
|
19
|
-
A queued call is an immutable entrypoint protocol whose route names `
|
|
20
|
+
A queued call is an immutable entrypoint protocol whose route names `QUEUE_PROTOCOL`.
|
|
20
21
|
Ordinary calls use `ctx.entrypoint(protocol).call(request)`. Code that needs broker options or a
|
|
21
22
|
job handle uses `enqueueProtocol(ctx, protocol, request, options)` and
|
|
22
23
|
`waitForProtocol(ctx, protocol, job)`. Both derive queue and job name from the protocol object and
|
|
23
|
-
preserve its exact request/reply pair; raw strings and non-
|
|
24
|
+
preserve its exact request/reply pair; raw strings and non-queue declarations are rejected.
|
|
24
25
|
|
|
25
26
|
That is what makes it worth having: moving a service-to-service call onto the broker is a change
|
|
26
27
|
to a declaration, not to the places that call it, and the broker holds the work across a restart of
|
|
@@ -60,6 +61,9 @@ protocol(
|
|
|
60
61
|
)
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
Import `job`, `QUEUE_PROTOCOL`, `isQueueRoute` and `queueRouteOptions` from `@owlmeans/queue`.
|
|
65
|
+
The generic route package deliberately knows none of the queue option shape.
|
|
66
|
+
|
|
63
67
|
The alias appears in `declareQueue(...jobs)` only because the broker is a string-keyed adapter.
|
|
64
68
|
Application producers and handlers keep the protocol object.
|
|
65
69
|
|
|
@@ -142,6 +146,43 @@ run twice — skip work already recorded, delete the rows a previous attempt cre
|
|
|
142
146
|
regenerating over a baseline. Decide this per job and say so in a comment; there is no way to make
|
|
143
147
|
it automatic.
|
|
144
148
|
|
|
149
|
+
## Recurring jobs
|
|
150
|
+
|
|
151
|
+
A recurring sweep is a SCHEDULE on a queue, run by a registered processor:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
declareQueue(cfg, APP_MAINTENANCE, ['app:maintenance:reconcile'], {
|
|
155
|
+
worker: { concurrency: 1, lockDuration: 60_000 }
|
|
156
|
+
})
|
|
157
|
+
declareSchedule(cfg, {
|
|
158
|
+
id: 'app-nightly-reconcile', queue: APP_MAINTENANCE, name: 'app:maintenance:reconcile',
|
|
159
|
+
pattern: '17 3 * * *', tz: 'UTC', data: {},
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- **Declare it in the shared package, after its queue.** `declareSchedule` runs `assertSchedule`
|
|
164
|
+
first: the queue must be declared (`UnknownQueue`) and must accept the job name
|
|
165
|
+
(`UnknownJobName`), exactly one of `every` (a positive integer of milliseconds) and `pattern`
|
|
166
|
+
(cron), `tz` and `immediately` only with a pattern, never `immediately` with `startDate`, and
|
|
167
|
+
`opts.id` / `opts.delay` refused — the broker names and places every run
|
|
168
|
+
(`ScheduleMisdeclared`, message `<id>:<reason>`). Re-declaring an id replaces it.
|
|
169
|
+
- **The id is the identity.** Changing any other field updates the schedule in place; renaming the
|
|
170
|
+
id is one schedule removed and another created.
|
|
171
|
+
- **Every process that listens to the queue reconciles it at worker start** — the driver creates
|
|
172
|
+
and updates what is declared and removes the schedules it owns that are not. Two listeners of one
|
|
173
|
+
queue must therefore load the same schedule list, which is why it lives in the shared package.
|
|
174
|
+
A producer creates nothing.
|
|
175
|
+
- **A scheduled run is a processor job, never a guarded entrypoint.** Nobody's credentials travel
|
|
176
|
+
with it. `job.scheduled` carries the schedule id; it is absent on a job enqueued any other way.
|
|
177
|
+
- **It needs a consumer.** The broker produces the next run when the previous one starts
|
|
178
|
+
processing, so a schedule on a queue nothing consumes stops producing.
|
|
179
|
+
- **The processor obeys every rule above** — `touch()` through the sweep, and safe to run twice,
|
|
180
|
+
because a restart mid-sweep re-runs it from the start.
|
|
181
|
+
|
|
182
|
+
`schedulesOf(cfg, queue?)` reads the declarations back; `assertSchedules(cfg)` checks all of them,
|
|
183
|
+
including one written into `cfg.queue.schedules` directly, and refuses a duplicate id.
|
|
184
|
+
Which recurring work belongs on a schedule at all is `scheduled-jobs`.
|
|
185
|
+
|
|
145
186
|
## A queued call is still a guarded call
|
|
146
187
|
|
|
147
188
|
`handleJob` rebuilds the request from the envelope and runs the entrypoint as the HTTP boundary
|
|
@@ -183,34 +224,36 @@ listens to, grouped by queue, which is what a driver binds.
|
|
|
183
224
|
| `enqueueProtocol` / `waitForProtocol` | Typed protocol-object enqueue and result wait; validates protocol kind and queue configuration. |
|
|
184
225
|
| `queueJobOf(request)` / `QueueJobMeta` | Explicit broker identity boundary for a protocol handler, including `touch()` for long work. |
|
|
185
226
|
| `declareQueue` / `listenQueues` | Configuration — what exists, and what this process consumes |
|
|
227
|
+
| `declareSchedule` / `schedulesOf` / `assertSchedule` / `assertSchedules` | Recurring jobs — declare, read back, check |
|
|
186
228
|
| `queueOf` / `queueOfJob` / `isListening` | Reading it back; `queueOf` throws `UnknownQueue` |
|
|
187
|
-
| `QueueConfig` / `QueueDeclaration` / `QueueWorkerOptions` / `JobOptions` | The configuration shapes |
|
|
229
|
+
| `QueueConfig` / `QueueDeclaration` / `QueueWorkerOptions` / `JobOptions` / `ScheduleDeclaration` | The configuration shapes |
|
|
188
230
|
| `QueueResource<D, R>` | A queue addressed as a resource, plus `wait` / `flow` / `counts` / `close` |
|
|
189
231
|
| `JobRecord<D, R>` / `JobState` / `isSettled` / `JobEvent` / `JobEventType` | The record shape and lifecycle |
|
|
190
232
|
| `FlowSpec<D>` | A graph node — `name`, optional `queue`, `data`, `opts`, `children` |
|
|
191
233
|
| `QueueWorkerService` | `process(queue, name, fn)`, `start`, `stop`, `listening`, `hooks` |
|
|
192
|
-
| `JobContext<D>` | What a processor gets: `id`, `name`, `queue`, `attempt`, `data`, `signal`, `touch`, `progress`, `children`, `failedChildren` |
|
|
234
|
+
| `JobContext<D>` | What a processor gets: `id`, `name`, `queue`, `attempt`, `data`, `signal`, `scheduled?`, `touch`, `progress`, `children`, `failedChildren` |
|
|
193
235
|
| `QueueHooks` | `wrapHandler`, `onJobResult`, `onJobStalled`, `onJobDead` — the compensation seam |
|
|
194
236
|
| `QueueAppend` | The `ctx.jobs(queue?)` mixin a driver installs; `QueueDriver` is what a driver supplies |
|
|
195
237
|
| `appendQueueTransport` / `makeQueueTransport` | Binds the QUEUE protocol so `call()` routes through the broker |
|
|
196
238
|
| `queueWorkerMiddleware` | Starts the worker at Ready stage |
|
|
197
239
|
| `handleJob` / `servedJobs` / `entrypointProcessor` | The bridge a driver dispatches through |
|
|
198
240
|
| `requestOf` / `assertFresh` / `JobEnvelope` / `JobReply` | The envelope, and rebuilding a request from it |
|
|
199
|
-
| Errors | `QueueError`, `QueueTimeout`, `UnknownJob`, `UnknownJobName`, `UnknownQueue`, `QueueNotListening`, `JobNotServed`, `EnvelopeExpired` |
|
|
241
|
+
| Errors | `QueueError`, `QueueTimeout`, `UnknownJob`, `UnknownJobName`, `UnknownQueue`, `QueueNotListening`, `JobNotServed`, `EnvelopeExpired`, `ScheduleMisdeclared` |
|
|
200
242
|
| Constants | `DEFAULT_ALIAS` (`queue`), `DEFAULT_JOB_TIMEOUT` (60 000 ms), `DEFAULT_ATTEMPTS` (1) |
|
|
201
243
|
|
|
202
244
|
## Depends On
|
|
203
245
|
|
|
204
246
|
- `@owlmeans/context` — service registration and the middleware stage
|
|
205
247
|
- `@owlmeans/entrypoint` — the transport seam and the served-entrypoint dispatch
|
|
206
|
-
- `@owlmeans/route` — `
|
|
248
|
+
- `@owlmeans/route` — the generic declaration model that `QUEUE_PROTOCOL` and `job()` extend
|
|
207
249
|
- `@owlmeans/resource` — the `Resource` contract jobs are read through
|
|
208
250
|
- `@owlmeans/auth` / `@owlmeans/auth-common` — guarding a queued call and attaching its entity
|
|
209
251
|
- `@owlmeans/error` — marshalling a refusal across the hop
|
|
210
252
|
|
|
211
253
|
## Related
|
|
212
254
|
|
|
213
|
-
- `redis-queue` — the BullMQ driver;
|
|
255
|
+
- `redis-queue` — the BullMQ driver; how schedules are reconciled; integration tests live there
|
|
256
|
+
- `scheduled-jobs` — when recurring work is a schedule, and how to write the sweep it runs
|
|
214
257
|
- `server-job` / `client-job` — exposing a queue's jobs to an application's UI (list, cancel,
|
|
215
258
|
progress over a socket), without touching the contracts here
|
|
216
259
|
- `entrypoint` — protocol declarations, bindings, and the transport lookup
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scheduled-jobs
|
|
3
|
+
description: Recurring background work on OwlMeans queues — declareSchedule in @owlmeans/queue, reconciled into BullMQ job schedulers by @owlmeans/redis-queue when a listening worker starts. Covers interval vs cron schedules, the processor a sweep runs, overlap and restart semantics, renaming and removing a schedule, multi-replica and rolling-deploy behaviour, and testing. Use when adding a nightly or periodic job, replacing a platform CronJob or a setInterval with a schedule, or diagnosing a schedule that does not run.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
7
|
+
|
|
8
|
+
# Scheduled jobs
|
|
9
|
+
|
|
10
|
+
**Layer:** Infra · **Packages:** `@owlmeans/queue` (lead — the declaration), `@owlmeans/redis-queue`
|
|
11
|
+
(the reconciliation)
|
|
12
|
+
|
|
13
|
+
A schedule is a declaration that makes the broker produce an ordinary job on an interval or a cron
|
|
14
|
+
pattern. A worker that consumes the queue runs it with a processor. There is no second scheduler
|
|
15
|
+
process, no cron table and no timer in application code.
|
|
16
|
+
|
|
17
|
+
## When a schedule is the answer
|
|
18
|
+
|
|
19
|
+
| Situation | Answer |
|
|
20
|
+
|---|---|
|
|
21
|
+
| Periodic sweep, reconciliation, digest, retention delete, rollup | A schedule on a queue a worker consumes |
|
|
22
|
+
| "Remind this user in 20 minutes" | `JobOptions.delay` on one job — a schedule is not per record |
|
|
23
|
+
| A far-future or cancellable per-record deadline | A row with a due date, swept by one schedule |
|
|
24
|
+
| A deployment that runs no worker | A platform `CronJob` calling a guarded entrypoint |
|
|
25
|
+
| A `setInterval` inside a request handler or a long-lived service | A schedule — the interval dies with the process and runs once per replica |
|
|
26
|
+
|
|
27
|
+
Whether the work belongs on a queue at all is `resource-choice`.
|
|
28
|
+
|
|
29
|
+
## Declaring
|
|
30
|
+
|
|
31
|
+
In the SHARED package every process loads, after the queue:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
declareQueue(cfg, APP_MAINTENANCE, ['app:maintenance:reconcile', 'app:maintenance:purge'], {
|
|
35
|
+
worker: { concurrency: 1, lockDuration: 60_000 }
|
|
36
|
+
})
|
|
37
|
+
declareSchedule(cfg, {
|
|
38
|
+
id: 'app-nightly-reconcile', queue: APP_MAINTENANCE, name: 'app:maintenance:reconcile',
|
|
39
|
+
pattern: '17 3 * * *', tz: 'UTC', data: {},
|
|
40
|
+
})
|
|
41
|
+
declareSchedule(cfg, {
|
|
42
|
+
id: 'app-purge-expired', queue: APP_MAINTENANCE, name: 'app:maintenance:purge', every: 15 * 60_000,
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
And only in the process that runs them: `listenQueues(cfg, APP_MAINTENANCE)` plus a processor per
|
|
47
|
+
job name, registered while the context is wired.
|
|
48
|
+
|
|
49
|
+
- **`pattern` for the calendar, `every` for a frequency.** A nightly job is a pattern with an
|
|
50
|
+
explicit `tz`; "every quarter hour, whenever" is `every`. Pick an odd minute (`17 3 * * *`) so
|
|
51
|
+
many applications' sweeps do not stampede one Redis at the top of the hour.
|
|
52
|
+
- **`every` runs at once** when the scheduler is created (unless `startDate` says otherwise); a
|
|
53
|
+
pattern waits for its next match unless `immediately: true`, which fires only when the scheduler
|
|
54
|
+
is created or its declaration changes — never on an unchanged restart.
|
|
55
|
+
- **The id is permanent.** It is the scheduler's identity in the broker: any other field changes in
|
|
56
|
+
place, a renamed id is a removed schedule plus a new one. Prefix it with the application
|
|
57
|
+
(`app-…`) — ids share one namespace per queue.
|
|
58
|
+
- **Declaration errors surface at boot.** `declareSchedule` throws `UnknownQueue`,
|
|
59
|
+
`UnknownJobName` or `ScheduleMisdeclared` (`<id>:<reason>`) rather than letting the broker fail
|
|
60
|
+
on every tick.
|
|
61
|
+
- **`opts` sets retries and retention for every run; `id` and `delay` are refused** — the broker
|
|
62
|
+
names each run and places it on the schedule.
|
|
63
|
+
|
|
64
|
+
## Writing the processor
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
worker.process<Record<string, never>, SweepReport>(APP_MAINTENANCE, 'app:maintenance:reconcile',
|
|
68
|
+
async job => {
|
|
69
|
+
const report = { scanned: 0, repaired: 0, failed: 0 }
|
|
70
|
+
for await (const page of organizations({ size: 200 })) {
|
|
71
|
+
await job.touch()
|
|
72
|
+
for (const organization of page) {
|
|
73
|
+
try {
|
|
74
|
+
report.repaired += await reconcile(organization.entityId) ? 1 : 0
|
|
75
|
+
} catch (error) {
|
|
76
|
+
report.failed++
|
|
77
|
+
console.error(`reconcile ${organization.entityId}`, error)
|
|
78
|
+
}
|
|
79
|
+
report.scanned++
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
console.info(`[maintenance] ${job.scheduled}`, report)
|
|
83
|
+
return report
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- **A scheduled run has no caller.** No credentials, no entity, no request: it is a processor job,
|
|
88
|
+
never a guarded entrypoint. A step that must go through another service's guard calls that
|
|
89
|
+
service with service credentials.
|
|
90
|
+
- **Idempotent per item, not per run.** A worker rolled out mid-sweep, a stalled lock or a manual
|
|
91
|
+
re-run repeats the whole sweep; each item's repair must be a no-op the second time.
|
|
92
|
+
- **One item's failure is logged and counted, never thrown.** A thrown sweep is retried from the
|
|
93
|
+
start, or dropped after its attempts — either way the other items pay for one bad record.
|
|
94
|
+
- **`touch()` per page.** A sweep over every organization outlives `lockDuration` easily; silence
|
|
95
|
+
hands the job to another worker while this one is still going.
|
|
96
|
+
- **Return a small report.** It lands on the completed job, where an operator reads it.
|
|
97
|
+
|
|
98
|
+
## Overlap, replicas and deploys
|
|
99
|
+
|
|
100
|
+
- **One job per tick, whatever the replica count.** The broker produces one job; one worker takes
|
|
101
|
+
it. Several replicas listening to the queue do not multiply runs.
|
|
102
|
+
- **Runs can overlap on a concurrent queue.** The next run is produced when the previous one
|
|
103
|
+
starts, so a run that outlasts the interval meets its successor. Put sweeps on their own queue
|
|
104
|
+
with `concurrency: 1`, apart from interactive lanes.
|
|
105
|
+
- **A schedule needs a consumer.** Only a listening worker creates the scheduler, and the next run
|
|
106
|
+
is produced only when the previous one starts processing. A queue nothing consumes stops.
|
|
107
|
+
- **Every listener reconciles at start, from its own configuration.** It creates or updates what it
|
|
108
|
+
declares and removes the schedules it owns (`owlmeans:` scheduler ids) that it does not. Every
|
|
109
|
+
process listening to one queue must load the same schedule list; during a rolling deploy the
|
|
110
|
+
newest process to start wins, and the old pods leave the broker alone after their own start.
|
|
111
|
+
- **Removing a schedule is deleting its declaration.** The next worker start removes the scheduler.
|
|
112
|
+
So is a declaration whose job name the queue no longer accepts, or whose `endDate` has passed.
|
|
113
|
+
- **Scheduling never costs consumption.** A refused declaration or a broker failure while
|
|
114
|
+
reconciling is logged and skipped; the worker keeps consuming. Look for
|
|
115
|
+
`redis-queue-schedules:<queue>` in the worker's log when a schedule does not appear.
|
|
116
|
+
|
|
117
|
+
## Diagnosing "it never ran"
|
|
118
|
+
|
|
119
|
+
1. Is some process listening to the queue (`listenQueues`) and is its worker up?
|
|
120
|
+
2. Did that process's start log a `redis-queue-schedules:<queue>` refusal?
|
|
121
|
+
3. Does `new Queue(queue, { connection, prefix }).getJobSchedulers()` list `owlmeans:<id>` with the
|
|
122
|
+
expected `pattern`/`every`, `tz` and `next`?
|
|
123
|
+
4. Is a processor registered for the job name? A run without one fails once as `UnknownJobName`
|
|
124
|
+
and shows in the failed state.
|
|
125
|
+
5. Is a previous run still active on a `concurrency: 1` queue?
|
|
126
|
+
|
|
127
|
+
## Testing
|
|
128
|
+
|
|
129
|
+
- **The declaration** is category A in the application's own shared package: build the config,
|
|
130
|
+
`assertSchedules(cfg)`, and pin ids and job names against the queue declaration.
|
|
131
|
+
- **The processor** is tested as a function over a fake `JobContext` or a real context — it is
|
|
132
|
+
ordinary code with a `touch`.
|
|
133
|
+
- **Against a broker** (Redis-gated): the worker's Ready-stage start is not awaited by
|
|
134
|
+
`context.init()`, so poll the broker (`getJobSchedulers`) or wait for the first run instead of
|
|
135
|
+
asserting straight after boot. A queue's schedulers go with `obliterate()`.
|
|
136
|
+
|
|
137
|
+
## External docs
|
|
138
|
+
|
|
139
|
+
- https://docs.bullmq.io/guide/job-schedulers — `upsertJobScheduler(id, { every | pattern, tz,
|
|
140
|
+
startDate, endDate, limit, immediately }, { name, data, opts })` creates or updates one scheduler
|
|
141
|
+
per id and replaces its pending run; `getJobSchedulers()` / `removeJobScheduler(id)`. The next job
|
|
142
|
+
is added when the previous one starts processing, so a worker must be consuming. Produced jobs
|
|
143
|
+
carry `repeatJobKey` = the scheduler id.
|
|
144
|
+
|
|
145
|
+
## Related
|
|
146
|
+
|
|
147
|
+
- `queue` — `declareSchedule`, `JobContext.scheduled`, processor rules
|
|
148
|
+
- `redis-queue` — `syncSchedules`, the `owlmeans:` ownership prefix, the reconciliation rules
|
|
149
|
+
- `resource-choice` — whether recurring work belongs on a queue at all
|
package/build/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Config, JobOptions, QueueDeclaration, QueueWorkerOptions } from './types.js';
|
|
1
|
+
import type { Config, JobOptions, QueueDeclaration, QueueWorkerOptions, ScheduleDeclaration } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Add a queue to the shared configuration. Every process in the monorepo loads the same
|
|
4
4
|
* declarations — a producer needs them to know a queue exists and what it accepts, a worker needs
|
|
@@ -19,4 +19,32 @@ export declare const listenQueues: <C extends Config>(cfg: C, ...names: string[]
|
|
|
19
19
|
export declare const queueOf: <C extends Config>(cfg: C, name: string) => QueueDeclaration;
|
|
20
20
|
export declare const queueOfJob: <C extends Config>(cfg: C, job: string) => QueueDeclaration | undefined;
|
|
21
21
|
export declare const isListening: <C extends Config>(cfg: C, name: string) => boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the broker can carry a schedule out as written. Nothing is recorded or changed.
|
|
24
|
+
*
|
|
25
|
+
* @throws {ScheduleMisdeclared} naming the schedule id and the reason.
|
|
26
|
+
* @throws {UnknownQueue} when the schedule's queue is not declared.
|
|
27
|
+
* @throws {UnknownJobName} when that queue does not accept the schedule's job name — a scheduled
|
|
28
|
+
* run of a name nothing declares would fail on every tick instead of once, at declaration.
|
|
29
|
+
*/
|
|
30
|
+
export declare const assertSchedule: <C extends Config>(cfg: C, schedule: ScheduleDeclaration) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Add a recurring job to the shared configuration. Declare the queue first — a schedule is checked
|
|
33
|
+
* against it here. Declaring an id twice replaces, like `declareQueue`.
|
|
34
|
+
*
|
|
35
|
+
* Declaring is all this does: the driver creates, updates and removes the broker's schedulers when
|
|
36
|
+
* a worker that listens to the queue starts.
|
|
37
|
+
*
|
|
38
|
+
* @throws {ScheduleMisdeclared} / {UnknownQueue} / {UnknownJobName} — see `assertSchedule`.
|
|
39
|
+
*/
|
|
40
|
+
export declare const declareSchedule: <C extends Config>(cfg: C, schedule: ScheduleDeclaration) => C;
|
|
41
|
+
/** The declared schedules, of one queue when it is named. */
|
|
42
|
+
export declare const schedulesOf: <C extends Config>(cfg: C, queue?: string) => ScheduleDeclaration[];
|
|
43
|
+
/**
|
|
44
|
+
* Check every declared schedule, including any written into the configuration without
|
|
45
|
+
* `declareSchedule`, and refuse two under one id.
|
|
46
|
+
*
|
|
47
|
+
* @throws {ScheduleMisdeclared} / {UnknownQueue} / {UnknownJobName} for the first one that fails.
|
|
48
|
+
*/
|
|
49
|
+
export declare const assertSchedules: <C extends Config>(cfg: C) => void;
|
|
22
50
|
//# sourceMappingURL=config.d.ts.map
|
package/build/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EAAE,UAAU,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EAC9E,MAAM,YAAY,CAAA;AAGnB;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,MAAM,OACtC,CAAC,QAAQ,MAAM,QAAQ,MAAM,EAAE,SAAS;IAAE,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,EAAE,UAAU,CAAA;CAAE,KAClG,CAcF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,MAAM,OAAO,CAAC,YAAY,MAAM,EAAE,KAAG,CAM3E,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,OAAO,GAAI,CAAC,SAAS,MAAM,OAAO,CAAC,QAAQ,MAAM,KAAG,gBAQhE,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,MAAM,OAAO,CAAC,OAAO,MAAM,KAAG,gBAAgB,GAAG,SACpB,CAAA;AAElE,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,MAAM,OAAO,CAAC,QAAQ,MAAM,KAAG,OACzB,CAAA;AAM5C;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,MAAM,OAAO,CAAC,YAAY,mBAAmB,KAAG,IAoDxF,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,MAAM,OAAO,CAAC,YAAY,mBAAmB,KAAG,CAezF,CAAA;AAED,6DAA6D;AAC7D,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,MAAM,OAAO,CAAC,UAAU,MAAM,KAAG,mBAAmB,EACE,CAAA;AAE5F;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,MAAM,OAAO,CAAC,KAAG,IAS1D,CAAA"}
|
package/build/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UnknownQueue } from './errors.js';
|
|
1
|
+
import { ScheduleMisdeclared, UnknownJobName, UnknownQueue } from './errors.js';
|
|
2
2
|
/**
|
|
3
3
|
* Add a queue to the shared configuration. Every process in the monorepo loads the same
|
|
4
4
|
* declarations — a producer needs them to know a queue exists and what it accepts, a worker needs
|
|
@@ -38,4 +38,104 @@ export const queueOf = (cfg, name) => {
|
|
|
38
38
|
};
|
|
39
39
|
export const queueOfJob = (cfg, job) => cfg.queue?.queues?.find(declared => declared.jobs.includes(job));
|
|
40
40
|
export const isListening = (cfg, name) => cfg.queue?.listen?.includes(name) === true;
|
|
41
|
+
/** A date option the broker can read — a `Date`, epoch milliseconds or a parsable string. */
|
|
42
|
+
const isInstant = (value) => Number.isFinite((value instanceof Date ? value : new Date(value)).getTime());
|
|
43
|
+
/**
|
|
44
|
+
* Whether the broker can carry a schedule out as written. Nothing is recorded or changed.
|
|
45
|
+
*
|
|
46
|
+
* @throws {ScheduleMisdeclared} naming the schedule id and the reason.
|
|
47
|
+
* @throws {UnknownQueue} when the schedule's queue is not declared.
|
|
48
|
+
* @throws {UnknownJobName} when that queue does not accept the schedule's job name — a scheduled
|
|
49
|
+
* run of a name nothing declares would fail on every tick instead of once, at declaration.
|
|
50
|
+
*/
|
|
51
|
+
export const assertSchedule = (cfg, schedule) => {
|
|
52
|
+
const { id } = schedule;
|
|
53
|
+
if (typeof id !== 'string' || id.trim() === '') {
|
|
54
|
+
throw new ScheduleMisdeclared('(empty):id');
|
|
55
|
+
}
|
|
56
|
+
const queue = queueOf(cfg, schedule.queue);
|
|
57
|
+
if (!queue.jobs.includes(schedule.name)) {
|
|
58
|
+
throw new UnknownJobName(`${schedule.queue}:${schedule.name}`);
|
|
59
|
+
}
|
|
60
|
+
const every = schedule.every != null;
|
|
61
|
+
const pattern = schedule.pattern != null;
|
|
62
|
+
if (every === pattern) {
|
|
63
|
+
throw new ScheduleMisdeclared(`${id}:every-or-pattern`);
|
|
64
|
+
}
|
|
65
|
+
if (every && !(Number.isSafeInteger(schedule.every) && schedule.every > 0)) {
|
|
66
|
+
throw new ScheduleMisdeclared(`${id}:every`);
|
|
67
|
+
}
|
|
68
|
+
if (pattern && (typeof schedule.pattern !== 'string' || schedule.pattern.trim() === '')) {
|
|
69
|
+
throw new ScheduleMisdeclared(`${id}:pattern`);
|
|
70
|
+
}
|
|
71
|
+
// Both only mean something to a cron pattern: an interval has no wall clock to read in a time
|
|
72
|
+
// zone, and its first run is immediate already.
|
|
73
|
+
if (!pattern && schedule.tz != null) {
|
|
74
|
+
throw new ScheduleMisdeclared(`${id}:tz`);
|
|
75
|
+
}
|
|
76
|
+
if (!pattern && schedule.immediately === true) {
|
|
77
|
+
throw new ScheduleMisdeclared(`${id}:immediately`);
|
|
78
|
+
}
|
|
79
|
+
if (schedule.immediately === true && schedule.startDate != null) {
|
|
80
|
+
throw new ScheduleMisdeclared(`${id}:immediately-start-date`);
|
|
81
|
+
}
|
|
82
|
+
if (schedule.limit != null && !(Number.isSafeInteger(schedule.limit) && schedule.limit > 0)) {
|
|
83
|
+
throw new ScheduleMisdeclared(`${id}:limit`);
|
|
84
|
+
}
|
|
85
|
+
if (schedule.startDate != null && !isInstant(schedule.startDate)) {
|
|
86
|
+
throw new ScheduleMisdeclared(`${id}:start-date`);
|
|
87
|
+
}
|
|
88
|
+
if (schedule.endDate != null && !isInstant(schedule.endDate)) {
|
|
89
|
+
throw new ScheduleMisdeclared(`${id}:end-date`);
|
|
90
|
+
}
|
|
91
|
+
// The type already omits both; a declaration assembled at runtime still reaches this check. The
|
|
92
|
+
// broker names every run itself and places it on the schedule, so either would be overridden.
|
|
93
|
+
const opts = schedule.opts;
|
|
94
|
+
if (opts?.id != null) {
|
|
95
|
+
throw new ScheduleMisdeclared(`${id}:opts-id`);
|
|
96
|
+
}
|
|
97
|
+
if (opts?.delay != null) {
|
|
98
|
+
throw new ScheduleMisdeclared(`${id}:opts-delay`);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Add a recurring job to the shared configuration. Declare the queue first — a schedule is checked
|
|
103
|
+
* against it here. Declaring an id twice replaces, like `declareQueue`.
|
|
104
|
+
*
|
|
105
|
+
* Declaring is all this does: the driver creates, updates and removes the broker's schedulers when
|
|
106
|
+
* a worker that listens to the queue starts.
|
|
107
|
+
*
|
|
108
|
+
* @throws {ScheduleMisdeclared} / {UnknownQueue} / {UnknownJobName} — see `assertSchedule`.
|
|
109
|
+
*/
|
|
110
|
+
export const declareSchedule = (cfg, schedule) => {
|
|
111
|
+
assertSchedule(cfg, schedule);
|
|
112
|
+
const schedules = cfg.queue?.schedules ?? [];
|
|
113
|
+
const existing = schedules.findIndex(declared => declared.id === schedule.id);
|
|
114
|
+
if (existing < 0) {
|
|
115
|
+
schedules.push(schedule);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
schedules[existing] = schedule;
|
|
119
|
+
}
|
|
120
|
+
cfg.queue = { ...cfg.queue, schedules };
|
|
121
|
+
return cfg;
|
|
122
|
+
};
|
|
123
|
+
/** The declared schedules, of one queue when it is named. */
|
|
124
|
+
export const schedulesOf = (cfg, queue) => (cfg.queue?.schedules ?? []).filter(schedule => queue == null || schedule.queue === queue);
|
|
125
|
+
/**
|
|
126
|
+
* Check every declared schedule, including any written into the configuration without
|
|
127
|
+
* `declareSchedule`, and refuse two under one id.
|
|
128
|
+
*
|
|
129
|
+
* @throws {ScheduleMisdeclared} / {UnknownQueue} / {UnknownJobName} for the first one that fails.
|
|
130
|
+
*/
|
|
131
|
+
export const assertSchedules = (cfg) => {
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
for (const schedule of schedulesOf(cfg)) {
|
|
134
|
+
assertSchedule(cfg, schedule);
|
|
135
|
+
if (seen.has(schedule.id)) {
|
|
136
|
+
throw new ScheduleMisdeclared(`${schedule.id}:duplicate`);
|
|
137
|
+
}
|
|
138
|
+
seen.add(schedule.id);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
41
141
|
//# sourceMappingURL=config.js.map
|
package/build/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/build/errors.d.ts
CHANGED
|
@@ -56,4 +56,13 @@ export declare class EnvelopeExpired extends QueueError {
|
|
|
56
56
|
static typeName: string;
|
|
57
57
|
constructor(msg: string);
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* A schedule the broker could not carry out as written: no id, both or neither of `every` and
|
|
61
|
+
* `pattern`, an option that only means something for the other one, or a job option the broker
|
|
62
|
+
* owns for a scheduled run. The message names the schedule id and the reason.
|
|
63
|
+
*/
|
|
64
|
+
export declare class ScheduleMisdeclared extends QueueError {
|
|
65
|
+
static typeName: string;
|
|
66
|
+
constructor(msg: string);
|
|
67
|
+
}
|
|
59
68
|
//# sourceMappingURL=errors.d.ts.map
|