import { assertMethodDecorator, isDecoratorCall, } from "../common/decorators.js"; import type { AsyncMethod } from "../common/types.js"; import { isWeakMapKey } from "../common/utils.js"; import { CanceledPromise } from "./canceled-promise.js"; type CancelPreviousDecorator = ( value: AsyncMethod, context: ClassMethodDecoratorContext>, ) => AsyncMethod; export function createCancelableMethod( originalMethod: AsyncMethod, ): AsyncMethod { const rejectors = new WeakMap void>(); let fallbackRejector: ((error: CanceledPromise) => void) | undefined; return function(this: This, ...args: Args): Promise { const getRejector = (): ((error: CanceledPromise) => void) | undefined => { if (isWeakMapKey(this)) { return rejectors.get(this as object); } return fallbackRejector; }; const setRejector = (rejector?: (error: CanceledPromise) => void): void => { if (isWeakMapKey(this)) { if (rejector === undefined) { rejectors.delete(this as object); return; } rejectors.set(this as object, rejector); return; } fallbackRejector = rejector; }; getRejector()?.(new CanceledPromise()); return new Promise((resolve, reject) => { const currentReject = (error: CanceledPromise): void => { reject(error); }; setRejector(currentReject); originalMethod.apply(this, args).then(resolve, reject).finally(() => { if (getRejector() === currentReject) { setRejector(undefined); } }); }); }; } export function cancelPrevious( value: AsyncMethod, context: ClassMethodDecoratorContext>, ): AsyncMethod; export function cancelPrevious(): CancelPreviousDecorator; export function cancelPrevious(inputOrValue?: unknown, context?: unknown): unknown { const decorate: CancelPreviousDecorator = function( value: AsyncMethod, decoratorContext: ClassMethodDecoratorContext>, ): AsyncMethod { assertMethodDecorator("cancelPrevious", value, decoratorContext); return createCancelableMethod(value); }; if (arguments.length === 2 && isDecoratorCall(context)) { return decorate( inputOrValue as AsyncMethod, context as ClassMethodDecoratorContext>, ); } return decorate; }