Skip to content

Commit

Permalink
✨ Backfill the scoped() API
Browse files Browse the repository at this point in the history
Allow v3 users to "upgrade" to the "scoped" API.

`call()` and `action()` are the ways to establish concurrency
boundaries and error handling boundaries in v3, but they are getting
simplified and losing those capabilities (it's a good thing!) However,
in order to make the upgrade seemless, we want to have a backfill that
let's you use the scoped API _before_ you switch the version of
Effection to 4.

This wraps `call()` which has the same function signature, and copies
over its jsdoc so that the `scoped()` intellisense will be available
in IDEs
  • Loading branch information
cowboyd committed Jan 20, 2025
1 parent e378114 commit 985cf74
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 985cf74

Please sign in to comment.