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