@qlever-llc/trellis 0.10.17 → 0.19.0-rc.1

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 (283) hide show
  1. package/esm/auth/mod.d.ts +1 -1
  2. package/esm/auth/mod.d.ts.map +1 -1
  3. package/esm/auth/mod.js +1 -1
  4. package/esm/auth/protocol.d.ts +416 -398
  5. package/esm/auth/protocol.d.ts.map +1 -1
  6. package/esm/auth/protocol.js +35 -33
  7. package/esm/codec.d.ts +1 -0
  8. package/esm/codec.d.ts.map +1 -1
  9. package/esm/codec.js +1 -0
  10. package/esm/contract_support/canonical.d.ts +3 -0
  11. package/esm/contract_support/canonical.d.ts.map +1 -1
  12. package/esm/contract_support/canonical.js +3 -0
  13. package/esm/contract_support/mod.d.ts +28 -4
  14. package/esm/contract_support/mod.d.ts.map +1 -1
  15. package/esm/contract_support/mod.js +85 -4
  16. package/esm/contract_support/protocol.d.ts +92 -7
  17. package/esm/contract_support/protocol.d.ts.map +1 -1
  18. package/esm/contract_support/protocol.js +40 -6
  19. package/esm/errors/index.d.ts +9 -9
  20. package/esm/generated-sdk/auth/api.d.ts +4 -1
  21. package/esm/generated-sdk/auth/api.d.ts.map +1 -1
  22. package/esm/generated-sdk/auth/api.js +6 -1
  23. package/esm/generated-sdk/auth/client.d.ts +47 -32
  24. package/esm/generated-sdk/auth/client.d.ts.map +1 -1
  25. package/esm/generated-sdk/auth/contract.d.ts +1 -1
  26. package/esm/generated-sdk/auth/contract.d.ts.map +1 -1
  27. package/esm/generated-sdk/auth/contract.js +1570 -1202
  28. package/esm/generated-sdk/auth/schemas.d.ts +1757 -1759
  29. package/esm/generated-sdk/auth/schemas.d.ts.map +1 -1
  30. package/esm/generated-sdk/auth/schemas.js +963 -768
  31. package/esm/generated-sdk/auth/types.d.ts +292 -302
  32. package/esm/generated-sdk/auth/types.d.ts.map +1 -1
  33. package/esm/generated-sdk/auth/types.js +1 -1
  34. package/esm/generated-sdk/health/client.d.ts +4 -4
  35. package/esm/generated-sdk/health/client.d.ts.map +1 -1
  36. package/esm/generated-sdk/health/contract.d.ts +1 -1
  37. package/esm/generated-sdk/health/contract.d.ts.map +1 -1
  38. package/esm/generated-sdk/health/contract.js +2 -10
  39. package/esm/generated-sdk/health/schemas.d.ts +1 -14
  40. package/esm/generated-sdk/health/schemas.d.ts.map +1 -1
  41. package/esm/generated-sdk/health/schemas.js +1 -9
  42. package/esm/generated-sdk/health/types.d.ts +3 -5
  43. package/esm/generated-sdk/health/types.d.ts.map +1 -1
  44. package/esm/generated-sdk/health/types.js +1 -2
  45. package/esm/generated-sdk/jobs/api.d.ts +13 -0
  46. package/esm/generated-sdk/jobs/api.d.ts.map +1 -1
  47. package/esm/generated-sdk/jobs/client.d.ts +6 -4
  48. package/esm/generated-sdk/jobs/client.d.ts.map +1 -1
  49. package/esm/generated-sdk/jobs/contract.d.ts +14 -1
  50. package/esm/generated-sdk/jobs/contract.d.ts.map +1 -1
  51. package/esm/generated-sdk/jobs/contract.js +268 -1
  52. package/esm/generated-sdk/jobs/owned_api.d.ts +13 -0
  53. package/esm/generated-sdk/jobs/owned_api.d.ts.map +1 -1
  54. package/esm/generated-sdk/jobs/owned_api.js +20 -1
  55. package/esm/generated-sdk/jobs/schemas.d.ts +437 -0
  56. package/esm/generated-sdk/jobs/schemas.d.ts.map +1 -1
  57. package/esm/generated-sdk/jobs/schemas.js +211 -0
  58. package/esm/generated-sdk/jobs/types.d.ts +123 -1
  59. package/esm/generated-sdk/jobs/types.d.ts.map +1 -1
  60. package/esm/generated-sdk/jobs/types.js +1 -1
  61. package/esm/generated-sdk/state/client.d.ts +4 -4
  62. package/esm/generated-sdk/state/client.d.ts.map +1 -1
  63. package/esm/generated-sdk/trellis-core/api.d.ts +4 -1
  64. package/esm/generated-sdk/trellis-core/api.d.ts.map +1 -1
  65. package/esm/generated-sdk/trellis-core/api.js +6 -1
  66. package/esm/generated-sdk/trellis-core/client.d.ts +28 -3
  67. package/esm/generated-sdk/trellis-core/client.d.ts.map +1 -1
  68. package/esm/generated-sdk/trellis-core/contract.d.ts +1 -1
  69. package/esm/generated-sdk/trellis-core/contract.d.ts.map +1 -1
  70. package/esm/generated-sdk/trellis-core/contract.js +91 -1
  71. package/esm/generated-sdk/trellis-core/schemas.d.ts +113 -0
  72. package/esm/generated-sdk/trellis-core/schemas.d.ts.map +1 -1
  73. package/esm/generated-sdk/trellis-core/schemas.js +81 -0
  74. package/esm/generated-sdk/trellis-core/types.d.ts +23 -1
  75. package/esm/generated-sdk/trellis-core/types.d.ts.map +1 -1
  76. package/esm/generated-sdk/trellis-core/types.js +1 -1
  77. package/esm/health.d.ts +1 -1
  78. package/esm/health.d.ts.map +1 -1
  79. package/esm/health.js +1 -1
  80. package/esm/index.d.ts +2 -2
  81. package/esm/index.d.ts.map +1 -1
  82. package/esm/index.js +1 -1
  83. package/esm/jobs.d.ts +69 -3
  84. package/esm/jobs.d.ts.map +1 -1
  85. package/esm/jobs.js +92 -3
  86. package/esm/server/health.d.ts +2 -6
  87. package/esm/server/health.d.ts.map +1 -1
  88. package/esm/server/health_schemas.d.ts +0 -8
  89. package/esm/server/health_schemas.d.ts.map +1 -1
  90. package/esm/server/health_schemas.js +0 -5
  91. package/esm/server/internal_jobs/bindings.d.ts +11 -0
  92. package/esm/server/internal_jobs/bindings.d.ts.map +1 -1
  93. package/esm/server/internal_jobs/job-manager.d.ts +44 -1
  94. package/esm/server/internal_jobs/job-manager.d.ts.map +1 -1
  95. package/esm/server/internal_jobs/job-manager.js +361 -51
  96. package/esm/server/internal_jobs/key-coordinator.d.ts +260 -0
  97. package/esm/server/internal_jobs/key-coordinator.d.ts.map +1 -0
  98. package/esm/server/internal_jobs/key-coordinator.js +580 -0
  99. package/esm/server/internal_jobs/projection.d.ts.map +1 -1
  100. package/esm/server/internal_jobs/projection.js +2 -0
  101. package/esm/server/internal_jobs/runtime-worker.d.ts +5 -1
  102. package/esm/server/internal_jobs/runtime-worker.d.ts.map +1 -1
  103. package/esm/server/internal_jobs/runtime-worker.js +14 -2
  104. package/esm/server/internal_jobs/types.d.ts +5 -5
  105. package/esm/server/internal_jobs/types.d.ts.map +1 -1
  106. package/esm/server/internal_jobs/types.js +6 -0
  107. package/esm/server/service.d.ts +8 -3
  108. package/esm/server/service.d.ts.map +1 -1
  109. package/esm/server/service.js +127 -6
  110. package/esm/service/drizzle.d.ts +27 -0
  111. package/esm/service/drizzle.d.ts.map +1 -0
  112. package/esm/service/drizzle.js +84 -0
  113. package/esm/service/mod.d.ts +1 -1
  114. package/esm/service/mod.d.ts.map +1 -1
  115. package/esm/service/mod.js +1 -1
  116. package/esm/service/outbox_inbox.d.ts.map +1 -1
  117. package/esm/service/outbox_inbox.js +13 -2
  118. package/esm/trellis.d.ts +24 -8
  119. package/esm/trellis.d.ts.map +1 -1
  120. package/esm/trellis.js +44 -22
  121. package/package.json +14 -2
  122. package/script/auth/mod.d.ts +1 -1
  123. package/script/auth/mod.d.ts.map +1 -1
  124. package/script/auth/mod.js +22 -6
  125. package/script/auth/protocol.d.ts +416 -398
  126. package/script/auth/protocol.d.ts.map +1 -1
  127. package/script/auth/protocol.js +40 -37
  128. package/script/codec.d.ts +1 -0
  129. package/script/codec.d.ts.map +1 -1
  130. package/script/codec.js +1 -0
  131. package/script/contract_support/canonical.d.ts +3 -0
  132. package/script/contract_support/canonical.d.ts.map +1 -1
  133. package/script/contract_support/canonical.js +3 -0
  134. package/script/contract_support/mod.d.ts +28 -4
  135. package/script/contract_support/mod.d.ts.map +1 -1
  136. package/script/contract_support/mod.js +90 -4
  137. package/script/contract_support/protocol.d.ts +92 -7
  138. package/script/contract_support/protocol.d.ts.map +1 -1
  139. package/script/contract_support/protocol.js +41 -7
  140. package/script/errors/index.d.ts +9 -9
  141. package/script/generated-sdk/auth/api.d.ts +4 -1
  142. package/script/generated-sdk/auth/api.d.ts.map +1 -1
  143. package/script/generated-sdk/auth/api.js +6 -1
  144. package/script/generated-sdk/auth/client.d.ts +47 -32
  145. package/script/generated-sdk/auth/client.d.ts.map +1 -1
  146. package/script/generated-sdk/auth/contract.d.ts +1 -1
  147. package/script/generated-sdk/auth/contract.d.ts.map +1 -1
  148. package/script/generated-sdk/auth/contract.js +1570 -1202
  149. package/script/generated-sdk/auth/schemas.d.ts +1757 -1759
  150. package/script/generated-sdk/auth/schemas.d.ts.map +1 -1
  151. package/script/generated-sdk/auth/schemas.js +963 -768
  152. package/script/generated-sdk/auth/types.d.ts +292 -302
  153. package/script/generated-sdk/auth/types.d.ts.map +1 -1
  154. package/script/generated-sdk/auth/types.js +1 -1
  155. package/script/generated-sdk/health/client.d.ts +4 -4
  156. package/script/generated-sdk/health/client.d.ts.map +1 -1
  157. package/script/generated-sdk/health/contract.d.ts +1 -1
  158. package/script/generated-sdk/health/contract.d.ts.map +1 -1
  159. package/script/generated-sdk/health/contract.js +2 -10
  160. package/script/generated-sdk/health/schemas.d.ts +1 -14
  161. package/script/generated-sdk/health/schemas.d.ts.map +1 -1
  162. package/script/generated-sdk/health/schemas.js +1 -9
  163. package/script/generated-sdk/health/types.d.ts +3 -5
  164. package/script/generated-sdk/health/types.d.ts.map +1 -1
  165. package/script/generated-sdk/health/types.js +1 -2
  166. package/script/generated-sdk/jobs/api.d.ts +13 -0
  167. package/script/generated-sdk/jobs/api.d.ts.map +1 -1
  168. package/script/generated-sdk/jobs/client.d.ts +6 -4
  169. package/script/generated-sdk/jobs/client.d.ts.map +1 -1
  170. package/script/generated-sdk/jobs/contract.d.ts +14 -1
  171. package/script/generated-sdk/jobs/contract.d.ts.map +1 -1
  172. package/script/generated-sdk/jobs/contract.js +268 -1
  173. package/script/generated-sdk/jobs/owned_api.d.ts +13 -0
  174. package/script/generated-sdk/jobs/owned_api.d.ts.map +1 -1
  175. package/script/generated-sdk/jobs/owned_api.js +19 -0
  176. package/script/generated-sdk/jobs/schemas.d.ts +437 -0
  177. package/script/generated-sdk/jobs/schemas.d.ts.map +1 -1
  178. package/script/generated-sdk/jobs/schemas.js +212 -1
  179. package/script/generated-sdk/jobs/types.d.ts +123 -1
  180. package/script/generated-sdk/jobs/types.d.ts.map +1 -1
  181. package/script/generated-sdk/jobs/types.js +1 -1
  182. package/script/generated-sdk/state/client.d.ts +4 -4
  183. package/script/generated-sdk/state/client.d.ts.map +1 -1
  184. package/script/generated-sdk/trellis-core/api.d.ts +4 -1
  185. package/script/generated-sdk/trellis-core/api.d.ts.map +1 -1
  186. package/script/generated-sdk/trellis-core/api.js +6 -1
  187. package/script/generated-sdk/trellis-core/client.d.ts +28 -3
  188. package/script/generated-sdk/trellis-core/client.d.ts.map +1 -1
  189. package/script/generated-sdk/trellis-core/contract.d.ts +1 -1
  190. package/script/generated-sdk/trellis-core/contract.d.ts.map +1 -1
  191. package/script/generated-sdk/trellis-core/contract.js +91 -1
  192. package/script/generated-sdk/trellis-core/schemas.d.ts +113 -0
  193. package/script/generated-sdk/trellis-core/schemas.d.ts.map +1 -1
  194. package/script/generated-sdk/trellis-core/schemas.js +81 -0
  195. package/script/generated-sdk/trellis-core/types.d.ts +23 -1
  196. package/script/generated-sdk/trellis-core/types.d.ts.map +1 -1
  197. package/script/generated-sdk/trellis-core/types.js +1 -1
  198. package/script/health.d.ts +1 -1
  199. package/script/health.d.ts.map +1 -1
  200. package/script/health.js +1 -2
  201. package/script/index.d.ts +2 -2
  202. package/script/index.d.ts.map +1 -1
  203. package/script/index.js +2 -3
  204. package/script/jobs.d.ts +69 -3
  205. package/script/jobs.d.ts.map +1 -1
  206. package/script/jobs.js +93 -3
  207. package/script/server/health.d.ts +2 -6
  208. package/script/server/health.d.ts.map +1 -1
  209. package/script/server/health_schemas.d.ts +0 -8
  210. package/script/server/health_schemas.d.ts.map +1 -1
  211. package/script/server/health_schemas.js +1 -6
  212. package/script/server/internal_jobs/bindings.d.ts +11 -0
  213. package/script/server/internal_jobs/bindings.d.ts.map +1 -1
  214. package/script/server/internal_jobs/job-manager.d.ts +44 -1
  215. package/script/server/internal_jobs/job-manager.d.ts.map +1 -1
  216. package/script/server/internal_jobs/job-manager.js +361 -51
  217. package/script/server/internal_jobs/key-coordinator.d.ts +260 -0
  218. package/script/server/internal_jobs/key-coordinator.d.ts.map +1 -0
  219. package/script/server/internal_jobs/key-coordinator.js +593 -0
  220. package/script/server/internal_jobs/projection.d.ts.map +1 -1
  221. package/script/server/internal_jobs/projection.js +2 -0
  222. package/script/server/internal_jobs/runtime-worker.d.ts +5 -1
  223. package/script/server/internal_jobs/runtime-worker.d.ts.map +1 -1
  224. package/script/server/internal_jobs/runtime-worker.js +14 -2
  225. package/script/server/internal_jobs/types.d.ts +5 -5
  226. package/script/server/internal_jobs/types.d.ts.map +1 -1
  227. package/script/server/internal_jobs/types.js +6 -0
  228. package/script/server/service.d.ts +8 -3
  229. package/script/server/service.d.ts.map +1 -1
  230. package/script/server/service.js +126 -5
  231. package/script/service/drizzle.d.ts +27 -0
  232. package/script/service/drizzle.d.ts.map +1 -0
  233. package/script/service/drizzle.js +88 -0
  234. package/script/service/mod.d.ts +1 -1
  235. package/script/service/mod.d.ts.map +1 -1
  236. package/script/service/mod.js +1 -2
  237. package/script/service/outbox_inbox.d.ts.map +1 -1
  238. package/script/service/outbox_inbox.js +13 -2
  239. package/script/trellis.d.ts +24 -8
  240. package/script/trellis.d.ts.map +1 -1
  241. package/script/trellis.js +44 -22
  242. package/src/auth/mod.ts +28 -2
  243. package/src/auth/protocol.ts +72 -37
  244. package/src/codec.ts +1 -0
  245. package/src/contract_support/canonical.ts +3 -0
  246. package/src/contract_support/mod.ts +158 -5
  247. package/src/contract_support/protocol.ts +56 -9
  248. package/src/health.ts +0 -1
  249. package/src/index.ts +2 -1
  250. package/src/jobs.ts +138 -1
  251. package/src/sdk/_generated/auth/api.ts +9 -2
  252. package/src/sdk/_generated/auth/client.ts +85 -78
  253. package/src/sdk/_generated/auth/contract.ts +1803 -1435
  254. package/src/sdk/_generated/auth/schemas.ts +1166 -971
  255. package/src/sdk/_generated/auth/types.ts +330 -314
  256. package/src/sdk/_generated/core/api.ts +9 -2
  257. package/src/sdk/_generated/core/client.ts +41 -2
  258. package/src/sdk/_generated/core/contract.ts +91 -1
  259. package/src/sdk/_generated/core/schemas.ts +81 -0
  260. package/src/sdk/_generated/core/types.ts +23 -1
  261. package/src/sdk/_generated/health/client.ts +6 -6
  262. package/src/sdk/_generated/health/contract.ts +2 -10
  263. package/src/sdk/_generated/health/schemas.ts +1 -9
  264. package/src/sdk/_generated/health/types.ts +6 -2
  265. package/src/sdk/_generated/jobs/client.ts +17 -6
  266. package/src/sdk/_generated/jobs/contract.ts +269 -1
  267. package/src/sdk/_generated/jobs/owned_api.ts +21 -0
  268. package/src/sdk/_generated/jobs/schemas.ts +213 -0
  269. package/src/sdk/_generated/jobs/types.ts +116 -1
  270. package/src/sdk/_generated/state/client.ts +6 -6
  271. package/src/server/health.ts +2 -6
  272. package/src/server/health_schemas.ts +0 -6
  273. package/src/server/internal_jobs/bindings.ts +11 -0
  274. package/src/server/internal_jobs/job-manager.ts +436 -16
  275. package/src/server/internal_jobs/key-coordinator.ts +955 -0
  276. package/src/server/internal_jobs/projection.ts +2 -0
  277. package/src/server/internal_jobs/runtime-worker.ts +17 -1
  278. package/src/server/internal_jobs/types.ts +6 -0
  279. package/src/server/service.ts +168 -9
  280. package/src/service/drizzle.ts +125 -0
  281. package/src/service/mod.ts +0 -1
  282. package/src/service/outbox_inbox.ts +16 -6
  283. package/src/trellis.ts +76 -27
@@ -5,6 +5,8 @@ const TERMINAL_STATES = new Set<JobState>([
5
5
  "failed",
6
6
  "cancelled",
7
7
  "expired",
8
+ "skipped",
9
+ "stale",
8
10
  "dead",
9
11
  "dismissed",
10
12
  ]);
@@ -177,6 +177,8 @@ type StartQueueWorkerLoopOptions<TResult> = {
177
177
  args: ResultValidationArgs<TResult>,
178
178
  ) => Promise<void> | void;
179
179
  handler: (job: ActiveJob<unknown, TResult>) => Promise<TResult>;
180
+ instanceId?: string;
181
+ deferralBackoffMs?: number;
180
182
  };
181
183
 
182
184
  type StartNatsQueueWorkerOptions<TResult> =
@@ -199,6 +201,7 @@ type StartNatsQueueWorkerOptions<TResult> =
199
201
  args: ResultValidationArgs<TResult>,
200
202
  ) => Promise<void> | void;
201
203
  handler: (job: ActiveJob<unknown, TResult>) => Promise<TResult>;
204
+ instanceId?: string;
202
205
  };
203
206
 
204
207
  function toWorkerConsumer(
@@ -252,7 +255,8 @@ export async function processWorkPayloadWithContextAndHeartbeat<TResult>(
252
255
  args: ResultValidationArgs<TResult>,
253
256
  ) => Promise<void> | void;
254
257
  },
255
- runtime?: { redeliveryCount?: number },
258
+ runtime?: { latestState?: Job["state"]; redeliveryCount?: number },
259
+ instanceId?: string,
256
260
  ): Promise<JobProcessOutcome<TResult> | undefined> {
257
261
  const event = parseWorkPayloadEvent(payload);
258
262
  if (!event) {
@@ -280,7 +284,9 @@ export async function processWorkPayloadWithContextAndHeartbeat<TResult>(
280
284
  return await handler(activeJob);
281
285
  },
282
286
  {
287
+ latestState: runtime?.latestState,
283
288
  redeliveryCount: runtime?.redeliveryCount,
289
+ instanceId,
284
290
  },
285
291
  {
286
292
  validateResult: validation?.validateResult
@@ -323,6 +329,7 @@ export function ackActionForOutcome(
323
329
  switch (outcome.outcome) {
324
330
  case "retry":
325
331
  case "interrupted":
332
+ case "deferred":
326
333
  return "nak";
327
334
  default:
328
335
  return "ack";
@@ -381,6 +388,7 @@ export async function startQueueWorkerLoop<TResult>(
381
388
  continue;
382
389
  }
383
390
  if (lifecycleWorkDecision(latestLifecycle) === "skip-ack") {
391
+ await options.manager.cleanupQueuedKeyedJob(job);
384
392
  registry.clearPending(key);
385
393
  await msg.ack();
386
394
  continue;
@@ -394,6 +402,7 @@ export async function startQueueWorkerLoop<TResult>(
394
402
  continue;
395
403
  }
396
404
  if (projectedWorkDecision(projected, job) === "skip-ack") {
405
+ await options.manager.cleanupQueuedKeyedJob(job);
397
406
  registry.clearPending(key);
398
407
  await msg.ack();
399
408
  continue;
@@ -422,11 +431,15 @@ export async function startQueueWorkerLoop<TResult>(
422
431
  validateResult: options.validateResult,
423
432
  },
424
433
  {
434
+ latestState: latestLifecycle?.state,
425
435
  redeliveryCount: msg.info?.redeliveryCount,
426
436
  },
437
+ options.instanceId,
427
438
  );
428
439
  if (ackActionForOutcome(outcome) === "ack") {
429
440
  await msg.ack();
441
+ } else if (outcome?.outcome === "deferred") {
442
+ await msg.nak(options.deferralBackoffMs ?? 1_000);
430
443
  } else {
431
444
  await msg.nak();
432
445
  }
@@ -512,6 +525,8 @@ export async function startNatsQueueWorker<TResult>(
512
525
  resultSchema: queue.result,
513
526
  validateResult: options.validateResult,
514
527
  handler: options.handler,
528
+ instanceId: options.instanceId,
529
+ deferralBackoffMs: queue.backoffMs[0] ?? 1_000,
515
530
  });
516
531
  }
517
532
 
@@ -624,6 +639,7 @@ export async function startNatsWorkerHostFromBinding<TResult>(
624
639
  validatePayload: options.validatePayload,
625
640
  validateResult: options.validateResult,
626
641
  handler: options.handler,
642
+ instanceId: options.instanceId,
627
643
  };
628
644
  return await (isCustomNatsRuntimeDeps(options)
629
645
  ? startNatsQueueWorker({
@@ -19,6 +19,8 @@ export const JobStateSchema = Type.Union([
19
19
  Type.Literal("failed"),
20
20
  Type.Literal("cancelled"),
21
21
  Type.Literal("expired"),
22
+ Type.Literal("skipped"),
23
+ Type.Literal("stale"),
22
24
  Type.Literal("dead"),
23
25
  Type.Literal("dismissed"),
24
26
  ]);
@@ -87,6 +89,10 @@ export const JobEventSchema = Type.Object({
87
89
  Type.Literal("failed"),
88
90
  Type.Literal("cancelled"),
89
91
  Type.Literal("expired"),
92
+ Type.Literal("skipped"),
93
+ Type.Literal("stale"),
94
+ Type.Literal("heartbeat"),
95
+ Type.Literal("staleCompletionIgnored"),
90
96
  Type.Literal("retried"),
91
97
  Type.Literal("dead"),
92
98
  Type.Literal("dismissed"),
@@ -105,9 +105,11 @@ import {
105
105
  ActiveJob as PublicActiveJob,
106
106
  type JobIdentity,
107
107
  type JobLogEntry,
108
+ JobNotEnqueuedError,
108
109
  type JobProgress,
109
110
  JobRef,
110
111
  type JobSnapshot,
112
+ type JobSubmitOutcome,
111
113
  JobWorkerHostAdapter,
112
114
  type TerminalJob,
113
115
  } from "../jobs.js";
@@ -116,6 +118,18 @@ import {
116
118
  JobProcessError as InternalJobProcessError,
117
119
  } from "./internal_jobs/job-manager.js";
118
120
  import { startNatsWorkerHostFromBinding } from "./internal_jobs/runtime-worker.js";
121
+ import {
122
+ createNatsJobKeyCoordinator,
123
+ normalizeJobKeyPolicy,
124
+ } from "./internal_jobs/key-coordinator.js";
125
+ import type {
126
+ JobKeyConcurrencyBinding,
127
+ JobQueuePolicyBinding,
128
+ } from "./internal_jobs/key-coordinator.js";
129
+ import type {
130
+ JobsBinding,
131
+ JobsQueueBinding,
132
+ } from "./internal_jobs/bindings.js";
119
133
  import type { ActiveJob as InternalActiveJob } from "./internal_jobs/active-job.js";
120
134
  import {
121
135
  type Job as InternalJob,
@@ -149,6 +163,8 @@ type ResourceBindingJobsQueue = {
149
163
  logs: boolean;
150
164
  dlq: boolean;
151
165
  concurrency: number;
166
+ keyConcurrency?: JobKeyConcurrencyBinding;
167
+ queue?: JobQueuePolicyBinding;
152
168
  };
153
169
 
154
170
  type ResourceBindingJobs = {
@@ -157,6 +173,71 @@ type ResourceBindingJobs = {
157
173
  queues: Record<string, ResourceBindingJobsQueue>;
158
174
  };
159
175
 
176
+ function normalizeResourceJobsBinding(
177
+ binding: ResourceBindingJobs,
178
+ ): JobsBinding {
179
+ const queues: Record<string, JobsQueueBinding> = {};
180
+ for (const [name, queue] of Object.entries(binding.queues)) {
181
+ const baseQueue = baseJobsQueueBinding(queue);
182
+ if (!queue.keyConcurrency) {
183
+ queues[name] = {
184
+ ...baseQueue,
185
+ ...(queue.queue ? { queue: normalizeQueuePolicy(queue.queue) } : {}),
186
+ };
187
+ continue;
188
+ }
189
+
190
+ const policy = normalizeJobKeyPolicy({
191
+ keyConcurrency: queue.keyConcurrency,
192
+ queue: queue.queue,
193
+ });
194
+ queues[name] = {
195
+ ...baseQueue,
196
+ keyConcurrency: {
197
+ key: policy.key,
198
+ maxActive: policy.maxActive,
199
+ heartbeatIntervalMs: policy.heartbeatIntervalMs,
200
+ heartbeatTtlMs: policy.heartbeatTtlMs,
201
+ stalePolicy: policy.stalePolicy,
202
+ },
203
+ queue: policy.queue,
204
+ };
205
+ }
206
+ return { namespace: binding.namespace, queues };
207
+ }
208
+
209
+ function baseJobsQueueBinding(
210
+ queue: ResourceBindingJobsQueue,
211
+ ): Omit<JobsQueueBinding, "keyConcurrency" | "queue"> {
212
+ return {
213
+ queueType: queue.queueType,
214
+ publishPrefix: queue.publishPrefix,
215
+ workSubject: queue.workSubject,
216
+ consumerName: queue.consumerName,
217
+ payload: queue.payload,
218
+ ...(queue.result ? { result: queue.result } : {}),
219
+ maxDeliver: queue.maxDeliver,
220
+ backoffMs: queue.backoffMs,
221
+ ackWaitMs: queue.ackWaitMs,
222
+ ...(queue.defaultDeadlineMs !== undefined
223
+ ? { defaultDeadlineMs: queue.defaultDeadlineMs }
224
+ : {}),
225
+ progress: queue.progress,
226
+ logs: queue.logs,
227
+ dlq: queue.dlq,
228
+ concurrency: queue.concurrency,
229
+ };
230
+ }
231
+
232
+ function normalizeQueuePolicy(
233
+ queue: JobQueuePolicyBinding,
234
+ ): JobsQueueBinding["queue"] {
235
+ return {
236
+ maxQueuedPerKey: queue.maxQueuedPerKey ?? 0,
237
+ whenFull: queue.whenFull ?? "reject",
238
+ };
239
+ }
240
+
160
241
  type ResourceBindingEventConsumer = {
161
242
  stream: string;
162
243
  consumerName: string;
@@ -1208,6 +1289,9 @@ export type JobQueue<
1208
1289
  TJobs extends ContractJobsMetadata = {},
1209
1290
  > = {
1210
1291
  create(payload: TPayload): AsyncResult<JobRef<TPayload, TResult>, BaseError>;
1292
+ submit(
1293
+ payload: TPayload,
1294
+ ): AsyncResult<JobSubmitOutcome<TPayload, TResult>, BaseError>;
1211
1295
  handle(
1212
1296
  handler: (args: {
1213
1297
  job: PublicActiveJob<TPayload, TResult>;
@@ -1239,7 +1323,7 @@ type ServiceEventOf<
1239
1323
  type ServiceEventPayloadOf<
1240
1324
  TA extends TrellisAPI,
1241
1325
  E extends ServiceEventName<TA>,
1242
- > = Omit<ServiceEventOf<TA, E>, "header">;
1326
+ > = ServiceEventOf<TA, E> & Record<string, unknown>;
1243
1327
 
1244
1328
  type BoundEventHandleFn<
1245
1329
  TEventApi extends TrellisAPI,
@@ -1249,7 +1333,7 @@ type BoundEventHandleFn<
1249
1333
  TJobs extends ContractJobsMetadata,
1250
1334
  TDeps,
1251
1335
  > = (args: {
1252
- event: ServiceEventOf<TEventApi, E>;
1336
+ event: ServiceEventPayloadOf<TEventApi, E>;
1253
1337
  context: EventListenerContext;
1254
1338
  client: Trellis<TTrellisApi, TKv, TJobs>;
1255
1339
  deps: TDeps;
@@ -1419,6 +1503,9 @@ type BoundJobQueue<
1419
1503
  TDeps,
1420
1504
  > = {
1421
1505
  create(payload: TPayload): AsyncResult<JobRef<TPayload, TResult>, BaseError>;
1506
+ submit(
1507
+ payload: TPayload,
1508
+ ): AsyncResult<JobSubmitOutcome<TPayload, TResult>, BaseError>;
1422
1509
  handle(
1423
1510
  handler: (args: {
1424
1511
  job: PublicActiveJob<TPayload, TResult>;
@@ -1922,6 +2009,7 @@ export async function createConnectedService<
1922
2009
  prepare: (event, data) => outbound.prepare(event, data),
1923
2010
  publish: (event, data) => outbound.publish(event, data),
1924
2011
  publishPrepared: (event) => outbound.publishPrepared(event),
2012
+ stopEventListeners: () => outbound.stopEventListeners(),
1925
2013
  get kv() {
1926
2014
  return getHandlerResources().kv;
1927
2015
  },
@@ -2081,7 +2169,8 @@ function isTerminalJobState(
2081
2169
  state: string,
2082
2170
  ): state is TerminalJob<unknown, unknown>["state"] {
2083
2171
  return state === "completed" || state === "failed" || state === "cancelled" ||
2084
- state === "expired" || state === "dead" || state === "dismissed";
2172
+ state === "expired" || state === "skipped" || state === "stale" ||
2173
+ state === "dead" || state === "dismissed";
2085
2174
  }
2086
2175
 
2087
2176
  function isTerminalJobSnapshot<TPayload, TResult>(
@@ -2202,6 +2291,9 @@ function snapshotFromLifecycleEvent<TPayload, TResult>(
2202
2291
  case "failed":
2203
2292
  case "cancelled":
2204
2293
  case "expired":
2294
+ case "skipped":
2295
+ case "stale":
2296
+ case "staleCompletionIgnored":
2205
2297
  case "dead":
2206
2298
  case "dismissed":
2207
2299
  return event.error === undefined ? base : {
@@ -2359,8 +2451,8 @@ function createJobLifecycleTracker(nc: NatsConnection): JobLifecycleTracker {
2359
2451
  function createJobRef<TPayload, TResult>(args: {
2360
2452
  nc: NatsConnection;
2361
2453
  queueType: string;
2362
- jobsBinding: ResourceBindingJobs;
2363
- queueBinding: ResourceBindingJobsQueue;
2454
+ jobsBinding: JobsBinding;
2455
+ queueBinding: JobsQueueBinding;
2364
2456
  seed: JobSnapshot<TPayload, TResult>;
2365
2457
  lifecycle: JobLifecycleTracker;
2366
2458
  }): JobRef<TPayload, TResult> {
@@ -2467,6 +2559,10 @@ function createJobsFacade<
2467
2559
  const handlers = new Map<string, RegisteredJobHandler<unknown, unknown>>();
2468
2560
  const jobsFacade: Record<string, unknown> = {};
2469
2561
  const lifecycle = createJobLifecycleTracker(args.nc);
2562
+ const keyCoordinator = createNatsJobKeyCoordinator(args.nc);
2563
+ const jobsBinding = args.jobsBinding
2564
+ ? normalizeResourceJobsBinding(args.jobsBinding)
2565
+ : undefined;
2470
2566
  let activeHost: JobWorkerHostAdapter | undefined;
2471
2567
  let startupPromise:
2472
2568
  | Promise<Result<JobWorkerHostAdapter, BaseError>>
@@ -2474,14 +2570,13 @@ function createJobsFacade<
2474
2570
  let stopPromise: Promise<Result<void, BaseError>> | undefined;
2475
2571
 
2476
2572
  for (const queueType of Object.keys(args.contractJobs ?? {})) {
2477
- const queueBinding = args.jobsBinding?.queues[queueType];
2573
+ const queueBinding = jobsBinding?.queues[queueType];
2478
2574
  if (queueBinding) lifecycle.watch(queueBinding);
2479
2575
 
2480
2576
  jobsFacade[queueType] = {
2481
2577
  create: (payload) =>
2482
2578
  AsyncResult.from((async () => {
2483
2579
  try {
2484
- const jobsBinding = args.jobsBinding;
2485
2580
  if (!jobsBinding) {
2486
2581
  return Result.err(
2487
2582
  toUnexpectedError(new Error("Jobs bindings are unavailable")),
@@ -2499,6 +2594,7 @@ function createJobsFacade<
2499
2594
  const manager = new InternalJobManager<unknown, unknown>({
2500
2595
  nc: args.nc,
2501
2596
  jobs: jobsBinding,
2597
+ keyCoordinator,
2502
2598
  });
2503
2599
  await args.nc.flush();
2504
2600
  const created = await manager.create(queueType, payload);
@@ -2510,6 +2606,67 @@ function createJobsFacade<
2510
2606
  seed: created as JobSnapshot<unknown, unknown>,
2511
2607
  lifecycle,
2512
2608
  }));
2609
+ } catch (cause) {
2610
+ if (cause instanceof JobNotEnqueuedError) {
2611
+ return Result.err(cause);
2612
+ }
2613
+ return Result.err(toUnexpectedError(cause));
2614
+ }
2615
+ })()),
2616
+ submit: (payload) =>
2617
+ AsyncResult.from((async () => {
2618
+ try {
2619
+ if (!jobsBinding) {
2620
+ return Result.err(
2621
+ toUnexpectedError(new Error("Jobs bindings are unavailable")),
2622
+ );
2623
+ }
2624
+ const queueBinding = jobsBinding.queues[queueType];
2625
+ if (!queueBinding) {
2626
+ return Result.err(toUnexpectedError(
2627
+ new Error(
2628
+ `Jobs binding for queue '${queueType}' is unavailable`,
2629
+ ),
2630
+ ));
2631
+ }
2632
+
2633
+ const manager = new InternalJobManager<unknown, unknown>({
2634
+ nc: args.nc,
2635
+ jobs: jobsBinding,
2636
+ keyCoordinator,
2637
+ });
2638
+ await args.nc.flush();
2639
+ const outcome = await manager.submit(queueType, payload);
2640
+ if (outcome.kind === "accepted") {
2641
+ return Result.ok({
2642
+ kind: "accepted",
2643
+ key: outcome.key,
2644
+ ref: createJobRef({
2645
+ nc: args.nc,
2646
+ queueType,
2647
+ jobsBinding,
2648
+ queueBinding,
2649
+ seed: outcome.job as JobSnapshot<unknown, unknown>,
2650
+ lifecycle,
2651
+ }),
2652
+ });
2653
+ }
2654
+ if (outcome.kind === "replaced") {
2655
+ return Result.ok({
2656
+ kind: "replaced",
2657
+ key: outcome.key,
2658
+ replaced: outcome.replaced,
2659
+ ref: createJobRef({
2660
+ nc: args.nc,
2661
+ queueType,
2662
+ jobsBinding,
2663
+ queueBinding,
2664
+ seed: outcome.job as JobSnapshot<unknown, unknown>,
2665
+ lifecycle,
2666
+ }),
2667
+ });
2668
+ }
2669
+ return Result.ok(outcome);
2513
2670
  } catch (cause) {
2514
2671
  return Result.err(toUnexpectedError(cause));
2515
2672
  }
@@ -2554,7 +2711,7 @@ function createJobsFacade<
2554
2711
  return Result.ok(host);
2555
2712
  }
2556
2713
 
2557
- if (!args.jobsBinding || !args.workStream) {
2714
+ if (!jobsBinding || !args.workStream) {
2558
2715
  return Result.err(toUnexpectedError(
2559
2716
  new Error(
2560
2717
  "Jobs infrastructure bindings are unavailable for this service",
@@ -2562,7 +2719,6 @@ function createJobsFacade<
2562
2719
  ));
2563
2720
  }
2564
2721
 
2565
- const jobsBinding = args.jobsBinding;
2566
2722
  const workStream = args.workStream;
2567
2723
 
2568
2724
  const hosts = [] as Array<{ stop(): Promise<void> }>;
@@ -2582,6 +2738,7 @@ function createJobsFacade<
2582
2738
  const manager = new InternalJobManager<unknown, unknown>({
2583
2739
  nc: args.nc,
2584
2740
  jobs: jobsBinding,
2741
+ keyCoordinator,
2585
2742
  });
2586
2743
  const host = await startNatsWorkerHostFromBinding<unknown>({
2587
2744
  jobs: jobsBinding,
@@ -2750,6 +2907,7 @@ function createBoundJobsFacade<
2750
2907
  if (!queue) continue;
2751
2908
  boundJobs[queueType] = {
2752
2909
  create: (payload) => queue.create(payload),
2910
+ submit: (payload) => queue.submit(payload),
2753
2911
  handle: (handler) =>
2754
2912
  queue.handle(({ job, client }) =>
2755
2913
  handler({
@@ -3339,6 +3497,7 @@ export class TrellisService<
3339
3497
  async stop(): Promise<void> {
3340
3498
  this.#stopPromise ??= (async () => {
3341
3499
  this.connection.stopObserving();
3500
+ this.#handlerTrellis.stopEventListeners();
3342
3501
 
3343
3502
  try {
3344
3503
  await this.#stopHealthPublishing();
@@ -0,0 +1,125 @@
1
+ import "../_dnt.polyfills.js";
2
+ import { type SQL, sql } from "drizzle-orm";
3
+ import type { SqlExecutor, SqlRow } from "./outbox_inbox.js";
4
+
5
+ /**
6
+ * Structural Drizzle SQLite database or transaction shape accepted by Trellis
7
+ * SQL outbox helpers.
8
+ */
9
+ export type DrizzleSqlDatabase = {
10
+ /** Runs a SQL statement that returns rows. */
11
+ all(query: SQL): Promise<readonly SqlRow[]>;
12
+ /** Runs a SQL statement that does not return rows. */
13
+ run(query: SQL): Promise<unknown>;
14
+ };
15
+
16
+ /**
17
+ * Adapts a caller-owned Drizzle SQLite database or transaction to Trellis'
18
+ * generic `SqlExecutor` interface.
19
+ */
20
+ export function createDrizzleSqlExecutor(
21
+ database: DrizzleSqlDatabase,
22
+ ): SqlExecutor {
23
+ return {
24
+ query(statement, params) {
25
+ return database.all(bindDrizzleSqlStatement(statement, params));
26
+ },
27
+ async execute(statement, params) {
28
+ await database.run(bindDrizzleSqlStatement(statement, params));
29
+ },
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Converts a Trellis SQL statement with positional placeholders into a Drizzle
35
+ * `SQL` object with parameters bound in order.
36
+ *
37
+ * Supports SQLite-style `?` placeholders and PostgreSQL-style `$1`, `$2`, ...
38
+ * placeholders. A single statement must not mix both styles.
39
+ */
40
+ export function bindDrizzleSqlStatement(
41
+ statement: string,
42
+ params: readonly unknown[],
43
+ ): SQL {
44
+ const matches = [...statement.matchAll(/\?|\$(\d+)/g)];
45
+ const hasQuestionPlaceholders = matches.some((match) => match[0] === "?");
46
+ const hasNumberedPlaceholders = matches.some((match) =>
47
+ match[1] !== undefined
48
+ );
49
+
50
+ if (hasQuestionPlaceholders && hasNumberedPlaceholders) {
51
+ throw new Error(
52
+ "SQL statement cannot mix ? and $n placeholders for Drizzle binding",
53
+ );
54
+ }
55
+
56
+ if (hasQuestionPlaceholders) {
57
+ validateQuestionPlaceholders(matches.length, params.length);
58
+ } else if (hasNumberedPlaceholders) {
59
+ validateNumberedPlaceholders(matches, params.length);
60
+ } else if (params.length !== 0) {
61
+ throw new Error(
62
+ `SQL statement has no placeholders but received ${params.length} parameters`,
63
+ );
64
+ }
65
+
66
+ const chunks: SQL[] = [];
67
+ let cursor = 0;
68
+ let nextQuestionParam = 0;
69
+ for (const match of matches) {
70
+ const matchIndex = match.index;
71
+ chunks.push(sql.raw(statement.slice(cursor, matchIndex)));
72
+ if (match[0] === "?") {
73
+ chunks.push(sql`${params[nextQuestionParam]}`);
74
+ nextQuestionParam += 1;
75
+ } else {
76
+ const index = Number(match[1]) - 1;
77
+ chunks.push(sql`${params[index]}`);
78
+ }
79
+ cursor = matchIndex + match[0].length;
80
+ }
81
+ chunks.push(sql.raw(statement.slice(cursor)));
82
+
83
+ return sql.join(chunks);
84
+ }
85
+
86
+ function validateQuestionPlaceholders(
87
+ placeholders: number,
88
+ paramCount: number,
89
+ ): void {
90
+ if (placeholders !== paramCount) {
91
+ throw new Error(
92
+ `SQL statement expected ${placeholders} parameters for ? placeholders, received ${paramCount}`,
93
+ );
94
+ }
95
+ }
96
+
97
+ function validateNumberedPlaceholders(
98
+ matches: RegExpMatchArray[],
99
+ paramCount: number,
100
+ ): void {
101
+ const referenced = new Set<number>();
102
+ for (const match of matches) {
103
+ if (match[1] === undefined) continue;
104
+ const index = Number(match[1]);
105
+ if (index < 1) {
106
+ throw new Error("PostgreSQL SQL placeholder indexes must start at $1");
107
+ }
108
+ referenced.add(index);
109
+ }
110
+
111
+ const maxIndex = Math.max(...referenced);
112
+ if (maxIndex !== paramCount) {
113
+ throw new Error(
114
+ `SQL statement expected ${maxIndex} parameters for $n placeholders, received ${paramCount}`,
115
+ );
116
+ }
117
+
118
+ for (let index = 1; index <= maxIndex; index += 1) {
119
+ if (!referenced.has(index)) {
120
+ throw new Error(
121
+ `SQL statement is missing PostgreSQL placeholder $${index}`,
122
+ );
123
+ }
124
+ }
125
+ }
@@ -28,7 +28,6 @@ export {
28
28
  export { mountStandardHealthRpc } from "../server/health_rpc.js";
29
29
  export {
30
30
  HealthCheckResultSchema,
31
- HealthHeartbeatHeaderSchema,
32
31
  HealthHeartbeatSchema,
33
32
  HealthHeartbeatServiceSchema,
34
33
  HealthInfoSchema,
@@ -698,14 +698,13 @@ export async function dispatchOutbox(
698
698
  export function outboxMessageToPrepared(
699
699
  message: OutboxMessage,
700
700
  ): PreparedTrellisEvent {
701
+ const payload = JSON.parse(message.payload) as Record<string, unknown>;
702
+ const header = eventHeaderFromMessage(message.headers);
701
703
  return Object.freeze({
702
704
  event: message.event,
703
705
  subject: message.subject,
704
- payload: Object.freeze(
705
- JSON.parse(message.payload) as Record<string, unknown> & {
706
- header: { id: string; time: string };
707
- },
708
- ),
706
+ header: Object.freeze(header),
707
+ payload: Object.freeze(payload),
709
708
  encodedPayload: message.payload,
710
709
  headers: Object.freeze({ ...message.headers }),
711
710
  });
@@ -713,7 +712,18 @@ export function outboxMessageToPrepared(
713
712
 
714
713
  function messageId(event: PreparedTrellisEvent): string {
715
714
  return event.headers["Nats-Msg-Id"] ?? event.headers["nats-msg-id"] ??
716
- event.payload.header.id;
715
+ event.header.id;
716
+ }
717
+
718
+ function eventHeaderFromMessage(
719
+ headers: Record<string, string>,
720
+ ): { id: string; time: string } {
721
+ const id = headers["Nats-Msg-Id"] ?? headers["nats-msg-id"];
722
+ const time = headers["Trellis-Event-Time"] ?? headers["trellis-event-time"];
723
+ return {
724
+ id: typeof id === "string" ? id : "",
725
+ time: typeof time === "string" ? time : new Date(0).toISOString(),
726
+ };
717
727
  }
718
728
 
719
729
  function rowToOutboxMessage(row: SqlRow): OutboxMessage {