UNPKG

11 kBJavaScriptView Raw
1import _ from 'lodash';
2import Vue from 'vue';
3import angular from './angularCompatibility.js';
4import Bridge from './Bridge.js';
5import Connector from './Connector.js';
6import Dispatcher from './Dispatcher.js';
7import KeyGenerator from './KeyGenerator.js';
8import MetaTree from './MetaTree.js';
9import {Handle} from './Reference.js';
10import {BaseValue} from './Modeler.js';
11import Tree from './Tree.js';
12import stats from './utils/stats.js';
13import {escapeKey, unescapeKey} from './utils/paths.js';
14import {wrapPromiseCallback, promiseCancel, promiseFinally} from './utils/promises.js';
15import {SERVER_TIMESTAMP, copyPrototype} from './utils/utils.js';
16
17
18let bridge, logging;
19const workerFunctions = {};
20// This version is filled in by the build, don't reformat the line.
21const VERSION = '3.0.6';
22
23
24export default class Truss {
25
26 /**
27 * Create a new Truss instance, specific to a given datastore. To avoid confusion there should be
28 * exactly one Truss per root datastore URL, so in most code this will be a singleton.
29 *
30 * @param rootUrl {String} The root URL, https://{project}.firebaseio.com.
31 */
32 constructor(rootUrl) {
33 // TODO: allow rootUrl to be a test database object for testing
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 * Mount a set of classes against the datastore structure. Must be called at most once, and
54 * cannot be called once any data has been loaded into the tree.
55 * @param classes {Array<Function> | Object<Function>} A list of the classes to map onto the
56 * datastore structure. Each class must have a static $trussMount property that is a
57 * (wildcarded) unescaped datastore path, or an options object
58 * {path: string, placeholder: object}, or an array of either. If the list is an object then
59 * the keys serve as default option-less $trussMount paths for classes that don't define an
60 * explicit $trussMount.
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 // connections are {key: Query | Object | fn() -> (Query | Object)}
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 // target is Reference, Query, or connection Object like above
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) oldValueClone = _.clone(options.precise, options.deep);
148
149 const unwatch = this._vue.$watch(subjectFn, (newValue, oldValue) => {
150 if (options && options.precise) {
151 const newValueClone = usePreciseDefaults ?
152 (options.deep ?
153 _.defaultsDeep({}, newValue, options.precise) :
154 _.defaults({}, newValue, options.precise)) :
155 _.clone(newValue, options.deep);
156 if (_.isEqual(newValueClone, oldValueClone)) return;
157 oldValueClone = newValueClone;
158 }
159 numCallbacks++;
160 if (!unwatch) {
161 // Delay the immediate callback until we've had a chance to return the unwatch function.
162 Promise.resolve().then(() => {
163 if (numCallbacks > 1) return;
164 callbackFn(newValue, oldValue);
165 // No need to digest since under Angular we'll be using $q as Promise.
166 });
167 } else {
168 callbackFn(newValue, oldValue);
169 angular.digest();
170 }
171 }, {immediate: true, deep: options && options.deep});
172
173 if (options && options.scope) options.scope.$on('$destroy', unwatch);
174 return unwatch;
175 }
176
177 when(expression, options) {
178 let cleanup, timeoutHandle;
179 let promise = new Promise((resolve, reject) => {
180 let unobserve = this.observe(expression, value => {
181 if (!value) return;
182 // Wait for computed properties to settle and double-check.
183 Vue.nextTick(() => {
184 value = expression();
185 if (!value) return;
186 resolve(value);
187 cleanup();
188 });
189 });
190 if (_.has(options, 'timeout')) {
191 timeoutHandle = setTimeout(() => {
192 timeoutHandle = null;
193 reject(new Error(options.timeoutMessage || 'Timeout'));
194 cleanup();
195 }, options.timeout);
196 }
197 cleanup = () => {
198 if (unobserve) {unobserve(); unobserve = null;}
199 if (timeoutHandle) {clearTimeout(timeoutHandle); timeoutHandle = null;}
200 reject(new Error('Canceled'));
201 };
202 });
203 promise = promiseCancel(promiseFinally(promise, cleanup), cleanup);
204 if (options && options.scope) options.scope.$on('$destroy', () => {promise.cancel();});
205 return promise;
206 }
207
208 nextTick() {
209 let cleanup;
210 let promise = new Promise((resolve, reject) => {
211 Vue.nextTick(resolve);
212 cleanup = () => {
213 reject(new Error('Canceled'));
214 };
215 });
216 promise = promiseCancel(promise, cleanup);
217 return promise;
218 }
219
220 throttleRemoteDataUpdates(delay) {
221 this._tree.throttleRemoteDataUpdates(delay);
222 }
223
224 checkObjectsForRogueProperties() {
225 this._tree.checkVueObject(this._tree.root, '/');
226 }
227
228 static get computedPropertyStats() {
229 return stats;
230 }
231
232 static connectWorker(webWorker, config) {
233 if (bridge) throw new Error('Worker already connected');
234 if (_.isString(webWorker)) {
235 const Worker = window.SharedWorker || window.Worker;
236 if (!Worker) throw new Error('Browser does not implement Web Workers');
237 webWorker = new Worker(webWorker);
238 }
239 bridge = new Bridge(webWorker);
240 if (logging) bridge.enableLogging(logging);
241 return bridge.init(webWorker, config).then(
242 ({exposedFunctionNames, firebaseSdkVersion}) => {
243 Object.defineProperty(Truss, 'FIREBASE_SDK_VERSION', {value: firebaseSdkVersion});
244 for (const name of exposedFunctionNames) {
245 Truss.worker[name] = bridge.bindExposedFunction(name);
246 }
247 }
248 );
249 }
250
251 static get worker() {return workerFunctions;}
252 static preExpose(functionName) {
253 Truss.worker[functionName] = bridge.bindExposedFunction(functionName);
254 }
255
256 static bounceConnection() {return bridge.bounceConnection();}
257 static suspend() {return bridge.suspend();}
258 static debugPermissionDeniedErrors(simulatedTokenGenerator, maxSimulationDuration, callFilter) {
259 return bridge.debugPermissionDeniedErrors(
260 simulatedTokenGenerator, maxSimulationDuration, callFilter);
261 }
262
263 static debounceAngularDigest(wait) {
264 angular.debounceDigest(wait);
265 }
266
267 static escapeKey(key) {return escapeKey(key);}
268 static unescapeKey(escapedKey) {return unescapeKey(escapedKey);}
269
270 static enableLogging(fn) {
271 logging = fn;
272 if (bridge) bridge.enableLogging(fn);
273 }
274
275 // Duplicate static constants on instance for convenience.
276 get SERVER_TIMESTAMP() {return Truss.SERVER_TIMESTAMP;}
277 get VERSION() {return Truss.VERSION;}
278 get FIREBASE_SDK_VERSION() {return Truss.FIREBASE_SDK_VERSION;}
279}
280
281Object.defineProperties(Truss, {
282 SERVER_TIMESTAMP: {value: SERVER_TIMESTAMP},
283 VERSION: {value: VERSION},
284
285 ComponentPlugin: {value: {
286 install(Vue2, pluginOptions) {
287 if (Vue !== Vue2) throw new Error('Multiple versions of Vue detected');
288 if (!pluginOptions.truss) {
289 throw new Error('Need to pass `truss` instance as an option to use the ComponentPlugin');
290 }
291 const prototypeExtension = {
292 $truss: {value: pluginOptions.truss},
293 $destroyed: {get() {return this._isBeingDestroyed || this._isDestroyed;}},
294 $$touchThis: {value() {if (this.__ob__) this.__ob__.dep.depend();}}
295 };
296 const conflictingKeys = _(prototypeExtension).keys()
297 .union(_.keys(BaseValue.prototype)).intersection(_.keys(Vue.prototype)).value();
298 if (conflictingKeys.length) {
299 throw new Error(
300 'Truss extension properties conflict with Vue properties: ' + conflictingKeys.join(', '));
301 }
302 Object.defineProperties(Vue.prototype, prototypeExtension);
303 copyPrototype(BaseValue, Vue);
304 Vue.mixin({
305 destroyed() {
306 if (_.has(this, '$$finalizers')) {
307 // Some finalizers remove themselves from the array, so clone it before iterating.
308 for (const fn of _.clone(this.$$finalizers)) fn();
309 }
310 }
311 });
312 }
313 }}
314});
315
316angular.defineModule(Truss);
317