@c9up/bay 0.1.12 → 0.1.13

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 (42) hide show
  1. package/README.md +66 -1
  2. package/dist/BayProvider.d.ts +24 -30
  3. package/dist/BayProvider.d.ts.map +1 -1
  4. package/dist/BayProvider.js +58 -9
  5. package/dist/BayProvider.js.map +1 -1
  6. package/dist/QueueManager.d.ts +7 -1
  7. package/dist/QueueManager.d.ts.map +1 -1
  8. package/dist/QueueManager.js +49 -5
  9. package/dist/QueueManager.js.map +1 -1
  10. package/dist/configure.d.ts +18 -0
  11. package/dist/configure.d.ts.map +1 -0
  12. package/dist/configure.js +31 -0
  13. package/dist/configure.js.map +1 -0
  14. package/dist/drivers/RedisDriver.d.ts +6 -0
  15. package/dist/drivers/RedisDriver.d.ts.map +1 -1
  16. package/dist/drivers/RedisDriver.js +49 -17
  17. package/dist/drivers/RedisDriver.js.map +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/nodeEnv.d.ts +16 -0
  23. package/dist/nodeEnv.d.ts.map +1 -0
  24. package/dist/nodeEnv.js +32 -0
  25. package/dist/nodeEnv.js.map +1 -0
  26. package/dist/services/main.d.ts +5 -0
  27. package/dist/services/main.d.ts.map +1 -1
  28. package/dist/services/main.js +7 -0
  29. package/dist/services/main.js.map +1 -1
  30. package/dist/stores.d.ts +41 -0
  31. package/dist/stores.d.ts.map +1 -0
  32. package/dist/stores.js +46 -0
  33. package/dist/stores.js.map +1 -0
  34. package/package.json +5 -1
  35. package/src/BayProvider.ts +84 -19
  36. package/src/QueueManager.ts +48 -5
  37. package/src/configure.ts +45 -0
  38. package/src/drivers/RedisDriver.ts +69 -22
  39. package/src/index.ts +1 -0
  40. package/src/nodeEnv.ts +30 -0
  41. package/src/services/main.ts +8 -0
  42. package/src/stores.ts +59 -0
package/README.md CHANGED
@@ -11,9 +11,14 @@ pnpm add @c9up/bay
11
11
  ream configure @c9up/bay
12
12
  ```
13
13
 
14
+ `ream add @c9up/bay` installs it, registers the provider and writes
15
+ `config/queue.ts`. The rest of this page assumes that has run.
16
+
14
17
  ## Usage
15
18
 
16
- Register the provider in your app, then configure it under `config/queue.ts` — the provider reads `config.get('queue')`, so a `config/bay.ts` would be loaded under the key `bay` and never seen:
19
+ Register the provider, then name the queue backend in `config/queue.ts` the
20
+ provider reads `config.get('queue')`, so a `config/bay.ts` would be loaded under
21
+ the key `bay` and never seen:
17
22
 
18
23
  ```ts
19
24
  // reamrc.ts
@@ -22,6 +27,66 @@ providers: [
22
27
  ]
23
28
  ```
24
29
 
30
+ ```ts
31
+ // config/queue.ts
32
+ import { defineConfig, stores } from '@c9up/bay'
33
+ import env from '#start/env'
34
+
35
+ export default defineConfig({
36
+ default: env.get('QUEUE_STORE'),
37
+ stores: {
38
+ memory: stores.memory(),
39
+ redis: stores.redis({ connection: 'main' }),
40
+ },
41
+ })
42
+ ```
43
+
44
+ ```ts
45
+ // start/queue.ts
46
+ import queue from '@c9up/bay/services/main'
47
+
48
+ queue.register('send-email', new SendEmailJob())
49
+ await queue.dispatch('send-email', { to: 'user@example.com' })
50
+ ```
51
+
52
+ | Store | Keeps jobs | Use it when |
53
+ | --- | --- | --- |
54
+ | `stores.memory()` | until the process exits | tests, and local work |
55
+ | `stores.redis({ connection })` | in Redis | anything that must survive a restart |
56
+
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.
61
+
62
+ `stores.redis` takes a `@c9up/quasar` connection name, resolved at first use so
63
+ bay never imports quasar, which stays an optional peer. Pass an ioredis-shaped
64
+ client (or a function answering one) to use any other.
65
+
66
+ ## Delivery guarantee, and the one way to lose it
67
+
68
+ A job the queue accepted gets run: `pop()` moves it from pending to processing
69
+ in a single `LMOVE`, so a worker that dies mid-job leaves the job recoverable
70
+ rather than gone.
71
+
72
+ `LMOVE` needs **Redis 6.2 or later**. Without it the move is `lpop` then
73
+ `rpush`, and a crash between the two deletes the job from pending before it
74
+ reaches processing — nothing recovers it, because nothing knows it existed.
75
+ That turns at-least-once delivery into at-most-once.
76
+
77
+ So on an older Redis the driver **refuses to start in production**, and says
78
+ which two ways out there are:
79
+
80
+ ```ts
81
+ // Either upgrade the server, or state that losing a job is acceptable here:
82
+ stores.redis({ connection: 'jobs', allowNonAtomicPop: true })
83
+ ```
84
+
85
+ The opt-in is honoured and still logs a warning on every process that starts
86
+ with it, naming production — agreeing once in a config file is not the same as
87
+ being reminded, in the logs of an incident, that this is how the process was
88
+ running. Outside production the fallback simply warns.
89
+
25
90
  ## Entry points
26
91
 
27
92
  - `@c9up/bay` — main API
@@ -1,3 +1,4 @@
1
+ import type { QueueStoreFactory } from "./stores.js";
1
2
  /**
2
3
  * Slim, duck-typed host context — bay stays publishable without
3
4
  * importing `@c9up/ream`. Any framework that exposes a Container with
@@ -17,44 +18,37 @@ export interface BayAppContext {
17
18
  }
18
19
  export interface BayProviderConfig {
19
20
  /**
20
- * Driver to bind by default. The provider only auto-wires `"memory"`
21
- * (the default) it has no Redis connection to build a `RedisDriver`
22
- * from. For Redis, a custom driver, or a pre-built instance, wire
23
- * `QueueManager` directly in your app's startup and skip the provider;
24
- * the `services/main` singleton resolves whatever is registered.
25
- * Passing anything other than `"memory"` throws at boot.
26
- *
27
- * Default `"memory"`.
21
+ * Which named store to use a key of {@link stores}. Read from the
22
+ * environment in the generated config, so a deployment picks its queue
23
+ * backend without editing a file.
24
+ */
25
+ default?: string;
26
+ /**
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.
29
+ */
30
+ stores?: Record<string, QueueStoreFactory>;
31
+ /**
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
35
+ * environment choose.
28
36
  */
29
37
  driver?: "memory";
30
38
  }
31
- /**
32
- * BayProvider — registers a default in-memory `QueueManager` in the
33
- * host container so apps that don't need custom driver wiring can
34
- * `import queue from '@c9up/bay/services/main'` and dispatch
35
- * straight away. Job handlers are still registered manually via
36
- * `queue.register(name, handler)` — that's intrinsic to the queue
37
- * design (handlers are app-defined, not config-driven).
38
- *
39
- * Apps with non-trivial wiring (Redis driver, custom queue config)
40
- * can ignore this provider and bind their own `QueueManager` instance
41
- * in the container; the `services/main` proxy resolves whatever is
42
- * registered.
43
- *
44
- * // reamrc.ts
45
- * providers: [() => import('@c9up/bay/provider')]
46
- *
47
- * // start/queue.ts
48
- * import queue from '@c9up/bay/services/main'
49
- *
50
- * queue.register('send-email', new SendEmailJob())
51
- * await queue.dispatch('send-email', { to: 'user@example.com' })
52
- */
53
39
  export default class BayProvider {
40
+ #private;
54
41
  protected app: BayAppContext;
55
42
  constructor(app: BayAppContext);
56
43
  register(): void;
57
44
  boot(): Promise<void>;
45
+ /**
46
+ * Stop the worker the app started, and let the job in flight finish.
47
+ *
48
+ * A worker polls on a timer. Left running, it survives a dev reload, a test
49
+ * teardown and a SIGTERM — so the old process keeps pulling jobs the new
50
+ * one is also pulling, and the same job runs twice.
51
+ */
58
52
  shutdown(): Promise<void>;
59
53
  }
60
54
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"BayProvider.d.ts","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"AAIA;;;;;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;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,OAAO,OAAO,WAAW;IACnB,SAAS,CAAC,GAAG,EAAE,aAAa;gBAAlB,GAAG,EAAE,aAAa;IAExC,QAAQ,IAAI,IAAI;IAiBV,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAMrB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAC/B"}
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,6 +1,6 @@
1
1
  import { MemoryDriver } from "./drivers/MemoryDriver.js";
2
2
  import { QueueManager } from "./QueueManager.js";
3
- import { setQueue } from "./services/main.js";
3
+ import { clearQueue, getQueue, setQueue } from "./services/main.js";
4
4
  /**
5
5
  * BayProvider — registers a default in-memory `QueueManager` in the
6
6
  * host container so apps that don't need custom driver wiring can
@@ -23,6 +23,40 @@ import { setQueue } from "./services/main.js";
23
23
  * queue.register('send-email', new SendEmailJob())
24
24
  * await queue.dispatch('send-email', { to: 'user@example.com' })
25
25
  */
26
+ /**
27
+ * The driver the config asks for.
28
+ *
29
+ * `default` + `stores` first — the form an environment variable can steer. The
30
+ * `driver` key is the single-store form kept for configs written against it.
31
+ * Naming a store that does not exist throws rather than falling back to memory:
32
+ * an application that meant to queue in Redis and silently got an in-process
33
+ * queue would only find out when a restart dropped every pending job.
34
+ */
35
+ function buildDriver(config) {
36
+ const stores = config?.stores;
37
+ const name = config?.default;
38
+ if (stores && name !== undefined) {
39
+ const selected = stores[name];
40
+ if (!selected) {
41
+ const known = Object.keys(stores);
42
+ throw new Error(`[bay] config.queue names the store '${name}', which is not in \`stores\`. ` +
43
+ (known.length > 0
44
+ ? `Declared: ${known.join(", ")}.`
45
+ : "`stores` is empty — declare one with stores.memory() or stores.redis()."));
46
+ }
47
+ return selected();
48
+ }
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(", ")}.`);
52
+ }
53
+ const driverName = config?.driver ?? "memory";
54
+ if (driverName !== "memory") {
55
+ throw new Error(`[bay] Unsupported driver '${driverName}' — name it under \`stores\` instead: ` +
56
+ "stores: { redis: stores.redis({ connection: 'main' }) }.");
57
+ }
58
+ return new MemoryDriver();
59
+ }
26
60
  export default class BayProvider {
27
61
  app;
28
62
  constructor(app) {
@@ -31,20 +65,35 @@ export default class BayProvider {
31
65
  register() {
32
66
  this.app.container.singleton(QueueManager, () => {
33
67
  const config = this.app.config.get("queue");
34
- const driverName = config?.driver ?? "memory";
35
- if (driverName !== "memory") {
36
- throw new Error(`[bay] Unsupported driver '${driverName}' for default provider — ` +
37
- "wire QueueManager yourself in start/queue.ts for non-memory drivers.");
38
- }
39
- return new QueueManager(new MemoryDriver());
68
+ return new QueueManager(buildDriver(config));
40
69
  });
41
70
  this.app.container.singleton("queue", () => this.app.container.resolve(QueueManager));
42
71
  }
72
+ /** The queue THIS provider booted — not whatever the module singleton holds. */
73
+ #queue;
43
74
  async boot() {
44
75
  // Populate the `@c9up/bay/services/main` singleton so apps can
45
76
  // `import queue from '@c9up/bay/services/main'` from anywhere.
46
- setQueue(await this.app.container.resolve(QueueManager));
77
+ this.#queue = await this.app.container.resolve(QueueManager);
78
+ setQueue(this.#queue);
79
+ }
80
+ /**
81
+ * Stop the worker the app started, and let the job in flight finish.
82
+ *
83
+ * A worker polls on a timer. Left running, it survives a dev reload, a test
84
+ * teardown and a SIGTERM — so the old process keeps pulling jobs the new
85
+ * one is also pulling, and the same job runs twice.
86
+ */
87
+ async shutdown() {
88
+ if (!this.#queue)
89
+ return;
90
+ await this.#queue.stop();
91
+ // Two applications can share a process — parallel tests, a hot reload.
92
+ // The module singleton holds whichever booted last, so it is only ours
93
+ // to clear while it still points at the queue this provider booted.
94
+ if (getQueue() === this.#queue)
95
+ clearQueue();
96
+ this.#queue = undefined;
47
97
  }
48
- async shutdown() { }
49
98
  }
50
99
  //# sourceMappingURL=BayProvider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"BayProvider.js","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAkC9C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,OAAO,OAAO,WAAW;IACT;IAAtB,YAAsB,GAAkB;QAAlB,QAAG,GAAH,GAAG,CAAe;IAAG,CAAC;IAE5C,QAAQ;QACP,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,YAAY,EAAE,GAAG,EAAE;YAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAoB,OAAO,CAAC,CAAC;YAC/D,MAAM,UAAU,GAAG,MAAM,EAAE,MAAM,IAAI,QAAQ,CAAC;YAC9C,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;gBAC7B,MAAM,IAAI,KAAK,CACd,6BAA6B,UAAU,2BAA2B;oBACjE,sEAAsE,CACvE,CAAC;YACH,CAAC;YACD,OAAO,IAAI,YAAY,CAAC,IAAI,YAAY,EAAE,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,EAAE,CAC1C,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAe,YAAY,CAAC,CACtD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI;QACT,+DAA+D;QAC/D,+DAA+D;QAC/D,QAAQ,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAe,YAAY,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,KAAK,CAAC,QAAQ,KAAmB,CAAC;CAClC"}
1
+ {"version":3,"file":"BayProvider.js","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"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"}
@@ -62,7 +62,13 @@ export declare class QueueManager {
62
62
  recoverStale(): Promise<number>;
63
63
  /** Await the currently in-flight processOne, if any. */
64
64
  drain(): Promise<void>;
65
- /** Stop the worker. */
65
+ /**
66
+ * Stop the worker and wait for it to actually be gone.
67
+ *
68
+ * Awaits the LOOP, not just the job in flight: a stop that returns while
69
+ * the loop is still sleeping leaves a worker running past the teardown that
70
+ * asked it to stop.
71
+ */
66
72
  stop(): Promise<void>;
67
73
  /** Get failed jobs. */
68
74
  failedJobs(): Promise<Job[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"QueueManager.d.ts","sourceRoot":"","sources":["../src/QueueManager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,GAAG;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,CAAC;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IAC3B,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACzB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACjC;AAED,qBAAa,YAAY;;gBAMZ,MAAM,EAAE,WAAW;IAI/B,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,UAAU,CAAC,GAAG,IAAI;IAI1E,mCAAmC;IAC7B,QAAQ,CACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAChC,OAAO,CAAC,MAAM,CAAC;IAkBlB,yCAAyC;IACnC,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IA2CpC;;;;;OAKG;IACG,IAAI,CAAC,cAAc,SAAO,EAAE,cAAc,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IA8CzE;;;;;OAKG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAIrC,wDAAwD;IAClD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B,uBAAuB;IACjB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,uBAAuB;IACjB,UAAU,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAIlC,sBAAsB;IAChB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAG7B"}
1
+ {"version":3,"file":"QueueManager.d.ts","sourceRoot":"","sources":["../src/QueueManager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,GAAG;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,CAAC;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IAC3B,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACzB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACjC;AAED,qBAAa,YAAY;;gBAUZ,MAAM,EAAE,WAAW;IAI/B,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,UAAU,CAAC,GAAG,IAAI;IAI1E,mCAAmC;IAC7B,QAAQ,CACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAChC,OAAO,CAAC,MAAM,CAAC;IAkBlB,yCAAyC;IACnC,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IA2CpC;;;;;OAKG;IACG,IAAI,CAAC,cAAc,SAAO,EAAE,cAAc,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IA6EzE;;;;;OAKG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAIrC,wDAAwD;IAClD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B;;;;;;OAMG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B,uBAAuB;IACjB,UAAU,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAIlC,sBAAsB;IAChB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAG7B"}
@@ -10,6 +10,10 @@ export class QueueManager {
10
10
  #driver;
11
11
  #handlers = new Map();
12
12
  #running = false;
13
+ /** The running loop, so `stop()` can wait for it to finish. */
14
+ #loopPromise;
15
+ /** Cuts the sleep between polls short. */
16
+ #wake;
13
17
  #inflightPromise = null;
14
18
  constructor(driver) {
15
19
  this.#driver = driver;
@@ -89,19 +93,36 @@ export class QueueManager {
89
93
  throw new Error("QueueManager is already running");
90
94
  }
91
95
  this.#running = true;
96
+ const loop = this.#loop(pollIntervalMs, recoverStaleMs);
97
+ this.#loopPromise = loop;
98
+ try {
99
+ await loop;
100
+ }
101
+ finally {
102
+ this.#loopPromise = undefined;
103
+ }
104
+ }
105
+ /**
106
+ * The polling loop itself.
107
+ *
108
+ * Between jobs it sleeps, and that sleep is CANCELLABLE: `stop()` wakes it
109
+ * rather than waiting out the interval. Without that, stopping returned
110
+ * while the loop was still pending — up to a full poll interval of a worker
111
+ * that was supposed to be gone, and a timer holding the process open.
112
+ */
113
+ async #loop(pollIntervalMs, recoverStaleMs) {
92
114
  await this.#tryRecoverStale();
93
115
  let lastRecover = Date.now();
94
116
  while (this.#running) {
95
117
  try {
96
118
  this.#inflightPromise = this.processOne();
97
119
  const processed = await this.#inflightPromise;
98
- if (!processed) {
99
- await new Promise((r) => setTimeout(r, pollIntervalMs));
100
- }
120
+ if (!processed)
121
+ await this.#sleep(pollIntervalMs);
101
122
  }
102
123
  catch (err) {
103
124
  process.stderr.write(`QueueManager processOne error: ${err instanceof Error ? err.message : String(err)}\n`);
104
- await new Promise((r) => setTimeout(r, pollIntervalMs));
125
+ await this.#sleep(pollIntervalMs);
105
126
  }
106
127
  finally {
107
128
  this.#inflightPromise = null;
@@ -112,6 +133,20 @@ export class QueueManager {
112
133
  }
113
134
  }
114
135
  }
136
+ /** Wait, unless `stop()` says otherwise first. */
137
+ #sleep(ms) {
138
+ return new Promise((resolve) => {
139
+ const timer = setTimeout(() => {
140
+ this.#wake = undefined;
141
+ resolve();
142
+ }, ms);
143
+ this.#wake = () => {
144
+ clearTimeout(timer);
145
+ this.#wake = undefined;
146
+ resolve();
147
+ };
148
+ });
149
+ }
115
150
  /** recoverStale() wrapper that swallows driver errors — used by the work loop. */
116
151
  async #tryRecoverStale() {
117
152
  try {
@@ -136,10 +171,19 @@ export class QueueManager {
136
171
  await this.#inflightPromise.catch(() => { });
137
172
  }
138
173
  }
139
- /** Stop the worker. */
174
+ /**
175
+ * Stop the worker and wait for it to actually be gone.
176
+ *
177
+ * Awaits the LOOP, not just the job in flight: a stop that returns while
178
+ * the loop is still sleeping leaves a worker running past the teardown that
179
+ * asked it to stop.
180
+ */
140
181
  async stop() {
141
182
  this.#running = false;
183
+ this.#wake?.();
142
184
  await this.drain();
185
+ if (this.#loopPromise)
186
+ await this.#loopPromise.catch(() => { });
143
187
  }
144
188
  /** Get failed jobs. */
145
189
  async failedJobs() {
@@ -1 +1 @@
1
- {"version":3,"file":"QueueManager.js","sourceRoot":"","sources":["../src/QueueManager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"QueueManager.js","sourceRoot":"","sources":["../src/QueueManager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `ream configure @c9up/bay` — wire the job queue in one command.
3
+ *
4
+ * The provider alone is not enough: it reads `config/queue.ts`, and a package
5
+ * registered without one falls back to a default that is rarely the one an
6
+ * application wants. Writing both together is what makes `ream add` mean
7
+ * installed AND working.
8
+ */
9
+ interface Codemods {
10
+ addProvider(importPath: string): Promise<void>;
11
+ addEnvVars(vars: Record<string, string>): Promise<void>;
12
+ writeFile(filePath: string, content: string, options?: {
13
+ force?: boolean;
14
+ }): Promise<void>;
15
+ }
16
+ export declare function configure(codemods: Codemods): Promise<void>;
17
+ export {};
18
+ //# sourceMappingURL=configure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configure.d.ts","sourceRoot":"","sources":["../src/configure.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,UAAU,QAAQ;IACjB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,SAAS,CACR,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;CACjB;AAED,wBAAsB,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBjE"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `ream configure @c9up/bay` — wire the job queue in one command.
3
+ *
4
+ * The provider alone is not enough: it reads `config/queue.ts`, and a package
5
+ * registered without one falls back to a default that is rarely the one an
6
+ * application wants. Writing both together is what makes `ream add` mean
7
+ * installed AND working.
8
+ */
9
+ export async function configure(codemods) {
10
+ // The config below reads these, so they are declared here. Writing the file
11
+ // without them leaves an application whose config asks the environment for
12
+ // something nothing ever put there.
13
+ await codemods.addEnvVars({
14
+ QUEUE_STORE: "memory",
15
+ });
16
+ await codemods.addProvider("@c9up/bay/provider");
17
+ await codemods.writeFile("config/queue.ts", `import { defineConfig, stores } from '@c9up/bay'
18
+ import env from '#start/env'
19
+
20
+ export default defineConfig({
21
+ // Which store to run on. Memory forgets everything on restart, which is
22
+ // what a single process in development wants and nothing else does.
23
+ default: env.get('QUEUE_STORE', 'memory'),
24
+
25
+ stores: {
26
+ memory: stores.memory(),
27
+ redis: stores.redis({ connection: 'main' }),
28
+ },
29
+ })`);
30
+ }
31
+ //# sourceMappingURL=configure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configure.js","sourceRoot":"","sources":["../src/configure.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAYH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,QAAkB;IACjD,4EAA4E;IAC5E,2EAA2E;IAC3E,oCAAoC;IACpC,MAAM,QAAQ,CAAC,UAAU,CAAC;QACzB,WAAW,EAAE,QAAQ;KACrB,CAAC,CAAC;IAEH,MAAM,QAAQ,CAAC,WAAW,CAAC,oBAAoB,CAAC,CAAC;IACjD,MAAM,QAAQ,CAAC,SAAS,CACvB,iBAAiB,EACjB;;;;;;;;;;;;GAYC,CACD,CAAC;AACH,CAAC"}
@@ -41,6 +41,12 @@ export declare class RedisDriver implements QueueDriver {
41
41
  constructor(source: RedisClientSource, options?: {
42
42
  prefix?: string;
43
43
  visibilityTimeoutMs?: number;
44
+ /**
45
+ * Accept the non-atomic pop on a Redis older than 6.2, in
46
+ * production. Off by default: losing an accepted job is a choice a
47
+ * deployment makes, not one a version check makes for it.
48
+ */
49
+ allowNonAtomicPop?: boolean;
44
50
  });
45
51
  push(job: Job): Promise<void>;
46
52
  pop(): Promise<Job | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"RedisDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/RedisDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC3B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,KAAK,CAAC,CACL,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,GAAG,OAAO,EACtB,EAAE,EAAE,MAAM,GAAG,OAAO,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1B,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACpE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC3E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACzC;AAcD;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAC1B,WAAW,GACX,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;AA6B9C,qBAAa,WAAY,YAAW,WAAW;;gBAsC7C,MAAM,EAAE,iBAAiB,EACzB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;KAAE;IA8BtD,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7B,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;IAuC1B,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAMjC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS5C,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ9B,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IA8D/B,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAexB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAI7B"}
1
+ {"version":3,"file":"RedisDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/RedisDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC3B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,KAAK,CAAC,CACL,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,GAAG,OAAO,EACtB,EAAE,EAAE,MAAM,GAAG,OAAO,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1B,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACpE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC3E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACzC;AAcD;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAC1B,WAAW,GACX,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;AAmD9C,qBAAa,WAAY,YAAW,WAAW;;gBAsC7C,MAAM,EAAE,iBAAiB,EACzB,OAAO,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B;;;;WAIG;QACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC5B;IAkCI,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7B,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;IAkD1B,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAMjC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS5C,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ9B,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IA8D/B,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAexB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAI7B"}
@@ -17,6 +17,7 @@
17
17
  * { PX: ms }) does NOT satisfy this interface and would drop the lease TTL — it
18
18
  * needs a thin adapter.
19
19
  */
20
+ import { inProduction } from "../nodeEnv.js";
20
21
  function isValidJob(obj) {
21
22
  if (typeof obj !== "object" || obj === null)
22
23
  return false;
@@ -33,13 +34,32 @@ function isValidJob(obj) {
33
34
  * has no client to inspect yet.
34
35
  */
35
36
  const warned = new WeakSet();
36
- function warnWithoutLmove(client) {
37
- if (typeof client.lmove === "function" || warned.has(client))
37
+ function checkLmove(client, allowNonAtomicPop) {
38
+ if (typeof client.lmove === "function")
39
+ return;
40
+ // A queue's whole promise is that a job it accepted gets run. Without LMOVE
41
+ // the pop is `lpop` then `rpush`, and a crash between the two deletes the
42
+ // job from pending before it reaches processing: nothing recovers it,
43
+ // because nothing knows it existed. That is a different product, and in
44
+ // production it must be asked for rather than fallen into.
45
+ if (inProduction() && !allowNonAtomicPop) {
46
+ throw new Error("[bay] this Redis client has no LMOVE (Redis < 6.2), so pop() would be a non-atomic lpop+rpush — " +
47
+ "a crash between the two loses the in-flight job, turning at-least-once delivery into at-most-once.\n" +
48
+ " Upgrade to Redis 6.2 or later, or pass `allowNonAtomicPop: true` to state that losing a job is acceptable here.");
49
+ }
50
+ if (warned.has(client))
38
51
  return;
39
52
  warned.add(client);
53
+ // Said even when the deployment opted in: agreeing to lose a job once, in a
54
+ // config file, is not the same as being reminded that this process is
55
+ // running that way. The line has to be in the logs of the incident.
56
+ const optedIn = inProduction() && allowNonAtomicPop;
40
57
  console.warn("[bay] RedisDriver: client lacks LMOVE (Redis <6.2). pop() falls back to " +
41
58
  "a non-atomic lpop+rpush, downgrading delivery from at-least-once to " +
42
- "at-most-once — a crash between the two commands loses the in-flight job.");
59
+ "at-most-once — a crash between the two commands loses the in-flight job." +
60
+ (optedIn
61
+ ? "\n Running this way in PRODUCTION because allowNonAtomicPop was set."
62
+ : ""));
43
63
  }
44
64
  /**
45
65
  * A key prefix that ends in a separator.
@@ -67,7 +87,7 @@ export class RedisDriver {
67
87
  return this.#resolved;
68
88
  if (typeof this.#source !== "function") {
69
89
  this.#resolved = this.#source;
70
- warnWithoutLmove(this.#resolved);
90
+ checkLmove(this.#resolved, this.#allowNonAtomicPop);
71
91
  return this.#resolved;
72
92
  }
73
93
  if (!this.#pending) {
@@ -75,7 +95,7 @@ export class RedisDriver {
75
95
  this.#pending = Promise.resolve(resolver())
76
96
  .then((client) => {
77
97
  this.#resolved = client;
78
- warnWithoutLmove(client);
98
+ checkLmove(client, this.#allowNonAtomicPop);
79
99
  return client;
80
100
  })
81
101
  // Cleared on failure too. Clearing only on success left the
@@ -93,13 +113,15 @@ export class RedisDriver {
93
113
  // A client handed in directly can be checked now, so the warning keeps
94
114
  // landing at construction as it always did. A named connection has no
95
115
  // client yet — it is checked when the connection resolves.
96
- if (typeof source !== "function")
97
- warnWithoutLmove(source);
116
+ if (typeof source !== "function") {
117
+ checkLmove(source, options?.allowNonAtomicPop ?? false);
118
+ }
98
119
  // Normalised rather than documented: every key is built by concatenation
99
120
  // (`${prefix}pending`), so a prefix without a trailing separator yields
100
121
  // "myapppending" — unreadable, and able to collide with a neighbouring
101
122
  // prefix. Nothing warned, because nothing failed.
102
123
  this.#prefix = withSeparator(options?.prefix ?? "queue:");
124
+ this.#allowNonAtomicPop = options?.allowNonAtomicPop ?? false;
103
125
  const visibilityTimeout = options?.visibilityTimeoutMs ?? 30_000;
104
126
  // A non-positive / non-integer timeout makes pop()'s `SET … PX <ms>` fail
105
127
  // on a real Redis; the catch then removes the job from `processing` and
@@ -112,6 +134,7 @@ export class RedisDriver {
112
134
  }
113
135
  #pendingKey = () => `${this.#prefix}pending`;
114
136
  #processingKey = () => `${this.#prefix}processing`;
137
+ #allowNonAtomicPop = false;
115
138
  #failedKey = () => `${this.#prefix}failed`;
116
139
  #leaseKey = (jobId) => `${this.#prefix}lease:${jobId}`;
117
140
  async push(job) {
@@ -131,22 +154,31 @@ export class RedisDriver {
131
154
  }
132
155
  if (!raw)
133
156
  return null;
157
+ // Only a payload that can never be run is purged. Everything past this
158
+ // point is a REAL job that already sits in `processing`, and deleting
159
+ // it there is the one thing that loses it for good: it is gone from
160
+ // pending too, and recoverStale() scans processing, so nothing would
161
+ // ever find it again.
162
+ let parsed;
134
163
  try {
135
- const parsed = JSON.parse(raw);
136
- if (!isValidJob(parsed)) {
137
- // Malformed payload — purge from `processing` so it can't sit
138
- // there indefinitely as a poison pill. recoverStale() also
139
- // catches survivors but pop()'s own move is the primary path.
140
- await client.lrem(this.#processingKey(), 1, raw);
141
- return null;
142
- }
143
- await client.set(this.#leaseKey(parsed.id), raw, "PX", String(this.#visibilityTimeout));
144
- return parsed;
164
+ parsed = JSON.parse(raw);
145
165
  }
146
166
  catch {
167
+ // A poison pill: unparseable, and it would sit in processing
168
+ // forever blocking nothing but wasting every recovery pass.
169
+ await client.lrem(this.#processingKey(), 1, raw);
170
+ return null;
171
+ }
172
+ if (!isValidJob(parsed)) {
147
173
  await client.lrem(this.#processingKey(), 1, raw);
148
174
  return null;
149
175
  }
176
+ // A lease that cannot be written is a transient Redis failure, not a
177
+ // bad job. The error propagates and the job STAYS in processing with
178
+ // no lease, which is precisely the state recoverStale() puts back in
179
+ // pending — so the delivery guarantee survives the blip.
180
+ await client.set(this.#leaseKey(parsed.id), raw, "PX", String(this.#visibilityTimeout));
181
+ return parsed;
150
182
  }
151
183
  async complete(job) {
152
184
  const client = await this.#client();