Skip to content

Commit 78635c3

Browse files
authored
Merge pull request #10 from Exilz/2.2.0
v2.2.0
2 parents ce328ef + 956fa25 commit 78635c3

File tree

13 files changed

+405
-88
lines changed

13 files changed

+405
-88
lines changed

README.md

Lines changed: 6 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -213,93 +213,30 @@ The URL to your endpoints are being constructed with **your domain name, your op
213213
214214
## Limiting the size of your cache
215215

216-
If you fear your cache will keep growing, you have some options to make sure it doesn't get too big.
217-
218-
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.
219-
220-
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+
[Learn more about enabling capping in the documentation](docs/cache-size.md)
221217

222218
## Middlewares
223219

224-
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.**
225-
226-
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.
227-
228-
Anything you will resolve in those promises will be merged into your request's options !
229-
230-
Here's a barebone example :
231-
232-
```javascript
233-
const API_OPTIONS = {
234-
// ... all your api options
235-
middlewares: [exampleMiddleware],
236-
};
237-
238-
async function exampleMiddleware (serviceDefinition, serviceOptions) {
239-
// This will be printed everytime you call a service
240-
console.log('You just fired a request for the path ' + serviceDefinition.path);
241-
}
242-
```
243-
244-
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 :
245-
246-
```javascript
247-
const API_OPTIONS = {
248-
// ... all your api options
249-
middlewares: [authMiddleware]
250-
}
251-
252-
async function authMiddleware (serviceDefinition, serviceOptions) {
253-
if (authToken && !tokenExpired) {
254-
// Our token is up-to-date, add it to the headers of our request
255-
return { headers:'X-Auth-Token': authToken } };
256-
}
257-
// Token is missing or outdated, let's fetch a new one
258-
try {
259-
// Assuming our login service's method is already set to 'POST'
260-
const authData = await api.fetch(
261-
'login',
262-
// the 'fetcthOptions' key allows us to use any of react-native's fetch method options
263-
// here, the body of our post request
264-
{ fetchOptions: { body: 'username=user&password=password' } }
265-
);
266-
// Store our new authentication token and add it to the headers of our request
267-
authToken = authData.authToken;
268-
tokenExpired = false;
269-
return { headers:'X-Auth-Token': authData.authToken } };
270-
} catch (err) {
271-
throw new Error(`Couldn't auth to API, ${err}`);
272-
}
273-
}
274-
```
220+
[Check out middlewares documentation and examples](docs/middlewares.md)
275221

276222
## Using your own driver for caching
277223

278-
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).
279-
280-
> This is the first step for the wrapper to being also available on the browser and in any node.js environment.
281-
282-
Your custom driver must implement these 3 methods that are promises.
283-
284-
* `getItem(key: string, callback?: (error?: Error, result?: string) => void)`
285-
* `setItem(key: string, value: string, callback?: (error?: Error) => void);`
286-
* `removeItem(key: string, callback?: (error?: Error) => void);`
287-
* `multiRemove(keys: string[], callback?: (errors?: Error[]) => void);`
224+
> 💡 You can now use SQLite instead of AsyncStorage without additional code !
288225
289-
*Please note that, as of the 1.0 release, this hasn't been tested thoroughly.*
226+
[Check out drivers documentation and how to enable the SQLite driver](docs/custom-drivers.md)
290227

291228
## Types
292229

293230
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 :)
294231

295-
These are Typescript defintions, so they should be displayed in your editor/IDE if it supports it.
232+
> 💡 These are Typescript defintions, so they should be displayed in your editor/IDE if it supports it.
296233
297234
## Roadmap
298235

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

301238
- [ ] 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
302239
- [ ] 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
303-
- [ ] Thoroughly test custom caching drivers, maybe provide one (realm or sqlite)
304240
- [ ] Add automated testing
241+
- [x] Thoroughly test custom caching drivers, maybe provide one (realm or sqlite)
305242
- [x] Write a demo

demo/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"dependencies": {
1010
"react": "16.0.0-alpha.12",
1111
"react-native": "0.47.1",
12-
"react-native-offline-api": "2.1.0"
12+
"react-native-offline-api": "2.2.0"
1313
},
1414
"devDependencies": {
1515
"babel-jest": "20.0.3",

dist/drivers/sqlite.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"use strict";
2+
var __assign = (this && this.__assign) || Object.assign || function(t) {
3+
for (var s, i = 1, n = arguments.length; i < n; i++) {
4+
s = arguments[i];
5+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6+
t[p] = s[p];
7+
}
8+
return t;
9+
};
10+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
11+
return new (P || (P = Promise))(function (resolve, reject) {
12+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
13+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
14+
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
15+
step((generator = generator.apply(thisArg, _arguments || [])).next());
16+
});
17+
};
18+
var __generator = (this && this.__generator) || function (thisArg, body) {
19+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
20+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
21+
function verb(n) { return function (v) { return step([n, v]); }; }
22+
function step(op) {
23+
if (f) throw new TypeError("Generator is already executing.");
24+
while (_) try {
25+
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
26+
if (y = 0, t) op = [0, t.value];
27+
switch (op[0]) {
28+
case 0: case 1: t = op; break;
29+
case 4: _.label++; return { value: op[1], done: false };
30+
case 5: _.label++; y = op[1]; op = [0]; continue;
31+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
32+
default:
33+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
34+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
35+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
36+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
37+
if (t[2]) _.ops.pop();
38+
_.trys.pop(); continue;
39+
}
40+
op = body.call(thisArg, _);
41+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
42+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
43+
}
44+
};
45+
var _this = this;
46+
Object.defineProperty(exports, "__esModule", { value: true });
47+
exports.default = function (SQLite, options) { return __awaiter(_this, void 0, void 0, function () {
48+
var db, err_1;
49+
return __generator(this, function (_a) {
50+
switch (_a.label) {
51+
case 0:
52+
SQLite.DEBUG(options.debug || false);
53+
SQLite.enablePromise(true);
54+
_a.label = 1;
55+
case 1:
56+
_a.trys.push([1, 3, , 4]);
57+
return [4 /*yield*/, SQLite.openDatabase(__assign({ name: 'offlineapi.db', location: 'default' }, (options.openDatabaseOptions || {})))];
58+
case 2:
59+
db = _a.sent();
60+
db.transaction(function (tx) {
61+
tx.executeSql('CREATE TABLE IF NOT EXISTS cache (id TEXT PRIMARY KEY NOT NULL, value TEXT);');
62+
});
63+
return [2 /*return*/, {
64+
getItem: getItem(db),
65+
setItem: setItem(db),
66+
removeItem: removeItem(db),
67+
multiRemove: multiRemove(db)
68+
}];
69+
case 3:
70+
err_1 = _a.sent();
71+
throw new Error("react-native-offline-api : Cannot open SQLite database : " + err_1 + ". Check your SQLite configuration.");
72+
case 4: return [2 /*return*/];
73+
}
74+
});
75+
}); };
76+
function getItem(db) {
77+
return function (key) {
78+
return new Promise(function (resolve, reject) {
79+
db.transaction(function (tx) {
80+
tx.executeSql('SELECT * FROM cache WHERE id=?', [key])
81+
.then(function (res) {
82+
var results = res[1];
83+
var item = results.rows.item(0);
84+
return resolve(item && item.value || null);
85+
})
86+
.catch(function (err) {
87+
return reject(err);
88+
});
89+
});
90+
});
91+
};
92+
}
93+
function setItem(db) {
94+
return function (key, value) {
95+
return new Promise(function (resolve, reject) {
96+
db.transaction(function (tx) {
97+
tx.executeSql('INSERT OR REPLACE INTO cache VALUES (?,?)', [key, value])
98+
.then(function () {
99+
return resolve();
100+
}).
101+
catch(function (err) {
102+
return reject(err);
103+
});
104+
});
105+
});
106+
};
107+
}
108+
function removeItem(db) {
109+
return function (key) {
110+
return new Promise(function (resolve, reject) {
111+
db.transaction(function (tx) {
112+
tx.executeSql('DELETE FROM cache WHERE id=?', [key])
113+
.then(function () {
114+
return resolve();
115+
})
116+
.catch(function (err) {
117+
return reject(err);
118+
});
119+
});
120+
});
121+
};
122+
}
123+
function multiRemove(db) {
124+
return function (keys) {
125+
return new Promise(function (resolve, reject) {
126+
// This implmementation is not the most efficient, must delete using
127+
// WHERE id IN (...,...) doesn't seem to be working at the moment.
128+
db.transaction(function (tx) {
129+
var promises = [];
130+
keys.forEach(function (key) {
131+
promises.push(tx.executeSql('DELETE FROM cache WHERE id=?', [key]));
132+
});
133+
Promise.all(promises)
134+
.then(function () {
135+
return resolve();
136+
})
137+
.catch(function (err) {
138+
return reject(err);
139+
});
140+
});
141+
});
142+
};
143+
}

dist/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
4444
};
4545
Object.defineProperty(exports, "__esModule", { value: true });
4646
var react_native_1 = require("react-native");
47+
var sqlite_1 = require("./drivers/sqlite");
4748
var _mapValues = require("lodash.mapvalues");
4849
var _merge = require("lodash.merge");
4950
var sha = require("jssha");
@@ -64,6 +65,7 @@ var DEFAULT_SERVICE_OPTIONS = {
6465
prefix: 'default'
6566
};
6667
var DEFAULT_CACHE_DRIVER = react_native_1.AsyncStorage;
68+
exports.drivers = { sqliteDriver: sqlite_1.default };
6769
var OfflineFirstAPI = /** @class */ (function () {
6870
function OfflineFirstAPI(options, services, driver) {
6971
this._APIServices = {};
@@ -307,7 +309,7 @@ var OfflineFirstAPI = /** @class */ (function () {
307309
this._log("service " + service + " cap reached (" + cachedItemsCount + " / " + capLimit + "), removing the oldest cached item...");
308310
key = this._getOldestCachedItem(dictionary).key;
309311
delete dictionary[key];
310-
return [4 /*yield*/, this._APIDriver.removeItem(key)];
312+
return [4 /*yield*/, this._APIDriver.removeItem(this._getCacheObjectKey(key))];
311313
case 5:
312314
_a.sent();
313315
this._APIDriver.setItem(serviceDictionaryKey, JSON.stringify(dictionary));

docs/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Additional documentation
2+
3+
* [Limiting the size of your cache](cache-size.md)
4+
* [Using your own driver or SQLite for caching](custom-drivers.md)
5+
* [Middlewares documentation](middlewares.md)

docs/cache-size.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Limiting the size of your cache
2+
3+
If you fear your cache will keep growing, you have some options to make sure it doesn't get too big.
4+
5+
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.
6+
7+
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.

docs/custom-drivers.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Using your own driver for caching
2+
3+
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).
4+
5+
Your custom driver must implement these 3 methods that are promises.
6+
7+
* `getItem(key: string): Promise<any>;`
8+
* `setItem(key: string, value: string): Promise<void>;`
9+
* `removeItem(key: string): Promise<void>;`
10+
* `multiRemove(keys: string[]): Promise<void>;`
11+
12+
## SQLite Driver
13+
14+
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 :
15+
16+
```javascript
17+
import OfflineFirstAPI, { drivers } from 'react-native-offline-api';
18+
import SQLite from 'react-native-sqlite-storage';
19+
20+
// ...
21+
22+
const api = new OfflineFirstAPI(API_OPTIONS, API_SERVICES);
23+
24+
drivers.sqliteDriver(SQLite, { debug: false }).then((driver) => {
25+
api.setCacheDriver(driver);
26+
});
27+
```

docs/middlewares.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Middlewares
2+
3+
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.**
4+
5+
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.
6+
7+
Anything you will resolve in those promises will be merged into your request's options !
8+
9+
Here's a barebone example :
10+
11+
```javascript
12+
const API_OPTIONS = {
13+
// ... all your api options
14+
middlewares: [exampleMiddleware],
15+
};
16+
17+
async function exampleMiddleware (serviceDefinition, serviceOptions) {
18+
// This will be printed everytime you call a service
19+
console.log('You just fired a request for the path ' + serviceDefinition.path);
20+
}
21+
```
22+
23+
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 :
24+
25+
```javascript
26+
const API_OPTIONS = {
27+
// ... all your api options
28+
middlewares: [authMiddleware]
29+
}
30+
31+
async function authMiddleware (serviceDefinition, serviceOptions) {
32+
if (authToken && !tokenExpired) {
33+
// Our token is up-to-date, add it to the headers of our request
34+
return { headers:'X-Auth-Token': authToken } };
35+
}
36+
// Token is missing or outdated, let's fetch a new one
37+
try {
38+
// Assuming our login service's method is already set to 'POST'
39+
const authData = await api.fetch(
40+
'login',
41+
// the 'fetcthOptions' key allows us to use any of react-native's fetch method options
42+
// here, the body of our post request
43+
{ fetchOptions: { body: 'username=user&password=password' } }
44+
);
45+
// Store our new authentication token and add it to the headers of our request
46+
authToken = authData.authToken;
47+
tokenExpired = false;
48+
return { headers:'X-Auth-Token': authData.authToken } };
49+
} catch (err) {
50+
throw new Error(`Couldn't auth to API, ${err}`);
51+
}
52+
}
53+
```

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-offline-api",
3-
"version": "2.1.0",
3+
"version": "2.2.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)