Strategi penyimpanan dan state untuk WebAssembly: IndexedDB dan localStorage lewat interop JS di browser, Redis dan object storage via host functions di server, pola stateless dengan external state KV, serta snapshot/rehydrate memory Wasm

Setelah di episode 17 kalian membuka jalur networking — pada episode ini kita membahas salah satu kelemahan klasik Wasm: ia tidak menyimpan apa pun sendiri. Memory linear mati bersama instance; tidak ada disk, tidak ada database. Semua data yang harus bertahan harus disimpan di luar.
Mengapa topik ini penting? Karena hampir setiap aplikasi nyata butuh state: cache hasil komputasi, preferensi user, atau progress pekerjaan panjang. Memahami pola penyimpanan — dari IndexedDB di browser hingga Redis di server — menentukan apakah aplikasi Wasm kalian bisa berfungsi seperti aplikasi native, atau hanya playground sesaat.
Ciri arsitektur yang harus selalu diingat:
Jadi pertanyaan state selalu sama: di mana data disimpan ketika instance mati? Jawabannya tergantung konteks: browser, server, atau edge.
Browser menyediakan dua penyimpanan utama, keduanya diakses lewat interop JS (modul Wasm tidak bisa memanggil IndexedDB langsung):
| Storage | Volume | Cocok untuk | Catatan |
|---|---|---|---|
localStorage | ~5 MB | Preferensi, token kecil | Synchronous, blocking |
| IndexedDB | ~GB | Data besar, cache, blob | Async, transactional |
Pola standar: modul Wasm mengekspor fungsi untuk serialize state, lalu JS menyimpannya:
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
#[derive(Serialize, Deserialize)]
struct Progress {
tahap: String,
persen: u32,
hasil: Vec<f64>,
}
#[wasm_bindgen]
pub fn simpan_state(progress: Progress) -> Result<JsValue, JsValue> {
Ok(serde_wasm_bindgen::to_value(&progress)
.map_err(|e| JsValue::from_str(&e.to_string()))?)
}
#[wasm_bindgen]
pub fn muat_state(json: &JsValue) -> Result<Progress, JsValue> {
serde_wasm_bindgen::from_value(json.clone())
.map_err(|e| JsValue::from_str(&e.to_string()))
}Di JS:
import { simpan_state, muat_state } from "./wasm_state.js"
const db = await openDB("wasm-app", 1, {
upgrade(db) { db.createObjectStore("state"); },
})
// simpan: serialize di wasm, tulis ke IndexedDB
const json = simpan_state({ tahap: "kompresi", persen: 67, hasil: [] })
await db.put("state", JSON.stringify(json), "progress")
// muat: baca dari IndexedDB, parse di wasm
const raw = await db.get("state", "progress")
const progress = muat_state(JSON.parse(raw))Pola ini memakai kekuatan masing-masing: Wasm untuk serialisasi cepat dan deterministik, IndexedDB untuk persistensi volume besar.
Tip
Untuk hasil komputasi besar yang jarang berubah (mis. model ML terkompresi), simpan hasilnya sebagai blob di IndexedDB — bukan menyimpan data mentah lalu menghitung ulang. Load-blob jauh lebih murah daripada komputasi ulang setiap kunjungan.
Di server, persistensi disediakan host melalui host functions — imports yang dibungkus API Wasm yang aman:
package app:kv;
interface kv {
set: func(key: string, value: string) -> result;
get: func(key: string) -> result<string, string>;
del: func(key: string) -> result;
}
world service {
import kv;
}Host mengimplementasikan interface ini — misalnya memakai Redis:
use wasmtime::component::Resource;
use redis::Client;
impl crate::app::kv::Host for MyHost {
async fn set(&mut self, key: String, value: String) -> anyhow::Result<()> {
self.redis.set(&key, &value).await?;
Ok(())
}
async fn get(&mut self, key: String) -> anyhow::Result<Result<String, String>> {
let v: Option<String> = self.redis.get(&key).await?;
Ok(v.ok_or_else(|| "key tidak ada".to_string()))
}
}Modul Wasm tinggal memanggil kv::set/kv::get — ia tidak tahu (dan tidak perlu tahu) apakah di baliknya ada Redis, Memcached, atau S3.
Untuk skala horizontal di edge/serverless, pola yang dianjurkan adalah stateless module + external KV:
Keuntungannya: instance bisa dibuat/hancur kapan pun tanpa kehilangan data, dan skala horizontal tidak perlu sinkronisasi memori antar instance.
Saat koordinasi state perlu deterministik dan real-time — workflow panjang, WebSocket broadcast, counter terdistribusi. Di edge, ini yang ditangani Durable Objects (episode 12); di server, arsitektur actor/single-writer. Jangan menjadikan stateful sebagai default — mulai dari stateless.
Ada kalanya seluruh memory perlu dipertahankan — misalnya menyimpan state aplikasi lengkap antara sesi. Tekniknya: ambil snapshot byte dari memory, simpan sebagai blob, dan rekonstruksi saat dimuat.
async function snapshotMemory(instance, namaSesi) {
const memory = instance.exports.memory
const snapshot = new Uint8Array(memory.buffer) // salin seluruh memory
await db.put("snapshots", snapshot.buffer, namaSesi)
}
async function rehydrateMemory(namaSesi, wasmBytes) {
const snapshot = await db.get("snapshots", namaSesi)
const { instance } = await WebAssembly.instantiate(wasmBytes)
// salin balik ke memory instance baru
const buf = new Uint8Array(instance.exports.memory.buffer)
buf.set(new Uint8Array(snapshot))
return instance
}Peringatan penting: snapshot hanya valid jika modul tidak punya state eksternal (socket, file handle, imported host state) — teknik ini hanya untuk state murni dalam memory linear.
Warning
Snapshot memory tidak menangkap state host: koneksi jaringan, file descriptor, atau variabel global di sisi JS hilang saat rehydrate. Batasi teknik ini untuk state data murni, dan rancang ulang resource eksternal setelah instance baru lahir.
Pada episode 18 ini, kalian telah memahami strategi persistence dan state management untuk modul WebAssembly.
Inti yang harus dibawa pulang:
Di episode 19 selanjutnya kita akan membahas plugin systems & embedding — isolasi plugin dengan V8/SpiderMonkey, model plugin Envoy/extism dan proxy-wasm, aplikasi host + wasm plugin, serta ekosistem Extism dan Kubernetes. Sampai jumpa di episode 19!