@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,145 @@
1
+ import { JournalRoutingGuard as e } from "./journalRoutingGuard.js";
2
+ import { withWorkflowDelivery as t } from "./workflowDelivery.js";
3
+ import { ClusterDiagnostics as n } from "./clusterDiagnostics.js";
4
+ import { WorkflowRuntimeReadiness as r, makeWorkflowRuntimeReadiness as i } from "./workflowRuntimeReadiness.js";
5
+ import { journalRoutingLayout as a } from "./journalRoutingLayout.js";
6
+ import { recordWorkflowExecutorRouting as o } from "./workflowExecutorRouting.js";
7
+ import { JournalRoutingLayout as s, withJournalWorkflowRouting as c } from "./journalRoutingRuntime.js";
8
+ import { captureWorkflowShardGroups as ee } from "./clusterShardGroups.js";
9
+ import { acquireGuardedPostgresJournalRoutingAdmission as te, captureJournalRoutingState as l } from "./journalRoutingState.js";
10
+ import { WorkflowResumeDispatcher as u, makeWorkflowResumeDispatcher as d } from "./workflowResumeDispatch.js";
11
+ import { WorkflowRestartDispatcher as f, makeWorkflowRestartDispatcher as p } from "./workflowRestartDispatch.js";
12
+ import { clusterMigrationGateLayer as ne } from "./clusterMigrationGate.js";
13
+ import { clusterRunnerLayer as m } from "./clusterRunnerLayer.js";
14
+ import { acquireSqliteWorkflowOwnership as h } from "./sqliteWorkflowOwnership.js";
15
+ import { makeClusterDiagnostics as g } from "./clusterDiagnosticsSql.js";
16
+ import { WorkflowEngine as _ } from "effect/workflow";
17
+ import { RateLimiter as v } from "effect/persistence";
18
+ import { Context as y, Duration as b, Effect as x, Layer as S, Option as C } from "effect";
19
+ import { SqlClient as w } from "effect/sql";
20
+ import { ClusterWorkflowEngine as T, MessageStorage as E, RunnerAddress as D, ShardId as O, Sharding as k } from "effect/cluster";
21
+ import { sqlPoolCapacity as A } from "@voltro/database/sql";
22
+ import { createLogger as j } from "@voltro/logger";
23
+ //#region src/clusterLayer.ts
24
+ var M = j({ scope: "voltro:workflow" }), N = 34e3, P = (e) => {
25
+ let t = e.runnerListenHost ?? process.env.POD_IP ?? process.env.VOLTRO_WORKFLOW_RUNNER_HOST ?? "localhost";
26
+ return {
27
+ host: t,
28
+ port: e.runnerListenPort ?? 34e3,
29
+ localhostRisk: e.runnerStorage === "sql" && (t === "localhost" || t === "127.0.0.1")
30
+ };
31
+ }, F = (e) => {
32
+ let t = e.sqlRunnerSupported ?? !e.sqliteFamily, n = t ? "sql" : "memory", r = e.override ?? process.env.VOLTRO_WORKFLOW_RUNNER_STORAGE;
33
+ if (r === void 0 || r.trim() === "") return n;
34
+ let i = r.trim().toLowerCase();
35
+ if (i !== "memory" && i !== "sql") throw Error(`VOLTRO_WORKFLOW_RUNNER_STORAGE must be 'memory' or 'sql', got '${r}'.`);
36
+ if (i === "sql" && !t) throw Error("VOLTRO_WORKFLOW_RUNNER_STORAGE='sql' is unsupported for this database connection. Use 'memory' for a local or embedded SQLite-family connection.");
37
+ return i;
38
+ }, I = (e = process.env.VOLTRO_WORKFLOW_SHARD_LOCK) => {
39
+ if (e === void 0 || e.trim() === "") return "auto";
40
+ let t = e.trim().toLowerCase();
41
+ if (t === "auto" || t === "row" || t === "advisory") return t;
42
+ throw Error(`VOLTRO_WORKFLOW_SHARD_LOCK must be 'auto', 'row', or 'advisory', got '${e}'.`);
43
+ }, L = x.gen(function* () {
44
+ let e = yield* (yield* w.SqlClient)`SELECT @@wsrep_on AS wsrep`.pipe(x.catch(() => x.succeed([])));
45
+ if (e.length === 0) return !1;
46
+ let t = e[0].wsrep;
47
+ return t === 1 || t === "1" || t === !0 || t === "ON" || t === "on";
48
+ }), R = (e) => x.gen(function* () {
49
+ if (e.runnerStorage === "memory") return !1;
50
+ if (e.dialectId === "sqlite" || e.dialectId === "turso" || e.mode === "row") return !0;
51
+ if (e.mode === "advisory") return !1;
52
+ if (e.dialectId === "postgres") {
53
+ let e = (yield* (yield* w.SqlClient)`SELECT current_schema() AS schema`.pipe(x.orDie))[0]?.schema;
54
+ return e ? e !== "public" : yield* x.die(/* @__PURE__ */ Error("PostgreSQL workflow coordination requires an accessible current schema"));
55
+ }
56
+ return e.dialectId !== "mysql" && e.dialectId !== "mariadb" ? !1 : yield* L;
57
+ }), z = (e) => ({
58
+ coordination: e.disableAdvisory ? "row-based" : "advisory",
59
+ dialect: e.dialectId ?? "unknown",
60
+ mode: e.mode,
61
+ ...e.disableAdvisory && e.mode === "auto" ? { reason: e.dialectId === "sqlite" || e.dialectId === "turso" ? "SQLite-family connection uses row locks" : e.dialectId === "postgres" ? "PostgreSQL non-public schema uses row leases" : "wsrep/Galera cluster detected" } : {}
62
+ }), B = () => {
63
+ let e = (e) => {
64
+ if (e === void 0) return;
65
+ let t = Number(e);
66
+ return Number.isFinite(t) && t > 0 ? t : void 0;
67
+ }, t = e(process.env.VOLTRO_WORKFLOW_FAILOVER_LEASE), n = e(process.env.VOLTRO_WORKFLOW_FAILOVER_HEARTBEAT), r = e(process.env.VOLTRO_WORKFLOW_POLL_INTERVAL);
68
+ return {
69
+ ...t === void 0 ? {} : { failoverLeaseSeconds: t },
70
+ ...n === void 0 ? {} : { failoverHeartbeatSeconds: n },
71
+ ...r === void 0 ? {} : { messagePollSeconds: r }
72
+ };
73
+ }, V = x.flatMap(k.Sharding, (e) => e.pollStorage), H = /* @__PURE__ */ new Set(), U = (e) => Object.assign(Object.create(Object.getPrototypeOf(e)), e, { requestIdForPrimaryKey: (t) => e.requestIdForPrimaryKey(t).pipe(x.provideService(w.SafeIntegers, !0)) }), W = (j) => {
74
+ let N = j.runnerMode === void 0 ? "worker" : j.runnerMode;
75
+ if (N !== "worker" && N !== "client") throw Error("Invalid workflow runner mode");
76
+ let F = N === "client", L = j.journalRouting === void 0 ? void 0 : l(j.journalRouting);
77
+ if (L !== void 0 && j.dialectId !== "postgres") throw Error("Guarded journal routing currently requires PostgreSQL");
78
+ let B = L === void 0 ? void 0 : a(L.layout), V = ee(j.shardGroups ?? (B === void 0 ? void 0 : {
79
+ available: B.available,
80
+ assigned: F ? [] : B.available
81
+ }), F);
82
+ if (B !== void 0 && (V?.available.length !== B.available.length || B.available.some((e) => !V?.available.includes(e)))) throw Error("Shard inventory must match the admitted journal routing layout");
83
+ if (F && (j.runnerStorage !== "sql" || j.dialectId === "sqlite")) throw Error("Client-only workflows require shared remote SQL runner storage");
84
+ if (F && (j.extraShardingLayers?.length ?? 0) > 0) throw Error("Client-only workflows cannot own extra sharding layers");
85
+ let W = P({
86
+ runnerStorage: j.runnerStorage,
87
+ runnerListenHost: j.runnerListenHost,
88
+ runnerListenPort: j.runnerListenPort
89
+ }), G = W.host, K = W.port;
90
+ !F && W.localhostRisk && !H.has(G) && (H.add(G), M.warn("runner host is not routable from other pods with SQL cluster storage — a workflow cannot resume on another pod after a reschedule. Inject POD_IP via the K8s downward API (fieldRef: status.podIP) or set runnerListenHost / VOLTRO_WORKFLOW_RUNNER_HOST.", { runnerHost: G }));
91
+ let q = j.dialectId === "sqlite" ? S.effect(w.SqlClient, x.gen(function* () {
92
+ let e = yield* w.SqlClient, t = (yield* e.unsafe("PRAGMA database_list")).find((e) => e.name === "main");
93
+ return t === void 0 ? yield* x.die(/* @__PURE__ */ Error("SQLite workflow connection has no main database")) : (t.file !== "" && (yield* h(t.file)), e);
94
+ }).pipe(x.orDie)).pipe(S.provide(j.sqlClientLayer)) : j.sqlClientLayer, J = ne({
95
+ runnerStorage: j.runnerStorage,
96
+ charset: j.charset
97
+ }).pipe(S.provide(q)), Y = L === void 0 ? J : S.effect(e, x.gen(function* () {
98
+ let e = yield* w.SqlClient, t = A(e), n = j.runnerStorage === "sql" ? 3 : 2;
99
+ return t !== void 0 && t < n ? yield* x.die(/* @__PURE__ */ Error(`Guarded journal routing requires at least ${n} SQL connections for the admission lock, runner ownership and journal work`)) : yield* te(L);
100
+ }).pipe(x.orDie)).pipe(S.provideMerge(J)), X = j.shardLockMode ?? I(), re = S.unwrap(R({
101
+ dialectId: j.dialectId,
102
+ runnerStorage: j.runnerStorage,
103
+ mode: X
104
+ }).pipe(x.map((e) => {
105
+ !F && j.runnerStorage === "sql" && M.info("shard-lock coordination", z({
106
+ disableAdvisory: e,
107
+ dialectId: j.dialectId,
108
+ mode: X
109
+ }));
110
+ let t = S.effect(n, x.flatMap(k.Sharding, (t) => g({
111
+ ownAddress: D.make(G, K).toString(),
112
+ shardLockMode: j.runnerStorage === "sql" ? e ? "row" : "advisory" : "none",
113
+ runnerStorage: j.runnerStorage,
114
+ dialectId: j.dialectId,
115
+ hasShard: (e) => t.hasShardId(O.fromString(e))
116
+ }))), a = {
117
+ ...V === void 0 ? {} : {
118
+ availableShardGroups: V.available,
119
+ assignedShardGroups: V.assigned
120
+ },
121
+ runnerAddress: F ? C.none() : C.some(D.make(G, K)),
122
+ runnerListenAddress: F ? C.none() : C.some(D.make(G, K)),
123
+ ...e ? { shardLockDisableAdvisory: !0 } : {},
124
+ ...j.failoverLeaseSeconds === void 0 ? {} : { shardLockExpiration: b.seconds(j.failoverLeaseSeconds) },
125
+ ...j.failoverHeartbeatSeconds === void 0 ? {} : { shardLockRefreshInterval: b.seconds(j.failoverHeartbeatSeconds) },
126
+ ...j.messagePollSeconds === void 0 ? {} : { entityMessagePollInterval: b.seconds(j.messagePollSeconds) }
127
+ }, o = b.seconds(j.failoverHeartbeatSeconds ?? 10), s = m({
128
+ clientOnly: F,
129
+ runnerStorage: j.runnerStorage,
130
+ shardingConfig: a,
131
+ guard: {
132
+ interval: o,
133
+ giveUpAfter: b.times(o, 3),
134
+ log: M
135
+ }
136
+ });
137
+ return S.mergeAll(S.effect(r, i), t, S.effect(u, d), S.effect(f, p)).pipe(S.provideMerge(s));
138
+ }))).pipe(S.provide(L === void 0 ? Y : S.merge(Y, S.succeed(s, L.layout)))), Z = j.extraShardingLayers ?? [], Q = F ? S.effect(_.WorkflowEngine, T.make) : T.layer, $ = L === void 0 ? Q : Q.pipe(S.flatMap((e) => S.succeed(_.WorkflowEngine, c(y.get(e, _.WorkflowEngine), L.layout)))), ie = Z.length === 0 ? $ : S.mergeAll($, ...Z), ae = S.flatMap(re, (e) => {
139
+ let n = y.add(e, k.Sharding, t(y.get(e, k.Sharding)));
140
+ return S.succeedContext(j.dialectId === "sqlite" ? y.add(n, E.MessageStorage, U(y.get(n, E.MessageStorage))) : n);
141
+ }), oe = v.layer.pipe(S.provide(v.layerStoreMemory));
142
+ return S.merge(ie, oe).pipe(S.provideMerge(ae), S.flatMap((e) => S.succeedContext(o(e, y.get(e, _.WorkflowEngine), N, L, V))));
143
+ };
144
+ //#endregion
145
+ export { N as DEFAULT_RUNNER_LISTEN_PORT, L as detectWsrepCluster, I as parseShardLockMode, V as pollWorkflowStorage, B as resolveFailoverTuning, P as resolveRunnerIdentity, F as resolveRunnerStorage, R as resolveShardLockDisableAdvisory, z as shardLockLogFields, W as workflowEngineLayer };
@@ -0,0 +1,61 @@
1
+ import { webCryptoLayer as e } from "./webCrypto.js";
2
+ import { Effect as t, Layer as n } from "effect";
3
+ import { SqlClient as r } from "effect/sql";
4
+ import { ShardingConfig as i, SqlMessageStorage as a, SqlRunnerStorage as o } from "effect/cluster";
5
+ import { applyUndeclaredCharsetWork as s, fnv1a64 as c, readUndeclaredCharsetColumns as l, resolveMigrationLockSchema as u, sqlPoolCapacity as d, undeclaredCharsetWork as f } from "@voltro/database/sql";
6
+ //#region src/clusterMigrationGate.ts
7
+ var p = 6322741009312601n, m = () => {
8
+ let e = u();
9
+ return e === void 0 ? p : c(`voltro_cluster_migration_lock:${e}`) & 9223372036854775807n;
10
+ }, h = () => {
11
+ let e = `voltro_cluster_migration_${p.toString(36)}`, t = u();
12
+ if (t === void 0) return e;
13
+ let n = `${e}_${t}`;
14
+ return /^[A-Za-z0-9_]+$/.test(t) && n.length <= 64 ? n : `${e}_h${c(`voltro_cluster_migration_lock:${t}`).toString(16)}`;
15
+ }, g = () => {
16
+ let e = Number(process.env.VOLTRO_CLUSTER_MIGRATION_LOCK_TIMEOUT_MS);
17
+ return Number.isFinite(e) && e > 0 ? e : 6e4;
18
+ }, _ = 250, v = (e) => e[0]?.[0], y = (e, n) => e.onDialectOrElse({
19
+ sqlite: () => t.void,
20
+ mysql: () => n.executeValues(`SELECT GET_LOCK('${h()}', ${Math.ceil(g() / 1e3)})`, []).pipe(t.flatMap((e) => v(e) === 1 || v(e) === "1" ? t.void : t.die(/* @__PURE__ */ Error(`could not acquire cluster migration lock '${h()}' (GET_LOCK timed out)`)))),
21
+ mssql: () => n.executeValues(`DECLARE @res int; EXEC @res = sp_getapplock @Resource = '${h()}', @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = ${g()}; SELECT @res`, []).pipe(t.flatMap((e) => Number(v(e) ?? -999) >= 0 ? t.void : t.die(/* @__PURE__ */ Error(`could not acquire cluster migration lock '${h()}' (sp_getapplock ${String(v(e))})`)))),
22
+ orElse: () => {
23
+ let e = m().toString(), r = Date.now(), i = n.executeValues(`SELECT pg_try_advisory_lock(${e}::bigint)`, []).pipe(t.flatMap((e) => {
24
+ let n = v(e);
25
+ return n === !0 || n === "t" || n === 1 ? t.void : Date.now() - r >= g() ? t.die(/* @__PURE__ */ Error(`could not acquire the cluster migration advisory lock within ${Math.round(g() / 1e3)}s. Another runner is migrating the cluster schema, or a prior boot wedged while holding it. (Raise VOLTRO_CLUSTER_MIGRATION_LOCK_TIMEOUT_MS, or inspect pg_locks for the holder.)`)) : t.sleep(`${_} millis`).pipe(t.andThen(i));
26
+ }));
27
+ return i;
28
+ }
29
+ }), b = (e, n) => e.onDialectOrElse({
30
+ sqlite: () => t.void,
31
+ mysql: () => t.asVoid(n.executeValues(`SELECT RELEASE_LOCK('${h()}')`, [])),
32
+ mssql: () => t.asVoid(n.executeValues(`EXEC sp_releaseapplock @Resource = '${h()}', @LockOwner = 'Session'`, [])),
33
+ orElse: () => t.asVoid(n.executeValues(`SELECT pg_advisory_unlock(${m().toString()}::bigint)`, []))
34
+ }), x = (e, n) => e.onDialectOrElse({
35
+ sqlite: () => !0,
36
+ orElse: () => !1
37
+ }) ? n : t.scoped(t.gen(function* () {
38
+ let r = yield* e.reserve;
39
+ return yield* t.acquireRelease(y(e, r), () => b(e, r).pipe(t.orDie)), yield* n;
40
+ })), S = (e) => e === "sql" ? ["cluster_messages", "cluster_runners"] : ["cluster_messages"], C = (e, n) => e.onDialectOrElse({
41
+ pg: () => e`SELECT (to_regclass(${n}) IS NOT NULL) AS present`,
42
+ mysql: () => e`SELECT (COUNT(*) > 0) AS present FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ${n}`,
43
+ mssql: () => e`SELECT CASE WHEN OBJECT_ID(${n}, 'U') IS NULL THEN 0 ELSE 1 END AS present`,
44
+ orElse: () => e`SELECT (COUNT(*) > 0) AS present FROM sqlite_master WHERE type = 'table' AND name = ${n}`
45
+ }).pipe(t.map((e) => {
46
+ let t = e[0]?.present;
47
+ return t === !0 || t === 1 || t === "1" || t === "t" || typeof t == "bigint" && t === 1n;
48
+ }), t.catch(() => t.succeed(!1))), w = (e, n) => t.forEach(S(n), (t) => C(e, t), { concurrency: 1 }).pipe(t.map((e) => e.every(Boolean))), T = (a, o) => t.scoped(t.asVoid(n.build(o.pipe(n.provide(i.layerDefaults), n.provide(n.succeed(r.SqlClient, a)), n.provide(e))))), E = (e, n) => T(e, a.layer).pipe(t.andThen(n === "sql" ? T(e, o.layer) : t.void)), D = (e) => t.map(e.unsafe("SELECT table_name AS t FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name LIKE 'cluster\\_%'"), (e) => new Set(e.map((e) => String(e.t ?? e.T)))), O = (e, n, r) => r === void 0 || !e.onDialectOrElse({
49
+ mysql: () => !0,
50
+ orElse: () => !1
51
+ }) ? E(e, n) : t.gen(function* () {
52
+ let t = yield* D(e);
53
+ yield* E(e, n);
54
+ let i = new Set([...yield* D(e)].filter((e) => !t.has(e)));
55
+ i.size !== 0 && (yield* s(e, f(yield* l(e), /* @__PURE__ */ new Set(), r, i)));
56
+ }), k = (e) => e === "sql" ? 3 : 2, A = (e) => n.effect(r.SqlClient, t.gen(function* () {
57
+ let n = yield* r.SqlClient, i = k(e.runnerStorage), a = d(n);
58
+ return a !== void 0 && a < i ? yield* t.die(/* @__PURE__ */ Error(`the workflow runtime needs ${i} database connections at once and the pool has ${a}: ` + (e.runnerStorage === "sql" ? "on a first boot it holds its migration lock and its runner lock while the migration runs on a third, and once running it keeps the runner lock and needs one more to work. " : "on a first boot it holds its migration lock while the migration runs on a second. ") + `Set DB_MAX_CONNECTIONS (or PG_MAX_CONNECTIONS) to at least ${i}` + (e.runnerStorage === "sql" ? ", or VOLTRO_WORKFLOW_RUNNER_STORAGE=memory for a single-process runtime that needs 2 (no cross-pod shard handoff)." : "."))) : ((yield* w(n, e.runnerStorage)) || (yield* x(n, O(n, e.runnerStorage, e.charset))), n);
59
+ }));
60
+ //#endregion
61
+ export { k as clusterConnectionFloor, A as clusterMigrationGateLayer, C as tableExists, x as withClusterMigrationLock };
@@ -0,0 +1,31 @@
1
+ import { JournalRoutingGuard as e } from "./journalRoutingGuard.js";
2
+ import { JournalRoutingLayout as t, withJournalEntityRouting as n } from "./journalRoutingRuntime.js";
3
+ import { webCryptoLayer as r } from "./webCrypto.js";
4
+ import { withJournalRoutingGuard as i } from "./journalRoutingSharding.js";
5
+ import { guardJournalMessageStorage as a } from "./journalRoutingMessageStorage.js";
6
+ import { guardRunnerStorage as o } from "./runnerStorageGuard.js";
7
+ import { Effect as s, Layer as c, Option as l, Semaphore as u } from "effect";
8
+ import { SqlClient as d } from "effect/sql";
9
+ import { MessageStorage as f, RunnerHealth as p, RunnerStorage as m, Runners as h, Sharding as g, ShardingConfig as _, SqlMessageStorage as v, SqlRunnerStorage as y } from "effect/cluster";
10
+ //#region src/clusterRunnerLayer.ts
11
+ var b = (e) => s.map(u.make(1), (t) => ({
12
+ ...e,
13
+ release: (n, r) => t.withPermits(1)(s.suspend(() => e.release(n, r)))
14
+ })), x = c.unwrap(s.map(d.SqlClient, (e) => e.onDialectOrElse({
15
+ sqlite: () => c.effect(m.RunnerStorage, s.flatMap(m.RunnerStorage, b)).pipe(c.provide(y.layer)),
16
+ orElse: () => y.layer
17
+ }))), S = (e) => c.effect(m.RunnerStorage, s.gen(function* () {
18
+ let t = yield* d.SqlClient, n = yield* m.RunnerStorage;
19
+ return yield* o(n, {
20
+ ...e,
21
+ probe: t`SELECT 1`
22
+ });
23
+ })).pipe(c.provide(c.orDie(x))), C = c.effect(f.MessageStorage, s.gen(function* () {
24
+ let t = yield* f.MessageStorage, n = yield* s.serviceOption(e);
25
+ return l.isSome(n) ? a(t, n.value) : t;
26
+ })).pipe(c.provide(v.layer)), w = c.effect(g.Sharding, s.gen(function* () {
27
+ let r = yield* g.Sharding, a = yield* s.serviceOption(e), o = yield* s.serviceOption(t), c = l.isSome(o) ? n(r, o.value) : r;
28
+ return l.isSome(a) ? i(c, a.value) : c;
29
+ })).pipe(c.provide(g.layer)), T = (e) => w.pipe(c.provideMerge(h.layerNoop), c.provideMerge(C), c.provide([e.runnerStorage === "memory" ? m.layerMemory : e.clientOnly ? c.orDie(x) : S(e.guard), p.layerNoop]), c.provide(_.layerFromEnv(e.shardingConfig)), c.provide(r));
30
+ //#endregion
31
+ export { T as clusterRunnerLayer, b as limitSqliteShardReleases };
@@ -0,0 +1,33 @@
1
+ //#region src/clusterShardGroups.ts
2
+ var e = (e, t) => {
3
+ if (e === void 0) return;
4
+ if (!e || typeof e != "object" || Array.isArray(e) || ![Object.prototype, null].includes(Object.getPrototypeOf(e))) throw Error("Workflow shard groups require a plain data object");
5
+ let n = /* @__PURE__ */ new Map();
6
+ for (let t of Reflect.ownKeys(e)) {
7
+ let r = Object.getOwnPropertyDescriptor(e, t);
8
+ if (typeof t != "string" || !["available", "assigned"].includes(t) || !r.enumerable || !("value" in r)) throw Error("Invalid workflow shard group field");
9
+ n.set(t, r.value);
10
+ }
11
+ let r = (e) => {
12
+ let t = n.get(e);
13
+ if (!Array.isArray(t) || t.length > 64 || Reflect.ownKeys(t).length !== t.length + 1) throw Error("Invalid workflow shard group list");
14
+ let r = [];
15
+ for (let e = 0; e < t.length; e++) {
16
+ let n = Object.getOwnPropertyDescriptor(t, String(e));
17
+ if (!n?.enumerable || !("value" in n) || typeof n.value != "string" || !/^[a-z][a-z0-9._-]{0,31}$/.test(n.value)) throw Error("Invalid workflow shard group name");
18
+ r.push(n.value);
19
+ }
20
+ if (new Set(r).size !== r.length) throw Error("Duplicate workflow shard group");
21
+ return Object.freeze(r);
22
+ }, i = r("available"), a = r("assigned");
23
+ if (i.length === 0) throw Error("Workflow shard group inventory cannot be empty");
24
+ if (a.some((e) => !i.includes(e))) throw Error("Assigned workflow shard group is unavailable");
25
+ if (t && a.length > 0) throw Error("Client-only workflows cannot own shard groups");
26
+ if (!t && a.length === 0) throw Error("Workflow workers require an assigned shard group");
27
+ return Object.freeze({
28
+ available: i,
29
+ assigned: a
30
+ });
31
+ };
32
+ //#endregion
33
+ export { e as captureWorkflowShardGroups };
@@ -36,6 +36,11 @@ export declare interface ClusterEngineFixture {
36
36
  readonly reachable: () => Promise<boolean>;
37
37
  }
38
38
 
39
+ declare interface JournalRoutingState {
40
+ readonly generation: number;
41
+ readonly layout: string;
42
+ }
43
+
39
44
  /**
40
45
  * Clear cluster state left behind by EARLIER runs of this suite.
41
46
  *
@@ -122,6 +127,15 @@ declare type ShardLockMode = 'auto' | 'row' | 'advisory';
122
127
  export declare const tcpReachable: (host: string, port: number, timeoutMs?: number) => Promise<boolean>;
123
128
 
124
129
  declare interface WorkflowEngineLayerOptions {
130
+ /** Client mode submits/polls durable work without registering an executor,
131
+ * runner heartbeat or shard owner. Requires shared SQL runner storage. */
132
+ readonly runnerMode?: 'worker' | 'client';
133
+ /** Opt into the persisted PostgreSQL routing generation and guarded journal.
134
+ * Requires a completed stop-all routing transition before split ownership. */
135
+ readonly journalRouting?: JournalRoutingState;
136
+ /** Explicit cluster-wide routing inventory and this runner's ownership.
137
+ * Omitted preserves existing routing. Does not migrate persisted addresses. */
138
+ readonly shardGroups?: WorkflowShardGroups;
125
139
  /**
126
140
  * The dialect's SqlClient layer (postgres / mysql / mariadb / mssql).
127
141
  * For `runnerStorage: 'memory'` it's still required so the cluster's
@@ -204,4 +218,13 @@ declare interface WorkflowEngineLayerOptions {
204
218
  readonly extraShardingLayers?: ReadonlyArray<Layer.Layer<never, never, Sharding.Sharding>>;
205
219
  }
206
220
 
221
+ /** Explicit routing inventory shared by every runner in one cluster. Changing
222
+ * groups changes persistent addresses; this is not a journal migration. */
223
+ /** Group names are at most 32 ASCII characters: group + colon + a safe integer
224
+ * slot fits the existing SQL journal shard_id VARCHAR(50) on every dialect. */
225
+ declare interface WorkflowShardGroups {
226
+ readonly available: ReadonlyArray<string>;
227
+ readonly assigned: ReadonlyArray<string>;
228
+ }
229
+
207
230
  export { }