@c9up/bay 0.1.13 → 0.2.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/README.md +122 -16
- package/dist/BayProvider.d.ts +39 -11
- package/dist/BayProvider.d.ts.map +1 -1
- package/dist/BayProvider.js +35 -13
- package/dist/BayProvider.js.map +1 -1
- package/dist/Job.d.ts +99 -0
- package/dist/Job.d.ts.map +1 -0
- package/dist/Job.js +78 -0
- package/dist/Job.js.map +1 -0
- package/dist/QueueManager.d.ts +140 -21
- package/dist/QueueManager.d.ts.map +1 -1
- package/dist/QueueManager.js +247 -53
- package/dist/QueueManager.js.map +1 -1
- package/dist/adapters.d.ts +68 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +56 -0
- package/dist/adapters.js.map +1 -0
- package/dist/augmentations.d.ts +28 -0
- package/dist/augmentations.d.ts.map +1 -0
- package/dist/augmentations.js +17 -0
- package/dist/augmentations.js.map +1 -0
- package/dist/configure.d.ts +1 -0
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +24 -7
- package/dist/configure.js.map +1 -1
- package/dist/console/contract.d.ts +60 -0
- package/dist/console/contract.d.ts.map +1 -0
- package/dist/console/contract.js +36 -0
- package/dist/console/contract.js.map +1 -0
- package/dist/console/index.d.ts +29 -0
- package/dist/console/index.d.ts.map +1 -0
- package/dist/console/index.js +45 -0
- package/dist/console/index.js.map +1 -0
- package/dist/console/makeJob.d.ts +32 -0
- package/dist/console/makeJob.d.ts.map +1 -0
- package/dist/console/makeJob.js +118 -0
- package/dist/console/makeJob.js.map +1 -0
- package/dist/console/queueWork.d.ts +18 -0
- package/dist/console/queueWork.d.ts.map +1 -0
- package/dist/console/queueWork.js +58 -0
- package/dist/console/queueWork.js.map +1 -0
- package/dist/drivers/MemoryDriver.d.ts +14 -8
- package/dist/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/drivers/MemoryDriver.js +61 -7
- package/dist/drivers/MemoryDriver.js.map +1 -1
- package/dist/drivers/RedisDriver.d.ts +60 -8
- package/dist/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/drivers/RedisDriver.js +209 -29
- package/dist/drivers/RedisDriver.js.map +1 -1
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/jobs.d.ts +43 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +105 -0
- package/dist/jobs.js.map +1 -0
- package/dist/quasar.d.ts +1 -1
- package/dist/quasar.js +1 -1
- package/dist/testing/FakeQueue.d.ts +15 -9
- package/dist/testing/FakeQueue.d.ts.map +1 -1
- package/dist/testing/FakeQueue.js +13 -3
- package/dist/testing/FakeQueue.js.map +1 -1
- package/package.json +5 -3
- package/src/BayProvider.ts +79 -26
- package/src/Job.ts +137 -0
- package/src/QueueManager.ts +411 -56
- package/src/adapters.ts +75 -0
- package/src/augmentations.ts +31 -0
- package/src/configure.ts +25 -7
- package/src/console/contract.ts +94 -0
- package/src/console/index.ts +68 -0
- package/src/console/makeJob.ts +139 -0
- package/src/console/queueWork.ts +70 -0
- package/src/drivers/MemoryDriver.ts +66 -14
- package/src/drivers/RedisDriver.ts +298 -42
- package/src/index.ts +35 -6
- package/src/jobs.ts +111 -0
- package/src/quasar.ts +1 -1
- package/src/testing/FakeQueue.ts +25 -15
- package/dist/stores.d.ts +0 -41
- package/dist/stores.d.ts.map +0 -1
- package/dist/stores.js +0 -46
- package/dist/stores.js.map +0 -1
- package/src/stores.ts +0 -59
package/README.md
CHANGED
|
@@ -29,37 +29,112 @@ providers: [
|
|
|
29
29
|
|
|
30
30
|
```ts
|
|
31
31
|
// config/queue.ts
|
|
32
|
-
import { defineConfig,
|
|
32
|
+
import { defineConfig, drivers } from '@c9up/bay'
|
|
33
33
|
import env from '#start/env'
|
|
34
34
|
|
|
35
35
|
export default defineConfig({
|
|
36
|
-
default: env.get('
|
|
37
|
-
|
|
38
|
-
memory:
|
|
39
|
-
redis:
|
|
36
|
+
default: env.get('QUEUE_DRIVER'),
|
|
37
|
+
adapters: {
|
|
38
|
+
memory: drivers.memory(),
|
|
39
|
+
redis: drivers.redis({ connection: 'main' }),
|
|
40
40
|
},
|
|
41
41
|
})
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
The keys are the framework's: `default` + `adapters`, filled from a `drivers`
|
|
45
|
+
namespace, selected by `QUEUE_DRIVER`. Bay used to say `stores` / `QUEUE_STORE`;
|
|
46
|
+
both names still resolve, so an existing `config/queue.ts` keeps working.
|
|
47
|
+
|
|
48
|
+
## A job is a class
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// app/jobs/send_email.ts — ream make:job SendEmail
|
|
52
|
+
import { Job } from '@c9up/bay'
|
|
53
|
+
import type { JobOptions } from '@c9up/bay'
|
|
54
|
+
|
|
55
|
+
export default class SendEmail extends Job<{ to: string }> {
|
|
56
|
+
static options: JobOptions = {
|
|
57
|
+
queue: 'emails', // which queue it waits in
|
|
58
|
+
maxRetries: 5, // how many times the handler may run
|
|
59
|
+
delay: '10s', // hold it before any worker may take it
|
|
60
|
+
timeout: '1m', // how long the handler gets
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async execute() {
|
|
64
|
+
await mail.send(this.payload.to)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async failed(error: Error) {
|
|
68
|
+
// Once the last attempt has failed — the alert, not the retry.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
44
73
|
```ts
|
|
45
|
-
// start/queue.ts
|
|
46
74
|
import queue from '@c9up/bay/services/main'
|
|
47
75
|
|
|
48
|
-
queue.
|
|
76
|
+
await queue.dispatch(SendEmail, { to: 'user@example.com' })
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The class carries its own name, its options and the type of the payload it
|
|
80
|
+
reads, so a field the handler reads cannot be one the dispatcher never sent. The
|
|
81
|
+
call site overrides any of them: `dispatch(SendEmail, payload, { queue: 'critical' })`.
|
|
82
|
+
|
|
83
|
+
Registering by name still works, and is what a job whose name is computed at
|
|
84
|
+
runtime needs:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
queue.register('send-email', new SendEmailHandler())
|
|
49
88
|
await queue.dispatch('send-email', { to: 'user@example.com' })
|
|
50
89
|
```
|
|
51
90
|
|
|
52
|
-
|
|
91
|
+
## Workers
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
ream queue:work # the default queue, one at a time
|
|
95
|
+
ream queue:work --queue=critical,default # in that order of preference
|
|
96
|
+
ream queue:work --concurrency=10 # ten in flight
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// or in process
|
|
101
|
+
await queue.work({ queues: ['emails'], concurrency: 4 })
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Naming the queues is what keeps a slow one from starving a fast one: run a
|
|
105
|
+
worker for `emails` and another for `default` rather than one worker taking
|
|
106
|
+
whatever comes. `concurrency` is one by default, which is safe and a poor
|
|
107
|
+
default for anything that waits on the network.
|
|
108
|
+
|
|
109
|
+
A `timeout` fails the attempt; it does not kill the handler, because nothing in
|
|
110
|
+
Node can interrupt a running promise. What it buys is that the **worker** stops
|
|
111
|
+
waiting — otherwise one stuck job costs the whole worker, which never picks
|
|
112
|
+
anything up again.
|
|
113
|
+
|
|
114
|
+
The `worker` block carries the framework's names for what a worker does between
|
|
115
|
+
jobs — `idleDelay` (how long it waits after finding nothing, 2 s),
|
|
116
|
+
`stalledInterval` (how often it reclaims what a crashed worker left behind,
|
|
117
|
+
30 s), `concurrency` and `queues`. An argument to `work()` beats the block, and
|
|
118
|
+
the block beats the defaults.
|
|
119
|
+
|
|
120
|
+
`locations` says where the job classes live (`['app/jobs']` by default). Every
|
|
121
|
+
module under them is imported at boot and a default export that is a job class
|
|
122
|
+
is registered under its own name — which is what lets a **worker** process
|
|
123
|
+
resolve a record queued by an **HTTP** one. Without it the registration list is
|
|
124
|
+
a directory kept in step by hand, and the job nobody added to it fails as "no
|
|
125
|
+
handler registered".
|
|
126
|
+
|
|
127
|
+
| Adapter | Keeps jobs | Use it when |
|
|
53
128
|
| --- | --- | --- |
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
129
|
+
| `drivers.memory()` | until the process exits | tests, and local work |
|
|
130
|
+
| `drivers.redis({ connection })` | in Redis | anything that must survive a restart |
|
|
56
131
|
|
|
57
|
-
Factories are lazy: only the
|
|
58
|
-
queue in a config that runs in memory opens no connection. A `default`
|
|
59
|
-
names nothing throws, listing what exists — falling back to memory would
|
|
60
|
-
like it worked until a restart dropped every pending job.
|
|
132
|
+
Factories are lazy: only the adapter actually selected is built, so naming a
|
|
133
|
+
Redis queue in a config that runs in memory opens no connection. A `default`
|
|
134
|
+
that names nothing throws, listing what exists — falling back to memory would
|
|
135
|
+
look like it worked until a restart dropped every pending job.
|
|
61
136
|
|
|
62
|
-
`
|
|
137
|
+
`drivers.redis` takes a `@c9up/quasar` connection name, resolved at first use so
|
|
63
138
|
bay never imports quasar, which stays an optional peer. Pass an ioredis-shaped
|
|
64
139
|
client (or a function answering one) to use any other.
|
|
65
140
|
|
|
@@ -79,7 +154,7 @@ which two ways out there are:
|
|
|
79
154
|
|
|
80
155
|
```ts
|
|
81
156
|
// Either upgrade the server, or state that losing a job is acceptable here:
|
|
82
|
-
|
|
157
|
+
drivers.redis({ connection: 'jobs', allowNonAtomicPop: true })
|
|
83
158
|
```
|
|
84
159
|
|
|
85
160
|
The opt-in is honoured and still logs a warning on every process that starts
|
|
@@ -87,6 +162,37 @@ with it, naming production — agreeing once in a config file is not the same as
|
|
|
87
162
|
being reminded, in the logs of an incident, that this is how the process was
|
|
88
163
|
running. Outside production the fallback simply warns.
|
|
89
164
|
|
|
165
|
+
## A worker that dies, and a handler that is merely slow
|
|
166
|
+
|
|
167
|
+
A job taken off the queue is held under a **lease** — `visibilityTimeoutMs`,
|
|
168
|
+
30 s by default. While the lease is alive the job belongs to the worker holding
|
|
169
|
+
it; once it expires, `recoverStale()` puts the job back in pending, which is how
|
|
170
|
+
a crashed worker's job gets run at all.
|
|
171
|
+
|
|
172
|
+
Two things follow, and both are handled rather than left to the deployment:
|
|
173
|
+
|
|
174
|
+
**A slow handler is not a dead worker.** For as long as a handler runs, the
|
|
175
|
+
worker renews its own lease — half the timeout, so one slow round-trip is not
|
|
176
|
+
enough to lose the job. Without that, any handler outliving 30 s was recovered
|
|
177
|
+
and re-delivered *while it was still running*, and the same job ran twice.
|
|
178
|
+
Renewal is refused once the lease is gone or has passed to another worker, so a
|
|
179
|
+
late heartbeat cannot resurrect somebody else's claim.
|
|
180
|
+
|
|
181
|
+
**A job that kills its worker is bounded.** A crash never reaches the failure
|
|
182
|
+
path, so `attempts` never moves and `maxAttempts` never applies — a job that
|
|
183
|
+
takes the process down with it was recovered forever. `maxStalledCount`
|
|
184
|
+
(default `1`) caps how many times a job may be reclaimed before it is filed as
|
|
185
|
+
failed instead.
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
drivers.redis({
|
|
189
|
+
connection: 'jobs',
|
|
190
|
+
visibilityTimeoutMs: 30_000, // how long a worker owns a job it took
|
|
191
|
+
maxStalledCount: 1, // reclaims allowed before the job is failed
|
|
192
|
+
maxFailedJobs: 1_000, // failed jobs kept (needs LTRIM on the client)
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
90
196
|
## Entry points
|
|
91
197
|
|
|
92
198
|
- `@c9up/bay` — main API
|
package/dist/BayProvider.d.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AdapterFactory } from "./adapters.js";
|
|
2
|
+
import "./augmentations.js";
|
|
3
|
+
import { type WorkerOptions } from "./QueueManager.js";
|
|
2
4
|
/**
|
|
3
5
|
* Slim, duck-typed host context — bay stays publishable without
|
|
4
6
|
* importing `@c9up/ream`. Any framework that exposes a Container with
|
|
5
7
|
* `singleton(token, factory)` + `resolve(token)` and a config store
|
|
6
8
|
* with `get(key)` satisfies the contract.
|
|
7
9
|
*/
|
|
8
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The slice of a host container bay uses. Exported because `BayAppContext` is:
|
|
12
|
+
* a host — or a test — cannot describe one without being able to name its
|
|
13
|
+
* members, and the two that could not were reached for with `as any` instead.
|
|
14
|
+
*/
|
|
15
|
+
export interface BayContainer {
|
|
9
16
|
singleton(token: unknown, factory: () => unknown): void;
|
|
10
17
|
resolve<T = unknown>(token: unknown): Promise<T>;
|
|
11
18
|
}
|
|
12
|
-
interface BayConfigStore {
|
|
19
|
+
export interface BayConfigStore {
|
|
13
20
|
get<T = unknown>(key: string): T | undefined;
|
|
14
21
|
}
|
|
15
22
|
export interface BayAppContext {
|
|
@@ -18,23 +25,45 @@ export interface BayAppContext {
|
|
|
18
25
|
}
|
|
19
26
|
export interface BayProviderConfig {
|
|
20
27
|
/**
|
|
21
|
-
* Which named
|
|
28
|
+
* Which named adapter to use — a key of {@link adapters}. Read from the
|
|
22
29
|
* environment in the generated config, so a deployment picks its queue
|
|
23
30
|
* backend without editing a file.
|
|
24
31
|
*/
|
|
25
32
|
default?: string;
|
|
26
33
|
/**
|
|
27
|
-
* The queue
|
|
28
|
-
* from `
|
|
34
|
+
* The queue adapters this application can use, by name. Each is a factory
|
|
35
|
+
* from `drivers.*`, built only when it is the one selected.
|
|
29
36
|
*/
|
|
30
|
-
|
|
37
|
+
adapters?: Record<string, AdapterFactory>;
|
|
31
38
|
/**
|
|
32
|
-
* The
|
|
33
|
-
*
|
|
34
|
-
*
|
|
39
|
+
* The name this key had before it matched upstream's. Read when `adapters`
|
|
40
|
+
* is absent, so a config written against the older spelling keeps selecting
|
|
41
|
+
* the backend it named — silently falling back to an in-process queue is the
|
|
42
|
+
* one outcome `buildDriver` exists to prevent.
|
|
43
|
+
*/
|
|
44
|
+
stores?: Record<string, AdapterFactory>;
|
|
45
|
+
/**
|
|
46
|
+
* The single-adapter form, kept for configs written against it: only
|
|
47
|
+
* `"memory"` was ever accepted. Prefer `default` + `adapters`, which is how
|
|
48
|
+
* a pluggable backend is configured everywhere else and what lets the
|
|
35
49
|
* environment choose.
|
|
36
50
|
*/
|
|
37
51
|
driver?: "memory";
|
|
52
|
+
/**
|
|
53
|
+
* Defaults for the worker `queue.work()` starts — upstream's `worker` block,
|
|
54
|
+
* by the names it gives them. An argument to `work()` still wins.
|
|
55
|
+
*/
|
|
56
|
+
worker?: WorkerOptions;
|
|
57
|
+
/**
|
|
58
|
+
* Where the application's job classes live. Default `['app/jobs']`.
|
|
59
|
+
*
|
|
60
|
+
* Every module under them is imported at boot and a default export that is
|
|
61
|
+
* a job class is registered under its own name — which is what lets a
|
|
62
|
+
* worker process resolve a record queued by an HTTP one. Without it the
|
|
63
|
+
* registration list is a directory kept in step by hand, and the job
|
|
64
|
+
* nobody added to it fails as "no handler registered".
|
|
65
|
+
*/
|
|
66
|
+
locations?: readonly string[];
|
|
38
67
|
}
|
|
39
68
|
export default class BayProvider {
|
|
40
69
|
#private;
|
|
@@ -51,5 +80,4 @@ export default class BayProvider {
|
|
|
51
80
|
*/
|
|
52
81
|
shutdown(): Promise<void>;
|
|
53
82
|
}
|
|
54
|
-
export {};
|
|
55
83
|
//# sourceMappingURL=BayProvider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BayProvider.d.ts","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"BayProvider.d.ts","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,oBAAoB,CAAC;AAS5B,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGrE;;;;;GAKG;AACH;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC5B,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,GAAG,IAAI,CAAC;IACxD,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACjD;AACD,MAAM,WAAW,cAAc;IAC9B,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;CAC7C;AACD,MAAM,WAAW,aAAa;IAC7B,SAAS,EAAE,YAAY,CAAC;IACxB,MAAM,EAAE,cAAc,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IACjC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC1C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACxC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAwED,MAAM,CAAC,OAAO,OAAO,WAAW;;IACnB,SAAS,CAAC,GAAG,EAAE,aAAa;gBAAlB,GAAG,EAAE,aAAa;IAExC,QAAQ,IAAI,IAAI;IAmBV,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB3B;;;;;;OAMG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAS/B"}
|
package/dist/BayProvider.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import "./augmentations.js";
|
|
1
2
|
import { MemoryDriver } from "./drivers/MemoryDriver.js";
|
|
3
|
+
import { DEFAULT_JOBS_DIR, directoryOf, discoverJobs, setJobsDir, } from "./jobs.js";
|
|
2
4
|
import { QueueManager } from "./QueueManager.js";
|
|
3
5
|
import { clearQueue, getQueue, setQueue } from "./services/main.js";
|
|
4
6
|
/**
|
|
@@ -33,27 +35,31 @@ import { clearQueue, getQueue, setQueue } from "./services/main.js";
|
|
|
33
35
|
* queue would only find out when a restart dropped every pending job.
|
|
34
36
|
*/
|
|
35
37
|
function buildDriver(config) {
|
|
36
|
-
|
|
38
|
+
// `adapters` first, `stores` when it is absent: the key was renamed to the
|
|
39
|
+
// one upstream reads, and a config that still says `stores` must keep
|
|
40
|
+
// selecting its backend rather than quietly landing on memory.
|
|
41
|
+
const adapters = config?.adapters ?? config?.stores;
|
|
42
|
+
const key = config?.adapters ? "adapters" : "stores";
|
|
37
43
|
const name = config?.default;
|
|
38
|
-
if (
|
|
39
|
-
const selected =
|
|
44
|
+
if (adapters && name !== undefined) {
|
|
45
|
+
const selected = adapters[name];
|
|
40
46
|
if (!selected) {
|
|
41
|
-
const known = Object.keys(
|
|
42
|
-
throw new Error(`[bay] config.queue names the
|
|
47
|
+
const known = Object.keys(adapters);
|
|
48
|
+
throw new Error(`[bay] config.queue names the adapter '${name}', which is not in \`${key}\`. ` +
|
|
43
49
|
(known.length > 0
|
|
44
50
|
? `Declared: ${known.join(", ")}.`
|
|
45
|
-
:
|
|
51
|
+
: `\`${key}\` is empty — declare one with drivers.memory() or drivers.redis().`));
|
|
46
52
|
}
|
|
47
53
|
return selected();
|
|
48
54
|
}
|
|
49
|
-
if (
|
|
50
|
-
throw new Error(
|
|
51
|
-
`Set default to one of: ${Object.keys(
|
|
55
|
+
if (adapters && name === undefined) {
|
|
56
|
+
throw new Error(`[bay] config.queue declares \`${key}\` but no \`default\` naming which one to use. ` +
|
|
57
|
+
`Set default to one of: ${Object.keys(adapters).join(", ")}.`);
|
|
52
58
|
}
|
|
53
59
|
const driverName = config?.driver ?? "memory";
|
|
54
60
|
if (driverName !== "memory") {
|
|
55
|
-
throw new Error(`[bay] Unsupported driver '${driverName}' — name it under \`
|
|
56
|
-
"
|
|
61
|
+
throw new Error(`[bay] Unsupported driver '${driverName}' — name it under \`adapters\` instead: ` +
|
|
62
|
+
"adapters: { redis: drivers.redis({ connection: 'main' }) }.");
|
|
57
63
|
}
|
|
58
64
|
return new MemoryDriver();
|
|
59
65
|
}
|
|
@@ -65,9 +71,16 @@ export default class BayProvider {
|
|
|
65
71
|
register() {
|
|
66
72
|
this.app.container.singleton(QueueManager, () => {
|
|
67
73
|
const config = this.app.config.get("queue");
|
|
68
|
-
return new QueueManager(buildDriver(config));
|
|
74
|
+
return new QueueManager(buildDriver(config), config?.worker);
|
|
69
75
|
});
|
|
70
|
-
|
|
76
|
+
// Namespaced by the package that owns it, the way upstream namespaces
|
|
77
|
+
// `lucid.db`, `auth.manager` and `drive.manager` by theirs. The bare
|
|
78
|
+
// token stays bound beside it: it is what every existing
|
|
79
|
+
// `container.make(...)` asks for, and a token is not worth breaking an
|
|
80
|
+
// application over.
|
|
81
|
+
const queue = () => this.app.container.resolve(QueueManager);
|
|
82
|
+
this.app.container.singleton("bay.queue", queue);
|
|
83
|
+
this.app.container.singleton("queue", queue);
|
|
71
84
|
}
|
|
72
85
|
/** The queue THIS provider booted — not whatever the module singleton holds. */
|
|
73
86
|
#queue;
|
|
@@ -76,6 +89,15 @@ export default class BayProvider {
|
|
|
76
89
|
// `import queue from '@c9up/bay/services/main'` from anywhere.
|
|
77
90
|
this.#queue = await this.app.container.resolve(QueueManager);
|
|
78
91
|
setQueue(this.#queue);
|
|
92
|
+
const config = this.app.config.get("queue");
|
|
93
|
+
const locations = config?.locations ?? [DEFAULT_JOBS_DIR];
|
|
94
|
+
// `make:job` writes where discovery reads, so the two cannot drift.
|
|
95
|
+
const first = locations[0];
|
|
96
|
+
if (first !== undefined)
|
|
97
|
+
setJobsDir(directoryOf(first));
|
|
98
|
+
for (const job of await discoverJobs(locations)) {
|
|
99
|
+
this.#queue.registerJob(job);
|
|
100
|
+
}
|
|
79
101
|
}
|
|
80
102
|
/**
|
|
81
103
|
* Stop the worker the app started, and let the job in flight finish.
|
package/dist/BayProvider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BayProvider.js","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"BayProvider.js","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"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"}
|
package/dist/Job.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A job as a class — what an application writes, and what `dispatch` takes.
|
|
3
|
+
*
|
|
4
|
+
* The queue accepted a registered name and a payload:
|
|
5
|
+
*
|
|
6
|
+
* queue.register('send-email', new SendEmailHandler())
|
|
7
|
+
* await queue.dispatch('send-email', { to: '…' })
|
|
8
|
+
*
|
|
9
|
+
* Two places to keep in step, and nothing tying the payload to the handler that
|
|
10
|
+
* reads it. A class carries its own name, its own options and its payload type:
|
|
11
|
+
*
|
|
12
|
+
* export default class SendEmail extends Job<{ to: string }> {
|
|
13
|
+
* static options: JobOptions = { queue: 'emails', maxRetries: 5 }
|
|
14
|
+
*
|
|
15
|
+
* async execute() {
|
|
16
|
+
* await mail.send(this.payload.to)
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* async failed(error: Error) {
|
|
20
|
+
* // after the last retry, not after each one
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* await queue.dispatch(SendEmail, { to: 'user@example.com' })
|
|
25
|
+
*
|
|
26
|
+
* Registering by name still works, and is what a job whose name is computed at
|
|
27
|
+
* runtime still needs.
|
|
28
|
+
*/
|
|
29
|
+
/** Milliseconds, or a duration the way a config file writes one. */
|
|
30
|
+
export type Duration = number | string;
|
|
31
|
+
/**
|
|
32
|
+
* What a job class declares about how it should be run.
|
|
33
|
+
*
|
|
34
|
+
* Every field is optional, and the defaults are the manager's: the `default`
|
|
35
|
+
* queue, three attempts, no delay and no timeout.
|
|
36
|
+
*/
|
|
37
|
+
export interface JobOptions {
|
|
38
|
+
/** Named queue this job waits in. Default `"default"`. */
|
|
39
|
+
queue?: string;
|
|
40
|
+
/**
|
|
41
|
+
* How many times the handler may run before the job is filed as failed.
|
|
42
|
+
* Default `3`. Counts runs, not retries: `1` means one attempt and no
|
|
43
|
+
* second chance.
|
|
44
|
+
*/
|
|
45
|
+
maxRetries?: number;
|
|
46
|
+
/** Hold the job for this long before any worker may take it. */
|
|
47
|
+
delay?: Duration;
|
|
48
|
+
/**
|
|
49
|
+
* How long the handler gets. Past it the attempt is a failure and the job
|
|
50
|
+
* retries or fails like any other.
|
|
51
|
+
*
|
|
52
|
+
* The handler is not killed — nothing in Node can interrupt a running
|
|
53
|
+
* promise — so a job that ignores its timeout goes on burning CPU. What the
|
|
54
|
+
* timeout buys is that the WORKER stops waiting for it, which is what a
|
|
55
|
+
* stuck job otherwise costs: a worker that never picks anything up again.
|
|
56
|
+
*/
|
|
57
|
+
timeout?: Duration;
|
|
58
|
+
}
|
|
59
|
+
/** The queue a job goes to when nothing names one. */
|
|
60
|
+
export declare const DEFAULT_QUEUE = "default";
|
|
61
|
+
/**
|
|
62
|
+
* A background job.
|
|
63
|
+
*
|
|
64
|
+
* `execute()` takes no argument: the payload is on the instance, typed by the
|
|
65
|
+
* class's own parameter, so a handler cannot read a field the dispatcher never
|
|
66
|
+
* sent.
|
|
67
|
+
*/
|
|
68
|
+
export declare abstract class Job<Payload = unknown> {
|
|
69
|
+
/** Overridden by a subclass to change queue, retries, delay or timeout. */
|
|
70
|
+
static options: JobOptions;
|
|
71
|
+
/** What `dispatch` was given, as the class declared it. */
|
|
72
|
+
readonly payload: Payload;
|
|
73
|
+
/** Do the work. Throwing is what makes the attempt fail. */
|
|
74
|
+
abstract execute(): Promise<void> | void;
|
|
75
|
+
/**
|
|
76
|
+
* Called once the last attempt has failed — for the cleanup or the alert,
|
|
77
|
+
* not for the retry. A throw here is reported and swallowed: the job is
|
|
78
|
+
* already failed, and failing to say so must not fail it twice.
|
|
79
|
+
*/
|
|
80
|
+
failed?(error: Error): Promise<void> | void;
|
|
81
|
+
}
|
|
82
|
+
/** A job class, as `dispatch` receives it. */
|
|
83
|
+
export interface JobClass<Payload = unknown> {
|
|
84
|
+
new (): Job<Payload>;
|
|
85
|
+
readonly name: string;
|
|
86
|
+
readonly options?: JobOptions;
|
|
87
|
+
}
|
|
88
|
+
/** Is this a job class rather than a name or a handler instance? */
|
|
89
|
+
export declare function isJobClass(value: unknown): value is JobClass;
|
|
90
|
+
/**
|
|
91
|
+
* Milliseconds from a number or a duration string.
|
|
92
|
+
*
|
|
93
|
+
* `'10s'`, `'1m'`, `'2h'`, `'500ms'`, `'1d'` — the spellings a config file
|
|
94
|
+
* uses. A number is already milliseconds. Anything else throws, rather than
|
|
95
|
+
* silently becoming `NaN` and then a job that never runs: a typo in `delay`
|
|
96
|
+
* would otherwise park the job forever with nothing to read about it.
|
|
97
|
+
*/
|
|
98
|
+
export declare function toMilliseconds(value: Duration, label: string): number;
|
|
99
|
+
//# sourceMappingURL=Job.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Job.d.ts","sourceRoot":"","sources":["../src/Job.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,oEAAoE;AACpE,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IAC1B,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,QAAQ,CAAC;CACnB;AAED,sDAAsD;AACtD,eAAO,MAAM,aAAa,YAAY,CAAC;AAEvC;;;;;;GAMG;AACH,8BAAsB,GAAG,CAAC,OAAO,GAAG,OAAO;IAC1C,2EAA2E;IAC3E,MAAM,CAAC,OAAO,EAAE,UAAU,CAAM;IAEhC,2DAA2D;IAC3D,SAAiB,OAAO,EAAE,OAAO,CAAC;IAElC,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAExC;;;;OAIG;IACH,MAAM,CAAC,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;CAC3C;AAED,8CAA8C;AAC9C,MAAM,WAAW,QAAQ,CAAC,OAAO,GAAG,OAAO;IAC1C,QAAQ,GAAG,CAAC,OAAO,CAAC,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC;CAC9B;AAED,oEAAoE;AACpE,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAM5D;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAuBrE"}
|
package/dist/Job.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A job as a class — what an application writes, and what `dispatch` takes.
|
|
3
|
+
*
|
|
4
|
+
* The queue accepted a registered name and a payload:
|
|
5
|
+
*
|
|
6
|
+
* queue.register('send-email', new SendEmailHandler())
|
|
7
|
+
* await queue.dispatch('send-email', { to: '…' })
|
|
8
|
+
*
|
|
9
|
+
* Two places to keep in step, and nothing tying the payload to the handler that
|
|
10
|
+
* reads it. A class carries its own name, its own options and its payload type:
|
|
11
|
+
*
|
|
12
|
+
* export default class SendEmail extends Job<{ to: string }> {
|
|
13
|
+
* static options: JobOptions = { queue: 'emails', maxRetries: 5 }
|
|
14
|
+
*
|
|
15
|
+
* async execute() {
|
|
16
|
+
* await mail.send(this.payload.to)
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* async failed(error: Error) {
|
|
20
|
+
* // after the last retry, not after each one
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* await queue.dispatch(SendEmail, { to: 'user@example.com' })
|
|
25
|
+
*
|
|
26
|
+
* Registering by name still works, and is what a job whose name is computed at
|
|
27
|
+
* runtime still needs.
|
|
28
|
+
*/
|
|
29
|
+
/** The queue a job goes to when nothing names one. */
|
|
30
|
+
export const DEFAULT_QUEUE = "default";
|
|
31
|
+
/**
|
|
32
|
+
* A background job.
|
|
33
|
+
*
|
|
34
|
+
* `execute()` takes no argument: the payload is on the instance, typed by the
|
|
35
|
+
* class's own parameter, so a handler cannot read a field the dispatcher never
|
|
36
|
+
* sent.
|
|
37
|
+
*/
|
|
38
|
+
export class Job {
|
|
39
|
+
/** Overridden by a subclass to change queue, retries, delay or timeout. */
|
|
40
|
+
static options = {};
|
|
41
|
+
}
|
|
42
|
+
/** Is this a job class rather than a name or a handler instance? */
|
|
43
|
+
export function isJobClass(value) {
|
|
44
|
+
return (typeof value === "function" &&
|
|
45
|
+
value.prototype instanceof Job &&
|
|
46
|
+
typeof Reflect.get(value, "name") === "string");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Milliseconds from a number or a duration string.
|
|
50
|
+
*
|
|
51
|
+
* `'10s'`, `'1m'`, `'2h'`, `'500ms'`, `'1d'` — the spellings a config file
|
|
52
|
+
* uses. A number is already milliseconds. Anything else throws, rather than
|
|
53
|
+
* silently becoming `NaN` and then a job that never runs: a typo in `delay`
|
|
54
|
+
* would otherwise park the job forever with nothing to read about it.
|
|
55
|
+
*/
|
|
56
|
+
export function toMilliseconds(value, label) {
|
|
57
|
+
if (typeof value === "number") {
|
|
58
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
59
|
+
throw new Error(`${label} must be a non-negative number of milliseconds`);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
const match = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/.exec(value.trim());
|
|
64
|
+
if (!match) {
|
|
65
|
+
throw new Error(`${label} must be a number of milliseconds or a duration like '10s', '1m', '2h' — got '${value}'`);
|
|
66
|
+
}
|
|
67
|
+
const amount = Number(match[1]);
|
|
68
|
+
const unit = match[2];
|
|
69
|
+
const scale = {
|
|
70
|
+
ms: 1,
|
|
71
|
+
s: 1_000,
|
|
72
|
+
m: 60_000,
|
|
73
|
+
h: 3_600_000,
|
|
74
|
+
d: 86_400_000,
|
|
75
|
+
};
|
|
76
|
+
return amount * (scale[unit ?? "ms"] ?? 1);
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=Job.js.map
|
package/dist/Job.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Job.js","sourceRoot":"","sources":["../src/Job.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAkCH,sDAAsD;AACtD,MAAM,CAAC,MAAM,aAAa,GAAG,SAAS,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,OAAgB,GAAG;IACxB,2EAA2E;IAC3E,MAAM,CAAC,OAAO,GAAe,EAAE,CAAC;;AAuBjC,oEAAoE;AACpE,MAAM,UAAU,UAAU,CAAC,KAAc;IACxC,OAAO,CACN,OAAO,KAAK,KAAK,UAAU;QAC3B,KAAK,CAAC,SAAS,YAAY,GAAG;QAC9B,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,QAAQ,CAC9C,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,KAAe,EAAE,KAAa;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,gDAAgD,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,KAAK,CAAC;IACd,CAAC;IACD,MAAM,KAAK,GAAG,kCAAkC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IACpE,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACd,GAAG,KAAK,iFAAiF,KAAK,GAAG,CACjG,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACtB,MAAM,KAAK,GAA2B;QACrC,EAAE,EAAE,CAAC;QACL,CAAC,EAAE,KAAK;QACR,CAAC,EAAE,MAAM;QACT,CAAC,EAAE,SAAS;QACZ,CAAC,EAAE,UAAU;KACb,CAAC;IACF,OAAO,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAC5C,CAAC"}
|