@voltro/workflow 0.85.0 → 0.87.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.
Files changed (103) hide show
  1. package/CHANGELOG.md +5987 -0
  2. package/THIRD-PARTY-NOTICES.md +1122 -31
  3. package/dist/admission.js +140 -0
  4. package/dist/admissionDrainer.js +705 -0
  5. package/dist/admissionGate.js +115 -0
  6. package/dist/admissionStore.js +347 -0
  7. package/dist/admissionSubmission.js +297 -0
  8. package/dist/admissionTables.js +137 -0
  9. package/dist/admissionTransaction.js +21 -0
  10. package/dist/awaitEvent.js +82 -0
  11. package/dist/budgetHold.js +136 -0
  12. package/dist/cancelOnPlan.js +75 -0
  13. package/dist/cancelOnSweep.js +188 -0
  14. package/dist/cancelPendingStart.js +50 -0
  15. package/dist/cluster.d.ts +111 -27
  16. package/dist/cluster.js +12 -2
  17. package/dist/clusterCron.js +19 -0
  18. package/dist/clusterDiagnostics.js +20 -0
  19. package/dist/clusterDiagnosticsSql.js +145 -0
  20. package/dist/clusterLayer.js +145 -0
  21. package/dist/clusterMigrationGate.js +61 -0
  22. package/dist/clusterRunnerLayer.js +31 -0
  23. package/dist/clusterShardGroups.js +33 -0
  24. package/dist/clusterTestSuite.d.ts +23 -0
  25. package/dist/clusterTestSuite.js +209 -179
  26. package/dist/controlMonitor.js +13 -0
  27. package/dist/controlRequest.js +367 -0
  28. package/dist/executionRefusal.js +162 -0
  29. package/dist/failureHandoff.js +183 -0
  30. package/dist/failureMessage.js +12 -0
  31. package/dist/failureRecovery.js +50 -0
  32. package/dist/failureReportStore.js +60 -0
  33. package/dist/flowControl.js +142 -0
  34. package/dist/forwardStartReceipts.js +41 -0
  35. package/dist/inMemoryLayer.js +7 -0
  36. package/dist/inMemoryRecorder.js +56 -0
  37. package/dist/index.d.ts +129 -30
  38. package/dist/index.js +50 -5
  39. package/dist/inspectWorkflow.js +101 -0
  40. package/dist/{journalEncryption-DTyhUzaS.js → journalEncryption.js} +30 -37
  41. package/dist/journalRoutingGuard.js +50 -0
  42. package/dist/journalRoutingLayout.js +47 -0
  43. package/dist/journalRoutingLock.js +39 -0
  44. package/dist/journalRoutingMaintenance.js +24 -0
  45. package/dist/journalRoutingMessageStorage.js +32 -0
  46. package/dist/journalRoutingNamespace.js +23 -0
  47. package/dist/journalRoutingRuntime.js +33 -0
  48. package/dist/journalRoutingSharding.js +28 -0
  49. package/dist/journalRoutingState.js +93 -0
  50. package/dist/journalRoutingTable.js +11 -0
  51. package/dist/journalRoutingTransaction.js +16 -0
  52. package/dist/messages.js +92 -0
  53. package/dist/nondeterminism.js +51 -0
  54. package/dist/parentClosure.js +141 -0
  55. package/dist/parentClosureRecovery.js +54 -0
  56. package/dist/pendingSubmission.js +85 -0
  57. package/dist/prepareSubmissionDispatch.js +17 -0
  58. package/dist/preparedExecution.js +14 -0
  59. package/dist/primitives.js +225 -2
  60. package/dist/queuedCancellationRequest.js +364 -0
  61. package/dist/queuedControl.js +21 -0
  62. package/dist/recorder.js +11 -0
  63. package/dist/residentExecutionEvidence.js +64 -0
  64. package/dist/retryPolicy.js +57 -0
  65. package/dist/runEventsTable.js +17 -0
  66. package/dist/runGeneration.js +345 -0
  67. package/dist/runRecording.js +615 -0
  68. package/dist/runStepsTable.js +28 -0
  69. package/dist/runnerStorageGuard.js +29 -0
  70. package/dist/runsTable.js +42 -0
  71. package/dist/scheduleDeclaration.js +44 -0
  72. package/dist/signalHint.js +23 -0
  73. package/dist/signals.js +70 -0
  74. package/dist/signalsSuspending.js +57 -0
  75. package/dist/sqliteWorkflowOwnership.js +26 -0
  76. package/dist/stalenessSweep.js +191 -0
  77. package/dist/startContextsTable.js +20 -0
  78. package/dist/startReceipt.js +143 -0
  79. package/dist/submissionRecovery.js +122 -0
  80. package/dist/submissionRejection.js +10 -0
  81. package/dist/submissionRetry.js +71 -0
  82. package/dist/suspensionUnavailable.js +9 -0
  83. package/dist/updates.js +87 -0
  84. package/dist/watermarkTable.js +9 -0
  85. package/dist/webCrypto.js +17 -0
  86. package/dist/workflowDelivery.js +24 -0
  87. package/dist/workflowExecutorRouting.js +33 -0
  88. package/dist/workflowJournal.js +124 -0
  89. package/dist/workflowJournalReset.js +111 -0
  90. package/dist/workflowReadSelections.js +12 -0
  91. package/dist/workflowRestartDispatch.js +35 -0
  92. package/dist/workflowRestartObservation.js +60 -0
  93. package/dist/workflowRestartPreparation.js +109 -0
  94. package/dist/workflowRestartReservation.js +427 -0
  95. package/dist/workflowResumeDispatch.js +30 -0
  96. package/dist/workflowRuntimeReadiness.js +12 -0
  97. package/dist/workflowShardRelocation.js +58 -0
  98. package/dist/workflowShardRelocationSql.js +72 -0
  99. package/package.json +4 -4
  100. package/dist/cluster-F_bhqVP7.js +0 -679
  101. package/dist/primitives-DX04WTWh.js +0 -507
  102. package/dist/src-C2NEyUM8.js +0 -3925
  103. package/dist/workflowReadSelections-CZAUjP7f.js +0 -2096
@@ -0,0 +1,122 @@
1
+ import { loggableCause as e } from "./journalEncryption.js";
2
+ import { readResidentExecutionEvidence as t } from "./residentExecutionEvidence.js";
3
+ import { acknowledgeAdmissionSubmissionInTransaction as n, beginAdmissionSubmissionDispatchInTransaction as r, decodeAdmissionSubmission as i, noteAdmissionSubmissionFailure as a, readPendingAdmissionSubmissionRows as o, rejectAdmissionSubmissionInTransaction as s } from "./admissionSubmission.js";
4
+ import { decodePendingPayload as c, releaseLease as l, retryBackoffMs as u } from "./admissionStore.js";
5
+ import { prepareSubmissionDispatch as d } from "./prepareSubmissionDispatch.js";
6
+ import { Schema as f } from "effect";
7
+ //#region src/submissionRecovery.ts
8
+ var p = /* @__PURE__ */ new WeakMap(), m = async (m, h, g, _) => {
9
+ let v = p.get(m), y = await o(m.store, g, v?.afterId), b = 0, x = 0, S = 0, C = 0, w = v?.nextDueAt, T = (e) => {
10
+ w = Math.min(w ?? e, e);
11
+ };
12
+ for (let o of y) {
13
+ let p;
14
+ try {
15
+ p = i(o);
16
+ } catch {
17
+ b++, S++, T(h + u(1)), m.log?.warn?.("workflow submission metadata is invalid", { submissionId: typeof o.id == "string" ? o.id : null });
18
+ continue;
19
+ }
20
+ b++;
21
+ let g = p.dispatchAttempts === 0 ? 0 : void 0, v = p.retry.recovery?.attempts ?? 0;
22
+ try {
23
+ let e = p.preparedExecutionId, i = e === null || p.dispatchAttempts === 0 ? void 0 : await m.transaction(async (n) => {
24
+ let r = await t(n, p.workflowName, e);
25
+ return r === void 0 ? void 0 : {
26
+ executionId: e,
27
+ runId: r.runId
28
+ };
29
+ });
30
+ if (i === void 0 && p.retry.stoppedAt !== null) {
31
+ b--;
32
+ continue;
33
+ }
34
+ if (i === void 0 && p.retry.after > h) {
35
+ b--, T(p.retry.after);
36
+ continue;
37
+ }
38
+ if (i === void 0 && m.acquireSubmissionCapacity !== void 0 && !await m.acquireSubmissionCapacity(p)) {
39
+ T(h + 1e3);
40
+ continue;
41
+ }
42
+ let a = i ?? await (async () => {
43
+ let e = m.controls.get(p.workflowName)?.batch === void 0 ? c(m.store, p.workflowName, p.payload) : { items: f.decodeUnknownSync(f.Struct({ items: f.Array(f.Unknown) }))(p.payload).items.map((e) => c(m.store, p.workflowName, e)) }, t = await d(m, p, e), n = await m.transaction((e) => r(e, p.id, m.now?.() ?? Date.now()));
44
+ if (n === null) return;
45
+ v = n.retry.recovery?.attempts ?? 0, g = n.dispatchAttempts === 1 ? 1 : void 0;
46
+ let i = await m.startAdmitted(p.workflowName, e, p.callerContext, t);
47
+ if (g = void 0, i.executionId !== t) throw Error("Engine returned a different prepared execution identity");
48
+ return i;
49
+ })();
50
+ if (a === void 0) continue;
51
+ g = void 0, p.eviction !== null && (await m.cancelRun({
52
+ workflowName: p.workflowName,
53
+ ...p.eviction,
54
+ reason: "cancelled to make room for a newer start (singleton mode 'cancel')",
55
+ detail: {
56
+ source: "flow-control",
57
+ mode: "singleton"
58
+ }
59
+ }), await l(m.store, p.eviction.executionId, h)), await m.transaction((e) => n(e, p.id, {
60
+ executionId: a.executionId,
61
+ runId: a.runId ?? null
62
+ })), x++;
63
+ } catch (t) {
64
+ S++;
65
+ let n;
66
+ try {
67
+ if (g !== void 0 && m.isPermanentStartError?.(t) === !0) {
68
+ let e = g, n = await m.transaction((n) => s(n, p.id, e, {
69
+ handler: m.controls.get(p.workflowName)?.onFailure ?? null,
70
+ errorMessage: String(t),
71
+ now: h
72
+ }, v));
73
+ if (n !== void 0) {
74
+ C++, m.log?.warn?.("workflow submission rejected; failure report retained", {
75
+ workflow: p.workflowName,
76
+ submissionId: p.id,
77
+ failureId: n.id
78
+ });
79
+ continue;
80
+ }
81
+ }
82
+ n = await m.transaction((e) => a(e, p.id, String(t), h, _, v));
83
+ } catch (n) {
84
+ T(h), m.log?.warn?.("workflow submission retry state could not be recorded", {
85
+ workflow: p.workflowName,
86
+ submissionId: p.id,
87
+ cause: e(p.workflowName, t),
88
+ recordingCause: String(n)
89
+ });
90
+ continue;
91
+ }
92
+ n.retry.stoppedAt === null && T(n.retry.after), m.log?.warn?.("workflow submission remains unacknowledged", {
93
+ workflow: p.workflowName,
94
+ submissionId: p.id,
95
+ cause: e(p.workflowName, t),
96
+ retryStoppedAt: n.retry.stoppedAt,
97
+ acceptance: "unknown"
98
+ });
99
+ }
100
+ }
101
+ let E = y.at(-1);
102
+ return y.length === g && E !== void 0 && typeof E.id == "string" ? (p.set(m, {
103
+ afterId: E.id,
104
+ nextDueAt: w
105
+ }), {
106
+ scanned: b,
107
+ admitted: x,
108
+ failed: S,
109
+ rejected: C,
110
+ nextDueAt: h,
111
+ examined: !0
112
+ }) : (p.delete(m), {
113
+ scanned: b,
114
+ admitted: x,
115
+ failed: S,
116
+ rejected: C,
117
+ nextDueAt: w,
118
+ examined: y.length > 0
119
+ });
120
+ };
121
+ //#endregion
122
+ export { m as recoverAdmissionSubmissions };
@@ -0,0 +1,10 @@
1
+ import { readWorkflowFailureRecord as e } from "./failureReportStore.js";
2
+ //#region src/submissionRejection.ts
3
+ var t = async (t, n) => {
4
+ if (n.failureId === null) throw Error("Workflow submission has no rejection marker");
5
+ let r = await e(t, n.failureId);
6
+ if (n.executionId !== null || n.dispatchAttempts > 1 || r === void 0 || r.sourceKind !== "submission" || r.sourceId !== n.id || r.workflowName !== n.workflowName || r.tenantId !== n.tenantId || r.report.executionId !== null || r.report.runId !== null) throw Error("Workflow start rejection has contradictory ownership or execution evidence");
7
+ return r.id;
8
+ };
9
+ //#endregion
10
+ export { t as readProvenSubmissionRejection };
@@ -0,0 +1,71 @@
1
+ import { readAdmissionSubmission as e } from "./admissionSubmission.js";
2
+ import { ADMISSIONS_TABLE as t } from "./admissionStore.js";
3
+ import { eq as n } from "@voltro/database";
4
+ //#region src/submissionRetry.ts
5
+ var r = (e, t) => ({
6
+ kind: "refused",
7
+ submissionId: e,
8
+ reason: t
9
+ }), i = async (i, a) => {
10
+ let o = await e(i, a);
11
+ if (o === void 0) return r(a, "missing-submission");
12
+ if (!o.receiptsBound) return r(a, "incomplete-transfer");
13
+ if (o.executionId !== null || o.runId !== null) return r(a, "accepted");
14
+ if (o.failureId !== null) return r(a, "rejected");
15
+ if (o.dispatchAttempts !== 0) return r(a, "dispatch-attempted");
16
+ if (o.preparedExecutionId !== null) return r(a, "prepared-execution");
17
+ let [s] = await i.query({
18
+ table: t,
19
+ predicate: n("id", o.ledgerId),
20
+ order: [],
21
+ take: 1,
22
+ skip: void 0,
23
+ projection: void 0
24
+ });
25
+ return s === void 0 ? r(a, "missing-lease") : s.workflowName !== o.workflowName || s.tenantId !== o.tenantId || s.outcome !== "admitted" ? r(a, "foreign-lease") : s.releasedAt === null ? s.executionId !== null || s.runId !== null ? r(a, "bound-lease") : {
26
+ kind: "owner",
27
+ submission: o
28
+ } : r(a, "released-lease");
29
+ }, a = (e) => e.retry.stoppedAt === null ? void 0 : {
30
+ attempts: e.retry.attempts,
31
+ stoppedAt: e.retry.stoppedAt
32
+ }, o = async (e, t) => {
33
+ let n = await i(e, t);
34
+ if (n.kind === "refused") return n;
35
+ let o = a(n.submission);
36
+ return o === void 0 ? r(t, "not-stopped") : {
37
+ kind: "eligible",
38
+ submissionId: t,
39
+ expected: o
40
+ };
41
+ }, s = async (t, n, o, s) => {
42
+ if (!Number.isSafeInteger(o.attempts) || o.attempts < 1 || !Number.isFinite(o.stoppedAt) || !Number.isFinite(s)) throw Error("Submission recovery requires a valid stopped retry token and time");
43
+ let c = (await e(t, n))?.retry.recovery;
44
+ if (c?.attempts === o.attempts && c.stoppedAt === o.stoppedAt) return {
45
+ kind: "already-retried",
46
+ submissionId: n,
47
+ expected: o
48
+ };
49
+ let l = await i(t, n);
50
+ if (l.kind === "refused") return l;
51
+ let u = a(l.submission);
52
+ if (u === void 0) return r(n, "not-stopped");
53
+ if (u.attempts !== o.attempts || u.stoppedAt !== o.stoppedAt) return r(n, "changed-retry");
54
+ let d = {
55
+ ...l.submission.retry,
56
+ after: s,
57
+ stoppedAt: null,
58
+ recovery: {
59
+ ...u,
60
+ retriedAt: s
61
+ }
62
+ };
63
+ if (await t.update("_voltro_workflow_submissions", n, { retry: d }) === null) throw Error("Admission submission disappeared during stopped retry recovery");
64
+ return {
65
+ kind: "retried",
66
+ submissionId: n,
67
+ expected: o
68
+ };
69
+ };
70
+ //#endregion
71
+ export { o as inspectAdmissionSubmissionRetry, s as retryAdmissionSubmissionInTransaction };
@@ -0,0 +1,9 @@
1
+ import { Context as e } from "effect";
2
+ //#region src/suspensionUnavailable.ts
3
+ var t = e.Reference("@voltro/workflow/SuspensionUnavailable", { defaultValue: () => void 0 }), n = class extends Error {
4
+ constructor(e, t) {
5
+ super(`${e} cannot wait for a durable result here: ${t}`), this.name = "WorkflowCannotPark";
6
+ }
7
+ };
8
+ //#endregion
9
+ export { t as SuspensionUnavailable, n as WorkflowCannotPark };
@@ -0,0 +1,87 @@
1
+ import { failureMessage as e } from "./failureMessage.js";
2
+ import { encryptionAwareActivity as t, openEventPayload as n } from "./journalEncryption.js";
3
+ import { CurrentWorkflowRunId as r, WorkflowRunRecorder as i } from "./recorder.js";
4
+ import { terminalUpdates as a } from "./workflowReadSelections.js";
5
+ import { Cause as o, Effect as s, Schema as c } from "effect";
6
+ import { eq as l } from "@voltro/database";
7
+ //#region src/updates.ts
8
+ var u = async (e, t) => {
9
+ if (e) try {
10
+ await e.recordEvent(t);
11
+ } catch {}
12
+ }, d = (t) => {
13
+ let n = o.findErrorOption(t), r = t.reasons.filter(o.isDieReason).map((e) => e.defect), i = n._tag === "Some" ? n.value : r[0] ?? o.squash(t);
14
+ return {
15
+ errorTag: i && typeof i == "object" && "_tag" in i ? String(i._tag) : null,
16
+ errorMessage: e(i)
17
+ };
18
+ }, f = (e, o) => {
19
+ let f = s.gen(function* () {
20
+ let t = o.pollIntervalMs ?? 200, f = o.maxPollIntervalMs ?? 5e3, p = o.timeoutMs ?? 3e4, m = yield* r, h = yield* s.serviceOption(i), g = h._tag === "Some" ? h.value : void 0;
21
+ if (m === void 0) return yield* s.die(/* @__PURE__ */ Error("awaitUpdate: no active workflow run id in scope"));
22
+ let _ = m, v = Date.now(), y = t;
23
+ for (;;) {
24
+ let t = Date.now() - v;
25
+ if (t > p) return yield* s.die(/* @__PURE__ */ Error(`awaitUpdate('${o.name}') timed out after ${t}ms`));
26
+ let r = {
27
+ table: "_voltro_workflow_run_events",
28
+ order: [{
29
+ column: "occurredAt",
30
+ direction: "asc"
31
+ }],
32
+ predicate: l("runId", _),
33
+ projection: void 0,
34
+ skip: void 0,
35
+ take: 1e3
36
+ }, i = yield* s.tryPromise({
37
+ try: () => e.store.query(r),
38
+ catch: (e) => e
39
+ }), m = a(i), h = i.find((e) => e.eventType === "update-requested" && e.payload?.updateName === o.name && typeof e.payload.updateId == "string" && !m.has(e.payload.updateId));
40
+ if (h !== void 0) {
41
+ let e = h.payload.updateId;
42
+ yield* s.promise(() => u(g, {
43
+ runId: _,
44
+ eventType: "update-received",
45
+ payload: {
46
+ updateId: e,
47
+ updateName: o.name
48
+ }
49
+ }));
50
+ let t = yield* c.decodeUnknownEffect(o.schema)(n(h.payload)?.value).pipe(s.tapCause((t) => s.promise(() => u(g, {
51
+ runId: _,
52
+ eventType: "update-failed",
53
+ payload: {
54
+ updateId: e,
55
+ updateName: o.name,
56
+ ...d(t)
57
+ }
58
+ })))), r = yield* (o.handle ? o.handle(t) : s.succeed(t)).pipe(s.flatMap((e) => o.success ? c.decodeUnknownEffect(o.success)(e) : s.succeed(e))).pipe(s.tapCause((t) => s.promise(() => u(g, {
59
+ runId: _,
60
+ eventType: "update-failed",
61
+ payload: {
62
+ updateId: e,
63
+ updateName: o.name,
64
+ ...d(t)
65
+ }
66
+ }))));
67
+ return yield* s.promise(() => u(g, {
68
+ runId: _,
69
+ eventType: "update-completed",
70
+ payload: {
71
+ updateId: e,
72
+ updateName: o.name,
73
+ result: r
74
+ }
75
+ })), r;
76
+ }
77
+ yield* s.sleep(`${y} millis`), y = Math.min(y * 2, f);
78
+ }
79
+ });
80
+ return t({
81
+ name: `await-update:${o.name}`,
82
+ success: o.success ?? o.schema,
83
+ execute: f
84
+ });
85
+ };
86
+ //#endregion
87
+ export { f as awaitUpdate };
@@ -0,0 +1,9 @@
1
+ import { id as e, table as t, timestamp as n } from "@voltro/database";
2
+ //#region src/watermarkTable.ts
3
+ var r = t("_voltro_workflow_watermarks", {
4
+ id: e({ prefix: "wfwm" }),
5
+ positionAt: n(),
6
+ updatedAt: n().default("now")
7
+ });
8
+ //#endregion
9
+ export { r as _voltroWorkflowWatermarksTable };
@@ -0,0 +1,17 @@
1
+ import { Crypto as e, Effect as t, Layer as n, PlatformError as r } from "effect";
2
+ //#region src/webCrypto.ts
3
+ var i = e.make({
4
+ randomBytes: (e) => globalThis.crypto.getRandomValues(new Uint8Array(e)),
5
+ digest: (e, n) => t.tryPromise({
6
+ try: async () => new Uint8Array(await globalThis.crypto.subtle.digest(e, new Uint8Array(n).buffer)),
7
+ catch: (t) => r.systemError({
8
+ _tag: "Unknown",
9
+ module: "Crypto",
10
+ method: "digest",
11
+ description: `digest ${e} failed`,
12
+ cause: t
13
+ })
14
+ })
15
+ }), a = n.succeed(e.Crypto, i);
16
+ //#endregion
17
+ export { i as webCrypto, a as webCryptoLayer };
@@ -0,0 +1,24 @@
1
+ import { Context as e, Effect as t } from "effect";
2
+ //#region src/workflowDelivery.ts
3
+ var n = "voltro-workflow-restart", r = class extends e.Service()("voltro/CurrentWorkflowDelivery") {}, i = (e) => ({
4
+ ...e,
5
+ registerEntity: (n, i, a) => e.registerEntity(n, n.type.startsWith("Workflow/") ? i.pipe(t.map((e) => {
6
+ let n = e.run;
7
+ if (typeof n != "function") throw Error("Workflow entity is missing its run handler");
8
+ return {
9
+ ...e,
10
+ run: (e) => {
11
+ let i = {
12
+ requestId: String(e.requestId),
13
+ restartToken: e.headers["voltro-workflow-restart"] ?? null
14
+ };
15
+ return t.suspend(() => {
16
+ let r = n(e);
17
+ return t.isEffect(r) ? r : t.die(/* @__PURE__ */ Error("Workflow run handler must return an Effect"));
18
+ }).pipe(t.provideService(r, i));
19
+ }
20
+ };
21
+ })) : i, a)
22
+ });
23
+ //#endregion
24
+ export { r as CurrentWorkflowDelivery, n as WORKFLOW_RESTART_HEADER, i as withWorkflowDelivery };
@@ -0,0 +1,33 @@
1
+ import { journalExecutorGroups as e } from "./journalRoutingLayout.js";
2
+ import { WorkflowEngine as t } from "effect/workflow";
3
+ import { Context as n, Effect as r, Layer as i, Option as a } from "effect";
4
+ //#region src/workflowExecutorRouting.ts
5
+ var o = n.Service("@voltro/workflow/WorkflowExecutorRouting"), s = (e, t, r, i, a) => n.add(e, o, Object.freeze({
6
+ engine: t,
7
+ mode: r,
8
+ state: i,
9
+ groups: a ?? Object.freeze({
10
+ available: Object.freeze(["default"]),
11
+ assigned: Object.freeze(r === "client" ? [] : ["default"])
12
+ })
13
+ })), c = (n, s) => {
14
+ let c = Object.freeze({
15
+ workflows: s.workflows,
16
+ schedules: s.schedules,
17
+ webhooks: s.webhooks
18
+ });
19
+ if (Object.values(c).some((e) => typeof e != "boolean")) throw Error("Explicit executor selection is required");
20
+ let l = Object.values(c).every(Boolean), u = Object.values(c).some(Boolean), d = r.gen(function* () {
21
+ let n = yield* r.serviceOption(t.WorkflowEngine);
22
+ if (a.isNone(n)) return yield* r.die(/* @__PURE__ */ Error("Executor registration requires a workflow engine"));
23
+ let i = yield* r.serviceOption(o);
24
+ if (a.isNone(i)) return l ? void 0 : yield* r.die(/* @__PURE__ */ Error("Selected executor roles require an engine routing contract"));
25
+ let s = i.value;
26
+ if (s.engine !== n.value || s.mode !== (u ? "worker" : "client")) return yield* r.die(/* @__PURE__ */ Error("Executor roles do not match the provided workflow engine"));
27
+ let d = yield* r.sync(() => e(s.state?.layout ?? "default", c)), f = (e, t) => e.length === t.length && e.every((e) => t.includes(e));
28
+ if (!f(s.groups.available, d.available) || !f(s.groups.assigned, d.assigned)) return yield* r.die(/* @__PURE__ */ Error("Executor roles do not match the engine shard ownership"));
29
+ });
30
+ return i.effectDiscard(d).pipe(i.provideMerge(n));
31
+ };
32
+ //#endregion
33
+ export { s as recordWorkflowExecutorRouting, c as withWorkflowExecutorSelection };
@@ -0,0 +1,124 @@
1
+ import { latestExitReply as e } from "./workflowReadSelections.js";
2
+ import { Context as t, Effect as n, Option as r, Schema as i } from "effect";
3
+ import { ClusterSchema as a, EntityAddress as o, EntityId as s, EntityType as c, MessageStorage as l, Sharding as u } from "effect/cluster";
4
+ //#region src/workflowJournal.ts
5
+ var d = (e, t) => `${e}/${t}`, f = (e, n, r) => {
6
+ let i = s.make(n), l = t.get(e.annotations, a.ShardGroup)(i);
7
+ return new o.EntityAddress({
8
+ entityType: c.EntityType.make(`Workflow/${e._tag}`),
9
+ entityId: i,
10
+ shardId: r.getShardId(i, l)
11
+ });
12
+ }, p = (t, i) => n.gen(function* () {
13
+ let n = yield* u.Sharding, a = yield* l.MessageStorage, o = yield* a.requestIdForPrimaryKey({
14
+ address: f(t, i, n),
15
+ tag: "run",
16
+ id: ""
17
+ });
18
+ if (r.isNone(o)) return { kind: "not-found" };
19
+ let s = String(o.value), c = yield* a.repliesForUnfiltered([o.value]), d = e(c);
20
+ return d === void 0 ? {
21
+ kind: "pending",
22
+ requestId: s
23
+ } : {
24
+ kind: "reply",
25
+ requestId: s,
26
+ replyId: String(d.id),
27
+ exit: d.exit
28
+ };
29
+ }), m = i.Union([i.Struct({
30
+ _tag: i.Literal("Success"),
31
+ value: i.Unknown
32
+ }), i.Struct({
33
+ _tag: i.Literal("Failure"),
34
+ cause: i.Array(i.Union([
35
+ i.Struct({
36
+ _tag: i.Literal("Fail"),
37
+ error: i.Unknown
38
+ }),
39
+ i.Struct({
40
+ _tag: i.Literal("Die"),
41
+ defect: i.Unknown
42
+ }),
43
+ i.Struct({
44
+ _tag: i.Literal("Interrupt"),
45
+ fiberId: i.optional(i.NullOr(i.Number))
46
+ })
47
+ ]))
48
+ })]), h = i.decodeUnknownOption(m), g = (e) => {
49
+ let t = (e) => e && typeof e == "object" && "_tag" in e && typeof e._tag == "string" ? e._tag : void 0, n = h(e);
50
+ if (r.isNone(n)) return "unrecognized-reply";
51
+ if (n.value._tag === "Failure") return "failed";
52
+ let i = n.value.value, a = t(i);
53
+ if (a === "Suspended") return "suspended";
54
+ if (a === "Complete") {
55
+ let e = i.exit, t = h(e);
56
+ return r.isNone(t) ? "unrecognized-reply" : t.value._tag === "Success" ? "succeeded" : "failed";
57
+ }
58
+ return "unrecognized-reply";
59
+ }, _ = (e) => y(e.workflow, e.executionId, e.failedActivities), v = (e) => y(e.workflow, e.executionId, e.resetActivities), y = (t, i, a) => n.gen(function* () {
60
+ let o = yield* u.Sharding, s = yield* l.MessageStorage, c = f(t, i, o), p = yield* s.requestIdForPrimaryKey({
61
+ address: c,
62
+ tag: "run",
63
+ id: ""
64
+ }).pipe(n.orDie);
65
+ if (r.isNone(p)) return {
66
+ redriven: !1,
67
+ runReplyCleared: !1,
68
+ activitiesReset: 0,
69
+ reason: "no-journal"
70
+ };
71
+ let m = yield* s.repliesForUnfiltered([p.value]).pipe(n.orDie), h = e(m);
72
+ if (h === void 0) return {
73
+ redriven: !1,
74
+ runReplyCleared: !1,
75
+ activitiesReset: 0,
76
+ reason: "no-reply"
77
+ };
78
+ let _ = g(h.exit);
79
+ if (_ !== "failed") return {
80
+ redriven: !1,
81
+ runReplyCleared: !1,
82
+ activitiesReset: 0,
83
+ reason: _
84
+ };
85
+ let v = /* @__PURE__ */ new Set();
86
+ for (let e of a) {
87
+ let t = yield* s.requestIdForPrimaryKey({
88
+ address: c,
89
+ tag: "activity",
90
+ id: d(e.name, e.attempt)
91
+ }).pipe(n.orDie);
92
+ if (r.isNone(t)) return {
93
+ redriven: !1,
94
+ runReplyCleared: !1,
95
+ activitiesReset: 0,
96
+ reason: "activity-not-found"
97
+ };
98
+ v.add(t.value);
99
+ }
100
+ let y = 0;
101
+ for (let e of v) {
102
+ if (!(yield* o.reset(e))) return {
103
+ redriven: !1,
104
+ runReplyCleared: !1,
105
+ activitiesReset: y,
106
+ reason: "activity-reset-unconfirmed"
107
+ };
108
+ y++;
109
+ }
110
+ let b = yield* o.reset(p.value);
111
+ return b ? (yield* o.pollStorage, {
112
+ redriven: !0,
113
+ runReplyCleared: b,
114
+ activitiesReset: y,
115
+ reason: null
116
+ }) : {
117
+ redriven: !1,
118
+ runReplyCleared: !1,
119
+ activitiesReset: y,
120
+ reason: "run-reset-unconfirmed"
121
+ };
122
+ });
123
+ //#endregion
124
+ export { m as JournalExit, d as activityPrimaryKey, g as classifyRunReply, p as readWorkflowRunReply, _ as redriveFailedRun, v as resumeRunFromStep, f as runEntityAddress };
@@ -0,0 +1,111 @@
1
+ import { WORKFLOW_RESTART_HEADER as e } from "./workflowDelivery.js";
2
+ import { Effect as t, Schema as n } from "effect";
3
+ import { SqlClient as r } from "effect/sql";
4
+ //#region src/workflowJournalReset.ts
5
+ var i = n.fromJsonString(n.Struct({
6
+ _tag: n.Literal("Success"),
7
+ value: n.Struct({ _tag: n.Literal("Suspended") })
8
+ })), a = n.fromJsonString(n.Record(n.String, n.String)), o = (e) => t.gen(function* () {
9
+ if (!/^[1-9]\d*$/.test(e.requestId) || e.workflowName.length === 0 || e.executionId.length === 0) return yield* t.die(/* @__PURE__ */ Error("Suspension observation requires an exact engine delivery owner"));
10
+ let [o] = yield* (yield* r.SqlClient).withoutTransforms()`
11
+ SELECT m.entity_type, m.entity_id, m.tag, m.headers, r.id AS reply_id, r.payload
12
+ FROM cluster_messages m LEFT JOIN cluster_replies r
13
+ ON r.id = m.last_reply_id AND r.request_id = m.id AND r.kind = 0
14
+ WHERE m.id = ${e.requestId} AND m.kind = 0`;
15
+ if (o === void 0) return;
16
+ if (o.entity_type !== `Workflow/${e.workflowName}` || o.entity_id !== e.executionId || o.tag !== "run") return yield* t.die(/* @__PURE__ */ Error("Suspension observation belongs to another execution or procedure"));
17
+ if (((yield* t.sync(() => n.decodeUnknownSync(a)(o.headers)))["voltro-workflow-restart"] ?? null) !== e.restartToken || o.reply_id === null || n.decodeUnknownOption(i)(o.payload)._tag === "None") return;
18
+ let s = String(o.reply_id);
19
+ return /^[1-9]\d*$/.test(s) ? {
20
+ ...e,
21
+ replyId: s
22
+ } : yield* t.die(/* @__PURE__ */ Error("Suspension reply has an invalid identity"));
23
+ }).pipe(t.provideService(r.SafeIntegers, !0)), s = (a, o) => t.gen(function* () {
24
+ let { restart: s, suspension: c } = o, l = /* @__PURE__ */ new Map();
25
+ for (let e of a) {
26
+ if (!/^[1-9]\d*$/.test(e.requestId) || !/^[1-9]\d*$/.test(e.replyId)) return yield* t.die(/* @__PURE__ */ Error("Journal reset requires positive decimal request and reply identities"));
27
+ let n = l.get(e.requestId);
28
+ if (n !== void 0 && n !== e.replyId) return yield* t.die(/* @__PURE__ */ Error("Journal reset contains conflicting observations for one request"));
29
+ l.set(e.requestId, e.replyId);
30
+ }
31
+ if (s !== void 0 && (!l.has(s.requestId) || s.token.length === 0 || s.token === s.previousToken || s.workflowName.length === 0 || s.executionId.length === 0)) return yield* t.die(/* @__PURE__ */ Error("Journal restart requires a selected request, new token and execution owner"));
32
+ if (l.size === 0) return {
33
+ kind: "reset",
34
+ count: 0
35
+ };
36
+ let u = [...l].sort(([e], [t]) => e < t ? -1 : +(e > t)), d = (yield* r.SqlClient).withoutTransforms(), f = d("cluster_messages"), p = d("cluster_replies"), m = d.onDialectOrElse({
37
+ pg: () => d.literal("FALSE"),
38
+ orElse: () => d.literal("0")
39
+ });
40
+ return yield* t.gen(function* () {
41
+ let r = /* @__PURE__ */ new Map();
42
+ for (let [e] of u) {
43
+ let n = yield* d.onDialectOrElse({
44
+ mssql: () => d`
45
+ SELECT last_reply_id FROM ${f} WITH (UPDLOCK, HOLDLOCK)
46
+ WHERE id = ${e} AND kind = 0`,
47
+ sqlite: () => d`
48
+ SELECT last_reply_id FROM ${f} WHERE id = ${e} AND kind = 0`,
49
+ orElse: () => d`
50
+ SELECT last_reply_id FROM ${f} WHERE id = ${e} AND kind = 0 FOR UPDATE`
51
+ });
52
+ if (n.length !== 1) return {
53
+ kind: "changed",
54
+ requestId: e
55
+ };
56
+ if (r.set(e, String(n[0]?.last_reply_id)), s !== void 0) {
57
+ let [n] = yield* d`
58
+ SELECT entity_type, entity_id, tag FROM ${f} WHERE id = ${e}`;
59
+ if (n?.entity_type !== `Workflow/${s.workflowName}` || n.entity_id !== s.executionId || n.tag !== (e === s.requestId ? "run" : "activity")) return yield* t.die(/* @__PURE__ */ Error("Journal restart selection belongs to another execution or procedure"));
60
+ }
61
+ }
62
+ let a;
63
+ if (c !== void 0) {
64
+ let [e] = yield* d`
65
+ SELECT entity_type, entity_id, tag, headers FROM ${f} WHERE id = ${c.requestId}`;
66
+ if (e?.entity_type !== `Workflow/${c.workflowName}` || e.entity_id !== c.executionId || e.tag !== "run" || c.workflowName.length === 0 || c.executionId.length === 0) return yield* t.die(/* @__PURE__ */ Error("Journal resume selection belongs to another execution or procedure"));
67
+ if (((yield* t.sync(() => n.decodeUnknownSync(n.fromJsonString(n.Record(n.String, n.String)))(e.headers)))["voltro-workflow-restart"] ?? null) !== c.restartToken) return {
68
+ kind: "changed",
69
+ requestId: c.requestId
70
+ };
71
+ }
72
+ if (s !== void 0) {
73
+ let [e] = yield* d`
74
+ SELECT tag, entity_type, headers FROM ${f} WHERE id = ${s.requestId}`;
75
+ if (e?.tag !== "run" || !e.entity_type.startsWith("Workflow/")) return yield* t.die(/* @__PURE__ */ Error("Journal restart marker can only target a workflow run"));
76
+ if (a = yield* t.sync(() => n.decodeUnknownSync(n.fromJsonString(n.Record(n.String, n.String)))(e.headers)), a["voltro-workflow-restart"] === s.token) return {
77
+ kind: "already-reset",
78
+ count: u.length
79
+ };
80
+ if ((a["voltro-workflow-restart"] ?? null) !== s.previousToken) return {
81
+ kind: "changed",
82
+ requestId: s.requestId
83
+ };
84
+ }
85
+ for (let [e, t] of u) {
86
+ if (r.get(e) !== t) return {
87
+ kind: "changed",
88
+ requestId: e
89
+ };
90
+ let a = yield* d`SELECT payload FROM ${p}
91
+ WHERE id = ${t} AND request_id = ${e} AND kind = 0`;
92
+ if (a.length !== 1 || c !== void 0 && n.decodeUnknownOption(i)(a[0]?.payload)._tag === "None") return {
93
+ kind: "changed",
94
+ requestId: e
95
+ };
96
+ }
97
+ s !== void 0 && (yield* d`UPDATE ${f} SET headers = ${JSON.stringify({
98
+ ...a,
99
+ [e]: s.token
100
+ })}
101
+ WHERE id = ${s.requestId}`);
102
+ for (let [e] of u) yield* d`DELETE FROM ${p} WHERE request_id = ${e} AND kind = 0`, yield* d`DELETE FROM ${f} WHERE request_id = ${e} AND kind = 2`, yield* d`UPDATE ${f} SET processed = ${m}, last_reply_id = NULL,
103
+ last_read = NULL WHERE request_id = ${e}`;
104
+ return {
105
+ kind: "reset",
106
+ count: u.length
107
+ };
108
+ }).pipe(d.withTransaction, t.provideService(r.SafeIntegers, !0));
109
+ }), c = (e, t) => s(e, t === void 0 ? {} : { restart: t }), l = (e) => s([e], { suspension: e });
110
+ //#endregion
111
+ export { o as observeWorkflowSuspension, c as resetExpectedWorkflowReplies, l as resetExpectedWorkflowSuspension };
@@ -0,0 +1,12 @@
1
+ //#region src/workflowReadSelections.ts
2
+ var e = (e) => e.findLast((e, t, n) => t in n && e._tag === "WithExit"), t = (e) => {
3
+ let t = /* @__PURE__ */ new Set();
4
+ for (let n of e) {
5
+ if (n.eventType !== "update-completed" && n.eventType !== "update-failed") continue;
6
+ let e = n.payload?.updateId;
7
+ typeof e == "string" && e.length > 0 && t.add(e);
8
+ }
9
+ return t;
10
+ };
11
+ //#endregion
12
+ export { e as latestExitReply, t as terminalUpdates };
@@ -0,0 +1,35 @@
1
+ import { JournalRoutingGuard as e, protectJournalRoutingEffect as t, tryOwnedJournalRoutingPromise as n } from "./journalRoutingGuard.js";
2
+ import { resetReservedWorkflowJournal as r } from "./workflowRestartReservation.js";
3
+ import { JournalRoutingLayout as i, routeJournalWorkflow as a } from "./journalRoutingRuntime.js";
4
+ import { prepareWorkflowRestartCommand as o } from "./workflowRestartPreparation.js";
5
+ import { observeWorkflowRestartReplies as s } from "./workflowRestartObservation.js";
6
+ import { Context as c, Effect as l, Option as u } from "effect";
7
+ import { SqlClient as d } from "effect/sql";
8
+ import { MessageStorage as f, Sharding as p } from "effect/cluster";
9
+ //#region src/workflowRestartDispatch.ts
10
+ var m = (e) => l.gen(function* () {
11
+ let t = yield* r(e);
12
+ return t.kind === "waiting-capacity" ? t : (yield* (yield* p.Sharding).pollStorage, {
13
+ kind: "attempted",
14
+ journal: t
15
+ });
16
+ }), h = class extends c.Service()("@voltro/workflow/WorkflowRestartDispatcher") {}, g = l.gen(function* () {
17
+ let r = u.getOrUndefined(yield* l.serviceOption(e)), c = u.getOrUndefined(yield* l.serviceOption(i)), g = yield* d.SqlClient, _ = yield* p.Sharding, v = yield* f.MessageStorage, y = (e) => t(r, s(c === void 0 ? e : {
18
+ ...e,
19
+ workflow: a(e.workflow, c)
20
+ })).pipe(l.provideService(d.SqlClient, g), l.provideService(p.Sharding, _), l.provideService(f.MessageStorage, v));
21
+ return h.of({
22
+ prepare: (e) => t(r, n((t) => o({
23
+ ...e,
24
+ transaction: (n) => {
25
+ if (t.throwIfAborted(), r?.isClosed()) throw Error("Journal routing admission is closed");
26
+ return e.transaction(n);
27
+ },
28
+ observe: (e) => l.runPromise(y(e), { signal: t })
29
+ }))),
30
+ observe: y,
31
+ dispatch: (e) => t(r, m(e)).pipe(l.provideService(d.SqlClient, g), l.provideService(p.Sharding, _))
32
+ });
33
+ });
34
+ //#endregion
35
+ export { h as WorkflowRestartDispatcher, m as dispatchWorkflowRestart, g as makeWorkflowRestartDispatcher };