1/** 2 * Copyright (c) Facebook, Inc. and its affiliates. 3 * 4 * This source code is licensed under the MIT license found in the 5 * LICENSE file in the root directory of this source tree. 6 */ 7 8#import "RNCAsyncStorage.h" 9 10#import <CommonCrypto/CommonCryptor.h> 11#import <CommonCrypto/CommonDigest.h> 12 13#import <React/RCTConvert.h> 14#import <React/RCTLog.h> 15#import <React/RCTUtils.h> 16 17// NOTE(kudo): Use Expo storage directory for backward compatibility 18//static NSString *const RCTStorageDirectory = @"RCTAsyncLocalStorage_V1"; 19static NSString *const RCTStorageDirectory = @"RCTAsyncLocalStorage"; 20static NSString *const RCTOldStorageDirectory = @"RNCAsyncLocalStorage_V1"; 21static NSString *const RCTExpoStorageDirectory = @"RCTAsyncLocalStorage"; 22static NSString *const RCTManifestFileName = @"manifest.json"; 23static const NSUInteger RCTInlineValueThreshold = 1024; 24 25#pragma mark - Static helper functions 26 27static NSDictionary *RCTErrorForKey(NSString *key) 28{ 29 if (![key isKindOfClass:[NSString class]]) { 30 return RCTMakeAndLogError(@"Invalid key - must be a string. Key: ", key, @{@"key": key}); 31 } else if (key.length < 1) { 32 return RCTMakeAndLogError( 33 @"Invalid key - must be at least one character. Key: ", key, @{@"key": key}); 34 } else { 35 return nil; 36 } 37} 38 39static BOOL RCTAsyncStorageSetExcludedFromBackup(NSString *path, NSNumber *isExcluded) 40{ 41 NSFileManager *fileManager = [[NSFileManager alloc] init]; 42 43 BOOL isDir; 44 BOOL exists = [fileManager fileExistsAtPath:path isDirectory:&isDir]; 45 BOOL success = false; 46 47 if (isDir && exists) { 48 NSURL *pathUrl = [NSURL fileURLWithPath:path]; 49 NSError *error = nil; 50 success = [pathUrl setResourceValue:isExcluded 51 forKey:NSURLIsExcludedFromBackupKey 52 error:&error]; 53 54 if (!success) { 55 NSLog(@"Could not exclude AsyncStorage dir from backup %@", error); 56 } 57 } 58 return success; 59} 60 61static void RCTAppendError(NSDictionary *error, NSMutableArray<NSDictionary *> **errors) 62{ 63 if (error && errors) { 64 if (!*errors) { 65 *errors = [NSMutableArray new]; 66 } 67 [*errors addObject:error]; 68 } 69} 70 71static NSArray<NSDictionary *> *RCTMakeErrors(NSArray<id<NSObject>> *results) 72{ 73 NSMutableArray<NSDictionary *> *errors; 74 for (id object in results) { 75 if ([object isKindOfClass:[NSError class]]) { 76 NSError *error = (NSError *)object; 77 NSDictionary *keyError = RCTMakeError(error.localizedDescription, error, nil); 78 RCTAppendError(keyError, &errors); 79 } 80 } 81 return errors; 82} 83 84static NSString *RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut) 85{ 86 if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { 87 NSError *error; 88 NSStringEncoding encoding; 89 NSString *entryString = [NSString stringWithContentsOfFile:filePath 90 usedEncoding:&encoding 91 error:&error]; 92 NSDictionary *extraData = @{@"key": RCTNullIfNil(key)}; 93 94 if (error) { 95 if (errorOut) { 96 *errorOut = RCTMakeError(@"Failed to read storage file.", error, extraData); 97 } 98 return nil; 99 } 100 101 if (encoding != NSUTF8StringEncoding) { 102 if (errorOut) { 103 *errorOut = 104 RCTMakeError(@"Incorrect encoding of storage file: ", @(encoding), extraData); 105 } 106 return nil; 107 } 108 return entryString; 109 } 110 111 return nil; 112} 113 114// DO NOT USE 115// This is used internally to migrate data from the old file location to the new one. 116// Please use `RCTCreateStorageDirectoryPath` instead 117static NSString *RCTCreateStorageDirectoryPath_deprecated(NSString *storageDir) 118{ 119 NSString *storageDirectoryPath; 120#if TARGET_OS_TV 121 storageDirectoryPath = 122 NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; 123#else 124 storageDirectoryPath = 125 NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; 126#endif 127 storageDirectoryPath = [storageDirectoryPath stringByAppendingPathComponent:storageDir]; 128 return storageDirectoryPath; 129} 130 131static NSString *RCTCreateStorageDirectoryPath(NSString *storageDir) 132{ 133 NSString *storageDirectoryPath = @""; 134 135#if TARGET_OS_TV 136 storageDirectoryPath = 137 NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; 138#else 139 storageDirectoryPath = 140 NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) 141 .firstObject; 142 // We should use the "Application Support/[bundleID]" folder for persistent data storage that's 143 // hidden from users 144 storageDirectoryPath = [storageDirectoryPath 145 stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]]; 146#endif 147 148 // Per Apple's docs, all app content in Application Support must be within a subdirectory of the 149 // app's bundle identifier 150 storageDirectoryPath = [storageDirectoryPath stringByAppendingPathComponent:storageDir]; 151 152 return storageDirectoryPath; 153} 154 155static NSString *RCTCreateManifestFilePath(NSString *storageDirectory) 156{ 157 return [storageDirectory stringByAppendingPathComponent:RCTManifestFileName]; 158} 159 160// Only merges objects - all other types are just clobbered (including arrays) 161static BOOL RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) 162{ 163 BOOL modified = NO; 164 for (NSString *key in source) { 165 id sourceValue = source[key]; 166 id destinationValue = destination[key]; 167 if ([sourceValue isKindOfClass:[NSDictionary class]]) { 168 if ([destinationValue isKindOfClass:[NSDictionary class]]) { 169 if ([destinationValue classForCoder] != [NSMutableDictionary class]) { 170 destinationValue = [destinationValue mutableCopy]; 171 } 172 if (RCTMergeRecursive(destinationValue, sourceValue)) { 173 destination[key] = destinationValue; 174 modified = YES; 175 } 176 } else { 177 destination[key] = [sourceValue copy]; 178 modified = YES; 179 } 180 } else if (![source isEqual:destinationValue]) { 181 destination[key] = [sourceValue copy]; 182 modified = YES; 183 } 184 } 185 return modified; 186} 187 188static BOOL RCTHasCreatedStorageDirectory = NO; 189 190// NOTE(nikki93): We replace with scoped implementations of: 191// RCTGetStorageDirectory() 192// RCTGetManifestFilePath() 193// RCTGetMethodQueue() 194// RCTGetCache() 195// RCTDeleteStorageDirectory() 196 197#define RCTGetStorageDirectory() _storageDirectory 198#define RCTGetManifestFilePath() _manifestFilePath 199#define RCTGetMethodQueue() self.methodQueue 200#define RCTGetCache() self.cache 201 202static NSDictionary *RCTDeleteStorageDirectory(NSString *storageDirectory) 203{ 204 NSError *error; 205 [[NSFileManager defaultManager] removeItemAtPath:storageDirectory error:&error]; 206 return error ? RCTMakeError(@"Failed to delete storage directory.", error, nil) : nil; 207} 208#define RCTDeleteStorageDirectory() RCTDeleteStorageDirectory(_storageDirectory) 209 210static NSDate *RCTManifestModificationDate(NSString *manifestFilePath) 211{ 212 NSDictionary *attributes = 213 [[NSFileManager defaultManager] attributesOfItemAtPath:manifestFilePath error:nil]; 214 return [attributes fileModificationDate]; 215} 216 217/** 218 * Creates an NSException used during Storage Directory Migration. 219 */ 220static void RCTStorageDirectoryMigrationLogError(NSString *reason, NSError *error) 221{ 222 RCTLogWarn(@"%@: %@", reason, error ? error.description : @""); 223} 224 225static void RCTStorageDirectoryCleanupOld(NSString *oldDirectoryPath) 226{ 227 NSError *error; 228 if (![[NSFileManager defaultManager] removeItemAtPath:oldDirectoryPath error:&error]) { 229 RCTStorageDirectoryMigrationLogError( 230 @"Failed to remove old storage directory during migration", error); 231 } 232} 233 234static void _createStorageDirectory(NSString *storageDirectory, NSError **error) 235{ 236 [[NSFileManager defaultManager] createDirectoryAtPath:storageDirectory 237 withIntermediateDirectories:YES 238 attributes:nil 239 error:error]; 240} 241 242static void RCTStorageDirectoryMigrate(NSString *oldDirectoryPath, 243 NSString *newDirectoryPath, 244 BOOL shouldCleanupOldDirectory) 245{ 246 assert(false); 247} 248 249/** 250 * Determine which of RCTOldStorageDirectory or RCTExpoStorageDirectory needs to migrated. 251 * If both exist, we remove the least recently modified and return the most recently modified. 252 * Otherwise, this will return the path to whichever directory exists. 253 * If no directory exists, then return nil. 254 */ 255static NSString *RCTGetStoragePathForMigration() 256{ 257 BOOL isDir; 258 NSString *oldStoragePath = RCTCreateStorageDirectoryPath_deprecated(RCTOldStorageDirectory); 259 NSString *expoStoragePath = RCTCreateStorageDirectoryPath_deprecated(RCTExpoStorageDirectory); 260 NSFileManager *fileManager = [NSFileManager defaultManager]; 261 BOOL oldStorageDirectoryExists = 262 [fileManager fileExistsAtPath:oldStoragePath isDirectory:&isDir] && isDir; 263 BOOL expoStorageDirectoryExists = 264 [fileManager fileExistsAtPath:expoStoragePath isDirectory:&isDir] && isDir; 265 266 // Check if both the old storage directory and Expo storage directory exist 267 if (oldStorageDirectoryExists && expoStorageDirectoryExists) { 268 // If the old storage has been modified more recently than Expo storage, then clear Expo 269 // storage. Otherwise, clear the old storage. 270 if ([RCTManifestModificationDate(RCTCreateManifestFilePath(oldStoragePath)) 271 compare:RCTManifestModificationDate(RCTCreateManifestFilePath(expoStoragePath))] == 272 NSOrderedDescending) { 273 RCTStorageDirectoryCleanupOld(expoStoragePath); 274 return oldStoragePath; 275 } else { 276 RCTStorageDirectoryCleanupOld(oldStoragePath); 277 return expoStoragePath; 278 } 279 } else if (oldStorageDirectoryExists) { 280 return oldStoragePath; 281 } else if (expoStorageDirectoryExists) { 282 return expoStoragePath; 283 } else { 284 return nil; 285 } 286} 287 288/** 289 * This check is added to make sure that anyone coming from pre-1.2.2 does not lose cached data. 290 * Check that data is migrated from the old location to the new location 291 * fromStorageDirectory: the directory where the older data lives 292 * toStorageDirectory: the directory where the new data should live 293 * shouldCleanupOldDirectoryAndOverwriteNewDirectory: YES if we should delete the old directory's 294 * contents and overwrite the new directory's contents during the migration to the new directory 295 */ 296static void 297RCTStorageDirectoryMigrationCheck(NSString *fromStorageDirectory, 298 NSString *toStorageDirectory, 299 BOOL shouldCleanupOldDirectoryAndOverwriteNewDirectory) 300{ 301 NSError *error; 302 BOOL isDir; 303 NSFileManager *fileManager = [NSFileManager defaultManager]; 304 // If the old directory exists, it means we may need to migrate old data to the new directory 305 if ([fileManager fileExistsAtPath:fromStorageDirectory isDirectory:&isDir] && isDir) { 306 // Check if the new storage directory location already exists 307 if ([fileManager fileExistsAtPath:toStorageDirectory]) { 308 // If new storage location exists, check if the new storage has been modified sooner in 309 // which case we may want to cleanup the old location 310 if ([RCTManifestModificationDate(RCTCreateManifestFilePath(toStorageDirectory)) 311 compare:RCTManifestModificationDate( 312 RCTCreateManifestFilePath(fromStorageDirectory))] == 1) { 313 // If new location has been modified more recently, simply clean out old data 314 if (shouldCleanupOldDirectoryAndOverwriteNewDirectory) { 315 RCTStorageDirectoryCleanupOld(fromStorageDirectory); 316 } 317 } else if (shouldCleanupOldDirectoryAndOverwriteNewDirectory) { 318 // If old location has been modified more recently, remove new storage and migrate 319 if (![fileManager removeItemAtPath:toStorageDirectory error:&error]) { 320 RCTStorageDirectoryMigrationLogError( 321 @"Failed to remove new storage directory during migration", error); 322 } else { 323 RCTStorageDirectoryMigrate(fromStorageDirectory, 324 toStorageDirectory, 325 shouldCleanupOldDirectoryAndOverwriteNewDirectory); 326 } 327 } 328 } else { 329 // If new storage location doesn't exist, migrate data 330 RCTStorageDirectoryMigrate(fromStorageDirectory, 331 toStorageDirectory, 332 shouldCleanupOldDirectoryAndOverwriteNewDirectory); 333 } 334 } 335} 336 337#pragma mark - RNCAsyncStorage 338 339@interface RNCAsyncStorage () 340 341@property (nonatomic, copy) NSString *storageDirectory; 342@property (nonatomic, copy) NSString *manifestFilePath; 343 344@end 345 346@implementation RNCAsyncStorage { 347 BOOL _haveSetup; 348 // The manifest is a dictionary of all keys with small values inlined. Null values indicate 349 // values that are stored in separate files (as opposed to nil values which don't exist). The 350 // manifest is read off disk at startup, and written to disk after all mutations. 351 NSMutableDictionary<NSString *, NSString *> *_manifest; 352 NSCache *_cache; 353 dispatch_once_t _cacheOnceToken; 354} 355 356// NOTE(nikki93): Prevents the module from being auto-initialized and allows us to pass our own `storageDirectory` 357+ (NSString *)moduleName { return @"RCTAsyncLocalStorage"; } 358- (instancetype)initWithStorageDirectory:(NSString *)storageDirectory 359{ 360 if ((self = [super init])) { 361 _storageDirectory = storageDirectory; 362 _manifestFilePath = [RCTGetStorageDirectory() stringByAppendingPathComponent:RCTManifestFileName]; 363 } 364 return self; 365} 366 367// NOTE(nikki93): Use the default `methodQueue` since instances have different storage directories 368@synthesize methodQueue = _methodQueue; 369 370- (NSCache *)cache 371{ 372 dispatch_once(&_cacheOnceToken, ^{ 373 _cache = [NSCache new]; 374 _cache.totalCostLimit = 2 * 1024 * 1024; // 2MB 375 376 // Clear cache in the event of a memory warning 377 [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(__unused NSNotification *note) { 378 [_cache removeAllObjects]; 379 }]; 380 }); 381 return _cache; 382} 383 384+ (BOOL)requiresMainQueueSetup 385{ 386 return NO; 387} 388 389- (instancetype)init 390{ 391 assert(false); 392 if (!(self = [super init])) { 393 return nil; 394 } 395 396 // Get the path to any old storage directory that needs to be migrated. If multiple exist, 397 // the oldest are removed and the most recently modified is returned. 398 NSString *oldStoragePath = RCTGetStoragePathForMigration(); 399 if (oldStoragePath != nil) { 400 // Migrate our deprecated path "Documents/.../RNCAsyncLocalStorage_V1" or 401 // "Documents/.../RCTAsyncLocalStorage" to "Documents/.../RCTAsyncLocalStorage_V1" 402 RCTStorageDirectoryMigrationCheck( 403 oldStoragePath, RCTCreateStorageDirectoryPath_deprecated(RCTStorageDirectory), YES); 404 } 405 406 // Migrate what's in "Documents/.../RCTAsyncLocalStorage_V1" to 407 // "Application Support/[bundleID]/RCTAsyncLocalStorage_V1" 408 RCTStorageDirectoryMigrationCheck(RCTCreateStorageDirectoryPath_deprecated(RCTStorageDirectory), 409 RCTCreateStorageDirectoryPath(RCTStorageDirectory), 410 NO); 411 412 return self; 413} 414 415- (void)clearAllData 416{ 417 dispatch_async(RCTGetMethodQueue(), ^{ 418 [self->_manifest removeAllObjects]; 419 [RCTGetCache() removeAllObjects]; 420 RCTDeleteStorageDirectory(); 421 }); 422} 423 424- (void)invalidate 425{ 426 if (_clearOnInvalidate) { 427 [RCTGetCache() removeAllObjects]; 428 RCTDeleteStorageDirectory(); 429 } 430 _clearOnInvalidate = NO; 431 [_manifest removeAllObjects]; 432 _haveSetup = NO; 433} 434 435- (BOOL)isValid 436{ 437 return _haveSetup; 438} 439 440- (void)dealloc 441{ 442 [self invalidate]; 443} 444 445- (NSString *)_filePathForKey:(NSString *)key 446{ 447 NSString *safeFileName = RCTMD5Hash(key); 448 return [RCTGetStorageDirectory() stringByAppendingPathComponent:safeFileName]; 449} 450 451- (NSDictionary *)_ensureSetup 452{ 453 RCTAssertThread(RCTGetMethodQueue(), @"Must be executed on storage thread"); 454 455#if TARGET_OS_TV 456 RCTLogWarn( 457 @"Persistent storage is not supported on tvOS, your data may be removed at any point."); 458#endif 459 460 NSError *error = nil; 461 // NOTE(nikki93): `withIntermediateDirectories:YES` makes this idempotent 462 [[NSFileManager defaultManager] createDirectoryAtPath:RCTGetStorageDirectory() 463 withIntermediateDirectories:YES 464 attributes:nil 465 error:&error]; 466 if (error) { 467 return RCTMakeError(@"Failed to create storage directory.", error, nil); 468 } 469 470 if (!_haveSetup) { 471 // iCloud backup exclusion 472 NSNumber *isExcludedFromBackup = 473 [[NSBundle mainBundle] objectForInfoDictionaryKey:@"RCTAsyncStorageExcludeFromBackup"]; 474 if (isExcludedFromBackup == nil) { 475 // by default, we want to exclude AsyncStorage data from backup 476 isExcludedFromBackup = @YES; 477 } 478 // NOTE(kudo): We don't enable iCloud backup for Expo Go 479 // RCTAsyncStorageSetExcludedFromBackup(RCTCreateStorageDirectoryPath(RCTStorageDirectory), 480 // isExcludedFromBackup); 481 482 NSDictionary *errorOut = nil; 483 // NOTE(kudo): Keep data in Documents rather than Application Support for backward compatibility 484 // NSString *serialized = RCTReadFile(RCTCreateStorageDirectoryPath(RCTGetManifestFilePath()) 485 NSString *serialized = RCTReadFile(RCTGetManifestFilePath(), 486 RCTManifestFileName, 487 &errorOut); 488 if (!serialized) { 489 if (errorOut) { 490 // We cannot simply create a new manifest in case the file does exist but we have no 491 // access to it. This can happen when data protection is enabled for the app and we 492 // are trying to read the manifest while the device is locked. (The app can be 493 // started by the system even if the device is locked due to e.g. a geofence event.) 494 RCTLogError( 495 @"Could not open the existing manifest, perhaps data protection is " 496 @"enabled?\n\n%@", 497 errorOut); 498 return errorOut; 499 } else { 500 // We can get nil without errors only when the file does not exist. 501 RCTLogTrace(@"Manifest does not exist - creating a new one.\n\n%@", errorOut); 502 _manifest = [NSMutableDictionary new]; 503 } 504 } else { 505 _manifest = RCTJSONParseMutable(serialized, &error); 506 if (!_manifest) { 507 RCTLogError(@"Failed to parse manifest - creating a new one.\n\n%@", error); 508 _manifest = [NSMutableDictionary new]; 509 } 510 } 511 _haveSetup = YES; 512 } 513 514 return nil; 515} 516 517- (NSDictionary *)_writeManifest:(NSMutableArray<NSDictionary *> **)errors 518{ 519 NSError *error; 520 NSString *serialized = RCTJSONStringify(_manifest, &error); 521 // NOTE(kudo): Keep data in Documents rather than Application Support for backward compatibility 522 // [serialized writeToFile:RCTCreateStorageDirectoryPath(RCTGetManifestFilePath()) 523 [serialized writeToFile:RCTGetManifestFilePath() 524 atomically:YES 525 encoding:NSUTF8StringEncoding 526 error:&error]; 527 NSDictionary *errorOut; 528 if (error) { 529 errorOut = RCTMakeError(@"Failed to write manifest file.", error, nil); 530 RCTAppendError(errorOut, errors); 531 } 532 return errorOut; 533} 534 535- (NSDictionary *)_appendItemForKey:(NSString *)key 536 toArray:(NSMutableArray<NSArray<NSString *> *> *)result 537{ 538 NSDictionary *errorOut = RCTErrorForKey(key); 539 if (errorOut) { 540 return errorOut; 541 } 542 NSString *value = [self _getValueForKey:key errorOut:&errorOut]; 543 [result addObject:@[key, RCTNullIfNil(value)]]; // Insert null if missing or failure. 544 return errorOut; 545} 546 547- (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut 548{ 549 NSString *value = 550 _manifest[key]; // nil means missing, null means there may be a data file, else: NSString 551 if (value == (id)kCFNull) { 552 value = [RCTGetCache() objectForKey:key]; 553 if (!value) { 554 NSString *filePath = [self _filePathForKey:key]; 555 value = RCTReadFile(filePath, key, errorOut); 556 if (value) { 557 [RCTGetCache() setObject:value forKey:key cost:value.length]; 558 } else { 559 // file does not exist after all, so remove from manifest (no need to save 560 // manifest immediately though, as cost of checking again next time is negligible) 561 [_manifest removeObjectForKey:key]; 562 } 563 } 564 } 565 return value; 566} 567 568- (NSDictionary *)_writeEntry:(NSArray<NSString *> *)entry changedManifest:(BOOL *)changedManifest 569{ 570 if (entry.count != 2) { 571 return RCTMakeAndLogError( 572 @"Entries must be arrays of the form [key: string, value: string], got: ", entry, nil); 573 } 574 NSString *key = entry[0]; 575 NSDictionary *errorOut = RCTErrorForKey(key); 576 if (errorOut) { 577 return errorOut; 578 } 579 NSString *value = entry[1]; 580 NSString *filePath = [self _filePathForKey:key]; 581 NSError *error; 582 if (value.length <= RCTInlineValueThreshold) { 583 if (_manifest[key] == (id)kCFNull) { 584 // If the value already existed but wasn't inlined, remove the old file. 585 [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; 586 [RCTGetCache() removeObjectForKey:key]; 587 } 588 *changedManifest = YES; 589 _manifest[key] = value; 590 return nil; 591 } 592 [value writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error]; 593 [RCTGetCache() setObject:value forKey:key cost:value.length]; 594 if (error) { 595 errorOut = RCTMakeError(@"Failed to write value.", error, @{@"key": key}); 596 } else if (_manifest[key] != (id)kCFNull) { 597 *changedManifest = YES; 598 _manifest[key] = (id)kCFNull; 599 } 600 return errorOut; 601} 602 603- (void)_multiGet:(NSArray<NSString *> *)keys 604 callback:(RCTResponseSenderBlock)callback 605 getter:(NSString * (^)(NSUInteger i, NSString *key, NSDictionary **errorOut))getValue 606{ 607 NSMutableArray<NSDictionary *> *errors; 608 NSMutableArray<NSArray<NSString *> *> *result = [NSMutableArray arrayWithCapacity:keys.count]; 609 for (NSUInteger i = 0; i < keys.count; ++i) { 610 NSString *key = keys[i]; 611 id keyError; 612 id value = getValue(i, key, &keyError); 613 [result addObject:@[key, RCTNullIfNil(value)]]; 614 RCTAppendError(keyError, &errors); 615 } 616 callback(@[RCTNullIfNil(errors), result]); 617} 618 619- (BOOL)_passthroughDelegate 620{ 621 return 622 [self.delegate respondsToSelector:@selector(isPassthrough)] && self.delegate.isPassthrough; 623} 624 625#pragma mark - Exported JS Functions 626 627// clang-format off 628RCT_EXPORT_METHOD(multiGet:(NSArray<NSString *> *)keys 629 callback:(RCTResponseSenderBlock)callback) 630// clang-format on 631{ 632 if (self.delegate != nil) { 633 [self.delegate 634 valuesForKeys:keys 635 completion:^(NSArray<id<NSObject>> *valuesOrErrors) { 636 [self _multiGet:keys 637 callback:callback 638 getter:^NSString *(NSUInteger i, NSString *key, NSDictionary **errorOut) { 639 id valueOrError = valuesOrErrors[i]; 640 if ([valueOrError isKindOfClass:[NSError class]]) { 641 NSError *error = (NSError *)valueOrError; 642 NSDictionary *extraData = @{@"key": RCTNullIfNil(key)}; 643 *errorOut = 644 RCTMakeError(error.localizedDescription, error, extraData); 645 return nil; 646 } else { 647 return [valueOrError isKindOfClass:[NSString class]] 648 ? (NSString *)valueOrError 649 : nil; 650 } 651 }]; 652 }]; 653 654 if (![self _passthroughDelegate]) { 655 return; 656 } 657 } 658 659 NSDictionary *errorOut = [self _ensureSetup]; 660 if (errorOut) { 661 callback(@[@[errorOut], (id)kCFNull]); 662 return; 663 } 664 [self _multiGet:keys 665 callback:callback 666 getter:^(NSUInteger i, NSString *key, NSDictionary **errorOut) { 667 return [self _getValueForKey:key errorOut:errorOut]; 668 }]; 669} 670 671// clang-format off 672RCT_EXPORT_METHOD(multiSet:(NSArray<NSArray<NSString *> *> *)kvPairs 673 callback:(RCTResponseSenderBlock)callback) 674// clang-format on 675{ 676 if (self.delegate != nil) { 677 NSMutableArray<NSString *> *keys = [NSMutableArray arrayWithCapacity:kvPairs.count]; 678 NSMutableArray<NSString *> *values = [NSMutableArray arrayWithCapacity:kvPairs.count]; 679 for (NSArray<NSString *> *entry in kvPairs) { 680 [keys addObject:entry[0]]; 681 [values addObject:entry[1]]; 682 } 683 [self.delegate setValues:values 684 forKeys:keys 685 completion:^(NSArray<id<NSObject>> *results) { 686 NSArray<NSDictionary *> *errors = RCTMakeErrors(results); 687 callback(@[RCTNullIfNil(errors)]); 688 }]; 689 690 if (![self _passthroughDelegate]) { 691 return; 692 } 693 } 694 695 NSDictionary *errorOut = [self _ensureSetup]; 696 if (errorOut) { 697 callback(@[@[errorOut]]); 698 return; 699 } 700 BOOL changedManifest = NO; 701 NSMutableArray<NSDictionary *> *errors; 702 for (NSArray<NSString *> *entry in kvPairs) { 703 NSDictionary *keyError = [self _writeEntry:entry changedManifest:&changedManifest]; 704 RCTAppendError(keyError, &errors); 705 } 706 if (changedManifest) { 707 [self _writeManifest:&errors]; 708 } 709 callback(@[RCTNullIfNil(errors)]); 710} 711 712// clang-format off 713RCT_EXPORT_METHOD(multiMerge:(NSArray<NSArray<NSString *> *> *)kvPairs 714 callback:(RCTResponseSenderBlock)callback) 715// clang-format on 716{ 717 if (self.delegate != nil) { 718 NSMutableArray<NSString *> *keys = [NSMutableArray arrayWithCapacity:kvPairs.count]; 719 NSMutableArray<NSString *> *values = [NSMutableArray arrayWithCapacity:kvPairs.count]; 720 for (NSArray<NSString *> *entry in kvPairs) { 721 [keys addObject:entry[0]]; 722 [values addObject:entry[1]]; 723 } 724 [self.delegate mergeValues:values 725 forKeys:keys 726 completion:^(NSArray<id<NSObject>> *results) { 727 NSArray<NSDictionary *> *errors = RCTMakeErrors(results); 728 callback(@[RCTNullIfNil(errors)]); 729 }]; 730 731 if (![self _passthroughDelegate]) { 732 return; 733 } 734 } 735 736 NSDictionary *errorOut = [self _ensureSetup]; 737 if (errorOut) { 738 callback(@[@[errorOut]]); 739 return; 740 } 741 BOOL changedManifest = NO; 742 NSMutableArray<NSDictionary *> *errors; 743 for (__strong NSArray<NSString *> *entry in kvPairs) { 744 NSDictionary *keyError; 745 NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError]; 746 if (!keyError) { 747 if (value) { 748 NSError *jsonError; 749 NSMutableDictionary *mergedVal = RCTJSONParseMutable(value, &jsonError); 750 if (RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &jsonError))) { 751 entry = @[entry[0], RCTNullIfNil(RCTJSONStringify(mergedVal, NULL))]; 752 } 753 if (jsonError) { 754 keyError = RCTJSErrorFromNSError(jsonError); 755 } 756 } 757 if (!keyError) { 758 keyError = [self _writeEntry:entry changedManifest:&changedManifest]; 759 } 760 } 761 RCTAppendError(keyError, &errors); 762 } 763 if (changedManifest) { 764 [self _writeManifest:&errors]; 765 } 766 callback(@[RCTNullIfNil(errors)]); 767} 768 769// clang-format off 770RCT_EXPORT_METHOD(multiRemove:(NSArray<NSString *> *)keys 771 callback:(RCTResponseSenderBlock)callback) 772// clang-format on 773{ 774 if (self.delegate != nil) { 775 [self.delegate removeValuesForKeys:keys 776 completion:^(NSArray<id<NSObject>> *results) { 777 NSArray<NSDictionary *> *errors = RCTMakeErrors(results); 778 callback(@[RCTNullIfNil(errors)]); 779 }]; 780 781 if (![self _passthroughDelegate]) { 782 return; 783 } 784 } 785 786 NSDictionary *errorOut = [self _ensureSetup]; 787 if (errorOut) { 788 callback(@[@[errorOut]]); 789 return; 790 } 791 NSMutableArray<NSDictionary *> *errors; 792 BOOL changedManifest = NO; 793 for (NSString *key in keys) { 794 NSDictionary *keyError = RCTErrorForKey(key); 795 if (!keyError) { 796 if (_manifest[key] == (id)kCFNull) { 797 NSString *filePath = [self _filePathForKey:key]; 798 [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; 799 [RCTGetCache() removeObjectForKey:key]; 800 } 801 if (_manifest[key]) { 802 changedManifest = YES; 803 [_manifest removeObjectForKey:key]; 804 } 805 } 806 RCTAppendError(keyError, &errors); 807 } 808 if (changedManifest) { 809 [self _writeManifest:&errors]; 810 } 811 callback(@[RCTNullIfNil(errors)]); 812} 813 814// clang-format off 815RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) 816// clang-format on 817{ 818 if (self.delegate != nil) { 819 [self.delegate removeAllValues:^(NSError *error) { 820 NSDictionary *result = nil; 821 if (error != nil) { 822 result = RCTMakeError(error.localizedDescription, error, nil); 823 } 824 callback(@[RCTNullIfNil(result)]); 825 }]; 826 return; 827 } 828 829 [_manifest removeAllObjects]; 830 [RCTGetCache() removeAllObjects]; 831 NSDictionary *error = RCTDeleteStorageDirectory(); 832 callback(@[RCTNullIfNil(error)]); 833} 834 835// clang-format off 836RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback) 837// clang-format on 838{ 839 if (self.delegate != nil) { 840 [self.delegate allKeys:^(NSArray<id<NSObject>> *keys) { 841 callback(@[(id)kCFNull, keys]); 842 }]; 843 844 if (![self _passthroughDelegate]) { 845 return; 846 } 847 } 848 849 NSDictionary *errorOut = [self _ensureSetup]; 850 if (errorOut) { 851 callback(@[errorOut, (id)kCFNull]); 852 } else { 853 callback(@[(id)kCFNull, _manifest.allKeys]); 854 } 855} 856 857@end 858