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 
idblibrary 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:
jsonObjectsuses__idas the primary keyflagTypesusesidas the primary keyappConfiguseskeyas 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:
processedIdsSet 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.doneensures 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.doneensures proper cleanup 
This robust persistence layer enables the application to function as a fully offline-capable JSON data management tool.