@tstdl/base 0.93.175 → 0.93.177

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 (45) hide show
  1. package/api/server/middlewares/response-time.middleware.js +2 -5
  2. package/audit/auditor.js +1 -1
  3. package/authentication/server/authentication.service.js +1 -1
  4. package/circuit-breaker/postgres/circuit-breaker.js +1 -1
  5. package/document-management/server/services/singleton.js +1 -1
  6. package/errors/format.d.ts +6 -1
  7. package/errors/format.js +1 -1
  8. package/http/index.d.ts +1 -0
  9. package/http/index.js +1 -0
  10. package/http/server/http-server-response.d.ts +2 -0
  11. package/http/server/http-server-response.js +2 -0
  12. package/http/server/node/node-http-server.js +3 -0
  13. package/http/server-timing.d.ts +31 -0
  14. package/http/server-timing.js +47 -0
  15. package/http/tests/server-timing.test.d.ts +1 -0
  16. package/http/tests/server-timing.test.js +42 -0
  17. package/key-value-store/postgres/key-value-store.service.js +1 -1
  18. package/lock/postgres/provider.js +1 -1
  19. package/mail/README.md +1 -1
  20. package/mail/drizzle/0001_married_tarantula.sql +12 -0
  21. package/mail/drizzle/meta/0001_snapshot.json +69 -0
  22. package/mail/drizzle/meta/_journal.json +7 -0
  23. package/mail/index.d.ts +1 -0
  24. package/mail/index.js +1 -0
  25. package/mail/mail.service.d.ts +21 -4
  26. package/mail/mail.service.js +44 -13
  27. package/mail/models/mail-log.model.d.ts +2 -1
  28. package/mail/models/mail-log.model.js +1 -1
  29. package/mail/task-definitions.d.ts +20 -0
  30. package/mail/task-definitions.js +1 -0
  31. package/package.json +3 -3
  32. package/rate-limit/postgres/postgres-rate-limiter.js +1 -1
  33. package/task-queue/postgres/drizzle/0001_rapid_infant_terrible.sql +16 -0
  34. package/task-queue/postgres/drizzle/meta/0001_snapshot.json +753 -0
  35. package/task-queue/postgres/drizzle/meta/_journal.json +7 -0
  36. package/task-queue/postgres/task-queue.js +13 -13
  37. package/task-queue/postgres/task.model.d.ts +2 -1
  38. package/task-queue/postgres/task.model.js +3 -3
  39. package/task-queue/task-queue.d.ts +4 -3
  40. package/task-queue/task-queue.js +12 -2
  41. package/task-queue/tests/coverage-branch.test.js +1 -1
  42. package/task-queue/tests/dependencies.test.js +2 -2
  43. package/task-queue/tests/queue.test.js +2 -2
  44. package/task-queue/tests/worker.test.js +39 -5
  45. package/tsconfig.json +0 -2
@@ -8,6 +8,13 @@
8
8
  "when": 1772055508571,
9
9
  "tag": "0000_faithful_daimon_hellstrom",
10
10
  "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "7",
15
+ "when": 1774305322701,
16
+ "tag": "0001_rapid_infant_terrible",
17
+ "breakpoints": true
11
18
  }
12
19
  ]
13
20
  }
@@ -64,7 +64,7 @@ import { NotFoundError, serializeError, TimeoutError } from '../../errors/index.
64
64
  import { afterResolve, inject, provide, Singleton } from '../../injector/index.js';
65
65
  import { Logger } from '../../logger/index.js';
66
66
  import { MessageBus } from '../../message-bus/index.js';
67
- import { arrayOverlaps, caseWhen, coalesce, enumValue, greatest, interval, jsonbBuildObject, least, power, RANDOM_UUID_V4, TRANSACTION_TIMESTAMP } from '../../orm/index.js';
67
+ import { arrayOverlaps, buildJsonb, caseWhen, coalesce, enumValue, greatest, interval, least, power, RANDOM_UUID_V4, TRANSACTION_TIMESTAMP } from '../../orm/index.js';
68
68
  import { DatabaseConfig, injectRepository } from '../../orm/server/index.js';
69
69
  import { RateLimiter } from '../../rate-limit/index.js';
70
70
  import { distinct, toArray } from '../../utils/array/array.js';
@@ -133,7 +133,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
133
133
  progress: 0,
134
134
  data: sql `excluded.data`,
135
135
  state: null,
136
- error: null,
136
+ errors: buildJsonb([]),
137
137
  result: null,
138
138
  };
139
139
  [afterResolve]() {
@@ -183,7 +183,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
183
183
  data: item.data,
184
184
  state: null,
185
185
  result: null,
186
- error: null,
186
+ errors: buildJsonb([]),
187
187
  },
188
188
  }));
189
189
  const itemsWithIdempotency = entitiesWithIndex.filter((e) => isNotNull(e.entity.idempotencyKey));
@@ -721,7 +721,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
721
721
  status: caseWhen(exceededMaxExecutionTime, enumValue(TaskStatus, taskStatus, TaskStatus.TimedOut)).else(taskTable.status),
722
722
  visibilityDeadline: caseWhen(exceededMaxExecutionTime, null).else(sql `${TRANSACTION_TIMESTAMP} + ${interval(this.visibilityTimeout, 'milliseconds')}`),
723
723
  completeTimestamp: caseWhen(exceededMaxExecutionTime, TRANSACTION_TIMESTAMP).else(taskTable.completeTimestamp),
724
- error: caseWhen(exceededMaxExecutionTime, jsonbBuildObject({ code: 'MaxTimeExceeded', message: 'Hard Execution Timeout' })).else(taskTable.error),
724
+ errors: caseWhen(exceededMaxExecutionTime, sql `${taskTable.errors} || ${buildJsonb([{ code: 'MaxTimeExceeded', message: 'Hard Execution Timeout' }])}`).else(taskTable.errors),
725
725
  progress: caseWhen(exceededMaxExecutionTime, taskTable.progress).else(isDefined(options?.progress) ? options.progress : taskTable.progress),
726
726
  state: caseWhen(exceededMaxExecutionTime, taskTable.state).else(isDefined(options?.state) ? options.state : taskTable.state),
727
727
  })
@@ -844,7 +844,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
844
844
  .set({
845
845
  status: update.status,
846
846
  token: null,
847
- error: update.error,
847
+ errors: sql `${taskTable.errors} || ${buildJsonb([update.error])}`,
848
848
  visibilityDeadline: null,
849
849
  scheduleTimestamp: (update.status == TaskStatus.Retrying) ? update.scheduleTimestamp : taskTable.scheduleTimestamp,
850
850
  completeTimestamp: update.completeTimestamp,
@@ -866,7 +866,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
866
866
  const rows = tasks.map((task, index) => {
867
867
  const error = ensureTaskError(errors[index]);
868
868
  const update = this.getFailureUpdate(task.tries, error);
869
- return sql `(${task.id}::uuid, ${task.token}::uuid, ${task.tries}::int, ${update.status}::${taskStatus}, ${update.error}::jsonb, ${update.scheduleTimestamp}::timestamptz, ${update.completeTimestamp}::timestamptz)`;
869
+ return sql `(${task.id}::uuid, ${task.token}::uuid, ${task.tries}::int, ${update.status}::${taskStatus}, ${buildJsonb([update.error])}, ${update.scheduleTimestamp}::timestamptz, ${update.completeTimestamp}::timestamptz)`;
870
870
  });
871
871
  const updates = tx.pgTransaction.$with('updates').as((qb) => qb
872
872
  .select({
@@ -874,7 +874,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
874
874
  updateToken: sql `(token)`.as('update_token'),
875
875
  updateTries: sql `(tries)`.as('update_tries'),
876
876
  updateStatus: sql `(status)`.as('update_status'),
877
- updateError: sql `(error)`.as('update_error'),
877
+ updateErrors: sql `(error)`.as('update_errors'),
878
878
  updateSchedule: sql `(schedule_timestamp)`.as('update_schedule'),
879
879
  updateComplete: sql `(complete_timestamp)`.as('update_complete'),
880
880
  })
@@ -884,7 +884,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
884
884
  .set({
885
885
  status: sql `${updates.updateStatus}`,
886
886
  token: null,
887
- error: sql `${updates.updateError}`,
887
+ errors: sql `${taskTable.errors} || ${updates.updateErrors}`,
888
888
  visibilityDeadline: null,
889
889
  scheduleTimestamp: caseWhen(eq(updates.updateStatus, TaskStatus.Retrying), updates.updateSchedule).else(taskTable.scheduleTimestamp),
890
890
  completeTimestamp: sql `${updates.updateComplete}`,
@@ -985,7 +985,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
985
985
  .set({
986
986
  status: TaskStatus.Skipped,
987
987
  token: null,
988
- error: jsonbBuildObject({ code: 'DependencyFailed', message: 'One or more dependencies failed and abortOnDependencyFailure is enabled' }),
988
+ errors: sql `${taskTable.errors} || ${buildJsonb([{ code: 'DependencyFailed', message: 'One or more dependencies failed and abortOnDependencyFailure is enabled' }])}`,
989
989
  completeTimestamp: TRANSACTION_TIMESTAMP,
990
990
  })
991
991
  .where(inArray(taskTable.id, tasksToSkip))
@@ -1132,7 +1132,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
1132
1132
  .set({
1133
1133
  status: TaskStatus.Expired,
1134
1134
  token: null,
1135
- error: { code: 'Expired', message: 'Task expired before processing' },
1135
+ errors: sql `${taskTable.errors} || ${buildJsonb([{ code: 'Expired', message: 'Task expired before processing' }])}`,
1136
1136
  completeTimestamp: TRANSACTION_TIMESTAMP,
1137
1137
  })
1138
1138
  .where(inArray(taskTable.id, tx.pgTransaction.select().from(expiredSelection)))
@@ -1166,7 +1166,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
1166
1166
  visibilityDeadline: null,
1167
1167
  completeTimestamp: caseWhen(or(lt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`), gte(taskTable.tries, this.maxTries)), TRANSACTION_TIMESTAMP).else(null),
1168
1168
  scheduleTimestamp: caseWhen(and(lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP), lt(taskTable.tries, this.maxTries), not(lt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`))), sql `${TRANSACTION_TIMESTAMP} + ${interval(least(this.retryDelayMaximum, sql `${this.retryDelayMinimum} * ${power(this.retryDelayGrowth, sql `${taskTable.tries} - 1`)}`), 'milliseconds')}`).else(taskTable.scheduleTimestamp),
1169
- error: caseWhen(lt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`), jsonbBuildObject({ code: 'MaxTimeExceeded', message: sql `'Hard Execution Timeout: Task ran longer than ' || ${this.maxExecutionTime} || 'ms'`, lastError: taskTable.error })).else(caseWhen(lt(taskTable.tries, this.maxTries), jsonbBuildObject({ code: 'VisibilityTimeout', message: 'Worker Lost', lastError: taskTable.error })).else(jsonbBuildObject({ code: 'ZombieExhausted', message: 'Exceeded max retries after repeated crashes', lastError: taskTable.error }))),
1169
+ errors: caseWhen(lt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`), sql `${taskTable.errors} || ${buildJsonb([{ code: 'MaxTimeExceeded', message: sql `'Hard Execution Timeout: Task ran longer than ' || ${this.maxExecutionTime} || 'ms'` }])}`).else(caseWhen(lt(taskTable.tries, this.maxTries), sql `${taskTable.errors} || ${buildJsonb([{ code: 'VisibilityTimeout', message: 'Worker Lost' }])}`).else(sql `${taskTable.errors} || ${buildJsonb([{ code: 'ZombieExhausted', message: 'Exceeded max retries after repeated crashes' }])}`)),
1170
1170
  })
1171
1171
  .where(inArray(taskTable.id, tx.pgTransaction.select().from(selection)))
1172
1172
  .returning({ id: taskTable.id, status: taskTable.status });
@@ -1211,7 +1211,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
1211
1211
  .set({
1212
1212
  status: TaskStatus.Pending,
1213
1213
  token: null,
1214
- error: null,
1214
+ errors: [],
1215
1215
  result: null,
1216
1216
  scheduleTimestamp: TRANSACTION_TIMESTAMP,
1217
1217
  completeTimestamp: null,
@@ -1319,7 +1319,7 @@ PostgresTaskQueue = __decorate([
1319
1319
  Singleton({
1320
1320
  argumentIdentityProvider: JSON.stringify,
1321
1321
  providers: [
1322
- provide(DatabaseConfig, { useFactory: (_, context) => context.resolve(PostgresTaskQueueModuleConfig).database ?? context.resolve(DatabaseConfig, undefined, { skipSelf: 2 }) }),
1322
+ provide(DatabaseConfig, { useFactory: (_, context) => context.resolve(PostgresTaskQueueModuleConfig).database ?? context.resolve(DatabaseConfig, undefined, { skipSelf: true }) }),
1323
1323
  ],
1324
1324
  })
1325
1325
  ], PostgresTaskQueue);
@@ -1,4 +1,5 @@
1
1
  /** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
2
+ import type { NonRecursiveSerializedError } from '../../errors/index.js';
2
3
  import { BaseEntity, type Json, type Timestamp } from '../../orm/index.js';
3
4
  import type { ObjectLiteral } from '../../types/types.js';
4
5
  import { type Task, TaskDependencyType, TaskStatus } from '../task-queue.js';
@@ -27,7 +28,7 @@ export declare abstract class PostgresTaskBase<Data extends ObjectLiteral = Obje
27
28
  data: Json<Data> | null;
28
29
  state: Json<State> | null;
29
30
  result: Json<Result> | null;
30
- error: Json<ObjectLiteral> | null;
31
+ errors: Json<NonRecursiveSerializedError[]>;
31
32
  }
32
33
  export declare class PostgresTask<Data extends ObjectLiteral = ObjectLiteral, State extends ObjectLiteral = ObjectLiteral, Result extends ObjectLiteral = ObjectLiteral> extends PostgresTaskBase<Data, State, Result> implements Task {
33
34
  static readonly entityName = "Task";
@@ -37,7 +37,7 @@ export class PostgresTaskBase extends BaseEntity {
37
37
  data;
38
38
  state;
39
39
  result;
40
- error;
40
+ errors;
41
41
  }
42
42
  __decorate([
43
43
  StringProperty(),
@@ -136,9 +136,9 @@ __decorate([
136
136
  __metadata("design:type", Object)
137
137
  ], PostgresTaskBase.prototype, "result", void 0);
138
138
  __decorate([
139
- JsonProperty({ nullable: true }),
139
+ JsonProperty(),
140
140
  __metadata("design:type", Object)
141
- ], PostgresTaskBase.prototype, "error", void 0);
141
+ ], PostgresTaskBase.prototype, "errors", void 0);
142
142
  let PostgresTask = class PostgresTask extends PostgresTaskBase {
143
143
  static entityName = 'Task';
144
144
  };
@@ -1,12 +1,13 @@
1
1
  /** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
2
2
  import type { CancellationSignal } from '../cancellation/index.js';
3
3
  import { type EnumType } from '../enumeration/enumeration.js';
4
+ import type { NonRecursiveSerializedError } from '../errors/index.js';
4
5
  import { Injector } from '../injector/index.js';
5
6
  import type { Resolvable, resolveArgumentType } from '../injector/interfaces.js';
6
7
  import { Logger } from '../logger/logger.js';
7
8
  import type { Transaction } from '../orm/server/transaction.js';
8
9
  import { Transactional } from '../orm/server/transactional.js';
9
- import type { OneOrMany, Record, UndefinableJson } from '../types/types.js';
10
+ import type { OneOrMany, Record } from '../types/types.js';
10
11
  import { TaskQueueEnqueueBatch } from './enqueue-batch.js';
11
12
  import type { ProcessWorker, TaskData, TaskDefinitionMap, TaskOfType, TaskProcessResultPayload, TaskResult, TasksResults, TasksStates, TaskState, TaskTypes } from './types.js';
12
13
  /**
@@ -204,8 +205,8 @@ export type Task<Definitions extends TaskDefinitionMap = Record<string, {
204
205
  state: TaskState<Definitions, Type> | null;
205
206
  /** The result of the task, if successful. */
206
207
  result: TaskResult<Definitions, Type> | null;
207
- /** The error that occurred, if the task failed. */
208
- error: UndefinableJson | null;
208
+ /** The errors that occurred, if the task failed. */
209
+ errors: NonRecursiveSerializedError[];
209
210
  };
210
211
  }[Extract<keyof Definitions, string>];
211
212
  /** Default priority for tasks. */
@@ -283,9 +283,19 @@ export class TaskQueue extends Transactional {
283
283
  }
284
284
  }
285
285
  catch (error) {
286
+ const isUnsupportedAction = isError(error) && (error.message == 'Unsupported task result action.');
287
+ const isCancelledBeforeStart = isError(error) && (error.message == 'Task cancelled before start');
288
+ if (isCancelledBeforeStart) {
289
+ return;
290
+ }
286
291
  context.logger.error('Error processing task', error);
287
- await this.fail(task, error);
288
- if (isError(error) && ((error.message === 'Task cancelled before start') || (error.message == 'Unsupported task result action.'))) {
292
+ try {
293
+ await this.fail(task, error, { fatal: isUnsupportedAction });
294
+ }
295
+ catch (failError) {
296
+ context.logger.error('Error failing task after processing error', failError);
297
+ }
298
+ if (isUnsupportedAction) {
289
299
  throw error;
290
300
  }
291
301
  }
@@ -212,7 +212,7 @@ describe('Task Queue Branch Coverage Enhancement', () => {
212
212
  await ttlQueue.maintenance();
213
213
  const updated = await ttlQueue.getTask(task.id);
214
214
  expect(updated?.status).toBe(TaskStatus.Expired);
215
- expect(updated?.error?.message).toContain('Task expired');
215
+ expect(updated?.errors[0]?.message).toContain('Task expired');
216
216
  });
217
217
  it('should handle cancelMany with multiple valid IDs', async () => {
218
218
  const t1 = await queue.enqueue('c1', {});
@@ -73,7 +73,7 @@ describe('Queue Dependencies & Tree Tests', () => {
73
73
  await queue.waitForTasks([dependent.id]);
74
74
  const updatedDependent = await queue.getTask(dependent.id);
75
75
  expect(updatedDependent?.status).toBe(TaskStatus.Skipped);
76
- expect(updatedDependent?.error?.code).toBe('DependencyFailed');
76
+ expect(updatedDependent?.errors[0]?.code).toBe('DependencyFailed');
77
77
  });
78
78
  it('should NOT overwrite terminal states during cancellation (abortOnDependencyFailure + complete)', async () => {
79
79
  const dep = await queue.enqueue('dep', {});
@@ -261,7 +261,7 @@ describe('Queue Dependencies & Tree Tests', () => {
261
261
  const updated = await queue.getTask(task.id);
262
262
  expect(updated?.status).toBe(TaskStatus.Pending);
263
263
  expect(updated?.tries).toBe(0);
264
- expect(updated?.error).toBeNull();
264
+ expect(updated?.errors).toEqual([]);
265
265
  });
266
266
  it('should consume tasks via async iterator (getConsumer)', async () => {
267
267
  await queue.enqueue('c1', { val: 1 });
@@ -184,7 +184,7 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
184
184
  const updated = await queue.getTask(task.id);
185
185
  expect(updated?.status).toBe(TaskStatus.Retrying);
186
186
  expect(updated?.tries).toBe(1);
187
- expect(updated?.error).toBeDefined();
187
+ expect(updated?.errors).toHaveLength(1);
188
188
  });
189
189
  it('should NOT clear startTimestamp when transitioning to terminal or retry states', async () => {
190
190
  // Re-create queue with high circuit breaker threshold for this test
@@ -423,7 +423,7 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
423
423
  await shortQueue.maintenance();
424
424
  const updated = await shortQueue.getTask(task.id);
425
425
  expect(updated?.status).toBe(TaskStatus.TimedOut);
426
- expect(updated?.error?.code).toBe('MaxTimeExceeded');
426
+ expect(updated?.errors[0]?.code).toBe('MaxTimeExceeded');
427
427
  await shortQueue.clear();
428
428
  });
429
429
  it('should touch a task to extend token', async () => {
@@ -1,4 +1,4 @@
1
- import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import { CancellationToken } from '../../cancellation/index.js';
3
3
  import { getRepository } from '../../orm/server/index.js';
4
4
  import { TaskProcessResult, TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
@@ -33,6 +33,7 @@ describe('Worker & Base Class Tests', () => {
33
33
  await queue.clear();
34
34
  const queueProvider = injector.resolve(TaskQueueProvider);
35
35
  await queueProvider.get(otherQueueName).clear();
36
+ vi.restoreAllMocks();
36
37
  });
37
38
  afterAll(async () => {
38
39
  await injector?.dispose();
@@ -80,7 +81,7 @@ describe('Worker & Base Class Tests', () => {
80
81
  const updated = await queue.getTask(task.id);
81
82
  expect(updated?.status).toBe(TaskStatus.Retrying); // Should retry
82
83
  expect(updated?.tries).toBe(1);
83
- expect(updated?.error?.message).toBe('worker error');
84
+ expect(updated?.errors[0]?.message).toBe('worker error');
84
85
  });
85
86
  it('should extend lease (heartbeat) during long processing', async () => {
86
87
  const task = await queue.enqueue('long', {});
@@ -125,7 +126,7 @@ describe('Worker & Base Class Tests', () => {
125
126
  token.set();
126
127
  const uFail = await queue.getTask(tFail.id);
127
128
  expect(uFail?.status).toBe(TaskStatus.Retrying); // Retry
128
- expect(uFail?.error?.message).toBe('explicit fail');
129
+ expect(uFail?.errors[0]?.message).toBe('explicit fail');
129
130
  const uResched = await queue.getTask(tResched.id);
130
131
  expect(uResched?.status).toBe(TaskStatus.Pending);
131
132
  expect(uResched?.scheduleTimestamp).toBeGreaterThan(Date.now());
@@ -218,7 +219,40 @@ describe('Worker & Base Class Tests', () => {
218
219
  const workerPromise = queue.processWorker(token, () => ({ payload: { action: 'magic' } }));
219
220
  await expect(workerPromise).rejects.toThrow('Unsupported task result action');
220
221
  const updated = await queue.getTask(task.id);
221
- expect(updated?.status).toBe(TaskStatus.Retrying);
222
- expect(updated?.error?.message).toContain('Unsupported task result action');
222
+ expect(updated?.status).toBe(TaskStatus.Dead); // Fatal now
223
+ expect(updated?.errors[0]?.message).toContain('Unsupported task result action');
224
+ });
225
+ it('should not crash worker lane if fail() throws', async () => {
226
+ const t1 = await queue.enqueue('task1', {});
227
+ const t2 = await queue.enqueue('task2', {});
228
+ let t1Processed = false;
229
+ let t2Processed = false;
230
+ // Mock fail to throw once
231
+ const failSpy = vi.spyOn(queue, 'fail').mockRejectedValueOnce(new Error('DB DOWN'));
232
+ queue.process({ cancellationSignal: token }, async (context) => {
233
+ if (context.id === t1.id) {
234
+ t1Processed = true;
235
+ throw new Error('Task 1 failed');
236
+ }
237
+ if (context.id === t2.id) {
238
+ t2Processed = true;
239
+ return TaskProcessResult.Complete();
240
+ }
241
+ return TaskProcessResult.Complete();
242
+ });
243
+ // Wait until both tasks are processed
244
+ // If the bug exists, the worker lane will crash after t1 and t2 will never be processed
245
+ for (let i = 0; i < 40; i++) {
246
+ if (t1Processed && t2Processed)
247
+ break;
248
+ queue.notify();
249
+ await timeout(50);
250
+ }
251
+ token.set();
252
+ expect(t1Processed).toBe(true);
253
+ expect(t2Processed).toBe(true);
254
+ expect(failSpy).toHaveBeenCalled();
255
+ const check2 = await queue.getTask(t2.id);
256
+ expect(check2?.status).toBe(TaskStatus.Completed);
223
257
  });
224
258
  });
package/tsconfig.json CHANGED
@@ -5,7 +5,6 @@
5
5
  "moduleResolution": "nodenext",
6
6
  "lib": ["esnext", "dom"],
7
7
  "target": "es2024",
8
- "baseUrl": "source/",
9
8
  "rootDir": "source/",
10
9
  "outDir": "dist/",
11
10
  "declaration": true,
@@ -15,7 +14,6 @@
15
14
  "removeComments": false,
16
15
  "resolveJsonModule": true,
17
16
  "sourceMap": false,
18
- "downlevelIteration": true,
19
17
  "emitDecoratorMetadata": true,
20
18
  "experimentalDecorators": true,
21
19