@voltro/workflow 0.85.0 → 0.86.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/CHANGELOG.md +666 -0
- package/THIRD-PARTY-NOTICES.md +5 -33
- package/dist/admission.js +140 -0
- package/dist/admissionDrainer.js +705 -0
- package/dist/admissionGate.js +115 -0
- package/dist/admissionStore.js +347 -0
- package/dist/admissionSubmission.js +297 -0
- package/dist/admissionTables.js +137 -0
- package/dist/admissionTransaction.js +21 -0
- package/dist/awaitEvent.js +82 -0
- package/dist/budgetHold.js +136 -0
- package/dist/cancelOnPlan.js +75 -0
- package/dist/cancelOnSweep.js +188 -0
- package/dist/cancelPendingStart.js +50 -0
- package/dist/cluster.js +8 -2
- package/dist/clusterCron.js +11 -0
- package/dist/clusterDiagnostics.js +20 -0
- package/dist/clusterDiagnosticsSql.js +145 -0
- package/dist/clusterLayer.js +110 -0
- package/dist/clusterMigrationGate.js +61 -0
- package/dist/clusterRunnerLayer.js +21 -0
- package/dist/clusterTestSuite.js +101 -101
- package/dist/controlMonitor.js +13 -0
- package/dist/controlRequest.js +367 -0
- package/dist/executionRefusal.js +162 -0
- package/dist/failureHandoff.js +183 -0
- package/dist/failureMessage.js +12 -0
- package/dist/failureRecovery.js +50 -0
- package/dist/failureReportStore.js +60 -0
- package/dist/flowControl.js +142 -0
- package/dist/forwardStartReceipts.js +41 -0
- package/dist/inMemoryLayer.js +7 -0
- package/dist/inMemoryRecorder.js +56 -0
- package/dist/index.d.ts +82 -8
- package/dist/index.js +49 -5
- package/dist/inspectWorkflow.js +101 -0
- package/dist/{journalEncryption-DTyhUzaS.js → journalEncryption.js} +30 -37
- package/dist/messages.js +92 -0
- package/dist/nondeterminism.js +51 -0
- package/dist/parentClosure.js +141 -0
- package/dist/parentClosureRecovery.js +54 -0
- package/dist/pendingSubmission.js +85 -0
- package/dist/prepareSubmissionDispatch.js +17 -0
- package/dist/preparedExecution.js +14 -0
- package/dist/primitives.js +225 -2
- package/dist/queuedCancellationRequest.js +364 -0
- package/dist/queuedControl.js +21 -0
- package/dist/recorder.js +11 -0
- package/dist/residentExecutionEvidence.js +64 -0
- package/dist/retryPolicy.js +57 -0
- package/dist/runEventsTable.js +17 -0
- package/dist/runGeneration.js +345 -0
- package/dist/runRecording.js +615 -0
- package/dist/runStepsTable.js +28 -0
- package/dist/runnerStorageGuard.js +29 -0
- package/dist/runsTable.js +42 -0
- package/dist/scheduleDeclaration.js +44 -0
- package/dist/signalHint.js +23 -0
- package/dist/signals.js +70 -0
- package/dist/signalsSuspending.js +57 -0
- package/dist/sqliteWorkflowOwnership.js +26 -0
- package/dist/stalenessSweep.js +191 -0
- package/dist/startContextsTable.js +20 -0
- package/dist/startReceipt.js +143 -0
- package/dist/submissionRecovery.js +122 -0
- package/dist/submissionRejection.js +10 -0
- package/dist/submissionRetry.js +71 -0
- package/dist/suspensionUnavailable.js +9 -0
- package/dist/updates.js +87 -0
- package/dist/watermarkTable.js +9 -0
- package/dist/webCrypto.js +17 -0
- package/dist/workflowDelivery.js +24 -0
- package/dist/workflowJournal.js +124 -0
- package/dist/workflowJournalReset.js +111 -0
- package/dist/workflowReadSelections.js +12 -0
- package/dist/workflowRestartDispatch.js +29 -0
- package/dist/workflowRestartObservation.js +60 -0
- package/dist/workflowRestartPreparation.js +109 -0
- package/dist/workflowRestartReservation.js +426 -0
- package/dist/workflowResumeDispatch.js +29 -0
- package/package.json +4 -4
- package/dist/cluster-F_bhqVP7.js +0 -679
- package/dist/primitives-DX04WTWh.js +0 -507
- package/dist/src-C2NEyUM8.js +0 -3925
- package/dist/workflowReadSelections-CZAUjP7f.js +0 -2096
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { isWorkflowEncrypted as e, openErrorText as t, openPayloadFields as n, sealPayloadFields as r } from "./journalEncryption.js";
|
|
2
|
+
import { FAILURE_REPORTS_TABLE as i, readWorkflowFailureRecord as a } from "./failureReportStore.js";
|
|
3
|
+
import { dispatchedExecutionSubmissions as o, readAdmissionSubmission as s } from "./admissionSubmission.js";
|
|
4
|
+
import { START_RECEIPTS_TABLE as c, decodeWorkflowStartReceipt as l, readWorkflowStartReceipt as u, recordWorkflowStartReceipt as d } from "./startReceipt.js";
|
|
5
|
+
import { PENDING_TABLE as f, parkDelayedStart as p, retryBackoffMs as m } from "./admissionStore.js";
|
|
6
|
+
import { Schema as h } from "effect";
|
|
7
|
+
import { and as g, eq as _, gt as v, isNull as y } from "@voltro/database";
|
|
8
|
+
import { createHash as b } from "node:crypto";
|
|
9
|
+
//#region src/failureHandoff.ts
|
|
10
|
+
var x = async (e, t, n) => {
|
|
11
|
+
let r, a = t === "submissionId" ? _(t, n) : g(_(t, n), y("submissionId"), y("terminal"));
|
|
12
|
+
for (;;) {
|
|
13
|
+
let t = await e.query({
|
|
14
|
+
table: c,
|
|
15
|
+
predicate: r === void 0 ? a : g(a, v("id", r)),
|
|
16
|
+
order: [{
|
|
17
|
+
column: "id",
|
|
18
|
+
direction: "asc"
|
|
19
|
+
}],
|
|
20
|
+
take: 100,
|
|
21
|
+
skip: void 0,
|
|
22
|
+
projection: void 0
|
|
23
|
+
});
|
|
24
|
+
if (t.length === 0) return !1;
|
|
25
|
+
for (let n of t) {
|
|
26
|
+
let t = l(n), a = await e.query({
|
|
27
|
+
table: i,
|
|
28
|
+
predicate: _("handoffReceiptId", t.id),
|
|
29
|
+
order: [],
|
|
30
|
+
take: 1,
|
|
31
|
+
skip: void 0,
|
|
32
|
+
projection: void 0
|
|
33
|
+
});
|
|
34
|
+
if (a.length !== 0) {
|
|
35
|
+
if (a[0]?.handler !== t.workflowName || a[0]?.tenantId !== t.tenantId) throw Error("Failure delivery receipt belongs to a different workflow or tenant");
|
|
36
|
+
return !0;
|
|
37
|
+
}
|
|
38
|
+
r = t.id;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}, S = (e, t) => x(e, "submissionId", t), C = (e, t) => x(e, "pendingId", t), w = async (e, t, n, r) => {
|
|
42
|
+
for await (let i of o(e, t, n)) {
|
|
43
|
+
if (i.tenantId !== r) throw Error("Failure delivery belongs to a different resident tenant");
|
|
44
|
+
if (await S(e, i.id)) return !0;
|
|
45
|
+
}
|
|
46
|
+
return !1;
|
|
47
|
+
}, T = h.Struct({
|
|
48
|
+
handoffReceiptId: h.NullOr(h.String),
|
|
49
|
+
handoffAttempt: h.Number.check(h.isInt(), h.isGreaterThanOrEqualTo(0)),
|
|
50
|
+
acceptedExecutionId: h.NullOr(h.NonEmptyString)
|
|
51
|
+
}), E = (e, t) => `wfrc_${b("sha256").update(JSON.stringify([
|
|
52
|
+
"failure",
|
|
53
|
+
e,
|
|
54
|
+
t
|
|
55
|
+
])).digest("hex").slice(0, 40)}`, D = async (e, t) => {
|
|
56
|
+
let n = await e.query({
|
|
57
|
+
table: i,
|
|
58
|
+
predicate: _("id", t),
|
|
59
|
+
order: [],
|
|
60
|
+
take: 1,
|
|
61
|
+
skip: void 0,
|
|
62
|
+
projection: void 0
|
|
63
|
+
}), r = h.decodeUnknownSync(T)(n[0]);
|
|
64
|
+
if (r.handoffReceiptId === null && (r.handoffAttempt !== 0 || r.acceptedExecutionId !== null)) throw Error("Failure report has contradictory unstarted handoff metadata");
|
|
65
|
+
return r;
|
|
66
|
+
}, O = (i) => {
|
|
67
|
+
if (i.handler === null || !e(i.handler)) return i.report;
|
|
68
|
+
let a = `failure report for '${i.handler}'`;
|
|
69
|
+
return r({
|
|
70
|
+
...i.report,
|
|
71
|
+
payload: n(i.report.payload, `${a}.payload`),
|
|
72
|
+
errorMessage: t(i.report.errorMessage)
|
|
73
|
+
}, a);
|
|
74
|
+
}, k = async (e, t, n, r, i) => {
|
|
75
|
+
if (t.handler === null || t.handler.length === 0) throw Error("Cannot queue an unconfigured failure handler");
|
|
76
|
+
if (!Number.isSafeInteger(n) || n < 0) throw Error("Failure handoff attempt exhausted");
|
|
77
|
+
if (!Number.isFinite(r)) throw Error("Failure handoff retry deadline is invalid");
|
|
78
|
+
let a = await p({
|
|
79
|
+
store: e,
|
|
80
|
+
workflowName: t.handler,
|
|
81
|
+
payload: O(t),
|
|
82
|
+
callerContext: t.callerContext,
|
|
83
|
+
tenantId: t.tenantId,
|
|
84
|
+
at: r,
|
|
85
|
+
now: i
|
|
86
|
+
}), o = await d(e, {
|
|
87
|
+
id: E(t.id, n),
|
|
88
|
+
workflowName: t.handler,
|
|
89
|
+
tenantId: t.tenantId,
|
|
90
|
+
pendingId: a.intentId
|
|
91
|
+
});
|
|
92
|
+
if (await e.update("_voltro_workflow_failure_reports", t.id, {
|
|
93
|
+
handoffReceiptId: o.id,
|
|
94
|
+
handoffAttempt: n
|
|
95
|
+
}) === null) throw Error("Failure report disappeared during durable handoff");
|
|
96
|
+
return {
|
|
97
|
+
kind: "queued",
|
|
98
|
+
receiptId: o.id,
|
|
99
|
+
intentId: o.pendingId
|
|
100
|
+
};
|
|
101
|
+
}, A = async (e, t, n) => {
|
|
102
|
+
if (!Number.isFinite(n)) throw Error("Failure handoff requires a finite timestamp");
|
|
103
|
+
let r = await a(e, t);
|
|
104
|
+
if (r === void 0) throw Error("Cannot hand off an unknown workflow failure");
|
|
105
|
+
let { handoffReceiptId: i, handoffAttempt: o, acceptedExecutionId: s } = await D(e, t);
|
|
106
|
+
if (r.handler === null) {
|
|
107
|
+
if (i !== null || s !== null) throw Error("Unconfigured failure report has a contradictory handoff");
|
|
108
|
+
return { kind: "unconfigured" };
|
|
109
|
+
}
|
|
110
|
+
if (r.handler.length === 0) throw Error("Failure report has an empty handler");
|
|
111
|
+
let c = E(t, o);
|
|
112
|
+
if (i !== null) {
|
|
113
|
+
if (i !== c) throw Error("Failure handoff receipt belongs to a different report");
|
|
114
|
+
let t = await u(e, i);
|
|
115
|
+
if (t === void 0 || t.workflowName !== r.handler || t.tenantId !== r.tenantId) throw Error("Failure report has no matching durable start receipt");
|
|
116
|
+
return {
|
|
117
|
+
kind: "queued",
|
|
118
|
+
receiptId: t.id,
|
|
119
|
+
intentId: t.pendingId
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return k(e, r, 0, n, n);
|
|
123
|
+
}, j = class extends Error {
|
|
124
|
+
reason;
|
|
125
|
+
constructor(e) {
|
|
126
|
+
super(`Workflow failure delivery requires reconciliation: ${e}`), this.reason = e;
|
|
127
|
+
}
|
|
128
|
+
}, M = async (e, t, n) => {
|
|
129
|
+
let r = await A(e, t, n);
|
|
130
|
+
if (r.kind === "unconfigured") return r;
|
|
131
|
+
let i = await D(e, t), o = await u(e, r.receiptId);
|
|
132
|
+
if (o === void 0) throw Error("Failure handoff receipt disappeared during reconciliation");
|
|
133
|
+
if (i.acceptedExecutionId !== null) {
|
|
134
|
+
if (o.terminal !== null || o.submissionId === null) throw Error("Accepted failure report has no submitted receipt");
|
|
135
|
+
return {
|
|
136
|
+
kind: "accepted",
|
|
137
|
+
executionId: i.acceptedExecutionId
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (o.terminal !== null) {
|
|
141
|
+
if (o.terminal.kind === "cancelled") return {
|
|
142
|
+
kind: "cancelled",
|
|
143
|
+
receiptId: o.id
|
|
144
|
+
};
|
|
145
|
+
let r = await a(e, t);
|
|
146
|
+
if (r === void 0) throw Error("Failure report disappeared during retry");
|
|
147
|
+
let s = i.handoffAttempt + 1, c = Math.max(m(s, () => 1), o.terminal.kind === "dropped" ? o.terminal.retryAfterMs : 0);
|
|
148
|
+
return k(e, r, s, Math.max(n, o.terminal.at + c), n);
|
|
149
|
+
}
|
|
150
|
+
if (o.submissionId === null) {
|
|
151
|
+
let t = (await e.query({
|
|
152
|
+
table: f,
|
|
153
|
+
predicate: _("id", o.pendingId),
|
|
154
|
+
order: [],
|
|
155
|
+
take: 1,
|
|
156
|
+
skip: void 0,
|
|
157
|
+
projection: void 0
|
|
158
|
+
}))[0];
|
|
159
|
+
if (t === void 0) throw new j("arrival-missing");
|
|
160
|
+
if (t.workflowName !== o.workflowName || t.tenantId !== o.tenantId) throw new j("arrival-owner-mismatch");
|
|
161
|
+
if (t.deadLetteredAt !== null) throw new j("arrival-dead-lettered");
|
|
162
|
+
return r;
|
|
163
|
+
}
|
|
164
|
+
let c = await s(e, o.submissionId);
|
|
165
|
+
if (c === void 0 || !c.receiptsBound || c.workflowName !== o.workflowName || c.tenantId !== o.tenantId || !c.intentIds.includes(o.pendingId)) throw Error("Failure handoff receipt has no matching admission submission");
|
|
166
|
+
if (c.failureId !== null) {
|
|
167
|
+
let r = await a(e, c.failureId);
|
|
168
|
+
if (c.executionId !== null || c.dispatchAttempts > 1 || r === void 0 || r.sourceKind !== "submission" || r.sourceId !== c.id || r.workflowName !== c.workflowName || r.tenantId !== c.tenantId || r.report.executionId !== null || r.report.runId !== null) throw new j("rejection-proof-invalid");
|
|
169
|
+
let o = await a(e, t);
|
|
170
|
+
if (o === void 0) throw Error("Failure report disappeared during rejected handler retry");
|
|
171
|
+
let s = i.handoffAttempt + 1;
|
|
172
|
+
return k(e, o, s, Math.max(n, r.report.failedAt + m(s, () => 1)), n);
|
|
173
|
+
}
|
|
174
|
+
if (c.executionId === null) return r;
|
|
175
|
+
if (c.failureId !== null || c.executionId.length === 0 || c.preparedExecutionId !== c.executionId) throw Error("Failure handler acknowledgement contradicts its prepared submission");
|
|
176
|
+
if (await e.update("_voltro_workflow_failure_reports", t, { acceptedExecutionId: c.executionId }) === null) throw Error("Failure report disappeared during acceptance acknowledgement");
|
|
177
|
+
return {
|
|
178
|
+
kind: "accepted",
|
|
179
|
+
executionId: c.executionId
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
//#endregion
|
|
183
|
+
export { j as FailureReconciliationError, A as handoffWorkflowFailureInTransaction, w as isFailureDeliveryExecution, C as isFailureDeliveryPending, S as isFailureDeliverySubmission, M as reconcileWorkflowFailureInTransaction };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/failureMessage.ts
|
|
2
|
+
var e = (e) => {
|
|
3
|
+
let t = e?.message;
|
|
4
|
+
if (typeof t == "string" && t.length > 0) return t;
|
|
5
|
+
if (typeof e == "object" && e && typeof e.toJSON == "function") {
|
|
6
|
+
let { _tag: t, ...n } = e.toJSON(), r = typeof t == "string" ? t : String(e);
|
|
7
|
+
return Object.keys(n).length === 0 ? r : `${r}: ${JSON.stringify(n)}`;
|
|
8
|
+
}
|
|
9
|
+
return String(e);
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { e as failureMessage };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { FAILURE_REPORTS_TABLE as e } from "./failureReportStore.js";
|
|
2
|
+
import { FailureReconciliationError as t, reconcileWorkflowFailureInTransaction as n } from "./failureHandoff.js";
|
|
3
|
+
import { retryBackoffMs as r } from "./admissionStore.js";
|
|
4
|
+
import { and as i, gt as a, isNotNull as o, isNull as s } from "@voltro/database";
|
|
5
|
+
//#region src/failureRecovery.ts
|
|
6
|
+
var c = /* @__PURE__ */ new WeakMap(), l = async (l, u, d) => {
|
|
7
|
+
let f = c.get(l), p = await l.store.query({
|
|
8
|
+
table: e,
|
|
9
|
+
predicate: i(o("handler"), s("acceptedExecutionId"), ...f === void 0 ? [] : [a("id", f.afterId)]),
|
|
10
|
+
order: [{
|
|
11
|
+
column: "id",
|
|
12
|
+
direction: "asc"
|
|
13
|
+
}],
|
|
14
|
+
take: d,
|
|
15
|
+
skip: void 0,
|
|
16
|
+
projection: void 0
|
|
17
|
+
}), m = 0, h = !1, g = f?.nextDueAt, _ = () => {
|
|
18
|
+
let e = u + r(1, () => 1);
|
|
19
|
+
g = Math.min(g ?? e, e);
|
|
20
|
+
};
|
|
21
|
+
for (let e of p) try {
|
|
22
|
+
if (typeof e.id != "string" || e.id.length === 0) throw Error("Failure report has no identity");
|
|
23
|
+
let t = e.id;
|
|
24
|
+
(await l.transaction((e) => n(e, t, u))).kind === "queued" && (h = !0, _());
|
|
25
|
+
} catch (n) {
|
|
26
|
+
m++, _(), l.log?.warn?.("workflow failure delivery could not be reconciled", {
|
|
27
|
+
failureId: typeof e.id == "string" ? e.id : null,
|
|
28
|
+
...n instanceof t ? { reason: n.reason } : {}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
let v = p.at(-1);
|
|
32
|
+
return p.length === d && v !== void 0 && typeof v.id == "string" ? (c.set(l, {
|
|
33
|
+
afterId: v.id,
|
|
34
|
+
nextDueAt: g
|
|
35
|
+
}), {
|
|
36
|
+
scanned: p.length,
|
|
37
|
+
failed: m,
|
|
38
|
+
hasPending: h,
|
|
39
|
+
nextDueAt: u,
|
|
40
|
+
examined: !0
|
|
41
|
+
}) : (c.delete(l), {
|
|
42
|
+
scanned: p.length,
|
|
43
|
+
failed: m,
|
|
44
|
+
hasPending: h,
|
|
45
|
+
nextDueAt: g,
|
|
46
|
+
examined: p.length > 0
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
//#endregion
|
|
50
|
+
export { l as recoverWorkflowFailures };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { sealErrorText as e } from "./journalEncryption.js";
|
|
2
|
+
import { Schema as t } from "effect";
|
|
3
|
+
import { eq as n } from "@voltro/database";
|
|
4
|
+
//#region src/failureReportStore.ts
|
|
5
|
+
var r = "_voltro_workflow_failure_reports", i = t.Struct({
|
|
6
|
+
id: t.String,
|
|
7
|
+
workflowName: t.String,
|
|
8
|
+
tenantId: t.NullOr(t.String),
|
|
9
|
+
sourceId: t.String,
|
|
10
|
+
sourceKind: t.Literals([
|
|
11
|
+
"submission",
|
|
12
|
+
"intent",
|
|
13
|
+
"run"
|
|
14
|
+
]),
|
|
15
|
+
handler: t.NullOr(t.String),
|
|
16
|
+
callerContext: t.Unknown,
|
|
17
|
+
report: t.Struct({
|
|
18
|
+
workflow: t.String,
|
|
19
|
+
payload: t.Unknown,
|
|
20
|
+
errorTag: t.NullOr(t.String),
|
|
21
|
+
errorMessage: t.NullOr(t.String),
|
|
22
|
+
runId: t.NullOr(t.String),
|
|
23
|
+
executionId: t.NullOr(t.String),
|
|
24
|
+
reason: t.String,
|
|
25
|
+
failedAt: t.Number
|
|
26
|
+
})
|
|
27
|
+
}), a = async (e, a) => {
|
|
28
|
+
let o = await e.query({
|
|
29
|
+
table: r,
|
|
30
|
+
predicate: n("id", a),
|
|
31
|
+
order: [],
|
|
32
|
+
take: 1,
|
|
33
|
+
skip: void 0,
|
|
34
|
+
projection: void 0
|
|
35
|
+
});
|
|
36
|
+
return o[0] === void 0 ? void 0 : t.decodeUnknownSync(i)(o[0]);
|
|
37
|
+
}, o = async (n, o) => {
|
|
38
|
+
if (o.id.length === 0 || o.sourceId.length === 0) throw Error("Workflow failure requires a stable report and source identity");
|
|
39
|
+
let s = await a(n, o.id);
|
|
40
|
+
if (s !== void 0) {
|
|
41
|
+
if (s.workflowName !== o.workflowName || s.tenantId !== o.tenantId || s.sourceId !== o.sourceId || s.sourceKind !== o.sourceKind) throw Error("Workflow failure identity belongs to a different source, workflow or tenant");
|
|
42
|
+
return s;
|
|
43
|
+
}
|
|
44
|
+
let c = t.decodeUnknownSync(i)(o), l = {
|
|
45
|
+
...c,
|
|
46
|
+
report: {
|
|
47
|
+
...c.report,
|
|
48
|
+
errorMessage: e(c.workflowName, c.report.errorMessage)
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
if (l.report.workflow !== l.workflowName) throw Error("Workflow failure report does not describe its owning workflow");
|
|
52
|
+
return await n.insert(r, {
|
|
53
|
+
...l,
|
|
54
|
+
handoffReceiptId: null,
|
|
55
|
+
handoffAttempt: 0,
|
|
56
|
+
acceptedExecutionId: null
|
|
57
|
+
}), l;
|
|
58
|
+
};
|
|
59
|
+
//#endregion
|
|
60
|
+
export { r as FAILURE_REPORTS_TABLE, a as readWorkflowFailureRecord, o as recordWorkflowFailureInTransaction };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { sealedSchema as e } from "./journalEncryption.js";
|
|
2
|
+
import { Duration as t, Schema as n } from "effect";
|
|
3
|
+
//#region src/flowControl.ts
|
|
4
|
+
var r = (e) => e !== void 0 && (e.debounce !== void 0 || e.singleton !== void 0 || e.concurrency !== void 0 || e.throttle !== void 0 || e.rateLimit !== void 0 || e.batch !== void 0 || e.timeouts?.startMs !== void 0 || e.timeouts?.finishMs !== void 0), i = (e) => e?.cancelOn !== void 0 && e.cancelOn.length > 0, a = [
|
|
5
|
+
"debounce",
|
|
6
|
+
"batch",
|
|
7
|
+
"throttle",
|
|
8
|
+
"concurrency"
|
|
9
|
+
], o = (e) => {
|
|
10
|
+
if (e === void 0) return [];
|
|
11
|
+
let t = [];
|
|
12
|
+
return e.debounce !== void 0 && t.push("debounce"), e.batch !== void 0 && t.push("batch"), e.throttle !== void 0 && t.push("throttle"), e.concurrency !== void 0 && t.push("concurrency"), t;
|
|
13
|
+
}, s = (e, n, r) => {
|
|
14
|
+
let i;
|
|
15
|
+
try {
|
|
16
|
+
i = t.toMillis(t.fromInputUnsafe(e));
|
|
17
|
+
} catch {
|
|
18
|
+
throw Error(`workflow '${r}': ${n} is not a duration Effect can read (got ${JSON.stringify(e)}). Use a spelled-out unit ('15 minutes', '1 hour', '30 seconds'), a number of milliseconds, or a Duration — the abbreviated forms ('15m', '1h') are NOT accepted.`);
|
|
19
|
+
}
|
|
20
|
+
if (!Number.isFinite(i) || i <= 0) throw Error(`workflow '${r}': ${n} must be a positive finite duration, got ${String(e)}`);
|
|
21
|
+
return i;
|
|
22
|
+
}, c = (e, t, n) => {
|
|
23
|
+
if (!Number.isInteger(e) || e <= 0) throw Error(`workflow '${n}': ${t} must be a positive integer, got ${String(e)}`);
|
|
24
|
+
return e;
|
|
25
|
+
}, l = (e, t, n) => {
|
|
26
|
+
if (typeof e != "function") throw Error(`workflow '${n}': ${t} must be a function of the payload`);
|
|
27
|
+
return e;
|
|
28
|
+
}, u = (e) => {
|
|
29
|
+
let t = e?.ast?.propertySignatures;
|
|
30
|
+
if (!Array.isArray(t)) return !1;
|
|
31
|
+
let n = t.find((e) => e.name === "items");
|
|
32
|
+
return n !== void 0 && n.type?._tag === "Arrays";
|
|
33
|
+
}, d = (t) => {
|
|
34
|
+
let { workflowName: r, control: i } = t;
|
|
35
|
+
if (i === void 0) return;
|
|
36
|
+
let a = { workflowName: r };
|
|
37
|
+
if (i.debounce !== void 0) {
|
|
38
|
+
let e = i.debounce, t = s(e.period, "debounce.period", r), n = e.timeout === void 0 ? void 0 : s(e.timeout, "debounce.timeout", r);
|
|
39
|
+
if (n !== void 0 && n < t) throw Error(`workflow '${r}': debounce.timeout (${n}ms) is shorter than debounce.period (${t}ms), so the burst can never be collapsed — the run would be admitted before the quiet period could ever elapse. Raise the timeout or lower the period.`);
|
|
40
|
+
a.debounce = {
|
|
41
|
+
key: l(e.key, "debounce.key", r),
|
|
42
|
+
periodMs: t,
|
|
43
|
+
...n === void 0 ? {} : { timeoutMs: n }
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (i.singleton !== void 0) {
|
|
47
|
+
let e = i.singleton;
|
|
48
|
+
if (e.mode !== "skip" && e.mode !== "cancel") throw Error(`workflow '${r}': singleton.mode must be 'skip' or 'cancel'. There is no default — 'skip' discards the incoming request and 'cancel' discards the running one, and choosing for you would silently throw away work either way.`);
|
|
49
|
+
a.singleton = {
|
|
50
|
+
key: l(e.key, "singleton.key", r),
|
|
51
|
+
mode: e.mode
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (i.concurrency !== void 0) {
|
|
55
|
+
let e = i.concurrency;
|
|
56
|
+
if (e.pool !== void 0 && (typeof e.pool != "string" || e.pool.trim() === "")) throw Error(`workflow '${r}': concurrency.pool must be a non-empty name — it is the identity other workflows declare to share this budget, and an empty one would silently merge every empty-named pool in the deployment into one.`);
|
|
57
|
+
a.concurrency = {
|
|
58
|
+
limit: c(e.limit, "concurrency.limit", r),
|
|
59
|
+
...e.key === void 0 ? {} : { key: l(e.key, "concurrency.key", r) },
|
|
60
|
+
...e.pool === void 0 ? {} : { pool: e.pool.trim() }
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (i.throttle !== void 0) {
|
|
64
|
+
let e = i.throttle;
|
|
65
|
+
a.throttle = {
|
|
66
|
+
limit: c(e.limit, "throttle.limit", r),
|
|
67
|
+
periodMs: s(e.period, "throttle.period", r),
|
|
68
|
+
...e.key === void 0 ? {} : { key: l(e.key, "throttle.key", r) }
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (i.rateLimit !== void 0) {
|
|
72
|
+
let e = i.rateLimit;
|
|
73
|
+
a.rateLimit = {
|
|
74
|
+
limit: c(e.limit, "rateLimit.limit", r),
|
|
75
|
+
periodMs: s(e.period, "rateLimit.period", r),
|
|
76
|
+
...e.key === void 0 ? {} : { key: l(e.key, "rateLimit.key", r) }
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (i.batch !== void 0) {
|
|
80
|
+
let o = i.batch;
|
|
81
|
+
if (t.payloadSchema !== void 0 && !u(t.payloadSchema)) throw Error(`workflow '${r}': batch is declared, so the workflow's own \`payload\` must be the BATCH shape — \`Schema.Struct({ items: Schema.Array(Item) })\` — while callers start it with a SINGLE item. Declare the item's schema as \`batch.item\`. Checked here rather than at admission because a decode failure on the batching replica is a failure nobody is watching.`);
|
|
82
|
+
a.batch = {
|
|
83
|
+
...o.key === void 0 ? {} : { key: l(o.key, "batch.key", r) },
|
|
84
|
+
maxSize: c(o.maxSize, "batch.maxSize", r),
|
|
85
|
+
timeoutMs: s(o.timeout, "batch.timeout", r),
|
|
86
|
+
...o.item === void 0 ? {} : { item: i.encrypted === !0 && n.isSchema(o.item) ? e(o.item, `workflow '${r}' batch item`) : o.item }
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (i.priority !== void 0) {
|
|
90
|
+
if (typeof i.priority != "function") throw Error(`workflow '${r}': priority must be a function of the payload`);
|
|
91
|
+
a.priority = i.priority;
|
|
92
|
+
}
|
|
93
|
+
if (i.timeouts !== void 0) {
|
|
94
|
+
let e = i.timeouts, t = e.start === void 0 ? void 0 : s(e.start, "timeouts.start", r), n = e.finish === void 0 ? void 0 : s(e.finish, "timeouts.finish", r);
|
|
95
|
+
(t !== void 0 || n !== void 0) && (a.timeouts = {
|
|
96
|
+
...t === void 0 ? {} : { startMs: t },
|
|
97
|
+
...n === void 0 ? {} : { finishMs: n }
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (a.debounce !== void 0 && a.batch !== void 0) throw Error(`workflow '${r}': debounce and batch cannot both be declared. Both collect starts and admit one run, but debounce's timer RESETS on every arrival (collapse a burst, latest payload wins) while batch's does not (accumulate items until maxSize or timeout). Pick the one you mean.`);
|
|
101
|
+
if (a.throttle !== void 0 && a.rateLimit !== void 0) throw Error(`workflow '${r}': throttle and rateLimit cannot both be declared. throttle QUEUES the excess and rateLimit DROPS it; declaring both means the drop fires first and the queue is never reached. Pick whether surplus work is late or gone.`);
|
|
102
|
+
if (i.onFailure !== void 0) {
|
|
103
|
+
if (typeof i.onFailure != "string" || i.onFailure.length === 0) throw Error(`workflow '${r}': onFailure must be the NAME of another workflow. A function cannot be used — the failure may be noticed by a different replica, minutes later, after the process that held the closure is gone.`);
|
|
104
|
+
if (i.onFailure === r) throw Error(`workflow '${r}': onFailure points at itself. A workflow that fails would then start another copy of itself, which would fail, forever — one run per failure until something else breaks. Point it at a separate notifier workflow.`);
|
|
105
|
+
a.onFailure = i.onFailure;
|
|
106
|
+
}
|
|
107
|
+
if (i.encrypted === !0 && (a.encrypted = !0), i.cancelOn !== void 0) {
|
|
108
|
+
if (!Array.isArray(i.cancelOn)) throw Error(`workflow '${r}': cancelOn must be an array of { event, schema, match }`);
|
|
109
|
+
let e = i.cancelOn.map((e, t) => {
|
|
110
|
+
let n = `cancelOn[${t}]`;
|
|
111
|
+
if (typeof e?.event != "string" || e.event.length === 0) throw Error(`workflow '${r}': ${n}.event must be a non-empty event name, exactly as ctx.events.publish writes it.`);
|
|
112
|
+
if (e.schema === void 0 || typeof e.schema.ast != "object") throw Error(`workflow '${r}': ${n}.schema must be a Schema for the event payload. It is decoded before \`match\` runs, so the predicate reads the domain type rather than raw JSON.`);
|
|
113
|
+
if (typeof e.match != "function") throw Error(`workflow '${r}': ${n}.match is required. Omitting it would mean "cancel every live run of this workflow whenever '${e.event}' is published" — write \`match: () => true\` if that is what you mean, so it is visible in review.`);
|
|
114
|
+
let i = e.within === void 0 ? void 0 : s(e.within, `${n}.within`, r);
|
|
115
|
+
return {
|
|
116
|
+
event: e.event,
|
|
117
|
+
schema: e.schema,
|
|
118
|
+
match: e.match,
|
|
119
|
+
...i === void 0 ? {} : { withinMs: i },
|
|
120
|
+
reason: e.reason ?? `cancelOn:${e.event}`
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
e.length > 0 && (a.cancelOn = e);
|
|
124
|
+
}
|
|
125
|
+
if (a.timeouts?.startMs !== void 0 && o(a).length === 0) throw Error(`workflow '${r}': timeouts.start bounds how long a run may sit in the ADMISSION queue, but this workflow declares no control that can defer one (debounce, batch, throttle, concurrency). It would never fire. Use timeouts.finish to bound the run itself.`);
|
|
126
|
+
return a;
|
|
127
|
+
}, f = (e) => {
|
|
128
|
+
let t = /* @__PURE__ */ new Map();
|
|
129
|
+
for (let n of e) {
|
|
130
|
+
let e = n?.concurrency;
|
|
131
|
+
if (e?.pool === void 0) continue;
|
|
132
|
+
let r = t.get(e.pool);
|
|
133
|
+
if (r === void 0) t.set(e.pool, {
|
|
134
|
+
limit: e.limit,
|
|
135
|
+
workflows: [n.workflowName]
|
|
136
|
+
});
|
|
137
|
+
else if (r.limit !== e.limit) throw Error(`concurrency pool '${e.pool}': '${n.workflowName}' declares limit ${e.limit}, but ${r.workflows.map((e) => `'${e}'`).join(", ")} declare${r.workflows.length === 1 ? "s" : ""} limit ${r.limit}. A pool is ONE budget — every member must declare the same number, or the enforced limit would depend on which workflow's start arrived first. Align the limits, or give the workflows different pool names if they were never meant to share.`);
|
|
138
|
+
else r.workflows.push(n.workflowName);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
//#endregion
|
|
142
|
+
export { a as DEFERRING_CONTROLS, f as assertConsistentPools, o as deferringControlsOf, i as hasCancelOn, r as hasFlowControl, d as validateFlowControl };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { START_RECEIPTS_TABLE as e, decodeWorkflowStartReceipt as t } from "./startReceipt.js";
|
|
2
|
+
import { PENDING_TABLE as n } from "./admissionStore.js";
|
|
3
|
+
import { Schema as r } from "effect";
|
|
4
|
+
import { and as i, eq as a, isNull as o } from "@voltro/database";
|
|
5
|
+
//#region src/forwardStartReceipts.ts
|
|
6
|
+
var s = async (s, c) => {
|
|
7
|
+
if (c.from === c.to) throw Error("Queue transfer requires distinct source and target");
|
|
8
|
+
let l = await s.query({
|
|
9
|
+
table: n,
|
|
10
|
+
predicate: a("id", c.to),
|
|
11
|
+
order: [],
|
|
12
|
+
take: 1,
|
|
13
|
+
skip: void 0,
|
|
14
|
+
projection: void 0
|
|
15
|
+
}), u = r.decodeUnknownSync(r.Struct({
|
|
16
|
+
workflowName: r.String,
|
|
17
|
+
tenantId: r.NullOr(r.String)
|
|
18
|
+
}))(l[0]);
|
|
19
|
+
if (u.workflowName !== c.workflowName || u.tenantId !== c.tenantId) throw Error("Receipt target belongs to a different workflow or tenant");
|
|
20
|
+
for (;;) {
|
|
21
|
+
let n = await s.query({
|
|
22
|
+
table: e,
|
|
23
|
+
predicate: i(a("pendingId", c.from), o("submissionId"), o("terminal")),
|
|
24
|
+
order: [{
|
|
25
|
+
column: "id",
|
|
26
|
+
direction: "asc"
|
|
27
|
+
}],
|
|
28
|
+
take: 100,
|
|
29
|
+
skip: void 0,
|
|
30
|
+
projection: void 0
|
|
31
|
+
});
|
|
32
|
+
if (n.length === 0) return;
|
|
33
|
+
for (let e of n) {
|
|
34
|
+
let n = t(e);
|
|
35
|
+
if (n.workflowName !== c.workflowName || n.tenantId !== c.tenantId) throw Error("Forwarded receipt belongs to a different workflow or tenant");
|
|
36
|
+
if (await s.update("_voltro_workflow_start_receipts", n.id, { pendingId: c.to }) === null) throw Error("Start receipt disappeared during queue transfer");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { s as forwardStartReceipts };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { WorkflowEngine as e } from "effect/workflow";
|
|
2
|
+
import { RateLimiter as t } from "effect/persistence";
|
|
3
|
+
import { Layer as n } from "effect";
|
|
4
|
+
//#region src/inMemoryLayer.ts
|
|
5
|
+
var r = n.merge(e.layerMemory, t.layer.pipe(n.provide(t.layerStoreMemory)));
|
|
6
|
+
//#endregion
|
|
7
|
+
export { r as inMemoryWorkflowEngineLayer };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { WorkflowRunRecorder as e } from "./recorder.js";
|
|
2
|
+
import { Layer as t } from "effect";
|
|
3
|
+
//#region src/inMemoryRecorder.ts
|
|
4
|
+
var n = () => {
|
|
5
|
+
let n = [], r = [], i = 0, a = 0;
|
|
6
|
+
return {
|
|
7
|
+
layer: t.succeed(e, {
|
|
8
|
+
startStep: async ({ runId: e, stepName: t, attempt: r, stepInput: a, retryPolicy: o }) => {
|
|
9
|
+
let s = `wfstep_mem_${++i}`;
|
|
10
|
+
return n.push({
|
|
11
|
+
id: s,
|
|
12
|
+
runId: e,
|
|
13
|
+
stepName: t,
|
|
14
|
+
attempt: r,
|
|
15
|
+
status: "running",
|
|
16
|
+
input: a ?? null,
|
|
17
|
+
retryPolicy: o ?? null,
|
|
18
|
+
output: null,
|
|
19
|
+
errorTag: null,
|
|
20
|
+
errorMessage: null,
|
|
21
|
+
errorCause: null,
|
|
22
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
23
|
+
completedAt: null,
|
|
24
|
+
durationMs: null
|
|
25
|
+
}), s;
|
|
26
|
+
},
|
|
27
|
+
endStepSuccess: async ({ stepRecordId: e, output: t, durationMs: r }) => {
|
|
28
|
+
let i = n.find((t) => t.id === e);
|
|
29
|
+
i !== void 0 && (i.status = "succeeded", i.output = t, i.completedAt = /* @__PURE__ */ new Date(), i.durationMs = r);
|
|
30
|
+
},
|
|
31
|
+
endStepFailure: async ({ stepRecordId: e, errorTag: t, errorMessage: r, errorCause: i, durationMs: a }) => {
|
|
32
|
+
let o = n.find((t) => t.id === e);
|
|
33
|
+
o !== void 0 && (o.status = "failed", o.errorTag = t, o.errorMessage = r, o.errorCause = i ?? null, o.completedAt = /* @__PURE__ */ new Date(), o.durationMs = a);
|
|
34
|
+
},
|
|
35
|
+
recordEvent: async ({ runId: e, eventType: t, payload: n, stepName: i, attempt: o }) => {
|
|
36
|
+
let s = `wfev_mem_${++a}`;
|
|
37
|
+
return r.push({
|
|
38
|
+
id: s,
|
|
39
|
+
runId: e,
|
|
40
|
+
eventType: t,
|
|
41
|
+
payload: n ?? null,
|
|
42
|
+
stepName: i ?? null,
|
|
43
|
+
attempt: o ?? null,
|
|
44
|
+
occurredAt: /* @__PURE__ */ new Date()
|
|
45
|
+
}), {
|
|
46
|
+
id: s,
|
|
47
|
+
recorded: !0
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}),
|
|
51
|
+
readSteps: () => n,
|
|
52
|
+
readEvents: () => r
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
//#endregion
|
|
56
|
+
export { n as makeInMemoryRecorder };
|