UNPKG

firetruss

Version:

Advanced data sync layer for Firebase and Vue.js

234 lines (209 loc) 7.68 kB
import {Handle, Query, Reference} from './Reference.js'; import angular from './angularCompatibility.js'; import stats from './utils/stats.js'; import {isTrussEqual} from './utils/utils.js'; import _ from 'lodash'; import performanceNow from 'performance-now'; import Vue from 'vue'; export default class Connector { constructor(scope, connections, tree, method, refs) { Object.freeze(connections); this._scope = scope; this._connections = connections; this._tree = tree; this._method = method; this._subConnectors = {}; this._disconnects = {}; this._angularUnwatches = undefined; this._data = {}; this._vue = new Vue({data: { descriptors: {}, refs: refs || {}, values: _.mapValues(connections, _.constant(undefined)) }}); // allow instance-level overrides of destroy() method this.destroy = this.destroy; // eslint-disable-line no-self-assign Object.seal(this); this._linkScopeProperties(); _.forEach(connections, (descriptor, key) => { if (_.isFunction(descriptor)) { this._bindComputedConnection(key, descriptor); } else { this._connect(key, descriptor); } }); if (angular.active && scope && scope.$on && scope.$id) { scope.$on('$destroy', () => {this.destroy();}); } } get ready() { return _.every(this._connections, (ignored, key) => { const descriptor = this._vue.descriptors[key]; if (!descriptor) return false; if (descriptor instanceof Handle) return descriptor.ready; return this._subConnectors[key].ready; }); } get at() { return this._vue.refs; } get data() { return this._data; } destroy() { this._unlinkScopeProperties(); _.forEach(this._angularUnwatches, unwatch => {unwatch();}); _.forEach(this._connections, (descriptor, key) => {this._disconnect(key);}); this._vue.$destroy(); } _linkScopeProperties() { const dataProperties = _.mapValues(this._connections, (unused, key) => ({ configurable: true, enumerable: false, get: () => { const descriptor = this._vue.descriptors[key]; if (descriptor instanceof Reference) return descriptor.value; return this._vue.values[key]; } })); Object.defineProperties(this._data, dataProperties); if (this._scope) { for (const key in this._connections) { if (key in this._scope) { throw new Error(`Property already defined on connection target: ${key}`); } } Object.defineProperties(this._scope, dataProperties); if (this._scope.__ob__) this._scope.__ob__.dep.notify(); } } _unlinkScopeProperties() { if (!this._scope) return; _.forEach(this._connections, (descriptor, key) => { delete this._scope[key]; }); } _bindComputedConnection(key, fn) { const connectionStats = stats.for(`connection.at.${key}`); const getter = this._computeConnection.bind(this, fn, connectionStats); const update = this._updateComputedConnection.bind(this, key, fn, connectionStats); const angularWatch = angular.active && !fn.angularWatchSuppressed; // Use this._vue.$watch instead of truss.observe here so that we can disable the immediate // callback if we'll get one from Angular anyway. this._vue.$watch(getter, update, {immediate: !angularWatch}); if (angularWatch) { if (!this._angularUnwatches) this._angularUnwatches = []; this._angularUnwatches.push(angular.watch(getter, update, true)); } } _computeConnection(fn, connectionStats) { const startTime = performanceNow(); try { return flattenRefs(fn.call(this._scope)); } finally { connectionStats.runtime += performanceNow() - startTime; connectionStats.numRecomputes += 1; } } _updateComputedConnection(key, value, connectionStats) { const newDescriptor = _.isFunction(value) ? value(this._scope) : value; const oldDescriptor = this._vue.descriptors[key]; const descriptorChanged = !isTrussEqual(oldDescriptor, newDescriptor); if (!descriptorChanged) return; if (connectionStats && descriptorChanged) connectionStats.numUpdates += 1; if (!newDescriptor) { this._disconnect(key); return; } if (newDescriptor instanceof Handle || !_.has(this._subConnectors, key)) { this._disconnect(key); this._connect(key, newDescriptor); } else { this._subConnectors[key]._updateConnections(newDescriptor); } Vue.set(this._vue.descriptors, key, newDescriptor); angular.digest(); } _updateConnections(connections) { _.forEach(connections, (descriptor, key) => { this._updateComputedConnection(key, descriptor); }); _.forEach(this._connections, (descriptor, key) => { if (!_.has(connections, key)) this._updateComputedConnection(key); }); this._connections = connections; } _connect(key, descriptor) { Vue.set(this._vue.descriptors, key, descriptor); angular.digest(); if (!descriptor) return; Vue.set(this._vue.values, key, undefined); if (descriptor instanceof Reference) { Vue.set(this._vue.refs, key, descriptor); this._disconnects[key] = this._tree.connectReference(descriptor, this._method); } else if (descriptor instanceof Query) { Vue.set(this._vue.refs, key, descriptor); const updateFn = this._updateQueryValue.bind(this, key); this._disconnects[key] = this._tree.connectQuery(descriptor, updateFn, this._method); } else { const subScope = {}, subRefs = {}; Vue.set(this._vue.refs, key, subRefs); const subConnector = this._subConnectors[key] = new Connector(subScope, descriptor, this._tree, this._method, subRefs); // Use a truss.observe here instead of this._vue.$watch so that the "immediate" execution // actually takes place after we've captured the unwatch function, in case the subConnector // is ready immediately. const unobserve = this._disconnects[key] = this._tree.truss.observe( () => subConnector.ready, subReady => { if (!subReady) return; unobserve(); delete this._disconnects[key]; Vue.set(this._vue.values, key, subScope); angular.digest(); } ); } } _disconnect(key) { Vue.delete(this._vue.refs, key); this._updateRefValue(key, undefined); if (_.has(this._subConnectors, key)) { this._subConnectors[key].destroy(); delete this._subConnectors[key]; } if (this._disconnects[key]) this._disconnects[key](); delete this._disconnects[key]; Vue.delete(this._vue.descriptors, key); angular.digest(); } _updateRefValue(key, value) { if (this._vue.values[key] !== value) { Vue.set(this._vue.values, key, value); angular.digest(); } } _updateQueryValue(key, childKeys) { if (!this._vue.values[key]) { Vue.set(this._vue.values, key, {}); angular.digest(); } const subScope = this._vue.values[key]; for (const childKey in subScope) { if (!subScope.hasOwnProperty(childKey)) continue; if (!_.includes(childKeys, childKey)) { Vue.delete(subScope, childKey); angular.digest(); } } const object = this._tree.getObject(this._vue.descriptors[key].path); for (const childKey of childKeys) { if (subScope.hasOwnProperty(childKey)) continue; Vue.set(subScope, childKey, object[childKey]); angular.digest(); } } } function flattenRefs(refs) { if (!refs) return; if (refs instanceof Handle) return refs.toString(); return _.mapValues(refs, flattenRefs); }