@objectstack/service-queue 17.0.0-rc.2 → 17.0.0-rc.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,258 @@
1
1
  # @objectstack/service-queue
2
2
 
3
+ ## 17.0.0-rc.4
4
+
5
+ ### Minor Changes
6
+
7
+ - e98fb14: fix(service-queue): `sys_job_queue` no longer grows forever — `completed` rows expire on a declared 7-day retention (#5179)
8
+
9
+ `DbQueueAdapter` marked a delivered message `status: 'completed'` and then
10
+ **nothing ever touched that row again**. `purge()` had zero production callers
11
+ (tests only), `purgeFailed()` is a manual dead-letter API, and the object
12
+ declared no lifecycle policy at all — so every queue delivery left a permanent
13
+ row, which since #5160 means one permanent row per queued email.
14
+
15
+ `sys_job_queue` now declares an ADR-0057 policy and the platform
16
+ `LifecycleService` enforces it on its existing hourly sweep:
17
+
18
+ ```ts
19
+ lifecycle: {
20
+ class: 'transient',
21
+ retention: { maxAge: '7d', onlyWhen: { status: 'completed' } },
22
+ }
23
+ ```
24
+
25
+ **Only `completed` rows are swept.** `pending` / `running` are live work, and
26
+ `failed` / `dlq` are the dead-letter queue — they exist to wait for a human, so
27
+ they are never deleted automatically at any age. `listFailed()` / `replay()` /
28
+ `purgeFailed()` remain the only way a dead letter leaves the table. This is
29
+ also why the policy is `retention` (age + row filter) rather than a `ttl` on
30
+ `completed_at`: TTL has no row filter, and `dlq` rows stamp `completed_at` too.
31
+
32
+ **No new configuration, and no new sweeper.** ADR-0057 §3.3 puts one reaper in
33
+ the platform rather than one per plugin — the same call the sibling
34
+ `sys_job_run` (30d) already makes. Any kernel with a data engine already runs
35
+ it, its per-sweep `[lifecycle] sweep: … ~N rows reaped` line now accounts for
36
+ this table too, and the window is overridable per environment through the
37
+ `lifecycle` settings namespace without touching code.
38
+
39
+ **The dedup window is now an enforced invariant, not a coincidence.** Publish
40
+ dedups against a terminal row by comparing its `created_at` to
41
+ `idempotencyWindowMs` (default 24h), and the reaper cuts off on that same
42
+ `created_at` axis — so retention (7d) ≥ dedup window is what keeps "duplicate
43
+ publishes inside the window are suppressed" true. `DbQueueAdapter` reads the
44
+ declared window (new export `completedRetentionWindowMs()`) and **throws at
45
+ construction** if `idempotencyWindowMs` is configured longer than it, instead of
46
+ silently degrading into duplicate deliveries days later. If you raise
47
+ `idempotencyWindowMs` past 7 days, raise the object's declared retention (or the
48
+ `lifecycle` settings override) to match — the error message names both numbers.
49
+
50
+ `class: 'transient'` is deliberate: `telemetry`/`event`/`audit` classes
51
+ relocate their table to the dedicated `telemetry` datasource wherever one is
52
+ registered (ADR-0057 §3.6), and moving a live work queue's storage would be a
53
+ migration, not a cleanup.
54
+
55
+ ### Patch Changes
56
+
57
+ - 7c2f7dd: fix(objectql,service-queue): a `lifecycle` settings override can no longer undercut a consumer's retention floor (#5195)
58
+
59
+ ADR-0057 P4 lets an operator override any object's retention window per
60
+ environment and per tenant through the `lifecycle` settings namespace. Until now
61
+ the only validation on that override was **does it parse** — and a retention
62
+ window is not only the operator's business: other code can depend on the rows
63
+ still being there.
64
+
65
+ `sys_job_queue` is the worked example. `DbQueueAdapter` deduplicates publishes by
66
+ comparing a terminal row's `created_at` against its idempotency window, so the
67
+ dedup check only means anything while that row still exists; #5179 made the
68
+ ordering an invariant by refusing, at construction, an idempotency window longer
69
+ than the object's **declared** retention. A settings override the constructor
70
+ cannot see walks straight around it:
71
+
72
+ ```jsonc
73
+ // lifecycle → retention_overrides
74
+ { "sys_job_queue": { "maxAge": "1h" } }
75
+ ```
76
+
77
+ completed rows are reaped an hour after they are written, publish keeps
78
+ deduplicating against 24h, and duplicate deliveries resume **with nothing in any
79
+ log**.
80
+
81
+ **New: retention floors.** A consumer may now declare, at runtime, the shortest
82
+ window its own contract survives:
83
+
84
+ ```ts
85
+ lifecycle.registerRetentionFloor("sys_job_queue", {
86
+ policy: "retention", // or 'ttl'
87
+ minWindowMs: 24 * 60 * 60 * 1000,
88
+ declaredBy: "com.objectstack.service.queue",
89
+ consequence: "…what silently breaks below it",
90
+ remedy: "…the settings change that makes an override legal",
91
+ });
92
+ ```
93
+
94
+ - An override below the floor — **global or tenant-scoped** — is **rejected**,
95
+ and the declared window keeps running. Not clamped to the floor: clamping
96
+ would enforce a third number written in neither the declaration nor the
97
+ settings, and that number would move whenever an unrelated package changed
98
+ its floor. Rejection has exactly one fallback, the declaration, which is
99
+ already how an unparseable override resolves.
100
+ - The rejection is `error`-level and carries both the consequence and the fix,
101
+ because what it prevents leaves the system looking entirely healthy. It is
102
+ also on the sweep report as `LifecycleSweepReport.floorViolations` — machine-
103
+ readable, every sweep.
104
+ - A **declared** window below a registered floor is reported the same way and
105
+ still enforced: refusing to reap would trade a broken consumer contract for
106
+ the unbounded table #5179 just closed.
107
+ - Objects with no registered floor are completely unaffected — P4 overrides
108
+ behave exactly as before.
109
+
110
+ Floors are runtime wiring, not spec surface (the same call ADR-0057's reap-guard
111
+ amendment makes), plus a reason of their own: the queue's floor **is**
112
+ `DbQueueAdapterOptions.idempotencyWindowMs`, a per-kernel construction option, so
113
+ a static key on the object's `lifecycle` block could only ever be a second copy
114
+ of it that drifts. No `packages/spec` change.
115
+
116
+ `QueueServicePlugin` registers `sys_job_queue`'s floor on `kernel:ready`,
117
+ carrying the window the adapter was actually constructed with — so a non-default
118
+ `db.idempotencyWindowMs` is covered too. The ordering is now enforced from both
119
+ sides: the constructor rejects a too-long `idempotencyWindowMs`, the floor
120
+ rejects a too-short `maxAge`.
121
+
122
+ New exports from `@objectstack/objectql`: `LifecycleRetentionFloor`,
123
+ `LifecycleFloorViolation`, plus `LifecycleService.registerRetentionFloor()`.
124
+ `LifecycleLoggerLike` gained an optional `error()` (absent ⇒ falls back to
125
+ `warn`), and `LifecycleSweepReport` gained `floorViolations`.
126
+
127
+ - Updated dependencies [9fe9c1d]
128
+ - Updated dependencies [d4e0809]
129
+ - Updated dependencies [f724f69]
130
+ - Updated dependencies [28ad90e]
131
+ - Updated dependencies [f8644c7]
132
+ - Updated dependencies [306ca50]
133
+ - Updated dependencies [978fed2]
134
+ - Updated dependencies [cfc293f]
135
+ - Updated dependencies [de70b42]
136
+ - Updated dependencies [fb3d99b]
137
+ - Updated dependencies [cdfbee2]
138
+ - Updated dependencies [29c6c9d]
139
+ - Updated dependencies [d21c001]
140
+ - Updated dependencies [f1cc3a3]
141
+ - Updated dependencies [ddc2527]
142
+ - Updated dependencies [553a47f]
143
+ - Updated dependencies [a3a884d]
144
+ - Updated dependencies [cfed092]
145
+ - Updated dependencies [2e284b2]
146
+ - Updated dependencies [1b49eaf]
147
+ - Updated dependencies [0161c7f]
148
+ - Updated dependencies [e900015]
149
+ - Updated dependencies [b5bdf48]
150
+ - Updated dependencies [a019e52]
151
+ - Updated dependencies [64fc6d5]
152
+ - Updated dependencies [b746aa0]
153
+ - Updated dependencies [947d4f9]
154
+ - Updated dependencies [eaaf03c]
155
+ - Updated dependencies [d17df80]
156
+ - Updated dependencies [7d0e7b5]
157
+ - Updated dependencies [6513c17]
158
+ - Updated dependencies [c142ced]
159
+ - Updated dependencies [eda599e]
160
+ - Updated dependencies [c001422]
161
+ - Updated dependencies [77022a9]
162
+ - Updated dependencies [52760bf]
163
+ - Updated dependencies [5543020]
164
+ - Updated dependencies [880d343]
165
+ - Updated dependencies [6e82972]
166
+ - Updated dependencies [4615a18]
167
+ - Updated dependencies [7f62706]
168
+ - Updated dependencies [667fa44]
169
+ - Updated dependencies [37e38d1]
170
+ - Updated dependencies [1eb13a0]
171
+ - Updated dependencies [c52e608]
172
+ - Updated dependencies [4dfd002]
173
+ - Updated dependencies [77be690]
174
+ - Updated dependencies [811c30c]
175
+ - Updated dependencies [b49ccfd]
176
+ - Updated dependencies [85d95e7]
177
+ - Updated dependencies [168f60f]
178
+ - Updated dependencies [244ca86]
179
+ - Updated dependencies [546ab3c]
180
+ - Updated dependencies [0b51bb6]
181
+ - Updated dependencies [d9971d3]
182
+ - Updated dependencies [eb3e650]
183
+ - Updated dependencies [abeb375]
184
+ - Updated dependencies [ef4efa8]
185
+ - Updated dependencies [cbb6a5c]
186
+ - Updated dependencies [795b6e1]
187
+ - Updated dependencies [175d789]
188
+ - Updated dependencies [55dbbba]
189
+ - Updated dependencies [72c3c86]
190
+ - Updated dependencies [7f1a635]
191
+ - Updated dependencies [e98fb14]
192
+ - Updated dependencies [0f2fdcd]
193
+ - Updated dependencies [8ffa8b9]
194
+ - Updated dependencies [674ac99]
195
+ - Updated dependencies [1b9a53b]
196
+ - Updated dependencies [502564d]
197
+ - Updated dependencies [471839d]
198
+ - Updated dependencies [46365ab]
199
+ - Updated dependencies [b508244]
200
+ - Updated dependencies [594508e]
201
+ - Updated dependencies [1c625ca]
202
+ - Updated dependencies [71f205d]
203
+ - Updated dependencies [414395b]
204
+ - Updated dependencies [c5adfe1]
205
+ - Updated dependencies [26e1029]
206
+ - Updated dependencies [108ba8d]
207
+ - Updated dependencies [b4ad984]
208
+ - Updated dependencies [a9f32df]
209
+ - Updated dependencies [aeb9b27]
210
+ - Updated dependencies [7d27da0]
211
+ - Updated dependencies [089767f]
212
+ - Updated dependencies [e4c8b6c]
213
+ - Updated dependencies [acb10f6]
214
+ - Updated dependencies [1c3da1f]
215
+ - Updated dependencies [a34fd2e]
216
+ - Updated dependencies [889ae47]
217
+ - Updated dependencies [4f4c3fb]
218
+ - Updated dependencies [7adc841]
219
+ - Updated dependencies [4845f85]
220
+ - Updated dependencies [7b005b4]
221
+ - Updated dependencies [94f7b6a]
222
+ - Updated dependencies [5c94f83]
223
+ - Updated dependencies [73e576f]
224
+ - Updated dependencies [c5a5996]
225
+ - Updated dependencies [ae490ef]
226
+ - Updated dependencies [f61c8cf]
227
+ - Updated dependencies [e3ef52b]
228
+ - Updated dependencies [07f1822]
229
+ - Updated dependencies [04fab5e]
230
+ - Updated dependencies [efedd28]
231
+ - Updated dependencies [5278e11]
232
+ - Updated dependencies [23dba62]
233
+ - Updated dependencies [ba98e26]
234
+ - Updated dependencies [f104bab]
235
+ - Updated dependencies [fc5f536]
236
+ - Updated dependencies [f8cfbb4]
237
+ - Updated dependencies [c89d18c]
238
+ - Updated dependencies [aac90a5]
239
+ - Updated dependencies [1e6ab15]
240
+ - Updated dependencies [c87ef70]
241
+ - Updated dependencies [3cb0618]
242
+ - Updated dependencies [32a0874]
243
+ - Updated dependencies [7055c22]
244
+ - Updated dependencies [785a748]
245
+ - Updated dependencies [3af0354]
246
+ - Updated dependencies [866ff16]
247
+ - Updated dependencies [5a85e67]
248
+ - Updated dependencies [c183a12]
249
+ - Updated dependencies [8064b07]
250
+ - Updated dependencies [4a56dbd]
251
+ - Updated dependencies [06df4fa]
252
+ - @objectstack/spec@17.0.0-rc.4
253
+ - @objectstack/core@17.0.0-rc.4
254
+ - @objectstack/platform-objects@17.0.0-rc.4
255
+
3
256
  ## 17.0.0-rc.2
4
257
 
5
258
  ### Patch Changes
package/README.md CHANGED
@@ -70,6 +70,35 @@ new QueueServicePlugin({
70
70
  new QueueServicePlugin({ adapter: 'memory' });
71
71
  ```
72
72
 
73
+ ### Retention — how `sys_job_queue` stays bounded
74
+
75
+ Delivered messages are not kept forever. `sys_job_queue` declares an ADR-0057
76
+ lifecycle policy and the platform `LifecycleService` (shipped with
77
+ `@objectstack/objectql`, armed on every kernel that has data) enforces it — no
78
+ configuration, no extra scheduler:
79
+
80
+ | Row state | What happens |
81
+ |---|---|
82
+ | `completed` | deleted **7 days** after `created_at` |
83
+ | `pending` / `running` | never swept — live work |
84
+ | `failed` / `dlq` | never swept — the dead-letter queue waits for a human (`listFailed` / `replay` / `purgeFailed`) |
85
+
86
+ Two consequences worth knowing:
87
+
88
+ - **`idempotencyWindowMs` must not exceed the retention window.** Dedup against
89
+ a terminal message compares its `created_at` to that window, so a longer
90
+ setting would start accepting duplicates the moment the row was swept. The
91
+ `db` adapter throws at construction instead of degrading quietly.
92
+ - **The window is overridable per environment** through the `lifecycle`
93
+ settings namespace (`maxAge` per object), like every other ADR-0057 policy —
94
+ but **not below the idempotency window**. On startup this plugin registers a
95
+ *retention floor* with the `LifecycleService` carrying the window the adapter
96
+ was actually constructed with; a global or tenant-scoped override under it is
97
+ **rejected** at sweep time (the declared window keeps running) and logged at
98
+ `error` naming the consequence and the two settings that would make it legal.
99
+ So the ordering is enforced from both sides: the constructor rejects a
100
+ too-long `idempotencyWindowMs`, the floor rejects a too-short `maxAge`.
101
+
73
102
  ## Service API
74
103
 
75
104
  Implements `IQueueService` from `@objectstack/spec/contracts`:
package/dist/index.cjs CHANGED
@@ -22,12 +22,13 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  DbQueueAdapter: () => DbQueueAdapter,
24
24
  MemoryQueueAdapter: () => MemoryQueueAdapter,
25
- QueueServicePlugin: () => QueueServicePlugin
25
+ QueueServicePlugin: () => QueueServicePlugin,
26
+ completedRetentionWindowMs: () => completedRetentionWindowMs
26
27
  });
27
28
  module.exports = __toCommonJS(index_exports);
28
29
 
29
30
  // src/queue-service-plugin.ts
30
- var import_audit = require("@objectstack/platform-objects/audit");
31
+ var import_audit2 = require("@objectstack/platform-objects/audit");
31
32
 
32
33
  // src/memory-queue-adapter.ts
33
34
  var MemoryQueueAdapter = class {
@@ -83,6 +84,9 @@ var MemoryQueueAdapter = class {
83
84
  }
84
85
  };
85
86
 
87
+ // src/db-queue-adapter.ts
88
+ var import_audit = require("@objectstack/platform-objects/audit");
89
+
86
90
  // src/common.ts
87
91
  var SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] };
88
92
  function uid(prefix) {
@@ -93,6 +97,21 @@ function uid(prefix) {
93
97
  function nowIso(clock) {
94
98
  return (clock?.now() ?? /* @__PURE__ */ new Date()).toISOString();
95
99
  }
100
+ var LIFECYCLE_UNIT_MS = {
101
+ h: 36e5,
102
+ d: 864e5,
103
+ w: 7 * 864e5,
104
+ y: 365 * 864e5
105
+ };
106
+ function lifecycleDurationMs(literal) {
107
+ const m = /^(\d+)(h|d|w|y)$/.exec(literal);
108
+ if (!m) {
109
+ throw new Error(
110
+ `[service-queue] invalid lifecycle duration literal '${literal}' \u2014 expected <n><unit> with unit h|d|w|y (e.g. '7d')`
111
+ );
112
+ }
113
+ return Number(m[1]) * LIFECYCLE_UNIT_MS[m[2]];
114
+ }
96
115
  function parseJson(raw, fallback) {
97
116
  if (raw == null) return fallback;
98
117
  if (typeof raw === "string") {
@@ -108,6 +127,15 @@ function parseJson(raw, fallback) {
108
127
 
109
128
  // src/db-queue-adapter.ts
110
129
  var QUEUE_TABLE = "sys_job_queue";
130
+ function completedRetentionWindowMs() {
131
+ const maxAge = import_audit.SysJobQueue.lifecycle?.retention?.maxAge;
132
+ if (!maxAge) {
133
+ throw new Error(
134
+ "[service-queue] sys_job_queue no longer declares lifecycle.retention \u2014 DbQueueAdapter dedups against terminal rows by `created_at` window and relies on that declared retention to keep them (ADR-0057, #5179). Restore the declaration in @objectstack/platform-objects rather than sweeping the table from here."
135
+ );
136
+ }
137
+ return lifecycleDurationMs(maxAge);
138
+ }
111
139
  var DbQueueAdapter = class {
112
140
  constructor(args) {
113
141
  this.handlers = /* @__PURE__ */ new Map();
@@ -125,6 +153,42 @@ var DbQueueAdapter = class {
125
153
  autoStart: o.autoStart ?? true,
126
154
  workerId: o.workerId ?? uid("worker")
127
155
  };
156
+ const retentionMs = completedRetentionWindowMs();
157
+ if (this.opts.idempotencyWindowMs > retentionMs) {
158
+ throw new Error(
159
+ `[service-queue] idempotencyWindowMs (${this.opts.idempotencyWindowMs}ms) exceeds the retention window sys_job_queue declares for completed rows (${retentionMs}ms, lifecycle.retention.maxAge \u2014 ADR-0057). Terminal-row dedup is evaluated by \`created_at\` against that same window, so the longer setting would silently accept duplicates once a row is reaped. Lower idempotencyWindowMs, or raise the declared retention (both windows are measured from \`created_at\`).`
160
+ );
161
+ }
162
+ }
163
+ /** The configured dedup window (ms) — the number the floor below is made of. */
164
+ get idempotencyWindowMs() {
165
+ return this.opts.idempotencyWindowMs;
166
+ }
167
+ /**
168
+ * [#5195] The retention floor `sys_job_queue` must satisfy for this adapter's
169
+ * dedup contract to mean anything, handed to `LifecycleService`
170
+ * (`registerRetentionFloor`) by `QueueServicePlugin`.
171
+ *
172
+ * The constructor check above only reads the object's **declaration**. ADR-0057
173
+ * P4 lets an operator override that window per environment/tenant through the
174
+ * `lifecycle` settings namespace, which the constructor cannot see: set
175
+ * `lifecycle.retention_overrides.sys_job_queue.maxAge = '1h'` and completed
176
+ * rows vanish an hour after they are written while publish keeps dedupping
177
+ * against a 24h window — duplicate deliveries resume, with nothing in any log.
178
+ * Registering the floor is what closes that door, and it carries the number
179
+ * this adapter was actually CONSTRUCTED with rather than a static copy of the
180
+ * default (a per-kernel option cannot live in the object's declaration).
181
+ */
182
+ retentionFloor() {
183
+ const ms = this.opts.idempotencyWindowMs;
184
+ const literal = `${Math.ceil(ms / 36e5)}h`;
185
+ return {
186
+ policy: "retention",
187
+ minWindowMs: ms,
188
+ declaredBy: "com.objectstack.service.queue",
189
+ consequence: `DbQueueAdapter dedups sys_job_queue publishes by comparing created_at against its ${ms}ms idempotency window, so a shorter retention deletes the very rows that check reads \u2014 duplicate deliveries resume silently, with nothing in any log.`,
190
+ remedy: `set lifecycle.retention_overrides.sys_job_queue.maxAge to '${literal}' or longer, or lower QueueServicePlugin's db.idempotencyWindowMs to the window you actually want (both are measured from created_at).`
191
+ };
128
192
  }
129
193
  // ── IQueueService ────────────────────────────────────────────────
130
194
  async publish(queue, data, options) {
@@ -456,7 +520,7 @@ var QueueServicePlugin = class {
456
520
  scope: "system",
457
521
  defaultDatasource: "cloud",
458
522
  namespace: "sys",
459
- objects: [import_audit.SysJobQueue]
523
+ objects: [import_audit2.SysJobQueue]
460
524
  });
461
525
  } catch (err) {
462
526
  ctx.logger.warn("QueueServicePlugin: manifest service unavailable; sys_job_queue not registered", err);
@@ -492,6 +556,7 @@ var QueueServicePlugin = class {
492
556
  logger: ctx.logger,
493
557
  options: this.options.db
494
558
  });
559
+ this.registerRetentionFloor(ctx, this.dbAdapter);
495
560
  try {
496
561
  ctx.replaceService?.("queue", this.dbAdapter);
497
562
  this.dbAdapter.start();
@@ -501,6 +566,37 @@ var QueueServicePlugin = class {
501
566
  }
502
567
  });
503
568
  }
569
+ /**
570
+ * [#5195] Register the adapter's retention floor with the platform
571
+ * LifecycleService. Best-effort: a kernel without a lifecycle service has no
572
+ * sweeper either, so there is no override for anything to bypass.
573
+ *
574
+ * The lookup is typed to {@link LifecycleFloorRegistrar} — the slot's
575
+ * contract as this package consumes it — rather than erased to `any`
576
+ * (#4127/#4251). The `typeof … === 'function'` probe stays because the method
577
+ * is genuinely optional (a lifecycle service predating floors), but it is now
578
+ * a check the compiler can see rather than one `any` was hiding.
579
+ */
580
+ registerRetentionFloor(ctx, adapter) {
581
+ let lifecycle;
582
+ try {
583
+ lifecycle = ctx.getService("lifecycle");
584
+ } catch {
585
+ lifecycle = void 0;
586
+ }
587
+ if (!lifecycle || typeof lifecycle.registerRetentionFloor !== "function") return;
588
+ try {
589
+ lifecycle.registerRetentionFloor(import_audit2.SysJobQueue.name, adapter.retentionFloor());
590
+ ctx.logger.info(
591
+ `QueueServicePlugin: registered a ${adapter.idempotencyWindowMs}ms retention floor on ${import_audit2.SysJobQueue.name} with the lifecycle service (settings overrides below it are rejected)`
592
+ );
593
+ } catch (err) {
594
+ ctx.logger.error(
595
+ "QueueServicePlugin: the lifecycle service rejected the sys_job_queue retention floor. A `lifecycle` settings override may now shorten sys_job_queue.retention below the idempotency window, in which case publish would silently re-accept duplicates (#5195). Fix the floor registration, or keep lifecycle.retention_overrides.sys_job_queue unset.",
596
+ err
597
+ );
598
+ }
599
+ }
504
600
  async destroy() {
505
601
  await this.dbAdapter?.stop();
506
602
  }
@@ -509,6 +605,7 @@ var QueueServicePlugin = class {
509
605
  0 && (module.exports = {
510
606
  DbQueueAdapter,
511
607
  MemoryQueueAdapter,
512
- QueueServicePlugin
608
+ QueueServicePlugin,
609
+ completedRetentionWindowMs
513
610
  });
514
611
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/queue-service-plugin.ts","../src/memory-queue-adapter.ts","../src/common.ts","../src/db-queue-adapter.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { QueueServicePlugin } from './queue-service-plugin.js';\nexport type { QueueServicePluginOptions } from './queue-service-plugin.js';\nexport { MemoryQueueAdapter } from './memory-queue-adapter.js';\nexport type { MemoryQueueAdapterOptions } from './memory-queue-adapter.js';\nexport { DbQueueAdapter } from './db-queue-adapter.js';\nexport type { DbQueueAdapterOptions } from './db-queue-adapter.js';\nexport type { JobEngine, JobClock, JobLogger } from './common.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { SysJobQueue } from '@objectstack/platform-objects/audit';\nimport { MemoryQueueAdapter } from './memory-queue-adapter.js';\nimport type { MemoryQueueAdapterOptions } from './memory-queue-adapter.js';\nimport { DbQueueAdapter } from './db-queue-adapter.js';\nimport type { DbQueueAdapterOptions } from './db-queue-adapter.js';\n\n/**\n * Configuration options for the QueueServicePlugin.\n */\nexport interface QueueServicePluginOptions {\n /**\n * Queue adapter type.\n * - 'auto' (default): use DbQueueAdapter when objectql engine available, else MemoryQueueAdapter\n * - 'db': require objectql; persists messages, retries, and DLQ to sys_job_queue\n * - 'memory': in-process MemoryQueueAdapter (non-durable, dev/test)\n */\n adapter?: 'auto' | 'db' | 'memory';\n /** Options for the memory queue adapter */\n memory?: MemoryQueueAdapterOptions;\n /** Options for the DB adapter (polling, batch, lease, idempotency window…) */\n db?: DbQueueAdapterOptions;\n}\n\n/**\n * QueueServicePlugin — Production IQueueService implementation.\n *\n * Default: registers MemoryQueueAdapter synchronously so producers can\n * publish during plugin init; upgrades to DbQueueAdapter on `kernel:ready`\n * when an ObjectQL engine is available. Subscribers registered against\n * the (now-replaced) memory queue must re-subscribe after upgrade — for\n * that reason most plugins register subscribers inside their own\n * `kernel:ready` hook, which fires after this one.\n */\nexport class QueueServicePlugin implements Plugin {\n name = 'com.objectstack.service.queue';\n /**\n * Services init() registers on every path (ADR-0116, #4131) — lets the\n * kernel name this plugin when a consumer requires one before it inits.\n */\n providesServices = ['queue'];\n /**\n * init() registers sys_job_queue through the `manifest` service\n * ObjectQLPlugin provides — order-if-present so the registration is\n * deterministic (ADR-0116, #4471). Soft, not hard: without an engine the\n * plugin degrades on purpose (in-memory queue adapter).\n */\n optionalDependencies = ['com.objectstack.engine.objectql'];\n version = '1.1.0';\n type = 'standard';\n\n private readonly options: QueueServicePluginOptions;\n private dbAdapter?: DbQueueAdapter;\n\n constructor(options: QueueServicePluginOptions = {}) {\n this.options = { adapter: 'auto', ...options };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Register sys_job_queue (also serves as DLQ view) so Studio can list/replay.\n try {\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.service.queue',\n name: 'Queue Service',\n version: '1.1.0',\n type: 'plugin',\n scope: 'system',\n defaultDatasource: 'cloud',\n namespace: 'sys',\n objects: [SysJobQueue],\n });\n } catch (err) {\n ctx.logger.warn('QueueServicePlugin: manifest service unavailable; sys_job_queue not registered', err as any);\n }\n\n const choice = this.options.adapter ?? 'auto';\n\n if (choice === 'memory') {\n const q = new MemoryQueueAdapter(this.options.memory);\n ctx.registerService('queue', q);\n ctx.logger.info('QueueServicePlugin: registered MemoryQueueAdapter');\n return;\n }\n\n // auto / db — register memory placeholder, upgrade on kernel:ready\n ctx.registerService('queue', new MemoryQueueAdapter(this.options.memory));\n\n ctx.hook('kernel:ready', async () => {\n let engine: any = null;\n try { engine = ctx.getService<any>('objectql'); }\n catch { try { engine = ctx.getService<any>('data'); } catch { /* ignore */ } }\n\n if (!engine) {\n if (choice === 'db') {\n ctx.logger.warn('QueueServicePlugin: db adapter requested but no ObjectQL engine — staying on MemoryQueueAdapter');\n } else {\n ctx.logger.info('QueueServicePlugin: no ObjectQL engine — staying on MemoryQueueAdapter');\n }\n return;\n }\n\n this.dbAdapter = new DbQueueAdapter({\n engine,\n logger: ctx.logger,\n options: this.options.db,\n });\n\n try {\n (ctx as any).replaceService?.('queue', this.dbAdapter);\n this.dbAdapter.start();\n ctx.logger.info('QueueServicePlugin: upgraded to DbQueueAdapter (sys_job_queue persistence)');\n } catch (err) {\n ctx.logger.warn('QueueServicePlugin: replaceService failed; staying on MemoryQueueAdapter', err as any);\n }\n });\n }\n\n async destroy(): Promise<void> {\n await this.dbAdapter?.stop();\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IQueueService, QueuePublishOptions, QueueMessage, QueueHandler } from '@objectstack/spec/contracts';\n\n/**\n * Configuration options for MemoryQueueAdapter.\n */\nexport interface MemoryQueueAdapterOptions {\n /** Maximum number of messages retained per queue (0 = unlimited) */\n maxQueueSize?: number;\n}\n\n/**\n * In-memory queue adapter implementing IQueueService.\n *\n * Provides synchronous in-process pub/sub delivery.\n * Suitable for single-process environments, development, and testing.\n */\nexport class MemoryQueueAdapter implements IQueueService {\n private readonly handlers = new Map<string, QueueHandler[]>();\n private readonly deadLetters: QueueMessage[] = [];\n private msgCounter = 0;\n private readonly maxQueueSize: number;\n\n constructor(options: MemoryQueueAdapterOptions = {}) {\n this.maxQueueSize = options.maxQueueSize ?? 0;\n }\n\n async publish<T = unknown>(queue: string, data: T, options?: QueuePublishOptions): Promise<string> {\n const id = `msg-${++this.msgCounter}`;\n const msg: QueueMessage<T> = {\n id,\n data,\n attempts: 0,\n timestamp: Date.now(),\n };\n\n const fns = this.handlers.get(queue) ?? [];\n if (fns.length === 0) {\n // No subscribers — retain as dead letter if within limits\n if (this.maxQueueSize === 0 || this.deadLetters.length < this.maxQueueSize) {\n this.deadLetters.push(msg);\n }\n return id;\n }\n\n const maxRetries = options?.retries ?? 0;\n for (const handler of fns) {\n let attempt = 0;\n let success = false;\n while (!success && attempt <= maxRetries) {\n try {\n msg.attempts = attempt + 1;\n await handler(msg as QueueMessage);\n success = true;\n } catch {\n attempt++;\n }\n }\n }\n\n return id;\n }\n\n async subscribe<T = unknown>(queue: string, handler: QueueHandler<T>): Promise<void> {\n const existing = this.handlers.get(queue) ?? [];\n this.handlers.set(queue, [...existing, handler as QueueHandler]);\n }\n\n async unsubscribe(queue: string): Promise<void> {\n this.handlers.delete(queue);\n }\n\n async getQueueSize(_queue: string): Promise<number> {\n // In-memory: no persistent queue depth tracking\n return 0;\n }\n\n async purge(queue: string): Promise<void> {\n this.handlers.delete(queue);\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Narrow ObjectQL engine surface used by job/queue adapters.\n * Keeps the adapter testable without booting a real kernel.\n *\n * IMPORTANT: matches the canonical engine API:\n * - find: `where:` (NOT `filter:`)\n * - update: `(table, {id, ...patch}, opts)`\n */\nexport interface JobEngine {\n find(object: string, options?: any): Promise<any[]>;\n insert(object: string, data: any, options?: any): Promise<any>;\n update(object: string, idOrData: any, dataOrOptions?: any, options?: any): Promise<any>;\n delete(object: string, options?: any): Promise<any>;\n}\n\n/** Stamped only in tests to make `now` deterministic. */\nexport interface JobClock { now(): Date }\n\nexport interface JobLogger {\n info(msg: string, meta?: unknown): void;\n warn(msg: string, meta?: unknown): void;\n error?(msg: string, meta?: unknown): void;\n}\n\nexport const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;\n\nexport function uid(prefix: string): string {\n const g: any = globalThis as any;\n if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;\n return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nexport function nowIso(clock?: JobClock): string {\n return (clock?.now() ?? new Date()).toISOString();\n}\n\nexport function parseJson<T = unknown>(raw: unknown, fallback?: T): T | undefined {\n if (raw == null) return fallback;\n if (typeof raw === 'string') {\n try { return JSON.parse(raw) as T; } catch { return fallback; }\n }\n if (typeof raw === 'object') return raw as T;\n return fallback;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IQueueService,\n QueuePublishOptions,\n QueueMessage,\n QueueMessageRecord,\n QueueHandler,\n} from '@objectstack/spec/contracts';\nimport {\n SYSTEM_CTX,\n uid,\n nowIso,\n parseJson,\n type JobEngine,\n type JobClock,\n type JobLogger,\n} from './common.js';\n\nconst QUEUE_TABLE = 'sys_job_queue';\n\nexport interface DbQueueAdapterOptions {\n /** Polling interval for the worker loop (ms, default 1000) */\n pollIntervalMs?: number;\n /** Max messages claimed per poll tick (default 10) */\n batchSize?: number;\n /** Lease duration before another worker may reclaim (ms, default 30000) */\n leaseMs?: number;\n /** Idempotency window — how long the same key blocks re-publish (ms, default 24h) */\n idempotencyWindowMs?: number;\n /** Default maxAttempts when publish doesn't specify (default 3) */\n defaultMaxAttempts?: number;\n /** Unique identifier for this worker (default: random) */\n workerId?: string;\n /** Whether to auto-start the polling worker (default true) */\n autoStart?: boolean;\n}\n\ninterface RegisteredHandler {\n queue: string;\n fn: QueueHandler;\n}\n\n/**\n * DbQueueAdapter — durable, polling, DB-backed IQueueService.\n *\n * Persists every message to `sys_job_queue`. A polling worker leases\n * pending messages (CAS update status pending→running with a lease),\n * invokes registered subscribers, and retries with backoff on failure.\n * Messages that exceed `max_attempts` land in `status='dlq'`.\n *\n * Idempotency: publish suppresses duplicates within a configurable\n * window when `(queue, idempotencyKey)` is non-null.\n *\n * Designed for SQLite and Postgres alike — uses CAS via WHERE-clauses,\n * not row-level locking.\n */\nexport class DbQueueAdapter implements IQueueService {\n private readonly engine: JobEngine;\n private readonly logger?: JobLogger;\n private readonly clock?: JobClock;\n private readonly opts: Required<Omit<DbQueueAdapterOptions, 'workerId'>> & { workerId: string };\n\n private readonly handlers = new Map<string, RegisteredHandler[]>();\n private timer?: ReturnType<typeof setInterval>;\n private running = false;\n\n constructor(args: {\n engine: JobEngine;\n logger?: JobLogger;\n clock?: JobClock;\n options?: DbQueueAdapterOptions;\n }) {\n this.engine = args.engine;\n this.logger = args.logger;\n this.clock = args.clock;\n const o = args.options ?? {};\n this.opts = {\n pollIntervalMs: o.pollIntervalMs ?? 1000,\n batchSize: o.batchSize ?? 10,\n leaseMs: o.leaseMs ?? 30_000,\n idempotencyWindowMs: o.idempotencyWindowMs ?? 24 * 60 * 60 * 1000,\n defaultMaxAttempts: o.defaultMaxAttempts ?? 3,\n autoStart: o.autoStart ?? true,\n workerId: o.workerId ?? uid('worker'),\n };\n }\n\n // ── IQueueService ────────────────────────────────────────────────\n\n async publish<T = unknown>(\n queue: string,\n data: T,\n options?: QueuePublishOptions,\n ): Promise<string> {\n const opts = options ?? {};\n const now = this.now();\n\n // Idempotency check\n if (opts.idempotencyKey) {\n const windowStart = new Date(now.getTime() - this.opts.idempotencyWindowMs).toISOString();\n const existing = await this.engine.find(QUEUE_TABLE, {\n where: {\n queue,\n idempotency_key: opts.idempotencyKey,\n // Only block if not yet terminal — completed/dlq dedup is by window via created_at\n },\n limit: 5,\n context: SYSTEM_CTX,\n });\n const blocking = (existing ?? []).find((row: any) => {\n if (row.status === 'pending' || row.status === 'running') return true;\n return String(row.created_at ?? '') >= windowStart;\n });\n if (blocking) return String(blocking.id);\n }\n\n const id = uid('msg');\n const scheduledFor = opts.scheduledFor\n ? new Date(opts.scheduledFor).toISOString()\n : opts.delay\n ? new Date(now.getTime() + opts.delay).toISOString()\n : now.toISOString();\n\n const maxAttempts = opts.maxAttempts\n ?? (opts.retries != null ? opts.retries + 1 : this.opts.defaultMaxAttempts);\n const backoff = opts.backoff ?? { type: 'exponential' as const, delayMs: 1000 };\n\n await this.engine.insert(QUEUE_TABLE, {\n id,\n queue,\n idempotency_key: opts.idempotencyKey ?? null,\n payload_json: JSON.stringify(data ?? null),\n metadata_json: opts.metadata ? JSON.stringify(opts.metadata) : null,\n status: 'pending',\n priority: opts.priority ?? 100,\n attempts: 0,\n max_attempts: maxAttempts,\n backoff_type: backoff.type,\n backoff_delay_ms: backoff.delayMs,\n backoff_max_delay_ms: backoff.maxDelayMs ?? null,\n scheduled_for: scheduledFor,\n created_at: now.toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n\n return id;\n }\n\n async subscribe<T = unknown>(queue: string, handler: QueueHandler<T>): Promise<void> {\n const existing = this.handlers.get(queue) ?? [];\n existing.push({ queue, fn: handler as QueueHandler });\n this.handlers.set(queue, existing);\n if (this.opts.autoStart) this.start();\n }\n\n async unsubscribe(queue: string): Promise<void> {\n this.handlers.delete(queue);\n }\n\n async getQueueSize(queue: string): Promise<number> {\n const rows = await this.engine.find(QUEUE_TABLE, {\n where: { queue, status: 'pending' },\n limit: 10_000,\n context: SYSTEM_CTX,\n });\n return rows?.length ?? 0;\n }\n\n async purge(queue: string): Promise<void> {\n const rows = await this.engine.find(QUEUE_TABLE, {\n where: { queue, status: 'pending' },\n limit: 10_000,\n context: SYSTEM_CTX,\n });\n for (const row of rows ?? []) {\n // `where: { id }` — the engine's delete has no top-level `id` option.\n // The old `{ id: row.id }` bag carried no predicate at all, so every\n // purge delete threw \"Delete requires an ID or options.multi=true\"\n // straight into this catch: purge logged a warn per row and deleted\n // NOTHING (#4371 option-2 survey).\n try { await this.engine.delete(QUEUE_TABLE, { where: { id: row.id }, context: SYSTEM_CTX }); }\n catch (err) { this.logger?.warn?.('DbQueueAdapter: purge delete failed', err as any); }\n }\n }\n\n async listFailed(\n queue?: string,\n options?: { limit?: number; offset?: number },\n ): Promise<QueueMessageRecord[]> {\n const where: any = { status: 'dlq' };\n if (queue) where.queue = queue;\n const rows = await this.engine.find(QUEUE_TABLE, {\n where,\n limit: options?.limit ?? 100,\n offset: options?.offset,\n orderBy: [{ field: 'created_at', order: 'desc' }],\n context: SYSTEM_CTX,\n });\n return (rows ?? []).map((r: any) => this.rowToRecord(r));\n }\n\n async replay(messageId: string): Promise<void> {\n const row = await this.loadById(messageId);\n if (!row) throw new Error(`MESSAGE_NOT_FOUND: ${messageId}`);\n if (row.status !== 'dlq' && row.status !== 'failed') {\n throw new Error(`INVALID_STATE: cannot replay message in status=${row.status}`);\n }\n const now = this.now();\n await this.engine.update(QUEUE_TABLE, {\n id: messageId,\n status: 'pending',\n attempts: 0,\n last_error: null,\n locked_by: null,\n locked_until: null,\n scheduled_for: now.toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n }\n\n async purgeFailed(messageId: string): Promise<void> {\n const row = await this.loadById(messageId);\n if (!row) return;\n if (row.status !== 'dlq' && row.status !== 'failed') {\n throw new Error(`INVALID_STATE: cannot purge message in status=${row.status}`);\n }\n await this.engine.delete(QUEUE_TABLE, { where: { id: messageId }, context: SYSTEM_CTX });\n }\n\n // ── Worker lifecycle ─────────────────────────────────────────────\n\n start(): void {\n if (this.timer) return;\n this.timer = setInterval(() => {\n if (this.running) return;\n this.running = true;\n this.pollOnce()\n .catch((err) => { this.logger?.warn?.('DbQueueAdapter: poll tick failed', err); })\n .finally(() => { this.running = false; });\n }, this.opts.pollIntervalMs);\n (this.timer as any)?.unref?.();\n }\n\n async stop(): Promise<void> {\n if (this.timer) { clearInterval(this.timer); this.timer = undefined; }\n }\n\n /** Test-friendly synchronous poll. */\n async pollOnce(): Promise<number> {\n const queues = [...this.handlers.keys()];\n if (queues.length === 0) return 0;\n\n let processed = 0;\n for (const queue of queues) {\n const claimed = await this.claimBatch(queue, this.opts.batchSize);\n for (const row of claimed) {\n await this.dispatch(row);\n processed++;\n }\n }\n return processed;\n }\n\n // ── Internals ────────────────────────────────────────────────────\n\n private async claimBatch(queue: string, max: number): Promise<any[]> {\n const now = this.now();\n const candidates = await this.engine.find(QUEUE_TABLE, {\n where: { queue, status: 'pending' },\n limit: max * 3, // over-fetch in case of CAS contention\n orderBy: [\n { field: 'priority', order: 'asc' },\n { field: 'scheduled_for', order: 'asc' },\n ],\n context: SYSTEM_CTX,\n });\n\n const out: any[] = [];\n for (const row of candidates ?? []) {\n if (out.length >= max) break;\n const sched = row.scheduled_for ? new Date(row.scheduled_for).getTime() : 0;\n if (sched > now.getTime()) continue;\n // Honor existing lease\n const lockedUntil = row.locked_until ? new Date(row.locked_until).getTime() : 0;\n if (row.locked_by && lockedUntil > now.getTime()) continue;\n\n // CAS — only update if still pending (best-effort with engine.update which\n // typically does row-level update by id; concurrent workers will overwrite\n // each other but the dispatcher tolerates duplicate delivery via attempts).\n try {\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'running',\n locked_by: this.opts.workerId,\n locked_until: new Date(now.getTime() + this.opts.leaseMs).toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n out.push({ ...row, status: 'running' });\n } catch (err) {\n this.logger?.warn?.('DbQueueAdapter: claim CAS failed', err as any);\n }\n }\n return out;\n }\n\n private async dispatch(row: any): Promise<void> {\n const handlers = this.handlers.get(row.queue) ?? [];\n if (handlers.length === 0) {\n // No handler — release lease so another process can pick it up\n await this.releasePending(row.id);\n return;\n }\n\n const msg: QueueMessage = {\n id: String(row.id),\n data: parseJson(row.payload_json),\n attempts: (row.attempts ?? 0) + 1,\n timestamp: row.created_at ? new Date(row.created_at).getTime() : Date.now(),\n };\n\n let success = true;\n let lastError: string | undefined;\n for (const h of handlers) {\n try { await h.fn(msg); }\n catch (err) {\n success = false;\n lastError = err instanceof Error ? err.message : String(err);\n this.logger?.warn?.(`DbQueueAdapter: handler failed on ${row.queue}`, err as any);\n break;\n }\n }\n\n const now = this.now();\n if (success) {\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'completed',\n attempts: msg.attempts,\n completed_at: now.toISOString(),\n locked_by: null,\n locked_until: null,\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n return;\n }\n\n const attempts = msg.attempts;\n const max = row.max_attempts ?? this.opts.defaultMaxAttempts;\n if (attempts >= max) {\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'dlq',\n attempts,\n last_error: lastError ?? 'unknown error',\n completed_at: now.toISOString(),\n locked_by: null,\n locked_until: null,\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n return;\n }\n\n const backoffMs = this.computeBackoff(row, attempts);\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'pending',\n attempts,\n last_error: lastError ?? 'unknown error',\n scheduled_for: new Date(now.getTime() + backoffMs).toISOString(),\n locked_by: null,\n locked_until: null,\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n }\n\n private computeBackoff(row: any, attempt: number): number {\n const base = row.backoff_delay_ms ?? 1000;\n const cap = row.backoff_max_delay_ms ?? undefined;\n if ((row.backoff_type ?? 'exponential') === 'fixed') return base;\n const exp = base * Math.pow(2, Math.max(0, attempt - 1));\n return cap ? Math.min(exp, cap) : exp;\n }\n\n private async releasePending(id: string): Promise<void> {\n const now = this.now();\n try {\n await this.engine.update(QUEUE_TABLE, {\n id,\n status: 'pending',\n locked_by: null,\n locked_until: null,\n scheduled_for: new Date(now.getTime() + this.opts.pollIntervalMs * 5).toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n } catch (err) {\n this.logger?.warn?.('DbQueueAdapter: release failed', err as any);\n }\n }\n\n private async loadById(id: string): Promise<any | null> {\n const rows = await this.engine.find(QUEUE_TABLE, {\n where: { id },\n limit: 1,\n context: SYSTEM_CTX,\n });\n return rows?.[0] ?? null;\n }\n\n private rowToRecord(r: any): QueueMessageRecord {\n return {\n id: String(r.id),\n queue: String(r.queue),\n data: parseJson(r.payload_json),\n status: r.status,\n attempts: r.attempts ?? 0,\n maxAttempts: r.max_attempts ?? this.opts.defaultMaxAttempts,\n scheduledFor: r.scheduled_for ?? undefined,\n lockedBy: r.locked_by ?? undefined,\n lockedUntil: r.locked_until ?? undefined,\n lastError: r.last_error ?? undefined,\n idempotencyKey: r.idempotency_key ?? undefined,\n metadata: parseJson(r.metadata_json),\n createdAt: r.created_at ?? nowIso(this.clock),\n updatedAt: r.updated_at ?? undefined,\n completedAt: r.completed_at ?? undefined,\n };\n }\n\n private now(): Date {\n return this.clock?.now() ?? new Date();\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/queue-service-plugin.ts","../src/memory-queue-adapter.ts","../src/db-queue-adapter.ts","../src/common.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { QueueServicePlugin } from './queue-service-plugin.js';\nexport type { QueueServicePluginOptions } from './queue-service-plugin.js';\nexport { MemoryQueueAdapter } from './memory-queue-adapter.js';\nexport type { MemoryQueueAdapterOptions } from './memory-queue-adapter.js';\nexport { DbQueueAdapter, completedRetentionWindowMs } from './db-queue-adapter.js';\nexport type { DbQueueAdapterOptions } from './db-queue-adapter.js';\nexport type { JobEngine, JobClock, JobLogger } from './common.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { SysJobQueue } from '@objectstack/platform-objects/audit';\nimport { MemoryQueueAdapter } from './memory-queue-adapter.js';\nimport type { MemoryQueueAdapterOptions } from './memory-queue-adapter.js';\nimport { DbQueueAdapter } from './db-queue-adapter.js';\nimport type { DbQueueAdapterOptions, LifecycleFloorRegistrar } from './db-queue-adapter.js';\n\n/**\n * Configuration options for the QueueServicePlugin.\n */\nexport interface QueueServicePluginOptions {\n /**\n * Queue adapter type.\n * - 'auto' (default): use DbQueueAdapter when objectql engine available, else MemoryQueueAdapter\n * - 'db': require objectql; persists messages, retries, and DLQ to sys_job_queue\n * - 'memory': in-process MemoryQueueAdapter (non-durable, dev/test)\n */\n adapter?: 'auto' | 'db' | 'memory';\n /** Options for the memory queue adapter */\n memory?: MemoryQueueAdapterOptions;\n /** Options for the DB adapter (polling, batch, lease, idempotency window…) */\n db?: DbQueueAdapterOptions;\n}\n\n/**\n * QueueServicePlugin — Production IQueueService implementation.\n *\n * Default: registers MemoryQueueAdapter synchronously so producers can\n * publish during plugin init; upgrades to DbQueueAdapter on `kernel:ready`\n * when an ObjectQL engine is available. Subscribers registered against\n * the (now-replaced) memory queue must re-subscribe after upgrade — for\n * that reason most plugins register subscribers inside their own\n * `kernel:ready` hook, which fires after this one.\n */\nexport class QueueServicePlugin implements Plugin {\n name = 'com.objectstack.service.queue';\n /**\n * Services init() registers on every path (ADR-0116, #4131) — lets the\n * kernel name this plugin when a consumer requires one before it inits.\n */\n providesServices = ['queue'];\n /**\n * init() registers sys_job_queue through the `manifest` service\n * ObjectQLPlugin provides — order-if-present so the registration is\n * deterministic (ADR-0116, #4471). Soft, not hard: without an engine the\n * plugin degrades on purpose (in-memory queue adapter).\n */\n optionalDependencies = ['com.objectstack.engine.objectql'];\n version = '1.1.0';\n type = 'standard';\n\n private readonly options: QueueServicePluginOptions;\n private dbAdapter?: DbQueueAdapter;\n\n constructor(options: QueueServicePluginOptions = {}) {\n this.options = { adapter: 'auto', ...options };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Register sys_job_queue (also serves as DLQ view) so Studio can list/replay.\n try {\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.service.queue',\n name: 'Queue Service',\n version: '1.1.0',\n type: 'plugin',\n scope: 'system',\n defaultDatasource: 'cloud',\n namespace: 'sys',\n objects: [SysJobQueue],\n });\n } catch (err) {\n ctx.logger.warn('QueueServicePlugin: manifest service unavailable; sys_job_queue not registered', err as any);\n }\n\n const choice = this.options.adapter ?? 'auto';\n\n if (choice === 'memory') {\n const q = new MemoryQueueAdapter(this.options.memory);\n ctx.registerService('queue', q);\n ctx.logger.info('QueueServicePlugin: registered MemoryQueueAdapter');\n return;\n }\n\n // auto / db — register memory placeholder, upgrade on kernel:ready\n ctx.registerService('queue', new MemoryQueueAdapter(this.options.memory));\n\n ctx.hook('kernel:ready', async () => {\n let engine: any = null;\n try { engine = ctx.getService<any>('objectql'); }\n catch { try { engine = ctx.getService<any>('data'); } catch { /* ignore */ } }\n\n if (!engine) {\n if (choice === 'db') {\n ctx.logger.warn('QueueServicePlugin: db adapter requested but no ObjectQL engine — staying on MemoryQueueAdapter');\n } else {\n ctx.logger.info('QueueServicePlugin: no ObjectQL engine — staying on MemoryQueueAdapter');\n }\n return;\n }\n\n this.dbAdapter = new DbQueueAdapter({\n engine,\n logger: ctx.logger,\n options: this.options.db,\n });\n\n // [#5195] Tell the LifecycleService how short sys_job_queue's retention\n // may get. The adapter's constructor already refuses an idempotency\n // window longer than the DECLARED retention (#5179), but ADR-0057 P4\n // overrides live in the `lifecycle` settings namespace, which the\n // constructor never sees — an operator setting `maxAge: '1h'` would reap\n // the rows publish dedups against and duplicate deliveries would resume\n // silently. The floor carries the window this adapter was actually\n // constructed with, so a non-default `db.idempotencyWindowMs` is covered\n // too.\n this.registerRetentionFloor(ctx, this.dbAdapter);\n\n try {\n (ctx as any).replaceService?.('queue', this.dbAdapter);\n this.dbAdapter.start();\n ctx.logger.info('QueueServicePlugin: upgraded to DbQueueAdapter (sys_job_queue persistence)');\n } catch (err) {\n ctx.logger.warn('QueueServicePlugin: replaceService failed; staying on MemoryQueueAdapter', err as any);\n }\n });\n }\n\n /**\n * [#5195] Register the adapter's retention floor with the platform\n * LifecycleService. Best-effort: a kernel without a lifecycle service has no\n * sweeper either, so there is no override for anything to bypass.\n *\n * The lookup is typed to {@link LifecycleFloorRegistrar} — the slot's\n * contract as this package consumes it — rather than erased to `any`\n * (#4127/#4251). The `typeof … === 'function'` probe stays because the method\n * is genuinely optional (a lifecycle service predating floors), but it is now\n * a check the compiler can see rather than one `any` was hiding.\n */\n private registerRetentionFloor(ctx: PluginContext, adapter: DbQueueAdapter): void {\n let lifecycle: LifecycleFloorRegistrar | undefined;\n try {\n lifecycle = ctx.getService<LifecycleFloorRegistrar>('lifecycle');\n } catch {\n lifecycle = undefined;\n }\n if (!lifecycle || typeof lifecycle.registerRetentionFloor !== 'function') return;\n try {\n lifecycle.registerRetentionFloor(SysJobQueue.name, adapter.retentionFloor());\n ctx.logger.info(\n `QueueServicePlugin: registered a ${adapter.idempotencyWindowMs}ms retention floor on ${SysJobQueue.name} `\n + 'with the lifecycle service (settings overrides below it are rejected)',\n );\n } catch (err) {\n // A floor the service refused is a wiring bug in THIS plugin, not a\n // degraded deployment — but it must not stop the queue from coming up.\n ctx.logger.error(\n 'QueueServicePlugin: the lifecycle service rejected the sys_job_queue retention floor. A `lifecycle` '\n + 'settings override may now shorten sys_job_queue.retention below the idempotency window, in which case '\n + 'publish would silently re-accept duplicates (#5195). Fix the floor registration, or keep '\n + 'lifecycle.retention_overrides.sys_job_queue unset.',\n err as any,\n );\n }\n }\n\n async destroy(): Promise<void> {\n await this.dbAdapter?.stop();\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IQueueService, QueuePublishOptions, QueueMessage, QueueHandler } from '@objectstack/spec/contracts';\n\n/**\n * Configuration options for MemoryQueueAdapter.\n */\nexport interface MemoryQueueAdapterOptions {\n /** Maximum number of messages retained per queue (0 = unlimited) */\n maxQueueSize?: number;\n}\n\n/**\n * In-memory queue adapter implementing IQueueService.\n *\n * Provides synchronous in-process pub/sub delivery.\n * Suitable for single-process environments, development, and testing.\n */\nexport class MemoryQueueAdapter implements IQueueService {\n private readonly handlers = new Map<string, QueueHandler[]>();\n private readonly deadLetters: QueueMessage[] = [];\n private msgCounter = 0;\n private readonly maxQueueSize: number;\n\n constructor(options: MemoryQueueAdapterOptions = {}) {\n this.maxQueueSize = options.maxQueueSize ?? 0;\n }\n\n async publish<T = unknown>(queue: string, data: T, options?: QueuePublishOptions): Promise<string> {\n const id = `msg-${++this.msgCounter}`;\n const msg: QueueMessage<T> = {\n id,\n data,\n attempts: 0,\n timestamp: Date.now(),\n };\n\n const fns = this.handlers.get(queue) ?? [];\n if (fns.length === 0) {\n // No subscribers — retain as dead letter if within limits\n if (this.maxQueueSize === 0 || this.deadLetters.length < this.maxQueueSize) {\n this.deadLetters.push(msg);\n }\n return id;\n }\n\n const maxRetries = options?.retries ?? 0;\n for (const handler of fns) {\n let attempt = 0;\n let success = false;\n while (!success && attempt <= maxRetries) {\n try {\n msg.attempts = attempt + 1;\n await handler(msg as QueueMessage);\n success = true;\n } catch {\n attempt++;\n }\n }\n }\n\n return id;\n }\n\n async subscribe<T = unknown>(queue: string, handler: QueueHandler<T>): Promise<void> {\n const existing = this.handlers.get(queue) ?? [];\n this.handlers.set(queue, [...existing, handler as QueueHandler]);\n }\n\n async unsubscribe(queue: string): Promise<void> {\n this.handlers.delete(queue);\n }\n\n async getQueueSize(_queue: string): Promise<number> {\n // In-memory: no persistent queue depth tracking\n return 0;\n }\n\n async purge(queue: string): Promise<void> {\n this.handlers.delete(queue);\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IQueueService,\n QueuePublishOptions,\n QueueMessage,\n QueueMessageRecord,\n QueueHandler,\n} from '@objectstack/spec/contracts';\nimport { SysJobQueue } from '@objectstack/platform-objects/audit';\nimport {\n SYSTEM_CTX,\n uid,\n nowIso,\n parseJson,\n lifecycleDurationMs,\n type JobEngine,\n type JobClock,\n type JobLogger,\n} from './common.js';\n\nconst QUEUE_TABLE = 'sys_job_queue';\n\n/**\n * How long a `completed` row survives before the platform Reaper deletes it.\n *\n * Read from the object's own ADR-0057 declaration\n * (`sys_job_queue.lifecycle.retention`, #5179) instead of being a second\n * number here: the declaration is what actually runs (LifecycleService sweeps\n * every registered object hourly), so a copy in this file could only ever be\n * a copy that drifts. A missing or unparseable declaration throws: the queue's\n * dedup contract below is defined against this window, so \"no window\" is not a\n * state the adapter can run in.\n */\nexport function completedRetentionWindowMs(): number {\n const maxAge = SysJobQueue.lifecycle?.retention?.maxAge;\n if (!maxAge) {\n throw new Error(\n '[service-queue] sys_job_queue no longer declares lifecycle.retention — DbQueueAdapter dedups against '\n + 'terminal rows by `created_at` window and relies on that declared retention to keep them (ADR-0057, #5179). '\n + 'Restore the declaration in @objectstack/platform-objects rather than sweeping the table from here.',\n );\n }\n return lifecycleDurationMs(maxAge);\n}\n\n/**\n * [#5195] The shape `LifecycleService.registerRetentionFloor()` accepts.\n *\n * Restated here rather than imported: `@objectstack/objectql` is a\n * devDependency of this package on purpose (the queue must not drag the engine\n * into every install), so its types are not available to this package's\n * consumers at build time.\n */\nexport interface QueueRetentionFloor {\n policy: 'retention';\n minWindowMs: number;\n declaredBy: string;\n consequence: string;\n remedy: string;\n}\n\n/**\n * [#5195] The `lifecycle` slot's contract as THIS package consumes it — the one\n * method `QueueServicePlugin` calls, and nothing else.\n *\n * Declared rather than erased to `any` at the lookup (#4127/#4251): `any` would\n * switch off checking on the single call that carries the floor, so a rename or\n * a changed argument order in `LifecycleService.registerRetentionFloor` would\n * compile here and fail at runtime inside a `try` that logs and continues —\n * i.e. the floor would silently not exist, which is precisely the silent\n * bypass #5195 exists to close.\n *\n * `registerRetentionFloor` is **optional** on purpose, and that optionality is\n * the honest part of the contract: a kernel may carry a lifecycle service that\n * predates floors, so the runtime `typeof … === 'function'` probe below is a\n * real check and the type says so, instead of an `any` that hides both the\n * check and the call.\n */\nexport interface LifecycleFloorRegistrar {\n registerRetentionFloor?(object: string, floor: QueueRetentionFloor): void;\n}\n\nexport interface DbQueueAdapterOptions {\n /** Polling interval for the worker loop (ms, default 1000) */\n pollIntervalMs?: number;\n /** Max messages claimed per poll tick (default 10) */\n batchSize?: number;\n /** Lease duration before another worker may reclaim (ms, default 30000) */\n leaseMs?: number;\n /**\n * Idempotency window — how long the same key blocks re-publish (ms, default 24h).\n *\n * Must not exceed `sys_job_queue`'s declared retention for `completed` rows\n * ({@link completedRetentionWindowMs}, 7d): the window is evaluated against\n * rows that are still in the table, so a longer window would silently start\n * accepting duplicates as soon as the Reaper swept the row it dedups\n * against. The constructor rejects that configuration (#5179).\n */\n idempotencyWindowMs?: number;\n /** Default maxAttempts when publish doesn't specify (default 3) */\n defaultMaxAttempts?: number;\n /** Unique identifier for this worker (default: random) */\n workerId?: string;\n /** Whether to auto-start the polling worker (default true) */\n autoStart?: boolean;\n}\n\ninterface RegisteredHandler {\n queue: string;\n fn: QueueHandler;\n}\n\n/**\n * DbQueueAdapter — durable, polling, DB-backed IQueueService.\n *\n * Persists every message to `sys_job_queue`. A polling worker leases\n * pending messages (CAS update status pending→running with a lease),\n * invokes registered subscribers, and retries with backoff on failure.\n * Messages that exceed `max_attempts` land in `status='dlq'`.\n *\n * Idempotency: publish suppresses duplicates within a configurable\n * window when `(queue, idempotencyKey)` is non-null.\n *\n * Retention: this adapter does NOT sweep the table. `completed` rows are\n * bounded by `sys_job_queue`'s declared ADR-0057 retention (7d, filtered to\n * `status='completed'`), enforced by the one platform-owned\n * `LifecycleService` reaper — see the object definition in\n * `@objectstack/platform-objects` and {@link completedRetentionWindowMs}.\n * `dlq`/`failed` rows are never swept; they are the dead-letter surface\n * ({@link DbQueueAdapter.listFailed} / {@link DbQueueAdapter.replay} /\n * {@link DbQueueAdapter.purgeFailed}).\n *\n * Designed for SQLite and Postgres alike — uses CAS via WHERE-clauses,\n * not row-level locking.\n */\nexport class DbQueueAdapter implements IQueueService {\n private readonly engine: JobEngine;\n private readonly logger?: JobLogger;\n private readonly clock?: JobClock;\n private readonly opts: Required<Omit<DbQueueAdapterOptions, 'workerId'>> & { workerId: string };\n\n private readonly handlers = new Map<string, RegisteredHandler[]>();\n private timer?: ReturnType<typeof setInterval>;\n private running = false;\n\n constructor(args: {\n engine: JobEngine;\n logger?: JobLogger;\n clock?: JobClock;\n options?: DbQueueAdapterOptions;\n }) {\n this.engine = args.engine;\n this.logger = args.logger;\n this.clock = args.clock;\n const o = args.options ?? {};\n this.opts = {\n pollIntervalMs: o.pollIntervalMs ?? 1000,\n batchSize: o.batchSize ?? 10,\n leaseMs: o.leaseMs ?? 30_000,\n idempotencyWindowMs: o.idempotencyWindowMs ?? 24 * 60 * 60 * 1000,\n defaultMaxAttempts: o.defaultMaxAttempts ?? 3,\n autoStart: o.autoStart ?? true,\n workerId: o.workerId ?? uid('worker'),\n };\n\n // [#5179] The dedup window only means anything while the row it dedups\n // against still exists. `completed` rows now expire on the declared\n // retention window, so an idempotency window LONGER than it would quietly\n // degrade into \"dedup for as long as the Reaper happens not to have run\" —\n // duplicate deliveries appearing days later, with nothing in any log. The\n // two windows are ordered here, at construction, rather than tolerated at\n // publish time: the fix is a config or declaration change, and both are\n // named in the message.\n const retentionMs = completedRetentionWindowMs();\n if (this.opts.idempotencyWindowMs > retentionMs) {\n throw new Error(\n `[service-queue] idempotencyWindowMs (${this.opts.idempotencyWindowMs}ms) exceeds the retention window `\n + `sys_job_queue declares for completed rows (${retentionMs}ms, lifecycle.retention.maxAge — ADR-0057). `\n + 'Terminal-row dedup is evaluated by `created_at` against that same window, so the longer setting would '\n + 'silently accept duplicates once a row is reaped. Lower idempotencyWindowMs, or raise the declared '\n + 'retention (both windows are measured from `created_at`).',\n );\n }\n }\n\n /** The configured dedup window (ms) — the number the floor below is made of. */\n get idempotencyWindowMs(): number {\n return this.opts.idempotencyWindowMs;\n }\n\n /**\n * [#5195] The retention floor `sys_job_queue` must satisfy for this adapter's\n * dedup contract to mean anything, handed to `LifecycleService`\n * (`registerRetentionFloor`) by `QueueServicePlugin`.\n *\n * The constructor check above only reads the object's **declaration**. ADR-0057\n * P4 lets an operator override that window per environment/tenant through the\n * `lifecycle` settings namespace, which the constructor cannot see: set\n * `lifecycle.retention_overrides.sys_job_queue.maxAge = '1h'` and completed\n * rows vanish an hour after they are written while publish keeps dedupping\n * against a 24h window — duplicate deliveries resume, with nothing in any log.\n * Registering the floor is what closes that door, and it carries the number\n * this adapter was actually CONSTRUCTED with rather than a static copy of the\n * default (a per-kernel option cannot live in the object's declaration).\n */\n retentionFloor(): QueueRetentionFloor {\n const ms = this.opts.idempotencyWindowMs;\n // Settings are authored as ADR-0057 duration literals, not milliseconds, so\n // the remedy quotes one the operator can paste — rounded UP, since a\n // rounded-down literal would be rejected by the very floor it is meant to\n // satisfy.\n const literal = `${Math.ceil(ms / 3_600_000)}h`;\n return {\n policy: 'retention',\n minWindowMs: ms,\n declaredBy: 'com.objectstack.service.queue',\n consequence:\n `DbQueueAdapter dedups sys_job_queue publishes by comparing created_at against its ${ms}ms `\n + 'idempotency window, so a shorter retention deletes the very rows that check reads — '\n + 'duplicate deliveries resume silently, with nothing in any log.',\n remedy:\n `set lifecycle.retention_overrides.sys_job_queue.maxAge to '${literal}' or longer, or lower `\n + \"QueueServicePlugin's db.idempotencyWindowMs to the window you actually want (both are measured \"\n + 'from created_at).',\n };\n }\n\n // ── IQueueService ────────────────────────────────────────────────\n\n async publish<T = unknown>(\n queue: string,\n data: T,\n options?: QueuePublishOptions,\n ): Promise<string> {\n const opts = options ?? {};\n const now = this.now();\n\n // Idempotency check.\n //\n // [#5179] This is the reason `sys_job_queue`'s retention is filtered and\n // generous rather than aggressive: a terminal (`completed`/`dlq`) row\n // blocks a re-publish only while its `created_at` is inside the\n // idempotency window, so the row must SURVIVE that long. The declared\n // retention (7d on `completed`, nothing on `dlq`) is measured on the very\n // same `created_at` axis and is ≥ this window — enforced in the\n // constructor — which makes \"the reaper deleted a row the dedup check\n // needed\" unrepresentable rather than merely unlikely.\n if (opts.idempotencyKey) {\n const windowStart = new Date(now.getTime() - this.opts.idempotencyWindowMs).toISOString();\n const existing = await this.engine.find(QUEUE_TABLE, {\n where: {\n queue,\n idempotency_key: opts.idempotencyKey,\n // Only block if not yet terminal — completed/dlq dedup is by window via created_at\n },\n limit: 5,\n context: SYSTEM_CTX,\n });\n const blocking = (existing ?? []).find((row: any) => {\n if (row.status === 'pending' || row.status === 'running') return true;\n return String(row.created_at ?? '') >= windowStart;\n });\n if (blocking) return String(blocking.id);\n }\n\n const id = uid('msg');\n const scheduledFor = opts.scheduledFor\n ? new Date(opts.scheduledFor).toISOString()\n : opts.delay\n ? new Date(now.getTime() + opts.delay).toISOString()\n : now.toISOString();\n\n const maxAttempts = opts.maxAttempts\n ?? (opts.retries != null ? opts.retries + 1 : this.opts.defaultMaxAttempts);\n const backoff = opts.backoff ?? { type: 'exponential' as const, delayMs: 1000 };\n\n await this.engine.insert(QUEUE_TABLE, {\n id,\n queue,\n idempotency_key: opts.idempotencyKey ?? null,\n payload_json: JSON.stringify(data ?? null),\n metadata_json: opts.metadata ? JSON.stringify(opts.metadata) : null,\n status: 'pending',\n priority: opts.priority ?? 100,\n attempts: 0,\n max_attempts: maxAttempts,\n backoff_type: backoff.type,\n backoff_delay_ms: backoff.delayMs,\n backoff_max_delay_ms: backoff.maxDelayMs ?? null,\n scheduled_for: scheduledFor,\n created_at: now.toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n\n return id;\n }\n\n async subscribe<T = unknown>(queue: string, handler: QueueHandler<T>): Promise<void> {\n const existing = this.handlers.get(queue) ?? [];\n existing.push({ queue, fn: handler as QueueHandler });\n this.handlers.set(queue, existing);\n if (this.opts.autoStart) this.start();\n }\n\n async unsubscribe(queue: string): Promise<void> {\n this.handlers.delete(queue);\n }\n\n async getQueueSize(queue: string): Promise<number> {\n const rows = await this.engine.find(QUEUE_TABLE, {\n where: { queue, status: 'pending' },\n limit: 10_000,\n context: SYSTEM_CTX,\n });\n return rows?.length ?? 0;\n }\n\n async purge(queue: string): Promise<void> {\n const rows = await this.engine.find(QUEUE_TABLE, {\n where: { queue, status: 'pending' },\n limit: 10_000,\n context: SYSTEM_CTX,\n });\n for (const row of rows ?? []) {\n // `where: { id }` — the engine's delete has no top-level `id` option.\n // The old `{ id: row.id }` bag carried no predicate at all, so every\n // purge delete threw \"Delete requires an ID or options.multi=true\"\n // straight into this catch: purge logged a warn per row and deleted\n // NOTHING (#4371 option-2 survey).\n try { await this.engine.delete(QUEUE_TABLE, { where: { id: row.id }, context: SYSTEM_CTX }); }\n catch (err) { this.logger?.warn?.('DbQueueAdapter: purge delete failed', err as any); }\n }\n }\n\n async listFailed(\n queue?: string,\n options?: { limit?: number; offset?: number },\n ): Promise<QueueMessageRecord[]> {\n const where: any = { status: 'dlq' };\n if (queue) where.queue = queue;\n const rows = await this.engine.find(QUEUE_TABLE, {\n where,\n limit: options?.limit ?? 100,\n offset: options?.offset,\n orderBy: [{ field: 'created_at', order: 'desc' }],\n context: SYSTEM_CTX,\n });\n return (rows ?? []).map((r: any) => this.rowToRecord(r));\n }\n\n async replay(messageId: string): Promise<void> {\n const row = await this.loadById(messageId);\n if (!row) throw new Error(`MESSAGE_NOT_FOUND: ${messageId}`);\n if (row.status !== 'dlq' && row.status !== 'failed') {\n throw new Error(`INVALID_STATE: cannot replay message in status=${row.status}`);\n }\n const now = this.now();\n await this.engine.update(QUEUE_TABLE, {\n id: messageId,\n status: 'pending',\n attempts: 0,\n last_error: null,\n locked_by: null,\n locked_until: null,\n scheduled_for: now.toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n }\n\n async purgeFailed(messageId: string): Promise<void> {\n const row = await this.loadById(messageId);\n if (!row) return;\n if (row.status !== 'dlq' && row.status !== 'failed') {\n throw new Error(`INVALID_STATE: cannot purge message in status=${row.status}`);\n }\n await this.engine.delete(QUEUE_TABLE, { where: { id: messageId }, context: SYSTEM_CTX });\n }\n\n // ── Worker lifecycle ─────────────────────────────────────────────\n\n start(): void {\n if (this.timer) return;\n this.timer = setInterval(() => {\n if (this.running) return;\n this.running = true;\n this.pollOnce()\n .catch((err) => { this.logger?.warn?.('DbQueueAdapter: poll tick failed', err); })\n .finally(() => { this.running = false; });\n }, this.opts.pollIntervalMs);\n (this.timer as any)?.unref?.();\n }\n\n async stop(): Promise<void> {\n if (this.timer) { clearInterval(this.timer); this.timer = undefined; }\n }\n\n /** Test-friendly synchronous poll. */\n async pollOnce(): Promise<number> {\n const queues = [...this.handlers.keys()];\n if (queues.length === 0) return 0;\n\n let processed = 0;\n for (const queue of queues) {\n const claimed = await this.claimBatch(queue, this.opts.batchSize);\n for (const row of claimed) {\n await this.dispatch(row);\n processed++;\n }\n }\n return processed;\n }\n\n // ── Internals ────────────────────────────────────────────────────\n\n private async claimBatch(queue: string, max: number): Promise<any[]> {\n const now = this.now();\n const candidates = await this.engine.find(QUEUE_TABLE, {\n where: { queue, status: 'pending' },\n limit: max * 3, // over-fetch in case of CAS contention\n orderBy: [\n { field: 'priority', order: 'asc' },\n { field: 'scheduled_for', order: 'asc' },\n ],\n context: SYSTEM_CTX,\n });\n\n const out: any[] = [];\n for (const row of candidates ?? []) {\n if (out.length >= max) break;\n const sched = row.scheduled_for ? new Date(row.scheduled_for).getTime() : 0;\n if (sched > now.getTime()) continue;\n // Honor existing lease\n const lockedUntil = row.locked_until ? new Date(row.locked_until).getTime() : 0;\n if (row.locked_by && lockedUntil > now.getTime()) continue;\n\n // CAS — only update if still pending (best-effort with engine.update which\n // typically does row-level update by id; concurrent workers will overwrite\n // each other but the dispatcher tolerates duplicate delivery via attempts).\n try {\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'running',\n locked_by: this.opts.workerId,\n locked_until: new Date(now.getTime() + this.opts.leaseMs).toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n out.push({ ...row, status: 'running' });\n } catch (err) {\n this.logger?.warn?.('DbQueueAdapter: claim CAS failed', err as any);\n }\n }\n return out;\n }\n\n private async dispatch(row: any): Promise<void> {\n const handlers = this.handlers.get(row.queue) ?? [];\n if (handlers.length === 0) {\n // No handler — release lease so another process can pick it up\n await this.releasePending(row.id);\n return;\n }\n\n const msg: QueueMessage = {\n id: String(row.id),\n data: parseJson(row.payload_json),\n attempts: (row.attempts ?? 0) + 1,\n timestamp: row.created_at ? new Date(row.created_at).getTime() : Date.now(),\n };\n\n let success = true;\n let lastError: string | undefined;\n for (const h of handlers) {\n try { await h.fn(msg); }\n catch (err) {\n success = false;\n lastError = err instanceof Error ? err.message : String(err);\n this.logger?.warn?.(`DbQueueAdapter: handler failed on ${row.queue}`, err as any);\n break;\n }\n }\n\n const now = this.now();\n if (success) {\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'completed',\n attempts: msg.attempts,\n completed_at: now.toISOString(),\n locked_by: null,\n locked_until: null,\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n return;\n }\n\n const attempts = msg.attempts;\n const max = row.max_attempts ?? this.opts.defaultMaxAttempts;\n if (attempts >= max) {\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'dlq',\n attempts,\n last_error: lastError ?? 'unknown error',\n completed_at: now.toISOString(),\n locked_by: null,\n locked_until: null,\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n return;\n }\n\n const backoffMs = this.computeBackoff(row, attempts);\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'pending',\n attempts,\n last_error: lastError ?? 'unknown error',\n scheduled_for: new Date(now.getTime() + backoffMs).toISOString(),\n locked_by: null,\n locked_until: null,\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n }\n\n private computeBackoff(row: any, attempt: number): number {\n const base = row.backoff_delay_ms ?? 1000;\n const cap = row.backoff_max_delay_ms ?? undefined;\n if ((row.backoff_type ?? 'exponential') === 'fixed') return base;\n const exp = base * Math.pow(2, Math.max(0, attempt - 1));\n return cap ? Math.min(exp, cap) : exp;\n }\n\n private async releasePending(id: string): Promise<void> {\n const now = this.now();\n try {\n await this.engine.update(QUEUE_TABLE, {\n id,\n status: 'pending',\n locked_by: null,\n locked_until: null,\n scheduled_for: new Date(now.getTime() + this.opts.pollIntervalMs * 5).toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n } catch (err) {\n this.logger?.warn?.('DbQueueAdapter: release failed', err as any);\n }\n }\n\n private async loadById(id: string): Promise<any | null> {\n const rows = await this.engine.find(QUEUE_TABLE, {\n where: { id },\n limit: 1,\n context: SYSTEM_CTX,\n });\n return rows?.[0] ?? null;\n }\n\n private rowToRecord(r: any): QueueMessageRecord {\n return {\n id: String(r.id),\n queue: String(r.queue),\n data: parseJson(r.payload_json),\n status: r.status,\n attempts: r.attempts ?? 0,\n maxAttempts: r.max_attempts ?? this.opts.defaultMaxAttempts,\n scheduledFor: r.scheduled_for ?? undefined,\n lockedBy: r.locked_by ?? undefined,\n lockedUntil: r.locked_until ?? undefined,\n lastError: r.last_error ?? undefined,\n idempotencyKey: r.idempotency_key ?? undefined,\n metadata: parseJson(r.metadata_json),\n createdAt: r.created_at ?? nowIso(this.clock),\n updatedAt: r.updated_at ?? undefined,\n completedAt: r.completed_at ?? undefined,\n };\n }\n\n private now(): Date {\n return this.clock?.now() ?? new Date();\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Narrow ObjectQL engine surface used by job/queue adapters.\n * Keeps the adapter testable without booting a real kernel.\n *\n * IMPORTANT: matches the canonical engine API:\n * - find: `where:` (NOT `filter:`)\n * - update: `(table, {id, ...patch}, opts)`\n */\nexport interface JobEngine {\n find(object: string, options?: any): Promise<any[]>;\n insert(object: string, data: any, options?: any): Promise<any>;\n update(object: string, idOrData: any, dataOrOptions?: any, options?: any): Promise<any>;\n delete(object: string, options?: any): Promise<any>;\n}\n\n/** Stamped only in tests to make `now` deterministic. */\nexport interface JobClock { now(): Date }\n\nexport interface JobLogger {\n info(msg: string, meta?: unknown): void;\n warn(msg: string, meta?: unknown): void;\n error?(msg: string, meta?: unknown): void;\n}\n\nexport const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;\n\nexport function uid(prefix: string): string {\n const g: any = globalThis as any;\n if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;\n return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nexport function nowIso(clock?: JobClock): string {\n return (clock?.now() ?? new Date()).toISOString();\n}\n\n/**\n * Milliseconds per ADR-0057 lifecycle duration unit. Mirrors\n * `parseLifecycleDuration` in `@objectstack/objectql` (the canonical runtime\n * consumer), reproduced here rather than imported because the queue adapters\n * deliberately do not depend on the engine package — they duck-type\n * {@link JobEngine} so they stay testable without booting a kernel. Both\n * tables are fixed by the ADR (coarse operational bounds: `y` is 365 days),\n * and `job-queue-retention.test.ts` pins this one against them.\n */\nconst LIFECYCLE_UNIT_MS: Record<string, number> = {\n h: 3_600_000,\n d: 86_400_000,\n w: 7 * 86_400_000,\n y: 365 * 86_400_000,\n};\n\n/**\n * Parse an ADR-0057 duration literal (`'6h'`, `'7d'`, `'12w'`, `'7y'`) into\n * milliseconds. Throws on anything else: declarations reach this code already\n * validated by `LifecycleSchema`, so a failure here is a broken declaration,\n * not user input — and a queue that silently guessed a window would be exactly\n * the silent behaviour #5179 is about.\n */\nexport function lifecycleDurationMs(literal: string): number {\n const m = /^(\\d+)(h|d|w|y)$/.exec(literal);\n if (!m) {\n throw new Error(\n `[service-queue] invalid lifecycle duration literal '${literal}' — expected <n><unit> with unit h|d|w|y (e.g. '7d')`,\n );\n }\n return Number(m[1]) * LIFECYCLE_UNIT_MS[m[2]!]!;\n}\n\nexport function parseJson<T = unknown>(raw: unknown, fallback?: T): T | undefined {\n if (raw == null) return fallback;\n if (typeof raw === 'string') {\n try { return JSON.parse(raw) as T; } catch { return fallback; }\n }\n if (typeof raw === 'object') return raw as T;\n return fallback;\n}\n"],"mappings":"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","names":["import_audit"]}
package/dist/index.d.cts CHANGED
@@ -51,6 +51,33 @@ interface JobLogger {
51
51
  error?(msg: string, meta?: unknown): void;
52
52
  }
53
53
 
54
+ /**
55
+ * How long a `completed` row survives before the platform Reaper deletes it.
56
+ *
57
+ * Read from the object's own ADR-0057 declaration
58
+ * (`sys_job_queue.lifecycle.retention`, #5179) instead of being a second
59
+ * number here: the declaration is what actually runs (LifecycleService sweeps
60
+ * every registered object hourly), so a copy in this file could only ever be
61
+ * a copy that drifts. A missing or unparseable declaration throws: the queue's
62
+ * dedup contract below is defined against this window, so "no window" is not a
63
+ * state the adapter can run in.
64
+ */
65
+ declare function completedRetentionWindowMs(): number;
66
+ /**
67
+ * [#5195] The shape `LifecycleService.registerRetentionFloor()` accepts.
68
+ *
69
+ * Restated here rather than imported: `@objectstack/objectql` is a
70
+ * devDependency of this package on purpose (the queue must not drag the engine
71
+ * into every install), so its types are not available to this package's
72
+ * consumers at build time.
73
+ */
74
+ interface QueueRetentionFloor {
75
+ policy: 'retention';
76
+ minWindowMs: number;
77
+ declaredBy: string;
78
+ consequence: string;
79
+ remedy: string;
80
+ }
54
81
  interface DbQueueAdapterOptions {
55
82
  /** Polling interval for the worker loop (ms, default 1000) */
56
83
  pollIntervalMs?: number;
@@ -58,7 +85,15 @@ interface DbQueueAdapterOptions {
58
85
  batchSize?: number;
59
86
  /** Lease duration before another worker may reclaim (ms, default 30000) */
60
87
  leaseMs?: number;
61
- /** Idempotency window — how long the same key blocks re-publish (ms, default 24h) */
88
+ /**
89
+ * Idempotency window — how long the same key blocks re-publish (ms, default 24h).
90
+ *
91
+ * Must not exceed `sys_job_queue`'s declared retention for `completed` rows
92
+ * ({@link completedRetentionWindowMs}, 7d): the window is evaluated against
93
+ * rows that are still in the table, so a longer window would silently start
94
+ * accepting duplicates as soon as the Reaper swept the row it dedups
95
+ * against. The constructor rejects that configuration (#5179).
96
+ */
62
97
  idempotencyWindowMs?: number;
63
98
  /** Default maxAttempts when publish doesn't specify (default 3) */
64
99
  defaultMaxAttempts?: number;
@@ -78,6 +113,15 @@ interface DbQueueAdapterOptions {
78
113
  * Idempotency: publish suppresses duplicates within a configurable
79
114
  * window when `(queue, idempotencyKey)` is non-null.
80
115
  *
116
+ * Retention: this adapter does NOT sweep the table. `completed` rows are
117
+ * bounded by `sys_job_queue`'s declared ADR-0057 retention (7d, filtered to
118
+ * `status='completed'`), enforced by the one platform-owned
119
+ * `LifecycleService` reaper — see the object definition in
120
+ * `@objectstack/platform-objects` and {@link completedRetentionWindowMs}.
121
+ * `dlq`/`failed` rows are never swept; they are the dead-letter surface
122
+ * ({@link DbQueueAdapter.listFailed} / {@link DbQueueAdapter.replay} /
123
+ * {@link DbQueueAdapter.purgeFailed}).
124
+ *
81
125
  * Designed for SQLite and Postgres alike — uses CAS via WHERE-clauses,
82
126
  * not row-level locking.
83
127
  */
@@ -95,6 +139,24 @@ declare class DbQueueAdapter implements IQueueService {
95
139
  clock?: JobClock;
96
140
  options?: DbQueueAdapterOptions;
97
141
  });
142
+ /** The configured dedup window (ms) — the number the floor below is made of. */
143
+ get idempotencyWindowMs(): number;
144
+ /**
145
+ * [#5195] The retention floor `sys_job_queue` must satisfy for this adapter's
146
+ * dedup contract to mean anything, handed to `LifecycleService`
147
+ * (`registerRetentionFloor`) by `QueueServicePlugin`.
148
+ *
149
+ * The constructor check above only reads the object's **declaration**. ADR-0057
150
+ * P4 lets an operator override that window per environment/tenant through the
151
+ * `lifecycle` settings namespace, which the constructor cannot see: set
152
+ * `lifecycle.retention_overrides.sys_job_queue.maxAge = '1h'` and completed
153
+ * rows vanish an hour after they are written while publish keeps dedupping
154
+ * against a 24h window — duplicate deliveries resume, with nothing in any log.
155
+ * Registering the floor is what closes that door, and it carries the number
156
+ * this adapter was actually CONSTRUCTED with rather than a static copy of the
157
+ * default (a per-kernel option cannot live in the object's declaration).
158
+ */
159
+ retentionFloor(): QueueRetentionFloor;
98
160
  publish<T = unknown>(queue: string, data: T, options?: QueuePublishOptions): Promise<string>;
99
161
  subscribe<T = unknown>(queue: string, handler: QueueHandler<T>): Promise<void>;
100
162
  unsubscribe(queue: string): Promise<void>;
@@ -165,7 +227,19 @@ declare class QueueServicePlugin implements Plugin {
165
227
  private dbAdapter?;
166
228
  constructor(options?: QueueServicePluginOptions);
167
229
  init(ctx: PluginContext): Promise<void>;
230
+ /**
231
+ * [#5195] Register the adapter's retention floor with the platform
232
+ * LifecycleService. Best-effort: a kernel without a lifecycle service has no
233
+ * sweeper either, so there is no override for anything to bypass.
234
+ *
235
+ * The lookup is typed to {@link LifecycleFloorRegistrar} — the slot's
236
+ * contract as this package consumes it — rather than erased to `any`
237
+ * (#4127/#4251). The `typeof … === 'function'` probe stays because the method
238
+ * is genuinely optional (a lifecycle service predating floors), but it is now
239
+ * a check the compiler can see rather than one `any` was hiding.
240
+ */
241
+ private registerRetentionFloor;
168
242
  destroy(): Promise<void>;
169
243
  }
170
244
 
171
- export { DbQueueAdapter, type DbQueueAdapterOptions, type JobClock, type JobEngine, type JobLogger, MemoryQueueAdapter, type MemoryQueueAdapterOptions, QueueServicePlugin, type QueueServicePluginOptions };
245
+ export { DbQueueAdapter, type DbQueueAdapterOptions, type JobClock, type JobEngine, type JobLogger, MemoryQueueAdapter, type MemoryQueueAdapterOptions, QueueServicePlugin, type QueueServicePluginOptions, completedRetentionWindowMs };
package/dist/index.d.ts CHANGED
@@ -51,6 +51,33 @@ interface JobLogger {
51
51
  error?(msg: string, meta?: unknown): void;
52
52
  }
53
53
 
54
+ /**
55
+ * How long a `completed` row survives before the platform Reaper deletes it.
56
+ *
57
+ * Read from the object's own ADR-0057 declaration
58
+ * (`sys_job_queue.lifecycle.retention`, #5179) instead of being a second
59
+ * number here: the declaration is what actually runs (LifecycleService sweeps
60
+ * every registered object hourly), so a copy in this file could only ever be
61
+ * a copy that drifts. A missing or unparseable declaration throws: the queue's
62
+ * dedup contract below is defined against this window, so "no window" is not a
63
+ * state the adapter can run in.
64
+ */
65
+ declare function completedRetentionWindowMs(): number;
66
+ /**
67
+ * [#5195] The shape `LifecycleService.registerRetentionFloor()` accepts.
68
+ *
69
+ * Restated here rather than imported: `@objectstack/objectql` is a
70
+ * devDependency of this package on purpose (the queue must not drag the engine
71
+ * into every install), so its types are not available to this package's
72
+ * consumers at build time.
73
+ */
74
+ interface QueueRetentionFloor {
75
+ policy: 'retention';
76
+ minWindowMs: number;
77
+ declaredBy: string;
78
+ consequence: string;
79
+ remedy: string;
80
+ }
54
81
  interface DbQueueAdapterOptions {
55
82
  /** Polling interval for the worker loop (ms, default 1000) */
56
83
  pollIntervalMs?: number;
@@ -58,7 +85,15 @@ interface DbQueueAdapterOptions {
58
85
  batchSize?: number;
59
86
  /** Lease duration before another worker may reclaim (ms, default 30000) */
60
87
  leaseMs?: number;
61
- /** Idempotency window — how long the same key blocks re-publish (ms, default 24h) */
88
+ /**
89
+ * Idempotency window — how long the same key blocks re-publish (ms, default 24h).
90
+ *
91
+ * Must not exceed `sys_job_queue`'s declared retention for `completed` rows
92
+ * ({@link completedRetentionWindowMs}, 7d): the window is evaluated against
93
+ * rows that are still in the table, so a longer window would silently start
94
+ * accepting duplicates as soon as the Reaper swept the row it dedups
95
+ * against. The constructor rejects that configuration (#5179).
96
+ */
62
97
  idempotencyWindowMs?: number;
63
98
  /** Default maxAttempts when publish doesn't specify (default 3) */
64
99
  defaultMaxAttempts?: number;
@@ -78,6 +113,15 @@ interface DbQueueAdapterOptions {
78
113
  * Idempotency: publish suppresses duplicates within a configurable
79
114
  * window when `(queue, idempotencyKey)` is non-null.
80
115
  *
116
+ * Retention: this adapter does NOT sweep the table. `completed` rows are
117
+ * bounded by `sys_job_queue`'s declared ADR-0057 retention (7d, filtered to
118
+ * `status='completed'`), enforced by the one platform-owned
119
+ * `LifecycleService` reaper — see the object definition in
120
+ * `@objectstack/platform-objects` and {@link completedRetentionWindowMs}.
121
+ * `dlq`/`failed` rows are never swept; they are the dead-letter surface
122
+ * ({@link DbQueueAdapter.listFailed} / {@link DbQueueAdapter.replay} /
123
+ * {@link DbQueueAdapter.purgeFailed}).
124
+ *
81
125
  * Designed for SQLite and Postgres alike — uses CAS via WHERE-clauses,
82
126
  * not row-level locking.
83
127
  */
@@ -95,6 +139,24 @@ declare class DbQueueAdapter implements IQueueService {
95
139
  clock?: JobClock;
96
140
  options?: DbQueueAdapterOptions;
97
141
  });
142
+ /** The configured dedup window (ms) — the number the floor below is made of. */
143
+ get idempotencyWindowMs(): number;
144
+ /**
145
+ * [#5195] The retention floor `sys_job_queue` must satisfy for this adapter's
146
+ * dedup contract to mean anything, handed to `LifecycleService`
147
+ * (`registerRetentionFloor`) by `QueueServicePlugin`.
148
+ *
149
+ * The constructor check above only reads the object's **declaration**. ADR-0057
150
+ * P4 lets an operator override that window per environment/tenant through the
151
+ * `lifecycle` settings namespace, which the constructor cannot see: set
152
+ * `lifecycle.retention_overrides.sys_job_queue.maxAge = '1h'` and completed
153
+ * rows vanish an hour after they are written while publish keeps dedupping
154
+ * against a 24h window — duplicate deliveries resume, with nothing in any log.
155
+ * Registering the floor is what closes that door, and it carries the number
156
+ * this adapter was actually CONSTRUCTED with rather than a static copy of the
157
+ * default (a per-kernel option cannot live in the object's declaration).
158
+ */
159
+ retentionFloor(): QueueRetentionFloor;
98
160
  publish<T = unknown>(queue: string, data: T, options?: QueuePublishOptions): Promise<string>;
99
161
  subscribe<T = unknown>(queue: string, handler: QueueHandler<T>): Promise<void>;
100
162
  unsubscribe(queue: string): Promise<void>;
@@ -165,7 +227,19 @@ declare class QueueServicePlugin implements Plugin {
165
227
  private dbAdapter?;
166
228
  constructor(options?: QueueServicePluginOptions);
167
229
  init(ctx: PluginContext): Promise<void>;
230
+ /**
231
+ * [#5195] Register the adapter's retention floor with the platform
232
+ * LifecycleService. Best-effort: a kernel without a lifecycle service has no
233
+ * sweeper either, so there is no override for anything to bypass.
234
+ *
235
+ * The lookup is typed to {@link LifecycleFloorRegistrar} — the slot's
236
+ * contract as this package consumes it — rather than erased to `any`
237
+ * (#4127/#4251). The `typeof … === 'function'` probe stays because the method
238
+ * is genuinely optional (a lifecycle service predating floors), but it is now
239
+ * a check the compiler can see rather than one `any` was hiding.
240
+ */
241
+ private registerRetentionFloor;
168
242
  destroy(): Promise<void>;
169
243
  }
170
244
 
171
- export { DbQueueAdapter, type DbQueueAdapterOptions, type JobClock, type JobEngine, type JobLogger, MemoryQueueAdapter, type MemoryQueueAdapterOptions, QueueServicePlugin, type QueueServicePluginOptions };
245
+ export { DbQueueAdapter, type DbQueueAdapterOptions, type JobClock, type JobEngine, type JobLogger, MemoryQueueAdapter, type MemoryQueueAdapterOptions, QueueServicePlugin, type QueueServicePluginOptions, completedRetentionWindowMs };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/queue-service-plugin.ts
2
- import { SysJobQueue } from "@objectstack/platform-objects/audit";
2
+ import { SysJobQueue as SysJobQueue2 } from "@objectstack/platform-objects/audit";
3
3
 
4
4
  // src/memory-queue-adapter.ts
5
5
  var MemoryQueueAdapter = class {
@@ -55,6 +55,9 @@ var MemoryQueueAdapter = class {
55
55
  }
56
56
  };
57
57
 
58
+ // src/db-queue-adapter.ts
59
+ import { SysJobQueue } from "@objectstack/platform-objects/audit";
60
+
58
61
  // src/common.ts
59
62
  var SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] };
60
63
  function uid(prefix) {
@@ -65,6 +68,21 @@ function uid(prefix) {
65
68
  function nowIso(clock) {
66
69
  return (clock?.now() ?? /* @__PURE__ */ new Date()).toISOString();
67
70
  }
71
+ var LIFECYCLE_UNIT_MS = {
72
+ h: 36e5,
73
+ d: 864e5,
74
+ w: 7 * 864e5,
75
+ y: 365 * 864e5
76
+ };
77
+ function lifecycleDurationMs(literal) {
78
+ const m = /^(\d+)(h|d|w|y)$/.exec(literal);
79
+ if (!m) {
80
+ throw new Error(
81
+ `[service-queue] invalid lifecycle duration literal '${literal}' \u2014 expected <n><unit> with unit h|d|w|y (e.g. '7d')`
82
+ );
83
+ }
84
+ return Number(m[1]) * LIFECYCLE_UNIT_MS[m[2]];
85
+ }
68
86
  function parseJson(raw, fallback) {
69
87
  if (raw == null) return fallback;
70
88
  if (typeof raw === "string") {
@@ -80,6 +98,15 @@ function parseJson(raw, fallback) {
80
98
 
81
99
  // src/db-queue-adapter.ts
82
100
  var QUEUE_TABLE = "sys_job_queue";
101
+ function completedRetentionWindowMs() {
102
+ const maxAge = SysJobQueue.lifecycle?.retention?.maxAge;
103
+ if (!maxAge) {
104
+ throw new Error(
105
+ "[service-queue] sys_job_queue no longer declares lifecycle.retention \u2014 DbQueueAdapter dedups against terminal rows by `created_at` window and relies on that declared retention to keep them (ADR-0057, #5179). Restore the declaration in @objectstack/platform-objects rather than sweeping the table from here."
106
+ );
107
+ }
108
+ return lifecycleDurationMs(maxAge);
109
+ }
83
110
  var DbQueueAdapter = class {
84
111
  constructor(args) {
85
112
  this.handlers = /* @__PURE__ */ new Map();
@@ -97,6 +124,42 @@ var DbQueueAdapter = class {
97
124
  autoStart: o.autoStart ?? true,
98
125
  workerId: o.workerId ?? uid("worker")
99
126
  };
127
+ const retentionMs = completedRetentionWindowMs();
128
+ if (this.opts.idempotencyWindowMs > retentionMs) {
129
+ throw new Error(
130
+ `[service-queue] idempotencyWindowMs (${this.opts.idempotencyWindowMs}ms) exceeds the retention window sys_job_queue declares for completed rows (${retentionMs}ms, lifecycle.retention.maxAge \u2014 ADR-0057). Terminal-row dedup is evaluated by \`created_at\` against that same window, so the longer setting would silently accept duplicates once a row is reaped. Lower idempotencyWindowMs, or raise the declared retention (both windows are measured from \`created_at\`).`
131
+ );
132
+ }
133
+ }
134
+ /** The configured dedup window (ms) — the number the floor below is made of. */
135
+ get idempotencyWindowMs() {
136
+ return this.opts.idempotencyWindowMs;
137
+ }
138
+ /**
139
+ * [#5195] The retention floor `sys_job_queue` must satisfy for this adapter's
140
+ * dedup contract to mean anything, handed to `LifecycleService`
141
+ * (`registerRetentionFloor`) by `QueueServicePlugin`.
142
+ *
143
+ * The constructor check above only reads the object's **declaration**. ADR-0057
144
+ * P4 lets an operator override that window per environment/tenant through the
145
+ * `lifecycle` settings namespace, which the constructor cannot see: set
146
+ * `lifecycle.retention_overrides.sys_job_queue.maxAge = '1h'` and completed
147
+ * rows vanish an hour after they are written while publish keeps dedupping
148
+ * against a 24h window — duplicate deliveries resume, with nothing in any log.
149
+ * Registering the floor is what closes that door, and it carries the number
150
+ * this adapter was actually CONSTRUCTED with rather than a static copy of the
151
+ * default (a per-kernel option cannot live in the object's declaration).
152
+ */
153
+ retentionFloor() {
154
+ const ms = this.opts.idempotencyWindowMs;
155
+ const literal = `${Math.ceil(ms / 36e5)}h`;
156
+ return {
157
+ policy: "retention",
158
+ minWindowMs: ms,
159
+ declaredBy: "com.objectstack.service.queue",
160
+ consequence: `DbQueueAdapter dedups sys_job_queue publishes by comparing created_at against its ${ms}ms idempotency window, so a shorter retention deletes the very rows that check reads \u2014 duplicate deliveries resume silently, with nothing in any log.`,
161
+ remedy: `set lifecycle.retention_overrides.sys_job_queue.maxAge to '${literal}' or longer, or lower QueueServicePlugin's db.idempotencyWindowMs to the window you actually want (both are measured from created_at).`
162
+ };
100
163
  }
101
164
  // ── IQueueService ────────────────────────────────────────────────
102
165
  async publish(queue, data, options) {
@@ -428,7 +491,7 @@ var QueueServicePlugin = class {
428
491
  scope: "system",
429
492
  defaultDatasource: "cloud",
430
493
  namespace: "sys",
431
- objects: [SysJobQueue]
494
+ objects: [SysJobQueue2]
432
495
  });
433
496
  } catch (err) {
434
497
  ctx.logger.warn("QueueServicePlugin: manifest service unavailable; sys_job_queue not registered", err);
@@ -464,6 +527,7 @@ var QueueServicePlugin = class {
464
527
  logger: ctx.logger,
465
528
  options: this.options.db
466
529
  });
530
+ this.registerRetentionFloor(ctx, this.dbAdapter);
467
531
  try {
468
532
  ctx.replaceService?.("queue", this.dbAdapter);
469
533
  this.dbAdapter.start();
@@ -473,6 +537,37 @@ var QueueServicePlugin = class {
473
537
  }
474
538
  });
475
539
  }
540
+ /**
541
+ * [#5195] Register the adapter's retention floor with the platform
542
+ * LifecycleService. Best-effort: a kernel without a lifecycle service has no
543
+ * sweeper either, so there is no override for anything to bypass.
544
+ *
545
+ * The lookup is typed to {@link LifecycleFloorRegistrar} — the slot's
546
+ * contract as this package consumes it — rather than erased to `any`
547
+ * (#4127/#4251). The `typeof … === 'function'` probe stays because the method
548
+ * is genuinely optional (a lifecycle service predating floors), but it is now
549
+ * a check the compiler can see rather than one `any` was hiding.
550
+ */
551
+ registerRetentionFloor(ctx, adapter) {
552
+ let lifecycle;
553
+ try {
554
+ lifecycle = ctx.getService("lifecycle");
555
+ } catch {
556
+ lifecycle = void 0;
557
+ }
558
+ if (!lifecycle || typeof lifecycle.registerRetentionFloor !== "function") return;
559
+ try {
560
+ lifecycle.registerRetentionFloor(SysJobQueue2.name, adapter.retentionFloor());
561
+ ctx.logger.info(
562
+ `QueueServicePlugin: registered a ${adapter.idempotencyWindowMs}ms retention floor on ${SysJobQueue2.name} with the lifecycle service (settings overrides below it are rejected)`
563
+ );
564
+ } catch (err) {
565
+ ctx.logger.error(
566
+ "QueueServicePlugin: the lifecycle service rejected the sys_job_queue retention floor. A `lifecycle` settings override may now shorten sys_job_queue.retention below the idempotency window, in which case publish would silently re-accept duplicates (#5195). Fix the floor registration, or keep lifecycle.retention_overrides.sys_job_queue unset.",
567
+ err
568
+ );
569
+ }
570
+ }
476
571
  async destroy() {
477
572
  await this.dbAdapter?.stop();
478
573
  }
@@ -480,6 +575,7 @@ var QueueServicePlugin = class {
480
575
  export {
481
576
  DbQueueAdapter,
482
577
  MemoryQueueAdapter,
483
- QueueServicePlugin
578
+ QueueServicePlugin,
579
+ completedRetentionWindowMs
484
580
  };
485
581
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/queue-service-plugin.ts","../src/memory-queue-adapter.ts","../src/common.ts","../src/db-queue-adapter.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { SysJobQueue } from '@objectstack/platform-objects/audit';\nimport { MemoryQueueAdapter } from './memory-queue-adapter.js';\nimport type { MemoryQueueAdapterOptions } from './memory-queue-adapter.js';\nimport { DbQueueAdapter } from './db-queue-adapter.js';\nimport type { DbQueueAdapterOptions } from './db-queue-adapter.js';\n\n/**\n * Configuration options for the QueueServicePlugin.\n */\nexport interface QueueServicePluginOptions {\n /**\n * Queue adapter type.\n * - 'auto' (default): use DbQueueAdapter when objectql engine available, else MemoryQueueAdapter\n * - 'db': require objectql; persists messages, retries, and DLQ to sys_job_queue\n * - 'memory': in-process MemoryQueueAdapter (non-durable, dev/test)\n */\n adapter?: 'auto' | 'db' | 'memory';\n /** Options for the memory queue adapter */\n memory?: MemoryQueueAdapterOptions;\n /** Options for the DB adapter (polling, batch, lease, idempotency window…) */\n db?: DbQueueAdapterOptions;\n}\n\n/**\n * QueueServicePlugin — Production IQueueService implementation.\n *\n * Default: registers MemoryQueueAdapter synchronously so producers can\n * publish during plugin init; upgrades to DbQueueAdapter on `kernel:ready`\n * when an ObjectQL engine is available. Subscribers registered against\n * the (now-replaced) memory queue must re-subscribe after upgrade — for\n * that reason most plugins register subscribers inside their own\n * `kernel:ready` hook, which fires after this one.\n */\nexport class QueueServicePlugin implements Plugin {\n name = 'com.objectstack.service.queue';\n /**\n * Services init() registers on every path (ADR-0116, #4131) — lets the\n * kernel name this plugin when a consumer requires one before it inits.\n */\n providesServices = ['queue'];\n /**\n * init() registers sys_job_queue through the `manifest` service\n * ObjectQLPlugin provides — order-if-present so the registration is\n * deterministic (ADR-0116, #4471). Soft, not hard: without an engine the\n * plugin degrades on purpose (in-memory queue adapter).\n */\n optionalDependencies = ['com.objectstack.engine.objectql'];\n version = '1.1.0';\n type = 'standard';\n\n private readonly options: QueueServicePluginOptions;\n private dbAdapter?: DbQueueAdapter;\n\n constructor(options: QueueServicePluginOptions = {}) {\n this.options = { adapter: 'auto', ...options };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Register sys_job_queue (also serves as DLQ view) so Studio can list/replay.\n try {\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.service.queue',\n name: 'Queue Service',\n version: '1.1.0',\n type: 'plugin',\n scope: 'system',\n defaultDatasource: 'cloud',\n namespace: 'sys',\n objects: [SysJobQueue],\n });\n } catch (err) {\n ctx.logger.warn('QueueServicePlugin: manifest service unavailable; sys_job_queue not registered', err as any);\n }\n\n const choice = this.options.adapter ?? 'auto';\n\n if (choice === 'memory') {\n const q = new MemoryQueueAdapter(this.options.memory);\n ctx.registerService('queue', q);\n ctx.logger.info('QueueServicePlugin: registered MemoryQueueAdapter');\n return;\n }\n\n // auto / db — register memory placeholder, upgrade on kernel:ready\n ctx.registerService('queue', new MemoryQueueAdapter(this.options.memory));\n\n ctx.hook('kernel:ready', async () => {\n let engine: any = null;\n try { engine = ctx.getService<any>('objectql'); }\n catch { try { engine = ctx.getService<any>('data'); } catch { /* ignore */ } }\n\n if (!engine) {\n if (choice === 'db') {\n ctx.logger.warn('QueueServicePlugin: db adapter requested but no ObjectQL engine — staying on MemoryQueueAdapter');\n } else {\n ctx.logger.info('QueueServicePlugin: no ObjectQL engine — staying on MemoryQueueAdapter');\n }\n return;\n }\n\n this.dbAdapter = new DbQueueAdapter({\n engine,\n logger: ctx.logger,\n options: this.options.db,\n });\n\n try {\n (ctx as any).replaceService?.('queue', this.dbAdapter);\n this.dbAdapter.start();\n ctx.logger.info('QueueServicePlugin: upgraded to DbQueueAdapter (sys_job_queue persistence)');\n } catch (err) {\n ctx.logger.warn('QueueServicePlugin: replaceService failed; staying on MemoryQueueAdapter', err as any);\n }\n });\n }\n\n async destroy(): Promise<void> {\n await this.dbAdapter?.stop();\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IQueueService, QueuePublishOptions, QueueMessage, QueueHandler } from '@objectstack/spec/contracts';\n\n/**\n * Configuration options for MemoryQueueAdapter.\n */\nexport interface MemoryQueueAdapterOptions {\n /** Maximum number of messages retained per queue (0 = unlimited) */\n maxQueueSize?: number;\n}\n\n/**\n * In-memory queue adapter implementing IQueueService.\n *\n * Provides synchronous in-process pub/sub delivery.\n * Suitable for single-process environments, development, and testing.\n */\nexport class MemoryQueueAdapter implements IQueueService {\n private readonly handlers = new Map<string, QueueHandler[]>();\n private readonly deadLetters: QueueMessage[] = [];\n private msgCounter = 0;\n private readonly maxQueueSize: number;\n\n constructor(options: MemoryQueueAdapterOptions = {}) {\n this.maxQueueSize = options.maxQueueSize ?? 0;\n }\n\n async publish<T = unknown>(queue: string, data: T, options?: QueuePublishOptions): Promise<string> {\n const id = `msg-${++this.msgCounter}`;\n const msg: QueueMessage<T> = {\n id,\n data,\n attempts: 0,\n timestamp: Date.now(),\n };\n\n const fns = this.handlers.get(queue) ?? [];\n if (fns.length === 0) {\n // No subscribers — retain as dead letter if within limits\n if (this.maxQueueSize === 0 || this.deadLetters.length < this.maxQueueSize) {\n this.deadLetters.push(msg);\n }\n return id;\n }\n\n const maxRetries = options?.retries ?? 0;\n for (const handler of fns) {\n let attempt = 0;\n let success = false;\n while (!success && attempt <= maxRetries) {\n try {\n msg.attempts = attempt + 1;\n await handler(msg as QueueMessage);\n success = true;\n } catch {\n attempt++;\n }\n }\n }\n\n return id;\n }\n\n async subscribe<T = unknown>(queue: string, handler: QueueHandler<T>): Promise<void> {\n const existing = this.handlers.get(queue) ?? [];\n this.handlers.set(queue, [...existing, handler as QueueHandler]);\n }\n\n async unsubscribe(queue: string): Promise<void> {\n this.handlers.delete(queue);\n }\n\n async getQueueSize(_queue: string): Promise<number> {\n // In-memory: no persistent queue depth tracking\n return 0;\n }\n\n async purge(queue: string): Promise<void> {\n this.handlers.delete(queue);\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Narrow ObjectQL engine surface used by job/queue adapters.\n * Keeps the adapter testable without booting a real kernel.\n *\n * IMPORTANT: matches the canonical engine API:\n * - find: `where:` (NOT `filter:`)\n * - update: `(table, {id, ...patch}, opts)`\n */\nexport interface JobEngine {\n find(object: string, options?: any): Promise<any[]>;\n insert(object: string, data: any, options?: any): Promise<any>;\n update(object: string, idOrData: any, dataOrOptions?: any, options?: any): Promise<any>;\n delete(object: string, options?: any): Promise<any>;\n}\n\n/** Stamped only in tests to make `now` deterministic. */\nexport interface JobClock { now(): Date }\n\nexport interface JobLogger {\n info(msg: string, meta?: unknown): void;\n warn(msg: string, meta?: unknown): void;\n error?(msg: string, meta?: unknown): void;\n}\n\nexport const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;\n\nexport function uid(prefix: string): string {\n const g: any = globalThis as any;\n if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;\n return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nexport function nowIso(clock?: JobClock): string {\n return (clock?.now() ?? new Date()).toISOString();\n}\n\nexport function parseJson<T = unknown>(raw: unknown, fallback?: T): T | undefined {\n if (raw == null) return fallback;\n if (typeof raw === 'string') {\n try { return JSON.parse(raw) as T; } catch { return fallback; }\n }\n if (typeof raw === 'object') return raw as T;\n return fallback;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IQueueService,\n QueuePublishOptions,\n QueueMessage,\n QueueMessageRecord,\n QueueHandler,\n} from '@objectstack/spec/contracts';\nimport {\n SYSTEM_CTX,\n uid,\n nowIso,\n parseJson,\n type JobEngine,\n type JobClock,\n type JobLogger,\n} from './common.js';\n\nconst QUEUE_TABLE = 'sys_job_queue';\n\nexport interface DbQueueAdapterOptions {\n /** Polling interval for the worker loop (ms, default 1000) */\n pollIntervalMs?: number;\n /** Max messages claimed per poll tick (default 10) */\n batchSize?: number;\n /** Lease duration before another worker may reclaim (ms, default 30000) */\n leaseMs?: number;\n /** Idempotency window — how long the same key blocks re-publish (ms, default 24h) */\n idempotencyWindowMs?: number;\n /** Default maxAttempts when publish doesn't specify (default 3) */\n defaultMaxAttempts?: number;\n /** Unique identifier for this worker (default: random) */\n workerId?: string;\n /** Whether to auto-start the polling worker (default true) */\n autoStart?: boolean;\n}\n\ninterface RegisteredHandler {\n queue: string;\n fn: QueueHandler;\n}\n\n/**\n * DbQueueAdapter — durable, polling, DB-backed IQueueService.\n *\n * Persists every message to `sys_job_queue`. A polling worker leases\n * pending messages (CAS update status pending→running with a lease),\n * invokes registered subscribers, and retries with backoff on failure.\n * Messages that exceed `max_attempts` land in `status='dlq'`.\n *\n * Idempotency: publish suppresses duplicates within a configurable\n * window when `(queue, idempotencyKey)` is non-null.\n *\n * Designed for SQLite and Postgres alike — uses CAS via WHERE-clauses,\n * not row-level locking.\n */\nexport class DbQueueAdapter implements IQueueService {\n private readonly engine: JobEngine;\n private readonly logger?: JobLogger;\n private readonly clock?: JobClock;\n private readonly opts: Required<Omit<DbQueueAdapterOptions, 'workerId'>> & { workerId: string };\n\n private readonly handlers = new Map<string, RegisteredHandler[]>();\n private timer?: ReturnType<typeof setInterval>;\n private running = false;\n\n constructor(args: {\n engine: JobEngine;\n logger?: JobLogger;\n clock?: JobClock;\n options?: DbQueueAdapterOptions;\n }) {\n this.engine = args.engine;\n this.logger = args.logger;\n this.clock = args.clock;\n const o = args.options ?? {};\n this.opts = {\n pollIntervalMs: o.pollIntervalMs ?? 1000,\n batchSize: o.batchSize ?? 10,\n leaseMs: o.leaseMs ?? 30_000,\n idempotencyWindowMs: o.idempotencyWindowMs ?? 24 * 60 * 60 * 1000,\n defaultMaxAttempts: o.defaultMaxAttempts ?? 3,\n autoStart: o.autoStart ?? true,\n workerId: o.workerId ?? uid('worker'),\n };\n }\n\n // ── IQueueService ────────────────────────────────────────────────\n\n async publish<T = unknown>(\n queue: string,\n data: T,\n options?: QueuePublishOptions,\n ): Promise<string> {\n const opts = options ?? {};\n const now = this.now();\n\n // Idempotency check\n if (opts.idempotencyKey) {\n const windowStart = new Date(now.getTime() - this.opts.idempotencyWindowMs).toISOString();\n const existing = await this.engine.find(QUEUE_TABLE, {\n where: {\n queue,\n idempotency_key: opts.idempotencyKey,\n // Only block if not yet terminal — completed/dlq dedup is by window via created_at\n },\n limit: 5,\n context: SYSTEM_CTX,\n });\n const blocking = (existing ?? []).find((row: any) => {\n if (row.status === 'pending' || row.status === 'running') return true;\n return String(row.created_at ?? '') >= windowStart;\n });\n if (blocking) return String(blocking.id);\n }\n\n const id = uid('msg');\n const scheduledFor = opts.scheduledFor\n ? new Date(opts.scheduledFor).toISOString()\n : opts.delay\n ? new Date(now.getTime() + opts.delay).toISOString()\n : now.toISOString();\n\n const maxAttempts = opts.maxAttempts\n ?? (opts.retries != null ? opts.retries + 1 : this.opts.defaultMaxAttempts);\n const backoff = opts.backoff ?? { type: 'exponential' as const, delayMs: 1000 };\n\n await this.engine.insert(QUEUE_TABLE, {\n id,\n queue,\n idempotency_key: opts.idempotencyKey ?? null,\n payload_json: JSON.stringify(data ?? null),\n metadata_json: opts.metadata ? JSON.stringify(opts.metadata) : null,\n status: 'pending',\n priority: opts.priority ?? 100,\n attempts: 0,\n max_attempts: maxAttempts,\n backoff_type: backoff.type,\n backoff_delay_ms: backoff.delayMs,\n backoff_max_delay_ms: backoff.maxDelayMs ?? null,\n scheduled_for: scheduledFor,\n created_at: now.toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n\n return id;\n }\n\n async subscribe<T = unknown>(queue: string, handler: QueueHandler<T>): Promise<void> {\n const existing = this.handlers.get(queue) ?? [];\n existing.push({ queue, fn: handler as QueueHandler });\n this.handlers.set(queue, existing);\n if (this.opts.autoStart) this.start();\n }\n\n async unsubscribe(queue: string): Promise<void> {\n this.handlers.delete(queue);\n }\n\n async getQueueSize(queue: string): Promise<number> {\n const rows = await this.engine.find(QUEUE_TABLE, {\n where: { queue, status: 'pending' },\n limit: 10_000,\n context: SYSTEM_CTX,\n });\n return rows?.length ?? 0;\n }\n\n async purge(queue: string): Promise<void> {\n const rows = await this.engine.find(QUEUE_TABLE, {\n where: { queue, status: 'pending' },\n limit: 10_000,\n context: SYSTEM_CTX,\n });\n for (const row of rows ?? []) {\n // `where: { id }` — the engine's delete has no top-level `id` option.\n // The old `{ id: row.id }` bag carried no predicate at all, so every\n // purge delete threw \"Delete requires an ID or options.multi=true\"\n // straight into this catch: purge logged a warn per row and deleted\n // NOTHING (#4371 option-2 survey).\n try { await this.engine.delete(QUEUE_TABLE, { where: { id: row.id }, context: SYSTEM_CTX }); }\n catch (err) { this.logger?.warn?.('DbQueueAdapter: purge delete failed', err as any); }\n }\n }\n\n async listFailed(\n queue?: string,\n options?: { limit?: number; offset?: number },\n ): Promise<QueueMessageRecord[]> {\n const where: any = { status: 'dlq' };\n if (queue) where.queue = queue;\n const rows = await this.engine.find(QUEUE_TABLE, {\n where,\n limit: options?.limit ?? 100,\n offset: options?.offset,\n orderBy: [{ field: 'created_at', order: 'desc' }],\n context: SYSTEM_CTX,\n });\n return (rows ?? []).map((r: any) => this.rowToRecord(r));\n }\n\n async replay(messageId: string): Promise<void> {\n const row = await this.loadById(messageId);\n if (!row) throw new Error(`MESSAGE_NOT_FOUND: ${messageId}`);\n if (row.status !== 'dlq' && row.status !== 'failed') {\n throw new Error(`INVALID_STATE: cannot replay message in status=${row.status}`);\n }\n const now = this.now();\n await this.engine.update(QUEUE_TABLE, {\n id: messageId,\n status: 'pending',\n attempts: 0,\n last_error: null,\n locked_by: null,\n locked_until: null,\n scheduled_for: now.toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n }\n\n async purgeFailed(messageId: string): Promise<void> {\n const row = await this.loadById(messageId);\n if (!row) return;\n if (row.status !== 'dlq' && row.status !== 'failed') {\n throw new Error(`INVALID_STATE: cannot purge message in status=${row.status}`);\n }\n await this.engine.delete(QUEUE_TABLE, { where: { id: messageId }, context: SYSTEM_CTX });\n }\n\n // ── Worker lifecycle ─────────────────────────────────────────────\n\n start(): void {\n if (this.timer) return;\n this.timer = setInterval(() => {\n if (this.running) return;\n this.running = true;\n this.pollOnce()\n .catch((err) => { this.logger?.warn?.('DbQueueAdapter: poll tick failed', err); })\n .finally(() => { this.running = false; });\n }, this.opts.pollIntervalMs);\n (this.timer as any)?.unref?.();\n }\n\n async stop(): Promise<void> {\n if (this.timer) { clearInterval(this.timer); this.timer = undefined; }\n }\n\n /** Test-friendly synchronous poll. */\n async pollOnce(): Promise<number> {\n const queues = [...this.handlers.keys()];\n if (queues.length === 0) return 0;\n\n let processed = 0;\n for (const queue of queues) {\n const claimed = await this.claimBatch(queue, this.opts.batchSize);\n for (const row of claimed) {\n await this.dispatch(row);\n processed++;\n }\n }\n return processed;\n }\n\n // ── Internals ────────────────────────────────────────────────────\n\n private async claimBatch(queue: string, max: number): Promise<any[]> {\n const now = this.now();\n const candidates = await this.engine.find(QUEUE_TABLE, {\n where: { queue, status: 'pending' },\n limit: max * 3, // over-fetch in case of CAS contention\n orderBy: [\n { field: 'priority', order: 'asc' },\n { field: 'scheduled_for', order: 'asc' },\n ],\n context: SYSTEM_CTX,\n });\n\n const out: any[] = [];\n for (const row of candidates ?? []) {\n if (out.length >= max) break;\n const sched = row.scheduled_for ? new Date(row.scheduled_for).getTime() : 0;\n if (sched > now.getTime()) continue;\n // Honor existing lease\n const lockedUntil = row.locked_until ? new Date(row.locked_until).getTime() : 0;\n if (row.locked_by && lockedUntil > now.getTime()) continue;\n\n // CAS — only update if still pending (best-effort with engine.update which\n // typically does row-level update by id; concurrent workers will overwrite\n // each other but the dispatcher tolerates duplicate delivery via attempts).\n try {\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'running',\n locked_by: this.opts.workerId,\n locked_until: new Date(now.getTime() + this.opts.leaseMs).toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n out.push({ ...row, status: 'running' });\n } catch (err) {\n this.logger?.warn?.('DbQueueAdapter: claim CAS failed', err as any);\n }\n }\n return out;\n }\n\n private async dispatch(row: any): Promise<void> {\n const handlers = this.handlers.get(row.queue) ?? [];\n if (handlers.length === 0) {\n // No handler — release lease so another process can pick it up\n await this.releasePending(row.id);\n return;\n }\n\n const msg: QueueMessage = {\n id: String(row.id),\n data: parseJson(row.payload_json),\n attempts: (row.attempts ?? 0) + 1,\n timestamp: row.created_at ? new Date(row.created_at).getTime() : Date.now(),\n };\n\n let success = true;\n let lastError: string | undefined;\n for (const h of handlers) {\n try { await h.fn(msg); }\n catch (err) {\n success = false;\n lastError = err instanceof Error ? err.message : String(err);\n this.logger?.warn?.(`DbQueueAdapter: handler failed on ${row.queue}`, err as any);\n break;\n }\n }\n\n const now = this.now();\n if (success) {\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'completed',\n attempts: msg.attempts,\n completed_at: now.toISOString(),\n locked_by: null,\n locked_until: null,\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n return;\n }\n\n const attempts = msg.attempts;\n const max = row.max_attempts ?? this.opts.defaultMaxAttempts;\n if (attempts >= max) {\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'dlq',\n attempts,\n last_error: lastError ?? 'unknown error',\n completed_at: now.toISOString(),\n locked_by: null,\n locked_until: null,\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n return;\n }\n\n const backoffMs = this.computeBackoff(row, attempts);\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'pending',\n attempts,\n last_error: lastError ?? 'unknown error',\n scheduled_for: new Date(now.getTime() + backoffMs).toISOString(),\n locked_by: null,\n locked_until: null,\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n }\n\n private computeBackoff(row: any, attempt: number): number {\n const base = row.backoff_delay_ms ?? 1000;\n const cap = row.backoff_max_delay_ms ?? undefined;\n if ((row.backoff_type ?? 'exponential') === 'fixed') return base;\n const exp = base * Math.pow(2, Math.max(0, attempt - 1));\n return cap ? Math.min(exp, cap) : exp;\n }\n\n private async releasePending(id: string): Promise<void> {\n const now = this.now();\n try {\n await this.engine.update(QUEUE_TABLE, {\n id,\n status: 'pending',\n locked_by: null,\n locked_until: null,\n scheduled_for: new Date(now.getTime() + this.opts.pollIntervalMs * 5).toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n } catch (err) {\n this.logger?.warn?.('DbQueueAdapter: release failed', err as any);\n }\n }\n\n private async loadById(id: string): Promise<any | null> {\n const rows = await this.engine.find(QUEUE_TABLE, {\n where: { id },\n limit: 1,\n context: SYSTEM_CTX,\n });\n return rows?.[0] ?? null;\n }\n\n private rowToRecord(r: any): QueueMessageRecord {\n return {\n id: String(r.id),\n queue: String(r.queue),\n data: parseJson(r.payload_json),\n status: r.status,\n attempts: r.attempts ?? 0,\n maxAttempts: r.max_attempts ?? this.opts.defaultMaxAttempts,\n scheduledFor: r.scheduled_for ?? undefined,\n lockedBy: r.locked_by ?? undefined,\n lockedUntil: r.locked_until ?? undefined,\n lastError: r.last_error ?? undefined,\n idempotencyKey: r.idempotency_key ?? undefined,\n metadata: parseJson(r.metadata_json),\n createdAt: r.created_at ?? nowIso(this.clock),\n updatedAt: r.updated_at ?? undefined,\n completedAt: r.completed_at ?? undefined,\n };\n }\n\n private now(): Date {\n return this.clock?.now() ?? new Date();\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/queue-service-plugin.ts","../src/memory-queue-adapter.ts","../src/db-queue-adapter.ts","../src/common.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { SysJobQueue } from '@objectstack/platform-objects/audit';\nimport { MemoryQueueAdapter } from './memory-queue-adapter.js';\nimport type { MemoryQueueAdapterOptions } from './memory-queue-adapter.js';\nimport { DbQueueAdapter } from './db-queue-adapter.js';\nimport type { DbQueueAdapterOptions, LifecycleFloorRegistrar } from './db-queue-adapter.js';\n\n/**\n * Configuration options for the QueueServicePlugin.\n */\nexport interface QueueServicePluginOptions {\n /**\n * Queue adapter type.\n * - 'auto' (default): use DbQueueAdapter when objectql engine available, else MemoryQueueAdapter\n * - 'db': require objectql; persists messages, retries, and DLQ to sys_job_queue\n * - 'memory': in-process MemoryQueueAdapter (non-durable, dev/test)\n */\n adapter?: 'auto' | 'db' | 'memory';\n /** Options for the memory queue adapter */\n memory?: MemoryQueueAdapterOptions;\n /** Options for the DB adapter (polling, batch, lease, idempotency window…) */\n db?: DbQueueAdapterOptions;\n}\n\n/**\n * QueueServicePlugin — Production IQueueService implementation.\n *\n * Default: registers MemoryQueueAdapter synchronously so producers can\n * publish during plugin init; upgrades to DbQueueAdapter on `kernel:ready`\n * when an ObjectQL engine is available. Subscribers registered against\n * the (now-replaced) memory queue must re-subscribe after upgrade — for\n * that reason most plugins register subscribers inside their own\n * `kernel:ready` hook, which fires after this one.\n */\nexport class QueueServicePlugin implements Plugin {\n name = 'com.objectstack.service.queue';\n /**\n * Services init() registers on every path (ADR-0116, #4131) — lets the\n * kernel name this plugin when a consumer requires one before it inits.\n */\n providesServices = ['queue'];\n /**\n * init() registers sys_job_queue through the `manifest` service\n * ObjectQLPlugin provides — order-if-present so the registration is\n * deterministic (ADR-0116, #4471). Soft, not hard: without an engine the\n * plugin degrades on purpose (in-memory queue adapter).\n */\n optionalDependencies = ['com.objectstack.engine.objectql'];\n version = '1.1.0';\n type = 'standard';\n\n private readonly options: QueueServicePluginOptions;\n private dbAdapter?: DbQueueAdapter;\n\n constructor(options: QueueServicePluginOptions = {}) {\n this.options = { adapter: 'auto', ...options };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Register sys_job_queue (also serves as DLQ view) so Studio can list/replay.\n try {\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.service.queue',\n name: 'Queue Service',\n version: '1.1.0',\n type: 'plugin',\n scope: 'system',\n defaultDatasource: 'cloud',\n namespace: 'sys',\n objects: [SysJobQueue],\n });\n } catch (err) {\n ctx.logger.warn('QueueServicePlugin: manifest service unavailable; sys_job_queue not registered', err as any);\n }\n\n const choice = this.options.adapter ?? 'auto';\n\n if (choice === 'memory') {\n const q = new MemoryQueueAdapter(this.options.memory);\n ctx.registerService('queue', q);\n ctx.logger.info('QueueServicePlugin: registered MemoryQueueAdapter');\n return;\n }\n\n // auto / db — register memory placeholder, upgrade on kernel:ready\n ctx.registerService('queue', new MemoryQueueAdapter(this.options.memory));\n\n ctx.hook('kernel:ready', async () => {\n let engine: any = null;\n try { engine = ctx.getService<any>('objectql'); }\n catch { try { engine = ctx.getService<any>('data'); } catch { /* ignore */ } }\n\n if (!engine) {\n if (choice === 'db') {\n ctx.logger.warn('QueueServicePlugin: db adapter requested but no ObjectQL engine — staying on MemoryQueueAdapter');\n } else {\n ctx.logger.info('QueueServicePlugin: no ObjectQL engine — staying on MemoryQueueAdapter');\n }\n return;\n }\n\n this.dbAdapter = new DbQueueAdapter({\n engine,\n logger: ctx.logger,\n options: this.options.db,\n });\n\n // [#5195] Tell the LifecycleService how short sys_job_queue's retention\n // may get. The adapter's constructor already refuses an idempotency\n // window longer than the DECLARED retention (#5179), but ADR-0057 P4\n // overrides live in the `lifecycle` settings namespace, which the\n // constructor never sees — an operator setting `maxAge: '1h'` would reap\n // the rows publish dedups against and duplicate deliveries would resume\n // silently. The floor carries the window this adapter was actually\n // constructed with, so a non-default `db.idempotencyWindowMs` is covered\n // too.\n this.registerRetentionFloor(ctx, this.dbAdapter);\n\n try {\n (ctx as any).replaceService?.('queue', this.dbAdapter);\n this.dbAdapter.start();\n ctx.logger.info('QueueServicePlugin: upgraded to DbQueueAdapter (sys_job_queue persistence)');\n } catch (err) {\n ctx.logger.warn('QueueServicePlugin: replaceService failed; staying on MemoryQueueAdapter', err as any);\n }\n });\n }\n\n /**\n * [#5195] Register the adapter's retention floor with the platform\n * LifecycleService. Best-effort: a kernel without a lifecycle service has no\n * sweeper either, so there is no override for anything to bypass.\n *\n * The lookup is typed to {@link LifecycleFloorRegistrar} — the slot's\n * contract as this package consumes it — rather than erased to `any`\n * (#4127/#4251). The `typeof … === 'function'` probe stays because the method\n * is genuinely optional (a lifecycle service predating floors), but it is now\n * a check the compiler can see rather than one `any` was hiding.\n */\n private registerRetentionFloor(ctx: PluginContext, adapter: DbQueueAdapter): void {\n let lifecycle: LifecycleFloorRegistrar | undefined;\n try {\n lifecycle = ctx.getService<LifecycleFloorRegistrar>('lifecycle');\n } catch {\n lifecycle = undefined;\n }\n if (!lifecycle || typeof lifecycle.registerRetentionFloor !== 'function') return;\n try {\n lifecycle.registerRetentionFloor(SysJobQueue.name, adapter.retentionFloor());\n ctx.logger.info(\n `QueueServicePlugin: registered a ${adapter.idempotencyWindowMs}ms retention floor on ${SysJobQueue.name} `\n + 'with the lifecycle service (settings overrides below it are rejected)',\n );\n } catch (err) {\n // A floor the service refused is a wiring bug in THIS plugin, not a\n // degraded deployment — but it must not stop the queue from coming up.\n ctx.logger.error(\n 'QueueServicePlugin: the lifecycle service rejected the sys_job_queue retention floor. A `lifecycle` '\n + 'settings override may now shorten sys_job_queue.retention below the idempotency window, in which case '\n + 'publish would silently re-accept duplicates (#5195). Fix the floor registration, or keep '\n + 'lifecycle.retention_overrides.sys_job_queue unset.',\n err as any,\n );\n }\n }\n\n async destroy(): Promise<void> {\n await this.dbAdapter?.stop();\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IQueueService, QueuePublishOptions, QueueMessage, QueueHandler } from '@objectstack/spec/contracts';\n\n/**\n * Configuration options for MemoryQueueAdapter.\n */\nexport interface MemoryQueueAdapterOptions {\n /** Maximum number of messages retained per queue (0 = unlimited) */\n maxQueueSize?: number;\n}\n\n/**\n * In-memory queue adapter implementing IQueueService.\n *\n * Provides synchronous in-process pub/sub delivery.\n * Suitable for single-process environments, development, and testing.\n */\nexport class MemoryQueueAdapter implements IQueueService {\n private readonly handlers = new Map<string, QueueHandler[]>();\n private readonly deadLetters: QueueMessage[] = [];\n private msgCounter = 0;\n private readonly maxQueueSize: number;\n\n constructor(options: MemoryQueueAdapterOptions = {}) {\n this.maxQueueSize = options.maxQueueSize ?? 0;\n }\n\n async publish<T = unknown>(queue: string, data: T, options?: QueuePublishOptions): Promise<string> {\n const id = `msg-${++this.msgCounter}`;\n const msg: QueueMessage<T> = {\n id,\n data,\n attempts: 0,\n timestamp: Date.now(),\n };\n\n const fns = this.handlers.get(queue) ?? [];\n if (fns.length === 0) {\n // No subscribers — retain as dead letter if within limits\n if (this.maxQueueSize === 0 || this.deadLetters.length < this.maxQueueSize) {\n this.deadLetters.push(msg);\n }\n return id;\n }\n\n const maxRetries = options?.retries ?? 0;\n for (const handler of fns) {\n let attempt = 0;\n let success = false;\n while (!success && attempt <= maxRetries) {\n try {\n msg.attempts = attempt + 1;\n await handler(msg as QueueMessage);\n success = true;\n } catch {\n attempt++;\n }\n }\n }\n\n return id;\n }\n\n async subscribe<T = unknown>(queue: string, handler: QueueHandler<T>): Promise<void> {\n const existing = this.handlers.get(queue) ?? [];\n this.handlers.set(queue, [...existing, handler as QueueHandler]);\n }\n\n async unsubscribe(queue: string): Promise<void> {\n this.handlers.delete(queue);\n }\n\n async getQueueSize(_queue: string): Promise<number> {\n // In-memory: no persistent queue depth tracking\n return 0;\n }\n\n async purge(queue: string): Promise<void> {\n this.handlers.delete(queue);\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IQueueService,\n QueuePublishOptions,\n QueueMessage,\n QueueMessageRecord,\n QueueHandler,\n} from '@objectstack/spec/contracts';\nimport { SysJobQueue } from '@objectstack/platform-objects/audit';\nimport {\n SYSTEM_CTX,\n uid,\n nowIso,\n parseJson,\n lifecycleDurationMs,\n type JobEngine,\n type JobClock,\n type JobLogger,\n} from './common.js';\n\nconst QUEUE_TABLE = 'sys_job_queue';\n\n/**\n * How long a `completed` row survives before the platform Reaper deletes it.\n *\n * Read from the object's own ADR-0057 declaration\n * (`sys_job_queue.lifecycle.retention`, #5179) instead of being a second\n * number here: the declaration is what actually runs (LifecycleService sweeps\n * every registered object hourly), so a copy in this file could only ever be\n * a copy that drifts. A missing or unparseable declaration throws: the queue's\n * dedup contract below is defined against this window, so \"no window\" is not a\n * state the adapter can run in.\n */\nexport function completedRetentionWindowMs(): number {\n const maxAge = SysJobQueue.lifecycle?.retention?.maxAge;\n if (!maxAge) {\n throw new Error(\n '[service-queue] sys_job_queue no longer declares lifecycle.retention — DbQueueAdapter dedups against '\n + 'terminal rows by `created_at` window and relies on that declared retention to keep them (ADR-0057, #5179). '\n + 'Restore the declaration in @objectstack/platform-objects rather than sweeping the table from here.',\n );\n }\n return lifecycleDurationMs(maxAge);\n}\n\n/**\n * [#5195] The shape `LifecycleService.registerRetentionFloor()` accepts.\n *\n * Restated here rather than imported: `@objectstack/objectql` is a\n * devDependency of this package on purpose (the queue must not drag the engine\n * into every install), so its types are not available to this package's\n * consumers at build time.\n */\nexport interface QueueRetentionFloor {\n policy: 'retention';\n minWindowMs: number;\n declaredBy: string;\n consequence: string;\n remedy: string;\n}\n\n/**\n * [#5195] The `lifecycle` slot's contract as THIS package consumes it — the one\n * method `QueueServicePlugin` calls, and nothing else.\n *\n * Declared rather than erased to `any` at the lookup (#4127/#4251): `any` would\n * switch off checking on the single call that carries the floor, so a rename or\n * a changed argument order in `LifecycleService.registerRetentionFloor` would\n * compile here and fail at runtime inside a `try` that logs and continues —\n * i.e. the floor would silently not exist, which is precisely the silent\n * bypass #5195 exists to close.\n *\n * `registerRetentionFloor` is **optional** on purpose, and that optionality is\n * the honest part of the contract: a kernel may carry a lifecycle service that\n * predates floors, so the runtime `typeof … === 'function'` probe below is a\n * real check and the type says so, instead of an `any` that hides both the\n * check and the call.\n */\nexport interface LifecycleFloorRegistrar {\n registerRetentionFloor?(object: string, floor: QueueRetentionFloor): void;\n}\n\nexport interface DbQueueAdapterOptions {\n /** Polling interval for the worker loop (ms, default 1000) */\n pollIntervalMs?: number;\n /** Max messages claimed per poll tick (default 10) */\n batchSize?: number;\n /** Lease duration before another worker may reclaim (ms, default 30000) */\n leaseMs?: number;\n /**\n * Idempotency window — how long the same key blocks re-publish (ms, default 24h).\n *\n * Must not exceed `sys_job_queue`'s declared retention for `completed` rows\n * ({@link completedRetentionWindowMs}, 7d): the window is evaluated against\n * rows that are still in the table, so a longer window would silently start\n * accepting duplicates as soon as the Reaper swept the row it dedups\n * against. The constructor rejects that configuration (#5179).\n */\n idempotencyWindowMs?: number;\n /** Default maxAttempts when publish doesn't specify (default 3) */\n defaultMaxAttempts?: number;\n /** Unique identifier for this worker (default: random) */\n workerId?: string;\n /** Whether to auto-start the polling worker (default true) */\n autoStart?: boolean;\n}\n\ninterface RegisteredHandler {\n queue: string;\n fn: QueueHandler;\n}\n\n/**\n * DbQueueAdapter — durable, polling, DB-backed IQueueService.\n *\n * Persists every message to `sys_job_queue`. A polling worker leases\n * pending messages (CAS update status pending→running with a lease),\n * invokes registered subscribers, and retries with backoff on failure.\n * Messages that exceed `max_attempts` land in `status='dlq'`.\n *\n * Idempotency: publish suppresses duplicates within a configurable\n * window when `(queue, idempotencyKey)` is non-null.\n *\n * Retention: this adapter does NOT sweep the table. `completed` rows are\n * bounded by `sys_job_queue`'s declared ADR-0057 retention (7d, filtered to\n * `status='completed'`), enforced by the one platform-owned\n * `LifecycleService` reaper — see the object definition in\n * `@objectstack/platform-objects` and {@link completedRetentionWindowMs}.\n * `dlq`/`failed` rows are never swept; they are the dead-letter surface\n * ({@link DbQueueAdapter.listFailed} / {@link DbQueueAdapter.replay} /\n * {@link DbQueueAdapter.purgeFailed}).\n *\n * Designed for SQLite and Postgres alike — uses CAS via WHERE-clauses,\n * not row-level locking.\n */\nexport class DbQueueAdapter implements IQueueService {\n private readonly engine: JobEngine;\n private readonly logger?: JobLogger;\n private readonly clock?: JobClock;\n private readonly opts: Required<Omit<DbQueueAdapterOptions, 'workerId'>> & { workerId: string };\n\n private readonly handlers = new Map<string, RegisteredHandler[]>();\n private timer?: ReturnType<typeof setInterval>;\n private running = false;\n\n constructor(args: {\n engine: JobEngine;\n logger?: JobLogger;\n clock?: JobClock;\n options?: DbQueueAdapterOptions;\n }) {\n this.engine = args.engine;\n this.logger = args.logger;\n this.clock = args.clock;\n const o = args.options ?? {};\n this.opts = {\n pollIntervalMs: o.pollIntervalMs ?? 1000,\n batchSize: o.batchSize ?? 10,\n leaseMs: o.leaseMs ?? 30_000,\n idempotencyWindowMs: o.idempotencyWindowMs ?? 24 * 60 * 60 * 1000,\n defaultMaxAttempts: o.defaultMaxAttempts ?? 3,\n autoStart: o.autoStart ?? true,\n workerId: o.workerId ?? uid('worker'),\n };\n\n // [#5179] The dedup window only means anything while the row it dedups\n // against still exists. `completed` rows now expire on the declared\n // retention window, so an idempotency window LONGER than it would quietly\n // degrade into \"dedup for as long as the Reaper happens not to have run\" —\n // duplicate deliveries appearing days later, with nothing in any log. The\n // two windows are ordered here, at construction, rather than tolerated at\n // publish time: the fix is a config or declaration change, and both are\n // named in the message.\n const retentionMs = completedRetentionWindowMs();\n if (this.opts.idempotencyWindowMs > retentionMs) {\n throw new Error(\n `[service-queue] idempotencyWindowMs (${this.opts.idempotencyWindowMs}ms) exceeds the retention window `\n + `sys_job_queue declares for completed rows (${retentionMs}ms, lifecycle.retention.maxAge — ADR-0057). `\n + 'Terminal-row dedup is evaluated by `created_at` against that same window, so the longer setting would '\n + 'silently accept duplicates once a row is reaped. Lower idempotencyWindowMs, or raise the declared '\n + 'retention (both windows are measured from `created_at`).',\n );\n }\n }\n\n /** The configured dedup window (ms) — the number the floor below is made of. */\n get idempotencyWindowMs(): number {\n return this.opts.idempotencyWindowMs;\n }\n\n /**\n * [#5195] The retention floor `sys_job_queue` must satisfy for this adapter's\n * dedup contract to mean anything, handed to `LifecycleService`\n * (`registerRetentionFloor`) by `QueueServicePlugin`.\n *\n * The constructor check above only reads the object's **declaration**. ADR-0057\n * P4 lets an operator override that window per environment/tenant through the\n * `lifecycle` settings namespace, which the constructor cannot see: set\n * `lifecycle.retention_overrides.sys_job_queue.maxAge = '1h'` and completed\n * rows vanish an hour after they are written while publish keeps dedupping\n * against a 24h window — duplicate deliveries resume, with nothing in any log.\n * Registering the floor is what closes that door, and it carries the number\n * this adapter was actually CONSTRUCTED with rather than a static copy of the\n * default (a per-kernel option cannot live in the object's declaration).\n */\n retentionFloor(): QueueRetentionFloor {\n const ms = this.opts.idempotencyWindowMs;\n // Settings are authored as ADR-0057 duration literals, not milliseconds, so\n // the remedy quotes one the operator can paste — rounded UP, since a\n // rounded-down literal would be rejected by the very floor it is meant to\n // satisfy.\n const literal = `${Math.ceil(ms / 3_600_000)}h`;\n return {\n policy: 'retention',\n minWindowMs: ms,\n declaredBy: 'com.objectstack.service.queue',\n consequence:\n `DbQueueAdapter dedups sys_job_queue publishes by comparing created_at against its ${ms}ms `\n + 'idempotency window, so a shorter retention deletes the very rows that check reads — '\n + 'duplicate deliveries resume silently, with nothing in any log.',\n remedy:\n `set lifecycle.retention_overrides.sys_job_queue.maxAge to '${literal}' or longer, or lower `\n + \"QueueServicePlugin's db.idempotencyWindowMs to the window you actually want (both are measured \"\n + 'from created_at).',\n };\n }\n\n // ── IQueueService ────────────────────────────────────────────────\n\n async publish<T = unknown>(\n queue: string,\n data: T,\n options?: QueuePublishOptions,\n ): Promise<string> {\n const opts = options ?? {};\n const now = this.now();\n\n // Idempotency check.\n //\n // [#5179] This is the reason `sys_job_queue`'s retention is filtered and\n // generous rather than aggressive: a terminal (`completed`/`dlq`) row\n // blocks a re-publish only while its `created_at` is inside the\n // idempotency window, so the row must SURVIVE that long. The declared\n // retention (7d on `completed`, nothing on `dlq`) is measured on the very\n // same `created_at` axis and is ≥ this window — enforced in the\n // constructor — which makes \"the reaper deleted a row the dedup check\n // needed\" unrepresentable rather than merely unlikely.\n if (opts.idempotencyKey) {\n const windowStart = new Date(now.getTime() - this.opts.idempotencyWindowMs).toISOString();\n const existing = await this.engine.find(QUEUE_TABLE, {\n where: {\n queue,\n idempotency_key: opts.idempotencyKey,\n // Only block if not yet terminal — completed/dlq dedup is by window via created_at\n },\n limit: 5,\n context: SYSTEM_CTX,\n });\n const blocking = (existing ?? []).find((row: any) => {\n if (row.status === 'pending' || row.status === 'running') return true;\n return String(row.created_at ?? '') >= windowStart;\n });\n if (blocking) return String(blocking.id);\n }\n\n const id = uid('msg');\n const scheduledFor = opts.scheduledFor\n ? new Date(opts.scheduledFor).toISOString()\n : opts.delay\n ? new Date(now.getTime() + opts.delay).toISOString()\n : now.toISOString();\n\n const maxAttempts = opts.maxAttempts\n ?? (opts.retries != null ? opts.retries + 1 : this.opts.defaultMaxAttempts);\n const backoff = opts.backoff ?? { type: 'exponential' as const, delayMs: 1000 };\n\n await this.engine.insert(QUEUE_TABLE, {\n id,\n queue,\n idempotency_key: opts.idempotencyKey ?? null,\n payload_json: JSON.stringify(data ?? null),\n metadata_json: opts.metadata ? JSON.stringify(opts.metadata) : null,\n status: 'pending',\n priority: opts.priority ?? 100,\n attempts: 0,\n max_attempts: maxAttempts,\n backoff_type: backoff.type,\n backoff_delay_ms: backoff.delayMs,\n backoff_max_delay_ms: backoff.maxDelayMs ?? null,\n scheduled_for: scheduledFor,\n created_at: now.toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n\n return id;\n }\n\n async subscribe<T = unknown>(queue: string, handler: QueueHandler<T>): Promise<void> {\n const existing = this.handlers.get(queue) ?? [];\n existing.push({ queue, fn: handler as QueueHandler });\n this.handlers.set(queue, existing);\n if (this.opts.autoStart) this.start();\n }\n\n async unsubscribe(queue: string): Promise<void> {\n this.handlers.delete(queue);\n }\n\n async getQueueSize(queue: string): Promise<number> {\n const rows = await this.engine.find(QUEUE_TABLE, {\n where: { queue, status: 'pending' },\n limit: 10_000,\n context: SYSTEM_CTX,\n });\n return rows?.length ?? 0;\n }\n\n async purge(queue: string): Promise<void> {\n const rows = await this.engine.find(QUEUE_TABLE, {\n where: { queue, status: 'pending' },\n limit: 10_000,\n context: SYSTEM_CTX,\n });\n for (const row of rows ?? []) {\n // `where: { id }` — the engine's delete has no top-level `id` option.\n // The old `{ id: row.id }` bag carried no predicate at all, so every\n // purge delete threw \"Delete requires an ID or options.multi=true\"\n // straight into this catch: purge logged a warn per row and deleted\n // NOTHING (#4371 option-2 survey).\n try { await this.engine.delete(QUEUE_TABLE, { where: { id: row.id }, context: SYSTEM_CTX }); }\n catch (err) { this.logger?.warn?.('DbQueueAdapter: purge delete failed', err as any); }\n }\n }\n\n async listFailed(\n queue?: string,\n options?: { limit?: number; offset?: number },\n ): Promise<QueueMessageRecord[]> {\n const where: any = { status: 'dlq' };\n if (queue) where.queue = queue;\n const rows = await this.engine.find(QUEUE_TABLE, {\n where,\n limit: options?.limit ?? 100,\n offset: options?.offset,\n orderBy: [{ field: 'created_at', order: 'desc' }],\n context: SYSTEM_CTX,\n });\n return (rows ?? []).map((r: any) => this.rowToRecord(r));\n }\n\n async replay(messageId: string): Promise<void> {\n const row = await this.loadById(messageId);\n if (!row) throw new Error(`MESSAGE_NOT_FOUND: ${messageId}`);\n if (row.status !== 'dlq' && row.status !== 'failed') {\n throw new Error(`INVALID_STATE: cannot replay message in status=${row.status}`);\n }\n const now = this.now();\n await this.engine.update(QUEUE_TABLE, {\n id: messageId,\n status: 'pending',\n attempts: 0,\n last_error: null,\n locked_by: null,\n locked_until: null,\n scheduled_for: now.toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n }\n\n async purgeFailed(messageId: string): Promise<void> {\n const row = await this.loadById(messageId);\n if (!row) return;\n if (row.status !== 'dlq' && row.status !== 'failed') {\n throw new Error(`INVALID_STATE: cannot purge message in status=${row.status}`);\n }\n await this.engine.delete(QUEUE_TABLE, { where: { id: messageId }, context: SYSTEM_CTX });\n }\n\n // ── Worker lifecycle ─────────────────────────────────────────────\n\n start(): void {\n if (this.timer) return;\n this.timer = setInterval(() => {\n if (this.running) return;\n this.running = true;\n this.pollOnce()\n .catch((err) => { this.logger?.warn?.('DbQueueAdapter: poll tick failed', err); })\n .finally(() => { this.running = false; });\n }, this.opts.pollIntervalMs);\n (this.timer as any)?.unref?.();\n }\n\n async stop(): Promise<void> {\n if (this.timer) { clearInterval(this.timer); this.timer = undefined; }\n }\n\n /** Test-friendly synchronous poll. */\n async pollOnce(): Promise<number> {\n const queues = [...this.handlers.keys()];\n if (queues.length === 0) return 0;\n\n let processed = 0;\n for (const queue of queues) {\n const claimed = await this.claimBatch(queue, this.opts.batchSize);\n for (const row of claimed) {\n await this.dispatch(row);\n processed++;\n }\n }\n return processed;\n }\n\n // ── Internals ────────────────────────────────────────────────────\n\n private async claimBatch(queue: string, max: number): Promise<any[]> {\n const now = this.now();\n const candidates = await this.engine.find(QUEUE_TABLE, {\n where: { queue, status: 'pending' },\n limit: max * 3, // over-fetch in case of CAS contention\n orderBy: [\n { field: 'priority', order: 'asc' },\n { field: 'scheduled_for', order: 'asc' },\n ],\n context: SYSTEM_CTX,\n });\n\n const out: any[] = [];\n for (const row of candidates ?? []) {\n if (out.length >= max) break;\n const sched = row.scheduled_for ? new Date(row.scheduled_for).getTime() : 0;\n if (sched > now.getTime()) continue;\n // Honor existing lease\n const lockedUntil = row.locked_until ? new Date(row.locked_until).getTime() : 0;\n if (row.locked_by && lockedUntil > now.getTime()) continue;\n\n // CAS — only update if still pending (best-effort with engine.update which\n // typically does row-level update by id; concurrent workers will overwrite\n // each other but the dispatcher tolerates duplicate delivery via attempts).\n try {\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'running',\n locked_by: this.opts.workerId,\n locked_until: new Date(now.getTime() + this.opts.leaseMs).toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n out.push({ ...row, status: 'running' });\n } catch (err) {\n this.logger?.warn?.('DbQueueAdapter: claim CAS failed', err as any);\n }\n }\n return out;\n }\n\n private async dispatch(row: any): Promise<void> {\n const handlers = this.handlers.get(row.queue) ?? [];\n if (handlers.length === 0) {\n // No handler — release lease so another process can pick it up\n await this.releasePending(row.id);\n return;\n }\n\n const msg: QueueMessage = {\n id: String(row.id),\n data: parseJson(row.payload_json),\n attempts: (row.attempts ?? 0) + 1,\n timestamp: row.created_at ? new Date(row.created_at).getTime() : Date.now(),\n };\n\n let success = true;\n let lastError: string | undefined;\n for (const h of handlers) {\n try { await h.fn(msg); }\n catch (err) {\n success = false;\n lastError = err instanceof Error ? err.message : String(err);\n this.logger?.warn?.(`DbQueueAdapter: handler failed on ${row.queue}`, err as any);\n break;\n }\n }\n\n const now = this.now();\n if (success) {\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'completed',\n attempts: msg.attempts,\n completed_at: now.toISOString(),\n locked_by: null,\n locked_until: null,\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n return;\n }\n\n const attempts = msg.attempts;\n const max = row.max_attempts ?? this.opts.defaultMaxAttempts;\n if (attempts >= max) {\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'dlq',\n attempts,\n last_error: lastError ?? 'unknown error',\n completed_at: now.toISOString(),\n locked_by: null,\n locked_until: null,\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n return;\n }\n\n const backoffMs = this.computeBackoff(row, attempts);\n await this.engine.update(QUEUE_TABLE, {\n id: row.id,\n status: 'pending',\n attempts,\n last_error: lastError ?? 'unknown error',\n scheduled_for: new Date(now.getTime() + backoffMs).toISOString(),\n locked_by: null,\n locked_until: null,\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n }\n\n private computeBackoff(row: any, attempt: number): number {\n const base = row.backoff_delay_ms ?? 1000;\n const cap = row.backoff_max_delay_ms ?? undefined;\n if ((row.backoff_type ?? 'exponential') === 'fixed') return base;\n const exp = base * Math.pow(2, Math.max(0, attempt - 1));\n return cap ? Math.min(exp, cap) : exp;\n }\n\n private async releasePending(id: string): Promise<void> {\n const now = this.now();\n try {\n await this.engine.update(QUEUE_TABLE, {\n id,\n status: 'pending',\n locked_by: null,\n locked_until: null,\n scheduled_for: new Date(now.getTime() + this.opts.pollIntervalMs * 5).toISOString(),\n updated_at: now.toISOString(),\n }, { context: SYSTEM_CTX });\n } catch (err) {\n this.logger?.warn?.('DbQueueAdapter: release failed', err as any);\n }\n }\n\n private async loadById(id: string): Promise<any | null> {\n const rows = await this.engine.find(QUEUE_TABLE, {\n where: { id },\n limit: 1,\n context: SYSTEM_CTX,\n });\n return rows?.[0] ?? null;\n }\n\n private rowToRecord(r: any): QueueMessageRecord {\n return {\n id: String(r.id),\n queue: String(r.queue),\n data: parseJson(r.payload_json),\n status: r.status,\n attempts: r.attempts ?? 0,\n maxAttempts: r.max_attempts ?? this.opts.defaultMaxAttempts,\n scheduledFor: r.scheduled_for ?? undefined,\n lockedBy: r.locked_by ?? undefined,\n lockedUntil: r.locked_until ?? undefined,\n lastError: r.last_error ?? undefined,\n idempotencyKey: r.idempotency_key ?? undefined,\n metadata: parseJson(r.metadata_json),\n createdAt: r.created_at ?? nowIso(this.clock),\n updatedAt: r.updated_at ?? undefined,\n completedAt: r.completed_at ?? undefined,\n };\n }\n\n private now(): Date {\n return this.clock?.now() ?? new Date();\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Narrow ObjectQL engine surface used by job/queue adapters.\n * Keeps the adapter testable without booting a real kernel.\n *\n * IMPORTANT: matches the canonical engine API:\n * - find: `where:` (NOT `filter:`)\n * - update: `(table, {id, ...patch}, opts)`\n */\nexport interface JobEngine {\n find(object: string, options?: any): Promise<any[]>;\n insert(object: string, data: any, options?: any): Promise<any>;\n update(object: string, idOrData: any, dataOrOptions?: any, options?: any): Promise<any>;\n delete(object: string, options?: any): Promise<any>;\n}\n\n/** Stamped only in tests to make `now` deterministic. */\nexport interface JobClock { now(): Date }\n\nexport interface JobLogger {\n info(msg: string, meta?: unknown): void;\n warn(msg: string, meta?: unknown): void;\n error?(msg: string, meta?: unknown): void;\n}\n\nexport const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;\n\nexport function uid(prefix: string): string {\n const g: any = globalThis as any;\n if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;\n return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nexport function nowIso(clock?: JobClock): string {\n return (clock?.now() ?? new Date()).toISOString();\n}\n\n/**\n * Milliseconds per ADR-0057 lifecycle duration unit. Mirrors\n * `parseLifecycleDuration` in `@objectstack/objectql` (the canonical runtime\n * consumer), reproduced here rather than imported because the queue adapters\n * deliberately do not depend on the engine package — they duck-type\n * {@link JobEngine} so they stay testable without booting a kernel. Both\n * tables are fixed by the ADR (coarse operational bounds: `y` is 365 days),\n * and `job-queue-retention.test.ts` pins this one against them.\n */\nconst LIFECYCLE_UNIT_MS: Record<string, number> = {\n h: 3_600_000,\n d: 86_400_000,\n w: 7 * 86_400_000,\n y: 365 * 86_400_000,\n};\n\n/**\n * Parse an ADR-0057 duration literal (`'6h'`, `'7d'`, `'12w'`, `'7y'`) into\n * milliseconds. Throws on anything else: declarations reach this code already\n * validated by `LifecycleSchema`, so a failure here is a broken declaration,\n * not user input — and a queue that silently guessed a window would be exactly\n * the silent behaviour #5179 is about.\n */\nexport function lifecycleDurationMs(literal: string): number {\n const m = /^(\\d+)(h|d|w|y)$/.exec(literal);\n if (!m) {\n throw new Error(\n `[service-queue] invalid lifecycle duration literal '${literal}' — expected <n><unit> with unit h|d|w|y (e.g. '7d')`,\n );\n }\n return Number(m[1]) * LIFECYCLE_UNIT_MS[m[2]!]!;\n}\n\nexport function parseJson<T = unknown>(raw: unknown, fallback?: T): T | undefined {\n if (raw == null) return fallback;\n if (typeof raw === 'string') {\n try { return JSON.parse(raw) as T; } catch { return fallback; }\n }\n if (typeof raw === 'object') return raw as T;\n return fallback;\n}\n"],"mappings":"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","names":["SysJobQueue","SysJobQueue"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/service-queue",
3
- "version": "17.0.0-rc.2",
3
+ "version": "17.0.0-rc.4",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Queue Service for ObjectStack — implements IQueueService with in-memory and durable DB-backed (sys_job_queue) adapters",
6
6
  "type": "module",
@@ -14,14 +14,15 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "@objectstack/core": "17.0.0-rc.2",
18
- "@objectstack/platform-objects": "17.0.0-rc.2",
19
- "@objectstack/spec": "17.0.0-rc.2"
17
+ "@objectstack/core": "17.0.0-rc.4",
18
+ "@objectstack/platform-objects": "17.0.0-rc.4",
19
+ "@objectstack/spec": "17.0.0-rc.4"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^26.1.2",
23
23
  "typescript": "^6.0.3",
24
- "vitest": "^4.1.10"
24
+ "vitest": "^4.1.10",
25
+ "@objectstack/objectql": "17.0.0-rc.4"
25
26
  },
26
27
  "keywords": [
27
28
  "objectstack",