1 | /* eslint-disable */
|
2 | // Copyright 2015-present 650 Industries. All rights reserved.
|
3 |
|
4 | // Read-only access to legacy (unscoped) `RCTAsyncLocalStorage` backing for
|
5 | // access to legacy data, now that the new one we use in our fork is scoped
|
6 | // per-app
|
7 |
|
8 | // This code is basically based on react-native's built-in
|
9 | // `RCTAsyncStorage.js` except hardcoded to use
|
10 | // `ExponentLegacyAsyncLocalStorage` as the native module backing
|
11 |
|
12 | /**
|
13 | * Copyright (c) 2015-present, Facebook, Inc.
|
14 | * All rights reserved.
|
15 | *
|
16 | * This source code is licensed under the BSD-style license found in the
|
17 | * LICENSE file in the root directory of this source tree. An additional grant
|
18 | * of patent rights can be found in the PATENTS file in the same directory.
|
19 | *
|
20 | */
|
21 | ;
|
22 |
|
23 | var NativeModules = require('react-native').NativeModules;
|
24 | var AsyncStorage = require('react-native').AsyncStorage;
|
25 |
|
26 | // Use RocksDB if available, then SQLite, then file storage.
|
27 | var RCTAsyncStorage = NativeModules.ExponentLegacyAsyncLocalStorage;
|
28 |
|
29 | /**
|
30 | * @class
|
31 | * @description
|
32 | * `AsyncStorage` is a simple, unencrypted, asynchronous, persistent, key-value storage
|
33 | * system that is global to the app. It should be used instead of LocalStorage.
|
34 | *
|
35 | * It is recommended that you use an abstraction on top of `AsyncStorage`
|
36 | * instead of `AsyncStorage` directly for anything more than light usage since
|
37 | * it operates globally.
|
38 | *
|
39 | * On iOS, `AsyncStorage` is backed by native code that stores small values in a
|
40 | * serialized dictionary and larger values in separate files. On Android,
|
41 | * `AsyncStorage` will use either [RocksDB](http://rocksdb.org/) or SQLite
|
42 | * based on what is available.
|
43 | *
|
44 | * The `AsyncStorage` JavaScript code is a simple facade that provides a clear
|
45 | * JavaScript API, real `Error` objects, and simple non-multi functions. Each
|
46 | * method in the API returns a `Promise` object.
|
47 | *
|
48 | * Persisting data:
|
49 | * ```
|
50 | * try {
|
51 | * await AsyncStorage.setItem('@MySuperStore:key', 'I like to save it.');
|
52 | * } catch (error) {
|
53 | * // Error saving data
|
54 | * }
|
55 | * ```
|
56 | *
|
57 | * Fetching data:
|
58 | * ```
|
59 | * try {
|
60 | * const value = await AsyncStorage.getItem('@MySuperStore:key');
|
61 | * if (value !== null){
|
62 | * // We have data!!
|
63 | * console.log(value);
|
64 | * }
|
65 | * } catch (error) {
|
66 | * // Error retrieving data
|
67 | * }
|
68 | * ```
|
69 | */
|
70 | var LegacyAsyncStorage = {
|
71 | _getRequests: ([]: Array<any>),
|
72 | _getKeys: ([]: Array<string>),
|
73 | _immediate: (null: ?number),
|
74 |
|
75 | async migrateItems(items, { force = false } = {}) {
|
76 | // Skip if already migrated and not forcing
|
77 | if (!force && (await RCTAsyncStorage.isMigrationDone())) {
|
78 | return;
|
79 | }
|
80 |
|
81 | // Get the old values
|
82 | const oldValuesArray = await LegacyAsyncStorage.multiGet(items);
|
83 |
|
84 | // Skip missing or newly set values
|
85 | const newValuesArray = await AsyncStorage.multiGet(items);
|
86 | const newValuesMap = {};
|
87 | newValuesArray.forEach(([k, v]) => (newValuesMap[k] = v));
|
88 | const valuesToSet = oldValuesArray.filter(
|
89 | ([k, v]) => v !== null && newValuesMap[k] == null
|
90 | );
|
91 |
|
92 | // Migrate!
|
93 | await AsyncStorage.multiSet(valuesToSet);
|
94 | await RCTAsyncStorage.setMigrationDone();
|
95 | },
|
96 |
|
97 | /**
|
98 | * Fetches an item for a `key` and invokes a callback upon completion.
|
99 | * Returns a `Promise` object.
|
100 | * @param key Key of the item to fetch.
|
101 | * @param callback Function that will be called with a result if found or
|
102 | * any error.
|
103 | * @returns A `Promise` object.
|
104 | */
|
105 | getItem(
|
106 | key: string,
|
107 | callback?: ?(error: ?Error, result: ?string) => void
|
108 | ): Promise {
|
109 | return new Promise((resolve, reject) => {
|
110 | RCTAsyncStorage.multiGet([key], function(errors, result) {
|
111 | // Unpack result to get value from [[key,value]]
|
112 | var value = result && result[0] && result[0][1] ? result[0][1] : null;
|
113 | var errs = convertErrors(errors);
|
114 | callback && callback(errs && errs[0], value);
|
115 | if (errs) {
|
116 | reject(errs[0]);
|
117 | } else {
|
118 | resolve(value);
|
119 | }
|
120 | });
|
121 | });
|
122 | },
|
123 |
|
124 | /**
|
125 | * Gets *all* keys known to your app; for all callers, libraries, etc.
|
126 | * Returns a `Promise` object.
|
127 | * @param callback Function that will be called the keys found and any error.
|
128 | * @returns A `Promise` object.
|
129 | *
|
130 | * Example: see the `multiGet` example.
|
131 | */
|
132 | getAllKeys(
|
133 | callback?: ?(error: ?Error, keys: ?Array<string>) => void
|
134 | ): Promise {
|
135 | return new Promise((resolve, reject) => {
|
136 | RCTAsyncStorage.getAllKeys(function(error, keys) {
|
137 | callback && callback(convertError(error), keys);
|
138 | if (error) {
|
139 | reject(convertError(error));
|
140 | } else {
|
141 | resolve(keys);
|
142 | }
|
143 | });
|
144 | });
|
145 | },
|
146 |
|
147 | /**
|
148 | * The following batched functions are useful for executing a lot of
|
149 | * operations at once, allowing for native optimizations and provide the
|
150 | * convenience of a single callback after all operations are complete.
|
151 | *
|
152 | * These functions return arrays of errors, potentially one for every key.
|
153 | * For key-specific errors, the Error object will have a key property to
|
154 | * indicate which key caused the error.
|
155 | */
|
156 |
|
157 | /** Flushes any pending requests using a single batch call to get the data. */
|
158 | flushGetRequests(): void {
|
159 | const getRequests = this._getRequests;
|
160 | const getKeys = this._getKeys;
|
161 |
|
162 | this._getRequests = [];
|
163 | this._getKeys = [];
|
164 |
|
165 | RCTAsyncStorage.multiGet(getKeys, function(errors, result) {
|
166 | // Even though the runtime complexity of this is theoretically worse vs if we used a map,
|
167 | // it's much, much faster in practice for the data sets we deal with (we avoid
|
168 | // allocating result pair arrays). This was heavily benchmarked.
|
169 | //
|
170 | // Is there a way to avoid using the map but fix the bug in this breaking test?
|
171 | // https://github.com/facebook/react-native/commit/8dd8ad76579d7feef34c014d387bf02065692264
|
172 | const map = {};
|
173 | result &&
|
174 | result.forEach(([key, value]) => {
|
175 | map[key] = value;
|
176 | return value;
|
177 | });
|
178 | const reqLength = getRequests.length;
|
179 | for (let i = 0; i < reqLength; i++) {
|
180 | const request = getRequests[i];
|
181 | const requestKeys = request.keys;
|
182 | const requestResult = requestKeys.map(key => [key, map[key]]);
|
183 | request.callback && request.callback(null, requestResult);
|
184 | request.resolve && request.resolve(requestResult);
|
185 | }
|
186 | });
|
187 | },
|
188 |
|
189 | /**
|
190 | * This allows you to batch the fetching of items given an array of `key`
|
191 | * inputs. Your callback will be invoked with an array of corresponding
|
192 | * key-value pairs found:
|
193 | *
|
194 | * ```
|
195 | * multiGet(['k1', 'k2'], cb) -> cb([['k1', 'val1'], ['k2', 'val2']])
|
196 | * ```
|
197 | *
|
198 | * The method returns a `Promise` object.
|
199 | *
|
200 | * @param keys Array of key for the items to get.
|
201 | * @param callback Function that will be called with a key-value array of
|
202 | * the results, plus an array of any key-specific errors found.
|
203 | * @returns A `Promise` object.
|
204 | *
|
205 | * @example <caption>Example</caption>
|
206 | *
|
207 | * AsyncStorage.getAllKeys((err, keys) => {
|
208 | * AsyncStorage.multiGet(keys, (err, stores) => {
|
209 | * stores.map((result, i, store) => {
|
210 | * // get at each store's key/value so you can work with it
|
211 | * let key = store[i][0];
|
212 | * let value = store[i][1];
|
213 | * });
|
214 | * });
|
215 | * });
|
216 | */
|
217 | multiGet(
|
218 | keys: Array<string>,
|
219 | callback?: ?(errors: ?Array<Error>, result: ?Array<Array<string>>) => void
|
220 | ): Promise {
|
221 | if (!this._immediate) {
|
222 | this._immediate = setImmediate(() => {
|
223 | this._immediate = null;
|
224 | this.flushGetRequests();
|
225 | });
|
226 | }
|
227 |
|
228 | var getRequest = {
|
229 | keys,
|
230 | callback,
|
231 | // do we need this?
|
232 | keyIndex: this._getKeys.length,
|
233 | resolve: null,
|
234 | reject: null,
|
235 | };
|
236 |
|
237 | var promiseResult = new Promise((resolve, reject) => {
|
238 | getRequest.resolve = resolve;
|
239 | getRequest.reject = reject;
|
240 | });
|
241 |
|
242 | this._getRequests.push(getRequest);
|
243 | // avoid fetching duplicates
|
244 | keys.forEach(key => {
|
245 | if (this._getKeys.indexOf(key) === -1) {
|
246 | this._getKeys.push(key);
|
247 | }
|
248 | });
|
249 |
|
250 | return promiseResult;
|
251 | },
|
252 | };
|
253 |
|
254 | function convertErrors(errs) {
|
255 | if (!errs) {
|
256 | return null;
|
257 | }
|
258 | return (Array.isArray(errs) ? errs : [errs]).map(e => convertError(e));
|
259 | }
|
260 |
|
261 | function convertError(error) {
|
262 | if (!error) {
|
263 | return null;
|
264 | }
|
265 | var out = new Error(error.message);
|
266 | out.key = error.key; // flow doesn't like this :(
|
267 | return out;
|
268 | }
|
269 |
|
270 | module.exports = LegacyAsyncStorage;
|