How the OpenMAIC Asset Pool Architecture Works with AssetIds and Content Hashes
OpenMAIC uses a browser-based asset pool backed by IndexedDB where opaque AssetId identifiers are allocated via toAssetId while content hashes enable duplicate detection and deduplication.
The OpenMAIC framework (THU-MAIC/OpenMAIC) manages all user-generated media—audio, video, images, and recordings—through a unified asset pool abstraction. This architecture separates the concerns of unique identification (via allocated IDs) from content verification (via cryptographic hashes), enabling efficient storage, cross-tab coordination, and intelligent cache management.
Asset Pool Foundation: Lazy Initialization and IndexedDB Storage
The asset pool is instantiated lazily through getAssetPool() in [lib/media/asset-pool.ts](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/media/asset-pool.ts). When first called, this function creates a BrowserAssetStore backed by an IndexedDB database named maic-asset-pool.
// Lazy pool creation pattern
import { getAssetPool } from '@/lib/media/asset-pool';
// First call initializes the IndexedDB store
const pool = getAssetPool();
// Subsequent calls return the same singleton instance
The pool implements the AssetPoolStore interface defined in [lib/media/asset-pool-config.ts](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/media/asset-pool-config.ts), providing three core operations:
put(blob)— Stores a blob and returns anAssetIdresolve(assetId)— Returns an Object URL for renderingremove(assetId)— Deletes the asset from storage
AssetId Generation: Opaque Identifiers via toAssetId
Asset IDs in OpenMAIC are allocated, not derived. The toAssetId helper from the @openmaic/storage package generates opaque identifiers that serve as primary keys in IndexedDB.
Based on the package structure, the ID generation logic resides in:
- Path:
packages/@openmaic/storage/src/asset/id.ts - Function:
toAssetId(input: unknown): AssetId
// Conceptual usage based on architecture
import { toAssetId } from '@openmaic/storage';
// Generates a unique, opaque identifier
const assetId = toAssetId(someInput);
// Result: "asset-12345" or similar UUID-derived string
Key characteristics of AssetIds:
- Opaque — No semantic meaning embedded in the ID itself
- Unique — Guaranteed unique across browser sessions via allocation strategy
- Persistent — Survives page reloads because stored as IndexedDB primary keys
- Not content-derived — Same blob added twice receives different IDs unless deduplication intervenes
Content Hashes: SHA-256 for Duplicate Detection
While AssetIds handle uniqueness, content hashes (typically SHA-256) enable deterministic duplicate detection. The hash is computed once during blob ingestion and stored as asset metadata.
Hash Computation and Storage
When BrowserAssetStore.put() processes a new blob:
- Compute content hash of the blob bytes
- Generate opaque
AssetIdviatoAssetId - Store tuple:
(AssetId, contentHash, blobData)in IndexedDB
The hash lives in metadata, not as the primary key, allowing flexible deduplication strategies.
Deduplication in Practice
Higher-level collectors implement hash-based reuse. The test suite in [tests/video-export/collect.test.ts](https://github.com/THU-MAIC/OpenMAIC/blob/main/tests/video-export/collect.test.ts) demonstrates this pattern:
// Simplified deduplication workflow
import { hashBlob } from '@/lib/utils/hash';
async function storeWithDeduplication(newBlob: Blob) {
const hash = await hashBlob(newBlob);
// Query pool for existing hash match
const existing = await getAssetPool().findByHash(hash);
if (existing) {
// Reuse existing asset—saves storage
return existing.id;
}
// No match—store as new asset
return await getAssetPool().put(newBlob);
}
Complete Asset Lifecycle
| Phase | Operation | Implementation |
|---|---|---|
| Initialization | Create/retrieve pool | getAssetPool() → BrowserAssetStore constructor |
| Ingestion | Add blob with ID + hash | pool.put() → toAssetId() + hash computation |
| Resolution | Get renderable URL | pool.resolve() → blob: Object URL |
| Deduplication | Reuse by hash match | Collector-level hash comparison |
| Eviction | Remove single asset | pool.remove(assetId) |
| Clearing | Destroy entire pool | clearAssetPool() with deferred deletion handling |
Cross-Tab Coordination and Asset Replacement
The pool architecture includes mechanisms for concurrent editing scenarios:
observeAssetReplacements— Invalidates URL cache when assets are replacedbindAssetReplacementChannel— Presence channel for probing replacement safety across tabs
These features ensure that when one tab updates an asset, other tabs automatically reload the corrected version rather than serving stale cached URLs.
Code Examples
Basic Asset Storage and Retrieval
import { getAssetPool } from '@/lib/media/asset-pool';
async function handleRecording(blob: Blob) {
// Store and receive opaque ID
const assetId = await getAssetPool().put(blob);
console.log('Stored as:', assetId); // e.g., "asset-a1b2c3d4"
// Resolve for rendering
const objectUrl = await getAssetPool().resolve(assetId);
const videoElement = document.createElement('video');
videoElement.src = objectUrl;
return { assetId, objectUrl };
}
Cache Management Workflow
import { clearAssetPool } from '@/lib/media/asset-pool';
async function clearAllCache() {
try {
await clearAssetPool();
console.log('Asset pool cleared');
} catch (error) {
if (error.name === 'AssetPoolDeletionDeferredError') {
// Pool in use elsewhere—retry scheduled
console.log('Deletion deferred');
}
throw error;
}
}
React Hook for Asset URLs
// From lib/media/use-asset-url.ts
import { useAssetUrl } from '@/lib/media/use-asset-url';
function VideoPlayer({ assetId }: { assetId: string }) {
const { url, loading, error } = useAssetUrl(assetId);
if (loading) return <Spinner />;
if (error) return <ErrorMessage />;
return <video src={url} controls />;
}
Summary
- AssetIds are allocated via
toAssetIdin@openmaic/storage, providing opaque, unique identifiers for IndexedDB primary keys - Content hashes are computed separately (SHA-256) and stored as metadata to enable duplicate detection without conflating identity with content
- The asset pool (
getAssetPool()/BrowserAssetStore) abstracts IndexedDB operations behind a cleanput/resolve/removeinterface - Deduplication happens at the collector level, checking hash matches before storing new blobs
- Cross-tab coordination ensures asset replacements propagate correctly via presence channels and replacement observers
Frequently Asked Questions
What is the difference between an AssetId and a content hash in OpenMAIC?
An AssetId is an opaque, allocated string generated by toAssetId that uniquely identifies a stored asset in IndexedDB. A content hash is a deterministic SHA-256 digest computed from the blob's bytes. The ID provides a stable reference for the stored object, while the hash enables detecting whether two blobs contain identical content.
Why doesn't OpenMAIC use the content hash as the primary key?
Using allocated IDs rather than content hashes as primary keys allows the same logical content to exist with multiple identities when needed (different metadata, processing pipelines, or ownership contexts). It also decouples storage addressing from content validation, letting the system evolve hash algorithms or support hash-agnostic operations without migrating primary keys.
How does the asset pool handle storage when the same file is uploaded twice?
Without deduplication logic, the second upload receives a new AssetId and stores a duplicate copy. With deduplication enabled (as in video export collectors), the system computes the hash, finds the existing match via findByHash, and returns the original AssetId instead of storing redundant data.
What happens to AssetIds when the browser cache is cleared?
clearAssetPool() closes the IndexedDB connection and deletes the entire maic-asset-pool database. All AssetId-to-blob mappings are irrevocably lost. Applications using OpenMAIC must handle AssetPoolDeletionDeferredError when the pool is locked by another tab, scheduling retry attempts until the operation succeeds.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →