feathers-offline-realtime
Version:
Offline-first realtime replication with optimistic updates.
345 lines (264 loc) • 11 kB
JavaScript
'use strict';
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; }; }();
/*
Forked from feathers-memory/src/index.js
*/
exports.default = init;
var _uberproto = require('uberproto');
var _uberproto2 = _interopRequireDefault(_uberproto);
var _feathersErrors = require('feathers-errors');
var _feathersErrors2 = _interopRequireDefault(_feathersErrors);
var _feathersQueryFilters = require('feathers-query-filters');
var _feathersQueryFilters2 = _interopRequireDefault(_feathersQueryFilters);
var _feathersCommons = require('feathers-commons');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Service = function () {
function Service() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
_classCallCheck(this, Service);
this._replicator = options.replicator;
this._engine = this._replicator.engine;
if (!this._engine.useUuid) {
throw new Error('Replicator must be configured for uuid for optimistic updates. (offline)');
}
this._mutateStore = this._engine._mutateStore.bind(this._engine);
this._alwaysSelect = ['id', '_id', 'uuid'];
this._getUuid = this._replicator.getUuid;
this.store = this._engine.store || { records: [] };
this.paginate = options.paginate || {};
}
_createClass(Service, [{
key: 'extend',
value: function extend(obj) {
return _uberproto2.default.extend(obj, this);
}
// Find without hooks and mixins that can be used internally and always returns
// a pagination object
}, {
key: '_find',
value: function _find(params) {
var getFilter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _feathersQueryFilters2.default;
var _getFilter = getFilter(params.query || {}),
query = _getFilter.query,
filters = _getFilter.filters;
var values = _feathersCommons._.values(this.store.records).filter((0, _feathersCommons.matcher)(query));
var total = values.length;
if (filters.$sort) {
values.sort((0, _feathersCommons.sorter)(filters.$sort));
}
if (filters.$skip) {
values = values.slice(filters.$skip);
}
if (typeof filters.$limit !== 'undefined') {
values = values.slice(0, filters.$limit);
}
if (filters.$select) {
values = values.map(function (value) {
return _feathersCommons._.pick.apply(_feathersCommons._, [value].concat(_toConsumableArray(filters.$select)));
});
}
return Promise.resolve({
total: total,
limit: filters.$limit,
skip: filters.$skip || 0,
data: values
});
}
}, {
key: 'find',
value: function find(params) {
var paginate = typeof params.paginate !== 'undefined' ? params.paginate : this.paginate;
// Call the internal find with query parameter that include pagination
var result = this._find(params, function (query) {
return (0, _feathersQueryFilters2.default)(query, paginate);
});
if (!(paginate && paginate.default)) {
return result.then(function (page) {
return page.data;
});
}
return result;
}
}, {
key: 'get',
value: function get(uuid, params) {
var records = this.store.records;
var index = findUuidIndex(records, uuid);
if (index === -1) {
return Promise.reject(new _feathersErrors2.default.NotFound('No record found for uuid \'' + uuid + '\''));
}
return Promise.resolve(records[index]).then(_feathersCommons.select.apply(undefined, [params].concat(_toConsumableArray(this._alwaysSelect))));
}
// Create without hooks and mixins that can be used internally
}, {
key: '_create',
value: function _create(data, params) {
var _this = this;
this._checkConnected();
if (!('uuid' in data)) {
data.uuid = this._getUuid();
}
var records = this.store.records;
var index = findUuidIndex(records, data.uuid);
if (index > -1) {
throw new _feathersErrors2.default.BadRequest('Optimistic create requires unique uuid. (offline)');
}
// optimistic mutation
this._mutateStore('created', data, 1);
// Start actual mutation on remote service
this._replicator._service.create(shallowClone(data), params).catch(function () {
_this._mutateStore('removed', data, 2);
});
return Promise.resolve(data).then(_feathersCommons.select.apply(undefined, [params].concat(_toConsumableArray(this._alwaysSelect))));
}
}, {
key: 'create',
value: function create(data, params) {
var _this2 = this;
if (Array.isArray(data)) {
return Promise.all(data.map(function (current) {
return _this2._create(current);
}));
}
return this._create(data, params);
}
// Update without hooks and mixins that can be used internally
}, {
key: '_update',
value: function _update(uuid, data, params) {
var _this3 = this;
this._checkConnected();
checkUuidExists(data);
var records = this.store.records;
var index = findUuidIndex(records, uuid);
if (index === -1) {
return Promise.reject(new _feathersErrors2.default.NotFound('No record found for uuid \'' + uuid + '\''));
}
// We don't want our id to change type if it can be coerced
var beforeRecord = shallowClone(records[index]);
var beforeUuid = beforeRecord.uuid;
data.uuid = beforeUuid == uuid ? beforeUuid : uuid; // eslint-disable-line
// Optimistic mutation
this._mutateStore('updated', data, 1);
// Start actual mutation on remote service
this._replicator._service.update(getId(data), shallowClone(data), params).catch(function () {
_this3._mutateStore('updated', beforeRecord, 2);
});
return Promise.resolve(data).then(_feathersCommons.select.apply(undefined, [params].concat(_toConsumableArray(this._alwaysSelect))));
}
}, {
key: 'update',
value: function update(uuid, data, params) {
if (uuid === null || Array.isArray(data)) {
return Promise.reject(new _feathersErrors2.default.BadRequest('You can not replace multiple instances. Did you mean \'patch\'?'));
}
return this._update(uuid, data, params);
}
// Patch without hooks and mixins that can be used internally
}, {
key: '_patch',
value: function _patch(uuid, data, params) {
var _this4 = this;
this._checkConnected();
var records = this.store.records;
var index = findUuidIndex(records, uuid);
if (index === -1) {
return Promise.reject(new _feathersErrors2.default.NotFound('No record found for uuid \'' + uuid + '\''));
}
// Optimistic mutation
var beforeRecord = shallowClone(records[index]);
var afterRecord = Object.assign({}, beforeRecord, data);
this._mutateStore('patched', afterRecord, 1);
// Start actual mutation on remote service
this._replicator._service.patch(getId(beforeRecord), shallowClone(data), params).catch(function () {
_this4._mutateStore('updated', beforeRecord, 2);
});
return Promise.resolve(afterRecord).then(_feathersCommons.select.apply(undefined, [params].concat(_toConsumableArray(this._alwaysSelect))));
}
}, {
key: 'patch',
value: function patch(uuid, data, params) {
var _this5 = this;
if (uuid === null) {
return this._find(params).then(function (page) {
return Promise.all(page.data.map(function (current) {
return _this5._patch(current.uuid, data, params);
}));
});
}
return this._patch(uuid, data, params);
}
// Remove without hooks and mixins that can be used internally
}, {
key: '_remove',
value: function _remove(uuid, params) {
var _this6 = this;
this._checkConnected();
var records = this.store.records;
var index = findUuidIndex(records, uuid);
if (index === -1) {
return Promise.reject(new _feathersErrors2.default.NotFound('No record found for uuid \'' + uuid + '\''));
}
// Optimistic mutation
var beforeRecord = shallowClone(records[index]);
this._mutateStore('removed', beforeRecord, 1);
// Start actual mutation on remote service
this._replicator._service.remove(getId(beforeRecord), params).catch(function () {
_this6._mutateStore('created', beforeRecord, 2);
});
return Promise.resolve(beforeRecord).then(_feathersCommons.select.apply(undefined, [params].concat(_toConsumableArray(this._alwaysSelect))));
}
}, {
key: 'remove',
value: function remove(uuid, params) {
var _this7 = this;
if (uuid === null) {
return this._find(params).then(function (page) {
return Promise.all(page.data.map(function (current) {
return _this7._remove(current.uuid, params);
}));
});
}
return this._remove(uuid, params);
}
}, {
key: '_checkConnected',
value: function _checkConnected() {
if (!this._replicator.connected) {
throw new _feathersErrors2.default.BadRequest('Replicator not connected to remote. (offline)');
}
}
}]);
return Service;
}();
function init(options) {
return new Service(options);
}
init.Service = Service;
// Helpers
function findUuidIndex(array, uuid) {
for (var i = 0, len = array.length; i < len; i++) {
if (array[i].uuid == uuid) {
// eslint-disable-line
return i;
}
}
return -1;
}
function checkUuidExists(record) {
if (!('uuid' in record)) {
throw new _feathersErrors2.default.BadRequest('Optimistic mutation requires uuid. (offline)');
}
}
function getId(record) {
return 'id' in record ? record.id : record._id;
}
function shallowClone(obj) {
return Object.assign({}, obj);
}
module.exports = exports['default'];