react-chrome-redux
Version:
A set of utilities for building Redux applications in Google Chrome Extensions.
250 lines (198 loc) • 8.73 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
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 _lodash = require('lodash.assignin');
var _lodash2 = _interopRequireDefault(_lodash);
var _constants = require('../constants');
var _serialization = require('../serialization');
var _patch = require('../strategies/shallowDiff/patch');
var _patch2 = _interopRequireDefault(_patch);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var backgroundErrPrefix = '\nLooks like there is an error in the background page. ' + 'You might want to inspect your background page for more details.\n';
var Store = function () {
/**
* Creates a new Proxy store
* @param {object} options An object of form {portName, state, extensionId, serializer, deserializer, diffStrategy}, where `portName` is a required string and defines the name of the port for state transition changes, `state` is the initial state of this store (default `{}`) `extensionId` is the extension id as defined by chrome when extension is loaded (default `''`), `serializer` is a function to serialize outgoing message payloads (default is passthrough), `deserializer` is a function to deserialize incoming message payloads (default is passthrough), and patchStrategy is one of the included patching strategies (default is shallow diff) or a custom patching function.
*/
function Store(_ref) {
var _this = this;
var portName = _ref.portName,
_ref$state = _ref.state,
state = _ref$state === undefined ? {} : _ref$state,
_ref$extensionId = _ref.extensionId,
extensionId = _ref$extensionId === undefined ? null : _ref$extensionId,
_ref$serializer = _ref.serializer,
serializer = _ref$serializer === undefined ? _serialization.noop : _ref$serializer,
_ref$deserializer = _ref.deserializer,
deserializer = _ref$deserializer === undefined ? _serialization.noop : _ref$deserializer,
_ref$patchStrategy = _ref.patchStrategy,
patchStrategy = _ref$patchStrategy === undefined ? _patch2.default : _ref$patchStrategy;
_classCallCheck(this, Store);
if (!portName) {
throw new Error('portName is required in options');
}
if (typeof serializer !== 'function') {
throw new Error('serializer must be a function');
}
if (typeof deserializer !== 'function') {
throw new Error('deserializer must be a function');
}
if (typeof patchStrategy !== 'function') {
throw new Error('patchStrategy must be one of the included patching strategies or a custom patching function');
}
this.portName = portName;
this.readyResolved = false;
this.readyPromise = new Promise(function (resolve) {
return _this.readyResolve = resolve;
});
this.extensionId = extensionId; // keep the extensionId as an instance variable
this.port = chrome.runtime.connect(this.extensionId, { name: portName });
this.safetyHandler = this.safetyHandler.bind(this);
this.safetyMessage = chrome.runtime.onMessage.addListener(this.safetyHandler);
this.serializedPortListener = (0, _serialization.withDeserializer)(deserializer)(function () {
var _port$onMessage;
return (_port$onMessage = _this.port.onMessage).addListener.apply(_port$onMessage, arguments);
});
this.serializedMessageSender = (0, _serialization.withSerializer)(serializer)(function () {
var _chrome$runtime;
return (_chrome$runtime = chrome.runtime).sendMessage.apply(_chrome$runtime, arguments);
}, 1);
this.listeners = [];
this.state = state;
this.patchStrategy = patchStrategy;
// Don't use shouldDeserialize here, since no one else should be using this port
this.serializedPortListener(function (message) {
switch (message.type) {
case _constants.STATE_TYPE:
_this.replaceState(message.payload);
if (!_this.readyResolved) {
_this.readyResolved = true;
_this.readyResolve();
}
break;
case _constants.PATCH_STATE_TYPE:
_this.patchState(message.payload);
break;
default:
// do nothing
}
});
this.dispatch = this.dispatch.bind(this); // add this context to dispatch
}
/**
* Returns a promise that resolves when the store is ready. Optionally a callback may be passed in instead.
* @param [function] callback An optional callback that may be passed in and will fire when the store is ready.
* @return {object} promise A promise that resolves when the store has established a connection with the background page.
*/
_createClass(Store, [{
key: 'ready',
value: function ready() {
var cb = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
if (cb !== null) {
return this.readyPromise.then(cb);
}
return this.readyPromise;
}
/**
* Subscribes a listener function for all state changes
* @param {function} listener A listener function to be called when store state changes
* @return {function} An unsubscribe function which can be called to remove the listener from state updates
*/
}, {
key: 'subscribe',
value: function subscribe(listener) {
var _this2 = this;
this.listeners.push(listener);
return function () {
_this2.listeners = _this2.listeners.filter(function (l) {
return l !== listener;
});
};
}
/**
* Replaces the state for only the keys in the updated state. Notifies all listeners of state change.
* @param {object} state the new (partial) redux state
*/
}, {
key: 'patchState',
value: function patchState(difference) {
this.state = this.patchStrategy(this.state, difference);
this.listeners.forEach(function (l) {
return l();
});
}
/**
* Replace the current state with a new state. Notifies all listeners of state change.
* @param {object} state The new state for the store
*/
}, {
key: 'replaceState',
value: function replaceState(state) {
this.state = state;
this.listeners.forEach(function (l) {
return l();
});
}
/**
* Get the current state of the store
* @return {object} the current store state
*/
}, {
key: 'getState',
value: function getState() {
return this.state;
}
/**
* Stub function to stay consistent with Redux Store API. No-op.
*/
}, {
key: 'replaceReducer',
value: function replaceReducer() {
return;
}
/**
* Dispatch an action to the background using messaging passing
* @param {object} data The action data to dispatch
* @return {Promise} Promise that will resolve/reject based on the action response from the background
*/
}, {
key: 'dispatch',
value: function dispatch(data) {
var _this3 = this;
return new Promise(function (resolve, reject) {
_this3.serializedMessageSender(_this3.extensionId, {
type: _constants.DISPATCH_TYPE,
portName: _this3.portName,
payload: data
}, null, function (resp) {
var error = resp.error,
value = resp.value;
if (error) {
var bgErr = new Error('' + backgroundErrPrefix + error);
reject((0, _lodash2.default)(bgErr, error));
} else {
resolve(value && value.payload);
}
});
});
}
}, {
key: 'safetyHandler',
value: function safetyHandler(message) {
if (message.action === 'storeReady') {
// Remove Saftey Listener
chrome.runtime.onMessage.removeListener(this.safetyHandler);
// Resolve if readyPromise has not been resolved.
if (!this.readyResolved) {
this.readyResolved = true;
this.readyResolve();
}
}
}
}]);
return Store;
}();
exports.default = Store;