GenerAI / worldmonitor /src /components /map /deferred-layer-commit.ts
amogaddy's picture
Integra World Monitor (AGPL-3.0, self-hosted) nello Space: pagina, menu, e arricchimento notizie per la AI (part 7)
9d2d895 verified
Raw
History Blame Contribute Delete
4.69 kB
/**
* Two-phase heavy-layer data commit for DeckGLMap (#4558 / #4537).
*
* The interaction-attributed render frame must not synchronously tessellate a
* heavy deck.gl layer (conflict-zone GeoJson, Supercluster cluster layers). This
* gate models the *scheduling and per-layer data state* of a two-phase commit,
* deliberately free of any DOM/deck.gl/WebGL dependency so it is unit-testable
* under `tsx --test`:
*
* - Phase 1 (immediate): the render builds each heavy layer with its
* PREVIOUSLY-committed data via `present(key)` — so an already-visible layer
* never blanks (R2 no-flicker); a first build has no previous data and shows
* empty for one frame (R3, stable id, acceptable since it was not visible).
* - Phase 2 (deferred): a coalesced, teardown-guarded flush moves staged data
* to committed and calls `onCommit(changedKeys)`, which the owner uses to run
* a second render carrying the real heavy data — off the interaction frame.
*
* The owner (DeckGLMap) wires `schedule` to `yieldToMain`, `isAlive` to the
* `!renderPaused && !webglLost && maplibreMap` guard, and `onCommit` to a
* deferred `updateLayers`. This module owns none of that — only the state machine.
*/
export interface DeferredCommitDeps<T> {
/**
* Schedule the deferred flush to run later (e.g. `yieldToMain`/`setTimeout(0)`).
* Returns a cancel handle so a superseding stage can coalesce.
*/
schedule: (run: () => void) => () => void;
/** True while the map can still receive a commit (not paused/destroyed/WebGL-lost). */
isAlive: () => boolean;
/** Called on the deferred flush with the heavy-layer keys whose data changed. */
onCommit: (changedKeys: string[]) => void;
/** Equality used to decide whether staged data differs from committed (default `Object.is`). */
equals?: (a: T | undefined, b: T | undefined) => boolean;
}
export class DeferredHeavyCommit<T> {
private readonly committed = new Map<string, T>();
private readonly pending = new Map<string, T>();
private cancelHandle: (() => void) | null = null;
private readonly equals: (a: T | undefined, b: T | undefined) => boolean;
constructor(private readonly deps: DeferredCommitDeps<T>) {
this.equals = deps.equals ?? ((a, b) => Object.is(a, b));
}
/**
* Stage a heavy layer's newly-available data. If it differs from what is
* currently committed, schedule a coalesced deferred flush. A burst of stage()
* calls before the flush runs collapses to a single flush.
*/
stage(key: string, data: T): void {
this.pending.set(key, data);
if (this.equals(data, this.committed.get(key))) {
// No real change for this key; don't schedule on its account. If this
// empties the pending set, drop any flush a prior stage() scheduled so
// hasPending() doesn't report a flush that would commit nothing.
this.pending.delete(key);
if (this.pending.size === 0) this.clearHandle();
return;
}
this.scheduleFlush();
}
/** Data to present for `key` on the immediate (Phase 1) render: the last committed value, or undefined on first build. */
present(key: string): T | undefined {
return this.committed.get(key);
}
/** Whether a deferred flush is currently scheduled. */
hasPending(): boolean {
return this.cancelHandle !== null;
}
/** Keys currently staged but not yet committed (for assertions/diagnostics). */
pendingKeys(): string[] {
return [...this.pending.keys()];
}
/**
* Run any scheduled flush immediately. Honors the teardown guard: if the map
* is no longer alive, the flush is dropped without committing (R4).
*/
flushNow(): void {
this.clearHandle();
if (this.pending.size === 0) return;
if (!this.deps.isAlive()) {
// Teardown between schedule and flush — drop staged data, commit nothing.
this.pending.clear();
return;
}
const changed: string[] = [];
for (const [key, data] of this.pending) {
if (!this.equals(data, this.committed.get(key))) changed.push(key);
this.committed.set(key, data);
}
this.pending.clear();
if (changed.length > 0) this.deps.onCommit(changed);
}
/** Drop any scheduled flush and staged data without committing (teardown/destroy). */
cancel(): void {
this.clearHandle();
this.pending.clear();
}
private scheduleFlush(): void {
// Coalesce: cancel a prior scheduled flush and reschedule one.
this.clearHandle();
this.cancelHandle = this.deps.schedule(() => this.flushNow());
}
private clearHandle(): void {
if (this.cancelHandle) {
this.cancelHandle();
this.cancelHandle = null;
}
}
}