Local Object Pools

← Persistence
STRVCT

IndexedDB-backed persistence for domain object graphs using dirty tracking, the local record store (SvLocalRecordStore), and atomic event-loop commits.

Overview

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 Node Graph (SvNode) SvStorableNode Slot setters trigger didUpdateSlot & mark object dirty SvObjectPool In-memory dirty cache; serializes dirty nodes to SvRecordRow SvLocalRecordStore Row database holding all pool records (objectId == poolId) SvPersistentAtomicMap & SvIndexedDbFolder Synchronous Map cache; batched writes on commit IndexedDB (Browser) / LevelDB (Node.js Headless)

Opting Into Persistence

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());

Storage Flags Breakdown:

  • 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).

Inheritance Chain

Storage capabilities begin at SvStorableNode in the core node hierarchy:

SvNode -> SvTitledNode -> SvInspectableNode -> SvViewableNode -> SvStyledNode -> SvStorableNode

Classes above SvStorableNode cannot be persisted. Application domain models extend SvStorableNode or SvSummaryNode.

Persistent Unique IDs (`puuid`)

Every stored object has a puuid—a 10-character random string generated via crypto.getRandomValues() and stored as _puuid.

Puuids serve two key roles:

  1. Record Row Key: Combined with poolId, the puuid forms the object ID key in storage (objectId). For pool root objects, objectId == poolId.
  2. Object Pointers:
    • Near References ({ "*": "puuid" }): References between objects within the same pool store local puuid pointers.
    • Far References ({ "**": "poolId" }): Pointers across pool boundaries store external pool IDs.

Record Serialization & Local-Only Slots

Domain objects serialize to JSON record rows via recordForStore(aStore):

{
    "type": "Task",
    "entries": [
        ["title", "Buy Groceries"],
        ["score", 10],
        ["assignee", { "*": "u-99120" }]
    ]
}

Local-Only Slots (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.

Mutation Tracking & Batched Commits

  1. Setter Invoked: Setting a slot value calls didUpdateSlot().
  2. Dirty Registration: didUpdateSlot() calls didMutate(), notifying the SvObjectPool via a mutation observer.
  3. Coalesced Commit Pass: SvObjectPool adds the object to its dirty set and schedules a deferred commit pass with SvSyncScheduler.
  4. Atomic Transaction: At the end of the browser event loop, commitStoreDirtyObjects() serializes all dirty objects and writes them to IndexedDB in a single atomic transaction.

`SvPersistentAtomicMap` & Multi-Backend Abstraction

SvPersistentAtomicMap wraps SvIndexedDbFolder with a synchronous in-memory Map cache:

  • Synchronous Reads: Reads are served instantly from the in-memory cache without async IndexedDB latency.
  • Batched Writes: Writes modify the in-memory write cache and are flushed to disk in atomic transactions during commit passes.
  • Environment Abstraction (SvIndexedDbFolder): Uses native IndexedDB in the browser and LevelDB (classic-level) in Node.js, providing identical storage behavior for headless testing and browser execution.

Opening Pools & Class Migrations (`ClassRenames.json`)

The application opens local object pools at boot:

const pool = SvPersistentObjectPool.sharedPool();
await pool.promiseOpen();
const root = await pool.rootOrIfAbsentFromClosure(() => {
    return TaskRootNode.clone();
});

Class Migrations:

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"
}