@mevdragon/vidfarm-devcli 0.2.11 → 0.2.13

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 (199) hide show
  1. package/.env.example +37 -1
  2. package/README.md +27 -183
  3. package/SKILL.director.md +293 -0
  4. package/SKILL.platform.md +312 -0
  5. package/auto-create-hyperframe-templates/AUTO_CREATE_HYPERFRAME_TEMPLATES.md +275 -0
  6. package/auto-create-hyperframe-templates/extractor-system-prompt.md +128 -0
  7. package/auto-create-hyperframe-templates/input-manifest.schema.json +56 -0
  8. package/auto-create-hyperframe-templates/lambda-memory-estimator.md +118 -0
  9. package/auto-create-hyperframe-templates/production-graph.schema.json +314 -0
  10. package/auto-create-hyperframe-templates/runbook.md +198 -0
  11. package/auto-create-hyperframe-templates/scripts/create-hyperframe-template.mjs +368 -0
  12. package/auto-create-hyperframe-templates/template-plan.schema.json +311 -0
  13. package/auto-create-hyperframe-templates/template-planner-prompt.md +144 -0
  14. package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +124 -0
  15. package/auto-create-templates/extractor-system-prompt.md +8 -2
  16. package/auto-create-templates/template-plan.schema.json +73 -2
  17. package/auto-create-templates/template-planner-prompt.md +63 -1
  18. package/dist/src/account-pages-legacy.js +361 -89
  19. package/dist/src/account-pages-legacy.js.map +1 -0
  20. package/dist/src/account-pages.js +13 -3
  21. package/dist/src/account-pages.js.map +1 -0
  22. package/dist/src/app.js +7060 -946
  23. package/dist/src/app.js.map +1 -0
  24. package/dist/src/cli.js +111 -29
  25. package/dist/src/cli.js.map +1 -0
  26. package/dist/src/composition-runtime.js +581 -0
  27. package/dist/src/composition-runtime.js.map +1 -0
  28. package/dist/src/config.js +69 -11
  29. package/dist/src/config.js.map +1 -0
  30. package/dist/src/context.js +280 -21
  31. package/dist/src/context.js.map +1 -0
  32. package/dist/src/dev-app-legacy.js +18 -17
  33. package/dist/src/dev-app-legacy.js.map +1 -0
  34. package/dist/src/dev-app.js +1 -0
  35. package/dist/src/dev-app.js.map +1 -0
  36. package/dist/src/domain.js +2 -1
  37. package/dist/src/domain.js.map +1 -0
  38. package/dist/src/editor-chat-history.js +10 -0
  39. package/dist/src/editor-chat-history.js.map +1 -0
  40. package/dist/src/editor-chat.js +133 -24
  41. package/dist/src/editor-chat.js.map +1 -0
  42. package/dist/src/frontend/flockposter-cache-store.js +10 -2
  43. package/dist/src/frontend/flockposter-cache-store.js.map +1 -0
  44. package/dist/src/frontend/homepage-client.js +6 -3
  45. package/dist/src/frontend/homepage-client.js.map +1 -0
  46. package/dist/src/frontend/homepage-shared.js +1 -0
  47. package/dist/src/frontend/homepage-shared.js.map +1 -0
  48. package/dist/src/frontend/homepage-store.js +1 -0
  49. package/dist/src/frontend/homepage-store.js.map +1 -0
  50. package/dist/src/frontend/homepage-view.js +131 -18
  51. package/dist/src/frontend/homepage-view.js.map +1 -0
  52. package/dist/src/frontend/page-runtime-client.js +5 -2
  53. package/dist/src/frontend/page-runtime-client.js.map +1 -0
  54. package/dist/src/frontend/page-runtime-store.js +1 -0
  55. package/dist/src/frontend/page-runtime-store.js.map +1 -0
  56. package/dist/src/frontend/sentry.js +42 -0
  57. package/dist/src/frontend/sentry.js.map +1 -0
  58. package/dist/src/frontend/template-editor-chat.js +1719 -213
  59. package/dist/src/frontend/template-editor-chat.js.map +1 -0
  60. package/dist/src/help-page.js +333 -0
  61. package/dist/src/help-page.js.map +1 -0
  62. package/dist/src/homepage.js +169 -47
  63. package/dist/src/homepage.js.map +1 -0
  64. package/dist/src/hyperframes/composition.js +180 -0
  65. package/dist/src/hyperframes/composition.js.map +1 -0
  66. package/dist/src/index.js +1 -0
  67. package/dist/src/index.js.map +1 -0
  68. package/dist/src/instrument.js +30 -0
  69. package/dist/src/instrument.js.map +1 -0
  70. package/dist/src/lib/crypto.js +1 -0
  71. package/dist/src/lib/crypto.js.map +1 -0
  72. package/dist/src/lib/dev-log.js +54 -0
  73. package/dist/src/lib/dev-log.js.map +1 -0
  74. package/dist/src/lib/display-name.js +11 -0
  75. package/dist/src/lib/display-name.js.map +1 -0
  76. package/dist/src/lib/ids.js +21 -1
  77. package/dist/src/lib/ids.js.map +1 -0
  78. package/dist/src/lib/images.js +1 -0
  79. package/dist/src/lib/images.js.map +1 -0
  80. package/dist/src/lib/json.js +1 -0
  81. package/dist/src/lib/json.js.map +1 -0
  82. package/dist/src/lib/template-dna.js +1 -0
  83. package/dist/src/lib/template-dna.js.map +1 -0
  84. package/dist/src/lib/template-paths.js +1 -0
  85. package/dist/src/lib/template-paths.js.map +1 -0
  86. package/dist/src/lib/template-style-options.js +40 -2
  87. package/dist/src/lib/template-style-options.js.map +1 -0
  88. package/dist/src/lib/time.js +1 -0
  89. package/dist/src/lib/time.js.map +1 -0
  90. package/dist/src/lib/video-quality-harness.js +60 -0
  91. package/dist/src/lib/video-quality-harness.js.map +1 -0
  92. package/dist/src/page-runtime.js +1 -0
  93. package/dist/src/page-runtime.js.map +1 -0
  94. package/dist/src/page-shell.js +1501 -49
  95. package/dist/src/page-shell.js.map +1 -0
  96. package/dist/src/primitive-context.js +144 -8
  97. package/dist/src/primitive-context.js.map +1 -0
  98. package/dist/src/primitive-registry.js +1919 -78
  99. package/dist/src/primitive-registry.js.map +1 -0
  100. package/dist/src/primitive-sdk.js +1 -0
  101. package/dist/src/primitive-sdk.js.map +1 -0
  102. package/dist/src/primitives/remotion/html-image.js +3 -1
  103. package/dist/src/primitives/remotion/html-image.js.map +1 -0
  104. package/dist/src/primitives/remotion/media-slideshow.js +60 -0
  105. package/dist/src/primitives/remotion/media-slideshow.js.map +1 -0
  106. package/dist/src/react-page-shell.js +1 -0
  107. package/dist/src/react-page-shell.js.map +1 -0
  108. package/dist/src/ready-post-schedule-component.js +1 -0
  109. package/dist/src/ready-post-schedule-component.js.map +1 -0
  110. package/dist/src/registry.js +237 -24
  111. package/dist/src/registry.js.map +1 -0
  112. package/dist/src/runtime.js +3 -0
  113. package/dist/src/runtime.js.map +1 -0
  114. package/dist/src/services/api-call-history.js +4 -0
  115. package/dist/src/services/api-call-history.js.map +1 -0
  116. package/dist/src/services/auth.js +40 -50
  117. package/dist/src/services/auth.js.map +1 -0
  118. package/dist/src/services/billing.js +20 -44
  119. package/dist/src/services/billing.js.map +1 -0
  120. package/dist/src/services/chat-threads.js +10 -6
  121. package/dist/src/services/chat-threads.js.map +1 -0
  122. package/dist/src/services/fork-access.js +94 -0
  123. package/dist/src/services/fork-access.js.map +1 -0
  124. package/dist/src/services/ghostcut.js +132 -0
  125. package/dist/src/services/ghostcut.js.map +1 -0
  126. package/dist/src/services/hyperframes.js +1014 -0
  127. package/dist/src/services/hyperframes.js.map +1 -0
  128. package/dist/src/services/job-capacity.js +14 -0
  129. package/dist/src/services/job-capacity.js.map +1 -0
  130. package/dist/src/services/job-logs.js +4 -0
  131. package/dist/src/services/job-logs.js.map +1 -0
  132. package/dist/src/services/jobs.js +99 -91
  133. package/dist/src/services/jobs.js.map +1 -0
  134. package/dist/src/services/media-processing.js +743 -0
  135. package/dist/src/services/media-processing.js.map +1 -0
  136. package/dist/src/services/primitive-media-lambda.js +280 -0
  137. package/dist/src/services/primitive-media-lambda.js.map +1 -0
  138. package/dist/src/services/providers.js +1560 -178
  139. package/dist/src/services/providers.js.map +1 -0
  140. package/dist/src/services/rate-limits.js +3 -2
  141. package/dist/src/services/rate-limits.js.map +1 -0
  142. package/dist/src/services/remotion.js +495 -92
  143. package/dist/src/services/remotion.js.map +1 -0
  144. package/dist/src/services/serverless-auth.js +374 -0
  145. package/dist/src/services/serverless-auth.js.map +1 -0
  146. package/dist/src/services/serverless-jobs.js +1074 -0
  147. package/dist/src/services/serverless-jobs.js.map +1 -0
  148. package/dist/src/services/serverless-provider-keys.js +401 -0
  149. package/dist/src/services/serverless-provider-keys.js.map +1 -0
  150. package/dist/src/services/serverless-records.js +1086 -0
  151. package/dist/src/services/serverless-records.js.map +1 -0
  152. package/dist/src/services/serverless-template-configs.js +67 -0
  153. package/dist/src/services/serverless-template-configs.js.map +1 -0
  154. package/dist/src/services/storage.js +170 -35
  155. package/dist/src/services/storage.js.map +1 -0
  156. package/dist/src/services/template-certification.js +295 -3
  157. package/dist/src/services/template-certification.js.map +1 -0
  158. package/dist/src/services/template-loader.js +45 -1
  159. package/dist/src/services/template-loader.js.map +1 -0
  160. package/dist/src/services/template-runtime-bundles.js +217 -0
  161. package/dist/src/services/template-runtime-bundles.js.map +1 -0
  162. package/dist/src/services/template-sources.js +452 -87
  163. package/dist/src/services/template-sources.js.map +1 -0
  164. package/dist/src/services/video-normalization.js +2 -0
  165. package/dist/src/services/video-normalization.js.map +1 -0
  166. package/dist/src/services/webhooks.js +7 -6
  167. package/dist/src/services/webhooks.js.map +1 -0
  168. package/dist/src/template-editor-pages.js +5637 -1322
  169. package/dist/src/template-editor-pages.js.map +1 -0
  170. package/dist/src/template-editor-shell.js +1376 -170
  171. package/dist/src/template-editor-shell.js.map +1 -0
  172. package/dist/src/template-sdk.js +1 -0
  173. package/dist/src/template-sdk.js.map +1 -0
  174. package/dist/src/worker.js +11 -226
  175. package/dist/src/worker.js.map +1 -0
  176. package/package.json +47 -12
  177. package/public/assets/homepage-client-app.js +34 -8
  178. package/public/assets/page-runtime-client-app.js +44 -18
  179. package/readme.secret.md +89 -0
  180. package/templates/vidfarm_template_0000/README.md +106 -0
  181. package/templates/vidfarm_template_0000/SKILL.md +266 -0
  182. package/templates/vidfarm_template_0000/assets/Abel-Regular.ttf +0 -0
  183. package/templates/vidfarm_template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
  184. package/templates/vidfarm_template_0000/assets/Montserrat[wght].ttf +0 -0
  185. package/templates/vidfarm_template_0000/assets/SourceCodePro[wght].ttf +0 -0
  186. package/templates/vidfarm_template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
  187. package/templates/vidfarm_template_0000/assets/Yesteryear-Regular.ttf +0 -0
  188. package/templates/vidfarm_template_0000/composition.json +11 -0
  189. package/templates/vidfarm_template_0000/src/lib/images.js +221 -0
  190. package/templates/vidfarm_template_0000/src/remotion/Root.js +33 -0
  191. package/templates/vidfarm_template_0000/src/remotion/index.js +3 -0
  192. package/templates/vidfarm_template_0000/src/sdk.js +3 -0
  193. package/templates/vidfarm_template_0000/src/style-options.js +200 -0
  194. package/templates/vidfarm_template_0000/src/template-dna.js +9 -0
  195. package/templates/vidfarm_template_0000/src/template.js +1566 -0
  196. package/templates/vidfarm_template_0000/template.config.json +21 -0
  197. package/GETTING_STARTED.developers.md +0 -87
  198. package/SKILL.developer.md +0 -1177
  199. package/dist/src/db.js +0 -2007
@@ -0,0 +1,1074 @@
1
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
+ import { DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand, ScanCommand, TransactWriteCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb";
3
+ import { SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn";
4
+ import { config } from "../config.js";
5
+ import { createId } from "../lib/ids.js";
6
+ import { devLog } from "../lib/dev-log.js";
7
+ import { nowIso } from "../lib/time.js";
8
+ import { ActiveJobLimitExceededError } from "./job-capacity.js";
9
+ const dynamodb = DynamoDBDocumentClient.from(new DynamoDBClient({}), {
10
+ marshallOptions: {
11
+ removeUndefinedValues: true
12
+ }
13
+ });
14
+ const sfn = new SFNClient({});
15
+ const ACTIVE_ROOT_JOB_STATUSES = ["queued", "running", "waiting_for_provider", "waiting_for_child", "waiting_for_human"];
16
+ const ACTIVE_JOB_STATUSES = new Set(ACTIVE_ROOT_JOB_STATUSES);
17
+ export class ServerlessJobsService {
18
+ async createRootJob(input) {
19
+ const tableName = requireMainTableName();
20
+ const createdAt = nowIso();
21
+ const job = {
22
+ id: createId("job"),
23
+ templateId: input.templateId,
24
+ operationName: input.operationName,
25
+ workflowName: input.workflowName,
26
+ tracer: input.tracer,
27
+ status: "queued",
28
+ customerId: input.customer.id,
29
+ payload: {
30
+ ...input.payload,
31
+ _providerHint: input.providerHint ?? null
32
+ },
33
+ result: null,
34
+ error: null,
35
+ progress: 0,
36
+ webhookUrl: normalizeWebhookUrl(input.webhookUrl) ?? normalizeWebhookUrl(input.customer.defaultWebhookUrl),
37
+ parentJobId: null,
38
+ priority: 100,
39
+ attemptCount: 0,
40
+ maxAttempts: 6,
41
+ runAfter: createdAt,
42
+ createdAt,
43
+ updatedAt: createdAt,
44
+ startedAt: null,
45
+ completedAt: null
46
+ };
47
+ await this.persistRootAndStart(tableName, job);
48
+ devLog("jobs.root.created", {
49
+ job_id: job.id,
50
+ customer_id: job.customerId,
51
+ template_id: job.templateId,
52
+ operation_name: job.operationName,
53
+ workflow_name: job.workflowName,
54
+ tracer: job.tracer
55
+ });
56
+ return job;
57
+ }
58
+ async createChildJob(input) {
59
+ const createdAt = nowIso();
60
+ const job = {
61
+ id: createId("job"),
62
+ templateId: input.templateId,
63
+ operationName: input.operationName,
64
+ workflowName: input.workflowName,
65
+ tracer: input.tracer,
66
+ status: "queued",
67
+ customerId: input.customerId,
68
+ payload: {
69
+ ...input.payload,
70
+ _providerHint: input.providerHint ?? null
71
+ },
72
+ result: null,
73
+ error: null,
74
+ progress: 0,
75
+ webhookUrl: null,
76
+ parentJobId: input.parentJobId,
77
+ priority: 110,
78
+ attemptCount: 0,
79
+ maxAttempts: 6,
80
+ runAfter: createdAt,
81
+ createdAt,
82
+ updatedAt: createdAt,
83
+ startedAt: null,
84
+ completedAt: null
85
+ };
86
+ await dynamodb.send(new PutCommand({
87
+ TableName: requireMainTableName(),
88
+ Item: {
89
+ ...jobToItem(job),
90
+ parent_job_id: input.parentJobId
91
+ },
92
+ ConditionExpression: "attribute_not_exists(pk)"
93
+ }));
94
+ await dynamodb.send(new PutCommand({
95
+ TableName: requireMainTableName(),
96
+ Item: {
97
+ pk: jobPk(input.parentJobId),
98
+ sk: `CHILD#${createdAt}#${job.id}`,
99
+ entity_type: "JobChildPointer",
100
+ child_job_id: job.id,
101
+ job_id: input.parentJobId,
102
+ customer_id: input.customerId,
103
+ created_at: createdAt
104
+ }
105
+ }));
106
+ try {
107
+ const executionArn = await this.startWorkflow(job);
108
+ if (executionArn) {
109
+ await dynamodb.send(new UpdateCommand({
110
+ TableName: requireMainTableName(),
111
+ Key: jobKey(job.id),
112
+ UpdateExpression: "set step_execution_arn = :arn, updated_at = :updated",
113
+ ExpressionAttributeValues: {
114
+ ":arn": executionArn,
115
+ ":updated": nowIso()
116
+ }
117
+ }));
118
+ }
119
+ }
120
+ catch (error) {
121
+ devLog("jobs.child.workflow_start_failed", {
122
+ job_id: job.id,
123
+ parent_job_id: input.parentJobId,
124
+ error: error instanceof Error ? error.message : String(error)
125
+ }, "error");
126
+ }
127
+ devLog("jobs.child.created", {
128
+ job_id: job.id,
129
+ parent_job_id: input.parentJobId,
130
+ customer_id: job.customerId,
131
+ template_id: job.templateId,
132
+ operation_name: job.operationName,
133
+ workflow_name: job.workflowName,
134
+ tracer: job.tracer
135
+ });
136
+ return job;
137
+ }
138
+ async getJob(jobId) {
139
+ const tableName = requireMainTableName();
140
+ const response = await dynamodb.send(new GetCommand({
141
+ TableName: tableName,
142
+ Key: jobKey(jobId)
143
+ }));
144
+ const job = response.Item ? itemToJob(response.Item) : null;
145
+ devLog("jobs.get", {
146
+ job_id: jobId,
147
+ found: Boolean(job),
148
+ customer_id: job?.customerId ?? null,
149
+ status: job?.status ?? null
150
+ });
151
+ return job;
152
+ }
153
+ async listChildJobs(parentJobId) {
154
+ const response = await dynamodb.send(new QueryCommand({
155
+ TableName: requireMainTableName(),
156
+ KeyConditionExpression: "pk = :pk and begins_with(sk, :prefix)",
157
+ ExpressionAttributeValues: {
158
+ ":pk": jobPk(parentJobId),
159
+ ":prefix": "CHILD#"
160
+ },
161
+ ScanIndexForward: false,
162
+ Limit: 500
163
+ }));
164
+ const childIds = (response.Items ?? [])
165
+ .map((item) => typeof item.child_job_id === "string" ? item.child_job_id : null)
166
+ .filter((id) => Boolean(id));
167
+ const jobs = [];
168
+ for (const childId of childIds) {
169
+ const child = await this.getJob(childId);
170
+ if (child) {
171
+ jobs.push(child);
172
+ }
173
+ }
174
+ return jobs;
175
+ }
176
+ async listJobs(input) {
177
+ const tableName = requireMainTableName();
178
+ const limit = Math.max(1, Math.min(input.limit ?? 100, 500));
179
+ const index = input.templateId ? "gsi2" : input.tracer ? "gsi3" : "gsi1";
180
+ const partition = input.templateId
181
+ ? customerTemplatePk(input.customerId, input.templateId)
182
+ : input.tracer
183
+ ? customerTracerPk(input.customerId, input.tracer)
184
+ : customerPk(input.customerId);
185
+ const response = await dynamodb.send(new QueryCommand({
186
+ TableName: tableName,
187
+ IndexName: index,
188
+ KeyConditionExpression: `${index}pk = :pk`,
189
+ ExpressionAttributeValues: {
190
+ ":pk": partition
191
+ },
192
+ ScanIndexForward: false,
193
+ Limit: limit,
194
+ ExclusiveStartKey: input.cursor ? cursorToStartKey(input, index) : undefined
195
+ }));
196
+ const jobs = (response.Items ?? []).map(itemToJob);
197
+ const filtered = jobs.filter((job) => {
198
+ if (input.tracer && job.tracer !== input.tracer) {
199
+ return false;
200
+ }
201
+ if (input.startTime && job.createdAt < input.startTime) {
202
+ return false;
203
+ }
204
+ if (input.endTime && job.createdAt > input.endTime) {
205
+ return false;
206
+ }
207
+ return true;
208
+ });
209
+ devLog("jobs.list", {
210
+ customer_id: input.customerId,
211
+ template_id: input.templateId ?? null,
212
+ tracer: input.tracer ?? null,
213
+ count: filtered.length,
214
+ limit
215
+ });
216
+ return filtered;
217
+ }
218
+ async cancelJob(jobId) {
219
+ const now = nowIso();
220
+ await this.addLogEvent({
221
+ id: createId("evt"),
222
+ jobId,
223
+ level: "warn",
224
+ message: "Job cancelled",
225
+ metadata: { reason: "user_request" },
226
+ progress: null,
227
+ artifactKey: null,
228
+ createdAt: now
229
+ });
230
+ await this.updateJobStatus({
231
+ id: jobId,
232
+ status: "cancelled",
233
+ error: { message: "Cancelled by user request." },
234
+ completedAt: now
235
+ });
236
+ devLog("jobs.cancelled", {
237
+ job_id: jobId
238
+ }, "warn");
239
+ }
240
+ async updateJobStatus(input) {
241
+ const current = await this.getJob(input.id);
242
+ const timestamp = nowIso();
243
+ const isTerminalStatus = input.status === "succeeded" || input.status === "failed" || input.status === "cancelled";
244
+ const shouldReleaseRootCapacity = Boolean(isTerminalStatus
245
+ && current
246
+ && current.parentJobId === null
247
+ && ACTIVE_ROOT_JOB_STATUSES.includes(current.status));
248
+ if (isTerminalStatus && current && !ACTIVE_ROOT_JOB_STATUSES.includes(current.status)) {
249
+ devLog("jobs.status.terminal_update_skipped", {
250
+ job_id: input.id,
251
+ current_status: current.status,
252
+ attempted_status: input.status
253
+ }, "warn");
254
+ return;
255
+ }
256
+ const nextCompletedAt = input.completedAt
257
+ ?? (isTerminalStatus ? timestamp : current?.completedAt ?? null);
258
+ const nextStartedAt = input.startedAt !== undefined
259
+ ? input.startedAt
260
+ : input.status === "running"
261
+ ? current?.startedAt ?? timestamp
262
+ : input.status === "queued"
263
+ ? null
264
+ : current?.startedAt ?? null;
265
+ const expressionAttributeValues = {
266
+ ":status": input.status,
267
+ ":progress": input.progress ?? current?.progress ?? 0,
268
+ ":result": input.result === undefined ? current?.result ?? null : input.result,
269
+ ":error": input.error === undefined ? current?.error ?? null : input.error,
270
+ ":runAfter": input.runAfter ?? current?.runAfter ?? timestamp,
271
+ ":startedAt": nextStartedAt,
272
+ ":completedAt": nextCompletedAt,
273
+ ":updatedAt": timestamp
274
+ };
275
+ const conditionExpression = isTerminalStatus
276
+ ? [
277
+ "#status = :queuedStatus",
278
+ "#status = :runningStatus",
279
+ "#status = :waitingForProviderStatus",
280
+ "#status = :waitingForChildStatus",
281
+ "#status = :waitingForHumanStatus"
282
+ ].join(" OR ")
283
+ : [
284
+ "attribute_not_exists(#status)",
285
+ "(#status <> :succeededStatus AND #status <> :failedStatus AND #status <> :cancelledStatus)"
286
+ ].join(" OR ");
287
+ if (isTerminalStatus) {
288
+ expressionAttributeValues[":queuedStatus"] = "queued";
289
+ expressionAttributeValues[":runningStatus"] = "running";
290
+ expressionAttributeValues[":waitingForProviderStatus"] = "waiting_for_provider";
291
+ expressionAttributeValues[":waitingForChildStatus"] = "waiting_for_child";
292
+ expressionAttributeValues[":waitingForHumanStatus"] = "waiting_for_human";
293
+ }
294
+ else {
295
+ expressionAttributeValues[":succeededStatus"] = "succeeded";
296
+ expressionAttributeValues[":failedStatus"] = "failed";
297
+ expressionAttributeValues[":cancelledStatus"] = "cancelled";
298
+ }
299
+ try {
300
+ await dynamodb.send(new UpdateCommand({
301
+ TableName: requireMainTableName(),
302
+ Key: jobKey(input.id),
303
+ UpdateExpression: [
304
+ "set #status = :status",
305
+ "progress = :progress",
306
+ "result_json = :result",
307
+ "error_json = :error",
308
+ "run_after = :runAfter",
309
+ "started_at = :startedAt",
310
+ "completed_at = :completedAt",
311
+ "updated_at = :updatedAt"
312
+ ].join(", "),
313
+ ConditionExpression: conditionExpression,
314
+ ExpressionAttributeNames: {
315
+ "#status": "status"
316
+ },
317
+ ExpressionAttributeValues: expressionAttributeValues
318
+ }));
319
+ }
320
+ catch (error) {
321
+ if (isConditionalCheckFailed(error)) {
322
+ if (isTerminalStatus) {
323
+ devLog("jobs.status.terminal_update_condition_failed", {
324
+ job_id: input.id,
325
+ attempted_status: input.status
326
+ }, "warn");
327
+ return;
328
+ }
329
+ devLog("jobs.status.non_terminal_update_skipped", {
330
+ job_id: input.id,
331
+ attempted_status: input.status,
332
+ progress: input.progress ?? current?.progress ?? 0
333
+ }, "warn");
334
+ return;
335
+ }
336
+ throw error;
337
+ }
338
+ if (shouldReleaseRootCapacity && current) {
339
+ await this.decrementRootCapacity(current.customerId, current.id);
340
+ }
341
+ devLog("jobs.status.updated", {
342
+ job_id: input.id,
343
+ status: input.status,
344
+ progress: input.progress ?? current?.progress ?? 0,
345
+ completed_at: nextCompletedAt
346
+ }, input.status === "failed" ? "error" : input.status === "cancelled" ? "warn" : "info");
347
+ }
348
+ async insertArtifact(record) {
349
+ const createdAt = nowIso();
350
+ await dynamodb.send(new PutCommand({
351
+ TableName: requireMainTableName(),
352
+ Item: artifactToItem({
353
+ id: record.id,
354
+ jobId: record.jobId,
355
+ customerId: record.customerId,
356
+ templateId: record.templateId,
357
+ kind: record.kind,
358
+ storageKey: record.storageKey,
359
+ publicUrl: record.publicUrl ?? null,
360
+ metadata: record.metadata ?? {},
361
+ createdAt
362
+ })
363
+ }));
364
+ devLog("jobs.artifact.inserted", {
365
+ artifact_id: record.id,
366
+ job_id: record.jobId,
367
+ customer_id: record.customerId,
368
+ template_id: record.templateId,
369
+ kind: record.kind,
370
+ storage_key: record.storageKey,
371
+ has_public_url: Boolean(record.publicUrl)
372
+ });
373
+ }
374
+ async sweepStaleActiveJobs(input = {}) {
375
+ const customerIds = input.customerId
376
+ ? [input.customerId]
377
+ : await this.listCapacityCustomerIds(input.maxCustomers ?? 100);
378
+ let swept = 0;
379
+ let scanned = 0;
380
+ for (const customerId of customerIds) {
381
+ const result = await this.sweepStaleActiveJobsForCustomer(customerId, {
382
+ maxJobs: input.maxJobsPerCustomer ?? 1000
383
+ });
384
+ swept += result.swept;
385
+ scanned += result.scanned;
386
+ }
387
+ return {
388
+ customers: customerIds.length,
389
+ scanned,
390
+ swept
391
+ };
392
+ }
393
+ async listArtifactsForJob(input) {
394
+ const response = await dynamodb.send(new QueryCommand({
395
+ TableName: requireMainTableName(),
396
+ KeyConditionExpression: "pk = :pk and begins_with(sk, :prefix)",
397
+ ExpressionAttributeValues: {
398
+ ":pk": jobPk(input.jobId),
399
+ ":prefix": "ARTIFACT#"
400
+ },
401
+ ScanIndexForward: false,
402
+ Limit: Math.max(1, Math.min(input.limit ?? 100, 501))
403
+ }));
404
+ const artifacts = (response.Items ?? [])
405
+ .map(itemToArtifact)
406
+ .filter((artifact) => {
407
+ if (artifact.customerId !== input.customerId) {
408
+ return false;
409
+ }
410
+ if (input.templateId && artifact.templateId !== input.templateId) {
411
+ return false;
412
+ }
413
+ if (input.cursor && (artifact.createdAt > input.cursor.createdAt || (artifact.createdAt === input.cursor.createdAt && artifact.id >= input.cursor.id))) {
414
+ return false;
415
+ }
416
+ return true;
417
+ });
418
+ devLog("jobs.artifacts.list", {
419
+ job_id: input.jobId,
420
+ customer_id: input.customerId,
421
+ template_id: input.templateId ?? null,
422
+ count: artifacts.length,
423
+ limit: input.limit ?? 100
424
+ });
425
+ return artifacts;
426
+ }
427
+ async listLogs(input) {
428
+ const response = await dynamodb.send(new QueryCommand({
429
+ TableName: requireMainTableName(),
430
+ KeyConditionExpression: "pk = :pk and begins_with(sk, :prefix)",
431
+ ExpressionAttributeValues: {
432
+ ":pk": jobPk(input.jobId),
433
+ ":prefix": "LOG#"
434
+ },
435
+ ScanIndexForward: false,
436
+ Limit: Math.max(1, Math.min(input.limit ?? 100, 500))
437
+ }));
438
+ return (response.Items ?? [])
439
+ .map(itemToLog)
440
+ .filter((event) => {
441
+ if (input.startTime && event.createdAt < input.startTime) {
442
+ return false;
443
+ }
444
+ if (input.endTime && event.createdAt > input.endTime) {
445
+ return false;
446
+ }
447
+ if (input.cursor && (event.createdAt > input.cursor.createdAt || (event.createdAt === input.cursor.createdAt && event.id >= input.cursor.id))) {
448
+ return false;
449
+ }
450
+ return true;
451
+ });
452
+ }
453
+ async listLogStorageKeys() {
454
+ return [];
455
+ }
456
+ async getRootQueueCapacityStatus(customer) {
457
+ await this.sweepStaleActiveJobsForCustomer(customer.id);
458
+ await this.ensureRootCapacityCounter(customer.id);
459
+ const response = await dynamodb.send(new GetCommand({
460
+ TableName: requireMainTableName(),
461
+ Key: customerJobCapacityKey(customer.id)
462
+ }));
463
+ const activeCount = typeof response.Item?.active_count === "number" ? response.Item.active_count : 0;
464
+ const limit = config.MAX_ACTIVE_JOBS_PER_CUSTOMER;
465
+ return {
466
+ plan_tier: customer.planTier,
467
+ scope: "root_jobs",
468
+ customer: {
469
+ limit,
470
+ pending_count: activeCount,
471
+ active_count: activeCount,
472
+ remaining: Math.max(0, limit - activeCount),
473
+ enabled: true
474
+ },
475
+ pending_statuses: ACTIVE_ROOT_JOB_STATUSES,
476
+ retry_after_seconds: 30
477
+ };
478
+ }
479
+ async persistRootAndStart(tableName, job) {
480
+ await this.sweepStaleActiveJobsForCustomer(job.customerId);
481
+ await this.ensureRootCapacityCounter(job.customerId);
482
+ const queuedLogId = createId("evt");
483
+ for (let attempt = 0; attempt < 2; attempt += 1) {
484
+ try {
485
+ await this.writeRootJobTransaction(tableName, job, queuedLogId);
486
+ break;
487
+ }
488
+ catch (error) {
489
+ if (!isTransactionCancelledForCapacity(error)) {
490
+ throw error;
491
+ }
492
+ const activeCount = await this.reconcileRootCapacityCounter(job.customerId);
493
+ if (attempt === 0 && activeCount < config.MAX_ACTIVE_JOBS_PER_CUSTOMER) {
494
+ devLog("jobs.capacity.reconciled_retry", {
495
+ job_id: job.id,
496
+ customer_id: job.customerId,
497
+ active_count: activeCount,
498
+ limit: config.MAX_ACTIVE_JOBS_PER_CUSTOMER
499
+ }, "warn");
500
+ continue;
501
+ }
502
+ throw new ActiveJobLimitExceededError(`Customer has reached the active job limit of ${config.MAX_ACTIVE_JOBS_PER_CUSTOMER}.`, config.MAX_ACTIVE_JOBS_PER_CUSTOMER, activeCount, ACTIVE_ROOT_JOB_STATUSES, 30);
503
+ }
504
+ }
505
+ try {
506
+ const executionArn = await this.startWorkflow(job);
507
+ if (executionArn) {
508
+ await dynamodb.send(new UpdateCommand({
509
+ TableName: tableName,
510
+ Key: jobKey(job.id),
511
+ UpdateExpression: "set step_execution_arn = :arn, updated_at = :updated",
512
+ ExpressionAttributeValues: {
513
+ ":arn": executionArn,
514
+ ":updated": nowIso()
515
+ }
516
+ }));
517
+ }
518
+ }
519
+ catch (error) {
520
+ await this.updateJobStatus({
521
+ id: job.id,
522
+ status: "failed",
523
+ error: {
524
+ message: error instanceof Error ? error.message : "Unable to start job workflow.",
525
+ type: "job_workflow_start_failed"
526
+ }
527
+ });
528
+ throw error;
529
+ }
530
+ }
531
+ async writeRootJobTransaction(tableName, job, queuedLogId) {
532
+ await dynamodb.send(new TransactWriteCommand({
533
+ TransactItems: [
534
+ {
535
+ Update: {
536
+ TableName: tableName,
537
+ Key: customerJobCapacityKey(job.customerId),
538
+ UpdateExpression: [
539
+ "set active_count = if_not_exists(active_count, :zero) + :one",
540
+ "updated_at = :updated",
541
+ "capacity_limit = :limit",
542
+ "entity_type = :entityType"
543
+ ].join(", "),
544
+ ConditionExpression: "attribute_not_exists(active_count) OR active_count < :limit",
545
+ ExpressionAttributeValues: {
546
+ ":zero": 0,
547
+ ":one": 1,
548
+ ":limit": config.MAX_ACTIVE_JOBS_PER_CUSTOMER,
549
+ ":updated": job.createdAt,
550
+ ":entityType": "CustomerJobCapacity"
551
+ }
552
+ }
553
+ },
554
+ {
555
+ Put: {
556
+ TableName: tableName,
557
+ Item: jobToItem(job),
558
+ ConditionExpression: "attribute_not_exists(pk)"
559
+ }
560
+ },
561
+ {
562
+ Put: {
563
+ TableName: tableName,
564
+ Item: {
565
+ pk: jobPk(job.id),
566
+ sk: `LOG#${job.createdAt}#${queuedLogId}`,
567
+ entity_type: "JobLogEvent",
568
+ id: queuedLogId,
569
+ job_id: job.id,
570
+ level: "info",
571
+ message: "Job queued",
572
+ metadata_json: {
573
+ runtime: "serverless",
574
+ template_id: job.templateId,
575
+ operation_name: job.operationName
576
+ },
577
+ progress: 0,
578
+ artifact_key: null,
579
+ created_at: job.createdAt
580
+ }
581
+ }
582
+ }
583
+ ]
584
+ }));
585
+ }
586
+ async ensureRootCapacityCounter(customerId) {
587
+ const tableName = requireMainTableName();
588
+ const response = await dynamodb.send(new GetCommand({
589
+ TableName: tableName,
590
+ Key: customerJobCapacityKey(customerId)
591
+ }));
592
+ if (response.Item) {
593
+ return;
594
+ }
595
+ const activeCount = await this.countActiveRootJobs(customerId);
596
+ try {
597
+ await dynamodb.send(new PutCommand({
598
+ TableName: tableName,
599
+ Item: {
600
+ ...customerJobCapacityKey(customerId),
601
+ entity_type: "CustomerJobCapacity",
602
+ customer_id: customerId,
603
+ active_count: activeCount,
604
+ capacity_limit: config.MAX_ACTIVE_JOBS_PER_CUSTOMER,
605
+ updated_at: nowIso()
606
+ },
607
+ ConditionExpression: "attribute_not_exists(pk)"
608
+ }));
609
+ }
610
+ catch (error) {
611
+ if (isConditionalCheckFailed(error)) {
612
+ return;
613
+ }
614
+ throw error;
615
+ }
616
+ }
617
+ async reconcileRootCapacityCounter(customerId) {
618
+ await this.sweepStaleActiveJobsForCustomer(customerId);
619
+ const activeCount = await this.countActiveRootJobs(customerId);
620
+ await dynamodb.send(new UpdateCommand({
621
+ TableName: requireMainTableName(),
622
+ Key: customerJobCapacityKey(customerId),
623
+ UpdateExpression: "set active_count = :activeCount, capacity_limit = :limit, updated_at = :updated, entity_type = :entityType",
624
+ ExpressionAttributeValues: {
625
+ ":activeCount": activeCount,
626
+ ":limit": config.MAX_ACTIVE_JOBS_PER_CUSTOMER,
627
+ ":updated": nowIso(),
628
+ ":entityType": "CustomerJobCapacity"
629
+ }
630
+ }));
631
+ return activeCount;
632
+ }
633
+ async getRootCapacityCount(customerId) {
634
+ const response = await dynamodb.send(new GetCommand({
635
+ TableName: requireMainTableName(),
636
+ Key: customerJobCapacityKey(customerId)
637
+ }));
638
+ return typeof response.Item?.active_count === "number" ? response.Item.active_count : null;
639
+ }
640
+ async countActiveRootJobs(customerId) {
641
+ let count = 0;
642
+ let exclusiveStartKey;
643
+ do {
644
+ const response = await dynamodb.send(new QueryCommand({
645
+ TableName: requireMainTableName(),
646
+ IndexName: "gsi1",
647
+ KeyConditionExpression: "gsi1pk = :pk",
648
+ ExpressionAttributeValues: {
649
+ ":pk": customerPk(customerId)
650
+ },
651
+ ScanIndexForward: false,
652
+ Limit: 500,
653
+ ExclusiveStartKey: exclusiveStartKey
654
+ }));
655
+ for (const item of response.Items ?? []) {
656
+ const job = itemToJob(item);
657
+ if (job.parentJobId === null && ACTIVE_ROOT_JOB_STATUSES.includes(job.status)) {
658
+ count += 1;
659
+ }
660
+ }
661
+ exclusiveStartKey = response.LastEvaluatedKey;
662
+ } while (exclusiveStartKey);
663
+ return count;
664
+ }
665
+ async listCapacityCustomerIds(limit) {
666
+ const customerIds = [];
667
+ let exclusiveStartKey;
668
+ do {
669
+ const response = await dynamodb.send(new ScanCommand({
670
+ TableName: requireMainTableName(),
671
+ FilterExpression: "entity_type = :entityType",
672
+ ExpressionAttributeValues: {
673
+ ":entityType": "CustomerJobCapacity"
674
+ },
675
+ Limit: Math.max(1, Math.min(limit - customerIds.length, 100)),
676
+ ExclusiveStartKey: exclusiveStartKey
677
+ }));
678
+ for (const item of response.Items ?? []) {
679
+ const customerId = typeof item.customer_id === "string"
680
+ ? item.customer_id
681
+ : parseCustomerIdFromCapacityPk(item.pk);
682
+ if (customerId) {
683
+ customerIds.push(customerId);
684
+ }
685
+ if (customerIds.length >= limit) {
686
+ break;
687
+ }
688
+ }
689
+ exclusiveStartKey = customerIds.length >= limit
690
+ ? undefined
691
+ : response.LastEvaluatedKey;
692
+ } while (exclusiveStartKey);
693
+ return customerIds;
694
+ }
695
+ async sweepStaleActiveJobsForCustomer(customerId, input = {}) {
696
+ const maxJobs = Math.max(1, input.maxJobs ?? 1000);
697
+ let scanned = 0;
698
+ let swept = 0;
699
+ let exclusiveStartKey;
700
+ do {
701
+ const response = await dynamodb.send(new QueryCommand({
702
+ TableName: requireMainTableName(),
703
+ IndexName: "gsi1",
704
+ KeyConditionExpression: "gsi1pk = :pk",
705
+ ExpressionAttributeValues: {
706
+ ":pk": customerPk(customerId)
707
+ },
708
+ ScanIndexForward: false,
709
+ Limit: Math.min(500, maxJobs - scanned),
710
+ ExclusiveStartKey: exclusiveStartKey
711
+ }));
712
+ for (const item of response.Items ?? []) {
713
+ scanned += 1;
714
+ const job = itemToJob(item);
715
+ const stale = describeStaleActiveJob(job);
716
+ if (!stale) {
717
+ continue;
718
+ }
719
+ await this.failStaleActiveJob(job, stale);
720
+ swept += 1;
721
+ }
722
+ exclusiveStartKey = scanned >= maxJobs
723
+ ? undefined
724
+ : response.LastEvaluatedKey;
725
+ } while (exclusiveStartKey);
726
+ if (swept > 0) {
727
+ await this.reconcileRootCapacityCounterAfterSweep(customerId);
728
+ }
729
+ return { scanned, swept };
730
+ }
731
+ async failStaleActiveJob(job, stale) {
732
+ const now = nowIso();
733
+ await this.addLogEvent({
734
+ id: createId("evt"),
735
+ jobId: job.id,
736
+ level: "error",
737
+ message: "Job marked failed by stale-job sweeper",
738
+ metadata: {
739
+ type: "stale_job_swept",
740
+ stale_reason: stale.reason,
741
+ stale_threshold_seconds: stale.thresholdSeconds,
742
+ stale_age_seconds: stale.ageSeconds,
743
+ previous_status: job.status,
744
+ started_at: job.startedAt,
745
+ updated_at: job.updatedAt,
746
+ created_at: job.createdAt
747
+ },
748
+ progress: job.progress,
749
+ artifactKey: null,
750
+ createdAt: now
751
+ });
752
+ await this.updateJobStatus({
753
+ id: job.id,
754
+ status: "failed",
755
+ error: {
756
+ message: `Job was marked failed because it became stale: ${stale.reason}.`,
757
+ type: "stale_job_swept",
758
+ stale_reason: stale.reason,
759
+ stale_threshold_seconds: stale.thresholdSeconds,
760
+ stale_age_seconds: stale.ageSeconds
761
+ },
762
+ completedAt: now
763
+ });
764
+ }
765
+ async reconcileRootCapacityCounterAfterSweep(customerId) {
766
+ const activeCount = await this.countActiveRootJobs(customerId);
767
+ await dynamodb.send(new UpdateCommand({
768
+ TableName: requireMainTableName(),
769
+ Key: customerJobCapacityKey(customerId),
770
+ UpdateExpression: "set active_count = :activeCount, capacity_limit = :limit, updated_at = :updated, entity_type = :entityType",
771
+ ExpressionAttributeValues: {
772
+ ":activeCount": activeCount,
773
+ ":limit": config.MAX_ACTIVE_JOBS_PER_CUSTOMER,
774
+ ":updated": nowIso(),
775
+ ":entityType": "CustomerJobCapacity"
776
+ }
777
+ }));
778
+ }
779
+ async decrementRootCapacity(customerId, jobId) {
780
+ try {
781
+ await dynamodb.send(new UpdateCommand({
782
+ TableName: requireMainTableName(),
783
+ Key: customerJobCapacityKey(customerId),
784
+ UpdateExpression: "set active_count = active_count - :one, updated_at = :updated",
785
+ ConditionExpression: "active_count >= :one",
786
+ ExpressionAttributeValues: {
787
+ ":one": 1,
788
+ ":updated": nowIso()
789
+ }
790
+ }));
791
+ devLog("jobs.capacity.decremented", {
792
+ job_id: jobId,
793
+ customer_id: customerId
794
+ });
795
+ }
796
+ catch (error) {
797
+ if (isConditionalCheckFailed(error)) {
798
+ devLog("jobs.capacity.decrement_skipped", {
799
+ job_id: jobId,
800
+ customer_id: customerId
801
+ }, "warn");
802
+ return;
803
+ }
804
+ throw error;
805
+ }
806
+ }
807
+ async startWorkflow(job) {
808
+ if (!config.VIDFARM_JOB_STATE_MACHINE_ARN) {
809
+ return null;
810
+ }
811
+ const response = await sfn.send(new StartExecutionCommand({
812
+ stateMachineArn: config.VIDFARM_JOB_STATE_MACHINE_ARN,
813
+ name: job.id.replace(/[^A-Za-z0-9-_]/g, "-").slice(0, 80),
814
+ input: JSON.stringify({
815
+ job_id: job.id,
816
+ customer_id: job.customerId,
817
+ template_id: job.templateId,
818
+ operation_name: job.operationName,
819
+ workflow_name: job.workflowName,
820
+ stage: "run",
821
+ attempt: 1
822
+ })
823
+ }));
824
+ devLog("jobs.workflow.started", {
825
+ job_id: job.id,
826
+ execution_arn: response.executionArn ?? null,
827
+ state_machine_arn: config.VIDFARM_JOB_STATE_MACHINE_ARN
828
+ });
829
+ return response.executionArn ?? null;
830
+ }
831
+ async addLogEvent(event) {
832
+ await dynamodb.send(new PutCommand({
833
+ TableName: requireMainTableName(),
834
+ Item: {
835
+ pk: jobPk(event.jobId),
836
+ sk: `LOG#${event.createdAt}#${event.id}`,
837
+ entity_type: "JobLogEvent",
838
+ id: event.id,
839
+ job_id: event.jobId,
840
+ level: event.level,
841
+ message: event.message,
842
+ metadata_json: event.metadata,
843
+ progress: event.progress,
844
+ artifact_key: event.artifactKey,
845
+ created_at: event.createdAt
846
+ }
847
+ }));
848
+ }
849
+ }
850
+ function isConditionalCheckFailed(error) {
851
+ return Boolean(error
852
+ && typeof error === "object"
853
+ && "name" in error
854
+ && error.name === "ConditionalCheckFailedException");
855
+ }
856
+ function isTransactionCancelledForCapacity(error) {
857
+ if (!error || typeof error !== "object" || !("name" in error) || error.name !== "TransactionCanceledException") {
858
+ return false;
859
+ }
860
+ const reasons = "CancellationReasons" in error ? error.CancellationReasons : null;
861
+ return Array.isArray(reasons) && reasons.some((reason) => {
862
+ return reason && typeof reason === "object" && "Code" in reason && reason.Code === "ConditionalCheckFailed";
863
+ });
864
+ }
865
+ function requireMainTableName() {
866
+ if (!config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME) {
867
+ throw new Error("Serverless job store requires VIDFARM_SERVERLESS_MAIN_TABLE_NAME.");
868
+ }
869
+ return config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME;
870
+ }
871
+ function normalizeWebhookUrl(value) {
872
+ if (typeof value !== "string") {
873
+ return null;
874
+ }
875
+ const trimmed = value.trim();
876
+ return trimmed ? trimmed : null;
877
+ }
878
+ function jobPk(jobId) {
879
+ return `JOB#${jobId}`;
880
+ }
881
+ function jobKey(jobId) {
882
+ return {
883
+ pk: jobPk(jobId),
884
+ sk: "META"
885
+ };
886
+ }
887
+ function customerPk(customerId) {
888
+ return `CUSTOMER#${customerId}`;
889
+ }
890
+ function customerJobCapacityKey(customerId) {
891
+ return {
892
+ pk: customerPk(customerId),
893
+ sk: "JOB_CAPACITY"
894
+ };
895
+ }
896
+ function parseCustomerIdFromCapacityPk(value) {
897
+ if (typeof value !== "string" || !value.startsWith("CUSTOMER#")) {
898
+ return null;
899
+ }
900
+ return value.slice("CUSTOMER#".length) || null;
901
+ }
902
+ function describeStaleActiveJob(job) {
903
+ if (!ACTIVE_JOB_STATUSES.has(job.status)) {
904
+ return null;
905
+ }
906
+ const wallClockAgeSeconds = secondsSince(job.createdAt);
907
+ if (wallClockAgeSeconds >= config.MAX_JOB_WALL_CLOCK_SECONDS) {
908
+ return {
909
+ reason: "wall_clock_timeout",
910
+ thresholdSeconds: config.MAX_JOB_WALL_CLOCK_SECONDS,
911
+ ageSeconds: wallClockAgeSeconds
912
+ };
913
+ }
914
+ if (job.status === "running") {
915
+ const heartbeatAt = job.updatedAt || job.startedAt || job.createdAt;
916
+ const heartbeatAgeSeconds = secondsSince(heartbeatAt);
917
+ if (heartbeatAgeSeconds >= config.STALE_RUNNING_JOB_SECONDS) {
918
+ return {
919
+ reason: "running_heartbeat_timeout",
920
+ thresholdSeconds: config.STALE_RUNNING_JOB_SECONDS,
921
+ ageSeconds: heartbeatAgeSeconds
922
+ };
923
+ }
924
+ }
925
+ if (job.status === "queued") {
926
+ const runAfterAgeSeconds = secondsSince(job.runAfter);
927
+ if (runAfterAgeSeconds >= config.MAX_JOB_WALL_CLOCK_SECONDS) {
928
+ return {
929
+ reason: "queued_run_after_timeout",
930
+ thresholdSeconds: config.MAX_JOB_WALL_CLOCK_SECONDS,
931
+ ageSeconds: runAfterAgeSeconds
932
+ };
933
+ }
934
+ }
935
+ return null;
936
+ }
937
+ function secondsSince(value) {
938
+ const timestamp = value ? Date.parse(value) : Number.NaN;
939
+ const effectiveTimestamp = Number.isFinite(timestamp) ? timestamp : Date.now();
940
+ return Math.max(0, Math.floor((Date.now() - effectiveTimestamp) / 1000));
941
+ }
942
+ function customerTemplatePk(customerId, templateId) {
943
+ return `CUSTOMER#${customerId}#TEMPLATE#${templateId}`;
944
+ }
945
+ function customerTracerPk(customerId, tracer) {
946
+ return `CUSTOMER#${customerId}#TRACER#${tracer}`;
947
+ }
948
+ function jobSortKey(job) {
949
+ return `JOB#${job.createdAt}#${job.id}`;
950
+ }
951
+ function jobToItem(job) {
952
+ return {
953
+ pk: jobPk(job.id),
954
+ sk: "META",
955
+ entity_type: "Job",
956
+ gsi1pk: customerPk(job.customerId),
957
+ gsi1sk: jobSortKey(job),
958
+ gsi2pk: customerTemplatePk(job.customerId, job.templateId),
959
+ gsi2sk: jobSortKey(job),
960
+ gsi3pk: customerTracerPk(job.customerId, job.tracer),
961
+ gsi3sk: jobSortKey(job),
962
+ job_id: job.id,
963
+ template_id: job.templateId,
964
+ operation_name: job.operationName,
965
+ workflow_name: job.workflowName,
966
+ tracer: job.tracer,
967
+ status: job.status,
968
+ customer_id: job.customerId,
969
+ payload_json: job.payload,
970
+ result_json: job.result,
971
+ error_json: job.error,
972
+ progress: job.progress,
973
+ webhook_url: job.webhookUrl,
974
+ parent_job_id: job.parentJobId,
975
+ priority: job.priority,
976
+ attempt_count: job.attemptCount,
977
+ max_attempts: job.maxAttempts,
978
+ run_after: job.runAfter,
979
+ created_at: job.createdAt,
980
+ updated_at: job.updatedAt,
981
+ started_at: job.startedAt,
982
+ completed_at: job.completedAt
983
+ };
984
+ }
985
+ function itemToJob(item) {
986
+ return {
987
+ id: String(item.job_id),
988
+ templateId: String(item.template_id),
989
+ operationName: String(item.operation_name),
990
+ workflowName: String(item.workflow_name),
991
+ tracer: String(item.tracer),
992
+ status: item.status,
993
+ customerId: String(item.customer_id),
994
+ payload: asRecord(item.payload_json),
995
+ result: item.result_json == null ? null : asRecord(item.result_json),
996
+ error: item.error_json == null ? null : asRecord(item.error_json),
997
+ progress: typeof item.progress === "number" ? item.progress : 0,
998
+ webhookUrl: typeof item.webhook_url === "string" ? item.webhook_url : null,
999
+ parentJobId: typeof item.parent_job_id === "string" ? item.parent_job_id : null,
1000
+ priority: typeof item.priority === "number" ? item.priority : 100,
1001
+ attemptCount: typeof item.attempt_count === "number" ? item.attempt_count : 0,
1002
+ maxAttempts: typeof item.max_attempts === "number" ? item.max_attempts : 6,
1003
+ runAfter: String(item.run_after),
1004
+ createdAt: String(item.created_at),
1005
+ updatedAt: String(item.updated_at),
1006
+ startedAt: typeof item.started_at === "string" ? item.started_at : null,
1007
+ completedAt: typeof item.completed_at === "string" ? item.completed_at : null
1008
+ };
1009
+ }
1010
+ function itemToLog(item) {
1011
+ return {
1012
+ id: String(item.id),
1013
+ jobId: String(item.job_id),
1014
+ level: item.level,
1015
+ message: String(item.message),
1016
+ metadata: asRecord(item.metadata_json),
1017
+ progress: typeof item.progress === "number" ? item.progress : null,
1018
+ artifactKey: typeof item.artifact_key === "string" ? item.artifact_key : null,
1019
+ createdAt: String(item.created_at)
1020
+ };
1021
+ }
1022
+ function artifactToItem(artifact) {
1023
+ return {
1024
+ pk: jobPk(artifact.jobId),
1025
+ sk: `ARTIFACT#${artifact.createdAt}#${artifact.id}`,
1026
+ entity_type: "Artifact",
1027
+ id: artifact.id,
1028
+ artifact_id: artifact.id,
1029
+ job_id: artifact.jobId,
1030
+ customer_id: artifact.customerId,
1031
+ template_id: artifact.templateId,
1032
+ kind: artifact.kind,
1033
+ storage_key: artifact.storageKey,
1034
+ public_url: artifact.publicUrl,
1035
+ metadata_json: artifact.metadata,
1036
+ created_at: artifact.createdAt
1037
+ };
1038
+ }
1039
+ function itemToArtifact(item) {
1040
+ return {
1041
+ id: String(item.artifact_id ?? item.id),
1042
+ jobId: String(item.job_id),
1043
+ customerId: String(item.customer_id),
1044
+ templateId: String(item.template_id),
1045
+ kind: String(item.kind),
1046
+ storageKey: String(item.storage_key),
1047
+ publicUrl: typeof item.public_url === "string" ? item.public_url : null,
1048
+ metadata: asRecord(item.metadata_json),
1049
+ createdAt: String(item.created_at)
1050
+ };
1051
+ }
1052
+ function asRecord(value) {
1053
+ return value && typeof value === "object" && !Array.isArray(value)
1054
+ ? value
1055
+ : {};
1056
+ }
1057
+ function cursorToStartKey(input, index) {
1058
+ const cursor = input.cursor;
1059
+ if (!cursor) {
1060
+ return undefined;
1061
+ }
1062
+ const syntheticJob = { id: cursor.id, createdAt: cursor.createdAt };
1063
+ return {
1064
+ pk: jobPk(cursor.id),
1065
+ sk: "META",
1066
+ [`${index}pk`]: input.templateId
1067
+ ? customerTemplatePk(input.customerId, input.templateId)
1068
+ : input.tracer
1069
+ ? customerTracerPk(input.customerId, input.tracer)
1070
+ : customerPk(input.customerId),
1071
+ [`${index}sk`]: jobSortKey(syntheticJob)
1072
+ };
1073
+ }
1074
+ //# sourceMappingURL=serverless-jobs.js.map