@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/README.md +2 -2
- package/dist/{index-BjlCb0gP.d.ts → index-fppJjkF-.d.ts} +84 -52
- package/dist/index.d.ts +50 -89
- package/dist/index.js +470 -243
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +1 -1
- package/docs/llms.md +178 -60
- package/package.json +2 -2
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
|
-
|
|
119
|
-
unsubscribeFail();
|
|
124
|
+
for (const unsub of unsubscribes) unsub();
|
|
120
125
|
if (timeoutId) {
|
|
121
126
|
clearTimeout(timeoutId);
|
|
122
127
|
}
|
|
123
128
|
};
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
467
|
-
|
|
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(
|
|
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
|
|
961
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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 (
|
|
1315
|
-
|
|
1515
|
+
if (auth?.onTrigger && ctx !== void 0) {
|
|
1516
|
+
await auth.onTrigger(ctx, body);
|
|
1316
1517
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
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
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
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
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
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
|
-
|
|
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,
|