10 September 20268 menit
Dua minggu setelah insiden PolinRider ditutup dengan v0.4.2 yang bersih, saya balik lagi duduk di depan layar dengan satu tujuan sederhana. Nulis kode yang seharusnya udah jadi tiga bulan lalu.
Syncraft Labs versi 0.3.0 dulu punya constraint TypeScript yang sengaja ngeblokir Map dan Set. Alasannya masuk akal, soalnya keduanya nggak bisa diserialisasi ke IndexedDB. Tapi constraint itu juga berarti siapa pun yang butuh koleksi di dalam state harus nulis sendiri cara konversinya. Itu beban yang mestinya nggak perlu ada di library yang ngklaim mendukung data structures JavaScript secara natural.
Pas saya mulai ngerjain v0.5.0, saya nggak nyangka rilis ini bakal menarik perhatian dari luar ranah teknis. Ada email masuk, dan ceritanya ada di bagian akhir artikel ini. Tapi pertama, soal kode dulu.
Map & Set akhirnya masuk
Masalah utamanya simpel. Draft Proxy harus bisa ngelacak perubahan di dalam Map dan Set, lalu ngubahnya jadi patch yang bisa dikirim ke server. Tapi Map dan Set bukan plain object. Mereka punya method kayak set, delete, clear, add, dan semuanya harus ditangkep sebagai operasi patch yang bener.
Solusinya pake path convention baru. Setiap entri di dalam Map diakses lewat path $entries, dan values diakses lewat $values. Kunci dibatasi pada string | number, biar path-nya bisa direpresentasikan sebagai JSON Pointer.
// Di dalam produce, Map dan Set sekarang bisa dimutasi langsung
await store.set((draft) => {
draft.scores.set("alice", 100);
draft.tags.delete("deprecated");
draft.activeUsers.add("new-user");
});
Yang menarik, hybridClone sekarang jaga Date, Map, dan Set tetep hidup pas applyPatches dijalankan. Sebelumnya, objek-objek itu kehilangan type-nya dan berubah jadi plain object. Perubahan kecil, tapi dampaknya besar buat siapa pun yang bergantung pada method get() atau size dari koleksi mereka.
validateStateShape - jebakan yang disengaja
Sebelum v0.5.0, kalau kamu nyuntik Date ke dalam state, nggak ada yang nolak di waktu build. Compilernya senyum. TypeScript-nya aman. Tapi begitu data masuk ke IndexedDB, Date nggak bisa diserialisasi dengan bener. Error-nya muncul di runtime, jauh setelah kode kamu jalan, dan biasanya tepat pas user lagi pake aplikasi.
Sekarang ada validateStateShape() dan isUnsupportedType(). Keduanya aktif cuma di development mode, jadi nol overhead di production. Begitu ketemu properti dengan type yang nggak didukung, error-nya langsung nunjukin property path dan constructor name.
// Development mode: TypeError dengan path yang jelas
// "Unsupported type Date at /user/createdAt"
// Production: zero cost, validation dimatikan
Ini pola yang sama dengan Object.freeze() di v0.4.0. Kesalahan konfigurasi harus meledak di waktu development, bukan bocor diam-diam di production. Bedanya, kali ini pesannya lebih spesifik dan langsung nyebut path-nya.
Adapter yang belajar dari kesalahan
Empat issue dibuka dalam waktu berdekatan, dan semuanya soal hal yang sama, yaitu adapter. Issue #38 laporin kalau komponen yang berbagi store key menjalankan hydration dan fetcher secara terpisah. Issue #39 soal race condition di compactOutbox yang bisa baca outbox versi lama. Issue #40 soal kontrak error yang nggak konsisten antara update() dan refetch(). Issue #41 soal useSyncSuspense yang lempar error hydration berulang kali ke Error Boundary.
Keempatnya diperbaiki dalam satu pull request besar. Perubahan intinya adalah StoreController sekarang singleton per store key. Satu controller, satu hydration loop, satu fetcher loop, nggak peduli berapa komponen yang pake store yang sama.
// Sebelum: 3 komponen = 3 hydration loops
// Sesudah: 3 komponen = 1 hydration loop, 3 subscriber
Soal error, kontraknya sekarang jelas. update() fire-and-forget, error-nya masuk ke state error. refetch() throw, error-nya bisa ditangkep di luar. Dan SyncraftError jadi class khusus yang menormalisasi semua error jadi satu format, lengkap dengan source tag.
Property-based testing untuk keyakinan
Saya nggak pernah yakin 100% bahwa proxy engine saya menghasilkan patch yang bener. Ada ratusan jalur mutasi, dan nulis test case satu per satu untuk semua kombinasi itu nggak praktis.
Jadi di v0.5.0 saya nambahin property-based test suite pake fast-check. Prinsipnya sederhana, yaitu untuk state acak dan mutasi acak, applyPatches(base, patches) === nextState harus selalu benar. Plus, inverse patches harus bisa balikin state ke kondisi semula.
// 1,000 iterasi per properties × 5 properties = 5,000 assertions per run
fc.assert(
fc.property(stateArb, mutationsArb, (base, mutations) => {
const [next, patches] = produceWithPatches(base, mutations);
const applied = applyPatches(base, patches);
expect(applied).toEqual(next);
})
);
Selama 1.000 iterasi per property, suite ini nangkep dua bug yang sebelumnya nggak terdeteksi, yaitu parent propagation yang gagal pas mutasi berulang ke path yang sama, dan guard copy yang hilang untuk Map dan Set. Dua bug kecil yang kalau lolos ke production bisa bikin data corrupt diam-diam.
Spam email dan tanda-tanda
Tiga hari setelah v0.5.0 rilis, ada email masuk. Isinya tawaran promosi untuk Syncraft Labs. Bukan dari pengguna, bukan dari kontributor, tapi dari seseorang yang nawarin jasa "exposure" untuk open source project.
Yang menarik bukan tawarannya, tapi gimana saya nafsirinnya. Syncraft Labs baru berumur dua bulan. Jumlah star-nya masih bisa dibilang belum ada. Tapi seseorang udah ngeliatnya sebagai target promosi. Itu artinya project ini mulai keliatan di permukaan, setidaknya cukup buat algoritma atau pencarian manual nemuinnya.
Saya tolak tawarannya. Bukan karena promosi itu buruk, tapi karena cara terbaik buat bangun visibilitas adalah lewat dokumentasi yang jujur, rilis yang konsisten, dan percakapan langsung dengan pengguna. Spam email yang nawarin "exposure" adalah tanda bahwa kamu udah cukup besar buat diperhatiin, tapi belum cukup besar buat milih siapa yang ngomong sama kamu.
Dan itu sebenarnya tanda yang bagus.
Pelajaran yang saya bawa
- Constraint yang ketat di awal harus punya jalan keluar. Ngeblokir
MapdanSetdi v0.3.0 keputusan yang bener saat itu, tapi tanpa path buat membukanya nanti, itu jadi jebakan buat pengguna. - Singleton per key bukan optimasi, tapi correctness. Adapter yang jalanin hydration duplikat itu bukan cuma boros resource, tapi juga sumber race condition yang susah dilacak.
- Property-based testing nemuin apa yang case-based testing lewatin. Satu test case yang bener untuk satu input nggak jamin seribu input lain juga bener.
- Spam email yang nawarin promosi adalah tanda pertumbuhan yang nggak diminta. Bukan berarti kamu udah besar, tapi berarti kamu udah keliatan.
- Development-mode validation harus spesifik. Error yang bilang "unsupported type" kurang berguna. Error yang bilang "unsupported type Date at /user/createdAt" langsung nunjukin ke mana harus melihat.
Penutup
Dari tiga paket npm yang diam-diam dipake untuk project internal, sampai rilis yang punya adapter reliability, property-based testing, dan spam email yang nawarin promosi. Syncraft Labs masih di Milestone 1, dan v0.5.0 adalah langkah kecil yang pasti ke arah yang bener.
Kalau kamu lagi bangun local-first application dan butuh sync engine yang nggak ngorbanin integritas data, atau sekadar pengen diskusi soal adapter design, kabari saya. Kotak masuk email saya selalu terbuka, asalkan bukan spam promosi.