How objects are stored, loaded, and kept in sync with IndexedDB.
Persistence in STRVCT is automatic. Objects marked as storable are tracked for changes, serialized at the end of each event loop, and written to IndexedDB. On reload, they're deserialized and re-initialized through the same three-phase lifecycle as new objects. The goal is transparency — a class shouldn't need different code paths for "new" and "loaded."
When a storable slot changes, the framework automatically marks the object as dirty:
didMutate() is called from the slot's didUpdateSlot() hook.recordForStore(store)
├── Serialize slots marked with setShouldStoreSlot(true)
├── Convert object references to puuids
└── Return JSON-compatible recordOnly slots explicitly marked with setShouldStoreSlot(true) are persisted. Object references are stored as persistent unique IDs (puuids) rather than direct pointers, so the full object graph can be reconstructed on load.
Loading reverses the process in three steps:
instanceFromRecordInStore(record, store)
├── Create blank instance
├── Call init()
└── Return for populationloadFromRecord(record, store)
├── Restore slot values
├── Resolve object references via puuids
└── Skip slots with setFinalInitProto() (they'll be handled next)finalInit() re-establishes complex relationships. Slots configured with setFinalInitProto() only create default instances if the slot wasn't already populated from the store — this is the key mechanism that makes the three-phase lifecycle work for both new and loaded objects.
When you need initialization logic that only runs for newly created objects (not loaded ones):
finalInit () {
super.finalInit();
if (!this.hasStoredData()) {
this.randomizeValues();
}
}didLoadFromStore(store) — Called after all objects have been loaded and initialized. Safe to access the full object graph.willStore() — Called just before serialization. Optional hook for pre-save cleanup.shutdown () {
this.stopWatchingNode();
this.subnodes().forEach(node => node.shutdown());
this.removeFromParent();
}