Skip to content

System Invariants

OfflineSync is built around 9 invariants (referenced as INV-N throughout the source code). These are guarantees that the system maintains under all conditions, including crashes, network failures, and concurrent operations.

Overview

#InvariantDescription
INV-1Sequence orderingMutations are strictly ordered by sequence number within each collection
INV-2IdempotencySync operations can be safely retried without side effects
INV-3Cursor consistencyCursors are updated only after all changes are durable
INV-4Mutation durabilityPending mutations survive process restarts
INV-5Exactly-once processingEach mutation is processed exactly once by the server
INV-6Conflict detectionConflicts are detected when server and client versions diverge
INV-7Resolution determinismThe same conflict inputs always produce the same resolution output
INV-8Atomic writesEntity updates and mutation records are stored in the same transaction
INV-9BackoffConsecutive sync failures trigger exponential backoff

INV-1: Sequence Ordering

Every mutation within a collection is assigned a monotonically increasing sequence number. The MutationRecorder tracks the current maximum sequence per collection and assigns the next number on each write. This guarantees that mutations can be replayed in the correct order.

Enforced by: MutationRecorder

Tested by: Property-based tests that verify sequence numbers are strictly increasing across 300+ random operation sequences.

INV-2: Idempotency

All sync operations are designed to be safely retryable. If a sync cycle is interrupted (network drop, process crash), the next cycle will send the same mutations again. The server uses mutation IDs to deduplicate, ensuring no double-processing.

Enforced by: SyncEngine, ServerMutationTracker

Tested by: Integration tests that simulate interrupted sync cycles and verify no data duplication.

INV-3: Cursor Consistency

The sync cursor (used for incremental sync) is only updated after all received changes have been durably written to storage. This means if a crash occurs during change application, the next sync will re-fetch from the previous cursor — no data is lost.

Enforced by: SyncEngine (cursor update is the last step after transaction commit)

INV-4: Mutation Durability

Pending mutations are stored in the same storage transaction as the entity write. If the process crashes between writing an entity and queuing its mutation, both are rolled back. If the process crashes after the transaction commits, the mutation is safely persisted and will be sent on the next sync cycle.

Enforced by: Collection (uses storage.transaction() for atomic writes)

Tested by: Stress tests with 10,000 entities and crash simulation tests.

INV-5: Exactly-Once Processing

Each mutation is processed exactly once by the server. The ServerMutationTracker maintains a set of seen mutation IDs and rejects duplicates. This works in combination with INV-2 (idempotent retry) to ensure correctness.

Enforced by: ServerMutationTracker (INV-5 deduplication)

INV-6: Conflict Detection

Conflicts are detected when the server and client have divergent versions of the same entity. The protocol includes both clientVersion (the revision the client last knew about) and serverVersion (the current revision on the server). When these differ and the server has changes the client doesn't know about, a conflict is reported.

Enforced by: SyncEngine, protocol ConflictInfo type

INV-7: Resolution Determinism

Given the same conflict inputs (client data, server data, operation type), a resolution strategy always produces the same output. This is critical for correctness — two clients resolving the same conflict independently must arrive at the same result.

Enforced by: All built-in strategies (pure functions with no side effects)

Tested by: Property-based tests that verify deterministic output across 300+ random conflict scenarios.

INV-8: Atomic Writes

Entity data and mutation records are written in the same storage transaction. This is the foundation for INV-4 (durability) and ensures that the mutation queue is always consistent with the actual entity state.

Enforced by: Collection.create(), Collection.put(), Collection.update(), Collection.delete() — all use storage.transaction()

Tested by: Atomic write tests that verify mutation records exist iff the entity write succeeded.

INV-9: Backoff

When sync fails consecutively, the SyncScheduler increases the interval between retries using exponential backoff (baseInterval * backoffMultiplier^attempt). This prevents hammering a failing server while still recovering quickly when connectivity returns.

Enforced by: SyncScheduler

Default: 30s base, 2x multiplier, 5 minute cap. On successful sync, the interval resets to the base.

Released under the MIT License.