1import { Browser } from "@/Cloudflare/Workers/Browser.ts";
2import { Images } from "@/Cloudflare/Images/Images.ts";
3import { RateLimit } from "@/Cloudflare/Workers/RateLimit.ts";
4import { VersionMetadata } from "@/Cloudflare/Workers/VersionMetadata.ts";
5import { Worker } from "@/Cloudflare/Workers/Worker.ts";
6import { Resource } from "@/Resource";
7import * as Test from "@/Test/Alchemy";
8import { effectClass } from "@/Util/effect.ts";
9import { describe, expect } from "alchemy-test";
10import * as Effect from "effect/Effect";
11import { TestLayers, TestResource } from "./test.resources.ts";
12
13const { test } = Test.make({ providers: TestLayers() });
14
15// A throwaway resource type used to exercise the `({ methods })` overload
16// without mutating the shared `TestResource`.
17interface MethodsResource extends Resource<
18 "Test.EffectableMethods",
19 {},
20 { value: string }
21> {}
22
23describe(
24 "Effectable: migrated constructs are real Effects",
25 { tags: ["unit", "local"] },
26 () => {
27 test(
28 "Resource constructor class is an Effect",
29 Effect.gen(function* () {
30 expect(Effect.isEffect(TestResource)).toBe(true);
31 // still iterable (yield*-able)
32 expect(typeof (TestResource as any)[Symbol.iterator]).toBe("function");
33 }),
34 );
35
36 test(
37 "Resource.ref(...) returns an Effect",
38 Effect.gen(function* () {
39 expect(Effect.isEffect(TestResource.ref("X"))).toBe(true);
40 }),
41 );
42
43 test(
44 "a resource instance call is an Effect",
45 Effect.gen(function* () {
46 expect(Effect.isEffect(TestResource("A", { string: "x" }))).toBe(true);
47 }),
48 );
49
50 test(
51 "effectClass(...) and its subclasses are Effects",
52 Effect.gen(function* () {
53 const Klass = effectClass(Effect.succeed(1));
54 expect(Effect.isEffect(Klass)).toBe(true);
55
56 class Sub extends effectClass<{ x: number }>()(
57 Effect.succeed({ x: 1 }),
58 ) {}
59 // static Effect protocol is inherited through the constructor chain
60 expect(Effect.isEffect(Sub)).toBe(true);
61 }),
62 );
63
64 test(
65 "Platform/Worker construct is an Effect",
66 Effect.gen(function* () {
67 const w = Worker("EffectableProbeWorker", { main: "./unused.ts" });
68 expect(Effect.isEffect(w)).toBe(true);
69 expect(typeof (w as any)[Symbol.iterator]).toBe("function");
70 }),
71 { tags: ["provider:cloudflare", "provider:cloudflare:worker"] },
72 );
73 },
74);
75
76describe(
77 "Effectable: deliberate non-Effect binding markers",
78 {
79 tags: [
80 "unit",
81 "provider:cloudflare",
82 "provider:cloudflare:images",
83 "provider:cloudflare:worker",
84 "local",
85 ],
86 },
87 () => {
88 // These markers MUST stay non-Effects (InferEnv + the Worker `env`-value
89 // resolver both branch on `Effect.isEffect`). They remain yield*-able via
90 // `[Symbol.iterator]`. See the marker JSDoc for the rationale.
91 const markers: Array<[name: string, marker: any]> = [
92 ["Images", Images("IMAGES")],
93 [
94 "RateLimit",
95 RateLimit("THROTTLE", {
96 namespaceId: 1,
97 simple: { limit: 1, period: 60 },
98 }),
99 ],
100 ["Browser", Browser("BROWSER")],
101 ["VersionMetadata", VersionMetadata("CF_VERSION_METADATA")],
102 ];
103
104 for (const [name, marker] of markers) {
105 test(
106 `${name} marker is NOT an Effect but stays yield*-able`,
107 Effect.gen(function* () {
108 expect(Effect.isEffect(marker)).toBe(false);
109 expect(typeof marker[Symbol.iterator]).toBe("function");
110 }),
111 );
112 }
113 },
114);
115
116describe(
117 "Effectable: Effect.all / Effect.forEach over constructs",
118 { tags: ["unit", "local"] },
119 () => {
120 test(
121 "Effect.all over effectClass instances executes each",
122 Effect.gen(function* () {
123 const [a, b] = yield* Effect.all([
124 effectClass(Effect.succeed(1)),
125 effectClass(Effect.succeed(2)),
126 ]);
127 expect([a, b]).toEqual([1, 2]);
128 }),
129 );
130
131 test(
132 "Effect.forEach over effectClass instances executes each",
133 Effect.gen(function* () {
134 const result = yield* Effect.forEach(
135 [effectClass(Effect.succeed("a")), effectClass(Effect.succeed("b"))],
136 (eff) => eff,
137 );
138 expect(result).toEqual(["a", "b"]);
139 }),
140 );
141
142 test.provider(
143 "Effect.all over resource constructor calls deploys all of them",
144 (stack) =>
145 Effect.gen(function* () {
146 const strings = yield* Effect.gen(function* () {
147 const resources = yield* Effect.all([
148 TestResource("A", { string: "a" }),
149 TestResource("B", { string: "b" }),
150 ]);
151 return resources.map((r) => r.string);
152 }).pipe(stack.deploy);
153 expect(strings).toEqual(["a", "b"]);
154 }),
155 );
156
157 test.provider(
158 "Effect.forEach over resource constructor calls deploys all of them",
159 (stack) =>
160 Effect.gen(function* () {
161 const strings = yield* Effect.gen(function* () {
162 const resources = yield* Effect.forEach(["A", "B", "C"], (id) =>
163 TestResource(id, { string: id.toLowerCase() }),
164 );
165 return resources.map((r) => r.string);
166 }).pipe(stack.deploy);
167 expect(strings).toEqual(["a", "b", "c"]);
168 }),
169 );
170 },
171);
172
173describe("Effectable: overloads", { tags: ["unit", "local"] }, () => {
174 test(
175 "yield* a bare Resource class resolves to its constructor",
176 Effect.gen(function* () {
177 const ctor = yield* TestResource;
178 expect(typeof ctor).toBe("function");
179 }),
180 );
181
182 test(
183 "Resource class .pipe behaves as piping an Effect",
184 Effect.gen(function* () {
185 const piped = TestResource.pipe(Effect.map((ctor) => typeof ctor));
186 expect(Effect.isEffect(piped)).toBe(true);
187 expect(yield* piped).toBe("function");
188 }),
189 );
190
191 test(
192 "Resource({ methods }) augments the class and keeps it an Effect",
193 Effect.gen(function* () {
194 const R = Resource<MethodsResource>("Test.EffectableMethods");
195 const augmented = R({ greet: () => "hi" });
196 // the `({ methods })` overload mutates + returns the same class
197 expect(augmented).toBe(R);
198 expect((R as any).greet()).toBe("hi");
199 expect(Effect.isEffect(R)).toBe(true);
200 }),
201 );
202
203 test.provider(
204 "Resource accepts props-as-Effect (Effect<Props> overload)",
205 (stack) =>
206 Effect.gen(function* () {
207 const value = yield* Effect.gen(function* () {
208 const A = yield* TestResource(
209 "A",
210 Effect.succeed({ string: "fromEffect" }),
211 );
212 return A.string;
213 }).pipe(stack.deploy);
214 expect(value).toEqual("fromEffect");
215 }),
216 );
217
218 test(
219 "Worker.of(shape) returns the shape and the tagged class is an Effect",
220 Effect.gen(function* () {
221 const W = (Worker as any)()("EffectableTaggedWorker", {
222 main: "./unused.ts",
223 });
224 expect(Effect.isEffect(W)).toBe(true);
225 expect(typeof W.of).toBe("function");
226 expect(W.of({ foo: 1 })).toEqual({ foo: 1 });
227 }),
228 { tags: ["provider:cloudflare", "provider:cloudflare:worker"] },
229 );
230});