react-native-storage
Advanced tools
Comparing version 0.0.9 to 0.0.10
@@ -16,3 +16,3 @@ jest.dontMock('../storage.js'); | ||
}); | ||
it('saves and loads any type of data with keys', () => { | ||
pit('saves and loads any type of data', () => { | ||
let testCases = { | ||
@@ -32,79 +32,45 @@ testNumber: 11221, | ||
}; | ||
let returnCases = {}; | ||
let asyncCount = 0; | ||
let returnCases = {}, returnCasesWithId = {}; | ||
let tasks = []; | ||
for(let key in testCases) { | ||
storage.save({ | ||
key, | ||
rawData: testCases[key] | ||
}); | ||
tasks.push( | ||
storage.save({ | ||
key, | ||
rawData: testCases[key] | ||
}).then(() => | ||
storage.load({ | ||
key | ||
}).then( ret => { | ||
returnCases[key] = ret; | ||
}) | ||
) | ||
); | ||
tasks.push( | ||
storage.save({ | ||
key, | ||
id: 1, | ||
rawData: testCases[key] | ||
}).then(() => | ||
storage.load({ | ||
key, | ||
id: 1 | ||
}).then( ret => { | ||
returnCasesWithId[key] = ret; | ||
}) | ||
) | ||
); | ||
} | ||
runs(() => { | ||
return Promise.all(tasks).then(() => { | ||
for(let key in testCases) { | ||
storage.load({ | ||
key | ||
}).then( ret => { | ||
returnCases[key] = ret; | ||
asyncCount++; | ||
}); | ||
} | ||
}); | ||
waitsFor(() => { | ||
return asyncCount === 7; | ||
}, 'Values should be loaded', 1000); | ||
runs(() => { | ||
for(let key in testCases) { | ||
expect(JSON.stringify(testCases[key])).toBe(JSON.stringify(returnCases[key])); | ||
expect(JSON.stringify(testCases[key])).toBe(JSON.stringify(returnCasesWithId[key])); | ||
} | ||
}); | ||
}); | ||
it('saves and loads any type of data with keys and ids', () => { | ||
let testCases = { | ||
testNumber: 11221, | ||
testString: 'testString', | ||
testObject: { | ||
fname: 'foo', | ||
lname: 'bar' | ||
}, | ||
testArray: [ 'one', 'two', 'three' ], | ||
testBoolean: false, | ||
testNull: null, | ||
complexObject: { | ||
complexArray : [ 1, 2, 3, 'test', { a: 'b' } ] | ||
} | ||
}; | ||
let returnCases = {}; | ||
let asyncCount = 0; | ||
for(let key in testCases) { | ||
storage.save({ | ||
key, | ||
id: 1, | ||
rawData: testCases[key] | ||
}); | ||
} | ||
runs(() => { | ||
for(let key in testCases) { | ||
storage.load({ | ||
key, | ||
id: 1 | ||
}).then( ret => { | ||
returnCases[key] = ret; | ||
asyncCount++; | ||
}); | ||
} | ||
}); | ||
waitsFor(() => { | ||
return asyncCount === 7; | ||
}, 'Values should be loaded', 1000); | ||
runs(() => { | ||
for(let key in testCases) { | ||
expect(JSON.stringify(testCases[key])).toBe(JSON.stringify(returnCases[key])); | ||
} | ||
}); | ||
}); | ||
it('rejects when no data found and no sync method', () => { | ||
pit('rejects when no data found and no sync method', () => { | ||
let testKey1 = 'testKey' + Math.random(), | ||
testKey2 = 'testKey' + Math.random(), | ||
testId2 = 'testId' + Math.random(); | ||
let ret1, ret2, reject1, reject2, done1, done2; | ||
runs(() => { | ||
let ret1, ret2, reject1, reject2; | ||
let tasks = [ | ||
storage.load({ | ||
@@ -114,7 +80,5 @@ key: testKey1 | ||
ret1 = ret; | ||
done1 = true; | ||
}).catch( () => { | ||
reject1 = true; | ||
done1 = true; | ||
}); | ||
}), | ||
storage.load({ | ||
@@ -125,124 +89,158 @@ key: testKey2, | ||
ret2 = ret; | ||
done2 = true; | ||
}).catch( () => { | ||
reject2 = true; | ||
done2 = true; | ||
}); | ||
}); | ||
waitsFor(() => { | ||
return done1 && done2; | ||
}, 'Values should be loaded', 1000); | ||
runs(() => { | ||
expect(ret1).toBe(undefined); | ||
expect(ret2).toBe(undefined); | ||
}) | ||
]; | ||
return Promise.all(tasks).then(() => { | ||
expect(ret1).toBeUndefined(); | ||
expect(ret2).toBeUndefined(); | ||
expect(reject1 && reject2).toBe(true); | ||
}); | ||
}); | ||
it('overwrites "key+id" data when loops over(exceeds SIZE)', () => { | ||
let testKey = 'testKey' + Math.random(), | ||
testId = 'testId' + Math.random(), | ||
testData = 'testData' + Math.random(); | ||
let ret1, ret2, done1, done2, tmpIndex1, tmpIndex2; | ||
runs(() => { | ||
storage.save({ | ||
key: testKey, | ||
id: testId, | ||
rawData: testData | ||
}); | ||
tmpIndex1 = storage._m.index; | ||
for (let i = 0; i < SIZE - 1; i++) { | ||
storage.save({ | ||
key: 'testKey' + Math.random(), | ||
id: 'testId' + Math.random(), | ||
rawData: 'testData' + Math.random() | ||
//it('overwrites "key+id" data when loops over(exceeds SIZE)', () => { | ||
// let testKey = 'testKey' + Math.random(), | ||
// testId = 'testId' + Math.random(), | ||
// testData = 'testData' + Math.random(); | ||
// let ret1, ret2, done1, done2, tmpIndex1, tmpIndex2; | ||
// runs(() => { | ||
// storage.save({ | ||
// key: testKey, | ||
// id: testId, | ||
// rawData: testData | ||
// }); | ||
// tmpIndex1 = storage._m.index; | ||
// for (let i = 0; i < SIZE - 1; i++) { | ||
// storage.save({ | ||
// key: 'testKey' + Math.random(), | ||
// id: 'testId' + Math.random(), | ||
// rawData: 'testData' + Math.random() | ||
// }); | ||
// } | ||
// | ||
// //not overwrited yet | ||
// storage.load({ | ||
// key: testKey, | ||
// id: testId | ||
// }).then( ret => { | ||
// ret1 = ret; | ||
// done1 = true; | ||
// }).catch(() => { | ||
// done1 = true; | ||
// }); | ||
// | ||
// //overwrite | ||
// storage.save({ | ||
// key: 'testKey' + Math.random(), | ||
// id: 'testId' + Math.random(), | ||
// rawData: 'testData' + Math.random() | ||
// }); | ||
// tmpIndex2 = storage._m.index; | ||
// storage.load({ | ||
// key: testKey, | ||
// id: testId | ||
// }).then( ret => { | ||
// ret2 = ret; | ||
// done2 = true; | ||
// }).catch(() => { | ||
// done2 = true; | ||
// }); | ||
// }); | ||
// waitsFor(() => { | ||
// return done1 && done2; | ||
// }, 'Values should be loaded', 1000); | ||
// | ||
// runs(() => { | ||
// expect(tmpIndex1).toBe(tmpIndex2); | ||
// expect(ret1).toBe(testData); | ||
// expect(ret2).toNotBe(testData); | ||
// }); | ||
//}); | ||
//it('ignores all "key+id" data when innerVersion mismatched', () => { | ||
// let testKey = 'testKey' + Math.random(), | ||
// testId = 'testId' + Math.random(), | ||
// testData = 'testData' + Math.random(); | ||
// let ret1, ret2, done1, done2, tmpVersion; | ||
// runs(() => { | ||
// storage.save({ | ||
// key: testKey, | ||
// id: testId, | ||
// rawData: testData | ||
// }); | ||
// storage.load({ | ||
// key: testKey, | ||
// id: testId | ||
// }).then( ret => { | ||
// ret1 = ret; | ||
// done1 = true; | ||
// }).catch(() => { | ||
// done1 = true; | ||
// }); | ||
// | ||
// let map = JSON.parse(storage._s.getItem('map')); | ||
// tmpVersion = map.innerVersion; | ||
// map.innerVersion = -1; | ||
// storage._s.setItem('map', JSON.stringify(map)); | ||
// | ||
// let newStorage = new Storage(); | ||
// newStorage.load({ | ||
// key: testKey, | ||
// id: testId | ||
// }).then(ret => { | ||
// ret2 = ret; | ||
// done2 = true; | ||
// }).catch(() => { | ||
// done2 = true; | ||
// }); | ||
// }); | ||
// waitsFor(() => { | ||
// return done1 && done2; | ||
// }, 'Values should be loaded', 1000); | ||
// runs(() => { | ||
// expect(ret1).toBe(testData); | ||
// expect(ret2).toBeUndefined(); | ||
// | ||
// let newMap = JSON.parse(storage._s.getItem('map')); | ||
// newMap.innerVersion = tmpVersion; | ||
// storage._s.setItem('map', JSON.stringify(newMap)); | ||
// }); | ||
//}); | ||
pit('removes data correctly', () => { | ||
let testKey1 = 'testKey1' + Math.random(), | ||
testKey2 = 'testKey2' + Math.random(), | ||
testId2 = 'testId2' + Math.random(), | ||
testData1 = 'testData1' + Math.random(), | ||
testData2 = 'testData2' + Math.random(); | ||
let ret1 = [undefined, undefined], ret2 = [undefined, undefined]; | ||
let task = (key, id, rawData, retArray) => { | ||
return storage.save({ | ||
key, | ||
id, | ||
rawData | ||
}).then(() => { | ||
return storage.load({ | ||
key, | ||
id | ||
}); | ||
} | ||
//not overwrited yet | ||
storage.load({ | ||
key: testKey, | ||
id: testId | ||
}).then( ret => { | ||
ret1 = ret; | ||
done1 = true; | ||
}).catch(() => { | ||
done1 = true; | ||
}); | ||
//overwrite | ||
storage.save({ | ||
key: 'testKey' + Math.random(), | ||
id: 'testId' + Math.random(), | ||
rawData: 'testData' + Math.random() | ||
}); | ||
tmpIndex2 = storage._m.index; | ||
storage.load({ | ||
key: testKey, | ||
id: testId | ||
retArray[0] = ret; | ||
return storage.remove({ key, id }); | ||
}).then( () => { | ||
return storage.load({ key, id }); | ||
}).then( ret => { | ||
ret2 = ret; | ||
done2 = true; | ||
}).catch(() => { | ||
done2 = true; | ||
}); | ||
retArray[1] = ret; | ||
}).catch( () => { | ||
retArray[1] = 'catched'; | ||
});; | ||
}; | ||
return Promise.all([ | ||
task(testKey1, undefined, testData1, ret1), | ||
task(testKey2, testId2, testData2, ret2) | ||
]).then(() => { | ||
expect(ret1[0]).toBe(testData1); | ||
expect(ret1[1]).toBe('catched'); | ||
expect(ret2[0]).toBe(testData2); | ||
expect(ret2[1]).toBe('catched'); | ||
}); | ||
waitsFor(() => { | ||
return done1 && done2; | ||
}, 'Values should be loaded', 1000); | ||
runs(() => { | ||
expect(tmpIndex1).toBe(tmpIndex2); | ||
expect(ret1).toBe(testData); | ||
expect(ret2).toNotBe(testData); | ||
}); | ||
}); | ||
it('ignores all "key+id" data when innerVersion mismatched', () => { | ||
let testKey = 'testKey' + Math.random(), | ||
testId = 'testId' + Math.random(), | ||
testData = 'testData' + Math.random(); | ||
let ret1, ret2, done1, done2, tmpVersion; | ||
runs(() => { | ||
storage.save({ | ||
key: testKey, | ||
id: testId, | ||
rawData: testData | ||
}); | ||
storage.load({ | ||
key: testKey, | ||
id: testId | ||
}).then( ret => { | ||
ret1 = ret; | ||
done1 = true; | ||
}).catch(() => { | ||
done1 = true; | ||
}); | ||
let map = JSON.parse(storage._s.getItem('map')); | ||
tmpVersion = map.innerVersion; | ||
map.innerVersion = -1; | ||
storage._s.setItem('map', JSON.stringify(map)); | ||
let newStorage = new Storage(); | ||
newStorage.load({ | ||
key: testKey, | ||
id: testId | ||
}).then(ret => { | ||
ret2 = ret; | ||
done2 = true; | ||
}).catch(() => { | ||
done2 = true; | ||
}); | ||
}); | ||
waitsFor(() => { | ||
return done1 && done2; | ||
}, 'Values should be loaded', 1000); | ||
runs(() => { | ||
expect(ret1).toBe(testData); | ||
expect(ret2).toBeUndefined(); | ||
let newMap = JSON.parse(storage._s.getItem('map')); | ||
newMap.innerVersion = tmpVersion; | ||
storage._s.setItem('map', JSON.stringify(newMap)); | ||
}); | ||
}); | ||
}); |
@@ -314,3 +314,4 @@ /** | ||
let { resolve } = params; | ||
resolve && resolve(testData3); | ||
// when id is an array, the return value should be an ordered array too | ||
resolve && resolve([testData3]); | ||
}); | ||
@@ -317,0 +318,0 @@ storage.sync[testKey] = sync; |
{ | ||
"name": "react-native-storage", | ||
"version": "0.0.9", | ||
"version": "0.0.10", | ||
"description": "This is a local storage wrapper for both react-native(AsyncStorage) and browser(localStorage). ES6/babel is needed.", | ||
@@ -30,4 +30,4 @@ "main": "storage.js", | ||
"babel-jest": "^5.3.0", | ||
"jest-cli": "^0.5.10" | ||
"jest-cli": "^0.8.0" | ||
} | ||
} |
433
README.md
@@ -1,222 +0,267 @@ | ||
# react-native-storage | ||
This is a local storage wrapper for both react-native(AsyncStorage) and browser(localStorage). [ES6](http://babeljs.io/docs/learn-es2015/) syntax, promise for async load, fully tested with jest. | ||
You may need a [Promise polyfill](https://github.com/jakearchibald/es6-promise) for [legacy iOS devices/browsers](http://caniuse.com/#search=promise). | ||
# 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://badge.fury.io/js/react-native-storage.svg)](http://badge.fury.io/js/react-native-storage) | ||
这是一个本地持久存储的封装,可以同时支持react-native(AsyncStorage)和浏览器(localStorage)。ES6语法,promise异步读取,使用jest进行了完整的单元测试。由于代码使用ES6语法编写,因而需要[babel库](http://babeljs.io/docs/setup/#browserify)的支持。 | ||
如果iOS设备或浏览器版本较老(不支持[Promise](http://caniuse.com/#search=promise)),则还需要一个Promise的[兼容库](https://github.com/jakearchibald/es6-promise)。 | ||
This is a local storage wrapper for both react-native(AsyncStorage) and browser(localStorage). [ES6](http://babeljs.io/docs/learn-es2015/) syntax, promise for async load, fully tested with jest. | ||
## Install 安装 | ||
查看中文文档[请点击README-CHN.md](README-CHN.md) | ||
## Install | ||
npm install react-native-storage --save | ||
## Usage 使用说明 | ||
### Config 配置 | ||
You need to use [babel](https://babeljs.io/) to enable es6 modules for web development(I'll provide an example in the next version). For React-Native development, put this [babel config file](https://github.com/brentvatne/react-native-animated-demo-tinder/blob/master/.babelrc) under your project directory. | ||
对于Web开发你需要使用[babel](https://babeljs.io/)来支持es6模块导入功能。 | ||
如果是React-Native开发,把这个[babel配置文件](https://github.com/brentvatne/react-native-animated-demo-tinder/blob/master/.babelrc)放到你的项目根目录中即可。 | ||
## Usage | ||
### Import 导入 | ||
var Storage = require('react-native-storage'); | ||
### Config | ||
#### For Web | ||
You need to use [webpack](http://webpack.github.io/) and [babel](https://babeljs.io/) to enable es6 modules for web development. | ||
You should add the following lines to your webpack config: | ||
or 或者 | ||
```javascript | ||
// ... | ||
externals: { | ||
"react-native": {} // This line is required! Otherwise an error would be thrown. | ||
}, | ||
module: { | ||
loaders: [ | ||
// ... | ||
{ | ||
test: /\.js?$/, | ||
include: [ | ||
//path.join(__dirname, 'your-own-js-files'), | ||
//path.join(__dirname, 'node_modules/some-other-lib-that-needs-babel'), | ||
path.join(__dirname, 'node_modules/react-native-storage') | ||
], | ||
loader: 'babel', | ||
query: { | ||
cacheDirectory: true, | ||
presets: ['es2015', 'stage-1', 'react'], | ||
plugins: ['transform-runtime'] | ||
} | ||
} | ||
] | ||
} | ||
import Storage from 'react-native-storage'; | ||
``` | ||
#### For React Native | ||
You don't have to configure anything(but require react native version >= 0.13). | ||
### Import | ||
```bash | ||
import Storage from 'react-native-storage'; | ||
``` | ||
Do not use `require('react-native-storage')`, which would cause error in version 0.16. | ||
### Init | ||
```js | ||
var storage = new Storage({ | ||
// maximum capacity, default 1000 | ||
size: 1000, | ||
### Init 初始化 | ||
var storage = new Storage({ | ||
//maximum capacity, default 1000 | ||
//最大容量,默认值1000条数据循环存储 | ||
size: 1000, | ||
//expire time, default 1 day(1000 * 3600 * 24 secs) | ||
//数据过期时间,默认一整天(1000 * 3600 * 24秒) | ||
defaultExpires: 1000 * 3600 * 24, | ||
//cache data in the memory. default is true. | ||
//读写时在内存中缓存数据。默认启用。 | ||
enableCache: true, | ||
//if data was not found in storage or expired, | ||
//the corresponding sync method will be invoked and return | ||
//the latest data. | ||
//如果storage中没有相应数据,或数据已过期, | ||
//则会调用相应的sync同步方法,无缝返回最新数据。 | ||
sync : { | ||
//we'll talk about the details later. | ||
//同步方法的具体说明会在后文提到 | ||
} | ||
}) | ||
// expire time, default 1 day(1000 * 3600 * 24 secs) | ||
defaultExpires: 1000 * 3600 * 24, | ||
//I suggest you have one(and only one) storage instance in global scope. | ||
//最好在全局范围内创建一个(且只有一个)storage实例,方便使用 | ||
// cache data in the memory. default is true. | ||
enableCache: true, | ||
//for web | ||
//window.storage = storage; | ||
// if data was not found in storage or expired, | ||
// the corresponding sync method will be invoked and return | ||
// the latest data. | ||
sync : { | ||
// we'll talk about the details later. | ||
} | ||
}) | ||
// I suggest you have one(and only one) storage instance in global scope. | ||
// for web | ||
// window.storage = storage; | ||
// for react native | ||
// global.storage = storage; | ||
``` | ||
### Save & Load & Remove | ||
```js | ||
// Save something with key only. | ||
// Something more unique, and constantly being used. | ||
// They are perminently stored unless you remove. | ||
// Even expires, the data won't be removed. Only sync method would be invoked. | ||
storage.save({ | ||
key: 'loginState', // Note: Do not use underscore("_") in key! | ||
rawData: { | ||
from: 'some other site', | ||
userid: 'some userid', | ||
token: 'some token' | ||
}, | ||
//for react native | ||
//global.storage = storage; | ||
//or CMD | ||
//module.exports = storage; | ||
// if not specified, the defaultExpires will be applied instead. | ||
// if set to null, then it will never expires. | ||
expires: 1000 * 3600 | ||
}); | ||
### Save & Load 保存和读取 | ||
//Save something with key only. | ||
//Something more unique, and constantly being used. | ||
//They are perminently stored unless you remove. | ||
//Even expires, the data won't be removed. Only sync method would be invoked. | ||
//使用key来保存数据。这些数据一般是全局独有的,常常需要调用的。 | ||
//除非你手动移除,这些数据会被永久保存,而且默认不会过期。 | ||
//即便指定了且达到了过期时间,数据也不会被删除,而只是触发调用同步方法。 | ||
storage.save({ | ||
key: 'loginState', | ||
rawData: { | ||
from: 'some other site', | ||
userid: 'some userid', | ||
token: 'some token' | ||
}, | ||
//if not specified, the defaultExpires will be applied instead. | ||
//if set to null, then it will never expires. | ||
//如果不指定过期时间,则会使用defaultExpires参数 | ||
//如果设为null,则永不过期 | ||
expires: 1000 * 3600 | ||
}); | ||
// load | ||
storage.load({ | ||
key: 'loginState', | ||
//load 读取 | ||
storage.load({ | ||
key: 'loginState', | ||
//autoSync(default true) means if data not found or expired, | ||
//then invoke the corresponding sync method | ||
//autoSync(默认为true)意味着在没有找到数据或数据过期时自动调用相应的同步方法 | ||
autoSync: true, | ||
//syncInBackground(default true) means if data expired, | ||
//return the outdated data first while invoke the sync method. | ||
//It can be set to false to always return data provided by sync method when expired.(Of course it's slower) | ||
//syncInBackground(默认为true)意味着如果数据过期, | ||
//在调用同步方法的同时先返回已经过期的数据。 | ||
//设置为false的话,则始终强制返回同步方法提供的最新数据(当然会需要更多等待时间)。 | ||
syncInBackground: true | ||
}).then( ret => { //found data goes to then() | ||
console.log(ret.userid); //如果找到数据,则在then方法中返回 | ||
}).catch( err => { //any exception including data not found | ||
console.warn(err); //goes to catch() | ||
//如果没有找到数据且没有同步方法, | ||
//或者有其他异常,则在catch中返回 | ||
}) | ||
// autoSync(default true) means if data not found or expired, | ||
// then invoke the corresponding sync method | ||
autoSync: true, | ||
__________________________________________________________________ | ||
//Save something with key and id. Something of the same type(key). | ||
//There is a quota over "key-id" data(the size parameter you pass in constructor). | ||
//By default the 1001th data will overwrite the 1st data. | ||
//If you then load the 1st data, a catch(data not found) or sync will be invoked. | ||
//使用key和id来保存数据,一般是保存同类别(key)的大量数据。 | ||
//这些"key-id"数据有一个保存上限,即在初始化storage时传入的size参数。 | ||
//在默认上限参数下,第1001个数据会覆盖第1个数据。 | ||
//覆盖之后,再读取第1个数据,会返回catch或是相应的同步方法。 | ||
var userA = { | ||
name: 'A', | ||
age: 20, | ||
tags: [ | ||
'geek', | ||
'nerd', | ||
'otaku' | ||
] | ||
}; | ||
storage.save({ | ||
key: 'user', | ||
id: '1001', | ||
rawData: userA, | ||
expires: 1000 * 60 | ||
}); | ||
//load 读取 | ||
storage.load({ | ||
key: 'user' | ||
id: '1001' | ||
}).then( ret => { //found data goes to then() | ||
console.log(ret.userid); //如果找到数据,则在then方法中返回 | ||
}).catch( err => { //any exception including data not found | ||
console.warn(err); //goes to catch() | ||
//如果没有找到数据且没有同步方法, | ||
//或者有其他异常,则在catch中返回 | ||
}) | ||
// syncInBackground(default true) means if data expired, | ||
// return the outdated data first while invoke the sync method. | ||
// It can be set to false to always return data provided by sync method when expired.(Of course it's slower) | ||
syncInBackground: true | ||
}).then( ret => { | ||
// found data goes to then() | ||
console.log(ret.userid); | ||
}).catch( err => { | ||
// any exception including data not found | ||
// goes to catch() | ||
console.warn(err); | ||
}) | ||
// -------------------------------------------------- | ||
### Sync remote data(refresh) 同步远程数据(刷新) | ||
// Save something with key and id. Something of the same type(key). | ||
// There is a quota over "key-id" data(the size parameter you pass in constructor). | ||
// By default the 1001th data will overwrite the 1st data. | ||
// If you then load the 1st data, a catch(data not found) or sync will be invoked. | ||
var userA = { | ||
name: 'A', | ||
age: 20, | ||
tags: [ | ||
'geek', | ||
'nerd', | ||
'otaku' | ||
] | ||
}; | ||
storage.save({ | ||
key: 'user', // Note: Do not use underscore("_") in key! | ||
id: '1001', // Note: Do not use underscore("_") in id! | ||
rawData: userA, | ||
expires: 1000 * 60 | ||
}); | ||
// load | ||
storage.load({ | ||
key: 'user' | ||
id: '1001' | ||
}).then( ret => { | ||
// found data goes to then() | ||
console.log(ret.userid); | ||
}).catch( err => { | ||
// any exception including data not found | ||
// goes to catch() | ||
console.warn(err); | ||
}) | ||
// -------------------------------------------------- | ||
//remove single record | ||
storage.remove({ | ||
key: 'lastPage' | ||
}); | ||
storage.remove({ | ||
key: 'user' | ||
id: '1001' | ||
}); | ||
//!! clear map and remove all key-id data (but keep the key-only data) | ||
storage.clearMap(); | ||
``` | ||
### Sync remote data(refresh) | ||
You can pass sync methods as one object parameter to the storage constructor, but also you can add it any time. | ||
storage.sync = { | ||
//The name of the sync method must be the same of the data's key | ||
//And the passed params will be an all-in-one object. | ||
//同步方法的名字必须和所存数据的key完全相同 | ||
//方法接受的参数为一整个object,所有参数从object中解构取出 | ||
user(params){ | ||
let { id, resolve, reject } = params; | ||
fetch('user/', { | ||
method: 'GET', | ||
body: 'id=' + id | ||
}).then( response => { | ||
return response.json(); | ||
}).then( json => { | ||
//console.log(json); | ||
if(json && json.user){ | ||
storage.save({ | ||
key: 'user', | ||
id, | ||
rawData: json.user | ||
}); | ||
resolve && resolve(json.user); | ||
} | ||
else{ | ||
reject && reject('data parse error'); | ||
} | ||
}).catch( err => { | ||
console.warn(err); | ||
reject && reject(err); | ||
}); | ||
} | ||
```js | ||
storage.sync = { | ||
// The name of the sync method must be the same of the data's key | ||
// And the passed params will be an all-in-one object. | ||
// You can use promise here. | ||
// Or plain callback function with resolve/reject, like: | ||
user(params){ | ||
let { id, resolve, reject } = params; | ||
fetch('user/', { | ||
method: 'GET', | ||
body: 'id=' + id | ||
}).then( response => { | ||
return response.json(); | ||
}).then( json => { | ||
// console.log(json); | ||
if(json && json.user){ | ||
storage.save({ | ||
key: 'user', | ||
id, | ||
rawData: json.user | ||
}); | ||
// Call resolve() when succeed | ||
resolve && resolve(json.user); | ||
} | ||
else{ | ||
// Call reject() when failed | ||
reject && reject('data parse error'); | ||
} | ||
}).catch( err => { | ||
console.warn(err); | ||
reject && reject(err); | ||
}); | ||
} | ||
} | ||
``` | ||
With this example sync method, when you invoke: | ||
storage.load({ | ||
key: 'user', | ||
id: '1002' | ||
}).then(...) | ||
```js | ||
storage.load({ | ||
key: 'user', | ||
id: '1002' | ||
}).then(...) | ||
``` | ||
If there is no user 1002 stored currently, then storage.sync.user would be invoked to fetch remote data and returned. | ||
有了上面这个sync方法,以后再调用storage.load时,如果本地并没有存储相应的user,那么会自动触发storage.sync.user去远程取回数据并无缝返回。 | ||
### Load batch data | ||
### Load batch data 读取批量数据 | ||
```js | ||
// Load batch data with the same parameters as storage.load, but in an array. | ||
// It will invoke sync methods on demand, | ||
// and finally return them all in an ordered array. | ||
//Load batch data with the same parameters as storage.load, but in an array. | ||
//It will invoke sync methods on demand, | ||
//and finally return them all in an ordered array. | ||
//使用和load方法一样的参数读取批量数据,但是参数是以数组的方式提供。 | ||
//会在需要时分别调用相应的同步方法,最后统一返回一个有序数组。 | ||
storage.getBatchData([ | ||
{ key: 'loginState' }, | ||
{ key: 'checkPoint', syncInBackground: false }, | ||
{ key: 'balance' }, | ||
{ key: 'user', id: '1009' } | ||
]) | ||
.then( results => { | ||
results.forEach( result => { | ||
console.log(result); | ||
}) | ||
storage.getBatchData([ | ||
{ key: 'loginState' }, | ||
{ key: 'checkPoint', syncInBackground: false }, | ||
{ key: 'balance' }, | ||
{ key: 'user', id: '1009' } | ||
]) | ||
.then( results => { | ||
results.forEach( result => { | ||
console.log(result); | ||
}) | ||
//Load batch data with one key and an array of ids. | ||
//根据key和一个id数组来读取批量数据 | ||
storage.getBatchDataWithIds({ | ||
key: 'user', | ||
ids: ['1001', '1002', '1003'] | ||
}) | ||
.then( ... ) | ||
}) | ||
// Load batch data with one key and an array of ids. | ||
storage.getBatchDataWithIds({ | ||
key: 'user', | ||
ids: ['1001', '1002', '1003'] | ||
}) | ||
.then( ... ) | ||
``` | ||
There is a notable difference between the two methods except the arguments. **getBatchData** will invoke different sync methods(since the keys may be different) one by one when corresponding data is missing. However, **getBatchDataWithIds** will collect missing data, push their ids to an array, then pass the array to the corresponding sync method(to avoid too many requests) once, so you need to implement array query on server end and handle the parameters of sync method properly(cause the id parameter can be a single string or an array of strings). | ||
这两个方法除了参数形式不同,还有个值得注意的差异。**getBatchData**会在数据缺失时挨个调用不同的sync方法(因为key不同)。但是**getBatchDataWithIds**却会把缺失的数据统计起来,将它们的id收集到一个数组中,然后一次传递给对应的sync方法(避免挨个查询导致同时发起大量请求),所以你需要在服务端实现通过数组来查询返回,还要注意对应的sync方法的参数处理(因为id参数可能是一个字符串,也可能是一个数组的字符串)。 | ||
####You are welcome to ask any question in the [issues](https://github.com/sunnylqm/react-native-storage/issues) page. | ||
### Changelog | ||
__Caution: UPGRADEING MAY DROP ALL KEY-ID DATA (DUE TO MAP STRUCTURE CHANGES)!__ | ||
#### 0.0.10 | ||
1. All methods except remove and clearMap are now totally promisified. Even custom sync methods can be promise. So you can chain them now. | ||
2. This version changed map structure, so unfortunately all exsiting key-id data would be dropped after upgrading. | ||
3. Improved some test cases. |
334
storage.js
/* | ||
* local storage(web/react native) wrapper | ||
* sunnylqm 2015-10-01 | ||
* version 0.0.9 | ||
* sunnylqm 2016-01-03 | ||
* version 0.0.10 | ||
*/ | ||
@@ -15,3 +15,3 @@ | ||
me.enableCache = options.enableCache || true; | ||
me._innerVersion = 9; | ||
me._innerVersion = 10; | ||
me.cache = {}; | ||
@@ -23,6 +23,7 @@ | ||
try { | ||
window.localStorage.setItem('test', 'test'); | ||
// avoid key conflict | ||
window.localStorage.setItem('__react_native_storage_test', 'test'); | ||
me._s = window.localStorage; | ||
me.isBrowser = true; | ||
me._m = me._checkMap(JSON.parse(me._s.getItem('map'))); | ||
} | ||
@@ -35,17 +36,19 @@ catch(e) { | ||
else { | ||
me.readyQueue = []; | ||
me._s = require('react-native').AsyncStorage; | ||
me._s.getItem('map').then( map => { | ||
me._m = me._checkMap(JSON.parse(map)); | ||
me.onReady(); | ||
}); | ||
} | ||
me._mapPromise = me.getItem('map').then( map => { | ||
me._m = me._checkMap(JSON.parse(map)); | ||
delete me._mapPromise; | ||
}); | ||
} | ||
onReady() { | ||
let me = this; | ||
let rq = me.readyQueue; | ||
for(let i = 0, task; task = rq[i++];) { | ||
me[task.method](task.params); | ||
} | ||
getItem(key) { | ||
return this.isBrowser ? new Promise((resolve, reject) => resolve(this._s.getItem(key))) : this._s.getItem(key); | ||
} | ||
setItem(key, value) { | ||
return this.isBrowser ? new Promise((resolve, reject) => resolve(this._s.setItem(key, value))) : this._s.setItem(key, value); | ||
} | ||
removeItem(key) { | ||
return this.isBrowser ? new Promise((resolve, reject) => resolve(this._s.removeItem(key))) : this._s.removeItem(key); | ||
} | ||
_checkMap(map) { | ||
@@ -67,22 +70,11 @@ let me = this; | ||
_saveToMap(params) { | ||
let me = this, | ||
s = me._s, | ||
m = me._m; | ||
if(!m) { | ||
return me.readyQueue.push({ | ||
method: '_saveToMap', | ||
params | ||
}); | ||
} | ||
let { key, id, data } = params; | ||
//join key and id | ||
let newId = me._getId(key, id); | ||
//update existed data | ||
if(m[newId]) { | ||
s.setItem('map_' + m[newId], data); | ||
} | ||
//create new data | ||
else { | ||
var promise = new Promise((resolve, reject) => { | ||
let { key, id, data } = params, | ||
newId = this._getId(key, id), | ||
m = this._m; | ||
if(m[newId] !== undefined) { | ||
//update existed data | ||
if(this.enableCache) this.cache[newId] = JSON.parse(data); | ||
return resolve(this.setItem('map_' + m[newId], data)); | ||
} | ||
if(m[m.index] !== undefined){ | ||
@@ -92,4 +84,4 @@ //loop over, delete old data | ||
delete m[oldId]; | ||
if(me.enableCache) { | ||
delete me.cache[oldId]; | ||
if(this.enableCache) { | ||
delete this.cache[oldId]; | ||
} | ||
@@ -99,17 +91,22 @@ } | ||
m[m.index] = newId; | ||
if(me.enableCache) { | ||
if(this.enableCache) { | ||
const cacheData = JSON.parse(data); | ||
me.cache[newId] = cacheData; | ||
this.cache[newId] = cacheData; | ||
} | ||
s.setItem('map_' + m.index, data); | ||
s.setItem('map', JSON.stringify(m)); | ||
if(++m.index === me._SIZE) { | ||
resolve(this.setItem('map_' + m.index, data) | ||
.then(this.setItem('map', JSON.stringify(m)))); | ||
if(++m.index === this._SIZE) { | ||
m.index = 0; | ||
} | ||
} | ||
}); | ||
return this._mapPromise ? this._mapPromise.then(() => promise) : promise; | ||
} | ||
save(params) { | ||
let me = this, | ||
s = me._s; | ||
var promise; | ||
let me = this; | ||
let { key, id, rawData, expires } = params; | ||
if(key.toString().includes('_')) { | ||
console.error('Please do not use "_" in key!'); | ||
} | ||
let data = { | ||
@@ -131,6 +128,9 @@ rawData | ||
} | ||
s.setItem(key, data); | ||
promise = me.setItem(key, data); | ||
} | ||
else { | ||
me._saveToMap({ | ||
if(id.toString().includes('_')) { | ||
console.error('Please do not use "_" in id!'); | ||
} | ||
promise = me._saveToMap({ | ||
key, | ||
@@ -141,2 +141,3 @@ id, | ||
} | ||
return this._mapPromise ? this._mapPromise.then(() => promise) : promise; | ||
} | ||
@@ -157,9 +158,3 @@ getBatchData(querys) { | ||
} | ||
return new Promise((resolve, reject) => { | ||
Promise.all(tasks).then(values => { | ||
resolve(values); | ||
}).catch(() => { | ||
reject(); | ||
}) | ||
}) | ||
return Promise.all(tasks); | ||
} | ||
@@ -169,38 +164,15 @@ getBatchDataWithIds(params) { | ||
let { key, ids, syncInBackground } = params; | ||
let tasks = []; | ||
for(var i = 0, id; id = ids[i]; i++) { | ||
tasks[i] = me.load({ key, id, syncInBackground, autoSync: false, batched: true }); | ||
} | ||
return new Promise((resolve, reject) => { | ||
let missed = []; | ||
Promise.all(tasks).then(values => { | ||
values = values.filter(value => { | ||
if(value.syncId !== undefined) { | ||
missed.push(value.syncId); | ||
return false; | ||
} | ||
else { | ||
return true; | ||
} | ||
}); | ||
if(missed.length) { | ||
me.sync[key]({ | ||
id : missed, | ||
resolve: data => { | ||
resolve(values.concat(data)); | ||
}, | ||
reject | ||
}); | ||
} | ||
else { | ||
resolve(values); | ||
} | ||
}).catch(() => { | ||
reject(); | ||
}) | ||
}) | ||
return Promise.all( | ||
ids.map((id) => me.load({ key, id, syncInBackground, autoSync: false, batched: true })) | ||
).then((results) => handlePromise((resolve, reject) => me.sync[key]({ | ||
id: results | ||
.filter((value) => value.syncId !== undefined) | ||
.map((value) => value.syncId), | ||
resolve: resolve, | ||
reject: reject | ||
})).then((data) => results.map((value) => value.syncId ? data.shift() : value))) | ||
} | ||
_lookupGlobalItem(params) { | ||
let me = this, | ||
s = me._s, | ||
ret; | ||
@@ -210,43 +182,27 @@ let { key } = params; | ||
ret = me.cache[key]; | ||
me._loadGlobalItem({ ret, ...params }); | ||
return me._loadGlobalItem({ ret, ...params }); | ||
} | ||
else { | ||
if(me.isBrowser) { | ||
ret = s.getItem(key); | ||
me._loadGlobalItem({ ret, ...params }); | ||
} | ||
else { | ||
s.getItem(key).then(ret => { | ||
me._loadGlobalItem({ ret, ...params }); | ||
}) | ||
} | ||
} | ||
return me.getItem(key).then(ret => me._loadGlobalItem({ ret, ...params })); | ||
} | ||
_loadGlobalItem(params) { | ||
let me = this; | ||
let { key, ret, resolve, reject, autoSync, syncInBackground } = params; | ||
let { key, ret, autoSync, syncInBackground } = params; | ||
if(ret === null || ret === undefined) { | ||
if(autoSync && me.sync[key]) { | ||
me.sync[key]({resolve, reject}); | ||
return handlePromise((resolve, reject) => me.sync[key]({resolve, reject})); | ||
} | ||
else { | ||
reject(); | ||
} | ||
return Promise.reject(); | ||
} | ||
else { | ||
if(typeof ret === 'string') { | ||
ret = JSON.parse(ret); | ||
if(typeof ret === 'string') { | ||
ret = JSON.parse(ret); | ||
} | ||
let now = new Date().getTime(); | ||
if(autoSync && ret.expires < now && me.sync[key]) { | ||
if(syncInBackground) { | ||
me.sync[key]({}); | ||
return Promise.resolve(ret.rawData); | ||
} | ||
let now = new Date().getTime(); | ||
if(autoSync && ret.expires < now && me.sync[key]) { | ||
if(syncInBackground) { | ||
me.sync[key]({}); | ||
} | ||
else { | ||
me.sync[key]({resolve, reject}); | ||
return; | ||
} | ||
} | ||
resolve(ret.rawData); | ||
return handlePromise((resolve, reject) => me.sync[key]({resolve, reject})); | ||
} | ||
return Promise.resolve(ret.rawData); | ||
} | ||
@@ -258,13 +214,7 @@ _noItemFound(params) { | ||
if(autoSync) { | ||
me.sync[key]({id, resolve, reject}); | ||
return handlePromise((resolve, reject) => me.sync[key]({id, resolve, reject})); | ||
} | ||
else { | ||
resolve({ | ||
syncId: id | ||
}); | ||
} | ||
return Promise.resolve({ syncId: id }); | ||
} | ||
else { | ||
reject(); | ||
} | ||
return Promise.reject(); | ||
} | ||
@@ -275,32 +225,24 @@ _loadMapItem(params) { | ||
if(ret === null || ret === undefined) { | ||
me._noItemFound(params); | ||
return me._noItemFound(params); | ||
} | ||
else { | ||
if(typeof ret === 'string'){ | ||
ret = JSON.parse(ret); | ||
} | ||
let now = new Date().getTime(); | ||
if(autoSync && ret.expires < now) { | ||
if(me.sync[key]) { | ||
if(syncInBackground){ | ||
me.sync[key]({id}); | ||
} | ||
else{ | ||
me.sync[key]({id, resolve, reject}); | ||
return; | ||
} | ||
if(typeof ret === 'string'){ | ||
ret = JSON.parse(ret); | ||
} | ||
let now = new Date().getTime(); | ||
if(autoSync && ret.expires < now) { | ||
if(me.sync[key]) { | ||
if(syncInBackground){ | ||
me.sync[key]({id}); | ||
return Promise.resolve(ret.rawData); | ||
} | ||
else if(batched) { | ||
resolve({ | ||
syncId: id | ||
}); | ||
return; | ||
} | ||
return handlePromise((resolve, reject) => me.sync[key]({id, resolve, reject})); | ||
} | ||
resolve(ret.rawData); | ||
if(batched) { | ||
return Promise.resolve({ syncId: id }); | ||
} | ||
} | ||
return Promise.resolve(ret.rawData); | ||
} | ||
_lookUpInMap(params) { | ||
let me = this, | ||
s = me._s, | ||
m = me._m, | ||
@@ -312,48 +254,31 @@ ret; | ||
ret = me.cache[newId]; | ||
me._loadMapItem( {ret, ...params } ); | ||
return me._loadMapItem( {ret, ...params } ); | ||
} | ||
else if(m[newId] !== undefined) { | ||
if(me.isBrowser) { | ||
ret = s.getItem('map_' + m[newId]); | ||
me._loadMapItem( {ret, ...params } ); | ||
} | ||
else { | ||
s.getItem('map_' + m[newId]).then( ret => { | ||
me._loadMapItem( {ret, ...params } ); | ||
}) | ||
} | ||
if(m[newId] !== undefined) { | ||
return me.getItem('map_' + m[newId]).then( ret => me._loadMapItem( {ret, ...params } ) ); | ||
} | ||
else { | ||
me._noItemFound( {ret, ...params } ); | ||
} | ||
return me._noItemFound( {ret, ...params } ); | ||
} | ||
remove(params) { | ||
let me = this, | ||
m = me._m, | ||
s = me._s; | ||
m = me._m; | ||
let { key, id } = params; | ||
if(!m) { | ||
me.readyQueue.push({ | ||
method: 'remove', | ||
params | ||
}); | ||
} | ||
else if(id === undefined) { | ||
if(id === undefined) { | ||
if(me.enableCache && me.cache[key]) { | ||
delete me.cache[key]; | ||
} | ||
s.removeItem(key); | ||
return me.removeItem(key); | ||
} | ||
else { | ||
//join key and id | ||
let newId = me._getId(key, id); | ||
let newId = me._getId(key, id); | ||
//remove existed data | ||
if(m[newId]) { | ||
if(me.enableCache && me.cache[newId]) { | ||
delete me.cache[newId]; | ||
} | ||
delete m[newId]; | ||
s.removeItem('map_' + m[newId]); | ||
//remove existed data | ||
if(m[newId] !== undefined) { | ||
if(me.enableCache && me.cache[newId]) { | ||
delete me.cache[newId]; | ||
} | ||
let idTobeDeleted = m[newId]; | ||
delete m[newId]; | ||
me.setItem('map', JSON.stringify(m)); | ||
return me.removeItem('map_' + idTobeDeleted); | ||
} | ||
@@ -373,30 +298,11 @@ } | ||
if(id === undefined) { | ||
if(!m) { | ||
me.readyQueue.push({ | ||
method: '_lookupGlobalItem', | ||
params: {key, resolve, reject, autoSync, syncInBackground} | ||
}); | ||
} | ||
else { | ||
me._lookupGlobalItem({key, resolve, reject, autoSync, syncInBackground}); | ||
} | ||
return resolve(me._lookupGlobalItem({key, resolve, reject, autoSync, syncInBackground})); | ||
} | ||
else { | ||
if(!m) { | ||
me.readyQueue.push({ | ||
method: '_lookUpInMap', | ||
params: {key, id, resolve, reject, autoSync, syncInBackground} | ||
}); | ||
} | ||
else { | ||
me._lookUpInMap({key, id, resolve, reject, autoSync, syncInBackground}); | ||
} | ||
} | ||
return resolve(me._lookUpInMap({key, id, resolve, reject, autoSync, syncInBackground})); | ||
}); | ||
return promise; | ||
return this._mapPromise ? this._mapPromise.then(() => promise) : promise; | ||
} | ||
clearMap(){ | ||
let me = this, | ||
s = me._s; | ||
s.removeItem('map'); | ||
let me = this; | ||
me.removeItem('map'); | ||
me._m = { | ||
@@ -408,1 +314,11 @@ innerVersion: me._innerVersion, | ||
} | ||
function noop() {} | ||
// compatible with legacy version promise | ||
function handlePromise (fn) { | ||
return new Promise((resolve, reject) => { | ||
var promise, isPromise; | ||
if (isPromise = (promise = fn((data) => isPromise ? noop() : resolve(data), (err) => isPromise ? noop() : reject(err))) instanceof Promise) resolve(promise); | ||
}); | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
268
0
44681
932