Content-addressable storage for binary data like images, audio, and video.
The system addresses a fundamental challenge in web applications: efficiently managing large binary data (images, audio, video) separately from structured object data.
Key problems solved:
SvObjectPool persistence system uses synchronous operations for fast object loading. Binary blobs are large and require async I/O. Separating them prevents blocking.SvCloudBlobNode extends local storage to support pushing/pulling blobs to Google Cloud Storage.Firebase Firestore has constraints that make it unsuitable for storing the complete object graph directly:
The solution: Store structured object data as larger, more self-contained documents locally (IndexedDB), and sync to cloud storage at a higher granularity. Binary blobs are separated out because they're the primary driver of document size, and content-addressable storage means they can be efficiently synced and deduplicated independently.
The solution uses a dual-database architecture with hash-based references between them:
| Component | Purpose | Storage |
|---|---|---|
| SvObjectPool | Stores structured objects (characters, sessions, etc.) | IndexedDB (sync API) |
| SvBlobPool | Stores binary blobs (images, audio, video) | Separate IndexedDB (async API) |
The key insight: Objects don't store blobs directly. Instead, they store a SHA-256 hash that acts as a pointer to the blob in SvBlobPool. This provides:
Three-layer class hierarchy:
Media-specific classes like SvImageNode and SvVideoNode extend SvCloudBlobNode to add format-specific behavior (MIME type handling, thumbnails, etc.) while inheriting all the storage and sync capabilities.
Location: strvct/source/library/node/storage/base/SvBlobPool.js
Core concept: A singleton that provides content-addressable blob storage using IndexedDB.
{sha256-hash} → ArrayBuffer (the actual blob data){sha256-hash}/meta → JSON metadata (contentType, size, timeCreated, customMetadata)SvEnumerableWeakMap for activeBlobs - blobs in memory are cached by hash, but can be garbage collected when not in use.activeReadsMap and activeWritesMap to prevent duplicate concurrent operations on the same hash.asyncCollectUnreferencedKeySet() method removes blobs not in a provided set of referenced hashes.// Store - returns the SHA-256 hash
const hash = await SvBlobPool.shared().asyncStoreBlob(blob, {customMeta: "value"});
// Retrieve
const blob = await SvBlobPool.shared().asyncGetBlob(hash);
// Check existence
const exists = await SvBlobPool.shared().asyncHasBlob(hash);
// Metadata only (without loading blob data)
const meta = await SvBlobPool.shared().asyncGetMetadata(hash);Location: strvct/source/library/node/blobs/SvBlobNode.js
Core concept: A storable node that holds a reference to a blob via its hash, with lazy loading.
valueHash (stored): The SHA-256 hash - persisted with the objectblobValue (transient): The actual Blob object - not persisted, loaded on demandasyncBlobValue() checks if the blob is loaded; if not, fetches it from SvBlobPool using the stored hash.blobValue is set, didUpdateSlotBlobValue() automatically computes the hash and writes to local storage.referencedBlobHashesSet() returns the hash for garbage collection coordination with SvObjectPool.Location: strvct/source/library/node/blobs/SvCloudBlobNode.js
Core concept: Extends SvBlobNode with cloud storage push/pull capabilities.
hasInCloud: Boolean flag indicating cloud presencedownloadUrl: Public/private URL for cloud retrievaldoesAutoSyncToCloud: When true, automatically pushes to cloud after local storageasyncPushToCloud):SvApp.shared().asyncPublicUrlForBlob()downloadUrl and hasInCloud flagasyncPullFromCloudByHash):SvApp.shared().asyncBlobForHash(hash)asyncBlobValue):User uploads image
↓
SvImageNode.asyncSetBlobValue(blob)
↓
SvBlobNode computes SHA-256 hash
↓
SvBlobPool.asyncStoreBlob() stores ArrayBuffer + metadata
↓
SvBlobNode stores only the hash in SvObjectPoolObject loads from SvObjectPool (has valueHash: "abc123...")
↓
asyncBlobValue() called
↓
Check in-memory cache → miss
↓
SvBlobPool.asyncGetBlob("abc123...") → returns Blob
↓
Blob cached in activeBlobs for future accessSince blobs and objects are stored in separate databases, garbage collection requires coordination between SvObjectPool and SvBlobPool to prevent orphaned blobs from accumulating.
referencedBlobHashesSet(), which returns a Set of SHA-256 hashes it depends on.SvObjectPool.allBlobHashesSet() iterates through all stored objects, calling referencedBlobHashesSet() on each, and combines them into a master set of referenced hashes.SvObjectPool.asyncCollectBlobs() passes this master set to SvBlobPool.asyncCollectUnreferencedKeySet(), which:SvObjectPool.asyncCollectBlobs()
↓
SvObjectPool.allBlobHashesSet()
↓
┌───────────────────────────────────────┐
│ for each object in allObjects(): │
│ if object.referencedBlobHashesSet: │
│ hashesSet.addAll(object.referencedBlobHashesSet()) │
└───────────────────────────────────────┘
↓
SvBlobPool.asyncCollectUnreferencedKeySet(hashesSet)
↓
┌───────────────────────────────────────┐
│ allKeys = await idb.promiseAllKeys() │
│ orphans = allKeys.difference(hashesSet) │
│ await idb.promiseRemoveKeySet(orphans) │
└───────────────────────────────────────┘In SvObjectPool (SvObjectPool.js):
allBlobHashesSet () {
const hashesSet = new Set();
this.allObjects().forEach(obj => {
if (obj.referencedBlobHashesSet) {
const objBlobHashes = obj.referencedBlobHashesSet();
hashesSet.addAll(objBlobHashes);
}
});
return hashesSet;
}
async asyncCollectBlobs () {
const keySet = this.allBlobHashesSet();
const removedCount = await this.blobPool().asyncCollectUnreferencedKeySet(keySet);
return removedCount;
}In SvBlobNode (SvBlobNode.js):
referencedBlobHashesSet () {
const hashesSet = new Set();
const hash = this.valueHash();
if (hash) {
hashesSet.add(hash);
}
return hashesSet;
}/meta key is also deleted