Skip to content

Commit f1cfd48

Browse files
authored
Merge pull request #3 from Exilz/dev
v.1.1.0
2 parents 638ea8e + bf30902 commit f1cfd48

File tree

5 files changed

+163
-29
lines changed

5 files changed

+163
-29
lines changed

README.md

+24-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Easily write offline-first react-native applications with your own REST API. Thi
99
- [react-native-offline-api](#react-native-offline-api)
1010
- [Table of contents](#table-of-contents)
1111
- [Installation](#installation)
12+
- [How does it work ?](#how-does-it-work)
1213
- [How to use](#how-to-use)
1314
- [Setting up your global API options](#setting-up-your-global-api-options)
1415
- [Declaring your services definitions](#declaring-your-services-definitions)
@@ -18,6 +19,7 @@ Easily write offline-first react-native applications with your own REST API. Thi
1819
- [Services options](#services-options)
1920
- [Fetch options](#fetch-options)
2021
- [Path and query parameters](#path-and-query-parameters)
22+
- [Limiting the size of your cache](#limiting-the-size-of-your-cache)
2123
- [Middlewares](#middlewares)
2224
- [Using your own driver for caching](#using-your-own-driver-for-caching)
2325
- [Types](#types)
@@ -30,6 +32,11 @@ npm install --save react-native-offline-api # with npm
3032
yarn add react-native-offline-api # with yarn
3133
```
3234

35+
## How does it work ?
36+
37+
<p align="center"><a href="http://i.imgur.com/SBm5Xhj.png"><img src="http://i.imgur.com/TO1sGZU.png"/></a></p>
38+
<p align="center"><em>click to enlarge</em></p>
39+
3340
## How to use
3441

3542
Since this plugin is a fully-fledged wrapper and not just a network helper, you need to set up your API configuration.
@@ -153,6 +160,9 @@ Key | Type | Description | Example
153160
`printNetworkRequests` | `boolean` | Optional, prints all your network requests
154161
`disableCache` | `boolean` | Optional, completely disables caching (overriden by service definitions & `fetch`'s `option` parameter)
155162
`cacheExpiration` | `number` | Optional default expiration of cached data in ms (overriden by service definitions & `fetch`'s `option` parameter)
163+
`cachePrefix` | `string` | Optional, prefix of the keys stored on your cache, defaults to `offlineApiCache`
164+
`capServices` | `boolean` | Optional, enable capping for every service, defaults to `false`, see [limiting the size of your cache](#limiting-the-size-of-your-cache)
165+
`capLimit` | `number` | Optional quantity of cached items for each service, defaults to `50`, see [limiting the size of your cache](#limiting-the-size-of-your-cache)
156166
`offlineDriver` | `IAPIDriver` | Optional, see [use your own driver for caching](#use-your-own-driver-for-caching)
157167

158168
## Services options
@@ -168,6 +178,8 @@ Key | Type | Description | Example
168178
`prefix` | `string` | Optional specific prefix to use for this service, provide the key you set in your `prefixes` API option
169179
`middlewares` | `APIMiddleware[]` | Optional array of middlewares that override the ones set globally in your `middlewares` API option, , see [middlewares](#middlewares)
170180
`disableCache` | `boolean` | Optional, disables the cache for this service (override your [API's global options](#api-options))
181+
`capService` | `boolean` | Optional, enable or disable capping for this specific service, see [limiting the size of your cache](#limiting-the-size-of-your-cache)
182+
`capLimit` | `number` | Optional quantity of cached items for this specific service, defaults to `50`, see [limiting the size of your cache](#limiting-the-size-of-your-cache)
171183

172184
## Fetch options
173185

@@ -194,11 +206,19 @@ The URL to your endpoints are being constructed with **your domain name, your op
194206

195207
* The `queryParameters` are regular query string parameters. For instance, a request fired with this path : `/weather` and these `queryParameters` : `{ days: 'mon,tue,sun', location: 'Paris,France' }` will become `/weather?days=mon,tue,sun&location=Paris,France`.
196208

209+
## Limiting the size of your cache
210+
211+
If you fear your cache will keep growing, you have some options to make sure it doesn't get too big.
212+
213+
First, you can use the `clearCache` method to empty all stored data, or just a service's items. You might want to implement a button in your interface to give your users the ability to clear it whenever they want if they feel like their app is starting to take too much space.
214+
215+
The other solution would be to use the capping option. If you set `capServices` to true in your [API options](#api-options), or `capService` in your [service options](#services-options), the wrapper will make sure it never stores more items that the amount you configured in `capLimit`. This is a good way to restrict the size of stored data for sensitive services, while leaving some of them uncapped. Capping is disabled by default.
216+
197217
## Middlewares
198218

199219
Just like for the other request options, **you can provide middlewares at the global level in your API options, at the service's definition level, or in the `options` parameter of the `fetch` method.**
200220

201-
You must provide an **array of promises**, like so : `(serviceDefinition: IAPIService, options: IFetchOptions) => any;`, please [take a look at the types](#types) to know more. You don't necessarily need to write asynchronous code in them, but they all must be promises.
221+
You must provide an **array of promises**, like so : `(serviceDefinition: IAPIService, fullPath: string, options: IFetchOptions) => any;`, please [take a look at the types](#types) to know more. You don't necessarily need to write asynchronous code in them, but they all must be promises.
202222

203223
Anything you will resolve in those promises will be merged into your request's options !
204224

@@ -258,6 +278,7 @@ Your custom driver must implement these 3 methods that are promises.
258278

259279
* `getItem(key: string, callback?: (error?: Error, result?: string) => void)`
260280
* `setItem(key: string, value: string, callback?: (error?: Error) => void);`
281+
* `removeItem(key: string, callback?: (error?: Error) => void);`
261282
* `multiRemove(keys: string[], callback?: (errors?: Error[]) => void);`
262283

263284
*Please note that, as of the 1.0 release, this hasn't been tested thoroughly.*
@@ -273,5 +294,7 @@ These are Typescript defintions, so they should be displayed in your editor/IDE
273294
Pull requests are more than welcome for these items, or for any feature that might be missing.
274295

275296
- [ ] Write a demo
297+
- [ ] Improve capping performance by storing how many items are cached for each service so we don't have to parse the whole service's dictionary each time
298+
- [ ] Add a method to check for the total size of the cache, which would be useful to trigger a clearing if it reaches a certain size
276299
- [ ] Thoroughly test custom caching drivers, maybe provide one (realm or sqlite)
277300
- [ ] Add automated testing

dist/index.js

+61-15
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ var DEFAULT_API_OPTIONS = {
5252
prefixes: { default: '/' },
5353
printNetworkRequests: false,
5454
disableCache: false,
55-
cacheExpiration: 5 * 60 * 1000
55+
cacheExpiration: 5 * 60 * 1000,
56+
cachePrefix: 'offlineApiCache',
57+
capServices: false,
58+
capLimit: 50
5659
};
5760
var DEFAULT_SERVICE_OPTIONS = {
5861
method: 'GET',
@@ -61,7 +64,6 @@ var DEFAULT_SERVICE_OPTIONS = {
6164
disableCache: false
6265
};
6366
var DEFAULT_CACHE_DRIVER = react_native_1.AsyncStorage;
64-
var CACHE_PREFIX = 'offlineApiCache:';
6567
var OfflineFirstAPI = (function () {
6668
function OfflineFirstAPI(options, services, driver) {
6769
this._APIServices = {};
@@ -84,7 +86,7 @@ var OfflineFirstAPI = (function () {
8486
_a.label = 1;
8587
case 1:
8688
_a.trys.push([1, 8, , 9]);
87-
return [4 /*yield*/, this._applyMiddlewares(serviceDefinition, options)];
89+
return [4 /*yield*/, this._applyMiddlewares(serviceDefinition, fullPath, options)];
8890
case 2:
8991
middlewares = _a.sent();
9092
fetchOptions = _merge(middlewares, (options && options.fetchOptions) || {}, { method: serviceDefinition.method }, { headers: (options && options.headers) || {} });
@@ -133,7 +135,7 @@ var OfflineFirstAPI = (function () {
133135
case 7:
134136
// Cache if it hasn't been disabled and if the network request has been successful
135137
if (res.data.ok && shouldUseCache) {
136-
this._cache(service, requestId, parsedResponseData, expiration);
138+
this._cache(serviceDefinition, service, requestId, parsedResponseData, expiration);
137139
}
138140
this._log('parsed network response', parsedResponseData);
139141
return [2 /*return*/, parsedResponseData];
@@ -268,28 +270,49 @@ var OfflineFirstAPI = (function () {
268270
* @returns {(Promise<void|boolean>)}
269271
* @memberof OfflineFirstAPI
270272
*/
271-
OfflineFirstAPI.prototype._cache = function (service, requestId, response, expiration) {
273+
OfflineFirstAPI.prototype._cache = function (serviceDefinition, service, requestId, response, expiration) {
272274
return __awaiter(this, void 0, void 0, function () {
273-
var err_5;
275+
var shouldCap, capLimit, serviceDictionaryKey, dictionary, cachedItemsCount, key, err_5;
274276
return __generator(this, function (_a) {
275277
switch (_a.label) {
276278
case 0:
277-
this._log("Caching " + requestId + " ...");
279+
shouldCap = typeof serviceDefinition.capService !== 'undefined' ?
280+
serviceDefinition.capService :
281+
this._APIOptions.capServices;
278282
_a.label = 1;
279283
case 1:
280-
_a.trys.push([1, 4, , 5]);
284+
_a.trys.push([1, 7, , 8]);
285+
this._log("Caching " + requestId + " ...");
281286
return [4 /*yield*/, this._addKeyToServiceDictionary(service, requestId, expiration)];
282287
case 2:
283288
_a.sent();
284289
return [4 /*yield*/, this._APIDriver.setItem(this._getCacheObjectKey(requestId), JSON.stringify(response))];
285290
case 3:
286291
_a.sent();
287292
this._log("Updated cache for request " + requestId);
288-
return [2 /*return*/, true];
293+
if (!shouldCap) return [3 /*break*/, 6];
294+
capLimit = serviceDefinition.capLimit || this._APIOptions.capLimit;
295+
serviceDictionaryKey = this._getServiceDictionaryKey(service);
296+
return [4 /*yield*/, this._APIDriver.getItem(serviceDictionaryKey)];
289297
case 4:
298+
dictionary = _a.sent();
299+
if (!dictionary) return [3 /*break*/, 6];
300+
dictionary = JSON.parse(dictionary);
301+
cachedItemsCount = Object.keys(dictionary).length;
302+
if (!(cachedItemsCount > capLimit)) return [3 /*break*/, 6];
303+
this._log("service " + service + " cap reached (" + cachedItemsCount + " / " + capLimit + "), removing the oldest cached item...");
304+
key = this._getOldestCachedItem(dictionary).key;
305+
delete dictionary[key];
306+
return [4 /*yield*/, this._APIDriver.removeItem(key)];
307+
case 5:
308+
_a.sent();
309+
this._APIDriver.setItem(serviceDictionaryKey, JSON.stringify(dictionary));
310+
_a.label = 6;
311+
case 6: return [2 /*return*/, true];
312+
case 7:
290313
err_5 = _a.sent();
291314
throw new Error("Error while caching API response for " + requestId);
292-
case 5: return [2 /*return*/];
315+
case 8: return [2 /*return*/];
293316
}
294317
});
295318
});
@@ -402,6 +425,28 @@ var OfflineFirstAPI = (function () {
402425
});
403426
});
404427
};
428+
/**
429+
* Returns the key and the expiration date of the oldest cached item of a cache dictionary
430+
* @private
431+
* @param {ICacheDictionary} dictionary
432+
* @returns {*}
433+
* @memberof OfflineFirstAPI
434+
*/
435+
OfflineFirstAPI.prototype._getOldestCachedItem = function (dictionary) {
436+
var oldest;
437+
for (var key in dictionary) {
438+
var keyExpiration = dictionary[key];
439+
if (oldest) {
440+
if (keyExpiration < oldest.expiration) {
441+
oldest = { key: key, expiration: keyExpiration };
442+
}
443+
}
444+
else {
445+
oldest = { key: key, expiration: keyExpiration };
446+
}
447+
}
448+
return oldest;
449+
};
405450
/**
406451
* Promise that resolves every cache key associated to a service : the service dictionary's name, and all requestId
407452
* stored. This is useful to clear the cache without affecting the user's stored data not related to this API.
@@ -412,6 +457,7 @@ var OfflineFirstAPI = (function () {
412457
*/
413458
OfflineFirstAPI.prototype._getAllKeysForService = function (service) {
414459
return __awaiter(this, void 0, void 0, function () {
460+
var _this = this;
415461
var keys, serviceDictionaryKey, dictionary, dictionaryKeys, err_8;
416462
return __generator(this, function (_a) {
417463
switch (_a.label) {
@@ -425,7 +471,7 @@ var OfflineFirstAPI = (function () {
425471
dictionary = _a.sent();
426472
if (dictionary) {
427473
dictionary = JSON.parse(dictionary);
428-
dictionaryKeys = Object.keys(dictionary).map(function (key) { return CACHE_PREFIX + ":" + key; });
474+
dictionaryKeys = Object.keys(dictionary).map(function (key) { return _this._APIOptions.cachePrefix + ":" + key; });
429475
keys = keys.concat(dictionaryKeys);
430476
}
431477
return [2 /*return*/, keys];
@@ -445,7 +491,7 @@ var OfflineFirstAPI = (function () {
445491
* @memberof OfflineFirstAP
446492
*/
447493
OfflineFirstAPI.prototype._getServiceDictionaryKey = function (service) {
448-
return CACHE_PREFIX + ":dictionary:" + service;
494+
return this._APIOptions.cachePrefix + ":dictionary:" + service;
449495
};
450496
/**
451497
* Simple helper getting a request's cache key.
@@ -455,7 +501,7 @@ var OfflineFirstAPI = (function () {
455501
* @memberof OfflineFirstAP
456502
*/
457503
OfflineFirstAPI.prototype._getCacheObjectKey = function (requestId) {
458-
return CACHE_PREFIX + ":" + requestId;
504+
return this._APIOptions.cachePrefix + ":" + requestId;
459505
};
460506
/**
461507
* Resolve each middleware provided and merge them into a single object that will be passed to
@@ -466,7 +512,7 @@ var OfflineFirstAPI = (function () {
466512
* @returns {Promise<any>}
467513
* @memberof OfflineFirstAPI
468514
*/
469-
OfflineFirstAPI.prototype._applyMiddlewares = function (serviceDefinition, options) {
515+
OfflineFirstAPI.prototype._applyMiddlewares = function (serviceDefinition, fullPath, options) {
470516
return __awaiter(this, void 0, void 0, function () {
471517
var middlewares, resolvedMiddlewares, err_9;
472518
return __generator(this, function (_a) {
@@ -477,7 +523,7 @@ var OfflineFirstAPI = (function () {
477523
_a.label = 1;
478524
case 1:
479525
_a.trys.push([1, 3, , 4]);
480-
middlewares = middlewares.map(function (middleware) { return middleware(serviceDefinition, options); });
526+
middlewares = middlewares.map(function (middleware) { return middleware(serviceDefinition, fullPath, options); });
481527
return [4 /*yield*/, Promise.all(middlewares)];
482528
case 2:
483529
resolvedMiddlewares = _a.sent();

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-offline-api",
3-
"version": "1.0.1",
3+
"version": "1.1.0",
44
"description": "Offline first API wrapper for react-native",
55
"main": "./dist/index.js",
66
"types": "./src/index.d.ts",

0 commit comments

Comments
 (0)