├── .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 on Open Collective](https://opencollective.com/react-native-storage/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/react-native-storage/sponsors/badge.svg)](#sponsors) [![Build Status](https://travis-ci.org/sunnylqm/react-native-storage.svg)](https://travis-ci.org/sunnylqm/react-native-storage) ![npm version](https://img.shields.io/npm/v/react-native-storage.svg) 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 [![Build Status](https://travis-ci.org/sunnylqm/react-native-storage.svg)](https://travis-ci.org/sunnylqm/react-native-storage) ![npm version](https://img.shields.io/npm/v/react-native-storage.svg) 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 | --------------------------------------------------------------------------------