import { AtomicPromise, Internal, internal } from './promise'; import { Maybe, Just, Nothing } from './maybe'; import { Either, Left, Right } from './either'; import { noop } from './function'; import { causeAsyncException } from './exception'; export interface Canceller { readonly cancel: { (reason: L): void; (this: Canceller, reason?: L): void; }; readonly close: (reason?: unknown) => void; } export interface Cancellee extends Promise { isAlive(): boolean; isCancelled(): boolean; readonly register: (listener: Listener) => () => void; readonly promise: (value: T) => AtomicPromise; readonly maybe: (value: T) => Maybe; readonly either: (value: R) => Either; } type Listener = (reason: L) => void; const enum State { alive, cancelled, closed, } export interface Cancellation extends AtomicPromise { } export class Cancellation implements Canceller, Cancellee, AtomicPromise { public readonly [Symbol.toStringTag]: string = 'Cancellation'; constructor(cancellees?: Iterable>) { if (cancellees) for (const cancellee of cancellees) { cancellee.register(this.cancel); } } private state: State = State.alive; private reason: unknown = undefined; private handlers: Listener[] = []; public readonly [internal] = new Internal(); public isAlive(): boolean { return this.state === State.alive; } public isCancelled(): boolean { return this.state === State.cancelled; } public isClosed(): boolean { return this.state === State.closed; } private register$(listener: Listener): () => void { const { handlers } = this; if (!this.isAlive() && handlers.length === 0) { this.isCancelled() && handler(this.reason as L); return noop; } handlers.push(handler); return () => void (listener = noop); function handler(reason: L): void { try { listener(reason); } catch (reason) { causeAsyncException(reason); } } } public get register(): (listener: Listener) => () => void { return listener => this.register$(listener); } private cancel$(reason: L): void { if (!this.isAlive()) return; this.state = State.cancelled; this.reason = reason; for (let { handlers } = this, i = 0; i < handlers.length; ++i) { handlers[i](reason!); } this.handlers = []; assert(Object.freeze(this.handlers)); this[internal].resolve(reason!); } public get cancel(): Canceller['cancel'] { return reason => this.cancel$(reason!); } public close$(reason?: unknown): void { if (!this.isAlive()) return; this.state = State.closed; this.reason = reason; this.handlers = []; assert(Object.freeze(this.handlers)); this[internal].reject(reason); } public get close(): (reason?: unknown) => void { return reason => this.close$(reason); } public get promise(): (value: T) => AtomicPromise { return value => this.isCancelled() ? AtomicPromise.reject(this.reason) : AtomicPromise.resolve(value); } public get maybe(): (value: T) => Maybe { return value => Just(value) .bind(value => this.isCancelled() ? Nothing : Just(value)); } public get either(): (value: R) => Either { return value => Right(value) .bind(value => this.isCancelled() ? Left(this.reason as L) : Right(value)); } } Cancellation.prototype.then = AtomicPromise.prototype.then; Cancellation.prototype.catch = AtomicPromise.prototype.catch; Cancellation.prototype.finally = AtomicPromise.prototype.finally;