@coji/durably 0.4.0 → 0.6.1
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/chunk-UCUP6NMJ.js +22 -0
- package/dist/chunk-UCUP6NMJ.js.map +1 -0
- package/dist/index-CHQw-b_O.d.ts +632 -0
- package/dist/index.d.ts +98 -499
- package/dist/index.js +591 -75
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +3 -8
- package/dist/plugins/index.js +3 -6
- package/dist/plugins/index.js.map +1 -1
- package/docs/llms.md +129 -17
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
withLogPersistence
|
|
3
|
+
} from "./chunk-UCUP6NMJ.js";
|
|
4
|
+
|
|
1
5
|
// src/durably.ts
|
|
2
6
|
import { Kysely } from "kysely";
|
|
3
7
|
|
|
@@ -48,6 +52,15 @@ function createEventEmitter() {
|
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
// src/job.ts
|
|
55
|
+
import { prettifyError } from "zod";
|
|
56
|
+
function validateJobInputOrThrow(schema, input, context) {
|
|
57
|
+
const result = schema.safeParse(input);
|
|
58
|
+
if (!result.success) {
|
|
59
|
+
const prefix = context ? `${context}: ` : "";
|
|
60
|
+
throw new Error(`${prefix}Invalid input: ${prettifyError(result.error)}`);
|
|
61
|
+
}
|
|
62
|
+
return result.data;
|
|
63
|
+
}
|
|
51
64
|
function createJobRegistry() {
|
|
52
65
|
const jobs = /* @__PURE__ */ new Map();
|
|
53
66
|
return {
|
|
@@ -62,7 +75,7 @@ function createJobRegistry() {
|
|
|
62
75
|
}
|
|
63
76
|
};
|
|
64
77
|
}
|
|
65
|
-
function createJobHandle(jobDef, storage,
|
|
78
|
+
function createJobHandle(jobDef, storage, eventEmitter, registry) {
|
|
66
79
|
const existingJob = registry.get(jobDef.name);
|
|
67
80
|
if (existingJob) {
|
|
68
81
|
if (existingJob.jobDef === jobDef) {
|
|
@@ -77,16 +90,19 @@ function createJobHandle(jobDef, storage, _eventEmitter, registry) {
|
|
|
77
90
|
const handle = {
|
|
78
91
|
name: jobDef.name,
|
|
79
92
|
async trigger(input, options) {
|
|
80
|
-
const
|
|
81
|
-
if (!parseResult.success) {
|
|
82
|
-
throw new Error(`Invalid input: ${parseResult.error.message}`);
|
|
83
|
-
}
|
|
93
|
+
const validatedInput = validateJobInputOrThrow(inputSchema, input);
|
|
84
94
|
const run = await storage.createRun({
|
|
85
95
|
jobName: jobDef.name,
|
|
86
|
-
payload:
|
|
96
|
+
payload: validatedInput,
|
|
87
97
|
idempotencyKey: options?.idempotencyKey,
|
|
88
98
|
concurrencyKey: options?.concurrencyKey
|
|
89
99
|
});
|
|
100
|
+
eventEmitter.emit({
|
|
101
|
+
type: "run:trigger",
|
|
102
|
+
runId: run.id,
|
|
103
|
+
jobName: jobDef.name,
|
|
104
|
+
payload: validatedInput
|
|
105
|
+
});
|
|
90
106
|
return run;
|
|
91
107
|
},
|
|
92
108
|
async triggerAndWait(input, options) {
|
|
@@ -103,19 +119,16 @@ function createJobHandle(jobDef, storage, _eventEmitter, registry) {
|
|
|
103
119
|
clearTimeout(timeoutId);
|
|
104
120
|
}
|
|
105
121
|
};
|
|
106
|
-
const unsubscribeComplete =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
output: event.output
|
|
114
|
-
});
|
|
115
|
-
}
|
|
122
|
+
const unsubscribeComplete = eventEmitter.on("run:complete", (event) => {
|
|
123
|
+
if (event.runId === run.id && !resolved) {
|
|
124
|
+
cleanup();
|
|
125
|
+
resolve({
|
|
126
|
+
id: run.id,
|
|
127
|
+
output: event.output
|
|
128
|
+
});
|
|
116
129
|
}
|
|
117
|
-
);
|
|
118
|
-
const unsubscribeFail =
|
|
130
|
+
});
|
|
131
|
+
const unsubscribeFail = eventEmitter.on("run:fail", (event) => {
|
|
119
132
|
if (event.runId === run.id && !resolved) {
|
|
120
133
|
cleanup();
|
|
121
134
|
reject(new Error(event.error));
|
|
@@ -158,14 +171,13 @@ function createJobHandle(jobDef, storage, _eventEmitter, registry) {
|
|
|
158
171
|
});
|
|
159
172
|
const validated = [];
|
|
160
173
|
for (let i = 0; i < normalized.length; i++) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
174
|
+
const validatedInput = validateJobInputOrThrow(
|
|
175
|
+
inputSchema,
|
|
176
|
+
normalized[i].input,
|
|
177
|
+
`at index ${i}`
|
|
178
|
+
);
|
|
167
179
|
validated.push({
|
|
168
|
-
payload:
|
|
180
|
+
payload: validatedInput,
|
|
169
181
|
options: normalized[i].options
|
|
170
182
|
});
|
|
171
183
|
}
|
|
@@ -177,6 +189,14 @@ function createJobHandle(jobDef, storage, _eventEmitter, registry) {
|
|
|
177
189
|
concurrencyKey: v.options?.concurrencyKey
|
|
178
190
|
}))
|
|
179
191
|
);
|
|
192
|
+
for (let i = 0; i < runs.length; i++) {
|
|
193
|
+
eventEmitter.emit({
|
|
194
|
+
type: "run:trigger",
|
|
195
|
+
runId: runs[i].id,
|
|
196
|
+
jobName: jobDef.name,
|
|
197
|
+
payload: validated[i].payload
|
|
198
|
+
});
|
|
199
|
+
}
|
|
180
200
|
return runs;
|
|
181
201
|
},
|
|
182
202
|
async getRun(id) {
|
|
@@ -258,6 +278,7 @@ function rowToRun(row) {
|
|
|
258
278
|
idempotencyKey: row.idempotency_key,
|
|
259
279
|
concurrencyKey: row.concurrency_key,
|
|
260
280
|
currentStepIndex: row.current_step_index,
|
|
281
|
+
stepCount: Number(row.step_count ?? 0),
|
|
261
282
|
progress: row.progress ? JSON.parse(row.progress) : null,
|
|
262
283
|
output: row.output ? JSON.parse(row.output) : null,
|
|
263
284
|
error: row.error,
|
|
@@ -381,18 +402,22 @@ function createKyselyStorage(db) {
|
|
|
381
402
|
await db.deleteFrom("durably_runs").where("id", "=", runId).execute();
|
|
382
403
|
},
|
|
383
404
|
async getRun(runId) {
|
|
384
|
-
const row = await db.selectFrom("durably_runs").
|
|
405
|
+
const row = await db.selectFrom("durably_runs").leftJoin("durably_steps", "durably_runs.id", "durably_steps.run_id").selectAll("durably_runs").select(
|
|
406
|
+
(eb) => eb.fn.count("durably_steps.id").as("step_count")
|
|
407
|
+
).where("durably_runs.id", "=", runId).groupBy("durably_runs.id").executeTakeFirst();
|
|
385
408
|
return row ? rowToRun(row) : null;
|
|
386
409
|
},
|
|
387
410
|
async getRuns(filter) {
|
|
388
|
-
let query = db.selectFrom("durably_runs").selectAll()
|
|
411
|
+
let query = db.selectFrom("durably_runs").leftJoin("durably_steps", "durably_runs.id", "durably_steps.run_id").selectAll("durably_runs").select(
|
|
412
|
+
(eb) => eb.fn.count("durably_steps.id").as("step_count")
|
|
413
|
+
).groupBy("durably_runs.id");
|
|
389
414
|
if (filter?.status) {
|
|
390
|
-
query = query.where("status", "=", filter.status);
|
|
415
|
+
query = query.where("durably_runs.status", "=", filter.status);
|
|
391
416
|
}
|
|
392
417
|
if (filter?.jobName) {
|
|
393
|
-
query = query.where("job_name", "=", filter.jobName);
|
|
418
|
+
query = query.where("durably_runs.job_name", "=", filter.jobName);
|
|
394
419
|
}
|
|
395
|
-
query = query.orderBy("created_at", "desc");
|
|
420
|
+
query = query.orderBy("durably_runs.created_at", "desc");
|
|
396
421
|
if (filter?.limit !== void 0) {
|
|
397
422
|
query = query.limit(filter.limit);
|
|
398
423
|
}
|
|
@@ -406,12 +431,18 @@ function createKyselyStorage(db) {
|
|
|
406
431
|
return rows.map(rowToRun);
|
|
407
432
|
},
|
|
408
433
|
async getNextPendingRun(excludeConcurrencyKeys) {
|
|
409
|
-
let query = db.selectFrom("durably_runs").
|
|
434
|
+
let query = db.selectFrom("durably_runs").leftJoin("durably_steps", "durably_runs.id", "durably_steps.run_id").selectAll("durably_runs").select(
|
|
435
|
+
(eb) => eb.fn.count("durably_steps.id").as("step_count")
|
|
436
|
+
).where("durably_runs.status", "=", "pending").groupBy("durably_runs.id").orderBy("durably_runs.created_at", "asc").orderBy("durably_runs.id", "asc").limit(1);
|
|
410
437
|
if (excludeConcurrencyKeys.length > 0) {
|
|
411
438
|
query = query.where(
|
|
412
439
|
(eb) => eb.or([
|
|
413
|
-
eb("concurrency_key", "is", null),
|
|
414
|
-
eb(
|
|
440
|
+
eb("durably_runs.concurrency_key", "is", null),
|
|
441
|
+
eb(
|
|
442
|
+
"durably_runs.concurrency_key",
|
|
443
|
+
"not in",
|
|
444
|
+
excludeConcurrencyKeys
|
|
445
|
+
)
|
|
415
446
|
])
|
|
416
447
|
);
|
|
417
448
|
}
|
|
@@ -465,6 +496,9 @@ function createKyselyStorage(db) {
|
|
|
465
496
|
};
|
|
466
497
|
}
|
|
467
498
|
|
|
499
|
+
// src/worker.ts
|
|
500
|
+
import { prettifyError as prettifyError2 } from "zod";
|
|
501
|
+
|
|
468
502
|
// src/errors.ts
|
|
469
503
|
var CancelledError = class extends Error {
|
|
470
504
|
constructor(runId) {
|
|
@@ -472,6 +506,9 @@ var CancelledError = class extends Error {
|
|
|
472
506
|
this.name = "CancelledError";
|
|
473
507
|
}
|
|
474
508
|
};
|
|
509
|
+
function getErrorMessage(error) {
|
|
510
|
+
return error instanceof Error ? error.message : String(error);
|
|
511
|
+
}
|
|
475
512
|
|
|
476
513
|
// src/context.ts
|
|
477
514
|
function createStepContext(run, jobName, storage, eventEmitter) {
|
|
@@ -547,8 +584,13 @@ function createStepContext(run, jobName, storage, eventEmitter) {
|
|
|
547
584
|
}
|
|
548
585
|
},
|
|
549
586
|
progress(current, total, message) {
|
|
550
|
-
|
|
551
|
-
|
|
587
|
+
const progressData = { current, total, message };
|
|
588
|
+
storage.updateRun(run.id, { progress: progressData });
|
|
589
|
+
eventEmitter.emit({
|
|
590
|
+
type: "run:progress",
|
|
591
|
+
runId: run.id,
|
|
592
|
+
jobName,
|
|
593
|
+
progress: progressData
|
|
552
594
|
});
|
|
553
595
|
},
|
|
554
596
|
log: {
|
|
@@ -614,9 +656,6 @@ function createWorker(config, storage, eventEmitter, jobRegistry) {
|
|
|
614
656
|
});
|
|
615
657
|
}
|
|
616
658
|
}
|
|
617
|
-
function getErrorMessage(error) {
|
|
618
|
-
return error instanceof Error ? error.message : String(error);
|
|
619
|
-
}
|
|
620
659
|
async function handleRunSuccess(runId, jobName, output, startTime) {
|
|
621
660
|
const currentRun = await storage.getRun(runId);
|
|
622
661
|
if (currentRun?.status === "cancelled") {
|
|
@@ -663,7 +702,7 @@ function createWorker(config, storage, eventEmitter, jobRegistry) {
|
|
|
663
702
|
updateHeartbeat().catch((error) => {
|
|
664
703
|
eventEmitter.emit({
|
|
665
704
|
type: "worker:error",
|
|
666
|
-
error:
|
|
705
|
+
error: getErrorMessage(error),
|
|
667
706
|
context: "heartbeat",
|
|
668
707
|
runId: run.id
|
|
669
708
|
});
|
|
@@ -682,7 +721,7 @@ function createWorker(config, storage, eventEmitter, jobRegistry) {
|
|
|
682
721
|
if (job.outputSchema) {
|
|
683
722
|
const parseResult = job.outputSchema.safeParse(output);
|
|
684
723
|
if (!parseResult.success) {
|
|
685
|
-
throw new Error(`Invalid output: ${parseResult.error
|
|
724
|
+
throw new Error(`Invalid output: ${prettifyError2(parseResult.error)}`);
|
|
686
725
|
}
|
|
687
726
|
}
|
|
688
727
|
await handleRunSuccess(run.id, run.jobName, output, startTime);
|
|
@@ -780,35 +819,136 @@ var DEFAULTS = {
|
|
|
780
819
|
heartbeatInterval: 5e3,
|
|
781
820
|
staleThreshold: 3e4
|
|
782
821
|
};
|
|
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;
|
|
822
|
+
function createDurablyInstance(state, jobs) {
|
|
823
|
+
const { db, storage, eventEmitter, jobRegistry, worker } = state;
|
|
796
824
|
const durably = {
|
|
797
825
|
db,
|
|
798
826
|
storage,
|
|
827
|
+
jobs,
|
|
799
828
|
on: eventEmitter.on,
|
|
800
829
|
emit: eventEmitter.emit,
|
|
801
830
|
onError: eventEmitter.onError,
|
|
802
831
|
start: worker.start,
|
|
803
832
|
stop: worker.stop,
|
|
804
|
-
|
|
805
|
-
|
|
833
|
+
// biome-ignore lint/suspicious/noExplicitAny: flexible type constraint for job definitions
|
|
834
|
+
register(jobDefs) {
|
|
835
|
+
const newHandles = {};
|
|
836
|
+
for (const key of Object.keys(jobDefs)) {
|
|
837
|
+
const jobDef = jobDefs[key];
|
|
838
|
+
const handle = createJobHandle(
|
|
839
|
+
jobDef,
|
|
840
|
+
storage,
|
|
841
|
+
eventEmitter,
|
|
842
|
+
jobRegistry
|
|
843
|
+
);
|
|
844
|
+
newHandles[key] = handle;
|
|
845
|
+
}
|
|
846
|
+
const mergedJobs = { ...jobs, ...newHandles };
|
|
847
|
+
return createDurablyInstance(state, mergedJobs);
|
|
806
848
|
},
|
|
807
849
|
getRun: storage.getRun,
|
|
808
850
|
getRuns: storage.getRuns,
|
|
809
851
|
use(plugin) {
|
|
810
852
|
plugin.install(durably);
|
|
811
853
|
},
|
|
854
|
+
getJob(name) {
|
|
855
|
+
const registeredJob = jobRegistry.get(name);
|
|
856
|
+
if (!registeredJob) {
|
|
857
|
+
return void 0;
|
|
858
|
+
}
|
|
859
|
+
return registeredJob.handle;
|
|
860
|
+
},
|
|
861
|
+
subscribe(runId) {
|
|
862
|
+
let closed = false;
|
|
863
|
+
let cleanup = null;
|
|
864
|
+
return new ReadableStream({
|
|
865
|
+
start: (controller) => {
|
|
866
|
+
const unsubscribeStart = eventEmitter.on("run:start", (event) => {
|
|
867
|
+
if (!closed && event.runId === runId) {
|
|
868
|
+
controller.enqueue(event);
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
const unsubscribeComplete = eventEmitter.on(
|
|
872
|
+
"run:complete",
|
|
873
|
+
(event) => {
|
|
874
|
+
if (!closed && event.runId === runId) {
|
|
875
|
+
controller.enqueue(event);
|
|
876
|
+
closed = true;
|
|
877
|
+
cleanup?.();
|
|
878
|
+
controller.close();
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
);
|
|
882
|
+
const unsubscribeFail = eventEmitter.on("run:fail", (event) => {
|
|
883
|
+
if (!closed && event.runId === runId) {
|
|
884
|
+
controller.enqueue(event);
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
const unsubscribeCancel = eventEmitter.on("run:cancel", (event) => {
|
|
888
|
+
if (!closed && event.runId === runId) {
|
|
889
|
+
controller.enqueue(event);
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
const unsubscribeRetry = eventEmitter.on("run:retry", (event) => {
|
|
893
|
+
if (!closed && event.runId === runId) {
|
|
894
|
+
controller.enqueue(event);
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
const unsubscribeProgress = eventEmitter.on(
|
|
898
|
+
"run:progress",
|
|
899
|
+
(event) => {
|
|
900
|
+
if (!closed && event.runId === runId) {
|
|
901
|
+
controller.enqueue(event);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
);
|
|
905
|
+
const unsubscribeStepStart = eventEmitter.on(
|
|
906
|
+
"step:start",
|
|
907
|
+
(event) => {
|
|
908
|
+
if (!closed && event.runId === runId) {
|
|
909
|
+
controller.enqueue(event);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
);
|
|
913
|
+
const unsubscribeStepComplete = eventEmitter.on(
|
|
914
|
+
"step:complete",
|
|
915
|
+
(event) => {
|
|
916
|
+
if (!closed && event.runId === runId) {
|
|
917
|
+
controller.enqueue(event);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
);
|
|
921
|
+
const unsubscribeStepFail = eventEmitter.on("step:fail", (event) => {
|
|
922
|
+
if (!closed && event.runId === runId) {
|
|
923
|
+
controller.enqueue(event);
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
const unsubscribeLog = eventEmitter.on("log:write", (event) => {
|
|
927
|
+
if (!closed && event.runId === runId) {
|
|
928
|
+
controller.enqueue(event);
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
cleanup = () => {
|
|
932
|
+
unsubscribeStart();
|
|
933
|
+
unsubscribeComplete();
|
|
934
|
+
unsubscribeFail();
|
|
935
|
+
unsubscribeCancel();
|
|
936
|
+
unsubscribeRetry();
|
|
937
|
+
unsubscribeProgress();
|
|
938
|
+
unsubscribeStepStart();
|
|
939
|
+
unsubscribeStepComplete();
|
|
940
|
+
unsubscribeStepFail();
|
|
941
|
+
unsubscribeLog();
|
|
942
|
+
};
|
|
943
|
+
},
|
|
944
|
+
cancel: () => {
|
|
945
|
+
if (!closed) {
|
|
946
|
+
closed = true;
|
|
947
|
+
cleanup?.();
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
},
|
|
812
952
|
async retry(runId) {
|
|
813
953
|
const run = await storage.getRun(runId);
|
|
814
954
|
if (!run) {
|
|
@@ -827,6 +967,11 @@ function createDurably(options) {
|
|
|
827
967
|
status: "pending",
|
|
828
968
|
error: null
|
|
829
969
|
});
|
|
970
|
+
eventEmitter.emit({
|
|
971
|
+
type: "run:retry",
|
|
972
|
+
runId,
|
|
973
|
+
jobName: run.jobName
|
|
974
|
+
});
|
|
830
975
|
},
|
|
831
976
|
async cancel(runId) {
|
|
832
977
|
const run = await storage.getRun(runId);
|
|
@@ -845,6 +990,11 @@ function createDurably(options) {
|
|
|
845
990
|
await storage.updateRun(runId, {
|
|
846
991
|
status: "cancelled"
|
|
847
992
|
});
|
|
993
|
+
eventEmitter.emit({
|
|
994
|
+
type: "run:cancel",
|
|
995
|
+
runId,
|
|
996
|
+
jobName: run.jobName
|
|
997
|
+
});
|
|
848
998
|
},
|
|
849
999
|
async deleteRun(runId) {
|
|
850
1000
|
const run = await storage.getRun(runId);
|
|
@@ -860,22 +1010,48 @@ function createDurably(options) {
|
|
|
860
1010
|
await storage.deleteRun(runId);
|
|
861
1011
|
},
|
|
862
1012
|
async migrate() {
|
|
863
|
-
if (migrated) {
|
|
1013
|
+
if (state.migrated) {
|
|
864
1014
|
return;
|
|
865
1015
|
}
|
|
866
|
-
if (migrating) {
|
|
867
|
-
return migrating;
|
|
1016
|
+
if (state.migrating) {
|
|
1017
|
+
return state.migrating;
|
|
868
1018
|
}
|
|
869
|
-
migrating = runMigrations(db).then(() => {
|
|
870
|
-
migrated = true;
|
|
1019
|
+
state.migrating = runMigrations(db).then(() => {
|
|
1020
|
+
state.migrated = true;
|
|
871
1021
|
}).finally(() => {
|
|
872
|
-
migrating = null;
|
|
1022
|
+
state.migrating = null;
|
|
873
1023
|
});
|
|
874
|
-
return migrating;
|
|
1024
|
+
return state.migrating;
|
|
1025
|
+
},
|
|
1026
|
+
async init() {
|
|
1027
|
+
await this.migrate();
|
|
1028
|
+
this.start();
|
|
875
1029
|
}
|
|
876
1030
|
};
|
|
877
1031
|
return durably;
|
|
878
1032
|
}
|
|
1033
|
+
function createDurably(options) {
|
|
1034
|
+
const config = {
|
|
1035
|
+
pollingInterval: options.pollingInterval ?? DEFAULTS.pollingInterval,
|
|
1036
|
+
heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
|
|
1037
|
+
staleThreshold: options.staleThreshold ?? DEFAULTS.staleThreshold
|
|
1038
|
+
};
|
|
1039
|
+
const db = new Kysely({ dialect: options.dialect });
|
|
1040
|
+
const storage = createKyselyStorage(db);
|
|
1041
|
+
const eventEmitter = createEventEmitter();
|
|
1042
|
+
const jobRegistry = createJobRegistry();
|
|
1043
|
+
const worker = createWorker(config, storage, eventEmitter, jobRegistry);
|
|
1044
|
+
const state = {
|
|
1045
|
+
db,
|
|
1046
|
+
storage,
|
|
1047
|
+
eventEmitter,
|
|
1048
|
+
jobRegistry,
|
|
1049
|
+
worker,
|
|
1050
|
+
migrating: null,
|
|
1051
|
+
migrated: false
|
|
1052
|
+
};
|
|
1053
|
+
return createDurablyInstance(state, {});
|
|
1054
|
+
}
|
|
879
1055
|
|
|
880
1056
|
// src/define-job.ts
|
|
881
1057
|
function defineJob(config) {
|
|
@@ -887,26 +1063,366 @@ function defineJob(config) {
|
|
|
887
1063
|
};
|
|
888
1064
|
}
|
|
889
1065
|
|
|
890
|
-
// src/
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1066
|
+
// src/http.ts
|
|
1067
|
+
var JSON_HEADERS = {
|
|
1068
|
+
"Content-Type": "application/json"
|
|
1069
|
+
};
|
|
1070
|
+
function jsonResponse(data, status = 200) {
|
|
1071
|
+
return new Response(JSON.stringify(data), {
|
|
1072
|
+
status,
|
|
1073
|
+
headers: JSON_HEADERS
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
function errorResponse(message, status = 500) {
|
|
1077
|
+
return jsonResponse({ error: message }, status);
|
|
1078
|
+
}
|
|
1079
|
+
function successResponse() {
|
|
1080
|
+
return jsonResponse({ success: true });
|
|
1081
|
+
}
|
|
1082
|
+
function getRequiredQueryParam(url, paramName) {
|
|
1083
|
+
const value = url.searchParams.get(paramName);
|
|
1084
|
+
if (!value) {
|
|
1085
|
+
return errorResponse(`${paramName} query parameter is required`, 400);
|
|
1086
|
+
}
|
|
1087
|
+
return value;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/sse.ts
|
|
1091
|
+
var SSE_HEADERS = {
|
|
1092
|
+
"Content-Type": "text/event-stream",
|
|
1093
|
+
"Cache-Control": "no-cache",
|
|
1094
|
+
Connection: "keep-alive"
|
|
1095
|
+
};
|
|
1096
|
+
function formatSSE(data) {
|
|
1097
|
+
return `data: ${JSON.stringify(data)}
|
|
1098
|
+
|
|
1099
|
+
`;
|
|
1100
|
+
}
|
|
1101
|
+
function createSSEEncoder() {
|
|
1102
|
+
return new TextEncoder();
|
|
1103
|
+
}
|
|
1104
|
+
function encodeSSE(encoder, data) {
|
|
1105
|
+
return encoder.encode(formatSSE(data));
|
|
1106
|
+
}
|
|
1107
|
+
function createSSEResponse(stream) {
|
|
1108
|
+
return new Response(stream, {
|
|
1109
|
+
status: 200,
|
|
1110
|
+
headers: SSE_HEADERS
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
function createSSEStreamFromReader(reader) {
|
|
1114
|
+
const encoder = createSSEEncoder();
|
|
1115
|
+
return new ReadableStream({
|
|
1116
|
+
async start(controller) {
|
|
1117
|
+
try {
|
|
1118
|
+
while (true) {
|
|
1119
|
+
const { done, value } = await reader.read();
|
|
1120
|
+
if (done) {
|
|
1121
|
+
controller.close();
|
|
1122
|
+
break;
|
|
1123
|
+
}
|
|
1124
|
+
controller.enqueue(encodeSSE(encoder, value));
|
|
1125
|
+
}
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
controller.error(error);
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
cancel() {
|
|
1131
|
+
reader.releaseLock();
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
function createSSEStreamFromSubscriptions(setup) {
|
|
1136
|
+
const encoder = createSSEEncoder();
|
|
1137
|
+
let closed = false;
|
|
1138
|
+
let unsubscribes = [];
|
|
1139
|
+
return new ReadableStream({
|
|
1140
|
+
start(controller) {
|
|
1141
|
+
const sseController = {
|
|
1142
|
+
enqueue: (data) => {
|
|
1143
|
+
if (closed) return;
|
|
1144
|
+
controller.enqueue(encodeSSE(encoder, data));
|
|
1145
|
+
},
|
|
1146
|
+
close: () => {
|
|
1147
|
+
if (closed) return;
|
|
1148
|
+
closed = true;
|
|
1149
|
+
controller.close();
|
|
1150
|
+
},
|
|
1151
|
+
get closed() {
|
|
1152
|
+
return closed;
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
unsubscribes = setup(sseController);
|
|
1156
|
+
},
|
|
1157
|
+
cancel() {
|
|
1158
|
+
closed = true;
|
|
1159
|
+
for (const unsubscribe of unsubscribes) {
|
|
1160
|
+
unsubscribe();
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// src/server.ts
|
|
1167
|
+
function createDurablyHandler(durably, options) {
|
|
1168
|
+
const handler = {
|
|
1169
|
+
async handle(request, basePath) {
|
|
1170
|
+
if (options?.onRequest) {
|
|
1171
|
+
await options.onRequest();
|
|
1172
|
+
}
|
|
1173
|
+
const url = new URL(request.url);
|
|
1174
|
+
const path = url.pathname.replace(basePath, "");
|
|
1175
|
+
const method = request.method;
|
|
1176
|
+
if (method === "GET") {
|
|
1177
|
+
if (path === "/subscribe") return handler.subscribe(request);
|
|
1178
|
+
if (path === "/runs") return handler.runs(request);
|
|
1179
|
+
if (path === "/run") return handler.run(request);
|
|
1180
|
+
if (path === "/steps") return handler.steps(request);
|
|
1181
|
+
if (path === "/runs/subscribe") return handler.runsSubscribe(request);
|
|
1182
|
+
}
|
|
1183
|
+
if (method === "POST") {
|
|
1184
|
+
if (path === "/trigger") return handler.trigger(request);
|
|
1185
|
+
if (path === "/retry") return handler.retry(request);
|
|
1186
|
+
if (path === "/cancel") return handler.cancel(request);
|
|
1187
|
+
}
|
|
1188
|
+
if (method === "DELETE") {
|
|
1189
|
+
if (path === "/run") return handler.delete(request);
|
|
1190
|
+
}
|
|
1191
|
+
return new Response("Not Found", { status: 404 });
|
|
1192
|
+
},
|
|
1193
|
+
async trigger(request) {
|
|
1194
|
+
try {
|
|
1195
|
+
const body = await request.json();
|
|
1196
|
+
if (!body.jobName) {
|
|
1197
|
+
return errorResponse("jobName is required", 400);
|
|
1198
|
+
}
|
|
1199
|
+
const job = durably.getJob(body.jobName);
|
|
1200
|
+
if (!job) {
|
|
1201
|
+
return errorResponse(`Job not found: ${body.jobName}`, 404);
|
|
1202
|
+
}
|
|
1203
|
+
const run = await job.trigger(body.input ?? {}, {
|
|
1204
|
+
idempotencyKey: body.idempotencyKey,
|
|
1205
|
+
concurrencyKey: body.concurrencyKey
|
|
902
1206
|
});
|
|
903
|
-
|
|
1207
|
+
const response = { runId: run.id };
|
|
1208
|
+
return jsonResponse(response);
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
return errorResponse(getErrorMessage(error), 500);
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
subscribe(request) {
|
|
1214
|
+
const url = new URL(request.url);
|
|
1215
|
+
const runId = getRequiredQueryParam(url, "runId");
|
|
1216
|
+
if (runId instanceof Response) return runId;
|
|
1217
|
+
const stream = durably.subscribe(runId);
|
|
1218
|
+
const sseStream = createSSEStreamFromReader(
|
|
1219
|
+
stream.getReader()
|
|
1220
|
+
);
|
|
1221
|
+
return createSSEResponse(sseStream);
|
|
1222
|
+
},
|
|
1223
|
+
async runs(request) {
|
|
1224
|
+
try {
|
|
1225
|
+
const url = new URL(request.url);
|
|
1226
|
+
const jobName = url.searchParams.get("jobName") ?? void 0;
|
|
1227
|
+
const status = url.searchParams.get("status");
|
|
1228
|
+
const limit = url.searchParams.get("limit");
|
|
1229
|
+
const offset = url.searchParams.get("offset");
|
|
1230
|
+
const runs = await durably.getRuns({
|
|
1231
|
+
jobName,
|
|
1232
|
+
status,
|
|
1233
|
+
limit: limit ? Number.parseInt(limit, 10) : void 0,
|
|
1234
|
+
offset: offset ? Number.parseInt(offset, 10) : void 0
|
|
1235
|
+
});
|
|
1236
|
+
return jsonResponse(runs);
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
return errorResponse(getErrorMessage(error), 500);
|
|
1239
|
+
}
|
|
1240
|
+
},
|
|
1241
|
+
async run(request) {
|
|
1242
|
+
try {
|
|
1243
|
+
const url = new URL(request.url);
|
|
1244
|
+
const runId = getRequiredQueryParam(url, "runId");
|
|
1245
|
+
if (runId instanceof Response) return runId;
|
|
1246
|
+
const run = await durably.getRun(runId);
|
|
1247
|
+
if (!run) {
|
|
1248
|
+
return errorResponse("Run not found", 404);
|
|
1249
|
+
}
|
|
1250
|
+
return jsonResponse(run);
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
return errorResponse(getErrorMessage(error), 500);
|
|
1253
|
+
}
|
|
1254
|
+
},
|
|
1255
|
+
async retry(request) {
|
|
1256
|
+
try {
|
|
1257
|
+
const url = new URL(request.url);
|
|
1258
|
+
const runId = getRequiredQueryParam(url, "runId");
|
|
1259
|
+
if (runId instanceof Response) return runId;
|
|
1260
|
+
await durably.retry(runId);
|
|
1261
|
+
return successResponse();
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
return errorResponse(getErrorMessage(error), 500);
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
async cancel(request) {
|
|
1267
|
+
try {
|
|
1268
|
+
const url = new URL(request.url);
|
|
1269
|
+
const runId = getRequiredQueryParam(url, "runId");
|
|
1270
|
+
if (runId instanceof Response) return runId;
|
|
1271
|
+
await durably.cancel(runId);
|
|
1272
|
+
return successResponse();
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
return errorResponse(getErrorMessage(error), 500);
|
|
1275
|
+
}
|
|
1276
|
+
},
|
|
1277
|
+
async delete(request) {
|
|
1278
|
+
try {
|
|
1279
|
+
const url = new URL(request.url);
|
|
1280
|
+
const runId = getRequiredQueryParam(url, "runId");
|
|
1281
|
+
if (runId instanceof Response) return runId;
|
|
1282
|
+
await durably.deleteRun(runId);
|
|
1283
|
+
return successResponse();
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
return errorResponse(getErrorMessage(error), 500);
|
|
1286
|
+
}
|
|
1287
|
+
},
|
|
1288
|
+
async steps(request) {
|
|
1289
|
+
try {
|
|
1290
|
+
const url = new URL(request.url);
|
|
1291
|
+
const runId = getRequiredQueryParam(url, "runId");
|
|
1292
|
+
if (runId instanceof Response) return runId;
|
|
1293
|
+
const steps = await durably.storage.getSteps(runId);
|
|
1294
|
+
return jsonResponse(steps);
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
return errorResponse(getErrorMessage(error), 500);
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
1299
|
+
runsSubscribe(request) {
|
|
1300
|
+
const url = new URL(request.url);
|
|
1301
|
+
const jobNameFilter = url.searchParams.get("jobName");
|
|
1302
|
+
const matchesFilter = (jobName) => !jobNameFilter || jobName === jobNameFilter;
|
|
1303
|
+
const sseStream = createSSEStreamFromSubscriptions(
|
|
1304
|
+
(ctrl) => [
|
|
1305
|
+
durably.on("run:trigger", (event) => {
|
|
1306
|
+
if (matchesFilter(event.jobName)) {
|
|
1307
|
+
ctrl.enqueue({
|
|
1308
|
+
type: "run:trigger",
|
|
1309
|
+
runId: event.runId,
|
|
1310
|
+
jobName: event.jobName
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
}),
|
|
1314
|
+
durably.on("run:start", (event) => {
|
|
1315
|
+
if (matchesFilter(event.jobName)) {
|
|
1316
|
+
ctrl.enqueue({
|
|
1317
|
+
type: "run:start",
|
|
1318
|
+
runId: event.runId,
|
|
1319
|
+
jobName: event.jobName
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
}),
|
|
1323
|
+
durably.on("run:complete", (event) => {
|
|
1324
|
+
if (matchesFilter(event.jobName)) {
|
|
1325
|
+
ctrl.enqueue({
|
|
1326
|
+
type: "run:complete",
|
|
1327
|
+
runId: event.runId,
|
|
1328
|
+
jobName: event.jobName
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
}),
|
|
1332
|
+
durably.on("run:fail", (event) => {
|
|
1333
|
+
if (matchesFilter(event.jobName)) {
|
|
1334
|
+
ctrl.enqueue({
|
|
1335
|
+
type: "run:fail",
|
|
1336
|
+
runId: event.runId,
|
|
1337
|
+
jobName: event.jobName
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
}),
|
|
1341
|
+
durably.on("run:cancel", (event) => {
|
|
1342
|
+
if (matchesFilter(event.jobName)) {
|
|
1343
|
+
ctrl.enqueue({
|
|
1344
|
+
type: "run:cancel",
|
|
1345
|
+
runId: event.runId,
|
|
1346
|
+
jobName: event.jobName
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
}),
|
|
1350
|
+
durably.on("run:retry", (event) => {
|
|
1351
|
+
if (matchesFilter(event.jobName)) {
|
|
1352
|
+
ctrl.enqueue({
|
|
1353
|
+
type: "run:retry",
|
|
1354
|
+
runId: event.runId,
|
|
1355
|
+
jobName: event.jobName
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
}),
|
|
1359
|
+
durably.on("run:progress", (event) => {
|
|
1360
|
+
if (matchesFilter(event.jobName)) {
|
|
1361
|
+
ctrl.enqueue({
|
|
1362
|
+
type: "run:progress",
|
|
1363
|
+
runId: event.runId,
|
|
1364
|
+
jobName: event.jobName,
|
|
1365
|
+
progress: event.progress
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
}),
|
|
1369
|
+
durably.on("step:start", (event) => {
|
|
1370
|
+
if (matchesFilter(event.jobName)) {
|
|
1371
|
+
ctrl.enqueue({
|
|
1372
|
+
type: "step:start",
|
|
1373
|
+
runId: event.runId,
|
|
1374
|
+
jobName: event.jobName,
|
|
1375
|
+
stepName: event.stepName,
|
|
1376
|
+
stepIndex: event.stepIndex
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
}),
|
|
1380
|
+
durably.on("step:complete", (event) => {
|
|
1381
|
+
if (matchesFilter(event.jobName)) {
|
|
1382
|
+
ctrl.enqueue({
|
|
1383
|
+
type: "step:complete",
|
|
1384
|
+
runId: event.runId,
|
|
1385
|
+
jobName: event.jobName,
|
|
1386
|
+
stepName: event.stepName,
|
|
1387
|
+
stepIndex: event.stepIndex
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
}),
|
|
1391
|
+
durably.on("step:fail", (event) => {
|
|
1392
|
+
if (matchesFilter(event.jobName)) {
|
|
1393
|
+
ctrl.enqueue({
|
|
1394
|
+
type: "step:fail",
|
|
1395
|
+
runId: event.runId,
|
|
1396
|
+
jobName: event.jobName,
|
|
1397
|
+
stepName: event.stepName,
|
|
1398
|
+
stepIndex: event.stepIndex,
|
|
1399
|
+
error: event.error
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
}),
|
|
1403
|
+
durably.on("log:write", (event) => {
|
|
1404
|
+
if (!jobNameFilter) {
|
|
1405
|
+
ctrl.enqueue({
|
|
1406
|
+
type: "log:write",
|
|
1407
|
+
runId: event.runId,
|
|
1408
|
+
stepName: event.stepName,
|
|
1409
|
+
level: event.level,
|
|
1410
|
+
message: event.message,
|
|
1411
|
+
data: event.data
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
})
|
|
1415
|
+
]
|
|
1416
|
+
);
|
|
1417
|
+
return createSSEResponse(sseStream);
|
|
904
1418
|
}
|
|
905
1419
|
};
|
|
1420
|
+
return handler;
|
|
906
1421
|
}
|
|
907
1422
|
export {
|
|
908
1423
|
CancelledError,
|
|
909
1424
|
createDurably,
|
|
1425
|
+
createDurablyHandler,
|
|
910
1426
|
defineJob,
|
|
911
1427
|
withLogPersistence
|
|
912
1428
|
};
|