Skip to content

Commit 07f06e1

Browse files
committed
fix(sync): reconcile local cache on startup
1 parent 335d815 commit 07f06e1

3 files changed

Lines changed: 53 additions & 3 deletions

File tree

packages/core/src/sync/engine/sync-manager.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export function createSyncManager(
6767
let errors: SyncError[] = [];
6868
let cycleInFlight: Promise<void> | null = null;
6969
let rerunRequested = false;
70+
let needsFullReconcile = true;
7071

7172
async function runCycle(): Promise<void> {
7273
// Prevent concurrent cycles
@@ -119,9 +120,13 @@ export function createSyncManager(
119120

120121
try {
121122
// Download new remote files
122-
const downloadResults = await downloadAll(downloadDeps);
123+
const fullReconcile = needsFullReconcile;
124+
const downloadResults = await downloadAll(downloadDeps, {
125+
fullReconcile,
126+
});
127+
needsFullReconcile = false;
123128
downloadDeps.logger.debug(
124-
{ downloaded: downloadResults.length },
129+
{ downloaded: downloadResults.length, fullReconcile },
125130
"Download cycle complete",
126131
);
127132
} catch (err) {

packages/core/src/sync/workers/download.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,34 @@ describe("download worker", () => {
731731
);
732732
});
733733

734+
it("can reconcile missing local records even when the cursor is already advanced", async () => {
735+
const deps = makeMockDeps();
736+
(deps.cursor.read as ReturnType<typeof vi.fn>).mockResolvedValue(
737+
"2026-06-01T00:00:00Z",
738+
);
739+
(
740+
deps.gateway.listFilesSince as ReturnType<typeof vi.fn>
741+
).mockResolvedValue({
742+
files: [makeFileRecord({ createdAt: "2026-01-21T10:00:00Z" })],
743+
cursor: "2026-06-10T00:00:00Z",
744+
});
745+
746+
const results = await downloadAll(deps, { fullReconcile: true });
747+
748+
expect(results).toHaveLength(1);
749+
expect(deps.cursor.read).not.toHaveBeenCalled();
750+
expect(deps.gateway.listFilesSince).toHaveBeenCalledWith(OWNER, null, {
751+
includeDeleted: true,
752+
});
753+
expect(deps.storageAdapter.download).toHaveBeenCalledWith(STORAGE_URL);
754+
expect(deps.storage.insertEntry).toHaveBeenCalledWith(
755+
expect.objectContaining({
756+
fileId: FILE_ID,
757+
scope: SCOPE,
758+
}),
759+
);
760+
});
761+
734762
it("reconciles a remote deletion by dropping the local copy (no download)", async () => {
735763
const deps = makeMockDeps();
736764
const deletedRecord = makeFileRecord({

packages/core/src/sync/workers/download.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ export interface DownloadResult {
5353
path: string;
5454
}
5555

56+
export interface DownloadAllOptions {
57+
/**
58+
* Ignore the stored incremental cursor and reconcile from the beginning of the
59+
* owner's file registry. This repairs browser caches whose cursor advanced
60+
* while local IndexedDB/OPFS lost or failed to index some older records.
61+
*/
62+
fullReconcile?: boolean;
63+
}
64+
5665
interface SyncFailureMetadata {
5766
stage?: SyncFailureStage;
5867
scope?: string;
@@ -284,6 +293,7 @@ export async function downloadOne(
284293
*/
285294
export async function downloadAll(
286295
deps: DownloadWorkerDeps,
296+
options: DownloadAllOptions = {},
287297
): Promise<DownloadResult[]> {
288298
const { gateway, cursor, serverOwner, logger } = deps;
289299
const syncRunId = createSyncRunId();
@@ -292,7 +302,14 @@ export async function downloadAll(
292302

293303
// 1. Read cursor
294304
const lastProcessedTimestamp =
295-
repairSummary.missingEnvelopeEntries > 0 ? null : await cursor.read();
305+
options.fullReconcile || repairSummary.missingEnvelopeEntries > 0
306+
? null
307+
: await cursor.read();
308+
if (options.fullReconcile) {
309+
logger.info(
310+
"Running full registry reconciliation to repair any missing local scope records",
311+
);
312+
}
296313
if (repairSummary.missingEnvelopeEntries > 0) {
297314
logger.warn(
298315
{ missingEnvelopeEntries: repairSummary.missingEnvelopeEntries },

0 commit comments

Comments
 (0)