@coji/durably 0.3.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 +281 -22
- package/dist/index.js +683 -63
- package/dist/index.js.map +1 -1
- package/docs/llms.md +151 -22
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -51,10 +51,7 @@ function createEventEmitter() {
|
|
|
51
51
|
function createJobRegistry() {
|
|
52
52
|
const jobs = /* @__PURE__ */ new Map();
|
|
53
53
|
return {
|
|
54
|
-
|
|
55
|
-
if (jobs.has(job.name)) {
|
|
56
|
-
throw new Error(`Job "${job.name}" is already registered`);
|
|
57
|
-
}
|
|
54
|
+
set(job) {
|
|
58
55
|
jobs.set(job.name, job);
|
|
59
56
|
},
|
|
60
57
|
get(name) {
|
|
@@ -65,26 +62,37 @@ function createJobRegistry() {
|
|
|
65
62
|
}
|
|
66
63
|
};
|
|
67
64
|
}
|
|
68
|
-
function createJobHandle(
|
|
69
|
-
registry.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
65
|
+
function createJobHandle(jobDef, storage, eventEmitter, registry) {
|
|
66
|
+
const existingJob = registry.get(jobDef.name);
|
|
67
|
+
if (existingJob) {
|
|
68
|
+
if (existingJob.jobDef === jobDef) {
|
|
69
|
+
return existingJob.handle;
|
|
70
|
+
}
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Job "${jobDef.name}" is already registered with a different definition`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const inputSchema = jobDef.input;
|
|
76
|
+
const outputSchema = jobDef.output;
|
|
77
|
+
const handle = {
|
|
78
|
+
name: jobDef.name,
|
|
77
79
|
async trigger(input, options) {
|
|
78
|
-
const parseResult =
|
|
80
|
+
const parseResult = inputSchema.safeParse(input);
|
|
79
81
|
if (!parseResult.success) {
|
|
80
82
|
throw new Error(`Invalid input: ${parseResult.error.message}`);
|
|
81
83
|
}
|
|
82
84
|
const run = await storage.createRun({
|
|
83
|
-
jobName:
|
|
85
|
+
jobName: jobDef.name,
|
|
84
86
|
payload: parseResult.data,
|
|
85
87
|
idempotencyKey: options?.idempotencyKey,
|
|
86
88
|
concurrencyKey: options?.concurrencyKey
|
|
87
89
|
});
|
|
90
|
+
eventEmitter.emit({
|
|
91
|
+
type: "run:trigger",
|
|
92
|
+
runId: run.id,
|
|
93
|
+
jobName: jobDef.name,
|
|
94
|
+
payload: parseResult.data
|
|
95
|
+
});
|
|
88
96
|
return run;
|
|
89
97
|
},
|
|
90
98
|
async triggerAndWait(input, options) {
|
|
@@ -101,19 +109,16 @@ function createJobHandle(definition, fn, storage, _eventEmitter, registry) {
|
|
|
101
109
|
clearTimeout(timeoutId);
|
|
102
110
|
}
|
|
103
111
|
};
|
|
104
|
-
const unsubscribeComplete =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
output: event.output
|
|
112
|
-
});
|
|
113
|
-
}
|
|
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
|
+
});
|
|
114
119
|
}
|
|
115
|
-
);
|
|
116
|
-
const unsubscribeFail =
|
|
120
|
+
});
|
|
121
|
+
const unsubscribeFail = eventEmitter.on("run:fail", (event) => {
|
|
117
122
|
if (event.runId === run.id && !resolved) {
|
|
118
123
|
cleanup();
|
|
119
124
|
reject(new Error(event.error));
|
|
@@ -156,7 +161,7 @@ function createJobHandle(definition, fn, storage, _eventEmitter, registry) {
|
|
|
156
161
|
});
|
|
157
162
|
const validated = [];
|
|
158
163
|
for (let i = 0; i < normalized.length; i++) {
|
|
159
|
-
const parseResult =
|
|
164
|
+
const parseResult = inputSchema.safeParse(normalized[i].input);
|
|
160
165
|
if (!parseResult.success) {
|
|
161
166
|
throw new Error(
|
|
162
167
|
`Invalid input at index ${i}: ${parseResult.error.message}`
|
|
@@ -169,17 +174,25 @@ function createJobHandle(definition, fn, storage, _eventEmitter, registry) {
|
|
|
169
174
|
}
|
|
170
175
|
const runs = await storage.batchCreateRuns(
|
|
171
176
|
validated.map((v) => ({
|
|
172
|
-
jobName:
|
|
177
|
+
jobName: jobDef.name,
|
|
173
178
|
payload: v.payload,
|
|
174
179
|
idempotencyKey: v.options?.idempotencyKey,
|
|
175
180
|
concurrencyKey: v.options?.concurrencyKey
|
|
176
181
|
}))
|
|
177
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
|
+
}
|
|
178
191
|
return runs;
|
|
179
192
|
},
|
|
180
193
|
async getRun(id) {
|
|
181
194
|
const run = await storage.getRun(id);
|
|
182
|
-
if (!run || run.jobName !==
|
|
195
|
+
if (!run || run.jobName !== jobDef.name) {
|
|
183
196
|
return null;
|
|
184
197
|
}
|
|
185
198
|
return run;
|
|
@@ -187,11 +200,20 @@ function createJobHandle(definition, fn, storage, _eventEmitter, registry) {
|
|
|
187
200
|
async getRuns(filter) {
|
|
188
201
|
const runs = await storage.getRuns({
|
|
189
202
|
...filter,
|
|
190
|
-
jobName:
|
|
203
|
+
jobName: jobDef.name
|
|
191
204
|
});
|
|
192
205
|
return runs;
|
|
193
206
|
}
|
|
194
207
|
};
|
|
208
|
+
registry.set({
|
|
209
|
+
name: jobDef.name,
|
|
210
|
+
inputSchema,
|
|
211
|
+
outputSchema,
|
|
212
|
+
fn: jobDef.run,
|
|
213
|
+
jobDef,
|
|
214
|
+
handle
|
|
215
|
+
});
|
|
216
|
+
return handle;
|
|
195
217
|
}
|
|
196
218
|
|
|
197
219
|
// src/migrations.ts
|
|
@@ -247,6 +269,7 @@ function rowToRun(row) {
|
|
|
247
269
|
idempotencyKey: row.idempotency_key,
|
|
248
270
|
concurrencyKey: row.concurrency_key,
|
|
249
271
|
currentStepIndex: row.current_step_index,
|
|
272
|
+
stepCount: Number(row.step_count ?? 0),
|
|
250
273
|
progress: row.progress ? JSON.parse(row.progress) : null,
|
|
251
274
|
output: row.output ? JSON.parse(row.output) : null,
|
|
252
275
|
error: row.error,
|
|
@@ -370,18 +393,22 @@ function createKyselyStorage(db) {
|
|
|
370
393
|
await db.deleteFrom("durably_runs").where("id", "=", runId).execute();
|
|
371
394
|
},
|
|
372
395
|
async getRun(runId) {
|
|
373
|
-
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();
|
|
374
399
|
return row ? rowToRun(row) : null;
|
|
375
400
|
},
|
|
376
401
|
async getRuns(filter) {
|
|
377
|
-
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");
|
|
378
405
|
if (filter?.status) {
|
|
379
|
-
query = query.where("status", "=", filter.status);
|
|
406
|
+
query = query.where("durably_runs.status", "=", filter.status);
|
|
380
407
|
}
|
|
381
408
|
if (filter?.jobName) {
|
|
382
|
-
query = query.where("job_name", "=", filter.jobName);
|
|
409
|
+
query = query.where("durably_runs.job_name", "=", filter.jobName);
|
|
383
410
|
}
|
|
384
|
-
query = query.orderBy("created_at", "desc");
|
|
411
|
+
query = query.orderBy("durably_runs.created_at", "desc");
|
|
385
412
|
if (filter?.limit !== void 0) {
|
|
386
413
|
query = query.limit(filter.limit);
|
|
387
414
|
}
|
|
@@ -395,12 +422,18 @@ function createKyselyStorage(db) {
|
|
|
395
422
|
return rows.map(rowToRun);
|
|
396
423
|
},
|
|
397
424
|
async getNextPendingRun(excludeConcurrencyKeys) {
|
|
398
|
-
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);
|
|
399
428
|
if (excludeConcurrencyKeys.length > 0) {
|
|
400
429
|
query = query.where(
|
|
401
430
|
(eb) => eb.or([
|
|
402
|
-
eb("concurrency_key", "is", null),
|
|
403
|
-
eb(
|
|
431
|
+
eb("durably_runs.concurrency_key", "is", null),
|
|
432
|
+
eb(
|
|
433
|
+
"durably_runs.concurrency_key",
|
|
434
|
+
"not in",
|
|
435
|
+
excludeConcurrencyKeys
|
|
436
|
+
)
|
|
404
437
|
])
|
|
405
438
|
);
|
|
406
439
|
}
|
|
@@ -536,8 +569,13 @@ function createStepContext(run, jobName, storage, eventEmitter) {
|
|
|
536
569
|
}
|
|
537
570
|
},
|
|
538
571
|
progress(current, total, message) {
|
|
539
|
-
|
|
540
|
-
|
|
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
|
|
541
579
|
});
|
|
542
580
|
},
|
|
543
581
|
log: {
|
|
@@ -769,35 +807,136 @@ var DEFAULTS = {
|
|
|
769
807
|
heartbeatInterval: 5e3,
|
|
770
808
|
staleThreshold: 3e4
|
|
771
809
|
};
|
|
772
|
-
function
|
|
773
|
-
const
|
|
774
|
-
pollingInterval: options.pollingInterval ?? DEFAULTS.pollingInterval,
|
|
775
|
-
heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
|
|
776
|
-
staleThreshold: options.staleThreshold ?? DEFAULTS.staleThreshold
|
|
777
|
-
};
|
|
778
|
-
const db = new Kysely({ dialect: options.dialect });
|
|
779
|
-
const storage = createKyselyStorage(db);
|
|
780
|
-
const eventEmitter = createEventEmitter();
|
|
781
|
-
const jobRegistry = createJobRegistry();
|
|
782
|
-
const worker = createWorker(config, storage, eventEmitter, jobRegistry);
|
|
783
|
-
let migrating = null;
|
|
784
|
-
let migrated = false;
|
|
810
|
+
function createDurablyInstance(state, jobs) {
|
|
811
|
+
const { db, storage, eventEmitter, jobRegistry, worker } = state;
|
|
785
812
|
const durably = {
|
|
786
813
|
db,
|
|
787
814
|
storage,
|
|
815
|
+
jobs,
|
|
788
816
|
on: eventEmitter.on,
|
|
789
817
|
emit: eventEmitter.emit,
|
|
790
818
|
onError: eventEmitter.onError,
|
|
791
819
|
start: worker.start,
|
|
792
820
|
stop: worker.stop,
|
|
793
|
-
|
|
794
|
-
|
|
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);
|
|
795
836
|
},
|
|
796
837
|
getRun: storage.getRun,
|
|
797
838
|
getRuns: storage.getRuns,
|
|
798
839
|
use(plugin) {
|
|
799
840
|
plugin.install(durably);
|
|
800
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
|
+
},
|
|
801
940
|
async retry(runId) {
|
|
802
941
|
const run = await storage.getRun(runId);
|
|
803
942
|
if (!run) {
|
|
@@ -816,6 +955,11 @@ function createDurably(options) {
|
|
|
816
955
|
status: "pending",
|
|
817
956
|
error: null
|
|
818
957
|
});
|
|
958
|
+
eventEmitter.emit({
|
|
959
|
+
type: "run:retry",
|
|
960
|
+
runId,
|
|
961
|
+
jobName: run.jobName
|
|
962
|
+
});
|
|
819
963
|
},
|
|
820
964
|
async cancel(runId) {
|
|
821
965
|
const run = await storage.getRun(runId);
|
|
@@ -834,6 +978,11 @@ function createDurably(options) {
|
|
|
834
978
|
await storage.updateRun(runId, {
|
|
835
979
|
status: "cancelled"
|
|
836
980
|
});
|
|
981
|
+
eventEmitter.emit({
|
|
982
|
+
type: "run:cancel",
|
|
983
|
+
runId,
|
|
984
|
+
jobName: run.jobName
|
|
985
|
+
});
|
|
837
986
|
},
|
|
838
987
|
async deleteRun(runId) {
|
|
839
988
|
const run = await storage.getRun(runId);
|
|
@@ -849,22 +998,58 @@ function createDurably(options) {
|
|
|
849
998
|
await storage.deleteRun(runId);
|
|
850
999
|
},
|
|
851
1000
|
async migrate() {
|
|
852
|
-
if (migrated) {
|
|
1001
|
+
if (state.migrated) {
|
|
853
1002
|
return;
|
|
854
1003
|
}
|
|
855
|
-
if (migrating) {
|
|
856
|
-
return migrating;
|
|
1004
|
+
if (state.migrating) {
|
|
1005
|
+
return state.migrating;
|
|
857
1006
|
}
|
|
858
|
-
migrating = runMigrations(db).then(() => {
|
|
859
|
-
migrated = true;
|
|
1007
|
+
state.migrating = runMigrations(db).then(() => {
|
|
1008
|
+
state.migrated = true;
|
|
860
1009
|
}).finally(() => {
|
|
861
|
-
migrating = null;
|
|
1010
|
+
state.migrating = null;
|
|
862
1011
|
});
|
|
863
|
-
return migrating;
|
|
1012
|
+
return state.migrating;
|
|
1013
|
+
},
|
|
1014
|
+
async init() {
|
|
1015
|
+
await this.migrate();
|
|
1016
|
+
this.start();
|
|
864
1017
|
}
|
|
865
1018
|
};
|
|
866
1019
|
return durably;
|
|
867
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
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// src/define-job.ts
|
|
1045
|
+
function defineJob(config) {
|
|
1046
|
+
return {
|
|
1047
|
+
name: config.name,
|
|
1048
|
+
input: config.input,
|
|
1049
|
+
output: config.output,
|
|
1050
|
+
run: config.run
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
868
1053
|
|
|
869
1054
|
// src/plugins/log-persistence.ts
|
|
870
1055
|
function withLogPersistence() {
|
|
@@ -883,9 +1068,444 @@ function withLogPersistence() {
|
|
|
883
1068
|
}
|
|
884
1069
|
};
|
|
885
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
|
+
}
|
|
886
1504
|
export {
|
|
887
1505
|
CancelledError,
|
|
888
1506
|
createDurably,
|
|
1507
|
+
createDurablyHandler,
|
|
1508
|
+
defineJob,
|
|
889
1509
|
withLogPersistence
|
|
890
1510
|
};
|
|
891
1511
|
//# sourceMappingURL=index.js.map
|