Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/effected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2623,7 +2623,7 @@ export function runAsync<E extends Effected<Effect, unknown>>(

return new Promise((resolve, reject) => {
const iterate = (...args: [] | [unknown]) => {
if (context.terminated) return context.terminatedValue;
if (context.terminated) return resolve(context.terminatedValue as never);

let done: boolean | undefined;
let value: any;
Expand All @@ -2636,7 +2636,8 @@ export function runAsync<E extends Effected<Effect, unknown>>(
}

// We use a while loop to avoid stack overflow when there are many synchronous effects
while (!done) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (!done && !context.terminated) {
if (!value) {
reject(
new Error(
Expand All @@ -2649,7 +2650,7 @@ export function runAsync<E extends Effected<Effect, unknown>>(
while (value instanceof Effect && typeof (value as any).defaultHandler === "function") {
value = handleEffect(context, effect.name, value, (value as any).defaultHandler).value;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (context.terminated) return context.terminatedValue;
if (context.terminated) return resolve(context.terminatedValue as never);
}
if (value instanceof Effect) {
reject(new UnhandledEffectError(value, `Unhandled effect: ${stringifyEffect(value)}`));
Expand Down Expand Up @@ -2693,7 +2694,8 @@ export function runAsync<E extends Effected<Effect, unknown>>(
return;
}

resolve(value);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
resolve(context.terminated ? (context.terminatedValue as never) : value);
};

iterate();
Expand Down
124 changes: 124 additions & 0 deletions test/effected.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,130 @@ describe("Default handlers", () => {
expect(badProgram.runSync()).toBe("Cannot divide by zero");
});

describe("runAsync termination settlement", () => {
it.each([false, true])(
"should resolve a default termination value after an async boundary: %s",
async (afterAsync) => {
const events: string[] = [];
const stop = effect<[], never, string>()("stop", {
defaultHandler: ({ terminate }) => terminate("stopped"),
});
const program = effected(function* () {
if (afterAsync) yield* effectify(Promise.resolve());
yield* stop();
events.push("continued");
return "completed";
});

await expect(program.runAsync()).resolves.toBe("stopped");
expect(events).toEqual([]);
},
1000,
);

it("should resolve when a default generator terminates after awaiting", async () => {
const events: string[] = [];
const stop = effect<[], never, number>()("stop", {
*defaultHandler({ terminate }) {
yield* effectify(Promise.resolve());
terminate(7);
},
});
const program = effected(function* () {
yield* stop();
events.push("continued");
return 99;
});

await expect(program.runAsync()).resolves.toBe(7);
expect(events).toEqual([]);
});

it("should resolve when a default generator terminates after a synchronous effect", async () => {
const events: string[] = [];
const resume = effect<[], void>()("resume", {
defaultHandler: ({ resume }) => resume(),
});
const stop = effect<[], never, number>()("stop", {
*defaultHandler({ terminate }) {
yield* resume();
terminate(7);
},
});
const program = effected(function* () {
yield* stop();
events.push("continued");
return 99;
});

await expect(program.runAsync()).resolves.toBe(7);
expect(events).toEqual([]);
});

it("should stop parent handlers when a nested default generator terminates after awaiting", async () => {
const events: string[] = [];
const stop = effect<[], never, number>()("stop", {
*defaultHandler({ terminate }) {
yield* effectify(Promise.resolve());
terminate(7);
},
});
const outer = effect<[], number, number>()("outer", {
*defaultHandler({ resume }) {
yield* stop();
events.push("outer continued");
resume(1);
},
});
const program = effected(function* () {
yield* outer();
events.push("program continued");
return 99;
});

await expect(program.runAsync()).resolves.toBe(7);
expect(events).toEqual([]);
});

it("should resolve a default termination without a value", async () => {
const stop = effect<[], never, void>()("stop", {
defaultHandler: ({ terminate }) => terminate(),
});
const program = effected(function* () {
yield* effectify(Promise.resolve());
yield* stop();
return "completed";
});

await expect(program.runAsync()).resolves.toBeUndefined();
}, 1000);

it.each(["stopped", undefined])(
"should resolve when a default handler terminates asynchronously with %s",
async (value) => {
let terminate!: (value: string | undefined) => void;
const events: string[] = [];
const stop = effect<[], never, string | undefined>()("stop", {
defaultHandler: (context) => {
terminate = context.terminate;
},
});
const program = effected(function* () {
yield* stop();
events.push("continued");
return "completed";
});
const result = program.runAsync();

terminate(value);

await expect(result).resolves.toBe(value);
expect(events).toEqual([]);
},
1000,
);
});

it("should handle async operations in default handlers", async () => {
const fetchData = effect<[url: string], any>()("fetchData", {
defaultHandler: ({ resume }, url) => {
Expand Down
Loading