From db0aba4640d178c126ad16d37e214b4e3fa9c3aa Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Thu, 26 Mar 2015 02:58:06 -0700 Subject: [PATCH 01/75] [ReactNative] s/ReactKit/React/g --- RCTAsyncLocalStorage.h | 31 +++++ RCTAsyncLocalStorage.m | 299 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 RCTAsyncLocalStorage.h create mode 100644 RCTAsyncLocalStorage.m diff --git a/RCTAsyncLocalStorage.h b/RCTAsyncLocalStorage.h new file mode 100644 index 00000000..31ff98c6 --- /dev/null +++ b/RCTAsyncLocalStorage.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTBridgeModule.h" + +/** + * A simple, asynchronous, persistent, key-value storage system designed as a + * backend to the AsyncStorage JS module, which is modeled after LocalStorage. + * + * Current implementation stores small values in serialized dictionary and + * larger values in separate files. Since we use a serial file queue + * `RKFileQueue`, reading/writing from multiple threads should be perceived as + * being atomic, unless someone bypasses the `RCTAsyncLocalStorage` API. + * + * Keys and values must always be strings or an error is returned. + */ +@interface RCTAsyncLocalStorage : NSObject + +- (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; +- (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback; +- (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; +- (void)clear:(RCTResponseSenderBlock)callback; +- (void)getAllKeys:(RCTResponseSenderBlock)callback; + +@end diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m new file mode 100644 index 00000000..e1daeb2f --- /dev/null +++ b/RCTAsyncLocalStorage.m @@ -0,0 +1,299 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTAsyncLocalStorage.h" + +#import + +#import +#import + +#import "RCTLog.h" +#import "RCTUtils.h" + +static NSString *const kStorageDir = @"RCTAsyncLocalStorage_V1"; +static NSString *const kManifestFilename = @"manifest.json"; +static const NSUInteger kInlineValueThreshold = 100; + +#pragma mark - Static helper functions + +static id RCTErrorForKey(NSString *key) +{ + if (![key isKindOfClass:[NSString class]]) { + return RCTMakeAndLogError(@"Invalid key - must be a string. Key: ", key, @{@"key": key}); + } else if (key.length < 1) { + return RCTMakeAndLogError(@"Invalid key - must be at least one character. Key: ", key, @{@"key": key}); + } else { + return nil; + } +} + +static void RCTAppendError(id error, NSMutableArray **errors) +{ + if (error && errors) { + if (!*errors) { + *errors = [NSMutableArray new]; + } + [*errors addObject:error]; + } +} + +static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut) +{ + if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { + NSError *error; + NSStringEncoding encoding; + NSString *entryString = [NSString stringWithContentsOfFile:filePath usedEncoding:&encoding error:&error]; + if (error) { + *errorOut = RCTMakeError(@"Failed to read storage file.", error, @{@"key": key}); + } else if (encoding != NSUTF8StringEncoding) { + *errorOut = RCTMakeError(@"Incorrect encoding of storage file: ", @(encoding), @{@"key": key}); + } else { + return entryString; + } + } + return nil; +} + +static dispatch_queue_t RCTFileQueue(void) +{ + static dispatch_queue_t fileQueue = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // All JS is single threaded, so a serial queue is our only option. + fileQueue = dispatch_queue_create("com.facebook.rkFile", DISPATCH_QUEUE_SERIAL); + dispatch_set_target_queue(fileQueue, + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); + }); + + return fileQueue; +} + +#pragma mark - RCTAsyncLocalStorage + +@implementation RCTAsyncLocalStorage +{ + 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; + NSString *_manifestPath; + NSString *_storageDirectory; +} + +- (NSString *)_filePathForKey:(NSString *)key +{ + NSString *safeFileName = RCTMD5Hash(key); + return [_storageDirectory stringByAppendingPathComponent:safeFileName]; +} + +- (id)_ensureSetup +{ + if (_haveSetup) { + return nil; + } + NSString *documentDirectory = + [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + NSURL *homeURL = [NSURL fileURLWithPath:documentDirectory isDirectory:YES]; + _storageDirectory = [[homeURL URLByAppendingPathComponent:kStorageDir isDirectory:YES] path]; + NSError *error; + [[NSFileManager defaultManager] createDirectoryAtPath:_storageDirectory + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (error) { + return RCTMakeError(@"Failed to create storage directory.", error, nil); + } + _manifestPath = [_storageDirectory stringByAppendingPathComponent:kManifestFilename]; + NSDictionary *errorOut; + NSString *serialized = RCTReadFile(_manifestPath, nil, &errorOut); + _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [NSMutableDictionary new]; + if (error) { + RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); + _manifest = [NSMutableDictionary new]; + } + _haveSetup = YES; + return nil; +} + +- (id)_writeManifest:(NSMutableArray **)errors +{ + NSError *error; + NSString *serialized = RCTJSONStringify(_manifest, &error); + [serialized writeToFile:_manifestPath atomically:YES encoding:NSUTF8StringEncoding error:&error]; + id errorOut; + if (error) { + errorOut = RCTMakeError(@"Failed to write manifest file.", error, nil); + RCTAppendError(errorOut, errors); + } + return errorOut; +} + +- (id)_appendItemForKey:(NSString *)key toArray:(NSMutableArray *)result +{ + id errorOut = RCTErrorForKey(key); + if (errorOut) { + return errorOut; + } + id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. + if (value == [NSNull null]) { + NSString *filePath = [self _filePathForKey:key]; + value = RCTReadFile(filePath, key, &errorOut); + } + [result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure. + return errorOut; +} + +- (id)_writeEntry:(NSArray *)entry +{ + if (![entry isKindOfClass:[NSArray class]] || entry.count != 2) { + return RCTMakeAndLogError(@"Entries must be arrays of the form [key: string, value: string], got: ", entry, nil); + } + if (![entry[1] isKindOfClass:[NSString class]]) { + return RCTMakeAndLogError(@"Values must be strings, got: ", entry[1], entry[0]); + } + NSString *key = entry[0]; + id errorOut = RCTErrorForKey(key); + if (errorOut) { + return errorOut; + } + NSString *value = entry[1]; + NSString *filePath = [self _filePathForKey:key]; + NSError *error; + if (value.length <= kInlineValueThreshold) { + if (_manifest[key] && _manifest[key] != [NSNull null]) { + // If the value already existed but wasn't inlined, remove the old file. + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + } + _manifest[key] = value; + return nil; + } + [value writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error]; + if (error) { + errorOut = RCTMakeError(@"Failed to write value.", error, @{@"key": key}); + } else { + _manifest[key] = [NSNull null]; // Mark existence of file with null, any other value is inline data. + } + return errorOut; +} + +#pragma mark - Exported JS Functions + +- (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + if (!callback) { + RCTLogError(@"Called getItem without a callback."); + return; + } + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut], [NSNull null]]); + return; + } + NSMutableArray *errors; + NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; + for (NSString *key in keys) { + id keyError = [self _appendItemForKey:key toArray:result]; + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + callback(@[errors ?: [NSNull null], result]); + }); +} + +- (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (NSArray *entry in kvPairs) { + id keyError = [self _writeEntry:entry]; + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } + }); +} + +- (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (NSString *key in keys) { + id keyError = RCTErrorForKey(key); + if (!keyError) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + [_manifest removeObjectForKey:key]; + } + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } + }); +} + +- (void)clear:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (!errorOut) { + NSError *error; + for (NSString *key in _manifest) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; + } + [_manifest removeAllObjects]; + errorOut = [self _writeManifest:nil]; + } + if (callback) { + callback(@[errorOut ?: [NSNull null]]); + } + }); +} + +- (void)getAllKeys:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[errorOut, [NSNull null]]); + } else { + callback(@[[NSNull null], [_manifest allKeys]]); + } + }); +} + +@end From ddd2bce1f8b473e3b7b47b9f8154d65ddf37f0d3 Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Thu, 26 Mar 2015 06:32:01 -0700 Subject: [PATCH 02/75] Updates from Thu 26 Mar - [React Native] Fix incorrect if-statement in RCTGeolocation | Alex Akers - [ReactNative] s/ReactKit/React/g | Tadeu Zagallo - [React Native] View border support | Nick Lockwood - [Assets] Allow scripts to override assetRoots | Amjad Masad - [ReactNative] Navigator docs | Eric Vicenti - [ReactNative] License headers and renaming | Eric Vicenti - [React Native] Add CocoaPods spec | Tadeu Zagallo - Added explicit types for all view properties | Nick Lockwood - [ReactNative] s/ReactNavigator/Navigator/ | Tadeu Zagallo - [ReactNative] Add copyright header for code copied from the jQuery UI project | Martin Konicek - [ReactNative] PanResponder documentation | Eric Vicenti --- RCTAsyncLocalStorage.h | 31 +++++ RCTAsyncLocalStorage.m | 299 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 RCTAsyncLocalStorage.h create mode 100644 RCTAsyncLocalStorage.m diff --git a/RCTAsyncLocalStorage.h b/RCTAsyncLocalStorage.h new file mode 100644 index 00000000..31ff98c6 --- /dev/null +++ b/RCTAsyncLocalStorage.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTBridgeModule.h" + +/** + * A simple, asynchronous, persistent, key-value storage system designed as a + * backend to the AsyncStorage JS module, which is modeled after LocalStorage. + * + * Current implementation stores small values in serialized dictionary and + * larger values in separate files. Since we use a serial file queue + * `RKFileQueue`, reading/writing from multiple threads should be perceived as + * being atomic, unless someone bypasses the `RCTAsyncLocalStorage` API. + * + * Keys and values must always be strings or an error is returned. + */ +@interface RCTAsyncLocalStorage : NSObject + +- (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; +- (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback; +- (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; +- (void)clear:(RCTResponseSenderBlock)callback; +- (void)getAllKeys:(RCTResponseSenderBlock)callback; + +@end diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m new file mode 100644 index 00000000..e1daeb2f --- /dev/null +++ b/RCTAsyncLocalStorage.m @@ -0,0 +1,299 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTAsyncLocalStorage.h" + +#import + +#import +#import + +#import "RCTLog.h" +#import "RCTUtils.h" + +static NSString *const kStorageDir = @"RCTAsyncLocalStorage_V1"; +static NSString *const kManifestFilename = @"manifest.json"; +static const NSUInteger kInlineValueThreshold = 100; + +#pragma mark - Static helper functions + +static id RCTErrorForKey(NSString *key) +{ + if (![key isKindOfClass:[NSString class]]) { + return RCTMakeAndLogError(@"Invalid key - must be a string. Key: ", key, @{@"key": key}); + } else if (key.length < 1) { + return RCTMakeAndLogError(@"Invalid key - must be at least one character. Key: ", key, @{@"key": key}); + } else { + return nil; + } +} + +static void RCTAppendError(id error, NSMutableArray **errors) +{ + if (error && errors) { + if (!*errors) { + *errors = [NSMutableArray new]; + } + [*errors addObject:error]; + } +} + +static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut) +{ + if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { + NSError *error; + NSStringEncoding encoding; + NSString *entryString = [NSString stringWithContentsOfFile:filePath usedEncoding:&encoding error:&error]; + if (error) { + *errorOut = RCTMakeError(@"Failed to read storage file.", error, @{@"key": key}); + } else if (encoding != NSUTF8StringEncoding) { + *errorOut = RCTMakeError(@"Incorrect encoding of storage file: ", @(encoding), @{@"key": key}); + } else { + return entryString; + } + } + return nil; +} + +static dispatch_queue_t RCTFileQueue(void) +{ + static dispatch_queue_t fileQueue = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // All JS is single threaded, so a serial queue is our only option. + fileQueue = dispatch_queue_create("com.facebook.rkFile", DISPATCH_QUEUE_SERIAL); + dispatch_set_target_queue(fileQueue, + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); + }); + + return fileQueue; +} + +#pragma mark - RCTAsyncLocalStorage + +@implementation RCTAsyncLocalStorage +{ + 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; + NSString *_manifestPath; + NSString *_storageDirectory; +} + +- (NSString *)_filePathForKey:(NSString *)key +{ + NSString *safeFileName = RCTMD5Hash(key); + return [_storageDirectory stringByAppendingPathComponent:safeFileName]; +} + +- (id)_ensureSetup +{ + if (_haveSetup) { + return nil; + } + NSString *documentDirectory = + [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + NSURL *homeURL = [NSURL fileURLWithPath:documentDirectory isDirectory:YES]; + _storageDirectory = [[homeURL URLByAppendingPathComponent:kStorageDir isDirectory:YES] path]; + NSError *error; + [[NSFileManager defaultManager] createDirectoryAtPath:_storageDirectory + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (error) { + return RCTMakeError(@"Failed to create storage directory.", error, nil); + } + _manifestPath = [_storageDirectory stringByAppendingPathComponent:kManifestFilename]; + NSDictionary *errorOut; + NSString *serialized = RCTReadFile(_manifestPath, nil, &errorOut); + _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [NSMutableDictionary new]; + if (error) { + RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); + _manifest = [NSMutableDictionary new]; + } + _haveSetup = YES; + return nil; +} + +- (id)_writeManifest:(NSMutableArray **)errors +{ + NSError *error; + NSString *serialized = RCTJSONStringify(_manifest, &error); + [serialized writeToFile:_manifestPath atomically:YES encoding:NSUTF8StringEncoding error:&error]; + id errorOut; + if (error) { + errorOut = RCTMakeError(@"Failed to write manifest file.", error, nil); + RCTAppendError(errorOut, errors); + } + return errorOut; +} + +- (id)_appendItemForKey:(NSString *)key toArray:(NSMutableArray *)result +{ + id errorOut = RCTErrorForKey(key); + if (errorOut) { + return errorOut; + } + id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. + if (value == [NSNull null]) { + NSString *filePath = [self _filePathForKey:key]; + value = RCTReadFile(filePath, key, &errorOut); + } + [result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure. + return errorOut; +} + +- (id)_writeEntry:(NSArray *)entry +{ + if (![entry isKindOfClass:[NSArray class]] || entry.count != 2) { + return RCTMakeAndLogError(@"Entries must be arrays of the form [key: string, value: string], got: ", entry, nil); + } + if (![entry[1] isKindOfClass:[NSString class]]) { + return RCTMakeAndLogError(@"Values must be strings, got: ", entry[1], entry[0]); + } + NSString *key = entry[0]; + id errorOut = RCTErrorForKey(key); + if (errorOut) { + return errorOut; + } + NSString *value = entry[1]; + NSString *filePath = [self _filePathForKey:key]; + NSError *error; + if (value.length <= kInlineValueThreshold) { + if (_manifest[key] && _manifest[key] != [NSNull null]) { + // If the value already existed but wasn't inlined, remove the old file. + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + } + _manifest[key] = value; + return nil; + } + [value writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error]; + if (error) { + errorOut = RCTMakeError(@"Failed to write value.", error, @{@"key": key}); + } else { + _manifest[key] = [NSNull null]; // Mark existence of file with null, any other value is inline data. + } + return errorOut; +} + +#pragma mark - Exported JS Functions + +- (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + if (!callback) { + RCTLogError(@"Called getItem without a callback."); + return; + } + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut], [NSNull null]]); + return; + } + NSMutableArray *errors; + NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; + for (NSString *key in keys) { + id keyError = [self _appendItemForKey:key toArray:result]; + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + callback(@[errors ?: [NSNull null], result]); + }); +} + +- (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (NSArray *entry in kvPairs) { + id keyError = [self _writeEntry:entry]; + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } + }); +} + +- (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (NSString *key in keys) { + id keyError = RCTErrorForKey(key); + if (!keyError) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + [_manifest removeObjectForKey:key]; + } + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } + }); +} + +- (void)clear:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (!errorOut) { + NSError *error; + for (NSString *key in _manifest) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; + } + [_manifest removeAllObjects]; + errorOut = [self _writeManifest:nil]; + } + if (callback) { + callback(@[errorOut ?: [NSNull null]]); + } + }); +} + +- (void)getAllKeys:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[errorOut, [NSNull null]]); + } else { + callback(@[[NSNull null], [_manifest allKeys]]); + } + }); +} + +@end From 8e7eca4ed1980403153cbb3d58408c8f0bc836ee Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Tue, 31 Mar 2015 14:46:15 -0700 Subject: [PATCH 03/75] Fix a crash in RCTAsyncLocalStorage when the value is not a string. Summary: When you forget to pass the value parameter to AsyncStorage.setItem the entire app would crash instead of showing a useful error message. The problem was that the error function used in the file expected a dictionary but was passed the value of the key which caused the crash. Closes https://github.com/facebook/react-native/pull/535 Github Author: Janic Duplessis Test Plan: Imported from GitHub, without a `Test Plan:` line. --- RCTAsyncLocalStorage.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index e1daeb2f..95fb383e 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -157,7 +157,7 @@ - (id)_writeEntry:(NSArray *)entry return RCTMakeAndLogError(@"Entries must be arrays of the form [key: string, value: string], got: ", entry, nil); } if (![entry[1] isKindOfClass:[NSString class]]) { - return RCTMakeAndLogError(@"Values must be strings, got: ", entry[1], entry[0]); + return RCTMakeAndLogError(@"Values must be strings, got: ", entry[1], @{@"key": entry[0]}); } NSString *key = entry[0]; id errorOut = RCTErrorForKey(key); From 768e2c23259d07ac02a0c691ed2e2a800c10dd53 Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Tue, 31 Mar 2015 19:01:48 -0700 Subject: [PATCH 04/75] Updates from Tue 31 Mar - Bugfix/require module regexp | Amjad Masad - [ReactNative] RCTView's shadowOffset is of float type, not CGFloat | Kevin Gozali - Fix WebView automaticallyAdjustContentInsets error | Spencer Ahrens - [react-native] map view - add onTouch** props | Jiajie Zhu - [react-native] Fix documentation extraction for View | Ben Alpert - [ReactNative] Add few hints in the UI | Alex Kotliarskyi - Adding `scrollWithoutAnimationTo` method for ScrollViews | Felix Oghina - [ScrollView] Add "bounces" property to ScrollView propTypes | Spencer Ahrens - Fix a crash in RCTAsyncLocalStorage when the value is not a string. | Spencer Ahrens - [ReactNative] Remove global MutationObserver to fix Bluebird feature detection | Christopher Chedeau - [catalyst] fix typo | Jiajie Zhu - [react-packager] check-in bluebird | Amjad Masad - [react-native] v0.3.1 | Amjad Masad - [Pod] Preserve header directory structure | Alex Akers - [react-native] Bring React.render behavior in line with web | Ben Alpert - Expose html prop on WebView | Spencer Ahrens - missing '.' in ListView.DataSource example | Christopher Chedeau - [react-native] Support returning null from a component | Ben Alpert - [react-native] Fix race condition in removeSubviewsFromContainerWithID: | Ben Alpert --- RCTAsyncLocalStorage.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index e1daeb2f..95fb383e 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -157,7 +157,7 @@ - (id)_writeEntry:(NSArray *)entry return RCTMakeAndLogError(@"Entries must be arrays of the form [key: string, value: string], got: ", entry, nil); } if (![entry[1] isKindOfClass:[NSString class]]) { - return RCTMakeAndLogError(@"Values must be strings, got: ", entry[1], entry[0]); + return RCTMakeAndLogError(@"Values must be strings, got: ", entry[1], @{@"key": entry[0]}); } NSString *key = entry[0]; id errorOut = RCTErrorForKey(key); From a76cc63834b513157adccb9b525e8c5295012cac Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Wed, 8 Apr 2015 05:42:43 -0700 Subject: [PATCH 05/75] Added non-class-scanning-based approach fror registering js methods --- RCTAsyncLocalStorage.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 95fb383e..feec1704 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -88,6 +88,8 @@ @implementation RCTAsyncLocalStorage NSString *_storageDirectory; } +RCT_EXPORT_MODULE() + - (NSString *)_filePathForKey:(NSString *)key { NSString *safeFileName = RCTMD5Hash(key); From a9fbac97f8131f3fbb6508040f32ec20f81983ed Mon Sep 17 00:00:00 2001 From: Alex Akers Date: Wed, 8 Apr 2015 08:52:48 -0700 Subject: [PATCH 06/75] [React Native] RCT_EXPORT lvl.2 --- RCTAsyncLocalStorage.m | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index feec1704..8e6d414c 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -188,10 +188,9 @@ - (id)_writeEntry:(NSArray *)entry #pragma mark - Exported JS Functions -- (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback +RCT_EXPORT_METHOD(multiGet:(NSArray *)keys + callback:(RCTResponseSenderBlock)callback) { - RCT_EXPORT(); - if (!callback) { RCTLogError(@"Called getItem without a callback."); return; @@ -214,10 +213,9 @@ - (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback }); } -- (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback +RCT_EXPORT_METHOD(multiSet:(NSArray *)kvPairs + callback:(RCTResponseSenderBlock)callback) { - RCT_EXPORT(); - dispatch_async(RCTFileQueue(), ^{ id errorOut = [self _ensureSetup]; if (errorOut) { @@ -236,10 +234,9 @@ - (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback }); } -- (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback +RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys + callback:(RCTResponseSenderBlock)callback) { - RCT_EXPORT(); - dispatch_async(RCTFileQueue(), ^{ id errorOut = [self _ensureSetup]; if (errorOut) { @@ -263,10 +260,8 @@ - (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback }); } -- (void)clear:(RCTResponseSenderBlock)callback +RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { - RCT_EXPORT(); - dispatch_async(RCTFileQueue(), ^{ id errorOut = [self _ensureSetup]; if (!errorOut) { @@ -284,10 +279,8 @@ - (void)clear:(RCTResponseSenderBlock)callback }); } -- (void)getAllKeys:(RCTResponseSenderBlock)callback +RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback) { - RCT_EXPORT(); - dispatch_async(RCTFileQueue(), ^{ id errorOut = [self _ensureSetup]; if (errorOut) { From 1b6988b8ec7677b65d6c1a10ee0ae21364085f86 Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Thu, 9 Apr 2015 08:46:53 -0700 Subject: [PATCH 07/75] Updates from Thu 9 Apr - [React Native] Fix RCTText crashes | Alex Akers - Ensure that NSLocationWhenInUseUsageDescription is set, throw error if not | Alex Kotliarskyi - [ReactNative] fix exception handler method name | Spencer Ahrens - [ReactNative] Re-configure horizontal swipe animations | Eric Vicenti - [ReactNative] : apply the fontWeight correctly if fontFamily style is also present | Kevin Gozali - [MAdMan] Dimensions.get('window') considered harmful | Philipp von Weitershausen - Navigator: Changed transitioner background color to 'transparent' | Eric Vicenti - [react-native] Listen on all IPv6 interfaces | Ben Alpert - [react-packager] Don't depend on error.stack being available | Amjad Masad - [ReactNative] fixup AnimationExperimental a bit | Spencer Ahrens - [react-packager] Implement new style asset packaging (with dimensions) | Amjad Masad - [React Native] RCT_EXPORT lvl.2 | Alex Akers - [react_native] Implement TextInput end editing | Andrei Coman - [react_native] Make TextInput focus, blur, dismiss and show keyboard work | Andrei Coman - Added non-class-scanning-based approach fror registering js methods | Nick Lockwood - [ReactNative] Update package.json | Christopher Chedeau - [ReactNative] Do flow check when running packager | Spencer Ahrens - [ReactNative] Fix typo/bug in Navigator._completeTransition | Eric Vicenti - [ReactNative] Fix Navigator exception when touching during transition | Eric Vicenti - [ReactNative] Remove bridge retaining cycles | Tadeu Zagallo - [ReactNative] Fix and re-add WebView executor | Tadeu Zagallo --- RCTAsyncLocalStorage.m | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 95fb383e..8e6d414c 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -88,6 +88,8 @@ @implementation RCTAsyncLocalStorage NSString *_storageDirectory; } +RCT_EXPORT_MODULE() + - (NSString *)_filePathForKey:(NSString *)key { NSString *safeFileName = RCTMD5Hash(key); @@ -186,10 +188,9 @@ - (id)_writeEntry:(NSArray *)entry #pragma mark - Exported JS Functions -- (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback +RCT_EXPORT_METHOD(multiGet:(NSArray *)keys + callback:(RCTResponseSenderBlock)callback) { - RCT_EXPORT(); - if (!callback) { RCTLogError(@"Called getItem without a callback."); return; @@ -212,10 +213,9 @@ - (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback }); } -- (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback +RCT_EXPORT_METHOD(multiSet:(NSArray *)kvPairs + callback:(RCTResponseSenderBlock)callback) { - RCT_EXPORT(); - dispatch_async(RCTFileQueue(), ^{ id errorOut = [self _ensureSetup]; if (errorOut) { @@ -234,10 +234,9 @@ - (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback }); } -- (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback +RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys + callback:(RCTResponseSenderBlock)callback) { - RCT_EXPORT(); - dispatch_async(RCTFileQueue(), ^{ id errorOut = [self _ensureSetup]; if (errorOut) { @@ -261,10 +260,8 @@ - (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback }); } -- (void)clear:(RCTResponseSenderBlock)callback +RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { - RCT_EXPORT(); - dispatch_async(RCTFileQueue(), ^{ id errorOut = [self _ensureSetup]; if (!errorOut) { @@ -282,10 +279,8 @@ - (void)clear:(RCTResponseSenderBlock)callback }); } -- (void)getAllKeys:(RCTResponseSenderBlock)callback +RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback) { - RCT_EXPORT(); - dispatch_async(RCTFileQueue(), ^{ id errorOut = [self _ensureSetup]; if (errorOut) { From e077b3456392ee773ba857af451e0dc59d9af6f8 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Sat, 18 Apr 2015 10:43:20 -0700 Subject: [PATCH 08/75] Implemented thread control for exported methods --- RCTAsyncLocalStorage.m | 155 ++++++++++++++++++----------------------- 1 file changed, 68 insertions(+), 87 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 8e6d414c..2c01161d 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -61,20 +61,6 @@ static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut return nil; } -static dispatch_queue_t RCTFileQueue(void) -{ - static dispatch_queue_t fileQueue = NULL; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - // All JS is single threaded, so a serial queue is our only option. - fileQueue = dispatch_queue_create("com.facebook.rkFile", DISPATCH_QUEUE_SERIAL); - dispatch_set_target_queue(fileQueue, - dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); - }); - - return fileQueue; -} - #pragma mark - RCTAsyncLocalStorage @implementation RCTAsyncLocalStorage @@ -90,6 +76,11 @@ @implementation RCTAsyncLocalStorage RCT_EXPORT_MODULE() +- (dispatch_queue_t)methodQueue +{ + return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL); +} + - (NSString *)_filePathForKey:(NSString *)key { NSString *safeFileName = RCTMD5Hash(key); @@ -196,99 +187,89 @@ - (id)_writeEntry:(NSArray *)entry return; } - dispatch_async(RCTFileQueue(), ^{ - id errorOut = [self _ensureSetup]; - if (errorOut) { - callback(@[@[errorOut], [NSNull null]]); - return; - } - NSMutableArray *errors; - NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; - for (NSString *key in keys) { - id keyError = [self _appendItemForKey:key toArray:result]; - RCTAppendError(keyError, &errors); - } - [self _writeManifest:&errors]; - callback(@[errors ?: [NSNull null], result]); - }); + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut], [NSNull null]]); + return; + } + NSMutableArray *errors; + NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; + for (NSString *key in keys) { + id keyError = [self _appendItemForKey:key toArray:result]; + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + callback(@[errors ?: [NSNull null], result]); } RCT_EXPORT_METHOD(multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback) { - dispatch_async(RCTFileQueue(), ^{ - id errorOut = [self _ensureSetup]; - if (errorOut) { - callback(@[@[errorOut]]); - return; - } - NSMutableArray *errors; - for (NSArray *entry in kvPairs) { - id keyError = [self _writeEntry:entry]; - RCTAppendError(keyError, &errors); - } - [self _writeManifest:&errors]; - if (callback) { - callback(@[errors ?: [NSNull null]]); - } - }); + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (NSArray *entry in kvPairs) { + id keyError = [self _writeEntry:entry]; + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } } RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback) { - dispatch_async(RCTFileQueue(), ^{ - id errorOut = [self _ensureSetup]; - if (errorOut) { - callback(@[@[errorOut]]); - return; - } - NSMutableArray *errors; - for (NSString *key in keys) { - id keyError = RCTErrorForKey(key); - if (!keyError) { - NSString *filePath = [self _filePathForKey:key]; - [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; - [_manifest removeObjectForKey:key]; - } - RCTAppendError(keyError, &errors); - } - [self _writeManifest:&errors]; - if (callback) { - callback(@[errors ?: [NSNull null]]); + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (NSString *key in keys) { + id keyError = RCTErrorForKey(key); + if (!keyError) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + [_manifest removeObjectForKey:key]; } - }); + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } } RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { - dispatch_async(RCTFileQueue(), ^{ - id errorOut = [self _ensureSetup]; - if (!errorOut) { - NSError *error; - for (NSString *key in _manifest) { - NSString *filePath = [self _filePathForKey:key]; - [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; - } - [_manifest removeAllObjects]; - errorOut = [self _writeManifest:nil]; - } - if (callback) { - callback(@[errorOut ?: [NSNull null]]); + id errorOut = [self _ensureSetup]; + if (!errorOut) { + NSError *error; + for (NSString *key in _manifest) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; } - }); + [_manifest removeAllObjects]; + errorOut = [self _writeManifest:nil]; + } + if (callback) { + callback(@[errorOut ?: [NSNull null]]); + } } RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback) { - dispatch_async(RCTFileQueue(), ^{ - id errorOut = [self _ensureSetup]; - if (errorOut) { - callback(@[errorOut, [NSNull null]]); - } else { - callback(@[[NSNull null], [_manifest allKeys]]); - } - }); + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[errorOut, [NSNull null]]); + } else { + callback(@[[NSNull null], [_manifest allKeys]]); + } } @end From 0579fa1c7958f12b8ab4e74d6a4a00255941e89d Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Wed, 3 Jun 2015 16:57:08 -0700 Subject: [PATCH 09/75] [ReactNative] Implement merge functionality for AsyncStorage --- RCTAsyncLocalStorage.m | 73 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 2c01161d..76f7fa88 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -61,6 +61,34 @@ static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut return nil; } +// Only merges objects - all other types are just clobbered (including arrays) +static void RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) +{ + for (NSString *key in source) { + id sourceValue = source[key]; + if ([sourceValue isKindOfClass:[NSDictionary class]]) { + id destinationValue = destination[key]; + NSMutableDictionary *nestedDestination; + if ([destinationValue classForCoder] == [NSMutableDictionary class]) { + nestedDestination = destinationValue; + } else { + if ([destinationValue isKindOfClass:[NSDictionary class]]) { + // Ideally we wouldn't eagerly copy here... + nestedDestination = [destinationValue mutableCopy]; + } else { + destination[key] = [sourceValue copy]; + } + } + if (nestedDestination) { + RCTMergeRecursive(nestedDestination, sourceValue); + destination[key] = nestedDestination; + } + } else { + destination[key] = sourceValue; + } + } +} + #pragma mark - RCTAsyncLocalStorage @implementation RCTAsyncLocalStorage @@ -135,13 +163,19 @@ - (id)_appendItemForKey:(NSString *)key toArray:(NSMutableArray *)result if (errorOut) { return errorOut; } + id value = [self _getValueForKey:key errorOut:&errorOut]; + [result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure. + return errorOut; +} + +- (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut +{ id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. if (value == [NSNull null]) { NSString *filePath = [self _filePathForKey:key]; - value = RCTReadFile(filePath, key, &errorOut); + value = RCTReadFile(filePath, key, errorOut); } - [result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure. - return errorOut; + return value; } - (id)_writeEntry:(NSArray *)entry @@ -198,7 +232,6 @@ - (id)_writeEntry:(NSArray *)entry id keyError = [self _appendItemForKey:key toArray:result]; RCTAppendError(keyError, &errors); } - [self _writeManifest:&errors]; callback(@[errors ?: [NSNull null], result]); } @@ -221,6 +254,38 @@ - (id)_writeEntry:(NSArray *)entry } } +RCT_EXPORT_METHOD(multiMerge:(NSArray *)kvPairs + callback:(RCTResponseSenderBlock)callback) +{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (__strong NSArray *entry in kvPairs) { + id keyError; + NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError]; + if (keyError) { + RCTAppendError(keyError, &errors); + } else { + if (value) { + NSMutableDictionary *mergedVal = [RCTJSONParseMutable(value, &keyError) mutableCopy]; + RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &keyError)); + entry = @[entry[0], RCTJSONStringify(mergedVal, &keyError)]; + } + if (!keyError) { + keyError = [self _writeEntry:entry]; + } + RCTAppendError(keyError, &errors); + } + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } +} + RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback) { From b808ba2f7dacc8f3d7a9ccd8fa51f90b07cf9a27 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Fri, 12 Jun 2015 11:05:01 -0700 Subject: [PATCH 10/75] [ReactNative] Use RCTNullIfNill and (id)kCFNull Summary: @public Use consistent `null` handling: `value || null` -> `RCTNullIfNil(value)` `value == null ? nil : value` -> `RCTNilIfNull(value)` `[NSNull null]` -> `(id)kCFNull` Test Plan: The tests should be enough. --- RCTAsyncLocalStorage.m | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 76f7fa88..a7f38928 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -164,14 +164,14 @@ - (id)_appendItemForKey:(NSString *)key toArray:(NSMutableArray *)result return errorOut; } id value = [self _getValueForKey:key errorOut:&errorOut]; - [result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure. + [result addObject:@[key, RCTNullIfNil(value)]]; // Insert null if missing or failure. return errorOut; } - (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut { id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. - if (value == [NSNull null]) { + if (value == (id)kCFNull) { NSString *filePath = [self _filePathForKey:key]; value = RCTReadFile(filePath, key, errorOut); } @@ -195,7 +195,7 @@ - (id)_writeEntry:(NSArray *)entry NSString *filePath = [self _filePathForKey:key]; NSError *error; if (value.length <= kInlineValueThreshold) { - if (_manifest[key] && _manifest[key] != [NSNull null]) { + if (_manifest[key] && _manifest[key] != (id)kCFNull) { // If the value already existed but wasn't inlined, remove the old file. [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; } @@ -206,7 +206,7 @@ - (id)_writeEntry:(NSArray *)entry if (error) { errorOut = RCTMakeError(@"Failed to write value.", error, @{@"key": key}); } else { - _manifest[key] = [NSNull null]; // Mark existence of file with null, any other value is inline data. + _manifest[key] = (id)kCFNull; // Mark existence of file with null, any other value is inline data. } return errorOut; } @@ -223,7 +223,7 @@ - (id)_writeEntry:(NSArray *)entry id errorOut = [self _ensureSetup]; if (errorOut) { - callback(@[@[errorOut], [NSNull null]]); + callback(@[@[errorOut], (id)kCFNull]); return; } NSMutableArray *errors; @@ -232,7 +232,7 @@ - (id)_writeEntry:(NSArray *)entry id keyError = [self _appendItemForKey:key toArray:result]; RCTAppendError(keyError, &errors); } - callback(@[errors ?: [NSNull null], result]); + callback(@[RCTNullIfNil(errors), result]); } RCT_EXPORT_METHOD(multiSet:(NSArray *)kvPairs @@ -250,7 +250,7 @@ - (id)_writeEntry:(NSArray *)entry } [self _writeManifest:&errors]; if (callback) { - callback(@[errors ?: [NSNull null]]); + callback(@[RCTNullIfNil(errors)]); } } @@ -282,7 +282,7 @@ - (id)_writeEntry:(NSArray *)entry } [self _writeManifest:&errors]; if (callback) { - callback(@[errors ?: [NSNull null]]); + callback(@[RCTNullIfNil(errors)]); } } @@ -306,7 +306,7 @@ - (id)_writeEntry:(NSArray *)entry } [self _writeManifest:&errors]; if (callback) { - callback(@[errors ?: [NSNull null]]); + callback(@[RCTNullIfNil(errors)]); } } @@ -323,7 +323,7 @@ - (id)_writeEntry:(NSArray *)entry errorOut = [self _writeManifest:nil]; } if (callback) { - callback(@[errorOut ?: [NSNull null]]); + callback(@[RCTNullIfNil(errorOut)]); } } @@ -331,9 +331,9 @@ - (id)_writeEntry:(NSArray *)entry { id errorOut = [self _ensureSetup]; if (errorOut) { - callback(@[errorOut, [NSNull null]]); + callback(@[errorOut, (id)kCFNull]); } else { - callback(@[[NSNull null], [_manifest allKeys]]); + callback(@[(id)kCFNull, [_manifest allKeys]]); } } From 61249b114e71824bffaa92bbbe61194153609d97 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Fri, 12 Jun 2015 11:05:01 -0700 Subject: [PATCH 11/75] [ReactNative] Use RCTNullIfNill and (id)kCFNull Summary: @public Use consistent `null` handling: `value || null` -> `RCTNullIfNil(value)` `value == null ? nil : value` -> `RCTNilIfNull(value)` `[NSNull null]` -> `(id)kCFNull` Test Plan: The tests should be enough. --- RCTAsyncLocalStorage.m | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 76f7fa88..a7f38928 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -164,14 +164,14 @@ - (id)_appendItemForKey:(NSString *)key toArray:(NSMutableArray *)result return errorOut; } id value = [self _getValueForKey:key errorOut:&errorOut]; - [result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure. + [result addObject:@[key, RCTNullIfNil(value)]]; // Insert null if missing or failure. return errorOut; } - (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut { id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. - if (value == [NSNull null]) { + if (value == (id)kCFNull) { NSString *filePath = [self _filePathForKey:key]; value = RCTReadFile(filePath, key, errorOut); } @@ -195,7 +195,7 @@ - (id)_writeEntry:(NSArray *)entry NSString *filePath = [self _filePathForKey:key]; NSError *error; if (value.length <= kInlineValueThreshold) { - if (_manifest[key] && _manifest[key] != [NSNull null]) { + if (_manifest[key] && _manifest[key] != (id)kCFNull) { // If the value already existed but wasn't inlined, remove the old file. [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; } @@ -206,7 +206,7 @@ - (id)_writeEntry:(NSArray *)entry if (error) { errorOut = RCTMakeError(@"Failed to write value.", error, @{@"key": key}); } else { - _manifest[key] = [NSNull null]; // Mark existence of file with null, any other value is inline data. + _manifest[key] = (id)kCFNull; // Mark existence of file with null, any other value is inline data. } return errorOut; } @@ -223,7 +223,7 @@ - (id)_writeEntry:(NSArray *)entry id errorOut = [self _ensureSetup]; if (errorOut) { - callback(@[@[errorOut], [NSNull null]]); + callback(@[@[errorOut], (id)kCFNull]); return; } NSMutableArray *errors; @@ -232,7 +232,7 @@ - (id)_writeEntry:(NSArray *)entry id keyError = [self _appendItemForKey:key toArray:result]; RCTAppendError(keyError, &errors); } - callback(@[errors ?: [NSNull null], result]); + callback(@[RCTNullIfNil(errors), result]); } RCT_EXPORT_METHOD(multiSet:(NSArray *)kvPairs @@ -250,7 +250,7 @@ - (id)_writeEntry:(NSArray *)entry } [self _writeManifest:&errors]; if (callback) { - callback(@[errors ?: [NSNull null]]); + callback(@[RCTNullIfNil(errors)]); } } @@ -282,7 +282,7 @@ - (id)_writeEntry:(NSArray *)entry } [self _writeManifest:&errors]; if (callback) { - callback(@[errors ?: [NSNull null]]); + callback(@[RCTNullIfNil(errors)]); } } @@ -306,7 +306,7 @@ - (id)_writeEntry:(NSArray *)entry } [self _writeManifest:&errors]; if (callback) { - callback(@[errors ?: [NSNull null]]); + callback(@[RCTNullIfNil(errors)]); } } @@ -323,7 +323,7 @@ - (id)_writeEntry:(NSArray *)entry errorOut = [self _writeManifest:nil]; } if (callback) { - callback(@[errorOut ?: [NSNull null]]); + callback(@[RCTNullIfNil(errorOut)]); } } @@ -331,9 +331,9 @@ - (id)_writeEntry:(NSArray *)entry { id errorOut = [self _ensureSetup]; if (errorOut) { - callback(@[errorOut, [NSNull null]]); + callback(@[errorOut, (id)kCFNull]); } else { - callback(@[[NSNull null], [_manifest allKeys]]); + callback(@[(id)kCFNull, [_manifest allKeys]]); } } From cd1fe8933c83b993bc816a6747c95bb09dc2e53e Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Thu, 18 Jun 2015 09:29:31 -0700 Subject: [PATCH 12/75] [RN Events] clear disk cache on logout --- RCTAsyncLocalStorage.h | 8 +++++- RCTAsyncLocalStorage.m | 64 ++++++++++++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/RCTAsyncLocalStorage.h b/RCTAsyncLocalStorage.h index 31ff98c6..0a1aaacb 100644 --- a/RCTAsyncLocalStorage.h +++ b/RCTAsyncLocalStorage.h @@ -8,6 +8,7 @@ */ #import "RCTBridgeModule.h" +#import "RCTInvalidating.h" /** * A simple, asynchronous, persistent, key-value storage system designed as a @@ -20,7 +21,9 @@ * * Keys and values must always be strings or an error is returned. */ -@interface RCTAsyncLocalStorage : NSObject +@interface RCTAsyncLocalStorage : NSObject + +@property (nonatomic, assign) BOOL clearOnInvalidate; - (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; - (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback; @@ -28,4 +31,7 @@ - (void)clear:(RCTResponseSenderBlock)callback; - (void)getAllKeys:(RCTResponseSenderBlock)callback; +// For clearing data when the bridge may not exist, e.g. when logging out. ++ (NSError *)clearAllData; + @end diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index a7f38928..5ed2e48e 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -61,6 +61,13 @@ static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut return nil; } +static NSString *RCTGetStorageDir() +{ + NSString *documentDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + NSURL *homeURL = [NSURL fileURLWithPath:documentDirectory isDirectory:YES]; + return [[homeURL URLByAppendingPathComponent:kStorageDir isDirectory:YES] path]; +} + // Only merges objects - all other types are just clobbered (including arrays) static void RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) { @@ -106,7 +113,39 @@ @implementation RCTAsyncLocalStorage - (dispatch_queue_t)methodQueue { - return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL); + // 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; +} + ++ (NSError *)clearAllData +{ + NSError *error; + [[NSFileManager defaultManager] removeItemAtPath:RCTGetStorageDir() error:&error]; + return error; +} + +- (void)invalidate +{ + if (_clearOnInvalidate) { + [RCTAsyncLocalStorage clearAllData]; + } + _clearOnInvalidate = NO; + _manifest = [[NSMutableDictionary alloc] init]; + _haveSetup = NO; +} +- (BOOL)isValid +{ + return _haveSetup; +} + +- (void)dealloc +{ + [self invalidate]; } - (NSString *)_filePathForKey:(NSString *)key @@ -120,10 +159,7 @@ - (id)_ensureSetup if (_haveSetup) { return nil; } - NSString *documentDirectory = - [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; - NSURL *homeURL = [NSURL fileURLWithPath:documentDirectory isDirectory:YES]; - _storageDirectory = [[homeURL URLByAppendingPathComponent:kStorageDir isDirectory:YES] path]; + _storageDirectory = RCTGetStorageDir(); NSError *error; [[NSFileManager defaultManager] createDirectoryAtPath:_storageDirectory withIntermediateDirectories:YES @@ -135,10 +171,10 @@ - (id)_ensureSetup _manifestPath = [_storageDirectory stringByAppendingPathComponent:kManifestFilename]; NSDictionary *errorOut; NSString *serialized = RCTReadFile(_manifestPath, nil, &errorOut); - _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [NSMutableDictionary new]; + _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [[NSMutableDictionary alloc] init]; if (error) { RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); - _manifest = [NSMutableDictionary new]; + _manifest = [[NSMutableDictionary alloc] init]; } _haveSetup = YES; return nil; @@ -312,18 +348,10 @@ - (id)_writeEntry:(NSArray *)entry RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { - id errorOut = [self _ensureSetup]; - if (!errorOut) { - NSError *error; - for (NSString *key in _manifest) { - NSString *filePath = [self _filePathForKey:key]; - [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; - } - [_manifest removeAllObjects]; - errorOut = [self _writeManifest:nil]; - } + _manifest = [[NSMutableDictionary alloc] init]; + NSError *error = [RCTAsyncLocalStorage clearAllData]; if (callback) { - callback(@[RCTNullIfNil(errorOut)]); + callback(@[RCTNullIfNil(error)]); } } From 9021c69ef33bc9c95f8132ccd051c9717ceba6e8 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 22 Jun 2015 14:37:11 -0700 Subject: [PATCH 13/75] Fixed AsyncLocalStorage bug --- RCTAsyncLocalStorage.h | 2 +- RCTAsyncLocalStorage.m | 82 +++++++++++++++++++++++++----------------- 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/RCTAsyncLocalStorage.h b/RCTAsyncLocalStorage.h index 0a1aaacb..4fd1064a 100644 --- a/RCTAsyncLocalStorage.h +++ b/RCTAsyncLocalStorage.h @@ -32,6 +32,6 @@ - (void)getAllKeys:(RCTResponseSenderBlock)callback; // For clearing data when the bridge may not exist, e.g. when logging out. -+ (NSError *)clearAllData; ++ (void)clearAllData; @end diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 5ed2e48e..50b5312f 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -96,6 +96,26 @@ static void RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *so } } +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 BOOL RCTHasCreatedStorageDirectory = NO; +static NSError *RCTDeleteStorageDirectory() +{ + NSError *error; + [[NSFileManager defaultManager] removeItemAtPath:RCTGetStorageDir() error:&error]; + RCTHasCreatedStorageDirectory = NO; + return error; +} + #pragma mark - RCTAsyncLocalStorage @implementation RCTAsyncLocalStorage @@ -113,26 +133,20 @@ @implementation RCTAsyncLocalStorage - (dispatch_queue_t)methodQueue { - // 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; + return RCTGetMethodQueue(); } -+ (NSError *)clearAllData ++ (void)clearAllData { - NSError *error; - [[NSFileManager defaultManager] removeItemAtPath:RCTGetStorageDir() error:&error]; - return error; + dispatch_async(RCTGetMethodQueue(), ^{ + RCTDeleteStorageDirectory(); + }); } - (void)invalidate { if (_clearOnInvalidate) { - [RCTAsyncLocalStorage clearAllData]; + RCTDeleteStorageDirectory(); } _clearOnInvalidate = NO; _manifest = [[NSMutableDictionary alloc] init]; @@ -156,27 +170,31 @@ - (NSString *)_filePathForKey:(NSString *)key - (id)_ensureSetup { - if (_haveSetup) { - return nil; - } - _storageDirectory = RCTGetStorageDir(); - NSError *error; - [[NSFileManager defaultManager] createDirectoryAtPath:_storageDirectory - withIntermediateDirectories:YES - attributes:nil - error:&error]; - if (error) { - return RCTMakeError(@"Failed to create storage directory.", error, nil); + RCTAssertThread(RCTGetMethodQueue(), @"Must be executed on storage thread"); + + NSError *error = nil; + if (!RCTHasCreatedStorageDirectory) { + _storageDirectory = RCTGetStorageDir(); + [[NSFileManager defaultManager] createDirectoryAtPath:_storageDirectory + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (error) { + return RCTMakeError(@"Failed to create storage directory.", error, nil); + } + RCTHasCreatedStorageDirectory = YES; } - _manifestPath = [_storageDirectory stringByAppendingPathComponent:kManifestFilename]; - NSDictionary *errorOut; - NSString *serialized = RCTReadFile(_manifestPath, nil, &errorOut); - _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [[NSMutableDictionary alloc] init]; - if (error) { - RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); - _manifest = [[NSMutableDictionary alloc] init]; + if (!_haveSetup) { + _manifestPath = [_storageDirectory stringByAppendingPathComponent:kManifestFilename]; + NSDictionary *errorOut; + NSString *serialized = RCTReadFile(_manifestPath, nil, &errorOut); + _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [[NSMutableDictionary alloc] init]; + if (error) { + RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); + _manifest = [[NSMutableDictionary alloc] init]; + } + _haveSetup = YES; } - _haveSetup = YES; return nil; } @@ -349,7 +367,7 @@ - (id)_writeEntry:(NSArray *)entry RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { _manifest = [[NSMutableDictionary alloc] init]; - NSError *error = [RCTAsyncLocalStorage clearAllData]; + NSError *error = RCTDeleteStorageDirectory(); if (callback) { callback(@[RCTNullIfNil(error)]); } From 16b4789a8b22e437474f8b44a4382d134a02501a Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 23 Jun 2015 14:17:31 -0700 Subject: [PATCH 14/75] Fixed async local storage --- RCTAsyncLocalStorage.m | 44 ++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 50b5312f..8300cc86 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -17,9 +17,9 @@ #import "RCTLog.h" #import "RCTUtils.h" -static NSString *const kStorageDir = @"RCTAsyncLocalStorage_V1"; -static NSString *const kManifestFilename = @"manifest.json"; -static const NSUInteger kInlineValueThreshold = 100; +static NSString *const RCTStorageDirectory = @"RCTAsyncLocalStorage_V1"; +static NSString *const RCTManifestFileName = @"manifest.json"; +static const NSUInteger RCTInlineValueThreshold = 100; #pragma mark - Static helper functions @@ -61,11 +61,25 @@ static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut return nil; } -static NSString *RCTGetStorageDir() +static NSString *RCTGetStorageDirectory() { - NSString *documentDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; - NSURL *homeURL = [NSURL fileURLWithPath:documentDirectory isDirectory:YES]; - return [[homeURL URLByAppendingPathComponent:kStorageDir isDirectory:YES] path]; + static NSString *storageDirectory = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + storageDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + storageDirectory = [storageDirectory stringByAppendingPathComponent:RCTStorageDirectory]; + }); + return storageDirectory; +} + +static NSString *RCTGetManifestFilePath() +{ + static NSString *manifestFilePath = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + manifestFilePath = [RCTGetStorageDirectory() stringByAppendingPathComponent:RCTManifestFileName]; + }); + return manifestFilePath; } // Only merges objects - all other types are just clobbered (including arrays) @@ -111,7 +125,7 @@ static dispatch_queue_t RCTGetMethodQueue() static NSError *RCTDeleteStorageDirectory() { NSError *error; - [[NSFileManager defaultManager] removeItemAtPath:RCTGetStorageDir() error:&error]; + [[NSFileManager defaultManager] removeItemAtPath:RCTGetStorageDirectory() error:&error]; RCTHasCreatedStorageDirectory = NO; return error; } @@ -125,8 +139,6 @@ @implementation RCTAsyncLocalStorage // 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; - NSString *_manifestPath; - NSString *_storageDirectory; } RCT_EXPORT_MODULE() @@ -165,7 +177,7 @@ - (void)dealloc - (NSString *)_filePathForKey:(NSString *)key { NSString *safeFileName = RCTMD5Hash(key); - return [_storageDirectory stringByAppendingPathComponent:safeFileName]; + return [RCTGetStorageDirectory() stringByAppendingPathComponent:safeFileName]; } - (id)_ensureSetup @@ -174,8 +186,7 @@ - (id)_ensureSetup NSError *error = nil; if (!RCTHasCreatedStorageDirectory) { - _storageDirectory = RCTGetStorageDir(); - [[NSFileManager defaultManager] createDirectoryAtPath:_storageDirectory + [[NSFileManager defaultManager] createDirectoryAtPath:RCTGetStorageDirectory() withIntermediateDirectories:YES attributes:nil error:&error]; @@ -185,9 +196,8 @@ - (id)_ensureSetup RCTHasCreatedStorageDirectory = YES; } if (!_haveSetup) { - _manifestPath = [_storageDirectory stringByAppendingPathComponent:kManifestFilename]; NSDictionary *errorOut; - NSString *serialized = RCTReadFile(_manifestPath, nil, &errorOut); + NSString *serialized = RCTReadFile(RCTGetManifestFilePath(), nil, &errorOut); _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [[NSMutableDictionary alloc] init]; if (error) { RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); @@ -202,7 +212,7 @@ - (id)_writeManifest:(NSMutableArray **)errors { NSError *error; NSString *serialized = RCTJSONStringify(_manifest, &error); - [serialized writeToFile:_manifestPath atomically:YES encoding:NSUTF8StringEncoding error:&error]; + [serialized writeToFile:RCTGetManifestFilePath() atomically:YES encoding:NSUTF8StringEncoding error:&error]; id errorOut; if (error) { errorOut = RCTMakeError(@"Failed to write manifest file.", error, nil); @@ -248,7 +258,7 @@ - (id)_writeEntry:(NSArray *)entry NSString *value = entry[1]; NSString *filePath = [self _filePathForKey:key]; NSError *error; - if (value.length <= kInlineValueThreshold) { + if (value.length <= RCTInlineValueThreshold) { if (_manifest[key] && _manifest[key] != (id)kCFNull) { // If the value already existed but wasn't inlined, remove the old file. [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; From e5ade77902be97a67d7bcc47fc8769e4dfabdf5c Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Fri, 14 Aug 2015 01:59:42 -0700 Subject: [PATCH 15/75] Remove isValid from RCTInvalidating Summary: We only actually use it on RCTBridge and RCTJavaScriptExecutor, so add it to these interfaces explicitly --- RCTAsyncLocalStorage.h | 2 ++ RCTAsyncLocalStorage.m | 1 + 2 files changed, 3 insertions(+) diff --git a/RCTAsyncLocalStorage.h b/RCTAsyncLocalStorage.h index 4fd1064a..e7e871b0 100644 --- a/RCTAsyncLocalStorage.h +++ b/RCTAsyncLocalStorage.h @@ -25,6 +25,8 @@ @property (nonatomic, assign) BOOL clearOnInvalidate; +@property (nonatomic, readonly, getter=isValid) BOOL valid; + - (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; - (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback; - (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 8300cc86..923a3a14 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -164,6 +164,7 @@ - (void)invalidate _manifest = [[NSMutableDictionary alloc] init]; _haveSetup = NO; } + - (BOOL)isValid { return _haveSetup; From 951692c042733e67dbb412f32c2051d0b6294d43 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 17 Aug 2015 07:35:34 -0700 Subject: [PATCH 16/75] Convert alloc/init to new to please linter --- RCTAsyncLocalStorage.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 923a3a14..262b578b 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -161,7 +161,7 @@ - (void)invalidate RCTDeleteStorageDirectory(); } _clearOnInvalidate = NO; - _manifest = [[NSMutableDictionary alloc] init]; + _manifest = [NSMutableDictionary new]; _haveSetup = NO; } @@ -199,10 +199,10 @@ - (id)_ensureSetup if (!_haveSetup) { NSDictionary *errorOut; NSString *serialized = RCTReadFile(RCTGetManifestFilePath(), nil, &errorOut); - _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [[NSMutableDictionary alloc] init]; + _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [NSMutableDictionary new]; if (error) { RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); - _manifest = [[NSMutableDictionary alloc] init]; + _manifest = [NSMutableDictionary new]; } _haveSetup = YES; } @@ -377,7 +377,7 @@ - (id)_writeEntry:(NSArray *)entry RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { - _manifest = [[NSMutableDictionary alloc] init]; + _manifest = [NSMutableDictionary new]; NSError *error = RCTDeleteStorageDirectory(); if (callback) { callback(@[RCTNullIfNil(error)]); From 2c2a30853612611af9c52f40881359d53e55a81a Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 24 Aug 2015 09:14:33 -0100 Subject: [PATCH 17/75] Ran Convert > To Modern Objective C Syntax --- RCTAsyncLocalStorage.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 262b578b..1ef86eb8 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -66,7 +66,7 @@ static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut static NSString *storageDirectory = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - storageDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + storageDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; storageDirectory = [storageDirectory stringByAppendingPathComponent:RCTStorageDirectory]; }); return storageDirectory; @@ -390,7 +390,7 @@ - (id)_writeEntry:(NSArray *)entry if (errorOut) { callback(@[errorOut, (id)kCFNull]); } else { - callback(@[(id)kCFNull, [_manifest allKeys]]); + callback(@[(id)kCFNull, _manifest.allKeys]); } } From eec09e68959f94939fe901da10b4e36df1b7db2e Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Mon, 14 Sep 2015 15:35:58 +0100 Subject: [PATCH 18/75] Release React Native for Android This is an early release and there are several things that are known not to work if you're porting your iOS app to Android. See the Known Issues guide on the website. We will work with the community to reach platform parity with iOS. --- AsyncLocalStorageUtil.java | 147 +++++++++++++++ AsyncStorageErrorUtil.java | 47 +++++ AsyncStorageModule.java | 369 +++++++++++++++++++++++++++++++++++++ 3 files changed, 563 insertions(+) create mode 100644 AsyncLocalStorageUtil.java create mode 100644 AsyncStorageErrorUtil.java create mode 100644 AsyncStorageModule.java diff --git a/AsyncLocalStorageUtil.java b/AsyncLocalStorageUtil.java new file mode 100644 index 00000000..36340f0a --- /dev/null +++ b/AsyncLocalStorageUtil.java @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.storage; + +import javax.annotation.Nullable; + +import java.util.Arrays; +import java.util.Iterator; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.text.TextUtils; + +import com.facebook.react.bridge.ReadableArray; + +import org.json.JSONException; +import org.json.JSONObject; + +import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.KEY_COLUMN; +import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.TABLE_CATALYST; +import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.VALUE_COLUMN; + +/** + * Helper for database operations. + */ +/* package */ class AsyncLocalStorageUtil { + + /** + * Build the String required for an SQL select statement: + * WHERE key IN (?, ?, ..., ?) + * without 'WHERE' and with selectionCount '?' + */ + /* package */ static String buildKeySelection(int selectionCount) { + String[] list = new String[selectionCount]; + Arrays.fill(list, "?"); + return KEY_COLUMN + " IN (" + TextUtils.join(", ", list) + ")"; + } + + /** + * Build the String[] arguments needed for an SQL selection, i.e.: + * {a, b, c} + * to be used in the SQL select statement: WHERE key in (?, ?, ?) + */ + /* package */ static String[] buildKeySelectionArgs(ReadableArray keys) { + String[] selectionArgs = new String[keys.size()]; + for (int keyIndex = 0; keyIndex < keys.size(); keyIndex++) { + selectionArgs[keyIndex] = keys.getString(keyIndex); + } + return selectionArgs; + } + + /** + * Returns the value of the given key, or null if not found. + */ + /* package */ static @Nullable String getItemImpl(SQLiteDatabase db, String key) { + String[] columns = {VALUE_COLUMN}; + String[] selectionArgs = {key}; + + Cursor cursor = db.query( + TABLE_CATALYST, + columns, + KEY_COLUMN + "=?", + selectionArgs, + null, + null, + null); + + try { + if (!cursor.moveToFirst()) { + return null; + } else { + return cursor.getString(0); + } + } finally { + cursor.close(); + } + } + + /** + * Sets the value for the key given, returns true if successful, false otherwise. + */ + /* package */ static boolean setItemImpl(SQLiteDatabase db, String key, String value) { + ContentValues contentValues = new ContentValues(); + contentValues.put(KEY_COLUMN, key); + contentValues.put(VALUE_COLUMN, value); + + long inserted = db.insertWithOnConflict( + TABLE_CATALYST, + null, + contentValues, + SQLiteDatabase.CONFLICT_REPLACE); + + return (-1 != inserted); + } + + /** + * Does the actual merge of the (key, value) pair with the value stored in the database. + * NB: This assumes that a database lock is already in effect! + * @return the errorCode of the operation + */ + /* package */ static boolean mergeImpl(SQLiteDatabase db, String key, String value) + throws JSONException { + String oldValue = getItemImpl(db, key); + String newValue; + + if (oldValue == null) { + newValue = value; + } else { + JSONObject oldJSON = new JSONObject(oldValue); + JSONObject newJSON = new JSONObject(value); + deepMergeInto(oldJSON, newJSON); + newValue = oldJSON.toString(); + } + + return setItemImpl(db, key, newValue); + } + + /** + * Merges two {@link JSONObject}s. The newJSON object will be merged with the oldJSON object by + * either overriding its values, or merging them (if the values of the same key in both objects + * are of type {@link JSONObject}). oldJSON will contain the result of this merge. + */ + private static void deepMergeInto(JSONObject oldJSON, JSONObject newJSON) + throws JSONException { + Iterator keys = newJSON.keys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + + JSONObject newJSONObject = newJSON.optJSONObject(key); + JSONObject oldJSONObject = oldJSON.optJSONObject(key); + if (newJSONObject != null && oldJSONObject != null) { + deepMergeInto(oldJSONObject, newJSONObject); + oldJSON.put(key, oldJSONObject); + } else { + oldJSON.put(key, newJSON.get(key)); + } + } + } +} diff --git a/AsyncStorageErrorUtil.java b/AsyncStorageErrorUtil.java new file mode 100644 index 00000000..75f25617 --- /dev/null +++ b/AsyncStorageErrorUtil.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.storage; + +import javax.annotation.Nullable; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; + +/** + * Helper class for database errors. + */ +public class AsyncStorageErrorUtil { + + /** + * Create Error object to be passed back to the JS callback. + */ + /* package */ static WritableMap getError(@Nullable String key, String errorMessage) { + WritableMap errorMap = Arguments.createMap(); + errorMap.putString("message", errorMessage); + if (key != null) { + errorMap.putString("key", key); + } + return errorMap; + } + + /* package */ static WritableMap getInvalidKeyError(@Nullable String key) { + return getError(key, "Invalid key"); + } + + /* package */ static WritableMap getInvalidValueError(@Nullable String key) { + return getError(key, "Invalid Value"); + } + + /* package */ static WritableMap getDBError(@Nullable String key) { + return getError(key, "Database Error"); + } + + +} diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java new file mode 100644 index 00000000..601528a0 --- /dev/null +++ b/AsyncStorageModule.java @@ -0,0 +1,369 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.storage; + +import javax.annotation.Nullable; + +import java.util.HashSet; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; + +import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.GuardedAsyncTask; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.SetBuilder; +import com.facebook.react.modules.common.ModuleDataCleaner; + +import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.KEY_COLUMN; +import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.TABLE_CATALYST; +import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.VALUE_COLUMN; + +public final class AsyncStorageModule + extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable { + + private @Nullable SQLiteDatabase mDb; + private boolean mShuttingDown = false; + + public AsyncStorageModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return "AsyncSQLiteDBStorage"; + } + + @Override + public void initialize() { + super.initialize(); + mShuttingDown = false; + } + + @Override + public void onCatalystInstanceDestroy() { + mShuttingDown = true; + if (mDb != null && mDb.isOpen()) { + mDb.close(); + mDb = null; + } + } + + @Override + public void clearSensitiveData() { + // Clear local storage. If fails, crash, since the app is potentially in a bad state and could + // cause a privacy violation. We're still not recovering from this well, but at least the error + // will be reported to the server. + clear( + new Callback() { + @Override + public void invoke(Object... args) { + if (args.length > 0) { + throw new RuntimeException("Clearing AsyncLocalStorage failed: " + args[0]); + } + FLog.d(ReactConstants.TAG, "Cleaned AsyncLocalStorage."); + } + }); + } + + /** + * Given an array of keys, this returns a map of (key, value) pairs for the keys found, and + * (key, null) for the keys that haven't been found. + */ + @ReactMethod + public void multiGet(final ReadableArray keys, final Callback callback) { + if (keys == null) { + callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null), null); + return; + } + + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + if (!ensureDatabase()) { + callback.invoke(AsyncStorageErrorUtil.getDBError(null), null); + return; + } + + String[] columns = {KEY_COLUMN, VALUE_COLUMN}; + HashSet keysRemaining = SetBuilder.newHashSet(); + WritableArray data = Arguments.createArray(); + Cursor cursor = Assertions.assertNotNull(mDb).query( + TABLE_CATALYST, + columns, + AsyncLocalStorageUtil.buildKeySelection(keys.size()), + AsyncLocalStorageUtil.buildKeySelectionArgs(keys), + null, + null, + null); + + try { + if (cursor.getCount() != keys.size()) { + // some keys have not been found - insert them with null into the final array + for (int keyIndex = 0; keyIndex < keys.size(); keyIndex++) { + keysRemaining.add(keys.getString(keyIndex)); + } + } + + if (cursor.moveToFirst()) { + do { + WritableArray row = Arguments.createArray(); + row.pushString(cursor.getString(0)); + row.pushString(cursor.getString(1)); + data.pushArray(row); + keysRemaining.remove(cursor.getString(0)); + } while (cursor.moveToNext()); + + } + } catch (Exception e) { + FLog.w(ReactConstants.TAG, "Exception in database multiGet ", e); + callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()), null); + } finally { + cursor.close(); + } + + for (String key : keysRemaining) { + WritableArray row = Arguments.createArray(); + row.pushString(key); + row.pushNull(); + data.pushArray(row); + } + keysRemaining.clear(); + callback.invoke(null, data); + } + }.execute(); + } + + /** + * Inserts multiple (key, value) pairs. If one or more of the pairs cannot be inserted, this will + * return AsyncLocalStorageFailure, but all other pairs will have been inserted. + * The insertion will replace conflicting (key, value) pairs. + */ + @ReactMethod + public void multiSet(final ReadableArray keyValueArray, final Callback callback) { + if (keyValueArray.size() == 0) { + callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null)); + return; + } + + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + if (!ensureDatabase()) { + callback.invoke(AsyncStorageErrorUtil.getDBError(null)); + return; + } + + String sql = "INSERT OR REPLACE INTO " + TABLE_CATALYST + " VALUES (?, ?);"; + SQLiteStatement statement = Assertions.assertNotNull(mDb).compileStatement(sql); + mDb.beginTransaction(); + try { + for (int idx=0; idx < keyValueArray.size(); idx++) { + if (keyValueArray.getArray(idx).size() != 2) { + callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null)); + return; + } + if (keyValueArray.getArray(idx).getString(0) == null) { + callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null)); + return; + } + if (keyValueArray.getArray(idx).getString(1) == null) { + callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null)); + return; + } + + statement.clearBindings(); + statement.bindString(1, keyValueArray.getArray(idx).getString(0)); + statement.bindString(2, keyValueArray.getArray(idx).getString(1)); + statement.execute(); + } + mDb.setTransactionSuccessful(); + } catch (Exception e) { + FLog.w(ReactConstants.TAG, "Exception in database multiSet ", e); + callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); + } finally { + mDb.endTransaction(); + } + callback.invoke(); + } + }.execute(); + } + + /** + * Removes all rows of the keys given. + */ + @ReactMethod + public void multiRemove(final ReadableArray keys, final Callback callback) { + if (keys.size() == 0) { + callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null)); + return; + } + + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + if (!ensureDatabase()) { + callback.invoke(AsyncStorageErrorUtil.getDBError(null)); + return; + } + + try { + Assertions.assertNotNull(mDb).delete( + TABLE_CATALYST, + AsyncLocalStorageUtil.buildKeySelection(keys.size()), + AsyncLocalStorageUtil.buildKeySelectionArgs(keys)); + } catch (Exception e) { + FLog.w(ReactConstants.TAG, "Exception in database multiRemove ", e); + callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); + } + callback.invoke(); + } + }.execute(); + } + + /** + * Given an array of (key, value) pairs, this will merge the given values with the stored values + * of the given keys, if they exist. + */ + @ReactMethod + public void multiMerge(final ReadableArray keyValueArray, final Callback callback) { + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + if (!ensureDatabase()) { + callback.invoke(AsyncStorageErrorUtil.getDBError(null)); + return; + } + Assertions.assertNotNull(mDb).beginTransaction(); + try { + for (int idx = 0; idx < keyValueArray.size(); idx++) { + if (keyValueArray.getArray(idx).size() != 2) { + callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null)); + return; + } + + if (keyValueArray.getArray(idx).getString(0) == null) { + callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null)); + return; + } + + if (keyValueArray.getArray(idx).getString(1) == null) { + callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null)); + return; + } + + if (!AsyncLocalStorageUtil.mergeImpl( + mDb, + keyValueArray.getArray(idx).getString(0), + keyValueArray.getArray(idx).getString(1))) { + callback.invoke(AsyncStorageErrorUtil.getDBError(null)); + return; + } + } + mDb.setTransactionSuccessful(); + } catch (Exception e) { + FLog.w(ReactConstants.TAG, e.getMessage(), e); + callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); + } finally { + mDb.endTransaction(); + } + callback.invoke(); + } + }.execute(); + } + + /** + * Clears the database. + */ + @ReactMethod + public void clear(final Callback callback) { + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + if (!ensureDatabase()) { + callback.invoke(AsyncStorageErrorUtil.getDBError(null)); + return; + } + try { + Assertions.assertNotNull(mDb).delete(TABLE_CATALYST, null, null); + } catch (Exception e) { + FLog.w(ReactConstants.TAG, "Exception in database clear ", e); + callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); + } + callback.invoke(); + } + }.execute(); + } + + /** + * Returns an array with all keys from the database. + */ + @ReactMethod + public void getAllKeys(final Callback callback) { + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + if (!ensureDatabase()) { + callback.invoke(AsyncStorageErrorUtil.getDBError(null), null); + return; + } + WritableArray data = Arguments.createArray(); + String[] columns = {KEY_COLUMN}; + Cursor cursor = Assertions.assertNotNull(mDb) + .query(TABLE_CATALYST, columns, null, null, null, null, null); + try { + if (cursor.moveToFirst()) { + do { + data.pushString(cursor.getString(0)); + } while (cursor.moveToNext()); + } + } catch (Exception e) { + FLog.w(ReactConstants.TAG, "Exception in database getAllKeys ", e); + callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()), null); + } finally { + cursor.close(); + } + callback.invoke(null, data); + } + }.execute(); + } + + /** + * Verify the database exists and is open. + */ + private boolean ensureDatabase() { + if (mShuttingDown) { + return false; + } + if (mDb != null && mDb.isOpen()) { + return true; + } + mDb = initializeDatabase(); + return true; + } + + /** + * Create and/or open the database. + */ + private SQLiteDatabase initializeDatabase() { + CatalystSQLiteOpenHelper helperForDb = + new CatalystSQLiteOpenHelper(getReactApplicationContext()); + return helperForDb.getWritableDatabase(); + } +} From 71da896d0b4699e08d12b7a0c407da3be939a339 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Thu, 24 Sep 2015 02:43:25 -0700 Subject: [PATCH 19/75] AsyncStorage improvements Differential Revision: D2475604 committer: Service User --- AsyncLocalStorageUtil.java | 6 +-- AsyncStorageModule.java | 76 +++++++++++--------------- ReactDatabaseSupplier.java | 106 +++++++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 49 deletions(-) create mode 100644 ReactDatabaseSupplier.java diff --git a/AsyncLocalStorageUtil.java b/AsyncLocalStorageUtil.java index 36340f0a..34fc0909 100644 --- a/AsyncLocalStorageUtil.java +++ b/AsyncLocalStorageUtil.java @@ -24,9 +24,9 @@ import org.json.JSONException; import org.json.JSONObject; -import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.KEY_COLUMN; -import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.TABLE_CATALYST; -import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.VALUE_COLUMN; +import static com.facebook.react.modules.storage.ReactDatabaseSupplier.KEY_COLUMN; +import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST; +import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN; /** * Helper for database operations. diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java index 601528a0..c5ecd898 100644 --- a/AsyncStorageModule.java +++ b/AsyncStorageModule.java @@ -9,16 +9,12 @@ package com.facebook.react.modules.storage; -import javax.annotation.Nullable; - import java.util.HashSet; import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteStatement; import com.facebook.common.logging.FLog; -import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.GuardedAsyncTask; @@ -31,18 +27,19 @@ import com.facebook.react.common.SetBuilder; import com.facebook.react.modules.common.ModuleDataCleaner; -import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.KEY_COLUMN; -import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.TABLE_CATALYST; -import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.VALUE_COLUMN; +import static com.facebook.react.modules.storage.ReactDatabaseSupplier.KEY_COLUMN; +import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST; +import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN; public final class AsyncStorageModule extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable { - private @Nullable SQLiteDatabase mDb; + private ReactDatabaseSupplier mReactDatabaseSupplier; private boolean mShuttingDown = false; public AsyncStorageModule(ReactApplicationContext reactContext) { super(reactContext); + mReactDatabaseSupplier = new ReactDatabaseSupplier(reactContext); } @Override @@ -59,10 +56,6 @@ public void initialize() { @Override public void onCatalystInstanceDestroy() { mShuttingDown = true; - if (mDb != null && mDb.isOpen()) { - mDb.close(); - mDb = null; - } } @Override @@ -74,10 +67,17 @@ public void clearSensitiveData() { new Callback() { @Override public void invoke(Object... args) { - if (args.length > 0) { - throw new RuntimeException("Clearing AsyncLocalStorage failed: " + args[0]); + if (args.length == 0) { + FLog.d(ReactConstants.TAG, "Cleaned AsyncLocalStorage."); + return; } - FLog.d(ReactConstants.TAG, "Cleaned AsyncLocalStorage."); + // Clearing the database has failed, delete it instead. + if (mReactDatabaseSupplier.deleteDatabase()) { + FLog.d(ReactConstants.TAG, "Deleted Local Database AsyncLocalStorage."); + return; + } + // Everything failed, crash the app + throw new RuntimeException("Clearing and deleting database failed: " + args[0]); } }); } @@ -104,7 +104,7 @@ protected void doInBackgroundGuarded(Void... params) { String[] columns = {KEY_COLUMN, VALUE_COLUMN}; HashSet keysRemaining = SetBuilder.newHashSet(); WritableArray data = Arguments.createArray(); - Cursor cursor = Assertions.assertNotNull(mDb).query( + Cursor cursor = mReactDatabaseSupplier.get().query( TABLE_CATALYST, columns, AsyncLocalStorageUtil.buildKeySelection(keys.size()), @@ -171,8 +171,8 @@ protected void doInBackgroundGuarded(Void... params) { } String sql = "INSERT OR REPLACE INTO " + TABLE_CATALYST + " VALUES (?, ?);"; - SQLiteStatement statement = Assertions.assertNotNull(mDb).compileStatement(sql); - mDb.beginTransaction(); + SQLiteStatement statement = mReactDatabaseSupplier.get().compileStatement(sql); + mReactDatabaseSupplier.get().beginTransaction(); try { for (int idx=0; idx < keyValueArray.size(); idx++) { if (keyValueArray.getArray(idx).size() != 2) { @@ -193,12 +193,12 @@ protected void doInBackgroundGuarded(Void... params) { statement.bindString(2, keyValueArray.getArray(idx).getString(1)); statement.execute(); } - mDb.setTransactionSuccessful(); + mReactDatabaseSupplier.get().setTransactionSuccessful(); } catch (Exception e) { FLog.w(ReactConstants.TAG, "Exception in database multiSet ", e); callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); } finally { - mDb.endTransaction(); + mReactDatabaseSupplier.get().endTransaction(); } callback.invoke(); } @@ -224,7 +224,7 @@ protected void doInBackgroundGuarded(Void... params) { } try { - Assertions.assertNotNull(mDb).delete( + mReactDatabaseSupplier.get().delete( TABLE_CATALYST, AsyncLocalStorageUtil.buildKeySelection(keys.size()), AsyncLocalStorageUtil.buildKeySelectionArgs(keys)); @@ -250,7 +250,7 @@ protected void doInBackgroundGuarded(Void... params) { callback.invoke(AsyncStorageErrorUtil.getDBError(null)); return; } - Assertions.assertNotNull(mDb).beginTransaction(); + mReactDatabaseSupplier.get().beginTransaction(); try { for (int idx = 0; idx < keyValueArray.size(); idx++) { if (keyValueArray.getArray(idx).size() != 2) { @@ -269,19 +269,19 @@ protected void doInBackgroundGuarded(Void... params) { } if (!AsyncLocalStorageUtil.mergeImpl( - mDb, + mReactDatabaseSupplier.get(), keyValueArray.getArray(idx).getString(0), keyValueArray.getArray(idx).getString(1))) { callback.invoke(AsyncStorageErrorUtil.getDBError(null)); return; } } - mDb.setTransactionSuccessful(); + mReactDatabaseSupplier.get().setTransactionSuccessful(); } catch (Exception e) { FLog.w(ReactConstants.TAG, e.getMessage(), e); callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); } finally { - mDb.endTransaction(); + mReactDatabaseSupplier.get().endTransaction(); } callback.invoke(); } @@ -296,12 +296,12 @@ public void clear(final Callback callback) { new GuardedAsyncTask(getReactApplicationContext()) { @Override protected void doInBackgroundGuarded(Void... params) { - if (!ensureDatabase()) { + if (!mReactDatabaseSupplier.ensureDatabase()) { callback.invoke(AsyncStorageErrorUtil.getDBError(null)); return; } try { - Assertions.assertNotNull(mDb).delete(TABLE_CATALYST, null, null); + mReactDatabaseSupplier.get().delete(TABLE_CATALYST, null, null); } catch (Exception e) { FLog.w(ReactConstants.TAG, "Exception in database clear ", e); callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); @@ -325,7 +325,7 @@ protected void doInBackgroundGuarded(Void... params) { } WritableArray data = Arguments.createArray(); String[] columns = {KEY_COLUMN}; - Cursor cursor = Assertions.assertNotNull(mDb) + Cursor cursor = mReactDatabaseSupplier.get() .query(TABLE_CATALYST, columns, null, null, null, null, null); try { if (cursor.moveToFirst()) { @@ -345,25 +345,9 @@ protected void doInBackgroundGuarded(Void... params) { } /** - * Verify the database exists and is open. + * Verify the database is open for reads and writes. */ private boolean ensureDatabase() { - if (mShuttingDown) { - return false; - } - if (mDb != null && mDb.isOpen()) { - return true; - } - mDb = initializeDatabase(); - return true; - } - - /** - * Create and/or open the database. - */ - private SQLiteDatabase initializeDatabase() { - CatalystSQLiteOpenHelper helperForDb = - new CatalystSQLiteOpenHelper(getReactApplicationContext()); - return helperForDb.getWritableDatabase(); + return !mShuttingDown && mReactDatabaseSupplier.ensureDatabase(); } } diff --git a/ReactDatabaseSupplier.java b/ReactDatabaseSupplier.java new file mode 100644 index 00000000..be14cc3a --- /dev/null +++ b/ReactDatabaseSupplier.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.storage; + +import javax.annotation.Nullable; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteOpenHelper; + +// VisibleForTesting +public class ReactDatabaseSupplier extends SQLiteOpenHelper { + + // VisibleForTesting + public static final String DATABASE_NAME = "RKStorage"; + static final int DATABASE_VERSION = 1; + private static final int SLEEP_TIME_MS = 30; + + static final String TABLE_CATALYST = "catalystLocalStorage"; + static final String KEY_COLUMN = "key"; + static final String VALUE_COLUMN = "value"; + + static final String VERSION_TABLE_CREATE = + "CREATE TABLE " + TABLE_CATALYST + " (" + + KEY_COLUMN + " TEXT PRIMARY KEY, " + + VALUE_COLUMN + " TEXT NOT NULL" + + ")"; + + private Context mContext; + private @Nullable SQLiteDatabase mDb; + + public ReactDatabaseSupplier(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + mContext = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(VERSION_TABLE_CREATE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + deleteDatabase(); + onCreate(db); + } + } + + /** + * Verify the database exists and is open. + */ + /* package */ synchronized boolean ensureDatabase() { + if (mDb != null && mDb.isOpen()) { + return true; + } + // Sometimes retrieving the database fails. We do 2 retries: first without database deletion + // and then with deletion. + SQLiteException lastSQLiteException = null; + for (int tries = 0; tries < 2; tries++) { + try { + if (tries > 0) { + deleteDatabase(); + } + mDb = getWritableDatabase(); + break; + } catch (SQLiteException e) { + lastSQLiteException = e; + } + // Wait before retrying. + try { + Thread.sleep(SLEEP_TIME_MS); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } + if (mDb == null) { + throw lastSQLiteException; + } + return true; + } + + /** + * Create and/or open the database. + */ + /* package */ synchronized SQLiteDatabase get() { + ensureDatabase(); + return mDb; + } + + /* package */ synchronized boolean deleteDatabase() { + if (mDb != null && mDb.isOpen()) { + mDb.close(); + mDb = null; + } + return mContext.deleteDatabase(DATABASE_NAME); + } +} From 64b09ff010e30e4fc829b78af29f2ce8567a83b9 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Thu, 24 Sep 2015 05:18:47 -0700 Subject: [PATCH 20/75] Fix SQL errors caused by huge operations Differential Revision: D2475717 committer: Service User --- AsyncLocalStorageUtil.java | 8 ++-- AsyncStorageModule.java | 90 ++++++++++++++++++++++---------------- 2 files changed, 56 insertions(+), 42 deletions(-) diff --git a/AsyncLocalStorageUtil.java b/AsyncLocalStorageUtil.java index 34fc0909..78b58639 100644 --- a/AsyncLocalStorageUtil.java +++ b/AsyncLocalStorageUtil.java @@ -49,10 +49,10 @@ * {a, b, c} * to be used in the SQL select statement: WHERE key in (?, ?, ?) */ - /* package */ static String[] buildKeySelectionArgs(ReadableArray keys) { - String[] selectionArgs = new String[keys.size()]; - for (int keyIndex = 0; keyIndex < keys.size(); keyIndex++) { - selectionArgs[keyIndex] = keys.getString(keyIndex); + /* package */ static String[] buildKeySelectionArgs(ReadableArray keys, int start, int count) { + String[] selectionArgs = new String[count]; + for (int keyIndex = 0; keyIndex < count; keyIndex++) { + selectionArgs[keyIndex] = keys.getString(start + keyIndex); } return selectionArgs; } diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java index c5ecd898..fd2fa710 100644 --- a/AsyncStorageModule.java +++ b/AsyncStorageModule.java @@ -34,6 +34,10 @@ public final class AsyncStorageModule extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable { + // SQL variable number limit, defined by SQLITE_LIMIT_VARIABLE_NUMBER: + // https://raw.githubusercontent.com/android/platform_external_sqlite/master/dist/sqlite3.c + private static final int MAX_SQL_KEYS = 999; + private ReactDatabaseSupplier mReactDatabaseSupplier; private boolean mShuttingDown = false; @@ -104,47 +108,50 @@ protected void doInBackgroundGuarded(Void... params) { String[] columns = {KEY_COLUMN, VALUE_COLUMN}; HashSet keysRemaining = SetBuilder.newHashSet(); WritableArray data = Arguments.createArray(); - Cursor cursor = mReactDatabaseSupplier.get().query( - TABLE_CATALYST, - columns, - AsyncLocalStorageUtil.buildKeySelection(keys.size()), - AsyncLocalStorageUtil.buildKeySelectionArgs(keys), - null, - null, - null); + for (int keyStart = 0; keyStart < keys.size(); keyStart += MAX_SQL_KEYS) { + int keyCount = Math.min(keys.size() - keyStart, MAX_SQL_KEYS); + Cursor cursor = mReactDatabaseSupplier.get().query( + TABLE_CATALYST, + columns, + AsyncLocalStorageUtil.buildKeySelection(keyCount), + AsyncLocalStorageUtil.buildKeySelectionArgs(keys, keyStart, keyCount), + null, + null, + null); + keysRemaining.clear(); + try { + if (cursor.getCount() != keys.size()) { + // some keys have not been found - insert them with null into the final array + for (int keyIndex = keyStart; keyIndex < keyStart + keyCount; keyIndex++) { + keysRemaining.add(keys.getString(keyIndex)); + } + } - try { - if (cursor.getCount() != keys.size()) { - // some keys have not been found - insert them with null into the final array - for (int keyIndex = 0; keyIndex < keys.size(); keyIndex++) { - keysRemaining.add(keys.getString(keyIndex)); + if (cursor.moveToFirst()) { + do { + WritableArray row = Arguments.createArray(); + row.pushString(cursor.getString(0)); + row.pushString(cursor.getString(1)); + data.pushArray(row); + keysRemaining.remove(cursor.getString(0)); + } while (cursor.moveToNext()); } + } catch (Exception e) { + FLog.w(ReactConstants.TAG, "Exception in database multiGet ", e); + callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()), null); + } finally { + cursor.close(); } - if (cursor.moveToFirst()) { - do { - WritableArray row = Arguments.createArray(); - row.pushString(cursor.getString(0)); - row.pushString(cursor.getString(1)); - data.pushArray(row); - keysRemaining.remove(cursor.getString(0)); - } while (cursor.moveToNext()); - + for (String key : keysRemaining) { + WritableArray row = Arguments.createArray(); + row.pushString(key); + row.pushNull(); + data.pushArray(row); } - } catch (Exception e) { - FLog.w(ReactConstants.TAG, "Exception in database multiGet ", e); - callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()), null); - } finally { - cursor.close(); + keysRemaining.clear(); } - for (String key : keysRemaining) { - WritableArray row = Arguments.createArray(); - row.pushString(key); - row.pushNull(); - data.pushArray(row); - } - keysRemaining.clear(); callback.invoke(null, data); } }.execute(); @@ -223,14 +230,21 @@ protected void doInBackgroundGuarded(Void... params) { return; } + mReactDatabaseSupplier.get().beginTransaction(); try { - mReactDatabaseSupplier.get().delete( - TABLE_CATALYST, - AsyncLocalStorageUtil.buildKeySelection(keys.size()), - AsyncLocalStorageUtil.buildKeySelectionArgs(keys)); + for (int keyStart = 0; keyStart < keys.size(); keyStart += MAX_SQL_KEYS) { + int keyCount = Math.min(keys.size() - keyStart, MAX_SQL_KEYS); + mReactDatabaseSupplier.get().delete( + TABLE_CATALYST, + AsyncLocalStorageUtil.buildKeySelection(keyCount), + AsyncLocalStorageUtil.buildKeySelectionArgs(keys, keyStart, keyCount)); + } + mReactDatabaseSupplier.get().setTransactionSuccessful(); } catch (Exception e) { FLog.w(ReactConstants.TAG, "Exception in database multiRemove ", e); callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); + } finally { + mReactDatabaseSupplier.get().endTransaction(); } callback.invoke(); } From 302c54243ac921ef7933c30d3d419d81f548bc06 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Tue, 6 Oct 2015 06:10:37 -0700 Subject: [PATCH 21/75] Protect against SQLiteFullExceptions Reviewed By: @kmagiera Differential Revision: D2512317 fb-gh-sync-id: 93fd65ebd88e42b5afc4e06c0612576101f15c97 --- AsyncStorageModule.java | 87 +++++++++++++++++++++++++++----------- ReactDatabaseSupplier.java | 8 +++- 2 files changed, 70 insertions(+), 25 deletions(-) diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java index fd2fa710..abd6ed27 100644 --- a/AsyncStorageModule.java +++ b/AsyncStorageModule.java @@ -23,6 +23,7 @@ import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.SetBuilder; import com.facebook.react.modules.common.ModuleDataCleaner; @@ -137,8 +138,9 @@ protected void doInBackgroundGuarded(Void... params) { } while (cursor.moveToNext()); } } catch (Exception e) { - FLog.w(ReactConstants.TAG, "Exception in database multiGet ", e); + FLog.w(ReactConstants.TAG, e.getMessage(), e); callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()), null); + return; } finally { cursor.close(); } @@ -179,19 +181,20 @@ protected void doInBackgroundGuarded(Void... params) { String sql = "INSERT OR REPLACE INTO " + TABLE_CATALYST + " VALUES (?, ?);"; SQLiteStatement statement = mReactDatabaseSupplier.get().compileStatement(sql); - mReactDatabaseSupplier.get().beginTransaction(); + WritableMap error = null; try { + mReactDatabaseSupplier.get().beginTransaction(); for (int idx=0; idx < keyValueArray.size(); idx++) { if (keyValueArray.getArray(idx).size() != 2) { - callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null)); + error = AsyncStorageErrorUtil.getInvalidValueError(null); return; } if (keyValueArray.getArray(idx).getString(0) == null) { - callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null)); + error = AsyncStorageErrorUtil.getInvalidKeyError(null); return; } if (keyValueArray.getArray(idx).getString(1) == null) { - callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null)); + error = AsyncStorageErrorUtil.getInvalidValueError(null); return; } @@ -202,12 +205,23 @@ protected void doInBackgroundGuarded(Void... params) { } mReactDatabaseSupplier.get().setTransactionSuccessful(); } catch (Exception e) { - FLog.w(ReactConstants.TAG, "Exception in database multiSet ", e); - callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); + FLog.w(ReactConstants.TAG, e.getMessage(), e); + error = AsyncStorageErrorUtil.getError(null, e.getMessage()); } finally { - mReactDatabaseSupplier.get().endTransaction(); + try { + mReactDatabaseSupplier.get().endTransaction(); + } catch (Exception e) { + FLog.w(ReactConstants.TAG, e.getMessage(), e); + if (error == null) { + error = AsyncStorageErrorUtil.getError(null, e.getMessage()); + } + } + } + if (error != null) { + callback.invoke(error); + } else { + callback.invoke(); } - callback.invoke(); } }.execute(); } @@ -230,8 +244,9 @@ protected void doInBackgroundGuarded(Void... params) { return; } - mReactDatabaseSupplier.get().beginTransaction(); + WritableMap error = null; try { + mReactDatabaseSupplier.get().beginTransaction(); for (int keyStart = 0; keyStart < keys.size(); keyStart += MAX_SQL_KEYS) { int keyCount = Math.min(keys.size() - keyStart, MAX_SQL_KEYS); mReactDatabaseSupplier.get().delete( @@ -241,12 +256,23 @@ protected void doInBackgroundGuarded(Void... params) { } mReactDatabaseSupplier.get().setTransactionSuccessful(); } catch (Exception e) { - FLog.w(ReactConstants.TAG, "Exception in database multiRemove ", e); - callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); + FLog.w(ReactConstants.TAG, e.getMessage(), e); + error = AsyncStorageErrorUtil.getError(null, e.getMessage()); } finally { + try { mReactDatabaseSupplier.get().endTransaction(); + } catch (Exception e) { + FLog.w(ReactConstants.TAG, e.getMessage(), e); + if (error == null) { + error = AsyncStorageErrorUtil.getError(null, e.getMessage()); + } + } + } + if (error != null) { + callback.invoke(error); + } else { + callback.invoke(); } - callback.invoke(); } }.execute(); } @@ -264,21 +290,22 @@ protected void doInBackgroundGuarded(Void... params) { callback.invoke(AsyncStorageErrorUtil.getDBError(null)); return; } - mReactDatabaseSupplier.get().beginTransaction(); + WritableMap error = null; try { + mReactDatabaseSupplier.get().beginTransaction(); for (int idx = 0; idx < keyValueArray.size(); idx++) { if (keyValueArray.getArray(idx).size() != 2) { - callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null)); + error = AsyncStorageErrorUtil.getInvalidValueError(null); return; } if (keyValueArray.getArray(idx).getString(0) == null) { - callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null)); + error = AsyncStorageErrorUtil.getInvalidKeyError(null); return; } if (keyValueArray.getArray(idx).getString(1) == null) { - callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null)); + error = AsyncStorageErrorUtil.getInvalidValueError(null); return; } @@ -286,18 +313,29 @@ protected void doInBackgroundGuarded(Void... params) { mReactDatabaseSupplier.get(), keyValueArray.getArray(idx).getString(0), keyValueArray.getArray(idx).getString(1))) { - callback.invoke(AsyncStorageErrorUtil.getDBError(null)); + error = AsyncStorageErrorUtil.getDBError(null); return; } } mReactDatabaseSupplier.get().setTransactionSuccessful(); } catch (Exception e) { FLog.w(ReactConstants.TAG, e.getMessage(), e); - callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); + error = AsyncStorageErrorUtil.getError(null, e.getMessage()); } finally { - mReactDatabaseSupplier.get().endTransaction(); + try { + mReactDatabaseSupplier.get().endTransaction(); + } catch (Exception e) { + FLog.w(ReactConstants.TAG, e.getMessage(), e); + if (error == null) { + error = AsyncStorageErrorUtil.getError(null, e.getMessage()); + } + } + } + if (error != null) { + callback.invoke(error); + } else { + callback.invoke(); } - callback.invoke(); } }.execute(); } @@ -316,11 +354,11 @@ protected void doInBackgroundGuarded(Void... params) { } try { mReactDatabaseSupplier.get().delete(TABLE_CATALYST, null, null); + callback.invoke(); } catch (Exception e) { - FLog.w(ReactConstants.TAG, "Exception in database clear ", e); + FLog.w(ReactConstants.TAG, e.getMessage(), e); callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); } - callback.invoke(); } }.execute(); } @@ -348,8 +386,9 @@ protected void doInBackgroundGuarded(Void... params) { } while (cursor.moveToNext()); } } catch (Exception e) { - FLog.w(ReactConstants.TAG, "Exception in database getAllKeys ", e); + FLog.w(ReactConstants.TAG, e.getMessage(), e); callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()), null); + return; } finally { cursor.close(); } diff --git a/ReactDatabaseSupplier.java b/ReactDatabaseSupplier.java index be14cc3a..2f127c65 100644 --- a/ReactDatabaseSupplier.java +++ b/ReactDatabaseSupplier.java @@ -21,8 +21,10 @@ public class ReactDatabaseSupplier extends SQLiteOpenHelper { // VisibleForTesting public static final String DATABASE_NAME = "RKStorage"; - static final int DATABASE_VERSION = 1; + + private static final int DATABASE_VERSION = 1; private static final int SLEEP_TIME_MS = 30; + private static final long DEFAULT_MAX_DB_SIZE = 6L * 1024L * 1024L; // 6 MB in bytes static final String TABLE_CATALYST = "catalystLocalStorage"; static final String KEY_COLUMN = "key"; @@ -85,6 +87,10 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (mDb == null) { throw lastSQLiteException; } + // This is a sane limit to protect the user from the app storing too much data in the database. + // This also protects the database from filling up the disk cache and becoming malformed + // (endTransaction() calls will throw an exception, not rollback, and leave the db malformed). + mDb.setMaximumSize(DEFAULT_MAX_DB_SIZE); return true; } From d44081ca82e2145c696864e1869bcfea57661fde Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 20 Oct 2015 09:40:24 -0700 Subject: [PATCH 22/75] Improved AsyncLocalStorage performance Reviewed By: javache Differential Revision: D2540626 fb-gh-sync-id: f94def7463075d40c2dcfea1cb4c01502aefa117 --- RCTAsyncLocalStorage.m | 111 ++++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 1ef86eb8..a1060e4e 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -19,7 +19,7 @@ static NSString *const RCTStorageDirectory = @"RCTAsyncLocalStorage_V1"; static NSString *const RCTManifestFileName = @"manifest.json"; -static const NSUInteger RCTInlineValueThreshold = 100; +static const NSUInteger RCTInlineValueThreshold = 1024; #pragma mark - Static helper functions @@ -83,31 +83,32 @@ static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut } // Only merges objects - all other types are just clobbered (including arrays) -static void RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) +// returns YES if destination was modified, or NO if no changes were needed. +static BOOL RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) { + BOOL modified = NO; for (NSString *key in source) { id sourceValue = source[key]; + id destinationValue = destination[key]; if ([sourceValue isKindOfClass:[NSDictionary class]]) { - id destinationValue = destination[key]; - NSMutableDictionary *nestedDestination; - if ([destinationValue classForCoder] == [NSMutableDictionary class]) { - nestedDestination = destinationValue; - } else { - if ([destinationValue isKindOfClass:[NSDictionary class]]) { - // Ideally we wouldn't eagerly copy here... - nestedDestination = [destinationValue mutableCopy]; - } else { - destination[key] = [sourceValue copy]; + if ([destinationValue isKindOfClass:[NSDictionary class]]) { + if ([destinationValue classForCoder] != [NSMutableDictionary class]) { + destinationValue = [destinationValue mutableCopy]; } + if (RCTMergeRecursive(destinationValue, sourceValue)) { + destination[key] = destinationValue; + modified = YES; + } + } else { + destination[key] = [sourceValue copy]; + modified = YES; } - if (nestedDestination) { - RCTMergeRecursive(nestedDestination, sourceValue); - destination[key] = nestedDestination; - } - } else { - destination[key] = sourceValue; + } else if (![source isEqual:destinationValue]) { + destination[key] = [sourceValue copy]; + modified = YES; } } + return modified; } static dispatch_queue_t RCTGetMethodQueue() @@ -121,6 +122,23 @@ static dispatch_queue_t RCTGetMethodQueue() 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 + + // 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; +} + static BOOL RCTHasCreatedStorageDirectory = NO; static NSError *RCTDeleteStorageDirectory() { @@ -150,6 +168,7 @@ - (dispatch_queue_t)methodQueue + (void)clearAllData { + [RCTGetCache() removeAllObjects]; dispatch_async(RCTGetMethodQueue(), ^{ RCTDeleteStorageDirectory(); }); @@ -158,6 +177,7 @@ + (void)clearAllData - (void)invalidate { if (_clearOnInvalidate) { + [RCTGetCache() removeAllObjects]; RCTDeleteStorageDirectory(); } _clearOnInvalidate = NO; @@ -199,7 +219,7 @@ - (id)_ensureSetup if (!_haveSetup) { NSDictionary *errorOut; NSString *serialized = RCTReadFile(RCTGetManifestFilePath(), nil, &errorOut); - _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [NSMutableDictionary new]; + _manifest = serialized ? RCTJSONParseMutable(serialized, &error) : [NSMutableDictionary new]; if (error) { RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); _manifest = [NSMutableDictionary new]; @@ -222,23 +242,16 @@ - (id)_writeManifest:(NSMutableArray **)errors return errorOut; } -- (id)_appendItemForKey:(NSString *)key toArray:(NSMutableArray *)result -{ - id errorOut = RCTErrorForKey(key); - if (errorOut) { - return errorOut; - } - id value = [self _getValueForKey:key errorOut:&errorOut]; - [result addObject:@[key, RCTNullIfNil(value)]]; // Insert null if missing or failure. - return errorOut; -} - - (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut { - id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. + NSString *value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. if (value == (id)kCFNull) { - NSString *filePath = [self _filePathForKey:key]; - value = RCTReadFile(filePath, key, errorOut); + value = [RCTGetCache() objectForKey:key]; + if (!value) { + NSString *filePath = [self _filePathForKey:key]; + value = RCTReadFile(filePath, key, errorOut); + [RCTGetCache() setObject:value forKey:key cost:value.length]; + } } return value; } @@ -263,11 +276,13 @@ - (id)_writeEntry:(NSArray *)entry if (_manifest[key] && _manifest[key] != (id)kCFNull) { // If the value already existed but wasn't inlined, remove the old file. [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + [RCTGetCache() removeObjectForKey:key]; } _manifest[key] = value; return nil; } [value writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error]; + [RCTGetCache() setObject:value forKey:key cost:value.length]; if (error) { errorOut = RCTMakeError(@"Failed to write value.", error, @{@"key": key}); } else { @@ -294,7 +309,9 @@ - (id)_writeEntry:(NSArray *)entry NSMutableArray *errors; NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; for (NSString *key in keys) { - id keyError = [self _appendItemForKey:key toArray:result]; + id keyError; + id value = [self _getValueForKey:key errorOut:&keyError]; + [result addObject:@[key, RCTNullIfNil(value)]]; RCTAppendError(keyError, &errors); } callback(@[RCTNullIfNil(errors), result]); @@ -331,19 +348,18 @@ - (id)_writeEntry:(NSArray *)entry for (__strong NSArray *entry in kvPairs) { id keyError; NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError]; - if (keyError) { - RCTAppendError(keyError, &errors); - } else { + if (!keyError) { if (value) { - NSMutableDictionary *mergedVal = [RCTJSONParseMutable(value, &keyError) mutableCopy]; - RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &keyError)); - entry = @[entry[0], RCTJSONStringify(mergedVal, &keyError)]; - } - if (!keyError) { + NSMutableDictionary *mergedVal = RCTJSONParseMutable(value, &keyError); + if (RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &keyError))) { + entry = @[entry[0], RCTJSONStringify(mergedVal, &keyError)]; + keyError = [self _writeEntry:entry]; + } + } else { keyError = [self _writeEntry:entry]; } - RCTAppendError(keyError, &errors); } + RCTAppendError(keyError, &errors); } [self _writeManifest:&errors]; if (callback) { @@ -363,8 +379,10 @@ - (id)_writeEntry:(NSArray *)entry for (NSString *key in keys) { id keyError = RCTErrorForKey(key); if (!keyError) { - NSString *filePath = [self _filePathForKey:key]; - [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + if ( _manifest[key] == (id)kCFNull) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + } [_manifest removeObjectForKey:key]; } RCTAppendError(keyError, &errors); @@ -377,7 +395,8 @@ - (id)_writeEntry:(NSArray *)entry RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { - _manifest = [NSMutableDictionary new]; + [_manifest removeAllObjects]; + [RCTGetCache() removeAllObjects]; NSError *error = RCTDeleteStorageDirectory(); if (callback) { callback(@[RCTNullIfNil(error)]); From fcec61d7bf1390513a9e195e4a018e0f4a7273f8 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 20 Oct 2015 11:48:04 -0700 Subject: [PATCH 23/75] Clear cache entry when calling multiRemove Reviewed By: javache Differential Revision: D2560487 fb-gh-sync-id: 4d2c192cc659f118fd5667da2a029457328eae9f --- RCTAsyncLocalStorage.m | 1 + 1 file changed, 1 insertion(+) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index a1060e4e..8cb3d543 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -382,6 +382,7 @@ - (id)_writeEntry:(NSArray *)entry if ( _manifest[key] == (id)kCFNull) { NSString *filePath = [self _filePathForKey:key]; [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + [RCTGetCache() removeObjectForKey:key]; } [_manifest removeObjectForKey:key]; } From 7158c97c9abb8ed0fe1011ff27199cd91a5d22f8 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Mon, 26 Oct 2015 18:26:25 -0700 Subject: [PATCH 24/75] Add storage module to fb Differential Revision: D2584243 fb-gh-sync-id: 50dece06820aa754741b560cae5eb3318c1926bd --- AsyncStorageModule.java | 22 +++------------- ReactDatabaseSupplier.java | 51 +++++++++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java index abd6ed27..35b61506 100644 --- a/AsyncStorageModule.java +++ b/AsyncStorageModule.java @@ -44,7 +44,7 @@ public final class AsyncStorageModule public AsyncStorageModule(ReactApplicationContext reactContext) { super(reactContext); - mReactDatabaseSupplier = new ReactDatabaseSupplier(reactContext); + mReactDatabaseSupplier = ReactDatabaseSupplier.getInstance(reactContext); } @Override @@ -68,23 +68,7 @@ public void clearSensitiveData() { // Clear local storage. If fails, crash, since the app is potentially in a bad state and could // cause a privacy violation. We're still not recovering from this well, but at least the error // will be reported to the server. - clear( - new Callback() { - @Override - public void invoke(Object... args) { - if (args.length == 0) { - FLog.d(ReactConstants.TAG, "Cleaned AsyncLocalStorage."); - return; - } - // Clearing the database has failed, delete it instead. - if (mReactDatabaseSupplier.deleteDatabase()) { - FLog.d(ReactConstants.TAG, "Deleted Local Database AsyncLocalStorage."); - return; - } - // Everything failed, crash the app - throw new RuntimeException("Clearing and deleting database failed: " + args[0]); - } - }); + mReactDatabaseSupplier.clearAndCloseDatabase(); } /** @@ -353,7 +337,7 @@ protected void doInBackgroundGuarded(Void... params) { return; } try { - mReactDatabaseSupplier.get().delete(TABLE_CATALYST, null, null); + mReactDatabaseSupplier.clear(); callback.invoke(); } catch (Exception e) { FLog.w(ReactConstants.TAG, e.getMessage(), e); diff --git a/ReactDatabaseSupplier.java b/ReactDatabaseSupplier.java index 2f127c65..ef27b332 100644 --- a/ReactDatabaseSupplier.java +++ b/ReactDatabaseSupplier.java @@ -16,7 +16,13 @@ import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; -// VisibleForTesting +import com.facebook.common.logging.FLog; +import com.facebook.react.common.ReactConstants; + +/** + * Database supplier of the database used by react native. This creates, opens and deletes the + * database as necessary. + */ public class ReactDatabaseSupplier extends SQLiteOpenHelper { // VisibleForTesting @@ -38,12 +44,20 @@ public class ReactDatabaseSupplier extends SQLiteOpenHelper { private Context mContext; private @Nullable SQLiteDatabase mDb; + private static @Nullable ReactDatabaseSupplier mReactDatabaseSupplierInstance; - public ReactDatabaseSupplier(Context context) { + private ReactDatabaseSupplier(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); mContext = context; } + public static ReactDatabaseSupplier getInstance(Context context) { + if (mReactDatabaseSupplierInstance == null) { + mReactDatabaseSupplierInstance = new ReactDatabaseSupplier(context); + } + return mReactDatabaseSupplierInstance; + } + @Override public void onCreate(SQLiteDatabase db) { db.execSQL(VERSION_TABLE_CREATE); @@ -102,11 +116,40 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { return mDb; } - /* package */ synchronized boolean deleteDatabase() { + public synchronized void clearAndCloseDatabase() throws RuntimeException { + try { + clear(); + closeDatabase(); + FLog.d(ReactConstants.TAG, "Cleaned " + DATABASE_NAME); + } catch (Exception e) { + // Clearing the database has failed, delete it instead. + if (deleteDatabase()) { + FLog.d(ReactConstants.TAG, "Deleted Local Database " + DATABASE_NAME); + return; + } + // Everything failed, throw + throw new RuntimeException("Clearing and deleting database " + DATABASE_NAME + " failed"); + } + } + + /* package */ synchronized void clear() { + get().delete(TABLE_CATALYST, null, null); + } + + private synchronized boolean deleteDatabase() { + closeDatabase(); + return mContext.deleteDatabase(DATABASE_NAME); + } + + private synchronized void closeDatabase() { if (mDb != null && mDb.isOpen()) { mDb.close(); mDb = null; } - return mContext.deleteDatabase(DATABASE_NAME); + } + + // For testing purposes only! + public static void deleteInstance() { + mReactDatabaseSupplierInstance = null; } } From 276f329d2593051b5f74275d8df3cd818b49b9e7 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 27 Oct 2015 09:59:30 -0700 Subject: [PATCH 25/75] Merged the JS implementations of AsyncStorage, as there seem to be no significant differences. Reviewed By: sahrens Differential Revision: D2585257 fb-gh-sync-id: a1c930a675f40b4cd1b3ebe4dffb316747376522 --- AsyncStorage.js | 267 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 AsyncStorage.js diff --git a/AsyncStorage.js b/AsyncStorage.js new file mode 100644 index 00000000..37e0bd4a --- /dev/null +++ b/AsyncStorage.js @@ -0,0 +1,267 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AsyncStorage + * @flow-weak + */ +'use strict'; + +var NativeModules = require('NativeModules'); +var RCTAsyncSQLiteStorage = NativeModules.AsyncSQLiteDBStorage; +var RCTAsyncRocksDBStorage = NativeModules.AsyncRocksDBStorage; +var RCTAsyncFileStorage = NativeModules.AsyncLocalStorage; + +// Use RocksDB if available, then SQLite, then file storage. +var RCTAsyncStorage = RCTAsyncRocksDBStorage || RCTAsyncSQLiteStorage || RCTAsyncFileStorage; + +/** + * AsyncStorage is a simple, asynchronous, persistent, key-value storage + * system that is global to the app. It should be used instead of LocalStorage. + * + * It is recommended that you use an abstraction on top of AsyncStorage instead + * of AsyncStorage directly for anything more than light usage since it + * operates globally. + * + * This JS code is a simple facade over the native iOS implementation to provide + * a clear JS API, real Error objects, and simple non-multi functions. Each + * method returns a `Promise` object. + */ +var AsyncStorage = { + /** + * Fetches `key` and passes the result to `callback`, along with an `Error` if + * there is any. Returns a `Promise` object. + */ + getItem: function( + key: string, + callback?: ?(error: ?Error, result: ?string) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiGet([key], function(errors, result) { + // Unpack result to get value from [[key,value]] + var value = (result && result[0] && result[0][1]) ? result[0][1] : null; + callback && callback((errors && convertError(errors[0])) || null, value); + if (errors) { + reject(convertError(errors[0])); + } else { + resolve(value); + } + }); + }); + }, + + /** + * Sets `value` for `key` and calls `callback` on completion, along with an + * `Error` if there is any. Returns a `Promise` object. + */ + setItem: function( + key: string, + value: string, + callback?: ?(error: ?Error) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiSet([[key,value]], function(errors) { + callback && callback((errors && convertError(errors[0])) || null); + if (errors) { + reject(convertError(errors[0])); + } else { + resolve(null); + } + }); + }); + }, + + /** + * Returns a `Promise` object. + */ + removeItem: function( + key: string, + callback?: ?(error: ?Error) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiRemove([key], function(errors) { + callback && callback((errors && convertError(errors[0])) || null); + if (errors) { + reject(convertError(errors[0])); + } else { + resolve(null); + } + }); + }); + }, + + /** + * Merges existing value with input value, assuming they are stringified json. + * Returns a `Promise` object. Not supported by all native implementations. + */ + mergeItem: function( + key: string, + value: string, + callback?: ?(error: ?Error) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiMerge([[key,value]], function(errors) { + callback && callback((errors && convertError(errors[0])) || null); + if (errors) { + reject(convertError(errors[0])); + } else { + resolve(null); + } + }); + }); + }, + + /** + * Erases *all* AsyncStorage for all clients, libraries, etc. You probably + * don't want to call this - use removeItem or multiRemove to clear only your + * own keys instead. Returns a `Promise` object. + */ + clear: function(callback?: ?(error: ?Error) => void): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.clear(function(error) { + callback && callback(convertError(error)); + if (error && convertError(error)){ + reject(convertError(error)); + } else { + resolve(null); + } + }); + }); + }, + + /** + * Gets *all* keys known to the app, for all callers, libraries, etc. Returns a `Promise` object. + */ + getAllKeys: function(callback?: ?(error: ?Error, keys: ?Array) => void): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.getAllKeys(function(error, keys) { + callback && callback(convertError(error), keys); + if (error) { + reject(convertError(error)); + } else { + resolve(keys); + } + }); + }); + }, + + /** + * The following batched functions are useful for executing a lot of + * operations at once, allowing for native optimizations and provide the + * convenience of a single callback after all operations are complete. + * + * These functions return arrays of errors, potentially one for every key. + * For key-specific errors, the Error object will have a key property to + * indicate which key caused the error. + */ + + /** + * multiGet invokes callback with an array of key-value pair arrays that + * matches the input format of multiSet. Returns a `Promise` object. + * + * multiGet(['k1', 'k2'], cb) -> cb([['k1', 'val1'], ['k2', 'val2']]) + */ + multiGet: function( + keys: Array, + callback?: ?(errors: ?Array, result: ?Array>) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiGet(keys, function(errors, result) { + var error = (errors && errors.map((error) => convertError(error))) || null; + callback && callback(error, result); + if (errors) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, + + /** + * multiSet and multiMerge take arrays of key-value array pairs that match + * the output of multiGet, e.g. Returns a `Promise` object. + * + * multiSet([['k1', 'val1'], ['k2', 'val2']], cb); + */ + multiSet: function( + keyValuePairs: Array>, + callback?: ?(errors: ?Array) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiSet(keyValuePairs, function(errors) { + var error = (errors && errors.map((error) => convertError(error))) || null; + callback && callback(error); + if (errors) { + reject(error); + } else { + resolve(null); + } + }); + }); + }, + + /** + * Delete all the keys in the `keys` array. Returns a `Promise` object. + */ + multiRemove: function( + keys: Array, + callback?: ?(errors: ?Array) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiRemove(keys, function(errors) { + var error = (errors && errors.map((error) => convertError(error))) || null; + callback && callback(error); + if (errors) { + reject(error); + } else { + resolve(null); + } + }); + }); + }, + + /** + * Merges existing values with input values, assuming they are stringified + * json. Returns a `Promise` object. + * + * Not supported by all native implementations. + */ + multiMerge: function( + keyValuePairs: Array>, + callback?: ?(errors: ?Array) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiMerge(keyValuePairs, function(errors) { + var error = (errors && errors.map((error) => convertError(error))) || null; + callback && callback(error); + if (errors) { + reject(error); + } else { + resolve(null); + } + }); + }); + }, +}; + +// Not all native implementations support merge. +if (!RCTAsyncStorage.multiMerge) { + delete AsyncStorage.mergeItem; + delete AsyncStorage.multiMerge; +} + +function convertError(error) { + if (!error) { + return null; + } + var out = new Error(error.message); + out.key = error.key; // flow doesn't like this :( + return out; +} + +module.exports = AsyncStorage; From 5a4adcae2fb7b9370bb824423e30aed3a801eb50 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Wed, 28 Oct 2015 11:43:47 -0700 Subject: [PATCH 26/75] Backed out D2540626 Reviewed By: jingc Differential Revision: D2590392 fb-gh-sync-id: 2893a4ee6ddd0de12803560b355cee6f1b9d43e6 --- RCTAsyncLocalStorage.m | 112 +++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 66 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 8cb3d543..1ef86eb8 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -19,7 +19,7 @@ static NSString *const RCTStorageDirectory = @"RCTAsyncLocalStorage_V1"; static NSString *const RCTManifestFileName = @"manifest.json"; -static const NSUInteger RCTInlineValueThreshold = 1024; +static const NSUInteger RCTInlineValueThreshold = 100; #pragma mark - Static helper functions @@ -83,32 +83,31 @@ static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut } // Only merges objects - all other types are just clobbered (including arrays) -// returns YES if destination was modified, or NO if no changes were needed. -static BOOL RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) +static void RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) { - BOOL modified = NO; for (NSString *key in source) { id sourceValue = source[key]; - id destinationValue = destination[key]; if ([sourceValue isKindOfClass:[NSDictionary class]]) { - if ([destinationValue isKindOfClass:[NSDictionary class]]) { - if ([destinationValue classForCoder] != [NSMutableDictionary class]) { - destinationValue = [destinationValue mutableCopy]; - } - if (RCTMergeRecursive(destinationValue, sourceValue)) { - destination[key] = destinationValue; - modified = YES; - } + id destinationValue = destination[key]; + NSMutableDictionary *nestedDestination; + if ([destinationValue classForCoder] == [NSMutableDictionary class]) { + nestedDestination = destinationValue; } else { - destination[key] = [sourceValue copy]; - modified = YES; + if ([destinationValue isKindOfClass:[NSDictionary class]]) { + // Ideally we wouldn't eagerly copy here... + nestedDestination = [destinationValue mutableCopy]; + } else { + destination[key] = [sourceValue copy]; + } } - } else if (![source isEqual:destinationValue]) { - destination[key] = [sourceValue copy]; - modified = YES; + if (nestedDestination) { + RCTMergeRecursive(nestedDestination, sourceValue); + destination[key] = nestedDestination; + } + } else { + destination[key] = sourceValue; } } - return modified; } static dispatch_queue_t RCTGetMethodQueue() @@ -122,23 +121,6 @@ static dispatch_queue_t RCTGetMethodQueue() 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 - - // 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; -} - static BOOL RCTHasCreatedStorageDirectory = NO; static NSError *RCTDeleteStorageDirectory() { @@ -168,7 +150,6 @@ - (dispatch_queue_t)methodQueue + (void)clearAllData { - [RCTGetCache() removeAllObjects]; dispatch_async(RCTGetMethodQueue(), ^{ RCTDeleteStorageDirectory(); }); @@ -177,7 +158,6 @@ + (void)clearAllData - (void)invalidate { if (_clearOnInvalidate) { - [RCTGetCache() removeAllObjects]; RCTDeleteStorageDirectory(); } _clearOnInvalidate = NO; @@ -219,7 +199,7 @@ - (id)_ensureSetup if (!_haveSetup) { NSDictionary *errorOut; NSString *serialized = RCTReadFile(RCTGetManifestFilePath(), nil, &errorOut); - _manifest = serialized ? RCTJSONParseMutable(serialized, &error) : [NSMutableDictionary new]; + _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [NSMutableDictionary new]; if (error) { RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); _manifest = [NSMutableDictionary new]; @@ -242,16 +222,23 @@ - (id)_writeManifest:(NSMutableArray **)errors return errorOut; } +- (id)_appendItemForKey:(NSString *)key toArray:(NSMutableArray *)result +{ + id errorOut = RCTErrorForKey(key); + if (errorOut) { + return errorOut; + } + id value = [self _getValueForKey:key errorOut:&errorOut]; + [result addObject:@[key, RCTNullIfNil(value)]]; // Insert null if missing or failure. + return errorOut; +} + - (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut { - NSString *value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. + id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. if (value == (id)kCFNull) { - value = [RCTGetCache() objectForKey:key]; - if (!value) { - NSString *filePath = [self _filePathForKey:key]; - value = RCTReadFile(filePath, key, errorOut); - [RCTGetCache() setObject:value forKey:key cost:value.length]; - } + NSString *filePath = [self _filePathForKey:key]; + value = RCTReadFile(filePath, key, errorOut); } return value; } @@ -276,13 +263,11 @@ - (id)_writeEntry:(NSArray *)entry if (_manifest[key] && _manifest[key] != (id)kCFNull) { // If the value already existed but wasn't inlined, remove the old file. [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; - [RCTGetCache() removeObjectForKey:key]; } _manifest[key] = value; return nil; } [value writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error]; - [RCTGetCache() setObject:value forKey:key cost:value.length]; if (error) { errorOut = RCTMakeError(@"Failed to write value.", error, @{@"key": key}); } else { @@ -309,9 +294,7 @@ - (id)_writeEntry:(NSArray *)entry NSMutableArray *errors; NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; for (NSString *key in keys) { - id keyError; - id value = [self _getValueForKey:key errorOut:&keyError]; - [result addObject:@[key, RCTNullIfNil(value)]]; + id keyError = [self _appendItemForKey:key toArray:result]; RCTAppendError(keyError, &errors); } callback(@[RCTNullIfNil(errors), result]); @@ -348,18 +331,19 @@ - (id)_writeEntry:(NSArray *)entry for (__strong NSArray *entry in kvPairs) { id keyError; NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError]; - if (!keyError) { + if (keyError) { + RCTAppendError(keyError, &errors); + } else { if (value) { - NSMutableDictionary *mergedVal = RCTJSONParseMutable(value, &keyError); - if (RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &keyError))) { - entry = @[entry[0], RCTJSONStringify(mergedVal, &keyError)]; - keyError = [self _writeEntry:entry]; - } - } else { + NSMutableDictionary *mergedVal = [RCTJSONParseMutable(value, &keyError) mutableCopy]; + RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &keyError)); + entry = @[entry[0], RCTJSONStringify(mergedVal, &keyError)]; + } + if (!keyError) { keyError = [self _writeEntry:entry]; } + RCTAppendError(keyError, &errors); } - RCTAppendError(keyError, &errors); } [self _writeManifest:&errors]; if (callback) { @@ -379,11 +363,8 @@ - (id)_writeEntry:(NSArray *)entry for (NSString *key in keys) { id keyError = RCTErrorForKey(key); if (!keyError) { - if ( _manifest[key] == (id)kCFNull) { - NSString *filePath = [self _filePathForKey:key]; - [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; - [RCTGetCache() removeObjectForKey:key]; - } + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; [_manifest removeObjectForKey:key]; } RCTAppendError(keyError, &errors); @@ -396,8 +377,7 @@ - (id)_writeEntry:(NSArray *)entry RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { - [_manifest removeAllObjects]; - [RCTGetCache() removeAllObjects]; + _manifest = [NSMutableDictionary new]; NSError *error = RCTDeleteStorageDirectory(); if (callback) { callback(@[RCTNullIfNil(error)]); From e59c23f00d7eebb02d05c3b20859b4c9b85a8d20 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Wed, 28 Oct 2015 11:59:06 -0700 Subject: [PATCH 27/75] Fix AsyncStorage error multiplexing Reviewed By: nicklockwood Differential Revision: D2590231 fb-gh-sync-id: 399a783a474367855d526e0b2604d0f862594102 --- AsyncStorage.js | 51 ++++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index 37e0bd4a..c5be992e 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -44,9 +44,10 @@ var AsyncStorage = { RCTAsyncStorage.multiGet([key], function(errors, result) { // Unpack result to get value from [[key,value]] var value = (result && result[0] && result[0][1]) ? result[0][1] : null; - callback && callback((errors && convertError(errors[0])) || null, value); - if (errors) { - reject(convertError(errors[0])); + var errs = convertErrors(errors); + callback && callback(errs && errs[0], value); + if (errs) { + reject(errs[0]); } else { resolve(value); } @@ -65,9 +66,10 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiSet([[key,value]], function(errors) { - callback && callback((errors && convertError(errors[0])) || null); - if (errors) { - reject(convertError(errors[0])); + var errs = convertErrors(errors); + callback && callback(errs && errs[0]); + if (errs) { + reject(errs[0]); } else { resolve(null); } @@ -84,9 +86,10 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiRemove([key], function(errors) { - callback && callback((errors && convertError(errors[0])) || null); - if (errors) { - reject(convertError(errors[0])); + var errs = convertErrors(errors); + callback && callback(errs && errs[0]); + if (errs) { + reject(errs[0]); } else { resolve(null); } @@ -105,9 +108,10 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiMerge([[key,value]], function(errors) { - callback && callback((errors && convertError(errors[0])) || null); - if (errors) { - reject(convertError(errors[0])); + var errs = convertErrors(errors); + callback && callback(errs && errs[0]); + if (errs) { + reject(errs[0]); } else { resolve(null); } @@ -171,9 +175,9 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiGet(keys, function(errors, result) { - var error = (errors && errors.map((error) => convertError(error))) || null; + var error = convertErrors(errors); callback && callback(error, result); - if (errors) { + if (error) { reject(error); } else { resolve(result); @@ -194,9 +198,9 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiSet(keyValuePairs, function(errors) { - var error = (errors && errors.map((error) => convertError(error))) || null; + var error = convertErrors(errors); callback && callback(error); - if (errors) { + if (error) { reject(error); } else { resolve(null); @@ -214,9 +218,9 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiRemove(keys, function(errors) { - var error = (errors && errors.map((error) => convertError(error))) || null; + var error = convertErrors(errors); callback && callback(error); - if (errors) { + if (error) { reject(error); } else { resolve(null); @@ -237,9 +241,9 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiMerge(keyValuePairs, function(errors) { - var error = (errors && errors.map((error) => convertError(error))) || null; + var error = convertErrors(errors); callback && callback(error); - if (errors) { + if (error) { reject(error); } else { resolve(null); @@ -255,6 +259,13 @@ if (!RCTAsyncStorage.multiMerge) { delete AsyncStorage.multiMerge; } +function convertErrors(errs) { + if (!errs) { + return null; + } + return (Array.isArray(errs) ? errs : [errs]).map((e) => convertError(e)); +} + function convertError(error) { if (!error) { return null; From 3776a34b5a76cf587b6432f245b2f5163d518c42 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 3 Nov 2015 14:45:46 -0800 Subject: [PATCH 28/75] Added lightweight generic annotations Summary: public Added lightweight genarics annotations to make the code more readable and help the compiler catch bugs. Fixed some type bugs and improved bridge validation in a few places. Reviewed By: javache Differential Revision: D2600189 fb-gh-sync-id: f81e22f2cdc107bf8d0b15deec6d5b83aacc5b56 --- RCTAsyncLocalStorage.h | 7 +-- RCTAsyncLocalStorage.m | 114 ++++++++++++++++++++--------------------- 2 files changed, 58 insertions(+), 63 deletions(-) diff --git a/RCTAsyncLocalStorage.h b/RCTAsyncLocalStorage.h index e7e871b0..e6c129ef 100644 --- a/RCTAsyncLocalStorage.h +++ b/RCTAsyncLocalStorage.h @@ -27,11 +27,8 @@ @property (nonatomic, readonly, getter=isValid) BOOL valid; -- (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; -- (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback; -- (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; -- (void)clear:(RCTResponseSenderBlock)callback; -- (void)getAllKeys:(RCTResponseSenderBlock)callback; +// Clear the RCTAsyncLocalStorage data from native code +- (void)clearAllData; // For clearing data when the bridge may not exist, e.g. when logging out. + (void)clearAllData; diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 1ef86eb8..b1bb67ef 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -14,6 +14,7 @@ #import #import +#import "RCTConvert.h" #import "RCTLog.h" #import "RCTUtils.h" @@ -23,7 +24,7 @@ #pragma mark - Static helper functions -static id RCTErrorForKey(NSString *key) +static NSDictionary *RCTErrorForKey(NSString *key) { if (![key isKindOfClass:[NSString class]]) { return RCTMakeAndLogError(@"Invalid key - must be a string. Key: ", key, @{@"key": key}); @@ -34,7 +35,7 @@ static id RCTErrorForKey(NSString *key) } } -static void RCTAppendError(id error, NSMutableArray **errors) +static void RCTAppendError(NSDictionary *error, NSMutableArray **errors) { if (error && errors) { if (!*errors) { @@ -44,7 +45,7 @@ static void RCTAppendError(id error, NSMutableArray **errors) } } -static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut) +static NSString *RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut) { if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { NSError *error; @@ -148,6 +149,14 @@ - (dispatch_queue_t)methodQueue return RCTGetMethodQueue(); } +- (void)clearAllData +{ + dispatch_async(RCTGetMethodQueue(), ^{ + _manifest = [NSMutableDictionary new]; + RCTDeleteStorageDirectory(); + }); +} + + (void)clearAllData { dispatch_async(RCTGetMethodQueue(), ^{ @@ -181,7 +190,7 @@ - (NSString *)_filePathForKey:(NSString *)key return [RCTGetStorageDirectory() stringByAppendingPathComponent:safeFileName]; } -- (id)_ensureSetup +- (NSDictionary *)_ensureSetup { RCTAssertThread(RCTGetMethodQueue(), @"Must be executed on storage thread"); @@ -199,7 +208,7 @@ - (id)_ensureSetup if (!_haveSetup) { NSDictionary *errorOut; NSString *serialized = RCTReadFile(RCTGetManifestFilePath(), nil, &errorOut); - _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [NSMutableDictionary new]; + _manifest = serialized ? RCTJSONParseMutable(serialized, &error) : [NSMutableDictionary new]; if (error) { RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); _manifest = [NSMutableDictionary new]; @@ -209,12 +218,12 @@ - (id)_ensureSetup return nil; } -- (id)_writeManifest:(NSMutableArray **)errors +- (NSDictionary *)_writeManifest:(NSMutableArray **)errors { NSError *error; NSString *serialized = RCTJSONStringify(_manifest, &error); [serialized writeToFile:RCTGetManifestFilePath() atomically:YES encoding:NSUTF8StringEncoding error:&error]; - id errorOut; + NSDictionary *errorOut; if (error) { errorOut = RCTMakeError(@"Failed to write manifest file.", error, nil); RCTAppendError(errorOut, errors); @@ -222,20 +231,21 @@ - (id)_writeManifest:(NSMutableArray **)errors return errorOut; } -- (id)_appendItemForKey:(NSString *)key toArray:(NSMutableArray *)result +- (NSDictionary *)_appendItemForKey:(NSString *)key + toArray:(NSMutableArray *> *)result { - id errorOut = RCTErrorForKey(key); + NSDictionary *errorOut = RCTErrorForKey(key); if (errorOut) { return errorOut; } - id value = [self _getValueForKey:key errorOut:&errorOut]; + NSString *value = [self _getValueForKey:key errorOut:&errorOut]; [result addObject:@[key, RCTNullIfNil(value)]]; // Insert null if missing or failure. return errorOut; } - (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut { - id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. + id value = _manifest[key]; // nil means missing, null means there is a data file, else: NSString if (value == (id)kCFNull) { NSString *filePath = [self _filePathForKey:key]; value = RCTReadFile(filePath, key, errorOut); @@ -243,16 +253,13 @@ - (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut return value; } -- (id)_writeEntry:(NSArray *)entry +- (NSDictionary *)_writeEntry:(NSArray *)entry { - if (![entry isKindOfClass:[NSArray class]] || entry.count != 2) { + if (entry.count != 2) { return RCTMakeAndLogError(@"Entries must be arrays of the form [key: string, value: string], got: ", entry, nil); } - if (![entry[1] isKindOfClass:[NSString class]]) { - return RCTMakeAndLogError(@"Values must be strings, got: ", entry[1], @{@"key": entry[0]}); - } NSString *key = entry[0]; - id errorOut = RCTErrorForKey(key); + NSDictionary *errorOut = RCTErrorForKey(key); if (errorOut) { return errorOut; } @@ -278,66 +285,63 @@ - (id)_writeEntry:(NSArray *)entry #pragma mark - Exported JS Functions -RCT_EXPORT_METHOD(multiGet:(NSArray *)keys +RCT_EXPORT_METHOD(multiGet:(NSStringArray *)keys callback:(RCTResponseSenderBlock)callback) { - if (!callback) { - RCTLogError(@"Called getItem without a callback."); - return; - } - - id errorOut = [self _ensureSetup]; + NSDictionary *errorOut = [self _ensureSetup]; if (errorOut) { callback(@[@[errorOut], (id)kCFNull]); return; } - NSMutableArray *errors; - NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; + NSMutableArray *errors; + NSMutableArray *> *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; for (NSString *key in keys) { - id keyError = [self _appendItemForKey:key toArray:result]; + NSDictionary *keyError = [self _appendItemForKey:key toArray:result]; RCTAppendError(keyError, &errors); } callback(@[RCTNullIfNil(errors), result]); } -RCT_EXPORT_METHOD(multiSet:(NSArray *)kvPairs +RCT_EXPORT_METHOD(multiSet:(NSStringArrayArray *)kvPairs callback:(RCTResponseSenderBlock)callback) { - id errorOut = [self _ensureSetup]; + NSDictionary *errorOut = [self _ensureSetup]; if (errorOut) { callback(@[@[errorOut]]); return; } - NSMutableArray *errors; - for (NSArray *entry in kvPairs) { - id keyError = [self _writeEntry:entry]; + NSMutableArray *errors; + for (NSArray *entry in kvPairs) { + NSDictionary *keyError = [self _writeEntry:entry]; RCTAppendError(keyError, &errors); } [self _writeManifest:&errors]; - if (callback) { - callback(@[RCTNullIfNil(errors)]); - } + callback(@[RCTNullIfNil(errors)]); } -RCT_EXPORT_METHOD(multiMerge:(NSArray *)kvPairs - callback:(RCTResponseSenderBlock)callback) +RCT_EXPORT_METHOD(multiMerge:(NSStringArrayArray *)kvPairs + callback:(RCTResponseSenderBlock)callback) { - id errorOut = [self _ensureSetup]; + NSDictionary *errorOut = [self _ensureSetup]; if (errorOut) { callback(@[@[errorOut]]); return; } - NSMutableArray *errors; - for (__strong NSArray *entry in kvPairs) { - id keyError; + NSMutableArray *errors; + for (__strong NSArray *entry in kvPairs) { + NSDictionary *keyError; NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError]; if (keyError) { RCTAppendError(keyError, &errors); } else { if (value) { - NSMutableDictionary *mergedVal = [RCTJSONParseMutable(value, &keyError) mutableCopy]; - RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &keyError)); - entry = @[entry[0], RCTJSONStringify(mergedVal, &keyError)]; + NSError *jsonError; + NSMutableDictionary *mergedVal = RCTJSONParseMutable(value, &jsonError); + RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &jsonError)); + entry = @[entry[0], RCTNullIfNil(RCTJSONStringify(mergedVal, NULL))]; + if (jsonError) { + keyError = RCTJSErrorFromNSError(jsonError); + } } if (!keyError) { keyError = [self _writeEntry:entry]; @@ -346,22 +350,20 @@ - (id)_writeEntry:(NSArray *)entry } } [self _writeManifest:&errors]; - if (callback) { - callback(@[RCTNullIfNil(errors)]); - } + callback(@[RCTNullIfNil(errors)]); } -RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys +RCT_EXPORT_METHOD(multiRemove:(NSStringArray *)keys callback:(RCTResponseSenderBlock)callback) { - id errorOut = [self _ensureSetup]; + NSDictionary *errorOut = [self _ensureSetup]; if (errorOut) { callback(@[@[errorOut]]); return; } - NSMutableArray *errors; + NSMutableArray *errors; for (NSString *key in keys) { - id keyError = RCTErrorForKey(key); + NSDictionary *keyError = RCTErrorForKey(key); if (!keyError) { NSString *filePath = [self _filePathForKey:key]; [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; @@ -370,23 +372,19 @@ - (id)_writeEntry:(NSArray *)entry RCTAppendError(keyError, &errors); } [self _writeManifest:&errors]; - if (callback) { - callback(@[RCTNullIfNil(errors)]); - } + callback(@[RCTNullIfNil(errors)]); } RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { _manifest = [NSMutableDictionary new]; NSError *error = RCTDeleteStorageDirectory(); - if (callback) { - callback(@[RCTNullIfNil(error)]); - } + callback(@[RCTNullIfNil(error)]); } RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback) { - id errorOut = [self _ensureSetup]; + NSDictionary *errorOut = [self _ensureSetup]; if (errorOut) { callback(@[errorOut, (id)kCFNull]); } else { From 59f7ee45847439282d3b03fc0556d5e37b6efe7f Mon Sep 17 00:00:00 2001 From: Bhuwan Khattar Date: Wed, 4 Nov 2015 20:04:29 -0800 Subject: [PATCH 29/75] No `NSError *` over the bridge Summary: I was getting the following error when running `testAsyncStorageTest` ``` uncaught exception 'NSInvalidArgumentException', reason: 'Invalid type in JSON write (NSError)' ``` nicklockwood pointed out that it might be because we're attempting to send a `NSError *` over the bridge at some point. So I went looking for cases where that might happen and found this instance. public Reviewed By: jingc Differential Revision: D2619240 fb-gh-sync-id: dc26ec268f976fec44f2804831398d3b01ab6115 --- RCTAsyncLocalStorage.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index b1bb67ef..cb8e2485 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -123,12 +123,12 @@ static dispatch_queue_t RCTGetMethodQueue() } static BOOL RCTHasCreatedStorageDirectory = NO; -static NSError *RCTDeleteStorageDirectory() +static NSDictionary *RCTDeleteStorageDirectory() { NSError *error; [[NSFileManager defaultManager] removeItemAtPath:RCTGetStorageDirectory() error:&error]; RCTHasCreatedStorageDirectory = NO; - return error; + return error ? RCTMakeError(@"Failed to delete storage directory.", error, nil) : nil; } #pragma mark - RCTAsyncLocalStorage @@ -378,7 +378,7 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { _manifest = [NSMutableDictionary new]; - NSError *error = RCTDeleteStorageDirectory(); + NSDictionary *error = RCTDeleteStorageDirectory(); callback(@[RCTNullIfNil(error)]); } From 8e95b2daa9caa7137d4bb5b749e38622e0993ac0 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Wed, 11 Nov 2015 08:58:07 -0800 Subject: [PATCH 30/75] Improved AsyncStorage merge function Reviewed By: tadeuzagallo Differential Revision: D2641817 fb-gh-sync-id: 0ba526ce21039ccdb979ac75c44d41c522c910ca --- RCTAsyncLocalStorage.m | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index cb8e2485..cacf6a0b 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -84,31 +84,31 @@ static void RCTAppendError(NSDictionary *error, NSMutableArray * } // Only merges objects - all other types are just clobbered (including arrays) -static void RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) +static BOOL RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) { + BOOL modified = NO; for (NSString *key in source) { id sourceValue = source[key]; + id destinationValue = destination[key]; if ([sourceValue isKindOfClass:[NSDictionary class]]) { - id destinationValue = destination[key]; - NSMutableDictionary *nestedDestination; - if ([destinationValue classForCoder] == [NSMutableDictionary class]) { - nestedDestination = destinationValue; - } else { - if ([destinationValue isKindOfClass:[NSDictionary class]]) { - // Ideally we wouldn't eagerly copy here... - nestedDestination = [destinationValue mutableCopy]; - } else { - destination[key] = [sourceValue copy]; + if ([destinationValue isKindOfClass:[NSDictionary class]]) { + if ([destinationValue classForCoder] != [NSMutableDictionary class]) { + destinationValue = [destinationValue mutableCopy]; } + if (RCTMergeRecursive(destinationValue, sourceValue)) { + destination[key] = destinationValue; + modified = YES; + } + } else { + destination[key] = [sourceValue copy]; + modified = YES; } - if (nestedDestination) { - RCTMergeRecursive(nestedDestination, sourceValue); - destination[key] = nestedDestination; - } - } else { - destination[key] = sourceValue; + } else if (![source isEqual:destinationValue]) { + destination[key] = [sourceValue copy]; + modified = YES; } } + return modified; } static dispatch_queue_t RCTGetMethodQueue() @@ -337,8 +337,9 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry if (value) { NSError *jsonError; NSMutableDictionary *mergedVal = RCTJSONParseMutable(value, &jsonError); - RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &jsonError)); - entry = @[entry[0], RCTNullIfNil(RCTJSONStringify(mergedVal, NULL))]; + if (RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &jsonError))) { + entry = @[entry[0], RCTNullIfNil(RCTJSONStringify(mergedVal, NULL))]; + } if (jsonError) { keyError = RCTJSErrorFromNSError(jsonError); } From 35f8ea11bffb5d7aa56d6ef455bde9d90b0afcc7 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Sat, 14 Nov 2015 09:17:40 -0800 Subject: [PATCH 31/75] Added in-memory cache for AsyncLocalStorage Reviewed By: tadeuzagallo Differential Revision: D2641705 fb-gh-sync-id: 40b96b3084b82779e16f8845f9faeb0e3638d189 --- RCTAsyncLocalStorage.m | 100 +++++++++++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 24 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index cacf6a0b..273ef40c 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -122,6 +122,23 @@ static dispatch_queue_t RCTGetMethodQueue() 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 + + // 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; +} + static BOOL RCTHasCreatedStorageDirectory = NO; static NSDictionary *RCTDeleteStorageDirectory() { @@ -139,7 +156,7 @@ @implementation RCTAsyncLocalStorage // 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; + NSMutableDictionary *_manifest; } RCT_EXPORT_MODULE() @@ -152,7 +169,8 @@ - (dispatch_queue_t)methodQueue - (void)clearAllData { dispatch_async(RCTGetMethodQueue(), ^{ - _manifest = [NSMutableDictionary new]; + [_manifest removeAllObjects]; + [RCTGetCache() removeAllObjects]; RCTDeleteStorageDirectory(); }); } @@ -160,6 +178,7 @@ - (void)clearAllData + (void)clearAllData { dispatch_async(RCTGetMethodQueue(), ^{ + [RCTGetCache() removeAllObjects]; RCTDeleteStorageDirectory(); }); } @@ -167,10 +186,11 @@ + (void)clearAllData - (void)invalidate { if (_clearOnInvalidate) { + [RCTGetCache() removeAllObjects]; RCTDeleteStorageDirectory(); } _clearOnInvalidate = NO; - _manifest = [NSMutableDictionary new]; + [_manifest removeAllObjects]; _haveSetup = NO; } @@ -245,15 +265,25 @@ - (NSDictionary *)_appendItemForKey:(NSString *)key - (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut { - id value = _manifest[key]; // nil means missing, null means there is a data file, else: NSString + NSString *value = _manifest[key]; // nil means missing, null means there may be a data file, else: NSString if (value == (id)kCFNull) { - NSString *filePath = [self _filePathForKey:key]; - value = RCTReadFile(filePath, key, errorOut); + value = [RCTGetCache() objectForKey:key]; + if (!value) { + NSString *filePath = [self _filePathForKey:key]; + value = RCTReadFile(filePath, key, errorOut); + if (value) { + [RCTGetCache() setObject:value forKey:key cost:value.length]; + } else { + // file does not exist after all, so remove from manifest (no need to save + // manifest immediately though, as cost of checking again next time is negligible) + [_manifest removeObjectForKey:key]; + } + } } return value; } -- (NSDictionary *)_writeEntry:(NSArray *)entry +- (NSDictionary *)_writeEntry:(NSArray *)entry changedManifest:(BOOL *)changedManifest { if (entry.count != 2) { return RCTMakeAndLogError(@"Entries must be arrays of the form [key: string, value: string], got: ", entry, nil); @@ -267,18 +297,22 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry NSString *filePath = [self _filePathForKey:key]; NSError *error; if (value.length <= RCTInlineValueThreshold) { - if (_manifest[key] && _manifest[key] != (id)kCFNull) { + if (_manifest[key] == (id)kCFNull) { // If the value already existed but wasn't inlined, remove the old file. [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + [RCTGetCache() removeObjectForKey:key]; } + *changedManifest = YES; _manifest[key] = value; return nil; } [value writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error]; + [RCTGetCache() setObject:value forKey:key cost:value.length]; if (error) { errorOut = RCTMakeError(@"Failed to write value.", error, @{@"key": key}); - } else { - _manifest[key] = (id)kCFNull; // Mark existence of file with null, any other value is inline data. + } else if (_manifest[key] != (id)kCFNull) { + *changedManifest = YES; + _manifest[key] = (id)kCFNull; } return errorOut; } @@ -296,7 +330,9 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry NSMutableArray *errors; NSMutableArray *> *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; for (NSString *key in keys) { - NSDictionary *keyError = [self _appendItemForKey:key toArray:result]; + id keyError; + id value = [self _getValueForKey:key errorOut:&keyError]; + [result addObject:@[key, RCTNullIfNil(value)]]; RCTAppendError(keyError, &errors); } callback(@[RCTNullIfNil(errors), result]); @@ -310,12 +346,15 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry callback(@[@[errorOut]]); return; } + BOOL changedManifest = NO; NSMutableArray *errors; for (NSArray *entry in kvPairs) { - NSDictionary *keyError = [self _writeEntry:entry]; + NSDictionary *keyError = [self _writeEntry:entry changedManifest:&changedManifest]; RCTAppendError(keyError, &errors); } - [self _writeManifest:&errors]; + if (changedManifest) { + [self _writeManifest:&errors]; + } callback(@[RCTNullIfNil(errors)]); } @@ -327,13 +366,12 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry callback(@[@[errorOut]]); return; } + BOOL changedManifest = NO; NSMutableArray *errors; for (__strong NSArray *entry in kvPairs) { NSDictionary *keyError; NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError]; - if (keyError) { - RCTAppendError(keyError, &errors); - } else { + if (!keyError) { if (value) { NSError *jsonError; NSMutableDictionary *mergedVal = RCTJSONParseMutable(value, &jsonError); @@ -345,12 +383,14 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry } } if (!keyError) { - keyError = [self _writeEntry:entry]; + keyError = [self _writeEntry:entry changedManifest:&changedManifest]; } - RCTAppendError(keyError, &errors); } + RCTAppendError(keyError, &errors); + } + if (changedManifest) { + [self _writeManifest:&errors]; } - [self _writeManifest:&errors]; callback(@[RCTNullIfNil(errors)]); } @@ -363,22 +403,34 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry return; } NSMutableArray *errors; + BOOL changedManifest = NO; for (NSString *key in keys) { NSDictionary *keyError = RCTErrorForKey(key); if (!keyError) { - NSString *filePath = [self _filePathForKey:key]; - [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; - [_manifest removeObjectForKey:key]; + if (_manifest[key] == (id)kCFNull) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + [RCTGetCache() removeObjectForKey:key]; + // remove the key from manifest, but no need to mark as changed just for + // this, as the cost of checking again next time is negligible. + [_manifest removeObjectForKey:key]; + } else if (_manifest[key]) { + changedManifest = YES; + [_manifest removeObjectForKey:key]; + } } RCTAppendError(keyError, &errors); } - [self _writeManifest:&errors]; + if (changedManifest) { + [self _writeManifest:&errors]; + } callback(@[RCTNullIfNil(errors)]); } RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { - _manifest = [NSMutableDictionary new]; + [_manifest removeAllObjects]; + [RCTGetCache() removeAllObjects]; NSDictionary *error = RCTDeleteStorageDirectory(); callback(@[RCTNullIfNil(error)]); } From b1fa8ae25c625035514e0ab3ce9c9957b101c006 Mon Sep 17 00:00:00 2001 From: Alexander Blom Date: Tue, 24 Nov 2015 09:46:58 -0800 Subject: [PATCH 32/75] Make ReactDatabaseSupplier use application context Summary: public We pass in a `ReactContext` but we really only need a context. Make sure we're using the application one. Reviewed By: astreet Differential Revision: D2690692 fb-gh-sync-id: 857d6571c9c01d35e12f09be4c8733cca007306f --- ReactDatabaseSupplier.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ReactDatabaseSupplier.java b/ReactDatabaseSupplier.java index ef27b332..620d2314 100644 --- a/ReactDatabaseSupplier.java +++ b/ReactDatabaseSupplier.java @@ -42,9 +42,10 @@ public class ReactDatabaseSupplier extends SQLiteOpenHelper { VALUE_COLUMN + " TEXT NOT NULL" + ")"; + private static @Nullable ReactDatabaseSupplier sReactDatabaseSupplierInstance; + private Context mContext; private @Nullable SQLiteDatabase mDb; - private static @Nullable ReactDatabaseSupplier mReactDatabaseSupplierInstance; private ReactDatabaseSupplier(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -52,10 +53,10 @@ private ReactDatabaseSupplier(Context context) { } public static ReactDatabaseSupplier getInstance(Context context) { - if (mReactDatabaseSupplierInstance == null) { - mReactDatabaseSupplierInstance = new ReactDatabaseSupplier(context); + if (sReactDatabaseSupplierInstance == null) { + sReactDatabaseSupplierInstance = new ReactDatabaseSupplier(context.getApplicationContext()); } - return mReactDatabaseSupplierInstance; + return sReactDatabaseSupplierInstance; } @Override @@ -150,6 +151,6 @@ private synchronized void closeDatabase() { // For testing purposes only! public static void deleteInstance() { - mReactDatabaseSupplierInstance = null; + sReactDatabaseSupplierInstance = null; } } From 0f29b3bb23aee06f91b54029afe3a2c0016c8d3e Mon Sep 17 00:00:00 2001 From: Milen Dzhumerov Date: Wed, 25 Nov 2015 03:17:50 -0800 Subject: [PATCH 33/75] Implement efficient DiskCache.clear() Summary: public Ability to efficiently remove all keys with a particular prefix Reviewed By: tadeuzagallo Differential Revision: D2658741 fb-gh-sync-id: 3770f061c83288efe645162ae84a9fd9194d2fd6 --- AsyncStorage.js | 25 +++++++++++++++++++++++++ RCTAsyncLocalStorage.m | 18 ++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/AsyncStorage.js b/AsyncStorage.js index c5be992e..4b3a8e00 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -137,6 +137,26 @@ var AsyncStorage = { }); }, + /** + * Erases all keys with a particular prefix. Useful if all your keys have a + * specific prefix. + */ + clearPrefix: function( + prefix: string, + callback?: ?(error: ?Error) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.clearPrefix(prefix, function(error) { + callback && callback(convertError(error)); + if (error && convertError(error)){ + reject(convertError(error)); + } else { + resolve(null); + } + }); + }); + }, + /** * Gets *all* keys known to the app, for all callers, libraries, etc. Returns a `Promise` object. */ @@ -259,6 +279,11 @@ if (!RCTAsyncStorage.multiMerge) { delete AsyncStorage.multiMerge; } +// clearPrefix() only supported by certain backends +if (!RCTAsyncStorage.clearPrefix) { + delete AsyncStorage.clearPrefix; +} + function convertErrors(errs) { if (!errs) { return null; diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 273ef40c..eceb1584 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -435,6 +435,24 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry changedManifest:(BOOL callback(@[RCTNullIfNil(error)]); } +RCT_EXPORT_METHOD(clearPrefix:(NSString *)prefix callack:(RCTResponseSenderBlock)callback) +{ + NSDictionary *errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[errorOut]); + return; + } + + NSMutableArray *keys = [NSMutableArray array]; + for (NSString *key in _manifest.allKeys) { + if ([key hasPrefix:prefix]) { + [keys addObject:key]; + } + } + + [self multiRemove:keys callback:callback]; +} + RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback) { NSDictionary *errorOut = [self _ensureSetup]; From cceaceed4d8544d2feb3289f2f037464dda96fb6 Mon Sep 17 00:00:00 2001 From: Milen Dzhumerov Date: Wed, 25 Nov 2015 05:20:34 -0800 Subject: [PATCH 34/75] Revert 3770f061c832 for further investigation Reviewed By: idevelop Differential Revision: D2695659 fb-gh-sync-id: b1ba529c648681faef5d4f07273722764722fbe1 --- AsyncStorage.js | 25 ------------------------- RCTAsyncLocalStorage.m | 18 ------------------ 2 files changed, 43 deletions(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index 4b3a8e00..c5be992e 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -137,26 +137,6 @@ var AsyncStorage = { }); }, - /** - * Erases all keys with a particular prefix. Useful if all your keys have a - * specific prefix. - */ - clearPrefix: function( - prefix: string, - callback?: ?(error: ?Error) => void - ): Promise { - return new Promise((resolve, reject) => { - RCTAsyncStorage.clearPrefix(prefix, function(error) { - callback && callback(convertError(error)); - if (error && convertError(error)){ - reject(convertError(error)); - } else { - resolve(null); - } - }); - }); - }, - /** * Gets *all* keys known to the app, for all callers, libraries, etc. Returns a `Promise` object. */ @@ -279,11 +259,6 @@ if (!RCTAsyncStorage.multiMerge) { delete AsyncStorage.multiMerge; } -// clearPrefix() only supported by certain backends -if (!RCTAsyncStorage.clearPrefix) { - delete AsyncStorage.clearPrefix; -} - function convertErrors(errs) { if (!errs) { return null; diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index eceb1584..273ef40c 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -435,24 +435,6 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry changedManifest:(BOOL callback(@[RCTNullIfNil(error)]); } -RCT_EXPORT_METHOD(clearPrefix:(NSString *)prefix callack:(RCTResponseSenderBlock)callback) -{ - NSDictionary *errorOut = [self _ensureSetup]; - if (errorOut) { - callback(@[errorOut]); - return; - } - - NSMutableArray *keys = [NSMutableArray array]; - for (NSString *key in _manifest.allKeys) { - if ([key hasPrefix:prefix]) { - [keys addObject:key]; - } - } - - [self multiRemove:keys callback:callback]; -} - RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback) { NSDictionary *errorOut = [self _ensureSetup]; From d9e41193d3b7a1b4fea9a4a10b56abdb2aa0bbf7 Mon Sep 17 00:00:00 2001 From: Gabe Levi Date: Tue, 1 Dec 2015 19:09:01 -0800 Subject: [PATCH 35/75] Fix errors uncovered by v0.19.0 Reviewed By: mroch Differential Revision: D2706663 fb-gh-sync-id: 017c91bab849bf18767cacd2ebe32d1a1b10c715 --- AsyncStorage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/AsyncStorage.js b/AsyncStorage.js index c5be992e..9135088f 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule AsyncStorage + * @noflow * @flow-weak */ 'use strict'; From bc738dad180b7d4da872f06acb1906da776c3b33 Mon Sep 17 00:00:00 2001 From: Milen Dzhumerov Date: Thu, 3 Dec 2015 08:08:37 -0800 Subject: [PATCH 36/75] Batch AsyncStorage.multiGet calls Reviewed By: javache Differential Revision: D2636553 fb-gh-sync-id: d6351b67c615d8c01c11c10e32321a9764c54c67 --- AsyncStorage.js | 61 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index 9135088f..eb41c8a7 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -33,6 +33,10 @@ var RCTAsyncStorage = RCTAsyncRocksDBStorage || RCTAsyncSQLiteStorage || RCTAsyn * method returns a `Promise` object. */ var AsyncStorage = { + _getRequests: ([]: Array), + _getKeys: ([]: Array), + _immediate: (null: ?number), + /** * Fetches `key` and passes the result to `callback`, along with an `Error` if * there is any. Returns a `Promise` object. @@ -164,6 +168,32 @@ var AsyncStorage = { * indicate which key caused the error. */ + /** Flushes any pending requests using a single multiget */ + flushGetRequests: function(): void { + const getRequests = this._getRequests; + const getKeys = this._getKeys; + + this._getRequests = []; + this._getKeys = []; + + RCTAsyncStorage.multiGet(getKeys, function(errors, result) { + // Even though the runtime complexity of this is theoretically worse vs if we used a map, + // it's much, much faster in practice for the data sets we deal with (we avoid + // allocating result pair arrays). This was heavily benchmarked. + const reqLength = getRequests.length; + for (let i = 0; i < reqLength; i++) { + const request = getRequests[i]; + const requestKeys = request.keys; + var requestResult = result.filter(function(resultPair) { + return requestKeys.indexOf(resultPair[0]) !== -1; + }); + + request.callback && request.callback(null, requestResult); + request.resolve && request.resolve(requestResult); + } + }); + }, + /** * multiGet invokes callback with an array of key-value pair arrays that * matches the input format of multiSet. Returns a `Promise` object. @@ -174,17 +204,30 @@ var AsyncStorage = { keys: Array, callback?: ?(errors: ?Array, result: ?Array>) => void ): Promise { - return new Promise((resolve, reject) => { - RCTAsyncStorage.multiGet(keys, function(errors, result) { - var error = convertErrors(errors); - callback && callback(error, result); - if (error) { - reject(error); - } else { - resolve(result); - } + if (!this._immediate) { + this._immediate = setImmediate(() => { + this._immediate = null; + this.flushGetRequests(); }); + } + + var getRequest = { + keys: keys, + callback: callback, + keyIndex: this._getKeys.length, + resolve: null, + reject: null, + }; + + var promiseResult = new Promise((resolve, reject) => { + getRequest.resolve = resolve; + getRequest.reject = reject; }); + + this._getRequests.push(getRequest); + this._getKeys.push.apply(this._getKeys, keys); + + return promiseResult; }, /** From 813c82d209f3d658bbe2df403125d2f80210637b Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Thu, 10 Dec 2015 10:09:04 -0800 Subject: [PATCH 37/75] Replaced RegExp method parser with recursive descent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: public This diff replaces the RegEx module method parser with a handwritten recursive descent parser that's faster and easier to maintain. The new parser is ~8 times faster when tested on the UIManager.managerChildren() method, and uses ~1/10 as much RAM. The new parser also supports lightweight generics, and is more tolerant of white space. (This means that you now can – and should – use types like `NSArray *` for your exported properties and method arguments, instead of `NSStringArray`). Reviewed By: jspahrsummers Differential Revision: D2736636 fb-gh-sync-id: f6a11431935fa8acc8ac36f3471032ec9a1c8490 --- RCTAsyncLocalStorage.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 273ef40c..344832b9 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -319,7 +319,7 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry changedManifest:(BOOL #pragma mark - Exported JS Functions -RCT_EXPORT_METHOD(multiGet:(NSStringArray *)keys +RCT_EXPORT_METHOD(multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback) { NSDictionary *errorOut = [self _ensureSetup]; @@ -338,7 +338,7 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry changedManifest:(BOOL callback(@[RCTNullIfNil(errors), result]); } -RCT_EXPORT_METHOD(multiSet:(NSStringArrayArray *)kvPairs +RCT_EXPORT_METHOD(multiSet:(NSArray *> *)kvPairs callback:(RCTResponseSenderBlock)callback) { NSDictionary *errorOut = [self _ensureSetup]; @@ -358,7 +358,7 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry changedManifest:(BOOL callback(@[RCTNullIfNil(errors)]); } -RCT_EXPORT_METHOD(multiMerge:(NSStringArrayArray *)kvPairs +RCT_EXPORT_METHOD(multiMerge:(NSArray *> *)kvPairs callback:(RCTResponseSenderBlock)callback) { NSDictionary *errorOut = [self _ensureSetup]; @@ -394,7 +394,7 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry changedManifest:(BOOL callback(@[RCTNullIfNil(errors)]); } -RCT_EXPORT_METHOD(multiRemove:(NSStringArray *)keys +RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback) { NSDictionary *errorOut = [self _ensureSetup]; From 5dba2e53053175d5b120fb54f7d38d311a03c373 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Mon, 14 Dec 2015 12:04:38 -0800 Subject: [PATCH 38/75] Make it possible to set DB size Reviewed By: oli Differential Revision: D2749219 fb-gh-sync-id: 2165ed8a89c48687ad82cd1facf2b875d31ca997 --- ReactDatabaseSupplier.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ReactDatabaseSupplier.java b/ReactDatabaseSupplier.java index 620d2314..87f9daf5 100644 --- a/ReactDatabaseSupplier.java +++ b/ReactDatabaseSupplier.java @@ -30,7 +30,6 @@ public class ReactDatabaseSupplier extends SQLiteOpenHelper { private static final int DATABASE_VERSION = 1; private static final int SLEEP_TIME_MS = 30; - private static final long DEFAULT_MAX_DB_SIZE = 6L * 1024L * 1024L; // 6 MB in bytes static final String TABLE_CATALYST = "catalystLocalStorage"; static final String KEY_COLUMN = "key"; @@ -46,6 +45,7 @@ public class ReactDatabaseSupplier extends SQLiteOpenHelper { private Context mContext; private @Nullable SQLiteDatabase mDb; + private long mMaximumDatabaseSize = 6L * 1024L * 1024L; // 6 MB in bytes private ReactDatabaseSupplier(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -105,7 +105,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // This is a sane limit to protect the user from the app storing too much data in the database. // This also protects the database from filling up the disk cache and becoming malformed // (endTransaction() calls will throw an exception, not rollback, and leave the db malformed). - mDb.setMaximumSize(DEFAULT_MAX_DB_SIZE); + mDb.setMaximumSize(mMaximumDatabaseSize); return true; } @@ -137,6 +137,17 @@ public synchronized void clearAndCloseDatabase() throws RuntimeException { get().delete(TABLE_CATALYST, null, null); } + /** + * Sets the maximum size the database will grow to. The maximum size cannot + * be set below the current size. + */ + public synchronized void setMaximumSize(long size) { + mMaximumDatabaseSize = size; + if (mDb != null) { + mDb.setMaximumSize(mMaximumDatabaseSize); + } + } + private synchronized boolean deleteDatabase() { closeDatabase(); return mContext.deleteDatabase(DATABASE_NAME); From 22eca3d9342e0734351d36f82c94aff42eaa6b88 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 5 Jan 2016 10:00:37 -0800 Subject: [PATCH 39/75] Increased RCTInlineValueThreshold for asynclocalstorage Reviewed By: tadeuzagallo Differential Revision: D2641694 fb-gh-sync-id: e35df5408730ce9ec267cbeeb556f8eba154df1f --- RCTAsyncLocalStorage.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 344832b9..46d06222 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -20,7 +20,7 @@ static NSString *const RCTStorageDirectory = @"RCTAsyncLocalStorage_V1"; static NSString *const RCTManifestFileName = @"manifest.json"; -static const NSUInteger RCTInlineValueThreshold = 100; +static const NSUInteger RCTInlineValueThreshold = 1024; #pragma mark - Static helper functions From c6ed6179e94136fbe5e56da01c8f51021ec889cc Mon Sep 17 00:00:00 2001 From: Mark Vayngrib Date: Fri, 5 Feb 2016 16:44:44 -0800 Subject: [PATCH 40/75] multiGet breaking test and fix Summary: the flush + optimized multiGet result in an obscure bug that results when two multiGet requests with overlapping key sets get issued. The result array for both requests ends up bigger than the key array (because it has duplicates) Closes https://github.com/facebook/react-native/pull/5514 Reviewed By: svcscm Differential Revision: D2908264 Pulled By: nicklockwood fb-gh-sync-id: 60be1bce4acfc47083e4ae28bb8b63f9dfa56039 --- AsyncStorage.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index eb41c8a7..f1ed35c3 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -180,14 +180,16 @@ var AsyncStorage = { // Even though the runtime complexity of this is theoretically worse vs if we used a map, // it's much, much faster in practice for the data sets we deal with (we avoid // allocating result pair arrays). This was heavily benchmarked. + // + // Is there a way to avoid using the map but fix the bug in this breaking test? + // https://github.com/facebook/react-native/commit/8dd8ad76579d7feef34c014d387bf02065692264 + let map = {}; + result.forEach(([key, value]) => map[key] = value); const reqLength = getRequests.length; for (let i = 0; i < reqLength; i++) { const request = getRequests[i]; const requestKeys = request.keys; - var requestResult = result.filter(function(resultPair) { - return requestKeys.indexOf(resultPair[0]) !== -1; - }); - + let requestResult = requestKeys.map(key => [key, map[key]]); request.callback && request.callback(null, requestResult); request.resolve && request.resolve(requestResult); } @@ -214,6 +216,7 @@ var AsyncStorage = { var getRequest = { keys: keys, callback: callback, + // do we need this? keyIndex: this._getKeys.length, resolve: null, reject: null, @@ -225,7 +228,12 @@ var AsyncStorage = { }); this._getRequests.push(getRequest); - this._getKeys.push.apply(this._getKeys, keys); + // avoid fetching duplicates + keys.forEach(key => { + if (this._getKeys.indexOf(key) === -1) { + this._getKeys.push(key); + } + }); return promiseResult; }, From fd2eecacb475755b1cba738c2c8e95e1cbf8a7d1 Mon Sep 17 00:00:00 2001 From: Chris Geirman Date: Mon, 14 Mar 2016 17:37:34 -0700 Subject: [PATCH 41/75] =?UTF-8?q?add=20examples=20that=20demonstrate=20how?= =?UTF-8?q?=20to=20use=20mergeItem,=20multiMerge,=20multi=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary:Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: (You can skip this if you're fixing a typo or adding an app to the Showcase.) Explain the **motivation** for making this change. What existing problem does the pull request solve? it wasn't clear how to use various methods such as mergeItem, multiMerge, etc, so after figuring it out I thought it would be useful to provide clear examples for others. Example: When "Adding a function to do X", explain why it is necessary to have a way to do X. **Test plan (required)** Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. Make sure tests pass on both Travis and Circle CI. **Code formatting** See the simple [style guide](https://github.com/facebook/react-native/blob/master/CONTRIBUTING.md#style-guide). …Set, multiGet, and multiRemove Closes https://github.com/facebook/react-native/pull/6423 Differential Revision: D3048502 Pulled By: vjeux fb-gh-sync-id: dfe13c6a88fa9d3e7646b5ecaa5f594bfaba8271 shipit-source-id: dfe13c6a88fa9d3e7646b5ecaa5f594bfaba8271 --- AsyncStorage.js | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/AsyncStorage.js b/AsyncStorage.js index f1ed35c3..df570bc1 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -105,6 +105,30 @@ var AsyncStorage = { /** * Merges existing value with input value, assuming they are stringified json. * Returns a `Promise` object. Not supported by all native implementations. + * + * Example: + * ```javascript + * let UID123_object = { + * name: 'Chris', + * age: 30, + * traits: {hair: 'brown', eyes: 'brown'}, + * }; + + // need only define what will be added or updated + * let UID123_delta = { + * age: 31, + * traits: {eyes: 'blue', shoe_size: 10} + * }; + + * AsyncStorage.setItem(store_key, JSON.stringify(UID123_object), () => { + * AsyncStorage.mergeItem('UID123', JSON.stringify(UID123_delta), () => { + * AsyncStorage.getItem('UID123', (err, result) => { + * console.log(result); + * // => {'name':'Chris','age':31,'traits':{'shoe_size':10,'hair':'brown','eyes':'blue'}} + * }); + * }); + * }); + * ``` */ mergeItem: function( key: string, @@ -144,6 +168,8 @@ var AsyncStorage = { /** * Gets *all* keys known to the app, for all callers, libraries, etc. Returns a `Promise` object. + * + * Example: see multiGet for example */ getAllKeys: function(callback?: ?(error: ?Error, keys: ?Array) => void): Promise { return new Promise((resolve, reject) => { @@ -201,6 +227,19 @@ var AsyncStorage = { * matches the input format of multiSet. Returns a `Promise` object. * * multiGet(['k1', 'k2'], cb) -> cb([['k1', 'val1'], ['k2', 'val2']]) + * + * Example: + * ```javascript + * AsyncStorage.getAllKeys((err, keys) => { + * AsyncStorage.multiGet(keys, (err, stores) => { + * stores.map((result, i, store) => { + * // get at each store's key/value so you can work with it + * let key = store[i][0]; + * let value = store[i][1]; + * }); + * }); + * }); + * ``` */ multiGet: function( keys: Array, @@ -243,6 +282,8 @@ var AsyncStorage = { * the output of multiGet, e.g. Returns a `Promise` object. * * multiSet([['k1', 'val1'], ['k2', 'val2']], cb); + * + * Example: see multiMerge for an example */ multiSet: function( keyValuePairs: Array>, @@ -263,6 +304,15 @@ var AsyncStorage = { /** * Delete all the keys in the `keys` array. Returns a `Promise` object. + * + * Example: + * ```javascript + * let keys = ['k1', 'k2']; + * AsyncStorage.multiRemove(keys, (err) => { + * // keys k1 & k2 removed, if they existed + * // do most stuff after removal (if you want) + * }); + * ``` */ multiRemove: function( keys: Array, @@ -286,6 +336,52 @@ var AsyncStorage = { * json. Returns a `Promise` object. * * Not supported by all native implementations. + * + * Example: + * ```javascript + // first user, initial values + * let UID234_object = { + * name: 'Chris', + * age: 30, + * traits: {hair: 'brown', eyes: 'brown'}, + * }; + + * // first user, delta values + * let UID234_delta = { + * age: 31, + * traits: {eyes: 'blue', shoe_size: 10}, + * }; + + * // second user, initial values + * let UID345_object = { + * name: 'Marge', + * age: 25, + * traits: {hair: 'blonde', eyes: 'blue'}, + * }; + + * // second user, delta values + * let UID345_delta = { + * age: 26, + * traits: {eyes: 'green', shoe_size: 6}, + * }; + + * let multi_set_pairs = [['UID234', JSON.stringify(UID234_object)], ['UID345', JSON.stringify(UID345_object)]] + * let multi_merge_pairs = [['UID234', JSON.stringify(UID234_delta)], ['UID345', JSON.stringify(UID345_delta)]] + + * AsyncStorage.multiSet(multi_set_pairs, (err) => { + * AsyncStorage.multiMerge(multi_merge_pairs, (err) => { + * AsyncStorage.multiGet(['UID234','UID345'], (err, stores) => { + * stores.map( (result, i, store) => { + * let key = store[i][0]; + * let val = store[i][1]; + * console.log(key, val); + * // => UID234 {"name":"Chris","age":31,"traits":{"shoe_size":10,"hair":"brown","eyes":"blue"}} + * // => UID345 {"name":"Marge","age":26,"traits":{"shoe_size":6,"hair":"blonde","eyes":"green"}} + * }); + * }); + * }); + * }); + * ``` */ multiMerge: function( keyValuePairs: Array>, From 189dedf29c86a38177273b0ff6527fceae03bff0 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Fri, 1 Apr 2016 07:01:51 -0700 Subject: [PATCH 42/75] Increase RN devtools retry timeout Summary:The 200ms timeout was causing resource issues and causing a lot of overhead when you're not running the devtools, since it will basically create a new socket every 200ms. Also clean up the way we do logging so it's completely compiled out in prod, and standardize all the names we use for threading to lowercase react. Reviewed By: frantic Differential Revision: D3115975 fb-gh-sync-id: e6e51c0621d8e9fc4eadb864acd678b8b5d322a1 fbshipit-source-id: e6e51c0621d8e9fc4eadb864acd678b8b5d322a1 --- RCTAsyncLocalStorage.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 46d06222..f3f6a208 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -117,7 +117,7 @@ static dispatch_queue_t RCTGetMethodQueue() static dispatch_queue_t queue; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - queue = dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL); + queue = dispatch_queue_create("com.facebook.react.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL); }); return queue; } From 946be994561fd7a7b69f8d096d2a5ee00dbab6f1 Mon Sep 17 00:00:00 2001 From: Kushal Dave Date: Sun, 10 Apr 2016 18:01:53 -0700 Subject: [PATCH 43/75] Update AsyncStorage.js Summary:Updating docs to discuss both iOS and Android and to make more sense standing alone on the web site. Closes https://github.com/facebook/react-native/pull/6592 Differential Revision: D3161831 fb-gh-sync-id: 984621702fbf408445a04b771d3fc5f76a65af64 fbshipit-source-id: 984621702fbf408445a04b771d3fc5f76a65af64 --- AsyncStorage.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index df570bc1..48b9f59b 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -28,8 +28,10 @@ var RCTAsyncStorage = RCTAsyncRocksDBStorage || RCTAsyncSQLiteStorage || RCTAsyn * of AsyncStorage directly for anything more than light usage since it * operates globally. * - * This JS code is a simple facade over the native iOS implementation to provide - * a clear JS API, real Error objects, and simple non-multi functions. Each + * On iOS, AsyncStorage is backed by native code that stores small values in a serialized + * dictionary and larger values in separate files. On Android, AsyncStorage will use either + * RocksDB or SQLite based on what is available. This JS code is a simple facade that + * provides a clear JS API, real Error objects, and simple non-multi functions. Each * method returns a `Promise` object. */ var AsyncStorage = { From 99853c60104f0fb8bf54c17e321b4f6d84f7ff26 Mon Sep 17 00:00:00 2001 From: taelimoh Date: Thu, 26 May 2016 01:37:09 -0700 Subject: [PATCH 44/75] change undeclared variable to intended value Summary: Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: (You can skip this if you're fixing a typo or adding an app to the Showcase.) Explain the **motivation** for making this change. What existing problem does the pull request solve? Prefer **small pull requests**. These are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise please split it. **Test plan (required)** Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. Make sure tests pass on both Travis and Circle CI. **Code formatting** Look around. Match the style of the rest of the codebase. See also the simple [style guide](https://github.com/facebook/react-native/blob/master/CONTRIBUTING.md#style-guide). For more info, see the ["Pull Requests" section of our "Contributing" guidelines](https://github.com/facebook/react-native/blob/mas Closes https://github.com/facebook/react-native/pull/7770 Differential Revision: D3352007 fbshipit-source-id: eedb964d245445b61fed79245380f0803473c455 --- AsyncStorage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index 48b9f59b..01d76748 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -122,7 +122,7 @@ var AsyncStorage = { * traits: {eyes: 'blue', shoe_size: 10} * }; - * AsyncStorage.setItem(store_key, JSON.stringify(UID123_object), () => { + * AsyncStorage.setItem('UID123', JSON.stringify(UID123_object), () => { * AsyncStorage.mergeItem('UID123', JSON.stringify(UID123_delta), () => { * AsyncStorage.getItem('UID123', (err, result) => { * console.log(result); From fe1944190b1b33946a64b0e2a2c4fbf9636d0042 Mon Sep 17 00:00:00 2001 From: Christine Abernathy Date: Fri, 24 Jun 2016 08:42:28 -0700 Subject: [PATCH 45/75] Update AsyncStorage doc Summary: Relates to #8203 for AsyncStorage API update. - Added a small example to the intro section. - Added jsdoc format tags to show up class description, parameter descriptions. - Word-smithed many of the method descriptions. I also made a bug fix to the autogen. It wasn't handling the scenario where a method may have no parameters. **Test plan (required)** Wrote a small sample app to test the snippet added to the intro section. Ran website locally: http://localhost:8079/react-native/docs/asyncstorage.html ![api_asyncstorage](https://cloud.githubusercontent.com/assets/691109/16329457/84f9d69c-3997-11e6-9e68-3a475df90377.png) Ran changed files through the linter. Closes https://github.com/facebook/react-native/pull/8396 Differential Revision: D3481783 Pulled By: JoelMarcey fbshipit-source-id: ebc4b9695482ada8a3455e621534d2a7fb11edf4 --- AsyncStorage.js | 191 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 135 insertions(+), 56 deletions(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index 01d76748..471c5304 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -9,6 +9,7 @@ * @providesModule AsyncStorage * @noflow * @flow-weak + * @jsdoc */ 'use strict'; @@ -21,18 +22,45 @@ var RCTAsyncFileStorage = NativeModules.AsyncLocalStorage; var RCTAsyncStorage = RCTAsyncRocksDBStorage || RCTAsyncSQLiteStorage || RCTAsyncFileStorage; /** - * AsyncStorage is a simple, asynchronous, persistent, key-value storage + * @class + * @description + * `AsyncStorage` is a simple, asynchronous, persistent, key-value storage * system that is global to the app. It should be used instead of LocalStorage. * - * It is recommended that you use an abstraction on top of AsyncStorage instead - * of AsyncStorage directly for anything more than light usage since it - * operates globally. + * It is recommended that you use an abstraction on top of `AsyncStorage` + * instead of `AsyncStorage` directly for anything more than light usage since + * it operates globally. * - * On iOS, AsyncStorage is backed by native code that stores small values in a serialized - * dictionary and larger values in separate files. On Android, AsyncStorage will use either - * RocksDB or SQLite based on what is available. This JS code is a simple facade that - * provides a clear JS API, real Error objects, and simple non-multi functions. Each - * method returns a `Promise` object. + * On iOS, `AsyncStorage` is backed by native code that stores small values in a + * serialized dictionary and larger values in separate files. On Android, + * `AsyncStorage` will use either [RocksDB](http://rocksdb.org/) or SQLite + * based on what is available. + * + * The `AsyncStorage` JavaScript code is a simple facade that provides a clear + * JavaScript API, real `Error` objects, and simple non-multi functions. Each + * method in the API returns a `Promise` object. + * + * Persisting data: + * ``` + * try { + * await AsyncStorage.setItem('@MySuperStore:key', 'I like to save it.'); + * } catch (error) { + * // Error saving data + * } + * ``` + * + * Fetching data: + * ``` + * try { + * const value = await AsyncStorage.getItem('@MySuperStore:key'); + * if (value !== null){ + * // We have data!! + * console.log(value); + * } + * } catch (error) { + * // Error retrieving data + * } + * ``` */ var AsyncStorage = { _getRequests: ([]: Array), @@ -40,8 +68,12 @@ var AsyncStorage = { _immediate: (null: ?number), /** - * Fetches `key` and passes the result to `callback`, along with an `Error` if - * there is any. Returns a `Promise` object. + * Fetches an item for a `key` and invokes a callback upon completion. + * Returns a `Promise` object. + * @param key Key of the item to fetch. + * @param callback Function that will be called with a result if found or + * any error. + * @returns A `Promise` object. */ getItem: function( key: string, @@ -63,8 +95,12 @@ var AsyncStorage = { }, /** - * Sets `value` for `key` and calls `callback` on completion, along with an - * `Error` if there is any. Returns a `Promise` object. + * Sets the value for a `key` and invokes a callback upon completion. + * Returns a `Promise` object. + * @param key Key of the item to set. + * @param value Value to set for the `key`. + * @param callback Function that will be called with any error. + * @returns A `Promise` object. */ setItem: function( key: string, @@ -85,7 +121,11 @@ var AsyncStorage = { }, /** + * Removes an item for a `key` and invokes a callback upon completion. * Returns a `Promise` object. + * @param key Key of the item to remove. + * @param callback Function that will be called with any error. + * @returns A `Promise` object. */ removeItem: function( key: string, @@ -105,32 +145,39 @@ var AsyncStorage = { }, /** - * Merges existing value with input value, assuming they are stringified json. - * Returns a `Promise` object. Not supported by all native implementations. + * Merges an existing `key` value with an input value, assuming both values + * are stringified JSON. Returns a `Promise` object. * - * Example: - * ```javascript + * **NOTE:** This is not supported by all native implementations. + * + * @param key Key of the item to modify. + * @param value New value to merge for the `key`. + * @param callback Function that will be called with any error. + * @returns A `Promise` object. + * + * @example Example * let UID123_object = { * name: 'Chris', * age: 30, * traits: {hair: 'brown', eyes: 'brown'}, * }; - - // need only define what will be added or updated + * // You only need to define what will be added or updated * let UID123_delta = { * age: 31, * traits: {eyes: 'blue', shoe_size: 10} * }; - + * * AsyncStorage.setItem('UID123', JSON.stringify(UID123_object), () => { * AsyncStorage.mergeItem('UID123', JSON.stringify(UID123_delta), () => { * AsyncStorage.getItem('UID123', (err, result) => { * console.log(result); - * // => {'name':'Chris','age':31,'traits':{'shoe_size':10,'hair':'brown','eyes':'blue'}} * }); * }); * }); - * ``` + * + * // Console log result: + * // => {'name':'Chris','age':31,'traits': + * // {'shoe_size':10,'hair':'brown','eyes':'blue'}} */ mergeItem: function( key: string, @@ -151,9 +198,11 @@ var AsyncStorage = { }, /** - * Erases *all* AsyncStorage for all clients, libraries, etc. You probably - * don't want to call this - use removeItem or multiRemove to clear only your - * own keys instead. Returns a `Promise` object. + * Erases *all* `AsyncStorage` for all clients, libraries, etc. You probably + * don't want to call this; use `removeItem` or `multiRemove` to clear only + * your app's keys. Returns a `Promise` object. + * @param callback Function that will be called with any error. + * @returns A `Promise` object. */ clear: function(callback?: ?(error: ?Error) => void): Promise { return new Promise((resolve, reject) => { @@ -169,9 +218,12 @@ var AsyncStorage = { }, /** - * Gets *all* keys known to the app, for all callers, libraries, etc. Returns a `Promise` object. + * Gets *all* keys known to your app; for all callers, libraries, etc. + * Returns a `Promise` object. + * @param callback Function that will be called the keys found and any error. + * @returns A `Promise` object. * - * Example: see multiGet for example + * Example: see the `multiGet` example. */ getAllKeys: function(callback?: ?(error: ?Error, keys: ?Array) => void): Promise { return new Promise((resolve, reject) => { @@ -196,7 +248,7 @@ var AsyncStorage = { * indicate which key caused the error. */ - /** Flushes any pending requests using a single multiget */ + /** Flushes any pending requests using a single batch call to get the data. */ flushGetRequests: function(): void { const getRequests = this._getRequests; const getKeys = this._getKeys; @@ -225,13 +277,23 @@ var AsyncStorage = { }, /** - * multiGet invokes callback with an array of key-value pair arrays that - * matches the input format of multiSet. Returns a `Promise` object. + * This allows you to batch the fetching of items given an array of `key` + * inputs. Your callback will be invoked with an array of corresponding + * key-value pairs found: * - * multiGet(['k1', 'k2'], cb) -> cb([['k1', 'val1'], ['k2', 'val2']]) + * ``` + * multiGet(['k1', 'k2'], cb) -> cb([['k1', 'val1'], ['k2', 'val2']]) + * ``` + * + * The method returns a `Promise` object. + * + * @param keys Array of key for the items to get. + * @param callback Function that will be called with a key-value array of + * the results, plus an array of any key-specific errors found. + * @returns A `Promise` object. + * + * @example Example * - * Example: - * ```javascript * AsyncStorage.getAllKeys((err, keys) => { * AsyncStorage.multiGet(keys, (err, stores) => { * stores.map((result, i, store) => { @@ -241,7 +303,6 @@ var AsyncStorage = { * }); * }); * }); - * ``` */ multiGet: function( keys: Array, @@ -280,12 +341,20 @@ var AsyncStorage = { }, /** - * multiSet and multiMerge take arrays of key-value array pairs that match - * the output of multiGet, e.g. Returns a `Promise` object. + * Use this as a batch operation for storing multiple key-value pairs. When + * the operation completes you'll get a single callback with any errors: * - * multiSet([['k1', 'val1'], ['k2', 'val2']], cb); + * ``` + * multiSet([['k1', 'val1'], ['k2', 'val2']], cb); + * ``` + * + * The method returns a `Promise` object. * - * Example: see multiMerge for an example + * @param keyValuePairs Array of key-value array for the items to set. + * @param callback Function that will be called with an array of any + * key-specific errors found. + * @returns A `Promise` object. + * Example: see the `multiMerge` example. */ multiSet: function( keyValuePairs: Array>, @@ -305,16 +374,20 @@ var AsyncStorage = { }, /** - * Delete all the keys in the `keys` array. Returns a `Promise` object. + * Call this to batch the deletion of all keys in the `keys` array. Returns + * a `Promise` object. * - * Example: - * ```javascript + * @param keys Array of key for the items to delete. + * @param callback Function that will be called an array of any key-specific + * errors found. + * @returns A `Promise` object. + * + * @example Example * let keys = ['k1', 'k2']; * AsyncStorage.multiRemove(keys, (err) => { * // keys k1 & k2 removed, if they existed * // do most stuff after removal (if you want) * }); - * ``` */ multiRemove: function( keys: Array, @@ -334,42 +407,47 @@ var AsyncStorage = { }, /** - * Merges existing values with input values, assuming they are stringified - * json. Returns a `Promise` object. + * Batch operation to merge in existing and new values for a given set of + * keys. This assumes that the values are stringified JSON. Returns a + * `Promise` object. * - * Not supported by all native implementations. + * **NOTE**: This is not supported by all native implementations. * - * Example: - * ```javascript - // first user, initial values + * @param keyValuePairs Array of key-value array for the items to merge. + * @param callback Function that will be called with an array of any + * key-specific errors found. + * @returns A `Promise` object. + * + * @example Example + * // first user, initial values * let UID234_object = { * name: 'Chris', * age: 30, * traits: {hair: 'brown', eyes: 'brown'}, * }; - + * * // first user, delta values * let UID234_delta = { * age: 31, * traits: {eyes: 'blue', shoe_size: 10}, * }; - + * * // second user, initial values * let UID345_object = { * name: 'Marge', * age: 25, * traits: {hair: 'blonde', eyes: 'blue'}, * }; - + * * // second user, delta values * let UID345_delta = { * age: 26, * traits: {eyes: 'green', shoe_size: 6}, * }; - + * * let multi_set_pairs = [['UID234', JSON.stringify(UID234_object)], ['UID345', JSON.stringify(UID345_object)]] * let multi_merge_pairs = [['UID234', JSON.stringify(UID234_delta)], ['UID345', JSON.stringify(UID345_delta)]] - + * * AsyncStorage.multiSet(multi_set_pairs, (err) => { * AsyncStorage.multiMerge(multi_merge_pairs, (err) => { * AsyncStorage.multiGet(['UID234','UID345'], (err, stores) => { @@ -377,13 +455,14 @@ var AsyncStorage = { * let key = store[i][0]; * let val = store[i][1]; * console.log(key, val); - * // => UID234 {"name":"Chris","age":31,"traits":{"shoe_size":10,"hair":"brown","eyes":"blue"}} - * // => UID345 {"name":"Marge","age":26,"traits":{"shoe_size":6,"hair":"blonde","eyes":"green"}} * }); * }); * }); * }); - * ``` + * + * // Console log results: + * // => UID234 {"name":"Chris","age":31,"traits":{"shoe_size":10,"hair":"brown","eyes":"blue"}} + * // => UID345 {"name":"Marge","age":26,"traits":{"shoe_size":6,"hair":"blonde","eyes":"green"}} */ multiMerge: function( keyValuePairs: Array>, From 91e820a1d06f72d0ab4724e508bd15ac278f7b58 Mon Sep 17 00:00:00 2001 From: Skotch Vail Date: Thu, 7 Jul 2016 12:36:56 -0700 Subject: [PATCH 46/75] Automated changes to remove implicit capture of self in blocks: Libraries/FBReactKit/BUCK Reviewed By: javache Differential Revision: D3442470 fbshipit-source-id: 584a2bb3df5f7122166778b8fd44fae45560491e --- RCTAsyncLocalStorage.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index f3f6a208..6d150d07 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -169,7 +169,7 @@ - (dispatch_queue_t)methodQueue - (void)clearAllData { dispatch_async(RCTGetMethodQueue(), ^{ - [_manifest removeAllObjects]; + [self->_manifest removeAllObjects]; [RCTGetCache() removeAllObjects]; RCTDeleteStorageDirectory(); }); From a51f50b829ff6b39dbc75e060d3c9100efa8c1c0 Mon Sep 17 00:00:00 2001 From: Ariel Elkin Date: Fri, 15 Jul 2016 09:03:24 -0700 Subject: [PATCH 47/75] update AsyncStorage.js documentation Summary: Otherwise it could be perceived as safe for storing sensitive data. See https://medium.com/ntoscano/react-native-persistent-user-login-6a48ff380ab8#.2pxlht5ti Closes https://github.com/facebook/react-native/pull/8809 Differential Revision: D3570130 Pulled By: vjeux fbshipit-source-id: ee3e8f7fc882a67e9991838467fad75c86d26a52 --- AsyncStorage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index 471c5304..9006476d 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -24,7 +24,7 @@ var RCTAsyncStorage = RCTAsyncRocksDBStorage || RCTAsyncSQLiteStorage || RCTAsyn /** * @class * @description - * `AsyncStorage` is a simple, asynchronous, persistent, key-value storage + * `AsyncStorage` is a simple, unencrypted, asynchronous, persistent, key-value storage * system that is global to the app. It should be used instead of LocalStorage. * * It is recommended that you use an abstraction on top of `AsyncStorage` From ef51360468b0b6f616ba0d044980ef8dfa0f70d2 Mon Sep 17 00:00:00 2001 From: David Aurelio Date: Tue, 9 Aug 2016 06:32:41 -0700 Subject: [PATCH 48/75] Auto-fix lint errors Reviewed By: bestander Differential Revision: D3683952 fbshipit-source-id: 9484d0b0e86859e8edaca0da1aa13a667f200905 --- AsyncStorage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index 9006476d..a0666c06 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -263,13 +263,13 @@ var AsyncStorage = { // // Is there a way to avoid using the map but fix the bug in this breaking test? // https://github.com/facebook/react-native/commit/8dd8ad76579d7feef34c014d387bf02065692264 - let map = {}; + const map = {}; result.forEach(([key, value]) => map[key] = value); const reqLength = getRequests.length; for (let i = 0; i < reqLength; i++) { const request = getRequests[i]; const requestKeys = request.keys; - let requestResult = requestKeys.map(key => [key, map[key]]); + const requestResult = requestKeys.map(key => [key, map[key]]); request.callback && request.callback(null, requestResult); request.resolve && request.resolve(requestResult); } From ee40d2c11e08d9501b1cacc45e2d6b45d2ee242a Mon Sep 17 00:00:00 2001 From: Aaron Chiu Date: Thu, 11 Aug 2016 15:43:35 -0700 Subject: [PATCH 49/75] Convert modules to use @ReactModule instead of getName() Reviewed By: astreet Differential Revision: D3334273 fbshipit-source-id: a33bf72c5c184844885ef3ef610a05d9c102c8ea --- AsyncStorageModule.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java index 35b61506..98dc47c9 100644 --- a/AsyncStorageModule.java +++ b/AsyncStorageModule.java @@ -24,6 +24,7 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.annotations.ReactModule; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.SetBuilder; import com.facebook.react.modules.common.ModuleDataCleaner; @@ -32,6 +33,7 @@ import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST; import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN; +@ReactModule(name = "AsyncSQLiteDBStorage") public final class AsyncStorageModule extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable { @@ -47,11 +49,6 @@ public AsyncStorageModule(ReactApplicationContext reactContext) { mReactDatabaseSupplier = ReactDatabaseSupplier.getInstance(reactContext); } - @Override - public String getName() { - return "AsyncSQLiteDBStorage"; - } - @Override public void initialize() { super.initialize(); From d5456d241ee49cfa3ca7f615d1963b58b755cdd6 Mon Sep 17 00:00:00 2001 From: Aaron Chiu Date: Fri, 12 Aug 2016 15:49:13 -0700 Subject: [PATCH 50/75] Reverted commit D3334273 Reviewed By: astreet Differential Revision: D3334273 fbshipit-source-id: a3849604ea89db74900850c294685e7da9aeeacc --- AsyncStorageModule.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java index 98dc47c9..35b61506 100644 --- a/AsyncStorageModule.java +++ b/AsyncStorageModule.java @@ -24,7 +24,6 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; -import com.facebook.react.bridge.annotations.ReactModule; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.SetBuilder; import com.facebook.react.modules.common.ModuleDataCleaner; @@ -33,7 +32,6 @@ import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST; import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN; -@ReactModule(name = "AsyncSQLiteDBStorage") public final class AsyncStorageModule extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable { @@ -49,6 +47,11 @@ public AsyncStorageModule(ReactApplicationContext reactContext) { mReactDatabaseSupplier = ReactDatabaseSupplier.getInstance(reactContext); } + @Override + public String getName() { + return "AsyncSQLiteDBStorage"; + } + @Override public void initialize() { super.initialize(); From d3a2db4b2fa8419022c8c9ca2f62a56457ec86e8 Mon Sep 17 00:00:00 2001 From: Jennifer Wang Date: Thu, 18 Aug 2016 10:29:34 -0700 Subject: [PATCH 51/75] Add wrapper class for async storage Reviewed By: wsanville Differential Revision: D3654554 fbshipit-source-id: bc21da4b5c46228136cc80592f84206d2deb541e --- AsyncLocalStorageUtil.java | 4 ++-- ReactDatabaseSupplier.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AsyncLocalStorageUtil.java b/AsyncLocalStorageUtil.java index 78b58639..ea6487c9 100644 --- a/AsyncLocalStorageUtil.java +++ b/AsyncLocalStorageUtil.java @@ -31,7 +31,7 @@ /** * Helper for database operations. */ -/* package */ class AsyncLocalStorageUtil { +public class AsyncLocalStorageUtil { /** * Build the String required for an SQL select statement: @@ -60,7 +60,7 @@ /** * Returns the value of the given key, or null if not found. */ - /* package */ static @Nullable String getItemImpl(SQLiteDatabase db, String key) { + public static @Nullable String getItemImpl(SQLiteDatabase db, String key) { String[] columns = {VALUE_COLUMN}; String[] selectionArgs = {key}; diff --git a/ReactDatabaseSupplier.java b/ReactDatabaseSupplier.java index 87f9daf5..9787ddf6 100644 --- a/ReactDatabaseSupplier.java +++ b/ReactDatabaseSupplier.java @@ -112,7 +112,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { /** * Create and/or open the database. */ - /* package */ synchronized SQLiteDatabase get() { + public synchronized SQLiteDatabase get() { ensureDatabase(); return mDb; } From fbaef6cf72fd53eab7d46a6035aeb4631d43fa39 Mon Sep 17 00:00:00 2001 From: Aaron Chiu Date: Fri, 2 Sep 2016 16:15:11 -0700 Subject: [PATCH 52/75] covert RNFeedPackage and it's modules to use @ReactModule and @ReactModuleList Reviewed By: lexs Differential Revision: D3796860 fbshipit-source-id: d4b5f3635754ef28277b79cb1ea9bab07ba3ea6e --- AsyncStorageModule.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java index 35b61506..7ccee315 100644 --- a/AsyncStorageModule.java +++ b/AsyncStorageModule.java @@ -26,12 +26,14 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.SetBuilder; +import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.modules.common.ModuleDataCleaner; import static com.facebook.react.modules.storage.ReactDatabaseSupplier.KEY_COLUMN; import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST; import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN; +@ReactModule(name = "AsyncSQLiteDBStorage") public final class AsyncStorageModule extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable { From 6bc3d646dc37458499b986e092e3c8c9ea0d63d7 Mon Sep 17 00:00:00 2001 From: Douglas Lowder Date: Tue, 27 Sep 2016 06:19:45 -0700 Subject: [PATCH 53/75] Apple TV support 1: existing Objective C code should compile for tvOS Summary: First commit for Apple TV support: changes to existing Objective-C code so that it will compile correctly for tvOS. Closes https://github.com/facebook/react-native/pull/9649 Differential Revision: D3916021 Pulled By: javache fbshipit-source-id: 34acc9daf3efff835ffe38c43ba5d4098a02c830 --- RCTAsyncLocalStorage.m | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 6d150d07..e3972229 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -67,7 +67,11 @@ static void RCTAppendError(NSDictionary *error, NSMutableArray * static NSString *storageDirectory = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ +#if TARGET_OS_TV + storageDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; +#else storageDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; +#endif storageDirectory = [storageDirectory stringByAppendingPathComponent:RCTStorageDirectory]; }); return storageDirectory; @@ -214,6 +218,10 @@ - (NSDictionary *)_ensureSetup { RCTAssertThread(RCTGetMethodQueue(), @"Must be executed on storage thread"); +#if TARGET_OS_TV + RCTLogWarn(@"Persistent storage is not supported on tvOS, your data may be removed at any point.") +#endif + NSError *error = nil; if (!RCTHasCreatedStorageDirectory) { [[NSFileManager defaultManager] createDirectoryAtPath:RCTGetStorageDirectory() From 51bb218fa38c066ec27d49fdfbd1b857b019fb55 Mon Sep 17 00:00:00 2001 From: Dan Caspi Date: Wed, 16 Nov 2016 07:28:01 -0800 Subject: [PATCH 54/75] Enforcing semi-colon consistency between dev and release modes when calling RCTLog* Summary: The various RCTLog macros (`RCTLogWarn`, `RCTLogError`, etc..) are based on the `_RCTLog` macro, which, in its expanded form, has a semi-colon in DEV mode (i.e., when `RCTLOG_ENABLED` is set to 1), and doesn't have one when `RCTLOG_ENABLED` is set to 0. This could lead to a situation where code will compile in DEV but will fail to compile for prod. Fixing this by removing the semicolon from the DEV version (as should). Reviewed By: javache Differential Revision: D4189133 fbshipit-source-id: 54cb4e2c96d1e48d9df88464aa58b13af432c2bd --- RCTAsyncLocalStorage.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index e3972229..34442779 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -219,7 +219,7 @@ - (NSDictionary *)_ensureSetup RCTAssertThread(RCTGetMethodQueue(), @"Must be executed on storage thread"); #if TARGET_OS_TV - RCTLogWarn(@"Persistent storage is not supported on tvOS, your data may be removed at any point.") + RCTLogWarn(@"Persistent storage is not supported on tvOS, your data may be removed at any point."); #endif NSError *error = nil; From 59f0c6ac4d3b881dc23d04833317f255bd32de88 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Wed, 23 Nov 2016 07:47:52 -0800 Subject: [PATCH 55/75] Move all header imports to "" Summary: To make React Native play nicely with our internal build infrastructure we need to properly namespace all of our header includes. Where previously you could do `#import "RCTBridge.h"`, you must now write this as `#import `. If your xcode project still has a custom header include path, both variants will likely continue to work, but for new projects, we're defaulting the header include path to `$(BUILT_PRODUCTS_DIR)/usr/local/include`, where the React and CSSLayout targets will copy a subset of headers too. To make Xcode copy headers phase work properly, you may need to add React as an explicit dependency to your app's scheme and disable "parallelize build". Reviewed By: mmmulani Differential Revision: D4213120 fbshipit-source-id: 84a32a4b250c27699e6795f43584f13d594a9a82 --- RCTAsyncLocalStorage.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RCTAsyncLocalStorage.h b/RCTAsyncLocalStorage.h index e6c129ef..cbb8cb42 100644 --- a/RCTAsyncLocalStorage.h +++ b/RCTAsyncLocalStorage.h @@ -7,8 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTBridgeModule.h" -#import "RCTInvalidating.h" +#import +#import /** * A simple, asynchronous, persistent, key-value storage system designed as a From 999c9cee58375452cd3c3bb65ce82176226c83d0 Mon Sep 17 00:00:00 2001 From: zongjingyao Date: Thu, 15 Dec 2016 22:28:48 -0800 Subject: [PATCH 56/75] result in RCTAsyncStorage.multiGet could be null. Summary: App will crash if result is null. Closes https://github.com/facebook/react-native/pull/10338 Differential Revision: D4335305 Pulled By: lacker fbshipit-source-id: 4910bfd7c56525a2ef1b252b56b8debd21fa2bae --- AsyncStorage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index a0666c06..b8b68202 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -264,7 +264,7 @@ var AsyncStorage = { // Is there a way to avoid using the map but fix the bug in this breaking test? // https://github.com/facebook/react-native/commit/8dd8ad76579d7feef34c014d387bf02065692264 const map = {}; - result.forEach(([key, value]) => map[key] = value); + result && result.forEach(([key, value]) => { map[key] = value; return value; }); const reqLength = getRequests.length; for (let i = 0; i < reqLength; i++) { const request = getRequests[i]; From 029128e3ffc97a69cdb814a918365f16483baf7f Mon Sep 17 00:00:00 2001 From: Aaron Chiu Date: Tue, 31 Jan 2017 04:49:58 -0800 Subject: [PATCH 57/75] strip off RK/RCT prefix from NativeModules Reviewed By: javache Differential Revision: D4487530 fbshipit-source-id: ea16720dc15e490267ad244c43ea9d237f81e353 --- AsyncStorageModule.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java index 7ccee315..5fdcbeb4 100644 --- a/AsyncStorageModule.java +++ b/AsyncStorageModule.java @@ -33,10 +33,12 @@ import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST; import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN; -@ReactModule(name = "AsyncSQLiteDBStorage") +@ReactModule(name = AsyncStorageModule.NAME) public final class AsyncStorageModule extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable { + protected static final String NAME = "AsyncSQLiteDBStorage"; + // SQL variable number limit, defined by SQLITE_LIMIT_VARIABLE_NUMBER: // https://raw.githubusercontent.com/android/platform_external_sqlite/master/dist/sqlite3.c private static final int MAX_SQL_KEYS = 999; @@ -51,7 +53,7 @@ public AsyncStorageModule(ReactApplicationContext reactContext) { @Override public String getName() { - return "AsyncSQLiteDBStorage"; + return NAME; } @Override From d682652c5c8d2568b96d72964bfe05a9e1344444 Mon Sep 17 00:00:00 2001 From: Aaron Chiu Date: Fri, 17 Feb 2017 16:39:50 -0800 Subject: [PATCH 58/75] inline a bunch of NativeModule requires Reviewed By: shergin Differential Revision: D4578180 fbshipit-source-id: 3764ffd32eb7e4698e928740bc72bbad02876894 --- AsyncStorage.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index b8b68202..06be6c46 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -13,13 +13,12 @@ */ 'use strict'; -var NativeModules = require('NativeModules'); -var RCTAsyncSQLiteStorage = NativeModules.AsyncSQLiteDBStorage; -var RCTAsyncRocksDBStorage = NativeModules.AsyncRocksDBStorage; -var RCTAsyncFileStorage = NativeModules.AsyncLocalStorage; +const NativeModules = require('NativeModules'); // Use RocksDB if available, then SQLite, then file storage. -var RCTAsyncStorage = RCTAsyncRocksDBStorage || RCTAsyncSQLiteStorage || RCTAsyncFileStorage; +const RCTAsyncStorage = NativeModules.AsyncSQLiteDBStorage || + NativeModules.AsyncRocksDBStorage || + NativeModules.AsyncLocalStorage; /** * @class From 38511247d1248d3f67947f5258b0e395fbf54c19 Mon Sep 17 00:00:00 2001 From: Aaron Chiu Date: Tue, 21 Feb 2017 15:04:52 -0800 Subject: [PATCH 59/75] correctly order ASyncStorage Reviewed By: sahrens Differential Revision: D4585569 fbshipit-source-id: e0bebddea8a5810386e193eb6435f24ba3a5cbb0 --- AsyncStorage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index 06be6c46..0ba87ed6 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -16,8 +16,8 @@ const NativeModules = require('NativeModules'); // Use RocksDB if available, then SQLite, then file storage. -const RCTAsyncStorage = NativeModules.AsyncSQLiteDBStorage || - NativeModules.AsyncRocksDBStorage || +const RCTAsyncStorage = NativeModules.AsyncRocksDBStorage || + NativeModules.AsyncSQLiteDBStorage || NativeModules.AsyncLocalStorage; /** From e15d5f901f571563941f5bfb7fb04da9425bf20f Mon Sep 17 00:00:00 2001 From: Kathy Gray Date: Mon, 10 Apr 2017 03:01:00 -0700 Subject: [PATCH 60/75] Delay module creation on call for constants when module has none Reviewed By: AaaChiuuu Differential Revision: D4810252 fbshipit-source-id: b2b98c3a8355dbb5775f254f25304a21f0bfee5b --- AsyncStorageModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java index 5fdcbeb4..28267e12 100644 --- a/AsyncStorageModule.java +++ b/AsyncStorageModule.java @@ -33,7 +33,7 @@ import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST; import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN; -@ReactModule(name = AsyncStorageModule.NAME) +@ReactModule(name = AsyncStorageModule.NAME, hasConstants = false) public final class AsyncStorageModule extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable { From 432f7cd10a84dda3f600ba3f421b8e13fab895d5 Mon Sep 17 00:00:00 2001 From: Kathy Gray Date: Thu, 13 Apr 2017 05:19:07 -0700 Subject: [PATCH 61/75] Find hasConstant status via preprocessing Reviewed By: javache Differential Revision: D4867563 fbshipit-source-id: 66e4505d142fc4776cd727a025005b43d500b167 --- AsyncStorageModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java index 28267e12..5fdcbeb4 100644 --- a/AsyncStorageModule.java +++ b/AsyncStorageModule.java @@ -33,7 +33,7 @@ import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST; import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN; -@ReactModule(name = AsyncStorageModule.NAME, hasConstants = false) +@ReactModule(name = AsyncStorageModule.NAME) public final class AsyncStorageModule extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable { From 0869a5e60a97f4eb4195c522d6c040b3041f19b2 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Wed, 14 Jun 2017 16:43:16 -0700 Subject: [PATCH 62/75] Remove SetBuilder Reviewed By: AaaChiuuu Differential Revision: D5237184 fbshipit-source-id: dde09febd0d4a5a42a62c7c6fbda3e6713d26152 --- AsyncStorageModule.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java index 5fdcbeb4..0dba149d 100644 --- a/AsyncStorageModule.java +++ b/AsyncStorageModule.java @@ -25,7 +25,6 @@ import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.ReactConstants; -import com.facebook.react.common.SetBuilder; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.modules.common.ModuleDataCleaner; @@ -95,7 +94,7 @@ protected void doInBackgroundGuarded(Void... params) { } String[] columns = {KEY_COLUMN, VALUE_COLUMN}; - HashSet keysRemaining = SetBuilder.newHashSet(); + HashSet keysRemaining = new HashSet<>(); WritableArray data = Arguments.createArray(); for (int keyStart = 0; keyStart < keys.size(); keyStart += MAX_SQL_KEYS) { int keyCount = Math.min(keys.size() - keyStart, MAX_SQL_KEYS); From 8ce25d5bb2cae82353f690cb59279eda4d15f527 Mon Sep 17 00:00:00 2001 From: Alex Dvornikov Date: Thu, 28 Sep 2017 09:22:16 -0700 Subject: [PATCH 63/75] always pass key parameter to RCTReadFile Differential Revision: D5921064 fbshipit-source-id: ad2dd352060fcb2c873dc5a91781797c9abd6c33 --- RCTAsyncLocalStorage.m | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 34442779..1064e04f 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -51,14 +51,20 @@ static void RCTAppendError(NSDictionary *error, NSMutableArray * NSError *error; NSStringEncoding encoding; NSString *entryString = [NSString stringWithContentsOfFile:filePath usedEncoding:&encoding error:&error]; + NSDictionary *extraData = @{@"key": RCTNullIfNil(key)}; + if (error) { - *errorOut = RCTMakeError(@"Failed to read storage file.", error, @{@"key": key}); - } else if (encoding != NSUTF8StringEncoding) { - *errorOut = RCTMakeError(@"Incorrect encoding of storage file: ", @(encoding), @{@"key": key}); - } else { - return entryString; + if (errorOut) *errorOut = RCTMakeError(@"Failed to read storage file.", error, extraData); + return nil; + } + + if (encoding != NSUTF8StringEncoding) { + if (errorOut) *errorOut = RCTMakeError(@"Incorrect encoding of storage file: ", @(encoding), extraData); + return nil; } + return entryString; } + return nil; } @@ -235,7 +241,7 @@ - (NSDictionary *)_ensureSetup } if (!_haveSetup) { NSDictionary *errorOut; - NSString *serialized = RCTReadFile(RCTGetManifestFilePath(), nil, &errorOut); + NSString *serialized = RCTReadFile(RCTGetManifestFilePath(), RCTManifestFileName, &errorOut); _manifest = serialized ? RCTJSONParseMutable(serialized, &error) : [NSMutableDictionary new]; if (error) { RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); From c674b2a03c75fb4729b5bc692b1b5f8b2766f187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Mon, 20 Nov 2017 13:07:10 -0800 Subject: [PATCH 64/75] Migrate additional docs to the new format Summary: [DOCS] Closes https://github.com/facebook/react-native/pull/16874 Differential Revision: D6375515 Pulled By: hramos fbshipit-source-id: 64359b45a37c7b478919121573ca04dbb1ce6609 --- AsyncStorage.js | 230 ++++++++---------------------------------------- 1 file changed, 35 insertions(+), 195 deletions(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index 0ba87ed6..fb54d6be 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -21,45 +21,11 @@ const RCTAsyncStorage = NativeModules.AsyncRocksDBStorage || NativeModules.AsyncLocalStorage; /** - * @class - * @description - * `AsyncStorage` is a simple, unencrypted, asynchronous, persistent, key-value storage - * system that is global to the app. It should be used instead of LocalStorage. - * - * It is recommended that you use an abstraction on top of `AsyncStorage` - * instead of `AsyncStorage` directly for anything more than light usage since - * it operates globally. - * - * On iOS, `AsyncStorage` is backed by native code that stores small values in a - * serialized dictionary and larger values in separate files. On Android, - * `AsyncStorage` will use either [RocksDB](http://rocksdb.org/) or SQLite - * based on what is available. - * - * The `AsyncStorage` JavaScript code is a simple facade that provides a clear - * JavaScript API, real `Error` objects, and simple non-multi functions. Each - * method in the API returns a `Promise` object. - * - * Persisting data: - * ``` - * try { - * await AsyncStorage.setItem('@MySuperStore:key', 'I like to save it.'); - * } catch (error) { - * // Error saving data - * } - * ``` - * - * Fetching data: - * ``` - * try { - * const value = await AsyncStorage.getItem('@MySuperStore:key'); - * if (value !== null){ - * // We have data!! - * console.log(value); - * } - * } catch (error) { - * // Error retrieving data - * } - * ``` + * `AsyncStorage` is a simple, unencrypted, asynchronous, persistent, key-value + * storage system that is global to the app. It should be used instead of + * LocalStorage. + * + * See http://facebook.github.io/react-native/docs/asyncstorage.html */ var AsyncStorage = { _getRequests: ([]: Array), @@ -68,11 +34,8 @@ var AsyncStorage = { /** * Fetches an item for a `key` and invokes a callback upon completion. - * Returns a `Promise` object. - * @param key Key of the item to fetch. - * @param callback Function that will be called with a result if found or - * any error. - * @returns A `Promise` object. + * + * See http://facebook.github.io/react-native/docs/asyncstorage.html#getitem */ getItem: function( key: string, @@ -95,11 +58,8 @@ var AsyncStorage = { /** * Sets the value for a `key` and invokes a callback upon completion. - * Returns a `Promise` object. - * @param key Key of the item to set. - * @param value Value to set for the `key`. - * @param callback Function that will be called with any error. - * @returns A `Promise` object. + * + * See http://facebook.github.io/react-native/docs/asyncstorage.html#setitem */ setItem: function( key: string, @@ -121,10 +81,8 @@ var AsyncStorage = { /** * Removes an item for a `key` and invokes a callback upon completion. - * Returns a `Promise` object. - * @param key Key of the item to remove. - * @param callback Function that will be called with any error. - * @returns A `Promise` object. + * + * See http://facebook.github.io/react-native/docs/asyncstorage.html#removeitem */ removeItem: function( key: string, @@ -145,38 +103,11 @@ var AsyncStorage = { /** * Merges an existing `key` value with an input value, assuming both values - * are stringified JSON. Returns a `Promise` object. + * are stringified JSON. * * **NOTE:** This is not supported by all native implementations. * - * @param key Key of the item to modify. - * @param value New value to merge for the `key`. - * @param callback Function that will be called with any error. - * @returns A `Promise` object. - * - * @example Example - * let UID123_object = { - * name: 'Chris', - * age: 30, - * traits: {hair: 'brown', eyes: 'brown'}, - * }; - * // You only need to define what will be added or updated - * let UID123_delta = { - * age: 31, - * traits: {eyes: 'blue', shoe_size: 10} - * }; - * - * AsyncStorage.setItem('UID123', JSON.stringify(UID123_object), () => { - * AsyncStorage.mergeItem('UID123', JSON.stringify(UID123_delta), () => { - * AsyncStorage.getItem('UID123', (err, result) => { - * console.log(result); - * }); - * }); - * }); - * - * // Console log result: - * // => {'name':'Chris','age':31,'traits': - * // {'shoe_size':10,'hair':'brown','eyes':'blue'}} + * See http://facebook.github.io/react-native/docs/asyncstorage.html#mergeitem */ mergeItem: function( key: string, @@ -197,11 +128,11 @@ var AsyncStorage = { }, /** - * Erases *all* `AsyncStorage` for all clients, libraries, etc. You probably + * Erases *all* `AsyncStorage` for all clients, libraries, etc. You probably * don't want to call this; use `removeItem` or `multiRemove` to clear only - * your app's keys. Returns a `Promise` object. - * @param callback Function that will be called with any error. - * @returns A `Promise` object. + * your app's keys. + * + * See http://facebook.github.io/react-native/docs/asyncstorage.html#clear */ clear: function(callback?: ?(error: ?Error) => void): Promise { return new Promise((resolve, reject) => { @@ -218,11 +149,8 @@ var AsyncStorage = { /** * Gets *all* keys known to your app; for all callers, libraries, etc. - * Returns a `Promise` object. - * @param callback Function that will be called the keys found and any error. - * @returns A `Promise` object. * - * Example: see the `multiGet` example. + * See http://facebook.github.io/react-native/docs/asyncstorage.html#getallkeys */ getAllKeys: function(callback?: ?(error: ?Error, keys: ?Array) => void): Promise { return new Promise((resolve, reject) => { @@ -247,7 +175,11 @@ var AsyncStorage = { * indicate which key caused the error. */ - /** Flushes any pending requests using a single batch call to get the data. */ + /** + * Flushes any pending requests using a single batch call to get the data. + * + * See http://facebook.github.io/react-native/docs/asyncstorage.html#flushgetrequests + * */ flushGetRequests: function(): void { const getRequests = this._getRequests; const getKeys = this._getKeys; @@ -278,30 +210,9 @@ var AsyncStorage = { /** * This allows you to batch the fetching of items given an array of `key` * inputs. Your callback will be invoked with an array of corresponding - * key-value pairs found: - * - * ``` - * multiGet(['k1', 'k2'], cb) -> cb([['k1', 'val1'], ['k2', 'val2']]) - * ``` - * - * The method returns a `Promise` object. - * - * @param keys Array of key for the items to get. - * @param callback Function that will be called with a key-value array of - * the results, plus an array of any key-specific errors found. - * @returns A `Promise` object. - * - * @example Example - * - * AsyncStorage.getAllKeys((err, keys) => { - * AsyncStorage.multiGet(keys, (err, stores) => { - * stores.map((result, i, store) => { - * // get at each store's key/value so you can work with it - * let key = store[i][0]; - * let value = store[i][1]; - * }); - * }); - * }); + * key-value pairs found. + * + * See http://facebook.github.io/react-native/docs/asyncstorage.html#multiget */ multiGet: function( keys: Array, @@ -341,19 +252,9 @@ var AsyncStorage = { /** * Use this as a batch operation for storing multiple key-value pairs. When - * the operation completes you'll get a single callback with any errors: + * the operation completes you'll get a single callback with any errors. * - * ``` - * multiSet([['k1', 'val1'], ['k2', 'val2']], cb); - * ``` - * - * The method returns a `Promise` object. - * - * @param keyValuePairs Array of key-value array for the items to set. - * @param callback Function that will be called with an array of any - * key-specific errors found. - * @returns A `Promise` object. - * Example: see the `multiMerge` example. + * See http://facebook.github.io/react-native/docs/asyncstorage.html#multiset */ multiSet: function( keyValuePairs: Array>, @@ -373,20 +274,9 @@ var AsyncStorage = { }, /** - * Call this to batch the deletion of all keys in the `keys` array. Returns - * a `Promise` object. - * - * @param keys Array of key for the items to delete. - * @param callback Function that will be called an array of any key-specific - * errors found. - * @returns A `Promise` object. - * - * @example Example - * let keys = ['k1', 'k2']; - * AsyncStorage.multiRemove(keys, (err) => { - * // keys k1 & k2 removed, if they existed - * // do most stuff after removal (if you want) - * }); + * Call this to batch the deletion of all keys in the `keys` array. + * + * See http://facebook.github.io/react-native/docs/asyncstorage.html#multiremove */ multiRemove: function( keys: Array, @@ -407,61 +297,11 @@ var AsyncStorage = { /** * Batch operation to merge in existing and new values for a given set of - * keys. This assumes that the values are stringified JSON. Returns a - * `Promise` object. - * + * keys. This assumes that the values are stringified JSON. + * * **NOTE**: This is not supported by all native implementations. - * - * @param keyValuePairs Array of key-value array for the items to merge. - * @param callback Function that will be called with an array of any - * key-specific errors found. - * @returns A `Promise` object. - * - * @example Example - * // first user, initial values - * let UID234_object = { - * name: 'Chris', - * age: 30, - * traits: {hair: 'brown', eyes: 'brown'}, - * }; - * - * // first user, delta values - * let UID234_delta = { - * age: 31, - * traits: {eyes: 'blue', shoe_size: 10}, - * }; - * - * // second user, initial values - * let UID345_object = { - * name: 'Marge', - * age: 25, - * traits: {hair: 'blonde', eyes: 'blue'}, - * }; - * - * // second user, delta values - * let UID345_delta = { - * age: 26, - * traits: {eyes: 'green', shoe_size: 6}, - * }; - * - * let multi_set_pairs = [['UID234', JSON.stringify(UID234_object)], ['UID345', JSON.stringify(UID345_object)]] - * let multi_merge_pairs = [['UID234', JSON.stringify(UID234_delta)], ['UID345', JSON.stringify(UID345_delta)]] - * - * AsyncStorage.multiSet(multi_set_pairs, (err) => { - * AsyncStorage.multiMerge(multi_merge_pairs, (err) => { - * AsyncStorage.multiGet(['UID234','UID345'], (err, stores) => { - * stores.map( (result, i, store) => { - * let key = store[i][0]; - * let val = store[i][1]; - * console.log(key, val); - * }); - * }); - * }); - * }); - * - * // Console log results: - * // => UID234 {"name":"Chris","age":31,"traits":{"shoe_size":10,"hair":"brown","eyes":"blue"}} - * // => UID345 {"name":"Marge","age":26,"traits":{"shoe_size":6,"hair":"blonde","eyes":"green"}} + * + * See http://facebook.github.io/react-native/docs/asyncstorage.html#multimerge */ multiMerge: function( keyValuePairs: Array>, From 207a38254bd0056b3ef1ad7669bb479fb455130f Mon Sep 17 00:00:00 2001 From: Sophie Alpert Date: Fri, 16 Feb 2018 18:24:55 -0800 Subject: [PATCH 65/75] Update license headers for MIT license Summary: Includes React Native and its dependencies Fresco, Metro, and Yoga. Excludes samples/examples/docs. find: ^(?:( *)|( *(?:[\*~#]|::))( )? *)?Copyright (?:\(c\) )?(\d{4})\b.+Facebook[\s\S]+?BSD[\s\S]+?(?:this source tree|the same directory)\.$ replace: $1$2$3Copyright (c) $4-present, Facebook, Inc.\n$2\n$1$2$3This source code is licensed under the MIT license found in the\n$1$2$3LICENSE file in the root directory of this source tree. Reviewed By: TheSavior, yungsters Differential Revision: D7007050 fbshipit-source-id: 37dd6bf0ffec0923bfc99c260bb330683f35553e --- AsyncLocalStorageUtil.java | 6 ++---- AsyncStorage.js | 6 ++---- AsyncStorageErrorUtil.java | 6 ++---- AsyncStorageModule.java | 6 ++---- RCTAsyncLocalStorage.h | 6 ++---- RCTAsyncLocalStorage.m | 6 ++---- ReactDatabaseSupplier.java | 6 ++---- 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/AsyncLocalStorageUtil.java b/AsyncLocalStorageUtil.java index ea6487c9..11f9a569 100644 --- a/AsyncLocalStorageUtil.java +++ b/AsyncLocalStorageUtil.java @@ -1,10 +1,8 @@ /** * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. */ package com.facebook.react.modules.storage; diff --git a/AsyncStorage.js b/AsyncStorage.js index fb54d6be..62228f78 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -1,10 +1,8 @@ /** * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. * * @providesModule AsyncStorage * @noflow diff --git a/AsyncStorageErrorUtil.java b/AsyncStorageErrorUtil.java index 75f25617..381b46bd 100644 --- a/AsyncStorageErrorUtil.java +++ b/AsyncStorageErrorUtil.java @@ -1,10 +1,8 @@ /** * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. */ package com.facebook.react.modules.storage; diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java index 0dba149d..c9fc96e5 100644 --- a/AsyncStorageModule.java +++ b/AsyncStorageModule.java @@ -1,10 +1,8 @@ /** * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. */ package com.facebook.react.modules.storage; diff --git a/RCTAsyncLocalStorage.h b/RCTAsyncLocalStorage.h index cbb8cb42..446a54f7 100644 --- a/RCTAsyncLocalStorage.h +++ b/RCTAsyncLocalStorage.h @@ -1,10 +1,8 @@ /** * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. */ #import diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index 1064e04f..c33ddb25 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -1,10 +1,8 @@ /** * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. */ #import "RCTAsyncLocalStorage.h" diff --git a/ReactDatabaseSupplier.java b/ReactDatabaseSupplier.java index 9787ddf6..6e845fb8 100644 --- a/ReactDatabaseSupplier.java +++ b/ReactDatabaseSupplier.java @@ -1,10 +1,8 @@ /** * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. */ package com.facebook.react.modules.storage; From 396bae38f2b0723aea26ad92c33fb243bff20c08 Mon Sep 17 00:00:00 2001 From: Mats Byrkeland Date: Thu, 22 Feb 2018 07:04:35 -0800 Subject: [PATCH 66/75] Fix ESLint warnings using 'yarn lint --fix' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Hi! I would like to contribute to React Native, and I am just starting out. I forked the repo and found that it has quite a lot of ESLint warnings – many of which were automatically fixable. This PR is simply the result of running `yarn lint --fix` from the root folder. Most changes are removing trailing spaces from comments. Haven't really done any manual testing, since I haven't done any code changes manually. `yarn test` runs fine, `yarn flow` runs fine, `yarn prettier` is satisfied. N/A [INTERNAL][MINOR][] - Fix ESLint warnings Closes https://github.com/facebook/react-native/pull/18047 Differential Revision: D7054948 Pulled By: hramos fbshipit-source-id: d53e692698d1687de5821c3fb5cdb76a5e03b71e --- AsyncStorage.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index 62228f78..4e262e61 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -20,9 +20,9 @@ const RCTAsyncStorage = NativeModules.AsyncRocksDBStorage || /** * `AsyncStorage` is a simple, unencrypted, asynchronous, persistent, key-value - * storage system that is global to the app. It should be used instead of + * storage system that is global to the app. It should be used instead of * LocalStorage. - * + * * See http://facebook.github.io/react-native/docs/asyncstorage.html */ var AsyncStorage = { @@ -32,7 +32,7 @@ var AsyncStorage = { /** * Fetches an item for a `key` and invokes a callback upon completion. - * + * * See http://facebook.github.io/react-native/docs/asyncstorage.html#getitem */ getItem: function( @@ -56,7 +56,7 @@ var AsyncStorage = { /** * Sets the value for a `key` and invokes a callback upon completion. - * + * * See http://facebook.github.io/react-native/docs/asyncstorage.html#setitem */ setItem: function( @@ -79,7 +79,7 @@ var AsyncStorage = { /** * Removes an item for a `key` and invokes a callback upon completion. - * + * * See http://facebook.github.io/react-native/docs/asyncstorage.html#removeitem */ removeItem: function( @@ -129,7 +129,7 @@ var AsyncStorage = { * Erases *all* `AsyncStorage` for all clients, libraries, etc. You probably * don't want to call this; use `removeItem` or `multiRemove` to clear only * your app's keys. - * + * * See http://facebook.github.io/react-native/docs/asyncstorage.html#clear */ clear: function(callback?: ?(error: ?Error) => void): Promise { @@ -173,9 +173,9 @@ var AsyncStorage = { * indicate which key caused the error. */ - /** + /** * Flushes any pending requests using a single batch call to get the data. - * + * * See http://facebook.github.io/react-native/docs/asyncstorage.html#flushgetrequests * */ flushGetRequests: function(): void { @@ -209,7 +209,7 @@ var AsyncStorage = { * This allows you to batch the fetching of items given an array of `key` * inputs. Your callback will be invoked with an array of corresponding * key-value pairs found. - * + * * See http://facebook.github.io/react-native/docs/asyncstorage.html#multiget */ multiGet: function( @@ -273,7 +273,7 @@ var AsyncStorage = { /** * Call this to batch the deletion of all keys in the `keys` array. - * + * * See http://facebook.github.io/react-native/docs/asyncstorage.html#multiremove */ multiRemove: function( @@ -296,9 +296,9 @@ var AsyncStorage = { /** * Batch operation to merge in existing and new values for a given set of * keys. This assumes that the values are stringified JSON. - * + * * **NOTE**: This is not supported by all native implementations. - * + * * See http://facebook.github.io/react-native/docs/asyncstorage.html#multimerge */ multiMerge: function( From f006a7e3678f6f72f62a6f045cf0546aaca676de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Wed, 25 Apr 2018 07:00:46 -0700 Subject: [PATCH 67/75] Remove @providesModule from all modules Summary: This PR removes the need for having the `providesModule` tags in all the modules in the repository. It configures Flow, Jest and Metro to get the module names from the filenames (`Libraries/Animated/src/nodes/AnimatedInterpolation.js` => `AnimatedInterpolation`) * Checked the Flow configuration by running flow on the project root (no errors): ``` yarn flow ``` * Checked the Jest configuration by running the tests with a clean cache: ``` yarn jest --clearCache && yarn test ``` * Checked the Metro configuration by starting the server with a clean cache and requesting some bundles: ``` yarn run start --reset-cache curl 'localhost:8081/IntegrationTests/AccessibilityManagerTest.bundle?platform=android' curl 'localhost:8081/Libraries/Alert/Alert.bundle?platform=ios' ``` [INTERNAL] [FEATURE] [All] - Removed providesModule from all modules and configured tools. Closes https://github.com/facebook/react-native/pull/18995 Reviewed By: mjesun Differential Revision: D7729509 Pulled By: rubennorte fbshipit-source-id: 892f760a05ce1fddb088ff0cd2e97e521fb8e825 --- AsyncStorage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index 4e262e61..f953aecb 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -4,7 +4,6 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @providesModule AsyncStorage * @noflow * @flow-weak * @jsdoc From fb51e6b74c3cd156a88edb000882871dc903d85c Mon Sep 17 00:00:00 2001 From: Eli White Date: Thu, 10 May 2018 15:44:52 -0700 Subject: [PATCH 68/75] Convert react-native-github/Libraries to let/const Reviewed By: sahrens Differential Revision: D7956042 fbshipit-source-id: 221851aa311f3cdd6326497352b366048db0a1bb --- AsyncStorage.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index f953aecb..ed5042ed 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -24,7 +24,7 @@ const RCTAsyncStorage = NativeModules.AsyncRocksDBStorage || * * See http://facebook.github.io/react-native/docs/asyncstorage.html */ -var AsyncStorage = { +const AsyncStorage = { _getRequests: ([]: Array), _getKeys: ([]: Array), _immediate: (null: ?number), @@ -41,8 +41,8 @@ var AsyncStorage = { return new Promise((resolve, reject) => { RCTAsyncStorage.multiGet([key], function(errors, result) { // Unpack result to get value from [[key,value]] - var value = (result && result[0] && result[0][1]) ? result[0][1] : null; - var errs = convertErrors(errors); + const value = (result && result[0] && result[0][1]) ? result[0][1] : null; + const errs = convertErrors(errors); callback && callback(errs && errs[0], value); if (errs) { reject(errs[0]); @@ -65,7 +65,7 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiSet([[key,value]], function(errors) { - var errs = convertErrors(errors); + const errs = convertErrors(errors); callback && callback(errs && errs[0]); if (errs) { reject(errs[0]); @@ -87,7 +87,7 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiRemove([key], function(errors) { - var errs = convertErrors(errors); + const errs = convertErrors(errors); callback && callback(errs && errs[0]); if (errs) { reject(errs[0]); @@ -113,7 +113,7 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiMerge([[key,value]], function(errors) { - var errs = convertErrors(errors); + const errs = convertErrors(errors); callback && callback(errs && errs[0]); if (errs) { reject(errs[0]); @@ -222,7 +222,7 @@ var AsyncStorage = { }); } - var getRequest = { + const getRequest = { keys: keys, callback: callback, // do we need this? @@ -231,7 +231,7 @@ var AsyncStorage = { reject: null, }; - var promiseResult = new Promise((resolve, reject) => { + const promiseResult = new Promise((resolve, reject) => { getRequest.resolve = resolve; getRequest.reject = reject; }); @@ -259,7 +259,7 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiSet(keyValuePairs, function(errors) { - var error = convertErrors(errors); + const error = convertErrors(errors); callback && callback(error); if (error) { reject(error); @@ -281,7 +281,7 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiRemove(keys, function(errors) { - var error = convertErrors(errors); + const error = convertErrors(errors); callback && callback(error); if (error) { reject(error); @@ -306,7 +306,7 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiMerge(keyValuePairs, function(errors) { - var error = convertErrors(errors); + const error = convertErrors(errors); callback && callback(error); if (error) { reject(error); @@ -335,7 +335,7 @@ function convertError(error) { if (!error) { return null; } - var out = new Error(error.message); + const out = new Error(error.message); out.key = error.key; // flow doesn't like this :( return out; } From 35731af5a72d978157c00cc1f9a2212e8df6c51b Mon Sep 17 00:00:00 2001 From: Eli White Date: Thu, 10 May 2018 19:06:46 -0700 Subject: [PATCH 69/75] Prettier React Native Libraries Reviewed By: sahrens Differential Revision: D7961488 fbshipit-source-id: 05f9b8b0b91ae77f9040a5321ccc18f7c3c1ce9a --- AsyncStorage.js | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/AsyncStorage.js b/AsyncStorage.js index ed5042ed..0877e9b0 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -4,16 +4,19 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @format * @noflow * @flow-weak * @jsdoc */ + 'use strict'; const NativeModules = require('NativeModules'); // Use RocksDB if available, then SQLite, then file storage. -const RCTAsyncStorage = NativeModules.AsyncRocksDBStorage || +const RCTAsyncStorage = + NativeModules.AsyncRocksDBStorage || NativeModules.AsyncSQLiteDBStorage || NativeModules.AsyncLocalStorage; @@ -36,12 +39,12 @@ const AsyncStorage = { */ getItem: function( key: string, - callback?: ?(error: ?Error, result: ?string) => void + callback?: ?(error: ?Error, result: ?string) => void, ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiGet([key], function(errors, result) { // Unpack result to get value from [[key,value]] - const value = (result && result[0] && result[0][1]) ? result[0][1] : null; + const value = result && result[0] && result[0][1] ? result[0][1] : null; const errs = convertErrors(errors); callback && callback(errs && errs[0], value); if (errs) { @@ -61,10 +64,10 @@ const AsyncStorage = { setItem: function( key: string, value: string, - callback?: ?(error: ?Error) => void + callback?: ?(error: ?Error) => void, ): Promise { return new Promise((resolve, reject) => { - RCTAsyncStorage.multiSet([[key,value]], function(errors) { + RCTAsyncStorage.multiSet([[key, value]], function(errors) { const errs = convertErrors(errors); callback && callback(errs && errs[0]); if (errs) { @@ -83,7 +86,7 @@ const AsyncStorage = { */ removeItem: function( key: string, - callback?: ?(error: ?Error) => void + callback?: ?(error: ?Error) => void, ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiRemove([key], function(errors) { @@ -109,10 +112,10 @@ const AsyncStorage = { mergeItem: function( key: string, value: string, - callback?: ?(error: ?Error) => void + callback?: ?(error: ?Error) => void, ): Promise { return new Promise((resolve, reject) => { - RCTAsyncStorage.multiMerge([[key,value]], function(errors) { + RCTAsyncStorage.multiMerge([[key, value]], function(errors) { const errs = convertErrors(errors); callback && callback(errs && errs[0]); if (errs) { @@ -135,7 +138,7 @@ const AsyncStorage = { return new Promise((resolve, reject) => { RCTAsyncStorage.clear(function(error) { callback && callback(convertError(error)); - if (error && convertError(error)){ + if (error && convertError(error)) { reject(convertError(error)); } else { resolve(null); @@ -149,7 +152,9 @@ const AsyncStorage = { * * See http://facebook.github.io/react-native/docs/asyncstorage.html#getallkeys */ - getAllKeys: function(callback?: ?(error: ?Error, keys: ?Array) => void): Promise { + getAllKeys: function( + callback?: ?(error: ?Error, keys: ?Array) => void, + ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.getAllKeys(function(error, keys) { callback && callback(convertError(error), keys); @@ -192,7 +197,11 @@ const AsyncStorage = { // Is there a way to avoid using the map but fix the bug in this breaking test? // https://github.com/facebook/react-native/commit/8dd8ad76579d7feef34c014d387bf02065692264 const map = {}; - result && result.forEach(([key, value]) => { map[key] = value; return value; }); + result && + result.forEach(([key, value]) => { + map[key] = value; + return value; + }); const reqLength = getRequests.length; for (let i = 0; i < reqLength; i++) { const request = getRequests[i]; @@ -213,7 +222,7 @@ const AsyncStorage = { */ multiGet: function( keys: Array, - callback?: ?(errors: ?Array, result: ?Array>) => void + callback?: ?(errors: ?Array, result: ?Array>) => void, ): Promise { if (!this._immediate) { this._immediate = setImmediate(() => { @@ -255,7 +264,7 @@ const AsyncStorage = { */ multiSet: function( keyValuePairs: Array>, - callback?: ?(errors: ?Array) => void + callback?: ?(errors: ?Array) => void, ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiSet(keyValuePairs, function(errors) { @@ -277,7 +286,7 @@ const AsyncStorage = { */ multiRemove: function( keys: Array, - callback?: ?(errors: ?Array) => void + callback?: ?(errors: ?Array) => void, ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiRemove(keys, function(errors) { @@ -302,7 +311,7 @@ const AsyncStorage = { */ multiMerge: function( keyValuePairs: Array>, - callback?: ?(errors: ?Array) => void + callback?: ?(errors: ?Array) => void, ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiMerge(keyValuePairs, function(errors) { @@ -328,7 +337,7 @@ function convertErrors(errs) { if (!errs) { return null; } - return (Array.isArray(errs) ? errs : [errs]).map((e) => convertError(e)); + return (Array.isArray(errs) ? errs : [errs]).map(e => convertError(e)); } function convertError(error) { From 353c95a0c9b6612ce9891529b760adb8365b2977 Mon Sep 17 00:00:00 2001 From: Daniel Cochran Date: Mon, 30 Jul 2018 12:00:35 -0700 Subject: [PATCH 70/75] make AsyncStorage serially execute requests (#18522) Summary: This patch is a bit of a hack job, but I'd argue it's necessary to dramatically improve the dev UX on Android devices. Somewhere in react-native, there's a shared SerialExecutor which AsyncStorage uses that is getting blocked, causing remote debugging to occasionally hang indefinitely for folks making AsyncStorage requests. This is frustrating from a dev UX perspective, and has persisted across several versions as far back as RN 0.44, and still remains on RN 0.54. The issue seems to only happen on Android > 7+, which is likely because the ThreadPoolExecutor behavior changed in this version: https://stackoverflow.com/questions/9654148/android-asynctask-threads-limits Fixes #14101 We've been using this patch in production for the past 4 months on our team by overriding the AsyncStorage native module. We use AsyncStorage extensively for offline data and caching. You can test by compiling this commit version into a test react native repository that is set to build from source: ```sh git clone https://github.com/dannycochran/react-native rnAsyncStorage cd rnAsyncStorage git checkout asyncStorage cd .. git clone https://github.com/dannycochran/asyncStorageTest yarn install cp -r ../rnAsyncStorage node_modules/react-native react-native run-android ``` No documentation change is required. https://github.com/facebook/react-native/pull/16905 [Android] [BUGFIX] [AsyncStorage] - Fix AsyncStorage causing remote debugger to hang indefinitely. Pull Request resolved: https://github.com/facebook/react-native/pull/18522 Differential Revision: D8624088 Pulled By: hramos fbshipit-source-id: a1d2e3458d98467845cb34ac73f2aafaaa15ace2 --- AsyncStorageModule.java | 55 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java index c9fc96e5..6aa355f2 100644 --- a/AsyncStorageModule.java +++ b/AsyncStorageModule.java @@ -7,10 +7,13 @@ package com.facebook.react.modules.storage; +import java.util.ArrayDeque; import java.util.HashSet; +import java.util.concurrent.Executor; import android.database.Cursor; import android.database.sqlite.SQLiteStatement; +import android.os.AsyncTask; import com.facebook.common.logging.FLog; import com.facebook.react.bridge.Arguments; @@ -23,6 +26,7 @@ import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.annotations.VisibleForTesting; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.modules.common.ModuleDataCleaner; @@ -43,8 +47,47 @@ public final class AsyncStorageModule private ReactDatabaseSupplier mReactDatabaseSupplier; private boolean mShuttingDown = false; + // Adapted from https://android.googlesource.com/platform/frameworks/base.git/+/1488a3a19d4681a41fb45570c15e14d99db1cb66/core/java/android/os/AsyncTask.java#237 + private class SerialExecutor implements Executor { + private final ArrayDeque mTasks = new ArrayDeque(); + private Runnable mActive; + private final Executor executor; + + SerialExecutor(Executor executor) { + this.executor = executor; + } + + public synchronized void execute(final Runnable r) { + mTasks.offer(new Runnable() { + public void run() { + try { + r.run(); + } finally { + scheduleNext(); + } + } + }); + if (mActive == null) { + scheduleNext(); + } + } + synchronized void scheduleNext() { + if ((mActive = mTasks.poll()) != null) { + executor.execute(mActive); + } + } + } + + private final SerialExecutor executor; + public AsyncStorageModule(ReactApplicationContext reactContext) { + this(reactContext, AsyncTask.THREAD_POOL_EXECUTOR); + } + + @VisibleForTesting + AsyncStorageModule(ReactApplicationContext reactContext, Executor executor) { super(reactContext); + this.executor = new SerialExecutor(executor); mReactDatabaseSupplier = ReactDatabaseSupplier.getInstance(reactContext); } @@ -141,7 +184,7 @@ protected void doInBackgroundGuarded(Void... params) { callback.invoke(null, data); } - }.execute(); + }.executeOnExecutor(executor); } /** @@ -208,7 +251,7 @@ protected void doInBackgroundGuarded(Void... params) { callback.invoke(); } } - }.execute(); + }.executeOnExecutor(executor); } /** @@ -259,7 +302,7 @@ protected void doInBackgroundGuarded(Void... params) { callback.invoke(); } } - }.execute(); + }.executeOnExecutor(executor); } /** @@ -322,7 +365,7 @@ protected void doInBackgroundGuarded(Void... params) { callback.invoke(); } } - }.execute(); + }.executeOnExecutor(executor); } /** @@ -345,7 +388,7 @@ protected void doInBackgroundGuarded(Void... params) { callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); } } - }.execute(); + }.executeOnExecutor(executor); } /** @@ -379,7 +422,7 @@ protected void doInBackgroundGuarded(Void... params) { } callback.invoke(null, data); } - }.execute(); + }.executeOnExecutor(executor); } /** From 75557f1c52c6422bf56380af8f0e2b7c21d1545f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Tue, 11 Sep 2018 15:27:47 -0700 Subject: [PATCH 71/75] Update copyright headers to yearless format Summary: This change drops the year from the copyright headers and the LICENSE file. Reviewed By: yungsters Differential Revision: D9727774 fbshipit-source-id: df4fc1e4390733fe774b1a160dd41b4a3d83302a --- AsyncLocalStorageUtil.java | 2 +- AsyncStorage.js | 2 +- AsyncStorageErrorUtil.java | 2 +- AsyncStorageModule.java | 2 +- RCTAsyncLocalStorage.h | 2 +- RCTAsyncLocalStorage.m | 2 +- ReactDatabaseSupplier.java | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/AsyncLocalStorageUtil.java b/AsyncLocalStorageUtil.java index 11f9a569..9efcad64 100644 --- a/AsyncLocalStorageUtil.java +++ b/AsyncLocalStorageUtil.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. diff --git a/AsyncStorage.js b/AsyncStorage.js index 0877e9b0..877e7076 100644 --- a/AsyncStorage.js +++ b/AsyncStorage.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. diff --git a/AsyncStorageErrorUtil.java b/AsyncStorageErrorUtil.java index 381b46bd..fa5436ce 100644 --- a/AsyncStorageErrorUtil.java +++ b/AsyncStorageErrorUtil.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java index 6aa355f2..ca8f410c 100644 --- a/AsyncStorageModule.java +++ b/AsyncStorageModule.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. diff --git a/RCTAsyncLocalStorage.h b/RCTAsyncLocalStorage.h index 446a54f7..386f9f5e 100644 --- a/RCTAsyncLocalStorage.h +++ b/RCTAsyncLocalStorage.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index c33ddb25..dff7d903 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. diff --git a/ReactDatabaseSupplier.java b/ReactDatabaseSupplier.java index 6e845fb8..bcbb7f27 100644 --- a/ReactDatabaseSupplier.java +++ b/ReactDatabaseSupplier.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. From f2fbfc3fc9e40796e6b248c4a5a71e5180e32960 Mon Sep 17 00:00:00 2001 From: Ram N Date: Fri, 9 Nov 2018 10:48:49 -0800 Subject: [PATCH 72/75] Use static constants instead of strings when referring to View Managers and Native Modules Summary: Using strings to refer to Native Modules and View Managers in ReactPackages are prone to error. The compiler replaces the constants with the actual strings anyway, so using constants gives us better type safety Reviewed By: achen1 Differential Revision: D12843649 fbshipit-source-id: 7a7c6c854c8689193f40df92dc8426a3b37f82c8 --- AsyncStorageModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncStorageModule.java b/AsyncStorageModule.java index ca8f410c..ad3bed5c 100644 --- a/AsyncStorageModule.java +++ b/AsyncStorageModule.java @@ -38,7 +38,7 @@ public final class AsyncStorageModule extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable { - protected static final String NAME = "AsyncSQLiteDBStorage"; + public static final String NAME = "AsyncSQLiteDBStorage"; // SQL variable number limit, defined by SQLITE_LIMIT_VARIABLE_NUMBER: // https://raw.githubusercontent.com/android/platform_external_sqlite/master/dist/sqlite3.c From 42655c7c638e494927e4704b52b15f3eb7bf7c80 Mon Sep 17 00:00:00 2001 From: Teddy Martin Date: Tue, 29 Jan 2019 08:41:04 -0800 Subject: [PATCH 73/75] Expose AsyncLocalStorage get/set methods (#18454) Summary: Currently, if an app uses AsyncStorage on the JS side, there is no public interface to access stored data from the native side. In our app, written in Swift, I have written a [helper](https://gist.github.com/ejmartin504/d501abe55c28450a0e52ac39aee7b0e6) that pulls out the data. I accomplished this by reverse-engineering the code in RCTAsyncLocalStorage.m. It would have been far easier had this code been exposed to native. I made this change locally and tested out getting the data from Swift code. This worked like a charm: ```swift let storage = RCTAsyncLocalStorage() let cacheKey = "test" storage.methodQueue?.async { self.storage.multiGet([cacheKey]) { values in print(values) } } ``` [IOS ][ENHANCEMENT ][RCTAsyncLocalStorage.h] - Expose AsyncLocalStorage get/set methods to native code. Pull Request resolved: https://github.com/facebook/react-native/pull/18454 Differential Revision: D13860333 Pulled By: cpojer fbshipit-source-id: b33ee5bf1ec65c8291bfcb76b0d6f0df39376a7e --- RCTAsyncLocalStorage.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/RCTAsyncLocalStorage.h b/RCTAsyncLocalStorage.h index 386f9f5e..f079484a 100644 --- a/RCTAsyncLocalStorage.h +++ b/RCTAsyncLocalStorage.h @@ -31,4 +31,11 @@ // 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; + +// Add multiple key value pairs to the cache. +- (void)multiSet:(NSArray *> *)kvPairs callback:(RCTResponseSenderBlock)callback; + + @end From 92e9984d9eaf1b9b0cb86483a3314bcacf74f603 Mon Sep 17 00:00:00 2001 From: Elliott Sprehn Date: Tue, 29 Jan 2019 09:11:22 -0800 Subject: [PATCH 74/75] Always write the manifest in multiRemove (#18613) Summary: RCTAsyncLocalStorage did not write the manifest after removing a value that was larger than RCTInlineValueThreshold. This meant that the values would still be on disk as null in the manifest.json, and if the app didn't do anything to make the manifest get written out again the null values would persist in the AsyncStorage and be returned by getAllKeys. We need to always write out the manifest.json even if the value is in the overflow storage files. Thank you for sending the PR! We appreciate you spending the time to work on these changes. Help us understand your motivation by explaining why you decided to make this change. Fixes #9196. Not sure where the tests are for this? none. [IOS] [BUGFIX] [AsyncStorage] - Correctly remove keys of large values from AsyncStorage. tadeuzagallo nicklockwood dannycochran Pull Request resolved: https://github.com/facebook/react-native/pull/18613 Differential Revision: D13860820 Pulled By: cpojer fbshipit-source-id: ced1cd40273140335cd9b1f29fc1c1881ab8cebd --- RCTAsyncLocalStorage.m | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/RCTAsyncLocalStorage.m b/RCTAsyncLocalStorage.m index dff7d903..c77fac40 100644 --- a/RCTAsyncLocalStorage.m +++ b/RCTAsyncLocalStorage.m @@ -423,10 +423,8 @@ - (NSDictionary *)_writeEntry:(NSArray *)entry changedManifest:(BOOL NSString *filePath = [self _filePathForKey:key]; [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; [RCTGetCache() removeObjectForKey:key]; - // remove the key from manifest, but no need to mark as changed just for - // this, as the cost of checking again next time is negligible. - [_manifest removeObjectForKey:key]; - } else if (_manifest[key]) { + } + if (_manifest[key]) { changedManifest = YES; [_manifest removeObjectForKey:key]; } From 044a2aae03292011397f3461b9bd7c55c43897e1 Mon Sep 17 00:00:00 2001 From: Krzysztof Borowy Date: Sat, 9 Feb 2019 16:27:27 +0100 Subject: [PATCH 75/75] Module scaffolding --- .eslintrc | 290 + .flowconfig | 90 + .gitignore | 48 + .npmignore | 85 + .prettierrc | 8 + LICENSE | 21 + README.md | 23 + android/build.gradle | 40 + android/gradle.properties | 4 + android/src/main/AndroidManifest.xml | 6 + .../asyncstorage/AsyncLocalStorageUtil.java | 8 +- .../asyncstorage/AsyncStorageErrorUtil.java | 2 +- .../asyncstorage/AsyncStorageModule.java | 21 +- .../asyncstorage/AsyncStoragePackage.java | 36 + .../asyncstorage/ReactDatabaseSupplier.java | 4 +- babel.config.js | 14 + example/.gitignore | 56 + example/__tests__/App.js | 15 + example/android/app/BUCK | 55 + example/android/app/build.gradle | 147 + example/android/app/build_defs.bzl | 19 + example/android/app/proguard-rules.pro | 17 + .../android/app/src/main/AndroidManifest.xml | 27 + .../com/asyncstorageexample/MainActivity.java | 15 + .../asyncstorageexample/MainApplication.java | 47 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5024 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7098 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10676 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15523 bytes .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 8 + example/android/build.gradle | 39 + example/android/gradle.properties | 18 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + example/android/gradlew | 172 + example/android/gradlew.bat | 84 + example/android/keystores/BUCK | 8 + .../keystores/debug.keystore.properties | 4 + example/android/settings.gradle | 6 + example/app.json | 4 + example/index.js | 15 + .../project.pbxproj | 1128 +++ .../AsyncStorageExample-tvOS.xcscheme | 129 + .../xcschemes/AsyncStorageExample.xcscheme | 129 + example/ios/AsyncStorageExample/AppDelegate.h | 14 + example/ios/AsyncStorageExample/AppDelegate.m | 35 + .../Base.lproj/LaunchScreen.xib | 42 + .../AppIcon.appiconset/Contents.json | 53 + .../Images.xcassets/Contents.json | 6 + example/ios/AsyncStorageExample/Info.plist | 60 + example/ios/AsyncStorageExample/main.m | 16 + example/src/App.js | 122 + example/src/examples/ClearSingle.js | 46 + example/src/examples/GetSet.js | 67 + .../RNCAsyncStorage.h | 6 +- .../RNCAsyncStorage.m | 0 ios/RNCAsyncstorage.xcodeproj/project.pbxproj | 259 + AsyncStorage.js => lib/AsyncStorage.js | 3 +- package.json | 55 + yarn.lock | 6347 +++++++++++++++++ 67 files changed, 9960 insertions(+), 21 deletions(-) create mode 100644 .eslintrc create mode 100644 .flowconfig create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .prettierrc create mode 100644 LICENSE create mode 100644 android/build.gradle create mode 100644 android/gradle.properties create mode 100644 android/src/main/AndroidManifest.xml rename AsyncLocalStorageUtil.java => android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncLocalStorageUtil.java (92%) rename AsyncStorageErrorUtil.java => android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageErrorUtil.java (96%) rename AsyncStorageModule.java => android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageModule.java (97%) create mode 100644 android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java rename ReactDatabaseSupplier.java => android/src/main/java/com/reactnativecommunity/asyncstorage/ReactDatabaseSupplier.java (98%) create mode 100644 babel.config.js create mode 100644 example/.gitignore create mode 100644 example/__tests__/App.js create mode 100644 example/android/app/BUCK create mode 100644 example/android/app/build.gradle create mode 100644 example/android/app/build_defs.bzl create mode 100644 example/android/app/proguard-rules.pro create mode 100644 example/android/app/src/main/AndroidManifest.xml create mode 100644 example/android/app/src/main/java/com/asyncstorageexample/MainActivity.java create mode 100644 example/android/app/src/main/java/com/asyncstorageexample/MainApplication.java create mode 100644 example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 example/android/app/src/main/res/values/strings.xml create mode 100644 example/android/app/src/main/res/values/styles.xml create mode 100644 example/android/build.gradle create mode 100644 example/android/gradle.properties create mode 100644 example/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 example/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 example/android/gradlew create mode 100644 example/android/gradlew.bat create mode 100644 example/android/keystores/BUCK create mode 100644 example/android/keystores/debug.keystore.properties create mode 100644 example/android/settings.gradle create mode 100644 example/app.json create mode 100644 example/index.js create mode 100644 example/ios/AsyncStorageExample.xcodeproj/project.pbxproj create mode 100644 example/ios/AsyncStorageExample.xcodeproj/xcshareddata/xcschemes/AsyncStorageExample-tvOS.xcscheme create mode 100644 example/ios/AsyncStorageExample.xcodeproj/xcshareddata/xcschemes/AsyncStorageExample.xcscheme create mode 100644 example/ios/AsyncStorageExample/AppDelegate.h create mode 100644 example/ios/AsyncStorageExample/AppDelegate.m create mode 100644 example/ios/AsyncStorageExample/Base.lproj/LaunchScreen.xib create mode 100644 example/ios/AsyncStorageExample/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 example/ios/AsyncStorageExample/Images.xcassets/Contents.json create mode 100644 example/ios/AsyncStorageExample/Info.plist create mode 100644 example/ios/AsyncStorageExample/main.m create mode 100644 example/src/App.js create mode 100644 example/src/examples/ClearSingle.js create mode 100644 example/src/examples/GetSet.js rename RCTAsyncLocalStorage.h => ios/RNCAsyncStorage.h (86%) rename RCTAsyncLocalStorage.m => ios/RNCAsyncStorage.m (100%) create mode 100644 ios/RNCAsyncstorage.xcodeproj/project.pbxproj rename AsyncStorage.js => lib/AsyncStorage.js (98%) create mode 100644 package.json create mode 100644 yarn.lock diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..bd30a3c8 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,290 @@ +{ + "root": true, + + "parser": "babel-eslint", + + "env": { + "es6": true, + }, + + "plugins": [ + "eslint-comments", + "flowtype", + "prettier", + "react", + "react-hooks", + "react-native", + "jest", + ], + + // Map from global var to bool specifying if it can be redefined + "globals": { + "__DEV__": true, + "__dirname": false, + "__fbBatchedBridgeConfig": false, + "alert": false, + "cancelAnimationFrame": false, + "cancelIdleCallback": false, + "clearImmediate": true, + "clearInterval": false, + "clearTimeout": false, + "console": false, + "document": false, + "escape": false, + "Event": false, + "EventTarget": false, + "exports": false, + "fetch": false, + "FormData": false, + "global": false, + "Map": true, + "module": false, + "navigator": false, + "process": false, + "Promise": true, + "requestAnimationFrame": true, + "requestIdleCallback": true, + "require": false, + "Set": true, + "setImmediate": true, + "setInterval": false, + "setTimeout": false, + "window": false, + "XMLHttpRequest": false, + }, + + "rules": { + // General + "comma-dangle": [1, "always-multiline"], // allow or disallow trailing commas + "no-cond-assign": 1, // disallow assignment in conditional expressions + "no-console": 0, // disallow use of console (off by default in the node environment) + "no-const-assign": 2, // disallow assignment to const-declared variables + "no-constant-condition": 0, // disallow use of constant expressions in conditions + "no-control-regex": 1, // disallow control characters in regular expressions + "no-debugger": 1, // disallow use of debugger + "no-dupe-class-members": 2, // Disallow duplicate name in class members + "no-dupe-keys": 2, // disallow duplicate keys when creating object literals + "no-empty": 0, // disallow empty statements + "no-ex-assign": 1, // disallow assigning to the exception in a catch block + "no-extra-boolean-cast": 1, // disallow double-negation boolean casts in a boolean context + "no-extra-parens": 0, // disallow unnecessary parentheses (off by default) + "no-extra-semi": 1, // disallow unnecessary semicolons + "no-func-assign": 1, // disallow overwriting functions written as function declarations + "no-inner-declarations": 0, // disallow function or variable declarations in nested blocks + "no-invalid-regexp": 1, // disallow invalid regular expression strings in the RegExp constructor + "no-negated-in-lhs": 1, // disallow negation of the left operand of an in expression + "no-obj-calls": 1, // disallow the use of object properties of the global object (Math and JSON) as functions + "no-regex-spaces": 1, // disallow multiple spaces in a regular expression literal + "no-reserved-keys": 0, // disallow reserved words being used as object literal keys (off by default) + "no-sparse-arrays": 1, // disallow sparse arrays + "no-unreachable": 2, // disallow unreachable statements after a return, throw, continue, or break statement + "use-isnan": 1, // disallow comparisons with the value NaN + "valid-jsdoc": 0, // Ensure JSDoc comments are valid (off by default) + "valid-typeof": 1, // Ensure that the results of typeof are compared against a valid string + + // Best Practices + // These are rules designed to prevent you from making mistakes. They either prescribe a better way of doing something or help you avoid footguns. + + "block-scoped-var": 0, // treat var statements as if they were block scoped (off by default) + "complexity": 0, // specify the maximum cyclomatic complexity allowed in a program (off by default) + "consistent-return": 0, // require return statements to either always or never specify values + "curly": 1, // specify curly brace conventions for all control statements + "default-case": 0, // require default case in switch statements (off by default) + "dot-notation": 1, // encourages use of dot notation whenever possible + "eqeqeq": [1, "allow-null"], // require the use of === and !== + "guard-for-in": 0, // make sure for-in loops have an if statement (off by default) + "no-alert": 1, // disallow the use of alert, confirm, and prompt + "no-caller": 1, // disallow use of arguments.caller or arguments.callee + "no-div-regex": 1, // disallow division operators explicitly at beginning of regular expression (off by default) + "no-else-return": 0, // disallow else after a return in an if (off by default) + "no-eq-null": 0, // disallow comparisons to null without a type-checking operator (off by default) + "no-eval": 2, // disallow use of eval() + "no-extend-native": 1, // disallow adding to native types + "no-extra-bind": 1, // disallow unnecessary function binding + "no-fallthrough": 1, // disallow fallthrough of case statements + "no-floating-decimal": 1, // disallow the use of leading or trailing decimal points in numeric literals (off by default) + "no-implied-eval": 1, // disallow use of eval()-like methods + "no-labels": 1, // disallow use of labeled statements + "no-iterator": 1, // disallow usage of __iterator__ property + "no-lone-blocks": 1, // disallow unnecessary nested blocks + "no-loop-func": 0, // disallow creation of functions within loops + "no-multi-str": 0, // disallow use of multiline strings + "no-native-reassign": 0, // disallow reassignments of native objects + "no-new": 1, // disallow use of new operator when not part of the assignment or comparison + "no-new-func": 2, // disallow use of new operator for Function object + "no-new-wrappers": 1, // disallows creating new instances of String,Number, and Boolean + "no-octal": 1, // disallow use of octal literals + "no-octal-escape": 1, // disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251"; + "no-proto": 1, // disallow usage of __proto__ property + "no-redeclare": 0, // disallow declaring the same variable more then once + "no-return-assign": 1, // disallow use of assignment in return statement + "no-script-url": 1, // disallow use of javascript: urls. + "no-self-compare": 1, // disallow comparisons where both sides are exactly the same (off by default) + "no-sequences": 1, // disallow use of comma operator + "no-unused-expressions": 0, // disallow usage of expressions in statement position + "no-void": 1, // disallow use of void operator (off by default) + "no-warning-comments": 0, // disallow usage of configurable warning terms in comments": 1, // e.g. TODO or FIXME (off by default) + "no-with": 1, // disallow use of the with statement + "radix": 1, // require use of the second argument for parseInt() (off by default) + "semi-spacing": 1, // require a space after a semi-colon + "vars-on-top": 0, // requires to declare all vars on top of their containing scope (off by default) + "wrap-iife": 0, // require immediate function invocation to be wrapped in parentheses (off by default) + "yoda": 1, // require or disallow Yoda conditions + + // Variables + // These rules have to do with variable declarations. + + "no-catch-shadow": 1, // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment) + "no-delete-var": 1, // disallow deletion of variables + "no-label-var": 1, // disallow labels that share a name with a variable + "no-shadow": 1, // disallow declaration of variables already declared in the outer scope + "no-shadow-restricted-names": 1, // disallow shadowing of names such as arguments + "no-undef": 2, // disallow use of undeclared variables unless mentioned in a /*global */ block + "no-undefined": 0, // disallow use of undefined variable (off by default) + "no-undef-init": 1, // disallow use of undefined when initializing variables + "no-unused-vars": [1, {"vars": "all", "args": "none", ignoreRestSiblings: true}], // disallow declaration of variables that are not used in the code + "no-use-before-define": 0, // disallow use of variables before they are defined + + // Node.js + // These rules are specific to JavaScript running on Node.js. + + "handle-callback-err": 1, // enforces error handling in callbacks (off by default) (on by default in the node environment) + "no-mixed-requires": 1, // disallow mixing regular variable and require declarations (off by default) (on by default in the node environment) + "no-new-require": 1, // disallow use of new operator with the require function (off by default) (on by default in the node environment) + "no-path-concat": 1, // disallow string concatenation with __dirname and __filename (off by default) (on by default in the node environment) + "no-process-exit": 0, // disallow process.exit() (on by default in the node environment) + "no-restricted-modules": 1, // restrict usage of specified node modules (off by default) + "no-sync": 0, // disallow use of synchronous methods (off by default) + + // ESLint Comments Plugin + // The following rules are made available via `eslint-plugin-eslint-comments` + "eslint-comments/no-aggregating-enable": 1, // disallows eslint-enable comments for multiple eslint-disable comments + "eslint-comments/no-unlimited-disable": 1, // disallows eslint-disable comments without rule names + "eslint-comments/no-unused-disable": 1, // disallow disables that don't cover any errors + "eslint-comments/no-unused-enable": 1, // // disallow enables that don't enable anything or enable rules that weren't disabled + + // Flow Plugin + // The following rules are made available via `eslint-plugin-flowtype` + "flowtype/define-flow-type": 1, + "flowtype/use-flow-type": 1, + + // Prettier Plugin + // https://github.com/prettier/eslint-plugin-prettier + "prettier/prettier": [2, "fb", "@format"], + + // Stylistic Issues + // These rules are purely matters of style and are quite subjective. + + "key-spacing": 0, + "keyword-spacing": 1, // enforce spacing before and after keywords + "jsx-quotes": [1, "prefer-double"], // enforces the usage of double quotes for all JSX attribute values which doesn’t contain a double quote + "comma-spacing": 0, + "no-multi-spaces": 0, + "brace-style": 0, // enforce one true brace style (off by default) + "camelcase": 0, // require camel case names + "consistent-this": 1, // enforces consistent naming when capturing the current execution context (off by default) + "eol-last": 1, // enforce newline at the end of file, with no multiple empty lines + "func-names": 0, // require function expressions to have a name (off by default) + "func-style": 0, // enforces use of function declarations or expressions (off by default) + "new-cap": 0, // require a capital letter for constructors + "new-parens": 1, // disallow the omission of parentheses when invoking a constructor with no arguments + "no-nested-ternary": 0, // disallow nested ternary expressions (off by default) + "no-array-constructor": 1, // disallow use of the Array constructor + "no-empty-character-class": 1, // disallow the use of empty character classes in regular expressions + "no-lonely-if": 0, // disallow if as the only statement in an else block (off by default) + "no-new-object": 1, // disallow use of the Object constructor + "no-spaced-func": 1, // disallow space between function identifier and application + "no-ternary": 0, // disallow the use of ternary operators (off by default) + "no-trailing-spaces": 1, // disallow trailing whitespace at the end of lines + "no-underscore-dangle": 0, // disallow dangling underscores in identifiers + "no-mixed-spaces-and-tabs": 1, // disallow mixed spaces and tabs for indentation + "quotes": [1, "single", "avoid-escape"], // specify whether double or single quotes should be used + "quote-props": 0, // require quotes around object literal property names (off by default) + "semi": 1, // require or disallow use of semicolons instead of ASI + "sort-vars": 0, // sort variables within the same declaration block (off by default) + "space-in-brackets": 0, // require or disallow spaces inside brackets (off by default) + "space-in-parens": 0, // require or disallow spaces inside parentheses (off by default) + "space-infix-ops": 1, // require spaces around operators + "space-unary-ops": [1, { "words": true, "nonwords": false }], // require or disallow spaces before/after unary operators (words on by default, nonwords off by default) + "max-nested-callbacks": 0, // specify the maximum depth callbacks can be nested (off by default) + "one-var": 0, // allow just one var statement per function (off by default) + "wrap-regex": 0, // require regex literals to be wrapped in parentheses (off by default) + + // Legacy + // The following rules are included for compatibility with JSHint and JSLint. While the names of the rules may not match up with the JSHint/JSLint counterpart, the functionality is the same. + + "max-depth": 0, // specify the maximum depth that blocks can be nested (off by default) + "max-len": 0, // specify the maximum length of a line in your program (off by default) + "max-params": 0, // limits the number of parameters that can be used in the function declaration. (off by default) + "max-statements": 0, // specify the maximum number of statement allowed in a function (off by default) + "no-bitwise": 1, // disallow use of bitwise operators (off by default) + "no-plusplus": 0, // disallow use of unary operators, ++ and -- (off by default) + + // React Plugin + // The following rules are made available via `eslint-plugin-react`. + + "react/display-name": 0, + "react/jsx-boolean-value": 0, + "react/jsx-no-comment-textnodes": 1, + "react/jsx-no-duplicate-props": 2, + "react/jsx-no-undef": 2, + "react/jsx-sort-props": 0, + "react/jsx-uses-react": 1, + "react/jsx-uses-vars": 1, + "react/no-did-mount-set-state": 1, + "react/no-did-update-set-state": 1, + "react/no-multi-comp": 0, + "react/no-string-refs": 1, + "react/no-unknown-property": 0, + "react/prop-types": 0, + "react/react-in-jsx-scope": 1, + "react/self-closing-comp": 1, + "react/wrap-multilines": 0, + + // React-Hooks Plugin + // The following rules are made available via `eslint-plugin-react-hooks` + "react-hooks/rules-of-hooks": "error", + + // React-Native Plugin + // The following rules are made available via `eslint-plugin-react-native` + + "react-native/no-inline-styles": 1, + + // Jest Plugin + // The following rules are made available via `eslint-plugin-jest`. + "jest/no-disabled-tests": 1, + "jest/no-focused-tests": 1, + "jest/no-identical-title": 1, + "jest/valid-expect": 1, + }, + + "overrides": [ + { + "files": [ + "**/__fixtures__/**/*.js", + "**/__mocks__/**/*.js", + "**/__tests__/**/*.js", + "jest/**/*.js", + "RNTester/**/*.js", + ], + "globals": { + // Expose some Jest globals for test helpers + "afterAll": true, + "afterEach": true, + "beforeAll": true, + "beforeEach": true, + "expect": true, + "jest": true, + }, + }, + { + "files": [ + "**/__tests__/**/*-test.js", + ], + "env": { + "jasmine": true, + "jest": true, + }, + }, + ], +} \ No newline at end of file diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 00000000..7a7cd225 --- /dev/null +++ b/.flowconfig @@ -0,0 +1,90 @@ +[ignore] +; This flowconfig is forked by platform - the only difference between them is which suffix is ignored. +.*/*[.]android.js +;.*/*[.]ios.js + +; Ignore templates for 'react-native init' +.*/local-cli/templates/.* + +; Ignore the Dangerfile +node_modules/react-native/bots/dangerfile.js + +; Ignore "BUCK" generated dirs +node_modules/react-native/\.buckd/ + +; Ignore unexpected extra "@providesModule" +.*/node_modules/.*/node_modules/fbjs/.* + +; Ignore duplicate module providers +; For RN Apps installed via npm, "Libraries" folder is inside +; "node_modules/react-native" but in the source repo it is in the root +.*/Libraries/react-native/React.js + +; Ignore polyfills +.*/Libraries/polyfills/.* + +; Ignore metro +.*/node_modules/metro/.* + +; Ignore "config-chain"'s test folder - it has a corrupt JSON file that's tripping flow +.*/node_modules/config-chain/test/*. + +; These should not be required directly +; require from fbjs/lib instead: require('fbjs/lib/invariant') +.*/node_modules/invariant/.* +.*/node_modules/warning/.* + +[include] + +[libs] +node_modules/react-native/Libraries/react-native/react-native-interface.js +node_modules/react-native/flow/ +node_modules/react-native/flow-github/ + +[lints] + +[options] +emoji=true + +esproposal.optional_chaining=enable +esproposal.nullish_coalescing=enable + +module.system=haste +module.system.haste.use_name_reducers=true +# keep the following in sync with server/haste/hasteImpl.js +# get basename +module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1' +# strip .js or .js.flow suffix +module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1' +# strip platform suffix +module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1' +module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1' +module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1' +module.system.haste.paths.blacklist=.*/__tests__/.* +module.system.haste.paths.blacklist=.*/__mocks__/.* +module.system.haste.paths.whitelist=/js/.* +module.system.haste.paths.whitelist=/node_modules/react-native/Libraries/.* +module.system.haste.paths.whitelist=/node_modules/react-native/RNTester/.* +module.system.haste.paths.whitelist=/node_modules/react-native/IntegrationTests/.* +module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/Animated/src/polyfills/.* + +munge_underscores=true + +module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' +# Support the library import in examples +module.name_mapper='^\@react-native-community/async-storage$' -> '/lib/AsyncStorage.js' + +suppress_type=$FlowIssue +suppress_type=$FlowFixMe +suppress_type=$FlowFixMeProps +suppress_type=$FlowFixMeState + +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*[react_native\\(_android\\)?_oss|react_native\\(_android\\)?_fb][a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*[react_native\\(_android\\)?_oss|react_native\\(_android\\)?_fb][a-z,_]*\\)?)\\)?:? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy +suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError + +[strict] + +[version] +^0.86.0 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..95367758 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ + +# OSX +# +.DS_Store + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml + +# BUCK +buck-out/ +\.buckd/ +*.keystore + +# Editor config +.vscode \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..9b128286 --- /dev/null +++ b/.npmignore @@ -0,0 +1,85 @@ +# JS +node_modules +yarn.lock + +# Project files +CONTRIBUTING.md +CODE_OF_CONDUCT.md +README.md + +# Config files +.babelrc +babel.config.js +.editorconfig +.eslintrc +.flowconfig +.watchmanconfig +jsconfig.json +.npmrc +.gitattributes +.circleci +*.coverage.json +.opensource +.circleci +.eslintignore +codecov.yml + +# Example +example/ + +# Android +android/*/build/ +android/gradlew +android/build +android/gradlew.bat +android/gradle/ +android/com_crashlytics_export_strings.xml +android/local.properties +android/.gradle/ +android/.signing/ +android/.idea/gradle.xml +android/.idea/libraries/ +android/.idea/workspace.xml +android/.idea/tasks.xml +android/.idea/.name +android/.idea/compiler.xml +android/.idea/copyright/profiles_settings.xml +android/.idea/encodings.xml +android/.idea/misc.xml +android/.idea/modules.xml +android/.idea/scopes/scope_settings.xml +android/.idea/vcs.xml +android/*.iml +android/.settings + +# iOS +ios/*.xcodeproj/xcuserdata +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 +*.xcuserstate +project.xcworkspace/ +xcuserdata/ + +# Misc +.DS_Store +.DS_Store? +*.DS_Store +coverage.android.json +coverage.ios.json +coverage +npm-debug.log +.github +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.dbandroid/gradle +docs +.idea +tests/ +bin/test.js +codorials +.vscode +.nyc_output \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..1d4c3eff --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "requirePragma": true, + "singleQuote": true, + "trailingComma": "all", + "bracketSpacing": false, + "jsxBracketSameLine": true, + "parser": "flow" +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d9021534 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015-present, Facebook, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 4bb8f7d8..0547f860 100644 --- a/README.md +++ b/README.md @@ -1 +1,24 @@ # react-native-async-storage + +Asynchronous, persistent, key-value storage system for React Native. + +## Getting started + + +### Install + +`yarn add @react-native-community/async-storage` + +or + +`npm install @react-native-community/async-storage --save` + + +### Link + +`react-native link @react-native-community/async-storage` + +## Usage + +ToDo + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 00000000..436e8dec --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,40 @@ + +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.2.1' + } +} + +def getExtOrDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['RNAsyncStorage_' + name] +} + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['RNAsyncStorage_' + name]).toInteger() +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion getExtOrIntegerDefault('compileSdkVersion') + buildToolsVersion getExtOrDefault('buildToolsVersion') + + defaultConfig { + minSdkVersion getExtOrIntegerDefault('minSdkVersion') + targetSdkVersion getExtOrIntegerDefault('targetSdkVersion') + } +} + +repositories { + mavenCentral() +} + +dependencies { + api 'com.facebook.react:react-native:+' +} + \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 00000000..109465ba --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,4 @@ +RNAsyncStorage_compileSdkVersion=28 +RNAsyncStorage_buildToolsVersion=28.0.3 +RNAsyncStorage_targetSdkVersion=28 +RNAsyncStorage_minSdkVersion=19 \ No newline at end of file diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..7e180bd3 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/AsyncLocalStorageUtil.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncLocalStorageUtil.java similarity index 92% rename from AsyncLocalStorageUtil.java rename to android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncLocalStorageUtil.java index 9efcad64..fe29a269 100644 --- a/AsyncLocalStorageUtil.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncLocalStorageUtil.java @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -package com.facebook.react.modules.storage; +package com.reactnativecommunity.asyncstorage; import javax.annotation.Nullable; @@ -22,9 +22,9 @@ import org.json.JSONException; import org.json.JSONObject; -import static com.facebook.react.modules.storage.ReactDatabaseSupplier.KEY_COLUMN; -import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST; -import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN; +import static com.reactnativecommunity.asyncstorage.ReactDatabaseSupplier.KEY_COLUMN; +import static com.reactnativecommunity.asyncstorage.ReactDatabaseSupplier.TABLE_CATALYST; +import static com.reactnativecommunity.asyncstorage.ReactDatabaseSupplier.VALUE_COLUMN; /** * Helper for database operations. diff --git a/AsyncStorageErrorUtil.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageErrorUtil.java similarity index 96% rename from AsyncStorageErrorUtil.java rename to android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageErrorUtil.java index fa5436ce..d74a0f11 100644 --- a/AsyncStorageErrorUtil.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageErrorUtil.java @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -package com.facebook.react.modules.storage; +package com.reactnativecommunity.asyncstorage; import javax.annotation.Nullable; diff --git a/AsyncStorageModule.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageModule.java similarity index 97% rename from AsyncStorageModule.java rename to android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageModule.java index ad3bed5c..211fe8a4 100644 --- a/AsyncStorageModule.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageModule.java @@ -5,11 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -package com.facebook.react.modules.storage; - -import java.util.ArrayDeque; -import java.util.HashSet; -import java.util.concurrent.Executor; +package com.reactnativecommunity.asyncstorage; import android.database.Cursor; import android.database.sqlite.SQLiteStatement; @@ -30,15 +26,20 @@ import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.modules.common.ModuleDataCleaner; -import static com.facebook.react.modules.storage.ReactDatabaseSupplier.KEY_COLUMN; -import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST; -import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN; +import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.concurrent.Executor; + +import static com.reactnativecommunity.asyncstorage.ReactDatabaseSupplier.KEY_COLUMN; +import static com.reactnativecommunity.asyncstorage.ReactDatabaseSupplier.TABLE_CATALYST; +import static com.reactnativecommunity.asyncstorage.ReactDatabaseSupplier.VALUE_COLUMN; @ReactModule(name = AsyncStorageModule.NAME) public final class AsyncStorageModule extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable { - public static final String NAME = "AsyncSQLiteDBStorage"; + // changed name to not conflict with AsyncStorage from RN repo + public static final String NAME = "RNC_AsyncSQLiteDBStorage"; // SQL variable number limit, defined by SQLITE_LIMIT_VARIABLE_NUMBER: // https://raw.githubusercontent.com/android/platform_external_sqlite/master/dist/sqlite3.c @@ -79,7 +80,7 @@ synchronized void scheduleNext() { } private final SerialExecutor executor; - + public AsyncStorageModule(ReactApplicationContext reactContext) { this(reactContext, AsyncTask.THREAD_POOL_EXECUTOR); } diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java new file mode 100644 index 00000000..25eb5a14 --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.reactnativecommunity.asyncstorage; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class AsyncStoragePackage implements ReactPackage { + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Arrays.asList(new AsyncStorageModule(reactContext)); + } + + // Deprecated in RN 0.47 + public List> createJSModules() { + return Collections.emptyList(); + } + + @Override + @SuppressWarnings("rawtypes") + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/ReactDatabaseSupplier.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/ReactDatabaseSupplier.java similarity index 98% rename from ReactDatabaseSupplier.java rename to android/src/main/java/com/reactnativecommunity/asyncstorage/ReactDatabaseSupplier.java index bcbb7f27..9f992ca4 100644 --- a/ReactDatabaseSupplier.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/ReactDatabaseSupplier.java @@ -4,8 +4,8 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -package com.facebook.react.modules.storage; + +package com.reactnativecommunity.asyncstorage; import javax.annotation.Nullable; diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000..3cdb4dfb --- /dev/null +++ b/babel.config.js @@ -0,0 +1,14 @@ +module.exports = { + presets: ['module:metro-react-native-babel-preset'], + plugins: [ + [ + 'module-resolver', + { + alias: { + '@react-native-community/asyns-storage': './lib/AsyncStorage', + }, + cwd: 'babelrc', + }, + ], + ], +}; diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 00000000..5d647565 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,56 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +*.keystore + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +*/fastlane/report.xml +*/fastlane/Preview.html +*/fastlane/screenshots + +# Bundle artifact +*.jsbundle diff --git a/example/__tests__/App.js b/example/__tests__/App.js new file mode 100644 index 00000000..a79ec3d5 --- /dev/null +++ b/example/__tests__/App.js @@ -0,0 +1,15 @@ +/** + * @format + * @lint-ignore-every XPLATJSCOPYRIGHT1 + */ + +import 'react-native'; +import React from 'react'; +import App from '../App'; + +// Note: test renderer must be required after react-native. +import renderer from 'react-test-renderer'; + +it('renders correctly', () => { + renderer.create(); +}); diff --git a/example/android/app/BUCK b/example/android/app/BUCK new file mode 100644 index 00000000..6a95aeb0 --- /dev/null +++ b/example/android/app/BUCK @@ -0,0 +1,55 @@ +# To learn about Buck see [Docs](https://buckbuild.com/). +# To run your application with Buck: +# - install Buck +# - `npm start` - to start the packager +# - `cd android` +# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` +# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck +# - `buck install -r android/app` - compile, install and run application +# + +load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") + +lib_deps = [] + +create_aar_targets(glob(["libs/*.aar"])) + +create_jar_targets(glob(["libs/*.jar"])) + +android_library( + name = "all-libs", + exported_deps = lib_deps, +) + +android_library( + name = "app-code", + srcs = glob([ + "src/main/java/**/*.java", + ]), + deps = [ + ":all-libs", + ":build_config", + ":res", + ], +) + +android_build_config( + name = "build_config", + package = "com.asyncstorageexample", +) + +android_resource( + name = "res", + package = "com.asyncstorageexample", + res = "src/main/res", +) + +android_binary( + name = "app", + keystore = "//android/keystores:debug", + manifest = "src/main/AndroidManifest.xml", + package_type = "debug", + deps = [ + ":app-code", + ], +) diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 00000000..5af21ec5 --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,147 @@ +apply plugin: "com.android.application" + +import com.android.build.OutputFile + +/** + * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets + * and bundleReleaseJsAndAssets). + * These basically call `react-native bundle` with the correct arguments during the Android build + * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the + * bundle directly from the development server. Below you can see all the possible configurations + * and their defaults. If you decide to add a configuration block, make sure to add it before the + * `apply from: "../../node_modules/react-native/react.gradle"` line. + * + * project.ext.react = [ + * // the name of the generated asset file containing your JS bundle + * bundleAssetName: "index.android.bundle", + * + * // the entry file for bundle generation + * entryFile: "index.android.js", + * + * // whether to bundle JS and assets in debug mode + * bundleInDebug: false, + * + * // whether to bundle JS and assets in release mode + * bundleInRelease: true, + * + * // whether to bundle JS and assets in another build variant (if configured). + * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants + * // The configuration property can be in the following formats + * // 'bundleIn${productFlavor}${buildType}' + * // 'bundleIn${buildType}' + * // bundleInFreeDebug: true, + * // bundleInPaidRelease: true, + * // bundleInBeta: true, + * + * // whether to disable dev mode in custom build variants (by default only disabled in release) + * // for example: to disable dev mode in the staging build type (if configured) + * devDisabledInStaging: true, + * // The configuration property can be in the following formats + * // 'devDisabledIn${productFlavor}${buildType}' + * // 'devDisabledIn${buildType}' + * + * // the root of your project, i.e. where "package.json" lives + * root: "../../", + * + * // where to put the JS bundle asset in debug mode + * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", + * + * // where to put the JS bundle asset in release mode + * jsBundleDirRelease: "$buildDir/intermediates/assets/release", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in debug mode + * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in release mode + * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", + * + * // by default the gradle tasks are skipped if none of the JS files or assets change; this means + * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to + * // date; if you have any other folders that you want to ignore for performance reasons (gradle + * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ + * // for example, you might want to remove it from here. + * inputExcludes: ["android/**", "ios/**"], + * + * // override which node gets called and with what additional arguments + * nodeExecutableAndArgs: ["node"], + * + * // supply additional arguments to the packager + * extraPackagerArgs: [] + * ] + */ + +project.ext.react = [ + entryFile: "index.js" +] + +apply from: "../../../node_modules/react-native/react.gradle" + +/** + * Set this to true to create two separate APKs instead of one: + * - An APK that only works on ARM devices + * - An APK that only works on x86 devices + * The advantage is the size of the APK is reduced by about 4MB. + * Upload all the APKs to the Play Store and people will download + * the correct one based on the CPU architecture of their device. + */ +def enableSeparateBuildPerCPUArchitecture = false + +/** + * Run Proguard to shrink the Java bytecode in release builds. + */ +def enableProguardInReleaseBuilds = false + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + + defaultConfig { + applicationId "com.asyncstorageexample" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + } + splits { + abi { + reset() + enable enableSeparateBuildPerCPUArchitecture + universalApk false // If true, also generate a universal APK + include "armeabi-v7a", "x86", "arm64-v8a" + } + } + buildTypes { + release { + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } + } + // applicationVariants are e.g. debug, release + applicationVariants.all { variant -> + variant.outputs.each { output -> + // For each separate APK per architecture, set a unique version code as described here: + // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits + def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3] + def abi = output.getFilter(OutputFile.ABI) + if (abi != null) { // null for the universal-debug, universal-release variants + output.versionCodeOverride = + versionCodes.get(abi) * 1048576 + defaultConfig.versionCode + } + } + } +} + +dependencies { + implementation project(':rnAsyncStorage') + implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" + implementation "com.facebook.react:react-native:+" // From node_modules +} + +// Run this once to be able to run the application with BUCK +// puts all compile dependencies into folder libs for BUCK to use +task copyDownloadableDepsToLibs(type: Copy) { + from configurations.compile + into 'libs' +} diff --git a/example/android/app/build_defs.bzl b/example/android/app/build_defs.bzl new file mode 100644 index 00000000..fff270f8 --- /dev/null +++ b/example/android/app/build_defs.bzl @@ -0,0 +1,19 @@ +"""Helper definitions to glob .aar and .jar targets""" + +def create_aar_targets(aarfiles): + for aarfile in aarfiles: + name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] + lib_deps.append(":" + name) + android_prebuilt_aar( + name = name, + aar = aarfile, + ) + +def create_jar_targets(jarfiles): + for jarfile in jarfiles: + name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] + lib_deps.append(":" + name) + prebuilt_jar( + name = name, + binary_jar = jarfile, + ) diff --git a/example/android/app/proguard-rules.pro b/example/android/app/proguard-rules.pro new file mode 100644 index 00000000..a92fa177 --- /dev/null +++ b/example/android/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..12c72725 --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/java/com/asyncstorageexample/MainActivity.java b/example/android/app/src/main/java/com/asyncstorageexample/MainActivity.java new file mode 100644 index 00000000..8f093117 --- /dev/null +++ b/example/android/app/src/main/java/com/asyncstorageexample/MainActivity.java @@ -0,0 +1,15 @@ +package com.asyncstorageexample; + +import com.facebook.react.ReactActivity; + +public class MainActivity extends ReactActivity { + + /** + * Returns the name of the main component registered from JavaScript. + * This is used to schedule rendering of the component. + */ + @Override + protected String getMainComponentName() { + return "AsyncStorageExample"; + } +} diff --git a/example/android/app/src/main/java/com/asyncstorageexample/MainApplication.java b/example/android/app/src/main/java/com/asyncstorageexample/MainApplication.java new file mode 100644 index 00000000..2a0f8e77 --- /dev/null +++ b/example/android/app/src/main/java/com/asyncstorageexample/MainApplication.java @@ -0,0 +1,47 @@ +package com.asyncstorageexample; + +import android.app.Application; + +import com.facebook.react.ReactApplication; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.ReactPackage; +import com.facebook.react.shell.MainReactPackage; +import com.facebook.soloader.SoLoader; +import com.reactnativecommunity.asyncstorage.AsyncStoragePackage; + +import java.util.Arrays; +import java.util.List; + +public class MainApplication extends Application implements ReactApplication { + + private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { + @Override + public boolean getUseDeveloperSupport() { + return BuildConfig.DEBUG; + } + + @Override + protected List getPackages() { + return Arrays.asList( + new MainReactPackage(), + new AsyncStoragePackage() + ); + } + + @Override + protected String getJSMainModuleName() { + return "example/index"; + } + }; + + @Override + public ReactNativeHost getReactNativeHost() { + return mReactNativeHost; + } + + @Override + public void onCreate() { + super.onCreate(); + SoLoader.init(this, /* native exopackage */ false); + } +} diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a2f5908281d070150700378b64a84c7db1f97aa1 GIT binary patch literal 3056 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 axPAxe%7rDusX+w*00006jj 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⁣ zwiG+x#%j}{0gU~J8;<|!B1@-XaB@{KORFwrYg_8rOv({b0EO#DbeQRm;B6_9=mXGf z-x|VL{zd`)#@yN}HkCSJbjbNlE|zL3Wm9Q8HY`sV)}3%pgN>cL^67{Z;PPL(*wT8N zUjXU{@|*hvm}({wsAC=x0^ok0%UAz0;sogW{B!nDqk|JJ5x~4NfTDgP49^zeu`csl?5mY@JdQdISc zFs!E{^grmkLnUk9 zny~m)1vws@5BFI<-0Tuo2JWX(0v`W|t(wg;s--L47WTvTMz-8l#TL^=OJNRS2?_Qj z3AKT+gvbyBi#H*-tJ%tWD|>EV3wy|8qxfzS!5RW;Jpl5*zo&^UBU=fG#2}UvRyNkK zA06Dy9;K1ca@r2T>yThYgI!ont$(G{6q#2QT+00r_x0(b)gsE`lBB?2gr55gq^D3Fi&p%E(p9>U%bv zkg1Jco(RbyTX7FDHOnl7-O@ zI$AaIl?9NJKPm(WiBP`1-#CB1QzU>&hKm)fpa5DKE{2$X0hGz-0uZ?cyTk(YC!Y&| zL=1VrNERSA5NA2jq7FACfX4JfPyj5XXl1yv0>~s;eF7L2$>&oMqeTFT2m$y7FlkON z_yurD1yIOvA;5C6016pyxBznGUt0kJ&k5r#;&>Jow`r)sp9R~PmK~lz$3xH%LT*1U zJdOyABZ3!FvNoR*vN$5ykHS8f`jA4zV+|L}i1C4`B2c{R0;UdYxaU|H)2avz@ z=mEYc|2S<+(B2Tj+FkX+2D+yFI!k9lWMA61DJ{)e;lum$(;O87?vGJJe!KtK04+N_ zI*P~t@dUb>9Xh{dbyl{-ZQ(UMgz7$|QfL5XSPkskt^NgctYC#;4WcZB1@%@wy@2t3 z2z0DI7&%b$*Aw~abe?GxE`ez@+6hOh-6*8fHRV{1os$EL@}uUZeG4h1&Be`98q*7j z=3-v+lhIjfWVo12!<>%V^a6lTgW3+_#W6n|p*~==zOH7z$0{LSZk(Tpd7EaD04hnA zL;#fxS0aD{`5^&D`}>0Uq?byDD-l2=!wm_bLcUl4gc(% za1p|itVANvFF>hghAS07Im1;IK;|b*W)}VDyI;BIp2=K*yu2a)j?B|f<44NI$NbmJ z#dE0>jI$fMr&@>4kN8MLFb4&2O9fEKaQg%(QO$4_1rVQywG^CmBLh#}_7gKW3vd?| z2?1^&KWq8}8I^_S0|)MowU_pw$q@nl@Nkn$z>BQq_KA^9yaR`(R3u{{Ig;cwt z@AJ^{ODQCm^neroM9nKNUAXi9RCK`OsP_LuR0PUR(YZCCX5dNF6VzcoK&=b^r`W?ltt|*F zpkoae%ZT{C1h~EcFui~b7fF`vb<<~j_VquuUA$}QqIKYELPp#;{u?q8Dz}WAG-(3; zjrm$i%7UbyZMM(Y{>!uJ#vNB?R~B{6Htp=>e*<{fQQ5W7V(1coCWlOON!MzZxhum| ztZBQpGR z;~#ur^&PockKdV{Q6R>o`Pl{0x!DEbpZ7y9Y;*ZvE!*gU`V1W3znva{f=?WO5I&>B z&hw6}tjECtaghm5z|C#%M;Yf_*pI^};h}Vl=^r9EN=tVDj86D;C$jIJ?K7VP+00000NkvXXu0mjf D5i!M* literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..459ca609d3ae0d3943ab44cdc27feef9256dc6d7 GIT binary patch literal 7098 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 literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..8ca12fe024be86e868d14e91120a6902f8e88ac6 GIT binary patch literal 6464 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 B0*L?s literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..8e19b410a1b15ff180f3dacac19395fe3046cdec GIT binary patch literal 10676 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 literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..4c19a13c239cb67b8a2134ddd5f325db1d2d5bee GIT binary patch literal 15523 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 BIP(Ai literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/values/strings.xml b/example/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..a9f943c1 --- /dev/null +++ b/example/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + AsyncStorageExample + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..319eb0ca --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 00000000..f84862ff --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,39 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext { + buildToolsVersion = "28.0.3" + minSdkVersion = 16 + compileSdkVersion = 28 + targetSdkVersion = 27 + supportLibVersion = "28.0.0" + } + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.2.1' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + mavenLocal() + google() + jcenter() + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url "$rootDir/../../node_modules/react-native/android" + } + } +} + + +task wrapper(type: Wrapper) { + gradleVersion = '4.7' + distributionUrl = distributionUrl.replace("bin", "all") +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 00000000..89e0d99e --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,18 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.jar b/example/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..01b8bf6b1f99cad9213fc495b33ad5bbab8efd20 GIT binary patch literal 54329 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

<5G2M1WZP5 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/example/android/gradlew.bat b/example/android/gradlew.bat new file mode 100644 index 00000000..f9553162 --- /dev/null +++ b/example/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/example/android/keystores/BUCK b/example/android/keystores/BUCK new file mode 100644 index 00000000..88e4c31b --- /dev/null +++ b/example/android/keystores/BUCK @@ -0,0 +1,8 @@ +keystore( + name = "debug", + properties = "debug.keystore.properties", + store = "debug.keystore", + visibility = [ + "PUBLIC", + ], +) diff --git a/example/android/keystores/debug.keystore.properties b/example/android/keystores/debug.keystore.properties new file mode 100644 index 00000000..121bfb49 --- /dev/null +++ b/example/android/keystores/debug.keystore.properties @@ -0,0 +1,4 @@ +key.store=debug.keystore +key.alias=androiddebugkey +key.store.password=android +key.alias.password=android diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 00000000..1d2109a8 --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,6 @@ +rootProject.name = 'AsyncStorageExample' + +include ':app' +include ':rnAsyncStorage' + +project(':rnAsyncStorage').projectDir = new File(rootProject.projectDir, '../../android') diff --git a/example/app.json b/example/app.json new file mode 100644 index 00000000..de5021de --- /dev/null +++ b/example/app.json @@ -0,0 +1,4 @@ +{ + "name": "AsyncStorageExample", + "displayName": "AsyncStorageExample" +} \ No newline at end of file diff --git a/example/index.js b/example/index.js new file mode 100644 index 00000000..073d1aa1 --- /dev/null +++ b/example/index.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +import {AppRegistry} from 'react-native'; +import App from './src/App'; +import {name as appName} from './app.json'; + +AppRegistry.registerComponent(appName, () => App); diff --git a/example/ios/AsyncStorageExample.xcodeproj/project.pbxproj b/example/ios/AsyncStorageExample.xcodeproj/project.pbxproj new file mode 100644 index 00000000..8ed20ab3 --- /dev/null +++ b/example/ios/AsyncStorageExample.xcodeproj/project.pbxproj @@ -0,0 +1,1128 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; + 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; + 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; + 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; }; + 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; }; + 11D1A2F320CAFA9E000508D9 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; }; + 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; }; + 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */; }; + 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */; }; + 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; + 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; + 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; + ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */; }; + ED297163215061F000B7C4FE /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED297162215061F000B7C4FE /* JavaScriptCore.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTActionSheet; + }; + 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTGeolocation; + }; + 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5115D1A9E6B3D00147676; + remoteInfo = RCTImage; + }; + 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B511DB1A9E6C8500147676; + remoteInfo = RCTNetwork; + }; + 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; + remoteInfo = RCTVibration; + }; + 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTSettings; + }; + 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3C86DF461ADF2C930047B81A; + remoteInfo = RCTWebSocket; + }; + 146834031AC3E56700842450 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; + remoteInfo = React; + }; + 2D16E6711FA4F8DC00B85C8A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = ADD01A681E09402E00F6D226; + remoteInfo = "RCTBlob-tvOS"; + }; + 2D16E6831FA4F8DC00B85C8A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3DBE0D001F3B181A0099AA32; + remoteInfo = fishhook; + }; + 2D16E6851FA4F8DC00B85C8A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3DBE0D0D1F3B181C0099AA32; + remoteInfo = "fishhook-tvOS"; + }; + 2DF0FFDE2056DD460020B375 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = EBF21BDC1FC498900052F4D5; + remoteInfo = jsinspector; + }; + 2DF0FFE02056DD460020B375 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = EBF21BFA1FC4989A0052F4D5; + remoteInfo = "jsinspector-tvOS"; + }; + 2DF0FFE22056DD460020B375 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 139D7ECE1E25DB7D00323FB7; + remoteInfo = "third-party"; + }; + 2DF0FFE42056DD460020B375 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3D383D3C1EBD27B6005632C8; + remoteInfo = "third-party-tvOS"; + }; + 2DF0FFE62056DD460020B375 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 139D7E881E25C6D100323FB7; + remoteInfo = "double-conversion"; + }; + 2DF0FFE82056DD460020B375 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3D383D621EBD27B9005632C8; + remoteInfo = "double-conversion-tvOS"; + }; + 3DAD3E831DF850E9000B6D8A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A283A1D9B042B00D4039D; + remoteInfo = "RCTImage-tvOS"; + }; + 3DAD3E871DF850E9000B6D8A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A28471D9B043800D4039D; + remoteInfo = "RCTLinking-tvOS"; + }; + 3DAD3E8B1DF850E9000B6D8A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A28541D9B044C00D4039D; + remoteInfo = "RCTNetwork-tvOS"; + }; + 3DAD3E8F1DF850E9000B6D8A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A28611D9B046600D4039D; + remoteInfo = "RCTSettings-tvOS"; + }; + 3DAD3E931DF850E9000B6D8A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A287B1D9B048500D4039D; + remoteInfo = "RCTText-tvOS"; + }; + 3DAD3E981DF850E9000B6D8A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A28881D9B049200D4039D; + remoteInfo = "RCTWebSocket-tvOS"; + }; + 3DAD3EA21DF850E9000B6D8A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A28131D9B038B00D4039D; + remoteInfo = "React-tvOS"; + }; + 3DAD3EA41DF850E9000B6D8A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3D3C059A1DE3340900C268FA; + remoteInfo = yoga; + }; + 3DAD3EA61DF850E9000B6D8A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3D3C06751DE3340C00C268FA; + remoteInfo = "yoga-tvOS"; + }; + 3DAD3EA81DF850E9000B6D8A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3D3CD9251DE5FBEC00167DC4; + remoteInfo = cxxreact; + }; + 3DAD3EAA1DF850E9000B6D8A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3D3CD9321DE5FBEE00167DC4; + remoteInfo = "cxxreact-tvOS"; + }; + 3DC53982220F2C940035D3A3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = EDEBC6D6214B3E7000DD5AC8; + remoteInfo = jsi; + }; + 3DC53984220F2C940035D3A3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = EDEBC73B214B45A300DD5AC8; + remoteInfo = jsiexecutor; + }; + 3DC53986220F2C940035D3A3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = ED296FB6214C9A0900B7C4FE; + remoteInfo = "jsi-tvOS"; + }; + 3DC53988220F2C940035D3A3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = ED296FEE214C9CF800B7C4FE; + remoteInfo = "jsiexecutor-tvOS"; + }; + 3DC5398B220F2C940035D3A3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3DC5395A220F2C940035D3A3 /* RNCAsyncstorage.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RNCAsyncstorage; + }; + 5E9157321DD0AC6500FF2AA8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTAnimation; + }; + 5E9157341DD0AC6500FF2AA8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A28201D9B03D100D4039D; + remoteInfo = "RCTAnimation-tvOS"; + }; + 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTLinking; + }; + 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5119B1A9E6C1200147676; + remoteInfo = RCTText; + }; + ADBDB9261DFEBF0700ED6528 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 358F4ED71D1E81A9004DF814; + remoteInfo = RCTBlob; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; + 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = "../../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj"; sourceTree = ""; }; + 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = "../../node_modules/react-native/Libraries/Geolocation/RCTGeolocation.xcodeproj"; sourceTree = ""; }; + 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = "../../node_modules/react-native/Libraries/Image/RCTImage.xcodeproj"; sourceTree = ""; }; + 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = "../../node_modules/react-native/Libraries/Network/RCTNetwork.xcodeproj"; sourceTree = ""; }; + 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = "../../node_modules/react-native/Libraries/Vibration/RCTVibration.xcodeproj"; sourceTree = ""; }; + 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = "../../node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj"; sourceTree = ""; }; + 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = "../../node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj"; sourceTree = ""; }; + 13B07F961A680F5B00A75B9A /* AsyncStorageExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AsyncStorageExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = AsyncStorageExample/AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = AsyncStorageExample/AppDelegate.m; sourceTree = ""; }; + 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = AsyncStorageExample/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = AsyncStorageExample/Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = AsyncStorageExample/main.m; sourceTree = ""; }; + 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../../node_modules/react-native/React/React.xcodeproj"; sourceTree = ""; }; + 2D16E6891FA4F8E400B85C8A /* libReact.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libReact.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 3DC5395A220F2C940035D3A3 /* RNCAsyncstorage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNCAsyncstorage.xcodeproj; path = ../../ios/RNCAsyncstorage.xcodeproj; sourceTree = ""; }; + 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = ""; }; + 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; + 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; + ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTBlob.xcodeproj; path = "../../node_modules/react-native/Libraries/Blob/RCTBlob.xcodeproj"; sourceTree = ""; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ED297163215061F000B7C4FE /* JavaScriptCore.framework in Frameworks */, + ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */, + 11D1A2F320CAFA9E000508D9 /* libRCTAnimation.a in Frameworks */, + 146834051AC3E58100842450 /* libReact.a in Frameworks */, + 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, + 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */, + 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */, + 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */, + 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */, + 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */, + 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, + 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, + 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 00C302A81ABCB8CE00DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302B61ABCB90400DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302BC1ABCB91800DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */, + 3DAD3E841DF850E9000B6D8A /* libRCTImage-tvOS.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302D41ABCB9D200DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */, + 3DAD3E8C1DF850E9000B6D8A /* libRCTNetwork-tvOS.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302E01ABCB9EE00DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */, + ); + name = Products; + sourceTree = ""; + }; + 139105B71AF99BAD00B5F7CC /* Products */ = { + isa = PBXGroup; + children = ( + 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */, + 3DAD3E901DF850E9000B6D8A /* libRCTSettings-tvOS.a */, + ); + name = Products; + sourceTree = ""; + }; + 139FDEE71B06529A00C62182 /* Products */ = { + isa = PBXGroup; + children = ( + 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */, + 3DAD3E991DF850E9000B6D8A /* libRCTWebSocket-tvOS.a */, + 2D16E6841FA4F8DC00B85C8A /* libfishhook.a */, + 2D16E6861FA4F8DC00B85C8A /* libfishhook-tvOS.a */, + ); + name = Products; + sourceTree = ""; + }; + 13B07FAE1A68108700A75B9A /* AsyncStorageExample */ = { + isa = PBXGroup; + children = ( + 008F07F21AC5B25A0029DE68 /* main.jsbundle */, + 13B07FAF1A68108700A75B9A /* AppDelegate.h */, + 13B07FB01A68108700A75B9A /* AppDelegate.m */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, + 13B07FB71A68108700A75B9A /* main.m */, + ); + name = AsyncStorageExample; + sourceTree = ""; + }; + 146834001AC3E56700842450 /* Products */ = { + isa = PBXGroup; + children = ( + 146834041AC3E56700842450 /* libReact.a */, + 3DAD3EA31DF850E9000B6D8A /* libReact.a */, + 3DAD3EA51DF850E9000B6D8A /* libyoga.a */, + 3DAD3EA71DF850E9000B6D8A /* libyoga.a */, + 3DAD3EA91DF850E9000B6D8A /* libcxxreact.a */, + 3DAD3EAB1DF850E9000B6D8A /* libcxxreact.a */, + 2DF0FFDF2056DD460020B375 /* libjsinspector.a */, + 2DF0FFE12056DD460020B375 /* libjsinspector-tvOS.a */, + 2DF0FFE32056DD460020B375 /* libthird-party.a */, + 2DF0FFE52056DD460020B375 /* libthird-party.a */, + 2DF0FFE72056DD460020B375 /* libdouble-conversion.a */, + 2DF0FFE92056DD460020B375 /* libdouble-conversion.a */, + 3DC53983220F2C940035D3A3 /* libjsi.a */, + 3DC53985220F2C940035D3A3 /* libjsiexecutor.a */, + 3DC53987220F2C940035D3A3 /* libjsi-tvOS.a */, + 3DC53989220F2C940035D3A3 /* libjsiexecutor-tvOS.a */, + ); + name = Products; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + ED2971642150620600B7C4FE /* JavaScriptCore.framework */, + 2D16E6891FA4F8E400B85C8A /* libReact.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 3DC5395B220F2C940035D3A3 /* Products */ = { + isa = PBXGroup; + children = ( + 3DC5398C220F2C940035D3A3 /* libRNCAsyncstorage.a */, + ); + name = Products; + sourceTree = ""; + }; + 5E91572E1DD0AC6500FF2AA8 /* Products */ = { + isa = PBXGroup; + children = ( + 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */, + 5E9157351DD0AC6500FF2AA8 /* libRCTAnimation.a */, + ); + name = Products; + sourceTree = ""; + }; + 78C398B11ACF4ADC00677621 /* Products */ = { + isa = PBXGroup; + children = ( + 78C398B91ACF4ADC00677621 /* libRCTLinking.a */, + 3DAD3E881DF850E9000B6D8A /* libRCTLinking-tvOS.a */, + ); + name = Products; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */, + 146833FF1AC3E56700842450 /* React.xcodeproj */, + 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */, + ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */, + 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */, + 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */, + 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */, + 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */, + 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */, + 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, + 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, + 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */, + ); + name = Libraries; + sourceTree = ""; + }; + 832341B11AAA6A8300B99B32 /* Products */ = { + isa = PBXGroup; + children = ( + 832341B51AAA6A8300B99B32 /* libRCTText.a */, + 3DAD3E941DF850E9000B6D8A /* libRCTText-tvOS.a */, + ); + name = Products; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 3DC5395A220F2C940035D3A3 /* RNCAsyncstorage.xcodeproj */, + 13B07FAE1A68108700A75B9A /* AsyncStorageExample */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* AsyncStorageExample.app */, + ); + name = Products; + sourceTree = ""; + }; + ADBDB9201DFEBF0600ED6528 /* Products */ = { + isa = PBXGroup; + children = ( + ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */, + 2D16E6721FA4F8DC00B85C8A /* libRCTBlob-tvOS.a */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 13B07F861A680F5B00A75B9A /* AsyncStorageExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "AsyncStorageExample" */; + buildPhases = ( + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AsyncStorageExample; + productName = "Hello World"; + productReference = 13B07F961A680F5B00A75B9A /* AsyncStorageExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0940; + ORGANIZATIONNAME = Facebook; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "AsyncStorageExample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 00C302A81ABCB8CE00DB3ED1 /* Products */; + ProjectRef = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; + }, + { + ProductGroup = 5E91572E1DD0AC6500FF2AA8 /* Products */; + ProjectRef = 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */; + }, + { + ProductGroup = ADBDB9201DFEBF0600ED6528 /* Products */; + ProjectRef = ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */; + }, + { + ProductGroup = 00C302B61ABCB90400DB3ED1 /* Products */; + ProjectRef = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; + }, + { + ProductGroup = 00C302BC1ABCB91800DB3ED1 /* Products */; + ProjectRef = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; + }, + { + ProductGroup = 78C398B11ACF4ADC00677621 /* Products */; + ProjectRef = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; + }, + { + ProductGroup = 00C302D41ABCB9D200DB3ED1 /* Products */; + ProjectRef = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; + }, + { + ProductGroup = 139105B71AF99BAD00B5F7CC /* Products */; + ProjectRef = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; + }, + { + ProductGroup = 832341B11AAA6A8300B99B32 /* Products */; + ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; + }, + { + ProductGroup = 00C302E01ABCB9EE00DB3ED1 /* Products */; + ProjectRef = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; + }, + { + ProductGroup = 139FDEE71B06529A00C62182 /* Products */; + ProjectRef = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; + }, + { + ProductGroup = 146834001AC3E56700842450 /* Products */; + ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */; + }, + { + ProductGroup = 3DC5395B220F2C940035D3A3 /* Products */; + ProjectRef = 3DC5395A220F2C940035D3A3 /* RNCAsyncstorage.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* AsyncStorageExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTActionSheet.a; + remoteRef = 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTGeolocation.a; + remoteRef = 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTImage.a; + remoteRef = 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTNetwork.a; + remoteRef = 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTVibration.a; + remoteRef = 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTSettings.a; + remoteRef = 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTWebSocket.a; + remoteRef = 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 146834041AC3E56700842450 /* libReact.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libReact.a; + remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 2D16E6721FA4F8DC00B85C8A /* libRCTBlob-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRCTBlob-tvOS.a"; + remoteRef = 2D16E6711FA4F8DC00B85C8A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 2D16E6841FA4F8DC00B85C8A /* libfishhook.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libfishhook.a; + remoteRef = 2D16E6831FA4F8DC00B85C8A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 2D16E6861FA4F8DC00B85C8A /* libfishhook-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libfishhook-tvOS.a"; + remoteRef = 2D16E6851FA4F8DC00B85C8A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 2DF0FFDF2056DD460020B375 /* libjsinspector.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libjsinspector.a; + remoteRef = 2DF0FFDE2056DD460020B375 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 2DF0FFE12056DD460020B375 /* libjsinspector-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libjsinspector-tvOS.a"; + remoteRef = 2DF0FFE02056DD460020B375 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 2DF0FFE32056DD460020B375 /* libthird-party.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libthird-party.a"; + remoteRef = 2DF0FFE22056DD460020B375 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 2DF0FFE52056DD460020B375 /* libthird-party.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libthird-party.a"; + remoteRef = 2DF0FFE42056DD460020B375 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 2DF0FFE72056DD460020B375 /* libdouble-conversion.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libdouble-conversion.a"; + remoteRef = 2DF0FFE62056DD460020B375 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 2DF0FFE92056DD460020B375 /* libdouble-conversion.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libdouble-conversion.a"; + remoteRef = 2DF0FFE82056DD460020B375 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DAD3E841DF850E9000B6D8A /* libRCTImage-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRCTImage-tvOS.a"; + remoteRef = 3DAD3E831DF850E9000B6D8A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DAD3E881DF850E9000B6D8A /* libRCTLinking-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRCTLinking-tvOS.a"; + remoteRef = 3DAD3E871DF850E9000B6D8A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DAD3E8C1DF850E9000B6D8A /* libRCTNetwork-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRCTNetwork-tvOS.a"; + remoteRef = 3DAD3E8B1DF850E9000B6D8A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DAD3E901DF850E9000B6D8A /* libRCTSettings-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRCTSettings-tvOS.a"; + remoteRef = 3DAD3E8F1DF850E9000B6D8A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DAD3E941DF850E9000B6D8A /* libRCTText-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRCTText-tvOS.a"; + remoteRef = 3DAD3E931DF850E9000B6D8A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DAD3E991DF850E9000B6D8A /* libRCTWebSocket-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRCTWebSocket-tvOS.a"; + remoteRef = 3DAD3E981DF850E9000B6D8A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DAD3EA31DF850E9000B6D8A /* libReact.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libReact.a; + remoteRef = 3DAD3EA21DF850E9000B6D8A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DAD3EA51DF850E9000B6D8A /* libyoga.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libyoga.a; + remoteRef = 3DAD3EA41DF850E9000B6D8A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DAD3EA71DF850E9000B6D8A /* libyoga.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libyoga.a; + remoteRef = 3DAD3EA61DF850E9000B6D8A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DAD3EA91DF850E9000B6D8A /* libcxxreact.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libcxxreact.a; + remoteRef = 3DAD3EA81DF850E9000B6D8A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DAD3EAB1DF850E9000B6D8A /* libcxxreact.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libcxxreact.a; + remoteRef = 3DAD3EAA1DF850E9000B6D8A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DC53983220F2C940035D3A3 /* libjsi.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libjsi.a; + remoteRef = 3DC53982220F2C940035D3A3 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DC53985220F2C940035D3A3 /* libjsiexecutor.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libjsiexecutor.a; + remoteRef = 3DC53984220F2C940035D3A3 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DC53987220F2C940035D3A3 /* libjsi-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libjsi-tvOS.a"; + remoteRef = 3DC53986220F2C940035D3A3 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DC53989220F2C940035D3A3 /* libjsiexecutor-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libjsiexecutor-tvOS.a"; + remoteRef = 3DC53988220F2C940035D3A3 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 3DC5398C220F2C940035D3A3 /* libRNCAsyncstorage.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRNCAsyncstorage.a; + remoteRef = 3DC5398B220F2C940035D3A3 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTAnimation.a; + remoteRef = 5E9157321DD0AC6500FF2AA8 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 5E9157351DD0AC6500FF2AA8 /* libRCTAnimation.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTAnimation.a; + remoteRef = 5E9157341DD0AC6500FF2AA8 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTLinking.a; + remoteRef = 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 832341B51AAA6A8300B99B32 /* libRCTText.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTText.a; + remoteRef = 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTBlob.a; + remoteRef = ADBDB9261DFEBF0700ED6528 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export NODE_BINARY=node\n../../node_modules/react-native/scripts/react-native-xcode.sh"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, + 13B07FC11A68108700A75B9A /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { + isa = PBXVariantGroup; + children = ( + 13B07FB21A68108700A75B9A /* Base */, + ); + name = LaunchScreen.xib; + path = AsyncStorageExample; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = NO; + INFOPLIST_FILE = AsyncStorageExample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = AsyncStorageExample; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = AsyncStorageExample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = AsyncStorageExample; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "AsyncStorageExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "AsyncStorageExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/example/ios/AsyncStorageExample.xcodeproj/xcshareddata/xcschemes/AsyncStorageExample-tvOS.xcscheme b/example/ios/AsyncStorageExample.xcodeproj/xcshareddata/xcschemes/AsyncStorageExample-tvOS.xcscheme new file mode 100644 index 00000000..d7047ae8 --- /dev/null +++ b/example/ios/AsyncStorageExample.xcodeproj/xcshareddata/xcschemes/AsyncStorageExample-tvOS.xcscheme @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/AsyncStorageExample.xcodeproj/xcshareddata/xcschemes/AsyncStorageExample.xcscheme b/example/ios/AsyncStorageExample.xcodeproj/xcshareddata/xcschemes/AsyncStorageExample.xcscheme new file mode 100644 index 00000000..17666f5f --- /dev/null +++ b/example/ios/AsyncStorageExample.xcodeproj/xcshareddata/xcschemes/AsyncStorageExample.xcscheme @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/AsyncStorageExample/AppDelegate.h b/example/ios/AsyncStorageExample/AppDelegate.h new file mode 100644 index 00000000..4b5644f2 --- /dev/null +++ b/example/ios/AsyncStorageExample/AppDelegate.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface AppDelegate : UIResponder + +@property (nonatomic, strong) UIWindow *window; + +@end diff --git a/example/ios/AsyncStorageExample/AppDelegate.m b/example/ios/AsyncStorageExample/AppDelegate.m new file mode 100644 index 00000000..18f05547 --- /dev/null +++ b/example/ios/AsyncStorageExample/AppDelegate.m @@ -0,0 +1,35 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "AppDelegate.h" + +#import +#import + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + NSURL *jsCodeLocation; + + jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"example/index" fallbackResource:nil]; + + RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation + moduleName:@"AsyncStorageExample" + initialProperties:nil + launchOptions:launchOptions]; + rootView.backgroundColor = [UIColor blackColor]; + + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UIViewController *rootViewController = [UIViewController new]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/example/ios/AsyncStorageExample/Base.lproj/LaunchScreen.xib b/example/ios/AsyncStorageExample/Base.lproj/LaunchScreen.xib new file mode 100644 index 00000000..018333da --- /dev/null +++ b/example/ios/AsyncStorageExample/Base.lproj/LaunchScreen.xib @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/AsyncStorageExample/Images.xcassets/AppIcon.appiconset/Contents.json b/example/ios/AsyncStorageExample/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..19882d56 --- /dev/null +++ b/example/ios/AsyncStorageExample/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,53 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/example/ios/AsyncStorageExample/Images.xcassets/Contents.json b/example/ios/AsyncStorageExample/Images.xcassets/Contents.json new file mode 100644 index 00000000..2d92bd53 --- /dev/null +++ b/example/ios/AsyncStorageExample/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/AsyncStorageExample/Info.plist b/example/ios/AsyncStorageExample/Info.plist new file mode 100644 index 00000000..1c36c0fe --- /dev/null +++ b/example/ios/AsyncStorageExample/Info.plist @@ -0,0 +1,60 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + AsyncStorageExample + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSLocationWhenInUseUsageDescription + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + NSLocationWhenInUseUsageDescription + + NSAppTransportSecurity + + + NSAllowsArbitraryLoads + + NSExceptionDomains + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + + + + diff --git a/example/ios/AsyncStorageExample/main.m b/example/ios/AsyncStorageExample/main.m new file mode 100644 index 00000000..c316cf81 --- /dev/null +++ b/example/ios/AsyncStorageExample/main.m @@ -0,0 +1,16 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/example/src/App.js b/example/src/App.js new file mode 100644 index 00000000..946958e0 --- /dev/null +++ b/example/src/App.js @@ -0,0 +1,122 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +import React, {Component} from 'react'; +import { + StyleSheet, + SafeAreaView, + Text, + TouchableOpacity, + View, +} from 'react-native'; + +import SimpleGetSet from './examples/GetSet'; +import ClearStorage from './examples/ClearSingle'; + +const EXAMPLES = [ + { + title: 'Simple Get/Set value', + description: 'Store and retrieve persisted data', + render() { + return ; + }, + }, + { + title: 'Clear', + description: 'Clear persisting data storage', + render() { + return ; + }, + }, +]; + +type Props = {}; +type State = {restarting: boolean}; + +export default class App extends Component { + state = { + restarting: false, + }; + + componentDidUpdate() { + if (this.state.restarting) { + this.setState({restarting: false}); + } + } + + _simulateRestart = () => { + this.setState({restarting: true}); + }; + + render() { + const {restarting} = this.state; + return ( + + + Simulate Restart + + {restarting + ? null + : EXAMPLES.map(example => { + return ( + + {example.title} + + {example.description} + + + {example.render(this._simulateRestart)} + + + ); + })} + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#F5FCFF', + padding: 14, + }, + exampleContainer: { + padding: 16, + backgroundColor: '#FFF', + borderColor: '#EEE', + borderTopWidth: 1, + borderBottomWidth: 1, + }, + exampleTitle: { + fontSize: 18, + }, + exampleDescription: { + color: '#333333', + marginBottom: 16, + }, + exampleInnerContainer: { + borderColor: '#EEE', + borderTopWidth: 1, + paddingTop: 16, + }, + restartButton: { + padding: 15, + fontSize: 16, + borderRadius: 5, + backgroundColor: '#F3F3F3', + alignItems: 'center', + justifyContent: 'center', + alignSelf: 'flex-end', + }, +}); diff --git a/example/src/examples/ClearSingle.js b/example/src/examples/ClearSingle.js new file mode 100644 index 00000000..9c13c6e8 --- /dev/null +++ b/example/src/examples/ClearSingle.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +import React, {Component} from 'react'; +import {Text, View, Button} from 'react-native'; + +import AsyncStorage from '@react-native-community/asyns-storage'; + +import {STORAGE_KEY} from './GetSet'; + +type Props = { + resetFunction: () => void, +}; +type State = { + storedNumber: number, + needRestart: boolean, +}; +export default class Clear extends Component { + state = { + needRestart: false, + }; + + cleanItem = async () => { + await AsyncStorage.removeItem(STORAGE_KEY); + + this.setState({needRestart: true}); + }; + + render() { + const {needRestart} = this.state; + return ( + +