From 63b62960bcd798ca15ba94f0ed43866182c975ca Mon Sep 17 00:00:00 2001 From: JT Turner Date: Mon, 27 Aug 2018 07:10:04 -0700 Subject: [PATCH] Makes nested async helpers work correctly. --- lib/async.js | 90 -------------------------------- lib/generate-id.js | 16 ++++++ lib/hbs.js | 51 ++++++++++++++---- lib/resolver.js | 45 ++++++++++++++++ package-lock.json | 3 +- package.json | 2 + test/apps/async/index.js | 83 +++++++++++++++++++++++++++++ test/apps/async/views/failer.hbs | 4 ++ test/apps/async/views/index.hbs | 12 +++++ test/asyncSpecs.js | 19 ++++++- test/issues.js | 1 + 11 files changed, 221 insertions(+), 105 deletions(-) delete mode 100644 lib/async.js create mode 100644 lib/generate-id.js create mode 100644 lib/resolver.js create mode 100644 test/apps/async/views/failer.hbs diff --git a/lib/async.js b/lib/async.js deleted file mode 100644 index e4777e6..0000000 --- a/lib/async.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; - -/// provides the async helper functionality - -var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_'; - -function genId() { - var res = ''; - for (var i = 0; i < 8; ++i) { - res += alphabet[Math.floor(Math.random() * alphabet.length)]; - } - return res; -} - -// global baton which contains the current -// set of deferreds -var waiter; - -function Waiter() { - var self = this; - // found values - self.values = {}; - // callback when done - self.callback = null; - self.resolved = false; - self.count = 0; -} - -Waiter.prototype.wait = function() { - var self = this; - ++self.count; -}; - -// resolve the promise -Waiter.prototype.resolve = function(name, val) { - var self = this; - self.values[name] = val; - // done with all items - if (--self.count === 0) { - self.resolved = true; - // we may not have a done callback yet - if (self.callback) { - self.callback(self.values); - } - } -}; - -// sets the done callback for the waiter -// notifies when the promise is complete -Waiter.prototype.done = function(fn) { - var self = this; - - self.callback = fn; - if (self.resolved) { - fn(self.values); - // free mem - Object.keys(self.values).forEach(function(id) { - self.values[id] = null; - }); - } -}; - -// callback fn when all async helpers have finished running -// if there were no async helpers, then it will callback right away -Waiter.done = function(fn) { - // no async things called - if (!waiter) { - return fn({}); - } - waiter.done(fn); - // clear the waiter for the next template - waiter = undefined; -}; - -Waiter.resolve = function(fn, context) { - // we want to do async things, need a waiter for that - if (!waiter) { - waiter = new Waiter(); - } - var id = '__aSyNcId_<_' + genId() + '__'; - var curWaiter = waiter; - waiter.wait(); - fn(context, function(res) { - curWaiter.resolve(id, res); - }); - // return the id placeholder, which is replaced later - return id; -}; - -module.exports = Waiter; diff --git a/lib/generate-id.js b/lib/generate-id.js new file mode 100644 index 0000000..b12319f --- /dev/null +++ b/lib/generate-id.js @@ -0,0 +1,16 @@ +'use strict'; + +var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_'; + +function generateId(length) { + if (!length) { + length = 8; + } + var res = ''; + for (var i = 0; i < length; ++i) { + res += alphabet[Math.floor(Math.random() * alphabet.length)]; + } + return res; +} + +module.exports = generateId; diff --git a/lib/hbs.js b/lib/hbs.js index df546e9..7e02e23 100644 --- a/lib/hbs.js +++ b/lib/hbs.js @@ -4,7 +4,8 @@ var fs = require('fs'); var path = require('path'); var readdirp = require('readdirp'); var handlebars = require('handlebars'); -var async = require('./async'); +var resolver = require('./resolver'); +var _ = require('lodash'); /** * Regex pattern for layout directive. {{!< layout }} @@ -357,15 +358,28 @@ ExpressHbs.prototype.compile = function(source, filename) { */ ExpressHbs.prototype.registerAsyncHelper = function(name, fn) { this.handlebars.registerHelper(name, function(context, options) { + var resolverCache = this.resolverCache || + _.get(context, 'data.root.resolverCache') || + _.get(options, 'data.root.resolverCache'); + if (!resolverCache) { + throw new Error('Could not find resolver cache in async helper ' + name + '.'); + } if (options && fn.length > 2) { - var resolver = function(arr, cb) { + var resolveFunc = function(arr, cb) { return fn.call(this, arr[0], arr[1], cb); }; - return async.resolve(resolver.bind(this), [context, options]); + return resolver.resolve( + resolverCache, + resolveFunc.bind(this), + [context, options] + ); } - - return async.resolve(fn.bind(this), context); + return resolver.resolve( + resolverCache, + fn.bind(this), + context + ); }); }; @@ -419,6 +433,7 @@ ExpressHbs.prototype.___express = function ___express(filename, source, options, } options.blockCache = {}; + options.resolverCache = {}; this.viewsDir = options.settings.views || this.viewsDirOpt; var self = this; @@ -581,18 +596,32 @@ ExpressHbs.prototype.___express = function ___express(filename, source, options, }); } - // Handles waiting for async helpers - function handleAsync(err, res) { - if (err) return cb(err); - async.done(function(values) { + function replaceValue(values, text) { + if (typeof text === 'string') { Object.keys(values).forEach(function(id) { - res = res.replace(id, function() { + text = text.replace(id, function() { return values[id]; }); - res = res.replace(self.Utils.escapeExpression(id), function() { + text = text.replace(self.Utils.escapeExpression(id), function() { return self.Utils.escapeExpression(values[id]); }); }); + } + return text; + } + + // Handles waiting for async helpers + function handleAsync(err, res) { + if (err) return cb(err); + resolver.done(options.resolverCache, function(err, values) { + if (err) return cb(err); + Object.keys(values).forEach(function(key) { + values[key] = replaceValue(values, values[key]); + }); + res = replaceValue(values, res); + if (resolver.hasResolvers(res)) { + return handleAsync(null, res); + } cb(null, res); }); } diff --git a/lib/resolver.js b/lib/resolver.js new file mode 100644 index 0000000..ba31638 --- /dev/null +++ b/lib/resolver.js @@ -0,0 +1,45 @@ +'use strict'; + +var Promise = require('bluebird'); + +var generateId = require('./generate-id'); + +var ID_LENGTH = 8; +var MS_CHECK_TIME = 10; +var ID_PREFIX = '__aSyNcId_<_'; +var ID_SUFFIX = '__'; + +function resolve(cache, fn, context) { + var id = ID_PREFIX + generateId(ID_LENGTH) + ID_SUFFIX; + cache[id] = new Promise(function(passed, failed) { + try { + fn(context, function(res) { + passed(res); + }); + } catch(error) { + failed(error); + } + }); + return id; +} + +function done(cache, callback) { + Promise.props(cache).then(function(values) { + callback(null, values); + }).catch(function(error) { + callback(error); + }); +} + +function hasResolvers(text) { + if (text.search(ID_PREFIX) > 0) { + return true; + } + return false; +} + +module.exports = { + done: done, + hasResolvers: hasResolvers, + resolve: resolve +}; diff --git a/package-lock.json b/package-lock.json index 993d725..aa2585d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1094,8 +1094,7 @@ "lodash": { "version": "4.17.10", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", - "dev": true + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" }, "lodash._baseassign": { "version": "3.2.0", diff --git a/package.json b/package.json index e95830a..e767c4a 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,10 @@ "supertest": "2.0.1" }, "dependencies": { + "bluebird": "^3.5.1", "handlebars": "4.0.11", "js-beautify": "1.7.5", + "lodash": "^4.17.10", "readdirp": "2.1.0" } } diff --git a/test/apps/async/index.js b/test/apps/async/index.js index a674c7d..3ff587f 100644 --- a/test/apps/async/index.js +++ b/test/apps/async/index.js @@ -6,6 +6,54 @@ var fs = require('fs'); var path = require('path'); var url = require('url'); +var pages = [ + { + id: 1, + title: 'Title 1' + }, + { + id: 2, + title: 'Title 2' + }, + { + id: 3, + title: 'Title 3' + } +] + +var comments = [ + { + id: 1, + page: 1, + subject: 'Title 1 Comment 1', + auther: 'JT' + }, + { + id: 2, + page: 1, + subject: 'Title 1 Comment 2', + auther: 'Anna' + }, + { + id: 3, + page: 1, + subject: 'Title 1 Comment 3', + auther: 'Jane' + }, + { + id: 4, + page: 1, + subject: 'Title 1 Comment 4', + auther: 'Bob' + }, + { + id: 5, + page: 4, + subject: 'This should not show!', + auther: 'Jill' + } +] + function getRandomNumber(min, max) { return Math.random() * (max - min) + min; } @@ -31,12 +79,47 @@ function create(hbs, env) { }); }); + app.get('/fail', function (req, res) { + res.render('failer'); + }); + hbs.registerAsyncHelper('user', function(username, resultcb) { setTimeout(function() { resultcb(username); }, getRandomNumber(100, 900)) }); + hbs.registerAsyncHelper('pages', function(options, resultcb) { + var self = this; + setTimeout(function() { + var result = []; + for(var i = 0; i < pages.length; i++) { + options.data.page = pages[i]; + result.push(options.fn.call(self, pages[i], options)); + } + resultcb(result.join('')); + }, getRandomNumber(100, 900)) + }); + + hbs.registerAsyncHelper('comments', function(options, resultcb) { + var self = this; + setTimeout(function() { + var result = []; + for(var i = 0; i < comments.length; i++) { + if (options.hash.page === comments[i].page) { + result.push(options.fn(comments[i])); + } + } + resultcb(result.join('')); + }, getRandomNumber(100, 300)) + }); + + hbs.registerAsyncHelper('failer', function(_, resultcb) { + setTimeout(function() { + resultcb(options.fn()); + }, 100); + }) + return app; } diff --git a/test/apps/async/views/failer.hbs b/test/apps/async/views/failer.hbs new file mode 100644 index 0000000..7238e44 --- /dev/null +++ b/test/apps/async/views/failer.hbs @@ -0,0 +1,4 @@ +{{#contentFor "title"}} + Failer +{{/contentFor}} +This should fail {{#failer}}for sure{{/failer}}. diff --git a/test/apps/async/views/index.hbs b/test/apps/async/views/index.hbs index f8ee8e0..326a7e0 100644 --- a/test/apps/async/views/index.hbs +++ b/test/apps/async/views/index.hbs @@ -2,3 +2,15 @@ {{message}} {{{user username}}} {{/contentFor}} This should welcome {{username}}. +
+{{#pages filter="tags:test"}} +
+ {{title}} +
+ {{#comments top="3" page=id}} + {{subject}}-{{auther}} + {{/comments}} +
+
+{{/pages}} +
diff --git a/test/asyncSpecs.js b/test/asyncSpecs.js index dea55bb..e4d3c17 100644 --- a/test/asyncSpecs.js +++ b/test/asyncSpecs.js @@ -1,7 +1,10 @@ +'use stirct'; + var request = require('supertest'); var assert = require('assert'); var hbs = require('..'); var asyncApp = require('./apps/async'); +var resolver = require('../lib/resolver'); function makeUserRequest(app, user, cb) { request(app) @@ -12,12 +15,12 @@ function makeUserRequest(app, user, cb) { if (res.text.search('Hello, ' + user) <= 0) { return cb(new Error('Wrong template send for user ' + user + ': ' + res.text), user); } - return cb(null, user); + return cb(null, user, res.text); }); } function requestAll(app, users, cb) { - const status = {}; + var status = {}; for(var i = 0; i < users.length; i++) { status[users[i]] = 'Pending'; makeUserRequest(app, users[i], function(err, user) { @@ -51,4 +54,16 @@ describe('async', function() { done(); }); }); + + it('should render nested async helpers', function(done) { + var app = asyncApp.create(hbs.create(), 'production'); + makeUserRequest(app, 'jt', function(err, user, results) { + if (err) { + return done(err); + } + assert.equal(false, resolver.hasResolvers(results)); + assert.equal(-1, results.search('This should not show!')) + done(); + }); + }); }); diff --git a/test/issues.js b/test/issues.js index 37dc904..ed24084 100644 --- a/test/issues.js +++ b/test/issues.js @@ -234,6 +234,7 @@ describe('issue-53', function() { var render = hb.express3({}); var locals = H.createLocals('express3', dirname, {}); render(dirname + '/index.hbs', locals, function(err, html) { + assert.ifError(err); assert.ok(html.indexOf('__aSyNcId_') < 0); done(); });