Local Storage Persistence with Data Blocks
To persist configuration details, offline drafts, or user options across browser refreshes, the Aircada SDK provides local storage integration.
Rather than accessing browser APIs like localStorage or indexedDB directly, Aircada encapsulates this logic in a built-in DataBlockOperator with dataStoreType: "LOCAL_STORAGE". Under the hood, this operator relies on indexedDB to handle robust, asynchronous database storage.
Key Principles of Local Data Persistence
1. Store Integration: Local storage should operate in tandem with the Aircada Store. On system setup, load stored data and patch the global store. On store changes or trigger events, write the current store state back to the local storage operator.
2. Handling Flat Storage Mappings: The local storage engine works best with flat key-value records. Nested states must be flattened upon saving, and unflattened when loaded back into the store.
3. Local Media Storage: Media items configured with PropertySystemType.MEDIA_ITEM are automatically handled by Aircada's underlying media vault, storing them offline in indexedDB blob storage and restoring them automatically.
Local Data Manager System Example#
// src/systems/LocalDataManager.ts
import {
System, Context, Setup, Input, Dispose,
AircadaContext,
DataBlockOperator,
PropertySystemType
} from "@aircada/spec";
@System({
name: "Local Data Manager",
type: "local_data_manager"
})
export class LocalDataManager {
@Context()
air!: AircadaContext;
private operator!: DataBlockOperator;
private isApplyingLoad = false;
@Setup()
async setup() {
console.log("[LocalDataManager] Setup initializing...");
const defaultConfig = this.air.store.state.testData || {
id: "random_id", // In this example, we'll be storing with a static constant definedKey string (below)
mediaItemId: "",
floatValue: 0.5,
stringValue: "Default String",
details: {
description: "Default Description"
}
};
try {
// Create separate configuration and default data objects first, then merge them.
const operatorConfig = {
definedKey: "local_saved_state", // this will override our ID value - allowing passing id, but storing in a constant, defined location
dataStoreType: "LOCAL_STORAGE",
tableName: "test_data_table",
saveType: "KEY",
loadType: "KEY"
};
const defaultData = {
id: defaultConfig.id,
// To benefit from Aircada's underlying media management system we must specify the system type and value
// Media saved with this system is automatically stored in indexedDB blob storage and loaded automatically
mediaItemId: { systemType: PropertySystemType.MEDIA_ITEM, value: defaultConfig.mediaItemId },
floatValue: defaultConfig.floatValue,
stringValue: defaultConfig.stringValue,
// Flattening nested string property for local storage
details_description: defaultConfig.details?.description || "Default Description"
};
// Instantiate built-in DATA_BLOCK operator by merging properties
this.operator = await this.air.operators.create("DATA_BLOCK", {
// Operator's inbuilt properties
...operatorConfig,
// Our custom data object properties
...defaultData
});
if (!this.operator) {
console.error("[LocalDataManager] Failed to create DataBlockOperator.");
return;
}
console.log("[LocalDataManager] DataBlockOperator created successfully.");
// Listen for loaded data from indexedDB
this.operator.loadedData.addListener((data: Record<string, any>) => {
console.log("[LocalDataManager] Loaded data block:", data);
if (data) {
this.isApplyingLoad = true;
this.air.store.patch({
testData: {
id: data.id ?? "",
// The actual Media data will have already been imported by this point and associated with this mediaItemId
mediaItemId: data.mediaItemId ?? "",
floatValue: Number(data.floatValue ?? 0.5),
stringValue: data.stringValue ?? "",
// Unflattening nested string property from local storage
details: {
description: data.details_description ?? ""
}
}
});
this.isApplyingLoad = false;
}
});
this.operator.onSuccess.addListener(() => {
console.log("[LocalDataManager] DataBlockOperator operation succeeded.");
});
this.operator.onFailure.addListener(() => {
console.error("[LocalDataManager] DataBlockOperator operation failed.");
});
// Trigger a load operation to see if data already exists in indexedDB
console.log("[LocalDataManager] Loading data from storage...");
this.operator.load();
} catch (err) {
console.error("[LocalDataManager] Error during DataBlockOperator setup:", err);
}
}
@Input({ name: "On Trigger Save Event", onEvent: "TRIGGER_SAVE" })
onTriggerSave() {
if (!this.operator || this.isApplyingLoad) return;
const current = this.air.store.state.testData;
console.log("[LocalDataManager] Saving state to indexedDB:", current);
this.operator.dataInput({
id: current.id,
mediaItemId: current.mediaItemId,
floatValue: current.floatValue,
stringValue: current.stringValue,
// Flattening nested string property for local saving
details_description: current.details.description
});
// Trigger the save() operation on the operator
this.operator.save();
}
}





