Skip to content

Commit

Permalink
Merge pull request #964 from thefrontside/v3-scoped
Browse files Browse the repository at this point in the history
✨ Backfill the `scoped()` API to v3
  • Loading branch information
cowboyd authored Jan 20, 2025
2 parents e378114 + 985cf74 commit a4de07c
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export * from "./signal.ts";
export * from "./ensure.ts";
export * from "./race.ts";
export * from "./with-resolvers.ts";
export * from "./scoped.ts";
28 changes: 28 additions & 0 deletions lib/scoped.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Operation } from "./types.ts";
import { call } from "./call.ts";

/**
* Encapsulate an operation so that no effects will persist outside of
* it. All active effects such as concurrent tasks and resources will be
* shut down, and all contexts will be restored to their values outside
* of the scope.
*
* @example
* ```js
* import { useAbortSignal } from "effection";
* function* example() {
* let signal = yield* scoped(function*() {
* return yield* useAbortSignal();
* });
* return signal.aborted; //=> true
* }
* ```
*
* @param operation - the operation to be encapsulated
*
* @returns the scoped operation
*/
export function scoped<T>(operation: () => Operation<T>): Operation<T> {
return call(operation);
}
158 changes: 158 additions & 0 deletions test/scoped.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import {
createContext,
resource,
run,
scoped,
sleep,
spawn,
suspend,
} from "../mod.ts";
import { describe, expect, it } from "./suite.ts";

describe("scoped", () => {
describe("task", () => {
it("shuts down after completion", () =>
run(function* () {
let didEnter = false;
let didExit = false;

yield* scoped(function* () {
yield* spawn(function* () {
try {
didEnter = true;
yield* suspend();
} finally {
didExit = true;
}
});
yield* sleep(0);
});

expect(didEnter).toBe(true);
expect(didExit).toBe(true);
}));

it("shuts down after error", () =>
run(function* () {
let didEnter = false;
let didExit = false;

try {
yield* scoped(function* () {
yield* spawn(function* () {
try {
didEnter = true;
yield* suspend();
} finally {
didExit = true;
}
});
yield* sleep(0);
throw new Error("boom!");
});
} catch (error) {
expect(error).toMatchObject({ message: "boom!" });
expect(didEnter).toBe(true);
expect(didExit).toBe(true);
}
}));

it("delimits error boundaries", () =>
run(function* () {
try {
yield* scoped(function* () {
yield* spawn(function* () {
throw new Error("boom!");
});
yield* suspend();
});
} catch (error) {
expect(error).toMatchObject({ message: "boom!" });
}
}));
});
describe("resource", () => {
it("shuts down after completion", () =>
run(function* () {
let status = "pending";
yield* scoped(function* () {
yield* resource<void>(function* (provide) {
try {
status = "open";
yield* provide();
} finally {
status = "closed";
}
});
yield* sleep(0);
expect(status).toEqual("open");
});
expect(status).toEqual("closed");
}));

it("shuts down after error", () =>
run(function* () {
let status = "pending";
try {
yield* scoped(function* () {
yield* resource<void>(function* (provide) {
try {
status = "open";
yield* provide();
} finally {
status = "closed";
}
});
yield* sleep(0);
expect(status).toEqual("open");
throw new Error("boom!");
});
} catch (error) {
expect((error as Error).message).toEqual("boom!");
expect(status).toEqual("closed");
}
}));

it("delimits error boundaries", () =>
run(function* () {
try {
yield* scoped(function* () {
yield* resource<void>(function* (provide) {
yield* spawn(function* () {
yield* sleep(0);
throw new Error("boom!");
});
yield* provide();
});
yield* suspend();
});
} catch (error) {
expect(error).toMatchObject({ message: "boom!" });
}
}));
});
describe("context", () => {
let context = createContext<string>("greetting", "hi");
it("is restored after exiting scope", () =>
run(function* () {
yield* scoped(function* () {
yield* context.set("hola");
});
expect(yield* context.get()).toEqual("hi");
}));

it("is restored after erroring", () =>
run(function* () {
try {
yield* scoped(function* () {
yield* context.set("hola");
throw new Error("boom!");
});
} catch (error) {
expect(error).toMatchObject({ message: "boom!" });
} finally {
expect(yield* context.get()).toEqual("hi");
}
}));
});
});

0 comments on commit a4de07c

Please sign in to comment.