UNPKG

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