Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow yielding a chain of then on Task #950

Open
wants to merge 2 commits into
base: v3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
76 changes: 66 additions & 10 deletions lib/run/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { action } from "../instructions.ts";

import type { FrameResult } from "./types.ts";
import { create } from "./create.ts";
import { call } from "../call.ts";

type ThenFulfilledType<T, Then=T> = ((value: T) => Then | Promise<Then>) | undefined | null;
type CatchType<Catch=never> = ((reason: any) => Catch | Promise<Catch>) | undefined | null;

export function createTask<T>(
frame: Frame<T>,
Expand All @@ -32,7 +36,7 @@ export function createTask<T>(
return promise;
};

let task = create<Task<T>>("Task", {}, {
let task: Task<T> = create<Task<T>>("Task", {}, {
*[Symbol.iterator]() {
let frameResult = evaluate<FrameResult<T> | void>(() => frame);
if (frameResult) {
Expand All @@ -44,13 +48,38 @@ export function createTask<T>(
}
} else {
return yield* action<T>(function* (resolve, reject) {
awaitResult(resolve, reject);
getPromise().then(resolve, reject);
});
}
},
then: (...args) => getPromise().then(...args),
catch: (...args) => getPromise().catch(...args),
finally: (...args) => getPromise().finally(...args),
then: async <Result1=T, Result2=never>(onfulfilled?: ThenFulfilledType<T, Result1>, onrejected?: CatchType<Result2>) => {
type NewResult = Result1 | Result2;
const newPromise = getPromise().then(onfulfilled, onrejected);
const future: Future<NewResult> = create<Future<NewResult>>("Future", {}, {
*[Symbol.iterator]() {
return yield* call(() => newPromise);
},
then: (...args) => newPromise.then(...args),
catch: (...args) => newPromise.catch(...args),
finally: (...args) => newPromise.finally(...args),
});
return await future;
},
catch: async (...args) => {
return await task.then(undefined, ...args);
},
finally: async (onfinally) => {
const newPromise = getPromise().finally(onfinally);
const future: Future<T> = create<Future<T>>("Future", {}, {
*[Symbol.iterator]() {
return yield* call(() => newPromise);
},
then: (...args) => newPromise.then(...args),
catch: (...args) => newPromise.catch(...args),
finally: (...args) => newPromise.finally(...args),
});
return await future;
},
halt() {
let haltPromise: Promise<void>;
let getHaltPromise = () => {
Expand All @@ -71,7 +100,7 @@ export function createTask<T>(
}
});
};
return create<Future<void>>("Future", {}, {
const future: Future<void> = create<Future<void>>("Future", {}, {
*[Symbol.iterator]() {
let result = evaluate<FrameResult<T> | void>(() => frame);

Expand All @@ -86,10 +115,37 @@ export function createTask<T>(
});
}
},
then: (...args) => getHaltPromise().then(...args),
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: there is one very subtle change introduced by this PR: the result of these is no longer a Promise and, instead, is now a PromiseLike. In practice, this should work everywhere (the goal of PromiseLike is that they're meant to be supported by everything magically), but it is possible to write code that only works on Promises and not PromiseLike (ex: any code that uses instanceof Promise)

catch: (...args) => getHaltPromise().catch(...args),
finally: (...args) => getHaltPromise().finally(...args),
// then: (...args) => getHaltPromise().then(...args),
then: async <Result1=void, Result2=never>(onfulfilled?: ThenFulfilledType<void, Result1>, onrejected?: CatchType<Result2>) => {
type NewResult = Result1 | Result2;
const newPromise = getHaltPromise().then(onfulfilled, onrejected);
const future: Future<NewResult> = create<Future<NewResult>>("Future", {}, {
*[Symbol.iterator]() {
return yield* call(() => newPromise);
},
then: (...args) => newPromise.then(...args),
catch: (...args) => newPromise.catch(...args),
finally: (...args) => newPromise.finally(...args),
});
return await future;
Comment on lines +120 to +129
Copy link
Author

@SebastienGllmt SebastienGllmt Dec 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: the code here for handling the abort future is mostly the same as the then in the parent Task

This is why I think maybe it's better if we have a more general way to create a Future that is exposed somewhere that is used in both these places

There is, to some extent, a conceptual overlap between what this block of code is trying to do and what call is doing. That is to say, if call returned a Future instead of an Operation, using it here could also be an option

},
catch: async (...args) => {
return await task.then(undefined, ...args);
},
finally: async (onfinally) => {
const newPromise = getHaltPromise().finally(onfinally);
const future: Future<void> = create<Future<void>>("Future", {}, {
*[Symbol.iterator]() {
return yield* call(() => newPromise);
},
then: (...args) => newPromise.then(...args),
catch: (...args) => newPromise.catch(...args),
finally: (...args) => newPromise.finally(...args),
});
return await future;
},
});
return future;
},
});
return task;
Expand All @@ -105,4 +161,4 @@ function getResult<T>(result: FrameResult<T>): Result<T> {
} else {
return result.exit.result;
}
}
}
27 changes: 25 additions & 2 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,30 @@ export interface Operation<T> {
* things, if the operation resolves synchronously, it will continue within the
* same tick of the run loop.
*/
export interface Future<T> extends Promise<T>, Operation<T> {}
export interface Future<T> extends Promise<T>, Operation<T> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Future<TResult1 | TResult2>;

/**
* Attaches a callback for only the rejection of the Promise.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of the callback.
*/
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Future<T | TResult>;

/**
* Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The
* resolved value cannot be modified from the callback.
* @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected).
* @returns A Promise for the completion of the callback.
*/
finally(onfinally?: (() => void) | undefined | null): Future<T>;
Comment on lines +57 to +78
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: these three are copy-pasted from the definition of Promise, but have the return type changed from PromiseFuture

}

/**
* A handle to a concurrently running operation that lets you either use the
Expand Down Expand Up @@ -139,7 +162,7 @@ export interface Task<T> extends Future<T> {
* children.
*
* Any errors raised by the `halt()` operation only represent problems that
* occured during the teardown of the task. In other words, `halt()` can
* occurred during the teardown of the task. In other words, `halt()` can
* succeed even if the task failed.
*
* @returns a future that only resolves when all shutdown associated with this
Expand Down
18 changes: 18 additions & 0 deletions test/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@ describe("run()", () => {
expect(result).toEqual(67);
});

it("can await a task 'then'", () => {
let result = run(function* () {
return yield* Promise.resolve(12);
});
const plusOne = result.then((value) => value + 1);
expect(plusOne).resolves.toEqual(13);
});

it("can yield a task 'then'", async () => {
let result = run(function* () {
return yield* Promise.resolve(12);
});
const plusOne = result.then((value) => value + 1);
await expect(run(function* () {
return yield* plusOne;
})).resolves.toEqual(13);
});

it("rejects generator if subtask promise fails", async () => {
let error = new Error("boom");
let task = run(function* () {
Expand Down