@voltro/workflow 0.84.0 → 0.85.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.
@@ -0,0 +1,679 @@
1
+ import { C as e, S as t, d as n, g as r, h as i, i as a, l as o, r as s, s as c, t as l, u, y as d } from "./workflowReadSelections-CZAUjP7f.js";
2
+ import { RateLimiter as f } from "effect/persistence";
3
+ import { Context as p, Crypto as m, Duration as h, Effect as g, Layer as _, Option as v, PlatformError as y, Schedule as ee, Schema as b, Semaphore as te } from "effect";
4
+ import { eq as x } from "@voltro/database";
5
+ import { SqlClient as S } from "effect/sql";
6
+ import { ClusterCron as ne, ClusterSchema as re, ClusterWorkflowEngine as C, EntityAddress as ie, EntityId as ae, EntityType as oe, MessageStorage as w, RunnerAddress as T, RunnerHealth as se, RunnerStorage as E, Runners as ce, ShardId as le, Sharding as D, ShardingConfig as ue, SqlMessageStorage as O, SqlRunnerStorage as k } from "effect/cluster";
7
+ import { applyUndeclaredCharsetWork as de, fnv1a64 as A, readUndeclaredCharsetColumns as fe, resolveMigrationLockSchema as j, sqlPoolCapacity as pe, undeclaredCharsetWork as me } from "@voltro/database/sql";
8
+ import { PersistenceError as he } from "effect/cluster/ClusterError";
9
+ import { realpath as ge } from "node:fs/promises";
10
+ import { createLogger as _e } from "@voltro/logger";
11
+ //#region src/workflowResumeDispatch.ts
12
+ var ve = (n) => g.gen(function* () {
13
+ let a = yield* g.context(), o = yield* g.promise(() => n.transaction((t) => e(t, n.requestId, (e) => g.runPromiseWith(a)(i(e)))));
14
+ if (o.kind !== "bound") return o;
15
+ let s = yield* g.promise(() => n.transaction((e) => t(e, n.requestId)));
16
+ if (s.kind !== "dispatch") return s;
17
+ let { request: c } = s;
18
+ if (c.operation !== "resume" || c.resumeJournal === null) return yield* g.die(/* @__PURE__ */ Error("Resume dispatch requires its committed suspension binding"));
19
+ let l = yield* r({
20
+ workflowName: c.workflowName,
21
+ executionId: c.executionId,
22
+ ...c.resumeJournal
23
+ });
24
+ return yield* (yield* D.Sharding).pollStorage, {
25
+ kind: "attempted",
26
+ request: c,
27
+ journal: l
28
+ };
29
+ }), M = class extends p.Service()("@voltro/workflow/WorkflowResumeDispatcher") {}, ye = g.gen(function* () {
30
+ let e = yield* S.SqlClient, t = yield* D.Sharding;
31
+ return M.of({ dispatch: (n) => ve(n).pipe(g.provideService(S.SqlClient, e), g.provideService(D.Sharding, t)) });
32
+ }), be = async (e) => {
33
+ if (e.workflow._tag !== e.command.workflowName || e.control.workflowName !== e.command.workflowName) throw Error("Restart preparation requires the matching workflow");
34
+ let t = await e.transaction(async (t) => {
35
+ let n = await c(t, e.command);
36
+ if (n !== void 0) return {
37
+ kind: "accepted",
38
+ request: n
39
+ };
40
+ let [r] = await t.query({
41
+ table: "_voltro_workflow_runs",
42
+ predicate: x("id", e.command.runId),
43
+ lock: "update",
44
+ order: [],
45
+ take: 1,
46
+ skip: void 0,
47
+ projection: ["runnerGeneration"]
48
+ });
49
+ return {
50
+ kind: "captured",
51
+ generation: b.decodeUnknownSync(b.NonEmptyString)(r?.runnerGeneration)
52
+ };
53
+ });
54
+ return t.kind === "accepted" ? {
55
+ kind: "reserved",
56
+ request: t.request,
57
+ reused: !0
58
+ } : Se({
59
+ ...e,
60
+ command: {
61
+ ...e.command,
62
+ generation: t.generation
63
+ }
64
+ });
65
+ }, xe = b.Struct({
66
+ requestId: b.NonEmptyString,
67
+ restartToken: b.NullOr(b.NonEmptyString)
68
+ }), Se = async (e) => {
69
+ let { command: t } = e;
70
+ if (e.workflow._tag !== t.workflowName || e.control.workflowName !== t.workflowName) throw Error("Restart preparation requires the matching workflow");
71
+ let n = await e.transaction(async (n) => {
72
+ let r = await o(n, t);
73
+ if (r !== void 0) return {
74
+ kind: "accepted",
75
+ request: r
76
+ };
77
+ let [i] = await n.query({
78
+ table: "_voltro_workflow_runs",
79
+ predicate: x("id", t.runId),
80
+ lock: "update",
81
+ order: [],
82
+ take: 1,
83
+ skip: void 0,
84
+ projection: void 0
85
+ });
86
+ if (i === void 0 || i.tag !== t.workflowName || i.runnerGeneration !== t.generation || i.status !== "failed" || i.discardedAt != null || typeof i.executionId != "string" || i.executionId.length === 0) throw Error("Restart preparation requires its failed resident generation");
87
+ if ((b.decodeUnknownSync(b.NullOr(b.Struct({ tenantId: b.NullOr(b.NonEmptyString) })))(i.subject ?? null)?.tenantId ?? null) !== t.tenantId) throw Error("Restart preparation belongs to another tenant");
88
+ return {
89
+ kind: "observe",
90
+ executionId: i.executionId,
91
+ delivery: b.decodeUnknownSync(xe)(i.engineDelivery),
92
+ activities: await e.readActivities(n)
93
+ };
94
+ });
95
+ if (n.kind === "accepted") return {
96
+ kind: "reserved",
97
+ request: n.request,
98
+ reused: !0
99
+ };
100
+ let r = await e.observe({
101
+ workflow: e.workflow,
102
+ executionId: n.executionId,
103
+ delivery: n.delivery,
104
+ activities: n.activities
105
+ });
106
+ return e.transaction(async (i) => {
107
+ let a = await o(i, t);
108
+ if (a !== void 0) return {
109
+ kind: "reserved",
110
+ request: a,
111
+ reused: !0
112
+ };
113
+ if (r.kind === "unavailable") return r;
114
+ let [s] = await i.query({
115
+ table: "_voltro_workflow_runs",
116
+ predicate: x("id", t.runId),
117
+ lock: "update",
118
+ order: [],
119
+ take: 1,
120
+ skip: void 0,
121
+ projection: ["executionId"]
122
+ });
123
+ if (s?.executionId !== n.executionId) throw Error("Restart execution changed during observation");
124
+ return u(i, {
125
+ ...t,
126
+ token: e.createToken(),
127
+ delivery: n.delivery,
128
+ replies: r.replies,
129
+ control: e.control,
130
+ paused: await e.readPaused(i),
131
+ now: e.now
132
+ });
133
+ });
134
+ }, N = (e, t) => `${e}/${t}`, P = (e, t, n) => {
135
+ let r = ae.make(t), i = p.get(e.annotations, re.ShardGroup)(r);
136
+ return new ie.EntityAddress({
137
+ entityType: oe.EntityType.make(`Workflow/${e._tag}`),
138
+ entityId: r,
139
+ shardId: n.getShardId(r, i)
140
+ });
141
+ }, Ce = (e, t) => g.gen(function* () {
142
+ let n = yield* D.Sharding, r = yield* w.MessageStorage, i = yield* r.requestIdForPrimaryKey({
143
+ address: P(e, t, n),
144
+ tag: "run",
145
+ id: ""
146
+ });
147
+ if (v.isNone(i)) return { kind: "not-found" };
148
+ let a = String(i.value), o = yield* r.repliesForUnfiltered([i.value]), s = l(o);
149
+ return s === void 0 ? {
150
+ kind: "pending",
151
+ requestId: a
152
+ } : {
153
+ kind: "reply",
154
+ requestId: a,
155
+ replyId: String(s.id),
156
+ exit: s.exit
157
+ };
158
+ }), F = b.Union([b.Struct({
159
+ _tag: b.Literal("Success"),
160
+ value: b.Unknown
161
+ }), b.Struct({
162
+ _tag: b.Literal("Failure"),
163
+ cause: b.Array(b.Union([
164
+ b.Struct({
165
+ _tag: b.Literal("Fail"),
166
+ error: b.Unknown
167
+ }),
168
+ b.Struct({
169
+ _tag: b.Literal("Die"),
170
+ defect: b.Unknown
171
+ }),
172
+ b.Struct({
173
+ _tag: b.Literal("Interrupt"),
174
+ fiberId: b.optional(b.NullOr(b.Number))
175
+ })
176
+ ]))
177
+ })]), I = b.decodeUnknownOption(F), L = (e) => {
178
+ let t = (e) => e && typeof e == "object" && "_tag" in e && typeof e._tag == "string" ? e._tag : void 0, n = I(e);
179
+ if (v.isNone(n)) return "unrecognized-reply";
180
+ if (n.value._tag === "Failure") return "failed";
181
+ let r = n.value.value, i = t(r);
182
+ if (i === "Suspended") return "suspended";
183
+ if (i === "Complete") {
184
+ let e = r.exit, t = I(e);
185
+ return v.isNone(t) ? "unrecognized-reply" : t.value._tag === "Success" ? "succeeded" : "failed";
186
+ }
187
+ return "unrecognized-reply";
188
+ }, we = (e) => R(e.workflow, e.executionId, e.failedActivities), Te = (e) => R(e.workflow, e.executionId, e.resetActivities), R = (e, t, n) => g.gen(function* () {
189
+ let r = yield* D.Sharding, i = yield* w.MessageStorage, a = P(e, t, r), o = yield* i.requestIdForPrimaryKey({
190
+ address: a,
191
+ tag: "run",
192
+ id: ""
193
+ }).pipe(g.orDie);
194
+ if (v.isNone(o)) return {
195
+ redriven: !1,
196
+ runReplyCleared: !1,
197
+ activitiesReset: 0,
198
+ reason: "no-journal"
199
+ };
200
+ let s = yield* i.repliesForUnfiltered([o.value]).pipe(g.orDie), c = l(s);
201
+ if (c === void 0) return {
202
+ redriven: !1,
203
+ runReplyCleared: !1,
204
+ activitiesReset: 0,
205
+ reason: "no-reply"
206
+ };
207
+ let u = L(c.exit);
208
+ if (u !== "failed") return {
209
+ redriven: !1,
210
+ runReplyCleared: !1,
211
+ activitiesReset: 0,
212
+ reason: u
213
+ };
214
+ let d = /* @__PURE__ */ new Set();
215
+ for (let e of n) {
216
+ let t = yield* i.requestIdForPrimaryKey({
217
+ address: a,
218
+ tag: "activity",
219
+ id: N(e.name, e.attempt)
220
+ }).pipe(g.orDie);
221
+ if (v.isNone(t)) return {
222
+ redriven: !1,
223
+ runReplyCleared: !1,
224
+ activitiesReset: 0,
225
+ reason: "activity-not-found"
226
+ };
227
+ d.add(t.value);
228
+ }
229
+ let f = 0;
230
+ for (let e of d) {
231
+ if (!(yield* r.reset(e))) return {
232
+ redriven: !1,
233
+ runReplyCleared: !1,
234
+ activitiesReset: f,
235
+ reason: "activity-reset-unconfirmed"
236
+ };
237
+ f++;
238
+ }
239
+ let p = yield* r.reset(o.value);
240
+ return p ? (yield* r.pollStorage, {
241
+ redriven: !0,
242
+ runReplyCleared: p,
243
+ activitiesReset: f,
244
+ reason: null
245
+ }) : {
246
+ redriven: !1,
247
+ runReplyCleared: !1,
248
+ activitiesReset: f,
249
+ reason: "run-reset-unconfirmed"
250
+ };
251
+ }), Ee = b.fromJsonString(b.Record(b.String, b.String)), De = (e) => g.gen(function* () {
252
+ if (!/^[1-9]\d*$/.test(e.delivery.requestId) || e.executionId.length === 0 || e.activities.some((e) => e.name.length === 0 || !Number.isSafeInteger(e.attempt) || e.attempt < 1)) return yield* g.die(/* @__PURE__ */ Error("Restart observation requires an exact delivery and valid activity attempts"));
253
+ let t = (yield* S.SqlClient).withoutTransforms(), n = yield* w.MessageStorage, r = yield* D.Sharding, i = P(e.workflow, e.executionId, r), a = (n, r) => g.gen(function* () {
254
+ let [i] = yield* t`SELECT m.entity_type, m.entity_id, m.tag, m.headers, r.id AS reply_id, r.payload
255
+ FROM cluster_messages m
256
+ LEFT JOIN cluster_replies r ON r.id = m.last_reply_id AND r.request_id = m.id AND r.kind = 0
257
+ WHERE m.id = ${n} AND m.kind = 0`;
258
+ if (i !== void 0) return i.entity_type !== `Workflow/${e.workflow._tag}` || i.entity_id !== e.executionId || i.tag !== r ? yield* g.die(/* @__PURE__ */ Error("Restart observation belongs to another execution or procedure")) : i;
259
+ }), o = yield* a(e.delivery.requestId, "run");
260
+ if (o === void 0) return {
261
+ kind: "unavailable",
262
+ reason: "no-journal"
263
+ };
264
+ if (((yield* g.sync(() => b.decodeUnknownSync(Ee)(o.headers)))["voltro-workflow-restart"] ?? null) !== e.delivery.restartToken) return {
265
+ kind: "unavailable",
266
+ reason: "delivery-changed"
267
+ };
268
+ if (o.reply_id === null || o.payload === null) return {
269
+ kind: "unavailable",
270
+ reason: "no-reply"
271
+ };
272
+ let s = L(yield* g.sync(() => JSON.parse(o.payload)));
273
+ if (s !== "failed") return {
274
+ kind: "unavailable",
275
+ reason: s
276
+ };
277
+ let c = /* @__PURE__ */ new Map([[e.delivery.requestId, String(o.reply_id)]]);
278
+ for (let t of e.activities) {
279
+ let e = yield* n.requestIdForPrimaryKey({
280
+ address: i,
281
+ tag: "activity",
282
+ id: N(t.name, t.attempt)
283
+ });
284
+ if (v.isNone(e)) return {
285
+ kind: "unavailable",
286
+ reason: "activity-not-found"
287
+ };
288
+ let r = String(e.value), o = yield* a(r, "activity");
289
+ if (o?.reply_id == null) return {
290
+ kind: "unavailable",
291
+ reason: "activity-not-found"
292
+ };
293
+ c.set(r, String(o.reply_id));
294
+ }
295
+ return {
296
+ kind: "observed",
297
+ replies: [...c].map(([e, t]) => ({
298
+ requestId: e,
299
+ replyId: t
300
+ }))
301
+ };
302
+ }).pipe(g.provideService(S.SafeIntegers, !0)), Oe = (e) => g.gen(function* () {
303
+ let t = yield* n(e);
304
+ return t.kind === "waiting-capacity" ? t : (yield* (yield* D.Sharding).pollStorage, {
305
+ kind: "attempted",
306
+ journal: t
307
+ });
308
+ }), z = class extends p.Service()("@voltro/workflow/WorkflowRestartDispatcher") {}, ke = g.gen(function* () {
309
+ let e = yield* S.SqlClient, t = yield* D.Sharding, n = yield* w.MessageStorage, r = (r) => De(r).pipe(g.provideService(S.SqlClient, e), g.provideService(D.Sharding, t), g.provideService(w.MessageStorage, n));
310
+ return z.of({
311
+ prepare: (e) => g.tryPromise({
312
+ try: (t) => be({
313
+ ...e,
314
+ observe: (e) => g.runPromise(r(e), { signal: t })
315
+ }),
316
+ catch: (e) => e
317
+ }),
318
+ observe: r,
319
+ dispatch: (n) => Oe(n).pipe(g.provideService(S.SqlClient, e), g.provideService(D.Sharding, t))
320
+ });
321
+ }), Ae = m.make({
322
+ randomBytes: (e) => globalThis.crypto.getRandomValues(new Uint8Array(e)),
323
+ digest: (e, t) => g.tryPromise({
324
+ try: async () => new Uint8Array(await globalThis.crypto.subtle.digest(e, new Uint8Array(t).buffer)),
325
+ catch: (t) => y.systemError({
326
+ _tag: "Unknown",
327
+ module: "Crypto",
328
+ method: "digest",
329
+ description: `digest ${e} failed`,
330
+ cause: t
331
+ })
332
+ })
333
+ }), B = _.succeed(m.Crypto, Ae), V = 6322741009312601n, H = () => {
334
+ let e = j();
335
+ return e === void 0 ? V : A(`voltro_cluster_migration_lock:${e}`) & 9223372036854775807n;
336
+ }, U = () => {
337
+ let e = `voltro_cluster_migration_${V.toString(36)}`, t = j();
338
+ if (t === void 0) return e;
339
+ let n = `${e}_${t}`;
340
+ return /^[A-Za-z0-9_]+$/.test(t) && n.length <= 64 ? n : `${e}_h${A(`voltro_cluster_migration_lock:${t}`).toString(16)}`;
341
+ }, W = () => {
342
+ let e = Number(process.env.VOLTRO_CLUSTER_MIGRATION_LOCK_TIMEOUT_MS);
343
+ return Number.isFinite(e) && e > 0 ? e : 6e4;
344
+ }, je = 250, G = (e) => e[0]?.[0], Me = (e, t) => e.onDialectOrElse({
345
+ sqlite: () => g.void,
346
+ mysql: () => t.executeValues(`SELECT GET_LOCK('${U()}', ${Math.ceil(W() / 1e3)})`, []).pipe(g.flatMap((e) => G(e) === 1 || G(e) === "1" ? g.void : g.die(/* @__PURE__ */ Error(`could not acquire cluster migration lock '${U()}' (GET_LOCK timed out)`)))),
347
+ mssql: () => t.executeValues(`DECLARE @res int; EXEC @res = sp_getapplock @Resource = '${U()}', @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = ${W()}; SELECT @res`, []).pipe(g.flatMap((e) => Number(G(e) ?? -999) >= 0 ? g.void : g.die(/* @__PURE__ */ Error(`could not acquire cluster migration lock '${U()}' (sp_getapplock ${String(G(e))})`)))),
348
+ orElse: () => {
349
+ let e = H().toString(), n = Date.now(), r = t.executeValues(`SELECT pg_try_advisory_lock(${e}::bigint)`, []).pipe(g.flatMap((e) => {
350
+ let t = G(e);
351
+ return t === !0 || t === "t" || t === 1 ? g.void : Date.now() - n >= W() ? g.die(/* @__PURE__ */ Error(`could not acquire the cluster migration advisory lock within ${Math.round(W() / 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.)`)) : g.sleep(`${je} millis`).pipe(g.andThen(r));
352
+ }));
353
+ return r;
354
+ }
355
+ }), Ne = (e, t) => e.onDialectOrElse({
356
+ sqlite: () => g.void,
357
+ mysql: () => g.asVoid(t.executeValues(`SELECT RELEASE_LOCK('${U()}')`, [])),
358
+ mssql: () => g.asVoid(t.executeValues(`EXEC sp_releaseapplock @Resource = '${U()}', @LockOwner = 'Session'`, [])),
359
+ orElse: () => g.asVoid(t.executeValues(`SELECT pg_advisory_unlock(${H().toString()}::bigint)`, []))
360
+ }), Pe = (e, t) => e.onDialectOrElse({
361
+ sqlite: () => !0,
362
+ orElse: () => !1
363
+ }) ? t : g.scoped(g.gen(function* () {
364
+ let n = yield* e.reserve;
365
+ return yield* g.acquireRelease(Me(e, n), () => Ne(e, n).pipe(g.orDie)), yield* t;
366
+ })), Fe = (e) => e === "sql" ? ["cluster_messages", "cluster_runners"] : ["cluster_messages"], Ie = (e, t) => e.onDialectOrElse({
367
+ pg: () => e`SELECT (to_regclass(${t}) IS NOT NULL) AS present`,
368
+ mysql: () => e`SELECT (COUNT(*) > 0) AS present FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ${t}`,
369
+ mssql: () => e`SELECT CASE WHEN OBJECT_ID(${t}, 'U') IS NULL THEN 0 ELSE 1 END AS present`,
370
+ orElse: () => e`SELECT (COUNT(*) > 0) AS present FROM sqlite_master WHERE type = 'table' AND name = ${t}`
371
+ }).pipe(g.map((e) => {
372
+ let t = e[0]?.present;
373
+ return t === !0 || t === 1 || t === "1" || t === "t" || typeof t == "bigint" && t === 1n;
374
+ }), g.catch(() => g.succeed(!1))), Le = (e, t) => g.forEach(Fe(t), (t) => Ie(e, t), { concurrency: 1 }).pipe(g.map((e) => e.every(Boolean))), K = (e, t) => g.scoped(g.asVoid(_.build(t.pipe(_.provide(ue.layerDefaults), _.provide(_.succeed(S.SqlClient, e)), _.provide(B))))), q = (e, t) => K(e, O.layer).pipe(g.andThen(t === "sql" ? K(e, k.layer) : g.void)), J = (e) => g.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)))), Re = (e, t, n) => n === void 0 || !e.onDialectOrElse({
375
+ mysql: () => !0,
376
+ orElse: () => !1
377
+ }) ? q(e, t) : g.gen(function* () {
378
+ let r = yield* J(e);
379
+ yield* q(e, t);
380
+ let i = new Set([...yield* J(e)].filter((e) => !r.has(e)));
381
+ i.size !== 0 && (yield* de(e, me(yield* fe(e), /* @__PURE__ */ new Set(), n, i)));
382
+ }), ze = (e) => e === "sql" ? 3 : 2, Y = (e) => _.effect(S.SqlClient, g.gen(function* () {
383
+ let t = yield* S.SqlClient, n = ze(e.runnerStorage), r = pe(t);
384
+ return r !== void 0 && r < n ? yield* g.die(/* @__PURE__ */ Error(`the workflow runtime needs ${n} database connections at once and the pool has ${r}: ` + (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 ${n}` + (e.runnerStorage === "sql" ? ", or VOLTRO_WORKFLOW_RUNNER_STORAGE=memory for a single-process runtime that needs 2 (no cross-pod shard handoff)." : "."))) : ((yield* Le(t, e.runnerStorage)) || (yield* Pe(t, Re(t, e.runnerStorage, e.charset))), t);
385
+ })), Be = (e, t) => g.gen(function* () {
386
+ let n = h.fromInputUnsafe(t.interval), r = h.toMillis(t.giveUpAfter), i, a, o = !1, s = !1;
387
+ yield* t.probe.pipe(g.timeout(n), g.isSuccess, g.flatMap((e) => g.sync(() => {
388
+ let n = Date.now();
389
+ if (e) {
390
+ if (i = void 0, !o || (a ??= n, n - a < r)) return;
391
+ t.log.info("workflow runner: its storage answers again — taking part in shard assignment again"), a = void 0, o = !1, s = !1;
392
+ return;
393
+ }
394
+ a = void 0, i ??= n, !o && n - i >= r && (o = !0, t.log.warn(`workflow runner: its connection pool has not served a query for ${Math.round((n - i) / 1e3)}s — released its shards and stopped its heartbeat, so another runner takes them`, { fix: "look for what holds this process's database connections (`voltro_db_operations_in_flight` against DB_MAX_CONNECTIONS)" }));
395
+ }))).pipe(g.repeat(ee.spaced(n)), g.forkScoped);
396
+ let c = () => new he({ cause: /* @__PURE__ */ Error("workflow runner gave its shards up: its connection pool does not serve queries") });
397
+ return E.RunnerStorage.of({
398
+ ...e,
399
+ register: (t, n) => g.suspend(() => o ? g.fail(c()) : e.register(t, n)),
400
+ acquire: (t, n) => g.suspend(() => o ? g.succeed([]) : e.acquire(t, n)),
401
+ refresh: (t, n) => g.suspend(() => {
402
+ if (!o) return e.refresh(t, n);
403
+ let r = s ? g.void : e.releaseAll(t).pipe(g.tap(() => g.sync(() => {
404
+ s = !0;
405
+ })), g.ignore);
406
+ return g.andThen(r, g.fail(c()));
407
+ })
408
+ });
409
+ }), Ve = (e) => g.map(te.make(1), (t) => ({
410
+ ...e,
411
+ release: (n, r) => t.withPermits(1)(g.suspend(() => e.release(n, r)))
412
+ })), He = _.unwrap(g.map(S.SqlClient, (e) => e.onDialectOrElse({
413
+ sqlite: () => _.effect(E.RunnerStorage, g.flatMap(E.RunnerStorage, Ve)).pipe(_.provide(k.layer)),
414
+ orElse: () => k.layer
415
+ }))), Ue = (e) => _.effect(E.RunnerStorage, g.gen(function* () {
416
+ let t = yield* S.SqlClient;
417
+ return yield* Be(yield* E.RunnerStorage, {
418
+ ...e,
419
+ probe: t`SELECT 1`
420
+ });
421
+ })).pipe(_.provide(_.orDie(He))), We = (e) => D.layer.pipe(_.provideMerge(ce.layerNoop), _.provideMerge(O.layer), _.provide([e.runnerStorage === "memory" ? E.layerMemory : Ue(e.guard), se.layerNoop]), _.provide(ue.layerFromEnv(e.shardingConfig)), _.provide(B)), Ge = (e) => g.gen(function* () {
422
+ let t = yield* g.tryPromise({
423
+ try: () => ge(e),
424
+ catch: X
425
+ }), n = yield* g.acquireRelease(g.tryPromise({
426
+ try: async () => {
427
+ let { DatabaseSync: e } = await import("node:sqlite");
428
+ return new e(`${t}.voltro-workflow-lock`);
429
+ },
430
+ catch: X
431
+ }), (e) => g.sync(() => e.close()));
432
+ yield* g.try({
433
+ try: () => {
434
+ n.exec("PRAGMA busy_timeout=0"), n.exec("CREATE TABLE IF NOT EXISTS workflow_mutex(id INTEGER PRIMARY KEY)"), n.exec("BEGIN IMMEDIATE");
435
+ },
436
+ catch: (e) => {
437
+ let n = X(e), r = n.errcode;
438
+ return r !== void 0 && ((r & 255) == 5 || (r & 255) == 6) ? Error("SQLite workflow journal is already owned by another engine. Stop that engine before starting another, or use a SQL dialect with shared runner coordination for replicas.", { cause: n }) : Error(`Could not acquire SQLite workflow ownership for ${t}`, { cause: n });
439
+ }
440
+ });
441
+ }), X = (e) => e instanceof Error ? e : Error(String(e)), Z = (e) => {
442
+ if (e instanceof Date) return Number.isNaN(e.getTime()) ? null : e;
443
+ if (typeof e == "string" || typeof e == "number") {
444
+ let t = typeof e == "string" && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(e) ? `${e.replace(" ", "T")}Z` : e, n = new Date(t);
445
+ return Number.isNaN(n.getTime()) ? null : n;
446
+ }
447
+ return null;
448
+ }, Ke = (e) => e === !0 || e === 1 || e === "1" || e instanceof Uint8Array && e[0] === 1, qe = "cluster", Je = (e = qe) => {
449
+ let t = new TextEncoder().encode(`effect-cluster:${e}`), n = 2166136261;
450
+ for (let e of t) n = Math.imul(n ^ e, 16777619);
451
+ return (n | 0) >>> 0;
452
+ }, Ye = (e, t) => {
453
+ let n = e.lastIndexOf(":");
454
+ if (n < 0) return;
455
+ let r = e.slice(0, n), i = Number(e.slice(n + 1)), a = t.indexOf(r);
456
+ if (!(a < 0 || !Number.isInteger(i))) return (a + 1) * 1e6 + i;
457
+ }, Q = (e) => g.gen(function* () {
458
+ let t = yield* S.SqlClient, n = e.shardGroups ?? ["default"], r = e.dialectId ?? "", i = r.startsWith("postgres") || r === "pg", a = (e) => e.pipe(g.catch(() => g.succeed([]))), o = t.onDialectOrElse({
459
+ pg: () => t.literal("NOW()"),
460
+ mysql: () => t.literal("NOW()"),
461
+ mssql: () => t.literal("GETDATE()"),
462
+ orElse: () => t.literal("CURRENT_TIMESTAMP")
463
+ }), c = (e, t) => {
464
+ let n = Z(e.last_read), r = Z(e.server_now), i = n !== null && r !== null ? Math.max(0, r.getTime() - n.getTime()) : null;
465
+ return {
466
+ tag: typeof e.tag == "string" ? e.tag : null,
467
+ entityType: String(e.entity_type ?? ""),
468
+ entityId: String(e.entity_id ?? ""),
469
+ shardId: String(e.shard_id ?? ""),
470
+ processed: Ke(e.processed),
471
+ lastRead: i === null ? null : new Date(t - i)
472
+ };
473
+ }, l = (e) => t.onDialectOrElse({
474
+ pg: () => t.literal(`NOW() - INTERVAL '${e} seconds'`),
475
+ mysql: () => t.literal(`NOW() - INTERVAL ${e} SECOND`),
476
+ mssql: () => t.literal(`DATEADD(SECOND, -${e}, GETDATE())`),
477
+ orElse: () => t.literal(`DATETIME('now', '-${e} seconds')`)
478
+ }), u = t.onDialectOrElse({
479
+ pg: () => t.literal("FALSE"),
480
+ orElse: () => t.literal("0")
481
+ }), d = (e) => {
482
+ let n = Date.now();
483
+ return a(t`
484
+ SELECT tag, entity_type, entity_id, shard_id, processed, last_read, ${o} AS server_now
485
+ FROM cluster_messages
486
+ WHERE entity_id = ${e}
487
+ ORDER BY rowid ASC
488
+ `).pipe(g.map((e) => e.map((e) => c(e, n))));
489
+ }, f = (e = s) => {
490
+ let n = Math.max(1, Math.ceil(e / 1e3)), r = Date.now();
491
+ return a(t`
492
+ SELECT tag, entity_type, entity_id, shard_id, processed, last_read, ${o} AS server_now
493
+ FROM cluster_messages
494
+ WHERE processed = ${u}
495
+ AND last_read IS NOT NULL
496
+ AND last_read < ${l(n)}
497
+ AND (deliver_at IS NULL OR deliver_at <= ${r})
498
+ ORDER BY last_read ASC
499
+ `).pipe(g.map((e) => e.flatMap((e) => {
500
+ let t = c(e, r);
501
+ return t.lastRead === null ? [] : [{
502
+ ...t,
503
+ unreadForMs: Math.max(0, r - t.lastRead.getTime())
504
+ }];
505
+ })));
506
+ }, p = (n) => a(t`
507
+ SELECT shard_id, address, acquired_at FROM cluster_locks WHERE ${t.in("shard_id", n)}
508
+ `).pipe(g.map((t) => t.map((t) => {
509
+ let n = t, r = String(n.address ?? ""), i = String(n.shard_id ?? "");
510
+ return {
511
+ shardId: i,
512
+ mode: "row",
513
+ holder: r,
514
+ own: r === e.ownAddress || (e.hasShard?.(i) ?? !1),
515
+ since: Z(n.acquired_at)
516
+ };
517
+ }))), m = (r) => {
518
+ let i = /* @__PURE__ */ new Map();
519
+ for (let e of r) {
520
+ let t = Ye(e, n);
521
+ t !== void 0 && i.set(t, e);
522
+ }
523
+ return i.size === 0 ? g.succeed([]) : g.gen(function* () {
524
+ return (yield* a(t`
525
+ SELECT l.objid, a.pid, a.client_addr, a.application_name, a.state, a.state_change, a.backend_start, a.wait_event_type
526
+ FROM pg_locks l
527
+ JOIN pg_stat_activity a ON a.pid = l.pid
528
+ WHERE l.locktype = 'advisory' AND l.granted AND l.objsubid = 2
529
+ AND l.classid = ${Je()} AND ${t.in("l.objid", [...i.keys()])}
530
+ `)).flatMap((t) => {
531
+ let n = t, r = i.get(Number(n.objid));
532
+ if (r === void 0) return [];
533
+ let a = Number(n.pid), o = n.client_addr === null || n.client_addr === void 0 ? "local socket" : String(n.client_addr), s = e.hasShard?.(r) ?? !1;
534
+ return [{
535
+ shardId: r,
536
+ mode: "advisory",
537
+ holder: s ? `this runner (pid ${a})` : `pid ${a} from ${o}`,
538
+ own: s,
539
+ since: Z(n.state_change),
540
+ detail: {
541
+ pid: a,
542
+ clientAddr: n.client_addr ?? null,
543
+ applicationName: n.application_name ?? null,
544
+ state: n.state ?? null,
545
+ waitEventType: n.wait_event_type ?? null,
546
+ backendStart: Z(n.backend_start)?.toISOString() ?? null
547
+ }
548
+ }];
549
+ });
550
+ });
551
+ };
552
+ return {
553
+ ownAddress: e.ownAddress,
554
+ shardLockMode: e.shardLockMode,
555
+ messagesFor: d,
556
+ unreadMessages: f,
557
+ shardLockHolders: (t) => t.length === 0 || e.runnerStorage !== "sql" ? g.succeed([]) : e.shardLockMode === "row" ? p(t) : e.shardLockMode === "advisory" && i ? m(t) : g.succeed([]),
558
+ runners: () => e.runnerStorage === "sql" ? a(t`SELECT address, last_heartbeat, healthy FROM cluster_runners ORDER BY last_heartbeat DESC`).pipe(g.map((e) => e.map((e) => {
559
+ let t = e;
560
+ return {
561
+ address: String(t.address ?? ""),
562
+ lastHeartbeat: Z(t.last_heartbeat),
563
+ healthy: Ke(t.healthy)
564
+ };
565
+ }))) : g.succeed([]),
566
+ discardMessages: (e) => {
567
+ let n = e.unreadForMs === void 0 ? t.literal("") : t`AND last_read IS NOT NULL AND last_read < ${l(Math.max(1, Math.ceil(e.unreadForMs / 1e3)))}`;
568
+ return g.gen(function* () {
569
+ let r = (yield* t`
570
+ SELECT id FROM cluster_messages
571
+ WHERE entity_type = ${e.entityType} AND entity_id = ${e.entityId}
572
+ AND processed = ${u}
573
+ ${n}
574
+ `).map((e) => e.id).filter((e) => e != null).map(String);
575
+ return r.length === 0 ? 0 : (yield* t`DELETE FROM cluster_replies WHERE ${t.in("request_id", r)}`, yield* t`DELETE FROM cluster_messages WHERE ${t.in("id", r)}`, r.length);
576
+ }).pipe(t.withTransaction, g.provideService(S.SafeIntegers, !0), g.catch(() => g.succeed(0)));
577
+ }
578
+ };
579
+ }), $ = _e({ scope: "voltro:workflow" }), Xe = 34e3, Ze = (e) => {
580
+ let t = e.runnerListenHost ?? process.env.POD_IP ?? process.env.VOLTRO_WORKFLOW_RUNNER_HOST ?? "localhost";
581
+ return {
582
+ host: t,
583
+ port: e.runnerListenPort ?? 34e3,
584
+ localhostRisk: e.runnerStorage === "sql" && (t === "localhost" || t === "127.0.0.1")
585
+ };
586
+ }, Qe = (e) => {
587
+ let t = e.sqlRunnerSupported ?? !e.sqliteFamily, n = t ? "sql" : "memory", r = e.override ?? process.env.VOLTRO_WORKFLOW_RUNNER_STORAGE;
588
+ if (r === void 0 || r.trim() === "") return n;
589
+ let i = r.trim().toLowerCase();
590
+ if (i !== "memory" && i !== "sql") throw Error(`VOLTRO_WORKFLOW_RUNNER_STORAGE must be 'memory' or 'sql', got '${r}'.`);
591
+ 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.");
592
+ return i;
593
+ }, $e = (e = process.env.VOLTRO_WORKFLOW_SHARD_LOCK) => {
594
+ if (e === void 0 || e.trim() === "") return "auto";
595
+ let t = e.trim().toLowerCase();
596
+ if (t === "auto" || t === "row" || t === "advisory") return t;
597
+ throw Error(`VOLTRO_WORKFLOW_SHARD_LOCK must be 'auto', 'row', or 'advisory', got '${e}'.`);
598
+ }, et = g.gen(function* () {
599
+ let e = yield* (yield* S.SqlClient)`SELECT @@wsrep_on AS wsrep`.pipe(g.catch(() => g.succeed([])));
600
+ if (e.length === 0) return !1;
601
+ let t = e[0].wsrep;
602
+ return t === 1 || t === "1" || t === !0 || t === "ON" || t === "on";
603
+ }), tt = (e) => g.gen(function* () {
604
+ return e.runnerStorage === "memory" ? !1 : e.dialectId === "sqlite" || e.dialectId === "turso" || e.mode === "row" ? !0 : e.mode === "advisory" || e.dialectId !== "mysql" && e.dialectId !== "mariadb" ? !1 : yield* et;
605
+ }), nt = (e) => ({
606
+ coordination: e.disableAdvisory ? "row-based" : "advisory",
607
+ dialect: e.dialectId ?? "unknown",
608
+ mode: e.mode,
609
+ ...e.disableAdvisory && e.mode === "auto" ? { reason: e.dialectId === "sqlite" || e.dialectId === "turso" ? "SQLite-family connection uses row locks" : "wsrep/Galera cluster detected" } : {}
610
+ }), rt = () => {
611
+ let e = (e) => {
612
+ if (e === void 0) return;
613
+ let t = Number(e);
614
+ return Number.isFinite(t) && t > 0 ? t : void 0;
615
+ }, t = e(process.env.VOLTRO_WORKFLOW_FAILOVER_LEASE), n = e(process.env.VOLTRO_WORKFLOW_FAILOVER_HEARTBEAT), r = e(process.env.VOLTRO_WORKFLOW_POLL_INTERVAL);
616
+ return {
617
+ ...t === void 0 ? {} : { failoverLeaseSeconds: t },
618
+ ...n === void 0 ? {} : { failoverHeartbeatSeconds: n },
619
+ ...r === void 0 ? {} : { messagePollSeconds: r }
620
+ };
621
+ }, it = g.flatMap(D.Sharding, (e) => e.pollStorage), at = /* @__PURE__ */ new Set(), ot = (e) => Object.assign(Object.create(Object.getPrototypeOf(e)), e, { requestIdForPrimaryKey: (t) => e.requestIdForPrimaryKey(t).pipe(g.provideService(S.SafeIntegers, !0)) }), st = (e) => {
622
+ let t = Ze({
623
+ runnerStorage: e.runnerStorage,
624
+ runnerListenHost: e.runnerListenHost,
625
+ runnerListenPort: e.runnerListenPort
626
+ }), n = t.host, r = t.port;
627
+ t.localhostRisk && !at.has(n) && (at.add(n), $.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: n }));
628
+ let i = e.dialectId === "sqlite" ? _.effect(S.SqlClient, g.gen(function* () {
629
+ let e = yield* S.SqlClient, t = (yield* e.unsafe("PRAGMA database_list")).find((e) => e.name === "main");
630
+ return t === void 0 ? yield* g.die(/* @__PURE__ */ Error("SQLite workflow connection has no main database")) : (t.file !== "" && (yield* Ge(t.file)), e);
631
+ }).pipe(g.orDie)).pipe(_.provide(e.sqlClientLayer)) : e.sqlClientLayer, o = Y({
632
+ runnerStorage: e.runnerStorage,
633
+ charset: e.charset
634
+ }).pipe(_.provide(i)), s = e.shardLockMode ?? $e(), c = _.unwrap(tt({
635
+ dialectId: e.dialectId,
636
+ runnerStorage: e.runnerStorage,
637
+ mode: s
638
+ }).pipe(g.map((t) => {
639
+ e.runnerStorage === "sql" && $.info("shard-lock coordination", nt({
640
+ disableAdvisory: t,
641
+ dialectId: e.dialectId,
642
+ mode: s
643
+ }));
644
+ let i = _.effect(a, g.flatMap(D.Sharding, (i) => Q({
645
+ ownAddress: T.make(n, r).toString(),
646
+ shardLockMode: e.runnerStorage === "sql" ? t ? "row" : "advisory" : "none",
647
+ runnerStorage: e.runnerStorage,
648
+ dialectId: e.dialectId,
649
+ hasShard: (e) => i.hasShardId(le.fromString(e))
650
+ }))), o = {
651
+ runnerAddress: v.some(T.make(n, r)),
652
+ runnerListenAddress: v.some(T.make(n, r)),
653
+ ...t ? { shardLockDisableAdvisory: !0 } : {},
654
+ ...e.failoverLeaseSeconds === void 0 ? {} : { shardLockExpiration: h.seconds(e.failoverLeaseSeconds) },
655
+ ...e.failoverHeartbeatSeconds === void 0 ? {} : { shardLockRefreshInterval: h.seconds(e.failoverHeartbeatSeconds) },
656
+ ...e.messagePollSeconds === void 0 ? {} : { entityMessagePollInterval: h.seconds(e.messagePollSeconds) }
657
+ }, c = h.seconds(e.failoverHeartbeatSeconds ?? 10), l = We({
658
+ runnerStorage: e.runnerStorage,
659
+ shardingConfig: o,
660
+ guard: {
661
+ interval: c,
662
+ giveUpAfter: h.times(c, 3),
663
+ log: $
664
+ }
665
+ });
666
+ return _.mergeAll(i, _.effect(M, ye), _.effect(z, ke)).pipe(_.provideMerge(l));
667
+ }))).pipe(_.provide(o)), l = e.extraShardingLayers ?? [], u = l.length === 0 ? C.layer : _.mergeAll(C.layer, ...l), m = _.flatMap(c, (t) => {
668
+ let n = p.add(t, D.Sharding, d(p.get(t, D.Sharding)));
669
+ return _.succeedContext(e.dialectId === "sqlite" ? p.add(n, w.MessageStorage, ot(p.get(n, w.MessageStorage))) : n);
670
+ }), y = f.layer.pipe(_.provide(f.layerStoreMemory));
671
+ return _.merge(u, y).pipe(_.provideMerge(m));
672
+ }, ct = (e) => ne.make({
673
+ name: e.name,
674
+ cron: e.cron,
675
+ execute: g.promise(() => e.execute()),
676
+ skipIfOlderThan: e.skipIfOlderThan ?? h.days(1)
677
+ });
678
+ //#endregion
679
+ export { Ce as _, it as a, M as b, Qe as c, Je as d, Q as f, N as g, F as h, $e as i, tt as l, z as m, Xe as n, rt as o, Y as p, et as r, Ze as s, ct as t, st as u, we as v, Te as y };