Skip to content

fix(client): stop ExpiredShapesCache writing to localStorage on every read - #4799

Merged
kevin-dp merged 1 commit into
electric-sql:mainfrom
PepijnSenders:fix/expired-shapes-cache-read-write
Sep 8, 2026
Merged

fix(client): stop ExpiredShapesCache writing to localStorage on every read#4799
kevin-dp merged 1 commit into
electric-sql:mainfrom
PepijnSenders:fix/expired-shapes-cache-read-write

Conversation

@PepijnSenders

Copy link
Copy Markdown
Contributor

closes #4795

why the change

any electric app in a webkit webview grows localStorage forever while it's open — our tauri app hit a 4.2gb wal next to a 49kb db after three days, 736mb on macos. getExpiredHandle is a read accessor that persists the whole cache, and it runs on every shape request, so a live stream writes once per poll. webkit never checkpoints the wal while the process lives, so nothing reclaims it.

special things to note

  • lastUsed still updates in memory, so lru order within a session is unchanged; only eviction order after a reload can be staler
  • evicting on a successful non-409 response isn't valid, it fails should not accept expired handle from stale cached response after 409
  • the read test fails on main with 25 writes for 25 reads, 452 pass with the change
  • this stops the growth, it doesn't reclaim existing wal files — those only collapse on a clean exit of the app, and on macos the storage lives in a shared xpc process that outlives the client, so even that isn't guaranteed

change outline

the write leaves the read path

  ExpiredShapesCache.getExpiredHandle(shapeUrl)
  └─ if (entry)
     ├─ Date.now()
-    └─ ExpiredShapesCache.save()
-       ├─ if (typeof localStorage === `undefined`)
-       ├─ localStorage.setItem()
-       └─ JSON.stringify()

… read

getExpiredHandle is a read accessor that also persisted the whole cache, and it
runs while building the URL of every shape request. In live mode that is a full
JSON.stringify plus localStorage.setItem once per poll, indefinitely.

lastUsed is still updated in memory, and markExpired reads it from memory when
picking an eviction candidate, so LRU ordering within a session is unchanged.
After a reload the persisted values reflect the last markExpired rather than the
last read, so eviction order at the 250-entry cap can be staler.
@PepijnSenders
PepijnSenders force-pushed the fix/expired-shapes-cache-read-write branch from 8ea9aa2 to 88b7508 Compare September 7, 2026 21:42

@kevin-dp kevin-dp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks for spotting this and fixing it!

@pkg-pr-new

pkg-pr-new Bot commented Sep 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@electric-sql/react@4799
npm i https://pkg.pr.new/@electric-sql/client@4799
npm i https://pkg.pr.new/@electric-sql/y-electric@4799

commit: 88b7508

@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 71.59%. Comparing base (0f40420) to head (88b7508).

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #4799       +/-   ##
===========================================
+ Coverage   60.04%   71.59%   +11.55%     
===========================================
  Files         397       70      -327     
  Lines       43773     8975    -34798     
  Branches    12592     2848     -9744     
===========================================
- Hits        26283     6426    -19857     
+ Misses      17409     2531    -14878     
+ Partials       81       18       -63     
Flag Coverage Δ
packages/agents ?
packages/agents-mcp ?
packages/agents-mobile ?
packages/agents-runtime ?
packages/agents-server 75.67% <ø> (+0.13%) ⬆️
packages/agents-server-ui ?
packages/electric-ax 51.06% <ø> (ø)
packages/experimental ?
packages/react-hooks ?
packages/start ?
packages/typescript-client ?
packages/y-electric 56.05% <ø> (ø)
typescript 71.59% <ø> (+11.55%) ⬆️
unit-tests 71.59% <ø> (+11.55%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@kevin-dp
kevin-dp merged commit ff9d221 into electric-sql:main Sep 8, 2026
32 of 33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ExpiredShapesCache writes to localStorage on every read

2 participants