@coji/durably 0.3.0 → 0.6.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
@@ -51,10 +51,7 @@ function createEventEmitter() {
51
51
  function createJobRegistry() {
52
52
  const jobs = /* @__PURE__ */ new Map();
53
53
  return {
54
- register(job) {
55
- if (jobs.has(job.name)) {
56
- throw new Error(`Job "${job.name}" is already registered`);
57
- }
54
+ set(job) {
58
55
  jobs.set(job.name, job);
59
56
  },
60
57
  get(name) {
@@ -65,26 +62,37 @@ function createJobRegistry() {
65
62
  }
66
63
  };
67
64
  }
68
- function createJobHandle(definition, fn, storage, _eventEmitter, registry) {
69
- registry.register({
70
- name: definition.name,
71
- inputSchema: definition.input,
72
- outputSchema: definition.output,
73
- fn
74
- });
75
- return {
76
- name: definition.name,
65
+ function createJobHandle(jobDef, storage, eventEmitter, registry) {
66
+ const existingJob = registry.get(jobDef.name);
67
+ if (existingJob) {
68
+ if (existingJob.jobDef === jobDef) {
69
+ return existingJob.handle;
70
+ }
71
+ throw new Error(
72
+ `Job "${jobDef.name}" is already registered with a different definition`
73
+ );
74
+ }
75
+ const inputSchema = jobDef.input;
76
+ const outputSchema = jobDef.output;
77
+ const handle = {
78
+ name: jobDef.name,
77
79
  async trigger(input, options) {
78
- const parseResult = definition.input.safeParse(input);
80
+ const parseResult = inputSchema.safeParse(input);
79
81
  if (!parseResult.success) {
80
82
  throw new Error(`Invalid input: ${parseResult.error.message}`);
81
83
  }
82
84
  const run = await storage.createRun({
83
- jobName: definition.name,
85
+ jobName: jobDef.name,
84
86
  payload: parseResult.data,
85
87
  idempotencyKey: options?.idempotencyKey,
86
88
  concurrencyKey: options?.concurrencyKey
87
89
  });
90
+ eventEmitter.emit({
91
+ type: "run:trigger",
92
+ runId: run.id,
93
+ jobName: jobDef.name,
94
+ payload: parseResult.data
95
+ });
88
96
  return run;
89
97
  },
90
98
  async triggerAndWait(input, options) {
@@ -101,19 +109,16 @@ function createJobHandle(definition, fn, storage, _eventEmitter, registry) {
101
109
  clearTimeout(timeoutId);
102
110
  }
103
111
  };
104
- const unsubscribeComplete = _eventEmitter.on(
105
- "run:complete",
106
- (event) => {
107
- if (event.runId === run.id && !resolved) {
108
- cleanup();
109
- resolve({
110
- id: run.id,
111
- output: event.output
112
- });
113
- }
112
+ const unsubscribeComplete = eventEmitter.on("run:complete", (event) => {
113
+ if (event.runId === run.id && !resolved) {
114
+ cleanup();
115
+ resolve({
116
+ id: run.id,
117
+ output: event.output
118
+ });
114
119
  }
115
- );
116
- const unsubscribeFail = _eventEmitter.on("run:fail", (event) => {
120
+ });
121
+ const unsubscribeFail = eventEmitter.on("run:fail", (event) => {
117
122
  if (event.runId === run.id && !resolved) {
118
123
  cleanup();
119
124
  reject(new Error(event.error));
@@ -156,7 +161,7 @@ function createJobHandle(definition, fn, storage, _eventEmitter, registry) {
156
161
  });
157
162
  const validated = [];
158
163
  for (let i = 0; i < normalized.length; i++) {
159
- const parseResult = definition.input.safeParse(normalized[i].input);
164
+ const parseResult = inputSchema.safeParse(normalized[i].input);
160
165
  if (!parseResult.success) {
161
166
  throw new Error(
162
167
  `Invalid input at index ${i}: ${parseResult.error.message}`
@@ -169,17 +174,25 @@ function createJobHandle(definition, fn, storage, _eventEmitter, registry) {
169
174
  }
170
175
  const runs = await storage.batchCreateRuns(
171
176
  validated.map((v) => ({
172
- jobName: definition.name,
177
+ jobName: jobDef.name,
173
178
  payload: v.payload,
174
179
  idempotencyKey: v.options?.idempotencyKey,
175
180
  concurrencyKey: v.options?.concurrencyKey
176
181
  }))
177
182
  );
183
+ for (let i = 0; i < runs.length; i++) {
184
+ eventEmitter.emit({
185
+ type: "run:trigger",
186
+ runId: runs[i].id,
187
+ jobName: jobDef.name,
188
+ payload: validated[i].payload
189
+ });
190
+ }
178
191
  return runs;
179
192
  },
180
193
  async getRun(id) {
181
194
  const run = await storage.getRun(id);
182
- if (!run || run.jobName !== definition.name) {
195
+ if (!run || run.jobName !== jobDef.name) {
183
196
  return null;
184
197
  }
185
198
  return run;
@@ -187,11 +200,20 @@ function createJobHandle(definition, fn, storage, _eventEmitter, registry) {
187
200
  async getRuns(filter) {
188
201
  const runs = await storage.getRuns({
189
202
  ...filter,
190
- jobName: definition.name
203
+ jobName: jobDef.name
191
204
  });
192
205
  return runs;
193
206
  }
194
207
  };
208
+ registry.set({
209
+ name: jobDef.name,
210
+ inputSchema,
211
+ outputSchema,
212
+ fn: jobDef.run,
213
+ jobDef,
214
+ handle
215
+ });
216
+ return handle;
195
217
  }
196
218
 
197
219
  // src/migrations.ts
@@ -247,6 +269,7 @@ function rowToRun(row) {
247
269
  idempotencyKey: row.idempotency_key,
248
270
  concurrencyKey: row.concurrency_key,
249
271
  currentStepIndex: row.current_step_index,
272
+ stepCount: Number(row.step_count ?? 0),
250
273
  progress: row.progress ? JSON.parse(row.progress) : null,
251
274
  output: row.output ? JSON.parse(row.output) : null,
252
275
  error: row.error,
@@ -370,18 +393,22 @@ function createKyselyStorage(db) {
370
393
  await db.deleteFrom("durably_runs").where("id", "=", runId).execute();
371
394
  },
372
395
  async getRun(runId) {
373
- const row = await db.selectFrom("durably_runs").selectAll().where("id", "=", runId).executeTakeFirst();
396
+ const row = await db.selectFrom("durably_runs").leftJoin("durably_steps", "durably_runs.id", "durably_steps.run_id").selectAll("durably_runs").select(
397
+ (eb) => eb.fn.count("durably_steps.id").as("step_count")
398
+ ).where("durably_runs.id", "=", runId).groupBy("durably_runs.id").executeTakeFirst();
374
399
  return row ? rowToRun(row) : null;
375
400
  },
376
401
  async getRuns(filter) {
377
- let query = db.selectFrom("durably_runs").selectAll();
402
+ let query = db.selectFrom("durably_runs").leftJoin("durably_steps", "durably_runs.id", "durably_steps.run_id").selectAll("durably_runs").select(
403
+ (eb) => eb.fn.count("durably_steps.id").as("step_count")
404
+ ).groupBy("durably_runs.id");
378
405
  if (filter?.status) {
379
- query = query.where("status", "=", filter.status);
406
+ query = query.where("durably_runs.status", "=", filter.status);
380
407
  }
381
408
  if (filter?.jobName) {
382
- query = query.where("job_name", "=", filter.jobName);
409
+ query = query.where("durably_runs.job_name", "=", filter.jobName);
383
410
  }
384
- query = query.orderBy("created_at", "desc");
411
+ query = query.orderBy("durably_runs.created_at", "desc");
385
412
  if (filter?.limit !== void 0) {
386
413
  query = query.limit(filter.limit);
387
414
  }
@@ -395,12 +422,18 @@ function createKyselyStorage(db) {
395
422
  return rows.map(rowToRun);
396
423
  },
397
424
  async getNextPendingRun(excludeConcurrencyKeys) {
398
- let query = db.selectFrom("durably_runs").selectAll().where("status", "=", "pending").orderBy("created_at", "asc").limit(1);
425
+ let query = db.selectFrom("durably_runs").leftJoin("durably_steps", "durably_runs.id", "durably_steps.run_id").selectAll("durably_runs").select(
426
+ (eb) => eb.fn.count("durably_steps.id").as("step_count")
427
+ ).where("durably_runs.status", "=", "pending").groupBy("durably_runs.id").orderBy("durably_runs.created_at", "asc").limit(1);
399
428
  if (excludeConcurrencyKeys.length > 0) {
400
429
  query = query.where(
401
430
  (eb) => eb.or([
402
- eb("concurrency_key", "is", null),
403
- eb("concurrency_key", "not in", excludeConcurrencyKeys)
431
+ eb("durably_runs.concurrency_key", "is", null),
432
+ eb(
433
+ "durably_runs.concurrency_key",
434
+ "not in",
435
+ excludeConcurrencyKeys
436
+ )
404
437
  ])
405
438
  );
406
439
  }
@@ -536,8 +569,13 @@ function createStepContext(run, jobName, storage, eventEmitter) {
536
569
  }
537
570
  },
538
571
  progress(current, total, message) {
539
- storage.updateRun(run.id, {
540
- progress: { current, total, message }
572
+ const progressData = { current, total, message };
573
+ storage.updateRun(run.id, { progress: progressData });
574
+ eventEmitter.emit({
575
+ type: "run:progress",
576
+ runId: run.id,
577
+ jobName,
578
+ progress: progressData
541
579
  });
542
580
  },
543
581
  log: {
@@ -769,35 +807,136 @@ var DEFAULTS = {
769
807
  heartbeatInterval: 5e3,
770
808
  staleThreshold: 3e4
771
809
  };
772
- function createDurably(options) {
773
- const config = {
774
- pollingInterval: options.pollingInterval ?? DEFAULTS.pollingInterval,
775
- heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
776
- staleThreshold: options.staleThreshold ?? DEFAULTS.staleThreshold
777
- };
778
- const db = new Kysely({ dialect: options.dialect });
779
- const storage = createKyselyStorage(db);
780
- const eventEmitter = createEventEmitter();
781
- const jobRegistry = createJobRegistry();
782
- const worker = createWorker(config, storage, eventEmitter, jobRegistry);
783
- let migrating = null;
784
- let migrated = false;
810
+ function createDurablyInstance(state, jobs) {
811
+ const { db, storage, eventEmitter, jobRegistry, worker } = state;
785
812
  const durably = {
786
813
  db,
787
814
  storage,
815
+ jobs,
788
816
  on: eventEmitter.on,
789
817
  emit: eventEmitter.emit,
790
818
  onError: eventEmitter.onError,
791
819
  start: worker.start,
792
820
  stop: worker.stop,
793
- defineJob(definition, fn) {
794
- return createJobHandle(definition, fn, storage, eventEmitter, jobRegistry);
821
+ // biome-ignore lint/suspicious/noExplicitAny: flexible type constraint for job definitions
822
+ register(jobDefs) {
823
+ const newHandles = {};
824
+ for (const key of Object.keys(jobDefs)) {
825
+ const jobDef = jobDefs[key];
826
+ const handle = createJobHandle(
827
+ jobDef,
828
+ storage,
829
+ eventEmitter,
830
+ jobRegistry
831
+ );
832
+ newHandles[key] = handle;
833
+ }
834
+ const mergedJobs = { ...jobs, ...newHandles };
835
+ return createDurablyInstance(state, mergedJobs);
795
836
  },
796
837
  getRun: storage.getRun,
797
838
  getRuns: storage.getRuns,
798
839
  use(plugin) {
799
840
  plugin.install(durably);
800
841
  },
842
+ getJob(name) {
843
+ const registeredJob = jobRegistry.get(name);
844
+ if (!registeredJob) {
845
+ return void 0;
846
+ }
847
+ return registeredJob.handle;
848
+ },
849
+ subscribe(runId) {
850
+ let closed = false;
851
+ let cleanup = null;
852
+ return new ReadableStream({
853
+ start: (controller) => {
854
+ const unsubscribeStart = eventEmitter.on("run:start", (event) => {
855
+ if (!closed && event.runId === runId) {
856
+ controller.enqueue(event);
857
+ }
858
+ });
859
+ const unsubscribeComplete = eventEmitter.on(
860
+ "run:complete",
861
+ (event) => {
862
+ if (!closed && event.runId === runId) {
863
+ controller.enqueue(event);
864
+ closed = true;
865
+ cleanup?.();
866
+ controller.close();
867
+ }
868
+ }
869
+ );
870
+ const unsubscribeFail = eventEmitter.on("run:fail", (event) => {
871
+ if (!closed && event.runId === runId) {
872
+ controller.enqueue(event);
873
+ }
874
+ });
875
+ const unsubscribeCancel = eventEmitter.on("run:cancel", (event) => {
876
+ if (!closed && event.runId === runId) {
877
+ controller.enqueue(event);
878
+ }
879
+ });
880
+ const unsubscribeRetry = eventEmitter.on("run:retry", (event) => {
881
+ if (!closed && event.runId === runId) {
882
+ controller.enqueue(event);
883
+ }
884
+ });
885
+ const unsubscribeProgress = eventEmitter.on(
886
+ "run:progress",
887
+ (event) => {
888
+ if (!closed && event.runId === runId) {
889
+ controller.enqueue(event);
890
+ }
891
+ }
892
+ );
893
+ const unsubscribeStepStart = eventEmitter.on(
894
+ "step:start",
895
+ (event) => {
896
+ if (!closed && event.runId === runId) {
897
+ controller.enqueue(event);
898
+ }
899
+ }
900
+ );
901
+ const unsubscribeStepComplete = eventEmitter.on(
902
+ "step:complete",
903
+ (event) => {
904
+ if (!closed && event.runId === runId) {
905
+ controller.enqueue(event);
906
+ }
907
+ }
908
+ );
909
+ const unsubscribeStepFail = eventEmitter.on("step:fail", (event) => {
910
+ if (!closed && event.runId === runId) {
911
+ controller.enqueue(event);
912
+ }
913
+ });
914
+ const unsubscribeLog = eventEmitter.on("log:write", (event) => {
915
+ if (!closed && event.runId === runId) {
916
+ controller.enqueue(event);
917
+ }
918
+ });
919
+ cleanup = () => {
920
+ unsubscribeStart();
921
+ unsubscribeComplete();
922
+ unsubscribeFail();
923
+ unsubscribeCancel();
924
+ unsubscribeRetry();
925
+ unsubscribeProgress();
926
+ unsubscribeStepStart();
927
+ unsubscribeStepComplete();
928
+ unsubscribeStepFail();
929
+ unsubscribeLog();
930
+ };
931
+ },
932
+ cancel: () => {
933
+ if (!closed) {
934
+ closed = true;
935
+ cleanup?.();
936
+ }
937
+ }
938
+ });
939
+ },
801
940
  async retry(runId) {
802
941
  const run = await storage.getRun(runId);
803
942
  if (!run) {
@@ -816,6 +955,11 @@ function createDurably(options) {
816
955
  status: "pending",
817
956
  error: null
818
957
  });
958
+ eventEmitter.emit({
959
+ type: "run:retry",
960
+ runId,
961
+ jobName: run.jobName
962
+ });
819
963
  },
820
964
  async cancel(runId) {
821
965
  const run = await storage.getRun(runId);
@@ -834,6 +978,11 @@ function createDurably(options) {
834
978
  await storage.updateRun(runId, {
835
979
  status: "cancelled"
836
980
  });
981
+ eventEmitter.emit({
982
+ type: "run:cancel",
983
+ runId,
984
+ jobName: run.jobName
985
+ });
837
986
  },
838
987
  async deleteRun(runId) {
839
988
  const run = await storage.getRun(runId);
@@ -849,22 +998,58 @@ function createDurably(options) {
849
998
  await storage.deleteRun(runId);
850
999
  },
851
1000
  async migrate() {
852
- if (migrated) {
1001
+ if (state.migrated) {
853
1002
  return;
854
1003
  }
855
- if (migrating) {
856
- return migrating;
1004
+ if (state.migrating) {
1005
+ return state.migrating;
857
1006
  }
858
- migrating = runMigrations(db).then(() => {
859
- migrated = true;
1007
+ state.migrating = runMigrations(db).then(() => {
1008
+ state.migrated = true;
860
1009
  }).finally(() => {
861
- migrating = null;
1010
+ state.migrating = null;
862
1011
  });
863
- return migrating;
1012
+ return state.migrating;
1013
+ },
1014
+ async init() {
1015
+ await this.migrate();
1016
+ this.start();
864
1017
  }
865
1018
  };
866
1019
  return durably;
867
1020
  }
1021
+ function createDurably(options) {
1022
+ const config = {
1023
+ pollingInterval: options.pollingInterval ?? DEFAULTS.pollingInterval,
1024
+ heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
1025
+ staleThreshold: options.staleThreshold ?? DEFAULTS.staleThreshold
1026
+ };
1027
+ const db = new Kysely({ dialect: options.dialect });
1028
+ const storage = createKyselyStorage(db);
1029
+ const eventEmitter = createEventEmitter();
1030
+ const jobRegistry = createJobRegistry();
1031
+ const worker = createWorker(config, storage, eventEmitter, jobRegistry);
1032
+ const state = {
1033
+ db,
1034
+ storage,
1035
+ eventEmitter,
1036
+ jobRegistry,
1037
+ worker,
1038
+ migrating: null,
1039
+ migrated: false
1040
+ };
1041
+ return createDurablyInstance(state, {});
1042
+ }
1043
+
1044
+ // src/define-job.ts
1045
+ function defineJob(config) {
1046
+ return {
1047
+ name: config.name,
1048
+ input: config.input,
1049
+ output: config.output,
1050
+ run: config.run
1051
+ };
1052
+ }
868
1053
 
869
1054
  // src/plugins/log-persistence.ts
870
1055
  function withLogPersistence() {
@@ -883,9 +1068,444 @@ function withLogPersistence() {
883
1068
  }
884
1069
  };
885
1070
  }
1071
+
1072
+ // src/server.ts
1073
+ function createDurablyHandler(durably, options) {
1074
+ const handler = {
1075
+ async handle(request, basePath) {
1076
+ if (options?.onRequest) {
1077
+ await options.onRequest();
1078
+ }
1079
+ const url = new URL(request.url);
1080
+ const path = url.pathname.replace(basePath, "");
1081
+ const method = request.method;
1082
+ if (method === "GET") {
1083
+ if (path === "/subscribe") return handler.subscribe(request);
1084
+ if (path === "/runs") return handler.runs(request);
1085
+ if (path === "/run") return handler.run(request);
1086
+ if (path === "/steps") return handler.steps(request);
1087
+ if (path === "/runs/subscribe") return handler.runsSubscribe(request);
1088
+ }
1089
+ if (method === "POST") {
1090
+ if (path === "/trigger") return handler.trigger(request);
1091
+ if (path === "/retry") return handler.retry(request);
1092
+ if (path === "/cancel") return handler.cancel(request);
1093
+ }
1094
+ if (method === "DELETE") {
1095
+ if (path === "/run") return handler.delete(request);
1096
+ }
1097
+ return new Response("Not Found", { status: 404 });
1098
+ },
1099
+ async trigger(request) {
1100
+ try {
1101
+ const body = await request.json();
1102
+ if (!body.jobName) {
1103
+ return new Response(
1104
+ JSON.stringify({ error: "jobName is required" }),
1105
+ { status: 400, headers: { "Content-Type": "application/json" } }
1106
+ );
1107
+ }
1108
+ const job = durably.getJob(body.jobName);
1109
+ if (!job) {
1110
+ return new Response(
1111
+ JSON.stringify({ error: `Job not found: ${body.jobName}` }),
1112
+ { status: 404, headers: { "Content-Type": "application/json" } }
1113
+ );
1114
+ }
1115
+ const run = await job.trigger(body.input ?? {}, {
1116
+ idempotencyKey: body.idempotencyKey,
1117
+ concurrencyKey: body.concurrencyKey
1118
+ });
1119
+ const response = { runId: run.id };
1120
+ return new Response(JSON.stringify(response), {
1121
+ status: 200,
1122
+ headers: { "Content-Type": "application/json" }
1123
+ });
1124
+ } catch (error) {
1125
+ const message = error instanceof Error ? error.message : "Unknown error";
1126
+ return new Response(JSON.stringify({ error: message }), {
1127
+ status: 500,
1128
+ headers: { "Content-Type": "application/json" }
1129
+ });
1130
+ }
1131
+ },
1132
+ subscribe(request) {
1133
+ const url = new URL(request.url);
1134
+ const runId = url.searchParams.get("runId");
1135
+ if (!runId) {
1136
+ return new Response(
1137
+ JSON.stringify({ error: "runId query parameter is required" }),
1138
+ { status: 400, headers: { "Content-Type": "application/json" } }
1139
+ );
1140
+ }
1141
+ const stream = durably.subscribe(runId);
1142
+ const encoder = new TextEncoder();
1143
+ const sseStream = new ReadableStream({
1144
+ async start(controller) {
1145
+ const reader = stream.getReader();
1146
+ try {
1147
+ while (true) {
1148
+ const { done, value } = await reader.read();
1149
+ if (done) {
1150
+ controller.close();
1151
+ break;
1152
+ }
1153
+ const event = value;
1154
+ const data = `data: ${JSON.stringify(event)}
1155
+
1156
+ `;
1157
+ controller.enqueue(encoder.encode(data));
1158
+ }
1159
+ } catch (error) {
1160
+ controller.error(error);
1161
+ }
1162
+ }
1163
+ });
1164
+ return new Response(sseStream, {
1165
+ status: 200,
1166
+ headers: {
1167
+ "Content-Type": "text/event-stream",
1168
+ "Cache-Control": "no-cache",
1169
+ Connection: "keep-alive"
1170
+ }
1171
+ });
1172
+ },
1173
+ async runs(request) {
1174
+ try {
1175
+ const url = new URL(request.url);
1176
+ const jobName = url.searchParams.get("jobName") ?? void 0;
1177
+ const status = url.searchParams.get("status");
1178
+ const limit = url.searchParams.get("limit");
1179
+ const offset = url.searchParams.get("offset");
1180
+ const runs = await durably.getRuns({
1181
+ jobName,
1182
+ status,
1183
+ limit: limit ? Number.parseInt(limit, 10) : void 0,
1184
+ offset: offset ? Number.parseInt(offset, 10) : void 0
1185
+ });
1186
+ return new Response(JSON.stringify(runs), {
1187
+ status: 200,
1188
+ headers: { "Content-Type": "application/json" }
1189
+ });
1190
+ } catch (error) {
1191
+ const message = error instanceof Error ? error.message : "Unknown error";
1192
+ return new Response(JSON.stringify({ error: message }), {
1193
+ status: 500,
1194
+ headers: { "Content-Type": "application/json" }
1195
+ });
1196
+ }
1197
+ },
1198
+ async run(request) {
1199
+ try {
1200
+ const url = new URL(request.url);
1201
+ const runId = url.searchParams.get("runId");
1202
+ if (!runId) {
1203
+ return new Response(
1204
+ JSON.stringify({ error: "runId query parameter is required" }),
1205
+ { status: 400, headers: { "Content-Type": "application/json" } }
1206
+ );
1207
+ }
1208
+ const run = await durably.getRun(runId);
1209
+ if (!run) {
1210
+ return new Response(JSON.stringify({ error: "Run not found" }), {
1211
+ status: 404,
1212
+ headers: { "Content-Type": "application/json" }
1213
+ });
1214
+ }
1215
+ return new Response(JSON.stringify(run), {
1216
+ status: 200,
1217
+ headers: { "Content-Type": "application/json" }
1218
+ });
1219
+ } catch (error) {
1220
+ const message = error instanceof Error ? error.message : "Unknown error";
1221
+ return new Response(JSON.stringify({ error: message }), {
1222
+ status: 500,
1223
+ headers: { "Content-Type": "application/json" }
1224
+ });
1225
+ }
1226
+ },
1227
+ async retry(request) {
1228
+ try {
1229
+ const url = new URL(request.url);
1230
+ const runId = url.searchParams.get("runId");
1231
+ if (!runId) {
1232
+ return new Response(
1233
+ JSON.stringify({ error: "runId query parameter is required" }),
1234
+ { status: 400, headers: { "Content-Type": "application/json" } }
1235
+ );
1236
+ }
1237
+ await durably.retry(runId);
1238
+ return new Response(JSON.stringify({ success: true }), {
1239
+ status: 200,
1240
+ headers: { "Content-Type": "application/json" }
1241
+ });
1242
+ } catch (error) {
1243
+ const message = error instanceof Error ? error.message : "Unknown error";
1244
+ return new Response(JSON.stringify({ error: message }), {
1245
+ status: 500,
1246
+ headers: { "Content-Type": "application/json" }
1247
+ });
1248
+ }
1249
+ },
1250
+ async cancel(request) {
1251
+ try {
1252
+ const url = new URL(request.url);
1253
+ const runId = url.searchParams.get("runId");
1254
+ if (!runId) {
1255
+ return new Response(
1256
+ JSON.stringify({ error: "runId query parameter is required" }),
1257
+ { status: 400, headers: { "Content-Type": "application/json" } }
1258
+ );
1259
+ }
1260
+ await durably.cancel(runId);
1261
+ return new Response(JSON.stringify({ success: true }), {
1262
+ status: 200,
1263
+ headers: { "Content-Type": "application/json" }
1264
+ });
1265
+ } catch (error) {
1266
+ const message = error instanceof Error ? error.message : "Unknown error";
1267
+ return new Response(JSON.stringify({ error: message }), {
1268
+ status: 500,
1269
+ headers: { "Content-Type": "application/json" }
1270
+ });
1271
+ }
1272
+ },
1273
+ async delete(request) {
1274
+ try {
1275
+ const url = new URL(request.url);
1276
+ const runId = url.searchParams.get("runId");
1277
+ if (!runId) {
1278
+ return new Response(
1279
+ JSON.stringify({ error: "runId query parameter is required" }),
1280
+ { status: 400, headers: { "Content-Type": "application/json" } }
1281
+ );
1282
+ }
1283
+ await durably.deleteRun(runId);
1284
+ return new Response(JSON.stringify({ success: true }), {
1285
+ status: 200,
1286
+ headers: { "Content-Type": "application/json" }
1287
+ });
1288
+ } catch (error) {
1289
+ const message = error instanceof Error ? error.message : "Unknown error";
1290
+ return new Response(JSON.stringify({ error: message }), {
1291
+ status: 500,
1292
+ headers: { "Content-Type": "application/json" }
1293
+ });
1294
+ }
1295
+ },
1296
+ async steps(request) {
1297
+ try {
1298
+ const url = new URL(request.url);
1299
+ const runId = url.searchParams.get("runId");
1300
+ if (!runId) {
1301
+ return new Response(
1302
+ JSON.stringify({ error: "runId query parameter is required" }),
1303
+ { status: 400, headers: { "Content-Type": "application/json" } }
1304
+ );
1305
+ }
1306
+ const steps = await durably.storage.getSteps(runId);
1307
+ return new Response(JSON.stringify(steps), {
1308
+ status: 200,
1309
+ headers: { "Content-Type": "application/json" }
1310
+ });
1311
+ } catch (error) {
1312
+ const message = error instanceof Error ? error.message : "Unknown error";
1313
+ return new Response(JSON.stringify({ error: message }), {
1314
+ status: 500,
1315
+ headers: { "Content-Type": "application/json" }
1316
+ });
1317
+ }
1318
+ },
1319
+ runsSubscribe(request) {
1320
+ const url = new URL(request.url);
1321
+ const jobNameFilter = url.searchParams.get("jobName");
1322
+ const encoder = new TextEncoder();
1323
+ let closed = false;
1324
+ const sseStream = new ReadableStream({
1325
+ start(controller) {
1326
+ const unsubscribeTrigger = durably.on("run:trigger", (event) => {
1327
+ if (closed) return;
1328
+ if (jobNameFilter && event.jobName !== jobNameFilter) return;
1329
+ const data = `data: ${JSON.stringify({
1330
+ type: "run:trigger",
1331
+ runId: event.runId,
1332
+ jobName: event.jobName
1333
+ })}
1334
+
1335
+ `;
1336
+ controller.enqueue(encoder.encode(data));
1337
+ });
1338
+ const unsubscribeStart = durably.on("run:start", (event) => {
1339
+ if (closed) return;
1340
+ if (jobNameFilter && event.jobName !== jobNameFilter) return;
1341
+ const data = `data: ${JSON.stringify({
1342
+ type: "run:start",
1343
+ runId: event.runId,
1344
+ jobName: event.jobName
1345
+ })}
1346
+
1347
+ `;
1348
+ controller.enqueue(encoder.encode(data));
1349
+ });
1350
+ const unsubscribeComplete = durably.on("run:complete", (event) => {
1351
+ if (closed) return;
1352
+ if (jobNameFilter && event.jobName !== jobNameFilter) return;
1353
+ const data = `data: ${JSON.stringify({
1354
+ type: "run:complete",
1355
+ runId: event.runId,
1356
+ jobName: event.jobName
1357
+ })}
1358
+
1359
+ `;
1360
+ controller.enqueue(encoder.encode(data));
1361
+ });
1362
+ const unsubscribeFail = durably.on("run:fail", (event) => {
1363
+ if (closed) return;
1364
+ if (jobNameFilter && event.jobName !== jobNameFilter) return;
1365
+ const data = `data: ${JSON.stringify({
1366
+ type: "run:fail",
1367
+ runId: event.runId,
1368
+ jobName: event.jobName
1369
+ })}
1370
+
1371
+ `;
1372
+ controller.enqueue(encoder.encode(data));
1373
+ });
1374
+ const unsubscribeCancel = durably.on("run:cancel", (event) => {
1375
+ if (closed) return;
1376
+ if (jobNameFilter && event.jobName !== jobNameFilter) return;
1377
+ const data = `data: ${JSON.stringify({
1378
+ type: "run:cancel",
1379
+ runId: event.runId,
1380
+ jobName: event.jobName
1381
+ })}
1382
+
1383
+ `;
1384
+ controller.enqueue(encoder.encode(data));
1385
+ });
1386
+ const unsubscribeRetry = durably.on("run:retry", (event) => {
1387
+ if (closed) return;
1388
+ if (jobNameFilter && event.jobName !== jobNameFilter) return;
1389
+ const data = `data: ${JSON.stringify({
1390
+ type: "run:retry",
1391
+ runId: event.runId,
1392
+ jobName: event.jobName
1393
+ })}
1394
+
1395
+ `;
1396
+ controller.enqueue(encoder.encode(data));
1397
+ });
1398
+ const unsubscribeProgress = durably.on("run:progress", (event) => {
1399
+ if (closed) return;
1400
+ if (jobNameFilter && event.jobName !== jobNameFilter) return;
1401
+ const data = `data: ${JSON.stringify({
1402
+ type: "run:progress",
1403
+ runId: event.runId,
1404
+ jobName: event.jobName,
1405
+ progress: event.progress
1406
+ })}
1407
+
1408
+ `;
1409
+ controller.enqueue(encoder.encode(data));
1410
+ });
1411
+ const unsubscribeStepStart = durably.on("step:start", (event) => {
1412
+ if (closed) return;
1413
+ if (jobNameFilter && event.jobName !== jobNameFilter) return;
1414
+ const data = `data: ${JSON.stringify({
1415
+ type: "step:start",
1416
+ runId: event.runId,
1417
+ jobName: event.jobName,
1418
+ stepName: event.stepName,
1419
+ stepIndex: event.stepIndex
1420
+ })}
1421
+
1422
+ `;
1423
+ controller.enqueue(encoder.encode(data));
1424
+ });
1425
+ const unsubscribeStepComplete = durably.on(
1426
+ "step:complete",
1427
+ (event) => {
1428
+ if (closed) return;
1429
+ if (jobNameFilter && event.jobName !== jobNameFilter) return;
1430
+ const data = `data: ${JSON.stringify({
1431
+ type: "step:complete",
1432
+ runId: event.runId,
1433
+ jobName: event.jobName,
1434
+ stepName: event.stepName,
1435
+ stepIndex: event.stepIndex
1436
+ })}
1437
+
1438
+ `;
1439
+ controller.enqueue(encoder.encode(data));
1440
+ }
1441
+ );
1442
+ const unsubscribeStepFail = durably.on("step:fail", (event) => {
1443
+ if (closed) return;
1444
+ if (jobNameFilter && event.jobName !== jobNameFilter) return;
1445
+ const data = `data: ${JSON.stringify({
1446
+ type: "step:fail",
1447
+ runId: event.runId,
1448
+ jobName: event.jobName,
1449
+ stepName: event.stepName,
1450
+ stepIndex: event.stepIndex,
1451
+ error: event.error
1452
+ })}
1453
+
1454
+ `;
1455
+ controller.enqueue(encoder.encode(data));
1456
+ });
1457
+ const unsubscribeLogWrite = durably.on("log:write", (event) => {
1458
+ if (closed) return;
1459
+ if (jobNameFilter) return;
1460
+ const data = `data: ${JSON.stringify({
1461
+ type: "log:write",
1462
+ runId: event.runId,
1463
+ stepName: event.stepName,
1464
+ level: event.level,
1465
+ message: event.message,
1466
+ data: event.data
1467
+ })}
1468
+
1469
+ `;
1470
+ controller.enqueue(encoder.encode(data));
1471
+ });
1472
+ controller.cleanup = () => {
1473
+ closed = true;
1474
+ unsubscribeTrigger();
1475
+ unsubscribeStart();
1476
+ unsubscribeComplete();
1477
+ unsubscribeFail();
1478
+ unsubscribeCancel();
1479
+ unsubscribeRetry();
1480
+ unsubscribeProgress();
1481
+ unsubscribeStepStart();
1482
+ unsubscribeStepComplete();
1483
+ unsubscribeStepFail();
1484
+ unsubscribeLogWrite();
1485
+ };
1486
+ },
1487
+ cancel(controller) {
1488
+ const cleanup = controller.cleanup;
1489
+ if (cleanup) cleanup();
1490
+ }
1491
+ });
1492
+ return new Response(sseStream, {
1493
+ status: 200,
1494
+ headers: {
1495
+ "Content-Type": "text/event-stream",
1496
+ "Cache-Control": "no-cache",
1497
+ Connection: "keep-alive"
1498
+ }
1499
+ });
1500
+ }
1501
+ };
1502
+ return handler;
1503
+ }
886
1504
  export {
887
1505
  CancelledError,
888
1506
  createDurably,
1507
+ createDurablyHandler,
1508
+ defineJob,
889
1509
  withLogPersistence
890
1510
  };
891
1511
  //# sourceMappingURL=index.js.map