@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/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
- payload: validatedInput,
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
- payload: validatedInput
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
- payload: validatedInput,
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
- payload: v.payload,
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
- payload: validated[i].payload
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("payload", "text", (col) => col.notNull()).addColumn("status", "text", (col) => col.notNull()).addColumn("idempotency_key", "text").addColumn("concurrency_key", "text").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 migration.up(db);
262
- await db.insertInto("durably_schema_versions").values({
263
- version: migration.version,
264
- applied_at: (/* @__PURE__ */ new Date()).toISOString()
265
- }).execute();
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 { ulid } from "ulidx";
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
- payload: JSON.parse(row.payload),
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
- payload: JSON.stringify(input.payload),
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
- payload: JSON.stringify(input.payload),
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 getNextPendingRun(excludeConcurrencyKeys) {
434
- let query = db.selectFrom("durably_runs").leftJoin("durably_steps", "durably_runs.id", "durably_steps.run_id").selectAll("durably_runs").select(
435
- (eb) => eb.fn.count("durably_steps.id").as("step_count")
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
- query = query.where(
492
+ subquery = subquery.where(
439
493
  (eb) => eb.or([
440
- eb("durably_runs.concurrency_key", "is", null),
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 query.executeTakeFirst();
450
- return row ? rowToRun(row) : null;
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
- return {
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
- error: errorMessage
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?.status === "cancelled") {
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?.status === "cancelled") {
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
- payload: run.payload
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 step = createStepContext(run, run.jobName, storage, eventEmitter);
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.getNextPendingRun(excludeConcurrencyKeys);
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 matchesFilter = (jobName) => !jobNameFilter || jobName === jobNameFilter;
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 (!jobNameFilter) {
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