From 8a84a3a419254b2273898266e8e32ebd36843020 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Wed, 15 Oct 2025 23:36:01 -0700 Subject: [PATCH] feat(lib): cancelable promise helper --- lib/async.ts | 59 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/lib/async.ts b/lib/async.ts index e0ae0df70890116dcd175304c35493c7b6991775..3bfa34cd1792eb3b7109f433d3b8481006ca3620 100644 --- a/lib/async.ts +++ b/lib/async.ts @@ -61,6 +61,7 @@ export function RefCountedExpirable( }; } +/** evaluates `fn` one time, caching the result forever. */ export function once(fn: () => Promise): () => Promise { let result: T | Promise | null = null; return async () => { @@ -75,6 +76,7 @@ export function once(fn: () => Promise): () => Promise { }; } +/** a variable that can be watched for changes */ export class Watch { #value: T; #observers = new Set>(); @@ -116,7 +118,7 @@ export class Watch { } } } -type WatchCallback = (next: T, prev: T) => void; +export type WatchCallback = (next: T, prev: T) => void; /** * when two requests with the same args come in at the same time, the response @@ -124,10 +126,16 @@ type WatchCallback = (next: T, prev: T) => void; */ export class DedupeConcurrent { pending: Map> = new Map(); + fn: (...args: Args) => Promise; + keyFn: (args: Args) => unknown = JSON.stringify; + constructor( - public fn: (...args: Args) => Promise, - public keyFn: (args: Args) => unknown = JSON.stringify, - ) {} + fn: (...args: Args) => Promise, + keyFn: (args: Args) => unknown = JSON.stringify, + ) { + this.fn = fn; + this.keyFn = keyFn; + } isPending(...args: Args): boolean { return this.pending.has(this.keyFn(args)); @@ -146,14 +154,21 @@ export class DedupeConcurrent { } /** - * when two requests with the same args come, the result of the first is memoized. + * when two requests with the same args come, the result + * of the first is memoized. */ export class OnceMap { cache: Map> = new Map(); + fn: (...args: Args) => Promise; + keyFn: (args: Args) => unknown = JSON.stringify; + constructor( - public fn: (...args: Args) => Promise, - public keyFn: (args: Args) => unknown = JSON.stringify, - ) {} + fn: (...args: Args) => Promise, + keyFn: (args: Args) => unknown = JSON.stringify, + ) { + this.fn = fn; + this.keyFn = keyFn; + } has(...args: Args): boolean { return this.cache.has(this.keyFn(args)); @@ -204,4 +219,32 @@ export class PromiseAggregator { } } +/** a promise that can be cancelled. */ +export type Cancelable = Promise & Disposable & { cancel: VoidFunction }; + +/** make an existing promise cancelable. */ +export function makeCancelable( + promise: Promise, + cancel: ts.VoidFunction, +): Cancelable { + const p = promise as Cancelable; + p[Symbol.dispose] = cancel; + p.cancel = cancel; + return p; +} + +/** wait `ms` milliseconds, then resolve. can be cancelled. */ +export function delay(ms: number): Cancelable { + let t: ts.Timer | null = null; + return makeCancelable( + new Promise((resolve) => { + t = globalThis.setTimeout(() => (t = null, resolve()), ms); + }), + () => { + if (t != null) clearTimeout(t); + }, + ); +} + import { ASSERT } from "./assert.ts"; +import * as ts from "./ts.ts"; -- 2.54.0