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