---
description: AsyncStorage patterns, fallback strategies, and persistence management
globs: ["**/*storage*.js", "**/*persistent*.js", "**/*queue*.js"]
alwaysApply: false
---

# Storage Operations Patterns

## AsyncStorage Abstraction (MANDATORY)
ALWAYS use abstraction layer with graceful fallback:

```javascript
// REQUIRED: AsyncStorage adapter with fallback
export class AsyncStorageAdapter {
  constructor(storage) {
    if (!storage) {
      try {
        const storageModule = require("@react-native-async-storage/async-storage");
        // ⚠️ Evolution [Updated: 2025-05-30]: Handle both ES6 and CommonJS exports
        if (storageModule.default) {
          this.storage = storageModule.default;
        } else {
          this.storage = storageModule;
        }
      } catch {
        console.error("[@RNC/AsyncStorage]: NativeModule: AsyncStorage is null");
        console.error("[Mixpanel] Falling back to in-memory storage");
        this.storage = new InMemoryStorage();
      }
    } else {
      this.storage = storage;
    }
  }
}

// REQUIRED: In-memory fallback implementation
class InMemoryStorage {
  constructor() {
    this.store = {};
  }

  async getItem(key) {
    return this.store.hasOwnProperty(key) ? this.store[key] : null;
  }

  async setItem(key, value) {
    this.store[key] = value;
  }

  async removeItem(key) {
    delete this.store[key];
  }
}
```

## Error Handling Pattern (CRITICAL)
ALL storage operations MUST be wrapped with error handling:

```javascript
// ALWAYS wrap in try-catch with silent failure
async getItem(key) {
  try {
    return await this.storage.getItem(key);
  } catch {
    MixpanelLogger.error("error getting item from storage");
    return null;  // ALWAYS return usable default
  }
}

async setItem(key, value) {
  try {
    await this.storage.setItem(key, value);
  } catch {
    MixpanelLogger.error("error setting item in storage");
    // Silent failure - continue operation without throwing
  }
}

async removeItem(key) {
  try {
    await this.storage.removeItem(key);
  } catch {
    MixpanelLogger.error("error removing item from storage");
    // Silent failure - continue operation
  }
}
```

## Key Management Strategy (REQUIRED)
Use token-scoped keys with consistent naming:

```javascript
// REQUIRED: Token-scoped key generation functions
export const getQueueKey = (token, type) => `MIXPANEL_${token}_${type}_QUEUE`;
export const getDeviceIdKey = (token) => `MIXPANEL_${token}_DEVICE_ID`;
export const getDistinctIdKey = (token) => `MIXPANEL_${token}_DISTINCT_ID`;
export const getUserIdKey = (token) => `MIXPANEL_${token}_USER_ID`;
export const getOptedOutKey = (token) => `MIXPANEL_${token}_OPT_OUT`;
export const getSuperPropertiesKey = (token) => `MIXPANEL_${token}_SUPER_PROPERTIES`;
export const getTimeEventsKey = (token) => `MIXPANEL_${token}_TIME_EVENTS`;

// ALWAYS use these functions for consistency
const key = getSuperPropertiesKey(token);
await this.storageAdapter.setItem(key, JSON.stringify(properties));
```

## Serialization Pattern (MANDATORY)
Handle JSON serialization/deserialization safely:

```javascript
// Serialization: Object → JSON string
async persistSuperProperties(token) {
  if (this._superProperties[token] === null) {
    return; // Don't persist null values
  }
  
  try {
    const serialized = JSON.stringify(this._superProperties[token]);
    await this.storageAdapter.setItem(getSuperPropertiesKey(token), serialized);
  } catch (error) {
    MixpanelLogger.error(`Failed to serialize super properties: ${error.message}`);
  }
}

// Deserialization: JSON string → Object with fallback
async loadSuperProperties(token) {
  try {
    const superPropertiesString = await this.storageAdapter.getItem(
      getSuperPropertiesKey(token)
    );
    
    this._superProperties[token] = superPropertiesString
      ? JSON.parse(superPropertiesString)
      : {}; // ALWAYS provide default value
  } catch (error) {
    MixpanelLogger.error(`Failed to parse super properties, using defaults: ${error.message}`);
    this._superProperties[token] = {}; // Fallback to empty object
  }
}
```

## Write-Through Caching Pattern (REQUIRED)
Maintain both memory cache AND persistent storage:

```javascript
export class MixpanelPersistent {
  constructor(storageAdapter) {
    this.storageAdapter = storageAdapter;
    
    // In-memory caches for fast access
    this._superProperties = {};
    this._timeEvents = {};
    this._identity = {};
    this._optedOut = {};
  }

  // Fast reads from memory cache
  getSuperProperties(token) {
    return this._superProperties[token] || {};
  }

  // Write-through: Update cache AND persist
  async updateSuperProperties(token, superProperties) {
    // Update in-memory cache immediately
    this._superProperties[token] = { ...superProperties };
    
    // Persist to storage asynchronously
    await this.persistSuperProperties(token);
  }
}
```

## Lazy Loading Pattern
Load storage data only when needed:

```javascript
// ONLY load when first accessed
async loadDeviceId(token) {
  if (!token) return; // Early exit for invalid token

  // Check if already loaded
  if (this._identity[token]?.deviceId) {
    return;
  }

  const storageDeviceId = await this.storageAdapter.getItem(getDeviceIdKey(token));
  
  // Initialize token state only when needed
  if (!this._identity[token]) {
    this._identity[token] = {};
  }

  this._identity[token].deviceId = storageDeviceId;

  // Generate new ID only if missing
  if (!this._identity[token].deviceId) {
    try {
      this._identity[token].deviceId = randomUUID(); // Expo crypto
    } catch (e) {
      this._identity[token].deviceId = uuid.v4();    // uuid package fallback
    }
    
    // Persist the new ID
    await this.storageAdapter.setItem(
      getDeviceIdKey(token),
      this._identity[token].deviceId
    );
  }
}
```

## Queue Persistence Pattern
Handle complex data structures efficiently:

```javascript
// Queue operations (arrays of objects)
async loadQueue(token, type) {
  try {
    const queueString = await this.storageAdapter.getItem(getQueueKey(token, type));
    return queueString ? JSON.parse(queueString) : []; // Default to empty array
  } catch (error) {
    MixpanelLogger.error(`Failed to load ${type} queue, using empty queue: ${error.message}`);
    return []; // Always return usable array
  }
}

async saveQueue(token, type, queue) {
  try {
    await this.storageAdapter.setItem(
      getQueueKey(token, type),
      JSON.stringify(queue)
    );
  } catch (error) {
    MixpanelLogger.error(`Failed to save ${type} queue: ${error.message}`);
    // Don't throw - let operation continue
  }
}

// Boolean persistence (explicit string conversion)
async persistOptedOut(token) {
  if (this._optedOut[token] === null || this._optedOut[token] === undefined) {
    return; // Don't persist null/undefined
  }
  
  await this.storageAdapter.setItem(
    getOptedOutKey(token),
    this._optedOut[token].toString() // Boolean → String
  );
}

async loadOptOut(token) {
  const optOutString = await this.storageAdapter.getItem(getOptedOutKey(token));
  this._optedOut[token] = optOutString === "true"; // String → Boolean
}
```

## Data Migration Pattern
Handle identity generation with fallbacks:

```javascript
async loadDistinctId(token) {
  const distinctId = await this.storageAdapter.getItem(getDistinctIdKey(token));
  
  if (!this._identity[token]) {
    this._identity[token] = {};
  }
  
  this._identity[token].distinctId = distinctId;
  
  if (!this._identity[token].distinctId) {
    // Generate device-based distinct ID
    await this.loadDeviceId(token); // Ensure device ID exists
    this._identity[token].distinctId = "$device:" + this._identity[token].deviceId;
    
    await this.storageAdapter.setItem(
      getDistinctIdKey(token),
      this._identity[token].distinctId
    );
  }
}
```

## Cleanup Operations
Implement comprehensive token cleanup:

```javascript
async reset(token) {
  // Remove ALL token-related keys
  await this.storageAdapter.removeItem(getDeviceIdKey(token));
  await this.storageAdapter.removeItem(getDistinctIdKey(token));
  await this.storageAdapter.removeItem(getUserIdKey(token));
  await this.storageAdapter.removeItem(getSuperPropertiesKey(token));
  await this.storageAdapter.removeItem(getTimeEventsKey(token));
  await this.storageAdapter.removeItem(getOptedOutKey(token));
  
  // Clear memory cache
  delete this._identity[token];
  delete this._superProperties[token];
  delete this._timeEvents[token];
  delete this._optedOut[token];
  
  // Reload fresh state
  await this.loadIdentity(token);
  await this.loadSuperProperties(token);
  await this.loadTimeEvents(token);
}
```

## Testing Storage Operations
```javascript
// Mock AsyncStorage completely for tests
beforeEach(() => {
  AsyncStorage.getItem.mockResolvedValue(null);
  AsyncStorage.setItem.mockResolvedValue(undefined);
  AsyncStorage.removeItem.mockResolvedValue(undefined);
});

it('should handle storage failures gracefully', async () => {
  // Mock storage failure
  AsyncStorage.setItem.mockRejectedValue(new Error('Storage unavailable'));

  // Should not throw error
  await expect(
    mixpanel.registerSuperProperties({ prop: 'value' })
  ).resolves.toBeUndefined();

  // Should log error
  expect(console.error).toHaveBeenCalledWith(
    expect.stringContaining('error setting item in storage')
  );
});
```

## Performance Considerations
- Use write-through caching for frequently accessed data
- Implement lazy loading to avoid unnecessary storage I/O
- Batch storage operations when possible
- Minimize JSON.parse/stringify calls
- Use efficient queue operations (splice vs recreation)