Skip to main content

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):

  1. jsonObjects - Stores the actual JSON data with unique IDs
  2. flagTypes - Stores custom flag definitions (name, color, icon)
  3. 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:

  1. Opens database - Uses the idb library wrapper around native IndexedDB
  2. Version control - Database version determines when upgrades run
  3. Upgrade function - Only runs when database is first created or version increases
  4. Object store creation - Creates the three stores with their primary keys:
    • jsonObjects uses __id as the primary key
    • flagTypes uses id as the primary key
    • appConfig uses key as the primary key
  5. 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:

  1. 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
  2. Random ID strategy - If no unique fields configured:

    • Uses nanoid() to generate random unique identifier
  3. Duplicate prevention:

    • processedIds Set prevents processing the same ID twice in one batch
    • Skips objects that would create duplicate IDs
  4. Update vs Insert logic:

    • Checks if object with same ID already exists
    • Preserves existing flags when updating objects
    • Tracks counts for user feedback
  5. 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:

  1. Retrieves object by ID
  2. Initializes flags object if it doesn't exist
  3. Sets flag value (true/false) for the specific flag ID
  4. 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:

  1. Try-catch blocks around all database operations
  2. Graceful degradation - Returns sensible defaults on failure
  3. Error logging - Console errors for debugging
  4. User feedback - Some operations trigger toast notifications
  5. 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.