marko/taglibs/async/async-fragment-tag.js
2016-05-09 14:19:42 -07:00

212 lines
5.9 KiB
JavaScript

'use strict';
var logger = require('raptor-logging').logger(module);
var asyncWriter = require('async-writer');
var AsyncValue = require('raptor-async/AsyncValue');
var isClientReorderSupported = require('./client-reorder').isSupported;
function isPromise(o) {
return o && typeof o.then === 'function';
}
function promiseToCallback(promise, callback, thisObj) {
if (callback) {
var finalPromise = promise
.then(function(data) {
callback(null, data);
});
if (typeof promise.catch === 'function') {
finalPromise = finalPromise.catch(function(err) {
callback(err);
});
} else if (typeof promise.fail === 'function') {
finalPromise = finalPromise.fail(function(err) {
callback(err);
});
}
if (finalPromise.done) {
finalPromise.done();
}
}
return promise;
}
function requestData(provider, args, callback, thisObj) {
if (isPromise(provider)) {
// promises don't support a scope so we can ignore thisObj
promiseToCallback(provider, callback);
return;
}
if (typeof provider === 'function') {
var data = (provider.length === 1) ?
// one argument so only provide callback to function call
provider.call(thisObj, callback) :
// two arguments so provide args and callback to function call
provider.call(thisObj, args, callback);
if (data !== undefined) {
if (isPromise(data)) {
promiseToCallback(data, callback);
}
else {
callback(null, data);
}
}
} else {
// Assume the provider is a data object...
callback(null, provider);
}
}
module.exports = function render(input, out) {
var dataProvider = input.dataProvider;
var arg = input.arg || {};
arg.out = out;
var events = out.global.events;
var clientReorder = isClientReorderSupported && input.clientReorder === true;
var asyncOut;
var done = false;
var timeoutId = null;
var name = input.name || input._name;
var scope = input.scope || this;
function renderBody(err, data, renderTimeout) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
done = true;
var targetOut = asyncOut || out;
events.emit('asyncFragmentBeforeRender', {
clientReorder: clientReorder,
out: targetOut,
name: name
});
if (err) {
if (input.renderError) {
console.error('Async fragment (' + name + ') failed. Error:', (err.stack || err));
input.renderError(targetOut);
} else {
targetOut.error(err);
}
} else if (renderTimeout) {
renderTimeout(asyncOut);
} else {
if (input.renderBody) {
input.renderBody(targetOut, data);
}
}
if (!clientReorder) {
events.emit('asyncFragmentFinish', {
clientReorder: false,
out: targetOut,
name: name
});
}
if (asyncOut) {
asyncOut.end();
// Only flush if we rendered asynchronously and we aren't using
// client-reordering
if (!clientReorder) {
out.flush();
}
}
}
var method = input.method;
if (method) {
dataProvider = dataProvider[method].bind(dataProvider);
}
requestData(dataProvider, arg, renderBody, scope);
if (!done) {
var timeout = input.timeout;
var renderTimeout = input.renderTimeout;
var renderPlaceholder = input.renderPlaceholder;
if (timeout == null) {
timeout = 10000;
} else if (timeout <= 0) {
timeout = null;
}
if (timeout != null) {
timeoutId = setTimeout(function() {
var message = 'Async fragment (' + name + ') timed out after ' + timeout + 'ms';
if (renderTimeout) {
logger.error(message);
renderBody(null, null, renderTimeout);
} else {
renderBody(new Error(message));
}
}, timeout);
}
if (clientReorder) {
var asyncFragmentContext = out.global.__asyncFragments || (asyncFragmentContext = out.global.__asyncFragments = {
fragments: [],
nextId: 0
});
var id = input.name || asyncFragmentContext.nextId++;
if (renderPlaceholder) {
out.write('<span id="afph' + id + '">');
renderPlaceholder(out);
out.write('</span>');
} else {
out.write('<noscript id="afph' + id + '"></noscript>');
}
var asyncValue = new AsyncValue();
// Write to an in-memory buffer
asyncOut = asyncWriter.create(null, {global: out.global});
asyncOut
.on('finish', function() {
asyncValue.resolve(asyncOut.getOutput());
})
.on('error', function(err) {
asyncValue.reject(err);
});
var fragmentInfo = {
id: id,
asyncValue: asyncValue,
out: asyncOut,
after: input.showAfter
};
if (asyncFragmentContext.fragments) {
asyncFragmentContext.fragments.push(fragmentInfo);
} else {
events.emit('asyncFragmentBegin', fragmentInfo);
}
} else {
out.flush(); // Flush everything up to this async fragment
asyncOut = out.beginAsync({
timeout: 0, // We will use our code for controlling timeout
name: name
});
}
}
};