Belajar Desktop Developer - Local-First & Sync
Episode 22 of 28

Belajar Desktop Developer - Local-First & Sync

Menyempurnakan offline-first CatatanKu: memahami prinsip local-first, memakai CRDT dan LWW untuk resolusi konflik, serta merancang mesin sinkronisasi antar perangkat yang berfungsi di jaringan lambat atau tanpa koneksi

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Sejak episode 8 CatatanKu menganut pola offline-first: data lokal adalah sumber utama, jaringan hanya pelengkap. Episode ini mewujudkan janji itu sepenuhnya: local-first & sync. Pengguna CatatanKu menulis di laptop kantor tanpa jaringan, lalu menulis lagi di komputer rumah — dan kedua catatan harus bersatu tanpa kehilangan satu karakter pun.

Ini masalah yang jauh lebih dalam dari sekadar "upload file". Dua perangkat mengedit data yang sama pada waktu berbeda — siapa yang menang? Jawaban naif ("yang terakhir menulis") menimpa pekerjaan yang satu lagi. Episode ini membangun mesin sinkronisasi CatatanKu dengan CRDT (struktur data tanpa konflik) dan LWW (last-write-wins) untuk metadata — fondasi yang dipakai aplikasi seperti Obsidian, Notion, dan Figma.

Prinsip Local-First

Data Lokal Adalah Data Utama

Sinkronisasi yang baik dimulai dari desain yang benar. Empat prinsip local-first yang kita pegang:

Prinsip local-first
1. Data lokal = sumber kebenaran; cloud hanya replika
2. Aplikasi berfungsi penuh tanpa jaringan
3. Multi-device: setiap perangkat punya salinan lengkap
4. Konflik diselesaikan secara otomatis & deterministik

Konsekuensi desainnya: database lokal (episode 8) menjadi kanonik, bukan cache dari server. Jika server hilang, pengguna tidak kehilangan apa pun. Ini kebalikan dari pola web tradisional di mana browser hanya "pemandangan" dari state server.

ID Lokal yang Global

Di episode 8 kita memakai UUID sebagai id catatan — bukan kebetulan. Dengan UUID, setiap perangkat bisa membuat catatan baru secara mandiri tanpa khawatir tabrakan id saat sinkron. Ini prasyarat pertama sync yang sehat.

CRDT: Konflik yang Tidak Menjadi Konflik

LWW untuk Metadata

Untuk metadata sederhana (judul, updated_at), Last-Write-Wins sudah cukup: entri dengan timestamp terbesar yang menang. Murni, deterministik, dan mudah:

Sinkronisasi metadata LWW
-- Tabel perubahan yang dikirim antar perangkat
CREATE TABLE outbox (
  note_id   TEXT NOT NULL,
  field     TEXT NOT NULL,
  value     TEXT NOT NULL,
  version   INTEGER NOT NULL,   -- clock logika, bukan jam dinding
  PRIMARY KEY (note_id, field)
);
 
-- Saat menerima: versi lebih besar menang
INSERT INTO notes (id, title, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
  title      = excluded.title,
  updated_at = excluded.updated_at
WHERE excluded.updated_at > notes.updated_at;

Penting: pakai version clock (jumlah perubahan) atau timestamp dengan jam yang disinkronkan (NTP), bukan new Date() mentah — jam perangkat bisa meleset, dan meleset berarti data salah menang.

CRDT untuk Isi Teks

Isi catatan yang diedit bersamaan butuh lebih dari LWW — kalau dua perangkat menambahkan kalimat berbeda di paragraf yang sama, satu kalimat akan hilang. CRDT (Conflict-free Replicated Data Type) memungkinkan penggabungan tanpa kehilangan:

Contoh: dua edit offline di catatan yang sama
Perangkat A menulis:  "Beli susu dan kopi"
Perangkat B menulis:  "Beli susu dan gula"
Gabungan CRDT:        "Beli susu dan kopi gula"  ← tidak ada yang hilang

Implementasi CRDT isi teks untuk CatatanKu bisa berupa Yjs (yang paling matang) atau Automerge:

Electron: Yjs untuk isi catatan
import * as Y from "yjs";
 
const doc = new Y.Doc();
const notes = doc.getMap<Y.Text>("notes");
 
// update lokal: langsung operan pada struktur CRDT
const text = notes.get(noteId) ?? new Y.Text();
text.insert(text.length, " beli kopi");
 
// kirim update (diff) ke perangkat lain
const update = Y.encodeStateAsUpdate(doc);
sendToServer(update);
 
// terima update dari perangkat lain
Y.applyUpdate(doc, receivedUpdate);

Kekuatan CRDT: update bisa tiba dalam urutan apa pun dan tetap menghasilkan state akhir yang sama di semua perangkat. Tidak ada server yang menunggu, tidak ada konflik yang harus dipecahkan pengguna — keajaiban matematika yang membuat kolaborasi offline terasa mulus.

Note

Memakai Yjs/Automerge bukan berarti tidak memahami konsepnya — justru sebaliknya. Kalian tetap perlu tahu di mana CRDT dipakai (isi teks) vs di mana LWW cukup (metadata), karena keduanya punya biaya berbeda. Jangan pakai CRDT untuk segala sesuatu.

Mesin Sinkronisasi

Arsitektur Sederhana yang Tahan Gangguan

Mesin sinkronisasi CatatanKu dirancang sebagai background task (episode 9) dengan pola antrean:

100%
Sync engine: pull-push dengan cursor
export class SyncEngine {
  private cursor: number = 0;
 
  async syncOnce() {
    const localChanges = await db.query(
      "SELECT * FROM outbox WHERE version > $1", [this.cursor]
    );
 
    const remoteChanges = await api.pushPull({
      since: this.cursor,          // incremental, bukan full upload
      changes: localChanges,
    });
 
    for (const change of remoteChanges) {
      await this.applyChange(change);      // merge CRDT/LWW
    }
    await this.ackChanges(localChanges);   // kirim terima
  }
}

Pola yang dipakai:

  • Incremental — setiap sync hanya mengirim perubahan sejak cursor terakhir, bukan seluruh database.
  • Idempotent — mengirim/menerima data yang sama dua kali menghasilkan hasil yang sama; aman untuk retry.
  • Resumable — jaringan putus di tengah → lanjut dari cursor terakhir, tidak mulai ulang.
  • Background + throttled — sync berjalan saat idle/online, tidak mengganggu pengguna (episode 9 dan 20).

Konflik Edit Bersamaan di UI

CRDT menangani isi teks; LWW menangani metadata. Untuk pengeditan nyata yang bersamaan (dua orang mengedit catatan yang sama dalam waktu dekat), gabungkan dengan kehadiran kolaboratif (episode 23-24 membahas extensibility; untuk sekarang, cukup tahu bahwa CRDT memberi fondasi yang memungkinkan itu).

Common Pitfalls

  • Timestamp jam dinding untuk konflik — jam perangkat bisa meleset; pakai version clock.
  • Sync full upload — boros bandwidth dan lambat; selalu incremental.
  • Menimpa dengan LWW untuk isi teks — edit paralel hilang; CRDT untuk isi.
  • Tidak idempotent — retry menghasilkan duplikat; pastikan operasi aman diulang.
  • Sync di thread UI — memblokir; selalu background task dengan progres event.

Penutup

Inti yang harus dibawa pulang:

  • Local-first: data lokal kanonik, cloud hanya replika, aplikasi penuh tanpa jaringan.
  • LWW + version clock untuk metadata; CRDT (Yjs/Automerge) untuk isi teks yang diedit paralel.
  • Sync engine: incremental, idempotent, resumable, berjalan di background.
  • Konflik adalah masalah desain — CRDT menyelesaikannya secara matematis, bukan lewat dialog "salinan mana yang mau disimpan?"

Di episode 23 selanjutnya kita membuka CatatanKu untuk ekosistem: plugins & extensibility — arsitektur plugin, extension API, dan SDK untuk pengembang pihak ketiga. Sampai jumpa di episode 23!