@coji/durably 0.10.0 → 0.12.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
@@ -53,6 +53,8 @@ function createEventEmitter() {
53
53
 
54
54
  // src/job.ts
55
55
  import { prettifyError } from "zod";
56
+ var noop = () => {
57
+ };
56
58
  function validateJobInputOrThrow(schema, input, context) {
57
59
  const result = schema.safeParse(input);
58
60
  if (!result.success) {
@@ -75,7 +77,7 @@ function createJobRegistry() {
75
77
  }
76
78
  };
77
79
  }
78
- function createJobHandle(jobDef, storage, eventEmitter, registry) {
80
+ function createJobHandle(jobDef, storage, eventEmitter, registry, labelsSchema) {
79
81
  const existingJob = registry.get(jobDef.name);
80
82
  if (existingJob) {
81
83
  if (existingJob.jobDef === jobDef) {
@@ -91,6 +93,9 @@ function createJobHandle(jobDef, storage, eventEmitter, registry) {
91
93
  name: jobDef.name,
92
94
  async trigger(input, options) {
93
95
  const validatedInput = validateJobInputOrThrow(inputSchema, input);
96
+ if (labelsSchema && options?.labels) {
97
+ validateJobInputOrThrow(labelsSchema, options.labels, "labels");
98
+ }
94
99
  const run = await storage.createRun({
95
100
  jobName: jobDef.name,
96
101
  input: validatedInput,
@@ -112,30 +117,57 @@ function createJobHandle(jobDef, storage, eventEmitter, registry) {
112
117
  return new Promise((resolve, reject) => {
113
118
  let timeoutId;
114
119
  let resolved = false;
120
+ const unsubscribes = [];
115
121
  const cleanup = () => {
116
122
  if (resolved) return;
117
123
  resolved = true;
118
- unsubscribeComplete();
119
- unsubscribeFail();
124
+ for (const unsub of unsubscribes) unsub();
120
125
  if (timeoutId) {
121
126
  clearTimeout(timeoutId);
122
127
  }
123
128
  };
124
- const unsubscribeComplete = eventEmitter.on("run:complete", (event) => {
125
- if (event.runId === run.id && !resolved) {
126
- cleanup();
127
- resolve({
128
- id: run.id,
129
- output: event.output
130
- });
131
- }
132
- });
133
- const unsubscribeFail = eventEmitter.on("run:fail", (event) => {
134
- if (event.runId === run.id && !resolved) {
135
- cleanup();
136
- reject(new Error(event.error));
137
- }
138
- });
129
+ unsubscribes.push(
130
+ eventEmitter.on("run:complete", (event) => {
131
+ if (event.runId === run.id && !resolved) {
132
+ cleanup();
133
+ resolve({
134
+ id: run.id,
135
+ output: event.output
136
+ });
137
+ }
138
+ })
139
+ );
140
+ unsubscribes.push(
141
+ eventEmitter.on("run:fail", (event) => {
142
+ if (event.runId === run.id && !resolved) {
143
+ cleanup();
144
+ reject(new Error(event.error));
145
+ }
146
+ })
147
+ );
148
+ if (options?.onProgress) {
149
+ const onProgress = options.onProgress;
150
+ unsubscribes.push(
151
+ eventEmitter.on("run:progress", (event) => {
152
+ if (event.runId === run.id && !resolved) {
153
+ void Promise.resolve(onProgress(event.progress)).catch(noop);
154
+ }
155
+ })
156
+ );
157
+ }
158
+ if (options?.onLog) {
159
+ const onLog = options.onLog;
160
+ unsubscribes.push(
161
+ eventEmitter.on("log:write", (event) => {
162
+ if (event.runId === run.id && !resolved) {
163
+ const { level, message, data, stepName } = event;
164
+ void Promise.resolve(
165
+ onLog({ level, message, data, stepName })
166
+ ).catch(noop);
167
+ }
168
+ })
169
+ );
170
+ }
139
171
  storage.getRun(run.id).then((currentRun) => {
140
172
  if (resolved || !currentRun) return;
141
173
  if (currentRun.status === "completed") {
@@ -148,6 +180,10 @@ function createJobHandle(jobDef, storage, eventEmitter, registry) {
148
180
  cleanup();
149
181
  reject(new Error(currentRun.error || "Run failed"));
150
182
  }
183
+ }).catch((error) => {
184
+ if (resolved) return;
185
+ cleanup();
186
+ reject(error instanceof Error ? error : new Error(String(error)));
151
187
  });
152
188
  if (options?.timeout !== void 0) {
153
189
  timeoutId = setTimeout(() => {
@@ -178,6 +214,13 @@ function createJobHandle(jobDef, storage, eventEmitter, registry) {
178
214
  normalized[i].input,
179
215
  `at index ${i}`
180
216
  );
217
+ if (labelsSchema && normalized[i].options?.labels) {
218
+ validateJobInputOrThrow(
219
+ labelsSchema,
220
+ normalized[i].options?.labels,
221
+ `labels at index ${i}`
222
+ );
223
+ }
181
224
  validated.push({
182
225
  input: validatedInput,
183
226
  options: normalized[i].options
@@ -222,6 +265,7 @@ function createJobHandle(jobDef, storage, eventEmitter, registry) {
222
265
  name: jobDef.name,
223
266
  inputSchema,
224
267
  outputSchema,
268
+ labelsSchema,
225
269
  fn: jobDef.run,
226
270
  jobDef,
227
271
  handle
@@ -460,11 +504,19 @@ function createKyselyStorage(db) {
460
504
  query = query.where("durably_runs.status", "=", filter.status);
461
505
  }
462
506
  if (filter?.jobName) {
463
- query = query.where("durably_runs.job_name", "=", filter.jobName);
507
+ if (Array.isArray(filter.jobName)) {
508
+ if (filter.jobName.length > 0) {
509
+ query = query.where("durably_runs.job_name", "in", filter.jobName);
510
+ }
511
+ } else {
512
+ query = query.where("durably_runs.job_name", "=", filter.jobName);
513
+ }
464
514
  }
465
515
  if (filter?.labels) {
466
- validateLabels(filter.labels);
467
- for (const [key, value] of Object.entries(filter.labels)) {
516
+ const labels = filter.labels;
517
+ validateLabels(labels);
518
+ for (const [key, value] of Object.entries(labels)) {
519
+ if (value === void 0) continue;
468
520
  query = query.where(
469
521
  sql`json_extract(durably_runs.labels, ${`$."${key}"`})`,
470
522
  "=",
@@ -526,6 +578,9 @@ function createKyselyStorage(db) {
526
578
  await db.insertInto("durably_steps").values(step).execute();
527
579
  return rowToStep(step);
528
580
  },
581
+ async deleteSteps(runId) {
582
+ await db.deleteFrom("durably_steps").where("run_id", "=", runId).execute();
583
+ },
529
584
  async getSteps(runId) {
530
585
  const rows = await db.selectFrom("durably_steps").selectAll().where("run_id", "=", runId).orderBy("index", "asc").execute();
531
586
  return rows.map(rowToStep);
@@ -825,6 +880,12 @@ function createWorker(config, storage, eventEmitter, jobRegistry) {
825
880
  } catch (error) {
826
881
  await handleRunFailure(run.id, run.jobName, error);
827
882
  } finally {
883
+ if (config.cleanupSteps) {
884
+ try {
885
+ await storage.deleteSteps(run.id);
886
+ } catch {
887
+ }
888
+ }
828
889
  dispose();
829
890
  if (heartbeatInterval) {
830
891
  clearInterval(heartbeatInterval);
@@ -911,10 +972,18 @@ function createWorker(config, storage, eventEmitter, jobRegistry) {
911
972
  var DEFAULTS = {
912
973
  pollingInterval: 1e3,
913
974
  heartbeatInterval: 5e3,
914
- staleThreshold: 3e4
975
+ staleThreshold: 3e4,
976
+ cleanupSteps: true
915
977
  };
916
978
  function createDurablyInstance(state, jobs) {
917
979
  const { db, storage, eventEmitter, jobRegistry, worker } = state;
980
+ async function getRunOrThrow(runId) {
981
+ const run = await storage.getRun(runId);
982
+ if (!run) {
983
+ throw new Error(`Run not found: ${runId}`);
984
+ }
985
+ return run;
986
+ }
918
987
  const durably = {
919
988
  db,
920
989
  storage,
@@ -933,12 +1002,16 @@ function createDurablyInstance(state, jobs) {
933
1002
  jobDef,
934
1003
  storage,
935
1004
  eventEmitter,
936
- jobRegistry
1005
+ jobRegistry,
1006
+ state.labelsSchema
937
1007
  );
938
1008
  newHandles[key] = handle;
939
1009
  }
940
1010
  const mergedJobs = { ...jobs, ...newHandles };
941
- return createDurablyInstance(state, mergedJobs);
1011
+ return createDurablyInstance(
1012
+ state,
1013
+ mergedJobs
1014
+ );
942
1015
  },
943
1016
  getRun: storage.getRun,
944
1017
  getRuns: storage.getRuns,
@@ -955,93 +1028,34 @@ function createDurablyInstance(state, jobs) {
955
1028
  subscribe(runId) {
956
1029
  let closed = false;
957
1030
  let cleanup = null;
1031
+ const closeEvents = /* @__PURE__ */ new Set(["run:complete", "run:delete"]);
1032
+ const subscribedEvents = [
1033
+ "run:start",
1034
+ "run:complete",
1035
+ "run:fail",
1036
+ "run:cancel",
1037
+ "run:delete",
1038
+ "run:progress",
1039
+ "step:start",
1040
+ "step:complete",
1041
+ "step:fail",
1042
+ "log:write"
1043
+ ];
958
1044
  return new ReadableStream({
959
1045
  start: (controller) => {
960
- const unsubscribeStart = eventEmitter.on("run:start", (event) => {
961
- if (!closed && event.runId === runId) {
1046
+ const unsubscribes = subscribedEvents.map(
1047
+ (type) => eventEmitter.on(type, (event) => {
1048
+ if (closed || event.runId !== runId) return;
962
1049
  controller.enqueue(event);
963
- }
964
- });
965
- const unsubscribeComplete = eventEmitter.on(
966
- "run:complete",
967
- (event) => {
968
- if (!closed && event.runId === runId) {
969
- controller.enqueue(event);
1050
+ if (closeEvents.has(type)) {
970
1051
  closed = true;
971
1052
  cleanup?.();
972
1053
  controller.close();
973
1054
  }
974
- }
975
- );
976
- const unsubscribeFail = eventEmitter.on("run:fail", (event) => {
977
- if (!closed && event.runId === runId) {
978
- controller.enqueue(event);
979
- }
980
- });
981
- const unsubscribeCancel = eventEmitter.on("run:cancel", (event) => {
982
- if (!closed && event.runId === runId) {
983
- controller.enqueue(event);
984
- }
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
- });
994
- const unsubscribeRetry = eventEmitter.on("run:retry", (event) => {
995
- if (!closed && event.runId === runId) {
996
- controller.enqueue(event);
997
- }
998
- });
999
- const unsubscribeProgress = eventEmitter.on(
1000
- "run:progress",
1001
- (event) => {
1002
- if (!closed && event.runId === runId) {
1003
- controller.enqueue(event);
1004
- }
1005
- }
1006
- );
1007
- const unsubscribeStepStart = eventEmitter.on(
1008
- "step:start",
1009
- (event) => {
1010
- if (!closed && event.runId === runId) {
1011
- controller.enqueue(event);
1012
- }
1013
- }
1014
- );
1015
- const unsubscribeStepComplete = eventEmitter.on(
1016
- "step:complete",
1017
- (event) => {
1018
- if (!closed && event.runId === runId) {
1019
- controller.enqueue(event);
1020
- }
1021
- }
1055
+ })
1022
1056
  );
1023
- const unsubscribeStepFail = eventEmitter.on("step:fail", (event) => {
1024
- if (!closed && event.runId === runId) {
1025
- controller.enqueue(event);
1026
- }
1027
- });
1028
- const unsubscribeLog = eventEmitter.on("log:write", (event) => {
1029
- if (!closed && event.runId === runId) {
1030
- controller.enqueue(event);
1031
- }
1032
- });
1033
1057
  cleanup = () => {
1034
- unsubscribeStart();
1035
- unsubscribeComplete();
1036
- unsubscribeFail();
1037
- unsubscribeCancel();
1038
- unsubscribeDelete();
1039
- unsubscribeRetry();
1040
- unsubscribeProgress();
1041
- unsubscribeStepStart();
1042
- unsubscribeStepComplete();
1043
- unsubscribeStepFail();
1044
- unsubscribeLog();
1058
+ for (const unsub of unsubscribes) unsub();
1045
1059
  };
1046
1060
  },
1047
1061
  cancel: () => {
@@ -1052,36 +1066,34 @@ function createDurablyInstance(state, jobs) {
1052
1066
  }
1053
1067
  });
1054
1068
  },
1055
- async retry(runId) {
1056
- const run = await storage.getRun(runId);
1057
- if (!run) {
1058
- throw new Error(`Run not found: ${runId}`);
1059
- }
1060
- if (run.status === "completed") {
1061
- throw new Error(`Cannot retry completed run: ${runId}`);
1062
- }
1069
+ async retrigger(runId) {
1070
+ const run = await getRunOrThrow(runId);
1063
1071
  if (run.status === "pending") {
1064
- throw new Error(`Cannot retry pending run: ${runId}`);
1072
+ throw new Error(`Cannot retrigger pending run: ${runId}`);
1065
1073
  }
1066
1074
  if (run.status === "running") {
1067
- throw new Error(`Cannot retry running run: ${runId}`);
1075
+ throw new Error(`Cannot retrigger running run: ${runId}`);
1068
1076
  }
1069
- await storage.updateRun(runId, {
1070
- status: "pending",
1071
- error: null
1077
+ if (!jobRegistry.get(run.jobName)) {
1078
+ throw new Error(`Unknown job: ${run.jobName}`);
1079
+ }
1080
+ const nextRun = await storage.createRun({
1081
+ jobName: run.jobName,
1082
+ input: run.input,
1083
+ concurrencyKey: run.concurrencyKey ?? void 0,
1084
+ labels: run.labels
1072
1085
  });
1073
1086
  eventEmitter.emit({
1074
- type: "run:retry",
1075
- runId,
1087
+ type: "run:trigger",
1088
+ runId: nextRun.id,
1076
1089
  jobName: run.jobName,
1090
+ input: run.input,
1077
1091
  labels: run.labels
1078
1092
  });
1093
+ return nextRun;
1079
1094
  },
1080
1095
  async cancel(runId) {
1081
- const run = await storage.getRun(runId);
1082
- if (!run) {
1083
- throw new Error(`Run not found: ${runId}`);
1084
- }
1096
+ const run = await getRunOrThrow(runId);
1085
1097
  if (run.status === "completed") {
1086
1098
  throw new Error(`Cannot cancel completed run: ${runId}`);
1087
1099
  }
@@ -1091,9 +1103,14 @@ function createDurablyInstance(state, jobs) {
1091
1103
  if (run.status === "cancelled") {
1092
1104
  throw new Error(`Cannot cancel already cancelled run: ${runId}`);
1093
1105
  }
1106
+ const wasPending = run.status === "pending";
1094
1107
  await storage.updateRun(runId, {
1095
- status: "cancelled"
1108
+ status: "cancelled",
1109
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
1096
1110
  });
1111
+ if (wasPending && state.cleanupSteps) {
1112
+ await storage.deleteSteps(runId);
1113
+ }
1097
1114
  eventEmitter.emit({
1098
1115
  type: "run:cancel",
1099
1116
  runId,
@@ -1102,10 +1119,7 @@ function createDurablyInstance(state, jobs) {
1102
1119
  });
1103
1120
  },
1104
1121
  async deleteRun(runId) {
1105
- const run = await storage.getRun(runId);
1106
- if (!run) {
1107
- throw new Error(`Run not found: ${runId}`);
1108
- }
1122
+ const run = await getRunOrThrow(runId);
1109
1123
  if (run.status === "pending") {
1110
1124
  throw new Error(`Cannot delete pending run: ${runId}`);
1111
1125
  }
@@ -1145,7 +1159,8 @@ function createDurably(options) {
1145
1159
  const config = {
1146
1160
  pollingInterval: options.pollingInterval ?? DEFAULTS.pollingInterval,
1147
1161
  heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
1148
- staleThreshold: options.staleThreshold ?? DEFAULTS.staleThreshold
1162
+ staleThreshold: options.staleThreshold ?? DEFAULTS.staleThreshold,
1163
+ cleanupSteps: options.cleanupSteps ?? DEFAULTS.cleanupSteps
1149
1164
  };
1150
1165
  const db = new Kysely({ dialect: options.dialect });
1151
1166
  const storage = createKyselyStorage(db);
@@ -1158,10 +1173,19 @@ function createDurably(options) {
1158
1173
  eventEmitter,
1159
1174
  jobRegistry,
1160
1175
  worker,
1176
+ labelsSchema: options.labels,
1177
+ cleanupSteps: config.cleanupSteps,
1161
1178
  migrating: null,
1162
1179
  migrated: false
1163
1180
  };
1164
- return createDurablyInstance(state, {});
1181
+ const instance = createDurablyInstance(
1182
+ state,
1183
+ {}
1184
+ );
1185
+ if (options.jobs) {
1186
+ return instance.register(options.jobs);
1187
+ }
1188
+ return instance;
1165
1189
  }
1166
1190
 
1167
1191
  // src/define-job.ts
@@ -1400,6 +1424,14 @@ function createThrottledSSEController(inner, throttleMs) {
1400
1424
  }
1401
1425
 
1402
1426
  // src/server.ts
1427
+ var VALID_STATUSES = [
1428
+ "pending",
1429
+ "running",
1430
+ "completed",
1431
+ "failed",
1432
+ "cancelled"
1433
+ ];
1434
+ var VALID_STATUSES_SET = new Set(VALID_STATUSES);
1403
1435
  function parseLabelsFromParams(searchParams) {
1404
1436
  const labels = {};
1405
1437
  for (const [key, value] of searchParams.entries()) {
@@ -1409,6 +1441,51 @@ function parseLabelsFromParams(searchParams) {
1409
1441
  }
1410
1442
  return Object.keys(labels).length > 0 ? labels : void 0;
1411
1443
  }
1444
+ function parseRunFilter(url) {
1445
+ const jobNames = url.searchParams.getAll("jobName");
1446
+ const statusParam = url.searchParams.get("status");
1447
+ const limitParam = url.searchParams.get("limit");
1448
+ const offsetParam = url.searchParams.get("offset");
1449
+ const labels = parseLabelsFromParams(url.searchParams);
1450
+ if (statusParam && !VALID_STATUSES_SET.has(statusParam)) {
1451
+ return errorResponse(
1452
+ `Invalid status: ${statusParam}. Must be one of: ${VALID_STATUSES.join(", ")}`,
1453
+ 400
1454
+ );
1455
+ }
1456
+ let limit;
1457
+ if (limitParam) {
1458
+ limit = Number.parseInt(limitParam, 10);
1459
+ if (Number.isNaN(limit) || limit < 0) {
1460
+ return errorResponse("Invalid limit: must be a non-negative integer", 400);
1461
+ }
1462
+ }
1463
+ let offset;
1464
+ if (offsetParam) {
1465
+ offset = Number.parseInt(offsetParam, 10);
1466
+ if (Number.isNaN(offset) || offset < 0) {
1467
+ return errorResponse(
1468
+ "Invalid offset: must be a non-negative integer",
1469
+ 400
1470
+ );
1471
+ }
1472
+ }
1473
+ return {
1474
+ jobName: jobNames.length > 0 ? jobNames : void 0,
1475
+ status: statusParam,
1476
+ labels,
1477
+ limit,
1478
+ offset
1479
+ };
1480
+ }
1481
+ function parseRunsSubscribeFilter(url) {
1482
+ const jobNames = url.searchParams.getAll("jobName");
1483
+ const labels = parseLabelsFromParams(url.searchParams);
1484
+ return {
1485
+ jobName: jobNames.length > 0 ? jobNames : void 0,
1486
+ labels
1487
+ };
1488
+ }
1412
1489
  function matchesLabels(eventLabels, filterLabels) {
1413
1490
  for (const [key, value] of Object.entries(filterLabels)) {
1414
1491
  if (eventLabels[key] !== value) return false;
@@ -1417,310 +1494,334 @@ function matchesLabels(eventLabels, filterLabels) {
1417
1494
  }
1418
1495
  function createDurablyHandler(durably, options) {
1419
1496
  const throttleMs = options?.sseThrottleMs ?? 100;
1420
- const handler = {
1421
- async handle(request, basePath) {
1422
- if (options?.onRequest) {
1423
- await options.onRequest();
1424
- }
1425
- const url = new URL(request.url);
1426
- const path = url.pathname.replace(basePath, "");
1427
- const method = request.method;
1428
- if (method === "GET") {
1429
- if (path === "/subscribe") return handler.subscribe(request);
1430
- if (path === "/runs") return handler.runs(request);
1431
- if (path === "/run") return handler.run(request);
1432
- if (path === "/steps") return handler.steps(request);
1433
- if (path === "/runs/subscribe") return handler.runsSubscribe(request);
1497
+ const auth = options?.auth;
1498
+ if (auth && !auth.authenticate) {
1499
+ throw new Error(
1500
+ "createDurablyHandler: auth.authenticate is required when auth is provided"
1501
+ );
1502
+ }
1503
+ async function withErrorHandling(fn) {
1504
+ try {
1505
+ return await fn();
1506
+ } catch (error) {
1507
+ if (error instanceof Response) throw error;
1508
+ return errorResponse(getErrorMessage(error), 500);
1509
+ }
1510
+ }
1511
+ async function requireRunAccess(url, ctx, operation) {
1512
+ const runId = getRequiredQueryParam(url, "runId");
1513
+ if (runId instanceof Response) return runId;
1514
+ const run = await durably.getRun(runId);
1515
+ if (!run) return errorResponse("Run not found", 404);
1516
+ if (auth?.onRunAccess && ctx !== void 0) {
1517
+ await auth.onRunAccess(ctx, run, {
1518
+ operation
1519
+ });
1520
+ }
1521
+ return { run, runId };
1522
+ }
1523
+ async function handleTrigger(request, ctx) {
1524
+ return withErrorHandling(async () => {
1525
+ const body = await request.json();
1526
+ if (!body.jobName) {
1527
+ return errorResponse("jobName is required", 400);
1434
1528
  }
1435
- if (method === "POST") {
1436
- if (path === "/trigger") return handler.trigger(request);
1437
- if (path === "/retry") return handler.retry(request);
1438
- if (path === "/cancel") return handler.cancel(request);
1529
+ const job = durably.getJob(body.jobName);
1530
+ if (!job) {
1531
+ return errorResponse(`Job not found: ${body.jobName}`, 404);
1439
1532
  }
1440
- if (method === "DELETE") {
1441
- if (path === "/run") return handler.delete(request);
1533
+ if (auth?.onTrigger && ctx !== void 0) {
1534
+ await auth.onTrigger(ctx, body);
1442
1535
  }
1443
- return new Response("Not Found", { status: 404 });
1444
- },
1445
- async trigger(request) {
1446
- try {
1447
- const body = await request.json();
1448
- if (!body.jobName) {
1449
- return errorResponse("jobName is required", 400);
1450
- }
1451
- const job = durably.getJob(body.jobName);
1452
- if (!job) {
1453
- return errorResponse(`Job not found: ${body.jobName}`, 404);
1454
- }
1455
- const run = await job.trigger(body.input ?? {}, {
1536
+ const run = await job.trigger(
1537
+ body.input ?? {},
1538
+ {
1456
1539
  idempotencyKey: body.idempotencyKey,
1457
1540
  concurrencyKey: body.concurrencyKey,
1458
1541
  labels: body.labels
1459
- });
1460
- const response = { runId: run.id };
1461
- return jsonResponse(response);
1462
- } catch (error) {
1463
- return errorResponse(getErrorMessage(error), 500);
1464
- }
1465
- },
1466
- subscribe(request) {
1467
- const url = new URL(request.url);
1468
- const runId = getRequiredQueryParam(url, "runId");
1469
- if (runId instanceof Response) return runId;
1470
- const stream = durably.subscribe(runId);
1471
- const sseStream = createThrottledSSEStreamFromReader(
1472
- stream.getReader(),
1473
- throttleMs
1542
+ }
1474
1543
  );
1475
- return createSSEResponse(sseStream);
1476
- },
1477
- async runs(request) {
1478
- try {
1479
- const url = new URL(request.url);
1480
- const jobName = url.searchParams.get("jobName") ?? void 0;
1481
- const status = url.searchParams.get("status");
1482
- const limit = url.searchParams.get("limit");
1483
- const offset = url.searchParams.get("offset");
1484
- const labels = parseLabelsFromParams(url.searchParams);
1485
- const runs = await durably.getRuns({
1486
- jobName,
1487
- status,
1488
- labels,
1489
- limit: limit ? Number.parseInt(limit, 10) : void 0,
1490
- offset: offset ? Number.parseInt(offset, 10) : void 0
1491
- });
1492
- return jsonResponse(runs.map(toClientRun));
1493
- } catch (error) {
1494
- return errorResponse(getErrorMessage(error), 500);
1544
+ const response = { runId: run.id };
1545
+ return jsonResponse(response);
1546
+ });
1547
+ }
1548
+ async function handleSubscribe(url, ctx) {
1549
+ const result = await requireRunAccess(url, ctx, "subscribe");
1550
+ if (result instanceof Response) return result;
1551
+ const stream = durably.subscribe(result.runId);
1552
+ const sseStream = createThrottledSSEStreamFromReader(
1553
+ stream.getReader(),
1554
+ throttleMs
1555
+ );
1556
+ return createSSEResponse(sseStream);
1557
+ }
1558
+ async function handleRuns(url, ctx) {
1559
+ return withErrorHandling(async () => {
1560
+ const filterOrError = parseRunFilter(url);
1561
+ if (filterOrError instanceof Response) return filterOrError;
1562
+ let filter = filterOrError;
1563
+ if (auth?.scopeRuns && ctx !== void 0) {
1564
+ filter = await auth.scopeRuns(ctx, filter);
1495
1565
  }
1496
- },
1497
- async run(request) {
1498
- try {
1499
- const url = new URL(request.url);
1500
- const runId = getRequiredQueryParam(url, "runId");
1501
- if (runId instanceof Response) return runId;
1502
- const run = await durably.getRun(runId);
1503
- if (!run) {
1504
- return errorResponse("Run not found", 404);
1566
+ const runs = await durably.getRuns(filter);
1567
+ return jsonResponse(runs.map(toClientRun));
1568
+ });
1569
+ }
1570
+ async function handleRun(url, ctx) {
1571
+ return withErrorHandling(async () => {
1572
+ const result = await requireRunAccess(url, ctx, "read");
1573
+ if (result instanceof Response) return result;
1574
+ return jsonResponse(toClientRun(result.run));
1575
+ });
1576
+ }
1577
+ async function handleSteps(url, ctx) {
1578
+ return withErrorHandling(async () => {
1579
+ const result = await requireRunAccess(url, ctx, "steps");
1580
+ if (result instanceof Response) return result;
1581
+ const steps = await durably.storage.getSteps(result.runId);
1582
+ return jsonResponse(steps);
1583
+ });
1584
+ }
1585
+ async function handleRetrigger(url, ctx) {
1586
+ return withErrorHandling(async () => {
1587
+ const result = await requireRunAccess(url, ctx, "retrigger");
1588
+ if (result instanceof Response) return result;
1589
+ const run = await durably.retrigger(result.runId);
1590
+ return jsonResponse({ success: true, runId: run.id });
1591
+ });
1592
+ }
1593
+ async function handleCancel(url, ctx) {
1594
+ return withErrorHandling(async () => {
1595
+ const result = await requireRunAccess(url, ctx, "cancel");
1596
+ if (result instanceof Response) return result;
1597
+ await durably.cancel(result.runId);
1598
+ return successResponse();
1599
+ });
1600
+ }
1601
+ async function handleDelete(url, ctx) {
1602
+ return withErrorHandling(async () => {
1603
+ const result = await requireRunAccess(url, ctx, "delete");
1604
+ if (result instanceof Response) return result;
1605
+ await durably.deleteRun(result.runId);
1606
+ return successResponse();
1607
+ });
1608
+ }
1609
+ async function handleRunsSubscribe(url, ctx) {
1610
+ let filter;
1611
+ if (ctx !== void 0 && auth?.scopeRunsSubscribe) {
1612
+ const parsed = parseRunsSubscribeFilter(
1613
+ url
1614
+ );
1615
+ filter = await auth.scopeRunsSubscribe(ctx, parsed);
1616
+ } else if (ctx !== void 0 && auth?.scopeRuns) {
1617
+ const parsed = parseRunsSubscribeFilter(
1618
+ url
1619
+ );
1620
+ const scoped = await auth.scopeRuns(
1621
+ ctx,
1622
+ {
1623
+ ...parsed
1505
1624
  }
1506
- return jsonResponse(toClientRun(run));
1507
- } catch (error) {
1508
- return errorResponse(getErrorMessage(error), 500);
1509
- }
1510
- },
1511
- async retry(request) {
1512
- try {
1513
- const url = new URL(request.url);
1514
- const runId = getRequiredQueryParam(url, "runId");
1515
- if (runId instanceof Response) return runId;
1516
- await durably.retry(runId);
1517
- return successResponse();
1518
- } catch (error) {
1519
- return errorResponse(getErrorMessage(error), 500);
1520
- }
1521
- },
1522
- async cancel(request) {
1523
- try {
1524
- const url = new URL(request.url);
1525
- const runId = getRequiredQueryParam(url, "runId");
1526
- if (runId instanceof Response) return runId;
1527
- await durably.cancel(runId);
1528
- return successResponse();
1529
- } catch (error) {
1530
- return errorResponse(getErrorMessage(error), 500);
1531
- }
1532
- },
1533
- async delete(request) {
1534
- try {
1535
- const url = new URL(request.url);
1536
- const runId = getRequiredQueryParam(url, "runId");
1537
- if (runId instanceof Response) return runId;
1538
- await durably.deleteRun(runId);
1539
- return successResponse();
1540
- } catch (error) {
1541
- return errorResponse(getErrorMessage(error), 500);
1625
+ );
1626
+ filter = { jobName: scoped.jobName, labels: scoped.labels };
1627
+ } else {
1628
+ filter = parseRunsSubscribeFilter(url);
1629
+ }
1630
+ return createRunsSSEStream(filter);
1631
+ }
1632
+ function createRunsSSEStream(filter) {
1633
+ const jobNameFilter = Array.isArray(filter.jobName) ? filter.jobName : filter.jobName ? [filter.jobName] : [];
1634
+ const labelsFilter = filter.labels;
1635
+ const matchesFilter = (jobName, labels) => {
1636
+ if (jobNameFilter.length > 0 && !jobNameFilter.includes(jobName))
1637
+ return false;
1638
+ if (labelsFilter && (!labels || !matchesLabels(labels, labelsFilter)))
1639
+ return false;
1640
+ return true;
1641
+ };
1642
+ const sseStream = createSSEStreamFromSubscriptions(
1643
+ (innerCtrl) => {
1644
+ const { controller: ctrl, dispose } = createThrottledSSEController(
1645
+ innerCtrl,
1646
+ throttleMs
1647
+ );
1648
+ const unsubscribes = [
1649
+ durably.on("run:trigger", (event) => {
1650
+ if (matchesFilter(event.jobName, event.labels)) {
1651
+ ctrl.enqueue({
1652
+ type: "run:trigger",
1653
+ runId: event.runId,
1654
+ jobName: event.jobName,
1655
+ labels: event.labels
1656
+ });
1657
+ }
1658
+ }),
1659
+ durably.on("run:start", (event) => {
1660
+ if (matchesFilter(event.jobName, event.labels)) {
1661
+ ctrl.enqueue({
1662
+ type: "run:start",
1663
+ runId: event.runId,
1664
+ jobName: event.jobName,
1665
+ labels: event.labels
1666
+ });
1667
+ }
1668
+ }),
1669
+ durably.on("run:complete", (event) => {
1670
+ if (matchesFilter(event.jobName, event.labels)) {
1671
+ ctrl.enqueue({
1672
+ type: "run:complete",
1673
+ runId: event.runId,
1674
+ jobName: event.jobName,
1675
+ labels: event.labels
1676
+ });
1677
+ }
1678
+ }),
1679
+ durably.on("run:fail", (event) => {
1680
+ if (matchesFilter(event.jobName, event.labels)) {
1681
+ ctrl.enqueue({
1682
+ type: "run:fail",
1683
+ runId: event.runId,
1684
+ jobName: event.jobName,
1685
+ labels: event.labels
1686
+ });
1687
+ }
1688
+ }),
1689
+ durably.on("run:cancel", (event) => {
1690
+ if (matchesFilter(event.jobName, event.labels)) {
1691
+ ctrl.enqueue({
1692
+ type: "run:cancel",
1693
+ runId: event.runId,
1694
+ jobName: event.jobName,
1695
+ labels: event.labels
1696
+ });
1697
+ }
1698
+ }),
1699
+ durably.on("run:delete", (event) => {
1700
+ if (matchesFilter(event.jobName, event.labels)) {
1701
+ ctrl.enqueue({
1702
+ type: "run:delete",
1703
+ runId: event.runId,
1704
+ jobName: event.jobName,
1705
+ labels: event.labels
1706
+ });
1707
+ }
1708
+ }),
1709
+ durably.on("run:progress", (event) => {
1710
+ if (matchesFilter(event.jobName, event.labels)) {
1711
+ ctrl.enqueue({
1712
+ type: "run:progress",
1713
+ runId: event.runId,
1714
+ jobName: event.jobName,
1715
+ progress: event.progress,
1716
+ labels: event.labels
1717
+ });
1718
+ }
1719
+ }),
1720
+ durably.on("step:start", (event) => {
1721
+ if (matchesFilter(event.jobName, event.labels)) {
1722
+ ctrl.enqueue({
1723
+ type: "step:start",
1724
+ runId: event.runId,
1725
+ jobName: event.jobName,
1726
+ stepName: event.stepName,
1727
+ stepIndex: event.stepIndex,
1728
+ labels: event.labels
1729
+ });
1730
+ }
1731
+ }),
1732
+ durably.on("step:complete", (event) => {
1733
+ if (matchesFilter(event.jobName, event.labels)) {
1734
+ ctrl.enqueue({
1735
+ type: "step:complete",
1736
+ runId: event.runId,
1737
+ jobName: event.jobName,
1738
+ stepName: event.stepName,
1739
+ stepIndex: event.stepIndex,
1740
+ labels: event.labels
1741
+ });
1742
+ }
1743
+ }),
1744
+ durably.on("step:fail", (event) => {
1745
+ if (matchesFilter(event.jobName, event.labels)) {
1746
+ ctrl.enqueue({
1747
+ type: "step:fail",
1748
+ runId: event.runId,
1749
+ jobName: event.jobName,
1750
+ stepName: event.stepName,
1751
+ stepIndex: event.stepIndex,
1752
+ error: event.error,
1753
+ labels: event.labels
1754
+ });
1755
+ }
1756
+ }),
1757
+ durably.on("step:cancel", (event) => {
1758
+ if (matchesFilter(event.jobName, event.labels)) {
1759
+ ctrl.enqueue({
1760
+ type: "step:cancel",
1761
+ runId: event.runId,
1762
+ jobName: event.jobName,
1763
+ stepName: event.stepName,
1764
+ stepIndex: event.stepIndex,
1765
+ labels: event.labels
1766
+ });
1767
+ }
1768
+ }),
1769
+ durably.on("log:write", (event) => {
1770
+ if (matchesFilter(event.jobName, event.labels)) {
1771
+ ctrl.enqueue({
1772
+ type: "log:write",
1773
+ runId: event.runId,
1774
+ jobName: event.jobName,
1775
+ labels: event.labels,
1776
+ stepName: event.stepName,
1777
+ level: event.level,
1778
+ message: event.message,
1779
+ data: event.data
1780
+ });
1781
+ }
1782
+ })
1783
+ ];
1784
+ return [...unsubscribes, dispose];
1542
1785
  }
1543
- },
1544
- async steps(request) {
1786
+ );
1787
+ return createSSEResponse(sseStream);
1788
+ }
1789
+ return {
1790
+ async handle(request, basePath) {
1545
1791
  try {
1792
+ let ctx;
1793
+ if (auth?.authenticate) {
1794
+ ctx = await auth.authenticate(request);
1795
+ }
1796
+ if (options?.onRequest) {
1797
+ await options.onRequest();
1798
+ }
1546
1799
  const url = new URL(request.url);
1547
- const runId = getRequiredQueryParam(url, "runId");
1548
- if (runId instanceof Response) return runId;
1549
- const steps = await durably.storage.getSteps(runId);
1550
- return jsonResponse(steps);
1800
+ const path = url.pathname.replace(basePath, "");
1801
+ const method = request.method;
1802
+ if (method === "GET") {
1803
+ if (path === "/subscribe") return await handleSubscribe(url, ctx);
1804
+ if (path === "/runs") return await handleRuns(url, ctx);
1805
+ if (path === "/run") return await handleRun(url, ctx);
1806
+ if (path === "/steps") return await handleSteps(url, ctx);
1807
+ if (path === "/runs/subscribe")
1808
+ return await handleRunsSubscribe(url, ctx);
1809
+ }
1810
+ if (method === "POST") {
1811
+ if (path === "/trigger") return await handleTrigger(request, ctx);
1812
+ if (path === "/retrigger") return await handleRetrigger(url, ctx);
1813
+ if (path === "/cancel") return await handleCancel(url, ctx);
1814
+ }
1815
+ if (method === "DELETE") {
1816
+ if (path === "/run") return await handleDelete(url, ctx);
1817
+ }
1818
+ return new Response("Not Found", { status: 404 });
1551
1819
  } catch (error) {
1820
+ if (error instanceof Response) return error;
1552
1821
  return errorResponse(getErrorMessage(error), 500);
1553
1822
  }
1554
- },
1555
- runsSubscribe(request) {
1556
- const url = new URL(request.url);
1557
- const jobNameFilter = url.searchParams.get("jobName");
1558
- const labelsFilter = parseLabelsFromParams(url.searchParams);
1559
- const matchesFilter = (jobName, labels) => {
1560
- if (jobNameFilter && jobName !== jobNameFilter) return false;
1561
- if (labelsFilter && (!labels || !matchesLabels(labels, labelsFilter)))
1562
- return false;
1563
- return true;
1564
- };
1565
- const sseStream = createSSEStreamFromSubscriptions(
1566
- (innerCtrl) => {
1567
- const { controller: ctrl, dispose } = createThrottledSSEController(
1568
- innerCtrl,
1569
- throttleMs
1570
- );
1571
- const unsubscribes = [
1572
- durably.on("run:trigger", (event) => {
1573
- if (matchesFilter(event.jobName, event.labels)) {
1574
- ctrl.enqueue({
1575
- type: "run:trigger",
1576
- runId: event.runId,
1577
- jobName: event.jobName,
1578
- labels: event.labels
1579
- });
1580
- }
1581
- }),
1582
- durably.on("run:start", (event) => {
1583
- if (matchesFilter(event.jobName, event.labels)) {
1584
- ctrl.enqueue({
1585
- type: "run:start",
1586
- runId: event.runId,
1587
- jobName: event.jobName,
1588
- labels: event.labels
1589
- });
1590
- }
1591
- }),
1592
- durably.on("run:complete", (event) => {
1593
- if (matchesFilter(event.jobName, event.labels)) {
1594
- ctrl.enqueue({
1595
- type: "run:complete",
1596
- runId: event.runId,
1597
- jobName: event.jobName,
1598
- labels: event.labels
1599
- });
1600
- }
1601
- }),
1602
- durably.on("run:fail", (event) => {
1603
- if (matchesFilter(event.jobName, event.labels)) {
1604
- ctrl.enqueue({
1605
- type: "run:fail",
1606
- runId: event.runId,
1607
- jobName: event.jobName,
1608
- labels: event.labels
1609
- });
1610
- }
1611
- }),
1612
- durably.on("run:cancel", (event) => {
1613
- if (matchesFilter(event.jobName, event.labels)) {
1614
- ctrl.enqueue({
1615
- type: "run:cancel",
1616
- runId: event.runId,
1617
- jobName: event.jobName,
1618
- labels: event.labels
1619
- });
1620
- }
1621
- }),
1622
- durably.on("run:delete", (event) => {
1623
- if (matchesFilter(event.jobName, event.labels)) {
1624
- ctrl.enqueue({
1625
- type: "run:delete",
1626
- runId: event.runId,
1627
- jobName: event.jobName,
1628
- labels: event.labels
1629
- });
1630
- }
1631
- }),
1632
- durably.on("run:retry", (event) => {
1633
- if (matchesFilter(event.jobName, event.labels)) {
1634
- ctrl.enqueue({
1635
- type: "run:retry",
1636
- runId: event.runId,
1637
- jobName: event.jobName,
1638
- labels: event.labels
1639
- });
1640
- }
1641
- }),
1642
- durably.on("run:progress", (event) => {
1643
- if (matchesFilter(event.jobName, event.labels)) {
1644
- ctrl.enqueue({
1645
- type: "run:progress",
1646
- runId: event.runId,
1647
- jobName: event.jobName,
1648
- progress: event.progress,
1649
- labels: event.labels
1650
- });
1651
- }
1652
- }),
1653
- durably.on("step:start", (event) => {
1654
- if (matchesFilter(event.jobName, event.labels)) {
1655
- ctrl.enqueue({
1656
- type: "step:start",
1657
- runId: event.runId,
1658
- jobName: event.jobName,
1659
- stepName: event.stepName,
1660
- stepIndex: event.stepIndex,
1661
- labels: event.labels
1662
- });
1663
- }
1664
- }),
1665
- durably.on("step:complete", (event) => {
1666
- if (matchesFilter(event.jobName, event.labels)) {
1667
- ctrl.enqueue({
1668
- type: "step:complete",
1669
- runId: event.runId,
1670
- jobName: event.jobName,
1671
- stepName: event.stepName,
1672
- stepIndex: event.stepIndex,
1673
- labels: event.labels
1674
- });
1675
- }
1676
- }),
1677
- durably.on("step:fail", (event) => {
1678
- if (matchesFilter(event.jobName, event.labels)) {
1679
- ctrl.enqueue({
1680
- type: "step:fail",
1681
- runId: event.runId,
1682
- jobName: event.jobName,
1683
- stepName: event.stepName,
1684
- stepIndex: event.stepIndex,
1685
- error: event.error,
1686
- labels: event.labels
1687
- });
1688
- }
1689
- }),
1690
- durably.on("step:cancel", (event) => {
1691
- if (matchesFilter(event.jobName, event.labels)) {
1692
- ctrl.enqueue({
1693
- type: "step:cancel",
1694
- runId: event.runId,
1695
- jobName: event.jobName,
1696
- stepName: event.stepName,
1697
- stepIndex: event.stepIndex,
1698
- labels: event.labels
1699
- });
1700
- }
1701
- }),
1702
- durably.on("log:write", (event) => {
1703
- if (matchesFilter(event.jobName, event.labels)) {
1704
- ctrl.enqueue({
1705
- type: "log:write",
1706
- runId: event.runId,
1707
- jobName: event.jobName,
1708
- labels: event.labels,
1709
- stepName: event.stepName,
1710
- level: event.level,
1711
- message: event.message,
1712
- data: event.data
1713
- });
1714
- }
1715
- })
1716
- ];
1717
- return [...unsubscribes, dispose];
1718
- }
1719
- );
1720
- return createSSEResponse(sseStream);
1721
1823
  }
1722
1824
  };
1723
- return handler;
1724
1825
  }
1725
1826
  export {
1726
1827
  CancelledError,