@coji/durably 0.4.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ import {
2
+ withLogPersistence
3
+ } from "./chunk-UCUP6NMJ.js";
4
+
1
5
  // src/durably.ts
2
6
  import { Kysely } from "kysely";
3
7
 
@@ -48,6 +52,15 @@ function createEventEmitter() {
48
52
  }
49
53
 
50
54
  // src/job.ts
55
+ import { prettifyError } from "zod";
56
+ function validateJobInputOrThrow(schema, input, context) {
57
+ const result = schema.safeParse(input);
58
+ if (!result.success) {
59
+ const prefix = context ? `${context}: ` : "";
60
+ throw new Error(`${prefix}Invalid input: ${prettifyError(result.error)}`);
61
+ }
62
+ return result.data;
63
+ }
51
64
  function createJobRegistry() {
52
65
  const jobs = /* @__PURE__ */ new Map();
53
66
  return {
@@ -62,7 +75,7 @@ function createJobRegistry() {
62
75
  }
63
76
  };
64
77
  }
65
- function createJobHandle(jobDef, storage, _eventEmitter, registry) {
78
+ function createJobHandle(jobDef, storage, eventEmitter, registry) {
66
79
  const existingJob = registry.get(jobDef.name);
67
80
  if (existingJob) {
68
81
  if (existingJob.jobDef === jobDef) {
@@ -77,16 +90,19 @@ function createJobHandle(jobDef, storage, _eventEmitter, registry) {
77
90
  const handle = {
78
91
  name: jobDef.name,
79
92
  async trigger(input, options) {
80
- const parseResult = inputSchema.safeParse(input);
81
- if (!parseResult.success) {
82
- throw new Error(`Invalid input: ${parseResult.error.message}`);
83
- }
93
+ const validatedInput = validateJobInputOrThrow(inputSchema, input);
84
94
  const run = await storage.createRun({
85
95
  jobName: jobDef.name,
86
- payload: parseResult.data,
96
+ payload: validatedInput,
87
97
  idempotencyKey: options?.idempotencyKey,
88
98
  concurrencyKey: options?.concurrencyKey
89
99
  });
100
+ eventEmitter.emit({
101
+ type: "run:trigger",
102
+ runId: run.id,
103
+ jobName: jobDef.name,
104
+ payload: validatedInput
105
+ });
90
106
  return run;
91
107
  },
92
108
  async triggerAndWait(input, options) {
@@ -103,19 +119,16 @@ function createJobHandle(jobDef, storage, _eventEmitter, registry) {
103
119
  clearTimeout(timeoutId);
104
120
  }
105
121
  };
106
- const unsubscribeComplete = _eventEmitter.on(
107
- "run:complete",
108
- (event) => {
109
- if (event.runId === run.id && !resolved) {
110
- cleanup();
111
- resolve({
112
- id: run.id,
113
- output: event.output
114
- });
115
- }
122
+ const unsubscribeComplete = eventEmitter.on("run:complete", (event) => {
123
+ if (event.runId === run.id && !resolved) {
124
+ cleanup();
125
+ resolve({
126
+ id: run.id,
127
+ output: event.output
128
+ });
116
129
  }
117
- );
118
- const unsubscribeFail = _eventEmitter.on("run:fail", (event) => {
130
+ });
131
+ const unsubscribeFail = eventEmitter.on("run:fail", (event) => {
119
132
  if (event.runId === run.id && !resolved) {
120
133
  cleanup();
121
134
  reject(new Error(event.error));
@@ -158,14 +171,13 @@ function createJobHandle(jobDef, storage, _eventEmitter, registry) {
158
171
  });
159
172
  const validated = [];
160
173
  for (let i = 0; i < normalized.length; i++) {
161
- const parseResult = inputSchema.safeParse(normalized[i].input);
162
- if (!parseResult.success) {
163
- throw new Error(
164
- `Invalid input at index ${i}: ${parseResult.error.message}`
165
- );
166
- }
174
+ const validatedInput = validateJobInputOrThrow(
175
+ inputSchema,
176
+ normalized[i].input,
177
+ `at index ${i}`
178
+ );
167
179
  validated.push({
168
- payload: parseResult.data,
180
+ payload: validatedInput,
169
181
  options: normalized[i].options
170
182
  });
171
183
  }
@@ -177,6 +189,14 @@ function createJobHandle(jobDef, storage, _eventEmitter, registry) {
177
189
  concurrencyKey: v.options?.concurrencyKey
178
190
  }))
179
191
  );
192
+ for (let i = 0; i < runs.length; i++) {
193
+ eventEmitter.emit({
194
+ type: "run:trigger",
195
+ runId: runs[i].id,
196
+ jobName: jobDef.name,
197
+ payload: validated[i].payload
198
+ });
199
+ }
180
200
  return runs;
181
201
  },
182
202
  async getRun(id) {
@@ -258,6 +278,7 @@ function rowToRun(row) {
258
278
  idempotencyKey: row.idempotency_key,
259
279
  concurrencyKey: row.concurrency_key,
260
280
  currentStepIndex: row.current_step_index,
281
+ stepCount: Number(row.step_count ?? 0),
261
282
  progress: row.progress ? JSON.parse(row.progress) : null,
262
283
  output: row.output ? JSON.parse(row.output) : null,
263
284
  error: row.error,
@@ -381,18 +402,22 @@ function createKyselyStorage(db) {
381
402
  await db.deleteFrom("durably_runs").where("id", "=", runId).execute();
382
403
  },
383
404
  async getRun(runId) {
384
- const row = await db.selectFrom("durably_runs").selectAll().where("id", "=", runId).executeTakeFirst();
405
+ const row = await db.selectFrom("durably_runs").leftJoin("durably_steps", "durably_runs.id", "durably_steps.run_id").selectAll("durably_runs").select(
406
+ (eb) => eb.fn.count("durably_steps.id").as("step_count")
407
+ ).where("durably_runs.id", "=", runId).groupBy("durably_runs.id").executeTakeFirst();
385
408
  return row ? rowToRun(row) : null;
386
409
  },
387
410
  async getRuns(filter) {
388
- let query = db.selectFrom("durably_runs").selectAll();
411
+ let query = db.selectFrom("durably_runs").leftJoin("durably_steps", "durably_runs.id", "durably_steps.run_id").selectAll("durably_runs").select(
412
+ (eb) => eb.fn.count("durably_steps.id").as("step_count")
413
+ ).groupBy("durably_runs.id");
389
414
  if (filter?.status) {
390
- query = query.where("status", "=", filter.status);
415
+ query = query.where("durably_runs.status", "=", filter.status);
391
416
  }
392
417
  if (filter?.jobName) {
393
- query = query.where("job_name", "=", filter.jobName);
418
+ query = query.where("durably_runs.job_name", "=", filter.jobName);
394
419
  }
395
- query = query.orderBy("created_at", "desc");
420
+ query = query.orderBy("durably_runs.created_at", "desc");
396
421
  if (filter?.limit !== void 0) {
397
422
  query = query.limit(filter.limit);
398
423
  }
@@ -406,12 +431,18 @@ function createKyselyStorage(db) {
406
431
  return rows.map(rowToRun);
407
432
  },
408
433
  async getNextPendingRun(excludeConcurrencyKeys) {
409
- let query = db.selectFrom("durably_runs").selectAll().where("status", "=", "pending").orderBy("created_at", "asc").limit(1);
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);
410
437
  if (excludeConcurrencyKeys.length > 0) {
411
438
  query = query.where(
412
439
  (eb) => eb.or([
413
- eb("concurrency_key", "is", null),
414
- eb("concurrency_key", "not in", excludeConcurrencyKeys)
440
+ eb("durably_runs.concurrency_key", "is", null),
441
+ eb(
442
+ "durably_runs.concurrency_key",
443
+ "not in",
444
+ excludeConcurrencyKeys
445
+ )
415
446
  ])
416
447
  );
417
448
  }
@@ -465,6 +496,9 @@ function createKyselyStorage(db) {
465
496
  };
466
497
  }
467
498
 
499
+ // src/worker.ts
500
+ import { prettifyError as prettifyError2 } from "zod";
501
+
468
502
  // src/errors.ts
469
503
  var CancelledError = class extends Error {
470
504
  constructor(runId) {
@@ -472,6 +506,9 @@ var CancelledError = class extends Error {
472
506
  this.name = "CancelledError";
473
507
  }
474
508
  };
509
+ function getErrorMessage(error) {
510
+ return error instanceof Error ? error.message : String(error);
511
+ }
475
512
 
476
513
  // src/context.ts
477
514
  function createStepContext(run, jobName, storage, eventEmitter) {
@@ -547,8 +584,13 @@ function createStepContext(run, jobName, storage, eventEmitter) {
547
584
  }
548
585
  },
549
586
  progress(current, total, message) {
550
- storage.updateRun(run.id, {
551
- progress: { current, total, message }
587
+ const progressData = { current, total, message };
588
+ storage.updateRun(run.id, { progress: progressData });
589
+ eventEmitter.emit({
590
+ type: "run:progress",
591
+ runId: run.id,
592
+ jobName,
593
+ progress: progressData
552
594
  });
553
595
  },
554
596
  log: {
@@ -614,9 +656,6 @@ function createWorker(config, storage, eventEmitter, jobRegistry) {
614
656
  });
615
657
  }
616
658
  }
617
- function getErrorMessage(error) {
618
- return error instanceof Error ? error.message : String(error);
619
- }
620
659
  async function handleRunSuccess(runId, jobName, output, startTime) {
621
660
  const currentRun = await storage.getRun(runId);
622
661
  if (currentRun?.status === "cancelled") {
@@ -663,7 +702,7 @@ function createWorker(config, storage, eventEmitter, jobRegistry) {
663
702
  updateHeartbeat().catch((error) => {
664
703
  eventEmitter.emit({
665
704
  type: "worker:error",
666
- error: error instanceof Error ? error.message : String(error),
705
+ error: getErrorMessage(error),
667
706
  context: "heartbeat",
668
707
  runId: run.id
669
708
  });
@@ -682,7 +721,7 @@ function createWorker(config, storage, eventEmitter, jobRegistry) {
682
721
  if (job.outputSchema) {
683
722
  const parseResult = job.outputSchema.safeParse(output);
684
723
  if (!parseResult.success) {
685
- throw new Error(`Invalid output: ${parseResult.error.message}`);
724
+ throw new Error(`Invalid output: ${prettifyError2(parseResult.error)}`);
686
725
  }
687
726
  }
688
727
  await handleRunSuccess(run.id, run.jobName, output, startTime);
@@ -780,35 +819,136 @@ var DEFAULTS = {
780
819
  heartbeatInterval: 5e3,
781
820
  staleThreshold: 3e4
782
821
  };
783
- function createDurably(options) {
784
- const config = {
785
- pollingInterval: options.pollingInterval ?? DEFAULTS.pollingInterval,
786
- heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
787
- staleThreshold: options.staleThreshold ?? DEFAULTS.staleThreshold
788
- };
789
- const db = new Kysely({ dialect: options.dialect });
790
- const storage = createKyselyStorage(db);
791
- const eventEmitter = createEventEmitter();
792
- const jobRegistry = createJobRegistry();
793
- const worker = createWorker(config, storage, eventEmitter, jobRegistry);
794
- let migrating = null;
795
- let migrated = false;
822
+ function createDurablyInstance(state, jobs) {
823
+ const { db, storage, eventEmitter, jobRegistry, worker } = state;
796
824
  const durably = {
797
825
  db,
798
826
  storage,
827
+ jobs,
799
828
  on: eventEmitter.on,
800
829
  emit: eventEmitter.emit,
801
830
  onError: eventEmitter.onError,
802
831
  start: worker.start,
803
832
  stop: worker.stop,
804
- register(jobDef) {
805
- return createJobHandle(jobDef, storage, eventEmitter, jobRegistry);
833
+ // biome-ignore lint/suspicious/noExplicitAny: flexible type constraint for job definitions
834
+ register(jobDefs) {
835
+ const newHandles = {};
836
+ for (const key of Object.keys(jobDefs)) {
837
+ const jobDef = jobDefs[key];
838
+ const handle = createJobHandle(
839
+ jobDef,
840
+ storage,
841
+ eventEmitter,
842
+ jobRegistry
843
+ );
844
+ newHandles[key] = handle;
845
+ }
846
+ const mergedJobs = { ...jobs, ...newHandles };
847
+ return createDurablyInstance(state, mergedJobs);
806
848
  },
807
849
  getRun: storage.getRun,
808
850
  getRuns: storage.getRuns,
809
851
  use(plugin) {
810
852
  plugin.install(durably);
811
853
  },
854
+ getJob(name) {
855
+ const registeredJob = jobRegistry.get(name);
856
+ if (!registeredJob) {
857
+ return void 0;
858
+ }
859
+ return registeredJob.handle;
860
+ },
861
+ subscribe(runId) {
862
+ let closed = false;
863
+ let cleanup = null;
864
+ return new ReadableStream({
865
+ start: (controller) => {
866
+ const unsubscribeStart = eventEmitter.on("run:start", (event) => {
867
+ if (!closed && event.runId === runId) {
868
+ controller.enqueue(event);
869
+ }
870
+ });
871
+ const unsubscribeComplete = eventEmitter.on(
872
+ "run:complete",
873
+ (event) => {
874
+ if (!closed && event.runId === runId) {
875
+ controller.enqueue(event);
876
+ closed = true;
877
+ cleanup?.();
878
+ controller.close();
879
+ }
880
+ }
881
+ );
882
+ const unsubscribeFail = eventEmitter.on("run:fail", (event) => {
883
+ if (!closed && event.runId === runId) {
884
+ controller.enqueue(event);
885
+ }
886
+ });
887
+ const unsubscribeCancel = eventEmitter.on("run:cancel", (event) => {
888
+ if (!closed && event.runId === runId) {
889
+ controller.enqueue(event);
890
+ }
891
+ });
892
+ const unsubscribeRetry = eventEmitter.on("run:retry", (event) => {
893
+ if (!closed && event.runId === runId) {
894
+ controller.enqueue(event);
895
+ }
896
+ });
897
+ const unsubscribeProgress = eventEmitter.on(
898
+ "run:progress",
899
+ (event) => {
900
+ if (!closed && event.runId === runId) {
901
+ controller.enqueue(event);
902
+ }
903
+ }
904
+ );
905
+ const unsubscribeStepStart = eventEmitter.on(
906
+ "step:start",
907
+ (event) => {
908
+ if (!closed && event.runId === runId) {
909
+ controller.enqueue(event);
910
+ }
911
+ }
912
+ );
913
+ const unsubscribeStepComplete = eventEmitter.on(
914
+ "step:complete",
915
+ (event) => {
916
+ if (!closed && event.runId === runId) {
917
+ controller.enqueue(event);
918
+ }
919
+ }
920
+ );
921
+ const unsubscribeStepFail = eventEmitter.on("step:fail", (event) => {
922
+ if (!closed && event.runId === runId) {
923
+ controller.enqueue(event);
924
+ }
925
+ });
926
+ const unsubscribeLog = eventEmitter.on("log:write", (event) => {
927
+ if (!closed && event.runId === runId) {
928
+ controller.enqueue(event);
929
+ }
930
+ });
931
+ cleanup = () => {
932
+ unsubscribeStart();
933
+ unsubscribeComplete();
934
+ unsubscribeFail();
935
+ unsubscribeCancel();
936
+ unsubscribeRetry();
937
+ unsubscribeProgress();
938
+ unsubscribeStepStart();
939
+ unsubscribeStepComplete();
940
+ unsubscribeStepFail();
941
+ unsubscribeLog();
942
+ };
943
+ },
944
+ cancel: () => {
945
+ if (!closed) {
946
+ closed = true;
947
+ cleanup?.();
948
+ }
949
+ }
950
+ });
951
+ },
812
952
  async retry(runId) {
813
953
  const run = await storage.getRun(runId);
814
954
  if (!run) {
@@ -827,6 +967,11 @@ function createDurably(options) {
827
967
  status: "pending",
828
968
  error: null
829
969
  });
970
+ eventEmitter.emit({
971
+ type: "run:retry",
972
+ runId,
973
+ jobName: run.jobName
974
+ });
830
975
  },
831
976
  async cancel(runId) {
832
977
  const run = await storage.getRun(runId);
@@ -845,6 +990,11 @@ function createDurably(options) {
845
990
  await storage.updateRun(runId, {
846
991
  status: "cancelled"
847
992
  });
993
+ eventEmitter.emit({
994
+ type: "run:cancel",
995
+ runId,
996
+ jobName: run.jobName
997
+ });
848
998
  },
849
999
  async deleteRun(runId) {
850
1000
  const run = await storage.getRun(runId);
@@ -860,22 +1010,48 @@ function createDurably(options) {
860
1010
  await storage.deleteRun(runId);
861
1011
  },
862
1012
  async migrate() {
863
- if (migrated) {
1013
+ if (state.migrated) {
864
1014
  return;
865
1015
  }
866
- if (migrating) {
867
- return migrating;
1016
+ if (state.migrating) {
1017
+ return state.migrating;
868
1018
  }
869
- migrating = runMigrations(db).then(() => {
870
- migrated = true;
1019
+ state.migrating = runMigrations(db).then(() => {
1020
+ state.migrated = true;
871
1021
  }).finally(() => {
872
- migrating = null;
1022
+ state.migrating = null;
873
1023
  });
874
- return migrating;
1024
+ return state.migrating;
1025
+ },
1026
+ async init() {
1027
+ await this.migrate();
1028
+ this.start();
875
1029
  }
876
1030
  };
877
1031
  return durably;
878
1032
  }
1033
+ function createDurably(options) {
1034
+ const config = {
1035
+ pollingInterval: options.pollingInterval ?? DEFAULTS.pollingInterval,
1036
+ heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
1037
+ staleThreshold: options.staleThreshold ?? DEFAULTS.staleThreshold
1038
+ };
1039
+ const db = new Kysely({ dialect: options.dialect });
1040
+ const storage = createKyselyStorage(db);
1041
+ const eventEmitter = createEventEmitter();
1042
+ const jobRegistry = createJobRegistry();
1043
+ const worker = createWorker(config, storage, eventEmitter, jobRegistry);
1044
+ const state = {
1045
+ db,
1046
+ storage,
1047
+ eventEmitter,
1048
+ jobRegistry,
1049
+ worker,
1050
+ migrating: null,
1051
+ migrated: false
1052
+ };
1053
+ return createDurablyInstance(state, {});
1054
+ }
879
1055
 
880
1056
  // src/define-job.ts
881
1057
  function defineJob(config) {
@@ -887,26 +1063,366 @@ function defineJob(config) {
887
1063
  };
888
1064
  }
889
1065
 
890
- // src/plugins/log-persistence.ts
891
- function withLogPersistence() {
892
- return {
893
- name: "log-persistence",
894
- install(durably) {
895
- durably.on("log:write", async (event) => {
896
- await durably.storage.createLog({
897
- runId: event.runId,
898
- stepName: event.stepName,
899
- level: event.level,
900
- message: event.message,
901
- data: event.data
1066
+ // src/http.ts
1067
+ var JSON_HEADERS = {
1068
+ "Content-Type": "application/json"
1069
+ };
1070
+ function jsonResponse(data, status = 200) {
1071
+ return new Response(JSON.stringify(data), {
1072
+ status,
1073
+ headers: JSON_HEADERS
1074
+ });
1075
+ }
1076
+ function errorResponse(message, status = 500) {
1077
+ return jsonResponse({ error: message }, status);
1078
+ }
1079
+ function successResponse() {
1080
+ return jsonResponse({ success: true });
1081
+ }
1082
+ function getRequiredQueryParam(url, paramName) {
1083
+ const value = url.searchParams.get(paramName);
1084
+ if (!value) {
1085
+ return errorResponse(`${paramName} query parameter is required`, 400);
1086
+ }
1087
+ return value;
1088
+ }
1089
+
1090
+ // src/sse.ts
1091
+ var SSE_HEADERS = {
1092
+ "Content-Type": "text/event-stream",
1093
+ "Cache-Control": "no-cache",
1094
+ Connection: "keep-alive"
1095
+ };
1096
+ function formatSSE(data) {
1097
+ return `data: ${JSON.stringify(data)}
1098
+
1099
+ `;
1100
+ }
1101
+ function createSSEEncoder() {
1102
+ return new TextEncoder();
1103
+ }
1104
+ function encodeSSE(encoder, data) {
1105
+ return encoder.encode(formatSSE(data));
1106
+ }
1107
+ function createSSEResponse(stream) {
1108
+ return new Response(stream, {
1109
+ status: 200,
1110
+ headers: SSE_HEADERS
1111
+ });
1112
+ }
1113
+ function createSSEStreamFromReader(reader) {
1114
+ const encoder = createSSEEncoder();
1115
+ return new ReadableStream({
1116
+ async start(controller) {
1117
+ try {
1118
+ while (true) {
1119
+ const { done, value } = await reader.read();
1120
+ if (done) {
1121
+ controller.close();
1122
+ break;
1123
+ }
1124
+ controller.enqueue(encodeSSE(encoder, value));
1125
+ }
1126
+ } catch (error) {
1127
+ controller.error(error);
1128
+ }
1129
+ },
1130
+ cancel() {
1131
+ reader.releaseLock();
1132
+ }
1133
+ });
1134
+ }
1135
+ function createSSEStreamFromSubscriptions(setup) {
1136
+ const encoder = createSSEEncoder();
1137
+ let closed = false;
1138
+ let unsubscribes = [];
1139
+ return new ReadableStream({
1140
+ start(controller) {
1141
+ const sseController = {
1142
+ enqueue: (data) => {
1143
+ if (closed) return;
1144
+ controller.enqueue(encodeSSE(encoder, data));
1145
+ },
1146
+ close: () => {
1147
+ if (closed) return;
1148
+ closed = true;
1149
+ controller.close();
1150
+ },
1151
+ get closed() {
1152
+ return closed;
1153
+ }
1154
+ };
1155
+ unsubscribes = setup(sseController);
1156
+ },
1157
+ cancel() {
1158
+ closed = true;
1159
+ for (const unsubscribe of unsubscribes) {
1160
+ unsubscribe();
1161
+ }
1162
+ }
1163
+ });
1164
+ }
1165
+
1166
+ // src/server.ts
1167
+ function createDurablyHandler(durably, options) {
1168
+ const handler = {
1169
+ async handle(request, basePath) {
1170
+ if (options?.onRequest) {
1171
+ await options.onRequest();
1172
+ }
1173
+ const url = new URL(request.url);
1174
+ const path = url.pathname.replace(basePath, "");
1175
+ const method = request.method;
1176
+ if (method === "GET") {
1177
+ if (path === "/subscribe") return handler.subscribe(request);
1178
+ if (path === "/runs") return handler.runs(request);
1179
+ if (path === "/run") return handler.run(request);
1180
+ if (path === "/steps") return handler.steps(request);
1181
+ if (path === "/runs/subscribe") return handler.runsSubscribe(request);
1182
+ }
1183
+ if (method === "POST") {
1184
+ if (path === "/trigger") return handler.trigger(request);
1185
+ if (path === "/retry") return handler.retry(request);
1186
+ if (path === "/cancel") return handler.cancel(request);
1187
+ }
1188
+ if (method === "DELETE") {
1189
+ if (path === "/run") return handler.delete(request);
1190
+ }
1191
+ return new Response("Not Found", { status: 404 });
1192
+ },
1193
+ async trigger(request) {
1194
+ try {
1195
+ const body = await request.json();
1196
+ if (!body.jobName) {
1197
+ return errorResponse("jobName is required", 400);
1198
+ }
1199
+ const job = durably.getJob(body.jobName);
1200
+ if (!job) {
1201
+ return errorResponse(`Job not found: ${body.jobName}`, 404);
1202
+ }
1203
+ const run = await job.trigger(body.input ?? {}, {
1204
+ idempotencyKey: body.idempotencyKey,
1205
+ concurrencyKey: body.concurrencyKey
902
1206
  });
903
- });
1207
+ const response = { runId: run.id };
1208
+ return jsonResponse(response);
1209
+ } catch (error) {
1210
+ return errorResponse(getErrorMessage(error), 500);
1211
+ }
1212
+ },
1213
+ subscribe(request) {
1214
+ const url = new URL(request.url);
1215
+ const runId = getRequiredQueryParam(url, "runId");
1216
+ if (runId instanceof Response) return runId;
1217
+ const stream = durably.subscribe(runId);
1218
+ const sseStream = createSSEStreamFromReader(
1219
+ stream.getReader()
1220
+ );
1221
+ return createSSEResponse(sseStream);
1222
+ },
1223
+ async runs(request) {
1224
+ try {
1225
+ const url = new URL(request.url);
1226
+ const jobName = url.searchParams.get("jobName") ?? void 0;
1227
+ const status = url.searchParams.get("status");
1228
+ const limit = url.searchParams.get("limit");
1229
+ const offset = url.searchParams.get("offset");
1230
+ const runs = await durably.getRuns({
1231
+ jobName,
1232
+ status,
1233
+ limit: limit ? Number.parseInt(limit, 10) : void 0,
1234
+ offset: offset ? Number.parseInt(offset, 10) : void 0
1235
+ });
1236
+ return jsonResponse(runs);
1237
+ } catch (error) {
1238
+ return errorResponse(getErrorMessage(error), 500);
1239
+ }
1240
+ },
1241
+ async run(request) {
1242
+ try {
1243
+ const url = new URL(request.url);
1244
+ const runId = getRequiredQueryParam(url, "runId");
1245
+ if (runId instanceof Response) return runId;
1246
+ const run = await durably.getRun(runId);
1247
+ if (!run) {
1248
+ return errorResponse("Run not found", 404);
1249
+ }
1250
+ return jsonResponse(run);
1251
+ } catch (error) {
1252
+ return errorResponse(getErrorMessage(error), 500);
1253
+ }
1254
+ },
1255
+ async retry(request) {
1256
+ try {
1257
+ const url = new URL(request.url);
1258
+ const runId = getRequiredQueryParam(url, "runId");
1259
+ if (runId instanceof Response) return runId;
1260
+ await durably.retry(runId);
1261
+ return successResponse();
1262
+ } catch (error) {
1263
+ return errorResponse(getErrorMessage(error), 500);
1264
+ }
1265
+ },
1266
+ async cancel(request) {
1267
+ try {
1268
+ const url = new URL(request.url);
1269
+ const runId = getRequiredQueryParam(url, "runId");
1270
+ if (runId instanceof Response) return runId;
1271
+ await durably.cancel(runId);
1272
+ return successResponse();
1273
+ } catch (error) {
1274
+ return errorResponse(getErrorMessage(error), 500);
1275
+ }
1276
+ },
1277
+ async delete(request) {
1278
+ try {
1279
+ const url = new URL(request.url);
1280
+ const runId = getRequiredQueryParam(url, "runId");
1281
+ if (runId instanceof Response) return runId;
1282
+ await durably.deleteRun(runId);
1283
+ return successResponse();
1284
+ } catch (error) {
1285
+ return errorResponse(getErrorMessage(error), 500);
1286
+ }
1287
+ },
1288
+ async steps(request) {
1289
+ try {
1290
+ const url = new URL(request.url);
1291
+ const runId = getRequiredQueryParam(url, "runId");
1292
+ if (runId instanceof Response) return runId;
1293
+ const steps = await durably.storage.getSteps(runId);
1294
+ return jsonResponse(steps);
1295
+ } catch (error) {
1296
+ return errorResponse(getErrorMessage(error), 500);
1297
+ }
1298
+ },
1299
+ runsSubscribe(request) {
1300
+ const url = new URL(request.url);
1301
+ const jobNameFilter = url.searchParams.get("jobName");
1302
+ const matchesFilter = (jobName) => !jobNameFilter || jobName === jobNameFilter;
1303
+ const sseStream = createSSEStreamFromSubscriptions(
1304
+ (ctrl) => [
1305
+ durably.on("run:trigger", (event) => {
1306
+ if (matchesFilter(event.jobName)) {
1307
+ ctrl.enqueue({
1308
+ type: "run:trigger",
1309
+ runId: event.runId,
1310
+ jobName: event.jobName
1311
+ });
1312
+ }
1313
+ }),
1314
+ durably.on("run:start", (event) => {
1315
+ if (matchesFilter(event.jobName)) {
1316
+ ctrl.enqueue({
1317
+ type: "run:start",
1318
+ runId: event.runId,
1319
+ jobName: event.jobName
1320
+ });
1321
+ }
1322
+ }),
1323
+ durably.on("run:complete", (event) => {
1324
+ if (matchesFilter(event.jobName)) {
1325
+ ctrl.enqueue({
1326
+ type: "run:complete",
1327
+ runId: event.runId,
1328
+ jobName: event.jobName
1329
+ });
1330
+ }
1331
+ }),
1332
+ durably.on("run:fail", (event) => {
1333
+ if (matchesFilter(event.jobName)) {
1334
+ ctrl.enqueue({
1335
+ type: "run:fail",
1336
+ runId: event.runId,
1337
+ jobName: event.jobName
1338
+ });
1339
+ }
1340
+ }),
1341
+ durably.on("run:cancel", (event) => {
1342
+ if (matchesFilter(event.jobName)) {
1343
+ ctrl.enqueue({
1344
+ type: "run:cancel",
1345
+ runId: event.runId,
1346
+ jobName: event.jobName
1347
+ });
1348
+ }
1349
+ }),
1350
+ durably.on("run:retry", (event) => {
1351
+ if (matchesFilter(event.jobName)) {
1352
+ ctrl.enqueue({
1353
+ type: "run:retry",
1354
+ runId: event.runId,
1355
+ jobName: event.jobName
1356
+ });
1357
+ }
1358
+ }),
1359
+ durably.on("run:progress", (event) => {
1360
+ if (matchesFilter(event.jobName)) {
1361
+ ctrl.enqueue({
1362
+ type: "run:progress",
1363
+ runId: event.runId,
1364
+ jobName: event.jobName,
1365
+ progress: event.progress
1366
+ });
1367
+ }
1368
+ }),
1369
+ durably.on("step:start", (event) => {
1370
+ if (matchesFilter(event.jobName)) {
1371
+ ctrl.enqueue({
1372
+ type: "step:start",
1373
+ runId: event.runId,
1374
+ jobName: event.jobName,
1375
+ stepName: event.stepName,
1376
+ stepIndex: event.stepIndex
1377
+ });
1378
+ }
1379
+ }),
1380
+ durably.on("step:complete", (event) => {
1381
+ if (matchesFilter(event.jobName)) {
1382
+ ctrl.enqueue({
1383
+ type: "step:complete",
1384
+ runId: event.runId,
1385
+ jobName: event.jobName,
1386
+ stepName: event.stepName,
1387
+ stepIndex: event.stepIndex
1388
+ });
1389
+ }
1390
+ }),
1391
+ durably.on("step:fail", (event) => {
1392
+ if (matchesFilter(event.jobName)) {
1393
+ ctrl.enqueue({
1394
+ type: "step:fail",
1395
+ runId: event.runId,
1396
+ jobName: event.jobName,
1397
+ stepName: event.stepName,
1398
+ stepIndex: event.stepIndex,
1399
+ error: event.error
1400
+ });
1401
+ }
1402
+ }),
1403
+ durably.on("log:write", (event) => {
1404
+ if (!jobNameFilter) {
1405
+ ctrl.enqueue({
1406
+ type: "log:write",
1407
+ runId: event.runId,
1408
+ stepName: event.stepName,
1409
+ level: event.level,
1410
+ message: event.message,
1411
+ data: event.data
1412
+ });
1413
+ }
1414
+ })
1415
+ ]
1416
+ );
1417
+ return createSSEResponse(sseStream);
904
1418
  }
905
1419
  };
1420
+ return handler;
906
1421
  }
907
1422
  export {
908
1423
  CancelledError,
909
1424
  createDurably,
1425
+ createDurablyHandler,
910
1426
  defineJob,
911
1427
  withLogPersistence
912
1428
  };