@redflow/client 0.0.1

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,42 @@
1
+ import { RedisClient } from "bun";
2
+ import { defineWorkflow, startWorker } from "../../src/index";
3
+
4
+ const url = process.env.REDIS_URL;
5
+ const prefix = process.env.REDFLOW_PREFIX;
6
+ const workflowName = process.env.TEST_WORKFLOW;
7
+ const queue = process.env.TEST_QUEUE;
8
+ const step1Key = process.env.TEST_STEP1_KEY;
9
+
10
+ if (!url || !prefix || !workflowName || !queue || !step1Key) {
11
+ console.error("Missing env: REDIS_URL/REDFLOW_PREFIX/TEST_WORKFLOW/TEST_QUEUE/TEST_STEP1_KEY");
12
+ process.exit(2);
13
+ }
14
+
15
+ const redis = new RedisClient(url);
16
+
17
+ defineWorkflow(
18
+ {
19
+ name: workflowName,
20
+ queue,
21
+ retries: { maxAttempts: 3 },
22
+ },
23
+ async ({ step }) => {
24
+ await step.run({ name: "step1" }, async () => {
25
+ await redis.incr(step1Key);
26
+ return { ok: true };
27
+ });
28
+
29
+ // Simulate a hard crash after step is persisted.
30
+ process.exit(1);
31
+ },
32
+ );
33
+
34
+ await startWorker({
35
+ url,
36
+ prefix,
37
+ queues: [queue],
38
+ runtime: { leaseMs: 200, blmoveTimeoutSec: 0.2, reaperIntervalMs: 50 },
39
+ });
40
+
41
+ // Keep alive.
42
+ await new Promise(() => {});
@@ -0,0 +1,63 @@
1
+ import { RedisClient } from "bun";
2
+ import { createClient, defineWorkflow, startWorker } from "../../src/index";
3
+
4
+ const url = process.env.REDIS_URL;
5
+ const prefix = process.env.REDFLOW_PREFIX;
6
+ const workflowName = process.env.TEST_WORKFLOW;
7
+ const queue = process.env.TEST_QUEUE;
8
+ const step1Key = process.env.TEST_STEP1_KEY;
9
+ const runId = process.env.TEST_RUN_ID;
10
+
11
+ if (!url || !prefix || !workflowName || !queue || !step1Key || !runId) {
12
+ console.error("Missing env: REDIS_URL/REDFLOW_PREFIX/TEST_WORKFLOW/TEST_QUEUE/TEST_STEP1_KEY/TEST_RUN_ID");
13
+ process.exit(2);
14
+ }
15
+
16
+ const redis = new RedisClient(url);
17
+ const client = createClient({ redis, prefix });
18
+
19
+ defineWorkflow(
20
+ {
21
+ name: workflowName,
22
+ queue,
23
+ retries: { maxAttempts: 3 },
24
+ },
25
+ async ({ step }) => {
26
+ await step.run({ name: "step1" }, async () => {
27
+ await redis.incr(step1Key);
28
+ return { ok: true };
29
+ });
30
+
31
+ await step.run({ name: "step2" }, async () => true);
32
+ return { ok: true };
33
+ },
34
+ );
35
+
36
+ const worker = await startWorker({
37
+ url,
38
+ prefix,
39
+ queues: [queue],
40
+ runtime: { leaseMs: 200, reaperIntervalMs: 50, blmoveTimeoutSec: 0.2 },
41
+ });
42
+
43
+ const deadline = Date.now() + 10_000;
44
+ while (Date.now() < deadline) {
45
+ const run = await client.getRun(runId);
46
+ if (run?.status === "succeeded") {
47
+ await worker.stop();
48
+ redis.close();
49
+ process.exit(0);
50
+ }
51
+ if (run?.status === "failed") {
52
+ console.error("run failed", run.error);
53
+ await worker.stop();
54
+ redis.close();
55
+ process.exit(3);
56
+ }
57
+ await new Promise((r) => setTimeout(r, 50));
58
+ }
59
+
60
+ console.error("timeout waiting for run to complete");
61
+ await worker.stop();
62
+ redis.close();
63
+ process.exit(4);
@@ -0,0 +1,18 @@
1
+ import net from "node:net";
2
+
3
+ export function getFreePort(): Promise<number> {
4
+ return new Promise((resolve, reject) => {
5
+ const server = net.createServer();
6
+ server.unref();
7
+ server.on("error", reject);
8
+ server.listen(0, "127.0.0.1", () => {
9
+ const addr = server.address();
10
+ if (!addr || typeof addr === "string") {
11
+ server.close(() => reject(new Error("Failed to allocate port")));
12
+ return;
13
+ }
14
+ const port = addr.port;
15
+ server.close(() => resolve(port));
16
+ });
17
+ });
18
+ }
@@ -0,0 +1,77 @@
1
+ import { RedisClient } from "bun";
2
+ import { getFreePort } from "./getFreePort";
3
+
4
+ export type RedisTestServer = {
5
+ url: string;
6
+ port: number;
7
+ stop(): Promise<void>;
8
+ };
9
+
10
+ /**
11
+ * If REDIS_URL is set (e.g. in CI with a service container), use it directly
12
+ * instead of spawning a child process — bun test kills spawned processes.
13
+ */
14
+ export async function startRedisTestServer(): Promise<RedisTestServer> {
15
+ if (process.env.REDIS_URL) {
16
+ const url = process.env.REDIS_URL;
17
+ const port = Number(new URL(url).port) || 6379;
18
+
19
+ const client = new RedisClient(url);
20
+ const pong = await client.ping();
21
+ client.close();
22
+ if (pong !== "PONG") throw new Error(`External Redis at ${url} is not responding`);
23
+
24
+ return { url, port, async stop() {} };
25
+ }
26
+
27
+ const port = await getFreePort();
28
+ const url = `redis://127.0.0.1:${port}`;
29
+
30
+ const proc = Bun.spawn([
31
+ "redis-server",
32
+ "--port",
33
+ String(port),
34
+ "--bind",
35
+ "127.0.0.1",
36
+ "--save",
37
+ "",
38
+ "--appendonly",
39
+ "no",
40
+ ], {
41
+ stdout: "ignore",
42
+ stderr: "pipe",
43
+ });
44
+
45
+ const client = new RedisClient(url);
46
+
47
+ const started = Date.now();
48
+ while (true) {
49
+ try {
50
+ const pong = await client.ping();
51
+ if (pong === "PONG") break;
52
+ } catch {
53
+ // retry
54
+ }
55
+
56
+ if (Date.now() - started > 5000) {
57
+ const stderr = await new Response(proc.stderr).text().catch(() => "");
58
+ console.log('kill');
59
+ proc.kill("SIGKILL");
60
+ client.close();
61
+ throw new Error(`Redis failed to start. stderr:\n${stderr}`);
62
+ }
63
+
64
+ await new Promise((r) => setTimeout(r, 50));
65
+ }
66
+
67
+ client.close();
68
+
69
+ return {
70
+ url,
71
+ port,
72
+ async stop() {
73
+ proc.kill("SIGTERM");
74
+ await proc.exited.catch(() => {});
75
+ },
76
+ };
77
+ }
@@ -0,0 +1,16 @@
1
+ export async function waitFor(
2
+ fn: () => Promise<boolean> | boolean,
3
+ options?: { timeoutMs?: number; intervalMs?: number; label?: string },
4
+ ): Promise<void> {
5
+ const timeoutMs = options?.timeoutMs ?? 5000;
6
+ const intervalMs = options?.intervalMs ?? 50;
7
+ const deadline = Date.now() + timeoutMs;
8
+
9
+ while (Date.now() < deadline) {
10
+ if (await fn()) return;
11
+ await new Promise((r) => setTimeout(r, intervalMs));
12
+ }
13
+
14
+ const label = options?.label ? `: ${options.label}` : "";
15
+ throw new Error(`waitFor timeout${label}`);
16
+ }