export interface AsyncRefreshControllerOptions {
read(input: Input, signal: AbortSignal): Promise;
equal(left: Snapshot | undefined, right: Snapshot): boolean;
publish(snapshot: Snapshot): void;
onError?(error: unknown): void;
}
interface RefreshRequest {
generation: number;
requestId: number;
input: Input;
}
interface ActiveRefresh {
request: RefreshRequest;
controller: AbortController;
}
/** Coalesces refreshes to one active read plus the latest pending request. */
export class AsyncRefreshController {
private generation: number | undefined;
private requestId = 0;
private active: ActiveRefresh | undefined;
private pending: RefreshRequest | undefined;
private current: Snapshot | undefined;
constructor(private readonly options: AsyncRefreshControllerOptions) {}
start(generation: number): void {
this.cancelActive("Refresh generation replaced");
this.generation = generation;
this.requestId += 1;
this.pending = undefined;
this.current = undefined;
}
clear(): void {
this.current = undefined;
}
request(input: Input): void {
if (this.generation === undefined) return;
const request = {
generation: this.generation,
requestId: ++this.requestId,
input,
};
if (this.active) {
this.pending = request;
return;
}
this.run(request);
}
stop(): void {
this.generation = undefined;
this.requestId += 1;
this.pending = undefined;
this.current = undefined;
this.cancelActive("Refresh controller stopped");
}
private run(request: RefreshRequest): void {
if (!this.isCurrentTarget(request)) return;
const active: ActiveRefresh = {
request,
controller: new AbortController(),
};
this.active = active;
void this.options
.read(request.input, active.controller.signal)
.then((snapshot) => {
if (this.active !== active || !this.isCurrentRequest(request)) return;
if (this.options.equal(this.current, snapshot)) return;
this.current = snapshot;
this.options.publish(snapshot);
})
.catch((error: unknown) => {
if (
this.active === active &&
this.isCurrentRequest(request) &&
!active.controller.signal.aborted
) {
this.options.onError?.(error);
}
})
.finally(() => {
if (this.active !== active) return;
this.active = undefined;
const pending = this.pending;
this.pending = undefined;
if (pending) this.run(pending);
});
}
private cancelActive(reason: string): void {
this.active?.controller.abort(new DOMException(reason, "AbortError"));
}
private isCurrentTarget(request: RefreshRequest): boolean {
return this.generation !== undefined && request.generation === this.generation;
}
private isCurrentRequest(request: RefreshRequest): boolean {
return this.isCurrentTarget(request) && request.requestId === this.requestId;
}
}