UNPKG

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