diff --git a/lib/instructions.ts b/lib/instructions.ts index 043d34f5..ba49e26a 100644 --- a/lib/instructions.ts +++ b/lib/instructions.ts @@ -54,52 +54,42 @@ function Suspend(frame: Frame) { * Create an {@link Operation} that can be either resolved (or rejected) with * a synchronous callback. This is the Effection equivalent of `new Promise()`. * - * The action body is itself an operation that runs in a new scope that is - * destroyed completely before program execution returns to the point where the - * action was yielded to. + * The action body is a function that enters the effect, and returns a function that + * will be called to exit the action.. * * For example: * * ```js - * let five = yield* action(function*(resolve, reject) { - * setTimeout(() => { + * let five = yield* action((resolve, reject) => { + * let timeout = setTimeout(() => { * if (Math.random() > 5) { * resolve(5) * } else { * reject(new Error("bad luck!")); * } * }, 1000); - * }); - * - * ``` - * - * However, it is customary to explicitly {@link suspend} inside the body of the - * action so that whenever the action resolves, appropriate cleanup code can - * run. The preceeding example would be more correctly written as: - * - * ```js - * let five = yield* action(function*(resolve) { - * let timeoutId = setTimeout(() => { - * if (Math.random() > 5) { - * resolve(5) - * } else { - * reject(new Error("bad luck!")); - * } - * }, 1000); - * try { - * yield* suspend(); - * } finally { - * clearTimout(timeoutId); - * } + * return () => clearTimeout(timeout); * }); * ``` * * @typeParam T - type of the action's result. - * @param operation - body of the action + * @param body - enter and exit the action * @returns an operation producing the resolved value, or throwing the rejected error */ +export function action( + enter: (resolve: Resolve, reject: Reject) => () => void, +): Operation; + +/** + * @deprecated `action()` used with an operation will be removed in v4. + */ export function action( operation: (resolve: Resolve, reject: Reject) => Operation, +): Operation; +export function action( + operation: + | ((resolve: Resolve, reject: Reject) => Operation) + | ((resolve: Resolve, reject: Reject) => () => void), ): Operation { return instruction(function Action(frame) { return shift>(function* (k) { @@ -119,8 +109,17 @@ export function action( let reject: Reject = (error) => settle(Err(error)); let child = frame.createChild(function* () { - yield* operation(resolve, reject); - yield* suspend(); + let iterable = operation(resolve, reject); + if (typeof iterable === "function") { + try { + yield* suspend(); + } finally { + iterable(); + } + } else { + yield* iterable; + yield* suspend(); + } }); yield* reset(function* () { diff --git a/test/action.test.ts b/test/action.test.ts index f2287e37..657a3cac 100644 --- a/test/action.test.ts +++ b/test/action.test.ts @@ -119,4 +119,84 @@ describe("action", () => { }); expect(didReach).toEqual(false); }); + + describe("v4 api", () => { + it("can resolve", async () => { + let didClear = false; + let task = run(() => + action((resolve) => { + let timeout = setTimeout(() => resolve(42), 5); + return () => { + didClear = true; + clearTimeout(timeout); + }; + }) + ); + + await expect(task).resolves.toEqual(42); + expect(didClear).toEqual(true); + }); + + it("can reject", async () => { + let didClear = false; + let error = new Error("boom"); + let task = run(() => + action((_, reject) => { + let timeout = setTimeout(() => reject(error), 5); + return () => { + didClear = true; + clearTimeout(timeout); + }; + }) + ); + + await expect(task).rejects.toEqual(error); + expect(didClear).toEqual(true); + }); + + it("can resolve without ever suspending", async () => { + let result = await run(() => + action((resolve) => { + resolve("hello"); + return () => {}; + }) + ); + + expect(result).toEqual("hello"); + }); + + it("can reject without ever suspending", async () => { + let error = new Error("boom"); + let task = run(() => + action((_, reject) => { + reject(error); + return () => {}; + }) + ); + await expect(task).rejects.toEqual(error); + }); + + it("fails if the operation fails", async () => { + let task = run(() => + action(() => { + throw new Error("boom"); + }) + ); + await expect(task).rejects.toHaveProperty("message", "boom"); + }); + + it("fails if the shutdown fails", async () => { + let error = new Error("boom"); + let task = run(() => + action((resolve) => { + let timeout = setTimeout(resolve, 5); + return () => { + clearTimeout(timeout); + throw error; + }; + }) + ); + await expect(task).rejects.toEqual(error); + }); + }); });