Skip to content

fix(components): delete each boot-clear database name once per page load - #19

Open
pythonlearner1025 wants to merge 1 commit into
mainfrom
fix/boot-cache-clear-memo-deletes-forever
Open

fix(components): delete each boot-clear database name once per page load#19
pythonlearner1025 wants to merge 1 commit into
mainfrom
fix/boot-cache-clear-memo-deletes-forever

Conversation

@pythonlearner1025

@pythonlearner1025 pythonlearner1025 commented Sep 2, 2026

Copy link
Copy Markdown
Member

Related issue

Same-repository branch; no intake Issue per .github/AGENTS.md ("Same-repository branches do not create or require an Issue solely for intake").

Problem / pressure

maybeClearLodyCacheOnBoot memoizes the clear mode, not the fact that the clear already ran:

// packages/components/src/lib/clear-local-cache.ts:420-426
export async function maybeClearLodyCacheOnBoot(extraNames: string[] = []): Promise<void> {
  bootClearPromise ??= runPendingClearOnBoot();
  const mode = await bootClearPromise;
  // Nothing was pending, or this caller has no extra databases to contribute.
  if (!mode || extraNames.length === 0) return;
  await Promise.all(extraNames.map(deleteDatabaseBestEffort));
}

runPendingClearOnBoot removes the localStorage flag in its finally on the first run (clear-local-cache.ts:400-406) and then returns the mode, which the module-level promise at clear-local-cache.ts:388 holds for the rest of the page load. So mode stays truthy forever, the guard at :424 never closes, and every later caller deletes whatever extraNames it was handed — even though the pending clear is long finished.

RuntimeProvider is such a caller, and it passes the current workspace's two IndexedDB names on every runtime build:

// packages/components/src/providers/runtime-provider.tsx:241-244
await maybeClearLodyCacheOnBoot([
  `lody-loro-repo-db-${effectiveWorkspaceId}`,
  `lody-loro-stream-cursors-${effectiveWorkspaceId}`,
]);

That call sits inside the init effect whose deps include workspaceSlug and effectiveWorkspaceId (runtime-provider.tsx:355-369), so it re-runs on an ordinary workspace switch — or a switch away and back. The result: after one Settings → Clear cache, each subsequent runtime build in that tab deletes that workspace's Loro repo database again, including one the runtime has since re-created and fully re-synced. IndexedDB deleteDatabase does not care that the database is new; it destroys the live replica, and the user sees the workspace empty itself again for no reason they can connect to any action.

The intent of extraNames is documented at clear-local-cache.ts:416-418: a caller arriving later in the same boot (RuntimeProvider, after AppInitializer.tsx:59-61 has already started the clear) can still contribute its databases. So the memo genuinely cannot be a plain "already ran" flag — the actual invariant is per name, not per call.

Summary

Track the caller-supplied database names already deleted during this page load in a module-level Set<string>, and delete only the unclaimed ones. Names are claimed synchronously between await bootClearPromise resuming and the first delete starting, so two concurrent callers cannot both claim the same name. resetBootClearMemoForTests clears the set alongside the promise.

Two tests added next to the existing suite in packages/components/tests/clear-local-cache.test.ts, using that file's existing hand-rolled indexedDB fake (no new dependency).

Before / after

Before After
One Settings → Clear cache arms the boot clear; every later runtime build in that tab re-deletes the workspace's lody-loro-repo-db-* and lody-loro-stream-cursors-*. The boot clear deletes each contributed name once; a later build passing the same names is a no-op.
A name first contributed after the boot clear ran is deleted. Unchanged — still deleted, exactly once.

Test plan

Ran from packages/components against a standalone clone (Node 22.20.0, pnpm 10.20.0):

  • vitest run tests/clear-local-cache.test.ts9 → 11 tests. The new deletes a caller-supplied database once per page load, not on every later call fails on main with expected [ Array(2) ] to have a length of 1 but got 2 (the second delete, empirically), and passes with the fix. All 11 pass after.
  • vitest run (full @lody/components suite) — attempted, did not complete. The sandbox this was prepared in has 4 cores and was under a load average above 30 from an unrelated concurrent job; the run was killed by a wall-clock timeout, and several unrelated suites had already tripped vitest's 5s per-test timeout under that contention. I am reporting this rather than quoting a pass I did not observe. Re-run pending on a quiet box; CI is the authority here.
  • tsgo --noEmit in packages/components — clean.
  • oxlint --ignore-pattern packages/acp-extension-kimi (repo lint:fast) — 0 errors. The one warning reported on clear-local-cache.ts is the pre-existing no-use-before-define at :141, untouched by this change.
  • prettier --check on both changed files — clean.

Not run: the full pnpm check. pnpm install in this sandbox needed --ignore-scripts (no native postinstall builds), and pnpm test:ci spans workspaces such as apps/cli whose native better-sqlite3 binding is therefore absent. pnpm format was not run repo-wide; prettier --check on the changed files stands in for it. pnpm lint (the --type-aware variant) was not run — only lint:fast.

Context handoff

Instructions for reviewing agents

  • Review focus: clear-local-cache.ts:422-440 — whether a per-name Set is the right invariant versus a per-call "already ran" flag, given extraNames' documented late-contributor purpose at :416-418.
  • Decisions to challenge: the set is never pruned and lives for the page load, so a name deleted at boot can never be deleted again in that tab; if any surface legitimately needs to re-clear one workspace mid-session, this closes that door and should instead be an explicit API.
  • Plausible failures / evidence gaps: the repro is a unit test over the suite's hand-rolled indexedDB fake, not a real browser; I did not measure how often RuntimeProvider's init effect actually re-runs in production, only that its deps (workspaceSlug, effectiveWorkspaceId) change on a workspace switch.

Authoring context

  • User goal / directives: fix the boot cache-clear memo so it stops re-deleting workspace databases after the first clear, minimally and without changing extraNames' intent.
  • Constraints / non-goals: touch only clear-local-cache.ts and its existing test file; do not add dependencies; do not change the settings/crash-screen entry points or the hard-reset path.
  • Risk-bearing decisions: the deduplication key is the database name, page-load scoped. Nothing persists, so a reload restores the old behavior of allowing a fresh delete.
  • Destructive or irreversible behavior: this change only ever reduces deletions — it removes a delete that destroyed re-synced local data. It cannot cause a pending clear to be skipped: the first contribution of every name is unaffected.
  • Deliberately not done or tested: did not add fake-indexeddb to model database re-creation; the observable that matters is that deleteDatabase is not issued a second time for the name, which the existing fake records directly. Did not touch the runPendingClearOnBoot internal sweep, whose names are not caller-supplied.
  • Unknowns / confidence: high confidence in the mechanism (reproduced as a failing test on main); lower confidence in the frequency of the user-visible symptom, which depends on how often a session rebuilds its runtime.

`maybeClearLodyCacheOnBoot` memoized the clear MODE rather than the fact that
the clear had run. `runPendingClearOnBoot` removes the localStorage flag on its
first run, so every later caller in the same page load still awaited a truthy
mode and deleted whatever `extraNames` it was handed.

`RuntimeProvider` passes the current workspace's two IndexedDB names on every
runtime build (runtime-provider.tsx:241). So after one Settings -> Clear cache,
each subsequent runtime rebuild in that tab deleted that workspace's Loro repo
again -- including one the runtime had already re-created and re-synced.

Track the caller-supplied names deleted during this page load and skip the ones
already handled. A name first contributed later during boot -- the documented
purpose of `extraNames` -- is still deleted once.

Model: claude-opus-5
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.

1 participant