redux-pixies
Version:
The magical asynchronous Redux library
937 lines (775 loc) • 23.5 kB
JavaScript
;
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