# Good Example: Edge Offline-First + Sync Reconcile > A field-service tablet app that operates through a 90-minute network > partition, queues writes locally, and reconciles on reconnect using > the CRDT-vs-LWW decision matrix from `domains/edge/sync.md`. Each > aspect cites the edge principle it satisfies. ## The Code ```typescript // The local store: an offline-first write queue + a CRDT register // per record. The tablet keeps working through the partition; the // queue drains on reconnect; the merge converges (P4 bounded). interface LWWRegister { value: T; timestamp: number; // monotonic logical clock (HLC), NOT wall time nodeId: string; // tablet id — deterministic tiebreak (P4) } interface QueuedWrite { idempotencyKey: string; // P5: retried syncs are safe collection: string; recordId: string; register: LWWRegister; queuedAt: number; } class OfflineStore { private queue: QueuedWrite[] = []; private state: Map> = new Map(); private telemetryBuffer: TelemetryRecord[] = []; // P10 local-first // P2: write succeeds offline. The queue is the durable record of // intent; sync is deferred, not blocked. write(collection: string, recordId: string, value: unknown, key: string): void { const register: LWWRegister = { value, timestamp: this.hlc.now(), // monotonic; no wall-clock skew nodeId: this.nodeId, }; this.state.set(`${collection}:${recordId}`, register); this.queue.push({ idempotencyKey: key, // P5: idempotent sync collection, recordId, register, queuedAt: this.hlc.now(), }); // P10: buffer the write event locally; forwarded on reconnect. this.telemetryBuffer.push({ event: "local.write", at: Date.now(), key }); } // P2: read from local state offline. The UI never blocks on the // network. read(collection: string, recordId: string): unknown { return this.state.get(`${collection}:${recordId}`)?.value; } } ``` ```typescript // The CRDT register merge — a state-based CvRDT (convergent). Per the // CRDT-vs-LWW decision matrix in domains/edge/sync.md, a state-based // CRDT is the choice when the data model fits a register and the // network is unreliable (full-state merge tolerates dropped ops). function mergeLWWRegister( local: LWWRegister, remote: LWWRegister, ): LWWRegister { // Associative, commutative, idempotent (P4 bound, P5 idempotent). // (timestamp, nodeId) is a total order — no oscillation. if (local.timestamp > remote.timestamp) return local; if (local.timestamp < remote.timestamp) return remote; return local.nodeId > remote.nodeId ? local : remote; // deterministic tie } ``` ```typescript // The reconnect reconcile: drain the offline queue, pull remote // state, three-way merge (CRDT registers), push converged state. // Idempotent keys make a retried reconcile safe (P5). async function reconcile(store: OfflineStore, server: SyncServer): Promise { // P5: the reconcile is idempotent. The idempotency key on each // queued write means a retry (network flapped mid-reconcile) does // not double-apply. const queued = store.drainQueue(); let pushed = 0, merged = 0, conflicts = 0; // 1. Push local writes. The server dedups by idempotencyKey (P5). for (const w of queued) { await server.applyWrite(w.idempotencyKey, w.collection, w.recordId, w.register); pushed++; } // 2. Pull remote state for every record we touched + every record // the server changed since our last sync cursor. Merge via CRDT. const remoteRecords = await server.fetchChanged(store.syncCursor()); for (const [key, remoteReg] of remoteRecords) { const localReg = store.localRegister(key); if (localReg) { // P4: CRDT merge converges. The merge is a pure function of // two inputs; the total order (timestamp, nodeId) guarantees // no oscillation. const converged = mergeLWWRegister(localReg, remoteReg); if (converged !== localReg) { conflicts++; } store.setLocal(key, converged); merged++; } else { store.setLocal(key, remoteReg); // remote-only record merged++; } } // 3. P10: flush the buffered telemetry. The partition did not blind // the operator — the events survived on-node. store.flushTelemetry(); return { pushed, merged, conflicts, converged: true }; } ``` ```typescript // P10: local-first telemetry. Events are buffered on-node and // forwarded on reconnect. A fire-and-forget pipeline loses data when // the link drops; a local-first buffer survives. interface TelemetryRecord { event: string; at: number; key: string; } class TelemetryBuffer { private buffer: TelemetryRecord[] = []; push(rec: TelemetryRecord): void { this.buffer.push(rec); } // Called from reconcile() on reconnect. The buffer is the P10 // guarantee: the operator sees the partition-window activity, // not a gap. async flush(sink: TelemetrySink): Promise { for (const rec of this.buffer) { await sink.emit(rec); } this.buffer = []; } depth(): number { return this.buffer.length; } } ``` ## The Scenario A field-service tablet is dispatched to a basement site with no cellular coverage. The technician updates the work-order status (`started`, `parts-ordered`, `completed`) five times over 90 minutes. Each write lands in the local store immediately — the UI never blocks on the network. The writes are queued with an idempotency key (the work-order id + a monotonic sequence). When the tablet reconnects, the reconcile drains the queue: the server dedups by idempotency key (a retry mid-reconcile does not double-apply). The server also returns a remote update — the dispatcher re-assigned the work order to a different technician at minute 45, then reverted at minute 60. The CRDT merge converges: the register with the later logical timestamp wins; the tiebreak by node id is deterministic. The merge terminates in one pass (P4 bounded); it does not oscillate between the dispatcher's revert and the technician's status updates. The buffered telemetry flushes, and the operator sees the full partition-window activity — no gap. ## Principles Demonstrated ### Offline is a First-Class State (Edge P2, C1, C5) - The tablet writes and reads through the partition. The UI never blocks on the network; the offline write queue is the durable record of intent. Partition is the norm, not the exception; reconciliation happens on reconnect. An app that crashes on disconnect has no offline state; this app engineers it. - See `domains/edge/offline-first.md` (offline write-queue) and `domains/edge/first-principles.md` P2. ### Sync Conflicts are Bounded, Not Infinite (Edge P4, C1, C5) - The CRDT register merge is associative, commutative, and idempotent. The total order `(timestamp, nodeId)` guarantees convergence in one pass — no oscillation, no infinite loop. This is the P4 bound: the merge terminates. The CRDT-vs-LWW decision matrix in `domains/edge/sync.md` selected a state-based CRDT because the data model fits a register and the network is unreliable (full-state merge tolerates dropped ops). - See `domains/edge/sync.md` (CRDT-vs-LWW decision matrix, merge semantics) and `domains/edge/first-principles.md` P4. ### Edge Operations are Idempotent (Edge P5, C1) - Every queued write carries an idempotency key; the server dedups by key. A reconcile retried mid-flap does not double-apply. The merge function is idempotent (`merge(a, a) === a`) — a retried merge of the same two replicas yields the same result. Sync, the retried-by-nature operation, is safe. - See `domains/edge/sync.md` (Merge Semantics, P5) and `domains/edge/first-principles.md` P5. ### Edge Observability Survives Partition (Edge P10, C7, C5) - Telemetry is buffered on-node (`telemetryBuffer`) and flushed on reconnect. The partition did not blind the operator — the partition-window activity is forwarded, not lost. A fire-and-forget pipeline would have a 90-minute gap; the local-first buffer survives. - See `domains/edge/offline-first.md` (local-first logging) and `domains/edge/first-principles.md` P10. ## Cross-Domain Links - `domains/edge/offline-first.md` — the offline write-queue pattern this app instantiates; the partition-as-norm discipline. - `domains/edge/sync.md` — the CRDT-vs-LWW decision matrix exercised here (state-based CvRDT chosen for register + unreliable network); the merge-semantics properties (associative, commutative, idempotent). - `domains/edge/first-principles.md` — P2, P4, P5, P10 are the principles demonstrated. - `domains/concurrency/patterns` — the in-process immutability analog (`concurrency/P1 Immutability by Default`) that makes the merge a pure function of two inputs. - `domains/observability/metrics` — the generic SLI/SLO discipline the local-first telemetry buffer builds on. - `review/anti-patterns.md` — the `edge-sync-loop` chaos anti-pattern is the inverse of this example's bounded CRDT merge.