UNPKG

8.46 kBJavaScriptView Raw
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'use strict';
22
23var NativeModules = require('react-native').NativeModules;
24var AsyncStorage = require('react-native').AsyncStorage;
25
26// Use RocksDB if available, then SQLite, then file storage.
27var 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 */
70var 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
254function convertErrors(errs) {
255 if (!errs) {
256 return null;
257 }
258 return (Array.isArray(errs) ? errs : [errs]).map(e => convertError(e));
259}
260
261function 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
270module.exports = LegacyAsyncStorage;