feat(presentation): add cancellable frame renderer

This commit is contained in:
2026-09-05 12:24:48 +08:00
parent 1e168ae81e
commit 8b7efa7de0
2 changed files with 330 additions and 0 deletions
@@ -0,0 +1,67 @@
import type { FrameScheduler } from './ui-contracts.ts';
class FrameRendererFault extends Error {
readonly errors: readonly unknown[];
constructor(errors: readonly unknown[]) {
super('FrameRenderer render and disposal failed');
this.name = 'FrameRendererFault';
this.errors = Object.freeze([...errors]);
}
}
export class FrameRenderer {
private pending: (() => void) | null = null;
private generation = 0;
private disposed = false;
public constructor(
private readonly scheduler: FrameScheduler,
private readonly render: () => void,
private readonly onFault: (error: unknown) => void,
) {}
public request(): void {
if (this.disposed || this.pending !== null) return;
const generation = this.generation;
this.pending = this.scheduler.request(() => {
if (this.disposed || generation !== this.generation) return;
this.pending = null;
this.generation += 1;
try {
this.render();
} catch (error) {
let fault: unknown = error;
try {
this.dispose();
} catch (cancelError) {
fault = new FrameRendererFault([error, cancelError]);
}
this.onFault(fault);
}
});
}
public flush(): void {
if (this.disposed) return;
const cancel = this.pending;
this.pending = null;
this.generation += 1;
const generation = this.generation;
if (cancel !== null) cancel();
if (this.disposed || generation !== this.generation) return;
this.render();
}
public dispose(): void {
if (this.disposed) return;
this.disposed = true;
const cancel = this.pending;
this.pending = null;
this.generation += 1;
if (cancel !== null) cancel();
}
}