@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.
Files changed (85) hide show
  1. package/README.md +122 -16
  2. package/dist/BayProvider.d.ts +39 -11
  3. package/dist/BayProvider.d.ts.map +1 -1
  4. package/dist/BayProvider.js +35 -13
  5. package/dist/BayProvider.js.map +1 -1
  6. package/dist/Job.d.ts +99 -0
  7. package/dist/Job.d.ts.map +1 -0
  8. package/dist/Job.js +78 -0
  9. package/dist/Job.js.map +1 -0
  10. package/dist/QueueManager.d.ts +140 -21
  11. package/dist/QueueManager.d.ts.map +1 -1
  12. package/dist/QueueManager.js +247 -53
  13. package/dist/QueueManager.js.map +1 -1
  14. package/dist/adapters.d.ts +68 -0
  15. package/dist/adapters.d.ts.map +1 -0
  16. package/dist/adapters.js +56 -0
  17. package/dist/adapters.js.map +1 -0
  18. package/dist/augmentations.d.ts +28 -0
  19. package/dist/augmentations.d.ts.map +1 -0
  20. package/dist/augmentations.js +17 -0
  21. package/dist/augmentations.js.map +1 -0
  22. package/dist/configure.d.ts +1 -0
  23. package/dist/configure.d.ts.map +1 -1
  24. package/dist/configure.js +24 -7
  25. package/dist/configure.js.map +1 -1
  26. package/dist/console/contract.d.ts +60 -0
  27. package/dist/console/contract.d.ts.map +1 -0
  28. package/dist/console/contract.js +36 -0
  29. package/dist/console/contract.js.map +1 -0
  30. package/dist/console/index.d.ts +29 -0
  31. package/dist/console/index.d.ts.map +1 -0
  32. package/dist/console/index.js +45 -0
  33. package/dist/console/index.js.map +1 -0
  34. package/dist/console/makeJob.d.ts +32 -0
  35. package/dist/console/makeJob.d.ts.map +1 -0
  36. package/dist/console/makeJob.js +118 -0
  37. package/dist/console/makeJob.js.map +1 -0
  38. package/dist/console/queueWork.d.ts +18 -0
  39. package/dist/console/queueWork.d.ts.map +1 -0
  40. package/dist/console/queueWork.js +58 -0
  41. package/dist/console/queueWork.js.map +1 -0
  42. package/dist/drivers/MemoryDriver.d.ts +14 -8
  43. package/dist/drivers/MemoryDriver.d.ts.map +1 -1
  44. package/dist/drivers/MemoryDriver.js +61 -7
  45. package/dist/drivers/MemoryDriver.js.map +1 -1
  46. package/dist/drivers/RedisDriver.d.ts +60 -8
  47. package/dist/drivers/RedisDriver.d.ts.map +1 -1
  48. package/dist/drivers/RedisDriver.js +209 -29
  49. package/dist/drivers/RedisDriver.js.map +1 -1
  50. package/dist/index.d.ts +10 -6
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +8 -4
  53. package/dist/index.js.map +1 -1
  54. package/dist/jobs.d.ts +43 -0
  55. package/dist/jobs.d.ts.map +1 -0
  56. package/dist/jobs.js +105 -0
  57. package/dist/jobs.js.map +1 -0
  58. package/dist/quasar.d.ts +1 -1
  59. package/dist/quasar.js +1 -1
  60. package/dist/testing/FakeQueue.d.ts +15 -9
  61. package/dist/testing/FakeQueue.d.ts.map +1 -1
  62. package/dist/testing/FakeQueue.js +13 -3
  63. package/dist/testing/FakeQueue.js.map +1 -1
  64. package/package.json +5 -3
  65. package/src/BayProvider.ts +79 -26
  66. package/src/Job.ts +137 -0
  67. package/src/QueueManager.ts +411 -56
  68. package/src/adapters.ts +75 -0
  69. package/src/augmentations.ts +31 -0
  70. package/src/configure.ts +25 -7
  71. package/src/console/contract.ts +94 -0
  72. package/src/console/index.ts +68 -0
  73. package/src/console/makeJob.ts +139 -0
  74. package/src/console/queueWork.ts +70 -0
  75. package/src/drivers/MemoryDriver.ts +66 -14
  76. package/src/drivers/RedisDriver.ts +298 -42
  77. package/src/index.ts +35 -6
  78. package/src/jobs.ts +111 -0
  79. package/src/quasar.ts +1 -1
  80. package/src/testing/FakeQueue.ts +25 -15
  81. package/dist/stores.d.ts +0 -41
  82. package/dist/stores.d.ts.map +0 -1
  83. package/dist/stores.js +0 -46
  84. package/dist/stores.js.map +0 -1
  85. 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, stores } from '@c9up/bay'
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('QUEUE_STORE'),
37
- stores: {
38
- memory: stores.memory(),
39
- redis: stores.redis({ connection: 'main' }),
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.register('send-email', new SendEmailJob())
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
- | Store | Keeps jobs | Use it when |
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
- | `stores.memory()` | until the process exits | tests, and local work |
55
- | `stores.redis({ connection })` | in Redis | anything that must survive a restart |
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 store actually selected is built, so naming a Redis
58
- queue in a config that runs in memory opens no connection. A `default` that
59
- names nothing throws, listing what exists — falling back to memory would look
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
- `stores.redis` takes a `@c9up/quasar` connection name, resolved at first use so
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
- stores.redis({ connection: 'jobs', allowNonAtomicPop: true })
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
@@ -1,15 +1,22 @@
1
- import type { QueueStoreFactory } from "./stores.js";
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
- interface BayContainer {
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 store to use — a key of {@link stores}. Read from the
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 stores this application can use, by name. Each is a factory
28
- * from `stores.*`, built only when it is the one selected.
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
- stores?: Record<string, QueueStoreFactory>;
37
+ adapters?: Record<string, AdapterFactory>;
31
38
  /**
32
- * The single-store form, kept for configs written against it: only
33
- * `"memory"` was ever accepted. Prefer `default` + `stores`, which is how a
34
- * pluggable backend is configured everywhere else and what lets the
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":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD;;;;;GAKG;AACH,UAAU,YAAY;IACrB,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,UAAU,cAAc;IACvB,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,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC3C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC;CAClB;AAoED,MAAM,CAAC,OAAO,OAAO,WAAW;;IACnB,SAAS,CAAC,GAAG,EAAE,aAAa;gBAAlB,GAAG,EAAE,aAAa;IAExC,QAAQ,IAAI,IAAI;IAaV,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B;;;;;;OAMG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAS/B"}
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"}
@@ -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
- const stores = config?.stores;
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 (stores && name !== undefined) {
39
- const selected = stores[name];
44
+ if (adapters && name !== undefined) {
45
+ const selected = adapters[name];
40
46
  if (!selected) {
41
- const known = Object.keys(stores);
42
- throw new Error(`[bay] config.queue names the store '${name}', which is not in \`stores\`. ` +
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
- : "`stores` is empty — declare one with stores.memory() or stores.redis()."));
51
+ : `\`${key}\` is empty — declare one with drivers.memory() or drivers.redis().`));
46
52
  }
47
53
  return selected();
48
54
  }
49
- if (stores && name === undefined) {
50
- throw new Error("[bay] config.queue declares `stores` but no `default` naming which one to use. " +
51
- `Set default to one of: ${Object.keys(stores).join(", ")}.`);
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 \`stores\` instead: ` +
56
- "stores: { redis: stores.redis({ connection: 'main' }) }.");
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
- this.app.container.singleton("queue", () => this.app.container.resolve(QueueManager));
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.
@@ -1 +1 @@
1
- {"version":3,"file":"BayProvider.js","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"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"}
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
@@ -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"}