thundercats
Version:
RxJS Meets isomorphic Flux
355 lines (285 loc) • 11.4 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
exports['default'] = Store;
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _nodeUuid = require('node-uuid');
var _nodeUuid2 = _interopRequireDefault(_nodeUuid);
var _stampit = require('stampit');
var _stampit2 = _interopRequireDefault(_stampit);
var _rx = require('rx');
var _invariant = require('invariant');
var _invariant2 = _interopRequireDefault(_invariant);
var _debug = require('debug');
var _debug2 = _interopRequireDefault(_debug);
var _supermixer = require('supermixer');
var _utils = require('./utils');
var assign = Object.assign;
var debug = (0, _debug2['default'])('thundercats:store');
var __DEV__ = process.env.NODE_ENV !== 'production';
function validateObservable(observable) {
/* istanbul ignore else */
if (__DEV__) {
(0, _invariant2['default'])((0, _utils.isObservable)(observable), 'register should get observables but was given %s', observable);
}
return observable;
}
function addOperation(observable, validateItem, map) {
return validateObservable(observable).tap(validateItem).map(map);
}
function registerObservable(obs, actionsArr, storeName) {
actionsArr = actionsArr.slice();
(0, _invariant2['default'])((0, _utils.isObservable)(obs), '%s should register observables but was given %s', storeName, obs);
debug('%s registering action', storeName);
actionsArr.push(obs);
return actionsArr;
}
var Optimism = {
confirm: function confirm(uid, history) {
checkId(uid, history);
history.get(uid).confirmed = true;
history.forEach(function (operation, uid) {
/* istanbul ignore else */
if (operation.confirmed) {
history['delete'](uid);
}
});
return history;
},
revert: function revert(uid, history) {
checkId(uid, history);
// initial value
var value = history.get(uid).oldValue;
var found = false;
history.forEach(function (descriptor, _uid) {
if (uid === _uid) {
found = true;
return;
}
if (!found) {
return;
}
descriptor.oldValue = value;
value = applyOperation(value, descriptor.operation);
});
history['delete'](uid);
return {
history: history,
value: value
};
}
};
exports.Optimism = Optimism;
function applyOperation(oldValue, operation) {
var replace = operation.replace;
var transform = operation.transform;
var set = operation.set;
if (replace) {
return replace;
} else if (transform) {
return transform(oldValue);
} else if (set) {
return assign({}, oldValue, set);
} else {
return oldValue;
}
}
function notifyObservers(value, observers) {
debug('starting notify cycle');
observers.forEach(function (observer, uid) {
debug('notifying %s', uid);
observer.onNext(value);
});
}
function _dispose(subscription) {
if (subscription) {
subscription.dispose();
}
return new Map();
}
function checkId(id, history) {
(0, _invariant2['default'])(history.has(id), 'an unknown operation id was used that is not within its history.' + 'it may have been called outside of context');
}
var methods = {
register: function register(observable) {
this.actions = registerObservable(observable, this.actions, (0, _utils.getName)(this));
return this;
},
hasObservers: function hasObservers() {
return !!this.observers.size;
},
_init: function _init() {
debug('initiating %s', (0, _utils.getName)(this));
this.history = _dispose(this._operationsSubscription, this.history);
(0, _invariant2['default'])(this.actions.length, '%s must have at least one action to listen to but has %s', (0, _utils.getName)(this), this.actions.length);
var operations = [];
this.actions.forEach(function (observable) {
operations.push(observable);
});
(0, _invariant2['default'])((0, _utils.areObservable)(operations), '"%s" actions should be an array of observables', (0, _utils.getName)(this));
this._operationsSubscription = _rx.Observable.merge(operations).doOnNext(function (operation) {
(0, _invariant2['default'])(typeof operation !== 'undefined' && !!operation, 'operation should be an object but was given %s', operation);
}).filter(function (operation) {
return typeof operation.replace === 'object' ? !!operation.replace : true;
}).filter(function (operation) {
return typeof operation.set === 'object' ? !!operation.set : true;
}).doOnNext(function (operation) {
(0, _invariant2['default'])(typeof operation === 'object', 'invalid operation, operations should be an object, given : %s', operation);
(0, _invariant2['default'])(typeof operation.replace === 'object' || typeof operation.transform === 'function' || typeof operation.set === 'object', 'invalid operation, ' + 'operations should have a replace(an object), ' + 'transform(a function), or set(an object) property but got %s', Object.keys(operation));
if ('optimistic' in operation) {
(0, _invariant2['default'])((0, _utils.isPromise)(operation.optimistic) || (0, _utils.isObservable)(operation.optimistic), 'invalid operation, optimistic should be a ' + 'promise or observable,' + 'given : %s', operation.optimistic);
}
}).subscribe(this._opsOnNext.bind(this), this.opsOnError.bind(this), this.opsOnCompleted.bind(this));
},
_opsOnNext: function _opsOnNext(operation) {
var _this = this;
var ops = assign({}, operation);
debug('on next called');
var oldValue = this.value;
var newValue = applyOperation(this.value, ops);
if (!newValue) {
debug('%s operational noop', (0, _utils.getName)(this));
// do not change value
// do not update history
// do not collect 200 dollars
return;
}
// if shouldStoreNotify returns false
// do not change value or update history
// else continue as normal
if (this.shouldStoreNotify && typeof this.shouldStoreNotify === 'function' && !this.shouldStoreNotify(oldValue, newValue)) {
debug('%s will not notify', (0, _utils.getName)(this));
return;
}
this.value = newValue;
notifyObservers(this.value, this.observers);
var uid = _nodeUuid2['default'].v1();
this.history.set(uid, {
operation: ops,
oldValue: oldValue
});
if ('optimistic' in ops) {
var optimisticObs = (0, _utils.isPromise)(ops.optimistic) ? _rx.Observable.fromPromise(ops.optimistic) : ops.optimistic;
optimisticObs.first().subscribe(function () {}, function (err) {
debug('optimistic error. reverting changes', err);
var _Optimism$revert = Optimism.revert(uid, _this.history);
var value = _Optimism$revert.value;
var history = _Optimism$revert.history;
_this.history = history;
_this.value = value;
notifyObservers(value, _this.observers);
}, function () {
return _this.history = Optimism.confirm(uid, _this.history);
});
} else {
Optimism.confirm(uid, this.history);
}
},
opsOnError: function opsOnError(err) {
throw new Error('An error has occurred in the operations observer: ' + err);
},
opsOnCompleted: function opsOnCompleted() {
console.warn('operations observable has terminated without error');
},
_subscribe: function _subscribe(observer) {
var _this2 = this;
var uid = _nodeUuid2['default'].v1();
/* istanbul ignore else */
if (!this.hasObservers()) {
this._init();
}
debug('adding observer %s', uid);
this.observers.set(uid, observer);
observer.onNext(this.value);
return _rx.Disposable.create(function () {
debug('Disposing obserable %s', uid);
_this2.observers['delete'](uid);
/* istanbul ignore else */
if (!_this2.hasObservers()) {
debug('all observers cleared');
_this2.dispose();
}
});
},
dispose: function dispose() {
debug('disposing %s', (0, _utils.getName)(this));
this.observers = new Map();
this.history = _dispose(this._operationsSubscription, this.history);
},
serialize: function serialize() {
return this.value ? JSON.stringify(this.value) : '';
},
deserialize: function deserialize(stringyData) {
var data = JSON.parse(stringyData);
(0, _invariant2['default'])(data && typeof data === 'object', '%s deserialize must return an object but got: %s', (0, _utils.getName)(this), data);
this.value = data;
return this.value;
}
};
var staticMethods = {
createRegistrar: function createRegistrar(store) {
function register(observable) {
store.actions = registerObservable(observable, store.actions, (0, _utils.getName)(store));
return store;
}
return register;
},
fromMany: function fromMany() {
return _rx.Observable.from(arguments).tap(validateObservable).toArray().flatMap(function (observables) {
return _rx.Observable.merge(observables);
});
},
replacer: function replacer(observable) {
return addOperation(observable, (0, _utils.createObjectValidator)('setter should receive objects but was given %s'), function (replace) {
return { replace: replace };
});
},
setter: function setter(observable) {
return addOperation(observable, (0, _utils.createObjectValidator)('setter should receive objects but was given %s'), function (set) {
return { set: set };
});
},
transformer: function transformer(observable) {
return addOperation(observable, function (fun) {
/* istanbul ignore else */
if (__DEV__) {
(0, _invariant2['default'])(typeof fun === 'function', 'transform should receive functions but was given %s', fun);
}
}, function (transform) {
return { transform: transform };
});
}
};
// Store is a stamp factory
// It returns a factory that creates store instances
function Store() {
var stampSpec = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
var _stampSpec$init = stampSpec.init;
var init = _stampSpec$init === undefined ? [] : _stampSpec$init;
var _stampSpec$refs = stampSpec.refs;
var refs = _stampSpec$refs === undefined ? {} : _stampSpec$refs;
var _stampSpec$props = stampSpec.props;
var props = _stampSpec$props === undefined ? {} : _stampSpec$props;
var _stampSpec$statics = stampSpec.statics;
var statics = _stampSpec$statics === undefined ? {} : _stampSpec$statics;
var _refs$value = refs.value;
var value = _refs$value === undefined ? {} : _refs$value;
var stamp = (0, _stampit2['default'])();
stamp.fixed.refs = stamp.fixed.state = (0, _supermixer.mergeChainNonFunctions)(stamp.fixed.refs, _rx.Observable.prototype);
assign(stamp, assign(stamp.fixed['static'], _rx.Observable));
(0, _supermixer.mixinChainFunctions)(stamp.fixed.methods, _rx.Observable.prototype);
return stamp.refs({
value: value,
_operationsSubscription: null
})['static'](staticMethods).methods(methods).init(function (_ref) {
var instance = _ref.instance;
instance.observers = new Map();
instance.history = new Map();
instance.actions = [];
_rx.Observable.call(instance);
return instance;
}).props(props).refs(refs)['static'](statics).init(init);
}
// Make static methods also available on stamp factory
assign(Store, staticMethods);