@tstdl/base 0.93.138 → 0.93.140

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 (138) hide show
  1. package/README.md +166 -0
  2. package/ai/genkit/multi-region.plugin.js +5 -3
  3. package/ai/genkit/tests/multi-region.test.d.ts +1 -0
  4. package/ai/genkit/tests/multi-region.test.js +5 -2
  5. package/ai/parser/parser.js +2 -2
  6. package/ai/prompts/build.js +1 -0
  7. package/ai/prompts/instructions-formatter.d.ts +15 -2
  8. package/ai/prompts/instructions-formatter.js +36 -31
  9. package/ai/prompts/prompt-builder.js +5 -5
  10. package/ai/prompts/steering.d.ts +3 -2
  11. package/ai/prompts/steering.js +3 -1
  12. package/ai/tests/instructions-formatter.test.js +1 -0
  13. package/api/README.md +403 -0
  14. package/api/client/client.js +7 -13
  15. package/api/client/tests/api-client.test.js +10 -10
  16. package/api/default-error-handlers.js +1 -1
  17. package/api/response.d.ts +2 -2
  18. package/api/response.js +22 -33
  19. package/api/server/api-controller.d.ts +1 -1
  20. package/api/server/api-controller.js +3 -3
  21. package/api/server/api-request-token.provider.d.ts +1 -0
  22. package/api/server/api-request-token.provider.js +1 -0
  23. package/api/server/middlewares/allowed-methods.middleware.js +2 -1
  24. package/api/server/middlewares/content-type.middleware.js +2 -1
  25. package/api/types.d.ts +3 -2
  26. package/application/README.md +240 -0
  27. package/application/application.js +2 -2
  28. package/audit/README.md +267 -0
  29. package/authentication/README.md +288 -0
  30. package/authentication/client/authentication.service.d.ts +12 -11
  31. package/authentication/client/authentication.service.js +21 -21
  32. package/authentication/client/http-client.middleware.js +2 -2
  33. package/authentication/tests/authentication.client-error-handling.test.js +2 -1
  34. package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
  35. package/browser/README.md +401 -0
  36. package/cancellation/README.md +156 -0
  37. package/cancellation/tests/coverage.test.d.ts +1 -0
  38. package/cancellation/tests/coverage.test.js +49 -0
  39. package/cancellation/tests/leak.test.d.ts +1 -0
  40. package/cancellation/tests/leak.test.js +35 -0
  41. package/cancellation/tests/token.test.d.ts +1 -0
  42. package/cancellation/tests/token.test.js +136 -0
  43. package/cancellation/token.d.ts +53 -177
  44. package/cancellation/token.js +132 -201
  45. package/context/README.md +174 -0
  46. package/cookie/README.md +161 -0
  47. package/css/README.md +157 -0
  48. package/data-structures/README.md +320 -0
  49. package/decorators/README.md +140 -0
  50. package/distributed-loop/README.md +231 -0
  51. package/distributed-loop/distributed-loop.js +1 -1
  52. package/document-management/README.md +403 -0
  53. package/document-management/server/services/document-management.service.js +9 -7
  54. package/document-management/tests/document-management-core.test.js +2 -7
  55. package/document-management/tests/document-management.api.test.js +6 -7
  56. package/document-management/tests/document-statistics.service.test.js +11 -12
  57. package/document-management/tests/document.service.test.js +3 -3
  58. package/document-management/tests/enum-helpers.test.js +2 -3
  59. package/dom/README.md +213 -0
  60. package/enumerable/README.md +259 -0
  61. package/enumeration/README.md +121 -0
  62. package/errors/README.md +267 -0
  63. package/file/README.md +191 -0
  64. package/formats/README.md +210 -0
  65. package/function/README.md +144 -0
  66. package/http/README.md +318 -0
  67. package/http/client/adapters/undici.adapter.js +1 -1
  68. package/http/client/http-client-request.d.ts +6 -5
  69. package/http/client/http-client-request.js +8 -9
  70. package/http/server/node/node-http-server.js +1 -2
  71. package/image-service/README.md +137 -0
  72. package/injector/README.md +491 -0
  73. package/injector/injector.d.ts +1 -0
  74. package/injector/injector.js +17 -5
  75. package/injector/tests/leak.test.d.ts +1 -0
  76. package/injector/tests/leak.test.js +45 -0
  77. package/intl/README.md +113 -0
  78. package/json-path/README.md +182 -0
  79. package/jsx/README.md +154 -0
  80. package/key-value-store/README.md +191 -0
  81. package/lock/README.md +249 -0
  82. package/lock/web/web-lock.js +119 -47
  83. package/logger/README.md +287 -0
  84. package/mail/README.md +256 -0
  85. package/memory/README.md +144 -0
  86. package/message-bus/README.md +244 -0
  87. package/message-bus/message-bus-base.js +1 -1
  88. package/module/README.md +182 -0
  89. package/module/module.d.ts +1 -1
  90. package/module/module.js +77 -17
  91. package/module/modules/web-server.module.js +1 -1
  92. package/notification/tests/notification-type.service.test.js +24 -15
  93. package/object-storage/README.md +300 -0
  94. package/openid-connect/README.md +274 -0
  95. package/orm/README.md +423 -0
  96. package/package.json +8 -6
  97. package/password/README.md +164 -0
  98. package/pdf/README.md +246 -0
  99. package/polyfills.js +1 -0
  100. package/pool/README.md +198 -0
  101. package/process/README.md +237 -0
  102. package/promise/README.md +252 -0
  103. package/promise/cancelable-promise.js +1 -1
  104. package/random/README.md +193 -0
  105. package/reflection/README.md +305 -0
  106. package/rpc/README.md +386 -0
  107. package/rxjs-utils/README.md +262 -0
  108. package/schema/README.md +342 -0
  109. package/serializer/README.md +342 -0
  110. package/signals/implementation/README.md +134 -0
  111. package/sse/README.md +278 -0
  112. package/task-queue/README.md +300 -0
  113. package/task-queue/postgres/task-queue.d.ts +2 -1
  114. package/task-queue/postgres/task-queue.js +32 -2
  115. package/task-queue/task-context.js +1 -1
  116. package/task-queue/task-queue.d.ts +17 -0
  117. package/task-queue/task-queue.js +103 -44
  118. package/task-queue/tests/complex.test.js +4 -4
  119. package/task-queue/tests/dependencies.test.js +4 -2
  120. package/task-queue/tests/queue.test.js +111 -0
  121. package/task-queue/tests/worker.test.js +21 -13
  122. package/templates/README.md +287 -0
  123. package/testing/README.md +157 -0
  124. package/text/README.md +346 -0
  125. package/threading/README.md +238 -0
  126. package/types/README.md +311 -0
  127. package/utils/README.md +322 -0
  128. package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
  129. package/utils/async-iterable-helpers/observable-iterable.js +4 -8
  130. package/utils/async-iterable-helpers/take-until.js +4 -4
  131. package/utils/backoff.js +89 -30
  132. package/utils/retry-with-backoff.js +1 -1
  133. package/utils/timer.d.ts +1 -1
  134. package/utils/timer.js +5 -7
  135. package/utils/timing.d.ts +1 -1
  136. package/utils/timing.js +2 -4
  137. package/utils/z-base32.d.ts +1 -0
  138. package/utils/z-base32.js +1 -0
@@ -175,6 +175,14 @@ export type QueueConfig = {
175
175
  export type TaskQueueArgument = string | (QueueConfig & {
176
176
  namespace: string;
177
177
  });
178
+ export type TaskQueueWaitOptions = {
179
+ interval?: number;
180
+ timeout?: number;
181
+ cancellationSignal?: CancellationSignal;
182
+ };
183
+ export type TaskQueueWaitResult = {
184
+ cancelled: boolean;
185
+ };
178
186
  export declare const defaultQueueConfig: Required<QueueConfig>;
179
187
  export declare abstract class TaskQueue<Definitions extends TaskDefinitionMap = TaskDefinitionMap> extends Transactional<QueueConfig & {
180
188
  namespace: string;
@@ -211,6 +219,15 @@ export declare abstract class TaskQueue<Definitions extends TaskDefinitionMap =
211
219
  abstract getTree(rootId: string | string[], options?: {
212
220
  transaction?: Transaction;
213
221
  }): Promise<Task[]>;
222
+ /**
223
+ * Waits for the specified tasks to reach a finalized state (Completed, Cancelled, or Dead) or be removed/archived from queue (non-existent).
224
+ * @param ids The IDs of the tasks to wait for.
225
+ * @param options Timeout and cancellation options.
226
+ */
227
+ abstract waitForTasks(ids: string[], options?: {
228
+ timeout?: number;
229
+ cancellationSignal?: CancellationSignal;
230
+ }): Promise<TaskQueueWaitResult>;
214
231
  abstract cancel(id: string, options?: {
215
232
  transaction?: Transaction;
216
233
  }): Promise<void>;
@@ -1,3 +1,55 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
1
53
  import { defineEnum } from '../enumeration/enumeration.js';
2
54
  import { inject, injectArgument, Injector } from '../injector/index.js';
3
55
  import { Logger } from '../logger/logger.js';
@@ -103,59 +155,66 @@ export class TaskQueue extends Transactional {
103
155
  */
104
156
  async processWorker(cancellationSignal, handler, options) {
105
157
  for await (const task of this.getConsumer(cancellationSignal, options)) {
106
- const taskToken = cancellationSignal.createChild();
107
- const context = new TaskContext(this, task, taskToken, this.logger.with({ type: task.type }));
108
- let isTaskActive = true;
109
- context.logger.verbose(`Processing task`);
110
- void (async () => {
111
- while (taskToken.isUnset) {
112
- await cancelableTimeout(Math.min(this.visibilityTimeout / 2, 5000), taskToken);
158
+ const env_1 = { stack: [], error: void 0, hasError: false };
159
+ try {
160
+ const taskToken = __addDisposableResource(env_1, cancellationSignal.fork(), false);
161
+ const context = new TaskContext(this, task, taskToken, this.logger.with({ type: task.type }));
162
+ let isTaskActive = true;
163
+ context.logger.verbose(`Processing task`);
164
+ void (async () => {
165
+ while (taskToken.isUnset) {
166
+ await cancelableTimeout(Math.min(this.visibilityTimeout / 2, 5000), taskToken);
167
+ if (taskToken.isSet) {
168
+ break;
169
+ }
170
+ try {
171
+ const touchedTask = await this.touch(task);
172
+ if (isUndefined(touchedTask) && taskToken.isUnset) {
173
+ context.logger.warn(`Task lost lease. Aborting.`);
174
+ isTaskActive = false;
175
+ taskToken.set();
176
+ }
177
+ }
178
+ catch (error) {
179
+ context.logger.error('Error touching task', error);
180
+ }
181
+ }
182
+ })();
183
+ try {
113
184
  if (taskToken.isSet) {
114
- break;
185
+ throw new Error('Task cancelled before start');
115
186
  }
116
- try {
117
- const touchedTask = await this.touch(task);
118
- if (isUndefined(touchedTask) && taskToken.isUnset) {
119
- context.logger.warn(`Task lost lease. Aborting.`);
120
- isTaskActive = false;
121
- taskToken.set();
187
+ const result = await handler(context);
188
+ if (isDefined(result) && isTaskActive) {
189
+ switch (result.payload.action) {
190
+ case 'complete':
191
+ context.logger.verbose(`Completing task`);
192
+ await this.complete(task, { result: result.payload.result });
193
+ break;
194
+ case 'fail':
195
+ context.logger.verbose(`Failing task`);
196
+ await this.fail(task, result.payload.error, { fatal: result.payload.fatal });
197
+ break;
198
+ case 'reschedule':
199
+ context.logger.verbose(`Rescheduling task`);
200
+ await this.reschedule(task.id, result.payload.timestamp);
201
+ break;
202
+ default:
203
+ throw new Error(`Unsupported task result action.`);
122
204
  }
123
205
  }
124
- catch (error) {
125
- context.logger.error('Error touching task', error);
126
- }
127
206
  }
128
- })();
129
- try {
130
- if (taskToken.isSet) {
131
- throw new Error('Task cancelled before start');
132
- }
133
- const result = await handler(context);
134
- if (isDefined(result) && isTaskActive) {
135
- switch (result.payload.action) {
136
- case 'complete':
137
- context.logger.verbose(`Completing task`);
138
- await this.complete(task, { result: result.payload.result });
139
- break;
140
- case 'fail':
141
- context.logger.verbose(`Failing task`);
142
- await this.fail(task, result.payload.error, { fatal: result.payload.fatal });
143
- break;
144
- case 'reschedule':
145
- context.logger.verbose(`Rescheduling task`);
146
- await this.reschedule(task.id, result.payload.timestamp);
147
- break;
148
- default:
149
- throw new Error(`Unsupported task result action.`);
150
- }
207
+ catch (error) {
208
+ context.logger.error('Error processing task', error);
209
+ await this.fail(task, error);
151
210
  }
152
211
  }
153
- catch (error) {
154
- context.logger.error('Error processing task', error);
155
- await this.fail(task, error);
212
+ catch (e_1) {
213
+ env_1.error = e_1;
214
+ env_1.hasError = true;
156
215
  }
157
216
  finally {
158
- taskToken.set();
217
+ __disposeResources(env_1);
159
218
  }
160
219
  }
161
220
  }
@@ -18,7 +18,7 @@ describe('Complex Queue Scenarios', () => {
18
18
  priorityAgingInterval: 50, // Fast aging
19
19
  priorityAgingStep: 10,
20
20
  rateLimit: 5,
21
- rateInterval: 50,
21
+ rateInterval: 200,
22
22
  retryDelayMinimum: 50,
23
23
  retryDelayGrowth: 2,
24
24
  retention: 50, // Fast retention for archive test
@@ -143,7 +143,7 @@ describe('Complex Queue Scenarios', () => {
143
143
  const batch2 = await queue.dequeueMany(1);
144
144
  expect(batch2.length).toBe(0); // Rate limited
145
145
  // Wait for refill
146
- await timeout(60);
146
+ await timeout(300);
147
147
  const batch3 = await queue.dequeueMany(5);
148
148
  expect(batch3.length).toBe(5); // Refilled
149
149
  });
@@ -153,7 +153,7 @@ describe('Complex Queue Scenarios', () => {
153
153
  await limitQueue.enqueueMany([
154
154
  { type: 'c', data: {} },
155
155
  { type: 'c', data: {} },
156
- { type: 'c', data: {} }
156
+ { type: 'c', data: {} },
157
157
  ]);
158
158
  const t1 = await limitQueue.dequeue();
159
159
  const t2 = await limitQueue.dequeue();
@@ -290,7 +290,7 @@ describe('Complex Queue Scenarios', () => {
290
290
  it('should handle mixed AND/OR dependencies', async () => {
291
291
  const dep = await queue.enqueue('dep', {}, {
292
292
  scheduleAfterTags: ['A', 'B'],
293
- dependencyJoinMode: DependencyJoinMode.Or
293
+ dependencyJoinMode: DependencyJoinMode.Or,
294
294
  });
295
295
  const A = await queue.enqueue('A', {}, { tags: ['A'] });
296
296
  await queue.complete((await queue.dequeue({ types: ['A'] })));
@@ -45,7 +45,8 @@ describe('Queue Dependencies & Tree Tests', () => {
45
45
  const dequeued = await queue.dequeue({ types: ['prereq'] });
46
46
  expect(dequeued?.id).toBe(prereq.id);
47
47
  await queue.complete(dequeued);
48
- await waitForStatus(dependent.id, TaskStatus.Completed);
48
+ await queue.processPendingFanIn();
49
+ await queue.waitForTasks([dependent.id]);
49
50
  const updatedDependent = await queue.getTask(dependent.id);
50
51
  expect(updatedDependent?.status).toBe(TaskStatus.Completed);
51
52
  });
@@ -77,7 +78,8 @@ describe('Queue Dependencies & Tree Tests', () => {
77
78
  const dequeued = await queue.dequeue({ types: ['prereq'] });
78
79
  // Fail fatally
79
80
  await queue.fail(dequeued, new Error('boom'), { fatal: true });
80
- await waitForStatus(dependent.id, TaskStatus.Dead);
81
+ await queue.processPendingFanIn();
82
+ await queue.waitForTasks([dependent.id]);
81
83
  const updatedDependent = await queue.getTask(dependent.id);
82
84
  expect(updatedDependent?.status).toBe(TaskStatus.Dead);
83
85
  expect(updatedDependent?.error?.code).toBe('DependencyFailed');
@@ -1,7 +1,9 @@
1
1
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
+ import { CancellationToken } from '../../cancellation/index.js';
2
3
  import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
3
4
  import { setupIntegrationTest } from '../../testing/index.js';
4
5
  import { currentTimestamp } from '../../utils/date-time.js';
6
+ import { Timer } from '../../utils/timer.js';
5
7
  import { timeout } from '../../utils/timing.js';
6
8
  describe('Queue Integration Tests', () => {
7
9
  let injector;
@@ -331,4 +333,113 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
331
333
  expect(updated?.state).toEqual({ step: 1 });
332
334
  });
333
335
  });
336
+ describe('waitForTasks', () => {
337
+ it('should wait for multiple tasks to reach finalized state', async () => {
338
+ const t1 = await queue.enqueue('foo', { foo: 'wait-1' });
339
+ const t2 = await queue.enqueue('foo', { foo: 'wait-2' });
340
+ void (async () => {
341
+ await timeout(100);
342
+ const d1 = await queue.dequeue();
343
+ await queue.complete(d1);
344
+ await timeout(100);
345
+ const d2 = await queue.dequeue();
346
+ await queue.complete(d2);
347
+ })();
348
+ const result = await queue.waitForTasks([t1.id, t2.id], { timeout: 2000 });
349
+ expect(result.cancelled).toBe(false);
350
+ const check1 = await queue.getTask(t1.id);
351
+ const check2 = await queue.getTask(t2.id);
352
+ expect(check1?.status).toBe(TaskStatus.Completed);
353
+ expect(check2?.status).toBe(TaskStatus.Completed);
354
+ });
355
+ it('should throw TimeoutError on timeout', async () => {
356
+ const t1 = await queue.enqueue('foo', { foo: 'timeout' });
357
+ await expect(queue.waitForTasks([t1.id], { timeout: 100 })).rejects.toThrow('Timeout while waiting for tasks to complete');
358
+ });
359
+ it('should wait for Cancelled and Dead states', async () => {
360
+ const t1 = await queue.enqueue('foo', { foo: 'cancel' });
361
+ const t2 = await queue.enqueue('foo', { foo: 'dead' });
362
+ void (async () => {
363
+ await timeout(50);
364
+ const d1 = await queue.dequeue();
365
+ if (d1)
366
+ await queue.cancel(d1.id);
367
+ const d2 = await queue.dequeue();
368
+ if (d2)
369
+ await queue.fail(d2, new Error('fatal'), { fatal: true });
370
+ queue.notify();
371
+ })();
372
+ const result = await queue.waitForTasks([t1.id, t2.id], { timeout: 2000 });
373
+ expect(result.cancelled).toBe(false);
374
+ const c1 = await queue.getTask(t1.id);
375
+ const c2 = await queue.getTask(t2.id);
376
+ expect(c1?.status).toBe(TaskStatus.Cancelled);
377
+ expect(c2?.status).toBe(TaskStatus.Dead);
378
+ });
379
+ it('should handle cancellationSignal', async () => {
380
+ const t1 = await queue.enqueue('foo', { foo: 'long' });
381
+ const signal = new CancellationToken();
382
+ void timeout(100).then(() => signal.set());
383
+ const result = await queue.waitForTasks([t1.id], { cancellationSignal: signal, timeout: 5000 });
384
+ expect(result.cancelled).toBe(true);
385
+ });
386
+ it('should return immediately for non-existent tasks', async () => {
387
+ const result = await queue.waitForTasks([crypto.randomUUID()], { timeout: 1000 });
388
+ expect(result.cancelled).toBe(false);
389
+ });
390
+ it('should return immediately if all tasks are already finalized', async () => {
391
+ const t1 = await queue.enqueue('foo', { foo: 'immediate' });
392
+ const d1 = await queue.dequeue();
393
+ await queue.complete(d1);
394
+ const timer = Timer.startNew();
395
+ const result = await queue.waitForTasks([t1.id], { timeout: 1000 });
396
+ expect(result.cancelled).toBe(false);
397
+ expect(timer.milliseconds).toBeLessThan(100);
398
+ });
399
+ it('should handle a mix of active and archived tasks', async () => {
400
+ const queueProvider = injector.resolve(TaskQueueProvider);
401
+ const archiveQueue = queueProvider.get(`archive-test-${crypto.randomUUID()}`, {
402
+ retention: 0, // Archive immediately
403
+ });
404
+ const t1 = await archiveQueue.enqueue('foo', { foo: 'archived' });
405
+ const d1 = await archiveQueue.dequeue();
406
+ await archiveQueue.complete(d1);
407
+ // Run maintenance to move to archive
408
+ await archiveQueue.maintenance();
409
+ const t2 = await archiveQueue.enqueue('foo', { foo: 'active' });
410
+ const d2 = await archiveQueue.dequeue();
411
+ await archiveQueue.complete(d2);
412
+ // t1 is archived, t2 is completed (active)
413
+ const result = await archiveQueue.waitForTasks([t1.id, t2.id], { timeout: 1000 });
414
+ expect(result.cancelled).toBe(false);
415
+ await archiveQueue.clear();
416
+ });
417
+ it('should return immediately for empty ids array', async () => {
418
+ const result = await queue.waitForTasks([], { timeout: 1000 });
419
+ expect(result.cancelled).toBe(false);
420
+ });
421
+ it('should wait for parent task to reach finalized state after child completion', async () => {
422
+ const childTag = `child-of-${crypto.randomUUID()}`;
423
+ const parent = await queue.enqueue('test', { value: 'parent' }, { completeAfterTags: [childTag] });
424
+ const dParent = await queue.dequeue();
425
+ // Spawn a child
426
+ await queue.enqueueMany([{ type: 'test', data: { value: 'child' }, parentId: parent.id, tags: [childTag] }], { transaction: undefined });
427
+ // Complete parent (it will move to Waiting because of completeAfterTags)
428
+ await queue.complete(dParent);
429
+ const checkParent = await queue.getTask(parent.id);
430
+ expect(checkParent?.status).toBe(TaskStatus.Waiting);
431
+ void (async () => {
432
+ await timeout(100);
433
+ const dChild = await queue.dequeue();
434
+ if (dChild) {
435
+ await queue.complete(dChild);
436
+ }
437
+ // Manual fan-in processing since we are in a test environment and might want immediate result
438
+ await queue.processPendingFanIn();
439
+ })();
440
+ await queue.waitForTasks([parent.id], { timeout: 2000 });
441
+ const finalParent = await queue.getTask(parent.id);
442
+ expect(finalParent?.status).toBe(TaskStatus.Completed);
443
+ });
444
+ });
334
445
  });
@@ -37,11 +37,12 @@ describe('Worker & Base Class Tests', () => {
37
37
  });
38
38
  // Wait until 2 tasks are processed
39
39
  for (let i = 0; i < 50; i++) {
40
- if (processed.length === 2)
40
+ if (processed.length == 2)
41
41
  break;
42
42
  await timeout(20);
43
43
  }
44
44
  token.set(); // Stop worker
45
+ await queue.waitForTasks([t1.id, t2.id], { interval: 50, timeout: 1000 });
45
46
  expect(processed).toContain(1);
46
47
  expect(processed).toContain(2);
47
48
  expect(processed.length).toBe(2);
@@ -55,7 +56,14 @@ describe('Worker & Base Class Tests', () => {
55
56
  queue.process({ cancellationSignal: token }, async () => {
56
57
  throw new Error('worker error');
57
58
  });
58
- await timeout(50);
59
+ // Wait until task is processed (error recorded and status is Pending)
60
+ for (let i = 0; i < 50; i++) {
61
+ const updated = await queue.getTask(task.id);
62
+ if (updated?.tries == 1) {
63
+ break;
64
+ }
65
+ await timeout(20);
66
+ }
59
67
  token.set();
60
68
  const updated = await queue.getTask(task.id);
61
69
  expect(updated?.status).toBe(TaskStatus.Pending); // Should retry
@@ -71,7 +79,7 @@ describe('Worker & Base Class Tests', () => {
71
79
  executed = true;
72
80
  return TaskProcessResult.Complete();
73
81
  });
74
- await timeout(500);
82
+ await queue.waitForTasks([task.id], { timeout: 5000 });
75
83
  token.set();
76
84
  expect(executed).toBe(true);
77
85
  const updated = await queue.getTask(task.id);
@@ -83,17 +91,21 @@ describe('Worker & Base Class Tests', () => {
83
91
  const processed = new Set();
84
92
  queue.process({ cancellationSignal: token }, async (context) => {
85
93
  processed.add(context.id);
86
- if (context.id === tFail.id) {
94
+ if (context.id == tFail.id) {
87
95
  return TaskProcessResult.Fail(new Error('explicit fail'));
88
96
  }
89
- if (context.id === tResched.id) {
97
+ if (context.id == tResched.id) {
90
98
  return TaskProcessResult.RescheduleBy(1000);
91
99
  }
92
100
  return TaskProcessResult.Complete();
93
101
  });
102
+ // Wait until tasks are processed (error/reschedule recorded and status is Pending)
94
103
  for (let i = 0; i < 50; i++) {
95
- if (processed.size === 2)
104
+ const uFail = await queue.getTask(tFail.id);
105
+ const uResched = await queue.getTask(tResched.id);
106
+ if (uFail?.tries == 1 && uResched?.status == TaskStatus.Pending && (uResched?.scheduleTimestamp ?? 0) > Date.now()) {
96
107
  break;
108
+ }
97
109
  await timeout(20);
98
110
  }
99
111
  token.set();
@@ -129,11 +141,7 @@ describe('Worker & Base Class Tests', () => {
129
141
  executed = true;
130
142
  return TaskProcessResult.Complete();
131
143
  });
132
- for (let i = 0; i < 50; i++) {
133
- if (executed)
134
- break;
135
- await timeout(20);
136
- }
144
+ await queue.waitForTasks([task.id], { interval: 50, timeout: 1000 });
137
145
  token.set();
138
146
  expect(executed).toBe(true);
139
147
  });
@@ -150,13 +158,13 @@ describe('Worker & Base Class Tests', () => {
150
158
  const finalAttemptValues = [];
151
159
  testQueue.process({ cancellationSignal: token }, async (context) => {
152
160
  finalAttemptValues.push(context.isFinalAttempt);
153
- if (context.attempt === 1) {
161
+ if (context.attempt == 1) {
154
162
  throw new Error('fail first attempt');
155
163
  }
156
164
  return TaskProcessResult.Complete();
157
165
  });
158
166
  for (let i = 0; i < 100; i++) {
159
- if (finalAttemptValues.length === 2)
167
+ if (finalAttemptValues.length == 2)
160
168
  break;
161
169
  testQueue.notify();
162
170
  await timeout(20);