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>(...@@ -61,6 +61,7 @@ export function RefCountedExpirable<T>(
61 };61 };
62}62}
6363
64/** evaluates `fn` one time, caching the result forever. */
64export function once<T>(fn: () => Promise<T>): () => Promise<T> {65export function once<T>(fn: () => Promise<T>): () => Promise<T> {
65 let result: T | Promise<T> | null = null;66 let result: T | Promise<T> | null = null;
66 return async () => {67 return async () => {
...@@ -75,6 +76,7 @@ export function once<T>(fn: () => Promise<T>): () => Promise<T> {...@@ -75,6 +76,7 @@ export function once<T>(fn: () => Promise<T>): () => Promise<T> {
75 };76 };
76}77}
7778
79/** a variable that can be watched for changes */
78export class Watch<T> {80export class Watch<T> {
79 #value: T;81 #value: T;
80 #observers = new Set<WatchCallback<T>>();82 #observers = new Set<WatchCallback<T>>();
...@@ -116,7 +118,7 @@ export class Watch<T> {...@@ -116,7 +118,7 @@ export class Watch<T> {
116 }118 }
117 }119 }
118}120}
119type WatchCallback<T> = (next: T, prev: T) => void;121export type WatchCallback<T> = (next: T, prev: T) => void;
120122
121/**123/**
122 * when two requests with the same args come in at the same time, the response124 * 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;...@@ -124,10 +126,16 @@ type WatchCallback<T> = (next: T, prev: T) => void;
124 */126 */
125export class DedupeConcurrent<Args extends unknown[], Ret> {127export class DedupeConcurrent<Args extends unknown[], Ret> {
126 pending: Map<unknown, Promise<Ret>> = new Map();128 pending: Map<unknown, Promise<Ret>> = new Map();
129 fn: (...args: Args) => Promise<Ret>;
130 keyFn: (args: Args) => unknown = JSON.stringify;
131
127 constructor(132 constructor(
128 public fn: (...args: Args) => Promise<Ret>,133 fn: (...args: Args) => Promise<Ret>,
129 public keyFn: (args: Args) => unknown = JSON.stringify,134 keyFn: (args: Args) => unknown = JSON.stringify,
130 ) {}135 ) {
136 this.fn = fn;
137 this.keyFn = keyFn;
138 }
131139
132 isPending(...args: Args): boolean {140 isPending(...args: Args): boolean {
133 return this.pending.has(this.keyFn(args));141 return this.pending.has(this.keyFn(args));
...@@ -146,14 +154,21 @@ export class DedupeConcurrent<Args extends unknown[], Ret> {...@@ -146,14 +154,21 @@ export class DedupeConcurrent<Args extends unknown[], Ret> {
146}154}
147155
148/**156/**
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.
150 */159 */
151export class OnceMap<Args extends unknown[], Ret> {160export class OnceMap<Args extends unknown[], Ret> {
152 cache: Map<unknown, Promise<Ret>> = new Map();161 cache: Map<unknown, Promise<Ret>> = new Map();
162 fn: (...args: Args) => Promise<Ret>;
163 keyFn: (args: Args) => unknown = JSON.stringify;
164
153 constructor(165 constructor(
154 public fn: (...args: Args) => Promise<Ret>,166 fn: (...args: Args) => Promise<Ret>,
155 public keyFn: (args: Args) => unknown = JSON.stringify,167 keyFn: (args: Args) => unknown = JSON.stringify,
156 ) {}168 ) {
169 this.fn = fn;
170 this.keyFn = keyFn;
171 }
157172
158 has(...args: Args): boolean {173 has(...args: Args): boolean {
159 return this.cache.has(this.keyFn(args));174 return this.cache.has(this.keyFn(args));
...@@ -204,4 +219,32 @@ export class PromiseAggregator {...@@ -204,4 +219,32 @@ export class PromiseAggregator {
204 }219 }
205}220}
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
207import { ASSERT } from "./assert.ts";249import { ASSERT } from "./assert.ts";
250import * as ts from "./ts.ts";