UNPKG

redux-pixies

Version:

The magical asynchronous Redux library

937 lines (775 loc) 23.5 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function makePixieShutdownError() { var e = new Error('Pixie has been destroyed'); e.name = 'PixieShutdownError'; return e; } function isPixieShutdownError(e) { return e instanceof Error && e.name === 'PixieShutdownError'; } /** * If a wild pixie returns a bare function, turn that into a proper object. */ function fixInstance(instance) { if (typeof instance === 'function') { return { update: instance, destroy: function destroy() {} }; } return instance; } /** * Catches synchronous errors and sends them through `onError`, * terminating the inner pixie in response. Also prevents `update` * from running in parallel if if returns a promise. */ function babysitPixie(wildPixie) { function outPixie(input) { var instance = void 0; var propsCache = void 0; var propsDirty = true; var updating = false; var destroyed = false; var nextPromise = void 0; var rejector = void 0; var resolver = void 0; function destroy() { if (instance) { try { if (rejector) { var _copy = rejector; nextPromise = undefined; rejector = undefined; resolver = undefined; _copy(makePixieShutdownError()); } var copy = instance; instance = undefined; copy.destroy(); } catch (e) { onError(e); } destroyed = true; } } // Ignore any callbacks once `destroy` has completed: function onError(e) { if (!destroyed) input.onError(e); destroy(); } function onOutput(data) { if (!destroyed) input.onOutput(data); } function onUpdateDone() { updating = false; tryUpdate(); } function tryUpdate() { // eslint-disable-next-line no-unmodified-loop-condition while (instance && propsDirty && !updating) { propsDirty = false; updating = true; try { var thenable = instance.update(propsCache); if (thenable && typeof thenable.then === 'function') { thenable.then(onUpdateDone, onError); } else { updating = false; } } catch (e) { onError(e); } } } function getNextPromise() { if (!nextPromise) { nextPromise = new Promise(function (resolve, reject) { resolver = resolve; rejector = reject; }); } return nextPromise; } var childInput = { onError: onError, onOutput: onOutput, get props() { return propsCache; }, nextProps: getNextPromise, waitFor: function waitFor(condition) { return new Promise(function (resolve, reject) { function checkProps() { try { var result = condition(propsCache); if (result != null) resolve(result);else getNextPromise().then(checkProps, reject); } catch (e) { reject(e); } } return checkProps(); }); } }; try { instance = fixInstance(wildPixie(childInput)); } catch (e) { onError(e); } return { update: function update(props) { propsCache = props; propsDirty = true; // Update the `nextProps` promise right away: if (resolver) { var copy = resolver; nextPromise = undefined; rejector = undefined; resolver = undefined; copy(props); } tryUpdate(); }, destroy: destroy }; } outPixie.tame = true; outPixie.defaultOutput = wildPixie.defaultOutput; return outPixie; } /** * Accepts a hand-written reducer, and hardens it with error checking. */ function tamePixie(pixie) { return pixie.tame ? pixie : babysitPixie(pixie); } function defaultErrorHandler(e, props, onError) { onError(e); } /** * Intercepts `onError`, shutting down the inner pixie. */ function catchPixieError(pixie) { var errorHandler = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultErrorHandler; var tamedPixie = tamePixie(pixie); function outPixie(input) { var onOutput = input.onOutput; var instance = void 0; var propsCache = void 0; var destroy = function destroy() { var copy = instance; instance = undefined; if (copy) copy.destroy(); }; function onError(e) { destroy(); try { errorHandler(e, propsCache, input.onError); } catch (e) { input.onError(e); } } var childInput = { onError: onError, onOutput: onOutput }; return { update: function update(props) { propsCache = props; if (!instance) instance = tamedPixie(childInput); instance.update(props); }, destroy: destroy }; } outPixie.tame = true; outPixie.defaultOutput = pixie.defaultOutput; return outPixie; } var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var asyncGenerator = function () { function AwaitValue(value) { this.value = value; } function AsyncGenerator(gen) { var front, back; function send(key, arg) { return new Promise(function (resolve, reject) { var request = { key: key, arg: arg, resolve: resolve, reject: reject, next: null }; if (back) { back = back.next = request; } else { front = back = request; resume(key, arg); } }); } function resume(key, arg) { try { var result = gen[key](arg); var value = result.value; if (value instanceof AwaitValue) { Promise.resolve(value.value).then(function (arg) { resume("next", arg); }, function (arg) { resume("throw", arg); }); } else { settle(result.done ? "return" : "normal", result.value); } } catch (err) { settle("throw", err); } } function settle(type, value) { switch (type) { case "return": front.resolve({ value: value, done: true }); break; case "throw": front.reject(value); break; default: front.resolve({ value: value, done: false }); break; } front = front.next; if (front) { resume(front.key, front.arg); } else { back = null; } } this._invoke = send; if (typeof gen.return !== "function") { this.return = undefined; } } if (typeof Symbol === "function" && Symbol.asyncIterator) { AsyncGenerator.prototype[Symbol.asyncIterator] = function () { return this; }; } AsyncGenerator.prototype.next = function (arg) { return this._invoke("next", arg); }; AsyncGenerator.prototype.throw = function (arg) { return this._invoke("throw", arg); }; AsyncGenerator.prototype.return = function (arg) { return this._invoke("return", arg); }; return { wrap: function (fn) { return function () { return new AsyncGenerator(fn.apply(this, arguments)); }; }, await: function (value) { return new AwaitValue(value); } }; }(); var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /** * Copies the `output` back into the pixie as a prop. */ function reflectPixieOutput(pixie) { var tamedPixie = tamePixie(pixie); function outPixie(input) { var onError = input.onError; var instance = void 0; var output = pixie.defaultOutput; var propsCache = void 0; var propsDirty = true; var updating = false; var tryUpdate = function tryUpdate() { // The `update` function can call `onOutput` or `onError`, so loop: // eslint-disable-next-line no-unmodified-loop-condition while (instance && propsDirty && !updating) { propsDirty = false; updating = true; instance.update(_extends({}, propsCache, { output: output })); updating = false; } }; var onOutput = function onOutput(data) { if (data !== output) { output = data; propsDirty = true; input.onOutput(data); tryUpdate(); } }; var childInput = { onError: onError, onOutput: onOutput }; return { update: function update(props) { propsCache = props; propsDirty = true; if (!instance) instance = tamedPixie(childInput); tryUpdate(); }, destroy: function destroy() { var copy = instance; instance = undefined; if (copy) copy.destroy(); } }; } outPixie.tame = true; outPixie.defaultOutput = pixie.defaultOutput; return outPixie; } function defaultOnError(e) {} function defaultOnOutput(data) {} /** * Instantiates a pixie object. */ function startPixie(pixie) { var onError = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultOnError; var onOutput = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : defaultOnOutput; return catchPixieError(reflectPixieOutput(pixie))({ onError: onError, onOutput: onOutput }); } /** * Instantiates a pixie object and attaches it to a redux store. */ function attachPixie(store, pixie, onError, onOutput) { var instance = startPixie(pixie, onError, onOutput); instance.update({ dispatch: store.dispatch, output: undefined, state: store.getState() }); var unsubscribe = store.subscribe(function () { instance.update({ dispatch: store.dispatch, output: undefined, state: store.getState() }); }); return function () { unsubscribe(); instance.destroy(); }; } /** * Combines one or more pixies into one. */ function combinePixies(pixieMap) { var defaultOutput = {}; for (var _iterator = Object.keys(pixieMap), _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { var _ref; if (_isArray) { if (_i >= _iterator.length) break; _ref = _iterator[_i++]; } else { _i = _iterator.next(); if (_i.done) break; _ref = _i.value; } var _id3 = _ref; defaultOutput[_id3] = pixieMap[_id3].defaultOutput; } function outPixie(input) { var onError = input.onError; var childInputs = {}; var instances = {}; var outputs = _extends({}, defaultOutput); var destroyed = false; var _loop = function _loop() { if (_isArray2) { if (_i2 >= _iterator2.length) return 'break'; _ref2 = _iterator2[_i2++]; } else { _i2 = _iterator2.next(); if (_i2.done) return 'break'; _ref2 = _i2.value; } var id = _ref2; var onOutput = function onOutput(data) { if (data !== outputs[id]) { outputs = _extends({}, outputs); outputs[id] = data; input.onOutput(outputs); } }; childInputs[id] = { onError: onError, onOutput: onOutput }; instances[id] = tamePixie(pixieMap[id])(childInputs[id]); if (destroyed) return 'break'; }; for (var _iterator2 = Object.keys(pixieMap), _isArray2 = Array.isArray(_iterator2), _i2 = 0, _iterator2 = _isArray2 ? _iterator2 : _iterator2[Symbol.iterator]();;) { var _ref2; var _ret = _loop(); if (_ret === 'break') break; } return { update: function update(props) { for (var _iterator3 = Object.keys(instances), _isArray3 = Array.isArray(_iterator3), _i3 = 0, _iterator3 = _isArray3 ? _iterator3 : _iterator3[Symbol.iterator]();;) { var _ref3; if (_isArray3) { if (_i3 >= _iterator3.length) break; _ref3 = _iterator3[_i3++]; } else { _i3 = _iterator3.next(); if (_i3.done) break; _ref3 = _i3.value; } var _id = _ref3; instances[_id].update(props); if (destroyed) return; } }, destroy: function destroy() { destroyed = true; for (var _iterator4 = Object.keys(instances), _isArray4 = Array.isArray(_iterator4), _i4 = 0, _iterator4 = _isArray4 ? _iterator4 : _iterator4[Symbol.iterator]();;) { var _ref4; if (_isArray4) { if (_i4 >= _iterator4.length) break; _ref4 = _iterator4[_i4++]; } else { _i4 = _iterator4.next(); if (_i4.done) break; _ref4 = _i4.value; } var _id2 = _ref4; instances[_id2].destroy(); } } }; } outPixie.tame = true; outPixie.defaultOutput = defaultOutput; return outPixie; } /** * If the function throws, send that through `onError`. */ function catchify(f, onError) { return function () { try { return f.apply(undefined, arguments); } catch (e) { onError(e); } }; } /** * Returns true if two Javascript values are equal (non-recursively). */ function shallowCompare(a, b) { if (a === b) return true; // Fast path for primitives: if ((typeof a === 'undefined' ? 'undefined' : _typeof(a)) !== 'object') return false; if ((typeof b === 'undefined' ? 'undefined' : _typeof(b)) !== 'object') return false; // Filter out `null`: if (!a || !b) return false; var keys = Object.getOwnPropertyNames(a); if (keys.length !== Object.getOwnPropertyNames(b).length) return false; // We know that both objects have the same number of properties, // so if every property in `a` has a matching property in `b`, // the objects must be identical, regardless of key order. for (var _iterator = keys, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { var _ref; if (_isArray) { if (_i >= _iterator.length) break; _ref = _iterator[_i++]; } else { _i = _iterator.next(); if (_i.done) break; _ref = _i.value; } var key = _ref; if (!Object.prototype.hasOwnProperty.call(b, key) || a[key] !== b[key]) { return false; } } return true; } /** * Filters the props going into a pixie. */ function filterPixie(pixie, filter) { var tamedPixie = tamePixie(pixie); function outPixie(input) { var onError = input.onError, onOutput = input.onOutput; var instance = void 0; var propsCache = void 0; var destroyed = false; var safeFilter = catchify(filter, onError); var childInput = { onError: onError, onOutput: onOutput }; return { update: function update(props) { var innerProps = safeFilter(props); if (destroyed) return; var dirty = !shallowCompare(innerProps, propsCache); propsCache = innerProps; // Start or stop the instance: if (innerProps) { if (!instance) instance = tamedPixie(childInput); if (destroyed) return; if (dirty) instance.update(innerProps); } else { if (instance) instance.destroy(); instance = undefined; } }, destroy: function destroy() { destroyed = true; if (instance) instance.destroy(); instance = undefined; } }; } outPixie.tame = true; outPixie.defaultOutput = pixie.defaultOutput; return outPixie; } /** * Combines one or more pixies into one, using a list of keys. */ function mapPixie(pixie, listIds, filter) { var tamedPixie = tamePixie(pixie); function outPixie(input) { var onError = input.onError; var instances = {}; var outputs = {}; var outputsDirty = false; var propsCache = {}; var updating = false; var destroyed = false; var safeListIds = catchify(listIds, onError); var safeFilter = catchify(filter, onError); var updateOutputs = function updateOutputs() { if (outputsDirty && !updating) { outputsDirty = false; var newOutputs = {}; for (var _iterator = Object.keys(instances), _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { var _ref; if (_isArray) { if (_i >= _iterator.length) break; _ref = _iterator[_i++]; } else { _i = _iterator.next(); if (_i.done) break; _ref = _i.value; } var _id = _ref; newOutputs[_id] = outputs[_id]; } outputs = newOutputs; input.onOutput(outputs); } }; return { update: function update(outerProps) { var ids = safeListIds(outerProps); if (destroyed || !ids) return; // Update or create instances for all keys: updating = true; var newInstances = {}; var _loop = function _loop() { if (_isArray2) { if (_i2 >= _iterator2.length) return 'break'; _ref2 = _iterator2[_i2++]; } else { _i2 = _iterator2.next(); if (_i2.done) return 'break'; _ref2 = _i2.value; } var id = _ref2; var innerProps = safeFilter(outerProps, id); if (destroyed) return { v: void 0 }; var dirty = !shallowCompare(innerProps, propsCache[id]); propsCache[id] = innerProps; if (innerProps) { if (!instances[id]) { var onOutput = function onOutput(data) { if (data !== outputs[id]) { outputs[id] = data; outputsDirty = true; updateOutputs(); } }; instances[id] = tamedPixie({ onError: onError, onOutput: onOutput }); if (destroyed) return { v: void 0 }; } if (dirty) instances[id].update(innerProps); if (destroyed) return { v: void 0 }; newInstances[id] = instances[id]; } }; _loop2: for (var _iterator2 = ids, _isArray2 = Array.isArray(_iterator2), _i2 = 0, _iterator2 = _isArray2 ? _iterator2 : _iterator2[Symbol.iterator]();;) { var _ref2; var _ret = _loop(); switch (_ret) { case 'break': break _loop2; default: if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; } } // Swap out the instance list, removing unwanted pixies: var oldInstances = instances; instances = newInstances; // Destroy any old instances that are not on the list. // We need to finish this even if it triggers `destroyed`: for (var _iterator3 = Object.keys(oldInstances), _isArray3 = Array.isArray(_iterator3), _i3 = 0, _iterator3 = _isArray3 ? _iterator3 : _iterator3[Symbol.iterator]();;) { var _ref3; if (_isArray3) { if (_i3 >= _iterator3.length) break; _ref3 = _iterator3[_i3++]; } else { _i3 = _iterator3.next(); if (_i3.done) break; _ref3 = _i3.value; } var _id2 = _ref3; if (!newInstances[_id2]) { outputsDirty = true; oldInstances[_id2].destroy(); } } if (destroyed) return; updating = false; updateOutputs(); }, destroy: function destroy() { destroyed = true; for (var _iterator4 = Object.keys(instances), _isArray4 = Array.isArray(_iterator4), _i4 = 0, _iterator4 = _isArray4 ? _iterator4 : _iterator4[Symbol.iterator]();;) { var _ref4; if (_isArray4) { if (_i4 >= _iterator4.length) break; _ref4 = _iterator4[_i4++]; } else { _i4 = _iterator4.next(); if (_i4.done) break; _ref4 = _i4.value; } var _id3 = _ref4; if (instances[_id3]) instances[_id3].destroy(); } } }; } outPixie.tame = true; outPixie.defaultOutput = {}; return outPixie; } /** * Class-style pixies should inherit from this type. */ var Pixie = function () { function Pixie(props, callbacks) { classCallCheck(this, Pixie); this.props = props; } /** * Called every time the props change. */ createClass(Pixie, [{ key: 'update', value: function update(props, callbacks) {} /** * Called before the pixie is destroyed. * This is a great place to clean up any resources. */ }, { key: 'destroy', value: function destroy(props, callbacks) {} }]); return Pixie; }(); /** * Turns a class-style pixie into a tame pixie. */ function tameClassPixie(Constructor) { return tamePixie(function (_ref) { var onError = _ref.onError, onOutput = _ref.onOutput; var callbacks = { onError: onError, onOutput: onOutput }; var instance = void 0; var propsCache = void 0; return { update: function update(props) { propsCache = props; if (!instance) { instance = new Constructor(props, callbacks); } return instance.update(props, callbacks); }, destroy: function destroy() { return instance.destroy(propsCache, callbacks); } }; }); } /** * Update functions can return this to stop all future updates. */ var stopUpdates = { then: function then() {} }; exports.stopUpdates = stopUpdates; exports.catchPixieError = catchPixieError; exports.reflectPixieOutput = reflectPixieOutput; exports.tamePixie = tamePixie; exports.isPixieShutdownError = isPixieShutdownError; exports.attachPixie = attachPixie; exports.startPixie = startPixie; exports.combinePixies = combinePixies; exports.filterPixie = filterPixie; exports.mapPixie = mapPixie; exports.Pixie = Pixie; exports.tameClassPixie = tameClassPixie; //# sourceMappingURL=redux-pixies.js.map