@react-native-async-storage/async-storage
Version:
Asynchronous, persistent, key-value storage system for React Native.
367 lines (333 loc) • 11.7 kB
text/typescript
/**
* 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 {
checkValidArgs,
checkValidInput,
convertError,
convertErrors,
} from './helpers';
import RCTAsyncStorage from './RCTAsyncStorage';
import type {
AsyncStorageStatic,
ErrorLike,
KeyValuePair,
MultiRequest,
} from './types';
if (!RCTAsyncStorage) {
throw new Error(`[@RNC/AsyncStorage]: NativeModule: AsyncStorage is null.
To fix this issue try these steps:
• Rebuild and restart the app.
• Run the packager with \`--reset-cache\` flag.
• If you are using CocoaPods on iOS, run \`pod install\` in the \`ios\` directory and then rebuild and re-run the app.
• If this happens while testing with Jest, check out docs how to integrate AsyncStorage with it: https://react-native-async-storage.github.io/async-storage/docs/advanced/jest
If none of these fix the issue, please open an issue on the Github repository: https://github.com/react-native-async-storage/async-storage/issues
`);
}
/**
* `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 https://react-native-async-storage.github.io/async-storage/docs/api
*/
const AsyncStorage = ((): AsyncStorageStatic => {
let _getRequests: MultiRequest[] = [];
let _getKeys: string[] = [];
let _immediate: ReturnType<typeof setImmediate> | null = null;
return {
/**
* Fetches an item for a `key` and invokes a callback upon completion.
*
* See https://react-native-async-storage.github.io/async-storage/docs/api#getitem
*/
getItem: (key, callback) => {
return new Promise((resolve, reject) => {
checkValidInput(key);
RCTAsyncStorage.multiGet(
[key],
(errors?: ErrorLike[], result?: string[][]) => {
// Unpack result to get value from [[key,value]]
const value = result?.[0]?.[1] ? result[0][1] : null;
const errs = convertErrors(errors);
callback?.(errs?.[0], value);
if (errs) {
reject(errs[0]);
} else {
resolve(value);
}
}
);
});
},
/**
* Sets the value for a `key` and invokes a callback upon completion.
*
* See https://react-native-async-storage.github.io/async-storage/docs/api#setitem
*/
setItem: (key, value, callback) => {
return new Promise((resolve, reject) => {
checkValidInput(key, value);
RCTAsyncStorage.multiSet([[key, value]], (errors?: ErrorLike[]) => {
const errs = convertErrors(errors);
callback?.(errs?.[0]);
if (errs) {
reject(errs[0]);
} else {
resolve();
}
});
});
},
/**
* Removes an item for a `key` and invokes a callback upon completion.
*
* See https://react-native-async-storage.github.io/async-storage/docs/api#removeitem
*/
removeItem: (key, callback) => {
return new Promise((resolve, reject) => {
checkValidInput(key);
RCTAsyncStorage.multiRemove([key], (errors?: ErrorLike[]) => {
const errs = convertErrors(errors);
callback?.(errs?.[0]);
if (errs) {
reject(errs[0]);
} else {
resolve();
}
});
});
},
/**
* Merges an existing `key` value with an input value, assuming both values
* are stringified JSON.
*
* **NOTE:** This is not supported by all native implementations.
*
* See https://react-native-async-storage.github.io/async-storage/docs/api#mergeitem
*/
mergeItem: (key, value, callback) => {
return new Promise((resolve, reject) => {
checkValidInput(key, value);
RCTAsyncStorage.multiMerge([[key, value]], (errors?: ErrorLike[]) => {
const errs = convertErrors(errors);
callback?.(errs?.[0]);
if (errs) {
reject(errs[0]);
} else {
resolve();
}
});
});
},
/**
* 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 https://react-native-async-storage.github.io/async-storage/docs/api#clear
*/
clear: (callback) => {
return new Promise((resolve, reject) => {
RCTAsyncStorage.clear((error?: ErrorLike) => {
const err = convertError(error);
callback?.(err);
if (err) {
reject(err);
} else {
resolve();
}
});
});
},
/**
* Gets *all* keys known to your app; for all callers, libraries, etc.
*
* See https://react-native-async-storage.github.io/async-storage/docs/api#getallkeys
*/
getAllKeys: (callback) => {
return new Promise((resolve, reject) => {
RCTAsyncStorage.getAllKeys((error?: ErrorLike, keys?: string[]) => {
const err = convertError(error);
callback?.(err, keys);
if (keys) {
resolve(keys);
} else {
reject(err);
}
});
});
},
/**
* 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.
*/
/**
* Flushes any pending requests using a single batch call to get the data.
*
* See https://react-native-async-storage.github.io/async-storage/docs/api#flushgetrequests
* */
flushGetRequests: () => {
const getRequests = _getRequests;
const getKeys = _getKeys;
_getRequests = [];
_getKeys = [];
RCTAsyncStorage.multiGet(
getKeys,
(errors?: ErrorLike[], result?: string[][]) => {
// 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
const map: Record<string, string> = {};
result?.forEach(([key, value]) => {
map[key] = value;
return value;
});
const reqLength = getRequests.length;
/**
* As mentioned few lines above, this method could be called with the array of potential error,
* in case of anything goes wrong. The problem is, if any of the batched calls fails
* the rest of them would fail too, but the error would be consumed by just one. The rest
* would simply return `undefined` as their result, rendering false negatives.
*
* In order to avoid this situation, in case of any call failing,
* the rest of them will be rejected as well (with the same error).
*/
const errorList = convertErrors(errors);
const error = errorList?.length ? errorList[0] : null;
for (let i = 0; i < reqLength; i++) {
const request = getRequests[i];
if (error) {
request.callback?.(errorList);
request.reject?.(error);
continue;
}
const requestResult = request.keys.map<KeyValuePair>((key) => [
key,
map[key],
]);
request.callback?.(null, requestResult);
request.resolve?.(requestResult);
}
}
);
},
/**
* 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 https://react-native-async-storage.github.io/async-storage/docs/api#multiget
*/
multiGet: (keys, callback) => {
if (!_immediate) {
_immediate = setImmediate(() => {
_immediate = null;
AsyncStorage.flushGetRequests();
});
}
const getRequest: MultiRequest = {
keys: keys,
callback: callback,
// do we need this?
keyIndex: _getKeys.length,
resolve: null as any,
reject: null as any,
};
const promiseResult = new Promise<readonly KeyValuePair[]>(
(resolve, reject) => {
getRequest.resolve = resolve;
getRequest.reject = reject;
}
);
_getRequests.push(getRequest);
// avoid fetching duplicates
keys.forEach((key) => {
if (_getKeys.indexOf(key) === -1) {
_getKeys.push(key);
}
});
return promiseResult;
},
/**
* 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.
*
* See https://react-native-async-storage.github.io/async-storage/docs/api#multiset
*/
multiSet: (keyValuePairs, callback) => {
checkValidArgs(keyValuePairs, callback);
return new Promise((resolve, reject) => {
keyValuePairs.forEach(([key, value]) => {
checkValidInput(key, value);
});
RCTAsyncStorage.multiSet(keyValuePairs, (errors?: ErrorLike[]) => {
const error = convertErrors(errors);
callback?.(error);
if (error) {
reject(error);
} else {
resolve();
}
});
});
},
/**
* Call this to batch the deletion of all keys in the `keys` array.
*
* See https://react-native-async-storage.github.io/async-storage/docs/api#multiremove
*/
multiRemove: (keys, callback) => {
return new Promise((resolve, reject) => {
keys.forEach((key) => checkValidInput(key));
RCTAsyncStorage.multiRemove(keys, (errors?: ErrorLike[]) => {
const error = convertErrors(errors);
callback?.(error);
if (error) {
reject(error);
} else {
resolve();
}
});
});
},
/**
* 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 https://react-native-async-storage.github.io/async-storage/docs/api#multimerge
*/
multiMerge: (keyValuePairs, callback) => {
return new Promise((resolve, reject) => {
RCTAsyncStorage.multiMerge(keyValuePairs, (errors?: ErrorLike[]) => {
const error = convertErrors(errors);
callback?.(error);
if (error) {
reject(error);
} else {
resolve();
}
});
});
},
};
})();
// Not all native implementations support merge.
if (!RCTAsyncStorage.multiMerge) {
delete AsyncStorage.mergeItem;
delete AsyncStorage.multiMerge;
}
export default AsyncStorage;