@basaltkit/queue 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -330,6 +330,14 @@ The sync driver always runs immediately. Delays, timed backoff, and priority onl
330
330
  **Do I need Redis to run the tests?**
331
331
  No. Without `connection`, the plugin uses `SyncQueueDriver`. You can also instantiate the driver directly and inspect `driver.executed`.
332
332
 
333
+ ## Sync driver semantics
334
+
335
+ The inline sync driver (the default without a `connection`) is at-most-once:
336
+ handler errors reject `dispatch()` and an exhausted job is lost. Selecting it
337
+ implicitly in production logs a boot warning — pass `driver: new
338
+ SyncQueueDriver()` to opt in deliberately. Its `executed[]` history is capped
339
+ at 1000 entries.
340
+
333
341
  ## How it connects to other modules
334
342
 
335
343
  - **`@basaltkit/core`** — provides `createApp`/`definePlugin` (`queuePlugin` is a core plugin), the ALS context (`runWithContext`/`ctx`) propagated to workers, `parseDuration` (formats `'30s'`, `'10m'`), and the base `BasaltError` class.
@@ -3,6 +3,15 @@ import type { AddJobOptions, JobExecutor, QueueDriver } from '../driver.js';
3
3
  * Synchronous driver: executes the job inline on dispatch, honoring `attempts`
4
4
  * (immediate retry). It is the driver for tests and Redis-less dev — the
5
5
  * equivalent of Laravel's `sync` queue driver.
6
+ *
7
+ * Semantics to be aware of (they differ from a broker-backed driver):
8
+ * - **At-most-once.** A job that exhausts its inline retries is LOST — there is
9
+ * no persistence and no later redelivery.
10
+ * - **Errors propagate to the dispatcher.** `job.dispatch()` rejects when the
11
+ * handler fails (useful in tests/dev; a broker driver would return
12
+ * immediately and retry in the background).
13
+ * Deploying this driver to production is almost always a misconfiguration —
14
+ * `queuePlugin` warns when it is selected by default there.
6
15
  */
7
16
  export declare class SyncQueueDriver implements QueueDriver {
8
17
  readonly name = "sync";
@@ -22,5 +31,8 @@ export declare class SyncQueueDriver implements QueueDriver {
22
31
  setExecutor(executor: JobExecutor): void;
23
32
  add(queue: string, jobName: string, data: unknown, options: AddJobOptions): Promise<void>;
24
33
  startWorker(): void;
34
+ /** Appends to `executed[]`, evicting the oldest entries past the cap so a
35
+ * long-running process on this driver cannot leak memory unboundedly. */
36
+ private record;
25
37
  close(): Promise<void>;
26
38
  }
@@ -1,7 +1,18 @@
1
+ /** `executed[]` keeps at most this many entries (oldest evicted first). */
2
+ const EXECUTED_HISTORY_LIMIT = 1000;
1
3
  /**
2
4
  * Synchronous driver: executes the job inline on dispatch, honoring `attempts`
3
5
  * (immediate retry). It is the driver for tests and Redis-less dev — the
4
6
  * equivalent of Laravel's `sync` queue driver.
7
+ *
8
+ * Semantics to be aware of (they differ from a broker-backed driver):
9
+ * - **At-most-once.** A job that exhausts its inline retries is LOST — there is
10
+ * no persistence and no later redelivery.
11
+ * - **Errors propagate to the dispatcher.** `job.dispatch()` rejects when the
12
+ * handler fails (useful in tests/dev; a broker driver would return
13
+ * immediately and retry in the background).
14
+ * Deploying this driver to production is almost always a misconfiguration —
15
+ * `queuePlugin` warns when it is selected by default there.
5
16
  */
6
17
  export class SyncQueueDriver {
7
18
  name = 'sync';
@@ -19,18 +30,26 @@ export class SyncQueueDriver {
19
30
  for (let attempt = 1; attempt <= options.attempts; attempt++) {
20
31
  try {
21
32
  await this.executor?.(jobName, data);
22
- this.executed.push({ queue, jobName, attempts: attempt });
33
+ this.record({ queue, jobName, attempts: attempt });
23
34
  return;
24
35
  }
25
36
  catch (error) {
26
37
  lastError = error;
27
38
  }
28
39
  }
29
- this.executed.push({ queue, jobName, attempts: options.attempts });
40
+ this.record({ queue, jobName, attempts: options.attempts });
30
41
  throw lastError;
31
42
  }
32
43
  startWorker() {
33
44
  // no-op: add() already executes inline
34
45
  }
46
+ /** Appends to `executed[]`, evicting the oldest entries past the cap so a
47
+ * long-running process on this driver cannot leak memory unboundedly. */
48
+ record(entry) {
49
+ this.executed.push(entry);
50
+ if (this.executed.length > EXECUTED_HISTORY_LIMIT) {
51
+ this.executed.splice(0, this.executed.length - EXECUTED_HISTORY_LIMIT);
52
+ }
53
+ }
35
54
  async close() { }
36
55
  }
package/dist/index.js CHANGED
@@ -14,10 +14,25 @@ export function queuePlugin(options = {}) {
14
14
  register({ container }) {
15
15
  registerQueueCommands(container);
16
16
  container.singleton(QUEUE, () => {
17
- const driver = options.driver ??
18
- (options.connection
19
- ? new BullmqQueueDriver({ connection: options.connection })
20
- : new SyncQueueDriver());
17
+ let driver = options.driver;
18
+ if (!driver) {
19
+ if (options.connection) {
20
+ driver = new BullmqQueueDriver({ connection: options.connection });
21
+ }
22
+ else {
23
+ driver = new SyncQueueDriver();
24
+ if (process.env['NODE_ENV'] === 'production') {
25
+ // The silent default without a Redis connection is the inline sync
26
+ // driver: at-most-once, no background retries, handler errors
27
+ // propagate into the dispatching request. Deliberate sync use in
28
+ // production stays possible — pass `driver: new SyncQueueDriver()`
29
+ // explicitly to silence this.
30
+ console.warn('[basalt:queue] No `connection` (Redis) configured — falling back to the inline sync driver. ' +
31
+ 'Jobs run at-most-once inside the dispatching request and are lost on failure. ' +
32
+ 'Configure a Redis `connection` for production, or pass `driver: new SyncQueueDriver()` to opt in explicitly.');
33
+ }
34
+ }
35
+ }
21
36
  const manager = new QueueManager(driver, {
22
37
  ...(options.onUnsupported !== undefined ? { onUnsupported: options.onUnsupported } : {}),
23
38
  ...(options.removeOnComplete !== undefined ? { removeOnComplete: options.removeOnComplete } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basaltkit/queue",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Basalt queues on top of BullMQ: declarative jobs with Zod payloads, context propagation (tenant/requestId) to workers and a sync driver for tests.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  "dependencies": {
17
17
  "bullmq": "^6.2.1",
18
18
  "@basaltkit/core": "^1.3.0",
19
- "@basaltkit/events": "^1.0.1"
19
+ "@basaltkit/events": "^1.1.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^26.3.0",