Skip to content

[pull] main from tldraw:main#459

Merged
pull[bot] merged 6 commits intocode:mainfrom
tldraw:main
Mar 23, 2026
Merged

[pull] main from tldraw:main#459
pull[bot] merged 6 commits intocode:mainfrom
tldraw:main

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented Mar 23, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

MitjaBezensek and others added 6 commits March 23, 2026 12:11
In order to gradually roll out proper Zero to production users with
instant rollback, this PR extends the feature flag system with
percentage-based rollout and adds a `proper_zero` flag.

### What changed

**Shared types** (`dotcom-shared`): Feature flags are now a
discriminated union — `BooleanFeatureFlag` (on/off for everyone) or
`PercentageFeatureFlag` (rolled out to X% of users via deterministic
FNV-1a hash of userId+flagName).

**Server** (`sync-worker`):
- `evaluateFlagForUser` gates on `enabled` first (master kill switch),
then checks `hash < percentage` for percentage flags
- Separate `getFeatureFlags` (per-user evaluated) and
`getFeatureFlagsAdmin` (raw stored values) endpoints
- Admin POST accepts partial updates (`{ enabled }` and `{ percentage }`
are independent)

**Client**:
- `fetchFeatureFlags()` is a module-level promise that fires immediately
on import (no React component or atoms needed for initial fetch).
Resolves with defaults on failure — never rejects, never hangs.
- `FeatureFlagsFetcher` component now only handles subsequent polling +
reload-on-change
- `shouldUseProperZero(flags, email)` takes flags as a parameter.
Priority: localStorage override > @tldraw.com email > server flag, with
reason logging
- `TldrawApp` accepts `flags` in `create()` and stores `useProperZero`
decision as an instance property — `preload()` uses the stored decision
instead of re-evaluating (fixes bug where @tldraw.com users with server
flag off would skip the `/init` endpoint)
- `window.zero()` dev escape hatch to toggle localStorage override
- Removed `featureFlagsAtom`, `featureFlagsLoadedAtom`, and the reactive
wait/timeout pattern
- Deleted unused `useFeatureFlags` hook

**Admin UI**: Percentage flags get a separate component with independent
checkbox (enabled) and text input (percentage) + Save button. Disabled
states with proper CSS.

### Change type

- [x] `feature`

### Test plan

1. Deploy to staging
2. In admin, toggle `proper_zero` checkbox on → confirm → flag enabled
at 0%
3. Set percentage to 10, save → 10% of users get proper Zero
4. Uncheck checkbox → flag disabled, percentage preserved
5. Re-check → previous percentage still there
6. Verify @tldraw.com users always get proper Zero
7. Verify `window.zero()` toggles localStorage override and reloads
8. Verify auto-reload when flag value changes server-side
9. Verify @tldraw.com user with server flag off: proper Zero used AND
`/init` endpoint called

- [ ] Unit tests
- [ ] End to end tests

### Release notes

- Internal infrastructure for gradual Zero rollout

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches rollout controls and client startup behavior for selecting the
Zero implementation; mistakes could unexpectedly flip users between
`Zero` and `ZeroPolyfill` or cause reloads, but changes are gated behind
feature flags and admin-only endpoints.
> 
> **Overview**
> Adds **percentage-based feature flags** alongside boolean flags by
updating shared types to a discriminated union and introducing an
evaluated flag shape for the user-facing endpoint.
> 
> On the server, feature flags now support per-user evaluation via
deterministic hashing for percentage rollouts, add a new `proper_zero`
flag, split endpoints into user-evaluated (`/app/feature-flags`) vs
admin-raw (`/app/admin/feature-flags`), and allow admin updates to set
`enabled` and/or `percentage` independently; internal flag checks (e.g.
SQLite DO storage) switch to `getFeatureFlagEnabled`.
> 
> On the client, replaces the atom-based
`FeatureFlagsFetcher`/`useFeatureFlags` with `FeatureFlagPoller` + a
cached `fetchFeatureFlags()` used during app startup, wires flags+email
into `TldrawApp` to decide between proper Zero vs `ZeroPolyfill` (with a
localStorage override and clearer logging), and updates the admin
feature-flag UI to edit percentage rollouts with disabled-state styling.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
02eb465. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: David Sheldrick <d.j.sheldrick@gmail.com>
### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`
…de (#8302)

followup to #7842
was seeing black on black with some error pages when loading :P

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`
followup to #8294

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`
followup again to #8319

bad bot 🤖 

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`
In order to eliminate layout thrashing when resizing multiple geo shapes
with text labels, this PR batches all label DOM measurements into a
single pass per frame.


https://github.com/user-attachments/assets/40f361f0-e619-4c75-9f64-7e302898d158

Previously, each geo shape's `onResize` independently measured its label
via `measureHtml`, causing the browser to recalculate layout for every
shape. With many labeled shapes selected, this created a costly
read-write-read-write cycle.

This PR introduces a three-part optimization:

1. **`TextManager.measureHtmlBatch`** — A new method that measures
multiple HTML strings in a single batch using a pool of reusable DOM
elements. All style/innerHTML writes happen first, then all measurements
are read, triggering only one browser layout reflow. The pool grows on
demand and caches innerHTML to skip redundant parsing.

2. **`GeoShapeUtil` measurement helpers** — `measureUnscaledLabelSize`
is refactored to extract `getGeoLabelMeasurementRequest` (builds html +
opts without measuring) and `getGeoResizeTargetWidth` (computes target
width during resize). A module-level batch cache
(`setBatchLabelSizeCache`) lets `onResize` and `getGeometry` skip
individual measurements when pre-computed sizes are available.

3. **`Resizing.batchMeasureGeoLabels`** — Before each resize loop,
collects measurement requests for all eligible geo shapes,
batch-measures them via `measureHtmlBatch`, and populates the cache. The
cache is cleared on exit.

Original branch: `steveruiz/batch-resize-measurements`

Before:

<img width="1324" height="708" alt="image"
src="https://github.com/user-attachments/assets/de0891f9-5936-4456-9af2-52da13e927b5"
/>

After:

<img width="1212" height="499" alt="image"
src="https://github.com/user-attachments/assets/ff2d625e-f85f-41c9-9249-0e1e80fadb39"
/>

### Change type

- [x] `improvement`

### Test plan

1. Create several geo shapes with text labels
2. Select all shapes and resize from a corner handle
3. Verify shapes resize correctly with labels wrapping as expected
4. Verify single-shape resize still works correctly
5. Verify shapes without labels resize normally

### API changes

- Added `TextManager.measureHtmlBatch()` for batched DOM text
measurement

### Release notes

- Improve resize performance for multiple geo shapes with text labels by
batching DOM measurements


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes core resize-path behavior for `geo` shapes by introducing
per-frame batched label measurement and caching, which could affect
label wrapping/sizing or cleanup if edge cases slip through. Adds new
public `TextManager` measurement types/APIs, so downstream consumers may
be impacted by signature/type changes.
> 
> **Overview**
> **Improves multi-shape resize performance** by batching `geo` label
measurements into a single DOM write-then-read pass per frame, avoiding
per-shape layout thrash.
> 
> Adds `TextManager.measureHtmlBatch()` plus new public types
(`BatchMeasurementRequest`, `TLMeasuredTextSize`) and updates
`measureHtml`/`measureText` to return the shared type. `GeoShapeUtil`
now supports building measurement requests, uses a per-editor batch
cache (set during resize), and `Resizing` populates/clears this cache
around the resize loop so `onResize`/geometry can reuse precomputed
label sizes.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
68dfd7d. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Kostya Farber <kostya.farber@gmail.com>
@pull pull Bot locked and limited conversation to collaborators Mar 23, 2026
@pull pull Bot added the ⤵️ pull label Mar 23, 2026
@pull pull Bot merged commit 3805153 into code:main Mar 23, 2026
9 of 15 checks passed
@pull pull Bot had a problem deploying to deploy-production March 23, 2026 21:13 Failure
@pull pull Bot had a problem deploying to deploy-staging March 23, 2026 21:13 Error
@pull pull Bot had a problem deploying to vsce publish March 23, 2026 21:13 Failure
@pull pull Bot had a problem deploying to deploy-staging March 23, 2026 21:13 Failure
@pull pull Bot had a problem deploying to deploy-staging March 24, 2026 00:28 Failure
@pull pull Bot temporarily deployed to e2e-dotcom March 24, 2026 02:36 Inactive
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants