@tstdl/base 0.93.140 → 0.93.141

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 (98) hide show
  1. package/application/application.d.ts +1 -1
  2. package/application/application.js +1 -1
  3. package/application/providers.d.ts +20 -2
  4. package/application/providers.js +34 -7
  5. package/audit/module.d.ts +5 -0
  6. package/audit/module.js +9 -1
  7. package/authentication/server/module.d.ts +5 -0
  8. package/authentication/server/module.js +9 -1
  9. package/authentication/tests/authentication.api-controller.test.js +1 -1
  10. package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
  11. package/authentication/tests/authentication.client-service.test.js +1 -1
  12. package/circuit-breaker/postgres/module.d.ts +1 -0
  13. package/circuit-breaker/postgres/module.js +5 -1
  14. package/document-management/server/configure.js +5 -1
  15. package/document-management/server/module.d.ts +1 -1
  16. package/document-management/server/module.js +1 -1
  17. package/document-management/server/services/document-management-ancillary.service.js +1 -1
  18. package/document-management/tests/ai-config-hierarchy.test.js +0 -5
  19. package/document-management/tests/document-management-ai-overrides.test.js +0 -1
  20. package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
  21. package/examples/document-management/main.d.ts +1 -0
  22. package/examples/document-management/main.js +14 -11
  23. package/key-value-store/postgres/module.d.ts +1 -0
  24. package/key-value-store/postgres/module.js +5 -1
  25. package/lock/postgres/module.d.ts +1 -0
  26. package/lock/postgres/module.js +5 -1
  27. package/mail/module.d.ts +5 -1
  28. package/mail/module.js +11 -6
  29. package/module/modules/web-server.module.js +2 -3
  30. package/notification/server/module.d.ts +1 -0
  31. package/notification/server/module.js +5 -1
  32. package/notification/tests/notification-flow.test.js +2 -2
  33. package/orm/decorators.d.ts +5 -1
  34. package/orm/decorators.js +1 -1
  35. package/orm/server/drizzle/schema-converter.js +17 -30
  36. package/orm/server/encryption.d.ts +0 -1
  37. package/orm/server/encryption.js +1 -4
  38. package/orm/server/index.d.ts +1 -6
  39. package/orm/server/index.js +1 -6
  40. package/orm/server/migration.d.ts +19 -0
  41. package/orm/server/migration.js +72 -0
  42. package/orm/server/repository.d.ts +1 -1
  43. package/orm/server/transaction.d.ts +5 -10
  44. package/orm/server/transaction.js +22 -26
  45. package/orm/server/transactional.js +3 -3
  46. package/orm/tests/database-migration.test.d.ts +1 -0
  47. package/orm/tests/database-migration.test.js +82 -0
  48. package/orm/tests/encryption.test.js +3 -4
  49. package/orm/utils.d.ts +17 -2
  50. package/orm/utils.js +49 -1
  51. package/package.json +4 -3
  52. package/rate-limit/postgres/module.d.ts +1 -0
  53. package/rate-limit/postgres/module.js +5 -1
  54. package/reflection/decorator-data.js +11 -12
  55. package/task-queue/README.md +2 -9
  56. package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
  57. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
  58. package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
  59. package/task-queue/postgres/module.d.ts +1 -0
  60. package/task-queue/postgres/module.js +5 -1
  61. package/task-queue/postgres/schemas.d.ts +9 -6
  62. package/task-queue/postgres/schemas.js +4 -3
  63. package/task-queue/postgres/task-queue.d.ts +2 -12
  64. package/task-queue/postgres/task-queue.js +431 -354
  65. package/task-queue/postgres/task.model.d.ts +12 -5
  66. package/task-queue/postgres/task.model.js +51 -25
  67. package/task-queue/task-context.d.ts +2 -2
  68. package/task-queue/task-context.js +7 -7
  69. package/task-queue/task-queue.d.ts +36 -19
  70. package/task-queue/task-queue.js +18 -10
  71. package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
  72. package/task-queue/tests/cascading-cancellations.test.js +38 -0
  73. package/task-queue/tests/complex.test.js +44 -228
  74. package/task-queue/tests/coverage-branch.test.d.ts +1 -0
  75. package/task-queue/tests/coverage-branch.test.js +407 -0
  76. package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
  77. package/task-queue/tests/coverage-enhancement.test.js +144 -0
  78. package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
  79. package/task-queue/tests/dag-dependencies.test.js +41 -0
  80. package/task-queue/tests/dependencies.test.js +26 -26
  81. package/task-queue/tests/extensive-dependencies.test.js +64 -139
  82. package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
  83. package/task-queue/tests/fan-out-spawning.test.js +53 -0
  84. package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
  85. package/task-queue/tests/idempotent-replacement.test.js +61 -0
  86. package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
  87. package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
  88. package/task-queue/tests/queue.test.js +33 -24
  89. package/task-queue/tests/worker.test.js +20 -5
  90. package/task-queue/tests/zombie-parent.test.d.ts +1 -0
  91. package/task-queue/tests/zombie-parent.test.js +45 -0
  92. package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
  93. package/task-queue/tests/zombie-recovery.test.js +51 -0
  94. package/test5.js +5 -5
  95. package/testing/integration-setup.d.ts +4 -4
  96. package/testing/integration-setup.js +54 -29
  97. package/text/localization.service.js +2 -2
  98. package/utils/file-reader.js +1 -2
@@ -56,7 +56,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
56
56
  var e = new Error(message);
57
57
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
58
58
  });
59
- import { and, asc, count, eq, gt, gte, inArray, lt, lte, notInArray, or, sql, isNull as sqlIsNull } from 'drizzle-orm';
59
+ import { aliasedTable, and, asc, count, eq, gt, gte, inArray, lt, lte, notExists, or, sql, count as sqlCount, isNull as sqlIsNull } from 'drizzle-orm';
60
60
  import { merge, throttleTime } from 'rxjs';
61
61
  import { CancellationSignal } from '../../cancellation/index.js';
62
62
  import { CircuitBreaker, CircuitBreakerState } from '../../circuit-breaker/index.js';
@@ -64,22 +64,20 @@ import { serializeError, TimeoutError } from '../../errors/index.js';
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, coalesce, interval, RANDOM_UUID_V4, TRANSACTION_TIMESTAMP, unnest } from '../../orm/index.js';
67
+ import { arrayOverlaps, coalesce, getEntityIds, interval, 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
- import { createArray, distinct, toArray } from '../../utils/array/array.js';
71
- import { digest } from '../../utils/cryptography.js';
70
+ import { createArray, toArray } from '../../utils/array/array.js';
72
71
  import { currentTimestamp } from '../../utils/date-time.js';
73
72
  import { Timer } from '../../utils/timer.js';
74
73
  import { cancelableTimeout } from '../../utils/timing.js';
75
- import { isDefined, isNotNull, isNull, isString, isUndefined } from '../../utils/type-guards.js';
74
+ import { isDefined, isNotNull, isNotNullOrUndefined, isNull, isString, isUndefined } from '../../utils/type-guards.js';
76
75
  import { millisecondsPerSecond } from '../../utils/units.js';
77
- import { defaultQueueConfig, DependencyJoinMode, TaskProcessResult, TaskQueue, TaskStatus } from '../task-queue.js';
76
+ import { defaultQueueConfig, TaskDependencyType, TaskQueue, TaskStatus } from '../task-queue.js';
78
77
  import { PostgresTaskQueueModuleConfig } from './module.js';
79
- import { taskArchive as taskArchiveTable, taskStatus, task as taskTable } from './schemas.js';
78
+ import { taskArchive as taskArchiveTable, taskDependency as taskDependencyTable, taskStatus, task as taskTable } from './schemas.js';
80
79
  import { PostgresTask, PostgresTaskArchive } from './task.model.js';
81
80
  let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
82
- #internalThis = this;
83
81
  #repository = injectRepository(PostgresTask);
84
82
  #archiveRepository = injectRepository(PostgresTaskArchive);
85
83
  #config = this.config;
@@ -113,20 +111,17 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
113
111
  idempotencyWindow = this.#config.idempotencyWindow ?? defaultQueueConfig.idempotencyWindow;
114
112
  globalConcurrency = this.#config.globalConcurrency ?? defaultQueueConfig.globalConcurrency;
115
113
  #takeNewUpdate = {
116
- id: RANDOM_UUID_V4,
117
114
  namespace: this.#namespace,
118
115
  type: sql `excluded.type`,
119
- status: TaskStatus.Pending,
116
+ status: sql `CASE WHEN excluded.unresolved_schedule_dependencies > 0 THEN ${TaskStatus.Waiting}::${taskStatus} ELSE ${TaskStatus.Pending}::${taskStatus} END`,
120
117
  token: null,
121
118
  priority: sql `excluded.priority`,
122
119
  idempotencyKey: sql `excluded.idempotency_key`,
123
120
  traceId: sql `excluded.trace_id`,
124
121
  tags: sql `excluded.tags`,
125
- completeAfterTags: sql `excluded.complete_after_tags`,
126
- scheduleAfterTags: sql `excluded.schedule_after_tags`,
122
+ unresolvedScheduleDependencies: sql `excluded.unresolved_schedule_dependencies`,
123
+ unresolvedCompleteDependencies: sql `excluded.unresolved_complete_dependencies`,
127
124
  failFast: sql `excluded.fail_fast`,
128
- dependencyJoinMode: sql `excluded.dependency_join_mode`,
129
- dependencyTriggerStatuses: sql `excluded.dependency_trigger_statuses`,
130
125
  tries: 0,
131
126
  creationTimestamp: TRANSACTION_TIMESTAMP,
132
127
  priorityAgeTimestamp: TRANSACTION_TIMESTAMP,
@@ -143,7 +138,6 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
143
138
  };
144
139
  [afterResolve]() {
145
140
  if (!this.isInTransaction) {
146
- this.startSystemWorker();
147
141
  void this.maintenanceLoop();
148
142
  }
149
143
  }
@@ -158,21 +152,21 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
158
152
  return (options?.returnTasks == true) ? [] : undefined;
159
153
  }
160
154
  const newEntities = items.map((item) => {
161
- const hasDependencies = (item.completeAfterTags?.length ?? 0) > 0 || (item.scheduleAfterTags?.length ?? 0) > 0;
155
+ const scheduleAfterCount = item.scheduleAfter?.length ?? 0;
156
+ const completeAfterCount = item.completeAfter?.length ?? 0;
157
+ const isWaiting = scheduleAfterCount > 0;
162
158
  return {
163
159
  namespace: this.#namespace,
164
160
  type: item.type,
165
- status: hasDependencies ? TaskStatus.Waiting : TaskStatus.Pending,
161
+ status: isWaiting ? TaskStatus.Waiting : TaskStatus.Pending,
166
162
  token: null,
167
163
  priority: item.priority ?? 1000,
168
164
  idempotencyKey: item.idempotencyKey ?? null,
169
165
  traceId: null,
170
166
  tags: item.tags ?? [],
171
- completeAfterTags: item.completeAfterTags ?? [],
172
- scheduleAfterTags: item.scheduleAfterTags ?? [],
167
+ unresolvedScheduleDependencies: scheduleAfterCount,
168
+ unresolvedCompleteDependencies: completeAfterCount,
173
169
  failFast: item.failFast ?? false,
174
- dependencyJoinMode: item.dependencyJoinMode ?? DependencyJoinMode.And,
175
- dependencyTriggerStatuses: item.dependencyTriggerStatuses ?? [TaskStatus.Completed],
176
170
  parentId: item.parentId ?? null,
177
171
  tries: 0,
178
172
  progress: 0,
@@ -191,42 +185,117 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
191
185
  });
192
186
  const itemsWithIdempotency = newEntities.filter((e) => isNotNull(e.idempotencyKey));
193
187
  const itemsWithoutIdempotency = newEntities.filter((e) => isNull(e.idempotencyKey));
194
- const hasDependencies = items.some((item) => ((item.completeAfterTags?.length ?? 0) > 0) || ((item.scheduleAfterTags?.length ?? 0) > 0));
188
+ const hasDependencies = items.some((item) => ((item.scheduleAfter?.length ?? 0) > 0) || ((item.completeAfter?.length ?? 0) > 0) || (isDefined(item.parentId) && (item.waitForCompletion ?? true)));
195
189
  const mustUseTransaction = ((itemsWithoutIdempotency.length + itemsWithIdempotency.length) > 1) || hasDependencies;
196
190
  const newTransaction = __addDisposableResource(env_1, (mustUseTransaction && isUndefined(options?.transaction)) ? await this.#repository.startTransaction() : undefined, true);
197
191
  const transaction = newTransaction ?? options?.transaction;
192
+ const session = transaction?.pgTransaction ?? this.#repository.session;
198
193
  const tasks = [];
199
194
  if (itemsWithoutIdempotency.length > 0) {
200
- const insertedTasks = await this.#repository.withOptionalTransaction(transaction).insertMany(itemsWithoutIdempotency);
201
- tasks.push(...insertedTasks);
195
+ const insertedRows = await session.insert(taskTable).values(itemsWithoutIdempotency).returning();
196
+ tasks.push(...insertedRows);
202
197
  }
203
198
  if (itemsWithIdempotency.length > 0) {
204
- const repository = this.#repository.withOptionalTransaction(transaction);
205
199
  const windowCutoff = sql `${TRANSACTION_TIMESTAMP} - ${interval(this.idempotencyWindow, 'milliseconds')}`;
206
200
  const setWhere = (options?.replace == true) ? undefined : sql `${taskTable.creationTimestamp} < ${windowCutoff}`;
207
- const upsertedTasks = await repository.upsertMany(['namespace', 'idempotencyKey'], itemsWithIdempotency, this.#takeNewUpdate, { set: setWhere });
208
- tasks.push(...upsertedTasks);
209
- if (options?.returnTasks == true && upsertedTasks.length < itemsWithIdempotency.length) {
210
- const upsertedKeys = new Set(upsertedTasks.map((t) => t.idempotencyKey));
201
+ const upsertedRows = await session
202
+ .insert(taskTable)
203
+ .values(itemsWithIdempotency)
204
+ .onConflictDoUpdate({
205
+ target: [taskTable.namespace, taskTable.idempotencyKey],
206
+ set: this.#takeNewUpdate,
207
+ setWhere,
208
+ })
209
+ .returning();
210
+ tasks.push(...upsertedRows);
211
+ if (options?.replace == true) {
212
+ const upsertedIds = upsertedRows.map((t) => t.id);
213
+ if (upsertedIds.length > 0) {
214
+ await session.delete(taskDependencyTable).where(and(eq(taskDependencyTable.namespace, this.#namespace), inArray(taskDependencyTable.taskId, upsertedIds)));
215
+ }
216
+ }
217
+ if (((options?.returnTasks == true) || hasDependencies) && (upsertedRows.length < itemsWithIdempotency.length)) {
218
+ const upsertedKeys = new Set(upsertedRows.map((t) => t.idempotencyKey));
211
219
  const missingKeys = itemsWithIdempotency.map((i) => i.idempotencyKey).filter((k) => !upsertedKeys.has(k));
212
220
  if (missingKeys.length > 0) {
213
- const existingTasks = await repository.loadManyByQuery({
221
+ const existingRows = await session
222
+ .select()
223
+ .from(taskTable)
224
+ .where(and(eq(taskTable.namespace, this.#namespace), inArray(taskTable.idempotencyKey, missingKeys)));
225
+ tasks.push(...existingRows);
226
+ }
227
+ }
228
+ }
229
+ if (hasDependencies) {
230
+ const dependencies = [];
231
+ const parentIncrements = new Map();
232
+ for (const [index, item] of items.entries()) {
233
+ const task = tasks[index];
234
+ if (isDefined(item.parentId) && (item.waitForCompletion ?? true)) {
235
+ dependencies.push({
214
236
  namespace: this.#namespace,
215
- idempotencyKey: { $in: missingKeys },
237
+ taskId: item.parentId,
238
+ dependencyTaskId: task.id,
239
+ type: TaskDependencyType.Complete,
240
+ requiredStatuses: [TaskStatus.Completed],
216
241
  });
217
- tasks.push(...existingTasks);
242
+ parentIncrements.set(item.parentId, (parentIncrements.get(item.parentId) ?? 0) + 1);
243
+ }
244
+ if (isDefined(item.scheduleAfter)) {
245
+ for (const dependency of item.scheduleAfter) {
246
+ const dependencyTaskId = isString(dependency) ? dependency : dependency.id;
247
+ const requiredStatuses = (isString(dependency) ? [TaskStatus.Completed] : (dependency.requiredStatuses ?? [TaskStatus.Completed]));
248
+ dependencies.push({
249
+ namespace: this.#namespace,
250
+ taskId: task.id,
251
+ dependencyTaskId,
252
+ type: TaskDependencyType.Schedule,
253
+ requiredStatuses,
254
+ });
255
+ }
256
+ }
257
+ if (isDefined(item.completeAfter)) {
258
+ for (const dependency of item.completeAfter) {
259
+ const dependencyTaskId = isString(dependency) ? dependency : dependency.id;
260
+ const requiredStatuses = (isString(dependency) ? [TaskStatus.Completed] : (dependency.requiredStatuses ?? [TaskStatus.Completed]));
261
+ dependencies.push({
262
+ namespace: this.#namespace,
263
+ taskId: task.id,
264
+ dependencyTaskId,
265
+ type: TaskDependencyType.Complete,
266
+ requiredStatuses,
267
+ });
268
+ }
269
+ }
270
+ }
271
+ if (dependencies.length > 0) {
272
+ const inserted = await session
273
+ .insert(taskDependencyTable)
274
+ .values(dependencies)
275
+ .onConflictDoNothing()
276
+ .returning();
277
+ if (parentIncrements.size > 0) {
278
+ for (const [parentId, increment] of parentIncrements) {
279
+ // We only increment for those that were actually inserted.
280
+ // Since we use onConflict: 'do-nothing', inserted array only contains actually inserted ones.
281
+ const actualIncrement = inserted.filter((d) => d.taskId == parentId && d.type == TaskDependencyType.Complete).length;
282
+ if (actualIncrement > 0) {
283
+ await session
284
+ .update(taskTable)
285
+ .set({
286
+ unresolvedCompleteDependencies: sql `${taskTable.unresolvedCompleteDependencies} + ${actualIncrement}`,
287
+ })
288
+ .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, parentId)));
289
+ }
290
+ }
218
291
  }
219
292
  }
220
293
  }
221
294
  await newTransaction?.commit();
222
295
  this.#messageBus.publishAndForget();
223
- const anyWaiting = tasks.some((t) => t.status == TaskStatus.Waiting);
224
- if (anyWaiting) {
225
- const tagsToTrigger = distinct(tasks.filter((t) => t.status == TaskStatus.Waiting).flatMap((t) => [...t.completeAfterTags, ...t.scheduleAfterTags]));
226
- await this.triggerTagFanIn(tagsToTrigger, { transaction });
227
- }
228
296
  if (options?.returnTasks == true) {
229
- return tasks;
297
+ const mapped = await this.#repository.mapManyToEntity(tasks);
298
+ return mapped;
230
299
  }
231
300
  return undefined;
232
301
  }
@@ -241,38 +310,55 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
241
310
  }
242
311
  }
243
312
  async has(id, options) {
244
- return await this.#repository.withOptionalTransaction(options?.transaction).hasByQuery({ namespace: this.#namespace, id });
313
+ const session = options?.transaction?.pgTransaction ?? this.#repository.session;
314
+ const [result] = await session
315
+ .select({ id: taskTable.id })
316
+ .from(taskTable)
317
+ .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, id)))
318
+ .limit(1);
319
+ return isDefined(result);
245
320
  }
246
321
  async countByTags(tags, options) {
247
322
  const tagArray = toArray(tags);
248
323
  if (tagArray.length == 0) {
249
324
  return 0;
250
325
  }
251
- const [result] = await this.#repository.withOptionalTransaction(options?.transaction).session
326
+ const session = options?.transaction?.pgTransaction ?? this.#repository.session;
327
+ const [result] = await session
252
328
  .select({ count: count() })
253
329
  .from(taskTable)
254
- .where(and(eq(taskTable.namespace, this.#namespace), arrayOverlaps(taskTable.tags, tagArray)))
255
- .execute();
330
+ .where(and(eq(taskTable.namespace, this.#namespace), arrayOverlaps(taskTable.tags, tagArray)));
256
331
  return result?.count ?? 0;
257
332
  }
258
333
  async getTask(id, options) {
259
- const active = await this.#repository.withOptionalTransaction(options?.transaction).tryLoadByQuery({ namespace: this.#namespace, id });
260
- if (isDefined(active)) {
261
- return active;
334
+ const session = options?.transaction?.pgTransaction ?? this.#repository.session;
335
+ const [activeRow] = await session
336
+ .select()
337
+ .from(taskTable)
338
+ .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, id)));
339
+ if (isDefined(activeRow)) {
340
+ return await this.#repository.mapToEntity(activeRow);
262
341
  }
263
- return await this.#archiveRepository.withOptionalTransaction(options?.transaction).tryLoadByQuery({ namespace: this.#namespace, id });
342
+ const [archiveRow] = await session
343
+ .select()
344
+ .from(taskArchiveTable)
345
+ .where(and(eq(taskArchiveTable.namespace, this.#namespace), eq(taskArchiveTable.id, id)));
346
+ if (isDefined(archiveRow)) {
347
+ return await this.#archiveRepository.mapToEntity(archiveRow);
348
+ }
349
+ return undefined;
264
350
  }
265
351
  async getManyByTags(tags, options) {
266
352
  const tagArray = toArray(tags);
267
353
  if (tagArray.length == 0) {
268
354
  return [];
269
355
  }
270
- const rows = await this.#repository.withOptionalTransaction(options?.transaction).session
356
+ const session = options?.transaction?.pgTransaction ?? this.#repository.session;
357
+ const rows = await session
271
358
  .select()
272
359
  .from(taskTable)
273
- .where(and(eq(taskTable.namespace, this.#namespace), arrayOverlaps(taskTable.tags, tagArray)))
274
- .execute();
275
- return await this.#repository.withOptionalTransaction(options?.transaction).mapManyToEntity(rows);
360
+ .where(and(eq(taskTable.namespace, this.#namespace), arrayOverlaps(taskTable.tags, tagArray)));
361
+ return await this.#repository.mapManyToEntity(rows);
276
362
  }
277
363
  async getTree(rootId, options) {
278
364
  const ids = toArray(rootId);
@@ -280,13 +366,16 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
280
366
  }
281
367
  async getTreeByQuery(query, options) {
282
368
  return await this.#repository.useTransaction(options?.transaction, async (tx) => {
283
- const repositoryWithTransaction = this.#repository.withTransaction(tx);
284
- const roots = await repositoryWithTransaction.loadManyByQuery(query);
369
+ const querySql = this.#repository.convertQuery(query);
370
+ const roots = await tx.pgTransaction
371
+ .select({ id: taskTable.id })
372
+ .from(taskTable)
373
+ .where(querySql);
285
374
  if (roots.length == 0) {
286
375
  return [];
287
376
  }
288
377
  const rootIds = roots.map((t) => t.id);
289
- const rawResult = await repositoryWithTransaction.session.execute(sql `
378
+ const { rows } = await tx.pgTransaction.execute(sql `
290
379
  WITH RECURSIVE task_tree AS (
291
380
  SELECT *, 0 as depth
292
381
  FROM ${taskTable}
@@ -301,8 +390,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
301
390
  )
302
391
  SELECT * FROM task_tree
303
392
  `);
304
- const rows = rawResult.rows;
305
- return await repositoryWithTransaction.mapManyToEntity(rows);
393
+ return await this.#repository.mapManyToEntity(rows);
306
394
  });
307
395
  }
308
396
  async waitForTasks(ids, options) {
@@ -312,7 +400,8 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
312
400
  const timeout = options?.timeout ?? Infinity;
313
401
  const interval = options?.interval ?? 1000;
314
402
  const cancellationSignal = this.#cancellationSignal.optionallyInherit(options?.cancellationSignal);
315
- const continue$ = merge(this.#messageBus.allMessages$.pipe(throttleTime(50, undefined, { leading: true, trailing: true })), cancellationSignal);
403
+ const messageBus$ = this.#messageBus.allMessages$.pipe(throttleTime(500, undefined, { leading: true, trailing: true }));
404
+ const continue$ = merge(messageBus$, cancellationSignal);
316
405
  const timer = Timer.startNew();
317
406
  const finalizedStatuses = [TaskStatus.Completed, TaskStatus.Cancelled, TaskStatus.Dead];
318
407
  while (true) {
@@ -344,13 +433,17 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
344
433
  if (treeIds.length == 0) {
345
434
  return;
346
435
  }
347
- await this.#repository.withTransaction(tx).updateMany(treeIds, {
436
+ await tx.pgTransaction
437
+ .update(taskTable)
438
+ .set({
348
439
  status: TaskStatus.Cancelled,
349
440
  token: null,
350
441
  completeTimestamp: TRANSACTION_TIMESTAMP,
351
- });
352
- const tags = tree.flatMap((t) => t.tags);
353
- await this.triggerTagFanIn(tags, { transaction: tx });
442
+ })
443
+ .where(and(eq(taskTable.namespace, this.#namespace), inArray(taskTable.id, treeIds)));
444
+ for (const task of tree) {
445
+ await this.resolveDependencies(task.id, TaskStatus.Cancelled, { transaction: tx });
446
+ }
354
447
  });
355
448
  }
356
449
  async cancelManyByTags(tags, options) {
@@ -362,17 +455,9 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
362
455
  }
363
456
  async clear(options) {
364
457
  await this.#repository.useTransaction(options?.transaction, async (tx) => {
365
- const repository = this.#repository.withTransaction(tx);
366
- const parentIds = repository.session
367
- .select({ id: taskTable.id })
368
- .from(taskTable)
458
+ await tx.pgTransaction
459
+ .delete(taskTable)
369
460
  .where(eq(taskTable.namespace, this.#namespace));
370
- await repository.session
371
- .update(taskTable)
372
- .set({ parentId: null })
373
- .where(inArray(taskTable.parentId, parentIds))
374
- .execute();
375
- await repository.hardDeleteManyByQuery({ namespace: this.#namespace });
376
461
  });
377
462
  }
378
463
  async dequeue(options) {
@@ -386,23 +471,31 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
386
471
  // 1. Flow Control
387
472
  const forceDequeue = options?.forceDequeue ?? false;
388
473
  let circuitBreakerResult;
474
+ let rateLimitAcquired = 0;
389
475
  if (!forceDequeue) {
390
476
  circuitBreakerResult = await this.#circuitBreaker.check();
391
477
  if (!circuitBreakerResult.allowed) {
392
478
  return [];
393
479
  }
394
- const rateLimitAllowed = await this.#rateLimiter.tryAcquire(this.#namespace, count);
395
- if (!rateLimitAllowed) {
480
+ for (let i = count; i > 0; i--) {
481
+ if (await this.#rateLimiter.tryAcquire(this.#namespace, i)) {
482
+ rateLimitAcquired = i;
483
+ break;
484
+ }
485
+ }
486
+ if (rateLimitAcquired == 0) {
396
487
  return [];
397
488
  }
398
489
  }
399
490
  const tasks = await this.#repository.useTransaction(options?.transaction, async (tx) => {
400
- const repository = this.#repository.withTransaction(tx);
401
- let effectiveCount = count;
491
+ let effectiveCount = forceDequeue ? count : rateLimitAcquired;
402
492
  if (!forceDequeue && circuitBreakerResult?.state == CircuitBreakerState.HalfOpen) {
403
493
  if (circuitBreakerResult.isProbe != true) {
404
- const runningCount = await repository.countByQuery({ namespace: this.#namespace, status: TaskStatus.Running });
405
- if (runningCount > 0) {
494
+ const [runningResult] = await tx.pgTransaction
495
+ .select({ count: sqlCount() })
496
+ .from(taskTable)
497
+ .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Running)));
498
+ if ((runningResult?.count ?? 0) > 0) {
406
499
  return [];
407
500
  }
408
501
  }
@@ -412,7 +505,11 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
412
505
  if (!forceDequeue && isNotNull(this.globalConcurrency)) {
413
506
  // WARN: This is a check-then-act race condition.
414
507
  // A distributed lock or an atomic update strategy is needed for strict enforcement.
415
- const runningCount = await repository.countByQuery({ namespace: this.#namespace, status: TaskStatus.Running });
508
+ const [runningResult] = await tx.pgTransaction
509
+ .select({ count: sqlCount() })
510
+ .from(taskTable)
511
+ .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Running)));
512
+ const runningCount = runningResult?.count ?? 0;
416
513
  if (runningCount >= this.globalConcurrency) {
417
514
  return [];
418
515
  }
@@ -423,7 +520,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
423
520
  * https://stackoverflow.com/questions/73966670/select-for-update-subquery-not-respecting-limit-clause-under-load
424
521
  * https://dba.stackexchange.com/questions/69471/postgres-update-limit-1
425
522
  */
426
- const selection = repository.session.$with('selection').as((qb) => qb
523
+ const selection = tx.pgTransaction.$with('selection').as((qb) => qb
427
524
  .select({ id: taskTable.id })
428
525
  .from(taskTable)
429
526
  .where(and(eq(taskTable.namespace, this.#namespace), lte(taskTable.scheduleTimestamp, TRANSACTION_TIMESTAMP), eq(taskTable.status, TaskStatus.Pending), or(sqlIsNull(taskTable.timeToLive), lt(TRANSACTION_TIMESTAMP, taskTable.timeToLive)), isDefined(options?.types) ? inArray(taskTable.type, options.types) : undefined, sql `pg_sleep(0) IS NOT NULL` // Materialization hack until drizzle implements https://github.com/drizzle-team/drizzle-orm/issues/2318
@@ -431,7 +528,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
431
528
  .orderBy(asc(taskTable.priority), asc(taskTable.scheduleTimestamp))
432
529
  .limit(effectiveCount)
433
530
  .for('update', { skipLocked: true }));
434
- const rows = await repository.session
531
+ const rows = await tx.pgTransaction
435
532
  .with(selection)
436
533
  .update(taskTable)
437
534
  .set({
@@ -443,13 +540,13 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
443
540
  // If it was RUNNING (Zombie), previous try failed -> increment tries.
444
541
  tries: sql `${taskTable.tries} + 1`,
445
542
  })
446
- .where(inArray(taskTable.id, repository.session.select().from(selection)))
543
+ .where(inArray(taskTable.id, tx.pgTransaction.select().from(selection)))
447
544
  .returning();
448
545
  return await this.#repository.mapManyToEntity(rows);
449
546
  });
450
547
  // 3. Compensation
451
- if (!forceDequeue && (tasks.length < count)) {
452
- const shortfall = count - tasks.length;
548
+ if (!forceDequeue && (tasks.length < rateLimitAcquired)) {
549
+ const shortfall = rateLimitAcquired - tasks.length;
453
550
  await this.#rateLimiter.refund(this.#namespace, shortfall);
454
551
  }
455
552
  return tasks;
@@ -458,19 +555,26 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
458
555
  await this.rescheduleMany([id], timestamp, options);
459
556
  }
460
557
  async rescheduleMany(ids, timestamp, options) {
461
- await this.#repository.withOptionalTransaction(options?.transaction).updateMany(ids, {
558
+ const session = options?.transaction?.pgTransaction ?? this.#repository.session;
559
+ await session
560
+ .update(taskTable)
561
+ .set({
462
562
  status: TaskStatus.Pending,
463
563
  token: null,
464
564
  scheduleTimestamp: timestamp,
465
565
  visibilityDeadline: null,
466
566
  tries: sql `CASE
467
- WHEN ${taskTable.status} = ${TaskStatus.Running} THEN GREATEST(0, ${taskTable.tries} - 1)
468
- ELSE ${taskTable.tries}
469
- END`,
470
- });
567
+ WHEN ${taskTable.status} = ${TaskStatus.Running} THEN GREATEST(0, ${taskTable.tries} - 1)
568
+ ELSE ${taskTable.tries}
569
+ END`,
570
+ })
571
+ .where(and(eq(taskTable.namespace, this.#namespace), inArray(taskTable.id, ids)));
471
572
  }
472
573
  async rescheduleManyByTags(tags, timestamp, options) {
473
- await this.#repository.withOptionalTransaction(options?.transaction).updateManyByQuery(and(eq(taskTable.namespace, this.#namespace), arrayOverlaps(taskTable.tags, toArray(tags))), {
574
+ const session = options?.transaction?.pgTransaction ?? this.#repository.session;
575
+ await session
576
+ .update(taskTable)
577
+ .set({
474
578
  status: TaskStatus.Pending,
475
579
  token: null,
476
580
  scheduleTimestamp: timestamp,
@@ -479,14 +583,14 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
479
583
  WHEN ${taskTable.status} = ${TaskStatus.Running} THEN GREATEST(0, ${taskTable.tries} - 1)
480
584
  ELSE ${taskTable.tries}
481
585
  END`,
482
- });
586
+ })
587
+ .where(and(eq(taskTable.namespace, this.#namespace), arrayOverlaps(taskTable.tags, toArray(tags))));
483
588
  }
484
589
  async touch(task, options) {
485
590
  if (isNull(task.token)) {
486
591
  return undefined;
487
592
  }
488
593
  return await this.#repository.useTransaction(options?.transaction, async (tx) => {
489
- const repository = this.#repository.withTransaction(tx);
490
594
  const update = {
491
595
  visibilityDeadline: sql `${TRANSACTION_TIMESTAMP} + ${interval(this.visibilityTimeout, 'milliseconds')}`,
492
596
  };
@@ -496,34 +600,35 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
496
600
  if (isDefined(options?.state)) {
497
601
  update.state = options.state;
498
602
  }
499
- const result = await repository.tryUpdateByQuery({
500
- namespace: this.#namespace,
501
- id: task.id,
502
- status: TaskStatus.Running,
503
- token: task.token,
504
- startTimestamp: { $gt: sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}` },
505
- }, update);
506
- // TODO: reduce required DB roundtrips
507
- if (isUndefined(result)) {
508
- const existing = await repository.tryLoadByQuery({ id: task.id, namespace: this.#namespace, status: TaskStatus.Running, token: task.token });
509
- if (isDefined(existing) && isNotNull(existing.startTimestamp) && (currentTimestamp() - existing.startTimestamp) > this.maxExecutionTime) {
510
- await this.fail(task, { message: 'Hard Execution Timeout' }, { fatal: true, transaction: tx });
511
- }
603
+ const [updatedRow] = await tx.pgTransaction
604
+ .update(taskTable)
605
+ .set(update)
606
+ .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, task.id), eq(taskTable.status, TaskStatus.Running), isNull(task.token) ? sqlIsNull(taskTable.token) : eq(taskTable.token, task.token), gt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`)))
607
+ .returning();
608
+ if (isDefined(updatedRow)) {
609
+ return await this.#repository.mapToEntity(updatedRow);
512
610
  }
513
- return result;
611
+ const [existingRow] = await tx.pgTransaction
612
+ .select({ startTimestamp: taskTable.startTimestamp })
613
+ .from(taskTable)
614
+ .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, task.id), eq(taskTable.status, TaskStatus.Running), isNull(task.token) ? sqlIsNull(taskTable.token) : eq(taskTable.token, task.token)));
615
+ if (isDefined(existingRow) && isNotNull(existingRow.startTimestamp) && (currentTimestamp() - existingRow.startTimestamp) > this.maxExecutionTime) {
616
+ await this.fail(task, { message: 'Hard Execution Timeout' }, { fatal: true, transaction: tx });
617
+ }
618
+ return undefined;
514
619
  });
515
620
  }
516
621
  async touchMany(tasks, options) {
517
622
  if (tasks.length == 0) {
518
623
  return [];
519
624
  }
520
- const repository = this.#repository.withOptionalTransaction(options?.transaction);
625
+ const session = options?.transaction?.pgTransaction ?? this.#repository.session;
521
626
  const rows = tasks.map((t, i) => {
522
627
  const progress = options?.progresses?.[i] ?? null;
523
628
  const state = options?.states?.[i] ?? null;
524
629
  return sql `(${t.id}::uuid, ${t.token}::uuid, ${progress}::numeric, ${state}::jsonb)`;
525
630
  });
526
- const updates = repository.session.$with('updates').as((qb) => qb
631
+ const updates = session.$with('updates').as((qb) => qb
527
632
  .select({
528
633
  updateId: sql `(id)::uuid`.as('update_id'),
529
634
  updateToken: sql `(token)::uuid`.as('update_token'),
@@ -531,7 +636,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
531
636
  updateState: sql `(state)::jsonb`.as('update_state'),
532
637
  })
533
638
  .from(sql `(VALUES ${sql.join(rows, sql `, `)}) AS t(id, token, progress, state)`));
534
- const updated = repository.session.$with('updated').as(() => repository.session
639
+ const updated = session.$with('updated').as(() => session
535
640
  .update(taskTable)
536
641
  .set({
537
642
  visibilityDeadline: sql `${TRANSACTION_TIMESTAMP} + ${interval(this.visibilityTimeout, 'milliseconds')}`,
@@ -541,32 +646,42 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
541
646
  .from(updates)
542
647
  .where(and(eq(taskTable.id, updates.updateId), eq(taskTable.namespace, this.#namespace), eq(taskTable.token, updates.updateToken), eq(taskTable.status, TaskStatus.Running), gt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`)))
543
648
  .returning({ id: taskTable.id }));
544
- const result = await repository.session
649
+ const result = await session
545
650
  .with(updates, updated)
546
651
  .select({ id: updated.id })
547
- .from(updated)
548
- .execute();
652
+ .from(updated);
549
653
  return result.map((r) => r.id);
550
654
  }
551
655
  async complete(task, options) {
552
656
  await this.#repository.useTransaction(options?.transaction, async (tx) => {
553
- const repository = this.#repository.withTransaction(tx);
554
- const updatedTask = await repository.tryUpdateByQuery({
555
- id: task.id,
556
- token: task.token,
557
- }, {
558
- status: TaskStatus.Completed,
657
+ const [freshTask] = await tx.pgTransaction
658
+ .select({ unresolvedCompleteDependencies: taskTable.unresolvedCompleteDependencies })
659
+ .from(taskTable)
660
+ .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, task.id)))
661
+ .for('update');
662
+ if (isUndefined(freshTask)) {
663
+ return;
664
+ }
665
+ const hasActiveChildren = freshTask.unresolvedCompleteDependencies > 0;
666
+ const nextStatus = hasActiveChildren ? TaskStatus.WaitingChildren : TaskStatus.Completed;
667
+ const [updatedTask] = await tx.pgTransaction.update(taskTable)
668
+ .set({
669
+ status: nextStatus,
559
670
  token: null,
560
671
  result: options?.result,
561
- progress: 1,
562
- completeTimestamp: TRANSACTION_TIMESTAMP,
672
+ progress: hasActiveChildren ? task.progress : 1,
673
+ completeTimestamp: (nextStatus == TaskStatus.Completed) ? TRANSACTION_TIMESTAMP : null,
563
674
  visibilityDeadline: null,
564
- });
675
+ })
676
+ .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, task.id), isNull(task.token) ? sqlIsNull(taskTable.token) : eq(taskTable.token, task.token)))
677
+ .returning({ id: taskTable.id });
565
678
  if (isUndefined(updatedTask)) {
566
679
  return;
567
680
  }
568
- await this.#circuitBreaker.recordSuccess();
569
- await this.triggerTagFanIn(task.tags, { transaction: tx });
681
+ if (nextStatus == TaskStatus.Completed) {
682
+ await this.#circuitBreaker.recordSuccess();
683
+ }
684
+ await this.resolveDependencies(task.id, nextStatus, { transaction: tx });
570
685
  });
571
686
  }
572
687
  async completeMany(tasks, options) {
@@ -574,40 +689,55 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
574
689
  return;
575
690
  }
576
691
  await this.#repository.useTransaction(options?.transaction, async (tx) => {
577
- const repository = this.#repository.withTransaction(tx);
692
+ const taskIds = getEntityIds(tasks);
693
+ const freshTasks = await tx.pgTransaction
694
+ .select({ id: taskTable.id, unresolvedCompleteDependencies: taskTable.unresolvedCompleteDependencies })
695
+ .from(taskTable)
696
+ .where(and(eq(taskTable.namespace, this.#namespace), inArray(taskTable.id, taskIds)))
697
+ .for('update');
698
+ const unresolvedDependencyCountMap = new Map(freshTasks.map((task) => [task.id, task.unresolvedCompleteDependencies]));
578
699
  const rows = tasks.map((t, i) => {
579
700
  const result = options?.results?.[i] ?? null;
580
- return sql `(${t.id}::uuid, ${t.token}::uuid, ${result}::jsonb)`;
701
+ const unresolvedCount = unresolvedDependencyCountMap.get(t.id) ?? 0;
702
+ const nextStatus = (unresolvedCount > 0) ? TaskStatus.WaitingChildren : TaskStatus.Completed;
703
+ const progress = (nextStatus == TaskStatus.Completed) ? 1 : t.progress;
704
+ const completeTimestamp = (nextStatus == TaskStatus.Completed) ? TRANSACTION_TIMESTAMP : null;
705
+ return sql `(${t.id}::uuid, ${t.token}::uuid, ${nextStatus}::text, ${result}::jsonb, ${progress}::numeric, ${completeTimestamp}::timestamptz)`;
581
706
  });
582
- const updates = repository.session.$with('updates').as((qb) => qb
707
+ const updates = tx.pgTransaction.$with('updates').as((qb) => qb
583
708
  .select({
584
709
  updateId: sql `(id)::uuid`.as('update_id'),
585
710
  updateToken: sql `(token)::uuid`.as('update_token'),
711
+ updateStatus: sql `(status)::text`.as('update_status'),
586
712
  updateResult: sql `(result)::jsonb`.as('update_result'),
713
+ updateProgress: sql `(progress)::numeric`.as('update_progress'),
714
+ updateComplete: sql `(complete_timestamp)::timestamptz`.as('update_complete'),
587
715
  })
588
- .from(sql `(VALUES ${sql.join(rows, sql `, `)}) AS t(id, token, result)`));
589
- const updated = repository.session.$with('updated').as(() => repository.session
716
+ .from(sql `(VALUES ${sql.join(rows, sql `, `)}) AS t(id, token, status, result, progress, complete_timestamp)`));
717
+ const updated = tx.pgTransaction.$with('updated').as(() => tx.pgTransaction
590
718
  .update(taskTable)
591
719
  .set({
592
- status: TaskStatus.Completed,
720
+ status: sql `${updates.updateStatus}::${taskStatus}`,
593
721
  token: null,
594
722
  result: updates.updateResult,
595
- progress: 1,
596
- completeTimestamp: TRANSACTION_TIMESTAMP,
723
+ progress: sql `${updates.updateProgress}`,
724
+ completeTimestamp: sql `${updates.updateComplete}`,
597
725
  visibilityDeadline: null,
598
726
  })
599
727
  .from(updates)
600
728
  .where(and(eq(taskTable.id, updates.updateId), eq(taskTable.token, updates.updateToken), eq(taskTable.namespace, this.#namespace)))
601
- .returning({ id: taskTable.id, tags: taskTable.tags }));
602
- const updatedRows = await repository.session
729
+ .returning({ id: taskTable.id, status: taskTable.status }));
730
+ const updatedRows = await tx.pgTransaction
603
731
  .with(updates, updated)
604
- .select({ id: updated.id, tags: updated.tags })
605
- .from(updated)
606
- .execute();
732
+ .select({ id: updated.id, status: updated.status })
733
+ .from(updated);
607
734
  if (updatedRows.length > 0) {
608
- await this.#circuitBreaker.recordSuccess();
609
- const tags = updatedRows.flatMap((t) => t.tags);
610
- await this.triggerTagFanIn(tags, { transaction: tx });
735
+ if (updatedRows.some((r) => r.status == TaskStatus.Completed)) {
736
+ await this.#circuitBreaker.recordSuccess();
737
+ }
738
+ for (const row of updatedRows) {
739
+ await this.resolveDependencies(row.id, row.status, { transaction: tx });
740
+ }
611
741
  }
612
742
  });
613
743
  }
@@ -619,12 +749,9 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
619
749
  : 0;
620
750
  const nextSchedule = currentTimestamp() + delay;
621
751
  await this.#repository.useTransaction(options?.transaction, async (tx) => {
622
- const updatedTask = await this.#repository.withTransaction(tx).tryUpdateByQuery({
623
- namespace: this.#namespace,
624
- id: task.id,
625
- token: task.token,
626
- tries: task.tries,
627
- }, {
752
+ const [updatedRow] = await tx.pgTransaction
753
+ .update(taskTable)
754
+ .set({
628
755
  status: nextStatus,
629
756
  token: null,
630
757
  error: serializeError(error),
@@ -632,28 +759,14 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
632
759
  scheduleTimestamp: nextSchedule,
633
760
  startTimestamp: null,
634
761
  completeTimestamp: (nextStatus == TaskStatus.Dead) ? TRANSACTION_TIMESTAMP : null,
635
- });
636
- if (isUndefined(updatedTask)) {
762
+ })
763
+ .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, task.id), isNull(task.token) ? sqlIsNull(taskTable.token) : eq(taskTable.token, task.token), eq(taskTable.tries, task.tries)))
764
+ .returning();
765
+ if (isUndefined(updatedRow)) {
637
766
  return;
638
767
  }
639
768
  await this.#circuitBreaker.recordFailure();
640
- if (nextStatus == TaskStatus.Dead) {
641
- await this.triggerTagFanIn(task.tags, { transaction: tx });
642
- }
643
- });
644
- }
645
- async triggerTagFanIn(tags, options) {
646
- if (tags.length == 0) {
647
- return;
648
- }
649
- const distinctSortedTags = distinct(tags).toSorted();
650
- const hash = await digest('SHA-256', distinctSortedTags.join(',')).toHex();
651
- const idempotencyKey = `Sys:FanIn:${hash}`;
652
- await this.#internalThis.enqueue('[SystemWorker]:FanIn', { targetTags: distinctSortedTags }, {
653
- priority: 0,
654
- idempotencyKey,
655
- replace: true,
656
- transaction: options?.transaction,
769
+ await this.resolveDependencies(task.id, nextStatus, { transaction: tx });
657
770
  });
658
771
  }
659
772
  async failMany(tasks, errors, options) {
@@ -661,7 +774,6 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
661
774
  return;
662
775
  }
663
776
  await this.#repository.useTransaction(options?.transaction, async (tx) => {
664
- const repository = this.#repository.withTransaction(tx);
665
777
  const rows = tasks.map((task, index) => {
666
778
  const error = errors[index];
667
779
  const isRetryable = (task.tries < this.maxTries);
@@ -673,7 +785,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
673
785
  const completeTimestamp = (nextStatus == TaskStatus.Dead) ? new Date() : null;
674
786
  return sql `(${task.id}::uuid, ${task.token}::uuid, ${task.tries}::int, ${nextStatus}::text, ${serializeError(error)}::jsonb, ${nextSchedule}::timestamptz, ${completeTimestamp}::timestamptz)`;
675
787
  });
676
- const updates = repository.session.$with('updates').as((qb) => qb
788
+ const updates = tx.pgTransaction.$with('updates').as((qb) => qb
677
789
  .select({
678
790
  updateId: sql `(id)::uuid`.as('update_id'),
679
791
  updateToken: sql `(token)::uuid`.as('update_token'),
@@ -684,7 +796,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
684
796
  updateComplete: sql `(complete_timestamp)::timestamptz`.as('update_complete'),
685
797
  })
686
798
  .from(sql `(VALUES ${sql.join(rows, sql `, `)}) AS t(id, token, tries, status, error, schedule_timestamp, complete_timestamp)`));
687
- const updated = repository.session.$with('updated').as(() => repository.session
799
+ const updated = tx.pgTransaction.$with('updated').as(() => tx.pgTransaction
688
800
  .update(taskTable)
689
801
  .set({
690
802
  status: sql `${updates.updateStatus}::${taskStatus}`,
@@ -697,136 +809,130 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
697
809
  })
698
810
  .from(updates)
699
811
  .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, updates.updateId), eq(taskTable.token, updates.updateToken), eq(taskTable.tries, updates.updateTries)))
700
- .returning({ id: taskTable.id, status: taskTable.status, tags: taskTable.tags }));
701
- const result = await repository.session
812
+ .returning({ id: taskTable.id, status: taskTable.status }));
813
+ const result = await tx.pgTransaction
702
814
  .with(updates, updated)
703
- .select()
704
- .from(updated)
705
- .execute();
815
+ .select({ id: updated.id, status: updated.status })
816
+ .from(updated);
706
817
  if (result.length > 0) {
707
818
  await Promise.all(createArray(result.length, async () => await this.#circuitBreaker.recordFailure()));
708
- const deadTags = result
709
- .filter((r) => r.status == TaskStatus.Dead)
710
- .flatMap((r) => r.tags);
711
- await this.triggerTagFanIn(deadTags, { transaction: tx });
819
+ for (const row of result) {
820
+ await this.resolveDependencies(row.id, row.status, { transaction: tx });
821
+ }
712
822
  }
713
823
  });
714
824
  }
715
- async resolveDependencies(targetTags) {
716
- await this.#repository.transaction(async (tx) => {
717
- const repository = this.#repository.withTransaction(tx);
718
- // Identify Dependents
719
- const waiters = await repository.session
720
- .select()
721
- .from(taskTable)
722
- .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Waiting), or(arrayOverlaps(taskTable.completeAfterTags, targetTags), arrayOverlaps(taskTable.scheduleAfterTags, targetTags))))
723
- .for('update', { skipLocked: true })
724
- .execute();
725
- const entities = await repository.mapManyToEntity(waiters);
726
- if (entities.length == 0) {
727
- return;
728
- }
729
- const allRequiredTags = distinct(entities.flatMap((t) => [...t.completeAfterTags, ...t.scheduleAfterTags]));
730
- const statsRows = await repository.session
825
+ async resolveDependencies(id, status, options) {
826
+ this.#logger.debug(`Resolving dependencies for task ${id} with status ${status}`);
827
+ await this.#repository.useTransaction(options?.transaction, async (tx) => {
828
+ // Find all dependents of the task that just finished
829
+ const dependents = await tx.pgTransaction
731
830
  .select({
732
- tag: unnest(taskTable.tags).as('tag'),
733
- status: taskTable.status,
734
- count: count().as('count'),
831
+ taskId: taskDependencyTable.taskId,
832
+ type: taskDependencyTable.type,
833
+ requiredStatuses: taskDependencyTable.requiredStatuses,
735
834
  })
736
- .from(taskTable)
737
- .where(and(eq(taskTable.namespace, this.#namespace), arrayOverlaps(taskTable.tags, allRequiredTags)))
738
- .groupBy(unnest(taskTable.tags), taskTable.status);
739
- const tagStats = new Map();
740
- for (const row of statsRows) {
741
- if (!tagStats.has(row.tag)) {
742
- tagStats.set(row.tag, new Map());
743
- }
744
- tagStats.get(row.tag).set(row.status, row.count);
835
+ .from(taskDependencyTable)
836
+ .where(and(eq(taskDependencyTable.namespace, this.#namespace), eq(taskDependencyTable.dependencyTaskId, id)));
837
+ if (dependents.length == 0) {
838
+ return;
745
839
  }
746
- const getCount = (tags, Statuses) => {
747
- let sum = 0;
748
- for (const tag of tags) {
749
- const stats = tagStats.get(tag);
750
- if (isDefined(stats)) {
751
- for (const status of Statuses) {
752
- sum += stats.get(status) ?? 0;
840
+ for (const dep of dependents) {
841
+ const isMatched = dep.requiredStatuses.includes(status);
842
+ const isTerminal = (status == TaskStatus.Completed || status == TaskStatus.Cancelled || status == TaskStatus.Dead);
843
+ if (isMatched || isTerminal) {
844
+ // Dependency resolved (either matched or finished in a different terminal status)
845
+ const deletedEdge = await tx.pgTransaction
846
+ .delete(taskDependencyTable)
847
+ .where(and(eq(taskDependencyTable.namespace, this.#namespace), eq(taskDependencyTable.taskId, dep.taskId), eq(taskDependencyTable.dependencyTaskId, id), eq(taskDependencyTable.type, dep.type)))
848
+ .returning();
849
+ if (deletedEdge.length > 0) {
850
+ const [dependentTask] = await tx.pgTransaction
851
+ .select({ failFast: taskTable.failFast, status: taskTable.status })
852
+ .from(taskTable)
853
+ .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, dep.taskId)));
854
+ if (isDefined(dependentTask)) {
855
+ if (!isMatched && dependentTask.failFast && dependentTask.status != TaskStatus.Dead && dependentTask.status != TaskStatus.Cancelled) {
856
+ // Dependency failed and failFast is enabled: fail the dependent task immediately
857
+ await this.transitionTask(dep.taskId, TaskStatus.Dead, { error: { code: 'DependencyFailed', message: `Dependency ${id} failed (status: ${status}). Task failed fast because it required one of: ${dep.requiredStatuses.join(', ')}` }, transaction: tx });
858
+ }
859
+ else {
860
+ // Dependency resolved (matched, or failed but no failFast): decrement counter
861
+ const [updatedTask] = await tx.pgTransaction
862
+ .update(taskTable)
863
+ .set((dep.type == TaskDependencyType.Schedule)
864
+ ? { unresolvedScheduleDependencies: sql `${taskTable.unresolvedScheduleDependencies} - 1` }
865
+ : { unresolvedCompleteDependencies: sql `${taskTable.unresolvedCompleteDependencies} - 1` })
866
+ .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, dep.taskId), gt((dep.type == TaskDependencyType.Schedule) ? taskTable.unresolvedScheduleDependencies : taskTable.unresolvedCompleteDependencies, 0)))
867
+ .returning();
868
+ if (isDefined(updatedTask)) {
869
+ // Check for transitions
870
+ if (updatedTask.unresolvedScheduleDependencies == 0 || updatedTask.unresolvedCompleteDependencies == 0) {
871
+ await this.evaluateTaskStatus(updatedTask.id, { transaction: tx });
872
+ }
873
+ }
874
+ }
753
875
  }
754
876
  }
755
877
  }
756
- return sum;
757
- };
758
- const idsToFail = [];
759
- const idsToSchedule = [];
760
- const idsToComplete = [];
761
- const fanInTags = [];
762
- for (const waiter of entities) {
763
- const requiredTags = distinct([...waiter.completeAfterTags, ...waiter.scheduleAfterTags]);
764
- let isReady = false;
765
- let shouldFail = false;
766
- // 1. Check Fail Fast
767
- if (waiter.failFast) {
768
- if (getCount(requiredTags, [TaskStatus.Dead, TaskStatus.Cancelled]) > 0) {
769
- shouldFail = true;
770
- }
771
- }
772
- // 2. Check Trigger Conditions
773
- if (!shouldFail) {
774
- if (waiter.dependencyJoinMode == DependencyJoinMode.Or) {
775
- if (getCount(requiredTags, waiter.dependencyTriggerStatuses) > 0) {
776
- isReady = true;
777
- }
778
- }
779
- else if (waiter.dependencyJoinMode == DependencyJoinMode.And) {
780
- isReady = requiredTags.every((tag) => {
781
- const stats = tagStats.get(tag);
782
- if (isUndefined(stats)) {
783
- return false;
784
- }
785
- const activeCount = (stats.get(TaskStatus.Pending) ?? 0)
786
- + (stats.get(TaskStatus.Running) ?? 0)
787
- + (stats.get(TaskStatus.Waiting) ?? 0);
788
- return activeCount == 0;
789
- });
878
+ }
879
+ });
880
+ }
881
+ async evaluateTaskStatus(id, options) {
882
+ await this.#repository.useTransaction(options?.transaction, async (tx) => {
883
+ const [rawTask] = await tx.pgTransaction
884
+ .select()
885
+ .from(taskTable)
886
+ .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, id)))
887
+ .for('update');
888
+ if (isUndefined(rawTask)) {
889
+ return;
890
+ }
891
+ const task = await this.#repository.mapToEntity(rawTask);
892
+ if (task.status != TaskStatus.Waiting && task.status != TaskStatus.WaitingChildren && task.status != TaskStatus.Pending) {
893
+ return;
894
+ }
895
+ if (task.unresolvedScheduleDependencies == 0) {
896
+ if (task.unresolvedCompleteDependencies == 0) {
897
+ // If it hasn't started yet (startTimestamp is null), it should be Pending.
898
+ // If it has already run (startTimestamp is set), it should be Completed.
899
+ const nextStatus = isNull(task.startTimestamp) ? TaskStatus.Pending : TaskStatus.Completed;
900
+ if (task.status != nextStatus) {
901
+ await this.transitionTask(id, nextStatus, { transaction: tx });
790
902
  }
791
903
  }
792
- // 3. Transition
793
- if (shouldFail) {
794
- idsToFail.push(waiter.id);
904
+ else if (task.status == TaskStatus.Waiting && isNull(task.startTimestamp)) {
905
+ await this.transitionTask(id, TaskStatus.Pending, { transaction: tx });
795
906
  }
796
- else if (isReady) {
797
- if (waiter.scheduleAfterTags.length > 0) {
798
- idsToSchedule.push(waiter.id);
799
- fanInTags.push(...waiter.tags);
800
- }
801
- else if (waiter.completeAfterTags.length > 0) {
802
- idsToComplete.push(waiter.id);
803
- fanInTags.push(...waiter.tags);
804
- }
907
+ else if (task.status == TaskStatus.Pending && isNotNull(task.startTimestamp)) {
908
+ // This case happens if a task was somehow Pending but already has a startTimestamp and now its completion dependencies are resolved
909
+ await this.transitionTask(id, TaskStatus.WaitingChildren, { transaction: tx });
805
910
  }
806
911
  }
807
- if (idsToFail.length > 0) {
808
- const rows = await repository.updateMany(idsToFail, {
809
- status: TaskStatus.Dead,
810
- error: { code: 'DependencyFailed' },
811
- completeTimestamp: TRANSACTION_TIMESTAMP,
812
- });
813
- const tags = rows.flatMap((r) => r.tags);
814
- await this.triggerTagFanIn(tags, { transaction: tx });
912
+ });
913
+ }
914
+ async transitionTask(id, status, options) {
915
+ this.#logger.debug(`Transitioning task ${id} to ${status}`);
916
+ await this.#repository.useTransaction(options?.transaction, async (tx) => {
917
+ const update = {
918
+ status,
919
+ completeTimestamp: ((status == TaskStatus.Completed) || (status == TaskStatus.Cancelled) || (status == TaskStatus.Dead)) ? TRANSACTION_TIMESTAMP : null,
920
+ };
921
+ if (isNotNullOrUndefined(options?.error)) {
922
+ update.error = options.error;
815
923
  }
816
- if (idsToSchedule.length > 0) {
817
- await repository.updateMany(idsToSchedule, {
818
- status: TaskStatus.Pending,
819
- scheduleTimestamp: TRANSACTION_TIMESTAMP,
820
- });
924
+ if (status == TaskStatus.Completed) {
925
+ update.progress = 1;
926
+ update.token = null;
821
927
  }
822
- if (idsToComplete.length > 0) {
823
- await repository.updateMany(idsToComplete, {
824
- status: TaskStatus.Completed,
825
- completeTimestamp: TRANSACTION_TIMESTAMP,
826
- progress: 1,
827
- token: null,
828
- });
829
- await this.triggerTagFanIn(fanInTags, { transaction: tx });
928
+ const [updatedRow] = await tx.pgTransaction
929
+ .update(taskTable)
930
+ .set(update)
931
+ .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, id)))
932
+ .returning();
933
+ if (isDefined(updatedRow)) {
934
+ await this.resolveDependencies(id, status, { transaction: tx });
935
+ this.#messageBus.publishAndForget();
830
936
  }
831
937
  });
832
938
  }
@@ -836,26 +942,22 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
836
942
  // 1. Archival: Move old terminal tasks to archive
837
943
  while (true) {
838
944
  const archivedCount = await repository.useTransaction(options?.transaction, async (tx) => {
839
- const repositoryWithTx = repository.withTransaction(tx);
840
- const archiveRepositoryWithTx = archiveRepository.withTransaction(tx);
841
- const tasksToArchive = await repositoryWithTx.loadManyByQuery({
842
- $and: [
843
- {
844
- namespace: this.#namespace,
845
- status: { $in: [TaskStatus.Completed, TaskStatus.Dead, TaskStatus.Cancelled] },
846
- completeTimestamp: { $lt: sql `${TRANSACTION_TIMESTAMP} - ${interval(this.retention, 'milliseconds')}` },
847
- },
848
- notInArray(taskTable.id, repositoryWithTx.session
849
- .select({ parentId: taskTable.parentId })
850
- .from(taskTable)
851
- .where(and(eq(taskTable.namespace, this.#namespace), sql `${taskTable.parentId} IS NOT NULL`))),
852
- ],
853
- }, { limit: 1000, for: { mode: 'update', skipLocked: true } });
854
- if (tasksToArchive.length > 0) {
855
- await archiveRepositoryWithTx.insertMany(tasksToArchive);
856
- await repositoryWithTx.hardDeleteMany(tasksToArchive.map((t) => t.id));
945
+ const childTaskTable = aliasedTable(taskTable, 'childTask');
946
+ const rowsToArchive = await tx.pgTransaction
947
+ .select()
948
+ .from(taskTable)
949
+ .where(and(eq(taskTable.namespace, this.#namespace), inArray(taskTable.status, [TaskStatus.Completed, TaskStatus.Dead, TaskStatus.Cancelled]), lte(taskTable.completeTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.retention, 'milliseconds')}`), notExists(tx.pgTransaction
950
+ .select({ id: childTaskTable.id })
951
+ .from(childTaskTable)
952
+ .where(and(eq(childTaskTable.namespace, this.#namespace), eq(childTaskTable.parentId, taskTable.id))))))
953
+ .limit(1000)
954
+ .for('update', { skipLocked: true });
955
+ if (rowsToArchive.length > 0) {
956
+ const rowsToArchiveIds = getEntityIds(rowsToArchive);
957
+ await tx.pgTransaction.insert(taskArchiveTable).values(rowsToArchive);
958
+ await tx.pgTransaction.delete(taskTable).where(inArray(taskTable.id, rowsToArchiveIds));
857
959
  }
858
- return tasksToArchive.length;
960
+ return rowsToArchive.length;
859
961
  });
860
962
  if (archivedCount < 1000) {
861
963
  break;
@@ -864,16 +966,15 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
864
966
  // 2. Purge Archive: Remove very old archived tasks
865
967
  while (true) {
866
968
  const deletedArchiveCount = await archiveRepository.useTransaction(options?.transaction, async (tx) => {
867
- const repositoryWithTx = archiveRepository.withTransaction(tx);
868
- const selection = repositoryWithTx.session.$with('archive_purge_selection').as((qb) => qb
969
+ const selection = tx.pgTransaction.$with('archive_purge_selection').as((qb) => qb
869
970
  .select({ id: taskArchiveTable.id })
870
971
  .from(taskArchiveTable)
871
- .where(and(eq(taskArchiveTable.namespace, this.#namespace), lt(taskArchiveTable.completeTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.archiveRetention, 'milliseconds')}`)))
972
+ .where(and(eq(taskArchiveTable.namespace, this.#namespace), lte(taskArchiveTable.completeTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.archiveRetention, 'milliseconds')}`)))
872
973
  .limit(1000));
873
- const result = await repositoryWithTx.session
974
+ const result = await tx.pgTransaction
874
975
  .with(selection)
875
976
  .delete(taskArchiveTable)
876
- .where(inArray(taskArchiveTable.id, repositoryWithTx.session.select().from(selection)))
977
+ .where(inArray(taskArchiveTable.id, tx.pgTransaction.select().from(selection)))
877
978
  .returning({ id: taskArchiveTable.id });
878
979
  return result.length;
879
980
  });
@@ -884,16 +985,15 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
884
985
  // 3. Maintenance Loop
885
986
  while (true) {
886
987
  const maintenanceCount = await repository.useTransaction(options?.transaction, async (tx) => {
887
- const repositoryWithTx = repository.withTransaction(tx);
888
988
  let totalUpdated = 0;
889
989
  // 3.1 Handle Pending Expiration
890
- const expiredSelection = repositoryWithTx.session.$with('expired_selection').as((qb) => qb
990
+ const expiredSelection = tx.pgTransaction.$with('expired_selection').as((qb) => qb
891
991
  .select({ id: taskTable.id })
892
992
  .from(taskTable)
893
993
  .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Pending), lt(taskTable.timeToLive, TRANSACTION_TIMESTAMP)))
894
994
  .limit(1000)
895
995
  .for('update', { skipLocked: true }));
896
- const expiredRows = await repositoryWithTx.session
996
+ const expiredRows = await tx.pgTransaction
897
997
  .with(expiredSelection)
898
998
  .update(taskTable)
899
999
  .set({
@@ -902,40 +1002,43 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
902
1002
  error: { code: 'Expired', message: 'Task expired before processing' },
903
1003
  completeTimestamp: TRANSACTION_TIMESTAMP,
904
1004
  })
905
- .where(inArray(taskTable.id, repositoryWithTx.session.select().from(expiredSelection)))
906
- .returning({ tags: taskTable.tags });
1005
+ .where(inArray(taskTable.id, tx.pgTransaction.select().from(expiredSelection)))
1006
+ .returning({ id: taskTable.id });
907
1007
  totalUpdated += expiredRows.length;
908
1008
  if (expiredRows.length > 0) {
909
- await this.triggerTagFanIn(expiredRows.flatMap((r) => r.tags), { transaction: tx });
1009
+ for (const row of expiredRows) {
1010
+ await this.resolveDependencies(row.id, TaskStatus.Dead, { transaction: tx });
1011
+ }
910
1012
  }
911
1013
  // 3.2 Handle Zombie Tasks (Retry)
912
- const zombieRetrySelection = repositoryWithTx.session.$with('zombie_retry_selection').as((qb) => qb
1014
+ const zombieRetrySelection = tx.pgTransaction.$with('zombie_retry_selection').as((qb) => qb
913
1015
  .select({ id: taskTable.id })
914
1016
  .from(taskTable)
915
1017
  .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Running), lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP), lt(taskTable.tries, this.maxTries)))
916
1018
  .limit(1000)
917
1019
  .for('update', { skipLocked: true }));
918
- const zombieRetryRows = await repositoryWithTx.session
1020
+ const zombieRetryRows = await tx.pgTransaction
919
1021
  .with(zombieRetrySelection)
920
1022
  .update(taskTable)
921
1023
  .set({
922
1024
  status: TaskStatus.Pending,
923
1025
  token: null,
924
1026
  visibilityDeadline: null,
1027
+ startTimestamp: null,
925
1028
  scheduleTimestamp: sql `${TRANSACTION_TIMESTAMP} + ${interval(this.retryDelayMinimum, 'milliseconds')}`, // Simple backoff for zombies
926
1029
  error: sql `jsonb_build_object('code', 'VisibilityTimeout', 'message', 'Worker Lost', 'last_error', ${taskTable.error})`,
927
1030
  })
928
- .where(inArray(taskTable.id, repositoryWithTx.session.select().from(zombieRetrySelection)))
1031
+ .where(inArray(taskTable.id, tx.pgTransaction.select().from(zombieRetrySelection)))
929
1032
  .returning({ id: taskTable.id });
930
1033
  totalUpdated += zombieRetryRows.length;
931
1034
  // 3.3 Handle Zombie Tasks (Exhaustion)
932
- const zombieExhaustionSelection = repositoryWithTx.session.$with('zombie_exhaustion_selection').as((qb) => qb
1035
+ const zombieExhaustionSelection = tx.pgTransaction.$with('zombie_exhaustion_selection').as((qb) => qb
933
1036
  .select({ id: taskTable.id })
934
1037
  .from(taskTable)
935
1038
  .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Running), lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP), gte(taskTable.tries, this.maxTries)))
936
1039
  .limit(1000)
937
1040
  .for('update', { skipLocked: true }));
938
- const exhaustionRows = await repositoryWithTx.session
1041
+ const exhaustionRows = await tx.pgTransaction
939
1042
  .with(zombieExhaustionSelection)
940
1043
  .update(taskTable)
941
1044
  .set({
@@ -945,20 +1048,22 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
945
1048
  completeTimestamp: TRANSACTION_TIMESTAMP,
946
1049
  error: sql `jsonb_build_object('code', 'ZombieExhausted', 'message', 'Exceeded max retries after repeated crashes', 'last_error', ${taskTable.error})`,
947
1050
  })
948
- .where(inArray(taskTable.id, repositoryWithTx.session.select().from(zombieExhaustionSelection)))
949
- .returning({ tags: taskTable.tags });
1051
+ .where(inArray(taskTable.id, tx.pgTransaction.select().from(zombieExhaustionSelection)))
1052
+ .returning({ id: taskTable.id });
950
1053
  totalUpdated += exhaustionRows.length;
951
1054
  if (exhaustionRows.length > 0) {
952
- await this.triggerTagFanIn(exhaustionRows.flatMap((r) => r.tags), { transaction: tx });
1055
+ for (const row of exhaustionRows) {
1056
+ await this.resolveDependencies(row.id, TaskStatus.Dead, { transaction: tx });
1057
+ }
953
1058
  }
954
1059
  // 3.4 Handle Hard Timeout
955
- const timeoutSelection = repositoryWithTx.session.$with('timeout_selection').as((qb) => qb
1060
+ const timeoutSelection = tx.pgTransaction.$with('timeout_selection').as((qb) => qb
956
1061
  .select({ id: taskTable.id })
957
1062
  .from(taskTable)
958
1063
  .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Running), lt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`)))
959
1064
  .limit(1000)
960
1065
  .for('update', { skipLocked: true }));
961
- const timeoutRows = await repositoryWithTx.session
1066
+ const timeoutRows = await tx.pgTransaction
962
1067
  .with(timeoutSelection)
963
1068
  .update(taskTable)
964
1069
  .set({
@@ -968,27 +1073,29 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
968
1073
  completeTimestamp: TRANSACTION_TIMESTAMP,
969
1074
  error: sql `jsonb_build_object('code', 'MaxTimeExceeded', 'message', 'Hard Execution Timeout: Task ran longer than ' || ${this.maxExecutionTime} || 'ms', 'last_error', ${taskTable.error})`,
970
1075
  })
971
- .where(inArray(taskTable.id, repositoryWithTx.session.select().from(timeoutSelection)))
972
- .returning({ tags: taskTable.tags });
1076
+ .where(inArray(taskTable.id, tx.pgTransaction.select().from(timeoutSelection)))
1077
+ .returning({ id: taskTable.id });
973
1078
  totalUpdated += timeoutRows.length;
974
1079
  if (timeoutRows.length > 0) {
975
- await this.triggerTagFanIn(timeoutRows.flatMap((r) => r.tags), { transaction: tx });
1080
+ for (const row of timeoutRows) {
1081
+ await this.resolveDependencies(row.id, TaskStatus.Dead, { transaction: tx });
1082
+ }
976
1083
  }
977
1084
  // 3.5 Promote Priority (Aging)
978
- const agingSelection = repositoryWithTx.session.$with('aging_selection').as((qb) => qb
1085
+ const agingSelection = tx.pgTransaction.$with('aging_selection').as((qb) => qb
979
1086
  .select({ id: taskTable.id })
980
1087
  .from(taskTable)
981
1088
  .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Pending), lt(taskTable.priorityAgeTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.priorityAgingInterval, 'milliseconds')}`)))
982
1089
  .limit(1000)
983
1090
  .for('update', { skipLocked: true }));
984
- const agingRows = await repositoryWithTx.session
1091
+ const agingRows = await tx.pgTransaction
985
1092
  .with(agingSelection)
986
1093
  .update(taskTable)
987
1094
  .set({
988
1095
  priority: sql `${taskTable.priority} - ${this.priorityAgingStep}`,
989
1096
  priorityAgeTimestamp: TRANSACTION_TIMESTAMP,
990
1097
  })
991
- .where(inArray(taskTable.id, repositoryWithTx.session.select().from(agingSelection)))
1098
+ .where(inArray(taskTable.id, tx.pgTransaction.select().from(agingSelection)))
992
1099
  .returning({ id: taskTable.id });
993
1100
  totalUpdated += agingRows.length;
994
1101
  return totalUpdated;
@@ -1009,13 +1116,13 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
1009
1116
  result: null,
1010
1117
  scheduleTimestamp: TRANSACTION_TIMESTAMP,
1011
1118
  completeTimestamp: null,
1119
+ startTimestamp: null,
1012
1120
  tries: 0,
1013
1121
  progress: 0,
1014
1122
  priorityAgeTimestamp: TRANSACTION_TIMESTAMP,
1015
1123
  state: (options?.resetState == true) ? null : undefined,
1016
1124
  })
1017
- .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, id), or(eq(taskTable.status, TaskStatus.Pending), eq(taskTable.status, TaskStatus.Completed), eq(taskTable.status, TaskStatus.Cancelled), eq(taskTable.status, TaskStatus.Dead), lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP))))
1018
- .execute();
1125
+ .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, id), or(eq(taskTable.status, TaskStatus.Pending), eq(taskTable.status, TaskStatus.Completed), eq(taskTable.status, TaskStatus.Cancelled), eq(taskTable.status, TaskStatus.Dead), lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP))));
1019
1126
  }
1020
1127
  notify() {
1021
1128
  this.#messageBus.publishAndForget();
@@ -1044,36 +1151,6 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
1044
1151
  }
1045
1152
  }
1046
1153
  }
1047
- /**
1048
- * @internal for internal unit test use only
1049
- * @param options
1050
- * @returns
1051
- */
1052
- async processPendingFanIn(options) {
1053
- let processedCount = 0;
1054
- while (true) {
1055
- const task = await this.#internalThis.dequeue({ types: ['[SystemWorker]:FanIn'], forceDequeue: true, transaction: options?.transaction });
1056
- if (isUndefined(task)) {
1057
- break;
1058
- }
1059
- try {
1060
- await this.resolveDependencies(task.data.targetTags);
1061
- await this.#internalThis.complete(task, { transaction: options?.transaction });
1062
- processedCount++;
1063
- }
1064
- catch (error) {
1065
- await this.#internalThis.fail(task, error, { transaction: options?.transaction });
1066
- throw error;
1067
- }
1068
- }
1069
- return processedCount;
1070
- }
1071
- startSystemWorker() {
1072
- this.#internalThis.process({ concurrency: 1, cancellationSignal: this.#cancellationSignal, forceDequeue: true, types: ['[SystemWorker]:FanIn'] }, async (context) => {
1073
- await this.resolveDependencies(context.data.targetTags);
1074
- return TaskProcessResult.Complete();
1075
- });
1076
- }
1077
1154
  async *getBatchConsumer(size, cancellationSignal, options) {
1078
1155
  const continue$ = merge(this.#messageBus.allMessages$, cancellationSignal);
1079
1156
  while (cancellationSignal.isUnset) {