@mevdragon/vidfarm-devcli 0.20.6 → 0.20.9

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