From 7e7b61b81ef0355b2609c251e47d48f09900fb5c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 21 Mar 2017 21:38:28 +0530 Subject: [PATCH] feat(cache): support for in-memory cache --- src/Cache/index.js | 58 +++++++++++++++++++++++++++++++ src/Edge/index.js | 13 ------- src/Loader/index.js | 57 ++---------------------------- src/Template/index.js | 23 ++++++++++-- test/unit/edge.spec.js | 22 ------------ test/unit/loader.spec.js | 71 ++++++++++++++++++++++---------------- test/unit/template.spec.js | 23 +++++++++++- 7 files changed, 145 insertions(+), 122 deletions(-) create mode 100644 src/Cache/index.js diff --git a/src/Cache/index.js b/src/Cache/index.js new file mode 100644 index 0000000..ec919f4 --- /dev/null +++ b/src/Cache/index.js @@ -0,0 +1,58 @@ +'use strict' + +/* + * edge + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. +*/ + +class Cache { + constructor () { + this._items = {} + } + + /** + * Add key value pair to the memory cache + * + * @method add + * + * @param {String} location + * @param {String} output + * + * @return {void} + */ + add (location, output) { + this._items[location] = output + } + + /** + * Get value back from the cache + * + * @method get + * + * @param {String} location + * + * @return {String} + */ + get (location) { + return this._items[location] + } + + /** + * Remove item from the cache + * + * @method remove + * + * @param {String} location + * + * @return {void} + */ + remove (location) { + delete this._items[location] + } +} + +module.exports = new Cache() diff --git a/src/Edge/index.js b/src/Edge/index.js index 6807016..b07550b 100644 --- a/src/Edge/index.js +++ b/src/Edge/index.js @@ -57,19 +57,6 @@ class Edge { return new Template(this._tags, this._globals, this._loader) } - /** - * The directory to be used for reading compiled templates - * - * @method compiledDir - * - * @param {String} compiledDir - * - * @return {String} - */ - compiledDir (compiledDir) { - this._loader.compiledDir = compiledDir - } - /** * Registers a new tag. A tag must have following * attributes. diff --git a/src/Loader/index.js b/src/Loader/index.js index 50d8d15..a8c483c 100644 --- a/src/Loader/index.js +++ b/src/Loader/index.js @@ -21,10 +21,9 @@ const CE = require('../Exceptions') * @class Loader */ class Loader { - constructor (viewsPath, presentersPath, compiledDir) { + constructor (viewsPath, presentersPath) { this._viewsPath = viewsPath this._presentersPath = presentersPath - this._compiledDir = compiledDir } /** @@ -67,30 +66,6 @@ class Loader { this._presentersPath = presentersPath } - /** - * The path from where the load the compiled - * views. - * - * @attribute compiledDir - * - * @return {String} - */ - get compiledDir () { - return this._compiledDir - } - - /** - * Set the path to the compiled directory - * for reading pre-compiled views - * - * @param {String} compiledDir - * - * @return {void} - */ - set compiledDir (compiledDir) { - this._compiledDir = compiledDir - } - /** * Normalizes the view name * @@ -110,8 +85,8 @@ class Loader { * * @private */ - _normalizeViewName (view, extension = 'edge') { - return `${view.replace(/\.edge$/, '').replace(/\.(\w+|\d+)/, '/$1')}.${extension}` + normalizeViewName (view, extension = 'edge') { + return `${view.replace(/\.edge$/, '').replace(/\.(\w+|\d+)/g, '/$1').replace(/\/{3}/, '.')}.${extension}` } /** @@ -149,8 +124,6 @@ class Loader { * @throws {RunTimeException} If unable to load the view */ load (view) { - view = this._normalizeViewName(view) - try { return fs.readFileSync(this.getViewPath(view), 'utf-8') } catch (error) { @@ -188,30 +161,6 @@ class Loader { throw error } } - - /** - * Load and return pre compiled template. It will - * return null when unable to load the precompiled - * view. - * - * @method loadPreCompiled - * - * @param {String} view - * - * @return {Function|Null} - */ - loadPreCompiled (view) { - view = this._normalizeViewName(view, 'js') - if (!this.compiledDir) { - return null - } - - try { - return require(path.join(this.compiledDir, view)) - } catch (error) { - return null - } - } } module.exports = Loader diff --git a/src/Template/index.js b/src/Template/index.js index 5073c9f..29a065f 100644 --- a/src/Template/index.js +++ b/src/Template/index.js @@ -10,10 +10,12 @@ */ const _ = require('lodash') +const debug = require('debug')('edge:template') const TemplateCompiler = require('./Compiler') const TemplateRunner = require('./Runner') const Context = require('../Context') const BasePresenter = require('../Presenter') +const cache = require('../Cache') /** * Template class is used to compile and render the @@ -161,9 +163,24 @@ class Template { * @return {String} */ compile (view, asFunction = false) { + const normalizedView = this._loader.normalizeViewName(view) + const preCompiledView = cache.get(normalizedView) + + /** + * Return the precompiled view from the cache if + * it exists. + */ + if (preCompiledView) { + debug('resolving view %s from cache', normalizedView) + return preCompiledView + } + const compiler = new TemplateCompiler(this._tags, this._loader, asFunction) try { - return compiler.compile(view) + const compiledView = compiler.compile(normalizedView) + cache.add(normalizedView, compiledView) + debug('adding view %s to cache', normalizedView) + return compiledView } catch (error) { throw (this._prepareStack(error)) } @@ -200,7 +217,7 @@ class Template { */ render (view, data = {}) { this.sourceView(view) - const compiledTemplate = this._loader.loadPreCompiled(view) || this.compile(view, true) + const compiledTemplate = this.compile(view, true) this._makeContext(data) return new TemplateRunner(compiledTemplate, this).run() } @@ -232,7 +249,7 @@ class Template { * @return {String} */ runTimeRender (view) { - const compiledTemplate = this._loader.loadPreCompiled(view) || this.compile(view, true) + const compiledTemplate = this.compile(view, true) const template = new TemplateRunner(compiledTemplate, this).run() return template } diff --git a/test/unit/edge.spec.js b/test/unit/edge.spec.js index 6ef4c04..5831d4c 100644 --- a/test/unit/edge.spec.js +++ b/test/unit/edge.spec.js @@ -253,28 +253,6 @@ test.group('Edge', (group) => { assert.equal(output.trim(), '

Hello virk

') }) - test('define compiled dir', (assert) => { - const edge = new Edge() - const compiledDir = path.join(__dirname, '../../test-helpers/views/compiled') - edge.compiledDir(compiledDir) - assert.equal(compiledDir, edge._loader.compiledDir) - }) - - test('load a file from the precompiled template over compiling it from source', (assert) => { - const edge = new Edge() - const compiledDir = path.join(__dirname, '../../test-helpers/views/compiled') - edge.compiledDir(compiledDir) - edge._loader.load = function () { - throw new Error('Not expecting a call to load') - } - const output = edge.render('loopUsers', { - users: [{username: 'virk'}, {username: 'nikk'}] - }) - - assert.equal(output.trim(), `virk - nikk`) - }) - test('should have access to inbuilt globals', (assert) => { const edge = new Edge() const statement = `{{ size('foo') }}` diff --git a/test/unit/loader.spec.js b/test/unit/loader.spec.js index 82fe2f6..3fd764e 100644 --- a/test/unit/loader.spec.js +++ b/test/unit/loader.spec.js @@ -50,48 +50,83 @@ test.group('Loader', () => { test('throw exception when views path has not been registered', (assert) => { const loader = new Loader() const output = () => loader.load('foo') - assert.throw(output, 'E_MISSING_VIEW: Cannot render foo.edge. Make sure to register the views path') + assert.throw(output, 'E_MISSING_VIEW: Cannot render foo. Make sure to register the views path') }) test('throw exception if unable to load view', (assert) => { const viewsPath = path.join(__dirname, './') const loader = new Loader(viewsPath) const output = () => loader.load('foo') - assert.throw(output, `E_MISSING_VIEW: Cannot render foo.edge. Make sure the file exists at ${viewsPath} location`) + assert.throw(output, `E_MISSING_VIEW: Cannot render foo. Make sure the file exists at ${viewsPath} location`) }) test('load and return the view', (assert) => { const viewsPath = path.join(__dirname, '../../test-helpers/views') const loader = new Loader(viewsPath) - const output = loader.load('welcome') + const output = loader.load(loader.normalizeViewName('welcome')) assert.equal(output.trim(), '{{ username }}') }) + test('normalize view name', (assert) => { + const viewsPath = path.join(__dirname, '../../test-helpers/views') + const loader = new Loader(viewsPath) + const output = loader.normalizeViewName('welcome') + assert.equal(output, 'welcome.edge') + }) + + test('normalize view name when .edge extension is defined', (assert) => { + const viewsPath = path.join(__dirname, '../../test-helpers/views') + const loader = new Loader(viewsPath) + const output = loader.normalizeViewName('welcome.edge') + assert.equal(output, 'welcome.edge') + }) + + test('normalize nested view name', (assert) => { + const viewsPath = path.join(__dirname, '../../test-helpers/views') + const loader = new Loader(viewsPath) + const output = loader.normalizeViewName('layouts.users.list.edge') + assert.equal(output, 'layouts/users/list.edge') + }) + + test('normalize pre formatted view name', (assert) => { + const viewsPath = path.join(__dirname, '../../test-helpers/views') + const loader = new Loader(viewsPath) + const output = loader.normalizeViewName('layouts/users/list.edge') + assert.equal(output, 'layouts/users/list.edge') + }) + + test('escape .', (assert) => { + const viewsPath = path.join(__dirname, '../../test-helpers/views') + const loader = new Loader(viewsPath) + const output = loader.normalizeViewName('layouts.users//.list.edge') + assert.equal(output, 'layouts/users.list.edge') + }) + test('make absolute path to view', (assert) => { const viewsPath = path.join(__dirname, '../../test-helpers/views') const loader = new Loader(viewsPath) - const output = loader.getViewPath(loader._normalizeViewName('welcome')) + const output = loader.getViewPath(loader.normalizeViewName('welcome')) assert.equal(output, path.join(viewsPath, 'welcome.edge')) }) test('make absolute path to view when name has .edge extension', (assert) => { const viewsPath = path.join(__dirname, '../test-helpers/views') const loader = new Loader(viewsPath) - const output = loader.getViewPath(loader._normalizeViewName('welcome.edge')) + const output = loader.getViewPath(loader.normalizeViewName('welcome.edge')) assert.equal(output, path.join(viewsPath, 'welcome.edge')) }) test('make absolute path to view when name is seperated with (.)', (assert) => { const viewsPath = path.join(__dirname, '../test-helpers/views') const loader = new Loader(viewsPath) - const output = loader.getViewPath(loader._normalizeViewName('partials.users')) + const output = loader.getViewPath(loader.normalizeViewName('partials.users')) assert.equal(output, path.join(viewsPath, 'partials/users.edge')) }) test('make absolute path to view when name is seperated with (.) and has edge extension', (assert) => { const viewsPath = path.join(__dirname, '../test-helpers/views') const loader = new Loader(viewsPath) - const output = loader.getViewPath(loader._normalizeViewName('partials.users.edge')) + const output = loader.getViewPath(loader.normalizeViewName('partials.users.edge')) assert.equal(output, path.join(viewsPath, 'partials/users.edge')) }) @@ -114,26 +149,4 @@ test.group('Loader', () => { const output = () => loader.loadPresenter('Foo') assert.throw(output, `E_MISSING_PRESENTER: Cannot load Foo Presenter. Make sure the file exists at ${presentersPath} location`) }) - - test('load a pre-compiled template', (assert) => { - const compiledDir = path.join(__dirname, '../../test-helpers/views/compiled') - const loader = new Loader() - loader.compiledDir = compiledDir - const template = loader.loadPreCompiled('loopUsers') - assert.isFunction(template) - }) - - test('return null when unable to load a pre-compiled template', (assert) => { - const compiledDir = path.join(__dirname, '../../test-helpers/views/compiled') - const loader = new Loader() - loader.compiledDir = compiledDir - const template = loader.loadPreCompiled('foo') - assert.isNull(template) - }) - - test('return null when compiled dir is not registered', (assert) => { - const loader = new Loader() - const template = loader.loadPreCompiled('foo') - assert.isNull(template) - }) }) diff --git a/test/unit/template.spec.js b/test/unit/template.spec.js index 0e1c4c3..af32faf 100644 --- a/test/unit/template.spec.js +++ b/test/unit/template.spec.js @@ -14,8 +14,10 @@ const test = require('japa') const cheerio = require('cheerio') const dedent = require('dedent-js') const Template = require('../../src/Template') +const TemplateCompiler = require('../../src/Template/Compiler') const Loader = require('../../src/Loader') const Context = require('../../src/Context') +const cache = require('../../src/Cache') test.group('Template Compiler', (group) => { group.before(() => { @@ -218,7 +220,7 @@ test.group('Template Runner', () => { test('throw exception when layout file has invalid section name', (assert) => { const loader = new Loader(path.join(__dirname, '../../test-helpers/views')) const statement = dedent` - @layout('layouts.invalid.master') + @layout('layouts.invalid//.master') @section('content')

Hello world

@endsection @@ -317,4 +319,23 @@ test.group('Template Runner', () => {

Hey virk

`) }) + + test('add template to cache after compile', (assert) => { + const loader = new Loader(path.join(__dirname, '../../test-helpers/views')) + const template = new Template(this.tags, {}, loader) + template.compile('welcome') + assert.isDefined(cache._items['welcome.edge']) + }) + + test('rendering a view multiple times should get it from the cache', (assert) => { + const loader = new Loader(path.join(__dirname, '../../test-helpers/views')) + const template = new Template(this.tags, {}, loader) + template.compile('welcome') + const existingCompile = TemplateCompiler.prototype.compile + TemplateCompiler.prototype.compile = function () { + throw new Error('Template should have been fetched from cache') + } + template.compile('welcome') + TemplateCompiler.prototype.compile = existingCompile + }) })