IndexedDB-backed persistence for domain object graphs using dirty tracking, the local record store (SvLocalRecordStore), and atomic event-loop commits.
Strvct's persistence engine is local-first: application state is stored directly in the browser's IndexedDB (or LevelDB under Node.js). Rather than requiring developers to write explicit SQL/ORM queries or API save calls, the framework monitors slot mutations automatically, serializes dirty domain objects into Record Rows (SvRecordRow), and commits them to IndexedDB at the end of each browser event loop.
Domain classes opt into local persistence by extending SvStorableNode and configuring storage flags:
(class Task extends SvStorableNode {
initPrototypeSlots () {
{
const slot = this.newSlot("title", "");
slot.setSlotType("String");
slot.setShouldStoreSlot(true); // Marks slot value for serialization
}
{
const slot = this.newSlot("score", 0);
slot.setSlotType("Number");
slot.setShouldStoreSlot(true);
}
{
const slot = this.newSlot("transientCache", null);
// Not stored — recomputed at runtime
}
}
initPrototype () {
this.setShouldStore(true); // Enables persistence for this node
this.setShouldStoreSubnodes(true); // Persists child subnodes array
}
}.initThisClass());setShouldStore(true): Called in initPrototype(). Enables persistence for instances of the node.setShouldStoreSlot(true): Called in initPrototypeSlots(). Includes individual slot values in serialized JSON record rows.setShouldStoreSubnodes(true): Persists the node's subnodes array. When false, subnodes are re-created dynamically on load (typically via setFinalInitProto() on setIsSubnodeField(true) slots).Storage capabilities begin at SvStorableNode in the core node hierarchy:
SvNode -> SvTitledNode -> SvInspectableNode -> SvViewableNode -> SvStyledNode -> SvStorableNodeClasses above SvStorableNode cannot be persisted. Application domain models extend SvStorableNode or SvSummaryNode.
Every stored object has a puuid—a 10-character random string generated via crypto.getRandomValues() and stored as _puuid.
Puuids serve two key roles:
poolId, the puuid forms the object ID key in storage (objectId). For pool root objects, objectId == poolId.{ "*": "puuid" }): References between objects within the same pool store local puuid pointers.{ "**": "poolId" }): Pointers across pool boundaries store external pool IDs.Domain objects serialize to JSON record rows via recordForStore(aStore):
{
"type": "Task",
"entries": [
["title", "Buy Groceries"],
["score", 10],
["assignee", { "*": "u-99120" }]
]
}slot.setIsInCloudRecord(false)):A slot can be kept on the local device without being included in cloud sync commits:
slot.setIsInCloudRecord(false);Local-only slots are saved into local IndexedDB rows but stripped when generating cloud commit payloads (SvObjectPool.cloudPayloadFor). Local view states, scroll offsets, and sync timers use local-only slots.
didUpdateSlot().didUpdateSlot() calls didMutate(), notifying the SvObjectPool via a mutation observer.SvObjectPool adds the object to its dirty set and schedules a deferred commit pass with SvSyncScheduler.commitStoreDirtyObjects() serializes all dirty objects and writes them to IndexedDB in a single atomic transaction.SvPersistentAtomicMap wraps SvIndexedDbFolder with a synchronous in-memory Map cache:
SvIndexedDbFolder): Uses native IndexedDB in the browser and LevelDB (classic-level) in Node.js, providing identical storage behavior for headless testing and browser execution.The application opens local object pools at boot:
const pool = SvPersistentObjectPool.sharedPool();
await pool.promiseOpen();
const root = await pool.rootOrIfAbsentFromClosure(() => {
return TaskRootNode.clone();
});When refactoring domain class names in code, existing stored records contain the old class name. Strvct maps old class names to new constructors via ClassRenames.json:
{
"OldTaskNode": "Task"
}