authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-10-15 23:36:01-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-14 01:08:14-08:00
log8a84a3a419254b2273898266e8e32ebd36843020
tree2357a363858ae1e8c5b24480b6f4669f42469b95
parentc2294014546473c5381f90da0ceba0de1d389f4a
signature Commit is signed but in an unrecognized format.

feat(lib): cancelable promise helper


1 files changed, 51 insertions(+), 8 deletions(-)

lib/async.ts+51-8
......@@ -61,6 +61,7 @@ export function RefCountedExpirable<T>(
6161 };
6262}
6363
64/** evaluates `fn` one time, caching the result forever. */
6465export function once<T>(fn: () => Promise<T>): () => Promise<T> {
6566 let result: T | Promise<T> | null = null;
6667 return async () => {
......@@ -75,6 +76,7 @@ export function once<T>(fn: () => Promise<T>): () => Promise<T> {
7576 };
7677}
7778
79/** a variable that can be watched for changes */
7880export class Watch<T> {
7981 #value: T;
8082 #observers = new Set<WatchCallback<T>>();
......@@ -116,7 +118,7 @@ export class Watch<T> {
116118 }
117119 }
118120}
119type WatchCallback<T> = (next: T, prev: T) => void;
121export type WatchCallback<T> = (next: T, prev: T) => void;
120122
121123/**
122124 * when two requests with the same args come in at the same time, the response
......@@ -124,10 +126,16 @@ type WatchCallback<T> = (next: T, prev: T) => void;
124126 */
125127export class DedupeConcurrent<Args extends unknown[], Ret> {
126128 pending: Map<unknown, Promise<Ret>> = new Map();
129 fn: (...args: Args) => Promise<Ret>;
130 keyFn: (args: Args) => unknown = JSON.stringify;
131
127132 constructor(
128 public fn: (...args: Args) => Promise<Ret>,
129 public keyFn: (args: Args) => unknown = JSON.stringify,
130 ) {}
133 fn: (...args: Args) => Promise<Ret>,
134 keyFn: (args: Args) => unknown = JSON.stringify,
135 ) {
136 this.fn = fn;
137 this.keyFn = keyFn;
138 }
131139
132140 isPending(...args: Args): boolean {
133141 return this.pending.has(this.keyFn(args));
......@@ -146,14 +154,21 @@ export class DedupeConcurrent<Args extends unknown[], Ret> {
146154}
147155
148156/**
149 * when two requests with the same args come, the result of the first is memoized.
157 * when two requests with the same args come, the result
158 * of the first is memoized.
150159 */
151160export class OnceMap<Args extends unknown[], Ret> {
152161 cache: Map<unknown, Promise<Ret>> = new Map();
162 fn: (...args: Args) => Promise<Ret>;
163 keyFn: (args: Args) => unknown = JSON.stringify;
164
153165 constructor(
154 public fn: (...args: Args) => Promise<Ret>,
155 public keyFn: (args: Args) => unknown = JSON.stringify,
156 ) {}
166 fn: (...args: Args) => Promise<Ret>,
167 keyFn: (args: Args) => unknown = JSON.stringify,
168 ) {
169 this.fn = fn;
170 this.keyFn = keyFn;
171 }
157172
158173 has(...args: Args): boolean {
159174 return this.cache.has(this.keyFn(args));
......@@ -204,4 +219,32 @@ export class PromiseAggregator {
204219 }
205220}
206221
222/** a promise that can be cancelled. */
223export type Cancelable<T> = Promise<T> & Disposable & { cancel: VoidFunction };
224
225/** make an existing promise cancelable. */
226export function makeCancelable<T>(
227 promise: Promise<T>,
228 cancel: ts.VoidFunction,
229): Cancelable<T> {
230 const p = promise as Cancelable<T>;
231 p[Symbol.dispose] = cancel;
232 p.cancel = cancel;
233 return p;
234}
235
236/** wait `ms` milliseconds, then resolve. can be cancelled. */
237export function delay(ms: number): Cancelable<void> {
238 let t: ts.Timer | null = null;
239 return makeCancelable(
240 new Promise((resolve) => {
241 t = globalThis.setTimeout(() => (t = null, resolve()), ms);
242 }),
243 () => {
244 if (t != null) clearTimeout(t);
245 },
246 );
247}
248
207249import { ASSERT } from "./assert.ts";
250import * as ts from "./ts.ts";