discord-bot-cdk-construct
Version:
A quick CDK Construct for creating a serverless Discord bot in AWS!
1,948 lines (1,662 loc) • 2.13 MB
JavaScript
require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
'use strict';
var collection = require('./collection');
//
// Pointless function that will replace callbacks once they are executed to
// prevent double execution from ever happening.
//
function noop() { /* you waste your time by reading this, see, I told you.. */ }
/**
* Asynchronously iterate over the given data.
*
* @param {Mixed} data The data we need to iterate over
* @param {Function} iterator Function that's called for each item.
* @param {Function} fn The completion callback
* @param {Object} options Async options.
* @api public
*/
exports.each = function each(data, iterator, fn, options) {
options = options || {};
var size = collection.size(data)
, completed = 0
, timeout;
if (!size) return fn();
collection.each(data, function iterating(item) {
iterator.call(options.context || iterator, item, function done(err) {
if (err) {
fn(err);
return fn = noop;
}
if (++completed === size) {
fn();
if (timeout) clearTimeout(timeout);
return fn = noop;
}
});
});
//
// Optional timeout for when the operation takes to long.
//
if (options.timeout) timeout = setTimeout(function kill() {
fn(new Error('Operation timed out'));
fn = noop;
}, options.timeout);
};
},{"./collection":2}],2:[function(require,module,exports){
'use strict';
var hasOwn = Object.prototype.hasOwnProperty
, undef;
/**
* Get an accurate type check for the given Object.
*
* @param {Mixed} obj The object that needs to be detected.
* @returns {String} The object type.
* @api public
*/
function type(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
/**
* Iterate over a collection.
*
* @param {Mixed} collection The object we want to iterate over.
* @param {Function} iterator The function that's called for each iteration.
* @param {Mixed} context The context of the function.
* @api public
*/
function each(collection, iterator, context) {
var i = 0;
if ('array' === type(collection)) {
for (; i < collection.length; i++) {
if (false === iterator.call(context || iterator, collection[i], i, collection)) {
return; // If false is returned by the callback we need to bail out.
}
}
} else {
for (i in collection) {
if (hasOwn.call(collection, i)) {
if (false === iterator.call(context || iterator, collection[i], i, collection)) {
return; // If false is returned by the callback we need to bail out.
}
}
}
}
}
/**
* Checks if the given object is empty. The only edge case here would be
* objects. Most object's have a `length` attribute that indicate if there's
* anything inside the object.
*
* @param {Mixed} collection The collection that needs to be checked.
* @returns {Boolean}
* @api public
*/
function empty(obj) {
if (undef === obj) return false;
return size(obj) === 0;
}
/**
* Determine the size of a collection.
*
* @param {Mixed} collection The object we want to know the size of.
* @returns {Number} The size of the collection.
* @api public
*/
function size(collection) {
var x, i = 0;
if ('object' === type(collection)) {
for (x in collection) i++;
return i;
}
return +collection.length;
}
/**
* Wrap the given object in an array if it's not an array already.
*
* @param {Mixed} obj The thing we might need to wrap.
* @returns {Array} We promise!
* @api public
*/
function array(obj) {
if ('array' === type(obj)) return obj;
if ('arguments' === type(obj)) return Array.prototype.slice.call(obj, 0);
return obj // Only transform objects in to an array when they exist.
? [obj]
: [];
}
/**
* Find the index of an item in the given array.
*
* @param {Array} arr The array we search in
* @param {Mixed} o The object/thing we search for.
* @returns {Number} Index of the thing.
* @api public
*/
function index(arr, o) {
if ('function' === typeof arr.indexOf) return arr.indexOf(o);
for (
var j = arr.length,
i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0;
i < j && arr[i] !== o;
i++
);
return j <= i ? -1 : i;
}
/**
* Merge all given objects in to one objects.
*
* @returns {Object}
* @api public
*/
function copy() {
var result = {}
, depth = 2
, seen = [];
(function worker() {
each(array(arguments), function each(obj) {
for (var prop in obj) {
if (hasOwn.call(obj, prop) && !~index(seen, obj[prop])) {
if (type(obj[prop]) !== 'object' || !depth) {
result[prop] = obj[prop];
seen.push(obj[prop]);
} else {
depth--;
worker(result[prop], obj[prop]);
}
}
}
});
}).apply(null, arguments);
return result;
}
//
// Expose the collection utilities.
//
exports.array = array;
exports.empty = empty;
exports.index = index;
exports.copy = copy;
exports.size = size;
exports.type = type;
exports.each = each;
},{}],3:[function(require,module,exports){
'use strict';
/**
* Representation of one single file that will be loaded.
*
* @constructor
* @param {String} url The file URL.
* @param {Function} fn Optional callback.
* @api private
*/
function File(url, fn) {
if (!(this instanceof File)) return new File(url, fn);
this.readyState = File.LOADING;
this.start = +new Date();
this.callbacks = [];
this.dependent = 0;
this.cleanup = [];
this.url = url;
if ('function' === typeof fn) {
this.add(fn);
}
}
//
// The different readyStates for our File class.
//
File.DEAD = -1;
File.LOADING = 0;
File.LOADED = 1;
/**
* Added cleanup hook.
*
* @param {Function} fn Clean up callback
* @api public
*/
File.prototype.unload = function unload(fn) {
this.cleanup.push(fn);
return this;
};
/**
* Add a new dependent.
*
* @param {Function} fn Completion callback.
* @returns {Boolean} Callback successfully added or queued.
* @api private
*/
File.prototype.add = function add(fn) {
if (File.LOADING === this.readyState) {
this.callbacks.push(fn);
} else if (File.LOADED === this.readyState) {
fn();
} else {
return false;
}
this.dependent++;
return true;
};
/**
* Remove a dependent. If all dependent's are removed we will automatically
* destroy the loaded file from the environment.
*
* @returns {
* @api private
*/
File.prototype.remove = function remove() {
if (0 === --this.dependent) {
this.destroy();
return true;
}
return false;
};
/**
* Execute the callbacks.
*
* @param {Error} err Optional error.
* @api public
*/
File.prototype.exec = function exec(err) {
this.readyState = File.LOADED;
if (!this.callbacks.length) return this;
for (var i = 0; i < this.callbacks.length; i++) {
this.callbacks[i].apply(this.callbacks[i], arguments);
}
this.callbacks.length = 0;
if (err) this.destroy();
return this;
};
/**
* Destroy the file.
*
* @api public
*/
File.prototype.destroy = function destroy() {
this.exec(new Error('Resource has been destroyed before it was loaded'));
if (this.cleanup.length) for (var i = 0; i < this.cleanup.length; i++) {
this.cleanup[i]();
}
this.readyState = File.DEAD;
this.cleanup.length = this.dependent = 0;
return this;
};
/**
* Asynchronously load JavaScript and Stylesheets.
*
* Options:
*
* - document: Document where elements should be created from.
* - prefix: Prefix for the id that we use to poll for stylesheet completion.
* - timeout: Load timeout.
* - onload: Stylesheet onload supported.
*
* @constructor
* @param {HTMLElement} root The root element we should append to.
* @param {Object} options Configuration.
* @api public
*/
function AsyncAsset(root, options) {
if (!(this instanceof AsyncAsset)) return new AsyncAsset(root, options);
options = options || {};
this.document = 'document' in options ? options.document : document;
this.prefix = 'prefix' in options ? options.prefix : 'pagelet_';
this.timeout = 'timeout' in options ? options.timeout : 30000;
this.onload = 'onload' in options ? options.onload : null;
this.root = root || this.document.head || this.document.body;
this.sheets = []; // List of active stylesheets.
this.files = {}; // List of loaded or loading files.
this.meta = {}; // List of meta elements for polling.
if (null === this.onload) {
this.feature();
}
}
/**
* Remove a asset.
*
* @param {String} url URL we need to load.
* @returns {AsyncAsset}
* @api public
*/
AsyncAsset.prototype.remove = function remove(url) {
var file = this.files[url];
if (!file) return this;
//
// If we are fully removed, just nuke the reference.
//
if (file.remove()) {
delete this.files[url];
}
return this;
};
/**
* Load a new asset.
*
* @param {String} url URL we need to load.
* @param {Function} fn Completion callback.
* @returns {AsyncAsset}
* @api public
*/
AsyncAsset.prototype.add = function add(url, fn) {
var type = this.type(url);
if (this.progress(url, fn)) return this;
if ('js' === type) return this.script(url, fn);
if ('css' === type) return this.style(url, fn);
throw new Error('Unsupported file type: '+ type);
};
/**
* Check if the given URL has already loaded or is currently in progress of
* being loaded.
*
* @param {String} url URL that needs to be loaded.
* @returns {Boolean} The loading is already in progress.
* @api private
*/
AsyncAsset.prototype.progress = function progress(url, fn) {
if (!(url in this.files)) return false;
return this.files[url].add(fn);
};
/**
* Trigger the callbacks for a given URL.
*
* @param {String} url URL that has been loaded.
* @param {Error} err Optional error argument when shit fails.
* @api private
*/
AsyncAsset.prototype.callback = function callback(url, err) {
var file = this.files[url]
, meta = this.meta[url];
if (!file) return;
file.exec(err);
if (err) delete this.files[url];
if (meta) {
meta.parentNode.removeChild(meta);
delete this.meta[url];
}
};
/**
* Determine the file type for a given URL.
*
* @param {String} url File URL.
* @returns {String} The extension of the URL.
* @api private
*/
AsyncAsset.prototype.type = function type(url) {
return url.split('.').pop().toLowerCase();
};
/**
* Load a new script with a source.
*
* @param {String} url The script file that needs to be loaded in to the page.
* @param {Function} fn The completion callback.
* @returns {AsyncAsset}
* @api private
*/
AsyncAsset.prototype.script = function scripts(url, fn) {
var script = this.document.createElement('script')
, file = this.files[url] = new File(url, fn)
, async = this;
//
// Add an unload handler which removes the DOM node from the root element.
//
file.unload(function unload() {
script.onerror = script.onload = script.onreadystatechange = null;
if (script.parentNode) script.parentNode.removeChild(script);
});
//
// Required for FireFox 3.6 / Opera async loading. Normally browsers would
// load the script async without this flag because we're using createElement
// but these browsers need explicit flags.
//
script.async = true;
//
// onerror is not triggered by all browsers, but should give us a clean
// indication of failures so it doesn't matter if you're browser supports it
// or not, we still want to listen for it.
//
script.onerror = function onerror() {
script.onerror = script.onload = script.onreadystatechange = null;
async.callback(url, new Error('Failed to load the script.'));
};
//
// All "latest" browser seem to support the onload event for detecting full
// script loading. Internet Explorer 11 no longer needs to use the
// onreadystatechange method for completion indication.
//
script.onload = function onload() {
script.onerror = script.onload = script.onreadystatechange = null;
async.callback(url);
};
//
// Fall-back for older IE versions, they do not support the onload event on the
// script tag and we need to check the script readyState to see if it's
// successfully loaded.
//
script.onreadystatechange = function onreadystatechange() {
if (this.readyState in { loaded: 1, complete: 1 }) {
script.onerror = script.onload = script.onreadystatechange = null;
async.callback(url);
}
};
//
// The src needs to be set after the element has been added to the document.
// If I remember correctly it had to do something with an IE8 bug.
//
this.root.appendChild(script);
script.src = url;
return this;
};
/**
* Load CSS files by using @import statements.
*
* @param {String} url URL to load.
* @param {Function} fn Completion callback.
* @returns {AsyncAsset}
* @api private
*/
AsyncAsset.prototype.style = function style(url, fn) {
if (!this.document.styleSheet) return this.link(url, fn);
var file = this.file[url] = new File(url, fn)
, sheet, i = 0;
//
// Internet Explorer can only have 31 style tags on a single page. One single
// style tag is also limited to 31 @import statements so this gives us room to
// have 961 style sheets totally. So we should queue style sheets. This
// limitation has been removed in Internet Explorer 10.
//
// @see http://john.albin.net/ie-css-limits/two-style-test.html
// @see http://support.microsoft.com/kb/262161
// @see http://blogs.msdn.com/b/ieinternals/archive/2011/05/14/internet-explorer-stylesheet-rule-selector-import-sheet-limit-maximum.aspx
//
for (; i < this.sheets.length; i++) {
if (this.sheets[i].imports.length < 31) {
sheet = this.sheets[i];
break;
}
}
//
// We didn't find suitable style Sheet to add another @import statement,
// create a new one so we can leverage that instead.
//
// @TODO we should probably check the amount of `document.styleSheets.length`
// to check if we're allowed to add more style sheets.
//
if (!sheet) {
sheet = this.document.createStyleSheet();
this.sheets.push(sheet);
}
//
// Remove the import from the stylesheet.
//
file.unload(function unload() {
sheet.removeImport(i);
});
sheet.addImport(url);
return this.setInterval(url);
};
/**
* Load CSS by adding link tags on to the page.
*
* @param {String} url URL to load.
* @param {Function} fn Completion callback.
* @returns {AsyncAsset}
* @api private
*/
AsyncAsset.prototype.link = function links(url, fn) {
var link = this.document.createElement('link')
, file = this.files[url] = new File(url, fn)
, async = this;
file.unload(function unload() {
link.onload = link.onerror = null;
link.parentNode.removeChild(link);
});
if (this.onload) {
link.onload = function onload() {
link.onload = link.onerror = null;
async.callback(url);
};
link.onerror = function onerror() {
link.onload = link.onerror = null;
async.callback(url, new Error('Failed to load the stylesheet'));
};
}
link.href = url;
link.type = 'text/css';
link.rel = 'stylesheet';
this.root.appendChild(link);
return this.setInterval(url);
};
/**
* Poll our stylesheets to see if the style's have been applied.
*
* @param {String} url URL to check
* @api private
*/
AsyncAsset.prototype.setInterval = function setIntervals(url) {
if (url in this.meta) return this;
//
// Create a meta tag which we can inject in to the page and give it the id of
// the prefixed CSS rule so we know when the style sheet is loaded based on the
// style of this meta element.
//
var meta = this.meta[url] = this.document.createElement('meta')
, async = this;
meta.id = [
this.prefix,
url.split('/').pop().split('.').shift()
].join('').toLowerCase();
this.root.appendChild(meta);
if (this.setInterval.timer) return this;
//
// Start the reaping process.
//
this.setInterval.timer = setInterval(function interval() {
var now = +new Date()
, url, file, style, meta
, compute = window.getComputedStyle;
for (url in async.meta) {
meta = async.meta[url];
if (!meta) continue;
file = async.files[url];
style = compute ? getComputedStyle(meta) : meta.currentStyle;
//
// We assume that CSS added an increased style to the given prefixed CSS
// tag.
//
if (file && style && parseInt(style.height, 10) > 1) {
file.exec();
}
if (
!file
|| file.readyState === File.DEAD
|| file.readyState === File.LOADED
|| (now - file.start > async.timeout)
) {
if (file) file.exec(new Error('Stylesheet loading has timed out'));
meta.parentNode.removeChild(meta);
delete async.meta[url];
}
}
//
// If we can iterate over the async.meta object there are still objects
// left that needs to be polled.
//
for (url in async.meta) return;
clearInterval(async.setInterval.timer);
delete async.setInterval.timer;
}, 20);
return this;
};
/**
* Prefetch resources without executing them. This ensures that the next lookup
* is primed in the cache when we need them. Of course this is only possible
* when the server sends the correct caching headers.
*
* @param {Array} urls The URLS that need to be cached.
* @returns {AsyncAsset}
* @api private
*/
AsyncAsset.prototype.prefetch = function prefetch(urls) {
//
// This check is here because I'm lazy, I don't want to add an `isArray` check
// to the code. So we're just going to flip the logic here. If it's an string
// transform it to an array.
//
if ('string' === typeof urls) urls = [urls];
var IE = navigator.userAgent.indexOf(' Trident/')
, img = /\.(jpg|jpeg|png|gif|webp)$/
, node;
for (var i = 0, l = urls.length; i < l; i++) {
if (IE || img.test(urls[i])) {
new Image().src = urls[i];
continue;
}
node = document.createElement('object');
node.height = node.width = 0;
//
// Position absolute is required because it can still add some minor spacing
// at the bottom of a page and that will break sticky footer
// implementations.
//
node.style.position = 'absolute';
document.body.appendChild(node);
}
return this;
};
/**
* Try to detect if this browser supports the onload events on the link tag.
* It's a known cross browser bug that can affect WebKit, FireFox and Opera.
* Internet Explorer is the only browser that supports the onload event
* consistency but it has other bigger issues that prevents us from using this
* method.
*
* @returns {AsyncAsset}
* @api private
*/
AsyncAsset.prototype.feature = function detect() {
if (this.feature.detecting) return this;
this.feature.detecting = true;
var link = document.createElement('link')
, async = this;
link.rel = 'stylesheet';
link.href = 'data:text/css;base64,';
link.onload = function loaded() {
link.parentNode.removeChild(link);
link.onload = false;
async.onload = true;
};
this.root.appendChild(link);
return this;
};
//
// Expose the file instance.
//
AsyncAsset.File = File;
//
// Expose the asset loader
//
module.exports = AsyncAsset;
},{}],4:[function(require,module,exports){
'use strict';
/**
* Create a function that will cleanup the instance.
*
* @param {Array|String} keys Properties on the instance that needs to be cleared.
* @param {Object} options Additional configuration.
* @returns {Function} Destroy function
* @api public
*/
module.exports = function demolish(keys, options) {
var split = /[, ]+/;
options = options || {};
keys = keys || [];
if ('string' === typeof keys) keys = keys.split(split);
/**
* Run addition cleanup hooks.
*
* @param {String} key Name of the clean up hook to run.
* @param {Mixed} selfie Reference to the instance we're cleaning up.
* @api private
*/
function run(key, selfie) {
if (!options[key]) return;
if ('string' === typeof options[key]) options[key] = options[key].split(split);
if ('function' === typeof options[key]) return options[key].call(selfie);
for (var i = 0, type, what; i < options[key].length; i++) {
what = options[key][i];
type = typeof what;
if ('function' === type) {
what.call(selfie);
} else if ('string' === type && 'function' === typeof selfie[what]) {
selfie[what]();
}
}
}
/**
* Destroy the instance completely and clean up all the existing references.
*
* @returns {Boolean}
* @api public
*/
return function destroy() {
var selfie = this
, i = 0
, prop;
if (selfie[keys[0]] === null) return false;
run('before', selfie);
for (; i < keys.length; i++) {
prop = keys[i];
if (selfie[prop]) {
if ('function' === typeof selfie[prop].destroy) selfie[prop].destroy();
selfie[prop] = null;
}
}
if (selfie.emit) selfie.emit('destroy');
run('after', selfie);
return true;
};
};
},{}],5:[function(require,module,exports){
'use strict';
/**
* Representation of a single EventEmitter function.
*
* @param {Function} fn Event handler to be called.
* @param {Mixed} context Context for function execution.
* @param {Boolean} once Only emit once
* @api private
*/
function EE(fn, context, once) {
this.fn = fn;
this.context = context;
this.once = once || false;
}
/**
* Minimal EventEmitter interface that is molded against the Node.js
* EventEmitter interface.
*
* @constructor
* @api public
*/
function EventEmitter() { /* Nothing to set */ }
/**
* Holds the assigned EventEmitters by name.
*
* @type {Object}
* @private
*/
EventEmitter.prototype._events = undefined;
/**
* Return a list of assigned event listeners.
*
* @param {String} event The events that should be listed.
* @returns {Array}
* @api public
*/
EventEmitter.prototype.listeners = function listeners(event) {
if (!this._events || !this._events[event]) return [];
if (this._events[event].fn) return [this._events[event].fn];
for (var i = 0, l = this._events[event].length, ee = new Array(l); i < l; i++) {
ee[i] = this._events[event][i].fn;
}
return ee;
};
/**
* Emit an event to all registered event listeners.
*
* @param {String} event The name of the event.
* @returns {Boolean} Indication if we've emitted an event.
* @api public
*/
EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
if (!this._events || !this._events[event]) return false;
var listeners = this._events[event]
, len = arguments.length
, args
, i;
if ('function' === typeof listeners.fn) {
if (listeners.once) this.removeListener(event, listeners.fn, true);
switch (len) {
case 1: return listeners.fn.call(listeners.context), true;
case 2: return listeners.fn.call(listeners.context, a1), true;
case 3: return listeners.fn.call(listeners.context, a1, a2), true;
case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true;
case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
}
for (i = 1, args = new Array(len -1); i < len; i++) {
args[i - 1] = arguments[i];
}
listeners.fn.apply(listeners.context, args);
} else {
var length = listeners.length
, j;
for (i = 0; i < length; i++) {
if (listeners[i].once) this.removeListener(event, listeners[i].fn, true);
switch (len) {
case 1: listeners[i].fn.call(listeners[i].context); break;
case 2: listeners[i].fn.call(listeners[i].context, a1); break;
case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break;
default:
if (!args) for (j = 1, args = new Array(len -1); j < len; j++) {
args[j - 1] = arguments[j];
}
listeners[i].fn.apply(listeners[i].context, args);
}
}
}
return true;
};
/**
* Register a new EventListener for the given event.
*
* @param {String} event Name of the event.
* @param {Functon} fn Callback function.
* @param {Mixed} context The context of the function.
* @api public
*/
EventEmitter.prototype.on = function on(event, fn, context) {
var listener = new EE(fn, context || this);
if (!this._events) this._events = {};
if (!this._events[event]) this._events[event] = listener;
else {
if (!this._events[event].fn) this._events[event].push(listener);
else this._events[event] = [
this._events[event], listener
];
}
return this;
};
/**
* Add an EventListener that's only called once.
*
* @param {String} event Name of the event.
* @param {Function} fn Callback function.
* @param {Mixed} context The context of the function.
* @api public
*/
EventEmitter.prototype.once = function once(event, fn, context) {
var listener = new EE(fn, context || this, true);
if (!this._events) this._events = {};
if (!this._events[event]) this._events[event] = listener;
else {
if (!this._events[event].fn) this._events[event].push(listener);
else this._events[event] = [
this._events[event], listener
];
}
return this;
};
/**
* Remove event listeners.
*
* @param {String} event The event we want to remove.
* @param {Function} fn The listener that we need to find.
* @param {Boolean} once Only remove once listeners.
* @api public
*/
EventEmitter.prototype.removeListener = function removeListener(event, fn, once) {
if (!this._events || !this._events[event]) return this;
var listeners = this._events[event]
, events = [];
if (fn) {
if (listeners.fn && (listeners.fn !== fn || (once && !listeners.once))) {
events.push(listeners);
}
if (!listeners.fn) for (var i = 0, length = listeners.length; i < length; i++) {
if (listeners[i].fn !== fn || (once && !listeners[i].once)) {
events.push(listeners[i]);
}
}
}
//
// Reset the array, or remove it completely if we have no more listeners.
//
if (events.length) {
this._events[event] = events.length === 1 ? events[0] : events;
} else {
delete this._events[event];
}
return this;
};
/**
* Remove all listeners or only the listeners for the specified event.
*
* @param {String} event The event want to remove all listeners for.
* @api public
*/
EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) {
if (!this._events) return this;
if (event) delete this._events[event];
else this._events = {};
return this;
};
//
// Alias methods names because people roll like that.
//
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
//
// This function doesn't apply anymore.
//
EventEmitter.prototype.setMaxListeners = function setMaxListeners() {
return this;
};
//
// Expose the module.
//
EventEmitter.EventEmitter = EventEmitter;
EventEmitter.EventEmitter2 = EventEmitter;
EventEmitter.EventEmitter3 = EventEmitter;
//
// Expose the module.
//
module.exports = EventEmitter;
},{}],6:[function(require,module,exports){
'use strict';
var Container = require('containerization')
, EventEmitter = require('eventemitter3')
, iframe = require('frames');
/**
* Fortress: Container and Image management for front-end code.
*
* @constructor
* @param {Object} options Fortress configuration
* @api private
*/
function Fortress(options) {
if (!(this instanceof Fortress)) return new Fortress(options);
options = options || {};
//
// Create a small dedicated container that houses all our iframes. This might
// add an extra DOM node to the page in addition to each iframe but it will
// ultimately result in a cleaner DOM as everything is nicely tucked away.
//
var scripts = document.getElementsByTagName('script')
, append = scripts[scripts.length - 1] || document.body
, div = document.createElement('div');
append.parentNode.insertBefore(div, append);
this.global = (function global() { return this; })() || window;
this.containers = {};
this.mount = div;
scripts = null;
EventEmitter.call(this);
}
//
// Fortress inherits from EventEmitter3.
//
Fortress.prototype = new EventEmitter();
Fortress.prototype.constructor = Fortress;
/**
* Detect the current globals that are loaded in to this page. This way we can
* see if we are leaking data.
*
* @param {Array} old Optional array with previous or known leaks.
* @returns {Array} Names of the leaked globals.
* @api private
*/
Fortress.prototype.globals = function globals(old) {
var i = iframe(this.mount, 'iframe_'+ (+new Date()))
, windoh = i.add().window()
, global = this.global
, result = [];
i.remove();
//
// Detect the globals and return them.
//
for (var key in global) {
var introduced = !(key in windoh);
//
// We've been given an array, so we should use that as the source of previous
// and acknowledged leaks and only return an array that contains newly
// introduced leaks.
//
if (introduced) {
if (old && old.length && !!~old.indexOf(key)) continue;
result.push(key);
}
}
return result;
};
/**
* List all active containers.
*
* @returns {Array} Active containers.
* @api public
*/
Fortress.prototype.all = function all() {
var everything = [];
for (var id in this.containers) {
everything.push(this.containers[id]);
}
return everything;
};
/**
* Generate an unique, unknown id that we can use for our container storage.
*
* @returns {String}
* @api private
*/
Fortress.prototype.id = function id() {
for (var i = 0, generated = []; i < 4; i++) {
generated.push(Math.random().toString(36).substring(2));
}
generated = 'fortress_'+ generated.join('_');
//
// Ensure that we didn't generate a pre-existing id, if we did, generate
// another id.
//
if (generated in this.containers) return this.id();
return generated;
};
/**
* Create a new container.
*
* @param {String} code
* @param {Object} options Options for the container
* @returns {Container}
* @api public
*/
Fortress.prototype.create = function create(code, options) {
var container = new Container(this.mount, this.id(), code, options);
this.containers[container.id] = container;
return container;
};
/**
* Get a container based on it's unique id.
*
* @param {String} id The container id.
* @returns {Container}
* @api public
*/
Fortress.prototype.get = function get(id) {
return this.containers[id];
};
/**
* Inspect a running Container in order to get more detailed information about
* the process and the state of the container.
*
* @param {String} id The container id.
* @api public
*/
Fortress.prototype.inspect = Fortress.prototype.top = function inspect(id) {
var container = this.get(id);
if (!container) return {};
return container.inspect();
};
/**
* Start the container with the given id.
*
* @param {String} id The container id.
* @api public
*/
Fortress.prototype.start = function start(id) {
var container = this.get(id);
if (!container) return this;
container.start();
return this;
};
/**
* Stop a running container, this does not fully destroy the container. It
* merely stops it from running. Stopping an container will cause the container
* to start from the beginning again once it's started. This is not a pause
* function.
*
* @param {String} id The container id.
* @api public
*/
Fortress.prototype.stop = function stop(id) {
var container = this.get(id);
if (!container) return this;
container.stop();
return this;
};
/**
* Restart a container. Basically, just a start and stop.
*
* @param {String} id The container id.
* @api public
*/
Fortress.prototype.restart = function restart(id) {
var container = this.get(id);
if (!container) return this;
container.stop().start();
return this;
};
/**
* Completely remove and shutdown the given container id.
*
* @param {String} id The container id.
* @api public
*/
Fortress.prototype.kill = function kill(id) {
var container = this.get(id);
if (!container) return this;
container.destroy();
delete this.containers[id];
return this;
};
/**
* Start streaming logging information and cached logs.
*
* @param {String} id The container id.
* @param {String} method The log method name.
* @param {Function} fn The function that needs to be called for each stream.
* @api public
*/
Fortress.prototype.attach = function attach(id, method, fn) {
var container = this.get(id);
if (!container) return this;
if ('function' === typeof method) {
fn = method;
method = 'attach';
} else {
method += 'attach::'+ method;
}
container.on(method, fn);
return this;
};
/**
* Stop streaming logging information and cached logs.
*
* @param {String} id The container id.
* @param {String} method The log method name.
* @param {Function} fn The function that needs to be called for each stream.
* @api public
*/
Fortress.prototype.detach = function detach(id, method, fn) {
var container = this.get(id);
if (!container) return this;
if ('function' === typeof method) {
fn = method;
method = 'attach';
} else {
method += 'attach::'+ method;
}
if (!fn) container.removeAllListeners(method);
else container.on(method, fn);
return this;
};
/**
* Destroy all active containers and clean up all references. We expect no more
* further calls to this Fortress instance.
*
* @api public
*/
Fortress.prototype.destroy = function destroy() {
for (var id in this.containers) {
this.kill(id);
}
this.mount.parentNode.removeChild(this.mount);
this.global = this.mount = this.containers = null;
};
/**
* Prepare a file or function to be loaded in to a Fortress based Container.
* When the transfer boolean is set we assume that you want to load pass the
* result of to a function or assign it a variable from the server to the client
* side:
*
* ```
* <script>
* var code = <%- Fortress.stringify(code, true) %>
* </script>
* ```
*
* @param {String|Function} code The code that needs to be transformed.
* @param {Boolean} transfer Prepare the code for transfer.
* @returns {String}
* @api public
*/
Fortress.stringify = function stringify(code, transfer) {
if ('function' === typeof code) {
//
// We've been given a pure function, so we need to wrap it a little bit
// after we've done a `toString` for the source retrieval so the function
// will automatically execute when it's activated.
//
code = '('+ code.toString() +'())';
} else {
//
// We've been given a string, so we're going to assume that it's path to file
// that should be included instead.
//
code = require('fs').readFileSync(code, 'utf-8');
}
return transfer ? JSON.stringify(code) : code;
};
//
// Expose the module.
//
module.exports = Fortress;
},{"containerization":7,"eventemitter3":9,"frames":10,"fs":15}],7:[function(require,module,exports){
'use strict';
var EventEmitter = require('eventemitter3')
, BaseImage = require('alcatraz')
, slice = Array.prototype.slice
, iframe = require('frames');
/**
* Representation of a single container.
*
* Options:
*
* - retries; When an error occurs, how many times should we attempt to restart
* the code before we automatically stop() the container.
* - stop; Stop the container when an error occurs.
* - timeout; How long can a ping packet timeout before we assume that the
* container has died and should be restarted.
*
* @constructor
* @param {Element} mount The element we should attach to.
* @param {String} id A unique id for this container.
* @param {String} code The actual that needs to run within the sandbox.
* @param {Object} options Container configuration.
* @api private
*/
function Container(mount, id, code, options) {
if (!(this instanceof Container)) return new Container(mount, id, code, options);
if ('object' === typeof code) {
options = code;
code = null;
}
options = options || {};
this.i = iframe(mount, id); // The generated iframe.
this.mount = mount; // Mount point of the container.
this.console = []; // Historic console.* output.
this.setTimeout = {}; // Stores our setTimeout references.
this.id = id; // Unique id.
this.readyState = Container.CLOSED; // The readyState of the container.
this.created = +new Date(); // Creation EPOCH.
this.started = null; // Start EPOCH.
this.retries = 'retries' in options // How many times should we reload
? +options.retries || 3
: 3;
this.timeout = 'timeout' in options // Ping timeout before we reboot.
? +options.timeout || 1050
: 1050;
//
// Initialise as an EventEmitter before we start loading in the code.
//
EventEmitter.call(this);
//
// Optional code to load in the container and start it directly.
//
if (code) this.load(code).start();
}
//
// The container inherits from the EventEmitter3.
//
Container.prototype = new EventEmitter();
Container.prototype.constructor = Container;
/**
* Internal readyStates for the container.
*
* @type {Number}
* @private
*/
Container.CLOSING = 1;
Container.OPENING = 2;
Container.CLOSED = 3;
Container.OPEN = 4;
/**
* Start a new ping timeout.
*
* @api private
*/
Container.prototype.ping = function ping() {
if (this.setTimeout.pong) clearTimeout(this.setTimeout.pong);
var self = this;
this.setTimeout.pong = setTimeout(function pong() {
self.onmessage({
type: 'error',
scope: 'iframe.timeout',
args: [
'the iframe is no longer responding with ping packets'
]
});
}, this.timeout);
return this;
};
/**
* Retry loading the code in the iframe. The container will be restored to a new
* state or completely reset the iframe.
*
* @api private
*/
Container.prototype.retry = function retry() {
switch (this.retries) {
//
// This is our last attempt, we've tried to have the iframe restart the code
// it self, so for our last attempt we're going to completely create a new
// iframe and re-compile the code for it.
//
case 1:
this.stop(); // Clear old iframe and nuke it's references
this.i = iframe(this.mount, this.id);
this.load(this.image.source).start();
break;
//
// No more attempts left.
//
case 0:
this.stop();
this.emit('end');
return;
//
// By starting and stopping (and there for removing and adding it back to
// the DOM) the iframe will reload it's HTML and the added code.
//
default:
this.stop().start();
break;
}
this.emit('retry', this.retries);
this.retries--;
return this;
};
/**
* Inspect the container to get some useful statistics about it and it's health.
*
* @returns {Object}
* @api public
*/
Container.prototype.inspect = function inspect() {
if (!this.i.attached()) return {};
var date = new Date()
, memory;
//
// Try to read out the `performance` information from the iframe.
//
if (this.i.window() && this.i.window().performance) {
memory = this.i.window().performance.memory;
}
memory = memory || {};
return {
readyState: this.readyState,
retries: this.retries,
uptime: this.started ? (+date) - this.started : 0,
date: date,
memory: {
limit: memory.jsHeapSizeLimit || 0,
total: memory.totalJSHeapSize || 0,
used: memory.usedJSHeapSize || 0
}
};
};
/**
* Parse and process incoming messages from the iframe. The incoming messages
* should be objects that have a `type` property. The main reason why we have
* this as a separate method is to give us flexibility. We are leveraging iframes
* at the moment, but in the future we might want to leverage WebWorkers for the
* sand boxing of JavaScript.
*
* @param {Object} packet The incoming message.
* @returns {Boolean} Message was handled y/n.
* @api private
*/
Container.prototype.onmessage = function onmessage(packet) {
if ('object' !== typeof packet) return false;
if (!('type' in packet)) return false;
packet.args = packet.args || [];
switch (packet.type) {
//
// The code in the iframe used the `console` method.
//
case 'console':
this.console.push({
scope: packet.scope,
epoch: +new Date(),
args: packet.args
});
if (packet.attach) {
this.emit.apply(this, ['attach::'+ packet.scope].concat(packet.args));
this.emit.apply(this, ['attach', packet.scope].concat(packet.args));
}
break;
//
// An error happened in the iframe, process it.
//
case 'error':
var failure = packet.args[0].stack ? packet.args[0] : new Error(packet.args[0]);
failure.scope = packet.scope || 'generic';
this.emit('error', failure);
this.retry();
break;
//
// The iframe and it's code has been loaded.
//
case 'load':
if (this.readyState !== Container.OPEN) {
this.readyState = Container.OPEN;
this.emit('start');
}
break;
//
// The iframe is unloading, attaching
//
case 'unload':
if (this.readyState !== Container.CLOSED) {
this.readyState = Container.CLOSED;
this.emit('stop');
}
break;
//
// We've received a ping response from the iframe, so we know it's still
// running as intended.
//
case 'ping':
this.ping();
this.emit('ping');
break;
//
// Handle unknown package types by just returning false after we've emitted
// it as an `regular` message.
//
default:
this.emit.apply(this, ['message'].concat(packet.args));
return false;
}
return true;
};
/**
* Small wrapper around sandbox evaluation.
*
* @param {String} cmd The command to executed in the iframe.
* @param {Function} fn Callback
* @api public
*/
Container.prototype.eval = function evil(cmd, fn) {
var data;
try {
data = this.i.add().window().eval(cmd);
} catch (e) {
return fn(e);
}
return fn(undefined, data);
};
/**
* Start the container.
*
* @returns {Container}
* @api public
*/
Container.prototype.start = function start() {
this.readyState = Container.OPENING;
var self = this;
/**
* Simple argument proxy.
*
* @api private
*/
function onmessage() {
self.onmessage.apply(self, arguments);
}
//
// Code loading is an sync process, but this COULD cause huge stack traces
// and really odd feedback loops in the stack trace. So we deliberately want
// to destroy the stack trace here.
//
this.setTimeout.start = setTimeout(function async() {
var doc = self.i.document();
//
// No doc.open, the iframe has already been destroyed!
//
if (!doc.open || !self.i) return;
//
// We need to open and close the iframe in order for it to trigger an onload
// event. Certain scripts might require in order to execute properly.
//
doc.open();
doc.write([
'<!doctype html>',
'<html><head>',
//
// iFrames can generate pointless requests by searching for a favicon.
// This can add up to three extra requests for a simple iframe. To battle
// this, we need to supply an empty icon.
//
// @see http://stackoverflow.com/questions/1321878/how-to-prevent-favicon-ico-requests
//
'<link rel="icon" href="data:;base64,=">',
'</head><body>'
].join('\n'));
//
// Introduce our messaging variable, this needs to be done before we eval
// our code. If we set this value before the setTimeout, it doesn't work in
// Opera due to reasons.
//
self.i.window()[self.id] = onmessage;
self.eval(self.image.toString(), function evil(err) {
if (err) return self.onmessage({
type: 'error',
scope: 'iframe.eval',
args: [ err ]
});
});
//
// If executing the code results to an error we could actually be stopping
// and removing the iframe from the source before we're able to close it.
// This is because executing the code inside the iframe is actually an sync
// operation.
//
if (doc.close) doc.close();
}, 0);
//
// We can only write to the iframe if it's actually in the DOM. The `i.add()`
// method ensures that the iframe is added to the DOM.
//
this.i.add();
this.started = +new Date();
return this;
};
/**
* Stop running the code inside the container.
*
* @returns {Container}
* @api private
*/
Container.prototype.stop = function stop() {
if (this.readyState !== Container.CLOSED && this.readyState !== Container.CLOSING) {
this.readyState = Container.CLOSING;
}
this.i.remove();
//
// Opera doesn't support unload events. So adding an listener inside the
// iframe for `unload` doesn't work. This is the only way around it.
//
this.onmessage({ type: 'unload' });
//
// It's super important that this removed AFTER we've cleaned up all other
// references as we might need to communicate back to our container when we
// are unloading or when an `unload` event causes an error.
//
this.i.window()[this.id] = null;
//
// Clear the timeouts.
//
for (var timeout in this.setTimeout) {
clearTimeout(this.setTimeout[timeout]);
delete this.setTimeout[timeout];
}
return this;
};
/**
* Load the given code as image on to the container.
*
* @param {String} code The code that should run on the container.
* @returns {Container}
* @api public
*/
Container.prototype.load = function load(code) {
this.image = new BaseImage(this.id, code);
return this;
};
/**
* Completely destroy the given container and ensure that all references are
* nuked so we can clean up as much memory as possible.
*
* @returns {Container}
* @api private
*/
Container.prototype.destroy = function destroy() {
if (!this.i) return this;
this.stop();
//
// Remove all possible references to release as much memory as possible.
//
this.mount = this.image = this.id = this.i = this.created = null;
this.console.length = 0;
this.removeAllListeners();
return this;
};
//
// Expose the module.
//
module.exports = Container;
},{"alcatraz":8,"eventemitter3":9,"frames":10}],8:[function(require,module,exports){
'use strict';
/**
* Alcatraz is our source code sandboxing.
*
* @constructor
* @param {String} method The global/method name that processes messages.
* @param {String} source The actual code.
* @param {String} domain The domain name.
* @api private
*/
function Alcatraz(method, source, domain) {
if (!(this instanceof Alcatraz)) return new Alcatraz(method, source);
this.domain = domain || ('undefined' !== typeof document ? document.domain : '');
this.method = 'if ('+method+') '+ method;
this.source = source;
this.compiled = null;
}
/**
* Assume that the source of the Alcatraz is loaded using toString() so it will be
* automatically transformed when the Alcatraz instance is concatenated or added to
* the DOM.
*
* @returns {String}
* @api public
*/
Alcatraz.prototype.toString = function toString() {
if (this.compiled) return this.compiled;
return this.compiled = this.transform();
};
/**
* Apply source code transformations to the code so it can work inside an
* iframe.
*
* @TODO allow custom code transformations.
* @returns {String}
* @api private
*/
Alcatraz.prototype.transform = function transform() {
var code = ('('+ (function alcatraz(global) {
//
// When you toString a function which is created while in strict mode,
// firefox will add "use strict"; to the body of the function. Chrome leaves
// the source intact. Knowing this, we cannot blindly assume that we can
// inject code after the first opening bracked `{`.
//
this.alcatraz();
/**
* Simple helper function to do nothing.
*
* @type {Function}
* @api private
*/
function noop() { /* I do nothing useful */ }
/**
* AddListener polyfill
*
* @param {Mixed} thing What ever we want to listen on.
* @param {String} evt The event we're listening for.
* @param {Function} fn The function that gets executed.
* @api private
*/
function on(thing, evt, fn) {
if (thing.attachEvent) {
thing.attachEvent('on'+ evt, fn);
} else if (thing.addEventListener) {
thing.addEventListener(evt, fn, false);
} else {
thing['on'+ evt] = fn;
}
return { on: on };
}
//
// Force the same domain as our 'root' script.
//
try { if ('_alcatraz_domain_') document.domain = '_alcatraz_domain_'; }
catch (e) { /* FireFox 26 throws an Security error for this as we use eval */ }
//
// Prevent common iframe detection scripts that do frame busting.
//
try { global.top = global.self = global.parent = global; }
catch (e) { /* Damn, read-only */ }
//
// Add a error listener. Adding it on the iframe it self doesn't make it
// bubble up to the container. So in order to capture errors and notifying
// the container we need to add a `window.onerror` listener inside the
// iframe it self.
// @TODO add proper stack trace tool here?
//
global.onerror = function onerror(