fix(headless): async-storage in memory first

This commit is contained in:
devthejo 2025-06-29 15:40:24 +02:00
parent 4280820e01
commit 9f6452d5e3
11 changed files with 308 additions and 13 deletions

View file

@ -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 () => {

View file

@ -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);

View file

@ -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";

View file

@ -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 {

View file

@ -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";

View 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;

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";