1 | import {Handle, Query, Reference} from './Reference.js';
|
2 | import angular from './angularCompatibility.js';
|
3 | import stats from './utils/stats.js';
|
4 | import {isTrussEqual} from './utils/utils.js';
|
5 |
|
6 | import _ from 'lodash';
|
7 | import performanceNow from 'performance-now';
|
8 | import Vue from 'vue';
|
9 |
|
10 |
|
11 | export 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 |
|
29 | this.destroy = this.destroy;
|
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 |
|
104 |
|
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 |
|
170 |
|
171 |
|
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 |
|
228 | function flattenRefs(refs) {
|
229 | if (!refs) return;
|
230 | if (refs instanceof Handle) return refs.toString();
|
231 | return _.mapValues(refs, flattenRefs);
|
232 | }
|
233 |
|