fix(headless): async-storage in memory first
This commit is contained in:
parent
4280820e01
commit
9f6452d5e3
11 changed files with 308 additions and 13 deletions
3
index.js
3
index.js
|
@ -32,7 +32,8 @@ registerRootComponent(App);
|
|||
|
||||
// Constants for persistence
|
||||
const LAST_SYNC_TIME_KEY = "@geolocation_last_sync_time";
|
||||
const FORCE_SYNC_INTERVAL = 24 * 60 * 60 * 1000;
|
||||
// const FORCE_SYNC_INTERVAL = 24 * 60 * 60 * 1000;
|
||||
const FORCE_SYNC_INTERVAL = 60 * 60 * 1000; // DEBUGGING
|
||||
|
||||
// Helper functions for persisting sync time
|
||||
const getLastSyncTime = async () => {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
|
|||
|
||||
import { authActions, permissionWizardActions } from "~/stores";
|
||||
import { secureStore } from "~/lib/memorySecureStore";
|
||||
import memoryAsyncStorage from "~/lib/memoryAsyncStorage";
|
||||
|
||||
import "~/lib/mapbox";
|
||||
import "~/i18n";
|
||||
|
@ -54,8 +55,9 @@ const initializeStores = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// Initialize memory secure store first
|
||||
// Initialize memory stores first
|
||||
initializeStore("memorySecureStore", secureStore.init);
|
||||
initializeStore("memoryAsyncStorage", memoryAsyncStorage.init);
|
||||
|
||||
// Then initialize other stores sequentially
|
||||
initializeStore("authActions", authActions.init);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import { View, ScrollView, StyleSheet, Platform } from "react-native";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import AsyncStorage from "~/lib/memoryAsyncStorage";
|
||||
|
||||
import Text from "../Text";
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
useNetworkState,
|
||||
} from "~/stores";
|
||||
import { secureStore } from "~/lib/memorySecureStore";
|
||||
import memoryAsyncStorage from "~/lib/memoryAsyncStorage";
|
||||
|
||||
import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground";
|
||||
import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground";
|
||||
|
@ -212,16 +213,23 @@ const AppLifecycleListener = () => {
|
|||
);
|
||||
checkPermissions(completed);
|
||||
|
||||
// Sync memory secure store back to persistent storage
|
||||
lifecycleLogger.info(
|
||||
"Syncing memory secure store to persistent storage",
|
||||
);
|
||||
// Sync memory stores back to persistent storage
|
||||
lifecycleLogger.info("Syncing memory stores to persistent storage");
|
||||
|
||||
// Sync secure store
|
||||
secureStore.syncToSecureStore().catch((error) => {
|
||||
lifecycleLogger.error("Failed to sync memory secure store", {
|
||||
error: error.message,
|
||||
});
|
||||
});
|
||||
|
||||
// Sync async storage
|
||||
memoryAsyncStorage.syncToAsyncStorage().catch((error) => {
|
||||
lifecycleLogger.error("Failed to sync memory async storage", {
|
||||
error: error.message,
|
||||
});
|
||||
});
|
||||
|
||||
// Then handle WebSocket reconnection with proper error handling
|
||||
activeTimeout.current = setTimeout(() => {
|
||||
try {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import AsyncStorage from "~/lib/memoryAsyncStorage";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
const EULA_STORAGE_KEY = "@eula_accepted";
|
||||
|
|
284
src/lib/memoryAsyncStorage.js
Normal file
284
src/lib/memoryAsyncStorage.js
Normal file
|
@ -0,0 +1,284 @@
|
|||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { createLogger } from "~/lib/logger";
|
||||
import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
|
||||
|
||||
const storageLogger = createLogger({
|
||||
module: SYSTEM_SCOPES.STORAGE,
|
||||
feature: "memory-async-storage",
|
||||
});
|
||||
|
||||
// In-memory cache for AsyncStorage values
|
||||
const memoryCache = new Map();
|
||||
|
||||
// Track if we've loaded from AsyncStorage
|
||||
let isInitialized = false;
|
||||
const initPromise = new Promise((resolve) => {
|
||||
global.__memoryAsyncStorageInitResolve = resolve;
|
||||
});
|
||||
|
||||
/**
|
||||
* Memory-first AsyncStorage wrapper that maintains an in-memory cache
|
||||
* for headless/background mode access when AsyncStorage is unavailable
|
||||
*/
|
||||
export const memoryAsyncStorage = {
|
||||
/**
|
||||
* Initialize the memory cache by loading all known keys from AsyncStorage
|
||||
*/
|
||||
async init() {
|
||||
if (isInitialized) return;
|
||||
|
||||
storageLogger.info("Initializing memory async storage");
|
||||
|
||||
// List of known keys that need to be cached
|
||||
const knownKeys = [
|
||||
"permission_wizard_completed",
|
||||
"override_messages",
|
||||
"last_known_location",
|
||||
"eula_accepted",
|
||||
"last_update_check",
|
||||
"emulator_mode_enabled",
|
||||
];
|
||||
|
||||
// Load all known keys into memory
|
||||
for (const key of knownKeys) {
|
||||
try {
|
||||
const value = await AsyncStorage.getItem(key);
|
||||
if (value !== null) {
|
||||
memoryCache.set(key, value);
|
||||
storageLogger.debug("Loaded key into memory", {
|
||||
key,
|
||||
hasValue: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
storageLogger.warn("Failed to load key from AsyncStorage", {
|
||||
key,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Also load any keys that might exist with getAllKeys
|
||||
try {
|
||||
const allKeys = await AsyncStorage.getAllKeys();
|
||||
for (const key of allKeys) {
|
||||
if (!memoryCache.has(key)) {
|
||||
try {
|
||||
const value = await AsyncStorage.getItem(key);
|
||||
if (value !== null) {
|
||||
memoryCache.set(key, value);
|
||||
storageLogger.debug("Loaded additional key into memory", { key });
|
||||
}
|
||||
} catch (error) {
|
||||
storageLogger.warn("Failed to load additional key", {
|
||||
key,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
storageLogger.warn("Failed to get all keys from AsyncStorage", {
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
isInitialized = true;
|
||||
if (global.__memoryAsyncStorageInitResolve) {
|
||||
global.__memoryAsyncStorageInitResolve();
|
||||
delete global.__memoryAsyncStorageInitResolve;
|
||||
}
|
||||
|
||||
storageLogger.info("Memory async storage initialized", {
|
||||
cachedKeys: Array.from(memoryCache.keys()),
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure initialization is complete before operations
|
||||
*/
|
||||
async ensureInitialized() {
|
||||
if (!isInitialized) {
|
||||
await initPromise;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get item from memory first, fallback to AsyncStorage
|
||||
*/
|
||||
async getItem(key) {
|
||||
await this.ensureInitialized();
|
||||
|
||||
// Try memory first
|
||||
if (memoryCache.has(key)) {
|
||||
const value = memoryCache.get(key);
|
||||
storageLogger.debug("Retrieved from memory cache", {
|
||||
key,
|
||||
hasValue: !!value,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
// Fallback to AsyncStorage
|
||||
try {
|
||||
const value = await AsyncStorage.getItem(key);
|
||||
if (value !== null) {
|
||||
// Cache for future use
|
||||
memoryCache.set(key, value);
|
||||
storageLogger.debug("Retrieved from AsyncStorage and cached", { key });
|
||||
}
|
||||
return value;
|
||||
} catch (error) {
|
||||
storageLogger.warn(
|
||||
"Failed to retrieve from AsyncStorage, returning null",
|
||||
{
|
||||
key,
|
||||
error: error.message,
|
||||
},
|
||||
);
|
||||
// In headless mode, AsyncStorage might not be accessible
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set item in both memory and AsyncStorage
|
||||
*/
|
||||
async setItem(key, value) {
|
||||
await this.ensureInitialized();
|
||||
|
||||
// Always set in memory first
|
||||
memoryCache.set(key, value);
|
||||
storageLogger.debug("Set in memory cache", { key });
|
||||
|
||||
// Try to persist to AsyncStorage
|
||||
try {
|
||||
await AsyncStorage.setItem(key, value);
|
||||
storageLogger.debug("Persisted to AsyncStorage", { key });
|
||||
} catch (error) {
|
||||
storageLogger.warn(
|
||||
"Failed to persist to AsyncStorage, kept in memory only",
|
||||
{
|
||||
key,
|
||||
error: error.message,
|
||||
},
|
||||
);
|
||||
// Continue - value is at least in memory
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove item from both memory and AsyncStorage
|
||||
*/
|
||||
async removeItem(key) {
|
||||
await this.ensureInitialized();
|
||||
|
||||
// Delete from memory
|
||||
memoryCache.delete(key);
|
||||
storageLogger.debug("Deleted from memory cache", { key });
|
||||
|
||||
// Try to delete from AsyncStorage
|
||||
try {
|
||||
await AsyncStorage.removeItem(key);
|
||||
storageLogger.debug("Deleted from AsyncStorage", { key });
|
||||
} catch (error) {
|
||||
storageLogger.warn("Failed to delete from AsyncStorage", {
|
||||
key,
|
||||
error: error.message,
|
||||
});
|
||||
// Continue - at least removed from memory
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all keys from memory cache
|
||||
*/
|
||||
async getAllKeys() {
|
||||
await this.ensureInitialized();
|
||||
return Array.from(memoryCache.keys());
|
||||
},
|
||||
|
||||
/**
|
||||
* Get multiple items
|
||||
*/
|
||||
async multiGet(keys) {
|
||||
await this.ensureInitialized();
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
const value = await this.getItem(key);
|
||||
result.push([key, value]);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set multiple items
|
||||
*/
|
||||
async multiSet(keyValuePairs) {
|
||||
await this.ensureInitialized();
|
||||
for (const [key, value] of keyValuePairs) {
|
||||
await this.setItem(key, value);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove multiple items
|
||||
*/
|
||||
async multiRemove(keys) {
|
||||
await this.ensureInitialized();
|
||||
for (const key of keys) {
|
||||
await this.removeItem(key);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all items (use with caution)
|
||||
*/
|
||||
async clear() {
|
||||
await this.ensureInitialized();
|
||||
|
||||
// Clear memory
|
||||
memoryCache.clear();
|
||||
storageLogger.info("Cleared memory cache");
|
||||
|
||||
// Try to clear AsyncStorage
|
||||
try {
|
||||
await AsyncStorage.clear();
|
||||
storageLogger.info("Cleared AsyncStorage");
|
||||
} catch (error) {
|
||||
storageLogger.warn("Failed to clear AsyncStorage", {
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync memory cache back to AsyncStorage (useful when returning from background)
|
||||
*/
|
||||
async syncToAsyncStorage() {
|
||||
storageLogger.info("Syncing memory cache to AsyncStorage");
|
||||
|
||||
const syncResults = {
|
||||
success: 0,
|
||||
failed: 0,
|
||||
};
|
||||
|
||||
for (const [key, value] of memoryCache.entries()) {
|
||||
try {
|
||||
await AsyncStorage.setItem(key, value);
|
||||
syncResults.success++;
|
||||
} catch (error) {
|
||||
syncResults.failed++;
|
||||
storageLogger.warn("Failed to sync key to AsyncStorage", {
|
||||
key,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
storageLogger.info("Memory cache sync completed", syncResults);
|
||||
},
|
||||
};
|
||||
|
||||
// Export as default to match the AsyncStorage interface
|
||||
export default memoryAsyncStorage;
|
|
@ -1,5 +1,5 @@
|
|||
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import AsyncStorage from "~/lib/memoryAsyncStorage";
|
||||
import { createLogger } from "~/lib/logger";
|
||||
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { createAtom } from "~/lib/atomic-zustand";
|
||||
import debounce from "lodash.debounce";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import AsyncStorage from "~/lib/memoryAsyncStorage";
|
||||
|
||||
const OVERRIDE_MESSAGES_STORAGE_KEY = "@override_messages";
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createAtom } from "~/lib/atomic-zustand";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import AsyncStorage from "~/lib/memoryAsyncStorage";
|
||||
|
||||
const WIZARD_COMPLETED_KEY = "@permission_wizard_completed";
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
import * as Updates from "expo-updates";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import AsyncStorage from "~/lib/memoryAsyncStorage";
|
||||
import useNow from "~/hooks/useNow";
|
||||
import * as Sentry from "@sentry/react-native";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import AsyncStorage from "~/lib/memoryAsyncStorage";
|
||||
import { createLogger } from "~/lib/logger";
|
||||
import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue