@oatscenter/trellis 0.100.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 (264) hide show
  1. package/README.md +59 -0
  2. package/auth/authorization/client_context.d.ts +61 -0
  3. package/auth/authorization/client_context.js +266 -0
  4. package/auth/authorization/nats_registry.d.ts +41 -0
  5. package/auth/authorization/nats_registry.js +283 -0
  6. package/auth/authorization/provider_cache.d.ts +90 -0
  7. package/auth/authorization/provider_cache.js +783 -0
  8. package/auth/authorization/refresh.d.ts +46 -0
  9. package/auth/authorization/refresh.js +246 -0
  10. package/auth/authorization/types.d.ts +207 -0
  11. package/auth/authorization/types.js +60 -0
  12. package/auth/authorization_context.d.ts +11 -0
  13. package/auth/authorization_context.js +10 -0
  14. package/auth/browser/flow_types.d.ts +84 -0
  15. package/auth/browser/flow_types.js +70 -0
  16. package/auth/browser/portal.d.ts +21 -0
  17. package/auth/browser/portal.js +307 -0
  18. package/auth/browser/storage.d.ts +26 -0
  19. package/auth/browser/storage.js +224 -0
  20. package/auth/browser.d.ts +11 -0
  21. package/auth/browser.js +10 -0
  22. package/auth/browser_recovery.d.ts +13 -0
  23. package/auth/browser_recovery.js +38 -0
  24. package/auth/device_activation.d.ts +134 -0
  25. package/auth/device_activation.js +376 -0
  26. package/auth/http_error.d.ts +12 -0
  27. package/auth/http_error.js +32 -0
  28. package/auth/keys.d.ts +5 -0
  29. package/auth/keys.js +63 -0
  30. package/auth/mod.d.ts +16 -0
  31. package/auth/mod.js +16 -0
  32. package/auth/proof.d.ts +31 -0
  33. package/auth/proof.js +95 -0
  34. package/auth/protocol_wasm/trellis_protocol_wasm.d.ts +138 -0
  35. package/auth/protocol_wasm/trellis_protocol_wasm.js +605 -0
  36. package/auth/protocol_wasm/trellis_protocol_wasm_bg.wasm +0 -0
  37. package/auth/protocol_wasm/trellis_protocol_wasm_bytes.d.ts +1 -0
  38. package/auth/protocol_wasm/trellis_protocol_wasm_bytes.js +2 -0
  39. package/auth/protocol_wasm.d.ts +179 -0
  40. package/auth/protocol_wasm.js +133 -0
  41. package/auth/session_auth.d.ts +30 -0
  42. package/auth/session_auth.js +77 -0
  43. package/auth/session_proof.d.ts +53 -0
  44. package/auth/session_proof.js +43 -0
  45. package/auth/time.d.ts +6 -0
  46. package/auth/time.js +7 -0
  47. package/auth/trellis_id.d.ts +1 -0
  48. package/auth/trellis_id.js +5 -0
  49. package/auth/utils.d.ts +10 -0
  50. package/auth/utils.js +69 -0
  51. package/auth.d.ts +1 -0
  52. package/auth.js +1 -0
  53. package/caller.d.ts +111 -0
  54. package/caller.js +85 -0
  55. package/client.d.ts +14 -0
  56. package/client.js +1 -0
  57. package/client_connect.d.ts +143 -0
  58. package/client_connect.js +1097 -0
  59. package/codec.d.ts +13 -0
  60. package/codec.js +192 -0
  61. package/connection.d.ts +107 -0
  62. package/connection.js +396 -0
  63. package/device/activation.d.ts +42 -0
  64. package/device/activation.js +319 -0
  65. package/device.d.ts +80 -0
  66. package/device.js +687 -0
  67. package/env.d.ts +2 -0
  68. package/env.js +10 -0
  69. package/errors/AuthError.d.ts +30 -0
  70. package/errors/AuthError.js +34 -0
  71. package/errors/KVError.d.ts +30 -0
  72. package/errors/KVError.js +36 -0
  73. package/errors/OperationLifecycleError.d.ts +110 -0
  74. package/errors/OperationLifecycleError.js +142 -0
  75. package/errors/RemoteError.d.ts +61 -0
  76. package/errors/RemoteError.js +72 -0
  77. package/errors/SchemaValidationError.d.ts +48 -0
  78. package/errors/SchemaValidationError.js +49 -0
  79. package/errors/StoreError.d.ts +21 -0
  80. package/errors/StoreError.js +31 -0
  81. package/errors/TransferError.d.ts +21 -0
  82. package/errors/TransferError.js +31 -0
  83. package/errors/TransportError.d.ts +29 -0
  84. package/errors/TransportError.js +33 -0
  85. package/errors/TrellisError.d.ts +15 -0
  86. package/errors/TrellisError.js +15 -0
  87. package/errors/ValidationError.d.ts +50 -0
  88. package/errors/ValidationError.js +59 -0
  89. package/errors/index.d.ts +254 -0
  90. package/errors/index.js +238 -0
  91. package/generated.d.ts +4 -0
  92. package/generated.js +3 -0
  93. package/generated_support.d.ts +120 -0
  94. package/generated_support.js +359 -0
  95. package/globals.d.ts +9 -0
  96. package/globals.js +16 -0
  97. package/health_transport.d.ts +18 -0
  98. package/health_transport.js +32 -0
  99. package/helpers.d.ts +11 -0
  100. package/helpers.js +53 -0
  101. package/index.d.ts +2 -0
  102. package/index.js +2 -0
  103. package/internal_sdk/generated/apis/auth/mod.d.ts +805 -0
  104. package/internal_sdk/generated/apis/auth/mod.js +958 -0
  105. package/internal_sdk/generated/apis/core/mod.d.ts +79 -0
  106. package/internal_sdk/generated/apis/core/mod.js +93 -0
  107. package/internal_sdk/generated/apis/events/mod.d.ts +224 -0
  108. package/internal_sdk/generated/apis/events/mod.js +313 -0
  109. package/internal_sdk/generated/apis/health/mod.d.ts +106 -0
  110. package/internal_sdk/generated/apis/health/mod.js +132 -0
  111. package/internal_sdk/generated/apis/index.d.ts +7 -0
  112. package/internal_sdk/generated/apis/index.js +8 -0
  113. package/internal_sdk/generated/apis/jobs/mod.d.ts +176 -0
  114. package/internal_sdk/generated/apis/jobs/mod.js +209 -0
  115. package/internal_sdk/generated/apis/state/mod.d.ts +104 -0
  116. package/internal_sdk/generated/apis/state/mod.js +139 -0
  117. package/internal_sdk/generated/index.d.ts +4 -0
  118. package/internal_sdk/generated/index.js +5 -0
  119. package/internal_sdk/generated/package.json +18 -0
  120. package/internal_sdk/generated/participants/cli/mod.d.ts +402 -0
  121. package/internal_sdk/generated/participants/cli/mod.js +460 -0
  122. package/internal_sdk/generated/participants/cli/types.d.ts +2 -0
  123. package/internal_sdk/generated/participants/cli/types.js +3 -0
  124. package/internal_sdk/generated/participants/console/mod.d.ts +489 -0
  125. package/internal_sdk/generated/participants/console/mod.js +579 -0
  126. package/internal_sdk/generated/participants/console/types.d.ts +2 -0
  127. package/internal_sdk/generated/participants/console/types.js +3 -0
  128. package/internal_sdk/generated/participants/eventsRuntime/mod.d.ts +64 -0
  129. package/internal_sdk/generated/participants/eventsRuntime/mod.js +42 -0
  130. package/internal_sdk/generated/participants/eventsRuntime/types.d.ts +2 -0
  131. package/internal_sdk/generated/participants/eventsRuntime/types.js +3 -0
  132. package/internal_sdk/generated/participants/healthRuntime/mod.d.ts +58 -0
  133. package/internal_sdk/generated/participants/healthRuntime/mod.js +36 -0
  134. package/internal_sdk/generated/participants/healthRuntime/types.d.ts +2 -0
  135. package/internal_sdk/generated/participants/healthRuntime/types.js +3 -0
  136. package/internal_sdk/generated/participants/index.d.ts +8 -0
  137. package/internal_sdk/generated/participants/index.js +9 -0
  138. package/internal_sdk/generated/participants/jobsRuntime/mod.d.ts +64 -0
  139. package/internal_sdk/generated/participants/jobsRuntime/mod.js +42 -0
  140. package/internal_sdk/generated/participants/jobsRuntime/types.d.ts +2 -0
  141. package/internal_sdk/generated/participants/jobsRuntime/types.js +3 -0
  142. package/internal_sdk/generated/participants/platform/mod.d.ts +164 -0
  143. package/internal_sdk/generated/participants/platform/mod.js +149 -0
  144. package/internal_sdk/generated/participants/platform/types.d.ts +7 -0
  145. package/internal_sdk/generated/participants/platform/types.js +5 -0
  146. package/internal_sdk/generated/participants/portal/mod.d.ts +131 -0
  147. package/internal_sdk/generated/participants/portal/mod.js +109 -0
  148. package/internal_sdk/generated/participants/portal/types.d.ts +2 -0
  149. package/internal_sdk/generated/participants/portal/types.js +3 -0
  150. package/internal_sdk/generated/types/_internal/p0.d.ts +13511 -0
  151. package/internal_sdk/generated/types/_internal/p0.js +6105 -0
  152. package/internal_sdk/generated/types/index.d.ts +2951 -0
  153. package/internal_sdk/generated/types/index.js +1477 -0
  154. package/jobs.d.ts +309 -0
  155. package/jobs.js +389 -0
  156. package/kv.d.ts +85 -0
  157. package/kv.js +330 -0
  158. package/operations.d.ts +189 -0
  159. package/operations.js +847 -0
  160. package/package.json +73 -0
  161. package/pagination.d.ts +25 -0
  162. package/pagination.js +117 -0
  163. package/participant.d.ts +6 -0
  164. package/participant.js +6 -0
  165. package/participant_runtime/api.d.ts +131 -0
  166. package/participant_runtime/api.js +42 -0
  167. package/participant_runtime/json.d.ts +16 -0
  168. package/participant_runtime/json.js +193 -0
  169. package/participant_runtime/metadata.d.ts +34 -0
  170. package/participant_runtime/metadata.js +4 -0
  171. package/participant_runtime/participant.d.ts +200 -0
  172. package/participant_runtime/participant.js +330 -0
  173. package/participant_runtime/schemas.d.ts +347 -0
  174. package/participant_runtime/schemas.js +211 -0
  175. package/participant_runtime/surface_names.d.ts +16 -0
  176. package/participant_runtime/surface_names.js +34 -0
  177. package/portable.d.ts +30 -0
  178. package/portable.js +14 -0
  179. package/provider.d.ts +69 -0
  180. package/provider.js +68 -0
  181. package/runtime_transport.d.ts +25 -0
  182. package/runtime_transport.js +48 -0
  183. package/service/mod.d.ts +14 -0
  184. package/service/mod.js +13 -0
  185. package/service/outbox_inbox.d.ts +259 -0
  186. package/service/outbox_inbox.js +908 -0
  187. package/service/runtime/bootstrap.d.ts +56 -0
  188. package/service/runtime/bootstrap.js +226 -0
  189. package/service/runtime/core.d.ts +46 -0
  190. package/service/runtime/core.js +1747 -0
  191. package/service/runtime/health.d.ts +97 -0
  192. package/service/runtime/health.js +153 -0
  193. package/service/runtime/health_schemas.d.ts +23 -0
  194. package/service/runtime/health_schemas.js +20 -0
  195. package/service/runtime/internal_jobs/active-job.d.ts +40 -0
  196. package/service/runtime/internal_jobs/active-job.js +91 -0
  197. package/service/runtime/internal_jobs/bindings.d.ts +40 -0
  198. package/service/runtime/internal_jobs/bindings.js +1 -0
  199. package/service/runtime/internal_jobs/cancellation-registry.d.ts +13 -0
  200. package/service/runtime/internal_jobs/cancellation-registry.js +55 -0
  201. package/service/runtime/internal_jobs/heartbeat.d.ts +33 -0
  202. package/service/runtime/internal_jobs/heartbeat.js +65 -0
  203. package/service/runtime/internal_jobs/job-manager.d.ts +126 -0
  204. package/service/runtime/internal_jobs/job-manager.js +821 -0
  205. package/service/runtime/internal_jobs/key-coordinator.d.ts +280 -0
  206. package/service/runtime/internal_jobs/key-coordinator.js +747 -0
  207. package/service/runtime/internal_jobs/projection.d.ts +3 -0
  208. package/service/runtime/internal_jobs/projection.js +42 -0
  209. package/service/runtime/internal_jobs/runtime-worker.d.ts +126 -0
  210. package/service/runtime/internal_jobs/runtime-worker.js +454 -0
  211. package/service/runtime/internal_jobs/types.d.ts +246 -0
  212. package/service/runtime/internal_jobs/types.js +163 -0
  213. package/service/runtime/logger.d.ts +2 -0
  214. package/service/runtime/logger.js +7 -0
  215. package/service/runtime/runtime.d.ts +16 -0
  216. package/service/runtime/runtime.js +1 -0
  217. package/service/runtime/service.d.ts +372 -0
  218. package/service/runtime/service.js +1896 -0
  219. package/service/runtime/transfer/download.d.ts +27 -0
  220. package/service/runtime/transfer/download.js +1 -0
  221. package/service/runtime/transfer/protocol.d.ts +15 -0
  222. package/service/runtime/transfer/protocol.js +46 -0
  223. package/service/runtime/transfer/queue.d.ts +18 -0
  224. package/service/runtime/transfer/queue.js +136 -0
  225. package/service/runtime/transfer/upload.d.ts +40 -0
  226. package/service/runtime/transfer/upload.js +27 -0
  227. package/service/runtime/transfer.d.ts +62 -0
  228. package/service/runtime/transfer.js +763 -0
  229. package/session.d.ts +862 -0
  230. package/session.js +3407 -0
  231. package/store.d.ts +71 -0
  232. package/store.js +412 -0
  233. package/store_entry.d.ts +20 -0
  234. package/store_entry.js +75 -0
  235. package/tasks.d.ts +10 -0
  236. package/tasks.js +28 -0
  237. package/telemetry/carrier.d.ts +8 -0
  238. package/telemetry/carrier.js +20 -0
  239. package/telemetry/core.d.ts +7 -0
  240. package/telemetry/core.js +14 -0
  241. package/telemetry/env.d.ts +2 -0
  242. package/telemetry/env.js +10 -0
  243. package/telemetry/init.d.ts +2 -0
  244. package/telemetry/init.js +7 -0
  245. package/telemetry/metrics.d.ts +81 -0
  246. package/telemetry/metrics.js +267 -0
  247. package/telemetry/mod.d.ts +13 -0
  248. package/telemetry/mod.js +8 -0
  249. package/telemetry/nats.d.ts +6 -0
  250. package/telemetry/nats.js +6 -0
  251. package/telemetry/result.d.ts +1 -0
  252. package/telemetry/result.js +18 -0
  253. package/telemetry/runtime.d.ts +1 -0
  254. package/telemetry/runtime.js +141 -0
  255. package/telemetry/trace.d.ts +4 -0
  256. package/telemetry/trace.js +26 -0
  257. package/telemetry/trellis.d.ts +1 -0
  258. package/telemetry/trellis.js +1 -0
  259. package/telemetry.d.ts +1 -0
  260. package/telemetry.js +1 -0
  261. package/transfer.d.ts +112 -0
  262. package/transfer.js +525 -0
  263. package/transfer_protocol.d.ts +1 -0
  264. package/transfer_protocol.js +13 -0
@@ -0,0 +1,821 @@
1
+ import { headers as natsHeaders } from "@nats-io/nats-core";
2
+ import { ulid } from "ulid";
3
+ import { getActiveJobSnapshot, JobNotEnqueuedError, runWithActiveJobContext, } from "../../../jobs.js";
4
+ import { createMapCarrier, injectTraceContext, } from "../../../telemetry/carrier.js";
5
+ import { recordTrellisError } from "../../../telemetry/mod.js";
6
+ import { ActiveJob, ActiveJobRuntimeError, JobCancellationToken, } from "./active-job.js";
7
+ import { normalizeJobKeyPolicy } from "./key-coordinator.js";
8
+ export class JobProcessError extends Error {
9
+ kind;
10
+ constructor(kind, message) {
11
+ super(message);
12
+ this.name = "JobProcessError";
13
+ this.kind = kind;
14
+ }
15
+ static retryable(message) {
16
+ return new JobProcessError("retryable", message);
17
+ }
18
+ static failed(message) {
19
+ return new JobProcessError("failed", message);
20
+ }
21
+ }
22
+ export class JobManager {
23
+ #context;
24
+ constructor(context) {
25
+ this.#context = context;
26
+ }
27
+ #meta() {
28
+ return {
29
+ nextJobId: this.#context.meta?.nextJobId ?? (() => ulid()),
30
+ nowIso: this.#context.meta?.nowIso ?? (() => new Date().toISOString()),
31
+ };
32
+ }
33
+ #getQueueBinding(type) {
34
+ const binding = this.#context.jobs?.queues[type];
35
+ if (!binding || !this.#context.jobs) {
36
+ throw new Error(`Missing jobs binding for queue '${type}'`);
37
+ }
38
+ return binding;
39
+ }
40
+ async #publishJobEvent(type, jobId, event, stableMessageId) {
41
+ const binding = this.#getQueueBinding(type);
42
+ const headers = headersFromJobContext(event.context, stableMessageId);
43
+ try {
44
+ await this.#context.nc.publish(`${binding.publishPrefix}.${jobId}.${event.eventType}`, new TextEncoder().encode(JSON.stringify(event)), { headers });
45
+ }
46
+ catch (error) {
47
+ recordTrellisError(error, {
48
+ surface: "job",
49
+ direction: "worker",
50
+ operation: type,
51
+ phase: "publish",
52
+ messagingSystem: "nats",
53
+ });
54
+ throw error;
55
+ }
56
+ }
57
+ async create(type, payload) {
58
+ const outcome = await this.#submit(type, payload, true);
59
+ if (outcome.kind === "accepted") {
60
+ return outcome.job;
61
+ }
62
+ if (outcome.kind === "coalesced") {
63
+ throw new JobNotEnqueuedError({
64
+ reason: "coalesced",
65
+ key: outcome.key,
66
+ active: 0,
67
+ queued: 1,
68
+ limit: 0,
69
+ existingJobId: outcome.existing.id,
70
+ });
71
+ }
72
+ if (outcome.kind === "replaced") {
73
+ throw new JobNotEnqueuedError({
74
+ reason: "queue-depth",
75
+ key: outcome.key,
76
+ active: 0,
77
+ queued: 1,
78
+ limit: 0,
79
+ existingJobId: outcome.replaced.id,
80
+ });
81
+ }
82
+ throw new JobNotEnqueuedError({
83
+ reason: outcome.reason,
84
+ key: outcome.key,
85
+ active: outcome.active,
86
+ queued: outcome.queued,
87
+ limit: outcome.limit,
88
+ });
89
+ }
90
+ async submit(type, payload) {
91
+ return await this.#submit(type, payload, false);
92
+ }
93
+ async createPrepared(submission) {
94
+ const outcome = await this.#submitPrepared(submission, true, true);
95
+ if (outcome.kind === "accepted") {
96
+ return outcome.job;
97
+ }
98
+ await this.#publishSkipped(submission.queue, { id: submission.jobId, context: submission.context }, submission.createdAt, `trellis-job-skipped:${submission.submissionId}:${submission.jobId}`, `job submission not enqueued: ${outcome.kind === "coalesced"
99
+ ? "coalesced"
100
+ : outcome.kind === "replaced"
101
+ ? "queue-depth"
102
+ : outcome.reason}`);
103
+ if (outcome.kind === "coalesced") {
104
+ throw new JobNotEnqueuedError({
105
+ reason: "coalesced",
106
+ key: outcome.key,
107
+ active: 0,
108
+ queued: 1,
109
+ limit: 0,
110
+ existingJobId: outcome.existing.id,
111
+ });
112
+ }
113
+ if (outcome.kind === "replaced") {
114
+ throw new JobNotEnqueuedError({
115
+ reason: "queue-depth",
116
+ key: outcome.key,
117
+ active: 0,
118
+ queued: 1,
119
+ limit: 0,
120
+ existingJobId: outcome.replaced.id,
121
+ });
122
+ }
123
+ throw new JobNotEnqueuedError({
124
+ reason: outcome.reason,
125
+ key: outcome.key,
126
+ active: outcome.active,
127
+ queued: outcome.queued,
128
+ limit: outcome.limit,
129
+ });
130
+ }
131
+ async submitPrepared(submission) {
132
+ const outcome = await this.#submitPrepared(submission, false, true);
133
+ if (outcome.kind === "rejected" || outcome.kind === "coalesced") {
134
+ await this.#publishSkipped(submission.queue, { id: submission.jobId, context: submission.context }, submission.createdAt, `trellis-job-skipped:${submission.submissionId}:${submission.jobId}`, `job submission not enqueued: ${outcome.kind === "coalesced" ? "coalesced" : outcome.reason}`);
135
+ }
136
+ return outcome;
137
+ }
138
+ async #submit(type, payload, strictCreate) {
139
+ const meta = this.#meta();
140
+ const now = meta.nowIso();
141
+ const id = meta.nextJobId();
142
+ const parent = getActiveJobSnapshot();
143
+ const context = createJobContext();
144
+ const serviceName = this.#context.jobs.serviceName;
145
+ const submission = prepareJobSubmission({
146
+ submissionId: id,
147
+ mode: strictCreate ? "create" : "submit",
148
+ service: serviceName,
149
+ queue: type,
150
+ jobId: id,
151
+ payload,
152
+ createdAt: now,
153
+ context,
154
+ parent,
155
+ });
156
+ return await this.#submitPrepared(submission, strictCreate);
157
+ }
158
+ async #submitPrepared(submission, strictCreate, stableMessageIds) {
159
+ const { queue: type, jobId: id, createdAt: now, context, service, payload, trigger = { kind: "serviceCode" }, lineage, } = submission;
160
+ const binding = this.#getQueueBinding(type);
161
+ const deadline = computeDeadline(now, binding.defaultDeadlineMs);
162
+ const job = {
163
+ id,
164
+ service,
165
+ type,
166
+ state: "pending",
167
+ context,
168
+ payload,
169
+ createdAt: now,
170
+ updatedAt: now,
171
+ tries: 0,
172
+ maxTries: binding.maxDeliver,
173
+ ...(deadline ? { deadline } : {}),
174
+ trigger,
175
+ ...(lineage ? { lineage } : {}),
176
+ };
177
+ const event = {
178
+ jobId: id,
179
+ service,
180
+ jobType: type,
181
+ eventType: "created",
182
+ state: "pending",
183
+ context,
184
+ tries: 0,
185
+ maxTries: binding.maxDeliver,
186
+ payload,
187
+ ...(deadline ? { deadline } : {}),
188
+ trigger,
189
+ ...(lineage ? { lineage } : {}),
190
+ timestamp: now,
191
+ };
192
+ const keyedPolicy = getKeyPolicy(binding);
193
+ if (keyedPolicy) {
194
+ const stableSubmissionId = stableMessageIds
195
+ ? submission.submissionId
196
+ : undefined;
197
+ const admission = await this.#admitKeyedCreate({
198
+ type,
199
+ job,
200
+ event,
201
+ policy: keyedPolicy,
202
+ strictCreate,
203
+ ...(stableSubmissionId ? { submissionId: stableSubmissionId } : {}),
204
+ });
205
+ if (admission.kind !== "accepted" && admission.kind !== "replaced") {
206
+ return admission;
207
+ }
208
+ if (strictCreate && admission.kind === "replaced") {
209
+ await this.#restoreReplacedKeyedReservation(job, admission.replaced, keyedPolicy, stableSubmissionId);
210
+ return {
211
+ kind: "rejected",
212
+ key: admission.key,
213
+ reason: "queue-depth",
214
+ active: admission.state.active.length,
215
+ queued: admission.state.queued.length,
216
+ limit: keyedPolicy.queue.maxQueuedPerKey,
217
+ };
218
+ }
219
+ if (admission.kind === "replaced") {
220
+ try {
221
+ const skippedMsgId = stableSubmissionId
222
+ ? `trellis-job-skipped:${stableSubmissionId}:${admission.replaced.id}`
223
+ : undefined;
224
+ await this.#publishSkipped(type, admission.replaced, now, skippedMsgId);
225
+ }
226
+ catch (error) {
227
+ await this.#restoreReplacedKeyedReservation(job, admission.replaced, keyedPolicy, stableSubmissionId);
228
+ throw error;
229
+ }
230
+ }
231
+ try {
232
+ const createdMsgId = stableSubmissionId
233
+ ? `trellis-job-created:${stableSubmissionId}`
234
+ : undefined;
235
+ await this.#publishJobEvent(type, id, event, createdMsgId);
236
+ }
237
+ catch (error) {
238
+ await this.#removeQueuedKeyedReservation(job, keyedPolicy, stableSubmissionId);
239
+ throw error;
240
+ }
241
+ if (admission.kind === "replaced") {
242
+ return {
243
+ kind: "replaced",
244
+ key: admission.key,
245
+ replaced: {
246
+ service: admission.replaced.service,
247
+ jobType: admission.replaced.jobType,
248
+ id: admission.replaced.id,
249
+ },
250
+ job,
251
+ };
252
+ }
253
+ return { kind: "accepted", job, key: admission.key };
254
+ }
255
+ const createdMsgId = stableMessageIds
256
+ ? `trellis-job-created:${submission.submissionId}`
257
+ : undefined;
258
+ await this.#publishJobEvent(type, id, event, createdMsgId);
259
+ return { kind: "accepted", job };
260
+ }
261
+ async #admitKeyedCreate(args) {
262
+ const coordinator = this.#context.keyCoordinator;
263
+ if (!coordinator) {
264
+ throw new Error(`Keyed jobs queue '${args.type}' requires the JOBS_KEYS coordinator`);
265
+ }
266
+ return await coordinator.admitCreate({
267
+ service: this.#coordinationService(),
268
+ jobType: args.type,
269
+ jobId: args.job.id,
270
+ payload: args.job.payload,
271
+ context: args.event.context,
272
+ createdAt: args.event.timestamp,
273
+ policy: args.policy,
274
+ strictCreate: args.strictCreate,
275
+ ...(args.submissionId ? { submissionId: args.submissionId } : {}),
276
+ });
277
+ }
278
+ async cleanupQueuedKeyedJob(job) {
279
+ const queue = this.#getQueueBinding(job.type);
280
+ const keyedPolicy = getKeyPolicy(queue);
281
+ if (!keyedPolicy)
282
+ return;
283
+ try {
284
+ await this.#removeQueuedKeyedReservation(job, keyedPolicy);
285
+ }
286
+ catch (cause) {
287
+ const coordinator = this.#context.keyCoordinator;
288
+ if (!coordinator?.removeQueuedJobById)
289
+ throw cause;
290
+ await coordinator.removeQueuedJobById({
291
+ service: this.#coordinationService(),
292
+ jobType: job.type,
293
+ jobId: job.id,
294
+ now: this.#meta().nowIso(),
295
+ });
296
+ }
297
+ }
298
+ async #removeQueuedKeyedReservation(job, policy, submissionId) {
299
+ const coordinator = this.#context.keyCoordinator;
300
+ if (!coordinator)
301
+ return;
302
+ await coordinator.removeQueuedJob({
303
+ service: this.#coordinationService(),
304
+ jobType: job.type,
305
+ jobId: job.id,
306
+ payload: job.payload,
307
+ now: this.#meta().nowIso(),
308
+ policy,
309
+ ...(submissionId ? { submissionId } : {}),
310
+ });
311
+ }
312
+ async #restoreReplacedKeyedReservation(job, replaced, policy, submissionId) {
313
+ const coordinator = this.#context.keyCoordinator;
314
+ if (!coordinator)
315
+ return;
316
+ await coordinator.restoreReplacedQueuedJob({
317
+ service: this.#coordinationService(),
318
+ jobType: job.type,
319
+ replacementJobId: job.id,
320
+ replaced,
321
+ payload: job.payload,
322
+ now: this.#meta().nowIso(),
323
+ policy,
324
+ ...(submissionId ? { submissionId } : {}),
325
+ });
326
+ }
327
+ async #publishSkipped(type, replaced, timestamp, stableMessageId, error = "replaced by newer keyed job") {
328
+ await this.#publishJobEvent(type, replaced.id, {
329
+ jobId: replaced.id,
330
+ service: this.#context.jobs.serviceName,
331
+ jobType: type,
332
+ eventType: "skipped",
333
+ state: "skipped",
334
+ previousState: "pending",
335
+ context: replaced.context,
336
+ tries: 0,
337
+ error,
338
+ timestamp,
339
+ }, stableMessageId);
340
+ }
341
+ async processWithHeartbeat(job, cancellation, heartbeat, handler, metadata = {}, validation = {}) {
342
+ const tries = job.tries + 1;
343
+ const queue = this.#getQueueBinding(job.type);
344
+ const keyedPolicy = getKeyPolicy(queue);
345
+ let lease;
346
+ let staleSlots = [];
347
+ if (keyedPolicy) {
348
+ const coordinator = this.#context.keyCoordinator;
349
+ if (!coordinator) {
350
+ throw new Error(`Keyed jobs queue '${job.type}' requires the JOBS_KEYS coordinator`);
351
+ }
352
+ const acquired = await coordinator.acquireActiveSlot({
353
+ service: this.#coordinationService(),
354
+ jobType: job.type,
355
+ jobId: job.id,
356
+ payload: job.payload,
357
+ context: job.context,
358
+ lifecycleState: metadata.latestState ?? job.state,
359
+ tries,
360
+ instanceId: metadata.instanceId ?? "unknown",
361
+ now: this.#meta().nowIso(),
362
+ policy: keyedPolicy,
363
+ ...(metadata.workEventType
364
+ ? { workEventType: metadata.workEventType }
365
+ : {}),
366
+ });
367
+ if (acquired.kind === "blocked") {
368
+ return { outcome: "deferred", tries, reason: acquired.reason };
369
+ }
370
+ lease = {
371
+ key: acquired.key,
372
+ keyHash: acquired.keyHash,
373
+ slotToken: acquired.slotToken,
374
+ policy: keyedPolicy,
375
+ };
376
+ staleSlots = acquired.stale;
377
+ }
378
+ try {
379
+ for (const stale of staleSlots) {
380
+ await this.#publishJobEvent(job.type, stale.jobId, {
381
+ jobId: stale.jobId,
382
+ service: job.service,
383
+ jobType: job.type,
384
+ eventType: "stale",
385
+ state: "stale",
386
+ previousState: "active",
387
+ context: stale.context,
388
+ tries: stale.tries,
389
+ error: "keyed job lease expired",
390
+ timestamp: this.#meta().nowIso(),
391
+ });
392
+ }
393
+ await this.#publishJobEvent(job.type, job.id, {
394
+ jobId: job.id,
395
+ service: job.service,
396
+ jobType: job.type,
397
+ eventType: "started",
398
+ state: "active",
399
+ previousState: job.state,
400
+ context: job.context,
401
+ tries,
402
+ timestamp: this.#meta().nowIso(),
403
+ });
404
+ }
405
+ catch (error) {
406
+ await this.#releaseKeyedSlotAfterPublishFailure(job, lease, error);
407
+ throw error;
408
+ }
409
+ try {
410
+ const result = await this.withActiveJobAndHeartbeat({ ...job, state: "active", tries }, cancellation, heartbeat, handler, metadata, lease);
411
+ if (cancellation.isHostShutdown() || cancellation.isLeaseLost()) {
412
+ await this.#releaseKeyedSlot(job, lease);
413
+ return { outcome: "interrupted", tries };
414
+ }
415
+ if (cancellation.isCancelled()) {
416
+ await this.#releaseKeyedSlot(job, lease);
417
+ return { outcome: "cancelled", tries };
418
+ }
419
+ try {
420
+ await validation.validateResult?.(result, {
421
+ ...job,
422
+ state: "active",
423
+ tries,
424
+ });
425
+ }
426
+ catch (error) {
427
+ throw JobProcessError.failed(error instanceof Error ? error.message : String(error));
428
+ }
429
+ const release = await this.#releaseKeyedSlot(job, lease);
430
+ if (release === "staleCompletion") {
431
+ await this.#publishStaleCompletionIgnored(job, tries);
432
+ return { outcome: "stale_completion_ignored", tries };
433
+ }
434
+ await this.#publishJobEvent(job.type, job.id, {
435
+ jobId: job.id,
436
+ service: job.service,
437
+ jobType: job.type,
438
+ eventType: "completed",
439
+ state: "completed",
440
+ previousState: "active",
441
+ context: job.context,
442
+ tries,
443
+ result,
444
+ timestamp: this.#meta().nowIso(),
445
+ });
446
+ return { outcome: "completed", tries, result };
447
+ }
448
+ catch (error) {
449
+ if (cancellation.isHostShutdown() || cancellation.isLeaseLost()) {
450
+ await this.#releaseKeyedSlot(job, lease);
451
+ return { outcome: "interrupted", tries };
452
+ }
453
+ if (cancellation.isCancelled()) {
454
+ await this.#releaseKeyedSlot(job, lease);
455
+ return { outcome: "cancelled", tries };
456
+ }
457
+ if (error instanceof JobProcessError) {
458
+ const detail = error.message;
459
+ if (error.kind === "retryable") {
460
+ recordTrellisError(error, {
461
+ surface: "job",
462
+ direction: "worker",
463
+ operation: job.type,
464
+ phase: "handler_result",
465
+ });
466
+ const release = await this.#releaseKeyedSlot(job, lease);
467
+ if (release === "staleCompletion") {
468
+ await this.#publishStaleCompletionIgnored(job, tries);
469
+ return { outcome: "stale_completion_ignored", tries };
470
+ }
471
+ await this.#publishJobEvent(job.type, job.id, {
472
+ jobId: job.id,
473
+ service: job.service,
474
+ jobType: job.type,
475
+ eventType: "retry",
476
+ state: "retry",
477
+ previousState: "active",
478
+ context: job.context,
479
+ tries,
480
+ error: detail,
481
+ timestamp: this.#meta().nowIso(),
482
+ });
483
+ return { outcome: "retry", tries, error: detail };
484
+ }
485
+ recordTrellisError(error, {
486
+ surface: "job",
487
+ direction: "worker",
488
+ operation: job.type,
489
+ phase: "handler_result",
490
+ });
491
+ const release = await this.#releaseKeyedSlot(job, lease);
492
+ if (release === "staleCompletion") {
493
+ await this.#publishStaleCompletionIgnored(job, tries);
494
+ return { outcome: "stale_completion_ignored", tries };
495
+ }
496
+ await this.#publishJobEvent(job.type, job.id, {
497
+ jobId: job.id,
498
+ service: job.service,
499
+ jobType: job.type,
500
+ eventType: "failed",
501
+ state: "failed",
502
+ previousState: "active",
503
+ context: job.context,
504
+ tries,
505
+ error: detail,
506
+ timestamp: this.#meta().nowIso(),
507
+ });
508
+ return { outcome: "failed", tries, error: detail };
509
+ }
510
+ recordTrellisError(error, {
511
+ surface: "job",
512
+ direction: "worker",
513
+ operation: job.type,
514
+ phase: "runtime",
515
+ });
516
+ throw error;
517
+ }
518
+ }
519
+ async emitProgress(job, progress) {
520
+ this.#getQueueBinding(job.type);
521
+ if (job.state !== "active") {
522
+ throw new Error(`Cannot emit progress for job '${job.id}' in state '${job.state}'`);
523
+ }
524
+ await this.#publishJobEvent(job.type, job.id, {
525
+ jobId: job.id,
526
+ service: job.service,
527
+ jobType: job.type,
528
+ eventType: "progress",
529
+ state: "active",
530
+ previousState: "active",
531
+ context: job.context,
532
+ tries: job.tries,
533
+ progress,
534
+ timestamp: this.#meta().nowIso(),
535
+ });
536
+ }
537
+ async emitLog(job, log) {
538
+ this.#getQueueBinding(job.type);
539
+ if (job.state !== "active") {
540
+ throw new Error(`Cannot emit log for job '${job.id}' in state '${job.state}'`);
541
+ }
542
+ await this.#publishJobEvent(job.type, job.id, {
543
+ jobId: job.id,
544
+ service: job.service,
545
+ jobType: job.type,
546
+ eventType: "logged",
547
+ state: "active",
548
+ previousState: "active",
549
+ context: job.context,
550
+ tries: job.tries,
551
+ logs: [log],
552
+ timestamp: this.#meta().nowIso(),
553
+ });
554
+ }
555
+ async waitFor(job, target, fn) {
556
+ const edge = {
557
+ id: ulid(),
558
+ target,
559
+ startedAt: this.#meta().nowIso(),
560
+ ...(target.label ? { label: target.label } : {}),
561
+ };
562
+ await this.#publishJobEvent(job.type, job.id, {
563
+ jobId: job.id,
564
+ service: job.service,
565
+ jobType: job.type,
566
+ eventType: "waiting",
567
+ state: "active",
568
+ previousState: "active",
569
+ context: job.context,
570
+ tries: job.tries,
571
+ waitEdge: edge,
572
+ timestamp: edge.startedAt,
573
+ });
574
+ try {
575
+ return await fn();
576
+ }
577
+ finally {
578
+ try {
579
+ await this.#publishJobEvent(job.type, job.id, {
580
+ jobId: job.id,
581
+ service: job.service,
582
+ jobType: job.type,
583
+ eventType: "resumed",
584
+ state: "active",
585
+ previousState: "active",
586
+ context: job.context,
587
+ tries: job.tries,
588
+ waitEdge: edge,
589
+ timestamp: this.#meta().nowIso(),
590
+ });
591
+ }
592
+ catch (error) {
593
+ recordTrellisError(error, {
594
+ surface: "job",
595
+ direction: "worker",
596
+ operation: job.type,
597
+ phase: "publish",
598
+ });
599
+ }
600
+ }
601
+ }
602
+ #keyedHeartbeat(job, lease) {
603
+ return async () => {
604
+ const renewed = await this.#context.keyCoordinator.renewHeartbeat({
605
+ service: this.#coordinationService(),
606
+ jobType: job.type,
607
+ jobId: job.id,
608
+ lease,
609
+ now: this.#meta().nowIso(),
610
+ });
611
+ if (renewed.kind === "lost") {
612
+ throw new ActiveJobRuntimeError("keyed job slot lease was lost");
613
+ }
614
+ };
615
+ }
616
+ async #releaseKeyedSlot(job, lease) {
617
+ if (!lease)
618
+ return undefined;
619
+ const released = await this.#context.keyCoordinator.releaseActiveSlot({
620
+ service: this.#coordinationService(),
621
+ jobType: job.type,
622
+ jobId: job.id,
623
+ lease,
624
+ now: this.#meta().nowIso(),
625
+ });
626
+ return released.kind === "released" ? "released" : "staleCompletion";
627
+ }
628
+ async #releaseKeyedSlotAfterPublishFailure(job, lease, publishError) {
629
+ try {
630
+ await this.#releaseKeyedSlot(job, lease);
631
+ }
632
+ catch (cleanupError) {
633
+ recordTrellisError(cleanupError, {
634
+ surface: "job",
635
+ direction: "worker",
636
+ operation: job.type,
637
+ phase: "keyed_slot_cleanup",
638
+ });
639
+ recordTrellisError(publishError, {
640
+ surface: "job",
641
+ direction: "worker",
642
+ operation: job.type,
643
+ phase: "publish",
644
+ });
645
+ }
646
+ }
647
+ async #publishStaleCompletionIgnored(job, tries) {
648
+ await this.#publishJobEvent(job.type, job.id, {
649
+ jobId: job.id,
650
+ service: job.service,
651
+ jobType: job.type,
652
+ eventType: "staleCompletionIgnored",
653
+ state: "stale",
654
+ previousState: "active",
655
+ context: job.context,
656
+ tries,
657
+ error: "keyed job slot lease was lost before completion",
658
+ timestamp: this.#meta().nowIso(),
659
+ });
660
+ }
661
+ async withActiveJobAndHeartbeat(job, cancellation, heartbeat, f, metadata = {}, lease) {
662
+ const stopProgressAcks = startAutoHeartbeat(heartbeat, metadata.progressAckIntervalMs ?? 1_000, () => cancellation.cancelForLeaseLoss());
663
+ const stopKeyHeartbeat = lease
664
+ ? startAutoHeartbeat(this.#keyedHeartbeat(job, lease), lease.policy.heartbeatIntervalMs, () => cancellation.cancelForLeaseLoss())
665
+ : () => { };
666
+ const activeJob = new ActiveJob(job, cancellation, {
667
+ updateProgress: (progress) => this.emitProgress(job, progress),
668
+ log: (entry) => this.emitLog(job, entry),
669
+ waitFor: (target, fn) => this.waitFor(job, target, fn),
670
+ heartbeat: async () => {
671
+ try {
672
+ await heartbeat();
673
+ }
674
+ catch (error) {
675
+ if (error instanceof ActiveJobRuntimeError) {
676
+ throw error;
677
+ }
678
+ throw new ActiveJobRuntimeError(error instanceof Error ? error.message : String(error));
679
+ }
680
+ },
681
+ }, {
682
+ redeliveryCount: metadata.redeliveryCount ?? 0,
683
+ });
684
+ try {
685
+ return await runWithActiveJobContext({
686
+ job,
687
+ waitFor: (target, fn) => this.waitFor(job, target, fn),
688
+ }, () => f(activeJob));
689
+ }
690
+ finally {
691
+ stopProgressAcks();
692
+ stopKeyHeartbeat();
693
+ }
694
+ }
695
+ #coordinationService() {
696
+ return this.#context.jobs.namespace;
697
+ }
698
+ }
699
+ /** Builds a durable job submission that can be dispatched after a transaction commits. */
700
+ export function prepareJobSubmission(args) {
701
+ const parent = args.parent ?? getActiveJobSnapshot();
702
+ return {
703
+ submissionId: args.submissionId,
704
+ mode: args.mode,
705
+ service: args.service,
706
+ queue: args.queue,
707
+ jobId: args.jobId,
708
+ payload: args.payload,
709
+ createdAt: args.createdAt,
710
+ context: args.context ?? createJobContext(),
711
+ trigger: parent ? parentJobTrigger(parent) : { kind: "serviceCode" },
712
+ ...(parent ? { lineage: childJobLineage(parent) } : {}),
713
+ };
714
+ }
715
+ function parentJobTrigger(parent) {
716
+ return {
717
+ kind: "parentJob",
718
+ parentJobId: parent.id,
719
+ traceId: parent.context.traceId,
720
+ requestId: parent.context.requestId,
721
+ ...(parent.lineage?.operationId
722
+ ? { operationId: parent.lineage.operationId }
723
+ : {}),
724
+ };
725
+ }
726
+ function childJobLineage(parent) {
727
+ return {
728
+ parentJobId: parent.id,
729
+ rootJobId: parent.lineage?.rootJobId ?? parent.id,
730
+ ...(parent.lineage?.operationId
731
+ ? { operationId: parent.lineage.operationId }
732
+ : {}),
733
+ };
734
+ }
735
+ function getKeyPolicy(binding) {
736
+ if (!binding.keyConcurrency) {
737
+ return undefined;
738
+ }
739
+ return normalizeJobKeyPolicy({
740
+ keyConcurrency: binding.keyConcurrency,
741
+ queue: binding.queue,
742
+ });
743
+ }
744
+ function startAutoHeartbeat(heartbeat, intervalMs, onFailure) {
745
+ const timer = setInterval(() => {
746
+ void heartbeat().catch((error) => {
747
+ recordTrellisError(error, {
748
+ surface: "job",
749
+ direction: "worker",
750
+ phase: "heartbeat",
751
+ });
752
+ clearInterval(timer);
753
+ onFailure();
754
+ });
755
+ }, intervalMs);
756
+ return () => clearInterval(timer);
757
+ }
758
+ function computeDeadline(now, deadlineMs) {
759
+ if (deadlineMs === undefined) {
760
+ return undefined;
761
+ }
762
+ const timestamp = new Date(now);
763
+ timestamp.setTime(timestamp.getTime() + deadlineMs);
764
+ return timestamp.toISOString();
765
+ }
766
+ export function createJobContext() {
767
+ const parent = getActiveJobSnapshot();
768
+ if (parent) {
769
+ return parent.context;
770
+ }
771
+ const carrier = createMapCarrier();
772
+ injectTraceContext(carrier);
773
+ const inheritedTraceparent = carrier.get("traceparent");
774
+ const traceparent = isValidTraceparent(inheritedTraceparent)
775
+ ? inheritedTraceparent
776
+ : synthesizeTraceparent();
777
+ const tracestate = carrier.get("tracestate");
778
+ return {
779
+ requestId: ulid(),
780
+ traceId: traceparent.slice(3, 35),
781
+ traceparent,
782
+ ...(tracestate ? { tracestate } : {}),
783
+ };
784
+ }
785
+ function headersFromJobContext(context, stableMessageId) {
786
+ const headers = natsHeaders();
787
+ headers.set("request-id", context.requestId);
788
+ headers.set("traceparent", context.traceparent);
789
+ if (context.tracestate) {
790
+ headers.set("tracestate", context.tracestate);
791
+ }
792
+ if (stableMessageId) {
793
+ headers.set("Nats-Msg-Id", stableMessageId);
794
+ }
795
+ return headers;
796
+ }
797
+ function isValidTraceparent(value) {
798
+ if (value === undefined ||
799
+ !/^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/.test(value)) {
800
+ return false;
801
+ }
802
+ return !isAllZeroHex(value.slice(3, 35)) &&
803
+ !isAllZeroHex(value.slice(36, 52));
804
+ }
805
+ function synthesizeTraceparent() {
806
+ return `00-${randomNonZeroHex(16)}-${randomNonZeroHex(8)}-01`;
807
+ }
808
+ function randomNonZeroHex(byteLength) {
809
+ let value = "";
810
+ do {
811
+ const bytes = new Uint8Array(byteLength);
812
+ crypto.getRandomValues(bytes);
813
+ value = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"))
814
+ .join("");
815
+ } while (isAllZeroHex(value));
816
+ return value;
817
+ }
818
+ function isAllZeroHex(value) {
819
+ return /^0+$/.test(value);
820
+ }
821
+ export { ActiveJob, ActiveJobRuntimeError, JobCancellationToken };