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 | this.destroy = this.destroy;
|
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 |
|
103 |
|
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 |
|
169 |
|
170 |
|
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 |
|
227 | function flattenRefs(refs) {
|
228 | if (!refs) return;
|
229 | if (refs instanceof Handle) return refs.toString();
|
230 | return _.mapValues(refs, flattenRefs);
|
231 | }
|
232 |
|