UNPKG

7.64 kBJavaScriptView Raw
1import {Handle, Query, Reference} from './Reference.js';
2import angular from './angularCompatibility.js';
3import stats from './utils/stats.js';
4import {isTrussEqual} from './utils/utils.js';
5
6import _ from 'lodash';
7import performanceNow from 'performance-now';
8import Vue from 'vue';
9
10
11export default class Connector {
12 constructor(scope, connections, tree, method, refs) {
13 Object.freeze(connections);
14 this._scope = scope;
15 this._connections = connections;
16 this._tree = tree;
17 this._method = method;
18
19 this._subConnectors = {};
20 this._disconnects = {};
21 this._angularUnwatches = undefined;
22 this._data = {};
23 this._vue = new Vue({data: {
24 descriptors: {},
25 refs: refs || {},
26 values: _.mapValues(connections, _.constant(undefined))
27 }});
28 this.destroy = this.destroy; // allow instance-level overrides of destroy() method
29 Object.seal(this);
30
31 this._linkScopeProperties();
32
33 _.forEach(connections, (descriptor, key) => {
34 if (_.isFunction(descriptor)) {
35 this._bindComputedConnection(key, descriptor);
36 } else {
37 this._connect(key, descriptor);
38 }
39 });
40
41 if (angular.active && scope && scope.$on && scope.$id) {
42 scope.$on('$destroy', () => {this.destroy();});
43 }
44 }
45
46 get ready() {
47 return _.every(this._connections, (ignored, key) => {
48 const descriptor = this._vue.descriptors[key];
49 if (!descriptor) return false;
50 if (descriptor instanceof Handle) return descriptor.ready;
51 return this._subConnectors[key].ready;
52 });
53 }
54
55 get at() {
56 return this._vue.refs;
57 }
58
59 get data() {
60 return this._data;
61 }
62
63 destroy() {
64 this._unlinkScopeProperties();
65 _.forEach(this._angularUnwatches, unwatch => {unwatch();});
66 _.forEach(this._connections, (descriptor, key) => {this._disconnect(key);});
67 this._vue.$destroy();
68 }
69
70 _linkScopeProperties() {
71 const dataProperties = _.mapValues(this._connections, (unused, key) => ({
72 configurable: true, enumerable: false, get: () => {
73 const descriptor = this._vue.descriptors[key];
74 if (descriptor instanceof Reference) return descriptor.value;
75 return this._vue.values[key];
76 }
77 }));
78 Object.defineProperties(this._data, dataProperties);
79 if (this._scope) {
80 for (const key in this._connections) {
81 if (key in this._scope) {
82 throw new Error(`Property already defined on connection target: ${key}`);
83 }
84 }
85 Object.defineProperties(this._scope, dataProperties);
86 if (this._scope.__ob__) this._scope.__ob__.dep.notify();
87 }
88 }
89
90 _unlinkScopeProperties() {
91 if (!this._scope) return;
92 _.forEach(this._connections, (descriptor, key) => {
93 delete this._scope[key];
94 });
95 }
96
97 _bindComputedConnection(key, fn) {
98 const connectionStats = stats.for(`connection.at.${key}`);
99 const getter = this._computeConnection.bind(this, fn, connectionStats);
100 const update = this._updateComputedConnection.bind(this, key, fn, connectionStats);
101 const angularWatch = angular.active && !fn.angularWatchSuppressed;
102 // Use this._vue.$watch instead of truss.observe here so that we can disable the immediate
103 // callback if we'll get one from Angular anyway.
104 this._vue.$watch(getter, update, {immediate: !angularWatch});
105 if (angularWatch) {
106 if (!this._angularUnwatches) this._angularUnwatches = [];
107 this._angularUnwatches.push(angular.watch(getter, update, true));
108 }
109 }
110
111 _computeConnection(fn, connectionStats) {
112 const startTime = performanceNow();
113 try {
114 return flattenRefs(fn.call(this._scope));
115 } finally {
116 connectionStats.runtime += performanceNow() - startTime;
117 connectionStats.numRecomputes += 1;
118 }
119 }
120
121 _updateComputedConnection(key, value, connectionStats) {
122 const newDescriptor = _.isFunction(value) ? value(this._scope) : value;
123 const oldDescriptor = this._vue.descriptors[key];
124 const descriptorChanged = !isTrussEqual(oldDescriptor, newDescriptor);
125 if (!descriptorChanged) return;
126 if (connectionStats && descriptorChanged) connectionStats.numUpdates += 1;
127 if (!newDescriptor) {
128 this._disconnect(key);
129 return;
130 }
131 if (newDescriptor instanceof Handle || !_.has(this._subConnectors, key)) {
132 this._disconnect(key);
133 this._connect(key, newDescriptor);
134 } else {
135 this._subConnectors[key]._updateConnections(newDescriptor);
136 }
137 Vue.set(this._vue.descriptors, key, newDescriptor);
138 angular.digest();
139 }
140
141 _updateConnections(connections) {
142 _.forEach(connections, (descriptor, key) => {
143 this._updateComputedConnection(key, descriptor);
144 });
145 _.forEach(this._connections, (descriptor, key) => {
146 if (!_.has(connections, key)) this._updateComputedConnection(key);
147 });
148 this._connections = connections;
149 }
150
151 _connect(key, descriptor) {
152 Vue.set(this._vue.descriptors, key, descriptor);
153 angular.digest();
154 if (!descriptor) return;
155 Vue.set(this._vue.values, key, undefined);
156 if (descriptor instanceof Reference) {
157 Vue.set(this._vue.refs, key, descriptor);
158 this._disconnects[key] = this._tree.connectReference(descriptor, this._method);
159 } else if (descriptor instanceof Query) {
160 Vue.set(this._vue.refs, key, descriptor);
161 const updateFn = this._updateQueryValue.bind(this, key);
162 this._disconnects[key] = this._tree.connectQuery(descriptor, updateFn, this._method);
163 } else {
164 const subScope = {}, subRefs = {};
165 Vue.set(this._vue.refs, key, subRefs);
166 const subConnector = this._subConnectors[key] =
167 new Connector(subScope, descriptor, this._tree, this._method, subRefs);
168 // Use a truss.observe here instead of this._vue.$watch so that the "immediate" execution
169 // actually takes place after we've captured the unwatch function, in case the subConnector
170 // is ready immediately.
171 const unobserve = this._disconnects[key] = this._tree.truss.observe(
172 () => subConnector.ready,
173 subReady => {
174 if (!subReady) return;
175 unobserve();
176 delete this._disconnects[key];
177 Vue.set(this._vue.values, key, subScope);
178 angular.digest();
179 }
180 );
181 }
182 }
183
184 _disconnect(key) {
185 Vue.delete(this._vue.refs, key);
186 this._updateRefValue(key, undefined);
187 if (_.has(this._subConnectors, key)) {
188 this._subConnectors[key].destroy();
189 delete this._subConnectors[key];
190 }
191 if (this._disconnects[key]) this._disconnects[key]();
192 delete this._disconnects[key];
193 Vue.delete(this._vue.descriptors, key);
194 angular.digest();
195 }
196
197 _updateRefValue(key, value) {
198 if (this._vue.values[key] !== value) {
199 Vue.set(this._vue.values, key, value);
200 angular.digest();
201 }
202 }
203
204 _updateQueryValue(key, childKeys) {
205 if (!this._vue.values[key]) {
206 Vue.set(this._vue.values, key, {});
207 angular.digest();
208 }
209 const subScope = this._vue.values[key];
210 for (const childKey in subScope) {
211 if (!subScope.hasOwnProperty(childKey)) continue;
212 if (!_.includes(childKeys, childKey)) {
213 Vue.delete(subScope, childKey);
214 angular.digest();
215 }
216 }
217 const object = this._tree.getObject(this._vue.descriptors[key].path);
218 for (const childKey of childKeys) {
219 if (subScope.hasOwnProperty(childKey)) continue;
220 Vue.set(subScope, childKey, object[childKey]);
221 angular.digest();
222 }
223 }
224
225}
226
227function flattenRefs(refs) {
228 if (!refs) return;
229 if (refs instanceof Handle) return refs.toString();
230 return _.mapValues(refs, flattenRefs);
231}
232