@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 +253 -0
- package/README.md +29 -0
- package/dist/index.cjs +101 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -2
- package/dist/index.d.ts +76 -2
- package/dist/index.js +99 -3
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
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
|
|
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: [
|
|
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
|
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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: [
|
|
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.
|
|
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.
|
|
18
|
-
"@objectstack/platform-objects": "17.0.0-rc.
|
|
19
|
-
"@objectstack/spec": "17.0.0-rc.
|
|
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",
|