Skip to content

Commit

Permalink
Merge pull request #838 from thefrontside/scope-run-link
Browse files Browse the repository at this point in the history
If an uncaught error happens in Scope.run(), crash the scope.
  • Loading branch information
cowboyd authored Nov 30, 2023
2 parents 55d40fa + 45b181f commit fab170c
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 1 deletion.
2 changes: 2 additions & 0 deletions lib/run/frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export function createFrame<T>(options: FrameOptions<T>): Frame<T> {
thunk = thunks.pop()!;
}

frame.exited = true;

let result = thunk.value;

let exit: Exit<T>;
Expand Down
17 changes: 17 additions & 0 deletions lib/run/scope.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Context, Frame, Future, Operation, Scope } from "../types.ts";
import { evaluate } from "../deps.ts";
import { create } from "./create.ts";
import { createFrame } from "./frame.ts";
import { getframe, suspend } from "../instructions.ts";
Expand Down Expand Up @@ -40,8 +41,24 @@ export function createScope(frame?: Frame): [Scope, () => Future<void>] {

let scope = create<Scope>("Scope", {}, {
run<T>(operation: () => Operation<T>) {
if (parent.exited) {
let error = new Error(
`cannot call run() on a scope that has already been exited`,
);
error.name = "InactiveScopeError";
throw error;
}

let frame = parent.createChild(operation);
frame.enter();

evaluate(function* () {
let result = yield* frame;
if (!result.ok) {
yield* parent.crash(result.error);
}
});

return frame.getTask();
},
get<T>(context: Context<T>) {
Expand Down
1 change: 1 addition & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ import type { FrameResult } from "./run/types.ts";
export interface Frame<T = unknown> extends Computation<FrameResult<T>> {
id: number;
context: Record<string, unknown>;
exited?: true;
aborted?: boolean;
getTask(): Task<T>;
createChild<C>(operation: () => Operation<C>): Frame<C>;
Expand Down
20 changes: 19 additions & 1 deletion test/scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe("Scope", () => {
scope.run(function* () {
throw error;
});
await expect(close()).resolves.toEqual(void 0);
await expect(close()).resolves.toBeUndefined();
expect(tester.status).toEqual("closed");
});

Expand Down Expand Up @@ -106,6 +106,24 @@ describe("Scope", () => {
expect(scope.get(context)).toEqual("Hello World!");
await expect(scope.run(() => context)).resolves.toEqual("Hello World!");
});

it("propagates uncaught errors within a scope", async () => {
let error = new Error("boom");
let result = run(function* () {
let scope = yield* useScope();
scope.run(function* () {
throw error;
});
yield* suspend();
});
await expect(result).rejects.toBe(error);
});

it("throws an error if you try to run() with a dead scope", async () => {
let scope = await run(useScope);

expect(() => scope.run(function* () {})).toThrow("cannot call");
});
});

interface Tester {
Expand Down

0 comments on commit fab170c

Please sign in to comment.