# Lazy

Import these from `@grafloria/renderer`.

## Classes

### `HtmlHostCuller`

The per-frame cull decision for HTML-layer node hosts.

Stateless with respect to the hosts themselves: `admits()` is told whether the host is
currently attached rather than remembering it. That is on purpose — a culler holding its
own attached-set is a second copy of a fact the DOM already owns, and the two desync the
first time a node is removed from the model mid-gesture. The DOM is the record; this is
only the policy.

culler.beginFrame(viewport.getViewBox(), viewport.getZoom(), heldByGesture);
    for (const node of customNodes) {
      if (culler.admits(node.id, bounds(node), host?.isConnected ?? false)) …
    }

```ts
class HtmlHostCuller
```

**Methods**

- `constructor(options: HostCullOptions = {}, freeze: FreezeQuery | null = null)`
- `getMode(): HostCullMode`
- `beginFrame(visible: Rectangle, zoom: number, exempt: ReadonlySet<string>): void` — Fix this frame's two rects and the set of nodes a live gesture owns.
- `admits(id: string, bounds: Rectangle, attached: boolean): boolean` — Should this node's host be in the document on this frame?

### `ProgressiveMounter`

```ts
class ProgressiveMounter
```

**Methods**

- `constructor( engine: DiagramEngine, lifecycle: ViewLifecycle, frame: MountFrame, deferred: DeferredQuery )`
- `isRunning(): boolean`
- `mount(viewport: Rectangle, zoom: number, options: ProgressiveMountOptions = {}): Promise<MountStats>` — Bring the scene up in rAF-yielded slices. Resolves when everything culling admits has a view (or when the mount is cancelled).
- `cancel(): void` — Stop mounting. Whatever is not yet mounted is mounted by the next normal render — the gate is handed back here, so a cancelled mount can never strand half a diagram on screen.
- `dispose(): void`

### `ViewLifecycle`

```ts
class ViewLifecycle implements MountGate
```

**Methods**

- `constructor(options: ViewLifecycleOptions = {})`
- `freeze(kind: EntityKind, id: string): void` — Give up this entity's view. It keeps its model and its spatial-index entry; it stops being drawn and stops costing anything per frame.
- `unfreeze(kind: EntityKind, id: string): void` — Give it a view again. It is rebuilt on the next frame that can see it.
- `isFrozen(kind: EntityKind, id: string): boolean`
- `isExplicitlyFrozen(kind: EntityKind, id: string): boolean` — Explicitly frozen only — NOT the ones autoFreeze is holding off-screen.
- `unfreezeAll(): void`
- `setAutoFreeze(on: boolean): void`
- `isAutoFreeze(): boolean`
- `retainedCount(): number` — The views currently retained — i.e. what the renderer is paying for.
- `frozenCount(): number`
- `retainVisible(visible: ReadonlyArray<readonly [EntityKind, string]>): void` — Called by the renderer each frame with what culling admitted, BEFORE the gate is applied. Anything that was on screen and no longer is gets its view dropped.
- `beginDeferred(): void` — Defer EVERYTHING. Nothing has a view until `admit()` says so.
- `admit(kind: EntityKind, id: string): void` — Let this entity's view be built from now on.
- `admitAll(kind: EntityKind): void` — Admit a whole KIND without naming its members. Slice 0 uses this for nodes: a node's view is cheap (no routing), and enumerating 10k ids to admit them one at a time would cost more than building the ~56 views culling actually keeps.
- `endDeferred(): void` — The mount is over (finished, cancelled, or pre-empted). Gate opens fully.
- `admits(kind: EntityKind, id: string): boolean` — May the renderer build (or refresh) this entity's view on this frame?
- `isDeferring(): boolean` — True while a progressive mount is running — the renderer's cue that the scene is being brought up in slices and is not yet whole.
- `setChangeHook(hook: (() => void) | null): void` — Tell the renderer that what it would draw has changed, even though the model has not. Set by `SVGRenderer.setViewLifecycle`; see `changeHook`.

## Interfaces

### `FreezeQuery`

The sliver of `ViewLifecycle` that host culling honours.

`isExplicitlyFrozen`, NOT `admits`, and the distinction is load-bearing. `admits` is also
false for anything autoFreeze is holding off-screen, and autoFreeze decides that against
the bare viewport with no margin — routing custom hosts through it would silently
override the hysteresis band with a zero-width one. `isExplicitlyFrozen` is the signal
that a HOST deliberately said "this node has no view", which is a decision custom nodes
should obey.

```ts
interface FreezeQuery
```

**Members**

- `isExplicitlyFrozen(kind: EntityKind, id: string): boolean`

### `HostCullOptions`

```ts
interface HostCullOptions
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `margin?` | `number` |  | How far beyond the viewport edge a host is still kept mounted, in CSS pixels. |
| `hysteresis?` | `number` |  | The extra distance, in CSS pixels, a host must travel BEYOND `margin` before it is culled. This is the hysteresis band, and it is the difference between culling and thrashing. |
| `mode?` | `HostCullMode` |  | What a cull does to the element. Default `'detach'` — see {@link HostCullMode}. |

### `MountGate`

The gate the renderer consults before instantiating an entity's VIEW.

A gate can only ever SUBTRACT from what culling already admitted — it never adds
an off-screen entity back in. That asymmetry is deliberate: a gate bug can make
something arrive late, never wrong.

```ts
interface MountGate
```

**Members**

- `admits(kind: EntityKind, id: string): boolean` — May the renderer build (or refresh) this entity's view on this frame?
- `isDeferring?(): boolean` — True while a progressive mount is running — the renderer's cue that the scene is being brought up in slices and is not yet whole.

### `MountStats`

What one `mount()` actually did — the numbers the claim lives on.

```ts
interface MountStats
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `firstPaintMs` | `number` |  | ms from `mount()` to the first frame that reached the screen. |
| `completeMs` | `number` |  | Wall clock from `mount()` to the last entity mounted — INCLUDING the rAF waits. |
| `cpuMs` | `number` |  | CPU actually spent, summed over the slices — i.e. `completeMs` minus the time spent yielded to the browser. |
| `slices` | `number` |  | rAF slices used (1 = it all fitted in the first frame). |
| `nodesMounted` | `number` |  | Entities whose views were built. |
| `linksMounted` | `number` |  |  |
| `worstSliceMs` | `number` |  | The worst single slice — the jank a user would actually feel. |
| `aborted` | `boolean` |  | True if the mount was cancelled or pre-empted by a model change. |

### `ProgressiveMountOptions`

```ts
interface ProgressiveMountOptions
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `sliceMs?` | `number` |  | Target ms per slice. The chunk size adapts to hit it. Default 8 (half a 60fps frame). |
| `initialChunk?` | `number` |  | Links admitted by the FIRST link slice — the one slice with no measurement to adapt from. Default 4, deliberately timid: link costs are wildly skewed (a typical link routes in 3ms, a long one against 10k obstacles in 850ms), so a big opening chunk is a coin-flip on a multi-second stall. It ramps up fast from here when links are cheap. |
| `maxSlices?` | `number` |  | Hard cap on slices, so a pathological scene still terminates. Default 500. |
| `onFirstPaint?` | `(stats: Readonly<MountStats>) => void` |  |  |
| `onSlice?` | `(stats: Readonly<MountStats>) => void` |  |  |

### `ViewLifecycleOptions`

```ts
interface ViewLifecycleOptions
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `autoFreeze?` | `boolean` |  | Drop the view of any entity that leaves the viewport. Default false — the historical behaviour (views linger in the LRU until evicted by pressure). |

## Types

### `DeferredQuery`

What the renderer deferred on the last frame — culling admitted it, the gate did not.

```ts
type DeferredQuery = () => ReadonlyArray<readonly [EntityKind, string]>;
```

### `EntityKind`

Also has every member of `String`, listed on its own entry.

The two things that have views.

```ts
type EntityKind = 'node' | 'link';
```

### `HostCullMode`

Also has every member of `String`, listed on its own entry.

What a cull does to the host element.

`'detach'` — remove the element from the document and keep the reference. Re-entry
re-appends the SAME element: `renderCustomNode` is NOT called again, `removeCustomNode`
is NOT called on the cull, and everything inside the widget survives byte for byte.

`'destroy'` — fire `removeCustomNode` and drop the element. Re-entry re-creates the host
and re-runs `renderCustomNode`. Frees the widget's memory; costs a full re-init and every
piece of state the widget was holding.

`'detach'` is the default, and the reasoning is that the cost this feature exists to
remove is a cost of being ATTACHED. Layout, style recalc, paint, compositing and hit
testing are all charged per element IN THE DOCUMENT; a detached subtree is inert heap. Detaching therefore captures essentially the whole win while keeping the mount-once
guarantee that makes custom nodes usable at all. Measured on a 300-widget board with ~16
tiles on screen: 2720 DOM nodes under the canvas fall to 164, a 94% reduction, and a pan
away and back re-runs the painter exactly zero times.

What `'detach'` does NOT bound is the RETAINED set. A host mounted once is kept for the
life of the instance, so panning a 10,000-widget board end to end eventually holds 10,000
detached elements — the same "cache versus leak" distinction `ViewLifecycle`'s header
draws about autoFreeze, and the honest reason `'destroy'` exists. (Same board, after a
sweep across and back: 20 attached, 78 retained off-screen.) Choose `'destroy'` when the
widgets are individually huge — a WebGL scene, a 50k-row grid — and heap rather than
frame time is the binding constraint. It is opt-in inside an opt-in, because a host that
asks for it is accepting that its painter re-runs and its widget state is lost.

```ts
type HostCullMode = 'detach' | 'destroy';
```

### `MountFrame`

Produce and PRESENT one frame. Deliberately not "the SVG renderer": the SVG
patcher, the canvas backend and the tier-switching backend all satisfy this, so
a progressive mount works on any of them.

```ts
type MountFrame = (viewport: Rectangle, zoom: number) => void;
```
