react-native-async-storage / async-storage

An asynchronous, persistent, key-value storage system for React Native.
https://react-native-async-storage.github.io/async-storage/
MIT License
4.7k stars 466 forks source link

Storage does not persist on app force close #962

Closed TheHeumanModean closed 1 year ago

TheHeumanModean commented 1 year ago

What happened?

Storage does not persist when app force closed on iOS.

This issue was closed but is not resolved for me, nor is it resolved for others in that thread.

I am not using expo

Expected

Actual

I've also tried delaying the loading of the storage with a setTimeout in case it was trying to load value before it could access storage, but there was no behavioral difference.

This is logged in xcode

Manifest does not exist - creating a new one.

So it seems on iOS at least a new manifest is created (and therefore not loading previously saved data) on start of app every time and that is what causes the value to be null

I do not have time to investigate android as deeply as I did iOS but there could be a similar problem there

Version

Have tried various between 1.17.3 and 1.18.1

What platforms are you seeing this issue on?

System Information

System:
    OS: macOS 13.3.1
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
    Memory: 142.58 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 16.13.0 - ~/.nvm/versions/node/v16.13.0/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 8.10.0 - ~/.nvm/versions/node/v16.13.0/bin/npm
    Watchman: 2023.04.03.00 - /usr/local/bin/watchman
  Managers:
    CocoaPods: 1.11.3 - /usr/local/bin/pod
  SDKs:
    iOS SDK:
      Platforms: DriverKit 22.1, iOS 16.1, macOS 13.0, tvOS 16.1, watchOS 9.1
    Android SDK:
      API Levels: 25, 27, 28, 29, 30, 31, 33
      Build Tools: 28.0.3, 29.0.2, 30.0.1, 30.0.2, 30.0.3, 31.0.0, 33.0.0
      System Images: android-28 | Google Play Intel x86 Atom, android-29 | Google APIs Intel x86 Atom, android-29 | Google Play Intel x86 Atom, android-30 | Google APIs Intel x86 Atom, android-30 | Google Play Intel x86 Atom
      Android NDK: Not Found
  IDEs:
    Android Studio: 2021.2 AI-212.5712.43.2112.8815526
    Xcode: 14.1/14B47b - /usr/bin/xcodebuild
  Languages:
    Java: 11.0.13 - /usr/bin/javac
  npmPackages:
    @react-native-community/cli: Not Found
    react: 18.0.0 => 18.0.0 
    react-native: 0.69.5 => 0.69.5 
    react-native-macos: Not Found
  npmGlobalPackages:
    *react-native*: Not Found

Steps to Reproduce

Code

import AsyncStorage from "@react-native-async-storage/async-storage"

...

  public static async SAVE_STRING(key: string, value: string): Promise<boolean> {
    try {
      console.log("Saving key", key, value)
      await AsyncStorage.setItem(key, value)
      return true
    } catch {
      return false
    }
  }

  public static async LOAD_STRING(key: string): Promise<string | null> {
    try {
      console.log("Loading key", key)
      const value = await AsyncStorage.getItem(key)
      console.log("loaded value", value)
      return value
    } catch {
      console.log("Failed to load string")
      return null
    }
  }

Logs

 LOG  Saving key SAVED_USERNAME penguin

app is refreshed

 BUNDLE  ./index.js 

 LOG  Loading key SAVED_USERNAME
 LOG  loaded value penguin

app is forcce closed

 BUNDLE  ./index.js 

 LOG  Loading key SAVED_USERNAME
 LOG  loaded value null
krizzu commented 1 year ago

Seems like there's the old AsyncStorage (from RN) creeping in. iOS has a migration process where it would copy over the old manifest file, if exists, and use it as a current storage destination. This happens on every app launch.

Make sure there's no other AsyncStorage used except for one from react-native-async-storage org

TheHeumanModean commented 1 year ago

Seems like there's the old AsyncStorage (from RN) creeping in. iOS has a migration process where it would copy over the old manifest file, if exists, and use it as a current storage destination. This happens on every app launch.

Make sure there's no other AsyncStorage used except for one from react-native-async-storage org

This is the only file in my project that imports AsyncStorage, so thats probably not it.

I do have some native modules that use persistent storage (not sure how, code is pretty obfuscated), could this be causing conflicts?

Edit: After asking another team using the same native modules as me, the native modules do not seem to be the problem as they are not experiencing this issue.

I am not fluent at all in objective-c so I can't tell why Manifest does not exist - creating a new one. gets logged but that definitely seems to be the culprit. Can someone provide me more info on how that manifest gets loaded?

1989TomE commented 1 year ago

Hi, I am experiencing the same issue, though I am using Expo.

"dependencies": { "expo": "~48.0.15", "@react-native-async-storage/async-storage": "1.17.11", "expo-secure-store": "~12.1.1", "react-native": "0.71.7" },

As @TheHeumanModean described, it works when app resumes (from background), but when it is freshly started (that is when it is really need for persisting users to be logged in), data in storage are alwaysnull. I tried to use different package Expo's SecureStore to work around this, but it behaves the same way. I am experiencing this on iOS app installed from Testflight. Not sure if those two can be connected somehow. It worked for me before updating my project to Expo SDK 48.

krizzu commented 1 year ago

@TheHeumanModean are you able to reproduce this on a new project? If not, I'd assume it has to be something related to your project. Can you spot the issue on android too?

TheHeumanModean commented 1 year ago

@TheHeumanModean are you able to reproduce this on a new project? If not, I'd assume it has to be something related to your project. Can you spot the issue on android too?

@krizzu I was mistaken when I first made this, it seems android is fine and this is just an iOS problem for my project. And no I can't reproduce it in a new project, I know it has something to do with mine, just need to figure out what it is.

I need help understanding why the "manifest" gets recreated with no data on app start so that I can resolve the issue

krizzu commented 1 year ago

@TheHeumanModean The "manifest" is a file (JSON) where all key-value values below 5MB are being saved on iOS. When AsyncStorage was extracted from React Native, the location of the manifest file is located has changed. Now we needed the migration code to move files to the new location so that users could work undisturbed. The migration code removes the old manifest file, once migration is successful, so that we know AsyncStorage operates on latest version of it.

We spotted the issue you have where two versions of AsyncStorage and AsyncStorage from RN are used. Old AsyncStorage creates a manifest on app launch and the new one removes it, after migration.

TheHeumanModean commented 1 year ago

@krizzu Looking at the comments in that method I don't think it really applies to our project. I've never used react-native/async-storage nor have I used this plugin pre 1.17.3. I put in some logs to confirm my suspicions and sure enough no migration happens

init

// Get the path to any old storage directory that needs to be migrated. If multiple exist,
// the oldest are removed and the most recently modified is returned.
NSString *oldStoragePath = RCTGetStoragePathForMigration();
if (oldStoragePath != nil) {
    printf("ASYNC STORAGE, old storage path is NOT nil");
    // Migrate our deprecated path "Documents/.../RNCAsyncLocalStorage_V1" or
    // "Documents/.../RCTAsyncLocalStorage" to "Documents/.../RCTAsyncLocalStorage_V1"
    RCTStorageDirectoryMigrationCheck(
            oldStoragePath, RCTCreateStorageDirectoryPath_deprecated(RCTStorageDirectory), YES);
}
else {
    printf("ASYNC STORAGE, old storage path is nil\n");
}
// Migrate what's in "Documents/.../RCTAsyncLocalStorage_V1" to
// "Application Support/[bundleID]/RCTAsyncLocalStorage_V1"
RCTStorageDirectoryMigrationCheck(RCTCreateStorageDirectoryPath_deprecated(RCTStorageDirectory),
                                      RCTCreateStorageDirectoryPath(RCTStorageDirectory),
                                      NO);

RCTStorageDirectoryMigrationCheck

if ([fileManager fileExistsAtPath:fromStorageDirectory isDirectory:&isDir] && isDir) {
        printf("ASYNC STORAGE, old storage path does exist and we need to migrate\n");
        ...
else {
        printf("ASYNC STORAGE, old storage path does not exist\n");
}

Logs on startup:

ASYNC STORAGE, old storage path is nil ASYNC STORAGE, old storage path does not exist

I think the problem is here on line 528 where data is not serialized because it does not read the file

static NSString *RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut)
{
    if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
        printf("Storage file does exist\n");
        ...
        return entryString;
    }
    printf("Storage file does NOT exist\n");

    return nil;
}

Storage file does NOT exist

hit reload

Storage file does exist

force close and cold start

Storage file does NOT exist

I don't know why but it looks like the file you write to and the file you read from differ on cold start, so I logged the file written to and the file read from on the next cold start. And on the simulator they differ!

write

/Users/my.username/Library/Developer/CoreSimulator/Devices/0148E0A0-9A01-471F-A676-AD5E08693911/data/Containers/Data/Application/6B2E78F2-865E-450E-A8E8-E55FF7069539/Library/Application Support/redacted.bundle.id/RCTAsyncLocalStorage_V1/manifest.json

read

/Users/my.username/Library/Developer/CoreSimulator/Devices/0148E0A0-9A01-471F-A676-AD5E08693911/data/Containers/Data/Application/8F1C5550-6E39-45C0-A8BE-CBCD2458E5E9/Library/Application Support/redacted.bundle.id/RCTAsyncLocalStorage_V1/manifest.json

They differ in the application id, (6B2E78F2-865E-450E-A8E8-E55FF7069539 vs 8F1C5550-6E39-45C0-A8BE-CBCD2458E5E9)

The same thing happens on an actual device

write

/var/mobile/Containers/Data/Application/CAA95996-C39C-408A-9228-C68D8F19E00F/Library/Application Support/redacted.bundle.id/RCTAsyncLocalStorage_V1/manifest.json

read

/var/mobile/Containers/Data/Application/B9966AEA-6A28-4760-A3B6-85C4A9450CB0/Library/Application Support/redacted.bundle.id/RCTAsyncLocalStorage_V1/manifest.json

so it seems for simulators the reason the data isn't persisting is because you're storing data under a specific application directory that changes from run to run.

Is there a way for me to correct this app side? Or is it a bug on the package side that would need to be fixed? Storage directory name is constructed here

It seems very weird that this would only happen to my project

Edit: after reviewing a different project using this package, the changing of the application id does not happen after force close. So definitely project specific

krizzu commented 1 year ago

@TheHeumanModean This behavior (different paths for Dodcuments/Library data) exists since iOS 8, here's the technical note on that. AsyncStorage uses system APIs to retrieve the application data location so that the manifest file can be stored there.

I've never used react-native/async-storage nor have I used this plugin pre 1.17.3.

My bet would be that one of your dependencies is using AsyncStorage and causing this - I can suggest scanning node_modules to see if any library is using AsyncStorage.

TheHeumanModean commented 1 year ago

@krizzu

 grep -R --exclude-dir=@react-native-async-storage --include \*.ts --include \*.js "AsyncStorage" node_modules/

returns only results from react native, react native types, metro and the actual react-native async storage library

node_modules//@types/react-native/index.d.ts: * AsyncStorage is a simple, unencrypted, asynchronous, persistent, key-value storage

 ... More results from same file

node_modules//react-native/Libraries/Storage/AsyncStorage.js:const RCTAsyncStorage = NativeAsyncSQLiteDBStorage || NativeAsyncLocalStorage;

... More results from same file

node_modules//react-native/index.js:  get AsyncStorage(): AsyncStorage {

... More results from same file

node_modules//metro/node_modules/metro-react-native-babel-preset/src/configs/lazy-imports.js:  "AsyncStorage",
node_modules//metro-react-native-babel-preset/src/configs/lazy-imports.js:  "AsyncStorage",

So looks like thats not the issue unless you have a better way to find references to that library?

krizzu commented 1 year ago

Do you use .clear method by any chance somewhere?

Otherwise, I'd try the divide-and-conquer approach, starting with commenting out all imports in index.js, so that App would run and test if storage is removed on restart. Then bring back import by import, checking where the issue is reproducible.

TheHeumanModean commented 1 year ago

@krizzu issue is with one of our native modules, a minor release from them caused the problem. Of course its a highly obfuscated one so I don't know what caused it. May report back later with info but closing it for now as its not an issue with the async storage package.

MohsinChopDawg commented 1 year ago

i am using redux toolkit with redux-persist value save on android but on ios persist not working properly sometimes gives undefined or sometimes gives me value

santo4ul commented 1 year ago

Here is my observation and fix. Hope this helps someone

My problem: Data stored is not persisted after reboot of the device. My OS: Android 13

AsyncStorage can be exported using the following two ways,

  1. import AsyncStorage from '@react-native-async-storage/async-storage';
  2. import {AsyncStorage} from 'react-native';

With 1 above, my data is lost after re-boot of the device. WIth 2, the data is persisted and it works as expected. However, with 2 above, I get a warning that "AsyncStorage would removed from react-native..", Basically the console warning is suggesting to import using method 1 above and instead of 2.

I've decided to go ahead with 2 above for now and keep watching for fixes using 1.

Inalegwu commented 4 months ago

This is still an issue on ```@react-native-async-storage/async-storage@^1.23.1```` after much troubleshooting, much nothing new has come out of the issue aside what has already been mentioned above