| ... | @@ -61,6 +61,7 @@ export function RefCountedExpirable<T>( | ... | @@ -61,6 +61,7 @@ export function RefCountedExpirable<T>( |
| 61 | }; | 61 | }; |
| 62 | } | 62 | } |
| 63 | | 63 | |
| | 64 | /** evaluates `fn` one time, caching the result forever. */ |
| 64 | export function once<T>(fn: () => Promise<T>): () => Promise<T> { | 65 | export 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 | } |
| 77 | | 78 | |
| | 79 | /** a variable that can be watched for changes */ |
| 78 | export class Watch<T> { | 80 | export 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 | } |
| 119 | type WatchCallback<T> = (next: T, prev: T) => void; | 121 | export type WatchCallback<T> = (next: T, prev: T) => void; |
| 120 | | 122 | |
| 121 | /** | 123 | /** |
| 122 | * when two requests with the same args come in at the same time, the response | 124 | * 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 | */ |
| 125 | export class DedupeConcurrent<Args extends unknown[], Ret> { | 127 | export 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 | } |
| 131 | | 139 | |
| 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 | } |
| 147 | | 155 | |
| 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 | */ |
| 151 | export class OnceMap<Args extends unknown[], Ret> { | 160 | export 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 | } |
| 157 | | 172 | |
| 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 | } |
| 206 | | 221 | |
| | 222 | /** a promise that can be cancelled. */ |
| | 223 | export type Cancelable<T> = Promise<T> & Disposable & { cancel: VoidFunction }; |
| | 224 | |
| | 225 | /** make an existing promise cancelable. */ |
| | 226 | export 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. */ |
| | 237 | export 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 | |
| 207 | import { ASSERT } from "./assert.ts"; | 249 | import { ASSERT } from "./assert.ts"; |
| | 250 | import * as ts from "./ts.ts"; |