@coji/durably 0.9.0 → 0.11.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
  "=",
@@ -933,12 +985,16 @@ function createDurablyInstance(state, jobs) {
933
985
  jobDef,
934
986
  storage,
935
987
  eventEmitter,
936
- jobRegistry
988
+ jobRegistry,
989
+ state.labelsSchema
937
990
  );
938
991
  newHandles[key] = handle;
939
992
  }
940
993
  const mergedJobs = { ...jobs, ...newHandles };
941
- return createDurablyInstance(state, mergedJobs);
994
+ return createDurablyInstance(
995
+ state,
996
+ mergedJobs
997
+ );
942
998
  },
943
999
  getRun: storage.getRun,
944
1000
  getRuns: storage.getRuns,
@@ -955,93 +1011,35 @@ function createDurablyInstance(state, jobs) {
955
1011
  subscribe(runId) {
956
1012
  let closed = false;
957
1013
  let cleanup = null;
1014
+ const closeEvents = /* @__PURE__ */ new Set(["run:complete", "run:delete"]);
1015
+ const subscribedEvents = [
1016
+ "run:start",
1017
+ "run:complete",
1018
+ "run:fail",
1019
+ "run:cancel",
1020
+ "run:delete",
1021
+ "run:retry",
1022
+ "run:progress",
1023
+ "step:start",
1024
+ "step:complete",
1025
+ "step:fail",
1026
+ "log:write"
1027
+ ];
958
1028
  return new ReadableStream({
959
1029
  start: (controller) => {
960
- const unsubscribeStart = eventEmitter.on("run:start", (event) => {
961
- if (!closed && event.runId === runId) {
1030
+ const unsubscribes = subscribedEvents.map(
1031
+ (type) => eventEmitter.on(type, (event) => {
1032
+ if (closed || event.runId !== runId) return;
962
1033
  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);
1034
+ if (closeEvents.has(type)) {
970
1035
  closed = true;
971
1036
  cleanup?.();
972
1037
  controller.close();
973
1038
  }
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
- }
1039
+ })
1014
1040
  );
1015
- const unsubscribeStepComplete = eventEmitter.on(
1016
- "step:complete",
1017
- (event) => {
1018
- if (!closed && event.runId === runId) {
1019
- controller.enqueue(event);
1020
- }
1021
- }
1022
- );
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
1041
  cleanup = () => {
1034
- unsubscribeStart();
1035
- unsubscribeComplete();
1036
- unsubscribeFail();
1037
- unsubscribeCancel();
1038
- unsubscribeDelete();
1039
- unsubscribeRetry();
1040
- unsubscribeProgress();
1041
- unsubscribeStepStart();
1042
- unsubscribeStepComplete();
1043
- unsubscribeStepFail();
1044
- unsubscribeLog();
1042
+ for (const unsub of unsubscribes) unsub();
1045
1043
  };
1046
1044
  },
1047
1045
  cancel: () => {
@@ -1158,10 +1156,18 @@ function createDurably(options) {
1158
1156
  eventEmitter,
1159
1157
  jobRegistry,
1160
1158
  worker,
1159
+ labelsSchema: options.labels,
1161
1160
  migrating: null,
1162
1161
  migrated: false
1163
1162
  };
1164
- return createDurablyInstance(state, {});
1163
+ const instance = createDurablyInstance(
1164
+ state,
1165
+ {}
1166
+ );
1167
+ if (options.jobs) {
1168
+ return instance.register(options.jobs);
1169
+ }
1170
+ return instance;
1165
1171
  }
1166
1172
 
1167
1173
  // src/define-job.ts
@@ -1243,6 +1249,48 @@ function createSSEStreamFromReader(reader) {
1243
1249
  }
1244
1250
  });
1245
1251
  }
1252
+ function createThrottledSSEStreamFromReader(reader, throttleMs) {
1253
+ if (throttleMs <= 0) {
1254
+ return createSSEStreamFromReader(reader);
1255
+ }
1256
+ const encoder = createSSEEncoder();
1257
+ let closed = false;
1258
+ let throttle = null;
1259
+ return new ReadableStream({
1260
+ async start(controller) {
1261
+ const innerCtrl = {
1262
+ enqueue: (data) => controller.enqueue(encodeSSE(encoder, data)),
1263
+ close: () => {
1264
+ closed = true;
1265
+ controller.close();
1266
+ },
1267
+ get closed() {
1268
+ return closed;
1269
+ }
1270
+ };
1271
+ throttle = createThrottledSSEController(innerCtrl, throttleMs);
1272
+ try {
1273
+ while (true) {
1274
+ const { done, value } = await reader.read();
1275
+ if (done) {
1276
+ throttle.controller.close();
1277
+ break;
1278
+ }
1279
+ throttle.controller.enqueue(value);
1280
+ }
1281
+ } catch (error) {
1282
+ throttle.dispose();
1283
+ reader.releaseLock();
1284
+ controller.error(error);
1285
+ }
1286
+ },
1287
+ cancel() {
1288
+ closed = true;
1289
+ throttle?.dispose();
1290
+ reader.releaseLock();
1291
+ }
1292
+ });
1293
+ }
1246
1294
  function createSSEStreamFromSubscriptions(setup) {
1247
1295
  const encoder = createSSEEncoder();
1248
1296
  let closed = false;
@@ -1273,8 +1321,99 @@ function createSSEStreamFromSubscriptions(setup) {
1273
1321
  }
1274
1322
  });
1275
1323
  }
1324
+ var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
1325
+ "run:complete",
1326
+ "run:fail",
1327
+ "run:cancel",
1328
+ "run:delete"
1329
+ ]);
1330
+ function createThrottledSSEController(inner, throttleMs) {
1331
+ if (throttleMs <= 0) {
1332
+ return { controller: inner, dispose: () => {
1333
+ } };
1334
+ }
1335
+ const pending = /* @__PURE__ */ new Map();
1336
+ const lastSent = /* @__PURE__ */ new Map();
1337
+ const controller = {
1338
+ enqueue(data) {
1339
+ if (inner.closed) return;
1340
+ const event = typeof data === "object" && data !== null ? data : null;
1341
+ if (event?.runId && TERMINAL_EVENT_TYPES.has(event.type ?? "")) {
1342
+ lastSent.delete(event.runId);
1343
+ const entry = pending.get(event.runId);
1344
+ if (entry) {
1345
+ clearTimeout(entry.timer);
1346
+ if (!inner.closed) inner.enqueue(entry.data);
1347
+ pending.delete(event.runId);
1348
+ }
1349
+ }
1350
+ if (event?.type !== "run:progress" || !event?.runId) {
1351
+ inner.enqueue(data);
1352
+ return;
1353
+ }
1354
+ const runId = event.runId;
1355
+ const now = Date.now();
1356
+ const last = lastSent.get(runId) ?? 0;
1357
+ if (now - last >= throttleMs) {
1358
+ lastSent.set(runId, now);
1359
+ const entry = pending.get(runId);
1360
+ if (entry) {
1361
+ clearTimeout(entry.timer);
1362
+ pending.delete(runId);
1363
+ }
1364
+ inner.enqueue(data);
1365
+ return;
1366
+ }
1367
+ const existing = pending.get(runId);
1368
+ if (existing) {
1369
+ clearTimeout(existing.timer);
1370
+ }
1371
+ const delay = Math.max(0, throttleMs - (now - last));
1372
+ const timer = setTimeout(() => {
1373
+ const current = pending.get(runId);
1374
+ if (!current || current.timer !== timer) return;
1375
+ pending.delete(runId);
1376
+ if (!inner.closed) {
1377
+ lastSent.set(runId, Date.now());
1378
+ inner.enqueue(current.data);
1379
+ }
1380
+ }, delay);
1381
+ pending.set(runId, { data, timer });
1382
+ },
1383
+ close() {
1384
+ for (const [, entry] of pending) {
1385
+ clearTimeout(entry.timer);
1386
+ if (!inner.closed) {
1387
+ inner.enqueue(entry.data);
1388
+ }
1389
+ }
1390
+ pending.clear();
1391
+ lastSent.clear();
1392
+ inner.close();
1393
+ },
1394
+ get closed() {
1395
+ return inner.closed;
1396
+ }
1397
+ };
1398
+ const dispose = () => {
1399
+ for (const [, entry] of pending) {
1400
+ clearTimeout(entry.timer);
1401
+ }
1402
+ pending.clear();
1403
+ lastSent.clear();
1404
+ };
1405
+ return { controller, dispose };
1406
+ }
1276
1407
 
1277
1408
  // src/server.ts
1409
+ var VALID_STATUSES = [
1410
+ "pending",
1411
+ "running",
1412
+ "completed",
1413
+ "failed",
1414
+ "cancelled"
1415
+ ];
1416
+ var VALID_STATUSES_SET = new Set(VALID_STATUSES);
1278
1417
  function parseLabelsFromParams(searchParams) {
1279
1418
  const labels = {};
1280
1419
  for (const [key, value] of searchParams.entries()) {
@@ -1284,6 +1423,51 @@ function parseLabelsFromParams(searchParams) {
1284
1423
  }
1285
1424
  return Object.keys(labels).length > 0 ? labels : void 0;
1286
1425
  }
1426
+ function parseRunFilter(url) {
1427
+ const jobNames = url.searchParams.getAll("jobName");
1428
+ const statusParam = url.searchParams.get("status");
1429
+ const limitParam = url.searchParams.get("limit");
1430
+ const offsetParam = url.searchParams.get("offset");
1431
+ const labels = parseLabelsFromParams(url.searchParams);
1432
+ if (statusParam && !VALID_STATUSES_SET.has(statusParam)) {
1433
+ return errorResponse(
1434
+ `Invalid status: ${statusParam}. Must be one of: ${VALID_STATUSES.join(", ")}`,
1435
+ 400
1436
+ );
1437
+ }
1438
+ let limit;
1439
+ if (limitParam) {
1440
+ limit = Number.parseInt(limitParam, 10);
1441
+ if (Number.isNaN(limit) || limit < 0) {
1442
+ return errorResponse("Invalid limit: must be a non-negative integer", 400);
1443
+ }
1444
+ }
1445
+ let offset;
1446
+ if (offsetParam) {
1447
+ offset = Number.parseInt(offsetParam, 10);
1448
+ if (Number.isNaN(offset) || offset < 0) {
1449
+ return errorResponse(
1450
+ "Invalid offset: must be a non-negative integer",
1451
+ 400
1452
+ );
1453
+ }
1454
+ }
1455
+ return {
1456
+ jobName: jobNames.length > 0 ? jobNames : void 0,
1457
+ status: statusParam,
1458
+ labels,
1459
+ limit,
1460
+ offset
1461
+ };
1462
+ }
1463
+ function parseRunsSubscribeFilter(url) {
1464
+ const jobNames = url.searchParams.getAll("jobName");
1465
+ const labels = parseLabelsFromParams(url.searchParams);
1466
+ return {
1467
+ jobName: jobNames.length > 0 ? jobNames : void 0,
1468
+ labels
1469
+ };
1470
+ }
1287
1471
  function matchesLabels(eventLabels, filterLabels) {
1288
1472
  for (const [key, value] of Object.entries(filterLabels)) {
1289
1473
  if (eventLabels[key] !== value) return false;
@@ -1291,152 +1475,159 @@ function matchesLabels(eventLabels, filterLabels) {
1291
1475
  return true;
1292
1476
  }
1293
1477
  function createDurablyHandler(durably, options) {
1294
- const handler = {
1295
- async handle(request, basePath) {
1296
- if (options?.onRequest) {
1297
- await options.onRequest();
1298
- }
1299
- const url = new URL(request.url);
1300
- const path = url.pathname.replace(basePath, "");
1301
- const method = request.method;
1302
- if (method === "GET") {
1303
- if (path === "/subscribe") return handler.subscribe(request);
1304
- if (path === "/runs") return handler.runs(request);
1305
- if (path === "/run") return handler.run(request);
1306
- if (path === "/steps") return handler.steps(request);
1307
- if (path === "/runs/subscribe") return handler.runsSubscribe(request);
1478
+ const throttleMs = options?.sseThrottleMs ?? 100;
1479
+ const auth = options?.auth;
1480
+ if (auth && !auth.authenticate) {
1481
+ throw new Error(
1482
+ "createDurablyHandler: auth.authenticate is required when auth is provided"
1483
+ );
1484
+ }
1485
+ async function withErrorHandling(fn) {
1486
+ try {
1487
+ return await fn();
1488
+ } catch (error) {
1489
+ if (error instanceof Response) throw error;
1490
+ return errorResponse(getErrorMessage(error), 500);
1491
+ }
1492
+ }
1493
+ async function requireRunAccess(url, ctx, operation) {
1494
+ const runId = getRequiredQueryParam(url, "runId");
1495
+ if (runId instanceof Response) return runId;
1496
+ const run = await durably.getRun(runId);
1497
+ if (!run) return errorResponse("Run not found", 404);
1498
+ if (auth?.onRunAccess && ctx !== void 0) {
1499
+ await auth.onRunAccess(ctx, run, {
1500
+ operation
1501
+ });
1502
+ }
1503
+ return { run, runId };
1504
+ }
1505
+ async function handleTrigger(request, ctx) {
1506
+ return withErrorHandling(async () => {
1507
+ const body = await request.json();
1508
+ if (!body.jobName) {
1509
+ return errorResponse("jobName is required", 400);
1308
1510
  }
1309
- if (method === "POST") {
1310
- if (path === "/trigger") return handler.trigger(request);
1311
- if (path === "/retry") return handler.retry(request);
1312
- if (path === "/cancel") return handler.cancel(request);
1511
+ const job = durably.getJob(body.jobName);
1512
+ if (!job) {
1513
+ return errorResponse(`Job not found: ${body.jobName}`, 404);
1313
1514
  }
1314
- if (method === "DELETE") {
1315
- if (path === "/run") return handler.delete(request);
1515
+ if (auth?.onTrigger && ctx !== void 0) {
1516
+ await auth.onTrigger(ctx, body);
1316
1517
  }
1317
- return new Response("Not Found", { status: 404 });
1318
- },
1319
- async trigger(request) {
1320
- try {
1321
- const body = await request.json();
1322
- if (!body.jobName) {
1323
- return errorResponse("jobName is required", 400);
1324
- }
1325
- const job = durably.getJob(body.jobName);
1326
- if (!job) {
1327
- return errorResponse(`Job not found: ${body.jobName}`, 404);
1328
- }
1329
- const run = await job.trigger(body.input ?? {}, {
1518
+ const run = await job.trigger(
1519
+ body.input ?? {},
1520
+ {
1330
1521
  idempotencyKey: body.idempotencyKey,
1331
1522
  concurrencyKey: body.concurrencyKey,
1332
1523
  labels: body.labels
1333
- });
1334
- const response = { runId: run.id };
1335
- return jsonResponse(response);
1336
- } catch (error) {
1337
- return errorResponse(getErrorMessage(error), 500);
1338
- }
1339
- },
1340
- subscribe(request) {
1341
- const url = new URL(request.url);
1342
- const runId = getRequiredQueryParam(url, "runId");
1343
- if (runId instanceof Response) return runId;
1344
- const stream = durably.subscribe(runId);
1345
- const sseStream = createSSEStreamFromReader(
1346
- stream.getReader()
1524
+ }
1347
1525
  );
1348
- return createSSEResponse(sseStream);
1349
- },
1350
- async runs(request) {
1351
- try {
1352
- const url = new URL(request.url);
1353
- const jobName = url.searchParams.get("jobName") ?? void 0;
1354
- const status = url.searchParams.get("status");
1355
- const limit = url.searchParams.get("limit");
1356
- const offset = url.searchParams.get("offset");
1357
- const labels = parseLabelsFromParams(url.searchParams);
1358
- const runs = await durably.getRuns({
1359
- jobName,
1360
- status,
1361
- labels,
1362
- limit: limit ? Number.parseInt(limit, 10) : void 0,
1363
- offset: offset ? Number.parseInt(offset, 10) : void 0
1364
- });
1365
- return jsonResponse(runs.map(toClientRun));
1366
- } catch (error) {
1367
- return errorResponse(getErrorMessage(error), 500);
1526
+ const response = { runId: run.id };
1527
+ return jsonResponse(response);
1528
+ });
1529
+ }
1530
+ async function handleSubscribe(url, ctx) {
1531
+ const result = await requireRunAccess(url, ctx, "subscribe");
1532
+ if (result instanceof Response) return result;
1533
+ const stream = durably.subscribe(result.runId);
1534
+ const sseStream = createThrottledSSEStreamFromReader(
1535
+ stream.getReader(),
1536
+ throttleMs
1537
+ );
1538
+ return createSSEResponse(sseStream);
1539
+ }
1540
+ async function handleRuns(url, ctx) {
1541
+ return withErrorHandling(async () => {
1542
+ const filterOrError = parseRunFilter(url);
1543
+ if (filterOrError instanceof Response) return filterOrError;
1544
+ let filter = filterOrError;
1545
+ if (auth?.scopeRuns && ctx !== void 0) {
1546
+ filter = await auth.scopeRuns(ctx, filter);
1368
1547
  }
1369
- },
1370
- async run(request) {
1371
- try {
1372
- const url = new URL(request.url);
1373
- const runId = getRequiredQueryParam(url, "runId");
1374
- if (runId instanceof Response) return runId;
1375
- const run = await durably.getRun(runId);
1376
- if (!run) {
1377
- return errorResponse("Run not found", 404);
1548
+ const runs = await durably.getRuns(filter);
1549
+ return jsonResponse(runs.map(toClientRun));
1550
+ });
1551
+ }
1552
+ async function handleRun(url, ctx) {
1553
+ return withErrorHandling(async () => {
1554
+ const result = await requireRunAccess(url, ctx, "read");
1555
+ if (result instanceof Response) return result;
1556
+ return jsonResponse(toClientRun(result.run));
1557
+ });
1558
+ }
1559
+ async function handleSteps(url, ctx) {
1560
+ return withErrorHandling(async () => {
1561
+ const result = await requireRunAccess(url, ctx, "steps");
1562
+ if (result instanceof Response) return result;
1563
+ const steps = await durably.storage.getSteps(result.runId);
1564
+ return jsonResponse(steps);
1565
+ });
1566
+ }
1567
+ async function handleRetry(url, ctx) {
1568
+ return withErrorHandling(async () => {
1569
+ const result = await requireRunAccess(url, ctx, "retry");
1570
+ if (result instanceof Response) return result;
1571
+ await durably.retry(result.runId);
1572
+ return successResponse();
1573
+ });
1574
+ }
1575
+ async function handleCancel(url, ctx) {
1576
+ return withErrorHandling(async () => {
1577
+ const result = await requireRunAccess(url, ctx, "cancel");
1578
+ if (result instanceof Response) return result;
1579
+ await durably.cancel(result.runId);
1580
+ return successResponse();
1581
+ });
1582
+ }
1583
+ async function handleDelete(url, ctx) {
1584
+ return withErrorHandling(async () => {
1585
+ const result = await requireRunAccess(url, ctx, "delete");
1586
+ if (result instanceof Response) return result;
1587
+ await durably.deleteRun(result.runId);
1588
+ return successResponse();
1589
+ });
1590
+ }
1591
+ async function handleRunsSubscribe(url, ctx) {
1592
+ let filter;
1593
+ if (ctx !== void 0 && auth?.scopeRunsSubscribe) {
1594
+ const parsed = parseRunsSubscribeFilter(
1595
+ url
1596
+ );
1597
+ filter = await auth.scopeRunsSubscribe(ctx, parsed);
1598
+ } else if (ctx !== void 0 && auth?.scopeRuns) {
1599
+ const parsed = parseRunsSubscribeFilter(
1600
+ url
1601
+ );
1602
+ const scoped = await auth.scopeRuns(
1603
+ ctx,
1604
+ {
1605
+ ...parsed
1378
1606
  }
1379
- return jsonResponse(toClientRun(run));
1380
- } catch (error) {
1381
- return errorResponse(getErrorMessage(error), 500);
1382
- }
1383
- },
1384
- async retry(request) {
1385
- try {
1386
- const url = new URL(request.url);
1387
- const runId = getRequiredQueryParam(url, "runId");
1388
- if (runId instanceof Response) return runId;
1389
- await durably.retry(runId);
1390
- return successResponse();
1391
- } catch (error) {
1392
- return errorResponse(getErrorMessage(error), 500);
1393
- }
1394
- },
1395
- async cancel(request) {
1396
- try {
1397
- const url = new URL(request.url);
1398
- const runId = getRequiredQueryParam(url, "runId");
1399
- if (runId instanceof Response) return runId;
1400
- await durably.cancel(runId);
1401
- return successResponse();
1402
- } catch (error) {
1403
- return errorResponse(getErrorMessage(error), 500);
1404
- }
1405
- },
1406
- async delete(request) {
1407
- try {
1408
- const url = new URL(request.url);
1409
- const runId = getRequiredQueryParam(url, "runId");
1410
- if (runId instanceof Response) return runId;
1411
- await durably.deleteRun(runId);
1412
- return successResponse();
1413
- } catch (error) {
1414
- return errorResponse(getErrorMessage(error), 500);
1415
- }
1416
- },
1417
- async steps(request) {
1418
- try {
1419
- const url = new URL(request.url);
1420
- const runId = getRequiredQueryParam(url, "runId");
1421
- if (runId instanceof Response) return runId;
1422
- const steps = await durably.storage.getSteps(runId);
1423
- return jsonResponse(steps);
1424
- } catch (error) {
1425
- return errorResponse(getErrorMessage(error), 500);
1426
- }
1427
- },
1428
- runsSubscribe(request) {
1429
- const url = new URL(request.url);
1430
- const jobNameFilter = url.searchParams.get("jobName");
1431
- const labelsFilter = parseLabelsFromParams(url.searchParams);
1432
- const matchesFilter = (jobName, labels) => {
1433
- if (jobNameFilter && jobName !== jobNameFilter) return false;
1434
- if (labelsFilter && (!labels || !matchesLabels(labels, labelsFilter)))
1435
- return false;
1436
- return true;
1437
- };
1438
- const sseStream = createSSEStreamFromSubscriptions(
1439
- (ctrl) => [
1607
+ );
1608
+ filter = { jobName: scoped.jobName, labels: scoped.labels };
1609
+ } else {
1610
+ filter = parseRunsSubscribeFilter(url);
1611
+ }
1612
+ return createRunsSSEStream(filter);
1613
+ }
1614
+ function createRunsSSEStream(filter) {
1615
+ const jobNameFilter = Array.isArray(filter.jobName) ? filter.jobName : filter.jobName ? [filter.jobName] : [];
1616
+ const labelsFilter = filter.labels;
1617
+ const matchesFilter = (jobName, labels) => {
1618
+ if (jobNameFilter.length > 0 && !jobNameFilter.includes(jobName))
1619
+ return false;
1620
+ if (labelsFilter && (!labels || !matchesLabels(labels, labelsFilter)))
1621
+ return false;
1622
+ return true;
1623
+ };
1624
+ const sseStream = createSSEStreamFromSubscriptions(
1625
+ (innerCtrl) => {
1626
+ const { controller: ctrl, dispose } = createThrottledSSEController(
1627
+ innerCtrl,
1628
+ throttleMs
1629
+ );
1630
+ const unsubscribes = [
1440
1631
  durably.on("run:trigger", (event) => {
1441
1632
  if (matchesFilter(event.jobName, event.labels)) {
1442
1633
  ctrl.enqueue({
@@ -1581,12 +1772,48 @@ function createDurablyHandler(durably, options) {
1581
1772
  });
1582
1773
  }
1583
1774
  })
1584
- ]
1585
- );
1586
- return createSSEResponse(sseStream);
1775
+ ];
1776
+ return [...unsubscribes, dispose];
1777
+ }
1778
+ );
1779
+ return createSSEResponse(sseStream);
1780
+ }
1781
+ return {
1782
+ async handle(request, basePath) {
1783
+ try {
1784
+ let ctx;
1785
+ if (auth?.authenticate) {
1786
+ ctx = await auth.authenticate(request);
1787
+ }
1788
+ if (options?.onRequest) {
1789
+ await options.onRequest();
1790
+ }
1791
+ const url = new URL(request.url);
1792
+ const path = url.pathname.replace(basePath, "");
1793
+ const method = request.method;
1794
+ if (method === "GET") {
1795
+ if (path === "/subscribe") return await handleSubscribe(url, ctx);
1796
+ if (path === "/runs") return await handleRuns(url, ctx);
1797
+ if (path === "/run") return await handleRun(url, ctx);
1798
+ if (path === "/steps") return await handleSteps(url, ctx);
1799
+ if (path === "/runs/subscribe")
1800
+ return await handleRunsSubscribe(url, ctx);
1801
+ }
1802
+ if (method === "POST") {
1803
+ if (path === "/trigger") return await handleTrigger(request, ctx);
1804
+ if (path === "/retry") return await handleRetry(url, ctx);
1805
+ if (path === "/cancel") return await handleCancel(url, ctx);
1806
+ }
1807
+ if (method === "DELETE") {
1808
+ if (path === "/run") return await handleDelete(url, ctx);
1809
+ }
1810
+ return new Response("Not Found", { status: 404 });
1811
+ } catch (error) {
1812
+ if (error instanceof Response) return error;
1813
+ return errorResponse(getErrorMessage(error), 500);
1814
+ }
1587
1815
  }
1588
1816
  };
1589
- return handler;
1590
1817
  }
1591
1818
  export {
1592
1819
  CancelledError,