-
Notifications
You must be signed in to change notification settings - Fork 32
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
Where would be an appropriate place to propose a related type? #198
Comments
That is a deficiency of all current implementations, rooted in spec text that if followed to the letter makes it difficult for a gc to collect the reactions and instance of an unresolved promise without reachable resolvers. I do want to update the spec text to make it clearer that an engine can and should collect these things. Edit: Actually I think I got Moddable to fix XS for this specific case (maybe not released), but yeah all promise implementations are currently leaky for this and other cases. See https://gist.github.com/mhofman/e4031aa4cd2375d0f151f62691724475 for some tests I wrote around this. Edit 2: it looks like an unresolved promise with reactions and dropped resolvers is not a case I tested for. If the promise is kept, it's currently a subset of the existing test for "kept promise with dropped resolvers should collect resolvers", which no engine does correctly. If the both the promise and resolvers are dropped, I should add a test to see if reactions are collected, but I suspect they would be given that's the behavior that would naturally fall from the spec text. |
The appropriate place to discuss this is probably https://github.com/tc39/proposal-cancellation, though that proposal hasn't seen much activity since WHATWG has been adamant that there shouldn't be two native cancellation mechanisms and AbortController already exists. |
AbortController/AbortSignal looked promising, but RxJS is walking away from it for the time being because it's really unergonomic and it lacks some safeties that our |
I'd suggest reading over the cancellation proposal and taking a look at https://www.npmjs.com/package/@esfx/canceltoken, which is based on an early implementation of the proposed API. I'd appreciate any feedback. |
Not exactly a solution, but you can combine a synchronous thenable and an async runner like co.js to achieve similar use case. This however exploits the fact that co.js resumes the generator using /**
* A {@link https://promisesaplus.com/ | Promises/A+} conforming Promise, but
* relaxing {@link https://promisesaplus.com/#point-34 | rule 2.2.4}. So that
* `onFulfilled` and `onRejected` callbacks added to the `then()` clause will be
* called synchronously either:
*
* 1. when calling `then()` after the promise was resolved, or
* 2. when resolving the promise.
*
* This relaxation is useful for ensuring state consistency between promise
* resolution and callback invocation.
* @param executor execute the Promise with resolve and reject callbacks
* @returns a {@link PromiseLike | Thenable Promise}
*/
export class SameTickPromise<T> implements PromiseLike<T> {
private status = Status.Pending;
private value: any = undefined;
private waiters: Array<Omit<PromiseResolvers<any>, "promise">> = [];
private onfulfilled: ((value: T) => any) | undefined | null = undefined;
private onrejected: ((reason: any) => any) | undefined | null = undefined;
private resolve!: (value: T | PromiseLike<T>) => void;
private reject!: (err: any) => void;
constructor(
executor?: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void
) => void
) {
this.resolve = (SameTickPromise.prototype.fulfill).bind(
this,
Status.Fulfilled
);
this.reject = (SameTickPromise.prototype.fulfill).bind(
this,
Status.Rejected
);
if (!executor) return;
try {
executor(this.resolve, this.reject);
} catch (err) {
this.reject(err);
}
}
public static withResolvers<T>(): PromiseResolvers<T, SameTickPromise<T>> {
const promise = new SameTickPromise<T>();
const { resolve, reject } = promise;
return { promise, resolve, reject };
}
public static resolve(): SameTickPromise<void>;
public static resolve<T>(value: T): SameTickPromise<Awaited<T>>;
public static resolve<T>(value?: T): SameTickPromise<Awaited<T>> {
return new SameTickPromise<Awaited<T>>((resolve, reject) => {
if (isPromiseLike(value)) {
return value.then(resolve, reject);
}
resolve(value as any);
});
}
public static reject(reason: any): SameTickPromise<never> {
return new SameTickPromise<never>((_, reject) => reject(reason));
}
public then<TResult1 = T, TResult2 = never>(
onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>)
| null
| undefined,
onrejected?:
| ((reason: any) => TResult2 | PromiseLike<TResult2>)
| null
| undefined
): PromiseLike<TResult1 | TResult2> {
const { promise, resolve, reject } =
SameTickPromise.withResolvers<any>();
promise.onfulfilled = onfulfilled;
promise.onrejected = onrejected;
if (this.status === Status.Pending) {
this.waiters.push({ resolve, reject });
} else {
(this.status === Status.Fulfilled ? resolve : reject)(this.value);
}
return promise;
}
private fulfill(
status: Status.Fulfilled | Status.Rejected,
value: any
): void {
if (this.status !== Status.Pending) return;
const cont =
status === Status.Fulfilled ? this.onfulfilled : this.onrejected;
if (cont) {
this.onfulfilled = this.onrejected = undefined;
try {
return this.resolve(cont(value));
} catch (err) {
return this.reject(err);
}
} else if (isPromiseLike(value)) {
return void value.then(this.resolve, this.reject);
}
this.value = value;
this.status = status;
while (this.waiters.length) {
const { resolve, reject } = this.waiters.shift()!;
(this.status === Status.Fulfilled ? resolve : reject)(this.value);
}
}
}
export type PromiseResolvers<T, P extends PromiseLike<T> = PromiseLike<T>> = {
promise: P;
resolve: (value: T | PromiseLike<T>) => void;
reject: (err: any) => void;
};
const enum Status {
Pending,
Fulfilled,
Rejected,
}
describe("SameTickPromise", () => {
test("co() should resume generator synchronously", async () => {
const log = jest.fn();
function* run(promise: PromiseLike<number>) {
try {
log(yield promise);
} catch (err) {
log("error", err);
throw err;
}
}
const { promise, resolve } = SameTickPromise.withResolvers<number>();
const result = await Promise.allSettled([
co(run, promise),
(async () => {
resolve(1);
expect(log.mock.calls).toStrictEqual([[1]]);
})(),
]);
expect(result).toStrictEqual([
{ status: "fulfilled", value: undefined },
{ status: "fulfilled", value: undefined },
]);
});
}); You can then wrap export function throwOnAbort(signal: AbortSignal | undefined): Disposable & PromiseLike<never> {
const { promise, reject } = SameTickPromise.withResolvers<never>();
let abort: undefined | (() => void);
function dispose() {
if (abort) {
signal.removeEventListener("abort", abort, { once:true });
abort = undefined;
}
}
if (signal) {
abort = function abort() {
dispose();
reject(signal.reason);
}
if (signal.aborted) {
abort();
} else {
signal.addEventListener("abort", abort, { once:true });
}
}
return Object.assign(promise, { [Symbol.dispose]: dispose });
} |
There's a need for observables, fetch, et al, to have some sort of token-type disposal mechanism. Further, the language could generally benefit from some sort of one-time explicit "flag" if you will.
Honestly, a Promise is almost ideal for a cancellation token with two problems:
That aside, a simple interface like
.then(fn)
is really nice for situations where you just want to notify that it's time to cleanup.What I'd love to see is basically something that:
All of the above would make an ideal cancellation mechanism for a wide variety of use cases, and might even make an interesting mechanism that could be used similar to a "semaphore" type thing in
async/await
.This is something I discussed with @ljharb and he seemed interested. @bakkot @rbuckton @littledan ... is there any interest here? What is the process?
The text was updated successfully, but these errors were encountered: