Skip to content

Commit

Permalink
Merge pull request #10 from Exilz/2.2.0
Browse files Browse the repository at this point in the history
v2.2.0
  • Loading branch information
Exilz authored Jan 31, 2018
2 parents ce328ef + 956fa25 commit 78635c3
Show file tree
Hide file tree
Showing 13 changed files with 405 additions and 88 deletions.
75 changes: 6 additions & 69 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,93 +213,30 @@ The URL to your endpoints are being constructed with **your domain name, your op
## 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.
[Learn more about enabling capping in the documentation](docs/cache-size.md)

## 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, paths: IMiddlewarePaths, 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 !

Here's a barebone example :

```javascript
const API_OPTIONS = {
// ... all your api options
middlewares: [exampleMiddleware],
};

async function exampleMiddleware (serviceDefinition, serviceOptions) {
// This will be printed everytime you call a service
console.log('You just fired a request for the path ' + serviceDefinition.path);
}
```

You can even make API calls in your middlewares. For instance, you might want to make sure the user is logged in into your API, or you might want to refresh its authentication token once in a while. Like so :

```javascript
const API_OPTIONS = {
// ... all your api options
middlewares: [authMiddleware]
}

async function authMiddleware (serviceDefinition, serviceOptions) {
if (authToken && !tokenExpired) {
// Our token is up-to-date, add it to the headers of our request
return { headers:'X-Auth-Token': authToken } };
}
// Token is missing or outdated, let's fetch a new one
try {
// Assuming our login service's method is already set to 'POST'
const authData = await api.fetch(
'login',
// the 'fetcthOptions' key allows us to use any of react-native's fetch method options
// here, the body of our post request
{ fetchOptions: { body: 'username=user&password=password' } }
);
// Store our new authentication token and add it to the headers of our request
authToken = authData.authToken;
tokenExpired = false;
return { headers:'X-Auth-Token': authData.authToken } };
} catch (err) {
throw new Error(`Couldn't auth to API, ${err}`);
}
}
```
[Check out middlewares documentation and examples](docs/middlewares.md)

## Using your own driver for caching

This wrapper has been written with the goal of **being storage-agnostic**. This means that by default, it will make use of react-native's `AsyncStorage` API, but feel free to write your own driver and use anything you want, like the amazing [realm](https://github.com/realm/realm-js) or [sqlite](https://github.com/andpor/react-native-sqlite-storage).

> This is the first step for the wrapper to being also available on the browser and in any node.js environment.
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);`
> 💡 You can now use SQLite instead of AsyncStorage without additional code !
*Please note that, as of the 1.0 release, this hasn't been tested thoroughly.*
[Check out drivers documentation and how to enable the SQLite driver](docs/custom-drivers.md)

## Types

Every API interfaces [can be seen here](src/interfaces.ts) so you don't need to poke around the parameters in your console to be aware of what's available to you :)

These are Typescript defintions, so they should be displayed in your editor/IDE if it supports it.
> 💡 These are Typescript defintions, so they should be displayed in your editor/IDE if it supports it.
## Roadmap

Pull requests are more than welcome for these items, or for any feature that might be missing.

- [ ] 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
- [x] Thoroughly test custom caching drivers, maybe provide one (realm or sqlite)
- [x] Write a demo
2 changes: 1 addition & 1 deletion demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"dependencies": {
"react": "16.0.0-alpha.12",
"react-native": "0.47.1",
"react-native-offline-api": "2.1.0"
"react-native-offline-api": "2.2.0"
},
"devDependencies": {
"babel-jest": "20.0.3",
Expand Down
143 changes: 143 additions & 0 deletions dist/drivers/sqlite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"use strict";
var __assign = (this && this.__assign) || Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [0, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var _this = this;
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = function (SQLite, options) { return __awaiter(_this, void 0, void 0, function () {
var db, err_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
SQLite.DEBUG(options.debug || false);
SQLite.enablePromise(true);
_a.label = 1;
case 1:
_a.trys.push([1, 3, , 4]);
return [4 /*yield*/, SQLite.openDatabase(__assign({ name: 'offlineapi.db', location: 'default' }, (options.openDatabaseOptions || {})))];
case 2:
db = _a.sent();
db.transaction(function (tx) {
tx.executeSql('CREATE TABLE IF NOT EXISTS cache (id TEXT PRIMARY KEY NOT NULL, value TEXT);');
});
return [2 /*return*/, {
getItem: getItem(db),
setItem: setItem(db),
removeItem: removeItem(db),
multiRemove: multiRemove(db)
}];
case 3:
err_1 = _a.sent();
throw new Error("react-native-offline-api : Cannot open SQLite database : " + err_1 + ". Check your SQLite configuration.");
case 4: return [2 /*return*/];
}
});
}); };
function getItem(db) {
return function (key) {
return new Promise(function (resolve, reject) {
db.transaction(function (tx) {
tx.executeSql('SELECT * FROM cache WHERE id=?', [key])
.then(function (res) {
var results = res[1];
var item = results.rows.item(0);
return resolve(item && item.value || null);
})
.catch(function (err) {
return reject(err);
});
});
});
};
}
function setItem(db) {
return function (key, value) {
return new Promise(function (resolve, reject) {
db.transaction(function (tx) {
tx.executeSql('INSERT OR REPLACE INTO cache VALUES (?,?)', [key, value])
.then(function () {
return resolve();
}).
catch(function (err) {
return reject(err);
});
});
});
};
}
function removeItem(db) {
return function (key) {
return new Promise(function (resolve, reject) {
db.transaction(function (tx) {
tx.executeSql('DELETE FROM cache WHERE id=?', [key])
.then(function () {
return resolve();
})
.catch(function (err) {
return reject(err);
});
});
});
};
}
function multiRemove(db) {
return function (keys) {
return new Promise(function (resolve, reject) {
// This implmementation is not the most efficient, must delete using
// WHERE id IN (...,...) doesn't seem to be working at the moment.
db.transaction(function (tx) {
var promises = [];
keys.forEach(function (key) {
promises.push(tx.executeSql('DELETE FROM cache WHERE id=?', [key]));
});
Promise.all(promises)
.then(function () {
return resolve();
})
.catch(function (err) {
return reject(err);
});
});
});
};
}
4 changes: 3 additions & 1 deletion dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
};
Object.defineProperty(exports, "__esModule", { value: true });
var react_native_1 = require("react-native");
var sqlite_1 = require("./drivers/sqlite");
var _mapValues = require("lodash.mapvalues");
var _merge = require("lodash.merge");
var sha = require("jssha");
Expand All @@ -64,6 +65,7 @@ var DEFAULT_SERVICE_OPTIONS = {
prefix: 'default'
};
var DEFAULT_CACHE_DRIVER = react_native_1.AsyncStorage;
exports.drivers = { sqliteDriver: sqlite_1.default };
var OfflineFirstAPI = /** @class */ (function () {
function OfflineFirstAPI(options, services, driver) {
this._APIServices = {};
Expand Down Expand Up @@ -307,7 +309,7 @@ var OfflineFirstAPI = /** @class */ (function () {
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)];
return [4 /*yield*/, this._APIDriver.removeItem(this._getCacheObjectKey(key))];
case 5:
_a.sent();
this._APIDriver.setItem(serviceDictionaryKey, JSON.stringify(dictionary));
Expand Down
5 changes: 5 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Additional documentation

* [Limiting the size of your cache](cache-size.md)
* [Using your own driver or SQLite for caching](custom-drivers.md)
* [Middlewares documentation](middlewares.md)
7 changes: 7 additions & 0 deletions docs/cache-size.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 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.
27 changes: 27 additions & 0 deletions docs/custom-drivers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Using your own driver for caching

This wrapper has been written with the goal of **being storage-agnostic**. This means that by default, it will make use of react-native's `AsyncStorage` API, but feel free to write your own driver and use anything you want, like the amazing [realm](https://github.com/realm/realm-js).

Your custom driver must implement these 3 methods that are promises.

* `getItem(key: string): Promise<any>;`
* `setItem(key: string, value: string): Promise<void>;`
* `removeItem(key: string): Promise<void>;`
* `multiRemove(keys: string[]): Promise<void>;`

## SQLite Driver

As of `2.2.0`, an SQLite driver is baked-in with the module. Install SQLite in your project by [following these instructions](https://github.com/andpor/react-native-sqlite-storage) and set it as your custom driver like this :

```javascript
import OfflineFirstAPI, { drivers } from 'react-native-offline-api';
import SQLite from 'react-native-sqlite-storage';

// ...

const api = new OfflineFirstAPI(API_OPTIONS, API_SERVICES);

drivers.sqliteDriver(SQLite, { debug: false }).then((driver) => {
api.setCacheDriver(driver);
});
```
53 changes: 53 additions & 0 deletions docs/middlewares.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 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, paths: IMiddlewarePaths, 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 !

Here's a barebone example :

```javascript
const API_OPTIONS = {
// ... all your api options
middlewares: [exampleMiddleware],
};

async function exampleMiddleware (serviceDefinition, serviceOptions) {
// This will be printed everytime you call a service
console.log('You just fired a request for the path ' + serviceDefinition.path);
}
```

You can even make API calls in your middlewares. For instance, you might want to make sure the user is logged in into your API, or you might want to refresh its authentication token once in a while. Like so :

```javascript
const API_OPTIONS = {
// ... all your api options
middlewares: [authMiddleware]
}

async function authMiddleware (serviceDefinition, serviceOptions) {
if (authToken && !tokenExpired) {
// Our token is up-to-date, add it to the headers of our request
return { headers:'X-Auth-Token': authToken } };
}
// Token is missing or outdated, let's fetch a new one
try {
// Assuming our login service's method is already set to 'POST'
const authData = await api.fetch(
'login',
// the 'fetcthOptions' key allows us to use any of react-native's fetch method options
// here, the body of our post request
{ fetchOptions: { body: 'username=user&password=password' } }
);
// Store our new authentication token and add it to the headers of our request
authToken = authData.authToken;
tokenExpired = false;
return { headers:'X-Auth-Token': authData.authToken } };
} catch (err) {
throw new Error(`Couldn't auth to API, ${err}`);
}
}
```
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": "2.1.0",
"version": "2.2.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 78635c3

Please sign in to comment.