1 | import _ from 'lodash';
|
2 | import Vue from 'vue';
|
3 | import angular from './angularCompatibility.js';
|
4 | import {splitPath} from './utils/paths.js';
|
5 |
|
6 |
|
7 | class QueryHandler {
|
8 | constructor(coupler, query) {
|
9 | this._coupler = coupler;
|
10 | this._query = query;
|
11 | this._listeners = [];
|
12 | this._keys = [];
|
13 | this._url = this._coupler._rootUrl + query.path;
|
14 | this._segments = splitPath(query.path, true);
|
15 | this._listening = false;
|
16 | this.ready = false;
|
17 | }
|
18 |
|
19 | attach(operation, keysCallback) {
|
20 | this._listen();
|
21 | this._listeners.push({operation, keysCallback});
|
22 | if (this.ready) {
|
23 | this._coupler._dispatcher.markReady(operation);
|
24 | if (keysCallback) keysCallback(this._keys);
|
25 | }
|
26 | }
|
27 |
|
28 | detach(operation) {
|
29 | const k = _.findIndex(this._listeners, {operation});
|
30 | if (k >= 0) this._listeners.splice(k, 1);
|
31 | return this._listeners.length;
|
32 | }
|
33 |
|
34 | _listen() {
|
35 | if (this._listening) return;
|
36 | this._coupler._bridge.on(
|
37 | this._query.toString(), this._url, this._query.constraints, 'value',
|
38 | this._handleSnapshot, this._handleError, this, {sync: true});
|
39 | this._listening = true;
|
40 | }
|
41 |
|
42 | destroy() {
|
43 | this._coupler._bridge.off(
|
44 | this._query.toString(), this._url, this._query.constraints, 'value', this._handleSnapshot,
|
45 | this);
|
46 | this._listening = false;
|
47 | this.ready = false;
|
48 | angular.digest();
|
49 | for (const key of this._keys) {
|
50 | this._coupler._decoupleSegments(this._segments.concat(key));
|
51 | }
|
52 | }
|
53 |
|
54 | _handleSnapshot(snap) {
|
55 | this._coupler._queueSnapshotCallback(() => {
|
56 |
|
57 |
|
58 | if (!this._listeners.length || !this._listening) return;
|
59 | const updatedKeys = this._updateKeysAndApplySnapshot(snap);
|
60 | if (!this.ready) {
|
61 | this.ready = true;
|
62 | angular.digest();
|
63 | for (const listener of this._listeners) {
|
64 | this._coupler._dispatcher.markReady(listener.operation);
|
65 | }
|
66 | }
|
67 | if (updatedKeys) {
|
68 | for (const listener of this._listeners) {
|
69 | if (listener.keysCallback) listener.keysCallback(updatedKeys);
|
70 | }
|
71 | }
|
72 | });
|
73 | }
|
74 |
|
75 | _updateKeysAndApplySnapshot(snap) {
|
76 | let updatedKeys;
|
77 | if (snap.path === this._query.path) {
|
78 | updatedKeys = _.keys(snap.value);
|
79 | updatedKeys.sort();
|
80 | if (_.isEqual(this._keys, updatedKeys)) {
|
81 | updatedKeys = null;
|
82 | } else {
|
83 | for (const key of _.difference(updatedKeys, this._keys)) {
|
84 | this._coupler._coupleSegments(this._segments.concat(key));
|
85 | }
|
86 | for (const key of _.difference(this._keys, updatedKeys)) {
|
87 |
|
88 |
|
89 | this._coupler._decoupleSegments(this._segments.concat(key));
|
90 | }
|
91 | this._keys = updatedKeys;
|
92 | }
|
93 |
|
94 |
|
95 | if (snap.exists) {
|
96 | const rootValue = snap.value;
|
97 | const rootPath = snap.path;
|
98 | for (const key of this._keys) {
|
99 | snap._path = rootPath + '/' + key;
|
100 | snap._key = undefined;
|
101 | snap._value = rootValue[key];
|
102 | this._coupler._applySnapshot(snap);
|
103 | }
|
104 |
|
105 | snap._path = rootPath;
|
106 | snap._key = undefined;
|
107 | snap._value = rootValue;
|
108 | }
|
109 | } else if (snap.path.replace(/\/[^/]+/, '') === this._query.path) {
|
110 | const hasKey = _.includes(this._keys, snap.key);
|
111 | if (snap.value) {
|
112 | if (!hasKey) {
|
113 | this._coupler._coupleSegments(this._segments.concat(snap.key));
|
114 | this._keys.push(snap.key);
|
115 | this._keys.sort();
|
116 | updatedKeys = this._keys;
|
117 | }
|
118 | } else if (hasKey) {
|
119 | this._coupler._decoupleSegments(this._segments.concat(snap.key));
|
120 | _.pull(this._keys, snap.key);
|
121 | this._keys.sort();
|
122 | updatedKeys = this._keys;
|
123 | }
|
124 |
|
125 |
|
126 | this._coupler._applySnapshot(snap);
|
127 | }
|
128 | return updatedKeys;
|
129 | }
|
130 |
|
131 | _handleError(error) {
|
132 | if (!this._listeners.length || !this._listening) return;
|
133 | this._listening = false;
|
134 | this.ready = false;
|
135 | angular.digest();
|
136 | Promise.all(_.map(this._listeners, listener => {
|
137 | this._coupler._dispatcher.clearReady(listener.operation);
|
138 | return this._coupler._dispatcher.retry(listener.operation, error).catch(e => {
|
139 | listener.operation._disconnect(e);
|
140 | return false;
|
141 | });
|
142 | })).then(results => {
|
143 | if (_.some(results)) {
|
144 | if (this._listeners.length) this._listen();
|
145 | } else {
|
146 | for (const listener of this._listeners) listener.operation._disconnect(error);
|
147 | }
|
148 | });
|
149 | }
|
150 | }
|
151 |
|
152 |
|
153 | class Node {
|
154 | constructor(coupler, path, parent) {
|
155 | this._coupler = coupler;
|
156 | this.path = path;
|
157 | this.parent = parent;
|
158 | this.url = this._coupler._rootUrl + path;
|
159 | this.operations = [];
|
160 | this.queryCount = 0;
|
161 | this.listening = false;
|
162 | this.ready = false;
|
163 | this.children = {};
|
164 | }
|
165 |
|
166 | get active() {
|
167 | return this.count || this.queryCount;
|
168 | }
|
169 |
|
170 | get count() {
|
171 | return this.operations.length;
|
172 | }
|
173 |
|
174 | listen(skip) {
|
175 | if (!skip && this.count) {
|
176 | if (this.listening) return;
|
177 | _.forEach(this.operations, op => {this._coupler._dispatcher.clearReady(op);});
|
178 | this._coupler._bridge.on(
|
179 | this.url, this.url, null, 'value', this._handleSnapshot, this._handleError, this,
|
180 | {sync: true});
|
181 | this.listening = true;
|
182 | } else {
|
183 | _.forEach(this.children, child => {child.listen();});
|
184 | }
|
185 | }
|
186 |
|
187 | unlisten(skip) {
|
188 | if (!skip && this.listening) {
|
189 | this._coupler._bridge.off(this.url, this.url, null, 'value', this._handleSnapshot, this);
|
190 | this.listening = false;
|
191 | this._forAllDescendants(node => {
|
192 | if (node.listening) return false;
|
193 | if (node.ready) {
|
194 | node.ready = false;
|
195 | angular.digest();
|
196 | }
|
197 | });
|
198 | } else {
|
199 | _.forEach(this.children, child => {child.unlisten();});
|
200 | }
|
201 | }
|
202 |
|
203 | _handleSnapshot(snap) {
|
204 | this._coupler._queueSnapshotCallback(() => {
|
205 | if (!this.listening || !this._coupler.isTrunkCoupled(snap.path)) return;
|
206 | this._coupler._applySnapshot(snap);
|
207 | if (!this.ready && snap.path === this.path) {
|
208 | this.ready = true;
|
209 | angular.digest();
|
210 | this.unlisten(true);
|
211 | this._forAllDescendants(node => {
|
212 | for (const op of node.operations) this._coupler._dispatcher.markReady(op);
|
213 | });
|
214 | }
|
215 | });
|
216 | }
|
217 |
|
218 | _handleError(error) {
|
219 | if (!this.count || !this.listening) return;
|
220 | this.listening = false;
|
221 | this._forAllDescendants(node => {
|
222 | if (node.listening) return false;
|
223 | if (node.ready) {
|
224 | node.ready = false;
|
225 | angular.digest();
|
226 | }
|
227 | for (const op of node.operations) this._coupler._dispatcher.clearReady(op);
|
228 | });
|
229 | return Promise.all(_.map(this.operations, op => {
|
230 | return this._coupler._dispatcher.retry(op, error).catch(e => {
|
231 | op._disconnect(e);
|
232 | return false;
|
233 | });
|
234 | })).then(results => {
|
235 | if (_.some(results)) {
|
236 | if (this.count) this.listen();
|
237 | } else {
|
238 | for (const op of this.operations) op._disconnect(error);
|
239 |
|
240 | }
|
241 | });
|
242 | }
|
243 |
|
244 | _forAllDescendants(iteratee) {
|
245 | if (iteratee(this) === false) return;
|
246 | _.forEach(this.children, child => child._forAllDescendants(iteratee));
|
247 | }
|
248 |
|
249 | collectCoupledDescendantPaths(paths) {
|
250 | if (!paths) paths = {};
|
251 | paths[this.path] = this.active;
|
252 | if (!this.active) {
|
253 | _.forEach(this.children, child => {child.collectCoupledDescendantPaths(paths);});
|
254 | }
|
255 | return paths;
|
256 | }
|
257 | }
|
258 |
|
259 |
|
260 | export default class Coupler {
|
261 | constructor(rootUrl, bridge, dispatcher, applySnapshot, prunePath) {
|
262 | this._rootUrl = rootUrl;
|
263 | this._bridge = bridge;
|
264 | this._dispatcher = dispatcher;
|
265 | this._applySnapshot = applySnapshot;
|
266 | this._pendingSnapshotCallbacks = [];
|
267 | this._throttled = {processPendingSnapshots: this._processPendingSnapshots};
|
268 | this._prunePath = prunePath;
|
269 | this._vue = new Vue({data: {root: undefined, queryHandlers: {}}});
|
270 |
|
271 |
|
272 | this._vue._renderProxy = this._vue;
|
273 | this._nodeIndex = Object.create(null);
|
274 | Object.freeze(this);
|
275 |
|
276 | this._vue.$data.root = new Node(this, '/');
|
277 | this._nodeIndex['/'] = this._root;
|
278 | }
|
279 |
|
280 | get _root() {
|
281 | return this._vue.$data.root;
|
282 | }
|
283 |
|
284 | get _queryHandlers() {
|
285 | return this._vue.$data.queryHandlers;
|
286 | }
|
287 |
|
288 | destroy() {
|
289 | _.forEach(this._queryHandlers, queryHandler => {queryHandler.destroy();});
|
290 | this._root.unlisten();
|
291 | this._vue.$destroy();
|
292 | }
|
293 |
|
294 | couple(path, operation) {
|
295 | return this._coupleSegments(splitPath(path, true), operation);
|
296 | }
|
297 |
|
298 | _coupleSegments(segments, operation) {
|
299 | let node;
|
300 | let superseded = !operation;
|
301 | let ready = false;
|
302 | for (const segment of segments) {
|
303 | let child = segment ? node.children && node.children[segment] : this._root;
|
304 | if (!child) {
|
305 | child = new Node(this, `${node.path === '/' ? '' : node.path}/${segment}`, node);
|
306 | Vue.set(node.children, segment, child);
|
307 | this._nodeIndex[child.path] = child;
|
308 | }
|
309 | superseded = superseded || child.listening;
|
310 | ready = ready || child.ready;
|
311 | node = child;
|
312 | }
|
313 | if (operation) {
|
314 | node.operations.push(operation);
|
315 | } else {
|
316 | node.queryCount++;
|
317 | }
|
318 | if (superseded) {
|
319 | if (operation && ready) this._dispatcher.markReady(operation);
|
320 | } else {
|
321 | node.listen();
|
322 | }
|
323 | }
|
324 |
|
325 | decouple(path, operation) {
|
326 | return this._decoupleSegments(splitPath(path, true), operation);
|
327 | }
|
328 |
|
329 | _decoupleSegments(segments, operation) {
|
330 | const ancestors = [];
|
331 | let node;
|
332 | for (const segment of segments) {
|
333 | node = segment ? node.children && node.children[segment] : this._root;
|
334 | if (!node) break;
|
335 | ancestors.push(node);
|
336 | }
|
337 | if (!node || !(operation ? node.count : node.queryCount)) {
|
338 | throw new Error(`Path not coupled: ${segments.join('/') || '/'}`);
|
339 | }
|
340 | if (operation) {
|
341 | _.pull(node.operations, operation);
|
342 | } else {
|
343 | node.queryCount--;
|
344 | }
|
345 | if (operation && !node.count) {
|
346 |
|
347 |
|
348 |
|
349 |
|
350 |
|
351 | node.listen();
|
352 | if (node.listening) node.unlisten();
|
353 | }
|
354 | if (!node.active) {
|
355 | for (let i = ancestors.length - 1; i > 0; i--) {
|
356 | node = ancestors[i];
|
357 | if (node === this._root || node.active || !_.isEmpty(node.children)) break;
|
358 | Vue.delete(ancestors[i - 1].children, segments[i]);
|
359 | node.ready = undefined;
|
360 | delete this._nodeIndex[node.path];
|
361 | }
|
362 | const path = segments.join('/') || '/';
|
363 | this._prunePath(path, this.findCoupledDescendantPaths(path));
|
364 | }
|
365 | }
|
366 |
|
367 | subscribe(query, operation, keysCallback) {
|
368 | let queryHandler = this._queryHandlers[query.toString()];
|
369 | if (!queryHandler) {
|
370 | queryHandler = new QueryHandler(this, query);
|
371 | Vue.set(this._queryHandlers, query.toString(), queryHandler);
|
372 | }
|
373 | queryHandler.attach(operation, keysCallback);
|
374 | }
|
375 |
|
376 | unsubscribe(query, operation) {
|
377 | const queryHandler = this._queryHandlers[query.toString()];
|
378 | if (queryHandler && !queryHandler.detach(operation)) {
|
379 | queryHandler.destroy();
|
380 | Vue.delete(this._queryHandlers, query.toString());
|
381 | }
|
382 | }
|
383 |
|
384 |
|
385 | isTrunkCoupled(path) {
|
386 | const segments = splitPath(path, true);
|
387 | let node;
|
388 | for (const segment of segments) {
|
389 | node = segment ? node.children && node.children[segment] : this._root;
|
390 | if (!node) return false;
|
391 | if (node.active) return true;
|
392 | }
|
393 | return false;
|
394 | }
|
395 |
|
396 | findCoupledDescendantPaths(path) {
|
397 | let node;
|
398 | for (const segment of splitPath(path, true)) {
|
399 | node = segment ? node.children && node.children[segment] : this._root;
|
400 | if (node && node.active) return {[path]: node.active};
|
401 | if (!node) break;
|
402 | }
|
403 | return node && node.collectCoupledDescendantPaths();
|
404 | }
|
405 |
|
406 | isSubtreeReady(path) {
|
407 | let node, childSegment;
|
408 | function extractChildSegment(match) {
|
409 | childSegment = match.slice(1);
|
410 | return '';
|
411 | }
|
412 | while (!(node = this._nodeIndex[path])) {
|
413 | path = path.replace(/\/[^/]*$/, extractChildSegment) || '/';
|
414 | }
|
415 | if (childSegment) void node.children;
|
416 | while (node) {
|
417 | if (node.ready) return true;
|
418 | node = node.parent;
|
419 | }
|
420 | return false;
|
421 | }
|
422 |
|
423 | isQueryReady(query) {
|
424 | const queryHandler = this._queryHandlers[query.toString()];
|
425 | return queryHandler && queryHandler.ready;
|
426 | }
|
427 |
|
428 | _queueSnapshotCallback(callback) {
|
429 | this._pendingSnapshotCallbacks.push(callback);
|
430 | this._throttled.processPendingSnapshots.call(this);
|
431 | }
|
432 |
|
433 | _processPendingSnapshots() {
|
434 | for (const callback of this._pendingSnapshotCallbacks) callback();
|
435 |
|
436 | this._pendingSnapshotCallbacks.splice(0, Infinity);
|
437 | }
|
438 |
|
439 | throttleSnapshots(delay) {
|
440 | if (delay) {
|
441 | this._throttled.processPendingSnapshots =
|
442 | _.debounce(_.throttle(this._processPendingSnapshots, delay));
|
443 | } else {
|
444 | this._throttled.processPendingSnapshots = this._processPendingSnapshots;
|
445 | }
|
446 | }
|
447 | }
|
448 |
|