-
Notifications
You must be signed in to change notification settings - Fork 3
fix(effect-ts): await Effect cleanup during Effection scope teardown #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import { type Effect, type Exit, Layer, ManagedRuntime } from "effect"; | ||
| import { type Operation, action, call, resource } from "effection"; | ||
| import { type Effect, Exit, Layer, ManagedRuntime } from "effect"; | ||
| import { type Operation, action, resource, until } from "effection"; | ||
|
|
||
| /** | ||
| * A runtime for executing Effect programs inside Effection operations. | ||
|
|
@@ -103,34 +103,87 @@ export function makeEffectRuntime<R = never>( | |
| layer ?? Layer.empty, | ||
| ) as ManagedRuntime.ManagedRuntime<R, never>; | ||
|
|
||
| interface PendingExecution { | ||
| abort: () => void; | ||
| settled: Promise<void>; | ||
| } | ||
|
|
||
| const pending = new Set<PendingExecution>(); | ||
|
|
||
| function startManaged<T>(runPromise: (signal: AbortSignal) => Promise<T>) { | ||
| const controller = new AbortController(); | ||
| let done = false; | ||
|
|
||
| const execution = { | ||
| abort: () => { | ||
| if (!done) { | ||
| controller.abort(); | ||
| } | ||
| }, | ||
| settled: Promise.resolve(), | ||
| } as PendingExecution; | ||
|
|
||
| const promise = runPromise(controller.signal); | ||
|
|
||
| execution.settled = promise | ||
| .then( | ||
| () => undefined, | ||
| () => undefined, | ||
| ) | ||
| .finally(() => { | ||
| done = true; | ||
| pending.delete(execution); | ||
| }); | ||
|
|
||
| pending.add(execution); | ||
|
|
||
| return { promise, abort: execution.abort, signal: controller.signal }; | ||
| } | ||
|
|
||
| const run: EffectRuntime<R>["run"] = <A, E>( | ||
| effect: Effect.Effect<A, E, R>, | ||
| ) => { | ||
| return action<A>((resolve, reject) => { | ||
| const controller = new AbortController(); | ||
| managedRuntime | ||
| .runPromise(effect, { signal: controller.signal }) | ||
| .then(resolve, reject); | ||
| return () => controller.abort(); | ||
| const { promise, abort, signal } = startManaged((signal) => | ||
| managedRuntime.runPromise(effect, { signal }), | ||
| ); | ||
|
|
||
| promise.then(resolve, (error) => { | ||
| if (!signal.aborted) { | ||
| reject(error); | ||
| } | ||
| }); | ||
| return abort; | ||
| }); | ||
| }; | ||
|
|
||
| const runExit: EffectRuntime<R>["runExit"] = <A, E>( | ||
| effect: Effect.Effect<A, E, R>, | ||
| ) => { | ||
| return action<Exit.Exit<A, E>>((resolve, reject) => { | ||
| const controller = new AbortController(); | ||
| managedRuntime | ||
| .runPromiseExit(effect, { signal: controller.signal }) | ||
| .then(resolve, reject); | ||
| return () => controller.abort(); | ||
| return action<Exit.Exit<A, E>>((resolve, _reject) => { | ||
| const { promise, abort, signal } = startManaged((signal) => | ||
| managedRuntime.runPromiseExit(effect, { signal }), | ||
| ); | ||
|
|
||
| promise.then(resolve, (error) => { | ||
| if (!signal.aborted) { | ||
| resolve(Exit.die(error) as Exit.Exit<A, E>); | ||
| } | ||
| }); | ||
| return abort; | ||
| }); | ||
| }; | ||
|
|
||
| try { | ||
| yield* provide({ run, runExit }); | ||
| } finally { | ||
| yield* call(() => managedRuntime.dispose()); | ||
| const active = Array.from(pending); | ||
| for (const execution of active) { | ||
| execution.abort(); | ||
| } | ||
|
|
||
| yield* until(Promise.all(active.map((execution) => execution.settled))); | ||
| yield* until(managedRuntime.dispose()); | ||
|
Comment on lines
+180
to
+186
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Document this teardown guarantee explicitly. This now makes scope exit wait for interrupted Effect executions to finish cleanup before disposing the 🤖 Prompt for AI Agents |
||
| } | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| { | ||
| "name": "@effectionx/effect-ts", | ||
| "description": "Bidirectional interop between Effect-TS and Effection", | ||
| "version": "0.1.2", | ||
| "version": "0.1.4", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Check if version 0.1.3 was ever published or tagged
# Check npm registry for published versions
echo "=== Checking npm registry for `@effectionx/effect-ts` versions ==="
npm view `@effectionx/effect-ts` versions --json 2>/dev/null || echo "Package not found in npm registry or not accessible"
# Check git tags for version 0.1.3
echo -e "\n=== Checking git tags for effect-ts@0.1.3 ==="
git tag -l | grep -E 'effect-ts.*0\.1\.3|@effectionx/effect-ts.*0\.1\.3' || echo "No tags found for version 0.1.3"
# Check git log for version bump commits
echo -e "\n=== Checking recent version bump commits in effect-ts/package.json ==="
git log --oneline --all -20 -- effect-ts/package.jsonRepository: thefrontside/effectionx Length of output: 844 Correct the version to 0.1.4 or clarify the double-patch bump. The version 0.1.3 was never published to npm or tagged in git history. Since this PR contains a single bug fix (race condition in effect-runtime.ts), semantic versioning requires only a single patch bump: 0.1.2 → 0.1.3. Verify that 0.1.4 is intentional (e.g., another version bump is planned in this PR), otherwise change the version to 0.1.3. 🤖 Prompt for AI Agents |
||
| "keywords": ["effection", "effectionx", "interop", "effect-ts", "effect"], | ||
| "type": "module", | ||
| "main": "./dist/mod.js", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Harden
startManaged()against synchronous launcher failures.The tracking invariant here still depends on the launcher never throwing before
pending.add(). Register first and normalize the call throughPromise.resolve().then(...)so sync failures stay on the samesettledcleanup path.🛠️ Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents