1import { Action } from "@/Action";
2import * as Cloudflare from "@/Cloudflare";
3import * as Test from "@/Test/Alchemy";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8
9const { test } = Test.make({ providers: Cloudflare.providers() });
10
11const logLevel = Effect.provideService(
12 MinimumLogLevel,
13 process.env.DEBUG ? "Debug" : "Info",
14);
15
16// Binding a KV namespace inside an Action via `ReadWriteNamespaceLocal` — the
17// local (current-credentials) implementation of the `ReadWriteNamespace`
18// binding. Exercises the client (put/get/list/delete) and the accessor
19// mechanism (`yield* namespace.namespaceId` resolved at apply time).
20test.provider(
21 "ReadWriteNamespaceLocal: seed and read a namespace from an Action",
22 (stack) =>
23 Effect.gen(function* () {
24 yield* stack.destroy();
25
26 const out = yield* stack.deploy(
27 Effect.gen(function* () {
28 const namespace = yield* Cloudflare.KV.Namespace("SeedNamespace");
29
30 const Seed = Action(
31 "Seed",
32 Effect.gen(function* () {
33 const kv = yield* Cloudflare.KV.ReadWriteNamespace(namespace);
34 // Accessor — resolved at apply time against the tracker.
35 const namespaceId = yield* namespace.namespaceId;
36
37 return Effect.fn(function* () {
38 yield* kv.put("greeting", "hello world");
39
40 // KV is eventually consistent — retry the read-back until the
41 // value propagates (bounded so the test fails fast).
42 const value = yield* kv.get("greeting").pipe(
43 Effect.flatMap((v) =>
44 v === "hello world"
45 ? Effect.succeed(v)
46 : Effect.fail("not yet propagated" as const),
47 ),
48 Effect.retry({
49 schedule: Schedule.spaced("1 second"),
50 times: 10,
51 }),
52 Effect.orElseSucceed(() => null),
53 );
54
55 const listed = yield* kv.list();
56 const names = listed.keys.map((k) => k.name);
57
58 yield* kv.delete("greeting");
59
60 const afterDelete = yield* kv.get("greeting").pipe(
61 Effect.flatMap((v) =>
62 v === null
63 ? Effect.succeed(v)
64 : Effect.fail("not yet deleted" as const),
65 ),
66 Effect.retry({
67 schedule: Schedule.spaced("1 second"),
68 times: 10,
69 }),
70 Effect.orElseSucceed(() => "still present" as string | null),
71 );
72
73 return {
74 namespaceId: yield* namespaceId,
75 value,
76 names,
77 afterDelete,
78 };
79 });
80 }).pipe(Effect.provide(Cloudflare.KV.ReadWriteNamespaceLocal)),
81 );
82
83 return yield* Seed({});
84 }),
85 );
86
87 expect(out.namespaceId).toBeTruthy();
88 expect(out.value).toBe("hello world");
89 expect(out.names).toContain("greeting");
90 expect(out.afterDelete).toBeNull();
91
92 yield* stack.destroy();
93 }).pipe(logLevel),
94 {
95 tags: ["provider:cloudflare", "provider:cloudflare:kv", "live"],
96 timeout: 120_000,
97 },
98);