hzl-core 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/__tests__/backup/backup-restore.test.js +2 -0
  2. package/dist/__tests__/backup/backup-restore.test.js.map +1 -1
  3. package/dist/__tests__/backup/import-export.test.js +2 -0
  4. package/dist/__tests__/backup/import-export.test.js.map +1 -1
  5. package/dist/__tests__/concurrency/stress.test.js +3 -1
  6. package/dist/__tests__/concurrency/stress.test.js.map +1 -1
  7. package/dist/__tests__/properties/invariants.test.js +126 -0
  8. package/dist/__tests__/properties/invariants.test.js.map +1 -1
  9. package/dist/db/__tests__/datastore.test.js +14 -0
  10. package/dist/db/__tests__/datastore.test.js.map +1 -1
  11. package/dist/db/migrations/index.d.ts.map +1 -1
  12. package/dist/db/migrations/index.js +44 -0
  13. package/dist/db/migrations/index.js.map +1 -1
  14. package/dist/db/schema.d.ts +1 -1
  15. package/dist/db/schema.d.ts.map +1 -1
  16. package/dist/db/schema.js +41 -0
  17. package/dist/db/schema.js.map +1 -1
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.test.js +4 -0
  23. package/dist/index.test.js.map +1 -1
  24. package/dist/projections/rebuild.d.ts.map +1 -1
  25. package/dist/projections/rebuild.js +29 -17
  26. package/dist/projections/rebuild.js.map +1 -1
  27. package/dist/services/backup-service.d.ts.map +1 -1
  28. package/dist/services/backup-service.js +2 -0
  29. package/dist/services/backup-service.js.map +1 -1
  30. package/dist/services/hook-drain-service.d.ts +58 -0
  31. package/dist/services/hook-drain-service.d.ts.map +1 -0
  32. package/dist/services/hook-drain-service.js +388 -0
  33. package/dist/services/hook-drain-service.js.map +1 -0
  34. package/dist/services/hook-drain-service.test.d.ts +2 -0
  35. package/dist/services/hook-drain-service.test.d.ts.map +1 -0
  36. package/dist/services/hook-drain-service.test.js +167 -0
  37. package/dist/services/hook-drain-service.test.js.map +1 -0
  38. package/dist/services/search-service.d.ts +1 -0
  39. package/dist/services/search-service.d.ts.map +1 -1
  40. package/dist/services/search-service.js +12 -14
  41. package/dist/services/search-service.js.map +1 -1
  42. package/dist/services/search-service.test.js +14 -1
  43. package/dist/services/search-service.test.js.map +1 -1
  44. package/dist/services/task-service.d.ts +26 -4
  45. package/dist/services/task-service.d.ts.map +1 -1
  46. package/dist/services/task-service.js +97 -35
  47. package/dist/services/task-service.js.map +1 -1
  48. package/dist/services/task-service.test.js +87 -10
  49. package/dist/services/task-service.test.js.map +1 -1
  50. package/dist/services/workflow-service.d.ts +141 -0
  51. package/dist/services/workflow-service.d.ts.map +1 -0
  52. package/dist/services/workflow-service.js +664 -0
  53. package/dist/services/workflow-service.js.map +1 -0
  54. package/dist/services/workflow-service.test.d.ts +2 -0
  55. package/dist/services/workflow-service.test.d.ts.map +1 -0
  56. package/dist/services/workflow-service.test.js +213 -0
  57. package/dist/services/workflow-service.test.js.map +1 -0
  58. package/package.json +2 -1
@@ -0,0 +1,388 @@
1
+ import crypto from 'node:crypto';
2
+ import { withWriteTransaction } from '../db/transaction.js';
3
+ const HOOK_OUTBOX_TABLE = 'hook_outbox';
4
+ const STATUS_QUEUED = 'queued';
5
+ const STATUS_PROCESSING = 'processing';
6
+ const STATUS_DELIVERED = 'delivered';
7
+ const STATUS_FAILED = 'failed';
8
+ function toIsoString(value) {
9
+ return value.toISOString();
10
+ }
11
+ function parseTimestampMs(value) {
12
+ if (!value)
13
+ return null;
14
+ const parsed = Date.parse(value);
15
+ return Number.isNaN(parsed) ? null : parsed;
16
+ }
17
+ function getChanges(result) {
18
+ if (result && typeof result === 'object' && 'changes' in result) {
19
+ const maybe = result.changes;
20
+ if (typeof maybe === 'number')
21
+ return maybe;
22
+ }
23
+ return 0;
24
+ }
25
+ function stringifyError(error) {
26
+ if (error instanceof Error)
27
+ return error.message;
28
+ return String(error);
29
+ }
30
+ function truncate(text, max = 280) {
31
+ if (text.length <= max)
32
+ return text;
33
+ return `${text.slice(0, max)}...`;
34
+ }
35
+ function parseHeaders(raw) {
36
+ try {
37
+ const parsed = JSON.parse(raw);
38
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
39
+ return {};
40
+ const headers = {};
41
+ for (const [key, value] of Object.entries(parsed)) {
42
+ if (typeof value === 'string')
43
+ headers[key] = value;
44
+ }
45
+ return headers;
46
+ }
47
+ catch {
48
+ return {};
49
+ }
50
+ }
51
+ export class HookDrainService {
52
+ db;
53
+ batchSize;
54
+ maxAttempts;
55
+ ttlMs;
56
+ lockTimeoutMs;
57
+ backoffBaseMs;
58
+ backoffMaxMs;
59
+ jitterRatio;
60
+ requestTimeoutMs;
61
+ workerId;
62
+ now;
63
+ random;
64
+ fetchFn;
65
+ constructor(db, config = {}) {
66
+ this.db = db;
67
+ this.batchSize = config.batchSize ?? 50;
68
+ this.maxAttempts = config.maxAttempts ?? 5;
69
+ this.ttlMs = config.ttlMs ?? 24 * 60 * 60 * 1000;
70
+ this.lockTimeoutMs = config.lockTimeoutMs ?? 5 * 60 * 1000;
71
+ this.backoffBaseMs = config.backoffBaseMs ?? 30_000;
72
+ this.backoffMaxMs = config.backoffMaxMs ?? 6 * 60 * 60 * 1000;
73
+ this.jitterRatio = config.jitterRatio ?? 0.2;
74
+ this.requestTimeoutMs = config.requestTimeoutMs ?? 10_000;
75
+ this.now = config.now ?? (() => new Date());
76
+ this.random = config.random ?? Math.random;
77
+ this.fetchFn = config.fetchFn ?? fetch;
78
+ this.workerId = config.workerId ?? `hook-drain-${process.pid}-${crypto.randomUUID()}`;
79
+ }
80
+ async drain(options = {}) {
81
+ const startedAt = this.now();
82
+ const nowIso = toIsoString(startedAt);
83
+ const limit = Math.max(1, options.limit ?? this.batchSize);
84
+ const claimBatch = this.claimDueRecords(limit, nowIso);
85
+ const result = {
86
+ worker_id: this.workerId,
87
+ claimed: claimBatch.records.length,
88
+ attempted: 0,
89
+ delivered: 0,
90
+ retried: 0,
91
+ failed: claimBatch.preflightFailed + claimBatch.reclaimedFailed,
92
+ reclaimed: claimBatch.reclaimed,
93
+ reclaimed_failed: claimBatch.reclaimedFailed,
94
+ preflight_failed: claimBatch.preflightFailed,
95
+ duration_ms: 0,
96
+ };
97
+ for (const record of claimBatch.records) {
98
+ result.attempted += 1;
99
+ try {
100
+ await this.deliver(record);
101
+ if (this.markDelivered(record.id, record.lock_token) === 1) {
102
+ result.delivered += 1;
103
+ }
104
+ }
105
+ catch (error) {
106
+ const disposition = this.markFailedAttempt(record, error);
107
+ if (disposition === 'failed') {
108
+ result.failed += 1;
109
+ }
110
+ else if (disposition === 'retried') {
111
+ result.retried += 1;
112
+ }
113
+ }
114
+ }
115
+ result.duration_ms = Math.max(0, this.now().getTime() - startedAt.getTime());
116
+ return result;
117
+ }
118
+ claimDueRecords(limit, nowIso) {
119
+ return withWriteTransaction(this.db, () => {
120
+ let preflightFailed = this.failTerminalQueuedRecords(nowIso);
121
+ const reclaimResult = this.reclaimStaleProcessing(nowIso);
122
+ const lockExpiresIso = toIsoString(new Date(this.now().getTime() + this.lockTimeoutMs));
123
+ const dueRows = this.db.prepare(`
124
+ SELECT
125
+ id,
126
+ url,
127
+ headers,
128
+ payload,
129
+ attempts,
130
+ created_at
131
+ FROM ${HOOK_OUTBOX_TABLE}
132
+ WHERE status = ?
133
+ AND next_attempt_at <= ?
134
+ ORDER BY next_attempt_at ASC, id ASC
135
+ LIMIT ?
136
+ `).all(STATUS_QUEUED, nowIso, limit);
137
+ const claimed = [];
138
+ for (const row of dueRows) {
139
+ if (this.isTerminal(row.attempts, row.created_at, nowIso)) {
140
+ preflightFailed += this.failQueuedRecord(row.id, nowIso, 'hook delivery exhausted before claim');
141
+ continue;
142
+ }
143
+ const lockToken = crypto.randomUUID();
144
+ const updateResult = this.db.prepare(`
145
+ UPDATE ${HOOK_OUTBOX_TABLE}
146
+ SET
147
+ status = ?,
148
+ lock_token = ?,
149
+ locked_by = ?,
150
+ processing_started_at = ?,
151
+ lock_expires_at = ?,
152
+ updated_at = ?
153
+ WHERE id = ?
154
+ AND status = ?
155
+ AND next_attempt_at <= ?
156
+ AND attempts < ?
157
+ `).run(STATUS_PROCESSING, lockToken, this.workerId, nowIso, lockExpiresIso, nowIso, row.id, STATUS_QUEUED, nowIso, this.maxAttempts);
158
+ if (getChanges(updateResult) === 1) {
159
+ claimed.push({
160
+ ...row,
161
+ lock_token: lockToken,
162
+ });
163
+ }
164
+ }
165
+ return {
166
+ records: claimed,
167
+ reclaimed: reclaimResult.reclaimed,
168
+ reclaimedFailed: reclaimResult.reclaimedFailed,
169
+ preflightFailed,
170
+ };
171
+ });
172
+ }
173
+ failTerminalQueuedRecords(nowIso) {
174
+ const ttlCutoffIso = toIsoString(new Date(this.now().getTime() - this.ttlMs));
175
+ const updateResult = this.db.prepare(`
176
+ UPDATE ${HOOK_OUTBOX_TABLE}
177
+ SET
178
+ status = ?,
179
+ failed_at = COALESCE(failed_at, ?),
180
+ lock_token = NULL,
181
+ locked_by = NULL,
182
+ lock_expires_at = NULL,
183
+ updated_at = ?,
184
+ last_error = COALESCE(last_error, 'hook delivery exhausted before claim')
185
+ WHERE status = ?
186
+ AND (attempts >= ? OR created_at <= ?)
187
+ `).run(STATUS_FAILED, nowIso, nowIso, STATUS_QUEUED, this.maxAttempts, ttlCutoffIso);
188
+ return getChanges(updateResult);
189
+ }
190
+ failQueuedRecord(id, nowIso, reason) {
191
+ const updateResult = this.db.prepare(`
192
+ UPDATE ${HOOK_OUTBOX_TABLE}
193
+ SET
194
+ status = ?,
195
+ failed_at = COALESCE(failed_at, ?),
196
+ lock_token = NULL,
197
+ locked_by = NULL,
198
+ lock_expires_at = NULL,
199
+ updated_at = ?,
200
+ last_error = ?
201
+ WHERE id = ?
202
+ AND status = ?
203
+ `).run(STATUS_FAILED, nowIso, nowIso, reason, id, STATUS_QUEUED);
204
+ return getChanges(updateResult);
205
+ }
206
+ reclaimStaleProcessing(nowIso) {
207
+ const staleRows = this.db.prepare(`
208
+ SELECT
209
+ id,
210
+ attempts,
211
+ created_at
212
+ FROM ${HOOK_OUTBOX_TABLE}
213
+ WHERE status = ?
214
+ AND lock_expires_at IS NOT NULL
215
+ AND lock_expires_at <= ?
216
+ `).all(STATUS_PROCESSING, nowIso);
217
+ let reclaimed = 0;
218
+ let reclaimedFailed = 0;
219
+ for (const row of staleRows) {
220
+ if (this.isTerminal(row.attempts, row.created_at, nowIso)) {
221
+ const failResult = this.db.prepare(`
222
+ UPDATE ${HOOK_OUTBOX_TABLE}
223
+ SET
224
+ status = ?,
225
+ failed_at = COALESCE(failed_at, ?),
226
+ lock_token = NULL,
227
+ locked_by = NULL,
228
+ lock_expires_at = NULL,
229
+ updated_at = ?,
230
+ last_error = COALESCE(last_error, 'stale processing lock reclaimed after exhaustion')
231
+ WHERE id = ?
232
+ AND status = ?
233
+ `).run(STATUS_FAILED, nowIso, nowIso, row.id, STATUS_PROCESSING);
234
+ if (getChanges(failResult) === 1)
235
+ reclaimedFailed += 1;
236
+ continue;
237
+ }
238
+ const reclaimResult = this.db.prepare(`
239
+ UPDATE ${HOOK_OUTBOX_TABLE}
240
+ SET
241
+ status = ?,
242
+ lock_token = NULL,
243
+ locked_by = NULL,
244
+ lock_expires_at = NULL,
245
+ updated_at = ?
246
+ WHERE id = ?
247
+ AND status = ?
248
+ `).run(STATUS_QUEUED, nowIso, row.id, STATUS_PROCESSING);
249
+ if (getChanges(reclaimResult) === 1)
250
+ reclaimed += 1;
251
+ }
252
+ return { reclaimed, reclaimedFailed };
253
+ }
254
+ async deliver(record) {
255
+ const headers = parseHeaders(record.headers);
256
+ if (!Object.keys(headers).some((key) => key.toLowerCase() === 'content-type')) {
257
+ headers['content-type'] = 'application/json';
258
+ }
259
+ let response;
260
+ try {
261
+ response = await this.fetchFn(record.url, {
262
+ method: 'POST',
263
+ headers,
264
+ body: record.payload,
265
+ signal: AbortSignal.timeout(this.requestTimeoutMs),
266
+ });
267
+ }
268
+ catch (error) {
269
+ throw new Error(`network error: ${stringifyError(error)}`);
270
+ }
271
+ if (!response.ok) {
272
+ let responseText = '';
273
+ try {
274
+ responseText = await response.text();
275
+ }
276
+ catch {
277
+ // Ignore response parsing errors.
278
+ }
279
+ const statusMessage = responseText
280
+ ? `HTTP ${response.status}: ${truncate(responseText)}`
281
+ : `HTTP ${response.status}`;
282
+ throw new Error(statusMessage);
283
+ }
284
+ }
285
+ markDelivered(id, lockToken) {
286
+ const nowIso = toIsoString(this.now());
287
+ const result = withWriteTransaction(this.db, () => this.db.prepare(`
288
+ UPDATE ${HOOK_OUTBOX_TABLE}
289
+ SET
290
+ status = ?,
291
+ delivered_at = ?,
292
+ lock_token = NULL,
293
+ locked_by = NULL,
294
+ lock_expires_at = NULL,
295
+ updated_at = ?,
296
+ last_error = NULL
297
+ WHERE id = ?
298
+ AND status = ?
299
+ AND lock_token = ?
300
+ `).run(STATUS_DELIVERED, nowIso, nowIso, id, STATUS_PROCESSING, lockToken));
301
+ return getChanges(result);
302
+ }
303
+ markFailedAttempt(record, error) {
304
+ const now = this.now();
305
+ const nowIso = toIsoString(now);
306
+ const nextAttempts = record.attempts + 1;
307
+ const errorMessage = truncate(stringifyError(error));
308
+ const expirationMs = this.expiresAtMs(record.created_at);
309
+ if (nextAttempts >= this.maxAttempts ||
310
+ (expirationMs !== null && now.getTime() >= expirationMs)) {
311
+ const failResult = withWriteTransaction(this.db, () => this.db.prepare(`
312
+ UPDATE ${HOOK_OUTBOX_TABLE}
313
+ SET
314
+ status = ?,
315
+ attempts = ?,
316
+ failed_at = COALESCE(failed_at, ?),
317
+ lock_token = NULL,
318
+ locked_by = NULL,
319
+ lock_expires_at = NULL,
320
+ updated_at = ?,
321
+ last_error = ?
322
+ WHERE id = ?
323
+ AND status = ?
324
+ AND lock_token = ?
325
+ `).run(STATUS_FAILED, nextAttempts, nowIso, nowIso, errorMessage, record.id, STATUS_PROCESSING, record.lock_token));
326
+ return getChanges(failResult) === 1 ? 'failed' : 'noop';
327
+ }
328
+ const delayMs = this.computeBackoffDelayMs(nextAttempts);
329
+ const nextAttemptMs = now.getTime() + delayMs;
330
+ if (expirationMs !== null && nextAttemptMs >= expirationMs) {
331
+ const failResult = withWriteTransaction(this.db, () => this.db.prepare(`
332
+ UPDATE ${HOOK_OUTBOX_TABLE}
333
+ SET
334
+ status = ?,
335
+ attempts = ?,
336
+ failed_at = COALESCE(failed_at, ?),
337
+ lock_token = NULL,
338
+ locked_by = NULL,
339
+ lock_expires_at = NULL,
340
+ updated_at = ?,
341
+ last_error = ?
342
+ WHERE id = ?
343
+ AND status = ?
344
+ AND lock_token = ?
345
+ `).run(STATUS_FAILED, nextAttempts, nowIso, nowIso, errorMessage, record.id, STATUS_PROCESSING, record.lock_token));
346
+ return getChanges(failResult) === 1 ? 'failed' : 'noop';
347
+ }
348
+ const retryResult = withWriteTransaction(this.db, () => this.db.prepare(`
349
+ UPDATE ${HOOK_OUTBOX_TABLE}
350
+ SET
351
+ status = ?,
352
+ attempts = ?,
353
+ next_attempt_at = ?,
354
+ lock_token = NULL,
355
+ locked_by = NULL,
356
+ lock_expires_at = NULL,
357
+ updated_at = ?,
358
+ last_error = ?
359
+ WHERE id = ?
360
+ AND status = ?
361
+ AND lock_token = ?
362
+ `).run(STATUS_QUEUED, nextAttempts, toIsoString(new Date(nextAttemptMs)), nowIso, errorMessage, record.id, STATUS_PROCESSING, record.lock_token));
363
+ return getChanges(retryResult) === 1 ? 'retried' : 'noop';
364
+ }
365
+ isTerminal(attempts, createdAt, nowIso) {
366
+ if (attempts >= this.maxAttempts)
367
+ return true;
368
+ const nowMs = parseTimestampMs(nowIso);
369
+ const createdMs = parseTimestampMs(createdAt);
370
+ if (nowMs === null || createdMs === null)
371
+ return false;
372
+ return nowMs >= createdMs + this.ttlMs;
373
+ }
374
+ expiresAtMs(createdAt) {
375
+ const createdMs = parseTimestampMs(createdAt);
376
+ if (createdMs === null)
377
+ return null;
378
+ return createdMs + this.ttlMs;
379
+ }
380
+ computeBackoffDelayMs(nextAttemptCount) {
381
+ const exponent = Math.max(0, nextAttemptCount - 1);
382
+ const uncapped = this.backoffBaseMs * Math.pow(2, exponent);
383
+ const base = Math.min(this.backoffMaxMs, uncapped);
384
+ const jitter = 1 + ((this.random() * 2) - 1) * this.jitterRatio;
385
+ return Math.max(0, Math.round(base * jitter));
386
+ }
387
+ }
388
+ //# sourceMappingURL=hook-drain-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-drain-service.js","sourceRoot":"","sources":["../../src/services/hook-drain-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=hook-drain-service.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-drain-service.test.d.ts","sourceRoot":"","sources":["../../src/services/hook-drain-service.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,167 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { createTestDb } from '../db/test-utils.js';
3
+ import { HookDrainService } from './hook-drain-service.js';
4
+ function insertOutboxRow(db, row) {
5
+ const result = db.prepare(`
6
+ INSERT INTO hook_outbox (
7
+ hook_name,
8
+ status,
9
+ url,
10
+ headers,
11
+ payload,
12
+ attempts,
13
+ next_attempt_at,
14
+ created_at,
15
+ lock_token,
16
+ locked_by,
17
+ lock_expires_at
18
+ )
19
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
20
+ `).run(row.hook_name ?? 'on_done', row.status ?? 'queued', row.url ?? 'https://example.com/hooks/done', row.headers ?? '{"Authorization":"Bearer test"}', row.payload ?? '{"task_id":"TASK-1","status":"done"}', row.attempts ?? 0, row.next_attempt_at, row.created_at ?? '2026-02-27T11:00:00.000Z', row.lock_token ?? null, row.locked_by ?? null, row.lock_expires_at ?? null);
21
+ return result.lastInsertRowid;
22
+ }
23
+ describe('HookDrainService', () => {
24
+ let db;
25
+ let now;
26
+ let fetchMock;
27
+ beforeEach(() => {
28
+ db = createTestDb();
29
+ now = new Date('2026-02-27T12:00:00.000Z');
30
+ fetchMock = vi.fn();
31
+ });
32
+ afterEach(() => {
33
+ db.close();
34
+ });
35
+ it('drains due queued hooks and marks deliveries as delivered', async () => {
36
+ insertOutboxRow(db, {
37
+ next_attempt_at: '2026-02-27T11:59:00.000Z',
38
+ created_at: '2026-02-27T11:00:00.000Z',
39
+ });
40
+ fetchMock.mockResolvedValue(new Response('ok', { status: 200 }));
41
+ const service = new HookDrainService(db, {
42
+ now: () => now,
43
+ random: () => 0.5,
44
+ fetchFn: fetchMock,
45
+ workerId: 'worker-success',
46
+ });
47
+ const result = await service.drain();
48
+ expect(result.claimed).toBe(1);
49
+ expect(result.delivered).toBe(1);
50
+ expect(result.retried).toBe(0);
51
+ expect(result.failed).toBe(0);
52
+ const row = db.prepare(`
53
+ SELECT status, delivered_at, attempts, lock_token, lock_expires_at
54
+ FROM hook_outbox
55
+ ORDER BY id ASC
56
+ LIMIT 1
57
+ `).get();
58
+ expect(row.status).toBe('delivered');
59
+ expect(row.delivered_at).toBe('2026-02-27T12:00:00.000Z');
60
+ expect(row.attempts).toBe(0);
61
+ expect(row.lock_token).toBeNull();
62
+ expect(row.lock_expires_at).toBeNull();
63
+ expect(fetchMock).toHaveBeenCalledTimes(1);
64
+ expect(fetchMock).toHaveBeenCalledWith('https://example.com/hooks/done', expect.objectContaining({
65
+ method: 'POST',
66
+ body: '{"task_id":"TASK-1","status":"done"}',
67
+ }));
68
+ });
69
+ it('schedules retries with exponential backoff when delivery fails', async () => {
70
+ insertOutboxRow(db, {
71
+ next_attempt_at: '2026-02-27T11:59:00.000Z',
72
+ created_at: '2026-02-27T11:00:00.000Z',
73
+ });
74
+ fetchMock.mockRejectedValue(new Error('connection refused'));
75
+ const service = new HookDrainService(db, {
76
+ now: () => now,
77
+ random: () => 0.5,
78
+ fetchFn: fetchMock,
79
+ workerId: 'worker-retry',
80
+ backoffBaseMs: 1_000,
81
+ backoffMaxMs: 60_000,
82
+ jitterRatio: 0.2,
83
+ ttlMs: 24 * 60 * 60 * 1000,
84
+ });
85
+ const result = await service.drain();
86
+ expect(result.claimed).toBe(1);
87
+ expect(result.delivered).toBe(0);
88
+ expect(result.retried).toBe(1);
89
+ expect(result.failed).toBe(0);
90
+ const row = db.prepare(`
91
+ SELECT status, attempts, next_attempt_at, last_error
92
+ FROM hook_outbox
93
+ ORDER BY id ASC
94
+ LIMIT 1
95
+ `).get();
96
+ expect(row.status).toBe('queued');
97
+ expect(row.attempts).toBe(1);
98
+ expect(row.next_attempt_at).toBe('2026-02-27T12:00:01.000Z');
99
+ expect(row.last_error).toContain('network error');
100
+ });
101
+ it('marks records as failed once max attempts are exhausted', async () => {
102
+ insertOutboxRow(db, {
103
+ attempts: 4,
104
+ next_attempt_at: '2026-02-27T11:59:00.000Z',
105
+ created_at: '2026-02-27T11:00:00.000Z',
106
+ });
107
+ fetchMock.mockResolvedValue(new Response('upstream failed', { status: 500 }));
108
+ const service = new HookDrainService(db, {
109
+ now: () => now,
110
+ random: () => 0.5,
111
+ fetchFn: fetchMock,
112
+ workerId: 'worker-fail',
113
+ maxAttempts: 5,
114
+ backoffBaseMs: 1_000,
115
+ backoffMaxMs: 60_000,
116
+ jitterRatio: 0.2,
117
+ ttlMs: 24 * 60 * 60 * 1000,
118
+ });
119
+ const result = await service.drain();
120
+ expect(result.claimed).toBe(1);
121
+ expect(result.retried).toBe(0);
122
+ expect(result.failed).toBe(1);
123
+ const row = db.prepare(`
124
+ SELECT status, attempts, failed_at, last_error
125
+ FROM hook_outbox
126
+ ORDER BY id ASC
127
+ LIMIT 1
128
+ `).get();
129
+ expect(row.status).toBe('failed');
130
+ expect(row.attempts).toBe(5);
131
+ expect(row.failed_at).toBe('2026-02-27T12:00:00.000Z');
132
+ expect(row.last_error).toContain('HTTP 500');
133
+ });
134
+ it('reclaims stale processing locks and redrives delivery', async () => {
135
+ insertOutboxRow(db, {
136
+ status: 'processing',
137
+ lock_token: 'stale-token',
138
+ locked_by: 'stale-worker',
139
+ lock_expires_at: '2026-02-27T11:59:00.000Z',
140
+ next_attempt_at: '2026-02-27T11:35:00.000Z',
141
+ created_at: '2026-02-27T11:00:00.000Z',
142
+ });
143
+ fetchMock.mockResolvedValue(new Response('ok', { status: 200 }));
144
+ const service = new HookDrainService(db, {
145
+ now: () => now,
146
+ random: () => 0.5,
147
+ fetchFn: fetchMock,
148
+ workerId: 'worker-reclaim',
149
+ lockTimeoutMs: 60_000,
150
+ });
151
+ const result = await service.drain();
152
+ expect(result.reclaimed).toBe(1);
153
+ expect(result.claimed).toBe(1);
154
+ expect(result.delivered).toBe(1);
155
+ const row = db.prepare(`
156
+ SELECT status, lock_token, lock_expires_at, delivered_at
157
+ FROM hook_outbox
158
+ ORDER BY id ASC
159
+ LIMIT 1
160
+ `).get();
161
+ expect(row.status).toBe('delivered');
162
+ expect(row.lock_token).toBeNull();
163
+ expect(row.lock_expires_at).toBeNull();
164
+ expect(row.delivered_at).toBe('2026-02-27T12:00:00.000Z');
165
+ });
166
+ });
167
+ //# sourceMappingURL=hook-drain-service.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-drain-service.test.js","sourceRoot":"","sources":["../../src/services/hook-drain-service.test.ts"],"names":[],"mappings":"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"}
@@ -16,6 +16,7 @@ export interface SearchResult {
16
16
  }
17
17
  export interface SearchOptions {
18
18
  project?: string;
19
+ status?: string;
19
20
  limit?: number;
20
21
  offset?: number;
21
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"search-service.d.ts","sourceRoot":"","sources":["../../src/services/search-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,QAAQ,MAAM,QAAQ,CAAC;AAEnC,MAAM,WAAW,gBAAgB;IAAG,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;CAAE;AAClK,MAAM,WAAW,YAAY;IAAG,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;CAAE;AAC1G,MAAM,WAAW,aAAa;IAAG,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAAE;AAErF,qBAAa,aAAa;IACZ,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAEzC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,YAAY;CA+B1D"}
1
+ {"version":3,"file":"search-service.d.ts","sourceRoot":"","sources":["../../src/services/search-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,QAAQ,MAAM,QAAQ,CAAC;AAEnC,MAAM,WAAW,gBAAgB;IAAG,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;CAAE;AAClK,MAAM,WAAW,YAAY;IAAG,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;CAAE;AAC1G,MAAM,WAAW,aAAa;IAAG,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAAE;AAEtG,qBAAa,aAAa;IACZ,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAEzC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,YAAY;CA+B1D"}
@@ -12,23 +12,21 @@ export class SearchService {
12
12
  const safeQuery = trimmedQuery.split(/\s+/).filter(w => w.length > 0).map(w => w.replace(/[^a-zA-Z0-9]/g, '')).filter(w => w.length > 0).join(' ');
13
13
  if (!safeQuery)
14
14
  return { tasks: [], total: 0, limit, offset };
15
- let countQuery, searchQuery;
16
- const params = [];
15
+ const where = ['task_search MATCH ?'];
16
+ const whereParams = [safeQuery];
17
17
  if (opts?.project) {
18
- countQuery = `SELECT COUNT(*) as total FROM task_search s JOIN tasks_current t ON s.task_id = t.task_id WHERE task_search MATCH ? AND t.project = ?`;
19
- searchQuery = `SELECT t.task_id, t.title, t.project, t.status, t.description, t.priority, rank FROM task_search s JOIN tasks_current t ON s.task_id = t.task_id WHERE task_search MATCH ? AND t.project = ? ORDER BY rank LIMIT ? OFFSET ?`;
20
- params.push(safeQuery, opts.project, limit, offset);
18
+ where.push('t.project = ?');
19
+ whereParams.push(opts.project);
21
20
  }
22
- else {
23
- countQuery = `SELECT COUNT(*) as total FROM task_search s JOIN tasks_current t ON s.task_id = t.task_id WHERE task_search MATCH ?`;
24
- searchQuery = `SELECT t.task_id, t.title, t.project, t.status, t.description, t.priority, rank FROM task_search s JOIN tasks_current t ON s.task_id = t.task_id WHERE task_search MATCH ? ORDER BY rank LIMIT ? OFFSET ?`;
25
- params.push(safeQuery, limit, offset);
21
+ if (opts?.status) {
22
+ where.push('t.status = ?');
23
+ whereParams.push(opts.status);
26
24
  }
27
- const countParams = opts?.project
28
- ? [safeQuery, opts.project]
29
- : [safeQuery];
30
- const total = this.db.prepare(countQuery).get(...countParams).total;
31
- const rows = this.db.prepare(searchQuery).all(...params);
25
+ const whereClause = where.join(' AND ');
26
+ const countQuery = `SELECT COUNT(*) as total FROM task_search s JOIN tasks_current t ON s.task_id = t.task_id WHERE ${whereClause}`;
27
+ const searchQuery = `SELECT t.task_id, t.title, t.project, t.status, t.description, t.priority, rank FROM task_search s JOIN tasks_current t ON s.task_id = t.task_id WHERE ${whereClause} ORDER BY rank LIMIT ? OFFSET ?`;
28
+ const total = this.db.prepare(countQuery).get(...whereParams).total;
29
+ const rows = this.db.prepare(searchQuery).all(...whereParams, limit, offset);
32
30
  return { tasks: rows, total, limit, offset };
33
31
  }
34
32
  }
@@ -1 +1 @@
1
- {"version":3,"file":"search-service.js","sourceRoot":"","sources":["../../src/services/search-service.ts"],"names":[],"mappings":"AAOA,MAAM,OAAO,aAAa;IACJ;IAApB,YAAoB,EAAqB;QAArB,OAAE,GAAF,EAAE,CAAmB;IAAG,CAAC;IAE7C,MAAM,CAAC,KAAa,EAAE,IAAoB;QACxC,MAAM,KAAK,GAAG,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;QAChC,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,IAAI,CAAC,CAAC;QACjC,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAElC,IAAI,CAAC,YAAY;YAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAEjE,MAAM,SAAS,GAAG,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACnJ,IAAI,CAAC,SAAS;YAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAE9D,IAAI,UAAkB,EAAE,WAAmB,CAAC;QAC5C,MAAM,MAAM,GAA2B,EAAE,CAAC;QAE1C,IAAI,IAAI,EAAE,OAAO,EAAE,CAAC;YAClB,UAAU,GAAG,uIAAuI,CAAC;YACrJ,WAAW,GAAG,6NAA6N,CAAC;YAC5O,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QACtD,CAAC;aAAM,CAAC;YACN,UAAU,GAAG,qHAAqH,CAAC;YACnI,WAAW,GAAG,2MAA2M,CAAC;YAC1N,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QACxC,CAAC;QAED,MAAM,WAAW,GAA2B,IAAI,EAAE,OAAO;YACvD,CAAC,CAAC,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC;YAC3B,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAChB,MAAM,KAAK,GAAI,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,GAAG,WAAW,CAAuB,CAAC,KAAK,CAAC;QAC3F,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAuB,CAAC;QAE/E,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAC/C,CAAC;CACF"}
1
+ {"version":3,"file":"search-service.js","sourceRoot":"","sources":["../../src/services/search-service.ts"],"names":[],"mappings":"AAOA,MAAM,OAAO,aAAa;IACJ;IAApB,YAAoB,EAAqB;QAArB,OAAE,GAAF,EAAE,CAAmB;IAAG,CAAC;IAE7C,MAAM,CAAC,KAAa,EAAE,IAAoB;QACxC,MAAM,KAAK,GAAG,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;QAChC,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,IAAI,CAAC,CAAC;QACjC,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAElC,IAAI,CAAC,YAAY;YAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAEjE,MAAM,SAAS,GAAG,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACnJ,IAAI,CAAC,SAAS;YAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAE9D,MAAM,KAAK,GAAa,CAAC,qBAAqB,CAAC,CAAC;QAChD,MAAM,WAAW,GAA2B,CAAC,SAAS,CAAC,CAAC;QAExD,IAAI,IAAI,EAAE,OAAO,EAAE,CAAC;YAClB,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YAC5B,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACjC,CAAC;QAED,IAAI,IAAI,EAAE,MAAM,EAAE,CAAC;YACjB,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YAC3B,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAChC,CAAC;QAED,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxC,MAAM,UAAU,GAAG,mGAAmG,WAAW,EAAE,CAAC;QACpI,MAAM,WAAW,GAAG,0JAA0J,WAAW,iCAAiC,CAAC;QAC3N,MAAM,KAAK,GAAI,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,GAAG,WAAW,CAAuB,CAAC,KAAK,CAAC;QAC3F,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,GAAG,WAAW,EAAE,KAAK,EAAE,MAAM,CAAuB,CAAC;QAEnG,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAC/C,CAAC;CACF"}