--- ios/RNCAsyncStorage.h +++ ios/RNCAsyncStorage.h @@ -31,12 +31,12 @@ @property (nonatomic, readonly, getter=isValid) BOOL valid; +// NOTE(nikki): Added to allow scoped per Expo app +- (instancetype)initWithStorageDirectory:(NSString *)storageDirectory; + // Clear the RNCAsyncStorage data from native code - (void)clearAllData; -// For clearing data when the bridge may not exist, e.g. when logging out. -+ (void)clearAllData; - // Grab data from the cache. ResponseBlock result array will have an error at position 0, and an // array of arrays at position 1. - (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; --- ios/RNCAsyncStorage.m +++ ios/RNCAsyncStorage.m @@ -14,7 +14,9 @@ #import #import -static NSString *const RCTStorageDirectory = @"RCTAsyncLocalStorage_V1"; +// NOTE(kudo): Use Expo storage directory for backward compatibility +//static NSString *const RCTStorageDirectory = @"RCTAsyncLocalStorage_V1"; +static NSString *const RCTStorageDirectory = @"RCTAsyncLocalStorage"; static NSString *const RCTOldStorageDirectory = @"RNCAsyncLocalStorage_V1"; static NSString *const RCTExpoStorageDirectory = @"RCTAsyncLocalStorage"; static NSString *const RCTManifestFileName = @"manifest.json"; @@ -150,31 +152,11 @@ static NSString *RCTCreateStorageDirectoryPath(NSString *storageDir) return storageDirectoryPath; } -static NSString *RCTGetStorageDirectory() -{ - static NSString *storageDirectory = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - storageDirectory = RCTCreateStorageDirectoryPath(RCTStorageDirectory); - }); - return storageDirectory; -} - static NSString *RCTCreateManifestFilePath(NSString *storageDirectory) { return [storageDirectory stringByAppendingPathComponent:RCTManifestFileName]; } -static NSString *RCTGetManifestFilePath() -{ - static NSString *manifestFilePath = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - manifestFilePath = RCTCreateManifestFilePath(RCTStorageDirectory); - }); - return manifestFilePath; -} - // Only merges objects - all other types are just clobbered (including arrays) static BOOL RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) { @@ -203,51 +185,27 @@ static BOOL RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *so return modified; } -static dispatch_queue_t RCTGetMethodQueue() -{ - // We want all instances to share the same queue since they will be reading/writing the same - // files. - static dispatch_queue_t queue; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - queue = - dispatch_queue_create("com.facebook.react.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL); - }); - return queue; -} - -static NSCache *RCTGetCache() -{ - // We want all instances to share the same cache since they will be reading/writing the same - // files. - static NSCache *cache; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - cache = [NSCache new]; - cache.totalCostLimit = 2 * 1024 * 1024; // 2MB - -#if !TARGET_OS_OSX - // Clear cache in the event of a memory warning - [[NSNotificationCenter defaultCenter] - addObserverForName:UIApplicationDidReceiveMemoryWarningNotification - object:nil - queue:nil - usingBlock:^(__unused NSNotification *note) { - [cache removeAllObjects]; - }]; -#endif // !TARGET_OS_OSX - }); - return cache; -} - static BOOL RCTHasCreatedStorageDirectory = NO; -static NSDictionary *RCTDeleteStorageDirectory() + +// NOTE(nikki93): We replace with scoped implementations of: +// RCTGetStorageDirectory() +// RCTGetManifestFilePath() +// RCTGetMethodQueue() +// RCTGetCache() +// RCTDeleteStorageDirectory() + +#define RCTGetStorageDirectory() _storageDirectory +#define RCTGetManifestFilePath() _manifestFilePath +#define RCTGetMethodQueue() self.methodQueue +#define RCTGetCache() self.cache + +static NSDictionary *RCTDeleteStorageDirectory(NSString *storageDirectory) { - NSError *error; - [[NSFileManager defaultManager] removeItemAtPath:RCTGetStorageDirectory() error:&error]; - RCTHasCreatedStorageDirectory = NO; - return error ? RCTMakeError(@"Failed to delete storage directory.", error, nil) : nil; + NSError *error; + [[NSFileManager defaultManager] removeItemAtPath:storageDirectory error:&error]; + return error ? RCTMakeError(@"Failed to delete storage directory.", error, nil) : nil; } +#define RCTDeleteStorageDirectory() RCTDeleteStorageDirectory(_storageDirectory) static NSDate *RCTManifestModificationDate(NSString *manifestFilePath) { @@ -285,35 +243,7 @@ static void RCTStorageDirectoryMigrate(NSString *oldDirectoryPath, NSString *newDirectoryPath, BOOL shouldCleanupOldDirectory) { - NSError *error; - // Migrate data by copying old storage directory to new storage directory location - if (![[NSFileManager defaultManager] copyItemAtPath:oldDirectoryPath - toPath:newDirectoryPath - error:&error]) { - // the new storage directory "Application Support/[bundleID]/RCTAsyncLocalStorage_V1" seems - // unable to migrate because folder "Application Support/[bundleID]" doesn't exist.. create - // this folder and attempt folder copying again - if (error != nil && error.code == 4 && - [newDirectoryPath isEqualToString:RCTGetStorageDirectory()]) { - NSError *error = nil; - _createStorageDirectory(RCTCreateStorageDirectoryPath(@""), &error); - if (error == nil) { - RCTStorageDirectoryMigrate( - oldDirectoryPath, newDirectoryPath, shouldCleanupOldDirectory); - } else { - RCTStorageDirectoryMigrationLogError( - @"Failed to create storage directory during migration.", error); - } - } else { - RCTStorageDirectoryMigrationLogError( - @"Failed to copy old storage directory to new storage directory location during " - @"migration", - error); - } - } else if (shouldCleanupOldDirectory) { - // If copying succeeds, remove old storage directory - RCTStorageDirectoryCleanupOld(oldDirectoryPath); - } + assert(false); } /** @@ -406,12 +336,49 @@ RCTStorageDirectoryMigrationCheck(NSString *fromStorageDirectory, #pragma mark - RNCAsyncStorage +@interface RNCAsyncStorage () + +@property (nonatomic, copy) NSString *storageDirectory; +@property (nonatomic, copy) NSString *manifestFilePath; + +@end + @implementation RNCAsyncStorage { BOOL _haveSetup; // The manifest is a dictionary of all keys with small values inlined. Null values indicate // values that are stored in separate files (as opposed to nil values which don't exist). The // manifest is read off disk at startup, and written to disk after all mutations. NSMutableDictionary *_manifest; + NSCache *_cache; + dispatch_once_t _cacheOnceToken; +} + +// NOTE(nikki93): Prevents the module from being auto-initialized and allows us to pass our own `storageDirectory` ++ (NSString *)moduleName { return @"RCTAsyncLocalStorage"; } +- (instancetype)initWithStorageDirectory:(NSString *)storageDirectory +{ + if ((self = [super init])) { + _storageDirectory = storageDirectory; + _manifestFilePath = [RCTGetStorageDirectory() stringByAppendingPathComponent:RCTManifestFileName]; + } + return self; +} + +// NOTE(nikki93): Use the default `methodQueue` since instances have different storage directories +@synthesize methodQueue = _methodQueue; + +- (NSCache *)cache +{ + dispatch_once(&_cacheOnceToken, ^{ + _cache = [NSCache new]; + _cache.totalCostLimit = 2 * 1024 * 1024; // 2MB + + // Clear cache in the event of a memory warning + [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(__unused NSNotification *note) { + [_cache removeAllObjects]; + }]; + }); + return _cache; } + (BOOL)requiresMainQueueSetup @@ -421,6 +388,7 @@ RCTStorageDirectoryMigrationCheck(NSString *fromStorageDirectory, - (instancetype)init { + assert(false); if (!(self = [super init])) { return nil; } @@ -444,13 +412,6 @@ RCTStorageDirectoryMigrationCheck(NSString *fromStorageDirectory, return self; } -RCT_EXPORT_MODULE() - -- (dispatch_queue_t)methodQueue -{ - return RCTGetMethodQueue(); -} - - (void)clearAllData { dispatch_async(RCTGetMethodQueue(), ^{ @@ -460,14 +421,6 @@ RCT_EXPORT_MODULE() }); } -+ (void)clearAllData -{ - dispatch_async(RCTGetMethodQueue(), ^{ - [RCTGetCache() removeAllObjects]; - RCTDeleteStorageDirectory(); - }); -} - - (void)invalidate { if (_clearOnInvalidate) { @@ -505,12 +458,13 @@ RCT_EXPORT_MODULE() #endif NSError *error = nil; - if (!RCTHasCreatedStorageDirectory) { - _createStorageDirectory(RCTGetStorageDirectory(), &error); - if (error) { - return RCTMakeError(@"Failed to create storage directory.", error, nil); - } - RCTHasCreatedStorageDirectory = YES; + // NOTE(nikki93): `withIntermediateDirectories:YES` makes this idempotent + [[NSFileManager defaultManager] createDirectoryAtPath:RCTGetStorageDirectory() + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (error) { + return RCTMakeError(@"Failed to create storage directory.", error, nil); } if (!_haveSetup) { @@ -521,11 +475,14 @@ RCT_EXPORT_MODULE() // by default, we want to exclude AsyncStorage data from backup isExcludedFromBackup = @YES; } - RCTAsyncStorageSetExcludedFromBackup(RCTCreateStorageDirectoryPath(RCTStorageDirectory), - isExcludedFromBackup); + // NOTE(kudo): We don't enable iCloud backup for Expo Go + // RCTAsyncStorageSetExcludedFromBackup(RCTCreateStorageDirectoryPath(RCTStorageDirectory), + // isExcludedFromBackup); NSDictionary *errorOut = nil; - NSString *serialized = RCTReadFile(RCTCreateStorageDirectoryPath(RCTGetManifestFilePath()), + // NOTE(kudo): Keep data in Documents rather than Application Support for backward compatibility + // NSString *serialized = RCTReadFile(RCTCreateStorageDirectoryPath(RCTGetManifestFilePath()) + NSString *serialized = RCTReadFile(RCTGetManifestFilePath(), RCTManifestFileName, &errorOut); if (!serialized) { @@ -561,7 +518,9 @@ RCT_EXPORT_MODULE() { NSError *error; NSString *serialized = RCTJSONStringify(_manifest, &error); - [serialized writeToFile:RCTCreateStorageDirectoryPath(RCTGetManifestFilePath()) + // NOTE(kudo): Keep data in Documents rather than Application Support for backward compatibility + // [serialized writeToFile:RCTCreateStorageDirectoryPath(RCTGetManifestFilePath()) + [serialized writeToFile:RCTGetManifestFilePath() atomically:YES encoding:NSUTF8StringEncoding error:&error];