const CACHE_NOT_SET = Symbol('CACHE_NOT_SET') export type UnwrapFuture = T extends any ? (T extends Future ? U : T) : never export type ResolvedDeps = { [K in keyof D]: UnwrapFuture } export type Operation = (...args: ResolvedDeps) => T | Promise export class Future { private operation: Operation private dependencies: D private optionNoCache: boolean private value: T | typeof CACHE_NOT_SET private resolving: boolean = false private resolvePromise?: Promise private clear = false constructor(operation: Operation, dependencies?: D, optionNoCache: boolean = false) { this.operation = operation this.dependencies = Object.freeze(dependencies || ([] as unknown as D)) this.optionNoCache = optionNoCache this.value = CACHE_NOT_SET } static resolve(value: T | Future): Future { return value instanceof Future ? value : new Future(() => value) } static async resolveOperation( operation: Operation, dependencies: D, ): Promise { const resolvedDependencies = await Promise.all(dependencies) return operation(...resolvedDependencies) } async cachedResolveOperation(operation: Operation, dependencies: D): Promise { const resolver = async () => { if (this.resolving) { return this.resolvePromise! } this.resolving = true try { if (this.optionNoCache) { return Future.resolveOperation(operation, dependencies) } this.value = await Future.resolveOperation(operation, dependencies) return this.value } finally { this.resolving = false } } if (this.value !== CACHE_NOT_SET && !this.clear) { return this.value } if (this.clear) this.clear = false this.resolvePromise = resolver() return this.resolvePromise } resolve({ /** clears the cache */ clear = false, } = {}): Promise { if (clear) this.clear = true return this.cachedResolveOperation(this.operation, this.dependencies) } then( onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | null, ): Promise { return this.resolve().then(onfulfilled, onrejected) } catch(onrejected?: ((reason: any) => TResult | PromiseLike) | null): Promise { return this.resolve().catch(onrejected) } finally(onfinally?: (() => void) | null): Promise { return this.resolve().finally(onfinally) } /** clones Future and deps with noCache, depth 0 is current Future */ noCache(depth: number = 0) { const shouldNoCache = depth >= 0 const shouldNoCacheDeps = depth > 0 const clonedDependencies = shouldNoCacheDeps ? (this.dependencies.map(dep => { return dep instanceof Future ? dep.noCache(depth - 1) : dep }) as unknown as D) : this.dependencies return shouldNoCache ? new Future(this.operation, clonedDependencies, true) : this } /** clones Future and clears cache values, depth 0 is current Future */ clone(depth: number = 0) { const shouldClone = depth >= 0 const shouldCloneDeps = depth > 0 const clonedDependencies = shouldCloneDeps ? (this.dependencies.map(dep => { return dep instanceof Future ? dep.clone(depth - 1) : dep }) as unknown as D) : this.dependencies return shouldClone ? new Future(this.operation, clonedDependencies) : this } /** creates a new future using this future as a dependency */ use( operation: Operation, dependencies: D = [] as unknown as D, ) { return new Future, ...D]>( async (resolvedValue, ...args) => { return operation(resolvedValue as UnwrapFuture, ...args) }, [this as unknown as Future, ...(dependencies as D)], ) } /** updates future value */ update( operation: Operation, dependencies: D = [] as unknown as D, ) { const originalOperation = this.operation this.operation = async () => { const currentValue = this.value !== CACHE_NOT_SET ? this.value : await Future.resolveOperation(originalOperation, this.dependencies) return Future.resolveOperation(operation, [currentValue, ...dependencies]) } this.clear = true return this } }