diff --git a/lib/mod.ts b/lib/mod.ts index 70f260b3..496ba273 100644 --- a/lib/mod.ts +++ b/lib/mod.ts @@ -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"; diff --git a/lib/scoped.ts b/lib/scoped.ts new file mode 100644 index 00000000..f1825ed5 --- /dev/null +++ b/lib/scoped.ts @@ -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(operation: () => Operation): Operation { + return call(operation); +} diff --git a/test/scoped.test.ts b/test/scoped.test.ts new file mode 100644 index 00000000..88a03c6f --- /dev/null +++ b/test/scoped.test.ts @@ -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(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(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(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("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"); + } + })); + }); +});