1 | import _ from 'lodash';
|
2 | import Vue from 'vue';
|
3 | import angular from './angularCompatibility.js';
|
4 | import Bridge from './Bridge.js';
|
5 | import Connector from './Connector.js';
|
6 | import Dispatcher from './Dispatcher.js';
|
7 | import KeyGenerator from './KeyGenerator.js';
|
8 | import MetaTree from './MetaTree.js';
|
9 | import {Handle} from './Reference.js';
|
10 | import {BaseValue} from './Modeler.js';
|
11 | import Tree from './Tree.js';
|
12 | import stats from './utils/stats.js';
|
13 | import {escapeKey, unescapeKey} from './utils/paths.js';
|
14 | import {wrapPromiseCallback, promiseCancel, promiseFinally} from './utils/promises.js';
|
15 | import {SERVER_TIMESTAMP, copyPrototype} from './utils/utils.js';
|
16 |
|
17 |
|
18 | let bridge, logging;
|
19 | const workerFunctions = {};
|
20 |
|
21 | const VERSION = '4.2.2';
|
22 |
|
23 |
|
24 | export default class Truss {
|
25 |
|
26 | |
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 | constructor(rootUrl) {
|
33 |
|
34 | if (!bridge) {
|
35 | throw new Error('Truss worker not connected, please call Truss.connectWorker first');
|
36 | }
|
37 | this._rootUrl = rootUrl.replace(/\/$/, '');
|
38 | this._keyGenerator = new KeyGenerator();
|
39 | this._dispatcher = new Dispatcher(bridge);
|
40 | this._vue = new Vue();
|
41 |
|
42 | bridge.trackServer(this._rootUrl);
|
43 | this._tree = new Tree(this, this._rootUrl, bridge, this._dispatcher);
|
44 | this._metaTree = new MetaTree(this._rootUrl, this._tree, bridge, this._dispatcher);
|
45 |
|
46 | Object.freeze(this);
|
47 | }
|
48 |
|
49 | get meta() {return this._metaTree.root;}
|
50 | get store() {return this._tree.root;}
|
51 |
|
52 | |
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 | mount(classes) {
|
63 | this._tree.init(classes);
|
64 | }
|
65 |
|
66 | destroy() {
|
67 | this._vue.$destroy();
|
68 | this._tree.destroy();
|
69 | this._metaTree.destroy();
|
70 | }
|
71 |
|
72 | get now() {return Date.now() + this.meta.timeOffset;}
|
73 | newKey() {return this._keyGenerator.generateUniqueKey(this.now);}
|
74 |
|
75 | authenticate(token) {
|
76 | return this._metaTree.authenticate(token);
|
77 | }
|
78 |
|
79 | unauthenticate() {
|
80 | return this._metaTree.unauthenticate();
|
81 | }
|
82 |
|
83 | intercept(actionType, callbacks) {
|
84 | return this._dispatcher.intercept(actionType, callbacks);
|
85 | }
|
86 |
|
87 |
|
88 | connect(scope, connections) {
|
89 | if (!connections) {
|
90 | connections = scope;
|
91 | scope = undefined;
|
92 | }
|
93 | if (connections instanceof Handle) connections = {_: connections};
|
94 | return new Connector(scope, connections, this._tree, 'connect');
|
95 | }
|
96 |
|
97 |
|
98 | peek(target, callback) {
|
99 | callback = wrapPromiseCallback(callback || _.identity);
|
100 | let cleanup, cancel;
|
101 | const promise = Promise.resolve().then(() => new Promise((resolve, reject) => {
|
102 | const scope = {};
|
103 | let callbackPromise;
|
104 |
|
105 | let connector = new Connector(scope, {result: target}, this._tree, 'peek');
|
106 |
|
107 | let unintercept = this.intercept('peek', {onFailure: op => {
|
108 | function match(descriptor) {
|
109 | if (!descriptor) return;
|
110 | if (descriptor instanceof Handle) return op.target.isEqual(descriptor);
|
111 | return _.some(descriptor, value => match(value));
|
112 | }
|
113 | if (match(connector.at)) {
|
114 | reject(op.error);
|
115 | cleanup();
|
116 | }
|
117 | }});
|
118 |
|
119 | let unobserve = this.observe(() => connector.ready, ready => {
|
120 | if (!ready) return;
|
121 | unobserve();
|
122 | unobserve = null;
|
123 | callbackPromise = promiseFinally(
|
124 | callback(scope.result), () => {angular.digest(); callbackPromise = null; cleanup();}
|
125 | ).then(result => {resolve(result);}, error => {reject(error);});
|
126 | });
|
127 |
|
128 | cleanup = () => {
|
129 | if (unobserve) {unobserve(); unobserve = null;}
|
130 | if (unintercept) {unintercept(); unintercept = null;}
|
131 | if (connector) {connector.destroy(); connector = null;}
|
132 | if (callbackPromise && callbackPromise.cancel) callbackPromise.cancel();
|
133 | };
|
134 |
|
135 | cancel = () => {
|
136 | reject(new Error('Canceled'));
|
137 | cleanup();
|
138 | };
|
139 | }));
|
140 | return promiseCancel(promise, cancel);
|
141 | }
|
142 |
|
143 | observe(subjectFn, callbackFn, options) {
|
144 | const usePreciseDefaults = _.isObject(options && options.precise);
|
145 | let numCallbacks = 0;
|
146 | let oldValueClone;
|
147 | if (usePreciseDefaults) {
|
148 | oldValueClone = options.deep ? _.cloneDeep(options.precise) : _.clone(options.precise);
|
149 | }
|
150 |
|
151 | const unwatch = this._vue.$watch(subjectFn, (newValue, oldValue) => {
|
152 | if (options && options.precise) {
|
153 | const newValueClone = usePreciseDefaults ?
|
154 | (options.deep ?
|
155 | _.defaultsDeep({}, newValue, options.precise) :
|
156 | _.defaults({}, newValue, options.precise)) :
|
157 | (options.deep ? _.cloneDeep(newValue) : _.clone(newValue));
|
158 | if (_.isEqual(newValueClone, oldValueClone)) return;
|
159 | oldValueClone = newValueClone;
|
160 | }
|
161 | numCallbacks++;
|
162 | if (!unwatch) {
|
163 |
|
164 | Promise.resolve().then(() => {
|
165 | if (numCallbacks > 1) return;
|
166 | callbackFn(newValue, oldValue);
|
167 |
|
168 | });
|
169 | } else {
|
170 | callbackFn(newValue, oldValue);
|
171 | angular.digest();
|
172 | }
|
173 | }, {immediate: true, deep: options && options.deep});
|
174 |
|
175 | if (options && options.scope) options.scope.$on('$destroy', unwatch);
|
176 | return unwatch;
|
177 | }
|
178 |
|
179 | when(expression, options) {
|
180 | let cleanup, timeoutHandle;
|
181 | let promise = new Promise((resolve, reject) => {
|
182 | let unobserve = this.observe(expression, value => {
|
183 | if (!value) return;
|
184 |
|
185 | Vue.nextTick(() => {
|
186 | value = expression();
|
187 | if (!value) return;
|
188 | resolve(value);
|
189 | cleanup();
|
190 | });
|
191 | });
|
192 | if (_.has(options, 'timeout')) {
|
193 | timeoutHandle = setTimeout(() => {
|
194 | timeoutHandle = null;
|
195 | reject(new Error(options.timeoutMessage || 'Timeout'));
|
196 | cleanup();
|
197 | }, options.timeout);
|
198 | }
|
199 | cleanup = () => {
|
200 | if (unobserve) {unobserve(); unobserve = null;}
|
201 | if (timeoutHandle) {clearTimeout(timeoutHandle); timeoutHandle = null;}
|
202 | reject(new Error('Canceled'));
|
203 | };
|
204 | });
|
205 | promise = promiseCancel(promiseFinally(promise, cleanup), cleanup);
|
206 | if (options && options.scope) options.scope.$on('$destroy', () => {promise.cancel();});
|
207 | return promise;
|
208 | }
|
209 |
|
210 | nextTick() {
|
211 | let cleanup;
|
212 | let promise = new Promise((resolve, reject) => {
|
213 | Vue.nextTick(resolve);
|
214 | cleanup = () => {
|
215 | reject(new Error('Canceled'));
|
216 | };
|
217 | });
|
218 | promise = promiseCancel(promise, cleanup);
|
219 | return promise;
|
220 | }
|
221 |
|
222 | throttleRemoteDataUpdates(delay) {
|
223 | this._tree.throttleRemoteDataUpdates(delay);
|
224 | }
|
225 |
|
226 | checkObjectsForRogueProperties() {
|
227 | this._tree.checkVueObject(this._tree.root, '/');
|
228 | }
|
229 |
|
230 | static get computedPropertyStats() {
|
231 | return stats;
|
232 | }
|
233 |
|
234 | static connectWorker(webWorker, config) {
|
235 | if (bridge) throw new Error('Worker already connected');
|
236 | if (_.isString(webWorker)) {
|
237 | const Worker = window.SharedWorker || window.Worker;
|
238 | if (!Worker) throw new Error('Browser does not implement Web Workers');
|
239 | webWorker = new Worker(webWorker);
|
240 | }
|
241 | bridge = new Bridge(webWorker);
|
242 | if (logging) bridge.enableLogging(logging);
|
243 | return bridge.init(webWorker, config).then(
|
244 | ({exposedFunctionNames, firebaseSdkVersion}) => {
|
245 | Object.defineProperty(Truss, 'FIREBASE_SDK_VERSION', {value: firebaseSdkVersion});
|
246 | for (const name of exposedFunctionNames) {
|
247 | Truss.worker[name] = bridge.bindExposedFunction(name);
|
248 | }
|
249 | }
|
250 | );
|
251 | }
|
252 |
|
253 | static get worker() {return workerFunctions;}
|
254 | static preExpose(functionName) {
|
255 | Truss.worker[functionName] = bridge.bindExposedFunction(functionName);
|
256 | }
|
257 |
|
258 | static bounceConnection() {return bridge.bounceConnection();}
|
259 | static suspend() {return bridge.suspend();}
|
260 | static debugPermissionDeniedErrors(simulatedTokenGenerator, maxSimulationDuration, callFilter) {
|
261 | return bridge.debugPermissionDeniedErrors(
|
262 | simulatedTokenGenerator, maxSimulationDuration, callFilter);
|
263 | }
|
264 |
|
265 | static debounceAngularDigest(wait) {
|
266 | angular.debounceDigest(wait);
|
267 | }
|
268 |
|
269 | static escapeKey(key) {return escapeKey(key);}
|
270 | static unescapeKey(escapedKey) {return unescapeKey(escapedKey);}
|
271 |
|
272 | static enableLogging(fn) {
|
273 | logging = fn;
|
274 | if (bridge) bridge.enableLogging(fn);
|
275 | }
|
276 |
|
277 |
|
278 | get SERVER_TIMESTAMP() {return Truss.SERVER_TIMESTAMP;}
|
279 | get VERSION() {return Truss.VERSION;}
|
280 | get FIREBASE_SDK_VERSION() {return Truss.FIREBASE_SDK_VERSION;}
|
281 | }
|
282 |
|
283 | Object.defineProperties(Truss, {
|
284 | SERVER_TIMESTAMP: {value: SERVER_TIMESTAMP},
|
285 | VERSION: {value: VERSION},
|
286 |
|
287 | ComponentPlugin: {value: {
|
288 | install(Vue2, pluginOptions) {
|
289 | if (Vue !== Vue2) throw new Error('Multiple versions of Vue detected');
|
290 | if (!pluginOptions.truss) {
|
291 | throw new Error('Need to pass `truss` instance as an option to use the ComponentPlugin');
|
292 | }
|
293 | const prototypeExtension = {
|
294 | $truss: {value: pluginOptions.truss},
|
295 | $destroyed: {get() {return this._isBeingDestroyed || this._isDestroyed;}},
|
296 | $$touchThis: {value() {if (this.__ob__) this.__ob__.dep.depend();}}
|
297 | };
|
298 | const conflictingKeys = _(prototypeExtension).keys()
|
299 | .union(_.keys(BaseValue.prototype)).intersection(_.keys(Vue.prototype)).value();
|
300 | if (conflictingKeys.length) {
|
301 | throw new Error(
|
302 | 'Truss extension properties conflict with Vue properties: ' + conflictingKeys.join(', '));
|
303 | }
|
304 | Object.defineProperties(Vue.prototype, prototypeExtension);
|
305 | copyPrototype(BaseValue, Vue);
|
306 | Vue.mixin({
|
307 | destroyed() {
|
308 | if (_.has(this, '$$finalizers')) {
|
309 |
|
310 | for (const fn of _.clone(this.$$finalizers)) fn();
|
311 | }
|
312 | }
|
313 | });
|
314 | }
|
315 | }}
|
316 | });
|
317 |
|
318 | angular.defineModule(Truss);
|
319 |
|