Skip to content

Commit 5a3c4ad

Browse files
committed
feat(api): add service capping logic
1 parent bca7fdc commit 5a3c4ad

File tree

3 files changed

+125
-15
lines changed

3 files changed

+125
-15
lines changed

dist/index.js

Lines changed: 58 additions & 12 deletions
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 = {};
@@ -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

src/index.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
IFetchOptions,
1010
IFetchResponse,
1111
ICachedData,
12+
ICacheDictionary,
1213
IAPIDriver,
1314
APIMiddleware
1415
} from './interfaces';
@@ -20,6 +21,8 @@ const DEFAULT_API_OPTIONS = {
2021
disableCache: false,
2122
cacheExpiration: 5 * 60 * 1000,
2223
cachePrefix: 'offlineApiCache',
24+
capServices: false,
25+
capLimit: 50
2326
};
2427

2528
const DEFAULT_SERVICE_OPTIONS = {
@@ -105,7 +108,7 @@ export default class OfflineFirstAPI {
105108

106109
// Cache if it hasn't been disabled and if the network request has been successful
107110
if (res.data.ok && shouldUseCache) {
108-
this._cache(service, requestId, parsedResponseData, expiration);
111+
this._cache(serviceDefinition, service, requestId, parsedResponseData, expiration);
109112
}
110113

111114
this._log('parsed network response', parsedResponseData);
@@ -199,12 +202,41 @@ export default class OfflineFirstAPI {
199202
* @returns {(Promise<void|boolean>)}
200203
* @memberof OfflineFirstAPI
201204
*/
202-
private async _cache (service: string, requestId: string, response: any, expiration: number): Promise<void|boolean> {
203-
this._log(`Caching ${requestId} ...`);
205+
private async _cache (
206+
serviceDefinition: IAPIService,
207+
service: string, requestId: string,
208+
response: any, expiration: number
209+
): Promise<void|boolean> {
210+
const shouldCap =
211+
typeof serviceDefinition.capService !== 'undefined' ?
212+
serviceDefinition.capService :
213+
this._APIOptions.capServices;
214+
204215
try {
216+
this._log(`Caching ${requestId} ...`);
205217
await this._addKeyToServiceDictionary(service, requestId, expiration);
206218
await this._APIDriver.setItem(this._getCacheObjectKey(requestId), JSON.stringify(response));
207219
this._log(`Updated cache for request ${requestId}`);
220+
221+
// If capping is enabled for this request, get the service's dictionary cached items.
222+
// If cap is reached, get the oldest cached item and remove it.
223+
if (shouldCap) {
224+
const capLimit = serviceDefinition.capLimit || this._APIOptions.capLimit;
225+
const serviceDictionaryKey = this._getServiceDictionaryKey(service);
226+
let dictionary = await this._APIDriver.getItem(serviceDictionaryKey);
227+
if (dictionary) {
228+
dictionary = JSON.parse(dictionary);
229+
const cachedItemsCount = Object.keys(dictionary).length;
230+
if (cachedItemsCount > capLimit) {
231+
this._log(`service ${service} cap reached (${cachedItemsCount} / ${capLimit}), removing the oldest cached item...`);
232+
const { key } = this._getOldestCachedItem(dictionary);
233+
delete dictionary[key];
234+
await this._APIDriver.removeItem(key);
235+
this._APIDriver.setItem(serviceDictionaryKey, JSON.stringify(dictionary));
236+
}
237+
}
238+
}
239+
208240
return true;
209241
} catch (err) {
210242
throw new Error(`Error while caching API response for ${requestId}`);
@@ -294,6 +326,29 @@ export default class OfflineFirstAPI {
294326
}
295327
}
296328

329+
330+
/**
331+
* Returns the key and the expiration date of the oldest cached item of a cache dictionary
332+
* @private
333+
* @param {ICacheDictionary} dictionary
334+
* @returns {*}
335+
* @memberof OfflineFirstAPI
336+
*/
337+
private _getOldestCachedItem (dictionary: ICacheDictionary): any {
338+
let oldest;
339+
for (let key in dictionary) {
340+
const keyExpiration: number = dictionary[key];
341+
if (oldest) {
342+
if (keyExpiration < oldest.expiration) {
343+
oldest = { key, expiration: keyExpiration };
344+
}
345+
} else {
346+
oldest = { key, expiration: keyExpiration };
347+
}
348+
}
349+
return oldest;
350+
}
351+
297352
/**
298353
* Promise that resolves every cache key associated to a service : the service dictionary's name, and all requestId
299354
* stored. This is useful to clear the cache without affecting the user's stored data not related to this API.

src/interfaces.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export interface IAPIOptions {
66
printNetworkRequests?: boolean;
77
disableCache?: boolean;
88
cacheExpiration?: number;
9+
cachePrefix?: string;
10+
capServices?: boolean;
11+
capLimit?: number;
912
offlineDriver: IAPIDriver;
1013
};
1114

@@ -17,6 +20,8 @@ export interface IAPIService {
1720
prefix?: string;
1821
middlewares?: APIMiddleware[];
1922
disableCache?: boolean;
23+
capService?: boolean;
24+
capLimit?: number;
2025
};
2126

2227
export interface IAPIServices {
@@ -43,6 +48,10 @@ export interface ICachedData {
4348
fresh?: boolean;
4449
}
4550

51+
export interface ICacheDictionary {
52+
[key: string]: number;
53+
}
54+
4655
export interface IAPIDriver {
4756
getItem(key: string, callback?: (error?: Error, result?: string) => void);
4857
setItem(key: string, value: string, callback?: (error?: Error) => void);

0 commit comments

Comments
 (0)