Skip to content

Commit

Permalink
Merge pull request #126 from opentable/client-memory-leak
Browse files Browse the repository at this point in the history
Client performance optimisations, readme, and cleanup
  • Loading branch information
matteofigus committed Oct 21, 2015
2 parents 545d48f + d41074d commit 7336c5e
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 151 deletions.
67 changes: 67 additions & 0 deletions client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,70 @@ Node.js client for [OpenComponents](https://github.com/opentable/oc)
Node.js version: **0.10.0** required

Build status: Linux: [![Build Status](https://secure.travis-ci.org/opentable/oc.png?branch=master)](http://travis-ci.org/opentable/oc) | Windows: [![Build status](https://ci.appveyor.com/api/projects/status/8cklgw4hymutqrsg?svg=true)](https://ci.appveyor.com/project/matteofigus/oc)


Disclaimer: This project is still under heavy development and the API is likely to change at any time. In case you would find any issues, check the [troubleshooting page](../CONTRIBUTING.md#troubleshooting).

# API

### new Client(options)

It will create an instance of the client. Options:

|Parameter|type|mandatory|description|
|---------|----|---------|-----------|
|`cache`|`object`|no|Cache options. If null or empty will use default settings (never flush the cache)|
|`cache.flushInterval`|`number` (seconds)|no|The interval for flushing the cache|
|`components`|`object`|yes|The components to consume with versions|
|`components[name]`|`string`|yes|The component version|
|`registries`|`array of string`|yes|The array of registries' endpoints|

Example:

```js
var Client = require('oc-client');

var client = new Client({
registries: ['https://myregistry.com/'],
components: {
hello: '1.2.3',
world: '~2.2.5',
bla: ''
}
});

```

### Client#renderComponent(componentName [, options], callback)

It will resolve a component href, will make a request to the registry, and will render the component. The callback will contain an error (if present) and rendered html.

Options:

|Parameter|type|mandatory|description|
|---------|----|---------|-----------|
|`container`|`boolean`|no|Default true, when false, renders a component without its <oc-component> container|
|`disableFailoverRendering`|`boolean`|no|Disables the automatic failover rendering in case the registry times-out. Default false|
|`headers`|`object`|no|An object containing all the headers that must be forwarded to the component|
|`ie8`|`boolean`|no|Default false, if true puts in place the necessary polyfills to make all the stuff work with ie8|
|`params`|`object`|no|An object containing the parameters for component's request|
|`render`|`string`|no|Default `server`. When `client` will produce the html to put in the page for post-poning the rendering to the browser|
|`timeout`|`number` (seconds)|no|Default 5. When request times-out, the callback will be fired with a timeout error and a client-side rendering response (unless `disableFailoverRendering` is set to `true`)|

Example:
```js
...
client.renderComponent('header', {
container: false,
headers: {
'accept-language': 'en-GB'
},
params: {
loggedIn: true
},
timeout: 2
}, function(err, html){
console.log(html);
// => "<div>This is the header. <a>Log-out</a></div>"
});
```
168 changes: 80 additions & 88 deletions client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,17 @@ var vm = require('vm');

var Handlebars = require('./renderers/handlebars');
var Jade = require('./renderers/jade');
var request = require('./request');
var request = require('./utils/request');
var settings = require('./settings');
var _ = require('./utils/helpers');

var isLocal = function(apiResponse){
return apiResponse.type === 'oc-component-local';
};

var readJson = function(file, callback){
fs.readFile(file, {}, function(err, data) {

if(err){
return callback(err);
}
if(err){ return callback(err); }

var obj = null;

Expand Down Expand Up @@ -50,7 +48,7 @@ var loadConfig = function(callback){
var nextConfigFolder = path.resolve(baseDir, '..');

if(nextConfigFolder === baseDir){
return callback('File not found');
return callback(settings.messages.fileNotFound);
} else {
return checkConfigInFolder(nextConfigFolder, callback);
}
Expand All @@ -60,20 +58,70 @@ var loadConfig = function(callback){

return checkConfigInFolder(currentFolder, callback);
};

var getRenderedComponent = function(data){

if(!data.container){ return data.html; }

var random = Math.floor(Math.random()*9999999999);

return format('<oc-component href="{0}" data-hash="{1}" id="{2}" data-rendered="true" data-version="{3}">{4}</oc-component>',
data.href, data.key, random, data.version, data.html);
};


var getUnrenderedComponent = function(href, options){

if(!options || !options.ie8){
return format('<oc-component href="{0}" data-rendered="false"></oc-component>', href);
}

return format('<script class="ocComponent">(function($d,$w,oc){var href=\'href="{0}"\';' +
'$d.write((!!$w.navigator.userAgent.match(/MSIE 8/))?(\'<div data-oc-component="true" \'+href+\'>' +
'</div>\'):(\'<oc-component \'+href+\'></oc-component>\'));if(oc) oc.renderUnloadedComponents();}' +
'(document,window,((typeof(oc)===\'undefined\')?undefined:oc)));</script>', href);
};

module.exports = function(conf){

var Client = function(conf){
this.renderers = {
handlebars: new Handlebars(),
jade: new Jade()
};

this.cacheManager = new Cache(!!conf && !!conf.cache ? conf.cache : {});
this.config = conf;
this.cacheManager = new Cache(_.has(conf, 'cache') ? conf.cache : {});

var self = this;

var getCompiledTemplate = function(template, tryGetCached, timeout, callback){
if(!!tryGetCached){
var compiledTemplate = self.cacheManager.get('template', template.key);
if(!!compiledTemplate){
return callback(null, compiledTemplate);
}
}

request(template.src, {}, timeout, function(err, templateText){
if(!!err){ return callback(err); }

var context = { jade: require('jade/runtime.js')};
vm.runInNewContext(templateText, context);
var compiledTemplate = context.oc.components[template.key];
self.cacheManager.set('template', template.key, compiledTemplate);
return callback(null, compiledTemplate);
});
};

this.renderComponent = function(componentName, options, callback){
var self = this;
if(_.isFunction(options)){
callback = options;
options = {};
}

options = options || {};
options.headers = options.headers || {};
options.timeout = options.timeout || 5;

var getConfig = function(callback){
if(!!self.config){
Expand All @@ -90,20 +138,16 @@ var Client = function(conf){
};

getConfig(function(err, config){
if(!!err){
return callback(err);
}
if(!!err){ return callback(err); }

if(!!config.registries && typeof(config.registries) === 'string'){
config.registries = [config.registries];
}
config.registries = _.toArray(config.registries);

if(!config.registries || config.registries.length === 0){
return callback('Configuration is not valid - Registry location not found');
if(_.isEmpty(config.registries)){
return callback(settings.messages.registryUrlMissing);
}

if(!config.components || !config.components.hasOwnProperty(componentName)){
return callback('Configuration is not valid - Component not found');
if(!_.has(config.components, componentName)){
return callback(settings.messages.componentMissing);
}

var version = config.components[componentName],
Expand All @@ -122,16 +166,15 @@ var Client = function(conf){
};

this.render = function(href, options, callback){
if(typeof options === 'function'){
if(_.isFunction(options)){
callback = options;
options = {};
}

var self = this;

options = options || {};
options.headers = options.headers || {};
options.headers.accept = 'application/vnd.oc.prerendered+json';
options.timeout = options.timeout || 5;

var processError = function(){
var errorDescription = settings.messages.serverSideRenderingFail;
Expand All @@ -141,12 +184,12 @@ var Client = function(conf){
}

fs.readFile(path.resolve(__dirname, './oc-client.min.js'), 'utf-8', function(err, clientJs){
var clientSideHtml = format('<script class="ocClientScript">{0}</script>{1}', clientJs, self.getUnrenderedComponent(href, options));
var clientSideHtml = format('<script class="ocClientScript">{0}</script>{1}', clientJs, getUnrenderedComponent(href, options));
return callback(errorDescription, clientSideHtml);
});
};

request(href, options.headers, function(err, apiResponse){
request(href, options.headers, options.timeout, function(err, apiResponse){

if(err){
return processError();
Expand All @@ -162,82 +205,31 @@ var Client = function(conf){
local = isLocal(apiResponse);

if(options.render === 'client'){
return callback(null, self.getUnrenderedComponent(href, options));
return callback(null, getUnrenderedComponent(href, options));
}

self.getStaticTemplate(apiResponse.template.src, !local, function(templateText){

var context = apiResponse.template.type === 'jade' ? {
jade: require('jade/runtime.js')
} : {};
getCompiledTemplate(apiResponse.template, !local, options.timeout, function(err, template){

vm.runInNewContext(templateText, context);
if(!!err){ return processError(); }

var key = apiResponse.template.key,
template = context.oc.components[key],
renderOptions = {
href: href,
key: key,
version: apiResponse.version,
templateType: apiResponse.template.type,
container: (options.container === true) ? true : false
};
var renderOptions = {
href: href,
key: apiResponse.template.key,
version: apiResponse.version,
templateType: apiResponse.template.type,
container: (options.container === true) ? true : false
};

self.renderTemplate(template, data, renderOptions, callback);
return self.renderTemplate(template, data, renderOptions, callback);
});
}
});
};

this.getUnrenderedComponent = function(href, options){

if(!options || !options.ie8){
return format('<oc-component href="{0}" data-rendered="false"></oc-component>', href);
}

return format('<script class="ocComponent">(function($d,$w,oc){var href=\'href="{0}"\';' +
'$d.write((!!$w.navigator.userAgent.match(/MSIE 8/))?(\'<div data-oc-component="true" \'+href+\'>' +
'</div>\'):(\'<oc-component \'+href+\'></oc-component>\'));if(oc) oc.renderUnloadedComponents();}' +
'(document,window,((typeof(oc)===\'undefined\')?undefined:oc)));</script>', href);
};

this.getRenderedComponent = function(data){

if(!data.container){
return data.html;
}

var random = Math.floor(Math.random()*9999999999);

return format('<oc-component href="{0}" data-hash="{1}" id="{2}" data-rendered="true" data-version="{3}">{4}</oc-component>',
data.href, data.key, random, data.version, data.html);
};

this.getStaticTemplate = function(templatePath, tryGetCached, callback){
var self = this;

if(!!tryGetCached){
var template = self.cacheManager.get('template', templatePath);
if(!!template){
return callback(template);
}
}

request(templatePath, function(err, template){
self.cacheManager.set('template', templatePath, template);
callback(template);
});
};

this.renderTemplate = function(template, data, options, callback){

var getRendered = this.getRenderedComponent;

this.renderers[options.templateType].render(template, data, function(err, html){
options.html = html;
callback(err, getRendered(options));
return callback(err, getRenderedComponent(options));
});
};
};

module.exports = Client;
};
51 changes: 0 additions & 51 deletions client/request.js

This file was deleted.

Loading

0 comments on commit 7336c5e

Please sign in to comment.