Skip to content

Instantly share code, notes, and snippets.

@eliseumds
Last active August 29, 2015 14:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eliseumds/8f970d8929e61b981438 to your computer and use it in GitHub Desktop.
Save eliseumds/8f970d8929e61b981438 to your computer and use it in GitHub Desktop.
Cached API client with decorator

Cached API client with decorator

This approach has some really cool benefits:

  • The API class stays as a small and clean unit
  • Easy to use other caching techniques. Even multiple are supported simultaneously
  • Explicit code

Http Client

Another cool thing is to explicitly pass your client (HTTP, WebSocket, database, etc) as an argument to the constructor of your API:

const api = new MyApi(new HttpClient());

What about the cache?

const api = new InMemoryApiCacheDecorator(
    new MyApi(new HttpClient());
);

api.get('/myresource/1').then(...);

Oh, what about cancelling my request?

Our HttpClient already returns a CancellablePromiseRequest. It is a wrapper over jQuery.ajax promises:

var myRequest = api.get('/myresource/1').then(...);

myRequest.cancel(); // it throws Promise.CancellationError

How to easily cancel bunches of requests?

See https://gist.github.com/eliseumds/1f01d9f017b356ba0286

/* global $ */
const DEFAULT_OPTIONS = {
timeout: null
};
class Cache {
_store = {};
/*
* @constructor
* @param {object} options
* @param {number} [options.timeout] timeout in milliseconds
*/
constructor(options = {}) {
options = $.extend(true, {}, DEFAULT_OPTIONS, options);
if (options.timeout !== null && typeof options.timeout !== 'number') {
throw new TypeError('`options.timeout` must be a number');
}
this._timeout = options.timeout;
}
/*
* @param {*} item
* @return {boolean}
*/
_isExpired(item) {
var now;
if (!this._timeout) {
return false;
}
now = +new Date();
if (now > (item.createdAt + this._timeout)) {
return true;
}
return false;
}
/*
* @param {string} key
* @return {*}
*/
get(key) {
if (!this.has(key)) {
return false;
}
return this._store[key].content;
}
/*
* @param {string} key
* @return {boolean}
*/
has(key) {
var item;
if (!(key in this._store)) {
return false;
}
item = this._store[key];
if (this._isExpired(item)) {
this.remove(key);
return false;
}
return true;
}
/*
* @param {string} key
* @return {boolean}
*/
remove(key) {
return delete this._store[key];
}
/*
* @param {string} key
* @param {*} content
* @return {boolean}
*/
set(key, content) {
this._store[key] = {
content: content,
createdAt: +new Date()
};
return true;
}
}
import Promise from 'bluebird';
class CancellablePromiseRequest {
/**
* @constructor
* @param {*} request
*/
constructor(request) {
if (request === undefined || request === null) {
throw new TypeError('`request` is required');
}
this.promise = Promise.resolve(request);
this.promise.cancellable();
this.promise.catch(Promise.CancellationError, function(error) {
var isAbortable = request.constructor === Object && 'abort' in request;
if (isAbortable) {
request.abort();
}
});
}
cancel() {
return this.promise.cancel.apply(
this.promise,
arguments
);
}
catch() {
return this.promise.catch.apply(
this.promise,
arguments
);
}
then(callback) {
return this.promise.then.apply(
this.promise,
arguments
);
}
}
/* global $ */
import CancellablePromiseRequest from './CancellablePromiseRequest';
class HttpClient /* implements NetworkClientInterface */ {
/**
* @param {string} HTTP request method
* @param {string} URL
* @param {object} [data]
* @return {CancellablePromiseRequest}
*/
_fire(type, url, data) {
return new CancellablePromiseRequest($.ajax({
data: data,
traditional: true,
type: type,
url: url
}));
}
delete(url, params) {
return this._fire('DELETE', url, params);
}
get(url, params) {
return this._fire('GET', url, params);
}
post(url, params) {
return this._fire('POST', url, params);
}
put(url, params) {
return this._fire('PUT', url, params);
}
}
import Cache from './cache';
import CancellablePromiseRequest from './CancellablePromiseRequest';
import Immutable from 'immutable';
import moment from 'moment';
class InMemoryApiCacheDecorator {
_cache = new Cache({
timeout: moment.duration({minutes: 10}).asMilliseconds()
});
/*
* @constructor
* @param {*} decorated instance
*/
constructor(decorated) {
this.decorated = decorated;
}
_hash(url, params) {
return Immutable.fromJS({url, params}).hashCode();
}
/*
* @param {string} url
* @param {object} [params]
* @return CancellablePromise
*/
get(url, params) {
const hash = this._hash(url, params);
if (this._cache.has(hash)) {
// pre-filled promise
return new CancellablePromiseRequest(
this._cache.get(hash)
);
}
return this.decorated.get.apply(this, arguments).then(response => {
this._cache.set(hash, Immutable.fromJS(response))
return response;
});
}
}
// TODO: implement
class MyApi {
/*
* @constructor
* @param {NetworkClientInterface} client
*/
constructor(client) {
this.client = client;
}
/*
* @param {string} url
* @param {object} [params]
* @return CancellablePromise
*/
get(url, params) {
return this.client.get(url, params);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment