feat(sync): tombstones, the wire protocol, and the client loop
Row-level last-write-wins on updated_at, cursor-based on a server-assigned change_seq. The schema was built for this in step 1, so the work here is the three things it did not yet have. Tombstones (migration 4). Row-level sync cannot express a delete: with the row gone there is nothing to compare timestamps against, so the other device pushes its still-live copy back and the row silently returns. Every delete path now writes a tombstone inside the same transaction. The wire format lives in shared/sync-protocol.mjs and is imported by both sides, so there is one definition rather than two that drift. It carries the syncable-meta allowlist, which is the load-bearing part: meta mixes the learner's preferences with bookkeeping that describes one install, and replicating dict.loadedBands would tell a phone that had loaded bands 0-2 it holds every row the desktop has — the word rail would then fail to find words it believes are present. The sync loop pushes first, then pages the pull. Two details it would be easy to get wrong, both commented at their site: - The pull cursor advances only as rows are applied, never from the push response. The server's newest change_seq includes rows this device has not seen; adopting it skips them permanently, and nothing ever asks for that range again. - Pulled rows advance sync.pushedAt too, bounded by the instant the sync started. Otherwise they look like local edits and get pushed straight back, and an edit made during the sync is not swept up with them. Seeded rows carry updated_at = 0, so a fresh device is never dirty and can never win a conflict — the artifact's clobbering bug stays unrepresentable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
17
types/shared/sync-protocol.mjs.d.ts
vendored
Normal file
17
types/shared/sync-protocol.mjs.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/* Declarations for shared/sync-protocol.mjs. */
|
||||
|
||||
export interface SyncTableSpec {
|
||||
pk: string[];
|
||||
cols: string[];
|
||||
}
|
||||
|
||||
export const SYNC_TABLES: Record<string, SyncTableSpec>;
|
||||
export const SYNC_TABLE_NAMES: string[];
|
||||
export const PAGE_SIZE: number;
|
||||
|
||||
export function pkFor(tbl: string): string[] | null;
|
||||
|
||||
export function isSyncableMetaKey(key: string): boolean;
|
||||
export function rowKey(table: string, row: Record<string, unknown>): string;
|
||||
export function isDirty(row: { updated_at: number }, pushedAt: number): boolean;
|
||||
export function incomingWins(incomingUpdatedAt: number, localUpdatedAt: number | null): boolean;
|
||||
Reference in New Issue
Block a user