mirror of
https://github.com/twentyhq/twenty.git
synced 2026-08-02 18:54:43 -04:00
Front components run in a Web Worker whose fake DOM has no layout APIs,
so any library that measures itself crashes. This is the reported
recharts bug: `ref.getBoundingClientRect is not a function`.
### Why this is needed
Layout only exists on the host: the worker builds a virtual tree, and
the host renders the real DOM nodes. Nothing in the worker knows how big
anything is.
```mermaid
flowchart LR
COMP["Front component<br/>recharts, twenty-ui"] -->|"el.getBoundingClientRect()"| DOM["remote-dom fake DOM<br/>in the Web Worker"]
DOM --> MISS["No layout APIs:<br/>method does not exist"]
MISS --> BOOM["TypeError, component crashes"]
HOST["Host: real DOM nodes<br/>with real sizes"] -.->|"never reaches the worker"| DOM
```
The worker cannot simply ask the host and wait: measurement APIs are
synchronous, and the worker must never block on a round trip.
### How the mirror works
The host measures and pushes; the worker only ever reads from a local
copy. Reads stay synchronous and are at most one frame behind.
```mermaid
flowchart TB
subgraph HOST["Host - main thread, real DOM"]
WAKE["Wake sources<br/>resize, scroll, mutations, animation events"]
TRACK["createGeometryTracker<br/>rAF loop, idles after 20 unchanged frames"]
NODES["Real DOM nodes<br/>registered per remote element id"]
end
subgraph WORKER["Web Worker - fake DOM"]
STORE["workerGeometryStore<br/>snapshot mirror"]
POLY["Element.prototype polyfill<br/>getBoundingClientRect, offset, client, scroll"]
COMP2["Front component"]
end
WAKE -->|"wake"| TRACK
NODES -->|"measure changed nodes only"| TRACK
TRACK ==>|"pushGeometryUpdates over MessagePort"| STORE
STORE -->|"synchronous read, one frame stale"| POLY
POLY --> COMP2
COMP2 -.->|"first read enrolls the element:<br/>observeElementGeometry"| TRACK
```
Enrollment is demand-driven: an element is only measured once the
component actually reads its geometry, so idle components cost nothing.
```mermaid
sequenceDiagram
participant C as Front component
participant P as Element polyfill
participant S as Worker geometry store
participant T as Host geometry tracker
participant D as Real DOM
C->>P: el.getBoundingClientRect
P->>S: resolve snapshot
S-->>P: none yet, returns zeros
S->>T: observeElementGeometry, batched in a microtask
T->>D: measure on the next animation frame
D-->>T: rect, offset, client, scroll
T->>S: pushGeometryUpdates with viewport and changed elements
Note over T: the loop stops after 20 unchanged frames, any wake source restarts it
C->>P: el.getBoundingClientRect on a later frame
P->>S: resolve snapshot
S-->>P: mirrored values
P-->>C: real numbers
```
### What changed
- The host measures the real DOM nodes on animation frames while wake
sources report activity, and pushes snapshots over the existing
MessagePort. The loop goes idle when nothing changes, and both sides cap
observation at 500 elements.
- In the worker, `getBoundingClientRect`, the
`offset*`/`client*`/`scroll*` getters and
`window.innerWidth`/`innerHeight` read those snapshots from the
worker-local mirror.
- The worker also gains the small DOM APIs libraries expect:
`getComputedStyle` (returns the element's declared style),
`getElementsByClassName`, `document.getElementById`, and a working
per-element `style` on base elements (remote-dom ships a no-op stub
whose `getPropertyValue` returns undefined, which crashed twenty-ui's
ThemeProvider).
Result: a fixed-size recharts `AreaChart` story renders, and the four
twenty-ui gallery stories that used to fail on the missing
`getComputedStyle` now run in strict zero-failure mode.
Moved, not new: `FrontComponentRenderer` now renders its thread effects
directly instead of through a pass-through component, and its output is
wrapped in a `<div style="width:100%;height:100%">` instead of a
fragment so geometry has a measurable root (a real layout change for
embedders).
Deferred to the ResizeObserver follow-up: text measurement (axis-label
overlap thinning), `offsetParent` mirroring, animation in-flight
tracking, `ResponsiveContainer`, the tooltip, and the
`measureElementGeometry` RPC.
Last of the three PRs splitting the geometry mirror work, after #23262
(host wrapper hooks) and #23263 (style proxy).