UNPKG

infusion

Version:

Infusion is an application framework for developing flexible stuff with JavaScript

345 lines (316 loc) 14.1 kB
/*! 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 */ "use strict"; // 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; };