How Zero achieves real-time synchronization using incremental view maintenance and change streaming
Zero’s sync model is built on three core principles: real-time replication from PostgreSQL, incremental view maintenance for efficiency, and optimistic client updates for responsiveness.
The IncrementalSyncer processes the replication stream:
// From packages/zero-cache/src/services/replicator/incremental-sync.tsexport class IncrementalSyncer { async run(lc: LogContext) { while (this.#state.shouldRun()) { const {replicaVersion, watermark} = getSubscriptionState(this.#replica); downstream = await this.#changeStreamer.subscribe({ protocolVersion: PROTOCOL_VERSION, watermark, // Resume from last processed position replicaVersion, }); for await (const message of downstream) { // Process each replication message const result = processor.processMessage(lc, message); if (result?.watermark && result?.changeLogUpdated) { // Notify clients when new version is ready void this.#notifier.notifySubscribers({state: 'version-ready'}); } } } }}
// Client sends its current versiontype PullRequestMessage = { pullVersion: number; profileID: string; clientGroupID: string; cookie: NullableVersion; // Last known server version};// Server responds with patchtype PullResponseMessage = { cookie: NullableVersion; // New server version lastMutationIDChanges: Record<ClientID, number>; patch: Patch[]; // Changes since client's version};
Clients apply mutations optimistically before server confirmation:
// Mutation applies immediately to local IndexedDBawait zero.mutate.updateIssue({ id: 'issue-123', title: 'Updated title', // Shows immediately in UI});// Meanwhile:// 1. Mutation queued for push// 2. Pushed to server over WebSocket// 3. Server validates and executes// 4. Replication streams back to zero-cache// 5. zero-cache pokes client// 6. Client pulls and reconciles
If optimistic mutation conflicts with server, it fails and UI reverts
// Example: Two clients update the same issue// Client A:await zero.mutate.updateIssue({id: 'issue-1', title: 'New Title A'});// Client B (simultaneously):await zero.mutate.updateIssue({id: 'issue-1', title: 'New Title B'});// Result: Whichever mutation reaches PostgreSQL first wins.// The other client's optimistic update is replaced by the server's version.
For complex conflict resolution, implement it in a server-side mutator where you can use transactions and business logic.
const zero = new Zero({ server: 'https://your-server.com', schema, userID, // Mutations are queued locally // Queries read from IndexedDB});// Works offline:const issues = useQuery(zero.query.issue.where('status', 'open'));await zero.mutate.createIssue({ id: nanoid(), title: 'Created offline', status: 'open',});// When online, mutations sync automatically
Offline mutations are queued and executed in order when connectivity is restored. Failed mutations (e.g., permission denied) are reported via PromiseWithServerResult.
Zero implements backpressure to prevent overwhelming clients:
Client-side: Replicache buffers updates if UI is busy
Server-side: zero-cache tracks memory usage and pauses replication if needed
Yielding: IVM operators yield control during large transactions
// Operators yield to avoid blockingexport interface Input { fetch(req: FetchRequest): Stream<Node | 'yield'>;}// Consumer must handle 'yield' and give control back periodicallyfor (const item of stream) { if (item === 'yield') { await sleep(0); // Let other tasks run } else { // Process node }}