# AsyncStorage Integration Patterns

## Overview

The mixpanel-react-native library demonstrates sophisticated AsyncStorage integration patterns, including storage abstraction, graceful fallbacks, and efficient data serialization strategies for persistent analytics state.

## 🏗️ Storage Architecture

### Storage Abstraction Layer

```javascript
// javascript/mixpanel-storage.js
export class AsyncStorageAdapter {
  constructor(storage) {
    if (!storage) {
      try {
        // Dynamic import to handle missing dependency
        const storageModule = require("@react-native-async-storage/async-storage");
        if (storageModule.default) {
          this.storage = storageModule.default
        } else {
          this.storage = storageModule
        }
      } catch {
        console.error(
          "[@RNC/AsyncStorage]: NativeModule: AsyncStorage is null. " +
          "Please run 'npm install @react-native-async-storage/async-storage'"
        );
        console.error("[Mixpanel] Falling back to in-memory storage");
        this.storage = new InMemoryStorage();
      }
    } else {
      this.storage = storage;
    }
  }
}
```

**Abstraction Benefits**:
- **Graceful degradation**: Falls back to in-memory storage
- **Dependency injection**: Supports custom storage implementations  
- **Module compatibility**: Handles different import patterns
- **Development flexibility**: Works without AsyncStorage in tests

### In-Memory Fallback Implementation

```javascript
// javascript/mixpanel-storage.js
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];
  }
}
```

**Fallback Strategy**:
- **API consistency**: Same interface as AsyncStorage
- **Session persistence**: Data survives within app session
- **Test compatibility**: Perfect for unit testing
- **Development workflow**: Works in environments without native storage

## 🔧 Error Handling Patterns

### Defensive Storage Operations

```javascript
// javascript/mixpanel-storage.js
export class AsyncStorageAdapter {
  async getItem(key) {
    try {
      return await this.storage.getItem(key);
    } catch {
      MixpanelLogger.error("error getting item from storage");
      return null;  // Always return null on failure
    }
  }

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

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

**Error Handling Philosophy**:
- **Silent failures**: Storage errors don't crash the app
- **Logging**: All errors logged for debugging
- **Fallback values**: Always return usable defaults
- **Continue operation**: App functionality maintained despite storage issues

## 🗝️ Key Management Strategy

### Token-Scoped Key Generation

```javascript
// javascript/mixpanel-constants.js
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`;
export const getAppHasOpenedBeforeKey = (token) => `MIXPANEL_${token}_APP_HAS_OPENED_BEFORE`;
```

**Key Strategy Benefits**:
- **Namespace isolation**: Prevents key collisions between projects
- **Predictable naming**: Easy to debug and inspect
- **Multi-tenant support**: Multiple Mixpanel projects per app
- **Clear organization**: Keys grouped by functionality

### Key Lifecycle Management

```javascript
// javascript/mixpanel-persistent.js
async reset(token) {
  // Comprehensive cleanup of 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));
  
  // Reload fresh state
  await this.loadIdentity(token);
  await this.loadSuperProperties(token);
  await this.loadTimeEvents(token);
}
```

## 💾 Data Serialization Patterns

### JSON Serialization Strategy

```javascript
// javascript/mixpanel-persistent.js

// Serialization: Objects to strings
async persistSuperProperties(token) {
  if (this._superProperties[token] === null) {
    return;
  }
  await this.storageAdapter.setItem(
    getSuperPropertiesKey(token),
    JSON.stringify(this._superProperties[token])  // Object → JSON string
  );
}

// Deserialization: Strings to objects with fallbacks
async loadSuperProperties(token) {
  const superPropertiesString = await this.storageAdapter.getItem(
    getSuperPropertiesKey(token)
  );
  this._superProperties[token] = superPropertiesString
    ? JSON.parse(superPropertiesString)  // JSON string → Object
    : {};  // Fallback to empty object
}
```

**Serialization Principles**:
- **JSON format**: Standard, debuggable, cross-platform
- **Null safety**: Handle missing/corrupted data gracefully
- **Default values**: Always provide sensible fallbacks
- **Type consistency**: Maintain expected data types

### Complex Data Structure Handling

```javascript
// javascript/mixpanel-persistent.js

// Queue persistence (arrays of objects)
async loadQueue(token, type) {
  const queueString = await this.storageAdapter.getItem(
    getQueueKey(token, type)
  );
  return queueString ? JSON.parse(queueString) : [];  // Default to empty array
}

async saveQueue(token, type, queue) {
  await this.storageAdapter.setItem(
    getQueueKey(token, type),
    JSON.stringify(queue)
  );
}

// Boolean persistence (string conversion)
async persistOptedOut(token) {
  if (this._optedOut[token] === null) {
    return;
  }
  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
}
```

## 🚀 Performance Optimization Patterns

### Write-Through Caching

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

  // Read from cache (fast)
  getSuperProperties(token) {
    return this._superProperties[token];
  }

  // Write to cache AND storage (write-through)
  updateSuperProperties(token, superProperties) {
    this._superProperties = {
      ...this._superProperties,
      [token]: { ...superProperties },  // Update cache
    };
    // Note: persistSuperProperties() called separately for storage
  }
}
```

**Caching Benefits**:
- **Fast reads**: Memory access vs. async storage I/O
- **Consistency**: Cache always reflects current state
- **Batched writes**: Can optimize storage writes
- **Reliability**: Storage failures don't affect memory state

### Lazy Loading Strategy

```javascript
// javascript/mixpanel-persistent.js
async loadDeviceId(token) {
  if (!token) {
    return;  // Early exit for invalid token
  }

  const storageToken = await this.storageAdapter.getItem(
    getDeviceIdKey(token)
  );

  if (!this._identity[token]) {
    this._identity[token] = {};  // Initialize token state only when needed
  }

  this._identity[token].deviceId = storageToken;

  if (!this._identity[token].deviceId) {
    // Generate only when missing
    try {
      this._identity[token].deviceId = randomUUID();
    } catch (e) {
      this._identity[token].deviceId = uuid.v4();
    }
    await this.storageAdapter.setItem(
      getDeviceIdKey(token),
      this._identity[token].deviceId
    );
  }
}
```

**Lazy Loading Benefits**:
- **Memory efficiency**: Only load data when needed
- **Startup performance**: Avoid loading unused token data
- **Scalability**: Supports many tokens without memory bloat

## 🔄 Data Migration & Versioning

### Identity Generation with Fallbacks

```javascript
// javascript/mixpanel-persistent.js
async loadDeviceId(token) {
  // Load existing ID from storage
  const storageToken = await this.storageAdapter.getItem(getDeviceIdKey(token));
  
  if (!this._identity[token]) {
    this._identity[token] = {};
  }

  this._identity[token].deviceId = storageToken;

  if (!this._identity[token].deviceId) {
    // Generate new ID with fallback strategy
    try {
      this._identity[token].deviceId = randomUUID();  // Expo crypto (preferred)
    } 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
    );
  }
}

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
    this._identity[token].distinctId = "$device:" + this._identity[token].deviceId;
    await this.storageAdapter.setItem(
      getDistinctIdKey(token),
      this._identity[token].distinctId
    );
  }
}
```

## 🧪 Testing Patterns

### AsyncStorage Mocking for Tests

```javascript
// __tests__/jest_setup.js
jest.mock("@react-native-async-storage/async-storage", () => ({
  getItem: jest.fn().mockResolvedValue(null),
  setItem: jest.fn().mockResolvedValue(undefined),
  removeItem: jest.fn().mockResolvedValue(undefined),
}));

// __mocks__/@react-native-async-storage/async-storage.js
export default {
  getItem: jest.fn((key) => {
    return new Promise((resolve) => {
      resolve(null);
    });
  }),
  setItem: jest.fn((key, value) => {
    return new Promise((resolve) => {
      resolve(null);
    });
  }),
  removeItem: jest.fn((key) => {
    return new Promise((resolve) => {
      resolve(null);
    });
  }),
};
```

### Storage Adapter Testing

```javascript
// Example test pattern for storage adapter
describe('AsyncStorageAdapter', () => {
  let adapter;
  let mockStorage;

  beforeEach(() => {
    mockStorage = {
      getItem: jest.fn(),
      setItem: jest.fn(),
      removeItem: jest.fn(),
    };
    adapter = new AsyncStorageAdapter(mockStorage);
  });

  it('should handle storage errors gracefully', async () => {
    mockStorage.getItem.mockRejectedValue(new Error('Storage error'));
    
    const result = await adapter.getItem('test-key');
    
    expect(result).toBeNull();
    expect(console.error).toHaveBeenCalledWith(
      expect.stringContaining('error getting item from storage')
    );
  });
});
```

## 📋 AsyncStorage Best Practices Discovered

### 1. **Graceful Degradation**
Always provide fallbacks when AsyncStorage is unavailable

### 2. **Error Resilience**  
Catch and handle all storage errors without crashing

### 3. **Key Namespacing**
Use consistent, predictable key naming patterns

### 4. **Data Type Safety**
Handle serialization/deserialization consistently

### 5. **Performance Optimization**
Use write-through caching for frequently accessed data

### 6. **Memory Management**
Implement lazy loading to avoid unnecessary memory usage

### 7. **Testing Strategy**
Mock AsyncStorage completely for reliable unit tests

### 8. **Dependency Injection**
Support custom storage implementations for flexibility

### 9. **Default Values**
Always provide sensible defaults for missing data

### 10. **Logging Strategy**
Log storage operations for debugging without exposing sensitive data