@effectionx/effect-ts 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/LICENSE +9 -0
- package/README.md +266 -0
- package/dist/effect-runtime.d.ts +96 -0
- package/dist/effect-runtime.d.ts.map +1 -0
- package/dist/effect-runtime.js +77 -0
- package/dist/effection-runtime.d.ts +100 -0
- package/dist/effection-runtime.d.ts.map +1 -0
- package/dist/effection-runtime.js +82 -0
- package/dist/mod.d.ts +5 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +4 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/effect-runtime.ts +136 -0
- package/effect.test.ts +479 -0
- package/effection-runtime.ts +126 -0
- package/mod.ts +7 -0
- package/package.json +34 -0
- package/tsconfig.json +10 -0
package/effect.test.ts
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { describe, it } from "@effectionx/bdd";
|
|
2
|
+
import { Context, Effect, Exit, Fiber, Layer } from "effect";
|
|
3
|
+
import { call, scoped, sleep, spawn, suspend } from "effection";
|
|
4
|
+
import { expect } from "expect";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
EffectionRuntime,
|
|
8
|
+
makeEffectRuntime,
|
|
9
|
+
makeEffectionRuntime,
|
|
10
|
+
} from "./mod.ts";
|
|
11
|
+
|
|
12
|
+
describe("@effectionx/effect-ts", () => {
|
|
13
|
+
describe("EffectRuntime - Effect inside Effection", () => {
|
|
14
|
+
describe("run()", () => {
|
|
15
|
+
it("runs a successful Effect and returns the value", function* () {
|
|
16
|
+
const runtime = yield* makeEffectRuntime();
|
|
17
|
+
const result = yield* runtime.run(Effect.succeed(42));
|
|
18
|
+
expect(result).toEqual(42);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("runs Effect with transformations (map, flatMap)", function* () {
|
|
22
|
+
const runtime = yield* makeEffectRuntime();
|
|
23
|
+
const program = Effect.succeed(10).pipe(
|
|
24
|
+
Effect.map((n) => n * 2),
|
|
25
|
+
Effect.flatMap((n) => Effect.succeed(n + 1)),
|
|
26
|
+
);
|
|
27
|
+
const result = yield* runtime.run(program);
|
|
28
|
+
expect(result).toEqual(21);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("throws Effect failures as JavaScript errors", function* () {
|
|
32
|
+
const runtime = yield* makeEffectRuntime();
|
|
33
|
+
let caught: unknown;
|
|
34
|
+
try {
|
|
35
|
+
yield* runtime.run(Effect.fail(new Error("boom")));
|
|
36
|
+
throw new Error("should have thrown");
|
|
37
|
+
} catch (error) {
|
|
38
|
+
caught = error;
|
|
39
|
+
}
|
|
40
|
+
expect((caught as Error).message).toEqual("boom");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("handles Effect.die (defects)", function* () {
|
|
44
|
+
const runtime = yield* makeEffectRuntime();
|
|
45
|
+
let caught: unknown;
|
|
46
|
+
try {
|
|
47
|
+
yield* runtime.run(Effect.die("unexpected"));
|
|
48
|
+
throw new Error("should have thrown");
|
|
49
|
+
} catch (error) {
|
|
50
|
+
caught = error;
|
|
51
|
+
}
|
|
52
|
+
// Effect.die wraps in FiberFailure, check the message
|
|
53
|
+
expect(String(caught)).toContain("unexpected");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("runs Effect.sleep correctly", function* () {
|
|
57
|
+
const runtime = yield* makeEffectRuntime();
|
|
58
|
+
const start = Date.now();
|
|
59
|
+
yield* runtime.run(Effect.sleep("50 millis"));
|
|
60
|
+
const elapsed = Date.now() - start;
|
|
61
|
+
expect(elapsed).toBeGreaterThanOrEqual(45);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("runs Effect.gen programs", function* () {
|
|
65
|
+
const runtime = yield* makeEffectRuntime();
|
|
66
|
+
const program = Effect.gen(function* () {
|
|
67
|
+
const a = yield* Effect.succeed(1);
|
|
68
|
+
const b = yield* Effect.succeed(2);
|
|
69
|
+
return a + b;
|
|
70
|
+
});
|
|
71
|
+
const result = yield* runtime.run(program);
|
|
72
|
+
expect(result).toEqual(3);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("works with Effect.async", function* () {
|
|
76
|
+
const runtime = yield* makeEffectRuntime();
|
|
77
|
+
const program = Effect.async<number>((resume) => {
|
|
78
|
+
const timer = setTimeout(() => resume(Effect.succeed(42)), 10);
|
|
79
|
+
return Effect.sync(() => clearTimeout(timer));
|
|
80
|
+
});
|
|
81
|
+
const result = yield* runtime.run(program);
|
|
82
|
+
expect(result).toEqual(42);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("runExit()", () => {
|
|
87
|
+
it("returns Exit.Success for successful Effect", function* () {
|
|
88
|
+
const runtime = yield* makeEffectRuntime();
|
|
89
|
+
const exit = yield* runtime.runExit(Effect.succeed(42));
|
|
90
|
+
expect(Exit.isSuccess(exit)).toEqual(true);
|
|
91
|
+
if (Exit.isSuccess(exit)) {
|
|
92
|
+
expect(exit.value).toEqual(42);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("returns Exit.Failure for failed Effect", function* () {
|
|
97
|
+
const runtime = yield* makeEffectRuntime();
|
|
98
|
+
const exit = yield* runtime.runExit(Effect.fail(new Error("boom")));
|
|
99
|
+
expect(Exit.isFailure(exit)).toEqual(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("returns Exit.Failure for Effect.die", function* () {
|
|
103
|
+
const runtime = yield* makeEffectRuntime();
|
|
104
|
+
const exit = yield* runtime.runExit(Effect.die("defect"));
|
|
105
|
+
expect(Exit.isFailure(exit)).toEqual(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("preserves the full Cause in Exit.Failure", function* () {
|
|
109
|
+
const runtime = yield* makeEffectRuntime();
|
|
110
|
+
const error = new Error("typed error");
|
|
111
|
+
const exit = yield* runtime.runExit(Effect.fail(error));
|
|
112
|
+
expect(Exit.isFailure(exit)).toEqual(true);
|
|
113
|
+
// Can inspect Cause for error details
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("with optional layer", () => {
|
|
118
|
+
it("provides services from the layer", function* () {
|
|
119
|
+
class Counter extends Context.Tag("Counter")<
|
|
120
|
+
Counter,
|
|
121
|
+
{ value: number }
|
|
122
|
+
>() {}
|
|
123
|
+
const CounterLive = Layer.succeed(Counter, { value: 100 });
|
|
124
|
+
|
|
125
|
+
const runtime = yield* makeEffectRuntime(CounterLive);
|
|
126
|
+
// Types flow correctly - runtime is EffectRuntime<Counter>
|
|
127
|
+
const result = yield* runtime.run(
|
|
128
|
+
Effect.gen(function* () {
|
|
129
|
+
const counter = yield* Counter;
|
|
130
|
+
return counter.value;
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
expect(result).toEqual(100);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("supports composed layers", function* () {
|
|
137
|
+
class A extends Context.Tag("A")<A, { a: number }>() {}
|
|
138
|
+
class B extends Context.Tag("B")<B, { b: number }>() {}
|
|
139
|
+
|
|
140
|
+
const ALive = Layer.succeed(A, { a: 1 });
|
|
141
|
+
const BLive = Layer.succeed(B, { b: 2 });
|
|
142
|
+
const AppLayer = Layer.mergeAll(ALive, BLive);
|
|
143
|
+
|
|
144
|
+
const runtime = yield* makeEffectRuntime(AppLayer);
|
|
145
|
+
// Types flow correctly - runtime is EffectRuntime<A | B>
|
|
146
|
+
const result = yield* runtime.run(
|
|
147
|
+
Effect.gen(function* () {
|
|
148
|
+
const a = yield* A;
|
|
149
|
+
const b = yield* B;
|
|
150
|
+
return a.a + b.b;
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
expect(result).toEqual(3);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("cancellation", () => {
|
|
158
|
+
it("interrupts Effect when Effection task is halted", function* () {
|
|
159
|
+
let finalizerRan = false;
|
|
160
|
+
let effectStarted = false;
|
|
161
|
+
|
|
162
|
+
// Run in a nested scope so we can control when it ends
|
|
163
|
+
yield* scoped(function* () {
|
|
164
|
+
const runtime = yield* makeEffectRuntime();
|
|
165
|
+
|
|
166
|
+
// Spawn so the effect runs concurrently and we can end the scope
|
|
167
|
+
yield* spawn(function* () {
|
|
168
|
+
yield* runtime.run(
|
|
169
|
+
Effect.gen(function* () {
|
|
170
|
+
effectStarted = true;
|
|
171
|
+
yield* Effect.addFinalizer(() =>
|
|
172
|
+
Effect.sync(() => {
|
|
173
|
+
finalizerRan = true;
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
yield* Effect.sleep("10 seconds");
|
|
177
|
+
}).pipe(Effect.scoped),
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Give the effect time to start before scope ends
|
|
182
|
+
yield* sleep(50);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// After the scoped block completes, the finalizer should have run
|
|
186
|
+
expect(effectStarted).toEqual(true);
|
|
187
|
+
expect(finalizerRan).toEqual(true);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("lifecycle", () => {
|
|
192
|
+
it("disposes ManagedRuntime when Effection scope ends", function* () {
|
|
193
|
+
let runtimeActive = false;
|
|
194
|
+
|
|
195
|
+
yield* scoped(function* () {
|
|
196
|
+
const runtime = yield* makeEffectRuntime();
|
|
197
|
+
yield* runtime.run(
|
|
198
|
+
Effect.sync(() => {
|
|
199
|
+
runtimeActive = true;
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
// After scoped block completes, runtime should be disposed
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Runtime was active during the scope
|
|
206
|
+
expect(runtimeActive).toEqual(true);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("EffectionRuntime - Effection inside Effect", () => {
|
|
212
|
+
// Helper to run Effect programs with EffectionRuntime
|
|
213
|
+
const runWithEffection = <A, E>(
|
|
214
|
+
effect: Effect.Effect<A, E, EffectionRuntime>,
|
|
215
|
+
): Promise<A> =>
|
|
216
|
+
Effect.runPromise(
|
|
217
|
+
effect.pipe(Effect.provide(makeEffectionRuntime()), Effect.scoped),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const runWithEffectionExit = <A, E>(
|
|
221
|
+
effect: Effect.Effect<A, E, EffectionRuntime>,
|
|
222
|
+
): Promise<Exit.Exit<A, E>> =>
|
|
223
|
+
Effect.runPromiseExit(
|
|
224
|
+
effect.pipe(Effect.provide(makeEffectionRuntime()), Effect.scoped),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
describe("run()", () => {
|
|
228
|
+
it("runs a successful Operation and returns the value", function* () {
|
|
229
|
+
const result = yield* call(() =>
|
|
230
|
+
runWithEffection(
|
|
231
|
+
Effect.gen(function* () {
|
|
232
|
+
const runtime = yield* EffectionRuntime;
|
|
233
|
+
return yield* runtime.run(function* () {
|
|
234
|
+
return 42;
|
|
235
|
+
});
|
|
236
|
+
}),
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
expect(result).toEqual(42);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("runs Operation with sleep", function* () {
|
|
243
|
+
const start = Date.now();
|
|
244
|
+
yield* call(() =>
|
|
245
|
+
runWithEffection(
|
|
246
|
+
Effect.gen(function* () {
|
|
247
|
+
const runtime = yield* EffectionRuntime;
|
|
248
|
+
yield* runtime.run(function* () {
|
|
249
|
+
yield* sleep(50);
|
|
250
|
+
});
|
|
251
|
+
}),
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
const elapsed = Date.now() - start;
|
|
255
|
+
expect(elapsed).toBeGreaterThanOrEqual(45);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("wraps Operation errors as UnknownException", function* () {
|
|
259
|
+
const exit = yield* call(() =>
|
|
260
|
+
runWithEffectionExit(
|
|
261
|
+
Effect.gen(function* () {
|
|
262
|
+
const runtime = yield* EffectionRuntime;
|
|
263
|
+
return yield* runtime.run(function* () {
|
|
264
|
+
throw new Error("boom");
|
|
265
|
+
});
|
|
266
|
+
}),
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(Exit.isFailure(exit)).toEqual(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("returns an Effect", function* () {
|
|
274
|
+
const result = yield* call(() =>
|
|
275
|
+
runWithEffection(
|
|
276
|
+
Effect.gen(function* () {
|
|
277
|
+
const runtime = yield* EffectionRuntime;
|
|
278
|
+
const effect = runtime.run(function* () {
|
|
279
|
+
return 42;
|
|
280
|
+
});
|
|
281
|
+
expect(Effect.isEffect(effect)).toEqual(true);
|
|
282
|
+
return yield* effect;
|
|
283
|
+
}),
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
expect(result).toEqual(42);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("cancellation", () => {
|
|
291
|
+
it("runs Effection finally blocks when Effect scope ends", function* () {
|
|
292
|
+
let finalizerRan = false;
|
|
293
|
+
|
|
294
|
+
yield* call(() =>
|
|
295
|
+
runWithEffection(
|
|
296
|
+
Effect.gen(function* () {
|
|
297
|
+
const runtime = yield* EffectionRuntime;
|
|
298
|
+
yield* runtime
|
|
299
|
+
.run(function* () {
|
|
300
|
+
try {
|
|
301
|
+
yield* suspend();
|
|
302
|
+
} finally {
|
|
303
|
+
finalizerRan = true;
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
.pipe(Effect.fork);
|
|
307
|
+
|
|
308
|
+
yield* Effect.sleep("50 millis");
|
|
309
|
+
// Scope ends here, which should close Effection scope
|
|
310
|
+
}),
|
|
311
|
+
),
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
expect(finalizerRan).toEqual(true);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("runs Effection finally blocks when Effect fiber is interrupted", function* () {
|
|
318
|
+
let finalizerRan = false;
|
|
319
|
+
|
|
320
|
+
yield* call(() =>
|
|
321
|
+
runWithEffection(
|
|
322
|
+
Effect.gen(function* () {
|
|
323
|
+
const runtime = yield* EffectionRuntime;
|
|
324
|
+
|
|
325
|
+
const fiber = yield* runtime
|
|
326
|
+
.run(function* () {
|
|
327
|
+
try {
|
|
328
|
+
yield* suspend();
|
|
329
|
+
} finally {
|
|
330
|
+
finalizerRan = true;
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
.pipe(Effect.fork);
|
|
334
|
+
|
|
335
|
+
yield* Effect.sleep("50 millis");
|
|
336
|
+
yield* Fiber.interrupt(fiber);
|
|
337
|
+
}),
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
expect(finalizerRan).toEqual(true);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe("lifecycle", () => {
|
|
346
|
+
it("closes Effection scope when Effect scope ends", function* () {
|
|
347
|
+
let scopeEnded = false;
|
|
348
|
+
|
|
349
|
+
yield* call(async () => {
|
|
350
|
+
await Effect.runPromise(
|
|
351
|
+
Effect.gen(function* () {
|
|
352
|
+
const runtime = yield* EffectionRuntime;
|
|
353
|
+
yield* runtime
|
|
354
|
+
.run(function* () {
|
|
355
|
+
try {
|
|
356
|
+
yield* suspend();
|
|
357
|
+
} finally {
|
|
358
|
+
scopeEnded = true;
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
.pipe(Effect.fork);
|
|
362
|
+
yield* Effect.sleep("10 millis");
|
|
363
|
+
}).pipe(Effect.provide(makeEffectionRuntime()), Effect.scoped),
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
expect(scopeEnded).toEqual(true);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe("bidirectional", () => {
|
|
373
|
+
it("Effect -> Effection: runs Effect pipeline in Effection", function* () {
|
|
374
|
+
const runtime = yield* makeEffectRuntime();
|
|
375
|
+
|
|
376
|
+
const result = yield* runtime.run(
|
|
377
|
+
Effect.succeed(42).pipe(Effect.map((n) => n * 2)),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
expect(result).toEqual(84);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("nested: Effect uses EffectionRuntime which runs Operation", function* () {
|
|
384
|
+
const effectRuntime = yield* makeEffectRuntime();
|
|
385
|
+
|
|
386
|
+
const result = yield* effectRuntime.run(
|
|
387
|
+
Effect.gen(function* () {
|
|
388
|
+
const effectionRuntime = yield* EffectionRuntime;
|
|
389
|
+
return yield* effectionRuntime.run(function* () {
|
|
390
|
+
yield* sleep(10);
|
|
391
|
+
return "nested";
|
|
392
|
+
});
|
|
393
|
+
}).pipe(Effect.provide(makeEffectionRuntime()), Effect.scoped),
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
expect(result).toEqual("nested");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("deeply nested: Effection -> Effect -> Effection -> Effect", function* () {
|
|
400
|
+
const outerEffectRuntime = yield* makeEffectRuntime();
|
|
401
|
+
|
|
402
|
+
const result = yield* outerEffectRuntime.run(
|
|
403
|
+
Effect.gen(function* () {
|
|
404
|
+
const effectionRuntime = yield* EffectionRuntime;
|
|
405
|
+
|
|
406
|
+
return yield* effectionRuntime.run(function* () {
|
|
407
|
+
const innerEffectRuntime = yield* makeEffectRuntime();
|
|
408
|
+
return yield* innerEffectRuntime.run(Effect.succeed("deep"));
|
|
409
|
+
});
|
|
410
|
+
}).pipe(Effect.provide(makeEffectionRuntime()), Effect.scoped),
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
expect(result).toEqual("deep");
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe("resource cleanup", () => {
|
|
418
|
+
it("cleans up Effect resources when Effection scope halts", function* () {
|
|
419
|
+
const cleanupOrder: string[] = [];
|
|
420
|
+
|
|
421
|
+
yield* scoped(function* () {
|
|
422
|
+
const runtime = yield* makeEffectRuntime();
|
|
423
|
+
|
|
424
|
+
yield* spawn(function* () {
|
|
425
|
+
yield* runtime.run(
|
|
426
|
+
Effect.gen(function* () {
|
|
427
|
+
yield* Effect.acquireRelease(
|
|
428
|
+
Effect.sync(() => {
|
|
429
|
+
cleanupOrder.push("acquired");
|
|
430
|
+
}),
|
|
431
|
+
() =>
|
|
432
|
+
Effect.sync(() => {
|
|
433
|
+
cleanupOrder.push("released");
|
|
434
|
+
}),
|
|
435
|
+
);
|
|
436
|
+
// Keep the effect running so we can test cleanup
|
|
437
|
+
yield* Effect.never;
|
|
438
|
+
}).pipe(Effect.scoped),
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
yield* sleep(50);
|
|
443
|
+
// Scope ends here, halting the spawned task
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// After scoped block completes, cleanup should have happened
|
|
447
|
+
expect(cleanupOrder).toContain("acquired");
|
|
448
|
+
expect(cleanupOrder).toContain("released");
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("cleans up Effection resources when Effect interrupts", function* () {
|
|
452
|
+
const cleanupOrder: string[] = [];
|
|
453
|
+
|
|
454
|
+
yield* call(() =>
|
|
455
|
+
Effect.runPromise(
|
|
456
|
+
Effect.gen(function* () {
|
|
457
|
+
const runtime = yield* EffectionRuntime;
|
|
458
|
+
|
|
459
|
+
const fiber = yield* runtime
|
|
460
|
+
.run(function* () {
|
|
461
|
+
try {
|
|
462
|
+
cleanupOrder.push("started");
|
|
463
|
+
yield* suspend();
|
|
464
|
+
} finally {
|
|
465
|
+
cleanupOrder.push("cleaned");
|
|
466
|
+
}
|
|
467
|
+
})
|
|
468
|
+
.pipe(Effect.fork);
|
|
469
|
+
|
|
470
|
+
yield* Effect.sleep("50 millis");
|
|
471
|
+
yield* Fiber.interrupt(fiber);
|
|
472
|
+
}).pipe(Effect.provide(makeEffectionRuntime()), Effect.scoped),
|
|
473
|
+
),
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
expect(cleanupOrder).toEqual(["started", "cleaned"]);
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import type { UnknownException } from "effect/Cause";
|
|
3
|
+
import { type Operation, type Scope, createScope } from "effection";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A runtime for executing Effection operations inside Effect programs.
|
|
7
|
+
*/
|
|
8
|
+
export interface EffectionRuntime {
|
|
9
|
+
/**
|
|
10
|
+
* Run an Effection operation and return its result as an Effect.
|
|
11
|
+
*
|
|
12
|
+
* Errors thrown in the operation become `UnknownException` in Effect.
|
|
13
|
+
* The Effection scope is automatically cleaned up when the Effect completes
|
|
14
|
+
* or is interrupted.
|
|
15
|
+
*
|
|
16
|
+
* @param operation - The Effection operation (generator function) to run
|
|
17
|
+
* @returns An Effect that yields the operation's result
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const program = Effect.gen(function* () {
|
|
22
|
+
* const runtime = yield* EffectionRuntime;
|
|
23
|
+
* return yield* runtime.run(function* () {
|
|
24
|
+
* yield* sleep(100);
|
|
25
|
+
* return "hello";
|
|
26
|
+
* });
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
run<T>(operation: () => Operation<T>): Effect.Effect<T, UnknownException>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Effect Context Tag for accessing the EffectionRuntime.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const program = Effect.gen(function* () {
|
|
39
|
+
* const runtime = yield* EffectionRuntime;
|
|
40
|
+
* // use runtime.run(...)
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export const EffectionRuntime =
|
|
45
|
+
Context.GenericTag<EffectionRuntime>("EffectionRuntime");
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create an Effect Layer that provides an EffectionRuntime.
|
|
49
|
+
*
|
|
50
|
+
* The Effection scope is automatically closed when the Effect scope ends,
|
|
51
|
+
* ensuring proper cleanup of Effection resources.
|
|
52
|
+
*
|
|
53
|
+
* @param parent - Optional parent Effection scope. If provided, the runtime's
|
|
54
|
+
* scope will inherit all contexts from the parent scope.
|
|
55
|
+
* @returns An Effect Layer providing EffectionRuntime
|
|
56
|
+
*
|
|
57
|
+
* @example Basic usage
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { Effect } from "effect";
|
|
60
|
+
* import { sleep } from "effection";
|
|
61
|
+
* import { makeEffectionRuntime, EffectionRuntime } from "@effectionx/effect";
|
|
62
|
+
*
|
|
63
|
+
* const program = Effect.gen(function* () {
|
|
64
|
+
* const runtime = yield* EffectionRuntime;
|
|
65
|
+
* const result = yield* runtime.run(function* () {
|
|
66
|
+
* yield* sleep(100);
|
|
67
|
+
* return "hello from effection";
|
|
68
|
+
* });
|
|
69
|
+
* return result;
|
|
70
|
+
* });
|
|
71
|
+
*
|
|
72
|
+
* await Effect.runPromise(
|
|
73
|
+
* program.pipe(
|
|
74
|
+
* Effect.provide(makeEffectionRuntime()),
|
|
75
|
+
* Effect.scoped
|
|
76
|
+
* )
|
|
77
|
+
* );
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* @example With parent scope (to inherit Effection contexts)
|
|
81
|
+
* ```ts
|
|
82
|
+
* import { Effect } from "effect";
|
|
83
|
+
* import { useScope } from "effection";
|
|
84
|
+
* import { makeEffectionRuntime, EffectionRuntime } from "@effectionx/effect";
|
|
85
|
+
*
|
|
86
|
+
* function* myOperation() {
|
|
87
|
+
* const scope = yield* useScope();
|
|
88
|
+
* const result = yield* call(() =>
|
|
89
|
+
* Effect.runPromise(
|
|
90
|
+
* Effect.gen(function* () {
|
|
91
|
+
* const runtime = yield* EffectionRuntime;
|
|
92
|
+
* return yield* runtime.run(function* () {
|
|
93
|
+
* // Can access Effection contexts from parent scope
|
|
94
|
+
* return "hello";
|
|
95
|
+
* });
|
|
96
|
+
* }).pipe(Effect.provide(makeEffectionRuntime(scope)), Effect.scoped)
|
|
97
|
+
* )
|
|
98
|
+
* );
|
|
99
|
+
* return result;
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export function makeEffectionRuntime(
|
|
104
|
+
parent?: Scope,
|
|
105
|
+
): Layer.Layer<EffectionRuntime> {
|
|
106
|
+
return Layer.scoped(
|
|
107
|
+
EffectionRuntime,
|
|
108
|
+
Effect.gen(function* () {
|
|
109
|
+
const [scope, close] = createScope(parent);
|
|
110
|
+
|
|
111
|
+
const run: EffectionRuntime["run"] = <T>(
|
|
112
|
+
operation: () => Operation<T>,
|
|
113
|
+
) => {
|
|
114
|
+
return Effect.tryPromise(() => scope.run(operation));
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
yield* Effect.addFinalizer(() =>
|
|
118
|
+
Effect.gen(function* () {
|
|
119
|
+
yield* Effect.tryPromise(() => close()).pipe(Effect.exit);
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return { run };
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
}
|
package/mod.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Effect -> Effection
|
|
2
|
+
export { makeEffectRuntime } from "./effect-runtime.ts";
|
|
3
|
+
export type { EffectRuntime } from "./effect-runtime.ts";
|
|
4
|
+
|
|
5
|
+
// Effection -> Effect
|
|
6
|
+
export { EffectionRuntime, makeEffectionRuntime } from "./effection-runtime.ts";
|
|
7
|
+
export type { EffectionRuntime as EffectionRuntimeType } from "./effection-runtime.ts";
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effectionx/effect-ts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/mod.js",
|
|
6
|
+
"types": "./dist/mod.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"development": "./mod.ts",
|
|
10
|
+
"default": "./dist/mod.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"effect": "^3",
|
|
15
|
+
"effection": "^3 || ^4"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"effect": "^3",
|
|
19
|
+
"@effectionx/bdd": "0.4.1"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "engineering@frontside.com",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/thefrontside/effectionx.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/thefrontside/effectionx/issues"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">= 22"
|
|
32
|
+
},
|
|
33
|
+
"sideEffects": false
|
|
34
|
+
}
|