@coji/durably 0.8.0 → 0.9.0
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.
- package/README.md +1 -1
- package/dist/{index-4aPZWn8r.d.ts → index-BjlCb0gP.d.ts} +69 -19
- package/dist/index.d.ts +3 -2
- package/dist/index.js +250 -80
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +1 -1
- package/docs/llms.md +61 -10
- package/package.json +16 -16
package/dist/index.js
CHANGED
|
@@ -93,15 +93,17 @@ function createJobHandle(jobDef, storage, eventEmitter, registry) {
|
|
|
93
93
|
const validatedInput = validateJobInputOrThrow(inputSchema, input);
|
|
94
94
|
const run = await storage.createRun({
|
|
95
95
|
jobName: jobDef.name,
|
|
96
|
-
|
|
96
|
+
input: validatedInput,
|
|
97
97
|
idempotencyKey: options?.idempotencyKey,
|
|
98
|
-
concurrencyKey: options?.concurrencyKey
|
|
98
|
+
concurrencyKey: options?.concurrencyKey,
|
|
99
|
+
labels: options?.labels
|
|
99
100
|
});
|
|
100
101
|
eventEmitter.emit({
|
|
101
102
|
type: "run:trigger",
|
|
102
103
|
runId: run.id,
|
|
103
104
|
jobName: jobDef.name,
|
|
104
|
-
|
|
105
|
+
input: validatedInput,
|
|
106
|
+
labels: run.labels
|
|
105
107
|
});
|
|
106
108
|
return run;
|
|
107
109
|
},
|
|
@@ -177,16 +179,17 @@ function createJobHandle(jobDef, storage, eventEmitter, registry) {
|
|
|
177
179
|
`at index ${i}`
|
|
178
180
|
);
|
|
179
181
|
validated.push({
|
|
180
|
-
|
|
182
|
+
input: validatedInput,
|
|
181
183
|
options: normalized[i].options
|
|
182
184
|
});
|
|
183
185
|
}
|
|
184
186
|
const runs = await storage.batchCreateRuns(
|
|
185
187
|
validated.map((v) => ({
|
|
186
188
|
jobName: jobDef.name,
|
|
187
|
-
|
|
189
|
+
input: v.input,
|
|
188
190
|
idempotencyKey: v.options?.idempotencyKey,
|
|
189
|
-
concurrencyKey: v.options?.concurrencyKey
|
|
191
|
+
concurrencyKey: v.options?.concurrencyKey,
|
|
192
|
+
labels: v.options?.labels
|
|
190
193
|
}))
|
|
191
194
|
);
|
|
192
195
|
for (let i = 0; i < runs.length; i++) {
|
|
@@ -194,7 +197,8 @@ function createJobHandle(jobDef, storage, eventEmitter, registry) {
|
|
|
194
197
|
type: "run:trigger",
|
|
195
198
|
runId: runs[i].id,
|
|
196
199
|
jobName: jobDef.name,
|
|
197
|
-
|
|
200
|
+
input: validated[i].input,
|
|
201
|
+
labels: runs[i].labels
|
|
198
202
|
});
|
|
199
203
|
}
|
|
200
204
|
return runs;
|
|
@@ -230,11 +234,11 @@ var migrations = [
|
|
|
230
234
|
{
|
|
231
235
|
version: 1,
|
|
232
236
|
up: async (db) => {
|
|
233
|
-
await db.schema.createTable("durably_runs").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("job_name", "text", (col) => col.notNull()).addColumn("
|
|
237
|
+
await db.schema.createTable("durably_runs").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("job_name", "text", (col) => col.notNull()).addColumn("input", "text", (col) => col.notNull()).addColumn("status", "text", (col) => col.notNull()).addColumn("idempotency_key", "text").addColumn("concurrency_key", "text").addColumn("labels", "text", (col) => col.notNull().defaultTo("{}")).addColumn(
|
|
234
238
|
"current_step_index",
|
|
235
239
|
"integer",
|
|
236
240
|
(col) => col.notNull().defaultTo(0)
|
|
237
|
-
).addColumn("progress", "text").addColumn("output", "text").addColumn("error", "text").addColumn("heartbeat_at", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.notNull()).addColumn("updated_at", "text", (col) => col.notNull()).execute();
|
|
241
|
+
).addColumn("progress", "text").addColumn("output", "text").addColumn("error", "text").addColumn("heartbeat_at", "text", (col) => col.notNull()).addColumn("started_at", "text").addColumn("completed_at", "text").addColumn("created_at", "text", (col) => col.notNull()).addColumn("updated_at", "text", (col) => col.notNull()).execute();
|
|
238
242
|
await db.schema.createIndex("idx_durably_runs_job_idempotency").ifNotExists().on("durably_runs").columns(["job_name", "idempotency_key"]).unique().execute();
|
|
239
243
|
await db.schema.createIndex("idx_durably_runs_status_concurrency").ifNotExists().on("durably_runs").columns(["status", "concurrency_key"]).execute();
|
|
240
244
|
await db.schema.createIndex("idx_durably_runs_status_created").ifNotExists().on("durably_runs").columns(["status", "created_at"]).execute();
|
|
@@ -258,22 +262,47 @@ async function runMigrations(db) {
|
|
|
258
262
|
const currentVersion = await getCurrentVersion(db);
|
|
259
263
|
for (const migration of migrations) {
|
|
260
264
|
if (migration.version > currentVersion) {
|
|
261
|
-
await
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
265
|
+
await db.transaction().execute(async (trx) => {
|
|
266
|
+
await migration.up(trx);
|
|
267
|
+
await trx.insertInto("durably_schema_versions").values({
|
|
268
|
+
version: migration.version,
|
|
269
|
+
applied_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
270
|
+
}).execute();
|
|
271
|
+
});
|
|
266
272
|
}
|
|
267
273
|
}
|
|
268
274
|
}
|
|
269
275
|
|
|
270
276
|
// src/storage.ts
|
|
271
|
-
import {
|
|
277
|
+
import { sql } from "kysely";
|
|
278
|
+
import { monotonicFactory } from "ulidx";
|
|
279
|
+
var ulid = monotonicFactory();
|
|
280
|
+
function toClientRun(run) {
|
|
281
|
+
const {
|
|
282
|
+
idempotencyKey,
|
|
283
|
+
concurrencyKey,
|
|
284
|
+
heartbeatAt,
|
|
285
|
+
updatedAt,
|
|
286
|
+
...clientRun
|
|
287
|
+
} = run;
|
|
288
|
+
return clientRun;
|
|
289
|
+
}
|
|
290
|
+
var LABEL_KEY_PATTERN = /^[a-zA-Z0-9\-_./]+$/;
|
|
291
|
+
function validateLabels(labels) {
|
|
292
|
+
if (!labels) return;
|
|
293
|
+
for (const key of Object.keys(labels)) {
|
|
294
|
+
if (!LABEL_KEY_PATTERN.test(key)) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`Invalid label key "${key}": must contain only alphanumeric characters, dashes, underscores, dots, and slashes`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
272
301
|
function rowToRun(row) {
|
|
273
302
|
return {
|
|
274
303
|
id: row.id,
|
|
275
304
|
jobName: row.job_name,
|
|
276
|
-
|
|
305
|
+
input: JSON.parse(row.input),
|
|
277
306
|
status: row.status,
|
|
278
307
|
idempotencyKey: row.idempotency_key,
|
|
279
308
|
concurrencyKey: row.concurrency_key,
|
|
@@ -282,7 +311,10 @@ function rowToRun(row) {
|
|
|
282
311
|
progress: row.progress ? JSON.parse(row.progress) : null,
|
|
283
312
|
output: row.output ? JSON.parse(row.output) : null,
|
|
284
313
|
error: row.error,
|
|
314
|
+
labels: JSON.parse(row.labels),
|
|
285
315
|
heartbeatAt: row.heartbeat_at,
|
|
316
|
+
startedAt: row.started_at,
|
|
317
|
+
completedAt: row.completed_at,
|
|
286
318
|
createdAt: row.created_at,
|
|
287
319
|
updatedAt: row.updated_at
|
|
288
320
|
};
|
|
@@ -321,11 +353,12 @@ function createKyselyStorage(db) {
|
|
|
321
353
|
return rowToRun(existing);
|
|
322
354
|
}
|
|
323
355
|
}
|
|
356
|
+
validateLabels(input.labels);
|
|
324
357
|
const id = ulid();
|
|
325
358
|
const run = {
|
|
326
359
|
id,
|
|
327
360
|
job_name: input.jobName,
|
|
328
|
-
|
|
361
|
+
input: JSON.stringify(input.input),
|
|
329
362
|
status: "pending",
|
|
330
363
|
idempotency_key: input.idempotencyKey ?? null,
|
|
331
364
|
concurrency_key: input.concurrencyKey ?? null,
|
|
@@ -333,7 +366,10 @@ function createKyselyStorage(db) {
|
|
|
333
366
|
progress: null,
|
|
334
367
|
output: null,
|
|
335
368
|
error: null,
|
|
369
|
+
labels: JSON.stringify(input.labels ?? {}),
|
|
336
370
|
heartbeat_at: now,
|
|
371
|
+
started_at: null,
|
|
372
|
+
completed_at: null,
|
|
337
373
|
created_at: now,
|
|
338
374
|
updated_at: now
|
|
339
375
|
};
|
|
@@ -347,6 +383,9 @@ function createKyselyStorage(db) {
|
|
|
347
383
|
return await db.transaction().execute(async (trx) => {
|
|
348
384
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
349
385
|
const runs = [];
|
|
386
|
+
for (const input of inputs) {
|
|
387
|
+
validateLabels(input.labels);
|
|
388
|
+
}
|
|
350
389
|
for (const input of inputs) {
|
|
351
390
|
if (input.idempotencyKey) {
|
|
352
391
|
const existing = await trx.selectFrom("durably_runs").selectAll().where("job_name", "=", input.jobName).where("idempotency_key", "=", input.idempotencyKey).executeTakeFirst();
|
|
@@ -359,7 +398,7 @@ function createKyselyStorage(db) {
|
|
|
359
398
|
runs.push({
|
|
360
399
|
id,
|
|
361
400
|
job_name: input.jobName,
|
|
362
|
-
|
|
401
|
+
input: JSON.stringify(input.input),
|
|
363
402
|
status: "pending",
|
|
364
403
|
idempotency_key: input.idempotencyKey ?? null,
|
|
365
404
|
concurrency_key: input.concurrencyKey ?? null,
|
|
@@ -367,7 +406,10 @@ function createKyselyStorage(db) {
|
|
|
367
406
|
progress: null,
|
|
368
407
|
output: null,
|
|
369
408
|
error: null,
|
|
409
|
+
labels: JSON.stringify(input.labels ?? {}),
|
|
370
410
|
heartbeat_at: now,
|
|
411
|
+
started_at: null,
|
|
412
|
+
completed_at: null,
|
|
371
413
|
created_at: now,
|
|
372
414
|
updated_at: now
|
|
373
415
|
});
|
|
@@ -394,6 +436,9 @@ function createKyselyStorage(db) {
|
|
|
394
436
|
if (data.error !== void 0) updates.error = data.error;
|
|
395
437
|
if (data.heartbeatAt !== void 0)
|
|
396
438
|
updates.heartbeat_at = data.heartbeatAt;
|
|
439
|
+
if (data.startedAt !== void 0) updates.started_at = data.startedAt;
|
|
440
|
+
if (data.completedAt !== void 0)
|
|
441
|
+
updates.completed_at = data.completedAt;
|
|
397
442
|
await db.updateTable("durably_runs").set(updates).where("id", "=", runId).execute();
|
|
398
443
|
},
|
|
399
444
|
async deleteRun(runId) {
|
|
@@ -417,6 +462,16 @@ function createKyselyStorage(db) {
|
|
|
417
462
|
if (filter?.jobName) {
|
|
418
463
|
query = query.where("durably_runs.job_name", "=", filter.jobName);
|
|
419
464
|
}
|
|
465
|
+
if (filter?.labels) {
|
|
466
|
+
validateLabels(filter.labels);
|
|
467
|
+
for (const [key, value] of Object.entries(filter.labels)) {
|
|
468
|
+
query = query.where(
|
|
469
|
+
sql`json_extract(durably_runs.labels, ${`$."${key}"`})`,
|
|
470
|
+
"=",
|
|
471
|
+
value
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
420
475
|
query = query.orderBy("durably_runs.created_at", "desc");
|
|
421
476
|
if (filter?.limit !== void 0) {
|
|
422
477
|
query = query.limit(filter.limit);
|
|
@@ -430,24 +485,29 @@ function createKyselyStorage(db) {
|
|
|
430
485
|
const rows = await query.execute();
|
|
431
486
|
return rows.map(rowToRun);
|
|
432
487
|
},
|
|
433
|
-
async
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
).where("durably_runs.status", "=", "pending").groupBy("durably_runs.id").orderBy("durably_runs.created_at", "asc").orderBy("durably_runs.id", "asc").limit(1);
|
|
488
|
+
async claimNextPendingRun(excludeConcurrencyKeys) {
|
|
489
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
490
|
+
let subquery = db.selectFrom("durably_runs").select("id").where("status", "=", "pending").orderBy("created_at", "asc").orderBy("id", "asc").limit(1);
|
|
437
491
|
if (excludeConcurrencyKeys.length > 0) {
|
|
438
|
-
|
|
492
|
+
subquery = subquery.where(
|
|
439
493
|
(eb) => eb.or([
|
|
440
|
-
eb("
|
|
441
|
-
eb(
|
|
442
|
-
"durably_runs.concurrency_key",
|
|
443
|
-
"not in",
|
|
444
|
-
excludeConcurrencyKeys
|
|
445
|
-
)
|
|
494
|
+
eb("concurrency_key", "is", null),
|
|
495
|
+
eb("concurrency_key", "not in", excludeConcurrencyKeys)
|
|
446
496
|
])
|
|
447
497
|
);
|
|
448
498
|
}
|
|
449
|
-
const row = await
|
|
450
|
-
|
|
499
|
+
const row = await db.updateTable("durably_runs").set({
|
|
500
|
+
status: "running",
|
|
501
|
+
heartbeat_at: now,
|
|
502
|
+
started_at: sql`COALESCE(started_at, ${now})`,
|
|
503
|
+
updated_at: now
|
|
504
|
+
}).where(
|
|
505
|
+
"id",
|
|
506
|
+
"=",
|
|
507
|
+
(eb) => eb.selectFrom(subquery.as("sub")).select("id")
|
|
508
|
+
).returningAll().executeTakeFirst();
|
|
509
|
+
if (!row) return null;
|
|
510
|
+
return rowToRun({ ...row, step_count: 0 });
|
|
451
511
|
},
|
|
452
512
|
async createStep(input) {
|
|
453
513
|
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -514,13 +574,26 @@ function getErrorMessage(error) {
|
|
|
514
574
|
function createStepContext(run, jobName, storage, eventEmitter) {
|
|
515
575
|
let stepIndex = run.currentStepIndex;
|
|
516
576
|
let currentStepName = null;
|
|
517
|
-
|
|
577
|
+
const controller = new AbortController();
|
|
578
|
+
const unsubscribe = eventEmitter.on("run:cancel", (event) => {
|
|
579
|
+
if (event.runId === run.id) {
|
|
580
|
+
controller.abort();
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
const step = {
|
|
518
584
|
get runId() {
|
|
519
585
|
return run.id;
|
|
520
586
|
},
|
|
521
587
|
async run(name, fn) {
|
|
588
|
+
if (controller.signal.aborted) {
|
|
589
|
+
throw new CancelledError(run.id);
|
|
590
|
+
}
|
|
522
591
|
const currentRun = await storage.getRun(run.id);
|
|
523
592
|
if (currentRun?.status === "cancelled") {
|
|
593
|
+
controller.abort();
|
|
594
|
+
throw new CancelledError(run.id);
|
|
595
|
+
}
|
|
596
|
+
if (controller.signal.aborted) {
|
|
524
597
|
throw new CancelledError(run.id);
|
|
525
598
|
}
|
|
526
599
|
const existingStep = await storage.getCompletedStep(run.id, name);
|
|
@@ -536,10 +609,11 @@ function createStepContext(run, jobName, storage, eventEmitter) {
|
|
|
536
609
|
runId: run.id,
|
|
537
610
|
jobName,
|
|
538
611
|
stepName: name,
|
|
539
|
-
stepIndex
|
|
612
|
+
stepIndex,
|
|
613
|
+
labels: run.labels
|
|
540
614
|
});
|
|
541
615
|
try {
|
|
542
|
-
const result = await fn();
|
|
616
|
+
const result = await fn(controller.signal);
|
|
543
617
|
await storage.createStep({
|
|
544
618
|
runId: run.id,
|
|
545
619
|
name,
|
|
@@ -557,27 +631,32 @@ function createStepContext(run, jobName, storage, eventEmitter) {
|
|
|
557
631
|
stepName: name,
|
|
558
632
|
stepIndex: stepIndex - 1,
|
|
559
633
|
output: result,
|
|
560
|
-
duration: Date.now() - startTime
|
|
634
|
+
duration: Date.now() - startTime,
|
|
635
|
+
labels: run.labels
|
|
561
636
|
});
|
|
562
637
|
return result;
|
|
563
638
|
} catch (error) {
|
|
639
|
+
const isCancelled = controller.signal.aborted;
|
|
564
640
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
565
641
|
await storage.createStep({
|
|
566
642
|
runId: run.id,
|
|
567
643
|
name,
|
|
568
644
|
index: stepIndex,
|
|
569
|
-
status: "failed",
|
|
645
|
+
status: isCancelled ? "cancelled" : "failed",
|
|
570
646
|
error: errorMessage,
|
|
571
647
|
startedAt
|
|
572
648
|
});
|
|
573
649
|
eventEmitter.emit({
|
|
574
|
-
type: "step:fail",
|
|
650
|
+
...isCancelled ? { type: "step:cancel" } : { type: "step:fail", error: errorMessage },
|
|
575
651
|
runId: run.id,
|
|
576
652
|
jobName,
|
|
577
653
|
stepName: name,
|
|
578
654
|
stepIndex,
|
|
579
|
-
|
|
655
|
+
labels: run.labels
|
|
580
656
|
});
|
|
657
|
+
if (isCancelled) {
|
|
658
|
+
throw new CancelledError(run.id);
|
|
659
|
+
}
|
|
581
660
|
throw error;
|
|
582
661
|
} finally {
|
|
583
662
|
currentStepName = null;
|
|
@@ -590,7 +669,8 @@ function createStepContext(run, jobName, storage, eventEmitter) {
|
|
|
590
669
|
type: "run:progress",
|
|
591
670
|
runId: run.id,
|
|
592
671
|
jobName,
|
|
593
|
-
progress: progressData
|
|
672
|
+
progress: progressData,
|
|
673
|
+
labels: run.labels
|
|
594
674
|
});
|
|
595
675
|
},
|
|
596
676
|
log: {
|
|
@@ -598,6 +678,8 @@ function createStepContext(run, jobName, storage, eventEmitter) {
|
|
|
598
678
|
eventEmitter.emit({
|
|
599
679
|
type: "log:write",
|
|
600
680
|
runId: run.id,
|
|
681
|
+
jobName,
|
|
682
|
+
labels: run.labels,
|
|
601
683
|
stepName: currentStepName,
|
|
602
684
|
level: "info",
|
|
603
685
|
message,
|
|
@@ -608,6 +690,8 @@ function createStepContext(run, jobName, storage, eventEmitter) {
|
|
|
608
690
|
eventEmitter.emit({
|
|
609
691
|
type: "log:write",
|
|
610
692
|
runId: run.id,
|
|
693
|
+
jobName,
|
|
694
|
+
labels: run.labels,
|
|
611
695
|
stepName: currentStepName,
|
|
612
696
|
level: "warn",
|
|
613
697
|
message,
|
|
@@ -618,6 +702,8 @@ function createStepContext(run, jobName, storage, eventEmitter) {
|
|
|
618
702
|
eventEmitter.emit({
|
|
619
703
|
type: "log:write",
|
|
620
704
|
runId: run.id,
|
|
705
|
+
jobName,
|
|
706
|
+
labels: run.labels,
|
|
621
707
|
stepName: currentStepName,
|
|
622
708
|
level: "error",
|
|
623
709
|
message,
|
|
@@ -626,6 +712,7 @@ function createStepContext(run, jobName, storage, eventEmitter) {
|
|
|
626
712
|
}
|
|
627
713
|
}
|
|
628
714
|
};
|
|
715
|
+
return { step, dispose: unsubscribe };
|
|
629
716
|
}
|
|
630
717
|
|
|
631
718
|
// src/worker.ts
|
|
@@ -658,19 +745,21 @@ function createWorker(config, storage, eventEmitter, jobRegistry) {
|
|
|
658
745
|
}
|
|
659
746
|
async function handleRunSuccess(runId, jobName, output, startTime) {
|
|
660
747
|
const currentRun = await storage.getRun(runId);
|
|
661
|
-
if (currentRun
|
|
748
|
+
if (!currentRun || currentRun.status === "cancelled") {
|
|
662
749
|
return;
|
|
663
750
|
}
|
|
664
751
|
await storage.updateRun(runId, {
|
|
665
752
|
status: "completed",
|
|
666
|
-
output
|
|
753
|
+
output,
|
|
754
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
667
755
|
});
|
|
668
756
|
eventEmitter.emit({
|
|
669
757
|
type: "run:complete",
|
|
670
758
|
runId,
|
|
671
759
|
jobName,
|
|
672
760
|
output,
|
|
673
|
-
duration: Date.now() - startTime
|
|
761
|
+
duration: Date.now() - startTime,
|
|
762
|
+
labels: currentRun.labels
|
|
674
763
|
});
|
|
675
764
|
}
|
|
676
765
|
async function handleRunFailure(runId, jobName, error) {
|
|
@@ -678,7 +767,7 @@ function createWorker(config, storage, eventEmitter, jobRegistry) {
|
|
|
678
767
|
return;
|
|
679
768
|
}
|
|
680
769
|
const currentRun = await storage.getRun(runId);
|
|
681
|
-
if (currentRun
|
|
770
|
+
if (!currentRun || currentRun.status === "cancelled") {
|
|
682
771
|
return;
|
|
683
772
|
}
|
|
684
773
|
const errorMessage = getErrorMessage(error);
|
|
@@ -686,14 +775,16 @@ function createWorker(config, storage, eventEmitter, jobRegistry) {
|
|
|
686
775
|
const failedStep = steps.find((s) => s.status === "failed");
|
|
687
776
|
await storage.updateRun(runId, {
|
|
688
777
|
status: "failed",
|
|
689
|
-
error: errorMessage
|
|
778
|
+
error: errorMessage,
|
|
779
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
690
780
|
});
|
|
691
781
|
eventEmitter.emit({
|
|
692
782
|
type: "run:fail",
|
|
693
783
|
runId,
|
|
694
784
|
jobName,
|
|
695
785
|
error: errorMessage,
|
|
696
|
-
failedStepName: failedStep?.name ?? "unknown"
|
|
786
|
+
failedStepName: failedStep?.name ?? "unknown",
|
|
787
|
+
labels: currentRun.labels
|
|
697
788
|
});
|
|
698
789
|
}
|
|
699
790
|
async function executeRun(run, job) {
|
|
@@ -712,12 +803,18 @@ function createWorker(config, storage, eventEmitter, jobRegistry) {
|
|
|
712
803
|
type: "run:start",
|
|
713
804
|
runId: run.id,
|
|
714
805
|
jobName: run.jobName,
|
|
715
|
-
|
|
806
|
+
input: run.input,
|
|
807
|
+
labels: run.labels
|
|
716
808
|
});
|
|
717
809
|
const startTime = Date.now();
|
|
810
|
+
const { step, dispose } = createStepContext(
|
|
811
|
+
run,
|
|
812
|
+
run.jobName,
|
|
813
|
+
storage,
|
|
814
|
+
eventEmitter
|
|
815
|
+
);
|
|
718
816
|
try {
|
|
719
|
-
const
|
|
720
|
-
const output = await job.fn(step, run.payload);
|
|
817
|
+
const output = await job.fn(step, run.input);
|
|
721
818
|
if (job.outputSchema) {
|
|
722
819
|
const parseResult = job.outputSchema.safeParse(output);
|
|
723
820
|
if (!parseResult.success) {
|
|
@@ -728,6 +825,7 @@ function createWorker(config, storage, eventEmitter, jobRegistry) {
|
|
|
728
825
|
} catch (error) {
|
|
729
826
|
await handleRunFailure(run.id, run.jobName, error);
|
|
730
827
|
} finally {
|
|
828
|
+
dispose();
|
|
731
829
|
if (heartbeatInterval) {
|
|
732
830
|
clearInterval(heartbeatInterval);
|
|
733
831
|
heartbeatInterval = null;
|
|
@@ -740,7 +838,7 @@ function createWorker(config, storage, eventEmitter, jobRegistry) {
|
|
|
740
838
|
const excludeConcurrencyKeys = runningRuns.filter(
|
|
741
839
|
(r) => r.concurrencyKey !== null
|
|
742
840
|
).map((r) => r.concurrencyKey);
|
|
743
|
-
const run = await storage.
|
|
841
|
+
const run = await storage.claimNextPendingRun(excludeConcurrencyKeys);
|
|
744
842
|
if (!run) {
|
|
745
843
|
return false;
|
|
746
844
|
}
|
|
@@ -752,10 +850,6 @@ function createWorker(config, storage, eventEmitter, jobRegistry) {
|
|
|
752
850
|
});
|
|
753
851
|
return true;
|
|
754
852
|
}
|
|
755
|
-
await storage.updateRun(run.id, {
|
|
756
|
-
status: "running",
|
|
757
|
-
heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
758
|
-
});
|
|
759
853
|
await executeRun(run, job);
|
|
760
854
|
return true;
|
|
761
855
|
}
|
|
@@ -889,6 +983,14 @@ function createDurablyInstance(state, jobs) {
|
|
|
889
983
|
controller.enqueue(event);
|
|
890
984
|
}
|
|
891
985
|
});
|
|
986
|
+
const unsubscribeDelete = eventEmitter.on("run:delete", (event) => {
|
|
987
|
+
if (!closed && event.runId === runId) {
|
|
988
|
+
controller.enqueue(event);
|
|
989
|
+
closed = true;
|
|
990
|
+
cleanup?.();
|
|
991
|
+
controller.close();
|
|
992
|
+
}
|
|
993
|
+
});
|
|
892
994
|
const unsubscribeRetry = eventEmitter.on("run:retry", (event) => {
|
|
893
995
|
if (!closed && event.runId === runId) {
|
|
894
996
|
controller.enqueue(event);
|
|
@@ -933,6 +1035,7 @@ function createDurablyInstance(state, jobs) {
|
|
|
933
1035
|
unsubscribeComplete();
|
|
934
1036
|
unsubscribeFail();
|
|
935
1037
|
unsubscribeCancel();
|
|
1038
|
+
unsubscribeDelete();
|
|
936
1039
|
unsubscribeRetry();
|
|
937
1040
|
unsubscribeProgress();
|
|
938
1041
|
unsubscribeStepStart();
|
|
@@ -970,7 +1073,8 @@ function createDurablyInstance(state, jobs) {
|
|
|
970
1073
|
eventEmitter.emit({
|
|
971
1074
|
type: "run:retry",
|
|
972
1075
|
runId,
|
|
973
|
-
jobName: run.jobName
|
|
1076
|
+
jobName: run.jobName,
|
|
1077
|
+
labels: run.labels
|
|
974
1078
|
});
|
|
975
1079
|
},
|
|
976
1080
|
async cancel(runId) {
|
|
@@ -993,7 +1097,8 @@ function createDurablyInstance(state, jobs) {
|
|
|
993
1097
|
eventEmitter.emit({
|
|
994
1098
|
type: "run:cancel",
|
|
995
1099
|
runId,
|
|
996
|
-
jobName: run.jobName
|
|
1100
|
+
jobName: run.jobName,
|
|
1101
|
+
labels: run.labels
|
|
997
1102
|
});
|
|
998
1103
|
},
|
|
999
1104
|
async deleteRun(runId) {
|
|
@@ -1008,6 +1113,12 @@ function createDurablyInstance(state, jobs) {
|
|
|
1008
1113
|
throw new Error(`Cannot delete running run: ${runId}`);
|
|
1009
1114
|
}
|
|
1010
1115
|
await storage.deleteRun(runId);
|
|
1116
|
+
eventEmitter.emit({
|
|
1117
|
+
type: "run:delete",
|
|
1118
|
+
runId,
|
|
1119
|
+
jobName: run.jobName,
|
|
1120
|
+
labels: run.labels
|
|
1121
|
+
});
|
|
1011
1122
|
},
|
|
1012
1123
|
async migrate() {
|
|
1013
1124
|
if (state.migrated) {
|
|
@@ -1164,6 +1275,21 @@ function createSSEStreamFromSubscriptions(setup) {
|
|
|
1164
1275
|
}
|
|
1165
1276
|
|
|
1166
1277
|
// src/server.ts
|
|
1278
|
+
function parseLabelsFromParams(searchParams) {
|
|
1279
|
+
const labels = {};
|
|
1280
|
+
for (const [key, value] of searchParams.entries()) {
|
|
1281
|
+
if (key.startsWith("label.")) {
|
|
1282
|
+
labels[key.slice(6)] = value;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return Object.keys(labels).length > 0 ? labels : void 0;
|
|
1286
|
+
}
|
|
1287
|
+
function matchesLabels(eventLabels, filterLabels) {
|
|
1288
|
+
for (const [key, value] of Object.entries(filterLabels)) {
|
|
1289
|
+
if (eventLabels[key] !== value) return false;
|
|
1290
|
+
}
|
|
1291
|
+
return true;
|
|
1292
|
+
}
|
|
1167
1293
|
function createDurablyHandler(durably, options) {
|
|
1168
1294
|
const handler = {
|
|
1169
1295
|
async handle(request, basePath) {
|
|
@@ -1202,7 +1328,8 @@ function createDurablyHandler(durably, options) {
|
|
|
1202
1328
|
}
|
|
1203
1329
|
const run = await job.trigger(body.input ?? {}, {
|
|
1204
1330
|
idempotencyKey: body.idempotencyKey,
|
|
1205
|
-
concurrencyKey: body.concurrencyKey
|
|
1331
|
+
concurrencyKey: body.concurrencyKey,
|
|
1332
|
+
labels: body.labels
|
|
1206
1333
|
});
|
|
1207
1334
|
const response = { runId: run.id };
|
|
1208
1335
|
return jsonResponse(response);
|
|
@@ -1227,13 +1354,15 @@ function createDurablyHandler(durably, options) {
|
|
|
1227
1354
|
const status = url.searchParams.get("status");
|
|
1228
1355
|
const limit = url.searchParams.get("limit");
|
|
1229
1356
|
const offset = url.searchParams.get("offset");
|
|
1357
|
+
const labels = parseLabelsFromParams(url.searchParams);
|
|
1230
1358
|
const runs = await durably.getRuns({
|
|
1231
1359
|
jobName,
|
|
1232
1360
|
status,
|
|
1361
|
+
labels,
|
|
1233
1362
|
limit: limit ? Number.parseInt(limit, 10) : void 0,
|
|
1234
1363
|
offset: offset ? Number.parseInt(offset, 10) : void 0
|
|
1235
1364
|
});
|
|
1236
|
-
return jsonResponse(runs);
|
|
1365
|
+
return jsonResponse(runs.map(toClientRun));
|
|
1237
1366
|
} catch (error) {
|
|
1238
1367
|
return errorResponse(getErrorMessage(error), 500);
|
|
1239
1368
|
}
|
|
@@ -1247,7 +1376,7 @@ function createDurablyHandler(durably, options) {
|
|
|
1247
1376
|
if (!run) {
|
|
1248
1377
|
return errorResponse("Run not found", 404);
|
|
1249
1378
|
}
|
|
1250
|
-
return jsonResponse(run);
|
|
1379
|
+
return jsonResponse(toClientRun(run));
|
|
1251
1380
|
} catch (error) {
|
|
1252
1381
|
return errorResponse(getErrorMessage(error), 500);
|
|
1253
1382
|
}
|
|
@@ -1299,112 +1428,152 @@ function createDurablyHandler(durably, options) {
|
|
|
1299
1428
|
runsSubscribe(request) {
|
|
1300
1429
|
const url = new URL(request.url);
|
|
1301
1430
|
const jobNameFilter = url.searchParams.get("jobName");
|
|
1302
|
-
const
|
|
1431
|
+
const labelsFilter = parseLabelsFromParams(url.searchParams);
|
|
1432
|
+
const matchesFilter = (jobName, labels) => {
|
|
1433
|
+
if (jobNameFilter && jobName !== jobNameFilter) return false;
|
|
1434
|
+
if (labelsFilter && (!labels || !matchesLabels(labels, labelsFilter)))
|
|
1435
|
+
return false;
|
|
1436
|
+
return true;
|
|
1437
|
+
};
|
|
1303
1438
|
const sseStream = createSSEStreamFromSubscriptions(
|
|
1304
1439
|
(ctrl) => [
|
|
1305
1440
|
durably.on("run:trigger", (event) => {
|
|
1306
|
-
if (matchesFilter(event.jobName)) {
|
|
1441
|
+
if (matchesFilter(event.jobName, event.labels)) {
|
|
1307
1442
|
ctrl.enqueue({
|
|
1308
1443
|
type: "run:trigger",
|
|
1309
1444
|
runId: event.runId,
|
|
1310
|
-
jobName: event.jobName
|
|
1445
|
+
jobName: event.jobName,
|
|
1446
|
+
labels: event.labels
|
|
1311
1447
|
});
|
|
1312
1448
|
}
|
|
1313
1449
|
}),
|
|
1314
1450
|
durably.on("run:start", (event) => {
|
|
1315
|
-
if (matchesFilter(event.jobName)) {
|
|
1451
|
+
if (matchesFilter(event.jobName, event.labels)) {
|
|
1316
1452
|
ctrl.enqueue({
|
|
1317
1453
|
type: "run:start",
|
|
1318
1454
|
runId: event.runId,
|
|
1319
|
-
jobName: event.jobName
|
|
1455
|
+
jobName: event.jobName,
|
|
1456
|
+
labels: event.labels
|
|
1320
1457
|
});
|
|
1321
1458
|
}
|
|
1322
1459
|
}),
|
|
1323
1460
|
durably.on("run:complete", (event) => {
|
|
1324
|
-
if (matchesFilter(event.jobName)) {
|
|
1461
|
+
if (matchesFilter(event.jobName, event.labels)) {
|
|
1325
1462
|
ctrl.enqueue({
|
|
1326
1463
|
type: "run:complete",
|
|
1327
1464
|
runId: event.runId,
|
|
1328
|
-
jobName: event.jobName
|
|
1465
|
+
jobName: event.jobName,
|
|
1466
|
+
labels: event.labels
|
|
1329
1467
|
});
|
|
1330
1468
|
}
|
|
1331
1469
|
}),
|
|
1332
1470
|
durably.on("run:fail", (event) => {
|
|
1333
|
-
if (matchesFilter(event.jobName)) {
|
|
1471
|
+
if (matchesFilter(event.jobName, event.labels)) {
|
|
1334
1472
|
ctrl.enqueue({
|
|
1335
1473
|
type: "run:fail",
|
|
1336
1474
|
runId: event.runId,
|
|
1337
|
-
jobName: event.jobName
|
|
1475
|
+
jobName: event.jobName,
|
|
1476
|
+
labels: event.labels
|
|
1338
1477
|
});
|
|
1339
1478
|
}
|
|
1340
1479
|
}),
|
|
1341
1480
|
durably.on("run:cancel", (event) => {
|
|
1342
|
-
if (matchesFilter(event.jobName)) {
|
|
1481
|
+
if (matchesFilter(event.jobName, event.labels)) {
|
|
1343
1482
|
ctrl.enqueue({
|
|
1344
1483
|
type: "run:cancel",
|
|
1345
1484
|
runId: event.runId,
|
|
1346
|
-
jobName: event.jobName
|
|
1485
|
+
jobName: event.jobName,
|
|
1486
|
+
labels: event.labels
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
}),
|
|
1490
|
+
durably.on("run:delete", (event) => {
|
|
1491
|
+
if (matchesFilter(event.jobName, event.labels)) {
|
|
1492
|
+
ctrl.enqueue({
|
|
1493
|
+
type: "run:delete",
|
|
1494
|
+
runId: event.runId,
|
|
1495
|
+
jobName: event.jobName,
|
|
1496
|
+
labels: event.labels
|
|
1347
1497
|
});
|
|
1348
1498
|
}
|
|
1349
1499
|
}),
|
|
1350
1500
|
durably.on("run:retry", (event) => {
|
|
1351
|
-
if (matchesFilter(event.jobName)) {
|
|
1501
|
+
if (matchesFilter(event.jobName, event.labels)) {
|
|
1352
1502
|
ctrl.enqueue({
|
|
1353
1503
|
type: "run:retry",
|
|
1354
1504
|
runId: event.runId,
|
|
1355
|
-
jobName: event.jobName
|
|
1505
|
+
jobName: event.jobName,
|
|
1506
|
+
labels: event.labels
|
|
1356
1507
|
});
|
|
1357
1508
|
}
|
|
1358
1509
|
}),
|
|
1359
1510
|
durably.on("run:progress", (event) => {
|
|
1360
|
-
if (matchesFilter(event.jobName)) {
|
|
1511
|
+
if (matchesFilter(event.jobName, event.labels)) {
|
|
1361
1512
|
ctrl.enqueue({
|
|
1362
1513
|
type: "run:progress",
|
|
1363
1514
|
runId: event.runId,
|
|
1364
1515
|
jobName: event.jobName,
|
|
1365
|
-
progress: event.progress
|
|
1516
|
+
progress: event.progress,
|
|
1517
|
+
labels: event.labels
|
|
1366
1518
|
});
|
|
1367
1519
|
}
|
|
1368
1520
|
}),
|
|
1369
1521
|
durably.on("step:start", (event) => {
|
|
1370
|
-
if (matchesFilter(event.jobName)) {
|
|
1522
|
+
if (matchesFilter(event.jobName, event.labels)) {
|
|
1371
1523
|
ctrl.enqueue({
|
|
1372
1524
|
type: "step:start",
|
|
1373
1525
|
runId: event.runId,
|
|
1374
1526
|
jobName: event.jobName,
|
|
1375
1527
|
stepName: event.stepName,
|
|
1376
|
-
stepIndex: event.stepIndex
|
|
1528
|
+
stepIndex: event.stepIndex,
|
|
1529
|
+
labels: event.labels
|
|
1377
1530
|
});
|
|
1378
1531
|
}
|
|
1379
1532
|
}),
|
|
1380
1533
|
durably.on("step:complete", (event) => {
|
|
1381
|
-
if (matchesFilter(event.jobName)) {
|
|
1534
|
+
if (matchesFilter(event.jobName, event.labels)) {
|
|
1382
1535
|
ctrl.enqueue({
|
|
1383
1536
|
type: "step:complete",
|
|
1384
1537
|
runId: event.runId,
|
|
1385
1538
|
jobName: event.jobName,
|
|
1386
1539
|
stepName: event.stepName,
|
|
1387
|
-
stepIndex: event.stepIndex
|
|
1540
|
+
stepIndex: event.stepIndex,
|
|
1541
|
+
labels: event.labels
|
|
1388
1542
|
});
|
|
1389
1543
|
}
|
|
1390
1544
|
}),
|
|
1391
1545
|
durably.on("step:fail", (event) => {
|
|
1392
|
-
if (matchesFilter(event.jobName)) {
|
|
1546
|
+
if (matchesFilter(event.jobName, event.labels)) {
|
|
1393
1547
|
ctrl.enqueue({
|
|
1394
1548
|
type: "step:fail",
|
|
1395
1549
|
runId: event.runId,
|
|
1396
1550
|
jobName: event.jobName,
|
|
1397
1551
|
stepName: event.stepName,
|
|
1398
1552
|
stepIndex: event.stepIndex,
|
|
1399
|
-
error: event.error
|
|
1553
|
+
error: event.error,
|
|
1554
|
+
labels: event.labels
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
}),
|
|
1558
|
+
durably.on("step:cancel", (event) => {
|
|
1559
|
+
if (matchesFilter(event.jobName, event.labels)) {
|
|
1560
|
+
ctrl.enqueue({
|
|
1561
|
+
type: "step:cancel",
|
|
1562
|
+
runId: event.runId,
|
|
1563
|
+
jobName: event.jobName,
|
|
1564
|
+
stepName: event.stepName,
|
|
1565
|
+
stepIndex: event.stepIndex,
|
|
1566
|
+
labels: event.labels
|
|
1400
1567
|
});
|
|
1401
1568
|
}
|
|
1402
1569
|
}),
|
|
1403
1570
|
durably.on("log:write", (event) => {
|
|
1404
|
-
if (
|
|
1571
|
+
if (matchesFilter(event.jobName, event.labels)) {
|
|
1405
1572
|
ctrl.enqueue({
|
|
1406
1573
|
type: "log:write",
|
|
1407
1574
|
runId: event.runId,
|
|
1575
|
+
jobName: event.jobName,
|
|
1576
|
+
labels: event.labels,
|
|
1408
1577
|
stepName: event.stepName,
|
|
1409
1578
|
level: event.level,
|
|
1410
1579
|
message: event.message,
|
|
@@ -1424,6 +1593,7 @@ export {
|
|
|
1424
1593
|
createDurably,
|
|
1425
1594
|
createDurablyHandler,
|
|
1426
1595
|
defineJob,
|
|
1596
|
+
toClientRun,
|
|
1427
1597
|
withLogPersistence
|
|
1428
1598
|
};
|
|
1429
1599
|
//# sourceMappingURL=index.js.map
|