@tstdl/base 0.93.141 → 0.93.142

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 (71) hide show
  1. package/authentication/client/authentication.service.d.ts +1 -0
  2. package/authentication/client/authentication.service.js +3 -2
  3. package/circuit-breaker/circuit-breaker.d.ts +6 -4
  4. package/circuit-breaker/postgres/circuit-breaker.d.ts +1 -0
  5. package/circuit-breaker/postgres/circuit-breaker.js +8 -5
  6. package/circuit-breaker/tests/circuit-breaker.test.js +20 -0
  7. package/examples/document-management/main.js +2 -2
  8. package/notification/tests/notification-api.test.js +5 -1
  9. package/notification/tests/notification-flow.test.js +9 -6
  10. package/orm/decorators.d.ts +17 -4
  11. package/orm/decorators.js +9 -0
  12. package/orm/server/bootstrap.d.ts +11 -0
  13. package/orm/server/bootstrap.js +31 -0
  14. package/orm/server/drizzle/schema-converter.d.ts +3 -1
  15. package/orm/server/drizzle/schema-converter.js +71 -29
  16. package/orm/server/extension.d.ts +14 -0
  17. package/orm/server/extension.js +27 -0
  18. package/orm/server/index.d.ts +2 -0
  19. package/orm/server/index.js +2 -0
  20. package/orm/server/migration.d.ts +2 -3
  21. package/orm/server/migration.js +7 -21
  22. package/orm/server/repository.d.ts +1 -0
  23. package/orm/server/repository.js +19 -9
  24. package/orm/server/transaction.d.ts +1 -0
  25. package/orm/server/transaction.js +3 -0
  26. package/orm/tests/database-extension.test.js +63 -0
  27. package/orm/tests/database-migration.test.js +7 -6
  28. package/orm/tests/repository-compound-primary-key.test.d.ts +2 -0
  29. package/orm/tests/repository-compound-primary-key.test.js +234 -0
  30. package/orm/tests/schema-generation.test.d.ts +1 -0
  31. package/orm/tests/schema-generation.test.js +52 -5
  32. package/package.json +4 -4
  33. package/task-queue/README.md +0 -1
  34. package/task-queue/postgres/drizzle/0000_great_gwen_stacy.sql +84 -0
  35. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +151 -68
  36. package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
  37. package/task-queue/postgres/module.js +2 -1
  38. package/task-queue/postgres/schemas.d.ts +6 -0
  39. package/task-queue/postgres/task-queue.d.ts +18 -5
  40. package/task-queue/postgres/task-queue.js +593 -372
  41. package/task-queue/postgres/task.model.d.ts +9 -5
  42. package/task-queue/postgres/task.model.js +26 -26
  43. package/task-queue/task-context.d.ts +10 -5
  44. package/task-queue/task-context.js +5 -3
  45. package/task-queue/task-queue.d.ts +339 -35
  46. package/task-queue/task-queue.js +135 -31
  47. package/task-queue/tests/coverage-branch.test.js +45 -57
  48. package/task-queue/tests/coverage-enhancement.test.js +123 -117
  49. package/task-queue/tests/{extensive-dependencies.test.js → dag.test.js} +61 -32
  50. package/task-queue/tests/dependencies.test.js +139 -21
  51. package/task-queue/tests/enqueue-batch.test.js +125 -0
  52. package/task-queue/tests/fan-out-spawning.test.js +43 -2
  53. package/task-queue/tests/idempotent-replacement.test.js +54 -1
  54. package/task-queue/tests/missing-idempotent-tasks.test.js +9 -8
  55. package/task-queue/tests/queue.test.js +261 -25
  56. package/task-queue/tests/shutdown.test.js +41 -0
  57. package/task-queue/tests/transactions.test.d.ts +1 -0
  58. package/task-queue/tests/transactions.test.js +47 -0
  59. package/task-queue/tests/worker.test.js +46 -13
  60. package/task-queue/tests/zombie-parent.test.js +1 -1
  61. package/task-queue/tests/zombie-recovery.test.js +3 -3
  62. package/testing/integration-setup.js +5 -3
  63. package/utils/timing.d.ts +2 -2
  64. package/task-queue/postgres/drizzle/0000_wakeful_sunspot.sql +0 -82
  65. package/task-queue/tests/cascading-cancellations.test.js +0 -38
  66. package/task-queue/tests/complex.test.js +0 -122
  67. package/task-queue/tests/dag-dependencies.test.js +0 -41
  68. /package/{task-queue/tests/cascading-cancellations.test.d.ts → orm/tests/database-extension.test.d.ts} +0 -0
  69. /package/task-queue/tests/{complex.test.d.ts → dag.test.d.ts} +0 -0
  70. /package/task-queue/tests/{dag-dependencies.test.d.ts → enqueue-batch.test.d.ts} +0 -0
  71. /package/task-queue/tests/{extensive-dependencies.test.d.ts → shutdown.test.d.ts} +0 -0
@@ -1,3 +1,4 @@
1
+ /** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
1
2
  var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
3
  if (value !== null && value !== void 0) {
3
4
  if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
@@ -55,30 +56,53 @@ import { inject, injectArgument, Injector } from '../injector/index.js';
55
56
  import { Logger } from '../logger/logger.js';
56
57
  import { Transactional } from '../orm/server/transactional.js';
57
58
  import { currentTimestamp } from '../utils/date-time.js';
58
- import { cancelableTimeout } from '../utils/timing.js';
59
- import { isDefined, isString, isUndefined } from '../utils/type-guards.js';
59
+ import { isDefined, isError, isString } from '../utils/type-guards.js';
60
60
  import { millisecondsPerDay, millisecondsPerMinute, millisecondsPerSecond } from '../utils/units.js';
61
61
  import { TaskQueueEnqueueBatch } from './enqueue-batch.js';
62
62
  import { TaskContext } from './task-context.js';
63
+ /**
64
+ * Represents the result of processing a task.
65
+ * @template Result The type of the result data.
66
+ */
63
67
  export class TaskProcessResult {
64
68
  payload;
65
69
  constructor(payload) {
66
70
  this.payload = payload;
67
71
  }
72
+ /**
73
+ * Creates a successful process result.
74
+ * @param result The optional result data.
75
+ */
68
76
  static Complete(result) {
69
77
  return new TaskProcessResult({ action: 'complete', result });
70
78
  }
79
+ /**
80
+ * Creates a failed process result.
81
+ * @param error The error that occurred.
82
+ * @param fatal Whether the error is fatal and the task should not be retried.
83
+ */
71
84
  static Fail(error, fatal = false) {
72
85
  return new TaskProcessResult({ action: 'fail', error, fatal });
73
86
  }
87
+ /**
88
+ * Creates a result that reschedules the task to a specific timestamp.
89
+ * @param timestamp The timestamp to reschedule to.
90
+ */
74
91
  static RescheduleTo(timestamp) {
75
92
  return new TaskProcessResult({ action: 'reschedule', timestamp });
76
93
  }
94
+ /**
95
+ * Creates a result that reschedules the task by a specific number of milliseconds.
96
+ * @param milliseconds The number of milliseconds to reschedule by.
97
+ */
77
98
  static RescheduleBy(milliseconds) {
78
99
  const timestamp = currentTimestamp() + milliseconds;
79
100
  return this.RescheduleTo(timestamp);
80
101
  }
81
102
  }
103
+ /**
104
+ * Represents the status of a task in the queue.
105
+ */
82
106
  export const TaskStatus = defineEnum('TaskStatus', {
83
107
  /**
84
108
  * The task is ready to be processed and is waiting for a worker.
@@ -112,18 +136,51 @@ export const TaskStatus = defineEnum('TaskStatus', {
112
136
  * The task has been manually paused and will not be dequeued until resumed.
113
137
  */
114
138
  Paused: 'paused',
139
+ /**
140
+ * The task has failed but has remaining attempts and is waiting for its next scheduled attempt.
141
+ */
142
+ Retrying: 'retrying',
143
+ /**
144
+ * The task was forcefully terminated because its execution time exceeded maxExecutionTime.
145
+ */
146
+ TimedOut: 'timed-out',
147
+ /**
148
+ * The task expired in the queue before it could be picked up by a worker.
149
+ */
150
+ Expired: 'expired',
151
+ /**
152
+ * The task was never attempted because one of its prerequisites failed or was cancelled.
153
+ */
154
+ Skipped: 'skipped',
155
+ /**
156
+ * The task was abandoned by a worker and has exhausted all retry attempts.
157
+ */
158
+ Orphaned: 'orphaned',
115
159
  });
160
+ /**
161
+ * Represents the type of dependency between tasks.
162
+ */
116
163
  export const TaskDependencyType = defineEnum('TaskDependencyType', {
164
+ /**
165
+ * The task should only be scheduled after the dependency has reached a specific status.
166
+ */
117
167
  Schedule: 'schedule',
168
+ /**
169
+ * The task is only considered complete after the dependency has reached a specific status.
170
+ */
118
171
  Complete: 'complete',
172
+ /**
173
+ * The task is only considered complete after the child task has reached a specific status.
174
+ */
175
+ Child: 'child',
119
176
  });
177
+ /** Default priority for tasks. */
120
178
  export const defaultTaskPriority = 1000;
121
179
  export const defaultQueueConfig = {
122
180
  visibilityTimeout: millisecondsPerMinute * 5,
123
181
  maxExecutionTime: millisecondsPerMinute * 60,
124
182
  maxTries: 3,
125
183
  retention: 30 * millisecondsPerDay,
126
- globalConcurrency: null,
127
184
  circuitBreakerThreshold: 5,
128
185
  circuitBreakerResetTimeout: 30 * millisecondsPerSecond,
129
186
  retryDelayMinimum: 5 * millisecondsPerSecond,
@@ -137,15 +194,30 @@ export const defaultQueueConfig = {
137
194
  rateInterval: 1000,
138
195
  idempotencyWindow: millisecondsPerMinute * 60,
139
196
  };
197
+ /**
198
+ * Abstract base class for task queues.
199
+ * @template Definitions The type map of task definitions.
200
+ */
140
201
  export class TaskQueue extends Transactional {
202
+ #activeTasks = new Map();
203
+ #heartbeatLoopRunning = false;
141
204
  injector = inject(Injector);
142
205
  config = this.transactionalContextData ?? (() => { const arg = injectArgument(this); return isString(arg) ? { namespace: arg } : arg; })();
143
206
  logger = inject(Logger, TaskQueue.name).with({ namespace: this.config.namespace });
207
+ get namespace() {
208
+ return this.config.namespace;
209
+ }
210
+ /**
211
+ * Starts a new batch operation for enqueuing multiple tasks.
212
+ * @returns A TaskQueueEnqueueBatch instance.
213
+ */
144
214
  batch() {
145
215
  return new TaskQueueEnqueueBatch(this);
146
216
  }
147
217
  /**
148
- * Starts processing tasks with the provided worker function in the background until the cancellation signal is triggered.
218
+ * Starts processing tasks with the provided worker function.
219
+ * @param options Concurrency, cancellation signal, task types, and forceDequeue flag.
220
+ * @param handler The worker function to process tasks.
149
221
  */
150
222
  process({ concurrency = 1, cancellationSignal, types, forceDequeue }, handler) {
151
223
  const promises = [];
@@ -159,7 +231,10 @@ export class TaskQueue extends Transactional {
159
231
  return this.config;
160
232
  }
161
233
  /**
162
- * Starts processing tasks with the provided worker function in the foreground until the cancellation signal is triggered.
234
+ * Internal method to process tasks using a worker function.
235
+ * @param cancellationSignal A signal to stop processing tasks.
236
+ * @param handler The worker function.
237
+ * @param options Dequeue options.
163
238
  */
164
239
  async processWorker(cancellationSignal, handler, options) {
165
240
  for await (const task of this.getConsumer(cancellationSignal, options)) {
@@ -167,54 +242,50 @@ export class TaskQueue extends Transactional {
167
242
  try {
168
243
  const taskToken = __addDisposableResource(env_1, cancellationSignal.fork(), false);
169
244
  const context = new TaskContext(this, task, taskToken, this.logger.with({ type: task.type }));
170
- let isTaskActive = true;
171
- context.logger.verbose(`Processing task`);
172
- void (async () => {
173
- while (taskToken.isUnset) {
174
- await cancelableTimeout(Math.min(this.visibilityTimeout / 2, 5000), taskToken);
175
- if (taskToken.isSet) {
176
- break;
177
- }
178
- try {
179
- const touchedTask = await this.touch(task);
180
- if (isUndefined(touchedTask) && taskToken.isUnset) {
181
- context.logger.warn(`Task lost lease. Aborting.`);
182
- isTaskActive = false;
183
- taskToken.set();
184
- }
185
- }
186
- catch (error) {
187
- context.logger.error('Error touching task', error);
188
- }
189
- }
190
- })();
245
+ const isTaskActiveObj = { value: true };
246
+ this.#activeTasks.set(task.id, {
247
+ task: task,
248
+ taskToken,
249
+ logger: context.logger,
250
+ isTaskActive: isTaskActiveObj,
251
+ });
252
+ if (!this.#heartbeatLoopRunning) {
253
+ void this.#runHeartbeatLoop();
254
+ }
255
+ context.logger.verbose('Processing task');
191
256
  try {
192
257
  if (taskToken.isSet) {
193
258
  throw new Error('Task cancelled before start');
194
259
  }
195
260
  const result = await handler(context);
196
- if (isDefined(result) && isTaskActive) {
261
+ if (isDefined(result) && isTaskActiveObj.value) {
197
262
  switch (result.payload.action) {
198
263
  case 'complete':
199
- context.logger.verbose(`Completing task`);
264
+ context.logger.verbose('Completing task');
200
265
  await this.complete(task, { result: result.payload.result });
201
266
  break;
202
267
  case 'fail':
203
- context.logger.verbose(`Failing task`);
268
+ context.logger.verbose('Failing task');
204
269
  await this.fail(task, result.payload.error, { fatal: result.payload.fatal });
205
270
  break;
206
271
  case 'reschedule':
207
- context.logger.verbose(`Rescheduling task`);
272
+ context.logger.verbose('Rescheduling task');
208
273
  await this.reschedule(task.id, result.payload.timestamp);
209
274
  break;
210
275
  default:
211
- throw new Error(`Unsupported task result action.`);
276
+ throw new Error('Unsupported task result action.');
212
277
  }
213
278
  }
214
279
  }
215
280
  catch (error) {
216
281
  context.logger.error('Error processing task', error);
217
282
  await this.fail(task, error);
283
+ if (isError(error) && ((error.message === 'Task cancelled before start') || (error.message == 'Unsupported task result action.'))) {
284
+ throw error;
285
+ }
286
+ }
287
+ finally {
288
+ this.#activeTasks.delete(task.id);
218
289
  }
219
290
  }
220
291
  catch (e_1) {
@@ -226,4 +297,37 @@ export class TaskQueue extends Transactional {
226
297
  }
227
298
  }
228
299
  }
300
+ async #runHeartbeatLoop() {
301
+ if (this.#heartbeatLoopRunning) {
302
+ return;
303
+ }
304
+ this.#heartbeatLoopRunning = true;
305
+ try {
306
+ while (this.#activeTasks.size > 0) {
307
+ await new Promise((resolve) => setTimeout(resolve, Math.min(this.visibilityTimeout / 2, 5000)));
308
+ if (this.#activeTasks.size === 0) {
309
+ break;
310
+ }
311
+ const entries = Array.from(this.#activeTasks.values());
312
+ const tasks = entries.map((e) => e.task);
313
+ try {
314
+ const touchedIds = await this.touchMany(tasks);
315
+ const touchedSet = new Set(touchedIds);
316
+ for (const entry of entries) {
317
+ if (entry.taskToken.isUnset && !touchedSet.has(entry.task.id)) {
318
+ entry.logger.warn(`Task lost lease. Aborting.`);
319
+ entry.isTaskActive.value = false;
320
+ entry.taskToken.set();
321
+ }
322
+ }
323
+ }
324
+ catch (error) {
325
+ this.logger.error('Error touching tasks', error);
326
+ }
327
+ }
328
+ }
329
+ finally {
330
+ this.#heartbeatLoopRunning = false;
331
+ }
332
+ }
229
333
  }
@@ -17,7 +17,7 @@ describe('Task Queue Branch Coverage Enhancement', () => {
17
17
  });
18
18
  beforeEach(() => {
19
19
  const queueProvider = injector.resolve(TaskQueueProvider);
20
- const queueName = `branch-coverage-queue-${Date.now()}-${Math.random()}`;
20
+ const queueName = `branch-coverage-queue-${crypto.randomUUID()}`;
21
21
  queue = queueProvider.get(queueName, {
22
22
  visibilityTimeout: 200,
23
23
  });
@@ -33,9 +33,7 @@ describe('Task Queue Branch Coverage Enhancement', () => {
33
33
  it('should handle TaskProcessResult.RescheduleTo in worker', async () => {
34
34
  const task = await queue.enqueue('reschedule-test', {});
35
35
  const future = currentTimestamp() + 5000;
36
- queue.process({ cancellationSignal: token }, async () => {
37
- return TaskProcessResult.RescheduleTo(future);
38
- });
36
+ queue.process({ cancellationSignal: token }, () => TaskProcessResult.RescheduleTo(future));
39
37
  await timeout(500);
40
38
  token.set();
41
39
  const updated = await queue.getTask(task.id);
@@ -44,9 +42,7 @@ describe('Task Queue Branch Coverage Enhancement', () => {
44
42
  });
45
43
  it('should handle TaskProcessResult.Fail(fatal: true) in worker', async () => {
46
44
  const task = await queue.enqueue('fail-fatal-test', {});
47
- queue.process({ cancellationSignal: token }, async () => {
48
- return TaskProcessResult.Fail(new Error('fatal error'), true);
49
- });
45
+ queue.process({ cancellationSignal: token }, () => TaskProcessResult.Fail(new Error('fatal error'), true));
50
46
  await timeout(500);
51
47
  token.set();
52
48
  const updated = await queue.getTask(task.id);
@@ -54,15 +50,15 @@ describe('Task Queue Branch Coverage Enhancement', () => {
54
50
  });
55
51
  it('should handle errors in touch during background lease extension', async () => {
56
52
  const task = await queue.enqueue('touch-error-test', {});
57
- // Mock touch to throw once
58
- const originalTouch = queue.touch.bind(queue);
53
+ // Mock touchMany to throw once
54
+ const originalTouchMany = queue.touchMany.bind(queue);
59
55
  let thrown = false;
60
- vi.spyOn(queue, 'touch').mockImplementation(async (t, o) => {
56
+ vi.spyOn(queue, 'touchMany').mockImplementation(async (t, o) => {
61
57
  if (!thrown) {
62
58
  thrown = true;
63
59
  throw new Error('touch error');
64
60
  }
65
- return originalTouch(t, o);
61
+ return originalTouchMany(t, o);
66
62
  });
67
63
  queue.process({ cancellationSignal: token }, async () => {
68
64
  await timeout(300); // Wait for background touch to trigger
@@ -78,8 +74,8 @@ describe('Task Queue Branch Coverage Enhancement', () => {
78
74
  // visibilityTimeout is 200ms. Background touch is every 100ms.
79
75
  const task = await queue.enqueue('lease-loss-test', {});
80
76
  let workerFinished = false;
81
- // Mock touch to return undefined (stolen/lost)
82
- vi.spyOn(queue, 'touch').mockResolvedValue(undefined);
77
+ // Mock touchMany to return empty array (stolen/lost)
78
+ vi.spyOn(queue, 'touchMany').mockResolvedValue([]);
83
79
  queue.process({ cancellationSignal: token }, async (context) => {
84
80
  // Wait for background touch to discover lease loss
85
81
  for (let i = 0; i < 20; i++) {
@@ -113,7 +109,10 @@ describe('Task Queue Branch Coverage Enhancement', () => {
113
109
  expect(d2?.type).toBe('type1');
114
110
  });
115
111
  it('should handle maintenance when no tasks need maintenance', async () => {
116
- await expect(queue.maintenance()).resolves.toBeUndefined();
112
+ await queue.enqueue('t1', {});
113
+ await queue.maintenance();
114
+ const count = await queue.count({ status: TaskStatus.Pending });
115
+ expect(count).toBe(1);
117
116
  });
118
117
  it('should handle multiple workers competing for tasks', async () => {
119
118
  await queue.enqueue('compete', {});
@@ -143,36 +142,30 @@ describe('Task Queue Branch Coverage Enhancement', () => {
143
142
  expect(result).toBeUndefined();
144
143
  });
145
144
  it('should handle complete with non-existent task', async () => {
146
- const fakeTask = { id: '00000000-0000-0000-0000-000000000000', token: '00000000-0000-0000-0000-000000000000' };
147
- // Should not throw
145
+ const fakeTask = { id: crypto.randomUUID(), token: crypto.randomUUID() };
146
+ // Should not throw and should not affect existing tasks
147
+ await queue.enqueue('t1', {});
148
148
  await queue.complete(fakeTask);
149
+ const count = await queue.count();
150
+ expect(count).toBe(1);
149
151
  });
150
152
  it('should handle dequeueMany with forceDequeue', async () => {
151
153
  await queue.enqueue('force', {});
152
154
  const tasks = await queue.dequeueMany(1, { forceDequeue: true });
153
155
  expect(tasks).toHaveLength(1);
154
- });
155
- it('should handle dequeueMany with globalConcurrency limit', async () => {
156
- const limitedQueue = injector.resolve(TaskQueueProvider).get(`limited-${Date.now()}`, { globalConcurrency: 1 });
157
- await limitedQueue.enqueue('t1', {});
158
- await limitedQueue.enqueue('t2', {});
159
- const d1 = await limitedQueue.dequeue();
160
- expect(d1).toBeDefined();
161
- const d2 = await limitedQueue.dequeue();
162
- expect(d2).toBeUndefined(); // Limited by globalConcurrency
163
- });
164
- it('should handle waitForTasks timeout', async () => {
165
- const task = await queue.enqueue('wait-timeout', {});
166
- await expect(queue.waitForTasks([task.id], { timeout: 100, interval: 50 })).rejects.toThrow('Timeout');
156
+ expect(tasks[0]?.status).toBe(TaskStatus.Running);
167
157
  });
168
158
  it('should handle cancelMany with empty ids', async () => {
169
- // Should not throw
159
+ await queue.enqueue('t1', {});
170
160
  await queue.cancelMany([]);
161
+ const count = await queue.count({ status: TaskStatus.Pending });
162
+ expect(count).toBe(1);
171
163
  });
172
164
  it('should handle touch with progress and state', async () => {
173
165
  const task = await queue.enqueue('touch-data', {});
174
166
  const dequeued = await queue.dequeue();
175
- await queue.touch(dequeued, { progress: 0.7, state: { ok: true } });
167
+ const result = await queue.touch(dequeued, { progress: 0.7, state: { ok: true } });
168
+ expect(result).toBeDefined();
176
169
  const updated = await queue.getTask(task.id);
177
170
  expect(updated?.progress).toBe(0.7);
178
171
  expect(updated?.state).toEqual({ ok: true });
@@ -193,20 +186,8 @@ describe('Task Queue Branch Coverage Enhancement', () => {
193
186
  expect(dependent.status).toBe(TaskStatus.Pending); // Because scheduleAfterCount is 0
194
187
  expect(dependent.unresolvedCompleteDependencies).toBe(1);
195
188
  });
196
- it('should handle maintenance recovering zombie tasks', async () => {
197
- const isolatedQueue = injector.resolve(TaskQueueProvider).get(`zombie-${Date.now()}`, { visibilityTimeout: 100 });
198
- await isolatedQueue.enqueue('z', {});
199
- const d1 = await isolatedQueue.dequeue();
200
- expect(d1).toBeDefined();
201
- // Wait for visibility timeout to expire
202
- await timeout(200);
203
- await isolatedQueue.maintenance();
204
- const updated = await isolatedQueue.getTask(d1.id);
205
- expect(updated?.status).toBe(TaskStatus.Pending);
206
- expect(updated?.token).toBeNull();
207
- });
208
189
  it('should handle getTask including archived tasks', async () => {
209
- const archQueue = injector.resolve(TaskQueueProvider).get(`arch-${Date.now()}`, { retention: 0, archiveRetention: 3600000 });
190
+ const archQueue = injector.resolve(TaskQueueProvider).get(`arch-${crypto.randomUUID()}`, { retention: 0, archiveRetention: 3600000 });
210
191
  const t = await archQueue.enqueue('t', {});
211
192
  const d = await archQueue.dequeue();
212
193
  await archQueue.complete(d);
@@ -217,17 +198,20 @@ describe('Task Queue Branch Coverage Enhancement', () => {
217
198
  expect(archived?.id).toBe(t.id);
218
199
  });
219
200
  it('should handle maintenance with no work to do', async () => {
220
- await queue.maintenance(); // Should not throw
201
+ await queue.enqueue('t1', {});
202
+ await queue.maintenance();
203
+ const count = await queue.count({ status: TaskStatus.Pending });
204
+ expect(count).toBe(1);
221
205
  });
222
206
  it('should handle task TTL failure in maintenance', async () => {
223
207
  // retention=0 to allow maintenance to process immediately
224
- const ttlQueue = injector.resolve(TaskQueueProvider).get(`ttl-${Date.now()}`, { retention: 0 });
208
+ const ttlQueue = injector.resolve(TaskQueueProvider).get(`ttl-${crypto.randomUUID()}`, { retention: 0 });
225
209
  // Enqueue with TTL in the past
226
210
  const task = await ttlQueue.enqueue('ttl-fail', {}, { timeToLive: currentTimestamp() - 1000 });
227
211
  await timeout(100);
228
212
  await ttlQueue.maintenance();
229
213
  const updated = await ttlQueue.getTask(task.id);
230
- expect(updated?.status).toBe(TaskStatus.Dead);
214
+ expect(updated?.status).toBe(TaskStatus.Expired);
231
215
  expect(updated?.error?.message).toContain('Task expired');
232
216
  });
233
217
  it('should handle cancelMany with multiple valid IDs', async () => {
@@ -241,7 +225,7 @@ describe('Task Queue Branch Coverage Enhancement', () => {
241
225
  });
242
226
  it('should handle pruning long expired tasks', async () => {
243
227
  // archiveRetention = 0
244
- const pruneQueue = injector.resolve(TaskQueueProvider).get(`prune-long-${Date.now()}`, { retention: 0, archiveRetention: 0 });
228
+ const pruneQueue = injector.resolve(TaskQueueProvider).get(`prune-long-${crypto.randomUUID()}`, { retention: 0, archiveRetention: 0 });
245
229
  const task = await pruneQueue.enqueue('p1', {});
246
230
  const d1 = await pruneQueue.dequeue();
247
231
  await pruneQueue.complete(d1);
@@ -279,7 +263,7 @@ describe('Task Queue Branch Coverage Enhancement', () => {
279
263
  expect(tasks).toHaveLength(1);
280
264
  });
281
265
  it('should handle CircuitBreaker Half-Open state in dequeue', async () => {
282
- const namespace = `cb-half-${Date.now()}`;
266
+ const namespace = `cb-half-${crypto.randomUUID()}`;
283
267
  const cbQueue = injector.resolve(TaskQueueProvider).get(namespace, { circuitBreakerThreshold: 1 });
284
268
  const cb = injector.resolve(CircuitBreaker, namespace);
285
269
  // Mock CB to be Half-Open
@@ -289,7 +273,7 @@ describe('Task Queue Branch Coverage Enhancement', () => {
289
273
  expect(tasks).toBeDefined();
290
274
  });
291
275
  it('should handle CircuitBreaker Half-Open state blocking when already probing', async () => {
292
- const namespace = `cb-block-${Date.now()}`;
276
+ const namespace = `cb-block-${crypto.randomUUID()}`;
293
277
  const cbQueue = injector.resolve(TaskQueueProvider).get(namespace, { circuitBreakerThreshold: 1 });
294
278
  const cb = injector.resolve(CircuitBreaker, namespace);
295
279
  // Mock CB to be Half-Open but NOT a probe (already probing)
@@ -313,28 +297,32 @@ describe('Task Queue Branch Coverage Enhancement', () => {
313
297
  expect(updated?.status).toBe(TaskStatus.WaitingChildren);
314
298
  });
315
299
  it('should handle maintenance recovering zombie tasks via touchMany logic in prune', async () => {
316
- const pruneQueue = injector.resolve(TaskQueueProvider).get(`prune-fail-${Date.now()}`, { maxExecutionTime: 50, retention: 0 });
300
+ const pruneQueue = injector.resolve(TaskQueueProvider).get(`prune-fail-${crypto.randomUUID()}`, { maxExecutionTime: 50, retention: 0 });
317
301
  const task = await pruneQueue.enqueue('p1', {});
318
302
  await pruneQueue.dequeue();
319
303
  await timeout(100);
320
304
  await pruneQueue.maintenance();
321
305
  const updated = await pruneQueue.getTask(task.id);
322
- expect(updated?.status).toBe(TaskStatus.Dead);
306
+ expect(updated?.status).toBe(TaskStatus.TimedOut);
323
307
  });
324
308
  it('should handle enqueueMany with parentId and waitForCompletion false', async () => {
325
309
  const parent = await queue.enqueue('p', {});
326
- await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id, waitForCompletion: false }]);
310
+ const tasks = await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id, waitForCompletion: false }], { returnTasks: true });
311
+ expect(tasks).toHaveLength(1);
312
+ expect(tasks[0]?.parentId).toBe(parent.id);
313
+ const updatedParent = await queue.getTask(parent.id);
314
+ expect(updatedParent?.unresolvedCompleteDependencies).toBe(0);
327
315
  });
328
316
  it('should handle failMany for running tasks', async () => {
329
317
  const t1 = await queue.enqueue('t1', {});
330
318
  const d1 = await queue.dequeue();
331
319
  await queue.failMany([d1], [new Error('fail')]);
332
320
  const updated = await queue.getTask(t1.id);
333
- expect(updated?.status).toBe(TaskStatus.Pending);
321
+ expect(updated?.status).toBe(TaskStatus.Retrying);
334
322
  expect(updated?.tries).toBe(1);
335
323
  });
336
324
  it('should handle failMany reaching max tries (Dead state)', async () => {
337
- const isolatedQueue = injector.resolve(TaskQueueProvider).get(`exhaust-${Date.now()}`, { maxTries: 1 });
325
+ const isolatedQueue = injector.resolve(TaskQueueProvider).get(`exhaust-${crypto.randomUUID()}`, { maxTries: 1 });
338
326
  const t1 = await isolatedQueue.enqueue('t1', {});
339
327
  const d1 = await isolatedQueue.dequeue();
340
328
  await isolatedQueue.failMany([d1], [new Error('fatal')]);
@@ -390,7 +378,7 @@ describe('Task Queue Branch Coverage Enhancement', () => {
390
378
  expect(result).toHaveLength(0);
391
379
  });
392
380
  it('should handle large batch maintenance (> 1000 tasks)', async () => {
393
- const namespace = `aging-batch-${Date.now()}`;
381
+ const namespace = `aging-batch-${crypto.randomUUID()}`;
394
382
  const agingQueue = injector.resolve(TaskQueueProvider).get(namespace, { priorityAgingInterval: 60000, priorityAgingStep: 1 });
395
383
  const items = Array.from({ length: 1001 }, (_, i) => ({ type: 'batch', data: { i } }));
396
384
  await agingQueue.enqueueMany(items);
@@ -401,7 +389,7 @@ describe('Task Queue Branch Coverage Enhancement', () => {
401
389
  await agingQueue.maintenance();
402
390
  });
403
391
  it('should handle maintenance with empty archive and active tables', async () => {
404
- const emptyQueue = injector.resolve(TaskQueueProvider).get(`empty-${Date.now()}`, { retention: 0, archiveRetention: 0 });
392
+ const emptyQueue = injector.resolve(TaskQueueProvider).get(`empty-${crypto.randomUUID()}`, { retention: 0, archiveRetention: 0 });
405
393
  await emptyQueue.maintenance();
406
394
  });
407
395
  });