@coji/durably 0.4.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
@@ -62,7 +62,7 @@ function createJobRegistry() {
62
62
  }
63
63
  };
64
64
  }
65
- function createJobHandle(jobDef, storage, _eventEmitter, registry) {
65
+ function createJobHandle(jobDef, storage, eventEmitter, registry) {
66
66
  const existingJob = registry.get(jobDef.name);
67
67
  if (existingJob) {
68
68
  if (existingJob.jobDef === jobDef) {
@@ -87,6 +87,12 @@ function createJobHandle(jobDef, storage, _eventEmitter, registry) {
87
87
  idempotencyKey: options?.idempotencyKey,
88
88
  concurrencyKey: options?.concurrencyKey
89
89
  });
90
+ eventEmitter.emit({
91
+ type: "run:trigger",
92
+ runId: run.id,
93
+ jobName: jobDef.name,
94
+ payload: parseResult.data
95
+ });
90
96
  return run;
91
97
  },
92
98
  async triggerAndWait(input, options) {
@@ -103,19 +109,16 @@ function createJobHandle(jobDef, storage, _eventEmitter, registry) {
103
109
  clearTimeout(timeoutId);
104
110
  }
105
111
  };
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
- }
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
+ });
116
119
  }
117
- );
118
- const unsubscribeFail = _eventEmitter.on("run:fail", (event) => {
120
+ });
121
+ const unsubscribeFail = eventEmitter.on("run:fail", (event) => {
119
122
  if (event.runId === run.id && !resolved) {
120
123
  cleanup();
121
124
  reject(new Error(event.error));
@@ -177,6 +180,14 @@ function createJobHandle(jobDef, storage, _eventEmitter, registry) {
177
180
  concurrencyKey: v.options?.concurrencyKey
178
181
  }))
179
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
+ }
180
191
  return runs;
181
192
  },
182
193
  async getRun(id) {
@@ -258,6 +269,7 @@ function rowToRun(row) {
258
269
  idempotencyKey: row.idempotency_key,
259
270
  concurrencyKey: row.concurrency_key,
260
271
  currentStepIndex: row.current_step_index,
272
+ stepCount: Number(row.step_count ?? 0),
261
273
  progress: row.progress ? JSON.parse(row.progress) : null,
262
274
  output: row.output ? JSON.parse(row.output) : null,
263
275
  error: row.error,
@@ -381,18 +393,22 @@ function createKyselyStorage(db) {
381
393
  await db.deleteFrom("durably_runs").where("id", "=", runId).execute();
382
394
  },
383
395
  async getRun(runId) {
384
- 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();
385
399
  return row ? rowToRun(row) : null;
386
400
  },
387
401
  async getRuns(filter) {
388
- 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");
389
405
  if (filter?.status) {
390
- query = query.where("status", "=", filter.status);
406
+ query = query.where("durably_runs.status", "=", filter.status);
391
407
  }
392
408
  if (filter?.jobName) {
393
- query = query.where("job_name", "=", filter.jobName);
409
+ query = query.where("durably_runs.job_name", "=", filter.jobName);
394
410
  }
395
- query = query.orderBy("created_at", "desc");
411
+ query = query.orderBy("durably_runs.created_at", "desc");
396
412
  if (filter?.limit !== void 0) {
397
413
  query = query.limit(filter.limit);
398
414
  }
@@ -406,12 +422,18 @@ function createKyselyStorage(db) {
406
422
  return rows.map(rowToRun);
407
423
  },
408
424
  async getNextPendingRun(excludeConcurrencyKeys) {
409
- 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);
410
428
  if (excludeConcurrencyKeys.length > 0) {
411
429
  query = query.where(
412
430
  (eb) => eb.or([
413
- eb("concurrency_key", "is", null),
414
- 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
+ )
415
437
  ])
416
438
  );
417
439
  }
@@ -547,8 +569,13 @@ function createStepContext(run, jobName, storage, eventEmitter) {
547
569
  }
548
570
  },
549
571
  progress(current, total, message) {
550
- storage.updateRun(run.id, {
551
- 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
552
579
  });
553
580
  },
554
581
  log: {
@@ -780,35 +807,136 @@ var DEFAULTS = {
780
807
  heartbeatInterval: 5e3,
781
808
  staleThreshold: 3e4
782
809
  };
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;
810
+ function createDurablyInstance(state, jobs) {
811
+ const { db, storage, eventEmitter, jobRegistry, worker } = state;
796
812
  const durably = {
797
813
  db,
798
814
  storage,
815
+ jobs,
799
816
  on: eventEmitter.on,
800
817
  emit: eventEmitter.emit,
801
818
  onError: eventEmitter.onError,
802
819
  start: worker.start,
803
820
  stop: worker.stop,
804
- register(jobDef) {
805
- return createJobHandle(jobDef, 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);
806
836
  },
807
837
  getRun: storage.getRun,
808
838
  getRuns: storage.getRuns,
809
839
  use(plugin) {
810
840
  plugin.install(durably);
811
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
+ },
812
940
  async retry(runId) {
813
941
  const run = await storage.getRun(runId);
814
942
  if (!run) {
@@ -827,6 +955,11 @@ function createDurably(options) {
827
955
  status: "pending",
828
956
  error: null
829
957
  });
958
+ eventEmitter.emit({
959
+ type: "run:retry",
960
+ runId,
961
+ jobName: run.jobName
962
+ });
830
963
  },
831
964
  async cancel(runId) {
832
965
  const run = await storage.getRun(runId);
@@ -845,6 +978,11 @@ function createDurably(options) {
845
978
  await storage.updateRun(runId, {
846
979
  status: "cancelled"
847
980
  });
981
+ eventEmitter.emit({
982
+ type: "run:cancel",
983
+ runId,
984
+ jobName: run.jobName
985
+ });
848
986
  },
849
987
  async deleteRun(runId) {
850
988
  const run = await storage.getRun(runId);
@@ -860,22 +998,48 @@ function createDurably(options) {
860
998
  await storage.deleteRun(runId);
861
999
  },
862
1000
  async migrate() {
863
- if (migrated) {
1001
+ if (state.migrated) {
864
1002
  return;
865
1003
  }
866
- if (migrating) {
867
- return migrating;
1004
+ if (state.migrating) {
1005
+ return state.migrating;
868
1006
  }
869
- migrating = runMigrations(db).then(() => {
870
- migrated = true;
1007
+ state.migrating = runMigrations(db).then(() => {
1008
+ state.migrated = true;
871
1009
  }).finally(() => {
872
- migrating = null;
1010
+ state.migrating = null;
873
1011
  });
874
- return migrating;
1012
+ return state.migrating;
1013
+ },
1014
+ async init() {
1015
+ await this.migrate();
1016
+ this.start();
875
1017
  }
876
1018
  };
877
1019
  return durably;
878
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
+ }
879
1043
 
880
1044
  // src/define-job.ts
881
1045
  function defineJob(config) {
@@ -904,9 +1068,443 @@ function withLogPersistence() {
904
1068
  }
905
1069
  };
906
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
+ }
907
1504
  export {
908
1505
  CancelledError,
909
1506
  createDurably,
1507
+ createDurablyHandler,
910
1508
  defineJob,
911
1509
  withLogPersistence
912
1510
  };