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