twig
Version:
JS port of the Twig templating language.
378 lines (304 loc) • 10.7 kB
JavaScript
// ## twig.async.js
//
// This file handles asynchronous tasks within twig.
module.exports = function (Twig) {
'use strict';
const STATE_UNKNOWN = 0;
const STATE_RESOLVED = 1;
const STATE_REJECTED = 2;
Twig.ParseState.prototype.parseAsync = function (tokens, context) {
return this.parse(tokens, context, true);
};
Twig.expression.parseAsync = function (tokens, context, tokensAreParameters) {
const state = this;
return Twig.expression.parse.call(state, tokens, context, tokensAreParameters, true);
};
Twig.logic.parseAsync = function (token, context, chain) {
const state = this;
return Twig.logic.parse.call(state, token, context, chain, true);
};
Twig.Template.prototype.renderAsync = function (context, params) {
return this.render(context, params, true);
};
Twig.async = {};
/**
* Checks for `thenable` objects
*/
Twig.isPromise = function (obj) {
return obj && obj.then && (typeof obj.then === 'function');
};
/**
* Handling of code paths that might either return a promise
* or a value depending on whether async code is used.
*
* @see https://github.com/twigjs/twig.js/blob/master/ASYNC.md#detecting-asynchronous-behaviour
*/
function potentiallyAsyncSlow(that, allowAsync, action) {
let result = action.call(that);
let err = null;
let isAsync = true;
if (!Twig.isPromise(result)) {
return result;
}
result.then(res => {
result = res;
isAsync = false;
}).catch(error => {
err = error;
});
if (err !== null) {
throw err;
}
if (isAsync) {
throw new Twig.Error('You are using Twig.js in sync mode in combination with async extensions.');
}
return result;
}
Twig.async.potentiallyAsync = function (that, allowAsync, action) {
if (allowAsync) {
return Twig.Promise.resolve(action.call(that));
}
return potentiallyAsyncSlow(that, allowAsync, action);
};
function run(fn, resolve, reject) {
try {
fn(resolve, reject);
} catch (error) {
reject(error);
}
}
function pending(handlers, onResolved, onRejected) {
const h = [onResolved, onRejected, -2];
// The promise has yet to be rejected or resolved.
if (!handlers) {
handlers = h;
} else if (handlers[2] === -2) {
// Only allocate an array when there are multiple handlers
handlers = [handlers, h];
} else {
handlers.push(h);
}
return handlers;
}
/**
* Really small thenable to represent promises that resolve immediately.
*
*/
Twig.Thenable = function (then, value, state) {
this.then = then;
this._value = state ? value : null;
this._state = state || STATE_UNKNOWN;
};
Twig.Thenable.prototype.catch = function (onRejected) {
// THe promise will not throw, it has already resolved.
if (this._state === STATE_RESOLVED) {
return this;
}
return this.then(null, onRejected);
};
/**
* The `then` method attached to a Thenable when it has resolved.
*
*/
Twig.Thenable.resolvedThen = function (onResolved) {
try {
return Twig.Promise.resolve(onResolved(this._value));
} catch (error) {
return Twig.Promise.reject(error);
}
};
/**
* The `then` method attached to a Thenable when it has rejected.
*
*/
Twig.Thenable.rejectedThen = function (onResolved, onRejected) {
// Shortcut for rejected twig promises
if (!onRejected || typeof onRejected !== 'function') {
return this;
}
const value = this._value;
let result;
try {
result = onRejected(value);
} catch (error) {
result = Twig.Promise.reject(error);
}
return Twig.Promise.resolve(result);
};
/**
* An alternate implementation of a Promise that does not fully follow
* the spec, but instead works fully synchronous while still being
* thenable.
*
* These promises can be mixed with regular promises at which point
* the synchronous behaviour is lost.
*/
Twig.Promise = function (executor) {
let state = STATE_UNKNOWN;
let value = null;
let changeState = function (nextState, nextValue) {
state = nextState;
value = nextValue;
};
function onReady(v) {
changeState(STATE_RESOLVED, v);
}
function onReject(e) {
changeState(STATE_REJECTED, e);
}
run(executor, onReady, onReject);
// If the promise settles right after running the executor we can
// return a Promise with it's state already set.
//
// Twig.Promise.resolve and Twig.Promise.reject both use the more
// efficient `Twig.Thenable` for this purpose.
if (state === STATE_RESOLVED) {
return Twig.Promise.resolve(value);
}
if (state === STATE_REJECTED) {
return Twig.Promise.reject(value);
}
// If we managed to get here our promise is going to resolve asynchronous.
changeState = new Twig.FullPromise();
return changeState.promise;
};
/**
* Promise implementation that can handle being resolved at any later time.
*
*/
Twig.FullPromise = function () {
let handlers = null;
// The state has been changed to either resolve, or reject
// which means we should call the handler.
function resolved(onResolved) {
onResolved(p._value);
}
function rejected(onResolved, onRejected) {
onRejected(p._value);
}
let append = function (onResolved, onRejected) {
handlers = pending(handlers, onResolved, onRejected);
};
function changeState(newState, v) {
if (p._state) {
return;
}
p._value = v;
p._state = newState;
append = newState === STATE_RESOLVED ? resolved : rejected;
if (!handlers) {
return;
}
if (handlers[2] === -2) {
append(handlers[0], handlers[1]);
handlers = null;
return;
}
handlers.forEach(h => {
append(h[0], h[1]);
});
handlers = null;
}
const p = new Twig.Thenable((onResolved, onRejected) => {
const hasResolved = typeof onResolved === 'function';
// Shortcut for resolved twig promises
if (p._state === STATE_RESOLVED && !hasResolved) {
return Twig.Promise.resolve(p._value);
}
if (p._state === STATE_RESOLVED) {
try {
return Twig.Promise.resolve(onResolved(p._value));
} catch (error) {
return Twig.Promise.reject(error);
}
}
const hasRejected = typeof onRejected === 'function';
return new Twig.Promise((resolve, reject) => {
append(
hasResolved ? result => {
try {
resolve(onResolved(result));
} catch (error) {
reject(error);
}
} : resolve,
hasRejected ? err => {
try {
resolve(onRejected(err));
} catch (error) {
reject(error);
}
} : reject
);
});
});
changeState.promise = p;
return changeState;
};
Twig.Promise.defaultResolved = new Twig.Thenable(Twig.Thenable.resolvedThen, undefined, STATE_RESOLVED);
Twig.Promise.emptyStringResolved = new Twig.Thenable(Twig.Thenable.resolvedThen, '', STATE_RESOLVED);
Twig.Promise.resolve = function (value) {
if (arguments.length === 0 || typeof value === 'undefined') {
return Twig.Promise.defaultResolved;
}
if (Twig.isPromise(value)) {
return value;
}
// Twig often resolves with an empty string, we optimize for this
// scenario by returning a fixed promise. This reduces the load on
// garbage collection.
if (value === '') {
return Twig.Promise.emptyStringResolved;
}
return new Twig.Thenable(Twig.Thenable.resolvedThen, value, STATE_RESOLVED);
};
Twig.Promise.reject = function (e) {
// `e` should never be a promise.
return new Twig.Thenable(Twig.Thenable.rejectedThen, e, STATE_REJECTED);
};
Twig.Promise.all = function (promises) {
const results = new Array(promises.length);
return Twig.async.forEach(promises, (p, index) => {
if (!Twig.isPromise(p)) {
results[index] = p;
return;
}
if (p._state === STATE_RESOLVED) {
results[index] = p._value;
return;
}
return p.then(v => {
results[index] = v;
});
}).then(() => {
return results;
});
};
/**
* Go over each item in a fashion compatible with Twig.forEach,
* allow the function to return a promise or call the third argument
* to signal it is finished.
*
* Each item in the array will be called sequentially.
*/
Twig.async.forEach = function (arr, callback) {
const len = arr ? arr.length : 0;
let index = 0;
function next() {
let resp = null;
do {
if (index === len) {
return Twig.Promise.resolve();
}
resp = callback(arr[index], index);
index++;
// While the result of the callback is not a promise or it is
// a promise that has settled we can use a regular loop which
// is much faster.
} while (!resp || !Twig.isPromise(resp) || resp._state === STATE_RESOLVED);
return resp.then(next);
}
return next();
};
return Twig;
};