marko/lib/Context.js
2014-03-14 16:05:48 -06:00

299 lines
9.0 KiB
JavaScript

/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var StringBuilder = require('raptor-strings/StringBuilder');
var logger = require('raptor-logging').logger(module);
var EventEmitter = require('events').EventEmitter;
var includeStack = false;
var voidWriter = {
write: function() {}
};
function getAsyncAttributes(context) {
var attrs = context.attributes;
return attrs.async;
}
function Fragment(context) {
this.context = context;
// The context that this async fragment is associated with
this.writer = context.writer;
// The original writer this fragment was associated with
this.finished = false;
// Used to keep track if this async fragment was ended
this.flushed = false;
// Set to true when the contents of this async fragment have been
// flushed to the original writer
this.next = null;
// A link to the next sibling async fragment (if any)
this.ready = true; // Will be set to true if this fragment is ready to be flushed
// (i.e. when there are no async fragments preceeding this fragment)
}
function flushNext(fragment, writer) {
var next = fragment.next;
if (next) {
next.ready = true;
// Since we have flushed the next fragment is ready
next.writer = next.context.writer = writer;
// Update the next fragment to use the original writer
next.flush(); // Now flush the next fragment (if it is not finish then it will just do nothing)
}
}
function BufferedFragment(context, buffer) {
Fragment.call(this, context);
this.buffer = buffer;
}
BufferedFragment.prototype = {
flush: function () {
var writer = this.writer;
writer.write(this.buffer.toString());
this.flushed = true;
flushNext(this, writer);
}
};
function AsyncFragment(context) {
Fragment.call(this, context);
}
AsyncFragment.prototype = {
end: function (e) {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
if (e) {
logger.error('Async fragment failed. Exception: ' + (e.stack || e) + (this.stack ? ('\nCreation stack trace: ' + this.stack) : ''));
this.context.emit('error', e);
}
if (this.finished) {
return;
}
// Make sure end is only called once by the user
this.finished = true;
var asyncAttributes = getAsyncAttributes(this.context);
if (this.ready) {
// There are no nested asynchronous fragments that are
// remaining and we are ready to be flushed then let's do it!
this.flush();
}
// Keep track of how many asynchronous fragments are in the template
// NOTE: firstPassComplete changes to true after processing all of the nodes of the template
if (--asyncAttributes.remaining === 0 && asyncAttributes.ended) {
// If we were the last fragment to complete then fulfil the promise for
// the template rendering using the output of the underlying writer
this.context.emit('end');
}
},
flush: function () {
if (!this.finished) {
// Skipped Flushing since not finished
return;
}
this.flushed = true;
var writer = this.writer;
this.writer = this.context.writer = voidWriter; // Prevent additional out-of-order writes
flushNext(this, writer);
},
addTimeoutHandler: function(timeoutMillis) {
var _this = this;
this.timeoutId = setTimeout(function () {
_this.end('Async fragment timed out after ' + timeoutMillis + 'ms.');
}, timeoutMillis);
}
};
function Context(writer, attributes) {
this.attributes = attributes || {
events: new EventEmitter()
};
if (!this.attributes.async) {
this.attributes.async = {
remaining: 0,
ended: false
};
}
if (writer) {
if (writer.pipe) {
this.on('end', function() {
writer.end();
});
}
} else {
writer = new StringBuilder();
}
this.writer = writer;
this.w = this.write;
}
Context.DEFAULT_TIMEOUT = 10000;
Context.prototype = {
constructor: Context,
getAttributes: function () {
return this.attributes;
},
getAttribute: function (name) {
return this.attributes[name];
},
write: function (str) {
if (str !== null && str !== undefined) {
if (typeof str !== 'string') {
str = str.toString();
}
this.writer.write(str);
}
return this;
},
getOutput: function () {
return this.writer.toString();
},
captureString: function (func, thisObj) {
var sb = new StringBuilder();
this.swapWriter(sb, func, thisObj);
return sb.toString();
},
swapWriter: function (newWriter, func, thisObj) {
var oldWriter = this.writer;
try {
this.writer = newWriter;
func.call(thisObj);
} finally {
this.writer = oldWriter;
}
},
createNestedContext: function (writer) {
return new Context(writer, this.attributes);
},
beginAsync: function (timeout) {
var asyncAttributes = getAsyncAttributes(this);
// Keep a count of all of the async fragments for this rendering
asyncAttributes.remaining++;
var ready = true;
// Create a new context that the async fragment can write to.
// The new async context will use the existing writer and
// the writer for the current context (which will continue to be used)
// will be replaced with a string buffer writer
var asyncContext = this.createNestedContext(this.writer);
var buffer = new StringBuilder();
this.writer = buffer;
var asyncFragment = new AsyncFragment(asyncContext);
var bufferedFragment = new BufferedFragment(this, buffer);
asyncFragment.next = bufferedFragment;
asyncContext.asyncFragment = asyncFragment;
asyncContext.parentAsyncFragment = asyncFragment;
var prevAsyncFragment = this.prevAsyncFragment || this.parentAsyncFragment;
// See if we are being buffered by a previous asynchronous
// fragment
if (prevAsyncFragment) {
// Splice in our two new fragments and add a link to the previous async fragment
// so that it can let us know when we are ready to be flushed
bufferedFragment.next = prevAsyncFragment.next;
prevAsyncFragment.next = asyncFragment;
if (!prevAsyncFragment.flushed) {
ready = false; // If we are preceeded by another async fragment then we aren't ready to be flushed
}
}
asyncFragment.ready = ready;
// Set the ready flag based on our earlier checks above
this.prevAsyncFragment = bufferedFragment;
// Record the previous async fragment for linking purposes
asyncFragment.stack = includeStack ? new Error().stack : null;
if (timeout == null) {
timeout = Context.DEFAULT_TIMEOUT;
}
if (timeout > 0) {
asyncFragment.addTimeoutHandler(timeout);
}
return asyncContext;
},
on: function(event, callback) {
var attributes = this.attributes;
if (event === 'end') {
if (attributes.ended) {
callback();
return this;
}
}
var events = attributes.events;
events.on.apply(events, arguments);
return this;
},
emit: function(event) {
var attributes = this.attributes;
if (event === 'end') {
attributes.ended = true;
}
var events = attributes.events;
events.emit.apply(events, arguments);
return this;
},
error: function(e) {
if (this.asyncFragment) {
this.asyncFragment.end(e);
} else {
this.emit('error', e);
}
},
end: function(data) {
if (data) {
this.write(data);
}
if (this.asyncFragment) {
this.asyncFragment.end();
} else {
var async = this.attributes.async;
async.ended = true;
if (async.remaining === 0) {
this.emit('end');
}
}
return this;
}
};
Context.enableAsyncStackTrace = function() {
includeStack = true;
};
module.exports = Context;