AsyncStorage.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  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. * @format
  8. * @noflow
  9. * @flow-weak
  10. * @jsdoc
  11. */
  12. 'use strict';
  13. import NativeAsyncStorage from './NativeAsyncStorage';
  14. import invariant from 'invariant';
  15. // Use SQLite if available, otherwise file storage.
  16. const RCTAsyncStorage = NativeAsyncStorage;
  17. /**
  18. * `AsyncStorage` is a simple, unencrypted, asynchronous, persistent, key-value
  19. * storage system that is global to the app. It should be used instead of
  20. * LocalStorage.
  21. *
  22. * See https://reactnative.dev/docs/asyncstorage.html
  23. */
  24. const AsyncStorage = {
  25. _getRequests: ([]: Array<any>),
  26. _getKeys: ([]: Array<string>),
  27. _immediate: (null: ?number),
  28. /**
  29. * Fetches an item for a `key` and invokes a callback upon completion.
  30. *
  31. * See https://reactnative.dev/docs/asyncstorage.html#getitem
  32. */
  33. getItem: function(
  34. key: string,
  35. callback?: ?(error: ?Error, result: ?string) => void,
  36. ): Promise {
  37. invariant(RCTAsyncStorage, 'RCTAsyncStorage not available');
  38. return new Promise((resolve, reject) => {
  39. RCTAsyncStorage.multiGet([key], function(errors, result) {
  40. // Unpack result to get value from [[key,value]]
  41. const value = result && result[0] && result[0][1] ? result[0][1] : null;
  42. const errs = convertErrors(errors);
  43. callback && callback(errs && errs[0], value);
  44. if (errs) {
  45. reject(errs[0]);
  46. } else {
  47. resolve(value);
  48. }
  49. });
  50. });
  51. },
  52. /**
  53. * Sets the value for a `key` and invokes a callback upon completion.
  54. *
  55. * See https://reactnative.dev/docs/asyncstorage.html#setitem
  56. */
  57. setItem: function(
  58. key: string,
  59. value: string,
  60. callback?: ?(error: ?Error) => void,
  61. ): Promise {
  62. invariant(RCTAsyncStorage, 'RCTAsyncStorage not available');
  63. return new Promise((resolve, reject) => {
  64. RCTAsyncStorage.multiSet([[key, value]], function(errors) {
  65. const errs = convertErrors(errors);
  66. callback && callback(errs && errs[0]);
  67. if (errs) {
  68. reject(errs[0]);
  69. } else {
  70. resolve(null);
  71. }
  72. });
  73. });
  74. },
  75. /**
  76. * Removes an item for a `key` and invokes a callback upon completion.
  77. *
  78. * See https://reactnative.dev/docs/asyncstorage.html#removeitem
  79. */
  80. removeItem: function(
  81. key: string,
  82. callback?: ?(error: ?Error) => void,
  83. ): Promise {
  84. invariant(RCTAsyncStorage, 'RCTAsyncStorage not available');
  85. return new Promise((resolve, reject) => {
  86. RCTAsyncStorage.multiRemove([key], function(errors) {
  87. const errs = convertErrors(errors);
  88. callback && callback(errs && errs[0]);
  89. if (errs) {
  90. reject(errs[0]);
  91. } else {
  92. resolve(null);
  93. }
  94. });
  95. });
  96. },
  97. /**
  98. * Merges an existing `key` value with an input value, assuming both values
  99. * are stringified JSON.
  100. *
  101. * **NOTE:** This is not supported by all native implementations.
  102. *
  103. * See https://reactnative.dev/docs/asyncstorage.html#mergeitem
  104. */
  105. mergeItem: function(
  106. key: string,
  107. value: string,
  108. callback?: ?(error: ?Error) => void,
  109. ): Promise {
  110. invariant(RCTAsyncStorage, 'RCTAsyncStorage not available');
  111. return new Promise((resolve, reject) => {
  112. RCTAsyncStorage.multiMerge([[key, value]], function(errors) {
  113. const errs = convertErrors(errors);
  114. callback && callback(errs && errs[0]);
  115. if (errs) {
  116. reject(errs[0]);
  117. } else {
  118. resolve(null);
  119. }
  120. });
  121. });
  122. },
  123. /**
  124. * Erases *all* `AsyncStorage` for all clients, libraries, etc. You probably
  125. * don't want to call this; use `removeItem` or `multiRemove` to clear only
  126. * your app's keys.
  127. *
  128. * See https://reactnative.dev/docs/asyncstorage.html#clear
  129. */
  130. clear: function(callback?: ?(error: ?Error) => void): Promise {
  131. invariant(RCTAsyncStorage, 'RCTAsyncStorage not available');
  132. return new Promise((resolve, reject) => {
  133. RCTAsyncStorage.clear(function(error) {
  134. callback && callback(convertError(error));
  135. if (error && convertError(error)) {
  136. reject(convertError(error));
  137. } else {
  138. resolve(null);
  139. }
  140. });
  141. });
  142. },
  143. /**
  144. * Gets *all* keys known to your app; for all callers, libraries, etc.
  145. *
  146. * See https://reactnative.dev/docs/asyncstorage.html#getallkeys
  147. */
  148. getAllKeys: function(
  149. callback?: ?(error: ?Error, keys: ?Array<string>) => void,
  150. ): Promise {
  151. invariant(RCTAsyncStorage, 'RCTAsyncStorage not available');
  152. return new Promise((resolve, reject) => {
  153. RCTAsyncStorage.getAllKeys(function(error, keys) {
  154. callback && callback(convertError(error), keys);
  155. if (error) {
  156. reject(convertError(error));
  157. } else {
  158. resolve(keys);
  159. }
  160. });
  161. });
  162. },
  163. /**
  164. * The following batched functions are useful for executing a lot of
  165. * operations at once, allowing for native optimizations and provide the
  166. * convenience of a single callback after all operations are complete.
  167. *
  168. * These functions return arrays of errors, potentially one for every key.
  169. * For key-specific errors, the Error object will have a key property to
  170. * indicate which key caused the error.
  171. */
  172. /**
  173. * Flushes any pending requests using a single batch call to get the data.
  174. *
  175. * See https://reactnative.dev/docs/asyncstorage.html#flushgetrequests
  176. * */
  177. flushGetRequests: function(): void {
  178. const getRequests = this._getRequests;
  179. const getKeys = this._getKeys;
  180. this._getRequests = [];
  181. this._getKeys = [];
  182. invariant(RCTAsyncStorage, 'RCTAsyncStorage not available');
  183. RCTAsyncStorage.multiGet(getKeys, function(errors, result) {
  184. // Even though the runtime complexity of this is theoretically worse vs if we used a map,
  185. // it's much, much faster in practice for the data sets we deal with (we avoid
  186. // allocating result pair arrays). This was heavily benchmarked.
  187. //
  188. // Is there a way to avoid using the map but fix the bug in this breaking test?
  189. // https://github.com/facebook/react-native/commit/8dd8ad76579d7feef34c014d387bf02065692264
  190. const map = {};
  191. result &&
  192. result.forEach(([key, value]) => {
  193. map[key] = value;
  194. return value;
  195. });
  196. const reqLength = getRequests.length;
  197. for (let i = 0; i < reqLength; i++) {
  198. const request = getRequests[i];
  199. const requestKeys = request.keys;
  200. const requestResult = requestKeys.map(key => [key, map[key]]);
  201. request.callback && request.callback(null, requestResult);
  202. request.resolve && request.resolve(requestResult);
  203. }
  204. });
  205. },
  206. /**
  207. * This allows you to batch the fetching of items given an array of `key`
  208. * inputs. Your callback will be invoked with an array of corresponding
  209. * key-value pairs found.
  210. *
  211. * See https://reactnative.dev/docs/asyncstorage.html#multiget
  212. */
  213. multiGet: function(
  214. keys: Array<string>,
  215. callback?: ?(errors: ?Array<Error>, result: ?Array<Array<string>>) => void,
  216. ): Promise {
  217. if (!this._immediate) {
  218. this._immediate = setImmediate(() => {
  219. this._immediate = null;
  220. this.flushGetRequests();
  221. });
  222. }
  223. const getRequest = {
  224. keys: keys,
  225. callback: callback,
  226. // do we need this?
  227. keyIndex: this._getKeys.length,
  228. resolve: null,
  229. reject: null,
  230. };
  231. const promiseResult = new Promise((resolve, reject) => {
  232. getRequest.resolve = resolve;
  233. getRequest.reject = reject;
  234. });
  235. this._getRequests.push(getRequest);
  236. // avoid fetching duplicates
  237. keys.forEach(key => {
  238. if (this._getKeys.indexOf(key) === -1) {
  239. this._getKeys.push(key);
  240. }
  241. });
  242. return promiseResult;
  243. },
  244. /**
  245. * Use this as a batch operation for storing multiple key-value pairs. When
  246. * the operation completes you'll get a single callback with any errors.
  247. *
  248. * See https://reactnative.dev/docs/asyncstorage.html#multiset
  249. */
  250. multiSet: function(
  251. keyValuePairs: Array<Array<string>>,
  252. callback?: ?(errors: ?Array<Error>) => void,
  253. ): Promise {
  254. invariant(RCTAsyncStorage, 'RCTAsyncStorage not available');
  255. return new Promise((resolve, reject) => {
  256. RCTAsyncStorage.multiSet(keyValuePairs, function(errors) {
  257. const error = convertErrors(errors);
  258. callback && callback(error);
  259. if (error) {
  260. reject(error);
  261. } else {
  262. resolve(null);
  263. }
  264. });
  265. });
  266. },
  267. /**
  268. * Call this to batch the deletion of all keys in the `keys` array.
  269. *
  270. * See https://reactnative.dev/docs/asyncstorage.html#multiremove
  271. */
  272. multiRemove: function(
  273. keys: Array<string>,
  274. callback?: ?(errors: ?Array<Error>) => void,
  275. ): Promise {
  276. invariant(RCTAsyncStorage, 'RCTAsyncStorage not available');
  277. return new Promise((resolve, reject) => {
  278. RCTAsyncStorage.multiRemove(keys, function(errors) {
  279. const error = convertErrors(errors);
  280. callback && callback(error);
  281. if (error) {
  282. reject(error);
  283. } else {
  284. resolve(null);
  285. }
  286. });
  287. });
  288. },
  289. /**
  290. * Batch operation to merge in existing and new values for a given set of
  291. * keys. This assumes that the values are stringified JSON.
  292. *
  293. * **NOTE**: This is not supported by all native implementations.
  294. *
  295. * See https://reactnative.dev/docs/asyncstorage.html#multimerge
  296. */
  297. multiMerge: function(
  298. keyValuePairs: Array<Array<string>>,
  299. callback?: ?(errors: ?Array<Error>) => void,
  300. ): Promise {
  301. invariant(RCTAsyncStorage, 'RCTAsyncStorage not available');
  302. return new Promise((resolve, reject) => {
  303. RCTAsyncStorage.multiMerge(keyValuePairs, function(errors) {
  304. const error = convertErrors(errors);
  305. callback && callback(error);
  306. if (error) {
  307. reject(error);
  308. } else {
  309. resolve(null);
  310. }
  311. });
  312. });
  313. },
  314. };
  315. // Not all native implementations support merge.
  316. if (!RCTAsyncStorage.multiMerge) {
  317. delete AsyncStorage.mergeItem;
  318. delete AsyncStorage.multiMerge;
  319. }
  320. function convertErrors(errs) {
  321. if (!errs) {
  322. return null;
  323. }
  324. return (Array.isArray(errs) ? errs : [errs]).map(e => convertError(e));
  325. }
  326. function convertError(error) {
  327. if (!error) {
  328. return null;
  329. }
  330. const out = new Error(error.message);
  331. out.key = error.key; // flow doesn't like this :(
  332. return out;
  333. }
  334. module.exports = AsyncStorage;