@objectstack/service-queue 4.0.5 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +408 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +113 -20
- package/dist/index.d.ts +113 -20
- package/dist/index.js +407 -8
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/dist/index.cjs
CHANGED
|
@@ -21,11 +21,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
BullMQQueueAdapter: () => BullMQQueueAdapter,
|
|
24
|
+
DbQueueAdapter: () => DbQueueAdapter,
|
|
24
25
|
MemoryQueueAdapter: () => MemoryQueueAdapter,
|
|
25
26
|
QueueServicePlugin: () => QueueServicePlugin
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(index_exports);
|
|
28
29
|
|
|
30
|
+
// src/queue-service-plugin.ts
|
|
31
|
+
var import_audit = require("@objectstack/platform-objects/audit");
|
|
32
|
+
|
|
29
33
|
// src/memory-queue-adapter.ts
|
|
30
34
|
var MemoryQueueAdapter = class {
|
|
31
35
|
constructor(options = {}) {
|
|
@@ -80,24 +84,419 @@ var MemoryQueueAdapter = class {
|
|
|
80
84
|
}
|
|
81
85
|
};
|
|
82
86
|
|
|
87
|
+
// src/common.ts
|
|
88
|
+
var SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
|
|
89
|
+
function uid(prefix) {
|
|
90
|
+
const g = globalThis;
|
|
91
|
+
if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;
|
|
92
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
93
|
+
}
|
|
94
|
+
function nowIso(clock) {
|
|
95
|
+
return (clock?.now() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
96
|
+
}
|
|
97
|
+
function parseJson(raw, fallback) {
|
|
98
|
+
if (raw == null) return fallback;
|
|
99
|
+
if (typeof raw === "string") {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(raw);
|
|
102
|
+
} catch {
|
|
103
|
+
return fallback;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (typeof raw === "object") return raw;
|
|
107
|
+
return fallback;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/db-queue-adapter.ts
|
|
111
|
+
var QUEUE_TABLE = "sys_job_queue";
|
|
112
|
+
var DbQueueAdapter = class {
|
|
113
|
+
constructor(args) {
|
|
114
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
115
|
+
this.running = false;
|
|
116
|
+
this.engine = args.engine;
|
|
117
|
+
this.logger = args.logger;
|
|
118
|
+
this.clock = args.clock;
|
|
119
|
+
const o = args.options ?? {};
|
|
120
|
+
this.opts = {
|
|
121
|
+
pollIntervalMs: o.pollIntervalMs ?? 1e3,
|
|
122
|
+
batchSize: o.batchSize ?? 10,
|
|
123
|
+
leaseMs: o.leaseMs ?? 3e4,
|
|
124
|
+
idempotencyWindowMs: o.idempotencyWindowMs ?? 24 * 60 * 60 * 1e3,
|
|
125
|
+
defaultMaxAttempts: o.defaultMaxAttempts ?? 3,
|
|
126
|
+
autoStart: o.autoStart ?? true,
|
|
127
|
+
workerId: o.workerId ?? uid("worker")
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// ── IQueueService ────────────────────────────────────────────────
|
|
131
|
+
async publish(queue, data, options) {
|
|
132
|
+
const opts = options ?? {};
|
|
133
|
+
const now = this.now();
|
|
134
|
+
if (opts.idempotencyKey) {
|
|
135
|
+
const windowStart = new Date(now.getTime() - this.opts.idempotencyWindowMs).toISOString();
|
|
136
|
+
const existing = await this.engine.find(QUEUE_TABLE, {
|
|
137
|
+
where: {
|
|
138
|
+
queue,
|
|
139
|
+
idempotency_key: opts.idempotencyKey
|
|
140
|
+
// Only block if not yet terminal — completed/dlq dedup is by window via created_at
|
|
141
|
+
},
|
|
142
|
+
limit: 5,
|
|
143
|
+
context: SYSTEM_CTX
|
|
144
|
+
});
|
|
145
|
+
const blocking = (existing ?? []).find((row) => {
|
|
146
|
+
if (row.status === "pending" || row.status === "running") return true;
|
|
147
|
+
return String(row.created_at ?? "") >= windowStart;
|
|
148
|
+
});
|
|
149
|
+
if (blocking) return String(blocking.id);
|
|
150
|
+
}
|
|
151
|
+
const id = uid("msg");
|
|
152
|
+
const scheduledFor = opts.scheduledFor ? new Date(opts.scheduledFor).toISOString() : opts.delay ? new Date(now.getTime() + opts.delay).toISOString() : now.toISOString();
|
|
153
|
+
const maxAttempts = opts.maxAttempts ?? (opts.retries != null ? opts.retries + 1 : this.opts.defaultMaxAttempts);
|
|
154
|
+
const backoff = opts.backoff ?? { type: "exponential", delayMs: 1e3 };
|
|
155
|
+
await this.engine.insert(QUEUE_TABLE, {
|
|
156
|
+
id,
|
|
157
|
+
queue,
|
|
158
|
+
idempotency_key: opts.idempotencyKey ?? null,
|
|
159
|
+
payload_json: JSON.stringify(data ?? null),
|
|
160
|
+
metadata_json: opts.metadata ? JSON.stringify(opts.metadata) : null,
|
|
161
|
+
status: "pending",
|
|
162
|
+
priority: opts.priority ?? 100,
|
|
163
|
+
attempts: 0,
|
|
164
|
+
max_attempts: maxAttempts,
|
|
165
|
+
backoff_type: backoff.type,
|
|
166
|
+
backoff_delay_ms: backoff.delayMs,
|
|
167
|
+
backoff_max_delay_ms: backoff.maxDelayMs ?? null,
|
|
168
|
+
scheduled_for: scheduledFor,
|
|
169
|
+
created_at: now.toISOString(),
|
|
170
|
+
updated_at: now.toISOString()
|
|
171
|
+
}, { context: SYSTEM_CTX });
|
|
172
|
+
return id;
|
|
173
|
+
}
|
|
174
|
+
async subscribe(queue, handler) {
|
|
175
|
+
const existing = this.handlers.get(queue) ?? [];
|
|
176
|
+
existing.push({ queue, fn: handler });
|
|
177
|
+
this.handlers.set(queue, existing);
|
|
178
|
+
if (this.opts.autoStart) this.start();
|
|
179
|
+
}
|
|
180
|
+
async unsubscribe(queue) {
|
|
181
|
+
this.handlers.delete(queue);
|
|
182
|
+
}
|
|
183
|
+
async getQueueSize(queue) {
|
|
184
|
+
const rows = await this.engine.find(QUEUE_TABLE, {
|
|
185
|
+
where: { queue, status: "pending" },
|
|
186
|
+
limit: 1e4,
|
|
187
|
+
context: SYSTEM_CTX
|
|
188
|
+
});
|
|
189
|
+
return rows?.length ?? 0;
|
|
190
|
+
}
|
|
191
|
+
async purge(queue) {
|
|
192
|
+
const rows = await this.engine.find(QUEUE_TABLE, {
|
|
193
|
+
where: { queue, status: "pending" },
|
|
194
|
+
limit: 1e4,
|
|
195
|
+
context: SYSTEM_CTX
|
|
196
|
+
});
|
|
197
|
+
for (const row of rows ?? []) {
|
|
198
|
+
try {
|
|
199
|
+
await this.engine.delete(QUEUE_TABLE, { id: row.id, context: SYSTEM_CTX });
|
|
200
|
+
} catch (err) {
|
|
201
|
+
this.logger?.warn?.("DbQueueAdapter: purge delete failed", err);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async listFailed(queue, options) {
|
|
206
|
+
const where = { status: "dlq" };
|
|
207
|
+
if (queue) where.queue = queue;
|
|
208
|
+
const rows = await this.engine.find(QUEUE_TABLE, {
|
|
209
|
+
where,
|
|
210
|
+
limit: options?.limit ?? 100,
|
|
211
|
+
offset: options?.offset,
|
|
212
|
+
orderBy: [{ field: "created_at", direction: "desc" }],
|
|
213
|
+
context: SYSTEM_CTX
|
|
214
|
+
});
|
|
215
|
+
return (rows ?? []).map((r) => this.rowToRecord(r));
|
|
216
|
+
}
|
|
217
|
+
async replay(messageId) {
|
|
218
|
+
const row = await this.loadById(messageId);
|
|
219
|
+
if (!row) throw new Error(`MESSAGE_NOT_FOUND: ${messageId}`);
|
|
220
|
+
if (row.status !== "dlq" && row.status !== "failed") {
|
|
221
|
+
throw new Error(`INVALID_STATE: cannot replay message in status=${row.status}`);
|
|
222
|
+
}
|
|
223
|
+
const now = this.now();
|
|
224
|
+
await this.engine.update(QUEUE_TABLE, {
|
|
225
|
+
id: messageId,
|
|
226
|
+
status: "pending",
|
|
227
|
+
attempts: 0,
|
|
228
|
+
last_error: null,
|
|
229
|
+
locked_by: null,
|
|
230
|
+
locked_until: null,
|
|
231
|
+
scheduled_for: now.toISOString(),
|
|
232
|
+
updated_at: now.toISOString()
|
|
233
|
+
}, { context: SYSTEM_CTX });
|
|
234
|
+
}
|
|
235
|
+
async purgeFailed(messageId) {
|
|
236
|
+
const row = await this.loadById(messageId);
|
|
237
|
+
if (!row) return;
|
|
238
|
+
if (row.status !== "dlq" && row.status !== "failed") {
|
|
239
|
+
throw new Error(`INVALID_STATE: cannot purge message in status=${row.status}`);
|
|
240
|
+
}
|
|
241
|
+
await this.engine.delete(QUEUE_TABLE, { id: messageId, context: SYSTEM_CTX });
|
|
242
|
+
}
|
|
243
|
+
// ── Worker lifecycle ─────────────────────────────────────────────
|
|
244
|
+
start() {
|
|
245
|
+
if (this.timer) return;
|
|
246
|
+
this.timer = setInterval(() => {
|
|
247
|
+
if (this.running) return;
|
|
248
|
+
this.running = true;
|
|
249
|
+
this.pollOnce().catch((err) => {
|
|
250
|
+
this.logger?.warn?.("DbQueueAdapter: poll tick failed", err);
|
|
251
|
+
}).finally(() => {
|
|
252
|
+
this.running = false;
|
|
253
|
+
});
|
|
254
|
+
}, this.opts.pollIntervalMs);
|
|
255
|
+
this.timer?.unref?.();
|
|
256
|
+
}
|
|
257
|
+
async stop() {
|
|
258
|
+
if (this.timer) {
|
|
259
|
+
clearInterval(this.timer);
|
|
260
|
+
this.timer = void 0;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/** Test-friendly synchronous poll. */
|
|
264
|
+
async pollOnce() {
|
|
265
|
+
const queues = [...this.handlers.keys()];
|
|
266
|
+
if (queues.length === 0) return 0;
|
|
267
|
+
let processed = 0;
|
|
268
|
+
for (const queue of queues) {
|
|
269
|
+
const claimed = await this.claimBatch(queue, this.opts.batchSize);
|
|
270
|
+
for (const row of claimed) {
|
|
271
|
+
await this.dispatch(row);
|
|
272
|
+
processed++;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return processed;
|
|
276
|
+
}
|
|
277
|
+
// ── Internals ────────────────────────────────────────────────────
|
|
278
|
+
async claimBatch(queue, max) {
|
|
279
|
+
const now = this.now();
|
|
280
|
+
const candidates = await this.engine.find(QUEUE_TABLE, {
|
|
281
|
+
where: { queue, status: "pending" },
|
|
282
|
+
limit: max * 3,
|
|
283
|
+
// over-fetch in case of CAS contention
|
|
284
|
+
orderBy: [
|
|
285
|
+
{ field: "priority", direction: "asc" },
|
|
286
|
+
{ field: "scheduled_for", direction: "asc" }
|
|
287
|
+
],
|
|
288
|
+
context: SYSTEM_CTX
|
|
289
|
+
});
|
|
290
|
+
const out = [];
|
|
291
|
+
for (const row of candidates ?? []) {
|
|
292
|
+
if (out.length >= max) break;
|
|
293
|
+
const sched = row.scheduled_for ? new Date(row.scheduled_for).getTime() : 0;
|
|
294
|
+
if (sched > now.getTime()) continue;
|
|
295
|
+
const lockedUntil = row.locked_until ? new Date(row.locked_until).getTime() : 0;
|
|
296
|
+
if (row.locked_by && lockedUntil > now.getTime()) continue;
|
|
297
|
+
try {
|
|
298
|
+
await this.engine.update(QUEUE_TABLE, {
|
|
299
|
+
id: row.id,
|
|
300
|
+
status: "running",
|
|
301
|
+
locked_by: this.opts.workerId,
|
|
302
|
+
locked_until: new Date(now.getTime() + this.opts.leaseMs).toISOString(),
|
|
303
|
+
updated_at: now.toISOString()
|
|
304
|
+
}, { context: SYSTEM_CTX });
|
|
305
|
+
out.push({ ...row, status: "running" });
|
|
306
|
+
} catch (err) {
|
|
307
|
+
this.logger?.warn?.("DbQueueAdapter: claim CAS failed", err);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
async dispatch(row) {
|
|
313
|
+
const handlers = this.handlers.get(row.queue) ?? [];
|
|
314
|
+
if (handlers.length === 0) {
|
|
315
|
+
await this.releasePending(row.id);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const msg = {
|
|
319
|
+
id: String(row.id),
|
|
320
|
+
data: parseJson(row.payload_json),
|
|
321
|
+
attempts: (row.attempts ?? 0) + 1,
|
|
322
|
+
timestamp: row.created_at ? new Date(row.created_at).getTime() : Date.now()
|
|
323
|
+
};
|
|
324
|
+
let success = true;
|
|
325
|
+
let lastError;
|
|
326
|
+
for (const h of handlers) {
|
|
327
|
+
try {
|
|
328
|
+
await h.fn(msg);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
success = false;
|
|
331
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
332
|
+
this.logger?.warn?.(`DbQueueAdapter: handler failed on ${row.queue}`, err);
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const now = this.now();
|
|
337
|
+
if (success) {
|
|
338
|
+
await this.engine.update(QUEUE_TABLE, {
|
|
339
|
+
id: row.id,
|
|
340
|
+
status: "completed",
|
|
341
|
+
attempts: msg.attempts,
|
|
342
|
+
completed_at: now.toISOString(),
|
|
343
|
+
locked_by: null,
|
|
344
|
+
locked_until: null,
|
|
345
|
+
updated_at: now.toISOString()
|
|
346
|
+
}, { context: SYSTEM_CTX });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const attempts = msg.attempts;
|
|
350
|
+
const max = row.max_attempts ?? this.opts.defaultMaxAttempts;
|
|
351
|
+
if (attempts >= max) {
|
|
352
|
+
await this.engine.update(QUEUE_TABLE, {
|
|
353
|
+
id: row.id,
|
|
354
|
+
status: "dlq",
|
|
355
|
+
attempts,
|
|
356
|
+
last_error: lastError ?? "unknown error",
|
|
357
|
+
completed_at: now.toISOString(),
|
|
358
|
+
locked_by: null,
|
|
359
|
+
locked_until: null,
|
|
360
|
+
updated_at: now.toISOString()
|
|
361
|
+
}, { context: SYSTEM_CTX });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const backoffMs = this.computeBackoff(row, attempts);
|
|
365
|
+
await this.engine.update(QUEUE_TABLE, {
|
|
366
|
+
id: row.id,
|
|
367
|
+
status: "pending",
|
|
368
|
+
attempts,
|
|
369
|
+
last_error: lastError ?? "unknown error",
|
|
370
|
+
scheduled_for: new Date(now.getTime() + backoffMs).toISOString(),
|
|
371
|
+
locked_by: null,
|
|
372
|
+
locked_until: null,
|
|
373
|
+
updated_at: now.toISOString()
|
|
374
|
+
}, { context: SYSTEM_CTX });
|
|
375
|
+
}
|
|
376
|
+
computeBackoff(row, attempt) {
|
|
377
|
+
const base = row.backoff_delay_ms ?? 1e3;
|
|
378
|
+
const cap = row.backoff_max_delay_ms ?? void 0;
|
|
379
|
+
if ((row.backoff_type ?? "exponential") === "fixed") return base;
|
|
380
|
+
const exp = base * Math.pow(2, Math.max(0, attempt - 1));
|
|
381
|
+
return cap ? Math.min(exp, cap) : exp;
|
|
382
|
+
}
|
|
383
|
+
async releasePending(id) {
|
|
384
|
+
const now = this.now();
|
|
385
|
+
try {
|
|
386
|
+
await this.engine.update(QUEUE_TABLE, {
|
|
387
|
+
id,
|
|
388
|
+
status: "pending",
|
|
389
|
+
locked_by: null,
|
|
390
|
+
locked_until: null,
|
|
391
|
+
scheduled_for: new Date(now.getTime() + this.opts.pollIntervalMs * 5).toISOString(),
|
|
392
|
+
updated_at: now.toISOString()
|
|
393
|
+
}, { context: SYSTEM_CTX });
|
|
394
|
+
} catch (err) {
|
|
395
|
+
this.logger?.warn?.("DbQueueAdapter: release failed", err);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async loadById(id) {
|
|
399
|
+
const rows = await this.engine.find(QUEUE_TABLE, {
|
|
400
|
+
where: { id },
|
|
401
|
+
limit: 1,
|
|
402
|
+
context: SYSTEM_CTX
|
|
403
|
+
});
|
|
404
|
+
return rows?.[0] ?? null;
|
|
405
|
+
}
|
|
406
|
+
rowToRecord(r) {
|
|
407
|
+
return {
|
|
408
|
+
id: String(r.id),
|
|
409
|
+
queue: String(r.queue),
|
|
410
|
+
data: parseJson(r.payload_json),
|
|
411
|
+
status: r.status,
|
|
412
|
+
attempts: r.attempts ?? 0,
|
|
413
|
+
maxAttempts: r.max_attempts ?? this.opts.defaultMaxAttempts,
|
|
414
|
+
scheduledFor: r.scheduled_for ?? void 0,
|
|
415
|
+
lockedBy: r.locked_by ?? void 0,
|
|
416
|
+
lockedUntil: r.locked_until ?? void 0,
|
|
417
|
+
lastError: r.last_error ?? void 0,
|
|
418
|
+
idempotencyKey: r.idempotency_key ?? void 0,
|
|
419
|
+
metadata: parseJson(r.metadata_json),
|
|
420
|
+
createdAt: r.created_at ?? nowIso(this.clock),
|
|
421
|
+
updatedAt: r.updated_at ?? void 0,
|
|
422
|
+
completedAt: r.completed_at ?? void 0
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
now() {
|
|
426
|
+
return this.clock?.now() ?? /* @__PURE__ */ new Date();
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
83
430
|
// src/queue-service-plugin.ts
|
|
84
431
|
var QueueServicePlugin = class {
|
|
85
432
|
constructor(options = {}) {
|
|
86
433
|
this.name = "com.objectstack.service.queue";
|
|
87
|
-
this.version = "1.
|
|
434
|
+
this.version = "1.1.0";
|
|
88
435
|
this.type = "standard";
|
|
89
|
-
this.options = { adapter: "
|
|
436
|
+
this.options = { adapter: "auto", ...options };
|
|
90
437
|
}
|
|
91
438
|
async init(ctx) {
|
|
92
|
-
|
|
93
|
-
|
|
439
|
+
try {
|
|
440
|
+
ctx.getService("manifest").register({
|
|
441
|
+
id: "com.objectstack.service.queue",
|
|
442
|
+
name: "Queue Service",
|
|
443
|
+
version: "1.1.0",
|
|
444
|
+
type: "plugin",
|
|
445
|
+
scope: "system",
|
|
446
|
+
defaultDatasource: "cloud",
|
|
447
|
+
namespace: "sys",
|
|
448
|
+
objects: [import_audit.SysJobQueue]
|
|
449
|
+
});
|
|
450
|
+
} catch (err) {
|
|
451
|
+
ctx.logger.warn("QueueServicePlugin: manifest service unavailable; sys_job_queue not registered", err);
|
|
452
|
+
}
|
|
453
|
+
const choice = this.options.adapter ?? "auto";
|
|
454
|
+
if (choice === "bullmq") {
|
|
94
455
|
throw new Error(
|
|
95
|
-
'BullMQ queue adapter is not yet implemented. Use adapter: "memory" or provide a custom IQueueService
|
|
456
|
+
'BullMQ queue adapter is not yet implemented (M10.43). Use adapter: "auto", "db", or "memory", or provide a custom IQueueService.'
|
|
96
457
|
);
|
|
97
458
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
459
|
+
if (choice === "memory") {
|
|
460
|
+
const q = new MemoryQueueAdapter(this.options.memory);
|
|
461
|
+
ctx.registerService("queue", q);
|
|
462
|
+
ctx.logger.info("QueueServicePlugin: registered MemoryQueueAdapter");
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
ctx.registerService("queue", new MemoryQueueAdapter(this.options.memory));
|
|
466
|
+
ctx.hook("kernel:ready", async () => {
|
|
467
|
+
let engine = null;
|
|
468
|
+
try {
|
|
469
|
+
engine = ctx.getService("objectql");
|
|
470
|
+
} catch {
|
|
471
|
+
try {
|
|
472
|
+
engine = ctx.getService("data");
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (!engine) {
|
|
477
|
+
if (choice === "db") {
|
|
478
|
+
ctx.logger.warn("QueueServicePlugin: db adapter requested but no ObjectQL engine \u2014 staying on MemoryQueueAdapter");
|
|
479
|
+
} else {
|
|
480
|
+
ctx.logger.info("QueueServicePlugin: no ObjectQL engine \u2014 staying on MemoryQueueAdapter");
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
this.dbAdapter = new DbQueueAdapter({
|
|
485
|
+
engine,
|
|
486
|
+
logger: ctx.logger,
|
|
487
|
+
options: this.options.db
|
|
488
|
+
});
|
|
489
|
+
try {
|
|
490
|
+
ctx.replaceService?.("queue", this.dbAdapter);
|
|
491
|
+
this.dbAdapter.start();
|
|
492
|
+
ctx.logger.info("QueueServicePlugin: upgraded to DbQueueAdapter (sys_job_queue persistence)");
|
|
493
|
+
} catch (err) {
|
|
494
|
+
ctx.logger.warn("QueueServicePlugin: replaceService failed; staying on MemoryQueueAdapter", err);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
async destroy() {
|
|
499
|
+
await this.dbAdapter?.stop();
|
|
101
500
|
}
|
|
102
501
|
};
|
|
103
502
|
|
|
@@ -125,6 +524,7 @@ var BullMQQueueAdapter = class {
|
|
|
125
524
|
// Annotate the CommonJS export names for ESM import in node:
|
|
126
525
|
0 && (module.exports = {
|
|
127
526
|
BullMQQueueAdapter,
|
|
527
|
+
DbQueueAdapter,
|
|
128
528
|
MemoryQueueAdapter,
|
|
129
529
|
QueueServicePlugin
|
|
130
530
|
});
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/memory-queue-adapter.ts","../src/queue-service-plugin.ts","../src/bullmq-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 { BullMQQueueAdapter } from './bullmq-queue-adapter.js';\nexport type { BullMQQueueAdapterOptions } from './bullmq-queue-adapter.js';\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 { Plugin, PluginContext } from '@objectstack/core';\nimport { MemoryQueueAdapter } from './memory-queue-adapter.js';\nimport type { MemoryQueueAdapterOptions } from './memory-queue-adapter.js';\n\n/**\n * Configuration options for the QueueServicePlugin.\n */\nexport interface QueueServicePluginOptions {\n /** Queue adapter type (default: 'memory') */\n adapter?: 'memory' | 'bullmq';\n /** Options for the memory queue adapter */\n memory?: MemoryQueueAdapterOptions;\n /** Redis connection URL (used when adapter is 'bullmq') */\n redisUrl?: string;\n}\n\n/**\n * QueueServicePlugin — Production IQueueService implementation.\n *\n * Registers a queue service with the kernel during the init phase.\n * Supports in-memory and BullMQ adapters.\n *\n * @example\n * ```ts\n * import { ObjectKernel } from '@objectstack/core';\n * import { QueueServicePlugin } from '@objectstack/service-queue';\n *\n * const kernel = new ObjectKernel();\n * kernel.use(new QueueServicePlugin({ adapter: 'memory' }));\n * await kernel.bootstrap();\n *\n * const queue = kernel.getService('queue');\n * await queue.publish('orders', { orderId: 123 });\n * ```\n */\nexport class QueueServicePlugin implements Plugin {\n name = 'com.objectstack.service.queue';\n version = '1.0.0';\n type = 'standard';\n\n private readonly options: QueueServicePluginOptions;\n\n constructor(options: QueueServicePluginOptions = {}) {\n this.options = { adapter: 'memory', ...options };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n const adapter = this.options.adapter;\n if (adapter === 'bullmq') {\n throw new Error(\n 'BullMQ queue adapter is not yet implemented. ' +\n 'Use adapter: \"memory\" or provide a custom IQueueService via ctx.registerService(\"queue\", impl).'\n );\n }\n\n const queue = new MemoryQueueAdapter(this.options.memory);\n ctx.registerService('queue', queue);\n ctx.logger.info('QueueServicePlugin: registered memory queue adapter');\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IQueueService, QueuePublishOptions, QueueHandler } from '@objectstack/spec/contracts';\n\n/**\n * Configuration for the BullMQ queue adapter.\n */\nexport interface BullMQQueueAdapterOptions {\n /** Redis connection URL (e.g. 'redis://localhost:6379') */\n redisUrl: string;\n /** Default job options */\n defaultJobOptions?: {\n /** Number of retry attempts */\n attempts?: number;\n /** Backoff strategy */\n backoff?: { type: 'fixed' | 'exponential'; delay: number };\n };\n}\n\n/**\n * BullMQ queue adapter skeleton implementing IQueueService.\n *\n * This is a placeholder for future BullMQ integration.\n * Concrete implementation will use the `bullmq` package.\n *\n * @example\n * ```ts\n * const queue = new BullMQQueueAdapter({ redisUrl: 'redis://localhost:6379' });\n * await queue.publish('orders', { orderId: 123 });\n * ```\n */\nexport class BullMQQueueAdapter implements IQueueService {\n private readonly redisUrl: string;\n\n constructor(options: BullMQQueueAdapterOptions) {\n this.redisUrl = options.redisUrl;\n }\n\n async publish<T = unknown>(_queue: string, _data: T, _options?: QueuePublishOptions): Promise<string> {\n throw new Error(`BullMQQueueAdapter not yet implemented (url: ${this.redisUrl})`);\n }\n\n async subscribe<T = unknown>(_queue: string, _handler: QueueHandler<T>): Promise<void> {\n throw new Error('BullMQQueueAdapter not yet implemented');\n }\n\n async unsubscribe(_queue: string): Promise<void> {\n throw new Error('BullMQQueueAdapter not yet implemented');\n }\n\n async getQueueSize(_queue: string): Promise<number> {\n throw new Error('BullMQQueueAdapter not yet implemented');\n }\n\n async purge(_queue: string): Promise<void> {\n throw new Error('BullMQQueueAdapter not yet implemented');\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACkBO,IAAM,qBAAN,MAAkD;AAAA,EAMvD,YAAY,UAAqC,CAAC,GAAG;AALrD,SAAiB,WAAW,oBAAI,IAA4B;AAC5D,SAAiB,cAA8B,CAAC;AAChD,SAAQ,aAAa;AAInB,SAAK,eAAe,QAAQ,gBAAgB;AAAA,EAC9C;AAAA,EAEA,MAAM,QAAqB,OAAe,MAAS,SAAgD;AACjG,UAAM,KAAK,OAAO,EAAE,KAAK,UAAU;AACnC,UAAM,MAAuB;AAAA,MAC3B;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV,WAAW,KAAK,IAAI;AAAA,IACtB;AAEA,UAAM,MAAM,KAAK,SAAS,IAAI,KAAK,KAAK,CAAC;AACzC,QAAI,IAAI,WAAW,GAAG;AAEpB,UAAI,KAAK,iBAAiB,KAAK,KAAK,YAAY,SAAS,KAAK,cAAc;AAC1E,aAAK,YAAY,KAAK,GAAG;AAAA,MAC3B;AACA,aAAO;AAAA,IACT;AAEA,UAAM,aAAa,SAAS,WAAW;AACvC,eAAW,WAAW,KAAK;AACzB,UAAI,UAAU;AACd,UAAI,UAAU;AACd,aAAO,CAAC,WAAW,WAAW,YAAY;AACxC,YAAI;AACF,cAAI,WAAW,UAAU;AACzB,gBAAM,QAAQ,GAAmB;AACjC,oBAAU;AAAA,QACZ,QAAQ;AACN;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,UAAuB,OAAe,SAAyC;AACnF,UAAM,WAAW,KAAK,SAAS,IAAI,KAAK,KAAK,CAAC;AAC9C,SAAK,SAAS,IAAI,OAAO,CAAC,GAAG,UAAU,OAAuB,CAAC;AAAA,EACjE;AAAA,EAEA,MAAM,YAAY,OAA8B;AAC9C,SAAK,SAAS,OAAO,KAAK;AAAA,EAC5B;AAAA,EAEA,MAAM,aAAa,QAAiC;AAElD,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,MAAM,OAA8B;AACxC,SAAK,SAAS,OAAO,KAAK;AAAA,EAC5B;AACF;;;AC5CO,IAAM,qBAAN,MAA2C;AAAA,EAOhD,YAAY,UAAqC,CAAC,GAAG;AANrD,gBAAO;AACP,mBAAU;AACV,gBAAO;AAKL,SAAK,UAAU,EAAE,SAAS,UAAU,GAAG,QAAQ;AAAA,EACjD;AAAA,EAEA,MAAM,KAAK,KAAmC;AAC5C,UAAM,UAAU,KAAK,QAAQ;AAC7B,QAAI,YAAY,UAAU;AACxB,YAAM,IAAI;AAAA,QACR;AAAA,MAEF;AAAA,IACF;AAEA,UAAM,QAAQ,IAAI,mBAAmB,KAAK,QAAQ,MAAM;AACxD,QAAI,gBAAgB,SAAS,KAAK;AAClC,QAAI,OAAO,KAAK,qDAAqD;AAAA,EACvE;AACF;;;AC9BO,IAAM,qBAAN,MAAkD;AAAA,EAGvD,YAAY,SAAoC;AAC9C,SAAK,WAAW,QAAQ;AAAA,EAC1B;AAAA,EAEA,MAAM,QAAqB,QAAgB,OAAU,UAAiD;AACpG,UAAM,IAAI,MAAM,gDAAgD,KAAK,QAAQ,GAAG;AAAA,EAClF;AAAA,EAEA,MAAM,UAAuB,QAAgB,UAA0C;AACrF,UAAM,IAAI,MAAM,wCAAwC;AAAA,EAC1D;AAAA,EAEA,MAAM,YAAY,QAA+B;AAC/C,UAAM,IAAI,MAAM,wCAAwC;AAAA,EAC1D;AAAA,EAEA,MAAM,aAAa,QAAiC;AAClD,UAAM,IAAI,MAAM,wCAAwC;AAAA,EAC1D;AAAA,EAEA,MAAM,MAAM,QAA+B;AACzC,UAAM,IAAI,MAAM,wCAAwC;AAAA,EAC1D;AACF;","names":[]}
|
|
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","../src/bullmq-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 { BullMQQueueAdapter } from './bullmq-queue-adapter.js';\nexport type { BullMQQueueAdapterOptions } from './bullmq-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 * - 'bullmq': reserved for M10.43 (throws today)\n */\n adapter?: 'auto' | 'db' | 'memory' | 'bullmq';\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 /** Redis connection URL (reserved for bullmq) */\n redisUrl?: string;\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 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 === 'bullmq') {\n throw new Error(\n 'BullMQ queue adapter is not yet implemented (M10.43). ' +\n 'Use adapter: \"auto\", \"db\", or \"memory\", or provide a custom IQueueService.',\n );\n }\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, roles: [], 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 try { await this.engine.delete(QUEUE_TABLE, { 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', direction: '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, { 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', direction: 'asc' },\n { field: 'scheduled_for', direction: '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\nimport type { IQueueService, QueuePublishOptions, QueueHandler } from '@objectstack/spec/contracts';\n\n/**\n * Configuration for the BullMQ queue adapter.\n */\nexport interface BullMQQueueAdapterOptions {\n /** Redis connection URL (e.g. 'redis://localhost:6379') */\n redisUrl: string;\n /** Default job options */\n defaultJobOptions?: {\n /** Number of retry attempts */\n attempts?: number;\n /** Backoff strategy */\n backoff?: { type: 'fixed' | 'exponential'; delay: number };\n };\n}\n\n/**\n * BullMQ queue adapter skeleton implementing IQueueService.\n *\n * This is a placeholder for future BullMQ integration.\n * Concrete implementation will use the `bullmq` package.\n *\n * @example\n * ```ts\n * const queue = new BullMQQueueAdapter({ redisUrl: 'redis://localhost:6379' });\n * await queue.publish('orders', { orderId: 123 });\n * ```\n */\nexport class BullMQQueueAdapter implements IQueueService {\n private readonly redisUrl: string;\n\n constructor(options: BullMQQueueAdapterOptions) {\n this.redisUrl = options.redisUrl;\n }\n\n async publish<T = unknown>(_queue: string, _data: T, _options?: QueuePublishOptions): Promise<string> {\n throw new Error(`BullMQQueueAdapter not yet implemented (url: ${this.redisUrl})`);\n }\n\n async subscribe<T = unknown>(_queue: string, _handler: QueueHandler<T>): Promise<void> {\n throw new Error('BullMQQueueAdapter not yet implemented');\n }\n\n async unsubscribe(_queue: string): Promise<void> {\n throw new Error('BullMQQueueAdapter not yet implemented');\n }\n\n async getQueueSize(_queue: string): Promise<number> {\n throw new Error('BullMQQueueAdapter not yet implemented');\n }\n\n async purge(_queue: string): Promise<void> {\n throw new Error('BullMQQueueAdapter not yet implemented');\n }\n}\n"],"mappings":"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","names":[]}
|