Skip to content

Commit

Permalink
Merge pull request #933 from thefrontside/scoped
Browse files Browse the repository at this point in the history
✨Add `scoped()` higher order operation
  • Loading branch information
cowboyd authored Dec 18, 2024
2 parents 9956833 + 48710b3 commit 351b1a3
Show file tree
Hide file tree
Showing 3 changed files with 190 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 @@ -22,3 +22,4 @@ export * from "./abort-signal.ts";
export * from "./main.ts";
export * from "./with-resolvers.ts";
export * from "./async.ts";
export * from "./scoped.ts";
31 changes: 31 additions & 0 deletions lib/scoped.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Operation } from "./types.ts";
import { trap } from "./task.ts";
import { createScopeInternal } from "./scope.ts";
import { useCoroutine } from "./coroutine.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.
*
* @param operation - the operation to be encapsulated
*
* @returns the scoped operation
*/
export function scoped<T>(operation: () => Operation<T>): Operation<T> {
return {
*[Symbol.iterator]() {
let routine = yield* useCoroutine();
let original = routine.scope;
let [scope, destroy] = createScopeInternal(original);
try {
routine.scope = scope;
return yield* trap(operation);
} finally {
routine.scope = original;
yield* destroy();
}
},
};
}
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 351b1a3

Please sign in to comment.