marko/runtime/html/AsyncStream.js

505 lines
14 KiB
JavaScript

'use strict';
var EventEmitter = require('events').EventEmitter;
var StringWriter = require('./StringWriter');
var BufferedWriter = require('./BufferedWriter');
var extend = require('raptor-util/extend');
var documentProvider = require('../document-provider');
var helpers;
var voidWriter = { write:function(){} };
function State(root, stream, writer, events) {
this.root = root;
this.stream = stream;
this.writer = writer;
this.events = events;
this.remaining = 0;
this.lastCount = 0;
this.last = undefined; // Array
this.ended = false;
this.finished = false;
this.ids = 0;
}
function AsyncStream(global, writer, state, shouldBuffer) {
var finalGlobal = this.attributes = global || {};
var originalStream;
if (state) {
originalStream = state.stream;
} else {
var events = finalGlobal.events /* deprecated */ = writer && writer.on ? writer : new EventEmitter();
if (writer) {
originalStream = writer;
if (shouldBuffer) {
writer = new BufferedWriter(writer);
}
} else {
writer = originalStream = new StringWriter(events);
}
state = new State(this, originalStream, writer, events);
}
this.global = finalGlobal;
this.stream = originalStream;
this._state = state;
this.data = {};
this.writer = writer;
writer.stream = this;
this._sync = false;
this._stack = undefined;
this.name = undefined;
this._timeoutId = undefined;
this._node = undefined;
this._elStack = undefined; // Array
}
AsyncStream.DEFAULT_TIMEOUT = 10000;
AsyncStream.INCLUDE_STACK = typeof process !== 'undefined' && process.env.NODE_ENV === 'development';
AsyncStream.enableAsyncStackTrace = function() {
AsyncStream.INCLUDE_STACK = true;
};
var proto = AsyncStream.prototype = {
constructor: AsyncStream,
isAsyncWriter: AsyncStream, // Legacy
isAsyncStream: AsyncStream,
sync: function() {
this._sync = true;
},
isSync: function() {
return this._sync === true;
},
write: function(str) {
if (str != null) {
this.writer.write(str.toString());
}
return this;
},
getOutput: function() {
return this._state.writer.toString();
},
beginAsync: function(options) {
if (this._sync) {
throw new Error('beginAsync() not allowed when using renderSync()');
}
var state = this._state;
var currentWriter = this.writer;
/* ┏━━━━━┓ this
┃ WAS ┃ ↓↑
┗━━━━━┛ prevWriter → currentWriter → nextWriter */
var newWriter = new StringWriter();
var newStream = new AsyncStream(this.global, currentWriter, state);
this.writer = newWriter;
newWriter.stream = this;
newWriter.next = currentWriter.next;
currentWriter.next = newWriter;
/* ┏━━━━━┓ newStream this
┃ NOW ┃ ↓↑ ↓↑
┗━━━━━┛ prevWriter → currentWriter → newWriter → nextWriter */
var timeout;
var name;
state.remaining++;
if (options != null) {
if (typeof options === 'number') {
timeout = options;
} else {
timeout = options.timeout;
if (options.last === true) {
if (timeout == null) {
// Don't assign a timeout to last flush fragments
// unless it is explicitly given a timeout
timeout = 0;
}
state.lastCount++;
}
name = options.name;
}
}
if (timeout == null) {
timeout = AsyncStream.DEFAULT_TIMEOUT;
}
newStream.stack = AsyncStream.INCLUDE_STACK ? new Error().stack : null;
newStream.name = name;
if (timeout > 0) {
newStream._timeoutId = setTimeout(function() {
newStream.error(new Error('Async fragment ' + (name ? '(' + name + ') ': '') + 'timed out after ' + timeout + 'ms'));
}, timeout);
}
state.events.emit('beginAsync', {
writer: newStream, // Legacy
parentWriter: this, // Legacy
out: newStream,
parentOut: this
});
return newStream;
},
end: function(data) {
if (data) {
this.write(data);
}
var currentWriter = this.writer;
/* ┏━━━━━┓ this nextStream
┃ WAS ┃ ↓↑ ↓↑
┗━━━━━┛ currentWriter → nextWriter → futureWriter */
// Prevent any more writes to the current steam
this.writer = voidWriter;
currentWriter.stream = null;
// Flush the contents of nextWriter to the currentWriter
this.flushNext(currentWriter);
/* ┏━━━━━┓ this ╵ nextStream
┃ ┃ ↓ ╵ ↓↑
┃ NOW ┃ voidWriter ╵ currentWriter → futureWriter
┃ ┃ ──────────────┴────────────────────────────────
┗━━━━━┛ Flushed & garbage collected: nextWriter */
var state = this._state;
if (state.finished) {
return;
}
var remaining;
if (this === state.root) {
remaining = state.remaining;
state.ended = true;
} else {
var timeoutId = this._timeoutId;
if (timeoutId) {
clearTimeout(timeoutId);
}
remaining = --state.remaining;
}
if (state.ended) {
if (!state.lastFired && (state.remaining - state.lastCount === 0)) {
state.lastFired = true;
state.lastCount = 0;
state.events.emit('last');
}
if (remaining === 0) {
state.finished = true;
if (state.writer.end) {
state.writer.end();
} else {
state.events.emit('finish', this);
}
}
}
return this;
},
// flushNextOld: function(currentWriter) {
// if (currentWriter === this._state.writer) {
// var nextStream;
// var nextWriter = currentWriter.next;
//
// // flush until there is no nextWriter
// // or the nextWriter is still attached
// // to a branch.
// while(nextWriter) {
// currentWriter.write(nextWriter.toString());
// nextStream = nextWriter.stream;
//
// if(nextStream) break;
// else nextWriter = nextWriter.next;
// }
//
// // Orphan the nextWriter and everything that
// // came before it. They have been flushed.
// currentWriter.next = nextWriter && nextWriter.next;
//
// // If there is a nextStream,
// // set its writer to currentWriter
// // (which is the state.writer)
// if(nextStream) {
// nextStream.writer = currentWriter;
// currentWriter.stream = nextStream;
// }
// }
// },
flushNext: function(currentWriter) {
// It is possible that currentWriter is the
// last writer in the chain, so let's make
// sure there is a nextWriter to flush.
var nextWriter = currentWriter.next;
if (nextWriter) {
// Flush the contents of nextWriter
// to the currentWriter
currentWriter.write(nextWriter.toString());
// Remove nextWriter from the chain.
// It has been flushed and can now be
// garbage collected.
currentWriter.next = nextWriter.next;
// It's possible that nextWriter is the last
// writer in the chain and its stream already
// ended, so let's make sure nextStream exists.
var nextStream = nextWriter.stream;
if (nextStream) {
// Point the nextStream to currentWriter
nextStream.writer = currentWriter;
currentWriter.stream = nextStream;
}
}
},
on: function(event, callback) {
var state = this._state;
if (event === 'finish' && state.finished) {
callback(this);
return this;
}
state.events.on(event, callback);
return this;
},
once: function(event, callback) {
var state = this._state;
if (event === 'finish' && state.finished) {
callback(this);
return this;
}
state.events.once(event, callback);
return this;
},
onLast: function(callback) {
var state = this._state;
var lastArray = state.last;
if (!lastArray) {
lastArray = state.last = [];
var i = 0;
var next = function next() {
if (i === lastArray.length) {
return;
}
var _next = lastArray[i++];
_next(next);
};
this.once('last', function() {
next();
});
}
lastArray.push(callback);
return this;
},
emit: function(type, arg) {
var events = this._state.events;
switch(arguments.length) {
case 1:
events.emit(type);
break;
case 2:
events.emit(type, arg);
break;
default:
events.emit.apply(events, arguments);
break;
}
return this;
},
removeListener: function() {
var events = this._state.events;
events.removeListener.apply(events, arguments);
return this;
},
prependListener: function() {
var events = this._state.events;
events.prependListener.apply(events, arguments);
return this;
},
pipe: function(stream) {
this._state.stream.pipe(stream);
return this;
},
error: function(e) {
var stack = this._stack;
var name = this.name;
var message = 'Async fragment failed' + (name ? ' (' + name + ')': '') + '. Exception: ' + (e.stack || e) + (stack ? ('\nCreation stack trace: ' + stack) : '');
e = new Error(message);
try {
this.emit('error', e);
} finally {
// If there is no listener for the error event then it will
// throw a new here. In order to ensure that the async fragment
// is still properly ended we need to put the end() in a `finally`
// block
this.end();
}
if (console) {
console.error(message);
}
return this;
},
flush: function() {
var state = this._state;
if (!state.finished) {
var writer = state.writer;
if (writer && writer.flush) {
writer.flush();
}
}
return this;
},
createOut: function() {
return new AsyncStream(this.global);
},
beginElement: function(name, elementAttrs) {
var str = '<' + name;
helpers.as(elementAttrs);
str += '>';
this.write(str);
if (this._elStack) {
this._elStack.push(name);
} else {
this._elStack = [name];
}
},
endElement: function() {
var tagName = this._elStack.pop();
this.write('</' + tagName + '>');
},
text: function(str) {
this.write(helpers.x(str));
},
getNode: function(doc) {
var node = this._node;
var curEl;
var newBodyEl;
var html = this.getOutput();
if (!doc) {
doc = documentProvider.document;
}
if (!node) {
if (html) {
newBodyEl = doc.createElement('body');
newBodyEl.innerHTML = html;
if (newBodyEl.childNodes.length == 1) {
// If the rendered component resulted in a single node then just use that node
node = newBodyEl.childNodes[0];
} else {
// Otherwise, wrap the nodes in a document fragment node
node = doc.createDocumentFragment();
while ((curEl = newBodyEl.firstChild)) {
node.appendChild(curEl);
}
}
} else {
// empty HTML so use empty document fragment (so that we're returning a valid DOM node)
node = doc.createDocumentFragment();
}
this._node = node;
}
return node;
},
toString: function() {
return this.getOutput();
},
// END DOM METHODS
// Deprecated BEGIN:
getAttributes: function() {
return this.global;
},
getAttribute: function(name) {
return this.global[name];
},
captureString: function(func, thisObj) {
var sb = new StringWriter();
this.swapWriter(sb, func, thisObj);
return sb.toString();
},
swapWriter: function(newWriter, func, thisObj) {
var currentWriter = this.writer;
this.writer = newWriter;
func.call(thisObj);
this.writer = currentWriter;
}
// // Deprecated END
};
// alias:
proto.w = proto.write;
extend(proto, require('../OutMixins'));
module.exports = AsyncStream;
helpers = require('./helpers');