IndexedDB Persistence Layer
Overview
The Dynamic JSON Visualizer uses IndexedDB, a browser-based NoSQL database, to store data locally on the user's device. This enables offline functionality, data persistence between sessions, and eliminates the need for a backend server. The persistence layer manages JSON objects, flag types, and application configuration.
Database Architecture
Database Structure
const DB_NAME = 'jsonVisualizerDB';
const DB_VERSION = 1;
const OBJECT_STORE = 'jsonObjects';
const FLAGS_STORE = 'flagTypes';
const CONFIG_STORE = 'appConfig';
The database contains three object stores (similar to tables in SQL databases):
jsonObjects
- Stores the actual JSON data with unique IDsflagTypes
- Stores custom flag definitions (name, color, icon)appConfig
- Stores application settings and user preferences
Database Initialization (initDB
)
export const initDB = async () => {
const db = await openDB(DB_NAME, DB_VERSION, {
upgrade(db) {
if (!db.objectStoreNames.contains(OBJECT_STORE)) {
db.createObjectStore(OBJECT_STORE, { keyPath: '__id' });
}
if (!db.objectStoreNames.contains(FLAGS_STORE)) {
db.createObjectStore(FLAGS_STORE, { keyPath: 'id' });
}
if (!db.objectStoreNames.contains(CONFIG_STORE)) {
db.createObjectStore(CONFIG_STORE, { keyPath: 'key' });
}
},
});
return db;
};
How database initialization works:
- Opens database - Uses the
idb
library wrapper around native IndexedDB - Version control - Database version determines when upgrades run
- Upgrade function - Only runs when database is first created or version increases
- Object store creation - Creates the three stores with their primary keys:
jsonObjects
uses__id
as the primary keyflagTypes
usesid
as the primary keyappConfig
useskey
as the primary key
- Idempotent - Safe to call multiple times; won't recreate existing stores
JSON Object Storage
Storing Objects (storeJsonObjects
)
export const storeJsonObjects = async (objects: JSONObject[], uniqueFields: string[]) => {
const db = await initDB();
const tx = db.transaction(OBJECT_STORE, 'readwrite');
const store = tx.objectStore(OBJECT_STORE);
const existingObjects = await store.getAll();
const existingMap = new Map(existingObjects.map(obj => [obj.__id, obj]));
const processedIds = new Set<string>();
let newCount = 0;
let updatedCount = 0;
for (const obj of objects) {
let objectId = '';
if (uniqueFields.length > 0) {
const uniqueValues = uniqueFields
.map(field => {
const value = getNestedValue(obj, field);
return value !== undefined ? String(value) : '';
})
.filter(Boolean)
.join('_');
objectId = uniqueValues || nanoid();
} else {
objectId = nanoid();
}
if (processedIds.has(objectId)) {
continue;
}
processedIds.add(objectId);
const existingObj = uniqueFields.length > 0 ? existingMap.get(objectId) : null;
if (existingObj) {
updatedCount++;
} else {
newCount++;
}
const newObj = {
...obj,
__id: objectId,
__flags: existingObj?.__flags || {}
};
await store.put(newObj);
}
await tx.done;
return { newCount, updatedCount };
};
Complex ID generation logic:
-
Unique field strategy - If unique fields are configured:
- Extracts values from specified fields using
getNestedValue
- Joins values with underscores to create composite ID
- Falls back to random ID if no values found
- Extracts values from specified fields using
-
Random ID strategy - If no unique fields configured:
- Uses
nanoid()
to generate random unique identifier
- Uses
-
Duplicate prevention:
processedIds
Set prevents processing the same ID twice in one batch- Skips objects that would create duplicate IDs
-
Update vs Insert logic:
- Checks if object with same ID already exists
- Preserves existing flags when updating objects
- Tracks counts for user feedback
-
Transaction safety:
- Uses database transactions for atomic operations
await tx.done
ensures all operations complete before returning
Nested Value Access (getNestedValue
)
export const getNestedValue = (obj: unknown, path: string): unknown => {
return path.split('.').reduce<unknown>((prev, curr) => {
if (prev && typeof prev === 'object' && curr in prev) {
return (prev as Record<string, unknown>)[curr];
}
return undefined;
}, obj);
};
Purpose: Safely extracts values from nested object paths for ID generation.
Example: For path "user.profile.email"
and object {user: {profile: {email: "test@example.com"}}}
, returns "test@example.com"
.
Data Retrieval and Manipulation
Getting All Objects (getAllJsonObjects
)
export const getAllJsonObjects = async (): Promise<JSONObject[]> => {
const db = await initDB();
return db.getAll(OBJECT_STORE);
};
Simple retrieval of all stored JSON objects. IndexedDB's getAll()
is efficient for loading complete datasets.
Flag Management (updateObjectFlag
)
export const updateObjectFlag = async (id: string, flagId: string, value: boolean) => {
const db = await initDB();
const tx = db.transaction(OBJECT_STORE, 'readwrite');
const store = tx.objectStore(OBJECT_STORE);
const obj = await store.get(id);
if (obj) {
if (!obj.__flags) {
obj.__flags = {};
}
obj.__flags[flagId] = value;
await store.put(obj);
}
await tx.done;
};
Flag update process:
- Retrieves object by ID
- Initializes flags object if it doesn't exist
- Sets flag value (true/false) for the specific flag ID
- Saves updated object back to database
Bulk Operations
Delete Multiple Objects:
export const deleteObjects = async (ids: string[]) => {
const db = await initDB();
const tx = db.transaction(OBJECT_STORE, 'readwrite');
const store = tx.objectStore(OBJECT_STORE);
for (const id of ids) {
await store.delete(id);
}
await tx.done;
};
Clear All Data:
export const clearAllObjects = async () => {
const db = await initDB();
const tx = db.transaction(OBJECT_STORE, 'readwrite');
await tx.objectStore(OBJECT_STORE).clear();
await tx.done;
};
Flag Type Management
Adding Flag Types (addFlagType
)
export const addFlagType = async (name: string, color: string, icon: string): Promise<FlagType> => {
const db = await initDB();
const newFlag: FlagType = {
id: nanoid(),
name,
color,
icon
};
await db.add(FLAGS_STORE, newFlag);
return newFlag;
};
Creates new flag types with:
- Random unique ID
- User-specified name, color, and icon
- Returns the created flag for immediate use
Retrieving Flag Types (getAllFlagTypes
)
export const getAllFlagTypes = async (): Promise<FlagType[]> => {
const db = await initDB();
try {
const existingFlags = await db.getAll(FLAGS_STORE);
return existingFlags;
} catch (error) {
console.error('Error getting flag types:', error);
return [];
}
};
Error handling: Returns empty array if database operation fails, preventing application crashes.
Configuration Management
Configuration Storage
The appConfig
store uses a key-value pattern where each setting is stored as:
{ key: 'settingName', value: actualValue }
Getting Configuration (getAppConfig
)
export const getAppConfig = async (): Promise<AppConfig> => {
const db = await initDB();
try {
const uniqueFields = await db.get(CONFIG_STORE, 'uniqueFields');
const imageField = await db.get(CONFIG_STORE, 'imageField');
const displayFields = await db.get(CONFIG_STORE, 'displayFields');
return {
uniqueFields: uniqueFields?.value || [],
imageField: imageField?.value || null,
displayFields: displayFields?.value || []
};
} catch (error) {
console.error('Error getting app config:', error);
return {
uniqueFields: [],
imageField: null,
displayFields: []
};
}
};
Graceful defaults: Returns sensible default values if configuration doesn't exist or loading fails.
Saving Configuration
export const saveUniqueFields = async (fields: string[]) => {
const db = await initDB();
await db.put(CONFIG_STORE, { key: 'uniqueFields', value: fields });
};
export const saveImageField = async (field: string | null) => {
const db = await initDB();
await db.put(CONFIG_STORE, { key: 'imageField', value: field });
};
export const saveDisplayFields = async (fields: string[]) => {
const db = await initDB();
await db.put(CONFIG_STORE, { key: 'displayFields', value: fields });
};
Individual setting updates: Each configuration aspect can be saved independently without affecting others.
Key Benefits of IndexedDB
Offline Capability
- No server required - All data stored locally in the browser
- Works offline - Application functions without internet connection
- Session persistence - Data survives browser restarts
Performance
- Asynchronous operations - Non-blocking database operations
- Indexed access - Fast lookups by primary key
- Bulk operations - Efficient handling of large datasets
Storage Capacity
- Large storage limits - Much larger than localStorage (typically GBs vs MBs)
- Structured data - Native support for complex objects
- Type preservation - Maintains JavaScript data types
Transaction Safety
- ACID properties - Atomic, Consistent, Isolated, Durable operations
- Rollback capability - Failed transactions don't corrupt data
- Concurrent access - Safe for multiple tabs/windows
Error Handling Patterns
The persistence layer implements consistent error handling:
- Try-catch blocks around all database operations
- Graceful degradation - Returns sensible defaults on failure
- Error logging - Console errors for debugging
- User feedback - Some operations trigger toast notifications
- Transaction cleanup -
await tx.done
ensures proper cleanup
This robust persistence layer enables the application to function as a fully offline-capable JSON data management tool.