@voltro/workflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +409 -0
- package/dist/cluster-Cl6mB2rW.js +31 -0
- package/dist/cluster.d.ts +112 -0
- package/dist/cluster.js +2 -0
- package/dist/clusterTestSuite.d.ts +84 -0
- package/dist/clusterTestSuite.js +180 -0
- package/dist/index.d.ts +865 -0
- package/dist/index.js +3 -0
- package/dist/primitives-CWy1iu5w.js +111 -0
- package/dist/primitives.d.ts +229 -0
- package/dist/primitives.js +2 -0
- package/dist/src-CNeLb-4L.js +817 -0
- package/package.json +68 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ConfigError } from 'effect';
|
|
2
|
+
import { Cron } from 'effect';
|
|
3
|
+
import { Duration } from 'effect';
|
|
4
|
+
import { Layer } from 'effect';
|
|
5
|
+
import { Sharding } from '@effect/cluster';
|
|
6
|
+
import { SqlClient } from '@effect/sql';
|
|
7
|
+
import { SqlError } from '@effect/sql';
|
|
8
|
+
import { WorkflowEngine } from '@effect/workflow';
|
|
9
|
+
|
|
10
|
+
export declare interface ClusterCronSpec {
|
|
11
|
+
/** Stable schedule name — used as the cluster cron's identity. */
|
|
12
|
+
readonly name: string;
|
|
13
|
+
/** Compiled cron (timezone already baked in by `Cron.parse`). */
|
|
14
|
+
readonly cron: Cron.Cron;
|
|
15
|
+
/** The wrapped firing — records the run row + invokes the user
|
|
16
|
+
* handler. Plain async; we lift it into an Effect. Must not throw
|
|
17
|
+
* in a way that breaks the cluster cron loop — the framework's
|
|
18
|
+
* recording wrapper swallows its own errors. */
|
|
19
|
+
readonly execute: () => Promise<void>;
|
|
20
|
+
/** Skip a firing whose scheduled time is older than this — the
|
|
21
|
+
* cluster's built-in backfill guard. Defaults to "1 day". */
|
|
22
|
+
readonly skipIfOlderThan?: Duration.DurationInput;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Default port for the cluster's single-runner listener. Override per deploy. */
|
|
26
|
+
export declare const DEFAULT_RUNNER_LISTEN_PORT: 34000;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build the cluster-cron layer for one schedule. Returns a
|
|
30
|
+
* `Layer<never, never, Sharding>` — the framework provides `Sharding`
|
|
31
|
+
* by merging this into the same runtime as the cluster workflow
|
|
32
|
+
* engine (see `workflowEngineLayer({ extraShardingLayers })`).
|
|
33
|
+
*/
|
|
34
|
+
export declare const makeClusterCronLayer: (spec: ClusterCronSpec) => Layer.Layer<never, never, Sharding.Sharding>;
|
|
35
|
+
|
|
36
|
+
/** Resolve the runner host/port the SAME way `workflowEngineLayer` does:
|
|
37
|
+
* explicit option → `POD_IP` → `VOLTRO_WORKFLOW_RUNNER_HOST` → loopback;
|
|
38
|
+
* port from option → `VOLTRO_WORKFLOW_RUNNER_PORT` → default. */
|
|
39
|
+
export declare const resolveRunnerIdentity: (opts: {
|
|
40
|
+
readonly runnerStorage: RunnerStorage;
|
|
41
|
+
readonly runnerListenHost?: string | undefined;
|
|
42
|
+
readonly runnerListenPort?: number | undefined;
|
|
43
|
+
}) => RunnerIdentity;
|
|
44
|
+
|
|
45
|
+
/** Resolved cluster-runner address + the silent-resume-breaker flag.
|
|
46
|
+
* Single source of truth for both the engine layer and the
|
|
47
|
+
* `/_voltro/inspect/cluster` snapshot, so the CLI never reports an
|
|
48
|
+
* address that differs from the one the runner actually advertises. */
|
|
49
|
+
export declare interface RunnerIdentity {
|
|
50
|
+
readonly host: string;
|
|
51
|
+
readonly port: number;
|
|
52
|
+
/** True iff a SQL-backed runner advertises a loopback host — other
|
|
53
|
+
* pods can't reach it, so cross-pod workflow resume silently breaks. */
|
|
54
|
+
readonly localhostRisk: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Runner-storage backend for `@effect/cluster.SingleRunner`.
|
|
59
|
+
*
|
|
60
|
+
* - `'sql'` — durable across crashes + multi-instance scale-out.
|
|
61
|
+
* Used for postgres / mysql / mariadb / mssql.
|
|
62
|
+
* - `'memory'` — durable replay within the SAME process; no
|
|
63
|
+
* horizontal scale-out. Used for `sqlite` because
|
|
64
|
+
* `@effect/cluster.SqlRunnerStorage` has no SQLite
|
|
65
|
+
* lock-acquisition branch (`pg_advisory_lock` and
|
|
66
|
+
* `GET_LOCK` aren't available there).
|
|
67
|
+
*/
|
|
68
|
+
export declare type RunnerStorage = 'sql' | 'memory';
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Compose the dialect's SqlClient → SingleRunner → ClusterWorkflowEngine
|
|
72
|
+
* into a single layer that user code provides to its program to get
|
|
73
|
+
* durable workflows.
|
|
74
|
+
*
|
|
75
|
+
* `extraShardingLayers` (e.g. `ClusterCron` layers from
|
|
76
|
+
* `cluster`-coordinated schedules) are merged into the same runtime so
|
|
77
|
+
* they share the runner identity + shard map.
|
|
78
|
+
*
|
|
79
|
+
* For dev / tests where no real engine is available, see
|
|
80
|
+
* `inMemoryWorkflowEngineLayer`.
|
|
81
|
+
*/
|
|
82
|
+
export declare const workflowEngineLayer: (options: WorkflowEngineLayerOptions) => Layer.Layer<WorkflowEngine.WorkflowEngine>;
|
|
83
|
+
|
|
84
|
+
export declare interface WorkflowEngineLayerOptions {
|
|
85
|
+
/**
|
|
86
|
+
* The dialect's SqlClient layer (postgres / mysql / mariadb / mssql).
|
|
87
|
+
* For `runnerStorage: 'memory'` it's still required so the cluster's
|
|
88
|
+
* Sharding infra has a SqlClient to satisfy — even though no SQL
|
|
89
|
+
* runs through it for sqlite, the layer's resource lifecycle keeps
|
|
90
|
+
* the cluster's accounting clean.
|
|
91
|
+
*/
|
|
92
|
+
readonly sqlClientLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>;
|
|
93
|
+
/** `'sql'` for postgres / mysql / mariadb / mssql, `'memory'` for sqlite. */
|
|
94
|
+
readonly runnerStorage: RunnerStorage;
|
|
95
|
+
readonly runnerListenHost?: string;
|
|
96
|
+
readonly runnerListenPort?: number;
|
|
97
|
+
/**
|
|
98
|
+
* Extra layers that need the cluster's `Sharding` service — most
|
|
99
|
+
* prominently `ClusterCron` layers for `cluster`-coordinated
|
|
100
|
+
* schedules (plan 45). They're merged into the SAME runtime as the
|
|
101
|
+
* workflow engine so they share the single `SingleRunner` /
|
|
102
|
+
* `Sharding` instance (one runner identity, one shard map). The
|
|
103
|
+
* framework passes these in; user code never touches `Sharding`
|
|
104
|
+
* directly.
|
|
105
|
+
*
|
|
106
|
+
* Each layer requires the cluster's `Sharding` service; that
|
|
107
|
+
* requirement is satisfied by `clusterLayer` below.
|
|
108
|
+
*/
|
|
109
|
+
readonly extraShardingLayers?: ReadonlyArray<Layer.Layer<never, never, Sharding.Sharding>>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { }
|
package/dist/cluster.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ConfigError } from 'effect';
|
|
2
|
+
import { Layer } from 'effect';
|
|
3
|
+
import { Sharding } from '@effect/cluster';
|
|
4
|
+
import { SqlClient } from '@effect/sql';
|
|
5
|
+
import { SqlError } from '@effect/sql';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Everything a dialect supplies to run the cluster-engine suite against
|
|
9
|
+
* its database. The scenarios are otherwise identical across dialects.
|
|
10
|
+
*/
|
|
11
|
+
export declare interface ClusterEngineFixture {
|
|
12
|
+
/** Dialect label for the describe block + workflow-name suffix. */
|
|
13
|
+
readonly name: string;
|
|
14
|
+
/** The dialect's `SqlClient` layer (postgres/mysql/mariadb/mssql/sqlite).
|
|
15
|
+
* Typed via the engine's own option so `@voltro/testing` needs no
|
|
16
|
+
* direct `@effect/sql` dependency. */
|
|
17
|
+
readonly sqlClientLayer: WorkflowEngineLayerOptions['sqlClientLayer'];
|
|
18
|
+
/** `'sql'` for server dialects, `'memory'` for sqlite. */
|
|
19
|
+
readonly runnerStorage: 'sql' | 'memory';
|
|
20
|
+
/** Base cluster-runner listen port (distinct per dialect so suites can
|
|
21
|
+
* run side by side). The suite uses `port`, `port+1`, `port+2`. */
|
|
22
|
+
readonly runnerPort: number;
|
|
23
|
+
/** True iff this dialect supports cross-process workflow resume
|
|
24
|
+
* (= `runnerStorage: 'sql'`). Gates the resume + dormancy scenarios. */
|
|
25
|
+
readonly clusterResume: boolean;
|
|
26
|
+
/** Is the database reachable? Soft-skips the whole suite when not (CI
|
|
27
|
+
* without docker, a dialect's container down). SQLite returns true. */
|
|
28
|
+
readonly reachable: () => Promise<boolean>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register the cluster-engine suite for one dialect. Each
|
|
33
|
+
* `@voltro/sql-*` (or the workflow package) calls this from a thin
|
|
34
|
+
* `cluster.<dialect>.integration.test.ts` with its fixture.
|
|
35
|
+
*/
|
|
36
|
+
export declare const runClusterEngineSuite: (fixture: ClusterEngineFixture) => void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Runner-storage backend for `@effect/cluster.SingleRunner`.
|
|
40
|
+
*
|
|
41
|
+
* - `'sql'` — durable across crashes + multi-instance scale-out.
|
|
42
|
+
* Used for postgres / mysql / mariadb / mssql.
|
|
43
|
+
* - `'memory'` — durable replay within the SAME process; no
|
|
44
|
+
* horizontal scale-out. Used for `sqlite` because
|
|
45
|
+
* `@effect/cluster.SqlRunnerStorage` has no SQLite
|
|
46
|
+
* lock-acquisition branch (`pg_advisory_lock` and
|
|
47
|
+
* `GET_LOCK` aren't available there).
|
|
48
|
+
*/
|
|
49
|
+
declare type RunnerStorage = 'sql' | 'memory';
|
|
50
|
+
|
|
51
|
+
/** Lightweight TCP reachability probe for a dialect's database — the
|
|
52
|
+
* soft-skip a fixture's `reachable()` uses (cheaper than a full connect).
|
|
53
|
+
* SQLite fixtures just return `true` directly. */
|
|
54
|
+
export declare const tcpReachable: (host: string, port: number, timeoutMs?: number) => Promise<boolean>;
|
|
55
|
+
|
|
56
|
+
declare interface WorkflowEngineLayerOptions {
|
|
57
|
+
/**
|
|
58
|
+
* The dialect's SqlClient layer (postgres / mysql / mariadb / mssql).
|
|
59
|
+
* For `runnerStorage: 'memory'` it's still required so the cluster's
|
|
60
|
+
* Sharding infra has a SqlClient to satisfy — even though no SQL
|
|
61
|
+
* runs through it for sqlite, the layer's resource lifecycle keeps
|
|
62
|
+
* the cluster's accounting clean.
|
|
63
|
+
*/
|
|
64
|
+
readonly sqlClientLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>;
|
|
65
|
+
/** `'sql'` for postgres / mysql / mariadb / mssql, `'memory'` for sqlite. */
|
|
66
|
+
readonly runnerStorage: RunnerStorage;
|
|
67
|
+
readonly runnerListenHost?: string;
|
|
68
|
+
readonly runnerListenPort?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Extra layers that need the cluster's `Sharding` service — most
|
|
71
|
+
* prominently `ClusterCron` layers for `cluster`-coordinated
|
|
72
|
+
* schedules (plan 45). They're merged into the SAME runtime as the
|
|
73
|
+
* workflow engine so they share the single `SingleRunner` /
|
|
74
|
+
* `Sharding` instance (one runner identity, one shard map). The
|
|
75
|
+
* framework passes these in; user code never touches `Sharding`
|
|
76
|
+
* directly.
|
|
77
|
+
*
|
|
78
|
+
* Each layer requires the cluster's `Sharding` service; that
|
|
79
|
+
* requirement is satisfied by `clusterLayer` below.
|
|
80
|
+
*/
|
|
81
|
+
readonly extraShardingLayers?: ReadonlyArray<Layer.Layer<never, never, Sharding.Sharding>>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { }
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { C as e, m as t, p as n, v as r, x as i } from "./primitives-CWy1iu5w.js";
|
|
2
|
+
import { i as a } from "./src-CNeLb-4L.js";
|
|
3
|
+
import { i as o, t as s } from "./cluster-Cl6mB2rW.js";
|
|
4
|
+
import { Cron as c, Deferred as l, Effect as u, Fiber as d, Layer as f, Schema as p } from "effect";
|
|
5
|
+
import { beforeAll as m, describe as h, expect as g, test as _ } from "vitest";
|
|
6
|
+
//#region src/clusterTestSuite.ts
|
|
7
|
+
var v = async (e, t, n = 750) => {
|
|
8
|
+
try {
|
|
9
|
+
let r = await import("node:net");
|
|
10
|
+
return await new Promise((i) => {
|
|
11
|
+
let a = new r.Socket(), o = setTimeout(() => {
|
|
12
|
+
a.destroy(), i(!1);
|
|
13
|
+
}, n);
|
|
14
|
+
a.once("connect", () => {
|
|
15
|
+
clearTimeout(o), a.end(), i(!0);
|
|
16
|
+
}), a.once("error", () => {
|
|
17
|
+
clearTimeout(o), i(!1);
|
|
18
|
+
}), a.connect(t, e);
|
|
19
|
+
});
|
|
20
|
+
} catch {
|
|
21
|
+
return !1;
|
|
22
|
+
}
|
|
23
|
+
}, y = (e, t) => u.gen(function* () {
|
|
24
|
+
let n = yield* u.sync(() => Date.now() + t);
|
|
25
|
+
for (; !e();) {
|
|
26
|
+
if (Date.now() > n) return !1;
|
|
27
|
+
yield* u.sleep("250 millis");
|
|
28
|
+
}
|
|
29
|
+
return !0;
|
|
30
|
+
}), b = (v) => {
|
|
31
|
+
let b = `${v.name}-${Date.now()}`, x = (e, t) => o({
|
|
32
|
+
sqlClientLayer: v.sqlClientLayer,
|
|
33
|
+
runnerStorage: v.runnerStorage,
|
|
34
|
+
runnerListenPort: e,
|
|
35
|
+
...t && t.length > 0 ? { extraShardingLayers: t } : {}
|
|
36
|
+
});
|
|
37
|
+
h(`cluster workflow engine — ${v.name}`, () => {
|
|
38
|
+
let o = !1;
|
|
39
|
+
m(async () => {
|
|
40
|
+
o = await v.reachable(), o || console.warn(`[skip] cluster suite — ${v.name} not reachable`);
|
|
41
|
+
}, 1e4), _("a two-step workflow runs end-to-end", async () => {
|
|
42
|
+
if (!o) return;
|
|
43
|
+
let e = `clusterTwoStep_${b}`, n = r({
|
|
44
|
+
name: e,
|
|
45
|
+
payload: { id: p.String },
|
|
46
|
+
success: p.Number,
|
|
47
|
+
idempotencyKey: ({ id: t }) => `${e}:${t}`
|
|
48
|
+
}), i = n.toLayer(() => u.gen(function* () {
|
|
49
|
+
let e = yield* t({
|
|
50
|
+
name: "first",
|
|
51
|
+
success: p.Number,
|
|
52
|
+
execute: u.succeed(40)
|
|
53
|
+
});
|
|
54
|
+
return yield* t({
|
|
55
|
+
name: "second",
|
|
56
|
+
success: p.Number,
|
|
57
|
+
execute: u.succeed(e + 2)
|
|
58
|
+
});
|
|
59
|
+
}));
|
|
60
|
+
g(await u.runPromise(u.scoped(n.execute({ id: `e2e-${b}` }).pipe(u.provide(i.pipe(f.provideMerge(x(v.runnerPort)))))))).toBe(42);
|
|
61
|
+
}, 3e4), _("idempotency-key journal — same payload returns same result", async () => {
|
|
62
|
+
if (!o) return;
|
|
63
|
+
let e = `clusterIdempotency_${b}`, n = 0, i = r({
|
|
64
|
+
name: e,
|
|
65
|
+
payload: { id: p.String },
|
|
66
|
+
success: p.Number,
|
|
67
|
+
idempotencyKey: ({ id: t }) => `${e}:${t}`
|
|
68
|
+
}), a = i.toLayer(() => t({
|
|
69
|
+
name: "compute",
|
|
70
|
+
success: p.Number,
|
|
71
|
+
execute: u.sync(() => (n++, n * 100))
|
|
72
|
+
})).pipe(f.provideMerge(x(v.runnerPort))), [s, c] = await u.runPromise(u.scoped(u.gen(function* () {
|
|
73
|
+
return [yield* i.execute({ id: `same-${b}` }), yield* i.execute({ id: `same-${b}` })];
|
|
74
|
+
}).pipe(u.provide(a))));
|
|
75
|
+
g(s).toBe(c), g(n).toBe(1);
|
|
76
|
+
}, 3e4), _("ClusterCron fires its execute on the shard owner", async () => {
|
|
77
|
+
if (!o) return;
|
|
78
|
+
let e = await u.runPromise(l.make()), t = 0, n = s({
|
|
79
|
+
name: `clusterCron_${b}`,
|
|
80
|
+
cron: c.unsafeParse("*/1 * * * * *", "UTC"),
|
|
81
|
+
execute: async () => {
|
|
82
|
+
t += 1, await u.runPromise(l.succeed(e, !0)).catch(() => {});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
g(await u.runPromise(u.scoped(u.gen(function* () {
|
|
86
|
+
return yield* f.build(x(v.runnerPort + 1, [n])), yield* l.await(e).pipe(u.timeout("25 seconds"), u.orElseSucceed(() => !1));
|
|
87
|
+
})))).toBe(!0), g(t).toBeGreaterThanOrEqual(1);
|
|
88
|
+
}, 35e3), _.runIf(v.clusterResume)("a sleeping workflow resumes on a fresh runner after the first is torn down", async () => {
|
|
89
|
+
if (!o) return;
|
|
90
|
+
let e = `dormancyResume_${b}`, i = [], a = [], s = () => r({
|
|
91
|
+
name: e,
|
|
92
|
+
payload: { id: p.String },
|
|
93
|
+
success: p.String,
|
|
94
|
+
idempotencyKey: ({ id: t }) => `${e}:${t}`
|
|
95
|
+
}), c = (e, r) => e.toLayer(() => u.gen(function* () {
|
|
96
|
+
return yield* t({
|
|
97
|
+
name: "step1",
|
|
98
|
+
success: p.Void,
|
|
99
|
+
execute: u.sync(() => {
|
|
100
|
+
i.push(r);
|
|
101
|
+
})
|
|
102
|
+
}), yield* n({
|
|
103
|
+
name: "nap",
|
|
104
|
+
duration: "30 seconds"
|
|
105
|
+
}), yield* t({
|
|
106
|
+
name: "step2",
|
|
107
|
+
success: p.Void,
|
|
108
|
+
execute: u.sync(() => {
|
|
109
|
+
a.push(r);
|
|
110
|
+
})
|
|
111
|
+
}), "done";
|
|
112
|
+
})), l = v.runnerPort + 2, m = (e) => {
|
|
113
|
+
let t = s();
|
|
114
|
+
return {
|
|
115
|
+
wf: t,
|
|
116
|
+
layer: c(t, e).pipe(f.provideMerge(x(l)))
|
|
117
|
+
};
|
|
118
|
+
}, h = { id: `resume-${b}` }, _ = await u.runPromise(u.scoped(u.gen(function* () {
|
|
119
|
+
let { wf: e, layer: t } = m("A"), n = yield* f.build(t), r = yield* u.forkDaemon(e.execute(h).pipe(u.provide(n), u.ignore));
|
|
120
|
+
return (yield* y(() => i.length > 0, 6e4)) ? (yield* u.sleep("1 second"), r) : yield* u.die(/* @__PURE__ */ Error("runner A never executed step1"));
|
|
121
|
+
})));
|
|
122
|
+
await u.runPromise(d.interrupt(_).pipe(u.ignore)), g(i).toEqual(["A"]), g(a).toEqual([]), g(await u.runPromise(u.scoped(u.gen(function* () {
|
|
123
|
+
let { wf: e, layer: t } = m("B"), n = yield* f.build(t);
|
|
124
|
+
return yield* e.execute(h).pipe(u.provide(n), u.timeout("45 seconds"), u.orElseSucceed(() => "TIMEOUT"));
|
|
125
|
+
})))).toBe("done"), g(i).toEqual(["A"]), g(a).toEqual(["B"]);
|
|
126
|
+
}, 12e4), _.runIf(v.clusterResume)("dormancy loop: a wake signal survives teardown and is cleared on resume", async () => {
|
|
127
|
+
if (!o) return;
|
|
128
|
+
let s = `dormancyLoop_${b}`, c = `wr_${s}`, l = [], m = /* @__PURE__ */ new Map(), h = a({
|
|
129
|
+
store: {
|
|
130
|
+
insert: async (e, t) => t,
|
|
131
|
+
update: async () => null,
|
|
132
|
+
query: async () => []
|
|
133
|
+
},
|
|
134
|
+
wakeups: {
|
|
135
|
+
register: async (e, t) => {
|
|
136
|
+
m.set(e, { wakeAt: t });
|
|
137
|
+
},
|
|
138
|
+
cancel: async (e) => {
|
|
139
|
+
m.delete(e);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}), _ = () => r({
|
|
143
|
+
name: s,
|
|
144
|
+
payload: { id: p.String },
|
|
145
|
+
success: p.String,
|
|
146
|
+
idempotencyKey: ({ id: e }) => `${s}:${e}`
|
|
147
|
+
}), S = (r, a) => r.toLayer(() => u.gen(function* () {
|
|
148
|
+
return yield* t({
|
|
149
|
+
name: "step1",
|
|
150
|
+
success: p.Void,
|
|
151
|
+
execute: u.void
|
|
152
|
+
}), yield* n({
|
|
153
|
+
name: "nap",
|
|
154
|
+
duration: "30 seconds"
|
|
155
|
+
}), yield* t({
|
|
156
|
+
name: "step2",
|
|
157
|
+
success: p.Void,
|
|
158
|
+
execute: u.sync(() => {
|
|
159
|
+
l.push(a);
|
|
160
|
+
})
|
|
161
|
+
}), "done";
|
|
162
|
+
}).pipe(u.locally(i, c), u.provideService(e, h))), C = v.runnerPort + 2, w = (e) => {
|
|
163
|
+
let t = _();
|
|
164
|
+
return {
|
|
165
|
+
wf: t,
|
|
166
|
+
layer: S(t, e).pipe(f.provideMerge(x(C)))
|
|
167
|
+
};
|
|
168
|
+
}, T = { id: `loop-${b}` }, E = await u.runPromise(u.scoped(u.gen(function* () {
|
|
169
|
+
let { wf: e, layer: t } = w("A"), n = yield* f.build(t), r = yield* u.forkDaemon(e.execute(T).pipe(u.provide(n), u.ignore));
|
|
170
|
+
return (yield* y(() => m.has(c), 6e4)) ? (yield* u.sleep("1 second"), r) : yield* u.die(/* @__PURE__ */ Error("workflow never registered a wakeup"));
|
|
171
|
+
})));
|
|
172
|
+
await u.runPromise(d.interrupt(E).pipe(u.ignore)), g(m.has(c)).toBe(!0), g(l).toEqual([]), g(await u.runPromise(u.scoped(u.gen(function* () {
|
|
173
|
+
let { wf: e, layer: t } = w("B"), n = yield* f.build(t);
|
|
174
|
+
return yield* e.execute(T).pipe(u.provide(n), u.timeout("45 seconds"), u.orElseSucceed(() => "TIMEOUT"));
|
|
175
|
+
})))).toBe("done"), g(l).toEqual(["B"]), g(m.has(c)).toBe(!1);
|
|
176
|
+
}, 12e4);
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
//#endregion
|
|
180
|
+
export { b as runClusterEngineSuite, v as tcpReachable };
|