infusion
Version:
Infusion is an application framework for developing flexible stuff with JavaScript
345 lines (316 loc) • 14.1 kB
JavaScript
/*!
Copyright 2011 unscriptable.com / John Hann
Copyright The Infusion copyright holders
See the AUTHORS.md file at the top-level directory of this distribution and at
https://github.com/fluid-project/infusion/raw/main/AUTHORS.md.
License MIT
*/
;
// Light fluidification of minimal promises library. See original gist at
// https://gist.github.com/unscriptable/814052 for limitations and commentary
// This implementation provides what could be described as "flat promises" with
// no support for structured programming idioms involving promise composition.
// It provides what a proponent of mainstream promises would describe as
// a "glorified callback aggregator"
fluid.promise = function () {
var that = {
// TODO: We probably can and should replace these with actual events, especially once we optimise out
// "byId" and perhaps also experiment with whether Object.defineProperty creates less garbage than that-ism
onResolve: [],
onReject: [],
onCancel: []
// disposition: "resolve"/"reject"/"cancel"
// directHandle: "resolve"/"reject"/"cancel" - signalling to unhandled rejection handler
// value: Any
};
that.then = function (onResolve, onReject, onCancel) {
fluid.promise.pushHandler(that, onResolve, "onResolve", "resolve");
fluid.promise.pushHandler(that, onReject, "onReject", "reject");
fluid.promise.pushHandler(that, onCancel, "onCancel", "cancel");
return that;
};
that.resolve = function (value) {
if (that.disposition) {
if (that.disposition !== "cancel") {
fluid.fail("Error: resolving promise ", that,
" which has already received \"" + that.disposition + "\"");
}
} else {
that.complete("resolve", that.onResolve, value);
}
return that;
};
that.reject = function (reason) {
if (that.disposition) {
if (that.disposition !== "cancel") {
fluid.fail("Error: rejecting promise ", that,
"which has already received \"" + that.disposition + "\"");
}
} else {
if (that.onReject.length === 0) {
fluid.armUnhandledRejection(that, reason);
}
that.complete("reject", that.onReject, reason);
}
return that;
};
that.cancel = function (reason) {
if (!that.disposition) {
that.complete("cancel", that.onCancel, reason);
}
};
// PRIVATE, NON-API METHOD
that.complete = function (which, queue, arg) {
that.disposition = which;
that.value = arg;
for (var i = 0; i < queue.length; ++i) {
queue[i](arg);
}
delete that.onResolve;
delete that.onReject;
delete that.onCancel;
};
return that;
};
fluid.promise.pushHandler = function (promise, handler, eventName, disposition) {
if (handler) {
if (promise.disposition) {
if (promise.disposition === disposition) {
handler(promise.value);
promise.directHandle = disposition;
}
} else {
promise[eventName].push(handler);
}
}
};
fluid.armUnhandledRejection = function (promise, reason) {
fluid.invokeLater(function () {
if (promise.directHandle !== "reject") {
fluid.fireUnhandledRejection(promise, reason);
}
});
};
fluid.fireUnhandledRejection = function (promise, reason) {
fluid.unhandledRejectionEvent.fire(reason, promise);
};
// A global event which will fire, and by default, foreward to fluid.fail in the event a promise rejection is not
// handled by the next tick. See FLUID-6453 for discussion on semantics and desirability
fluid.unhandledRejectionEvent = fluid.makeEventFirer({
name: "Global unhandled rejection handler"
});
fluid.logUnhandledRejection = function (reason) {
fluid.log(fluid.logLevel.WARN, "Unhandled promise rejection: ", reason);
};
fluid.unhandledRejectionEvent.addListener(fluid.logUnhandledRejection, "log");
/* Any object with a member <code>then</code> of type <code>function</code> passes this test.
* This includes essentially every known variety, including jQuery promises.
*/
fluid.isPromise = function (totest) {
return totest && typeof(totest.then) === "function";
};
/** Coerces any value to a promise
* @param {Any} promiseOrValue - The value to be coerced
* @return {Promise} - If the supplied value is already a promise, it is returned unchanged.
* Otherwise a fresh promise is created with the value as resolution and returned
*/
fluid.toPromise = function (promiseOrValue) {
if (fluid.isPromise(promiseOrValue)) {
return promiseOrValue;
} else {
var togo = fluid.promise();
togo.resolve(promiseOrValue);
return togo;
}
};
/** Chains the resolution methods of one promise (target) so that they follow those of another (source).
* That is, whenever source resolves, target will resolve, or when source rejects, target will reject, with the
* same payloads in each case. In addition, if the target promise is cancelled, this will be propagated to the
* source promise.
* @param {Promise} source - The promise that the target promise will subscribe to
* @param {Promise} target - The promise to which notifications will be forwarded from the source
*/
fluid.promise.follow = function (source, target) {
source.then(target.resolve, target.reject);
target.then(null, null, source.cancel);
};
/** Returns a promise whose resolved value is mapped from the source promise or value by the supplied function.
* @param {Object|Promise} source - An object or promise whose value is to be mapped
* @param {Function} func - A function which will map the resolved promise value
* @return {Promise} - A promise for the resolved mapped value.
*/
fluid.promise.map = function (source, func) {
var promise = fluid.toPromise(source);
var togo = fluid.promise();
promise.then(function (value) {
var mapped = func(value);
if (fluid.isPromise(mapped)) {
fluid.promise.follow(mapped, togo);
} else {
togo.resolve(mapped);
}
}, function (error) {
togo.reject(error);
});
return togo;
};
/** Construct a `sequencer` which is a general skeleton structure for all sequential promise algorithms,
* e.g. transform, reduce, sequence, etc.
* These accept a variable "strategy" pair to customise the interchange of values and final return
* @param {Array} sources - Array of values, promises, or tasks
* @param {Object} options - Algorithm-static options structure to be supplied to any task function discovered within
* `sources`
* @param {fluid.promise.strategy} strategy - A pair of function members `invokeNext` and `resolveResult` which determine the particular
* sequential promise algorithm to be operated.
* @return {fluid.promise.sequencer} A `sequencer` structure which will operate the algorithm and holds its state.
*/
fluid.promise.makeSequencer = function (sources, options, strategy) {
if (!fluid.isArrayable(sources)) {
fluid.fail("fluid.promise sequence algorithms must be supplied an array as source");
}
var sequencer = {
sources: sources,
resolvedSources: [], // the values of "sources" only with functions invoked (an array of promises or values)
index: 0,
strategy: strategy,
options: options, // available to be supplied to each listener
returns: [],
sequenceStarted: false,
sequenceCancelled: false,
promise: fluid.promise() // the final return value
};
sequencer.promise.then(null, null, function () {
fluid.promise.cancelSequencer(sequencer);
});
sequencer.promise.sequencer = sequencer; // An aid to debuggability
return sequencer;
};
fluid.promise.cancelSequencer = function (sequencer) {
sequencer.sequenceCancelled = true;
sequencer.resolvedSources.forEach(function (source) {
if (fluid.isPromise(source)) {
source.cancel();
}
});
};
fluid.promise.progressSequence = function (that, retValue) {
that.returns.push(retValue);
that.index++;
// No we dun't have no tail recursion elimination
fluid.promise.resumeSequence(that);
};
fluid.promise.processSequenceReject = function (that, error) { // Allow earlier promises in the sequence to wrap the rejection supplied by later ones (FLUID-5584)
for (var i = that.index - 1; i >= 0; --i) {
var resolved = that.resolvedSources[i];
var accumulator = fluid.isPromise(resolved) && typeof(resolved.accumulateRejectionReason) === "function" ? resolved.accumulateRejectionReason : fluid.identity;
error = accumulator(error);
}
that.promise.reject(error);
};
fluid.promise.resumeSequence = function (that) {
that.sequenceStarted = true;
if (that.sequenceCancelled) {
return;
} else if (that.index === that.sources.length) {
that.promise.resolve(that.strategy.resolveResult(that));
} else {
var value = that.strategy.invokeNext(that);
that.resolvedSources[that.index] = value;
if (fluid.isPromise(value)) {
value.then(function (retValue) {
fluid.promise.progressSequence(that, retValue);
}, function (error) {
fluid.promise.processSequenceReject(that, error);
});
} else {
fluid.promise.progressSequence(that, value);
}
}
};
// SEQUENCE ALGORITHM APPLYING PROMISES
fluid.promise.makeSequenceStrategy = function () {
return {
invokeNext: function (that) {
var source = that.sources[that.index];
return typeof(source) === "function" ? source(that.options) : source;
},
resolveResult: function (that) {
return that.returns;
}
};
};
// accepts an array of values, promises or functions returning promises - in the case of functions returning promises,
// will assure that at most one of these is "in flight" at a time - that is, the succeeding function will not be invoked
// until the promise at the preceding position has resolved
fluid.promise.sequence = function (sources, options) {
var sequencer = fluid.promise.makeSequencer(sources, options, fluid.promise.makeSequenceStrategy());
fluid.promise.resumeSequence(sequencer);
return sequencer.promise;
};
// TRANSFORM ALGORITHM APPLYING PROMISES
fluid.promise.makeTransformerStrategy = function () {
return {
invokeNext: function (that) {
var lisrec = that.sources[that.index];
lisrec.listener = fluid.event.resolveListener(lisrec.listener);
var value = lisrec.listener.apply(null, [that.returns[that.index], that.options]);
return value;
},
resolveResult: function (that) {
return that.returns[that.index];
}
};
};
// Construct a "mini-object" managing the process of a sequence of transforms,
// each of which may be synchronous or return a promise
fluid.promise.makeTransformer = function (listeners, payload, options) {
listeners.unshift({listener:
function () {
return payload;
}
});
var sequencer = fluid.promise.makeSequencer(listeners, options, fluid.promise.makeTransformerStrategy());
sequencer.returns.push(null); // first dummy return from initial entry
return sequencer;
};
fluid.promise.filterNamespaces = function (listeners, namespaces) {
if (!namespaces) {
return listeners;
}
return fluid.remove_if(fluid.makeArray(listeners), function (element) {
return element.namespace && !element.softNamespace && !namespaces.includes(element.namespace);
});
};
/**
* Top-level API to operate a Fluid event which manages a sequence of
* chained transforms. Rather than being a standard listener accepting the
* same payload, each listener to the event accepts the payload returned by the
* previous listener, and returns either a transformed payload or else a promise
* yielding such a payload.
*
* @param {fluid.eventFirer} event - A Fluid event to which the listeners are to be interpreted as
* elements cooperating in a chained transform. Each listener will receive arguments <code>(payload, options)</code> where <code>payload</code>
* is the (successful, resolved) return value of the previous listener, and <code>options</code> is the final argument to this function
* @param {Object|Promise} payload - The initial payload input to the transform chain
* @param {Object} options - A free object containing options governing the transform. Fields interpreted at this top level are:
* reverse {Boolean}: <code>true</code> if the listeners are to be called in reverse order of priority (typically the case for an inverse transform)
* filterTransforms {Array}: An array of listener namespaces. If this field is set, only the transform elements whose listener namespaces listed in this array will be applied.
* @return {fluid.promise} A promise which will yield either the final transformed value, or the response of the first transform which fails.
*/
fluid.promise.fireTransformEvent = function (event, payload, options) {
options = options || {};
var listeners = options.reverse ? fluid.makeArray(event.sortedListeners).reverse() :
fluid.makeArray(event.sortedListeners);
listeners = fluid.promise.filterNamespaces(listeners, options.filterNamespaces);
var sequencer = fluid.promise.makeTransformer(listeners, payload, options);
var canceller = sequencer.promise.cancel;
var remover = function () {
fluid.remove_if(event.onDestroy, function (func) {
return func === canceller;
});
};
fluid.event.addPrimitiveListener(event, "onDestroy", canceller);
sequencer.promise.then(remover, remover, remover);
fluid.promise.resumeSequence(sequencer);
return sequencer.promise;
};