├── .gitignore
├── .prettierrc.js
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── README.zh-CN.md
├── __tests__
├── basic-function-test.js
├── batch-and-sync-test.js
└── custom-error-test.js
├── babel.config.js
├── jestSupport
└── mockStorage.js
├── package.json
├── rollup.config.js
├── src
├── error.js
├── storage.d.ts
└── storage.js
├── tea.yaml
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | #ide
2 | .idea
3 |
4 | # Logs
5 | logs
6 | *.log
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 |
13 | # Directory for instrumented libs generated by jscoverage/JSCover
14 | lib-cov
15 |
16 | # Coverage directory used by tools like istanbul
17 | coverage
18 |
19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
20 | .grunt
21 |
22 | # node-waf configuration
23 | .lock-wscript
24 |
25 | # Compiled binary addons (http://nodejs.org/api/addons.html)
26 | build/Release
27 |
28 | # Dependency directory
29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
30 | node_modules
31 | lib
32 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 120,
3 | trailingComma: 'all',
4 | singleQuote: true,
5 | };
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '10'
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### Changelog
2 |
3 | #### 1.0.1
4 |
5 | 1. Fix getAllDataForKey crash.
6 | 2. Fix babel config for web compatiblity.
7 |
8 | #### 1.0.0
9 |
10 | 1. Refactor code.
11 | 2. Complete tests.
12 | 3. Provide types.
13 | 4. Provide prebuilt lib for web.
14 |
15 | #### 0.2.2
16 |
17 | 1. check key availability in clearMapForKey
18 |
19 | #### 0.2.0
20 |
21 | 1. `rawData` is now deprecated, use "data" instead!
22 | 2. Upgrade jest to 19.0.0
23 |
24 | #### 0.1.5
25 |
26 | 1. Now you can pass extra params to sync method.
27 | 2. Fix clearMap
28 |
29 | #### 0.1.4
30 |
31 | 1. Now you can check error type (NotFoundError and ExpiredError) in catch
32 | 2. Optimize cache strategy
33 |
34 | #### 0.1.3
35 |
36 | 1. Now you need to specify storageBackend(AsyncStorage or window.localStorage), otherwise the data would not be persisted.
37 |
38 | #### 0.1.2
39 |
40 | 1. Now when load() failed to find data, it will throw an Error with message instead of undefined.
41 |
42 | #### 0.1.1
43 |
44 | 1. `defaultExpires` can be `null` now, which means never expire.
45 |
46 | #### 0.1.0
47 |
48 | 1. add getIdsForKey, getAllDataForKey, clearMapForKey methods
49 | 2. fix some expires logic
50 | 3. refactor unit tests
51 |
52 | #### 0.0.16
53 |
54 | 1. getBatchDataWithIds now won't invoke sync if everything is ready in storage.
55 |
56 | #### 0.0.15
57 |
58 | 1. Fix bugs in promise chain.
59 | 2. Can be used without any storage backend.(Use in-memory map)
60 |
61 | #### 0.0.10
62 |
63 | 1. All methods except remove and clearMap are now totally promisified. Even custom sync methods can be promise. So you can chain them now.
64 | 2. Adjust map structure.
65 | 3. Improved some test cases.
66 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 sunnylqm
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-storage [](#backers) [](#sponsors) [](https://travis-ci.org/sunnylqm/react-native-storage) 
2 |
3 | This is a local storage wrapper for both react native apps (using AsyncStorage) and web apps (using localStorage). [ES6](http://babeljs.io/docs/learn-es2015/) syntax, promise for async load, fully tested with jest.
4 |
5 | 查看中文文档[请点击 README-CHN.md](README.zh-CN.md)
6 |
7 | ## Install
8 |
9 | ```
10 | npm install react-native-storage
11 | npm install @react-native-async-storage/async-storage
12 | ```
13 |
14 | or
15 |
16 | ```
17 | yarn add react-native-storage
18 | yarn add @react-native-async-storage/async-storage
19 | ```
20 |
21 | ## Usage
22 |
23 | ### Init
24 |
25 | ```js
26 | import Storage from 'react-native-storage';
27 | import AsyncStorage from '@react-native-async-storage/async-storage';
28 |
29 | const storage = new Storage({
30 | // maximum capacity, default 1000 key-ids
31 | size: 1000,
32 |
33 | // Use AsyncStorage for RN apps, or window.localStorage for web apps.
34 | // If storageBackend is not set, data will be lost after reload.
35 | storageBackend: AsyncStorage, // for web: window.localStorage
36 |
37 | // expire time, default: 1 day (1000 * 3600 * 24 milliseconds).
38 | // can be null, which means never expire.
39 | defaultExpires: 1000 * 3600 * 24,
40 |
41 | // cache data in the memory. default is true.
42 | enableCache: true,
43 |
44 | // if data was not found in storage or expired data was found,
45 | // the corresponding sync method will be invoked returning
46 | // the latest data.
47 | sync: {
48 | // we'll talk about the details later.
49 | }
50 | });
51 |
52 | export default storage;
53 | ```
54 |
55 | ### Save & Load & Remove
56 |
57 | ```js
58 | // Save something with key only. (using only a keyname but no id)
59 | // This key should be unique. This is for data frequently used.
60 | // The key and value pair is permanently stored unless you remove it yourself.
61 | storage.save({
62 | key: 'loginState', // Note: Do not use underscore("_") in key!
63 | data: {
64 | from: 'some other site',
65 | userid: 'some userid',
66 | token: 'some token'
67 | },
68 |
69 | // if expires not specified, the defaultExpires will be applied instead.
70 | // if set to null, then it will never expire.
71 | expires: 1000 * 3600
72 | });
73 |
74 | // load
75 | storage
76 | .load({
77 | key: 'loginState',
78 |
79 | // autoSync (default: true) means if data is not found or has expired,
80 | // then invoke the corresponding sync method
81 | autoSync: true,
82 |
83 | // syncInBackground (default: true) means if data expired,
84 | // return the outdated data first while invoking the sync method.
85 | // If syncInBackground is set to false, and there is expired data,
86 | // it will wait for the new data and return only after the sync completed.
87 | // (This, of course, is slower)
88 | syncInBackground: true,
89 |
90 | // you can pass extra params to the sync method
91 | // see sync example below
92 | syncParams: {
93 | extraFetchOptions: {
94 | // blahblah
95 | },
96 | someFlag: true
97 | }
98 | })
99 | .then(ret => {
100 | // found data go to then()
101 | console.log(ret.userid);
102 | })
103 | .catch(err => {
104 | // any exception including data not found
105 | // goes to catch()
106 | console.warn(err.message);
107 | switch (err.name) {
108 | case 'NotFoundError':
109 | // TODO;
110 | break;
111 | case 'ExpiredError':
112 | // TODO
113 | break;
114 | }
115 | });
116 |
117 | // --------------------------------------------------
118 |
119 | // Save something with key and id.
120 | // "key-id" data size cannot surpass the size parameter you pass in the constructor.
121 | // By default the 1001st data will overwrite the 1st data item.
122 | // If you then load the 1st data, a catch(NotFoundError) or sync will be invoked.
123 | var userA = {
124 | name: 'A',
125 | age: 20,
126 | tags: ['geek', 'nerd', 'otaku']
127 | };
128 |
129 | storage.save({
130 | key: 'user', // Note: Do not use underscore("_") in key!
131 | id: '1001', // Note: Do not use underscore("_") in id!
132 | data: userA,
133 | expires: 1000 * 60
134 | });
135 |
136 | // load
137 | storage
138 | .load({
139 | key: 'user',
140 | id: '1001'
141 | })
142 | .then(ret => {
143 | // found data goes to then()
144 | console.log(ret.userid);
145 | })
146 | .catch(err => {
147 | // any exception including data not found
148 | // goes to catch()
149 | console.warn(err.message);
150 | switch (err.name) {
151 | case 'NotFoundError':
152 | // TODO;
153 | break;
154 | case 'ExpiredError':
155 | // TODO
156 | break;
157 | }
158 | });
159 |
160 | // --------------------------------------------------
161 |
162 | // get all ids for "key-id" data under a key,
163 | // note: does not include "key-only" information (which has no ids)
164 | storage.getIdsForKey('user').then(ids => {
165 | console.log(ids);
166 | });
167 |
168 | // get all the "key-id" data under a key
169 | // !! important: this does not include "key-only" data
170 | storage.getAllDataForKey('user').then(users => {
171 | console.log(users);
172 | });
173 |
174 | // clear all "key-id" data under a key
175 | // !! important: "key-only" data is not cleared by this function
176 | storage.clearMapForKey('user');
177 |
178 | // --------------------------------------------------
179 |
180 | // remove a single record
181 | storage.remove({
182 | key: 'lastPage'
183 | });
184 | storage.remove({
185 | key: 'user',
186 | id: '1001'
187 | });
188 |
189 | // clear map and remove all "key-id" data
190 | // !! important: "key-only" data is not cleared, and is left intact
191 | storage.clearMap();
192 | ```
193 |
194 | ### Sync remote data(refresh)
195 |
196 | There are two ways to set the sync method.
197 | You can pass the sync method in the constructor's parameter, as a function in an object,
198 | or you can define it at any time as shown below:
199 |
200 | ```js
201 | storage.sync = {
202 | // The name of the sync method must be the same as the data's key name
203 | // And the passed params will be an all-in-one object.
204 | // You can return a value or a promise here
205 | async user(params) {
206 | let {
207 | id,
208 | syncParams: { extraFetchOptions, someFlag }
209 | } = params;
210 | const response = await fetch('user/?id=' + id, {
211 | ...extraFetchOptions
212 | });
213 | const responseText = await response.text();
214 | console.log(`user${id} sync resp: `, responseText);
215 | const json = JSON.parse(responseText);
216 | if (json && json.user) {
217 | storage.save({
218 | key: 'user',
219 | id,
220 | data: json.user
221 | });
222 | if (someFlag) {
223 | // do something for some custom flag
224 | }
225 | // return required data when succeed
226 | return json.user;
227 | } else {
228 | // throw error when failed
229 | throw new Error(`error syncing user${id}`));
230 | }
231 | }
232 | };
233 | ```
234 |
235 | In the following example the sync method is called, when you invoke `storage.load`:
236 |
237 | ```js
238 | storage.load({
239 | key: 'user',
240 | id: '1002'
241 | }).then(...)
242 | ```
243 |
244 | If there is no user 1002 currently in storage, then storage.sync.user will be invoked to fetch and return the remote data.
245 |
246 | ### Load batch data
247 |
248 | ```js
249 | // Load batch data with an array of `storage.load` parameters.
250 | // It will invoke each key's sync method,
251 | // and when all are complete will return all the data in an ordered array.
252 | // The sync methods behave according to the syncInBackground setting: (default true)
253 | // When set to true (the default), if timed out will return the current value
254 | // while when set to false, will wait till the sync method completes
255 |
256 | storage.getBatchData([
257 | { key: 'loginState' },
258 | { key: 'checkPoint', syncInBackground: false },
259 | { key: 'balance' },
260 | { key: 'user', id: '1009' }
261 | ])
262 | .then(results => {
263 | results.forEach(result => {
264 | console.log(result);
265 | })
266 | })
267 |
268 | // Load batch data with one key and an array of ids.
269 | storage.getBatchDataWithIds({
270 | key: 'user',
271 | ids: ['1001', '1002', '1003']
272 | })
273 | .then( ... )
274 | ```
275 |
276 | There is an important difference between the way these two methods perform:
277 | **getBatchData** will invoke separate sync methods for each different key one after the other when the corresponding data is missing or not in sync. However, **getBatchDataWithIds** will collect a list of the missing data, pushing their ids to an array, and then pass the array to the single corresponding sync method once, reducing the number of requests, so you need to implement array query on the server side and handle the parameters of sync method properly. Note that the id parameter can be a single string or an array of strings.
278 |
279 | #### You are welcome to ask any question in the [issues](https://github.com/sunnylqm/react-native-storage/issues) page.
280 |
281 | ## Contributors
282 |
283 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
284 |
285 |
286 |
287 | ## Backers
288 |
289 | Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/react-native-storage#backer)]
290 |
291 |
292 |
293 |
294 | ## Sponsors
295 |
296 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/react-native-storage#sponsor)]
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
--------------------------------------------------------------------------------
/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | # react-native-storage [](https://travis-ci.org/sunnylqm/react-native-storage) 
2 |
3 | [English version doc here](README.md)
4 |
5 | 这是一个本地持久存储的封装,可以同时支持 react-native(AsyncStorage)和浏览器(localStorage)。ES6 语法,promise 异步读取,使用 jest 进行了完整的单元测试。
6 |
7 | ## 安装
8 |
9 | ```
10 | npm install react-native-storage
11 | npm install @react-native-async-storage/async-storage
12 | ```
13 |
14 | 或者
15 |
16 | ```
17 | yarn add react-native-storage
18 | yarn add @react-native-async-storage/async-storage
19 | ```
20 |
21 |
22 | ## 使用说明
23 |
24 | ### 初始化
25 |
26 | ```javascript
27 | // storage.js
28 | import Storage from 'react-native-storage';
29 | import AsyncStorage from '@react-native-async-storage/async-storage';
30 |
31 | const storage = new Storage({
32 | // 最大容量,默认值1000条数据循环存储
33 | size: 1000,
34 |
35 | // 存储引擎:对于RN使用AsyncStorage,对于web使用window.localStorage
36 | // 如果不指定则数据只会保存在内存中,重启后即丢失
37 | storageBackend: AsyncStorage,
38 |
39 | // 数据过期时间,默认一整天(1000 * 3600 * 24 毫秒),设为null则永不过期
40 | defaultExpires: 1000 * 3600 * 24,
41 |
42 | // 读写时在内存中缓存数据。默认启用。
43 | enableCache: true,
44 |
45 | // 你可以在构造函数这里就写好sync的方法
46 | // 或是在任何时候,直接对storage.sync进行赋值修改
47 | // 或是写到另一个文件里,这里require引入
48 | // 如果storage中没有相应数据,或数据已过期,
49 | // 则会调用相应的sync方法,无缝返回最新数据。
50 | // sync方法的具体说明会在后文提到
51 | sync: require('你可以另外写一个文件专门处理sync'),
52 | });
53 |
54 | export default storage;
55 | ```
56 |
57 | ### 保存、读取和删除
58 |
59 | ```javascript
60 | // 使用key来保存数据(key-only)。这些数据一般是全局独有的,需要谨慎单独处理的数据
61 | // 批量数据请使用key和id来保存(key-id),具体请往后看
62 | // 除非你手动移除,这些数据会被永久保存,而且默认不会过期。
63 | storage.save({
64 | key: 'loginState', // 注意:请不要在key中使用_下划线符号!
65 | data: {
66 | from: 'some other site',
67 | userid: 'some userid',
68 | token: 'some token',
69 | },
70 |
71 | // 如果不指定过期时间,则会使用defaultExpires参数
72 | // 如果设为null,则永不过期
73 | expires: 1000 * 3600,
74 | });
75 |
76 | // 读取
77 | storage
78 | .load({
79 | key: 'loginState',
80 |
81 | // autoSync(默认为true)意味着在没有找到数据或数据过期时自动调用相应的sync方法
82 | autoSync: true, // 设置为false的话,则等待sync方法提供的最新数据(当然会需要更多时间)。
83 |
84 | // syncInBackground(默认为true)意味着如果数据过期,
85 | // 在调用sync方法的同时先返回已经过期的数据。
86 | syncInBackground: true,
87 | // 你还可以给sync方法传递额外的参数
88 | syncParams: {
89 | extraFetchOptions: {
90 | // 各种参数
91 | },
92 | someFlag: true,
93 | },
94 | })
95 | .then(ret => {
96 | // 如果找到数据,则在then方法中返回
97 | // 注意:这是异步返回的结果(不了解异步请自行搜索学习)
98 | // 你只能在then这个方法内继续处理ret数据
99 | // 而不能在then以外处理
100 | // 也没有办法“变成”同步返回
101 | // 你也可以使用“看似”同步的async/await语法
102 |
103 | console.log(ret.userid);
104 | this.setState({ user: ret });
105 | })
106 | .catch(err => {
107 | //如果没有找到数据且没有sync方法,
108 | //或者有其他异常,则在catch中返回
109 | console.warn(err.message);
110 | switch (err.name) {
111 | case 'NotFoundError':
112 | // TODO;
113 | break;
114 | case 'ExpiredError':
115 | // TODO
116 | break;
117 | }
118 | });
119 | ```
120 |
121 | ---
122 |
123 | ```javascript
124 | // 使用key和id来保存数据,一般是保存同类别(key)的大量数据。
125 | // 所有这些"key-id"数据共有一个保存上限(无论是否相同key)
126 | // 即在初始化storage时传入的size参数。
127 | // 在默认上限参数下,第1001个数据会覆盖第1个数据。
128 | // 覆盖之后,再读取第1个数据,会返回catch或是相应的sync方法。
129 | var userA = {
130 | name: 'A',
131 | age: 20,
132 | tags: ['geek', 'nerd', 'otaku'],
133 | };
134 |
135 | storage.save({
136 | key: 'user', // 注意:请不要在key中使用_下划线符号!
137 | id: '1001', // 注意:请不要在id中使用_下划线符号!
138 | data: userA,
139 | expires: 1000 * 60,
140 | });
141 |
142 | //load 读取
143 | storage
144 | .load({
145 | key: 'user',
146 | id: '1001',
147 | })
148 | .then(ret => {
149 | // 如果找到数据,则在then方法中返回
150 | console.log(ret.userid);
151 | })
152 | .catch(err => {
153 | // 如果没有找到数据且没有sync方法,
154 | // 或者有其他异常,则在catch中返回
155 | console.warn(err.message);
156 | switch (err.name) {
157 | case 'NotFoundError':
158 | // TODO;
159 | break;
160 | case 'ExpiredError':
161 | // TODO
162 | break;
163 | }
164 | });
165 |
166 | // --------------------------------------------------
167 |
168 | // 获取某个key下的所有id(仅key-id数据)
169 | storage.getIdsForKey('user').then(ids => {
170 | console.log(ids);
171 | });
172 |
173 | // 获取某个key下的所有数据(仅key-id数据)
174 | storage.getAllDataForKey('user').then(users => {
175 | console.log(users);
176 | });
177 |
178 | // !! 清除某个key下的所有数据(仅key-id数据)
179 | storage.clearMapForKey('user');
180 |
181 | // --------------------------------------------------
182 |
183 | // 删除单个数据
184 | storage.remove({
185 | key: 'lastPage',
186 | });
187 | storage.remove({
188 | key: 'user',
189 | id: '1001',
190 | });
191 |
192 | // !! 清空map,移除所有"key-id"数据(但会保留只有key的数据)
193 | storage.clearMap();
194 | ```
195 |
196 | ### 同步远程数据(刷新)
197 |
198 | ```javascript
199 | storage.sync = {
200 | // sync方法的名字必须和所存数据的key完全相同
201 | // 参数从params中解构取出
202 | // 最后返回所需数据或一个promise
203 | async user(params) {
204 | const {
205 | id,
206 | syncParams: { extraFetchOptions, someFlag }
207 | } = params;
208 | const response = await fetch('user/?id=' + id, {
209 | ...extraFetchOptions
210 | });
211 | const responseText = await response.text();
212 | console.log(`user${id} sync resp: `, responseText);
213 | const json = JSON.parse(responseText);
214 | if (json && json.user) {
215 | storage.save({
216 | key: 'user',
217 | id,
218 | data: json.user
219 | });
220 | if (someFlag) {
221 | // 根据一些自定义标志变量操作
222 | }
223 | // 返回所需数据
224 | return json.user;
225 | } else {
226 | // 出错时抛出异常
227 | throw new Error(`error syncing user${id}`));
228 | }
229 | }
230 | };
231 | ```
232 |
233 | 有了上面这个 sync 方法,以后再调用 storage.load 时,如果本地并没有存储相应的 user,那么会自动触发 storage.sync.user 去远程取回数据并无缝返回。
234 |
235 | ```javascript
236 | storage.load({
237 | key: 'user',
238 | id: '1002'
239 | }).then(...)
240 | ```
241 |
242 | ### 读取批量数据
243 |
244 | ```javascript
245 | // 使用和load方法一样的参数读取批量数据,但是参数是以数组的方式提供。
246 | // 会在需要时分别调用相应的sync方法,最后统一返回一个有序数组。
247 | storage.getBatchData([
248 | { key: 'loginState' },
249 | { key: 'checkPoint', syncInBackground: false },
250 | { key: 'balance' },
251 | { key: 'user', id: '1009' }
252 | ])
253 | .then(results => {
254 | results.forEach( result => {
255 | console.log(result);
256 | })
257 | })
258 |
259 | //根据key和一个id数组来读取批量数据
260 | storage.getBatchDataWithIds({
261 | key: 'user',
262 | ids: ['1001', '1002', '1003']
263 | })
264 | .then( ... )
265 | ```
266 |
267 | 这两个方法除了参数形式不同,还有个值得注意的差异。**getBatchData**会在数据缺失时挨个调用不同的 sync 方法(因为 key 不同)。但是**getBatchDataWithIds**却会把缺失的数据统计起来,将它们的 id 收集到一个数组中,然后一次传递给对应的 sync 方法(避免挨个查询导致同时发起大量请求),所以你需要在服务端实现通过数组来查询返回,还要注意对应的 sync 方法的参数处理(因为 id 参数可能是一个字符串,也可能是一个数组的字符串)。
268 |
269 | #### 如有任何问题,欢迎在[issues](https://github.com/sunnylqm/react-native-storage/issues)页面中提出。
270 |
--------------------------------------------------------------------------------
/__tests__/basic-function-test.js:
--------------------------------------------------------------------------------
1 | import Storage from '../src/storage';
2 | import { NotFoundError, ExpiredError } from '../src/error';
3 | const SIZE = 10,
4 | DEFAULTEXPIRES = 1000 * 3600;
5 |
6 | const localStorage = new Storage({
7 | size: SIZE,
8 | defaultExpires: DEFAULTEXPIRES,
9 | storageBackend: global.localStorage,
10 | });
11 | const asyncStorage = new Storage({
12 | size: SIZE,
13 | defaultExpires: DEFAULTEXPIRES,
14 | storageBackend: global.asyncStorage,
15 | });
16 | const stores = { localStorage, asyncStorage };
17 |
18 | beforeEach(() => {
19 | localStorage.clearAll();
20 | asyncStorage.clearAll();
21 | });
22 |
23 | describe('react-native-storage: basic function', () => {
24 | Object.keys(stores).map(storageKey => {
25 | let storage = stores[storageKey];
26 | test('accepts parameters in constructor' + `(${storageKey})`, () => {
27 | expect(storage._SIZE).toBe(SIZE);
28 | expect(storage.defaultExpires).toBe(DEFAULTEXPIRES);
29 | });
30 | test('saves and loads any type of data' + `(${storageKey})`, () => {
31 | let testCases = {
32 | testNumber: 11221,
33 | testString: 'testString',
34 | testObject: {
35 | fname: 'foo',
36 | lname: 'bar',
37 | },
38 | testArray: ['one', 'two', 'three'],
39 | testBoolean: false,
40 | testNull: null,
41 | complexObject: {
42 | complexArray: [1, 2, 3, 'test', { a: 'b' }],
43 | },
44 | };
45 | let returnCases = {},
46 | returnCasesWithId = {};
47 | let tasks = [];
48 | for (let key in testCases) {
49 | tasks.push(
50 | storage
51 | .save({
52 | key,
53 | data: testCases[key],
54 | })
55 | .then(() =>
56 | storage
57 | .load({
58 | key,
59 | })
60 | .then(ret => {
61 | returnCases[key] = ret;
62 | }),
63 | ),
64 | );
65 | tasks.push(
66 | storage
67 | .save({
68 | key,
69 | id: 1,
70 | data: testCases[key],
71 | })
72 | .then(() =>
73 | storage
74 | .load({
75 | key,
76 | id: 1,
77 | })
78 | .then(ret => {
79 | returnCasesWithId[key] = ret;
80 | }),
81 | ),
82 | );
83 | }
84 | return Promise.all(tasks).then(() => {
85 | for (let key in testCases) {
86 | expect(JSON.stringify(testCases[key])).toBe(JSON.stringify(returnCases[key]));
87 | expect(JSON.stringify(testCases[key])).toBe(JSON.stringify(returnCasesWithId[key]));
88 | }
89 | });
90 | });
91 | test('rejects when no data found and no sync method' + `(${storageKey})`, () => {
92 | let testKey1 = 'testKey' + Math.random(),
93 | testKey2 = 'testKey' + Math.random(),
94 | testId2 = 'testId' + Math.random();
95 | let ret1, ret2, error1, error2;
96 | let tasks = [
97 | storage
98 | .load({
99 | key: testKey1,
100 | })
101 | .then(ret => {
102 | ret1 = ret;
103 | })
104 | .catch(e => {
105 | error1 = e;
106 | }),
107 | storage
108 | .load({
109 | key: testKey2,
110 | id: testId2,
111 | })
112 | .then(ret => {
113 | ret2 = ret;
114 | })
115 | .catch(e => {
116 | error2 = e;
117 | }),
118 | ];
119 | return Promise.all(tasks).then(() => {
120 | expect(ret1).toBeUndefined();
121 | expect(ret2).toBeUndefined();
122 | expect(error1.name).toBe('NotFoundError');
123 | expect(error2.name).toBe('NotFoundError');
124 | });
125 | });
126 |
127 | test('rejects when data expired and no sync method' + `(${storageKey})`, () => {
128 | let originDateNow = Date.now;
129 | let starttime = 0;
130 | Date.now = jest.fn(() => {
131 | return (starttime += 100);
132 | });
133 | let testKey1 = 'testKey' + Math.random(),
134 | testKey2 = 'testKey' + Math.random(),
135 | testId2 = 'testId' + Math.random(),
136 | testData1 = 'testData1' + Math.random(),
137 | testData2 = 'testData2' + Math.random();
138 | let ret1, ret2, error1, error2;
139 | let tasks = [
140 | storage
141 | .save({
142 | key: testKey1,
143 | data: testData1,
144 | expires: 1,
145 | })
146 | .then(() =>
147 | storage.load({
148 | key: testKey1,
149 | }),
150 | )
151 | .then(ret => {
152 | ret1 = ret;
153 | })
154 | .catch(e => {
155 | error1 = e;
156 | }),
157 | storage
158 | .save({
159 | key: testKey2,
160 | id: testId2,
161 | data: testData2,
162 | expires: 1,
163 | })
164 | .then(() =>
165 | storage.load({
166 | key: testKey2,
167 | id: testId2,
168 | }),
169 | )
170 | .then(ret => {
171 | ret2 = ret;
172 | })
173 | .catch(e => {
174 | error2 = e;
175 | }),
176 | ];
177 | return Promise.all(tasks).then(() => {
178 | expect(ret1).toBeUndefined();
179 | expect(ret2).toBeUndefined();
180 | expect(error1.name).toBe('ExpiredError');
181 | expect(error2.name).toBe('ExpiredError');
182 | Date.now = originDateNow;
183 | });
184 | });
185 | test('overwrites "key+id" data when loops over(exceeds SIZE)', async () => {
186 | let testKey = 'testKey' + Math.random(),
187 | testId = 'testId' + Math.random(),
188 | testData = 'testData' + Math.random();
189 | let ret1, ret2, cursorIndex1, cursorIndex2;
190 |
191 | cursorIndex1 = storage._m.index; // 0
192 |
193 | await storage.save({
194 | key: testKey,
195 | id: testId,
196 | data: testData,
197 | });
198 |
199 | for (let i = 0; i < SIZE - 1; i++) {
200 | await storage.save({
201 | key: 'testKey' + Math.random(),
202 | id: 'testId' + Math.random(),
203 | data: 'testData' + Math.random(),
204 | });
205 | }
206 |
207 | cursorIndex2 = storage._m.index; // 0 again
208 |
209 | // not overwrited yet
210 | ret1 = await storage.load({
211 | key: testKey,
212 | id: testId,
213 | });
214 |
215 | // overwrite
216 | await storage.save({
217 | key: 'testKey' + Math.random(),
218 | id: 'testId' + Math.random(),
219 | data: 'testData' + Math.random(),
220 | });
221 |
222 | try {
223 | ret2 = await storage.load({
224 | key: testKey,
225 | id: testId,
226 | });
227 | } catch (e) {
228 | ret2 = e;
229 | }
230 |
231 | expect(cursorIndex1).toBe(cursorIndex2);
232 | expect(ret1).toBe(testData);
233 | expect(ret2 instanceof NotFoundError).toBeTruthy();
234 | });
235 |
236 | test('overwrites "key+id" data when exceeds SIZE with the same key', async () => {
237 | const testKey = 'testKey';
238 | let cursorIndex1, cursorIndex2, results;
239 |
240 | cursorIndex1 = storage._m.index;
241 | results = [];
242 | for (let i = 0; i < SIZE; i++) {
243 | const mockData = 'data' + i;
244 | results.push(mockData);
245 | await storage.save({
246 | key: testKey,
247 | id: 'testId' + i,
248 | data: mockData,
249 | });
250 | }
251 |
252 | // not overwrited yet
253 | expect(JSON.stringify(await storage.getAllDataForKey(testKey))).toBe(JSON.stringify(results));
254 |
255 | cursorIndex2 = storage._m.index; // 0 again
256 |
257 | // overwrite
258 | const mockData = 'data-overwrite';
259 | results.splice(0, 1);
260 | results.push(mockData);
261 | await storage.save({
262 | key: 'testKey',
263 | id: 'testId' + SIZE,
264 | data: mockData,
265 | });
266 |
267 | expect(JSON.stringify(await storage.getAllDataForKey(testKey))).toBe(JSON.stringify(results));
268 |
269 | expect(cursorIndex1).toBe(cursorIndex2);
270 | });
271 |
272 | test('removes data correctly' + `(${storageKey})`, () => {
273 | let testKey1 = 'testKey1' + Math.random(),
274 | testKey2 = 'testKey2' + Math.random(),
275 | testId2 = 'testId2' + Math.random(),
276 | testData1 = 'testData1' + Math.random(),
277 | testData2 = 'testData2' + Math.random();
278 | let ret1 = [undefined, undefined],
279 | ret2 = [undefined, undefined];
280 | let task = (key, id, data, retArray) => {
281 | return storage
282 | .save({
283 | key,
284 | id,
285 | data,
286 | })
287 | .then(() => {
288 | return storage.load({
289 | key,
290 | id,
291 | });
292 | })
293 | .then(ret => {
294 | retArray[0] = ret;
295 | return storage.remove({ key, id });
296 | })
297 | .then(() => {
298 | return storage.load({ key, id });
299 | })
300 | .then(ret => {
301 | retArray[1] = ret;
302 | })
303 | .catch(() => {
304 | retArray[1] = 'catched';
305 | });
306 | };
307 | return Promise.all([task(testKey1, undefined, testData1, ret1), task(testKey2, testId2, testData2, ret2)]).then(
308 | () => {
309 | expect(ret1[0]).toBe(testData1);
310 | expect(ret1[1]).toBe('catched');
311 | expect(ret2[0]).toBe(testData2);
312 | expect(ret2[1]).toBe('catched');
313 | },
314 | );
315 | });
316 |
317 | test('gets all data for key correctly' + `(${storageKey})`, () => {
318 | let key = 'testKey' + Math.random(),
319 | testIds = [Math.random(), Math.random(), Math.random()],
320 | testDatas = [Math.random(), Math.random(), Math.random()];
321 | return Promise.all(
322 | testIds.map((id, i) =>
323 | storage.save({
324 | key,
325 | id,
326 | data: testDatas[i],
327 | }),
328 | ),
329 | )
330 | .then(() => {
331 | return storage.getAllDataForKey(key);
332 | })
333 | .then(realRet => {
334 | expect(realRet).toEqual(testDatas);
335 | });
336 | });
337 |
338 | test('removes all data for key correctly' + `(${storageKey})`, () => {
339 | let key = 'testKey' + Math.random(),
340 | testIds = [Math.random(), Math.random(), Math.random()],
341 | testDatas = [Math.random(), Math.random(), Math.random()];
342 | let ret;
343 | return Promise.all(
344 | testIds.map((id, i) =>
345 | storage.save({
346 | key,
347 | id,
348 | data: testDatas[i],
349 | }),
350 | ),
351 | )
352 | .then(() => {
353 | return storage.clearMapForKey(key);
354 | })
355 | .then(() => {
356 | return storage.getAllDataForKey(key);
357 | })
358 | .then(realRet => {
359 | ret = realRet;
360 | })
361 | .catch(() => {
362 | ret = undefined;
363 | })
364 | .then(() => {
365 | expect(Array.isArray(ret)).toBe(true);
366 | expect(ret.length).toBe(0);
367 | });
368 | });
369 |
370 | test('loads ids by key correctly' + `(${storageKey})`, () => {
371 | let key = 'testKey' + Math.random(),
372 | testIds = [Math.random(), Math.random(), Math.random()],
373 | data = 'testData' + Math.random();
374 | let ret = [];
375 | let tasks = testIds.map(id =>
376 | storage.save({
377 | key,
378 | id,
379 | data,
380 | }),
381 | );
382 | return Promise.all(tasks).then(async () => {
383 | expect(await storage.getIdsForKey(key)).toEqual(testIds);
384 | });
385 | });
386 | });
387 | });
388 |
--------------------------------------------------------------------------------
/__tests__/batch-and-sync-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by sunny on 15/10/5.
3 | */
4 |
5 | import Storage from '../src/storage';
6 | const localStorage = new Storage({
7 | storageBackend: global.localStorage,
8 | });
9 | const asyncStorage = new Storage({
10 | storageBackend: global.asyncStorage,
11 | });
12 | const stores = { localStorage, asyncStorage };
13 |
14 | describe('react-native-storage: batch and sync test', () => {
15 | Object.keys(stores).map(storageKey => {
16 | const storage = stores[storageKey];
17 | test('triggers sync when no data found' + `(${storageKey})`, () => {
18 | const testKey1 = 'testKey1' + Math.random(),
19 | testKey2 = 'testKey2' + Math.random(),
20 | testId2 = 'testId2' + Math.random(),
21 | syncData = 'syncData';
22 | const sync1 = jest.fn(async params => {
23 | return syncData;
24 | });
25 | const sync2 = jest.fn(async params => {
26 | return syncData + params.id;
27 | });
28 | storage.sync[testKey1] = sync1;
29 | storage.sync[testKey2] = sync2;
30 |
31 | return Promise.all([
32 | // key not found
33 | storage.load({
34 | key: testKey1,
35 | }),
36 | // key and id not found
37 | storage.load({
38 | key: testKey2,
39 | id: testId2,
40 | }),
41 | ]).then(([ret1, ret2]) => {
42 | expect(ret1).toBe(syncData);
43 | expect(sync1.mock.calls.length).toBe(1);
44 | expect(sync2.mock.calls.length).toBe(1);
45 | expect(ret2).toBe(syncData + testId2);
46 | });
47 | });
48 | test('does not trigger sync when data found and do not expire' + `(${storageKey})`, () => {
49 | let testKey1 = 'testKey1' + Math.random(),
50 | testKey2 = 'testKey2' + Math.random(),
51 | testId2 = 'testId2' + Math.random(),
52 | testData1 = 'testData1',
53 | testData2 = 'testData2',
54 | syncData = 'syncData';
55 | let sync1 = jest.fn(params => {
56 | let { resolve } = params;
57 | resolve && resolve(syncData);
58 | });
59 | let sync2 = jest.fn(params => {
60 | let { id, resolve } = params;
61 | resolve && resolve(syncData + id);
62 | });
63 | storage.sync[testKey1] = sync1;
64 | storage.sync[testKey2] = sync2;
65 |
66 | // save data, expires in long time
67 | storage.save({
68 | key: testKey1,
69 | data: testData1,
70 | expires: 10000,
71 | });
72 | storage.save({
73 | key: testKey2,
74 | id: testId2,
75 | data: testData2,
76 | expires: 10000,
77 | });
78 |
79 | // instantly load
80 | return Promise.all([
81 | storage.load({
82 | key: testKey1,
83 | }),
84 | storage.load({
85 | key: testKey2,
86 | id: testId2,
87 | }),
88 | ]).then(([ret1, ret2]) => {
89 | expect(ret1).toBe(testData1);
90 | expect(sync1.mock.calls.length).toBe(0);
91 | expect(sync2.mock.calls.length).toBe(0);
92 | expect(ret2).toBe(testData2);
93 | });
94 | });
95 | test(
96 | 'triggers sync when data expires but still returns outdated data(syncInBackground: true)' + `(${storageKey})`,
97 | () => {
98 | let testKey1 = 'testKey1' + Math.random(),
99 | testKey2 = 'testKey2' + Math.random(),
100 | testId2 = 'testId2' + Math.random(),
101 | testData1 = 'testData1',
102 | testData2 = 'testData2',
103 | syncData = 'syncData';
104 | let sync1 = jest.fn(params => {
105 | return syncData;
106 | });
107 | let sync2 = jest.fn(params => {
108 | const { id } = params;
109 | return syncData + id;
110 | });
111 | storage.sync[testKey1] = sync1;
112 | storage.sync[testKey2] = sync2;
113 |
114 | // save data, expires in no time
115 | storage.save({
116 | key: testKey1,
117 | data: testData1,
118 | expires: -1,
119 | });
120 | storage.save({
121 | key: testKey2,
122 | id: testId2,
123 | data: testData2,
124 | expires: -1,
125 | });
126 |
127 | // instantly load
128 | return Promise.all([
129 | storage.load({
130 | key: testKey1,
131 | }),
132 | storage.load({
133 | key: testKey2,
134 | id: testId2,
135 | }),
136 | ]).then(([ret1, ret2]) => {
137 | expect(ret1).toBe(testData1);
138 | expect(sync1.mock.calls.length).toBe(1);
139 |
140 | expect(ret2).toBe(testData2);
141 | expect(sync2.mock.calls.length).toBe(1);
142 | });
143 | },
144 | );
145 | test('triggers sync when data expires and returns latest data(syncInBackground: false)' + `(${storageKey})`, () => {
146 | let testKey1 = 'testKey1' + Math.random(),
147 | testKey2 = 'testKey2' + Math.random(),
148 | testId2 = 'testId2' + Math.random(),
149 | testData1 = 'testData1',
150 | testData2 = 'testData2',
151 | syncData = 'syncData';
152 | let sync1 = jest.fn(async params => {
153 | return syncData;
154 | });
155 | let sync2 = jest.fn(async params => {
156 | return syncData + params.id;
157 | });
158 | storage.sync[testKey1] = sync1;
159 | storage.sync[testKey2] = sync2;
160 |
161 | // save data, expires in no time
162 | storage.save({
163 | key: testKey1,
164 | data: testData1,
165 | expires: -1,
166 | });
167 | storage.save({
168 | key: testKey2,
169 | id: testId2,
170 | data: testData2,
171 | expires: -1,
172 | });
173 |
174 | // instantly load
175 | return Promise.all([
176 | storage.load({
177 | key: testKey1,
178 | syncInBackground: false,
179 | }),
180 | storage.load({
181 | key: testKey2,
182 | id: testId2,
183 | syncInBackground: false,
184 | }),
185 | ]).then(([ret1, ret2]) => {
186 | expect(ret1).toBe(syncData);
187 | expect(sync1.mock.calls.length).toBe(1);
188 |
189 | expect(ret2).toBe(syncData + testId2);
190 | expect(sync2.mock.calls.length).toBe(1);
191 | });
192 | });
193 | test('returns batch data with batch keys' + `(${storageKey})`, () => {
194 | let testKey1 = 'testKey1' + Math.random(),
195 | testKey2 = 'testKey2' + Math.random(),
196 | testKey3 = 'testKey3' + Math.random(),
197 | testData1 = 'testData1',
198 | testData2 = 'testData2',
199 | testData3 = 'testData3';
200 | let sync3 = jest.fn(params => {
201 | return testData3;
202 | });
203 | storage.sync[testKey3] = sync3;
204 |
205 | // save key1 and key2
206 | storage.save({
207 | key: testKey1,
208 | data: testData1,
209 | });
210 | storage.save({
211 | key: testKey2,
212 | data: testData2,
213 | });
214 |
215 | // instantly load
216 | return storage.getBatchData([{ key: testKey1 }, { key: testKey2 }, { key: testKey3 }]).then(ret => {
217 | expect(ret[0]).toBe(testData1);
218 | expect(ret[1]).toBe(testData2);
219 | expect(ret[2]).toBe(testData3);
220 | expect(sync3.mock.calls.length).toBe(1);
221 | });
222 | });
223 | test('returns batch data with batch ids' + `(${storageKey})`, () => {
224 | const originDateNow = Date.now;
225 | let starttime = 0;
226 | Date.now = jest.fn(() => {
227 | return (starttime += 100);
228 | });
229 | const testKey = 'testKey' + Math.random(),
230 | testId1 = 'testId1' + Math.random(),
231 | testId2 = 'testId2' + Math.random(),
232 | testId3 = 'testId3' + Math.random(),
233 | testId4 = 'testId4' + Math.random(),
234 | testData1 = 'testData1',
235 | testData2 = 'testData2',
236 | testData3 = 'testData3',
237 | testData4 = 'testData4';
238 | const sync = jest.fn(async params => {
239 | // when id is an array, the return value should be an ordered array too
240 | return [testData2, testData4];
241 | });
242 | storage.sync[testKey] = sync;
243 | // save id1 and id3
244 | storage.save({
245 | key: testKey,
246 | id: testId1,
247 | data: testData1,
248 | });
249 | storage.save({
250 | key: testKey,
251 | id: testId3,
252 | data: testData3,
253 | });
254 | // save id2 and set it expired immediately
255 | storage.save({
256 | key: testKey,
257 | id: testId2,
258 | data: testData2,
259 | expires: 1,
260 | });
261 |
262 | // instantly load
263 | return storage
264 | .getBatchDataWithIds({
265 | key: testKey,
266 | ids: [testId1, testId2, testId3, testId4],
267 | })
268 | .then(ret => {
269 | expect(ret[0]).toBe(testData1);
270 | expect(ret[1]).toBe(testData2);
271 | expect(ret[2]).toBe(testData3);
272 | expect(ret[3]).toBe(testData4);
273 | expect(JSON.stringify(sync.mock.calls[0][0].id)).toBe(JSON.stringify([testId2, testId4]));
274 | Date.now = originDateNow;
275 | });
276 | });
277 | });
278 | });
279 |
--------------------------------------------------------------------------------
/__tests__/custom-error-test.js:
--------------------------------------------------------------------------------
1 | import { NotFoundError, ExpiredError } from '../src/error';
2 |
3 | describe('react-native-storage: custom error types', () => {
4 | test('NotFoundError instance should be error instance', () => {
5 | const error = new NotFoundError('somekey');
6 | expect(error instanceof Error).toBe(true);
7 | expect(error.toString().includes('somekey')).toBe(true);
8 | expect(error.toString().includes('NotFoundError')).toBe(true);
9 | });
10 |
11 | test('ExpiredError instance should be error instance', () => {
12 | const error = new ExpiredError('somekey');
13 | expect(error instanceof Error).toBe(true);
14 | expect(error.toString().includes('somekey')).toBe(true);
15 | expect(error.toString().includes('ExpiredError')).toBe(true);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/preset-env',
5 | {
6 | targets: 'defaults',
7 | modules: false,
8 | },
9 | ],
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/jestSupport/mockStorage.js:
--------------------------------------------------------------------------------
1 | const localStorageMock = (() => {
2 | const data = {};
3 | const localStorage = {
4 | getItem(key) {
5 | return data[key] === undefined ? null : data[key];
6 | },
7 | setItem(key, value) {
8 | data[key] = value;
9 | },
10 | removeItem(key) {
11 | delete data[key];
12 | }
13 | };
14 | return localStorage;
15 | })();
16 | const asyncStorageMock = (() => {
17 | const data = {};
18 | const asyncStorage = {
19 | getItem(key) {
20 | return new Promise((resolve, reject) => {
21 | resolve(data[key]);
22 | });
23 | },
24 | setItem(key, value) {
25 | return new Promise((resolve, reject) => {
26 | data[key] = value;
27 | resolve();
28 | });
29 | },
30 | removeItem(key) {
31 | return new Promise((resolve, reject) => {
32 | delete data[key];
33 | resolve();
34 | });
35 | }
36 | };
37 | return asyncStorage;
38 | })();
39 | global.localStorage = localStorageMock;
40 | global.asyncStorage = asyncStorageMock;
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-storage",
3 | "version": "1.0.1",
4 | "description": "A local storage wrapper for both react-native(AsyncStorage) and browser(localStorage).",
5 | "license": "MIT",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/sunnylqm/react-native-storage.git"
9 | },
10 | "bugs": {
11 | "url": "https://github.com/sunnylqm/react-native-storage/issues"
12 | },
13 | "homepage": "https://github.com/sunnylqm/react-native-storage#readme",
14 | "keywords": [
15 | "react-native",
16 | "localStorage",
17 | "AsyncStorage"
18 | ],
19 | "files": [
20 | "lib/",
21 | "src/"
22 | ],
23 | "author": "sunnylqm",
24 | "main": "lib/storage.cjs.js",
25 | "jsnext:main": "lib/storage.esm.js",
26 | "module": "lib/storage.esm.js",
27 | "react-native": "src/storage.js",
28 | "typings": "lib/storage.d.ts",
29 | "scripts": {
30 | "clean": "rm -rf lib/",
31 | "build": "yarn clean && rollup -c && cp src/storage.d.ts lib/",
32 | "prepare": "yarn build",
33 | "test": "jest",
34 | "postinstall": "opencollective-postinstall"
35 | },
36 | "jest": {
37 | "verbose": true,
38 | "bail": true,
39 | "setupFiles": [
40 | "./jestSupport/mockStorage.js"
41 | ]
42 | },
43 | "devDependencies": {
44 | "@babel/cli": "^7.4.3",
45 | "@babel/core": "^7.4.3",
46 | "@babel/preset-env": "^7.4.3",
47 | "babel-jest": "^24.7.1",
48 | "jest": "^24.7.1",
49 | "rollup": "^1.10.0",
50 | "rollup-plugin-babel": "^4.3.2",
51 | "rollup-plugin-node-resolve": "^4.2.3",
52 | "rollup-plugin-terser": "^4.0.4"
53 | },
54 | "dependencies": {
55 | "opencollective-postinstall": "^2.0.2",
56 | "opencollective": "^1.0.3"
57 | },
58 | "collective": {
59 | "type": "opencollective",
60 | "url": "https://opencollective.com/react-native-storage"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from 'rollup-plugin-node-resolve';
2 | import babel from 'rollup-plugin-babel';
3 | import { terser } from 'rollup-plugin-terser';
4 |
5 | const env = process.env.NODE_ENV;
6 | const config = {
7 | input: './src/storage.js',
8 | output: [
9 | {
10 | exports: 'named',
11 | file: 'lib/storage.umd.js',
12 | format: 'umd',
13 | sourcemap: true,
14 | name: 'storage',
15 | },
16 | {
17 | exports: 'named',
18 | file: 'lib/storage.cjs.js',
19 | sourcemap: true,
20 | format: 'cjs',
21 | },
22 | {
23 | file: 'lib/storage.esm.js',
24 | sourcemap: true,
25 | format: 'esm',
26 | },
27 | ],
28 | plugins: [
29 | resolve(),
30 | babel({
31 | exclude: 'node_modules/**',
32 | }),
33 | terser(),
34 | ],
35 | };
36 |
37 | export default config;
38 |
--------------------------------------------------------------------------------
/src/error.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by sunny on 9/1/16.
3 | */
4 |
5 | export class NotFoundError extends Error {
6 | constructor(message) {
7 | super(`Not Found! Params: ${message}`);
8 | this.name = 'NotFoundError';
9 | this.stack = new Error().stack; // Optional
10 | }
11 | }
12 | // NotFoundError.prototype = Object.create(Error.prototype);
13 |
14 | export class ExpiredError extends Error {
15 | constructor(message) {
16 | super(`Expired! Params: ${message}`);
17 | this.name = 'ExpiredError';
18 | this.stack = new Error().stack; // Optional
19 | }
20 | }
21 | // ExpiredError.prototype = Object.create(Error.prototype);
22 |
--------------------------------------------------------------------------------
/src/storage.d.ts:
--------------------------------------------------------------------------------
1 | export interface LoadParams {
2 | key: string;
3 | id?: string;
4 | autoSync?: boolean;
5 | syncInBackground?: boolean;
6 | syncParams?: any;
7 | }
8 |
9 | export class NotFoundError extends Error {
10 | name: string;
11 | }
12 |
13 | export class ExpiredError extends Error {
14 | name: string;
15 | }
16 |
17 | export default class Storage {
18 | sync: any;
19 |
20 | constructor(params?: {
21 | size?: number;
22 | storageBackend?: any;
23 | defaultExpires?: number | null;
24 | enableCache?: boolean;
25 | sync?: any;
26 | });
27 |
28 | save(params: { key: string; id?: string; data: any; expires?: number | null }): Promise;
29 |
30 | load(params: LoadParams): Promise;
31 |
32 | getIdsForKey(key: string): Promise;
33 |
34 | getAllDataForKey(key: string): Promise;
35 |
36 | getBatchData(params: LoadParams[]): Promise;
37 |
38 | getBatchDataWithIds(params: { key: string; ids: string[] }): Promise;
39 |
40 | clearMapForKey(key: string): Promise;
41 |
42 | remove(params: { key: string; id?: string }): Promise;
43 |
44 | clearMap(): Promise;
45 | }
46 |
--------------------------------------------------------------------------------
/src/storage.js:
--------------------------------------------------------------------------------
1 | /*
2 | * local storage(web/react native) wrapper
3 | * sunnylqm
4 | */
5 | import { NotFoundError, ExpiredError } from './error';
6 |
7 | export { NotFoundError, ExpiredError };
8 |
9 | export default class Storage {
10 | constructor(options = {}) {
11 | this._SIZE = options.size || 1000; // maximum key-ids capacity
12 | this.sync = options.sync || {}; // remote sync method
13 | this.defaultExpires = options.defaultExpires !== undefined ? options.defaultExpires : 1000 * 3600 * 24;
14 | this.enableCache = options.enableCache !== false;
15 | this._s = options.storageBackend || null;
16 | this._innerVersion = 11;
17 | this.cache = {};
18 |
19 | if (this._s && this._s.setItem) {
20 | try {
21 | var promiseTest = this._s.setItem('__react_native_storage_test', 'test');
22 | this.isPromise = !!(promiseTest && promiseTest.then);
23 | } catch (e) {
24 | console.warn(e);
25 | delete this._s;
26 | throw e;
27 | }
28 | } else {
29 | console.warn(`Data would be lost after reload cause there is no storageBackend specified!
30 | \nEither use localStorage(for web) or AsyncStorage(for React Native) as a storageBackend.`);
31 | }
32 |
33 | this._mapPromise = this.getItem('map').then(map => {
34 | this._m = this._checkMap((map && JSON.parse(map)) || {});
35 | });
36 | }
37 | getItem(key) {
38 | return this._s
39 | ? this.isPromise
40 | ? this._s.getItem(key)
41 | : Promise.resolve(this._s.getItem(key))
42 | : Promise.resolve();
43 | }
44 | setItem(key, value) {
45 | return this._s
46 | ? this.isPromise
47 | ? this._s.setItem(key, value)
48 | : Promise.resolve(this._s.setItem(key, value))
49 | : Promise.resolve();
50 | }
51 | removeItem(key) {
52 | return this._s
53 | ? this.isPromise
54 | ? this._s.removeItem(key)
55 | : Promise.resolve(this._s.removeItem(key))
56 | : Promise.resolve();
57 | }
58 | _initMap() {
59 | return {
60 | innerVersion: this._innerVersion,
61 | index: 0,
62 | __keys__: {},
63 | };
64 | }
65 | _checkMap(map) {
66 | if (map && map.innerVersion && map.innerVersion === this._innerVersion) {
67 | return map;
68 | } else {
69 | return this._initMap();
70 | }
71 | }
72 | _getId(key, id) {
73 | return key + '_' + id;
74 | }
75 | _saveToMap(params) {
76 | let { key, id, data } = params,
77 | newId = this._getId(key, id),
78 | m = this._m;
79 | if (m[newId] !== undefined) {
80 | // update existing data
81 | if (this.enableCache) this.cache[newId] = JSON.parse(data);
82 | return this.setItem('map_' + m[newId], data);
83 | }
84 | if (m[m.index] !== undefined) {
85 | // loop over, delete old data
86 | let oldId = m[m.index];
87 | let splitOldId = oldId.split('_');
88 | delete m[oldId];
89 | this._removeIdInKey(splitOldId[0], splitOldId[1]);
90 | if (this.enableCache) {
91 | delete this.cache[oldId];
92 | }
93 | }
94 | m[newId] = m.index;
95 | m[m.index] = newId;
96 |
97 | m.__keys__[key] = m.__keys__[key] || [];
98 | m.__keys__[key].push(id);
99 |
100 | if (this.enableCache) {
101 | const cacheData = JSON.parse(data);
102 | this.cache[newId] = cacheData;
103 | }
104 | let currentIndex = m.index;
105 | if (++m.index === this._SIZE) {
106 | m.index = 0;
107 | }
108 | this.setItem('map_' + currentIndex, data);
109 | this.setItem('map', JSON.stringify(m));
110 | }
111 | save(params) {
112 | const { key, id, data, rawData, expires = this.defaultExpires } = params;
113 | if (key.toString().indexOf('_') !== -1) {
114 | console.error('Please do not use "_" in key!');
115 | }
116 | let dataToSave = { rawData: data };
117 | if (data === undefined) {
118 | if (rawData !== undefined) {
119 | console.warn('"rawData" is deprecated, please use "data" instead!');
120 | dataToSave.rawData = rawData;
121 | } else {
122 | console.error('"data" is required in save()!');
123 | return;
124 | }
125 | }
126 | let now = Date.now();
127 | if (expires !== null) {
128 | dataToSave.expires = now + expires;
129 | }
130 | dataToSave = JSON.stringify(dataToSave);
131 | if (id === undefined) {
132 | if (this.enableCache) {
133 | const cacheData = JSON.parse(dataToSave);
134 | this.cache[key] = cacheData;
135 | }
136 | return this.setItem(key, dataToSave);
137 | } else {
138 | if (id.toString().indexOf('_') !== -1) {
139 | console.error('Please do not use "_" in id!');
140 | }
141 | return this._mapPromise.then(() =>
142 | this._saveToMap({
143 | key,
144 | id,
145 | data: dataToSave,
146 | }),
147 | );
148 | }
149 | }
150 | getBatchData(querys) {
151 | return Promise.all(querys.map(query => this.load(query)));
152 | }
153 | async getBatchDataWithIds(params) {
154 | let { key, ids, syncInBackground, syncParams } = params;
155 | const tasks = ids.map(id =>
156 | this.load({
157 | key,
158 | id,
159 | syncInBackground,
160 | autoSync: false,
161 | batched: true,
162 | }),
163 | );
164 | const results = await Promise.all(tasks);
165 | const missingIds = [];
166 | results.forEach(value => {
167 | if (value.syncId !== undefined) {
168 | missingIds.push(value.syncId);
169 | }
170 | });
171 | if (missingIds.length) {
172 | const syncData = await this.sync[key]({
173 | id: missingIds,
174 | syncParams,
175 | });
176 | return results.map(value => {
177 | return value.syncId ? syncData.shift() : value;
178 | });
179 | } else {
180 | return results;
181 | }
182 | }
183 | _lookupGlobalItem(params) {
184 | const { key } = params;
185 | if (this.enableCache && this.cache[key] !== undefined) {
186 | return this._loadGlobalItem({ ret: this.cache[key], ...params });
187 | }
188 | return this.getItem(key).then(ret => this._loadGlobalItem({ ret, ...params }));
189 | }
190 | _loadGlobalItem(params) {
191 | let { key, ret, autoSync, syncInBackground, syncParams } = params;
192 | if (ret === null || ret === undefined) {
193 | if (autoSync && this.sync[key]) {
194 | return this.sync[key]({ syncParams });
195 | }
196 | throw new NotFoundError(JSON.stringify(params));
197 | }
198 | if (typeof ret === 'string') {
199 | ret = JSON.parse(ret);
200 | if (this.enableCache) {
201 | this.cache[key] = ret;
202 | }
203 | }
204 | let now = Date.now();
205 | if (ret.expires < now) {
206 | if (autoSync && this.sync[key]) {
207 | if (syncInBackground) {
208 | try {
209 | this.sync[key]({ syncParams, syncInBackground });
210 | } catch (e) {
211 | // avoid uncaught exception
212 | }
213 | return ret.rawData;
214 | }
215 | return this.sync[key]({ syncParams, syncInBackground });
216 | }
217 | throw new ExpiredError(JSON.stringify(params));
218 | }
219 | return ret.rawData;
220 | }
221 | _noItemFound(params) {
222 | let { key, id, autoSync, syncParams } = params;
223 | if (this.sync[key]) {
224 | if (autoSync) {
225 | return this.sync[key]({ id, syncParams });
226 | }
227 | return { syncId: id };
228 | }
229 | throw new NotFoundError(JSON.stringify(params));
230 | }
231 | _loadMapItem(params) {
232 | let { ret, key, id, autoSync, batched, syncInBackground, syncParams } = params;
233 | if (ret === null || ret === undefined) {
234 | return this._noItemFound(params);
235 | }
236 | if (typeof ret === 'string') {
237 | ret = JSON.parse(ret);
238 | const { key, id } = params;
239 | const newId = this._getId(key, id);
240 | if (this.enableCache) {
241 | this.cache[newId] = ret;
242 | }
243 | }
244 | let now = Date.now();
245 | if (ret.expires < now) {
246 | if (autoSync && this.sync[key]) {
247 | if (syncInBackground) {
248 | try {
249 | this.sync[key]({ id, syncParams, syncInBackground });
250 | } catch (e) {
251 | // avoid uncaught exception
252 | }
253 | return ret.rawData;
254 | }
255 | return this.sync[key]({ id, syncParams, syncInBackground });
256 | }
257 | if (batched) {
258 | return { syncId: id };
259 | }
260 | throw new ExpiredError(JSON.stringify(params));
261 | }
262 | return ret.rawData;
263 | }
264 | _lookUpInMap(params) {
265 | let ret;
266 | const m = this._m;
267 | const { key, id } = params;
268 | const newId = this._getId(key, id);
269 | if (this.enableCache && this.cache[newId]) {
270 | ret = this.cache[newId];
271 | return this._loadMapItem({ ret, ...params });
272 | }
273 | if (m[newId] !== undefined) {
274 | return this.getItem('map_' + m[newId]).then(ret => this._loadMapItem({ ret, ...params }));
275 | }
276 | return this._noItemFound({ ret, ...params });
277 | }
278 | remove(params) {
279 | return this._mapPromise.then(() => {
280 | let m = this._m;
281 | let { key, id } = params;
282 |
283 | if (id === undefined) {
284 | if (this.enableCache && this.cache[key]) {
285 | delete this.cache[key];
286 | }
287 | return this.removeItem(key);
288 | }
289 | let newId = this._getId(key, id);
290 |
291 | // remove existing data
292 | if (m[newId] !== undefined) {
293 | if (this.enableCache && this.cache[newId]) {
294 | delete this.cache[newId];
295 | }
296 | this._removeIdInKey(key, id);
297 | let idTobeDeleted = m[newId];
298 | delete m[newId];
299 | this.setItem('map', JSON.stringify(m));
300 | return this.removeItem('map_' + idTobeDeleted);
301 | }
302 | });
303 | }
304 | _removeIdInKey(key, id) {
305 | const indexTobeRemoved = (this._m.__keys__[key] || []).indexOf(id);
306 | if (indexTobeRemoved !== -1) {
307 | this._m.__keys__[key].splice(indexTobeRemoved, 1);
308 | }
309 | }
310 | load(params) {
311 | const { key, id, autoSync = true, syncInBackground = true, syncParams, batched } = params;
312 | return this._mapPromise.then(() => {
313 | if (id === undefined) {
314 | return this._lookupGlobalItem({
315 | key,
316 | autoSync,
317 | syncInBackground,
318 | syncParams,
319 | });
320 | } else {
321 | return this._lookUpInMap({
322 | key,
323 | id,
324 | autoSync,
325 | syncInBackground,
326 | batched,
327 | syncParams,
328 | });
329 | }
330 | });
331 | }
332 | clearAll() {
333 | this._s.clear && this._s.clear();
334 | this._m = this._initMap();
335 | }
336 | clearMap() {
337 | return this.removeItem('map').then(() => {
338 | this.cache = {};
339 | this._m = this._initMap();
340 | });
341 | }
342 | clearMapForKey(key) {
343 | return this._mapPromise.then(() => {
344 | let tasks = (this._m.__keys__[key] || []).map(id => this.remove({ key, id }));
345 | return Promise.all(tasks);
346 | });
347 | }
348 | getIdsForKey(key) {
349 | return this._mapPromise.then(() => {
350 | return this._m.__keys__[key] || [];
351 | });
352 | }
353 | getAllDataForKey(key, options) {
354 | options = Object.assign({ syncInBackground: true }, options);
355 | return this.getIdsForKey(key).then(ids => {
356 | const querys = ids.map(id => ({ key, id, syncInBackground: options.syncInBackground }));
357 | return this.getBatchData(querys);
358 | });
359 | }
360 | }
361 |
--------------------------------------------------------------------------------
/tea.yaml:
--------------------------------------------------------------------------------
1 | # https://tea.xyz/what-is-this-file
2 | ---
3 | version: 1.0.0
4 | codeOwners:
5 | - '0x10D90dC0034E2e82F0AC55954B3ed4EC0550ECe7'
6 | quorum: 1
7 |
--------------------------------------------------------------------------------