Cloud persistence for object pools: one record per row, a version per pool, and an atomic commit protocol.
Every cloud document—a game session, a character sheet, a campaign catalog—is an Object Pool: a cyclic graph of records reachable from a single root record (objectId == poolId).
Locally, pool rows live in SvLocalRecordStore (IndexedDB). In the cloud, the exact same rows live in a cloud records collection (SvCloudRecordStore). There is no second format: a single mapper converts objects to rows on both sides, making cloud sync a simple operation of reading rows from one store and writing them into the other.
puuid (objectId == poolId).version, state, parentId, orderKey, and owner.parentId references without storing explicit child lists.{ "**": poolId }): The only way to point across pool boundaries. Following a far reference is asynchronous.In the cloud, a pool is identified by its scoped ID:
<scopeId>:<localPoolId>scopeId: The sharing boundary (e.g., user account ID, multiplayer room ID, public catalog ID).localPoolId: The root object's puuid string.Scoped pool IDs ensure that two accounts cloning the same template document never collide in cloud storage.
| Column | Root Row (objectId == poolId) | Child Row (objectId != poolId) | Written By |
|---|---|---|---|
poolId | The root object's puuid (identifies the pool) | The enclosing pool's poolId | Client |
objectId | The root object's puuid (objectId == poolId) | The child object's own distinct puuid (objectId != poolId) | Client |
parentId, orderKey | Folder ID & fractional sort key | Sort key for windowed collections | Client |
payloadJson | Root object's slot record | Child object's slot record | Client (commit payload) |
version | Monotonic content sequence number | — | Server (mirrored down) |
modifiedVersion, isDeleted | Version that last updated row | Last modified version; tombstone flag | Server (mirrored down) |
scopeId, state, owner | Sharing boundary & lifecycle state | scopeId only | Server |
Identity Rule: For the Root Row,
objectIdis identical topoolId(objectId == poolId), identifying the entire document pool and carrying server-owned metadata (version,state,owner). For Child Rows,objectIdis the child object's own uniquepuuid, whilepoolIdidentifies the parent pool it belongs to (objectId != poolId).
A stored slot can be kept exclusively on the device: slot.setIsInCloudRecord(false) keeps it in the local IndexedDB record row but strips it when generating cloud commit payloads (SvObjectPool.cloudPayloadFor). Local view settings, scroll positions, and device sync timestamps use this mechanism to prevent triggering unnecessary cloud commits.
Every backing implementation conforms to SvRecordStoreProtocol:
| Method | Answers / Action |
|---|---|
asyncOpen(poolId) | { root, records, version, state } — the entire pool, or null. |
asyncReadChanges(poolId, sinceVersion) | { rows, tombstones, version, reloadRequired } — modified rows since version. |
asyncChildren(nodeId, range) | Rows placed under nodeId ordered by (orderKey, objectId). |
asyncResolveFar(poolId) | Opens the pool a far reference points to. |
asyncCommit({ poolId, baseVersion, requestId, writes, deletes }) | Atomic commit: returns { status: "committed", version }, "conflict", or "refused". |
SvLocalRecordStore: The device's IndexedDB store holding every local pool row. Accepts asyncPut / asyncDelete.SvMemoryRecordStore: Reference implementation of both sides (including commit protocol) used for headless testing and in-memory cloud caches.SvCloudRecordStore: The cloud backing executing callFunction(name, args) over HTTPS. Refuses direct writes; modified only via asyncCommit.When opening a document, Strvct uses an optimized decision tree to minimize network bandwidth:
[ User Opens Document ]
|
v
Has Local In-Sync Version? (inSyncVersionOfPool)
/ \
YES / \ NO
v v
asyncReadChanges(sinceVersion) asyncOpen(poolId)
/ \ (Fetch Whole Pool)
Normal / \ reloadRequired=true
v v
Import Deltas Replace Local Copy
(0 to 1 row download) (Cloud Wins)SvLocalRecordStore.inSyncVersionOfPool(poolId). If a local copy exists with no unsent local edits, it calls asyncReadChanges(cloudPoolId, version). For returning users, this transfers 0 to 1 rows instead of re-downloading the entire document.asyncOpen(poolId) and imports the full pool (asyncImportOpenedPool).reloadRequired Handling: If the server has pruned tombstones since sinceVersion, it returns reloadRequired: true, falling back to a full pool download.version, ensuring a commit currently being written is never seen half-finished.Edits are saved locally first at the end of the event loop. Sending them to the cloud is a separate, debounced step: SvObjectPool.asyncCommitToCloud():
cloudChanges()).version as baseVersion and a unique requestId.requestId. If a network interruption causes the client to retry the request, the server returns the saved receipt without double-incrementing the pool version.committed: The server's new version is stamped on local rows, and the snapshot advances.conflict: Another client committed first. The caller invokes holdsCommitContent(commit) to check if its edits were included before reloading remote changes (cloud wins).refused: The commit violated a rule (e.g. attempting to edit server-owned columns).For commits exceeding transaction limits (maxOpsPerCommit() 450 operations or 4 MB of payload), SvStagedRecordCommit stages the write:
While a stage is open, the pool answers busy to other readers/writers, and SvCloudRecordStore retries automatically (busyRetryDelays). If a stage is abandoned mid-way, the next access to the pool rolls back incomplete batches from saved pre-images.
Folders (SvCloudFolder) declare setSubnodesArePools(true). A folder never stores a list of child IDs:
parentId to the folder's ID.asyncScopeRootRows(scopeId). Each root row contains enough metadata (title, subtitle, thumbnail hash) to render placeholder tiles without opening child pool bodies.pendingCloudDeletes) that survives reloads until confirmed by the server.A pointer across pool boundaries is stored as { "**": poolId }, naming the target pool root rather than an individual object. It keeps nothing alive and grants no permissions: resolving it opens the target pool under the target's own permissions. A dangling far reference is a normal state to display in the UI rather than an error.
| Class | Purpose |
|---|---|
SvObjectPool | Manages live objects for a document; handles asyncCommitToCloud & cloudChanges. |
SvLocalRecordStore | Manages local IndexedDB rows (inSyncVersionOfPool, asyncImportOpenedPool). |
SvCloudRecordStore | Manages cloud record reads, commits, staged commits, and scope root listings. |
SvMemoryRecordStore | Reference implementation of the record store protocol for headless tests. |
SvStagedRecordCommit | Drives multi-batch commits for large payload transactions. |
SvRecordRow | Defines the serialized row shape and column constraints. |
SvCloudFolder | Manages document collections via root parentId references and scope listings. |
SvOrderKey | Generates fractional sort keys for document order in collections. |