Skip to content

Commit

Permalink
Merge pull request #3 from Exilz/dev
Browse files Browse the repository at this point in the history
v.1.1.0
  • Loading branch information
Exilz authored Sep 4, 2017
2 parents 638ea8e + bf30902 commit f1cfd48
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 29 deletions.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Easily write offline-first react-native applications with your own REST API. Thi
- [react-native-offline-api](#react-native-offline-api)
- [Table of contents](#table-of-contents)
- [Installation](#installation)
- [How does it work ?](#how-does-it-work)
- [How to use](#how-to-use)
- [Setting up your global API options](#setting-up-your-global-api-options)
- [Declaring your services definitions](#declaring-your-services-definitions)
Expand All @@ -18,6 +19,7 @@ Easily write offline-first react-native applications with your own REST API. Thi
- [Services options](#services-options)
- [Fetch options](#fetch-options)
- [Path and query parameters](#path-and-query-parameters)
- [Limiting the size of your cache](#limiting-the-size-of-your-cache)
- [Middlewares](#middlewares)
- [Using your own driver for caching](#using-your-own-driver-for-caching)
- [Types](#types)
Expand All @@ -30,6 +32,11 @@ npm install --save react-native-offline-api # with npm
yarn add react-native-offline-api # with yarn
```

## How does it work ?

<p align="center"><a href="http://i.imgur.com/SBm5Xhj.png"><img src="http://i.imgur.com/TO1sGZU.png"/></a></p>
<p align="center"><em>click to enlarge</em></p>

## How to use

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

## Services options
Expand All @@ -168,6 +178,8 @@ Key | Type | Description | Example
`prefix` | `string` | Optional specific prefix to use for this service, provide the key you set in your `prefixes` API option
`middlewares` | `APIMiddleware[]` | Optional array of middlewares that override the ones set globally in your `middlewares` API option, , see [middlewares](#middlewares)
`disableCache` | `boolean` | Optional, disables the cache for this service (override your [API's global options](#api-options))
`capService` | `boolean` | Optional, enable or disable capping for this specific service, see [limiting the size of your cache](#limiting-the-size-of-your-cache)
`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)

## Fetch options

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

* 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`.

## Limiting the size of your cache

If you fear your cache will keep growing, you have some options to make sure it doesn't get too big.

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.

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.

## Middlewares

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.**

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.
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.

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

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

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

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

- [ ] Write a demo
- [ ] 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
- [ ] 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
- [ ] Thoroughly test custom caching drivers, maybe provide one (realm or sqlite)
- [ ] Add automated testing
76 changes: 61 additions & 15 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ var DEFAULT_API_OPTIONS = {
prefixes: { default: '/' },
printNetworkRequests: false,
disableCache: false,
cacheExpiration: 5 * 60 * 1000
cacheExpiration: 5 * 60 * 1000,
cachePrefix: 'offlineApiCache',
capServices: false,
capLimit: 50
};
var DEFAULT_SERVICE_OPTIONS = {
method: 'GET',
Expand All @@ -61,7 +64,6 @@ var DEFAULT_SERVICE_OPTIONS = {
disableCache: false
};
var DEFAULT_CACHE_DRIVER = react_native_1.AsyncStorage;
var CACHE_PREFIX = 'offlineApiCache:';
var OfflineFirstAPI = (function () {
function OfflineFirstAPI(options, services, driver) {
this._APIServices = {};
Expand All @@ -84,7 +86,7 @@ var OfflineFirstAPI = (function () {
_a.label = 1;
case 1:
_a.trys.push([1, 8, , 9]);
return [4 /*yield*/, this._applyMiddlewares(serviceDefinition, options)];
return [4 /*yield*/, this._applyMiddlewares(serviceDefinition, fullPath, options)];
case 2:
middlewares = _a.sent();
fetchOptions = _merge(middlewares, (options && options.fetchOptions) || {}, { method: serviceDefinition.method }, { headers: (options && options.headers) || {} });
Expand Down Expand Up @@ -133,7 +135,7 @@ var OfflineFirstAPI = (function () {
case 7:
// Cache if it hasn't been disabled and if the network request has been successful
if (res.data.ok && shouldUseCache) {
this._cache(service, requestId, parsedResponseData, expiration);
this._cache(serviceDefinition, service, requestId, parsedResponseData, expiration);
}
this._log('parsed network response', parsedResponseData);
return [2 /*return*/, parsedResponseData];
Expand Down Expand Up @@ -268,28 +270,49 @@ var OfflineFirstAPI = (function () {
* @returns {(Promise<void|boolean>)}
* @memberof OfflineFirstAPI
*/
OfflineFirstAPI.prototype._cache = function (service, requestId, response, expiration) {
OfflineFirstAPI.prototype._cache = function (serviceDefinition, service, requestId, response, expiration) {
return __awaiter(this, void 0, void 0, function () {
var err_5;
var shouldCap, capLimit, serviceDictionaryKey, dictionary, cachedItemsCount, key, err_5;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
this._log("Caching " + requestId + " ...");
shouldCap = typeof serviceDefinition.capService !== 'undefined' ?
serviceDefinition.capService :
this._APIOptions.capServices;
_a.label = 1;
case 1:
_a.trys.push([1, 4, , 5]);
_a.trys.push([1, 7, , 8]);
this._log("Caching " + requestId + " ...");
return [4 /*yield*/, this._addKeyToServiceDictionary(service, requestId, expiration)];
case 2:
_a.sent();
return [4 /*yield*/, this._APIDriver.setItem(this._getCacheObjectKey(requestId), JSON.stringify(response))];
case 3:
_a.sent();
this._log("Updated cache for request " + requestId);
return [2 /*return*/, true];
if (!shouldCap) return [3 /*break*/, 6];
capLimit = serviceDefinition.capLimit || this._APIOptions.capLimit;
serviceDictionaryKey = this._getServiceDictionaryKey(service);
return [4 /*yield*/, this._APIDriver.getItem(serviceDictionaryKey)];
case 4:
dictionary = _a.sent();
if (!dictionary) return [3 /*break*/, 6];
dictionary = JSON.parse(dictionary);
cachedItemsCount = Object.keys(dictionary).length;
if (!(cachedItemsCount > capLimit)) return [3 /*break*/, 6];
this._log("service " + service + " cap reached (" + cachedItemsCount + " / " + capLimit + "), removing the oldest cached item...");
key = this._getOldestCachedItem(dictionary).key;
delete dictionary[key];
return [4 /*yield*/, this._APIDriver.removeItem(key)];
case 5:
_a.sent();
this._APIDriver.setItem(serviceDictionaryKey, JSON.stringify(dictionary));
_a.label = 6;
case 6: return [2 /*return*/, true];
case 7:
err_5 = _a.sent();
throw new Error("Error while caching API response for " + requestId);
case 5: return [2 /*return*/];
case 8: return [2 /*return*/];
}
});
});
Expand Down Expand Up @@ -402,6 +425,28 @@ var OfflineFirstAPI = (function () {
});
});
};
/**
* Returns the key and the expiration date of the oldest cached item of a cache dictionary
* @private
* @param {ICacheDictionary} dictionary
* @returns {*}
* @memberof OfflineFirstAPI
*/
OfflineFirstAPI.prototype._getOldestCachedItem = function (dictionary) {
var oldest;
for (var key in dictionary) {
var keyExpiration = dictionary[key];
if (oldest) {
if (keyExpiration < oldest.expiration) {
oldest = { key: key, expiration: keyExpiration };
}
}
else {
oldest = { key: key, expiration: keyExpiration };
}
}
return oldest;
};
/**
* Promise that resolves every cache key associated to a service : the service dictionary's name, and all requestId
* stored. This is useful to clear the cache without affecting the user's stored data not related to this API.
Expand All @@ -412,6 +457,7 @@ var OfflineFirstAPI = (function () {
*/
OfflineFirstAPI.prototype._getAllKeysForService = function (service) {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
var keys, serviceDictionaryKey, dictionary, dictionaryKeys, err_8;
return __generator(this, function (_a) {
switch (_a.label) {
Expand All @@ -425,7 +471,7 @@ var OfflineFirstAPI = (function () {
dictionary = _a.sent();
if (dictionary) {
dictionary = JSON.parse(dictionary);
dictionaryKeys = Object.keys(dictionary).map(function (key) { return CACHE_PREFIX + ":" + key; });
dictionaryKeys = Object.keys(dictionary).map(function (key) { return _this._APIOptions.cachePrefix + ":" + key; });
keys = keys.concat(dictionaryKeys);
}
return [2 /*return*/, keys];
Expand All @@ -445,7 +491,7 @@ var OfflineFirstAPI = (function () {
* @memberof OfflineFirstAP
*/
OfflineFirstAPI.prototype._getServiceDictionaryKey = function (service) {
return CACHE_PREFIX + ":dictionary:" + service;
return this._APIOptions.cachePrefix + ":dictionary:" + service;
};
/**
* Simple helper getting a request's cache key.
Expand All @@ -455,7 +501,7 @@ var OfflineFirstAPI = (function () {
* @memberof OfflineFirstAP
*/
OfflineFirstAPI.prototype._getCacheObjectKey = function (requestId) {
return CACHE_PREFIX + ":" + requestId;
return this._APIOptions.cachePrefix + ":" + requestId;
};
/**
* Resolve each middleware provided and merge them into a single object that will be passed to
Expand All @@ -466,7 +512,7 @@ var OfflineFirstAPI = (function () {
* @returns {Promise<any>}
* @memberof OfflineFirstAPI
*/
OfflineFirstAPI.prototype._applyMiddlewares = function (serviceDefinition, options) {
OfflineFirstAPI.prototype._applyMiddlewares = function (serviceDefinition, fullPath, options) {
return __awaiter(this, void 0, void 0, function () {
var middlewares, resolvedMiddlewares, err_9;
return __generator(this, function (_a) {
Expand All @@ -477,7 +523,7 @@ var OfflineFirstAPI = (function () {
_a.label = 1;
case 1:
_a.trys.push([1, 3, , 4]);
middlewares = middlewares.map(function (middleware) { return middleware(serviceDefinition, options); });
middlewares = middlewares.map(function (middleware) { return middleware(serviceDefinition, fullPath, options); });
return [4 /*yield*/, Promise.all(middlewares)];
case 2:
resolvedMiddlewares = _a.sent();
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-offline-api",
"version": "1.0.1",
"version": "1.1.0",
"description": "Offline first API wrapper for react-native",
"main": "./dist/index.js",
"types": "./src/index.d.ts",
Expand Down
Loading

0 comments on commit f1cfd48

Please sign in to comment.