Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion packages/components/src/lib/clear-local-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ export function readPendingLocalClearMode(): PendingLocalClearMode | null {
// gets the wipe, while `RuntimeProvider` awaits the same promise so the repo DB
// is never reopened mid-delete.
let bootClearPromise: Promise<PendingLocalClearMode | null> | null = null;
// Caller-supplied database names already deleted during this page load.
const bootClearedDatabaseNames = new Set<string>();

async function runPendingClearOnBoot(): Promise<PendingLocalClearMode | null> {
const mode = readPendingLocalClearMode();
Expand Down Expand Up @@ -422,12 +424,25 @@ export async function maybeClearLodyCacheOnBoot(extraNames: string[] = []): Prom
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));
// The memo above resolves to the MODE, not to "already ran", because a caller
// that arrives later in the same boot still has to contribute its databases.
// But the clear is a one-shot: by the second time `RuntimeProvider` builds a
// runtime, the databases it names have been re-created and re-synced, so
// deleting them again destroys live data instead of stale cache. Claim the
// names synchronously, before the first await, so concurrent callers cannot
// both claim one.
const unclaimed = extraNames.filter((name) => !bootClearedDatabaseNames.has(name));
for (const name of unclaimed) {
bootClearedDatabaseNames.add(name);
}
if (unclaimed.length === 0) return;
await Promise.all(unclaimed.map(deleteDatabaseBestEffort));
}

/** Test-only: forget the per-page-load memo so each case starts clean. */
export function resetBootClearMemoForTests(): void {
bootClearPromise = null;
bootClearedDatabaseNames.clear();
}

/**
Expand Down
31 changes: 31 additions & 0 deletions packages/components/tests/clear-local-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,37 @@ describe('maybeClearLodyCacheOnBoot', () => {
expect(repoFilePathsDeletes).toHaveLength(1);
});

it('deletes a caller-supplied database once per page load, not on every later call', async () => {
markCacheClearPending();
const workspaceDatabases = [
'lody-loro-repo-db-ws-current',
'lody-loro-stream-cursors-ws-current',
];

// `AppInitializer` starts the boot clear; `RuntimeProvider` then contributes
// the current workspace's databases while building the runtime.
await maybeClearLodyCacheOnBoot();
await maybeClearLodyCacheOnBoot(workspaceDatabases);
// The runtime is rebuilt later in the same page load and passes the same
// names again. By then it has re-created and re-synced those databases, so a
// second delete destroys live data rather than stale cache.
await maybeClearLodyCacheOnBoot(workspaceDatabases);

for (const name of workspaceDatabases) {
expect(deletedDatabases.filter((deleted) => deleted === name)).toHaveLength(1);
}
});

it('still deletes a name first contributed after the boot clear already ran', async () => {
presentDatabases = [];
markCacheClearPending();

await maybeClearLodyCacheOnBoot();
await maybeClearLodyCacheOnBoot(['lody-loro-repo-db-ws-late']);

expect(deletedDatabases.filter((name) => name === 'lody-loro-repo-db-ws-late')).toHaveLength(1);
});

it('still deletes a caller-supplied database name that enumeration missed', async () => {
presentDatabases = [];
markCacheClearPending();
Expand Down