diff --git a/src/database/api/DataSnapshot.ts b/src/database/api/DataSnapshot.ts index 3c902589d33..0a141a90889 100644 --- a/src/database/api/DataSnapshot.ts +++ b/src/database/api/DataSnapshot.ts @@ -1,187 +1,177 @@ -import { validateArgCount, validateCallback } from "../../utils/validation"; -import { validatePathString } from "../core/util/validation"; -import { Path } from "../core/util/Path"; -import { exportPropGetter } from "../core/util/util"; -import { PRIORITY_INDEX } from "../core/snap/indexes/PriorityIndex"; +import { validateArgCount, validateCallback } from '../../utils/validation'; +import { validatePathString } from '../core/util/validation'; +import { Path } from '../core/util/Path'; +import { PRIORITY_INDEX } from '../core/snap/indexes/PriorityIndex'; +import { Node } from '../core/snap/Node'; +import { Reference } from './Reference'; +import { Index } from '../core/snap/indexes/Index'; +import { ChildrenNode } from '../core/snap/ChildrenNode'; /** * Class representing a firebase data snapshot. It wraps a SnapshotNode and * surfaces the public methods (val, forEach, etc.) we want to expose. - * - * @constructor - * @param {!fb.core.snap.Node} node A SnapshotNode to wrap. - * @param {!Firebase} ref The ref of the location this snapshot came from. - * @param {!fb.core.snap.Index} index The iteration order for this snapshot */ -export const DataSnapshot = function(node, ref, index) { +export class DataSnapshot { /** - * @private - * @const - * @type {!fb.core.snap.Node} + * @param {!Node} node_ A SnapshotNode to wrap. + * @param {!Reference} ref_ The ref of the location this snapshot came from. + * @param {!Index} index_ The iteration order for this snapshot */ - this.node_ = node; + constructor(private readonly node_: Node, + private readonly ref_: Reference, + private readonly index_: Index) { + } /** - * @private - * @type {!Firebase} - * @const + * Retrieves the snapshot contents as JSON. Returns null if the snapshot is + * empty. + * + * @return {*} JSON representation of the DataSnapshot contents, or null if empty. */ - this.query_ = ref; + val(): any { + validateArgCount('DataSnapshot.val', 0, 0, arguments.length); + return this.node_.val(); + } /** - * @const - * @type {!fb.core.snap.Index} - * @private + * Returns the snapshot contents as JSON, including priorities of node. Suitable for exporting + * the entire node contents. + * @return {*} JSON representation of the DataSnapshot contents, or null if empty. */ - this.index_ = index; -}; + exportVal(): any { + validateArgCount('DataSnapshot.exportVal', 0, 0, arguments.length); + return this.node_.val(true); + } + + // Do not create public documentation. This is intended to make JSON serialization work but is otherwise unnecessary + // for end-users + toJSON(): any { + // Optional spacer argument is unnecessary because we're depending on recursion rather than stringifying the content + validateArgCount('DataSnapshot.toJSON', 0, 1, arguments.length); + return this.exportVal(); + } + /** + * Returns whether the snapshot contains a non-null value. + * + * @return {boolean} Whether the snapshot contains a non-null value, or is empty. + */ + exists(): boolean { + validateArgCount('DataSnapshot.exists', 0, 0, arguments.length); + return !this.node_.isEmpty(); + } -/** - * Retrieves the snapshot contents as JSON. Returns null if the snapshot is - * empty. - * - * @return {*} JSON representation of the DataSnapshot contents, or null if empty. - */ -DataSnapshot.prototype.val = function() { - validateArgCount('Firebase.DataSnapshot.val', 0, 0, arguments.length); - return this.node_.val(); -}; - -/** - * Returns the snapshot contents as JSON, including priorities of node. Suitable for exporting - * the entire node contents. - * @return {*} JSON representation of the DataSnapshot contents, or null if empty. - */ -DataSnapshot.prototype.exportVal = function() { - validateArgCount('Firebase.DataSnapshot.exportVal', 0, 0, arguments.length); - return this.node_.val(true); -}; - -// Do not create public documentation. This is intended to make JSON serialization work but is otherwise unnecessary -// for end-users -DataSnapshot.prototype.toJSON = function() { - // Optional spacer argument is unnecessary because we're depending on recursion rather than stringifying the content - validateArgCount('Firebase.DataSnapshot.toJSON', 0, 1, arguments.length); - return this.exportVal(); -}; - -/** - * Returns whether the snapshot contains a non-null value. - * - * @return {boolean} Whether the snapshot contains a non-null value, or is empty. - */ -DataSnapshot.prototype.exists = function() { - validateArgCount('Firebase.DataSnapshot.exists', 0, 0, arguments.length); - return !this.node_.isEmpty(); -}; + /** + * Returns a DataSnapshot of the specified child node's contents. + * + * @param {!string} childPathString Path to a child. + * @return {!DataSnapshot} DataSnapshot for child node. + */ + child(childPathString: string): DataSnapshot { + validateArgCount('DataSnapshot.child', 0, 1, arguments.length); + // Ensure the childPath is a string (can be a number) + childPathString = String(childPathString); + validatePathString('DataSnapshot.child', 1, childPathString, false); -/** - * Returns a DataSnapshot of the specified child node's contents. - * - * @param {!string} childPathString Path to a child. - * @return {!DataSnapshot} DataSnapshot for child node. - */ -DataSnapshot.prototype.child = function(childPathString) { - validateArgCount('Firebase.DataSnapshot.child', 0, 1, arguments.length); - // Ensure the childPath is a string (can be a number) - childPathString = String(childPathString); - validatePathString('Firebase.DataSnapshot.child', 1, childPathString, false); + const childPath = new Path(childPathString); + const childRef = this.ref_.child(childPath); + return new DataSnapshot(this.node_.getChild(childPath), childRef, PRIORITY_INDEX); + } - var childPath = new Path(childPathString); - var childRef = this.query_.child(childPath); - return new DataSnapshot(this.node_.getChild(childPath), childRef, PRIORITY_INDEX); -}; + /** + * Returns whether the snapshot contains a child at the specified path. + * + * @param {!string} childPathString Path to a child. + * @return {boolean} Whether the child exists. + */ + hasChild(childPathString: string): boolean { + validateArgCount('DataSnapshot.hasChild', 1, 1, arguments.length); + validatePathString('DataSnapshot.hasChild', 1, childPathString, false); -/** - * Returns whether the snapshot contains a child at the specified path. - * - * @param {!string} childPathString Path to a child. - * @return {boolean} Whether the child exists. - */ -DataSnapshot.prototype.hasChild = function(childPathString) { - validateArgCount('Firebase.DataSnapshot.hasChild', 1, 1, arguments.length); - validatePathString('Firebase.DataSnapshot.hasChild', 1, childPathString, false); + const childPath = new Path(childPathString); + return !this.node_.getChild(childPath).isEmpty(); + } - var childPath = new Path(childPathString); - return !this.node_.getChild(childPath).isEmpty(); -}; + /** + * Returns the priority of the object, or null if no priority was set. + * + * @return {string|number|null} The priority. + */ + getPriority(): string | number | null { + validateArgCount('DataSnapshot.getPriority', 0, 0, arguments.length); -/** - * Returns the priority of the object, or null if no priority was set. - * - * @return {string|number|null} The priority. - */ -DataSnapshot.prototype.getPriority = function() { - validateArgCount('Firebase.DataSnapshot.getPriority', 0, 0, arguments.length); + // typecast here because we never return deferred values or internal priorities (MAX_PRIORITY) + return /**@type {string|number|null} */ (this.node_.getPriority().val()); + } - // typecast here because we never return deferred values or internal priorities (MAX_PRIORITY) - return /**@type {string|number|null} */ (this.node_.getPriority().val()); -}; + /** + * Iterates through child nodes and calls the specified action for each one. + * + * @param {function(!DataSnapshot)} action Callback function to be called + * for each child. + * @return {boolean} True if forEach was canceled by action returning true for + * one of the child nodes. + */ + forEach(action: (d: DataSnapshot) => any): boolean { + validateArgCount('DataSnapshot.forEach', 1, 1, arguments.length); + validateCallback('DataSnapshot.forEach', 1, action, false); -/** - * Iterates through child nodes and calls the specified action for each one. - * - * @param {function(!DataSnapshot)} action Callback function to be called - * for each child. - * @return {boolean} True if forEach was canceled by action returning true for - * one of the child nodes. - */ -DataSnapshot.prototype.forEach = function(action) { - validateArgCount('Firebase.DataSnapshot.forEach', 1, 1, arguments.length); - validateCallback('Firebase.DataSnapshot.forEach', 1, action, false); + if (this.node_.isLeafNode()) + return false; - if (this.node_.isLeafNode()) - return false; + const childrenNode = /**@type {ChildrenNode} */ (this.node_); + // Sanitize the return value to a boolean. ChildrenNode.forEachChild has a weird return type... + return !!childrenNode.forEachChild(this.index_, (key, node) => { + return action(new DataSnapshot(node, this.ref_.child(key), PRIORITY_INDEX)); + }); + } - var childrenNode = /** @type {!fb.core.snap.ChildrenNode} */ (this.node_); - var self = this; - // Sanitize the return value to a boolean. ChildrenNode.forEachChild has a weird return type... - return !!childrenNode.forEachChild(this.index_, function(key, node) { - return action(new DataSnapshot(node, self.query_.child(key), PRIORITY_INDEX)); - }); -}; + /** + * Returns whether this DataSnapshot has children. + * @return {boolean} True if the DataSnapshot contains 1 or more child nodes. + */ + hasChildren(): boolean { + validateArgCount('DataSnapshot.hasChildren', 0, 0, arguments.length); -/** - * Returns whether this DataSnapshot has children. - * @return {boolean} True if the DataSnapshot contains 1 or more child nodes. - */ -DataSnapshot.prototype.hasChildren = function() { - validateArgCount('Firebase.DataSnapshot.hasChildren', 0, 0, arguments.length); + if (this.node_.isLeafNode()) + return false; + else + return !this.node_.isEmpty(); + } - if (this.node_.isLeafNode()) - return false; - else - return !this.node_.isEmpty(); -}; + /** + * @return {?string} The key of the location this snapshot's data came from. + */ + getKey(): string | null { + validateArgCount('DataSnapshot.key', 0, 0, arguments.length); -/** - * @return {?string} The key of the location this snapshot's data came from. - */ -DataSnapshot.prototype.getKey = function() { - validateArgCount('Firebase.DataSnapshot.key', 0, 0, arguments.length); + return this.ref_.getKey(); + } - return this.query_.getKey(); -}; -exportPropGetter(DataSnapshot.prototype, 'key', DataSnapshot.prototype.getKey); + get key() { + return this.getKey(); + } + /** + * Returns the number of children for this DataSnapshot. + * @return {number} The number of children that this DataSnapshot contains. + */ + numChildren(): number { + validateArgCount('DataSnapshot.numChildren', 0, 0, arguments.length); -/** - * Returns the number of children for this DataSnapshot. - * @return {number} The number of children that this DataSnapshot contains. - */ -DataSnapshot.prototype.numChildren = function() { - validateArgCount('Firebase.DataSnapshot.numChildren', 0, 0, arguments.length); + return this.node_.numChildren(); + } - return this.node_.numChildren(); -}; + /** + * @return {Reference} The Firebase reference for the location this snapshot's data came from. + */ + getRef(): Reference { + validateArgCount('DataSnapshot.ref', 0, 0, arguments.length); -/** - * @return {Firebase} The Firebase reference for the location this snapshot's data came from. - */ -DataSnapshot.prototype.getRef = function() { - validateArgCount('Firebase.DataSnapshot.ref', 0, 0, arguments.length); + return this.ref_; + } - return this.query_; -}; -exportPropGetter(DataSnapshot.prototype, 'ref', DataSnapshot.prototype.getRef); + get ref() { + return this.getRef(); + } +} diff --git a/src/database/api/Query.ts b/src/database/api/Query.ts index 08569dacc05..aec0d6770e0 100644 --- a/src/database/api/Query.ts +++ b/src/database/api/Query.ts @@ -1,22 +1,24 @@ import { assert } from '../../utils/assert'; -import { KEY_INDEX } from "../core/snap/indexes/KeyIndex"; -import { PRIORITY_INDEX } from "../core/snap/indexes/PriorityIndex"; -import { VALUE_INDEX } from "../core/snap/indexes/ValueIndex"; -import { PathIndex } from "../core/snap/indexes/PathIndex"; -import { MIN_NAME,MAX_NAME,ObjectToUniqueKey } from "../core/util/util"; -import { Path } from "../core/util/Path"; -import { - isValidPriority, - validateEventType, +import { KEY_INDEX } from '../core/snap/indexes/KeyIndex'; +import { PRIORITY_INDEX } from '../core/snap/indexes/PriorityIndex'; +import { VALUE_INDEX } from '../core/snap/indexes/ValueIndex'; +import { PathIndex } from '../core/snap/indexes/PathIndex'; +import { MIN_NAME, MAX_NAME, ObjectToUniqueKey } from '../core/util/util'; +import { Path } from '../core/util/Path'; +import { + isValidPriority, + validateEventType, validatePathString, validateFirebaseDataArg, validateKey, -} from "../core/util/validation"; -import { errorPrefix, validateArgCount, validateCallback, validateContextObject } from "../../utils/validation"; -import { ValueEventRegistration, ChildEventRegistration } from "../core/view/EventRegistration"; -import { Deferred, attachDummyErrorHandler } from "../../utils/promise"; -import { Repo } from "../core/Repo"; -import { QueryParams } from "../core/view/QueryParams"; +} from '../core/util/validation'; +import { errorPrefix, validateArgCount, validateCallback, validateContextObject } from '../../utils/validation'; +import { ValueEventRegistration, ChildEventRegistration } from '../core/view/EventRegistration'; +import { Deferred, attachDummyErrorHandler } from '../../utils/promise'; +import { Repo } from '../core/Repo'; +import { QueryParams } from '../core/view/QueryParams'; +import { Reference } from './Reference'; +import { DataSnapshot } from './DataSnapshot'; let __referenceConstructor: new(repo: Repo, path: Path) => Query; @@ -30,10 +32,12 @@ export class Query { static set __referenceConstructor(val) { __referenceConstructor = val; } + static get __referenceConstructor() { assert(__referenceConstructor, 'Reference.ts has not been loaded'); return __referenceConstructor; } + constructor(public repo: Repo, public path: Path, private queryParams_: QueryParams, private orderByCalled_: boolean) {} /** @@ -41,9 +45,9 @@ export class Query { * @param {!QueryParams} params * @private */ - validateQueryEndpoints_(params) { - var startNode = null; - var endNode = null; + private static validateQueryEndpoints_(params: QueryParams) { + let startNode = null; + let endNode = null; if (params.hasStart()) { startNode = params.getIndexStartValue(); } @@ -52,12 +56,12 @@ export class Query { } if (params.getIndex() === KEY_INDEX) { - var tooManyArgsError = 'Query: When ordering by key, you may only pass one argument to ' + - 'startAt(), endAt(), or equalTo().'; - var wrongArgTypeError = 'Query: When ordering by key, the argument passed to startAt(), endAt(),' + - 'or equalTo() must be a string.'; + const tooManyArgsError = 'Query: When ordering by key, you may only pass one argument to ' + + 'startAt(), endAt(), or equalTo().'; + const wrongArgTypeError = 'Query: When ordering by key, the argument passed to startAt(), endAt(),' + + 'or equalTo() must be a string.'; if (params.hasStart()) { - var startName = params.getIndexStartName(); + const startName = params.getIndexStartName(); if (startName != MIN_NAME) { throw new Error(tooManyArgsError); } else if (typeof(startNode) !== 'string') { @@ -65,7 +69,7 @@ export class Query { } } if (params.hasEnd()) { - var endName = params.getIndexEndName(); + const endName = params.getIndexEndName(); if (endName != MAX_NAME) { throw new Error(tooManyArgsError); } else if (typeof(endNode) !== 'string') { @@ -75,17 +79,17 @@ export class Query { } else if (params.getIndex() === PRIORITY_INDEX) { if ((startNode != null && !isValidPriority(startNode)) || - (endNode != null && !isValidPriority(endNode))) { + (endNode != null && !isValidPriority(endNode))) { throw new Error('Query: When ordering by priority, the first argument passed to startAt(), ' + - 'endAt(), or equalTo() must be a valid priority value (null, a number, or a string).'); + 'endAt(), or equalTo() must be a valid priority value (null, a number, or a string).'); } } else { assert((params.getIndex() instanceof PathIndex) || - (params.getIndex() === VALUE_INDEX), 'unknown index type.'); + (params.getIndex() === VALUE_INDEX), 'unknown index type.'); if ((startNode != null && typeof startNode === 'object') || - (endNode != null && typeof endNode === 'object')) { + (endNode != null && typeof endNode === 'object')) { throw new Error('Query: First argument passed to startAt(), endAt(), or equalTo() cannot be ' + - 'an object.'); + 'an object.'); } } } @@ -95,10 +99,10 @@ export class Query { * @param {!QueryParams} params * @private */ - validateLimit_(params) { + private static validateLimit_(params: QueryParams) { if (params.hasStart() && params.hasEnd() && params.hasLimit() && !params.hasAnchoredLimit()) { throw new Error( - "Query: Can't combine startAt(), endAt(), and limit(). Use limitToFirst() or limitToLast() instead." + 'Query: Can\'t combine startAt(), endAt(), and limit(). Use limitToFirst() or limitToLast() instead.' ); } } @@ -108,48 +112,49 @@ export class Query { * @param {!string} fnName * @private */ - validateNoPreviousOrderByCall_(fnName) { + private validateNoPreviousOrderByCall_(fnName: string) { if (this.orderByCalled_ === true) { - throw new Error(fnName + ": You can't combine multiple orderBy calls."); + throw new Error(fnName + ': You can\'t combine multiple orderBy calls.'); } } /** * @return {!QueryParams} */ - getQueryParams() { + getQueryParams(): QueryParams { return this.queryParams_; } /** - * @return {!Firebase} + * @return {!Reference} */ - getRef() { + getRef(): Reference { validateArgCount('Query.ref', 0, 0, arguments.length); // This is a slight hack. We cannot goog.require('fb.api.Firebase'), since Firebase requires fb.api.Query. // However, we will always export 'Firebase' to the global namespace, so it's guaranteed to exist by the time this // method gets called. - return new Query.__referenceConstructor(this.repo, this.path); + return (new Query.__referenceConstructor(this.repo, this.path)); } /** * @param {!string} eventType * @param {!function(DataSnapshot, string=)} callback - * @param {(function(Error)|Object)=} opt_cancelCallbackOrContext - * @param {Object=} opt_context + * @param {(function(Error)|Object)=} cancelCallbackOrContext + * @param {Object=} context * @return {!function(DataSnapshot, string=)} */ - on(eventType, callback, opt_cancelCallbackOrContext?, opt_context?) { + on(eventType: string, callback: (a: DataSnapshot, b?: string) => any, + cancelCallbackOrContext?: ((a: Error) => any) | Object, context?: Object): (a: DataSnapshot, b?: string) => any { validateArgCount('Query.on', 2, 4, arguments.length); validateEventType('Query.on', 1, eventType, false); validateCallback('Query.on', 2, callback, false); - var ret = this.getCancelAndContextArgs_('Query.on', opt_cancelCallbackOrContext, opt_context); + const ret = Query.getCancelAndContextArgs_('Query.on', cancelCallbackOrContext, context); if (eventType === 'value') { this.onValueEvent(callback, ret.cancel, ret.context); } else { - var callbacks = {}; + const callbacks = {}; callbacks[eventType] = callback; this.onChildEvent(callbacks, ret.cancel, ret.context); } @@ -162,8 +167,8 @@ export class Query { * @param {?Object} context * @protected */ - onValueEvent(callback, cancelCallback, context) { - var container = new ValueEventRegistration(callback, cancelCallback || null, context || null); + onValueEvent(callback: (a: DataSnapshot) => any, cancelCallback: ((a: Error) => any) | null, context: Object | null) { + const container = new ValueEventRegistration(callback, cancelCallback || null, context || null); this.repo.addEventCallbackForQuery(this, container); } @@ -172,33 +177,34 @@ export class Query { * @param {?function(Error)} cancelCallback * @param {?Object} context */ - onChildEvent(callbacks, cancelCallback, context) { - var container = new ChildEventRegistration(callbacks, cancelCallback, context); + onChildEvent(callbacks: { [k: string]: (a: DataSnapshot, b: string | null) => any }, + cancelCallback: ((a: Error) => any) | null, context: Object | null) { + const container = new ChildEventRegistration(callbacks, cancelCallback, context); this.repo.addEventCallbackForQuery(this, container); } /** - * @param {string=} opt_eventType - * @param {(function(!DataSnapshot, ?string=))=} opt_callback - * @param {Object=} opt_context + * @param {string=} eventType + * @param {(function(!DataSnapshot, ?string=))=} callback + * @param {Object=} context */ - off(opt_eventType?, opt_callback?, opt_context?) { + off(eventType?: string, callback?: (a: DataSnapshot, b?: string | null) => any, context?: Object) { validateArgCount('Query.off', 0, 3, arguments.length); - validateEventType('Query.off', 1, opt_eventType, true); - validateCallback('Query.off', 2, opt_callback, true); - validateContextObject('Query.off', 3, opt_context, true); - - var container = null; - var callbacks = null; - if (opt_eventType === 'value') { - var valueCallback = /** @type {function(!DataSnapshot)} */ (opt_callback) || null; - container = new ValueEventRegistration(valueCallback, null, opt_context || null); - } else if (opt_eventType) { - if (opt_callback) { + validateEventType('Query.off', 1, eventType, true); + validateCallback('Query.off', 2, callback, true); + validateContextObject('Query.off', 3, context, true); + + let container = null; + let callbacks = null; + if (eventType === 'value') { + const valueCallback = /** @type {function(!DataSnapshot)} */ (callback) || null; + container = new ValueEventRegistration(valueCallback, null, context || null); + } else if (eventType) { + if (callback) { callbacks = {}; - callbacks[opt_eventType] = opt_callback; + callbacks[eventType] = callback; } - container = new ChildEventRegistration(callbacks, null, opt_context || null); + container = new ChildEventRegistration(callbacks, null, context || null); } this.repo.removeEventCallbackForQuery(this, container); } @@ -207,29 +213,32 @@ export class Query { * Attaches a listener, waits for the first event, and then removes the listener * @param {!string} eventType * @param {!function(!DataSnapshot, string=)} userCallback + * @param cancelOrContext + * @param context * @return {!firebase.Promise} */ - once(eventType, userCallback?, cancelOrContext?, context?) { + once(eventType: string, userCallback: (a: DataSnapshot, b?: string) => any, + cancelOrContext?, context?: Object) { validateArgCount('Query.once', 1, 4, arguments.length); validateEventType('Query.once', 1, eventType, false); validateCallback('Query.once', 2, userCallback, true); - var ret = this.getCancelAndContextArgs_('Query.once', cancelOrContext, context); + const ret = Query.getCancelAndContextArgs_('Query.once', cancelOrContext, context); // TODO: Implement this more efficiently (in particular, use 'get' wire protocol for 'value' event) // TODO: consider actually wiring the callbacks into the promise. We cannot do this without a breaking change // because the API currently expects callbacks will be called synchronously if the data is cached, but this is // against the Promise specification. - var self = this, firstCall = true; - var deferred = new Deferred(); + let firstCall = true; + const deferred = new Deferred(); attachDummyErrorHandler(deferred.promise); - var onceCallback = function(snapshot) { + const onceCallback = (snapshot) => { // NOTE: Even though we unsubscribe, we may get called multiple times if a single action (e.g. set() with JSON) // triggers multiple events (e.g. child_added or child_changed). if (firstCall) { firstCall = false; - self.off(eventType, onceCallback); + this.off(eventType, onceCallback); if (userCallback) { userCallback.bind(ret.context)(snapshot); @@ -238,8 +247,8 @@ export class Query { } }; - this.on(eventType, onceCallback, /*cancel=*/ function(err) { - self.off(eventType, onceCallback); + this.on(eventType, onceCallback, /*cancel=*/ (err) => { + this.off(eventType, onceCallback); if (ret.cancel) ret.cancel.bind(ret.context)(err); @@ -253,14 +262,14 @@ export class Query { * @param {!number} limit * @return {!Query} */ - limitToFirst(limit): Query { + limitToFirst(limit: number): Query { validateArgCount('Query.limitToFirst', 1, 1, arguments.length); if (typeof limit !== 'number' || Math.floor(limit) !== limit || limit <= 0) { throw new Error('Query.limitToFirst: First argument must be a positive integer.'); } if (this.queryParams_.hasLimit()) { throw new Error('Query.limitToFirst: Limit was already set (by another call to limit, ' + - 'limitToFirst, or limitToLast).'); + 'limitToFirst, or limitToLast).'); } return new Query(this.repo, this.path, this.queryParams_.limitToFirst(limit), this.orderByCalled_); @@ -271,18 +280,18 @@ export class Query { * @param {!number} limit * @return {!Query} */ - limitToLast(limit?): Query { + limitToLast(limit: number): Query { validateArgCount('Query.limitToLast', 1, 1, arguments.length); if (typeof limit !== 'number' || Math.floor(limit) !== limit || limit <= 0) { throw new Error('Query.limitToLast: First argument must be a positive integer.'); } if (this.queryParams_.hasLimit()) { throw new Error('Query.limitToLast: Limit was already set (by another call to limit, ' + - 'limitToFirst, or limitToLast).'); + 'limitToFirst, or limitToLast).'); } return new Query(this.repo, this.path, this.queryParams_.limitToLast(limit), - this.orderByCalled_); + this.orderByCalled_); } /** @@ -290,7 +299,7 @@ export class Query { * @param {!string} path * @return {!Query} */ - orderByChild(path) { + orderByChild(path: string): Query { validateArgCount('Query.orderByChild', 1, 1, arguments.length); if (path === '$key') { throw new Error('Query.orderByChild: "$key" is invalid. Use Query.orderByKey() instead.'); @@ -301,13 +310,13 @@ export class Query { } validatePathString('Query.orderByChild', 1, path, false); this.validateNoPreviousOrderByCall_('Query.orderByChild'); - var parsedPath = new Path(path); + const parsedPath = new Path(path); if (parsedPath.isEmpty()) { throw new Error('Query.orderByChild: cannot pass in empty path. Use Query.orderByValue() instead.'); } - var index = new PathIndex(parsedPath); - var newParams = this.queryParams_.orderBy(index); - this.validateQueryEndpoints_(newParams); + const index = new PathIndex(parsedPath); + const newParams = this.queryParams_.orderBy(index); + Query.validateQueryEndpoints_(newParams); return new Query(this.repo, this.path, newParams, /*orderByCalled=*/true); } @@ -316,11 +325,11 @@ export class Query { * Return a new query ordered by the KeyIndex * @return {!Query} */ - orderByKey() { + orderByKey(): Query { validateArgCount('Query.orderByKey', 0, 0, arguments.length); this.validateNoPreviousOrderByCall_('Query.orderByKey'); - var newParams = this.queryParams_.orderBy(KEY_INDEX); - this.validateQueryEndpoints_(newParams); + const newParams = this.queryParams_.orderBy(KEY_INDEX); + Query.validateQueryEndpoints_(newParams); return new Query(this.repo, this.path, newParams, /*orderByCalled=*/true); } @@ -328,11 +337,11 @@ export class Query { * Return a new query ordered by the PriorityIndex * @return {!Query} */ - orderByPriority() { + orderByPriority(): Query { validateArgCount('Query.orderByPriority', 0, 0, arguments.length); this.validateNoPreviousOrderByCall_('Query.orderByPriority'); - var newParams = this.queryParams_.orderBy(PRIORITY_INDEX); - this.validateQueryEndpoints_(newParams); + const newParams = this.queryParams_.orderBy(PRIORITY_INDEX); + Query.validateQueryEndpoints_(newParams); return new Query(this.repo, this.path, newParams, /*orderByCalled=*/true); } @@ -340,30 +349,30 @@ export class Query { * Return a new query ordered by the ValueIndex * @return {!Query} */ - orderByValue() { + orderByValue(): Query { validateArgCount('Query.orderByValue', 0, 0, arguments.length); this.validateNoPreviousOrderByCall_('Query.orderByValue'); - var newParams = this.queryParams_.orderBy(VALUE_INDEX); - this.validateQueryEndpoints_(newParams); + const newParams = this.queryParams_.orderBy(VALUE_INDEX); + Query.validateQueryEndpoints_(newParams); return new Query(this.repo, this.path, newParams, /*orderByCalled=*/true); } /** * @param {number|string|boolean|null} value - * @param {?string=} opt_name + * @param {?string=} name * @return {!Query} */ - startAt(value = null, name?) { + startAt(value: number | string | boolean | null = null, name?: string | null): Query { validateArgCount('Query.startAt', 0, 2, arguments.length); validateFirebaseDataArg('Query.startAt', 1, value, this.path, true); validateKey('Query.startAt', 2, name, true); - var newParams = this.queryParams_.startAt(value, name); - this.validateLimit_(newParams); - this.validateQueryEndpoints_(newParams); + const newParams = this.queryParams_.startAt(value, name); + Query.validateLimit_(newParams); + Query.validateQueryEndpoints_(newParams); if (this.queryParams_.hasStart()) { throw new Error('Query.startAt: Starting point was already set (by another call to startAt ' + - 'or equalTo).'); + 'or equalTo).'); } // Calling with no params tells us to start at the beginning. @@ -376,20 +385,20 @@ export class Query { /** * @param {number|string|boolean|null} value - * @param {?string=} opt_name + * @param {?string=} name * @return {!Query} */ - endAt(value = null, name?) { + endAt(value: number | string | boolean | null = null, name?: string | null): Query { validateArgCount('Query.endAt', 0, 2, arguments.length); validateFirebaseDataArg('Query.endAt', 1, value, this.path, true); validateKey('Query.endAt', 2, name, true); - var newParams = this.queryParams_.endAt(value, name); - this.validateLimit_(newParams); - this.validateQueryEndpoints_(newParams); + const newParams = this.queryParams_.endAt(value, name); + Query.validateLimit_(newParams); + Query.validateQueryEndpoints_(newParams); if (this.queryParams_.hasEnd()) { throw new Error('Query.endAt: Ending point was already set (by another call to endAt or ' + - 'equalTo).'); + 'equalTo).'); } return new Query(this.repo, this.path, newParams, this.orderByCalled_); @@ -399,20 +408,20 @@ export class Query { * Load the selection of children with exactly the specified value, and, optionally, * the specified name. * @param {number|string|boolean|null} value - * @param {string=} opt_name + * @param {string=} name * @return {!Query} */ - equalTo(value, name?) { + equalTo(value: number | string | boolean | null, name?: string) { validateArgCount('Query.equalTo', 1, 2, arguments.length); validateFirebaseDataArg('Query.equalTo', 1, value, this.path, false); validateKey('Query.equalTo', 2, name, true); if (this.queryParams_.hasStart()) { throw new Error('Query.equalTo: Starting point was already set (by another call to startAt or ' + - 'equalTo).'); + 'equalTo).'); } if (this.queryParams_.hasEnd()) { throw new Error('Query.equalTo: Ending point was already set (by another call to endAt or ' + - 'equalTo).'); + 'equalTo).'); } return this.startAt(value, name).endAt(value, name); } @@ -420,7 +429,7 @@ export class Query { /** * @return {!string} URL for this location. */ - toString() { + toString(): string { validateArgCount('Query.toString', 0, 0, arguments.length); return this.repo.toString() + this.path.toUrlEncodedString(); @@ -438,16 +447,16 @@ export class Query { * An object representation of the query parameters used by this Query. * @return {!Object} */ - queryObject() { + queryObject(): Object { return this.queryParams_.getQueryObject(); } /** * @return {!string} */ - queryIdentifier() { - var obj = this.queryObject(); - var id = ObjectToUniqueKey(obj); + queryIdentifier(): string { + const obj = this.queryObject(); + const id = ObjectToUniqueKey(obj); return (id === '{}') ? 'default' : id; } @@ -456,16 +465,16 @@ export class Query { * @param {Query} other * @return {boolean} */ - isEqual(other?) { + isEqual(other: Query): boolean { validateArgCount('Query.isEqual', 1, 1, arguments.length); if (!(other instanceof Query)) { - var error = 'Query.isEqual failed: First argument must be an instance of firebase.database.Query.'; + const error = 'Query.isEqual failed: First argument must be an instance of firebase.database.Query.'; throw new Error(error); } - var sameRepo = (this.repo === other.repo); - var samePath = this.path.equals(other.path); - var sameQueryIdentifier = (this.queryIdentifier() === other.queryIdentifier()); + const sameRepo = (this.repo === other.repo); + const samePath = this.path.equals(other.path); + const sameQueryIdentifier = (this.queryIdentifier() === other.queryIdentifier()); return (sameRepo && samePath && sameQueryIdentifier); } @@ -473,33 +482,34 @@ export class Query { /** * Helper used by .on and .once to extract the context and or cancel arguments. * @param {!string} fnName The function name (on or once) - * @param {(function(Error)|Object)=} opt_cancelOrContext - * @param {Object=} opt_context + * @param {(function(Error)|Object)=} cancelOrContext + * @param {Object=} context * @return {{cancel: ?function(Error), context: ?Object}} * @private */ - getCancelAndContextArgs_(fnName, opt_cancelOrContext, opt_context) { - var ret = {cancel: null, context: null}; - if (opt_cancelOrContext && opt_context) { - ret.cancel = /** @type {function(Error)} */ (opt_cancelOrContext); + private static getCancelAndContextArgs_(fnName: string, cancelOrContext?: ((a: Error) => any) | Object, + context?: Object): { cancel: ((a: Error) => any) | null, context: Object | null } { + const ret = {cancel: null, context: null}; + if (cancelOrContext && context) { + ret.cancel = /** @type {function(Error)} */ (cancelOrContext); validateCallback(fnName, 3, ret.cancel, true); - ret.context = opt_context; + ret.context = context; validateContextObject(fnName, 4, ret.context, true); - } else if (opt_cancelOrContext) { // we have either a cancel callback or a context. - if (typeof opt_cancelOrContext === 'object' && opt_cancelOrContext !== null) { // it's a context! - ret.context = opt_cancelOrContext; - } else if (typeof opt_cancelOrContext === 'function') { - ret.cancel = opt_cancelOrContext; + } else if (cancelOrContext) { // we have either a cancel callback or a context. + if (typeof cancelOrContext === 'object' && cancelOrContext !== null) { // it's a context! + ret.context = cancelOrContext; + } else if (typeof cancelOrContext === 'function') { + ret.cancel = cancelOrContext; } else { throw new Error(errorPrefix(fnName, 3, true) + - ' must either be a cancel callback or a context object.'); + ' must either be a cancel callback or a context object.'); } } return ret; } - get ref() { + get ref(): Reference { return this.getRef(); } -}; // end Query \ No newline at end of file +} diff --git a/src/database/api/Reference.ts b/src/database/api/Reference.ts index 6f55597bd52..5654eaf9b7b 100644 --- a/src/database/api/Reference.ts +++ b/src/database/api/Reference.ts @@ -1,11 +1,11 @@ -import { OnDisconnect } from "./onDisconnect"; -import { TransactionResult } from "./TransactionResult"; -import { warn, exportPropGetter } from "../core/util/util"; -import { nextPushId } from "../core/util/NextPushId"; -import { Query } from "./Query"; -import { Repo } from "../core/Repo"; -import { Path } from "../core/util/Path"; -import { QueryParams } from "../core/view/QueryParams"; +import { OnDisconnect } from './onDisconnect'; +import { TransactionResult } from './TransactionResult'; +import { warn } from '../core/util/util'; +import { nextPushId } from '../core/util/NextPushId'; +import { Query } from './Query'; +import { Repo } from '../core/Repo'; +import { Path } from '../core/util/Path'; +import { QueryParams } from '../core/view/QueryParams'; import { validateRootPathString, validatePathString, @@ -14,17 +14,20 @@ import { validatePriority, validateFirebaseDataArg, validateWritablePath, -} from "../core/util/validation"; +} from '../core/util/validation'; import { validateArgCount, validateCallback, -} from "../../utils/validation"; -import { Deferred, attachDummyErrorHandler, PromiseImpl } from "../../utils/promise"; -import { SyncPoint } from "../core/SyncPoint"; +} from '../../utils/validation'; +import { Deferred, attachDummyErrorHandler, PromiseImpl } from '../../utils/promise'; +import { SyncPoint } from '../core/SyncPoint'; +import { Database } from './Database'; +import { DataSnapshot } from './DataSnapshot'; export class Reference extends Query { public then; public catch; + /** * Call options: * new Reference(Repo, Path) or @@ -38,7 +41,7 @@ export class Reference extends Query { */ constructor(repo: Repo, path: Path) { if (!(repo instanceof Repo)) { - throw new Error("new Reference() no longer supported - use app.database()."); + throw new Error('new Reference() no longer supported - use app.database().'); } // call Query's constructor, passing in the repo and path. @@ -46,8 +49,8 @@ export class Reference extends Query { } /** @return {?string} */ - getKey() { - validateArgCount('Firebase.key', 0, 0, arguments.length); + getKey(): string | null { + validateArgCount('Reference.key', 0, 0, arguments.length); if (this.path.isEmpty()) return null; @@ -57,35 +60,35 @@ export class Reference extends Query { /** * @param {!(string|Path)} pathString - * @return {!Firebase} + * @return {!Reference} */ - child(pathString) { - validateArgCount('Firebase.child', 1, 1, arguments.length); + child(pathString: string | Path): Reference { + validateArgCount('Reference.child', 1, 1, arguments.length); if (typeof pathString === 'number') { pathString = String(pathString); } else if (!(pathString instanceof Path)) { if (this.path.getFront() === null) - validateRootPathString('Firebase.child', 1, pathString, false); + validateRootPathString('Reference.child', 1, pathString, false); else - validatePathString('Firebase.child', 1, pathString, false); + validatePathString('Reference.child', 1, pathString, false); } return new Reference(this.repo, this.path.child(pathString)); } - /** @return {?Firebase} */ - getParent() { - validateArgCount('Firebase.parent', 0, 0, arguments.length); + /** @return {?Reference} */ + getParent(): Reference | null { + validateArgCount('Reference.parent', 0, 0, arguments.length); - var parentPath = this.path.parent(); + const parentPath = this.path.parent(); return parentPath === null ? null : new Reference(this.repo, parentPath); } - /** @return {!Firebase} */ - getRoot() { - validateArgCount('Firebase.ref', 0, 0, arguments.length); + /** @return {!Reference} */ + getRoot(): Reference { + validateArgCount('Reference.root', 0, 0, arguments.length); - var ref = this; + let ref = this; while (ref.getParent() !== null) { ref = ref.getParent(); } @@ -93,50 +96,50 @@ export class Reference extends Query { } /** @return {!Database} */ - databaseProp() { + databaseProp(): Database { return this.repo.database; } /** * @param {*} newVal - * @param {function(?Error)=} opt_onComplete - * @return {!firebase.Promise} + * @param {function(?Error)=} onComplete + * @return {!Promise} */ - set(newVal, onComplete?): Promise { - validateArgCount('Firebase.set', 1, 2, arguments.length); - validateWritablePath('Firebase.set', this.path); - validateFirebaseDataArg('Firebase.set', 1, newVal, this.path, false); - validateCallback('Firebase.set', 2, onComplete, true); + set(newVal: any, onComplete?: (a: Error | null) => any): Promise { + validateArgCount('Reference.set', 1, 2, arguments.length); + validateWritablePath('Reference.set', this.path); + validateFirebaseDataArg('Reference.set', 1, newVal, this.path, false); + validateCallback('Reference.set', 2, onComplete, true); - var deferred = new Deferred(); + const deferred = new Deferred(); this.repo.setWithPriority(this.path, newVal, /*priority=*/ null, deferred.wrapCallback(onComplete)); return deferred.promise; } /** * @param {!Object} objectToMerge - * @param {function(?Error)=} opt_onComplete - * @return {!firebase.Promise} + * @param {function(?Error)=} onComplete + * @return {!Promise} */ - update(objectToMerge, onComplete?) { - validateArgCount('Firebase.update', 1, 2, arguments.length); - validateWritablePath('Firebase.update', this.path); + update(objectToMerge: Object, onComplete?: (a: Error | null) => any): Promise { + validateArgCount('Reference.update', 1, 2, arguments.length); + validateWritablePath('Reference.update', this.path); if (Array.isArray(objectToMerge)) { - var newObjectToMerge = {}; - for (var i = 0; i < objectToMerge.length; ++i) { + const newObjectToMerge = {}; + for (let i = 0; i < objectToMerge.length; ++i) { newObjectToMerge['' + i] = objectToMerge[i]; } objectToMerge = newObjectToMerge; warn('Passing an Array to Firebase.update() is deprecated. ' + - 'Use set() if you want to overwrite the existing data, or ' + - 'an Object with integer keys if you really do want to ' + - 'only update some of the children.' + 'Use set() if you want to overwrite the existing data, or ' + + 'an Object with integer keys if you really do want to ' + + 'only update some of the children.' ); } - validateFirebaseMergeDataArg('Firebase.update', 1, objectToMerge, this.path, false); - validateCallback('Firebase.update', 2, onComplete, true); - var deferred = new Deferred(); + validateFirebaseMergeDataArg('Reference.update', 1, objectToMerge, this.path, false); + validateCallback('Reference.update', 2, onComplete, true); + const deferred = new Deferred(); this.repo.update(this.path, objectToMerge, deferred.wrapCallback(onComplete)); return deferred.promise; } @@ -144,124 +147,127 @@ export class Reference extends Query { /** * @param {*} newVal * @param {string|number|null} newPriority - * @param {function(?Error)=} opt_onComplete + * @param {function(?Error)=} onComplete * @return {!Promise} */ - setWithPriority(newVal, newPriority, onComplete?): Promise { - validateArgCount('Firebase.setWithPriority', 2, 3, arguments.length); - validateWritablePath('Firebase.setWithPriority', this.path); - validateFirebaseDataArg('Firebase.setWithPriority', 1, newVal, this.path, false); - validatePriority('Firebase.setWithPriority', 2, newPriority, false); - validateCallback('Firebase.setWithPriority', 3, onComplete, true); + setWithPriority(newVal: any, newPriority: string | number | null, + onComplete?: (a: Error | null) => any): Promise { + validateArgCount('Reference.setWithPriority', 2, 3, arguments.length); + validateWritablePath('Reference.setWithPriority', this.path); + validateFirebaseDataArg('Reference.setWithPriority', 1, newVal, this.path, false); + validatePriority('Reference.setWithPriority', 2, newPriority, false); + validateCallback('Reference.setWithPriority', 3, onComplete, true); if (this.getKey() === '.length' || this.getKey() === '.keys') - throw 'Firebase.setWithPriority failed: ' + this.getKey() + ' is a read-only object.'; + throw 'Reference.setWithPriority failed: ' + this.getKey() + ' is a read-only object.'; - var deferred = new Deferred(); + const deferred = new Deferred(); this.repo.setWithPriority(this.path, newVal, newPriority, deferred.wrapCallback(onComplete)); return deferred.promise; } /** - * @param {function(?Error)=} opt_onComplete - * @return {!firebase.Promise} + * @param {function(?Error)=} onComplete + * @return {!Promise} */ - remove(onComplete?) { - validateArgCount('Firebase.remove', 0, 1, arguments.length); - validateWritablePath('Firebase.remove', this.path); - validateCallback('Firebase.remove', 1, onComplete, true); + remove(onComplete?: (a: Error | null) => any): Promise { + validateArgCount('Reference.remove', 0, 1, arguments.length); + validateWritablePath('Reference.remove', this.path); + validateCallback('Reference.remove', 1, onComplete, true); return this.set(null, onComplete); } /** * @param {function(*):*} transactionUpdate - * @param {(function(?Error, boolean, ?DataSnapshot))=} opt_onComplete - * @param {boolean=} opt_applyLocally - * @return {!firebase.Promise} + * @param {(function(?Error, boolean, ?DataSnapshot))=} onComplete + * @param {boolean=} applyLocally + * @return {!Promise} */ - transaction(transactionUpdate, opt_onComplete, opt_applyLocally) { - validateArgCount('Firebase.transaction', 1, 3, arguments.length); - validateWritablePath('Firebase.transaction', this.path); - validateCallback('Firebase.transaction', 1, transactionUpdate, false); - validateCallback('Firebase.transaction', 2, opt_onComplete, true); - // NOTE: opt_applyLocally is an internal-only option for now. We need to decide if we want to keep it and how + transaction(transactionUpdate: (a: any) => any, + onComplete?: (a: Error | null, b: boolean, c: DataSnapshot | null) => any, + applyLocally?: boolean): Promise { + validateArgCount('Reference.transaction', 1, 3, arguments.length); + validateWritablePath('Reference.transaction', this.path); + validateCallback('Reference.transaction', 1, transactionUpdate, false); + validateCallback('Reference.transaction', 2, onComplete, true); + // NOTE: applyLocally is an internal-only option for now. We need to decide if we want to keep it and how // to expose it. - validateBoolean('Firebase.transaction', 3, opt_applyLocally, true); + validateBoolean('Reference.transaction', 3, applyLocally, true); if (this.getKey() === '.length' || this.getKey() === '.keys') - throw 'Firebase.transaction failed: ' + this.getKey() + ' is a read-only object.'; + throw 'Reference.transaction failed: ' + this.getKey() + ' is a read-only object.'; - if (opt_applyLocally === undefined) - opt_applyLocally = true; + if (applyLocally === undefined) + applyLocally = true; - var deferred = new Deferred(); - if (typeof opt_onComplete === 'function') { + const deferred = new Deferred(); + if (typeof onComplete === 'function') { attachDummyErrorHandler(deferred.promise); } - var promiseComplete = function(error, committed, snapshot) { + const promiseComplete = function (error, committed, snapshot) { if (error) { deferred.reject(error); } else { deferred.resolve(new TransactionResult(committed, snapshot)); } - if (typeof opt_onComplete === 'function') { - opt_onComplete(error, committed, snapshot); + if (typeof onComplete === 'function') { + onComplete(error, committed, snapshot); } }; - this.repo.startTransaction(this.path, transactionUpdate, promiseComplete, opt_applyLocally); + this.repo.startTransaction(this.path, transactionUpdate, promiseComplete, applyLocally); return deferred.promise; } /** * @param {string|number|null} priority - * @param {function(?Error)=} opt_onComplete - * @return {!firebase.Promise} + * @param {function(?Error)=} onComplete + * @return {!Promise} */ - setPriority(priority, onComplete?): Promise { - validateArgCount('Firebase.setPriority', 1, 2, arguments.length); - validateWritablePath('Firebase.setPriority', this.path); - validatePriority('Firebase.setPriority', 1, priority, false); - validateCallback('Firebase.setPriority', 2, onComplete, true); + setPriority(priority: string | number | null, onComplete?: (a: Error | null) => any): Promise { + validateArgCount('Reference.setPriority', 1, 2, arguments.length); + validateWritablePath('Reference.setPriority', this.path); + validatePriority('Reference.setPriority', 1, priority, false); + validateCallback('Reference.setPriority', 2, onComplete, true); - var deferred = new Deferred(); + const deferred = new Deferred(); this.repo.setWithPriority(this.path.child('.priority'), priority, null, deferred.wrapCallback(onComplete)); return deferred.promise; } /** - * @param {*=} opt_value - * @param {function(?Error)=} opt_onComplete - * @return {!Firebase} + * @param {*=} value + * @param {function(?Error)=} onComplete + * @return {!Reference} */ - push(value?, onComplete?) { - validateArgCount('Firebase.push', 0, 2, arguments.length); - validateWritablePath('Firebase.push', this.path); - validateFirebaseDataArg('Firebase.push', 1, value, this.path, true); - validateCallback('Firebase.push', 2, onComplete, true); + push(value?: any, onComplete?: (a: Error | null) => any): Reference { + validateArgCount('Reference.push', 0, 2, arguments.length); + validateWritablePath('Reference.push', this.path); + validateFirebaseDataArg('Reference.push', 1, value, this.path, true); + validateCallback('Reference.push', 2, onComplete, true); - var now = this.repo.serverTime(); - var name = nextPushId(now); + const now = this.repo.serverTime(); + const name = nextPushId(now); // push() returns a ThennableReference whose promise is fulfilled with a regular Reference. // We use child() to create handles to two different references. The first is turned into a // ThennableReference below by adding then() and catch() methods and is used as the // return value of push(). The second remains a regular Reference and is used as the fulfilled // value of the first ThennableReference. - var thennablePushRef = this.child(name); - var pushRef = this.child(name); + const thennablePushRef = this.child(name); + const pushRef = this.child(name); - var promise; + let promise; if (value != null) { - promise = thennablePushRef.set(value, onComplete).then(function () { return pushRef; }); + promise = thennablePushRef.set(value, onComplete).then(() => pushRef); } else { promise = PromiseImpl.resolve(pushRef); } thennablePushRef.then = promise.then.bind(promise); - /** @type {letMeUseMapAccessors} */ (thennablePushRef)["catch"] = promise.then.bind(promise, undefined); + thennablePushRef.catch = promise.then.bind(promise, undefined); if (typeof onComplete === 'function') { attachDummyErrorHandler(promise); @@ -273,31 +279,31 @@ export class Reference extends Query { /** * @return {!OnDisconnect} */ - onDisconnect() { - validateWritablePath('Firebase.onDisconnect', this.path); + onDisconnect(): OnDisconnect { + validateWritablePath('Reference.onDisconnect', this.path); return new OnDisconnect(this.repo, this.path); } - get database() { + get database(): Database { return this.databaseProp(); } - get key() { + get key(): string | null { return this.getKey(); } - get parent() { + get parent(): Reference | null { return this.getParent(); } - get root() { + get root(): Reference { return this.getRoot(); } } /** * Define reference constructor in various modules - * + * * We are doing this here to avoid several circular * dependency issues */ diff --git a/src/database/api/onDisconnect.ts b/src/database/api/onDisconnect.ts index 6de165f007c..c6e9e5ce6f4 100644 --- a/src/database/api/onDisconnect.ts +++ b/src/database/api/onDisconnect.ts @@ -10,103 +10,104 @@ import { } from "../core/util/validation"; import { warn } from "../core/util/util"; import { Deferred } from "../../utils/promise"; +import { Repo } from '../core/Repo'; +import { Path } from '../core/util/Path'; /** * @constructor - * @param {!Repo} repo - * @param {!Path} path */ -export const OnDisconnect = function(repo, path) { - /** @private */ - this.repo_ = repo; - - /** @private */ - this.path_ = path; -}; - +export class OnDisconnect { + /** + * @param {!Repo} repo_ + * @param {!Path} path_ + */ + constructor(private repo_: Repo, + private path_: Path) { + } -/** - * @param {function(?Error)=} opt_onComplete - * @return {!firebase.Promise} - */ -OnDisconnect.prototype.cancel = function(opt_onComplete) { - validateArgCount('Firebase.onDisconnect().cancel', 0, 1, arguments.length); - validateCallback('Firebase.onDisconnect().cancel', 1, opt_onComplete, true); - var deferred = new Deferred(); - this.repo_.onDisconnectCancel(this.path_, deferred.wrapCallback(opt_onComplete)); - return deferred.promise; -}; + /** + * @param {function(?Error)=} opt_onComplete + * @return {!firebase.Promise} + */ + cancel(opt_onComplete) { + validateArgCount('OnDisconnect.cancel', 0, 1, arguments.length); + validateCallback('OnDisconnect.cancel', 1, opt_onComplete, true); + const deferred = new Deferred(); + this.repo_.onDisconnectCancel(this.path_, deferred.wrapCallback(opt_onComplete)); + return deferred.promise; + } -/** - * @param {function(?Error)=} opt_onComplete - * @return {!firebase.Promise} - */ -OnDisconnect.prototype.remove = function(opt_onComplete) { - validateArgCount('Firebase.onDisconnect().remove', 0, 1, arguments.length); - validateWritablePath('Firebase.onDisconnect().remove', this.path_); - validateCallback('Firebase.onDisconnect().remove', 1, opt_onComplete, true); - var deferred = new Deferred(); - this.repo_.onDisconnectSet(this.path_, null, deferred.wrapCallback(opt_onComplete)); - return deferred.promise; -}; + /** + * @param {function(?Error)=} opt_onComplete + * @return {!firebase.Promise} + */ + remove(opt_onComplete) { + validateArgCount('OnDisconnect.remove', 0, 1, arguments.length); + validateWritablePath('OnDisconnect.remove', this.path_); + validateCallback('OnDisconnect.remove', 1, opt_onComplete, true); + const deferred = new Deferred(); + this.repo_.onDisconnectSet(this.path_, null, deferred.wrapCallback(opt_onComplete)); + return deferred.promise; + } -/** - * @param {*} value - * @param {function(?Error)=} opt_onComplete - * @return {!firebase.Promise} - */ -OnDisconnect.prototype.set = function(value, opt_onComplete) { - validateArgCount('Firebase.onDisconnect().set', 1, 2, arguments.length); - validateWritablePath('Firebase.onDisconnect().set', this.path_); - validateFirebaseDataArg('Firebase.onDisconnect().set', 1, value, this.path_, false); - validateCallback('Firebase.onDisconnect().set', 2, opt_onComplete, true); - var deferred = new Deferred(); - this.repo_.onDisconnectSet(this.path_, value, deferred.wrapCallback(opt_onComplete)); - return deferred.promise; -}; + /** + * @param {*} value + * @param {function(?Error)=} opt_onComplete + * @return {!firebase.Promise} + */ + set(value, opt_onComplete) { + validateArgCount('OnDisconnect.set', 1, 2, arguments.length); + validateWritablePath('OnDisconnect.set', this.path_); + validateFirebaseDataArg('OnDisconnect.set', 1, value, this.path_, false); + validateCallback('OnDisconnect.set', 2, opt_onComplete, true); + const deferred = new Deferred(); + this.repo_.onDisconnectSet(this.path_, value, deferred.wrapCallback(opt_onComplete)); + return deferred.promise; + } -/** - * @param {*} value - * @param {number|string|null} priority - * @param {function(?Error)=} opt_onComplete - * @return {!firebase.Promise} - */ -OnDisconnect.prototype.setWithPriority = function(value, priority, opt_onComplete) { - validateArgCount('Firebase.onDisconnect().setWithPriority', 2, 3, arguments.length); - validateWritablePath('Firebase.onDisconnect().setWithPriority', this.path_); - validateFirebaseDataArg('Firebase.onDisconnect().setWithPriority', - 1, value, this.path_, false); - validatePriority('Firebase.onDisconnect().setWithPriority', 2, priority, false); - validateCallback('Firebase.onDisconnect().setWithPriority', 3, opt_onComplete, true); + /** + * @param {*} value + * @param {number|string|null} priority + * @param {function(?Error)=} opt_onComplete + * @return {!firebase.Promise} + */ + setWithPriority(value, priority, opt_onComplete) { + validateArgCount('OnDisconnect.setWithPriority', 2, 3, arguments.length); + validateWritablePath('OnDisconnect.setWithPriority', this.path_); + validateFirebaseDataArg('OnDisconnect.setWithPriority', + 1, value, this.path_, false); + validatePriority('OnDisconnect.setWithPriority', 2, priority, false); + validateCallback('OnDisconnect.setWithPriority', 3, opt_onComplete, true); - var deferred = new Deferred(); - this.repo_.onDisconnectSetWithPriority(this.path_, value, priority, deferred.wrapCallback(opt_onComplete)); - return deferred.promise; -}; + const deferred = new Deferred(); + this.repo_.onDisconnectSetWithPriority(this.path_, value, priority, deferred.wrapCallback(opt_onComplete)); + return deferred.promise; + } -/** - * @param {!Object} objectToMerge - * @param {function(?Error)=} opt_onComplete - * @return {!firebase.Promise} - */ -OnDisconnect.prototype.update = function(objectToMerge, opt_onComplete) { - validateArgCount('Firebase.onDisconnect().update', 1, 2, arguments.length); - validateWritablePath('Firebase.onDisconnect().update', this.path_); - if (Array.isArray(objectToMerge)) { - var newObjectToMerge = {}; - for (var i = 0; i < objectToMerge.length; ++i) { - newObjectToMerge['' + i] = objectToMerge[i]; - } - objectToMerge = newObjectToMerge; - warn( - 'Passing an Array to Firebase.onDisconnect().update() is deprecated. Use set() if you want to overwrite the ' + + /** + * @param {!Object} objectToMerge + * @param {function(?Error)=} opt_onComplete + * @return {!firebase.Promise} + */ + update(objectToMerge, opt_onComplete) { + validateArgCount('OnDisconnect.update', 1, 2, arguments.length); + validateWritablePath('OnDisconnect.update', this.path_); + if (Array.isArray(objectToMerge)) { + const newObjectToMerge = {}; + for (let i = 0; i < objectToMerge.length; ++i) { + newObjectToMerge['' + i] = objectToMerge[i]; + } + objectToMerge = newObjectToMerge; + warn( + 'Passing an Array to firebase.database.onDisconnect().update() is deprecated. Use set() if you want to overwrite the ' + 'existing data, or an Object with integer keys if you really do want to only update some of the children.' - ); - } - validateFirebaseMergeDataArg('Firebase.onDisconnect().update', 1, objectToMerge, + ); + } + validateFirebaseMergeDataArg('OnDisconnect.update', 1, objectToMerge, this.path_, false); - validateCallback('Firebase.onDisconnect().update', 2, opt_onComplete, true); - var deferred = new Deferred(); - this.repo_.onDisconnectUpdate(this.path_, objectToMerge, deferred.wrapCallback(opt_onComplete)); - return deferred.promise; -}; + validateCallback('OnDisconnect.update', 2, opt_onComplete, true); + const deferred = new Deferred(); + this.repo_.onDisconnectUpdate(this.path_, objectToMerge, deferred.wrapCallback(opt_onComplete)); + return deferred.promise; + } +} \ No newline at end of file diff --git a/src/database/core/ReadonlyRestClient.ts b/src/database/core/ReadonlyRestClient.ts index 77350a0f2f6..8228a613ca9 100644 --- a/src/database/core/ReadonlyRestClient.ts +++ b/src/database/core/ReadonlyRestClient.ts @@ -1,26 +1,21 @@ -import { assert } from "../../utils/assert"; -import { logWrapper, warn } from "./util/util"; -import { jsonEval } from "../../utils/json"; -import { safeGet } from "../../utils/obj"; -import { querystring } from "../../utils/util"; +import { assert } from '../../utils/assert'; +import { logWrapper, warn } from './util/util'; +import { jsonEval } from '../../utils/json'; +import { safeGet } from '../../utils/obj'; +import { querystring } from '../../utils/util'; +import { ServerActions } from './ServerActions'; +import { RepoInfo } from './RepoInfo'; +import { AuthTokenProvider } from './AuthTokenProvider'; +import { Query } from '../api/Query'; /** * An implementation of ServerActions that communicates with the server via REST requests. * This is mostly useful for compatibility with crawlers, where we don't want to spin up a full * persistent connection (using WebSockets or long-polling) */ -export class ReadonlyRestClient { +export class ReadonlyRestClient implements ServerActions { /** @private {function(...[*])} */ - private log_; - - /** @private {!RepoInfo} */ - private repoInfo_; - - /** @private {function(string, *, boolean, ?number)} */ - private onDataUpdate_; - - /** @private {!AuthTokenProvider} */ - private authTokenProvider_; + private log_: (...args: any[]) => any = logWrapper('p:rest:'); /** * We don't actually need to track listens, except to prevent us calling an onComplete for a listen @@ -28,50 +23,48 @@ export class ReadonlyRestClient { * * @private {!Object.} */ - private listens_; - + private listens_: { [k: string]: Object } = {}; + /** * @param {!Query} query - * @param {?number=} opt_tag + * @param {?number=} tag * @return {string} * @private */ - static getListenId_(query, tag?) { + static getListenId_(query: Query, tag?: number | null): string { if (tag !== undefined) { return 'tag$' + tag; } else { - assert(query.getQueryParams().isDefault(), "should have a tag if it's not a default query."); + assert(query.getQueryParams().isDefault(), 'should have a tag if it\'s not a default query.'); return query.path.toString(); } } + /** - * @param {!RepoInfo} repoInfo Data about the namespace we are connecting to - * @param {function(string, *, boolean, ?number)} onDataUpdate A callback for new data from the server + * @param {!RepoInfo} repoInfo_ Data about the namespace we are connecting to + * @param {function(string, *, boolean, ?number)} onDataUpdate_ A callback for new data from the server + * @param {AuthTokenProvider} authTokenProvider_ * @implements {ServerActions} */ - constructor(repoInfo, onDataUpdate, authTokenProvider) { - this.log_ = logWrapper('p:rest:'); - this.repoInfo_ = repoInfo; - this.onDataUpdate_ = onDataUpdate; - this.authTokenProvider_ = authTokenProvider; - this.listens_ = { }; + constructor(private repoInfo_: RepoInfo, + private onDataUpdate_: (a: string, b: any, c: boolean, d: number | null) => any, + private authTokenProvider_: AuthTokenProvider) { } /** @inheritDoc */ - listen(query, currentHashFn, tag, onComplete) { - var pathString = query.path.toString(); + listen(query: Query, currentHashFn: () => string, tag: number | null, onComplete: (a: string, b: any) => any) { + const pathString = query.path.toString(); this.log_('Listen called for ' + pathString + ' ' + query.queryIdentifier()); // Mark this listener so we can tell if it's removed. - var listenId = ReadonlyRestClient.getListenId_(query, tag); - var thisListen = new Object(); + const listenId = ReadonlyRestClient.getListenId_(query, tag); + const thisListen = {}; this.listens_[listenId] = thisListen; - var queryStringParamaters = query.getQueryParams().toRestQueryStringParameters(); + const queryStringParamaters = query.getQueryParams().toRestQueryStringParameters(); - var self = this; - this.restRequest_(pathString + '.json', queryStringParamaters, function(error, result) { - var data = result; + this.restRequest_(pathString + '.json', queryStringParamaters, (error, result) => { + let data = result; if (error === 404) { data = null; @@ -79,11 +72,11 @@ export class ReadonlyRestClient { } if (error === null) { - self.onDataUpdate_(pathString, data, /*isMerge=*/false, tag); + this.onDataUpdate_(pathString, data, /*isMerge=*/false, tag); } - if (safeGet(self.listens_, listenId) === thisListen) { - var status; + if (safeGet(this.listens_, listenId) === thisListen) { + let status; if (!error) { status = 'ok'; } else if (error == 401) { @@ -98,33 +91,33 @@ export class ReadonlyRestClient { } /** @inheritDoc */ - unlisten(query, tag) { - var listenId = ReadonlyRestClient.getListenId_(query, tag); + unlisten(query: Query, tag: number | null) { + const listenId = ReadonlyRestClient.getListenId_(query, tag); delete this.listens_[listenId]; } /** @inheritDoc */ - refreshAuthToken(token) { + refreshAuthToken(token: string) { // no-op since we just always call getToken. } /** @inheritDoc */ - onDisconnectPut(pathString, data, opt_onComplete) { } + onDisconnectPut(pathString: string, data: any, onComplete?: (a: string, b: string) => any) { } /** @inheritDoc */ - onDisconnectMerge(pathString, data, opt_onComplete) { } + onDisconnectMerge(pathString: string, data: any, onComplete?: (a: string, b: string) => any) { } /** @inheritDoc */ - onDisconnectCancel(pathString, opt_onComplete) { } + onDisconnectCancel(pathString: string, onComplete?: (a: string, b: string) => any) { } /** @inheritDoc */ - put(pathString, data, opt_onComplete, opt_hash) { } + put(pathString: string, data: any, onComplete?: (a: string, b: string) => any, hash?: string) { } /** @inheritDoc */ - merge(pathString, data, onComplete, opt_hash) { } + merge(pathString: string, data: any, onComplete: (a: string, b: string | null) => any, hash?: string) { } /** @inheritDoc */ - reportStats(stats) { } + reportStats(stats: { [k: string]: any }) { } /** * Performs a REST request to the given path, with the provided query string parameters, @@ -135,31 +128,28 @@ export class ReadonlyRestClient { * @param {?function(?number, *=)} callback * @private */ - restRequest_(pathString, queryStringParameters, callback) { - queryStringParameters = queryStringParameters || { }; - + private restRequest_(pathString: string, queryStringParameters: {[k: string]: any} = {}, + callback: ((a: number | null, b?: any) => any) | null) { queryStringParameters['format'] = 'export'; - var self = this; - - this.authTokenProvider_.getToken(/*forceRefresh=*/false).then(function(authTokenData) { - var authToken = authTokenData && authTokenData.accessToken; + this.authTokenProvider_.getToken(/*forceRefresh=*/false).then((authTokenData) => { + const authToken = authTokenData && authTokenData.accessToken; if (authToken) { queryStringParameters['auth'] = authToken; } - var url = (self.repoInfo_.secure ? 'https://' : 'http://') + - self.repoInfo_.host + + const url = (this.repoInfo_.secure ? 'https://' : 'http://') + + this.repoInfo_.host + pathString + '?' + querystring(queryStringParameters); - self.log_('Sending REST request for ' + url); - var xhr = new XMLHttpRequest(); - xhr.onreadystatechange = function () { + this.log_('Sending REST request for ' + url); + const xhr = new XMLHttpRequest(); + xhr.onreadystatechange = () => { if (callback && xhr.readyState === 4) { - self.log_('REST Response for ' + url + ' received. status:', xhr.status, 'response:', xhr.responseText); - var res = null; + this.log_('REST Response for ' + url + ' received. status:', xhr.status, 'response:', xhr.responseText); + let res = null; if (xhr.status >= 200 && xhr.status < 300) { try { res = jsonEval(xhr.responseText); @@ -182,4 +172,4 @@ export class ReadonlyRestClient { xhr.send(); }); } -}; // end ReadonlyRestClient +} diff --git a/src/database/core/Repo.ts b/src/database/core/Repo.ts index 00179a6b9a2..787ba1e0868 100644 --- a/src/database/core/Repo.ts +++ b/src/database/core/Repo.ts @@ -1,53 +1,62 @@ -import { +import { generateWithValues, resolveDeferredValueSnapshot, resolveDeferredValueTree -} from "./util/ServerValues"; -import { nodeFromJSON } from "./snap/nodeFromJSON"; -import { Path } from "./util/Path"; -import { SparseSnapshotTree } from "./SparseSnapshotTree"; -import { SyncTree } from "./SyncTree"; -import { SnapshotHolder } from "./SnapshotHolder"; -import { stringify } from "../../utils/json"; -import { beingCrawled, each, exceptionGuard, warn, log } from "./util/util"; -import { map, forEach } from "../../utils/obj"; -import { AuthTokenProvider } from "./AuthTokenProvider"; -import { StatsManager } from "./stats/StatsManager"; -import { StatsReporter } from "./stats/StatsReporter"; -import { StatsListener } from "./stats/StatsListener"; -import { EventQueue } from "./view/EventQueue"; -import { PersistentConnection } from "./PersistentConnection"; -import { ReadonlyRestClient } from "./ReadonlyRestClient"; -import { FirebaseApp } from "../../app/firebase_app"; -import { RepoInfo } from "./RepoInfo"; - -var INTERRUPT_REASON = 'repo_interrupt'; +} from './util/ServerValues'; +import { nodeFromJSON } from './snap/nodeFromJSON'; +import { Path } from './util/Path'; +import { SparseSnapshotTree } from './SparseSnapshotTree'; +import { SyncTree } from './SyncTree'; +import { SnapshotHolder } from './SnapshotHolder'; +import { stringify } from '../../utils/json'; +import { beingCrawled, each, exceptionGuard, warn, log } from './util/util'; +import { map, forEach, isEmpty } from '../../utils/obj'; +import { AuthTokenProvider } from './AuthTokenProvider'; +import { StatsManager } from './stats/StatsManager'; +import { StatsReporter } from './stats/StatsReporter'; +import { StatsListener } from './stats/StatsListener'; +import { EventQueue } from './view/EventQueue'; +import { PersistentConnection } from './PersistentConnection'; +import { ReadonlyRestClient } from './ReadonlyRestClient'; +import { FirebaseApp } from '../../app/firebase_app'; +import { RepoInfo } from './RepoInfo'; +import { Database } from '../api/Database'; +import { ServerActions } from './ServerActions'; +import { Query } from '../api/Query'; +import { EventRegistration } from './view/EventRegistration'; + +const INTERRUPT_REASON = 'repo_interrupt'; /** * A connection to a single data repository. */ export class Repo { - repoInfo_; - stats_; - statsListener_; - eventQueue_; - nextWriteId_; - persistentConnection_ : PersistentConnection | null; - server_; - statsReporter_; - transactions_init_; - infoData_; - infoSyncTree_: SyncTree; - onDisconnect_; - abortTransactions_; - rerunTransactions_; - /** @type {!Database} */ - database; + database: Database; + infoSyncTree_: SyncTree; dataUpdateCount; - interceptServerDataCallback_; serverSyncTree_: SyncTree; - + + private repoInfo_; + private stats_; + private statsListener_; + private eventQueue_; + private nextWriteId_; + private server_: ServerActions; + private statsReporter_; + private transactions_init_; + private infoData_; + private onDisconnect_; + private abortTransactions_; + private rerunTransactions_; + private interceptServerDataCallback_; + + /** + * TODO: This should be @private but it's used by test_access.js and internal.js + * @type {?PersistentConnection} + */ + persistentConnection_: PersistentConnection | null = null; + /** * @param {!RepoInfo} repoInfo * @param {boolean} forceRestClient @@ -55,7 +64,7 @@ export class Repo { */ constructor(repoInfo: RepoInfo, forceRestClient: boolean, public app: FirebaseApp) { /** @type {!AuthTokenProvider} */ - var authTokenProvider = new AuthTokenProvider(app); + const authTokenProvider = new AuthTokenProvider(app); this.repoInfo_ = repoInfo; this.stats_ = StatsManager.getCollection(repoInfo); @@ -64,17 +73,6 @@ export class Repo { this.eventQueue_ = new EventQueue(); this.nextWriteId_ = 1; - /** - * TODO: This should be @private but it's used by test_access.js and internal.js - * @type {?PersistentConnection} - */ - this.persistentConnection_ = null; - - /** - * @private {!ServerActions} - */ - this.server_; - if (forceRestClient || beingCrawled()) { this.server_ = new ReadonlyRestClient(this.repoInfo_, this.onDataUpdate_.bind(this), @@ -83,7 +81,7 @@ export class Repo { // Minor hack: Fire onConnect immediately, since there's no actual connection. setTimeout(this.onConnectStatus_.bind(this, true), 0); } else { - var authOverride = app.options['databaseAuthVariableOverride']; + const authOverride = app.options['databaseAuthVariableOverride']; // Validate authOverride if (typeof authOverride !== 'undefined' && authOverride !== null) { if (authOverride !== 'object') { @@ -105,29 +103,29 @@ export class Repo { this.server_ = this.persistentConnection_; } - var self = this; - authTokenProvider.addTokenChangeListener(function(token) { - self.server_.refreshAuthToken(token); + + authTokenProvider.addTokenChangeListener((token) => { + this.server_.refreshAuthToken(token); }); // In the case of multiple Repos for the same repoInfo (i.e. there are multiple Firebase.Contexts being used), // we only want to create one StatsReporter. As such, we'll report stats over the first Repo created. this.statsReporter_ = StatsManager.getOrCreateReporter(repoInfo, - function() { return new StatsReporter(this.stats_, this.server_); }.bind(this)); + () => new StatsReporter(this.stats_, this.server_)); this.transactions_init_(); // Used for .info. this.infoData_ = new SnapshotHolder(); this.infoSyncTree_ = new SyncTree({ - startListening(query, tag, currentHashFn, onComplete) { - var infoEvents = []; - var node = self.infoData_.getNode(query.path); + startListening: (query, tag, currentHashFn, onComplete) => { + let infoEvents = []; + const node = this.infoData_.getNode(query.path); // This is possibly a hack, but we have different semantics for .info endpoints. We don't raise null events // on initial data... if (!node.isEmpty()) { - infoEvents = self.infoSyncTree_.applyServerOverwrite(query.path, node); - setTimeout(function() { + infoEvents = this.infoSyncTree_.applyServerOverwrite(query.path, node); + setTimeout(() => { onComplete('ok'); }, 0); } @@ -145,16 +143,16 @@ export class Repo { this.interceptServerDataCallback_ = null; this.serverSyncTree_ = new SyncTree({ - startListening(query, tag, currentHashFn, onComplete) { - self.server_.listen(query, currentHashFn, tag, function(status, data) { - var events = onComplete(status, data); - self.eventQueue_.raiseEventsForChangedPath(query.path, events); + startListening: (query, tag, currentHashFn, onComplete) => { + this.server_.listen(query, currentHashFn, tag, (status, data) => { + const events = onComplete(status, data); + this.eventQueue_.raiseEventsForChangedPath(query.path, events); }); // No synchronous events for network-backed sync trees return []; }, - stopListening(query, tag) { - self.server_.unlisten(query, tag); + stopListening: (query, tag) => { + this.server_.unlisten(query, tag); } }); } @@ -162,23 +160,23 @@ export class Repo { /** * @return {string} The URL corresponding to the root of this Firebase. */ - toString() { + toString(): string { return (this.repoInfo_.secure ? 'https://' : 'http://') + this.repoInfo_.host; } /** * @return {!string} The namespace represented by the repo. */ - name() { + name(): string { return this.repoInfo_.namespace; } /** * @return {!number} The time in milliseconds, taking the server offset into account if we have one. */ - serverTime() { - var offsetNode = this.infoData_.getNode(new Path('.info/serverTimeOffset')); - var offset = /** @type {number} */ (offsetNode.val()) || 0; + serverTime(): number { + const offsetNode = this.infoData_.getNode(new Path('.info/serverTimeOffset')); + const offset = /** @type {number} */ (offsetNode.val()) || 0; return new Date().getTime() + offset; } @@ -186,7 +184,7 @@ export class Repo { * Generate ServerValues using some variables from the repo object. * @return {!Object} */ - generateServerValues() { + generateServerValues(): Object { return generateWithValues({ 'timestamp': this.serverTime() }); @@ -201,32 +199,28 @@ export class Repo { * @param {boolean} isMerge * @param {?number} tag */ - onDataUpdate_(pathString, data, isMerge, tag) { + private onDataUpdate_(pathString: string, data: any, isMerge: boolean, tag: number | null) { // For testing. this.dataUpdateCount++; - var path = new Path(pathString); + const path = new Path(pathString); data = this.interceptServerDataCallback_ ? this.interceptServerDataCallback_(pathString, data) : data; - var events = []; + let events = []; if (tag) { if (isMerge) { - var taggedChildren = map(/**@type {!Object.} */ (data), function(raw) { - return nodeFromJSON(raw); - }); + const taggedChildren = map(/**@type {!Object.} */ (data), (raw) => nodeFromJSON(raw)); events = this.serverSyncTree_.applyTaggedQueryMerge(path, taggedChildren, tag); } else { - var taggedSnap = nodeFromJSON(data); + const taggedSnap = nodeFromJSON(data); events = this.serverSyncTree_.applyTaggedQueryOverwrite(path, taggedSnap, tag); } } else if (isMerge) { - var changedChildren = map(/**@type {!Object.} */ (data), function(raw) { - return nodeFromJSON(raw); - }); + const changedChildren = map(/**@type {!Object.} */ (data), (raw) => nodeFromJSON(raw)); events = this.serverSyncTree_.applyServerMerge(path, changedChildren); } else { - var snap = nodeFromJSON(data); + const snap = nodeFromJSON(data); events = this.serverSyncTree_.applyServerOverwrite(path, snap); } - var affectedPath = path; + let affectedPath = path; if (events.length > 0) { // Since we have a listener outstanding for each transaction, receiving any events // is a proxy for some change having occurred. @@ -239,7 +233,7 @@ export class Repo { * @param {?function(!string, *):*} callback * @private */ - interceptServerData_(callback) { + private interceptServerData_(callback: (a: string, b: any) => any) { this.interceptServerDataCallback_ = callback; } @@ -247,7 +241,7 @@ export class Repo { * @param {!boolean} connectStatus * @private */ - onConnectStatus_(connectStatus) { + private onConnectStatus_(connectStatus: boolean) { this.updateInfo_('connected', connectStatus); if (connectStatus === false) { this.runOnDisconnectEvents_(); @@ -258,10 +252,9 @@ export class Repo { * @param {!Object} updates * @private */ - onServerInfoUpdate_(updates) { - var self = this; - each(updates, function(value, key) { - self.updateInfo_(key, value); + private onServerInfoUpdate_(updates: Object) { + each(updates, (value: any, key: string) => { + this.updateInfo_(key, value); }); } @@ -271,11 +264,11 @@ export class Repo { * @param {*} value * @private */ - updateInfo_(pathString, value) { - var path = new Path('/.info/' + pathString); - var newNode = nodeFromJSON(value); + private updateInfo_(pathString: string, value: any) { + const path = new Path('/.info/' + pathString); + const newNode = nodeFromJSON(value); this.infoData_.updateSnapshot(path, newNode); - var events = this.infoSyncTree_.applyServerOverwrite(path, newNode); + const events = this.infoSyncTree_.applyServerOverwrite(path, newNode); this.eventQueue_.raiseEventsForChangedPath(path, events); } @@ -283,7 +276,7 @@ export class Repo { * @return {!number} * @private */ - getNextWriteId_() { + private getNextWriteId_(): number { return this.nextWriteId_++; } @@ -293,30 +286,30 @@ export class Repo { * @param {number|string|null} newPriority * @param {?function(?Error, *=)} onComplete */ - setWithPriority(path, newVal, newPriority, onComplete) { + setWithPriority(path: Path, newVal: any, newPriority: number | string | null, + onComplete: ((status: Error | null, errorReason?: string) => any) | null) { this.log_('set', {path: path.toString(), value: newVal, priority: newPriority}); // TODO: Optimize this behavior to either (a) store flag to skip resolving where possible and / or // (b) store unresolved paths on JSON parse - var serverValues = this.generateServerValues(); - var newNodeUnresolved = nodeFromJSON(newVal, newPriority); - var newNode = resolveDeferredValueSnapshot(newNodeUnresolved, serverValues); + const serverValues = this.generateServerValues(); + const newNodeUnresolved = nodeFromJSON(newVal, newPriority); + const newNode = resolveDeferredValueSnapshot(newNodeUnresolved, serverValues); - var writeId = this.getNextWriteId_(); - var events = this.serverSyncTree_.applyUserOverwrite(path, newNode, writeId, true); + const writeId = this.getNextWriteId_(); + const events = this.serverSyncTree_.applyUserOverwrite(path, newNode, writeId, true); this.eventQueue_.queueEvents(events); - var self = this; - this.server_.put(path.toString(), newNodeUnresolved.val(/*export=*/true), function(status, errorReason) { - var success = status === 'ok'; + this.server_.put(path.toString(), newNodeUnresolved.val(/*export=*/true), (status, errorReason) => { + const success = status === 'ok'; if (!success) { warn('set at ' + path + ' failed: ' + status); } - var clearEvents = self.serverSyncTree_.ackUserWrite(writeId, !success); - self.eventQueue_.raiseEventsForChangedPath(path, clearEvents); - self.callOnCompleteCallback(onComplete, status, errorReason); + const clearEvents = this.serverSyncTree_.ackUserWrite(writeId, !success); + this.eventQueue_.raiseEventsForChangedPath(path, clearEvents); + this.callOnCompleteCallback(onComplete, status, errorReason); }); - var affectedPath = this.abortTransactions_(path); + const affectedPath = this.abortTransactions_(path); this.rerunTransactions_(affectedPath); // We queued the events above, so just flush the queue here this.eventQueue_.raiseEventsForChangedPath(affectedPath, []); @@ -327,42 +320,39 @@ export class Repo { * @param {!Object} childrenToMerge * @param {?function(?Error, *=)} onComplete */ - update(path: Path, childrenToMerge, onComplete) { + update(path: Path, childrenToMerge: Object, + onComplete: ((status: Error | null, errorReason?: string) => any) | null) { this.log_('update', {path: path.toString(), value: childrenToMerge}); // Start with our existing data and merge each child into it. - var empty = true; - var serverValues = this.generateServerValues(); - var changedChildren = {}; - forEach(childrenToMerge, function(changedKey, changedValue) { + let empty = true; + const serverValues = this.generateServerValues(); + const changedChildren = {}; + forEach(childrenToMerge, function (changedKey, changedValue) { empty = false; - var newNodeUnresolved = nodeFromJSON(changedValue); + const newNodeUnresolved = nodeFromJSON(changedValue); changedChildren[changedKey] = resolveDeferredValueSnapshot(newNodeUnresolved, serverValues); }); if (!empty) { - var writeId = this.getNextWriteId_(); - var events = this.serverSyncTree_.applyUserMerge(path, changedChildren, writeId); + const writeId = this.getNextWriteId_(); + const events = this.serverSyncTree_.applyUserMerge(path, changedChildren, writeId); this.eventQueue_.queueEvents(events); - var self = this; - this.server_.merge(path.toString(), childrenToMerge, function(status, errorReason) { - var success = status === 'ok'; + this.server_.merge(path.toString(), childrenToMerge, (status, errorReason) => { + const success = status === 'ok'; if (!success) { warn('update at ' + path + ' failed: ' + status); } - var clearEvents = self.serverSyncTree_.ackUserWrite(writeId, !success); - var affectedPath = path; - if (clearEvents.length > 0) { - affectedPath = self.rerunTransactions_(path); - } - self.eventQueue_.raiseEventsForChangedPath(affectedPath, clearEvents); - self.callOnCompleteCallback(onComplete, status, errorReason); + const clearEvents = this.serverSyncTree_.ackUserWrite(writeId, !success); + const affectedPath = (clearEvents.length > 0) ? this.rerunTransactions_(path) : path; + this.eventQueue_.raiseEventsForChangedPath(affectedPath, clearEvents); + this.callOnCompleteCallback(onComplete, status, errorReason); }); - forEach(childrenToMerge, function(changedPath, changedValue) { - var affectedPath = self.abortTransactions_(path.child(changedPath)); - self.rerunTransactions_(affectedPath); + forEach(childrenToMerge, (changedPath, changedValue) => { + const affectedPath = this.abortTransactions_(path.child(changedPath)); + this.rerunTransactions_(affectedPath); }); // We queued the events above, so just flush the queue here @@ -377,18 +367,17 @@ export class Repo { * Applies all of the changes stored up in the onDisconnect_ tree. * @private */ - runOnDisconnectEvents_() { + private runOnDisconnectEvents_() { this.log_('onDisconnectEvents'); - var self = this; - var serverValues = this.generateServerValues(); - var resolvedOnDisconnectTree = resolveDeferredValueTree(this.onDisconnect_, serverValues); - var events = []; + const serverValues = this.generateServerValues(); + const resolvedOnDisconnectTree = resolveDeferredValueTree(this.onDisconnect_, serverValues); + let events = []; - resolvedOnDisconnectTree.forEachTree(Path.Empty, function(path, snap) { - events = events.concat(self.serverSyncTree_.applyServerOverwrite(path, snap)); - var affectedPath = self.abortTransactions_(path); - self.rerunTransactions_(affectedPath); + resolvedOnDisconnectTree.forEachTree(Path.Empty, (path, snap) => { + events = events.concat(this.serverSyncTree_.applyServerOverwrite(path, snap)); + const affectedPath = this.abortTransactions_(path); + this.rerunTransactions_(affectedPath); }); this.onDisconnect_ = new SparseSnapshotTree(); @@ -397,60 +386,69 @@ export class Repo { /** * @param {!Path} path - * @param {?function(?Error)} onComplete + * @param {?function(?Error, *=)} onComplete */ - onDisconnectCancel(path, onComplete) { - var self = this; - this.server_.onDisconnectCancel(path.toString(), function(status, errorReason) { + onDisconnectCancel(path: Path, onComplete: ((status: Error | null, errorReason?: string) => any) | null) { + this.server_.onDisconnectCancel(path.toString(), (status, errorReason) => { if (status === 'ok') { - self.onDisconnect_.forget(path); + this.onDisconnect_.forget(path); } - self.callOnCompleteCallback(onComplete, status, errorReason); + this.callOnCompleteCallback(onComplete, status, errorReason); }); } - onDisconnectSet(path, value, onComplete) { - var self = this; - var newNode = nodeFromJSON(value); - this.server_.onDisconnectPut(path.toString(), newNode.val(/*export=*/true), function(status, errorReason) { + /** + * @param {!Path} path + * @param {*} value + * @param {?function(?Error, *=)} onComplete + */ + onDisconnectSet(path: Path, value: any, onComplete: ((status: Error | null, errorReason?: string) => any) | null) { + const newNode = nodeFromJSON(value); + this.server_.onDisconnectPut(path.toString(), newNode.val(/*export=*/true), (status, errorReason) => { if (status === 'ok') { - self.onDisconnect_.remember(path, newNode); + this.onDisconnect_.remember(path, newNode); } - self.callOnCompleteCallback(onComplete, status, errorReason); + this.callOnCompleteCallback(onComplete, status, errorReason); }); } - onDisconnectSetWithPriority(path, value, priority, onComplete) { - var self = this; - var newNode = nodeFromJSON(value, priority); - this.server_.onDisconnectPut(path.toString(), newNode.val(/*export=*/true), function(status, errorReason) { + /** + * @param {!Path} path + * @param {*} value + * @param {*} priority + * @param {?function(?Error, *=)} onComplete + */ + onDisconnectSetWithPriority(path, value, priority, onComplete: ((status: Error | null, errorReason?: string) => any) | null) { + const newNode = nodeFromJSON(value, priority); + this.server_.onDisconnectPut(path.toString(), newNode.val(/*export=*/true), (status, errorReason) => { if (status === 'ok') { - self.onDisconnect_.remember(path, newNode); + this.onDisconnect_.remember(path, newNode); } - self.callOnCompleteCallback(onComplete, status, errorReason); + this.callOnCompleteCallback(onComplete, status, errorReason); }); } - onDisconnectUpdate(path, childrenToMerge, onComplete) { - var empty = true; - for (var childName in childrenToMerge) { - empty = false; - } - if (empty) { + /** + * @param {!Path} path + * @param {*} childrenToMerge + * @param {?function(?Error, *=)} onComplete + */ + onDisconnectUpdate(path, childrenToMerge, + onComplete: ((status: Error | null, errorReason?: string) => any) | null) { + if (isEmpty(childrenToMerge)) { log('onDisconnect().update() called with empty data. Don\'t do anything.'); this.callOnCompleteCallback(onComplete, 'ok'); return; } - var self = this; - this.server_.onDisconnectMerge(path.toString(), childrenToMerge, function(status, errorReason) { + this.server_.onDisconnectMerge(path.toString(), childrenToMerge, (status, errorReason) => { if (status === 'ok') { - for (var childName in childrenToMerge) { - var newChildNode = nodeFromJSON(childrenToMerge[childName]); - self.onDisconnect_.remember(path.child(childName), newChildNode); - } + forEach(childrenToMerge, (childName: string, childNode: any) => { + const newChildNode = nodeFromJSON(childNode); + this.onDisconnect_.remember(path.child(childName), newChildNode); + }); } - self.callOnCompleteCallback(onComplete, status, errorReason); + this.callOnCompleteCallback(onComplete, status, errorReason); }); } @@ -458,8 +456,8 @@ export class Repo { * @param {!Query} query * @param {!EventRegistration} eventRegistration */ - addEventCallbackForQuery(query, eventRegistration) { - var events; + addEventCallbackForQuery(query: Query, eventRegistration: EventRegistration) { + let events; if (query.path.getFront() === '.info') { events = this.infoSyncTree_.addEventRegistration(query, eventRegistration); } else { @@ -472,10 +470,10 @@ export class Repo { * @param {!Query} query * @param {?EventRegistration} eventRegistration */ - removeEventCallbackForQuery(query, eventRegistration) { + removeEventCallbackForQuery(query: Query, eventRegistration: EventRegistration) { // These are guaranteed not to raise events, since we're not passing in a cancelError. However, we can future-proof // a little bit by handling the return values anyways. - var events; + let events; if (query.path.getFront() === '.info') { events = this.infoSyncTree_.removeEventRegistration(query, eventRegistration); } else { @@ -496,11 +494,11 @@ export class Repo { } } - stats(showDelta) { + stats(showDelta: boolean = false) { if (typeof console === 'undefined') return; - var stats; + let stats; if (showDelta) { if (!this.statsListener_) this.statsListener_ = new StatsListener(this.stats_); @@ -509,18 +507,17 @@ export class Repo { stats = this.stats_.get(); } - var longestName = Object.keys(stats).reduce( - function(previousValue, currentValue, index, array) { - return Math.max(currentValue.length, previousValue); - }, 0); + const longestName = Object.keys(stats).reduce( + function (previousValue, currentValue, index, array) { + return Math.max(currentValue.length, previousValue); + }, 0); - for (var stat in stats) { - var value = stats[stat]; + forEach(stats, (stat, value) => { // pad stat names to be the same length (plus 2 extra spaces). - for (var i = stat.length; i < longestName + 2; i++) + for (let i = stat.length; i < longestName + 2; i++) stat += ' '; console.log(stat + value); - } + }); } statsIncrementCounter(metric) { @@ -532,12 +529,12 @@ export class Repo { * @param {...*} var_args * @private */ - log_(...var_args) { - var prefix = ''; + private log_(...var_args: any[]) { + let prefix = ''; if (this.persistentConnection_) { prefix = this.persistentConnection_.id + ':'; } - log(prefix, arguments); + log(prefix, var_args); } /** @@ -545,22 +542,24 @@ export class Repo { * @param {!string} status * @param {?string=} errorReason */ - callOnCompleteCallback(callback, status, errorReason?) { + callOnCompleteCallback(callback: ((status: Error | null, errorReason?: string) => any) | null, + status: string, errorReason?: string | null) { if (callback) { - exceptionGuard(function() { + exceptionGuard(function () { if (status == 'ok') { callback(null); } else { - var code = (status || 'error').toUpperCase(); - var message = code; + const code = (status || 'error').toUpperCase(); + let message = code; if (errorReason) message += ': ' + errorReason; - var error = new Error(message); + const error = new Error(message); (error as any).code = code; callback(error); } }); } } -}; // end Repo +} + diff --git a/src/database/core/RepoInfo.ts b/src/database/core/RepoInfo.ts index 6f53a71eef2..96fd5567896 100644 --- a/src/database/core/RepoInfo.ts +++ b/src/database/core/RepoInfo.ts @@ -12,13 +12,13 @@ import { CONSTANTS } from "../realtime/Constants"; * @constructor */ export class RepoInfo { - host - domain - secure - namespace - webSocketOnly - persistenceKey - internalHost + host; + domain; + secure; + namespace; + webSocketOnly; + persistenceKey; + internalHost; constructor(host, secure, namespace, webSocketOnly, persistenceKey?) { this.host = host.toLowerCase(); diff --git a/src/database/core/RepoManager.ts b/src/database/core/RepoManager.ts index 1a928a6c2f8..e7066769fe2 100644 --- a/src/database/core/RepoManager.ts +++ b/src/database/core/RepoManager.ts @@ -5,6 +5,7 @@ import { fatal } from "./util/util"; import { parseRepoInfo } from "./util/libs/parser"; import { validateUrl } from "./util/validation"; import "./Repo_transaction"; +import { Database } from '../api/Database'; /** @const {string} */ var DATABASE_URL_OPTION = 'databaseURL'; @@ -54,7 +55,7 @@ export class RepoManager { * @param {!App} app * @return {!Database} */ - databaseFromApp(app: FirebaseApp) { + databaseFromApp(app: FirebaseApp): Database { var dbUrl: string = app.options[DATABASE_URL_OPTION]; if (dbUrl === undefined) { fatal("Can't determine Firebase Database URL. Be sure to include " + diff --git a/src/database/core/ServerActions.ts b/src/database/core/ServerActions.ts index cfc960adb39..e593a55487f 100644 --- a/src/database/core/ServerActions.ts +++ b/src/database/core/ServerActions.ts @@ -1,72 +1,74 @@ +import { Query } from '../api/Query'; + /** * Interface defining the set of actions that can be performed against the Firebase server * (basically corresponds to our wire protocol). * * @interface */ -export class ServerActions { +export interface ServerActions { /** - * @param {!fb.api.Query} query + * @param {!Query} query * @param {function():string} currentHashFn * @param {?number} tag * @param {function(string, *)} onComplete */ - listen() {} + listen(query: Query, currentHashFn: () => string, tag: number | null, onComplete: (a: string, b: any) => any); /** * Remove a listen. * - * @param {!fb.api.Query} query + * @param {!Query} query * @param {?number} tag */ - unlisten() {} + unlisten(query: Query, tag: number | null); /** * @param {string} pathString * @param {*} data - * @param {function(string, string)=} opt_onComplete - * @param {string=} opt_hash + * @param {function(string, string)=} onComplete + * @param {string=} hash */ - put() {} + put(pathString: string, data: any, onComplete?: (a: string, b: string) => any, hash?: string); /** * @param {string} pathString * @param {*} data * @param {function(string, ?string)} onComplete - * @param {string=} opt_hash + * @param {string=} hash */ - merge() {} + merge(pathString: string, data: any, onComplete: (a: string, b: string | null) => any, hash?: string); /** * Refreshes the auth token for the current connection. * @param {string} token The authentication token */ - refreshAuthToken() {} + refreshAuthToken(token: string); /** * @param {string} pathString * @param {*} data - * @param {function(string, string)=} opt_onComplete + * @param {function(string, string)=} onComplete */ - onDisconnectPut() {} + onDisconnectPut(pathString: string, data: any, onComplete?: (a: string, b: string) => any); /** * @param {string} pathString * @param {*} data - * @param {function(string, string)=} opt_onComplete + * @param {function(string, string)=} onComplete */ - onDisconnectMerge() {} + onDisconnectMerge(pathString: string, data: any, onComplete?: (a: string, b: string) => any); /** * @param {string} pathString - * @param {function(string, string)=} opt_onComplete + * @param {function(string, string)=} onComplete */ - onDisconnectCancel() {} + onDisconnectCancel(pathString: string, onComplete?: (a: string, b: string) => any); /** * @param {Object.} stats */ - reportStats() {} + reportStats(stats: { [k: string]: any }); -}; // fb.core.ServerActions +} diff --git a/src/database/core/operation/AckUserWrite.ts b/src/database/core/operation/AckUserWrite.ts index 2921b9d2122..cc7da8cde60 100644 --- a/src/database/core/operation/AckUserWrite.ts +++ b/src/database/core/operation/AckUserWrite.ts @@ -1,51 +1,42 @@ import { assert } from "../../../utils/assert"; import { Path } from "../util/Path"; -import { OperationSource, OperationType } from "./Operation"; +import { Operation, OperationSource, OperationType } from './Operation'; +import { ImmutableTree } from '../util/ImmutableTree'; -/** - * - * @param {!Path} path - * @param {!ImmutableTree} affectedTree - * @param {!boolean} revert - * @constructor - * @implements {fb.core.Operation} - */ -export const AckUserWrite = function(path, affectedTree, revert) { +export class AckUserWrite implements Operation { /** @inheritDoc */ - this.type = OperationType.ACK_USER_WRITE; + type = OperationType.ACK_USER_WRITE; /** @inheritDoc */ - this.source = OperationSource.User; - - /** @inheritDoc */ - this.path = path; + source = OperationSource.User; /** - * A tree containing true for each affected path. Affected paths can't overlap. - * @type {!util.ImmutableTree.} + * + * @param {!Path} path + * @param {!ImmutableTree} affectedTree A tree containing true for each affected path. Affected paths can't overlap. + * @param {!boolean} revert */ - this.affectedTree = affectedTree; + constructor(/**@inheritDoc */ public path: Path, + /**@inheritDoc */ public affectedTree: ImmutableTree, + /**@inheritDoc */ public revert: boolean) { + + } /** - * @type {boolean} + * @inheritDoc */ - this.revert = revert; -}; - -/** - * @inheritDoc - */ -AckUserWrite.prototype.operationForChild = function(childName) { - if (!this.path.isEmpty()) { - assert(this.path.getFront() === childName, 'operationForChild called for unrelated child.'); - return new AckUserWrite(this.path.popFront(), this.affectedTree, this.revert); - } else if (this.affectedTree.value != null) { - assert(this.affectedTree.children.isEmpty(), + operationForChild(childName: string): AckUserWrite { + if (!this.path.isEmpty()) { + assert(this.path.getFront() === childName, 'operationForChild called for unrelated child.'); + return new AckUserWrite(this.path.popFront(), this.affectedTree, this.revert); + } else if (this.affectedTree.value != null) { + assert(this.affectedTree.children.isEmpty(), 'affectedTree should not have overlapping affected paths.'); - // All child locations are affected as well; just return same operation. - return this; - } else { - var childTree = this.affectedTree.subtree(new Path(childName)); - return new AckUserWrite(Path.Empty, childTree, this.revert); + // All child locations are affected as well; just return same operation. + return this; + } else { + const childTree = this.affectedTree.subtree(new Path(childName)); + return new AckUserWrite(Path.Empty, childTree, this.revert); + } } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/database/core/operation/ListenComplete.ts b/src/database/core/operation/ListenComplete.ts index 3b1e2823418..0a9ab05858f 100644 --- a/src/database/core/operation/ListenComplete.ts +++ b/src/database/core/operation/ListenComplete.ts @@ -1,28 +1,20 @@ import { Path } from "../util/Path"; -import { OperationType } from "./Operation"; +import { Operation, OperationSource, OperationType } from './Operation'; /** - * @param {!fb.core.OperationSource} source + * @param {!OperationSource} source * @param {!Path} path * @constructor - * @implements {fb.core.Operation} + * @implements {Operation} */ -export class ListenComplete { - type; - source; - path; - - constructor(source, path) { - /** @inheritDoc */ - this.type = OperationType.LISTEN_COMPLETE; +export class ListenComplete implements Operation { + /** @inheritDoc */ + type = OperationType.LISTEN_COMPLETE; - /** @inheritDoc */ - this.source = source; - - /** @inheritDoc */ - this.path = path; + constructor(public source: OperationSource, public path: Path) { } - operationForChild(childName) { + + operationForChild(childName: string): ListenComplete { if (this.path.isEmpty()) { return new ListenComplete(this.source, Path.Empty); } else { diff --git a/src/database/core/operation/Merge.ts b/src/database/core/operation/Merge.ts index 9b817bd0d5e..9d1578a6785 100644 --- a/src/database/core/operation/Merge.ts +++ b/src/database/core/operation/Merge.ts @@ -1,57 +1,52 @@ -import { OperationType } from "./Operation"; +import { Operation, OperationSource, OperationType } from './Operation'; import { Overwrite } from "./Overwrite"; import { Path } from "../util/Path"; import { assert } from "../../../utils/assert"; +import { ImmutableTree } from '../util/ImmutableTree'; /** - * @param {!fb.core.OperationSource} source + * @param {!OperationSource} source * @param {!Path} path - * @param {!fb.core.util.ImmutableTree.} children + * @param {!ImmutableTree.} children * @constructor - * @implements {fb.core.Operation} + * @implements {Operation} */ -export const Merge = function(source, path, children) { +export class Merge implements Operation { /** @inheritDoc */ - this.type = OperationType.MERGE; + type = OperationType.MERGE; - /** @inheritDoc */ - this.source = source; - - /** @inheritDoc */ - this.path = path; + constructor(/**@inheritDoc */ public source: OperationSource, + /**@inheritDoc */ public path: Path, + /**@inheritDoc */ public children: ImmutableTree) { + } /** - * @type {!fb.core.util.ImmutableTree.} + * @inheritDoc */ - this.children = children; -}; - -/** - * @inheritDoc - */ -Merge.prototype.operationForChild = function(childName) { - if (this.path.isEmpty()) { - var childTree = this.children.subtree(new Path(childName)); - if (childTree.isEmpty()) { - // This child is unaffected - return null; - } else if (childTree.value) { - // We have a snapshot for the child in question. This becomes an overwrite of the child. - return new Overwrite(this.source, Path.Empty, childTree.value); + operationForChild(childName: string): Operation { + if (this.path.isEmpty()) { + const childTree = this.children.subtree(new Path(childName)); + if (childTree.isEmpty()) { + // This child is unaffected + return null; + } else if (childTree.value) { + // We have a snapshot for the child in question. This becomes an overwrite of the child. + return new Overwrite(this.source, Path.Empty, childTree.value); + } else { + // This is a merge at a deeper level + return new Merge(this.source, Path.Empty, childTree); + } } else { - // This is a merge at a deeper level - return new Merge(this.source, Path.Empty, childTree); + assert(this.path.getFront() === childName, + 'Can\'t get a merge for a child not on the path of the operation'); + return new Merge(this.source, this.path.popFront(), this.children); } - } else { - assert(this.path.getFront() === childName, - 'Can\'t get a merge for a child not on the path of the operation'); - return new Merge(this.source, this.path.popFront(), this.children); } -}; -/** - * @inheritDoc - */ -Merge.prototype.toString = function() { - return 'Operation(' + this.path + ': ' + this.source.toString() + ' merge: ' + this.children.toString() + ')'; -}; \ No newline at end of file + /** + * @inheritDoc + */ + toString(): string { + return 'Operation(' + this.path + ': ' + this.source.toString() + ' merge: ' + this.children.toString() + ')'; + } +} \ No newline at end of file diff --git a/src/database/core/operation/Operation.ts b/src/database/core/operation/Operation.ts index e6fa841a3b3..090763b634c 100644 --- a/src/database/core/operation/Operation.ts +++ b/src/database/core/operation/Operation.ts @@ -1,42 +1,42 @@ import { assert } from "../../../utils/assert"; +import { Path } from '../util/Path'; /** * * @enum */ -export const OperationType = { - OVERWRITE: 0, - MERGE: 1, - ACK_USER_WRITE: 2, - LISTEN_COMPLETE: 3 -}; +export enum OperationType { + OVERWRITE, + MERGE, + ACK_USER_WRITE, + LISTEN_COMPLETE +} /** * @interface */ -export const Operation = function() { }; - -/** - * @type {!OperationSource} - */ -Operation.prototype.source; - -/** - * @type {!OperationType} - */ -Operation.prototype.type; +export interface Operation { + /** + * @type {!OperationSource} + */ + source: OperationSource; -/** - * @type {!Path} - */ -Operation.prototype.path; + /** + * @type {!OperationType} + */ + type: OperationType; -/** - * @param {string} childName - * @return {?Operation} - */ -Operation.prototype.operationForChild = () => {}; + /** + * @type {!Path} + */ + path: Path; + /** + * @param {string} childName + * @return {?Operation} + */ + operationForChild(childName: string): Operation | null; +} /** * @param {boolean} fromUser @@ -46,16 +46,10 @@ Operation.prototype.operationForChild = () => {}; * @constructor */ export class OperationSource { - fromUser; - fromServer; - queryId; - tagged; - - constructor(fromUser, fromServer, queryId, tagged) { - this.fromUser = fromUser; - this.fromServer = fromServer; - this.queryId = queryId; - this.tagged = tagged; + constructor(public fromUser: boolean, + public fromServer: boolean, + public queryId: string | null, + public tagged: boolean) { assert(!tagged || fromServer, 'Tagged queries must be from server.'); } /** diff --git a/src/database/core/operation/Overwrite.ts b/src/database/core/operation/Overwrite.ts index ba4af3c5215..1af32ce1d26 100644 --- a/src/database/core/operation/Overwrite.ts +++ b/src/database/core/operation/Overwrite.ts @@ -1,5 +1,6 @@ -import { OperationType } from "./Operation"; +import { Operation, OperationSource, OperationType } from './Operation'; import { Path } from "../util/Path"; +import { Node } from '../snap/Node'; /** * @param {!OperationSource} source @@ -8,27 +9,16 @@ import { Path } from "../util/Path"; * @constructor * @implements {Operation} */ -export class Overwrite { +export class Overwrite implements Operation { /** @inheritDoc */ - type; + type = OperationType.OVERWRITE; - /** @inheritDoc */ - source; - - /** @inheritDoc */ - path; - /** - * @type {!Node} - */ - snap; - - constructor(source, path, snap) { - this.type = OperationType.OVERWRITE; - this.source = source; - this.path = path; - this.snap = snap; + constructor(public source: OperationSource, + public path: Path, + public snap: Node) { } - operationForChild(childName) { + + operationForChild(childName: string): Overwrite { if (this.path.isEmpty()) { return new Overwrite(this.source, Path.Empty, this.snap.getImmediateChild(childName)); diff --git a/src/database/core/stats/StatsListener.ts b/src/database/core/stats/StatsListener.ts index 4ede60b9ffc..9b829256f5d 100644 --- a/src/database/core/stats/StatsListener.ts +++ b/src/database/core/stats/StatsListener.ts @@ -1,27 +1,25 @@ -import { clone } from "../../../utils/obj"; +import { clone, forEach } from '../../../utils/obj'; /** * Returns the delta from the previous call to get stats. * - * @param collection The collection to "listen" to. + * @param collection_ The collection to "listen" to. * @constructor */ export class StatsListener { - collection_; - last_; + private last_ = null; - constructor(collection) { - this.collection_ = collection; - this.last_ = null; + constructor(private collection_) { } + get() { - var newStats = this.collection_.get(); + const newStats = this.collection_.get(); - var delta = clone(newStats); + const delta = clone(newStats); if (this.last_) { - for (var stat in this.last_) { - delta[stat] = delta[stat] - this.last_[stat]; - } + forEach(this.last_, (stat, value) => { + delta[stat] = delta[stat] - value; + }); } this.last_ = newStats; diff --git a/src/database/core/stats/StatsReporter.ts b/src/database/core/stats/StatsReporter.ts index d8040b10181..daa88fd1a0d 100644 --- a/src/database/core/stats/StatsReporter.ts +++ b/src/database/core/stats/StatsReporter.ts @@ -1,50 +1,54 @@ -import { contains } from "../../../utils/obj"; +import { contains, forEach } from '../../../utils/obj'; import { setTimeoutNonBlocking } from "../util/util"; import { StatsListener } from "./StatsListener"; // Assuming some apps may have a short amount of time on page, and a bulk of firebase operations probably // happen on page load, we try to report our first set of stats pretty quickly, but we wait at least 10 // seconds to try to ensure the Firebase connection is established / settled. -var FIRST_STATS_MIN_TIME = 10 * 1000; -var FIRST_STATS_MAX_TIME = 30 * 1000; +const FIRST_STATS_MIN_TIME = 10 * 1000; +const FIRST_STATS_MAX_TIME = 30 * 1000; // We'll continue to report stats on average every 5 minutes. -var REPORT_STATS_INTERVAL = 5 * 60 * 1000; +const REPORT_STATS_INTERVAL = 5 * 60 * 1000; /** * * @param collection - * @param connection + * @param server_ * @constructor */ -export const StatsReporter = function(collection, connection) { - this.statsToReport_ = {}; - this.statsListener_ = new StatsListener(collection); - this.server_ = connection; - - var timeout = FIRST_STATS_MIN_TIME + (FIRST_STATS_MAX_TIME - FIRST_STATS_MIN_TIME) * Math.random(); - setTimeoutNonBlocking(this.reportStats_.bind(this), Math.floor(timeout)); -}; - -StatsReporter.prototype.includeStat = function(stat) { - this.statsToReport_[stat] = true; -}; - -StatsReporter.prototype.reportStats_ = function() { - var stats = this.statsListener_.get(); - var reportedStats = { }; - var haveStatsToReport = false; - for (var stat in stats) { - if (stats[stat] > 0 && contains(this.statsToReport_, stat)) { - reportedStats[stat] = stats[stat]; - haveStatsToReport = true; - } +export class StatsReporter { + private statsListener_; + private statsToReport_ = {}; + + constructor(collection, private server_: any) { + this.statsListener_ = new StatsListener(collection); + + const timeout = FIRST_STATS_MIN_TIME + (FIRST_STATS_MAX_TIME - FIRST_STATS_MIN_TIME) * Math.random(); + setTimeoutNonBlocking(this.reportStats_.bind(this), Math.floor(timeout)); } - if (haveStatsToReport) { - this.server_.reportStats(reportedStats); + includeStat(stat) { + this.statsToReport_[stat] = true; } - // queue our next run. - setTimeoutNonBlocking(this.reportStats_.bind(this), Math.floor(Math.random() * 2 * REPORT_STATS_INTERVAL)); -}; + private reportStats_() { + const stats = this.statsListener_.get(); + const reportedStats = {}; + let haveStatsToReport = false; + + forEach(stats, (stat, value) => { + if (value > 0 && contains(this.statsToReport_, stat)) { + reportedStats[stat] = value; + haveStatsToReport = true; + } + }); + + if (haveStatsToReport) { + this.server_.reportStats(reportedStats); + } + + // queue our next run. + setTimeoutNonBlocking(this.reportStats_.bind(this), Math.floor(Math.random() * 2 * REPORT_STATS_INTERVAL)); + } +} diff --git a/src/database/core/view/CacheNode.ts b/src/database/core/view/CacheNode.ts index ecd5970cc94..db24e74a7c7 100644 --- a/src/database/core/view/CacheNode.ts +++ b/src/database/core/view/CacheNode.ts @@ -1,74 +1,66 @@ +import { Node } from '../snap/Node'; +import { Path } from '../util/Path'; + /** * A cache node only stores complete children. Additionally it holds a flag whether the node can be considered fully * initialized in the sense that we know at one point in time this represented a valid state of the world, e.g. * initialized with data from the server, or a complete overwrite by the client. The filtered flag also tracks * whether a node potentially had children removed due to a filter. - * - * @param {!fb.core.snap.Node} node - * @param {boolean} fullyInitialized - * @param {boolean} filtered - * @constructor */ -export const CacheNode = function(node, fullyInitialized, filtered) { +export class CacheNode { /** - * @type {!fb.core.snap.Node} - * @private + * @param {!Node} node_ + * @param {boolean} fullyInitialized_ + * @param {boolean} filtered_ */ - this.node_ = node; + constructor(private node_: Node, + private fullyInitialized_: boolean, + private filtered_: boolean) { + + } /** - * @type {boolean} - * @private + * Returns whether this node was fully initialized with either server data or a complete overwrite by the client + * @return {boolean} */ - this.fullyInitialized_ = fullyInitialized; + isFullyInitialized(): boolean { + return this.fullyInitialized_; + } /** - * @type {boolean} - * @private + * Returns whether this node is potentially missing children due to a filter applied to the node + * @return {boolean} */ - this.filtered_ = filtered; -}; - -/** - * Returns whether this node was fully initialized with either server data or a complete overwrite by the client - * @return {boolean} - */ -CacheNode.prototype.isFullyInitialized = function() { - return this.fullyInitialized_; -}; + isFiltered(): boolean { + return this.filtered_; + } -/** - * Returns whether this node is potentially missing children due to a filter applied to the node - * @return {boolean} - */ -CacheNode.prototype.isFiltered = function() { - return this.filtered_; -}; + /** + * @param {!Path} path + * @return {boolean} + */ + isCompleteForPath(path: Path): boolean { + if (path.isEmpty()) { + return this.isFullyInitialized() && !this.filtered_; + } -/** - * @param {!fb.core.util.Path} path - * @return {boolean} - */ -CacheNode.prototype.isCompleteForPath = function(path) { - if (path.isEmpty()) { - return this.isFullyInitialized() && !this.filtered_; - } else { - var childKey = path.getFront(); + const childKey = path.getFront(); return this.isCompleteForChild(childKey); } -}; -/** - * @param {!string} key - * @return {boolean} - */ -CacheNode.prototype.isCompleteForChild = function(key) { - return (this.isFullyInitialized() && !this.filtered_) || this.node_.hasChild(key); -}; + /** + * @param {!string} key + * @return {boolean} + */ + isCompleteForChild(key: string): boolean { + return (this.isFullyInitialized() && !this.filtered_) || this.node_.hasChild(key); + } -/** - * @return {!fb.core.snap.Node} - */ -CacheNode.prototype.getNode = function() { - return this.node_; -}; + /** + * @return {!Node} + */ + getNode(): Node { + return this.node_; + } + +} diff --git a/src/database/core/view/Change.ts b/src/database/core/view/Change.ts index c04e486cd24..8f9396849a4 100644 --- a/src/database/core/view/Change.ts +++ b/src/database/core/view/Change.ts @@ -1,102 +1,80 @@ +import { Node } from '../snap/Node'; + /** * @constructor * @struct * @param {!string} type The event type - * @param {!fb.core.snap.Node} snapshotNode The data - * @param {string=} opt_childName The name for this child, if it's a child event - * @param {fb.core.snap.Node=} opt_oldSnap Used for intermediate processing of child changed events - * @param {string=} opt_prevName The name for the previous child, if applicable + * @param {!Node} snapshotNode The data + * @param {string=} childName The name for this child, if it's a child event + * @param {Node=} oldSnap Used for intermediate processing of child changed events + * @param {string=} prevName The name for the previous child, if applicable */ export class Change { - childName; - oldSnap; - prevName; - snapshotNode; - type; - - constructor(type, snapshotNode, opt_childName?, opt_oldSnap?, opt_prevName?) { - this.type = type; - this.snapshotNode = snapshotNode; - this.childName = opt_childName; - this.oldSnap = opt_oldSnap; - this.prevName = opt_prevName; + constructor(public type: string, + public snapshotNode: Node, + public childName?: string, + public oldSnap?: Node, + public prevName?: string) { }; + /** - * @param {!fb.core.snap.Node} snapshot + * @param {!Node} snapshot * @return {!Change} */ - static valueChange(snapshot) { + static valueChange(snapshot: Node): Change { return new Change(Change.VALUE, snapshot); }; - /** * @param {string} childKey - * @param {!fb.core.snap.Node} snapshot + * @param {!Node} snapshot * @return {!Change} */ - static childAddedChange(childKey, snapshot) { + static childAddedChange(childKey: string, snapshot: Node): Change { return new Change(Change.CHILD_ADDED, snapshot, childKey); }; - /** * @param {string} childKey - * @param {!fb.core.snap.Node} snapshot + * @param {!Node} snapshot * @return {!Change} */ - static childRemovedChange(childKey, snapshot) { + static childRemovedChange(childKey: string, snapshot: Node): Change { return new Change(Change.CHILD_REMOVED, snapshot, childKey); }; - /** * @param {string} childKey - * @param {!fb.core.snap.Node} newSnapshot - * @param {!fb.core.snap.Node} oldSnapshot + * @param {!Node} newSnapshot + * @param {!Node} oldSnapshot * @return {!Change} */ - static childChangedChange(childKey, newSnapshot, oldSnapshot) { + static childChangedChange(childKey: string, newSnapshot: Node, oldSnapshot: Node): Change { return new Change(Change.CHILD_CHANGED, newSnapshot, childKey, oldSnapshot); }; - /** * @param {string} childKey - * @param {!fb.core.snap.Node} snapshot + * @param {!Node} snapshot * @return {!Change} */ - static childMovedChange(childKey, snapshot) { + static childMovedChange(childKey: string, snapshot: Node): Change { return new Change(Change.CHILD_MOVED, snapshot, childKey); }; - - /** - * @param {string} prevName - * @return {!Change} - */ - changeWithPrevName(prevName) { - return new Change(this.type, this.snapshotNode, this.childName, this.oldSnap, prevName); - }; - - //event types /** Event type for a child added */ static CHILD_ADDED = 'child_added'; - /** Event type for a child removed */ static CHILD_REMOVED = 'child_removed'; - /** Event type for a child changed */ static CHILD_CHANGED = 'child_changed'; - /** Event type for a child moved */ static CHILD_MOVED = 'child_moved'; - /** Event type for a value change */ static VALUE = 'value'; } diff --git a/src/database/core/view/ChildChangeAccumulator.ts b/src/database/core/view/ChildChangeAccumulator.ts index 8a93059a606..ae082bf2c2f 100644 --- a/src/database/core/view/ChildChangeAccumulator.ts +++ b/src/database/core/view/ChildChangeAccumulator.ts @@ -7,31 +7,32 @@ import { assert, assertionError } from "../../../utils/assert"; */ export class ChildChangeAccumulator { changeMap_ = {}; + /** * @param {!Change} change */ - trackChildChange(change) { - var type = change.type; - var childKey = /** @type {!string} */ (change.childName); + trackChildChange(change: Change) { + const type = change.type; + const childKey = /** @type {!string} */ (change.childName); assert(type == Change.CHILD_ADDED || type == Change.CHILD_CHANGED || type == Change.CHILD_REMOVED, 'Only child changes supported for tracking'); assert(childKey !== '.priority', 'Only non-priority child changes can be tracked.'); - var oldChange = safeGet(this.changeMap_, childKey); + const oldChange = safeGet(this.changeMap_, childKey); if (oldChange) { - var oldType = oldChange.type; + const oldType = oldChange.type; if (type == Change.CHILD_ADDED && oldType == Change.CHILD_REMOVED) { this.changeMap_[childKey] = Change.childChangedChange(childKey, change.snapshotNode, oldChange.snapshotNode); } else if (type == Change.CHILD_REMOVED && oldType == Change.CHILD_ADDED) { delete this.changeMap_[childKey]; } else if (type == Change.CHILD_REMOVED && oldType == Change.CHILD_CHANGED) { this.changeMap_[childKey] = Change.childRemovedChange(childKey, - /** @type {!fb.core.snap.Node} */ (oldChange.oldSnap)); + /** @type {!Node} */ (oldChange.oldSnap)); } else if (type == Change.CHILD_CHANGED && oldType == Change.CHILD_ADDED) { this.changeMap_[childKey] = Change.childAddedChange(childKey, change.snapshotNode); } else if (type == Change.CHILD_CHANGED && oldType == Change.CHILD_CHANGED) { this.changeMap_[childKey] = Change.childChangedChange(childKey, change.snapshotNode, - /** @type {!fb.core.snap.Node} */ (oldChange.oldSnap)); + /** @type {!Node} */ (oldChange.oldSnap)); } else { throw assertionError('Illegal combination of changes: ' + change + ' occurred after ' + oldChange); } @@ -44,7 +45,7 @@ export class ChildChangeAccumulator { /** * @return {!Array.} */ - getChanges() { + getChanges(): Change[] { return getValues(this.changeMap_); }; } diff --git a/src/database/core/view/CompleteChildSource.ts b/src/database/core/view/CompleteChildSource.ts index 6170e547a0d..18f5602614d 100644 --- a/src/database/core/view/CompleteChildSource.ts +++ b/src/database/core/view/CompleteChildSource.ts @@ -1,4 +1,8 @@ -import { CacheNode } from "./CacheNode"; +import { CacheNode } from './CacheNode'; +import { NamedNode, Node } from '../snap/Node'; +import { Index } from '../snap/indexes/Index'; +import { WriteTreeRef } from '../WriteTree'; +import { ViewCache } from './ViewCache'; /** * Since updates to filtered nodes might require nodes to be pulled in from "outside" the node, this interface @@ -8,21 +12,21 @@ import { CacheNode } from "./CacheNode"; * * @interface */ -export const CompleteChildSource = function() { }; - -/** - * @param {!string} childKey - * @return {?fb.core.snap.Node} - */ -CompleteChildSource.prototype.getCompleteChild = function(childKey) { }; +export interface CompleteChildSource { + /** + * @param {!string} childKey + * @return {?Node} + */ + getCompleteChild(childKey: string): Node | null; -/** - * @param {!fb.core.snap.Index} index - * @param {!fb.core.snap.NamedNode} child - * @param {boolean} reverse - * @return {?fb.core.snap.NamedNode} - */ -CompleteChildSource.prototype.getChildAfterChild = function(index, child, reverse) { }; + /** + * @param {!Index} index + * @param {!NamedNode} child + * @param {boolean} reverse + * @return {?NamedNode} + */ + getChildAfterChild(index: Index, child: NamedNode, reverse: boolean): NamedNode | null; +} /** @@ -32,22 +36,23 @@ CompleteChildSource.prototype.getChildAfterChild = function(index, child, revers * @constructor * @implements CompleteChildSource */ -const NoCompleteChildSource_ = function() { -}; +export class NoCompleteChildSource_ implements CompleteChildSource { -/** - * @inheritDoc - */ -NoCompleteChildSource_.prototype.getCompleteChild = function() { - return null; -}; + /** + * @inheritDoc + */ + getCompleteChild() { + return null; + } + + /** + * @inheritDoc + */ + getChildAfterChild() { + return null; + } +} -/** - * @inheritDoc - */ -NoCompleteChildSource_.prototype.getChildAfterChild = function() { - return null; -}; /** * Singleton instance. @@ -61,57 +66,45 @@ export const NO_COMPLETE_CHILD_SOURCE = new NoCompleteChildSource_(); * An implementation of CompleteChildSource that uses a WriteTree in addition to any other server data or * old event caches available to calculate complete children. * - * @param {!fb.core.WriteTreeRef} writes - * @param {!fb.core.view.ViewCache} viewCache - * @param {?fb.core.snap.Node} optCompleteServerCache * - * @constructor * @implements CompleteChildSource */ -export const WriteTreeCompleteChildSource = function(writes, viewCache, optCompleteServerCache) { +export class WriteTreeCompleteChildSource implements CompleteChildSource { /** - * @type {!fb.core.WriteTreeRef} - * @private + * @param {!WriteTreeRef} writes_ + * @param {!ViewCache} viewCache_ + * @param {?Node} optCompleteServerCache_ */ - this.writes_ = writes; - - /** - * @type {!fb.core.view.ViewCache} - * @private - */ - this.viewCache_ = viewCache; + constructor(private writes_: WriteTreeRef, + private viewCache_: ViewCache, + private optCompleteServerCache_: Node | null = null) { + } /** - * @type {?fb.core.snap.Node} - * @private + * @inheritDoc */ - this.optCompleteServerCache_ = optCompleteServerCache; -}; - -/** - * @inheritDoc - */ -WriteTreeCompleteChildSource.prototype.getCompleteChild = function(childKey) { - var node = this.viewCache_.getEventCache(); - if (node.isCompleteForChild(childKey)) { - return node.getNode().getImmediateChild(childKey); - } else { - var serverNode = this.optCompleteServerCache_ != null ? + getCompleteChild(childKey) { + const node = this.viewCache_.getEventCache(); + if (node.isCompleteForChild(childKey)) { + return node.getNode().getImmediateChild(childKey); + } else { + const serverNode = this.optCompleteServerCache_ != null ? new CacheNode(this.optCompleteServerCache_, true, false) : this.viewCache_.getServerCache(); - return this.writes_.calcCompleteChild(childKey, serverNode); + return this.writes_.calcCompleteChild(childKey, serverNode); + } } -}; -/** - * @inheritDoc - */ -WriteTreeCompleteChildSource.prototype.getChildAfterChild = function(index, child, reverse) { - var completeServerData = this.optCompleteServerCache_ != null ? this.optCompleteServerCache_ : + /** + * @inheritDoc + */ + getChildAfterChild(index, child, reverse) { + const completeServerData = this.optCompleteServerCache_ != null ? this.optCompleteServerCache_ : this.viewCache_.getCompleteServerSnap(); - var nodes = this.writes_.calcIndexedSlice(completeServerData, child, 1, reverse, index); - if (nodes.length === 0) { - return null; - } else { - return nodes[0]; + const nodes = this.writes_.calcIndexedSlice(completeServerData, child, 1, reverse, index); + if (nodes.length === 0) { + return null; + } else { + return nodes[0]; + } } -}; +} diff --git a/src/database/core/view/Event.ts b/src/database/core/view/Event.ts index 4109e1dac9a..a18729e1538 100644 --- a/src/database/core/view/Event.ts +++ b/src/database/core/view/Event.ts @@ -1,134 +1,124 @@ -import { stringify } from "../../../utils/json"; +import { stringify } from '../../../utils/json'; +import { Path } from '../util/Path'; +import { EventRegistration } from './EventRegistration'; +import { DataSnapshot } from '../../api/DataSnapshot'; /** * Encapsulates the data needed to raise an event * @interface */ -export const Event = function() {}; +export interface Event { + /** + * @return {!Path} + */ + getPath(): Path; + /** + * @return {!string} + */ + getEventType(): string; -/** - * @return {!fb.core.util.Path} - */ -Event.prototype.getPath = () => {}; - - -/** - * @return {!string} - */ -Event.prototype.getEventType = () => {}; - - -/** - * @return {!function()} - */ -Event.prototype.getEventRunner = () => {}; - - -/** - * @return {!string} - */ -Event.prototype.toString = () => {}; + /** + * @return {!function()} + */ + getEventRunner(): () => void; + /** + * @return {!string} + */ + toString(): string; +} /** * Encapsulates the data needed to raise an event - * @param {!string} eventType One of: value, child_added, child_changed, child_moved, child_removed - * @param {!EventRegistration} eventRegistration The function to call to with the event data. User provided - * @param {!fb.api.DataSnapshot} snapshot The data backing the event - * @param {?string=} opt_prevName Optional, the name of the previous child for child_* events. - * @constructor * @implements {Event} */ -export const DataEvent = function(eventType, eventRegistration, snapshot, opt_prevName?) { - this.eventRegistration = eventRegistration; - this.snapshot = snapshot; - this.prevName = opt_prevName; - this.eventType = eventType; -}; - - -/** - * @inheritDoc - */ -DataEvent.prototype.getPath = function() { - var ref = this.snapshot.getRef(); - if (this.eventType === 'value') { - return ref.path; - } else { - return ref.getParent().path; +export class DataEvent implements Event { + /** + * @param {!string} eventType One of: value, child_added, child_changed, child_moved, child_removed + * @param {!EventRegistration} eventRegistration The function to call to with the event data. User provided + * @param {!DataSnapshot} snapshot The data backing the event + * @param {?string=} prevName Optional, the name of the previous child for child_* events. + */ + constructor(public eventType: 'value' | ' child_added' | ' child_changed' | ' child_moved' | ' child_removed', + public eventRegistration: EventRegistration, + public snapshot: DataSnapshot, + public prevName?: string | null) { } -}; - - -/** - * @inheritDoc - */ -DataEvent.prototype.getEventType = function() { - return this.eventType; -}; + /** + * @inheritDoc + */ + getPath(): Path { + const ref = this.snapshot.getRef(); + if (this.eventType === 'value') { + return ref.path; + } else { + return ref.getParent().path; + } + } -/** - * @inheritDoc - */ -DataEvent.prototype.getEventRunner = function() { - return this.eventRegistration.getEventRunner(this); -}; + /** + * @inheritDoc + */ + getEventType(): string { + return this.eventType; + } + /** + * @inheritDoc + */ + getEventRunner(): () => void { + return this.eventRegistration.getEventRunner(this); + } -/** - * @inheritDoc - */ -DataEvent.prototype.toString = function() { - return this.getPath().toString() + ':' + this.eventType + ':' + + /** + * @inheritDoc + */ + toString(): string { + return this.getPath().toString() + ':' + this.eventType + ':' + stringify(this.snapshot.exportVal()); -}; - - - -/** - * @param {EventRegistration} eventRegistration - * @param {Error} error - * @param {!fb.core.util.Path} path - * @constructor - * @implements {Event} - */ -export const CancelEvent = function(eventRegistration, error, path) { - this.eventRegistration = eventRegistration; - this.error = error; - this.path = path; -}; - - -/** - * @inheritDoc - */ -CancelEvent.prototype.getPath = function() { - return this.path; -}; - - -/** - * @inheritDoc - */ -CancelEvent.prototype.getEventType = function() { - return 'cancel'; -}; + } +} + + +export class CancelEvent implements Event { + /** + * @param {EventRegistration} eventRegistration + * @param {Error} error + * @param {!Path} path + */ + constructor(public eventRegistration: EventRegistration, + public error: Error, + public path: Path) { + } + /** + * @inheritDoc + */ + getPath(): Path { + return this.path; + } -/** - * @inheritDoc - */ -CancelEvent.prototype.getEventRunner = function() { - return this.eventRegistration.getEventRunner(this); -}; + /** + * @inheritDoc + */ + getEventType(): string { + return 'cancel'; + } + /** + * @inheritDoc + */ + getEventRunner(): () => any { + return this.eventRegistration.getEventRunner(this); + } -/** - * @inheritDoc - */ -CancelEvent.prototype.toString = function() { - return this.path.toString() + ':cancel'; -}; + /** + * @inheritDoc + */ + toString(): string { + return this.path.toString() + ':cancel'; + } +} diff --git a/src/database/core/view/EventGenerator.ts b/src/database/core/view/EventGenerator.ts index 94fc3f9717e..9ef6061a4a5 100644 --- a/src/database/core/view/EventGenerator.ts +++ b/src/database/core/view/EventGenerator.ts @@ -1,119 +1,117 @@ -import { NamedNode } from "../snap/Node"; +import { NamedNode, Node } from '../snap/Node'; import { Change } from "./Change"; import { assertionError } from "../../../utils/assert"; +import { Query } from '../../api/Query'; +import { Index } from '../snap/indexes/Index'; +import { EventRegistration } from './EventRegistration'; +import { Event } from './Event'; /** * An EventGenerator is used to convert "raw" changes (Change) as computed by the - * CacheDiffer into actual events (fb.core.view.Event) that can be raised. See generateEventsForChanges() + * CacheDiffer into actual events (Event) that can be raised. See generateEventsForChanges() * for details. * - * @param {!fb.api.Query} query + * @param {!Query} query * @constructor */ -export const EventGenerator = function(query) { - /** - * @private - * @type {!fb.api.Query} - */ - this.query_ = query; +export class EventGenerator { + private index_: Index; + + constructor(private query_: Query) { + /** + * @private + * @type {!Index} + */ + this.index_ = this.query_.getQueryParams().getIndex(); + } /** - * @private - * @type {!Index} + * Given a set of raw changes (no moved events and prevName not specified yet), and a set of + * EventRegistrations that should be notified of these changes, generate the actual events to be raised. + * + * Notes: + * - child_moved events will be synthesized at this time for any child_changed events that affect + * our index. + * - prevName will be calculated based on the index ordering. + * + * @param {!Array.} changes + * @param {!Node} eventCache + * @param {!Array.} eventRegistrations + * @return {!Array.} */ - this.index_ = query.getQueryParams().getIndex(); -}; - -/** - * Given a set of raw changes (no moved events and prevName not specified yet), and a set of - * EventRegistrations that should be notified of these changes, generate the actual events to be raised. - * - * Notes: - * - child_moved events will be synthesized at this time for any child_changed events that affect - * our index. - * - prevName will be calculated based on the index ordering. - * - * @param {!Array.} changes - * @param {!Node} eventCache - * @param {!Array.} eventRegistrations - * @return {!Array.} - */ -EventGenerator.prototype.generateEventsForChanges = function(changes, eventCache, eventRegistrations) { - var events = [], self = this; + generateEventsForChanges(changes: Change[], eventCache: Node, eventRegistrations: EventRegistration[]): Event[] { + const events = []; + const moves = []; - var moves = []; - changes.forEach(function(change) { - if (change.type === Change.CHILD_CHANGED && - self.index_.indexedValueChanged(/** @type {!Node} */ (change.oldSnap), change.snapshotNode)) { - moves.push(Change.childMovedChange(/** @type {!string} */ (change.childName), change.snapshotNode)); - } - }); + changes.forEach((change) => { + if (change.type === Change.CHILD_CHANGED && + this.index_.indexedValueChanged(/** @type {!Node} */ (change.oldSnap), change.snapshotNode)) { + moves.push(Change.childMovedChange(/** @type {!string} */ (change.childName), change.snapshotNode)); + } + }); - this.generateEventsForType_(events, Change.CHILD_REMOVED, changes, eventRegistrations, eventCache); - this.generateEventsForType_(events, Change.CHILD_ADDED, changes, eventRegistrations, eventCache); - this.generateEventsForType_(events, Change.CHILD_MOVED, moves, eventRegistrations, eventCache); - this.generateEventsForType_(events, Change.CHILD_CHANGED, changes, eventRegistrations, eventCache); - this.generateEventsForType_(events, Change.VALUE, changes, eventRegistrations, eventCache); + this.generateEventsForType_(events, Change.CHILD_REMOVED, changes, eventRegistrations, eventCache); + this.generateEventsForType_(events, Change.CHILD_ADDED, changes, eventRegistrations, eventCache); + this.generateEventsForType_(events, Change.CHILD_MOVED, moves, eventRegistrations, eventCache); + this.generateEventsForType_(events, Change.CHILD_CHANGED, changes, eventRegistrations, eventCache); + this.generateEventsForType_(events, Change.VALUE, changes, eventRegistrations, eventCache); - return events; -}; + return events; + } -/** - * Given changes of a single change type, generate the corresponding events. - * - * @param {!Array.} events - * @param {!string} eventType - * @param {!Array.} changes - * @param {!Array.} registrations - * @param {!Node} eventCache - * @private - */ -EventGenerator.prototype.generateEventsForType_ = function(events, eventType, changes, registrations, - eventCache) { - var filteredChanges = changes.filter(function(change) { - return change.type === eventType; - }); + /** + * Given changes of a single change type, generate the corresponding events. + * + * @param {!Array.} events + * @param {!string} eventType + * @param {!Array.} changes + * @param {!Array.} registrations + * @param {!Node} eventCache + * @private + */ + private generateEventsForType_(events: Event[], eventType: string, changes: Change[], + registrations: EventRegistration[], eventCache: Node) { + const filteredChanges = changes.filter((change) => change.type === eventType); - var self = this; - filteredChanges.sort(this.compareChanges_.bind(this)); - filteredChanges.forEach(function(change) { - var materializedChange = self.materializeSingleChange_(change, eventCache); - registrations.forEach(function(registration) { - if (registration.respondsTo(change.type)) { - events.push(registration.createEvent(materializedChange, self.query_)); - } + filteredChanges.sort(this.compareChanges_.bind(this)); + filteredChanges.forEach((change) => { + const materializedChange = this.materializeSingleChange_(change, eventCache); + registrations.forEach((registration) => { + if (registration.respondsTo(change.type)) { + events.push(registration.createEvent(materializedChange, this.query_)); + } + }); }); - }); -}; - + } -/** - * @param {!Change} change - * @param {!Node} eventCache - * @return {!Change} - * @private - */ -EventGenerator.prototype.materializeSingleChange_ = function(change, eventCache) { - if (change.type === 'value' || change.type === 'child_removed') { - return change; - } else { - change.prevName = eventCache.getPredecessorChildName(/** @type {!string} */ (change.childName), change.snapshotNode, + /** + * @param {!Change} change + * @param {!Node} eventCache + * @return {!Change} + * @private + */ + private materializeSingleChange_(change: Change, eventCache: Node): Change { + if (change.type === 'value' || change.type === 'child_removed') { + return change; + } else { + change.prevName = eventCache.getPredecessorChildName(/** @type {!string} */ (change.childName), change.snapshotNode, this.index_); - return change; + return change; + } } -}; -/** - * @param {!Change} a - * @param {!Change} b - * @return {number} - * @private - */ -EventGenerator.prototype.compareChanges_ = function(a, b) { - if (a.childName == null || b.childName == null) { - throw assertionError('Should only compare child_ events.'); + /** + * @param {!Change} a + * @param {!Change} b + * @return {number} + * @private + */ + private compareChanges_(a: Change, b: Change) { + if (a.childName == null || b.childName == null) { + throw assertionError('Should only compare child_ events.'); + } + const aWrapped = new NamedNode(a.childName, a.snapshotNode); + const bWrapped = new NamedNode(b.childName, b.snapshotNode); + return this.index_.compare(aWrapped, bWrapped); } - var aWrapped = new NamedNode(a.childName, a.snapshotNode); - var bWrapped = new NamedNode(b.childName, b.snapshotNode); - return this.index_.compare(aWrapped, bWrapped); -}; +} diff --git a/src/database/core/view/EventQueue.ts b/src/database/core/view/EventQueue.ts index 599f23eb9ad..2fd51ddb91a 100644 --- a/src/database/core/view/EventQueue.ts +++ b/src/database/core/view/EventQueue.ts @@ -1,5 +1,6 @@ -import { Path } from "../util/Path"; -import { log, logger, exceptionGuard } from "../util/util"; +import { Path } from '../util/Path'; +import { log, logger, exceptionGuard } from '../util/util'; +import { Event } from './Event'; /** * The event queue serves a few purposes: @@ -15,155 +16,150 @@ import { log, logger, exceptionGuard } from "../util/util"; * * @constructor */ -export const EventQueue = function() { +export class EventQueue { /** * @private * @type {!Array.} */ - this.eventLists_ = []; + private eventLists_: EventList[] = []; /** * Tracks recursion depth of raiseQueuedEvents_, for debugging purposes. * @private * @type {!number} */ - this.recursionDepth_ = 0; -}; + private recursionDepth_ = 0; -/** - * @param {!Array.} eventDataList The new events to queue. - */ -EventQueue.prototype.queueEvents = function(eventDataList) { - // We group events by path, storing them in a single EventList, to make it easier to skip over them quickly. - var currList = null; - for (var i = 0; i < eventDataList.length; i++) { - var eventData = eventDataList[i]; - var eventPath = eventData.getPath(); - if (currList !== null && !eventPath.equals(currList.getPath())) { - this.eventLists_.push(currList); - currList = null; - } - if (currList === null) { - currList = new EventList(eventPath); - } + /** + * @param {!Array.} eventDataList The new events to queue. + */ + queueEvents(eventDataList: Event[]) { + // We group events by path, storing them in a single EventList, to make it easier to skip over them quickly. + let currList = null; + for (let i = 0; i < eventDataList.length; i++) { + const eventData = eventDataList[i]; + const eventPath = eventData.getPath(); + if (currList !== null && !eventPath.equals(currList.getPath())) { + this.eventLists_.push(currList); + currList = null; + } - currList.add(eventData); - } - if (currList) { - this.eventLists_.push(currList); - } -}; + if (currList === null) { + currList = new EventList(eventPath); + } -/** - * Queues the specified events and synchronously raises all events (including previously queued ones) - * for the specified path. - * - * It is assumed that the new events are all for the specified path. - * - * @param {!Path} path The path to raise events for. - * @param {!Array.} eventDataList The new events to raise. - */ -EventQueue.prototype.raiseEventsAtPath = function(path, eventDataList) { - this.queueEvents(eventDataList); + currList.add(eventData); + } + if (currList) { + this.eventLists_.push(currList); + } + } - this.raiseQueuedEventsMatchingPredicate_(function(eventPath) { - return eventPath.equals(path); - }); -}; + /** + * Queues the specified events and synchronously raises all events (including previously queued ones) + * for the specified path. + * + * It is assumed that the new events are all for the specified path. + * + * @param {!Path} path The path to raise events for. + * @param {!Array.} eventDataList The new events to raise. + */ + raiseEventsAtPath(path: Path, eventDataList: Event[]) { + this.queueEvents(eventDataList); + this.raiseQueuedEventsMatchingPredicate_((eventPath: Path) => eventPath.equals(path)); + } -/** - * Queues the specified events and synchronously raises all events (including previously queued ones) for - * locations related to the specified change path (i.e. all ancestors and descendants). - * - * It is assumed that the new events are all related (ancestor or descendant) to the specified path. - * - * @param {!Path} changedPath The path to raise events for. - * @param {!Array.} eventDataList The events to raise - */ -EventQueue.prototype.raiseEventsForChangedPath = function(changedPath, eventDataList) { - this.queueEvents(eventDataList); + /** + * Queues the specified events and synchronously raises all events (including previously queued ones) for + * locations related to the specified change path (i.e. all ancestors and descendants). + * + * It is assumed that the new events are all related (ancestor or descendant) to the specified path. + * + * @param {!Path} changedPath The path to raise events for. + * @param {!Array.} eventDataList The events to raise + */ + raiseEventsForChangedPath(changedPath: Path, eventDataList: Event[]) { + this.queueEvents(eventDataList); - this.raiseQueuedEventsMatchingPredicate_(function(eventPath) { - return eventPath.contains(changedPath) || changedPath.contains(eventPath); - }); -}; + this.raiseQueuedEventsMatchingPredicate_((eventPath: Path) => { + return eventPath.contains(changedPath) || changedPath.contains(eventPath); + }); + }; -/** - * @param {!function(!Path):boolean} predicate - * @private - */ -EventQueue.prototype.raiseQueuedEventsMatchingPredicate_ = function(predicate) { - this.recursionDepth_++; - - var sentAll = true; - for (var i = 0; i < this.eventLists_.length; i++) { - var eventList = this.eventLists_[i]; - if (eventList) { - var eventPath = eventList.getPath(); - if (predicate(eventPath)) { - this.eventLists_[i].raise(); - this.eventLists_[i] = null; - } else { - sentAll = false; + /** + * @param {!function(!Path):boolean} predicate + * @private + */ + private raiseQueuedEventsMatchingPredicate_(predicate: (path: Path) => boolean) { + this.recursionDepth_++; + + let sentAll = true; + for (let i = 0; i < this.eventLists_.length; i++) { + const eventList = this.eventLists_[i]; + if (eventList) { + const eventPath = eventList.getPath(); + if (predicate(eventPath)) { + this.eventLists_[i].raise(); + this.eventLists_[i] = null; + } else { + sentAll = false; + } } } - } - if (sentAll) { - this.eventLists_ = []; - } + if (sentAll) { + this.eventLists_ = []; + } - this.recursionDepth_--; -}; + this.recursionDepth_--; + } +} /** * @param {!Path} path * @constructor */ -export const EventList = function(path) { +export class EventList { /** - * @const - * @type {!Path} + * @type {!Array.} * @private */ - this.path_ = path; + private events_: Event[] = []; + + constructor(private readonly path_: Path) { + } + /** - * @type {!Array.} - * @private + * @param {!Event} eventData */ - this.events_ = []; -}; - -/** - * @param {!fb.core.view.Event} eventData - */ -EventList.prototype.add = function(eventData) { - this.events_.push(eventData); -}; + add(eventData: Event) { + this.events_.push(eventData); + } -/** - * Iterates through the list and raises each event - */ -EventList.prototype.raise = function() { - for (var i = 0; i < this.events_.length; i++) { - var eventData = this.events_[i]; - if (eventData !== null) { - this.events_[i] = null; - var eventFn = eventData.getEventRunner(); - if (logger) { - log('event: ' + eventData.toString()); + /** + * Iterates through the list and raises each event + */ + raise() { + for (let i = 0; i < this.events_.length; i++) { + const eventData = this.events_[i]; + if (eventData !== null) { + this.events_[i] = null; + const eventFn = eventData.getEventRunner(); + if (logger) { + log('event: ' + eventData.toString()); + } + exceptionGuard(eventFn); } - exceptionGuard(eventFn); } } -}; -/** - * @return {!Path} - */ -EventList.prototype.getPath = function() { - return this.path_; -}; + /** + * @return {!Path} + */ + getPath(): Path { + return this.path_; + } +} diff --git a/src/database/core/view/EventRegistration.ts b/src/database/core/view/EventRegistration.ts index 89ae8b61259..39febc024f1 100644 --- a/src/database/core/view/EventRegistration.ts +++ b/src/database/core/view/EventRegistration.ts @@ -1,8 +1,10 @@ -import { DataSnapshot } from "../../api/DataSnapshot"; -import { DataEvent, CancelEvent } from "./Event"; -import { contains, getCount, getAnyKey } from "../../../utils/obj"; -import { assert } from "../../../utils/assert"; -import { Path } from "../util/Path"; +import { DataSnapshot } from '../../api/DataSnapshot'; +import { DataEvent, CancelEvent, Event } from './Event'; +import { contains, getCount, getAnyKey, every } from '../../../utils/obj'; +import { assert } from '../../../utils/assert'; +import { Path } from '../util/Path'; +import { Change } from './Change'; +import { Query } from '../../api/Query'; /** * An EventRegistration is basically an event type ('value', 'child_added', etc.) and a callback @@ -11,153 +13,136 @@ import { Path } from "../util/Path"; * That said, it can also contain a cancel callback to be notified if the event is canceled. And * currently, this code is organized around the idea that you would register multiple child_ callbacks * together, as a single EventRegistration. Though currently we don't do that. - * - * @interface - */ -export const EventRegistration = function() {}; - - -/** - * True if this container has a callback to trigger for this event type - * @param {!string} eventType - * @return {boolean} - */ -EventRegistration.prototype.respondsTo; - - -/** - * @param {!fb.core.view.Change} change - * @param {!fb.api.Query} query - * @return {!fb.core.view.Event} - */ -EventRegistration.prototype.createEvent; - - -/** - * Given event data, return a function to trigger the user's callback - * @param {!fb.core.view.Event} eventData - * @return {function()} - */ -EventRegistration.prototype.getEventRunner; - - -/** - * @param {!Error} error - * @param {!Path} path - * @return {?CancelEvent} - */ -EventRegistration.prototype.createCancelEvent; - - -/** - * @param {!EventRegistration} other - * @return {boolean} - */ -EventRegistration.prototype.matches; - - -/** - * False basically means this is a "dummy" callback container being used as a sentinel - * to remove all callback containers of a particular type. (e.g. if the user does - * ref.off('value') without specifying a specific callback). - * - * (TODO: Rework this, since it's hacky) - * - * @return {boolean} */ -EventRegistration.prototype.hasAnyCallback; - +export interface EventRegistration { + /** + * True if this container has a callback to trigger for this event type + * @param {!string} eventType + * @return {boolean} + */ + respondsTo(eventType: string): boolean; + + /** + * @param {!Change} change + * @param {!Query} query + * @return {!Event} + */ + createEvent(change: Change, query: Query): Event; + + /** + * Given event data, return a function to trigger the user's callback + * @param {!Event} eventData + * @return {function()} + */ + getEventRunner(eventData: Event): () => any; + + /** + * @param {!Error} error + * @param {!Path} path + * @return {?CancelEvent} + */ + createCancelEvent(error: Error, path: Path): CancelEvent | null; + + /** + * @param {!EventRegistration} other + * @return {boolean} + */ + matches(other: EventRegistration): boolean; + + /** + * False basically means this is a "dummy" callback container being used as a sentinel + * to remove all callback containers of a particular type. (e.g. if the user does + * ref.off('value') without specifying a specific callback). + * + * (TODO: Rework this, since it's hacky) + * + * @return {boolean} + */ + hasAnyCallback(): boolean; +} /** * Represents registration for 'value' events. - * - * @param {?function(!DataSnapshot)} callback - * @param {?function(Error)} cancelCallback - * @param {?Object} context - * @constructor - * @implements {EventRegistration} - */ -export const ValueEventRegistration = function(callback, cancelCallback, context) { - this.callback_ = callback; - this.cancelCallback_ = cancelCallback; - this.context_ = context || null; -}; - - -/** - * @inheritDoc - */ -ValueEventRegistration.prototype.respondsTo = function(eventType) { - return eventType === 'value'; -}; - - -/** - * @inheritDoc */ -ValueEventRegistration.prototype.createEvent = function(change, query) { - var index = query.getQueryParams().getIndex(); - return new DataEvent('value', this, new DataSnapshot(change.snapshotNode, query.getRef(), index)); -}; - - -/** - * @inheritDoc - */ -ValueEventRegistration.prototype.getEventRunner = function(eventData) { - var ctx = this.context_; - if (eventData.getEventType() === 'cancel') { - assert(this.cancelCallback_, 'Raising a cancel event on a listener with no cancel callback'); - var cancelCB = this.cancelCallback_; - return function() { - // We know that error exists, we checked above that this is a cancel event - cancelCB.call(ctx, eventData.error); - }; - } else { - var cb = this.callback_; - return function() { - cb.call(ctx, eventData.snapshot); - }; +export class ValueEventRegistration implements EventRegistration { + /** + * @param {?function(!DataSnapshot)} callback_ + * @param {?function(Error)} cancelCallback_ + * @param {?Object} context_ + */ + constructor(private callback_: ((d: DataSnapshot) => any) | null, + private cancelCallback_: ((e: Error) => any) | null, + private context_: Object | null) { } -}; - -/** - * @inheritDoc - */ -ValueEventRegistration.prototype.createCancelEvent = function(error, path) { - if (this.cancelCallback_) { - return new CancelEvent(this, error, path); - } else { - return null; + /** + * @inheritDoc + */ + respondsTo(eventType: string): boolean { + return eventType === 'value'; } -}; - -/** - * @inheritDoc - */ -ValueEventRegistration.prototype.matches = function(other) { - if (!(other instanceof ValueEventRegistration)) { - return false; - } else if (!other.callback_ || !this.callback_) { - // If no callback specified, we consider it to match any callback. - return true; - } else { - return other.callback_ === this.callback_ && other.context_ === this.context_; + /** + * @inheritDoc + */ + createEvent(change: Change, query: Query): DataEvent { + const index = query.getQueryParams().getIndex(); + return new DataEvent('value', this, new DataSnapshot(change.snapshotNode, query.getRef(), index)); } -}; + /** + * @inheritDoc + */ + getEventRunner(eventData: CancelEvent | DataEvent): () => void { + const ctx = this.context_; + if (eventData.getEventType() === 'cancel') { + assert(this.cancelCallback_, 'Raising a cancel event on a listener with no cancel callback'); + const cancelCB = this.cancelCallback_; + return function () { + // We know that error exists, we checked above that this is a cancel event + cancelCB.call(ctx, (eventData).error); + }; + } else { + const cb = this.callback_; + return function () { + cb.call(ctx, (eventData).snapshot); + }; + } + } -/** - * @inheritDoc - */ -ValueEventRegistration.prototype.hasAnyCallback = function() { - return this.callback_ !== null; -}; + /** + * @inheritDoc + */ + createCancelEvent(error: Error, path: Path): CancelEvent | null { + if (this.cancelCallback_) { + return new CancelEvent(this, error, path); + } else { + return null; + } + } + /** + * @inheritDoc + */ + matches(other: EventRegistration): boolean { + if (!(other instanceof ValueEventRegistration)) { + return false; + } else if (!other.callback_ || !this.callback_) { + // If no callback specified, we consider it to match any callback. + return true; + } else { + return other.callback_ === this.callback_ && other.context_ === this.context_; + } + } + /** + * @inheritDoc + */ + hasAnyCallback(): boolean { + return this.callback_ !== null; + } +} /** * Represents the registration of 1 or more child_xxx events. @@ -165,115 +150,111 @@ ValueEventRegistration.prototype.hasAnyCallback = function() { * Currently, it is always exactly 1 child_xxx event, but the idea is we might let you * register a group of callbacks together in the future. * - * @param {?Object.} callbacks - * @param {?function(Error)} cancelCallback - * @param {Object=} opt_context * @constructor * @implements {EventRegistration} */ -export const ChildEventRegistration = function(callbacks, cancelCallback, opt_context) { - this.callbacks_ = callbacks; - this.cancelCallback_ = cancelCallback; - this.context_ = opt_context; -}; - - -/** - * @inheritDoc - */ -ChildEventRegistration.prototype.respondsTo = function(eventType) { - var eventToCheck = eventType === 'children_added' ? 'child_added' : eventType; - eventToCheck = eventToCheck === 'children_removed' ? 'child_removed' : eventToCheck; - return contains(this.callbacks_, eventToCheck); -}; - +export class ChildEventRegistration implements EventRegistration { + /** + * @param {?Object.} callbacks_ + * @param {?function(Error)} cancelCallback_ + * @param {Object=} context_ + */ + constructor(private callbacks_: ({ [k: string]: (d: DataSnapshot, s?: string | null) => any }) | null, + private cancelCallback_: ((e: Error) => any) | null, + private context_: Object) { + } -/** - * @inheritDoc - */ -ChildEventRegistration.prototype.createCancelEvent = function(error, path) { - if (this.cancelCallback_) { - return new CancelEvent(this, error, path); - } else { - return null; + /** + * @inheritDoc + */ + respondsTo(eventType): boolean { + let eventToCheck = eventType === 'children_added' ? 'child_added' : eventType; + eventToCheck = eventToCheck === 'children_removed' ? 'child_removed' : eventToCheck; + return contains(this.callbacks_, eventToCheck); } -}; + /** + * @inheritDoc + */ + createCancelEvent(error: Error, path: Path): CancelEvent | null { + if (this.cancelCallback_) { + return new CancelEvent(this, error, path); + } else { + return null; + } + } -/** - * @inheritDoc - */ -ChildEventRegistration.prototype.createEvent = function(change, query) { - assert(change.childName != null, 'Child events should have a childName.'); - var ref = query.getRef().child(/** @type {!string} */ (change.childName)); - var index = query.getQueryParams().getIndex(); - return new DataEvent(change.type, this, new DataSnapshot(change.snapshotNode, ref, index), + /** + * @inheritDoc + */ + createEvent(change: Change, query: Query): DataEvent { + assert(change.childName != null, 'Child events should have a childName.'); + const ref = query.getRef().child(/** @type {!string} */ (change.childName)); + const index = query.getQueryParams().getIndex(); + return new DataEvent(change.type, this, new DataSnapshot(change.snapshotNode, ref, index), change.prevName); -}; - + } -/** - * @inheritDoc - */ -ChildEventRegistration.prototype.getEventRunner = function(eventData) { - var ctx = this.context_; - if (eventData.getEventType() === 'cancel') { - assert(this.cancelCallback_, 'Raising a cancel event on a listener with no cancel callback'); - var cancelCB = this.cancelCallback_; - return function() { - // We know that error exists, we checked above that this is a cancel event - cancelCB.call(ctx, eventData.error); - }; - } else { - var cb = this.callbacks_[eventData.eventType]; - return function() { - cb.call(ctx, eventData.snapshot, eventData.prevName); + /** + * @inheritDoc + */ + getEventRunner(eventData: CancelEvent | DataEvent): () => void { + const ctx = this.context_; + if (eventData.getEventType() === 'cancel') { + assert(this.cancelCallback_, 'Raising a cancel event on a listener with no cancel callback'); + const cancelCB = this.cancelCallback_; + return function () { + // We know that error exists, we checked above that this is a cancel event + cancelCB.call(ctx, (eventData).error); + }; + } else { + const cb = this.callbacks_[(eventData).eventType]; + return function () { + cb.call(ctx, (eventData).snapshot, (eventData).prevName); + } } } -}; - -/** - * @inheritDoc - */ -ChildEventRegistration.prototype.matches = function(other) { - if (other instanceof ChildEventRegistration) { - if (!this.callbacks_ || !other.callbacks_) { - return true; - } else if (this.context_ === other.context_) { - var otherCount = getCount(other.callbacks_); - var thisCount = getCount(this.callbacks_); - if (otherCount === thisCount) { - // If count is 1, do an exact match on eventType, if either is defined but null, it's a match. - // If event types don't match, not a match - // If count is not 1, exact match across all - - if (otherCount === 1) { - var otherKey = /** @type {!string} */ (getAnyKey(other.callbacks_)); - var thisKey = /** @type {!string} */ (getAnyKey(this.callbacks_)); - return (thisKey === otherKey && ( - !other.callbacks_[otherKey] || - !this.callbacks_[thisKey] || - other.callbacks_[otherKey] === this.callbacks_[thisKey] + /** + * @inheritDoc + */ + matches(other: EventRegistration): boolean { + if (other instanceof ChildEventRegistration) { + if (!this.callbacks_ || !other.callbacks_) { + return true; + } else if (this.context_ === other.context_) { + const otherCount = getCount(other.callbacks_); + const thisCount = getCount(this.callbacks_); + if (otherCount === thisCount) { + // If count is 1, do an exact match on eventType, if either is defined but null, it's a match. + // If event types don't match, not a match + // If count is not 1, exact match across all + + if (otherCount === 1) { + const otherKey = /** @type {!string} */ (getAnyKey(other.callbacks_)); + const thisKey = /** @type {!string} */ (getAnyKey(this.callbacks_)); + return (thisKey === otherKey && ( + !other.callbacks_[otherKey] || + !this.callbacks_[thisKey] || + other.callbacks_[otherKey] === this.callbacks_[thisKey] ) - ); - } else { - // Exact match on each key. - return this.callbacks_.every(function(cb, eventType) { - return other.callbacks_[eventType] === cb; - }); + ); + } else { + // Exact match on each key. + return every(this.callbacks_, (eventType, cb) => other.callbacks_[eventType] === cb); + } } } } - } - return false; -}; + return false; + } + /** + * @inheritDoc + */ + hasAnyCallback(): boolean { + return (this.callbacks_ !== null); + } +} -/** - * @inheritDoc - */ -ChildEventRegistration.prototype.hasAnyCallback = function() { - return (this.callbacks_ !== null); -}; diff --git a/src/database/core/view/filter/IndexedFilter.ts b/src/database/core/view/filter/IndexedFilter.ts index 63abd58028b..f8b627a0708 100644 --- a/src/database/core/view/filter/IndexedFilter.ts +++ b/src/database/core/view/filter/IndexedFilter.ts @@ -2,28 +2,29 @@ import { assert } from "../../../../utils/assert"; import { Change } from "../Change"; import { ChildrenNode } from "../../snap/ChildrenNode"; import { PRIORITY_INDEX } from "../../snap/indexes/PriorityIndex"; +import { NodeFilter } from './NodeFilter'; +import { Index } from '../../snap/indexes/Index'; +import { Path } from '../../util/Path'; +import { CompleteChildSource } from '../CompleteChildSource'; +import { ChildChangeAccumulator } from '../ChildChangeAccumulator'; +import { Node } from '../../snap/Node'; /** * Doesn't really filter nodes but applies an index to the node and keeps track of any changes * * @constructor - * @implements {filter.NodeFilter} - * @param {!fb.core.snap.Index} index + * @implements {NodeFilter} + * @param {!Index} index */ -export class IndexedFilter { - /** - * @type {!fb.core.snap.Index} - * @const - * @private - */ - private index_; - constructor(index) { - this.index_ = index; +export class IndexedFilter implements NodeFilter { + constructor(private readonly index_: Index) { } - updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator) { + updateChild(snap: Node, key: string, newChild: Node, affectedPath: Path, + source: CompleteChildSource, + optChangeAccumulator: ChildChangeAccumulator | null): Node { assert(snap.isIndexed(this.index_), 'A node must be indexed if only a child is updated'); - var oldChild = snap.getImmediateChild(key); + const oldChild = snap.getImmediateChild(key); // Check if anything actually changed. if (oldChild.getChild(affectedPath).equals(newChild.getChild(affectedPath))) { // There's an edge case where a child can enter or leave the view because affectedPath was set to null. @@ -62,7 +63,8 @@ export class IndexedFilter { /** * @inheritDoc */ - updateFullNode(oldSnap, newSnap, optChangeAccumulator) { + updateFullNode(oldSnap: Node, newSnap: Node, + optChangeAccumulator: ChildChangeAccumulator | null): Node { if (optChangeAccumulator != null) { if (!oldSnap.isLeafNode()) { oldSnap.forEachChild(PRIORITY_INDEX, function(key, childNode) { @@ -74,7 +76,7 @@ export class IndexedFilter { if (!newSnap.isLeafNode()) { newSnap.forEachChild(PRIORITY_INDEX, function(key, childNode) { if (oldSnap.hasChild(key)) { - var oldChild = oldSnap.getImmediateChild(key); + const oldChild = oldSnap.getImmediateChild(key); if (!oldChild.equals(childNode)) { optChangeAccumulator.trackChildChange(Change.childChangedChange(key, childNode, oldChild)); } @@ -90,7 +92,7 @@ export class IndexedFilter { /** * @inheritDoc */ - updatePriority(oldSnap, newPriority) { + updatePriority(oldSnap: Node, newPriority: Node): Node { if (oldSnap.isEmpty()) { return ChildrenNode.EMPTY_NODE; } else { @@ -101,21 +103,21 @@ export class IndexedFilter { /** * @inheritDoc */ - filtersNodes() { + filtersNodes(): boolean { return false; }; /** * @inheritDoc */ - getIndexedFilter() { + getIndexedFilter(): IndexedFilter { return this; }; /** * @inheritDoc */ - getIndex() { + getIndex(): Index { return this.index_; }; } diff --git a/src/database/core/view/filter/NodeFilter.ts b/src/database/core/view/filter/NodeFilter.ts index 3bf83c8490d..eb0dea1cb52 100644 --- a/src/database/core/view/filter/NodeFilter.ts +++ b/src/database/core/view/filter/NodeFilter.ts @@ -1,60 +1,69 @@ +import { Node } from '../../snap/Node'; +import { Path } from '../../util/Path'; +import { CompleteChildSource } from '../CompleteChildSource'; +import { ChildChangeAccumulator } from '../ChildChangeAccumulator'; +import { Index } from '../../snap/indexes/Index'; + /** * NodeFilter is used to update nodes and complete children of nodes while applying queries on the fly and keeping * track of any child changes. This class does not track value changes as value changes depend on more * than just the node itself. Different kind of queries require different kind of implementations of this interface. * @interface */ -export const NodeFilter = function() { }; +export interface NodeFilter { -/** - * Update a single complete child in the snap. If the child equals the old child in the snap, this is a no-op. - * The method expects an indexed snap. - * - * @param {!fb.core.snap.Node} snap - * @param {string} key - * @param {!fb.core.snap.Node} newChild - * @param {!fb.core.util.Path} affectedPath - * @param {!fb.core.view.CompleteChildSource} source - * @param {?fb.core.view.ChildChangeAccumulator} optChangeAccumulator - * @return {!fb.core.snap.Node} - */ -NodeFilter.prototype.updateChild = function(snap, key, newChild, affectedPath, source, - optChangeAccumulator) {}; + /** + * Update a single complete child in the snap. If the child equals the old child in the snap, this is a no-op. + * The method expects an indexed snap. + * + * @param {!Node} snap + * @param {string} key + * @param {!Node} newChild + * @param {!Path} affectedPath + * @param {!CompleteChildSource} source + * @param {?ChildChangeAccumulator} optChangeAccumulator + * @return {!Node} + */ + updateChild(snap: Node, key: string, newChild: Node, affectedPath: Path, + source: CompleteChildSource, + optChangeAccumulator: ChildChangeAccumulator | null): Node; -/** - * Update a node in full and output any resulting change from this complete update. - * - * @param {!fb.core.snap.Node} oldSnap - * @param {!fb.core.snap.Node} newSnap - * @param {?fb.core.view.ChildChangeAccumulator} optChangeAccumulator - * @return {!fb.core.snap.Node} - */ -NodeFilter.prototype.updateFullNode = function(oldSnap, newSnap, optChangeAccumulator) { }; + /** + * Update a node in full and output any resulting change from this complete update. + * + * @param {!Node} oldSnap + * @param {!Node} newSnap + * @param {?ChildChangeAccumulator} optChangeAccumulator + * @return {!Node} + */ + updateFullNode(oldSnap: Node, newSnap: Node, + optChangeAccumulator: ChildChangeAccumulator | null): Node; -/** - * Update the priority of the root node - * - * @param {!fb.core.snap.Node} oldSnap - * @param {!fb.core.snap.Node} newPriority - * @return {!fb.core.snap.Node} - */ -NodeFilter.prototype.updatePriority = function(oldSnap, newPriority) { }; + /** + * Update the priority of the root node + * + * @param {!Node} oldSnap + * @param {!Node} newPriority + * @return {!Node} + */ + updatePriority(oldSnap: Node, newPriority: Node): Node; -/** - * Returns true if children might be filtered due to query criteria - * - * @return {boolean} - */ -NodeFilter.prototype.filtersNodes = function() { }; + /** + * Returns true if children might be filtered due to query criteria + * + * @return {boolean} + */ + filtersNodes(): boolean; -/** - * Returns the index filter that this filter uses to get a NodeFilter that doesn't filter any children. - * @return {!NodeFilter} - */ -NodeFilter.prototype.getIndexedFilter = function() { }; + /** + * Returns the index filter that this filter uses to get a NodeFilter that doesn't filter any children. + * @return {!NodeFilter} + */ + getIndexedFilter(): NodeFilter; -/** - * Returns the index that this filter uses - * @return {!fb.core.snap.Index} - */ -NodeFilter.prototype.getIndex = function() { }; + /** + * Returns the index that this filter uses + * @return {!Index} + */ + getIndex(): Index; +} diff --git a/src/database/realtime/BrowserPollConnection.ts b/src/database/realtime/BrowserPollConnection.ts index 9a89a93d80f..94082b272a3 100644 --- a/src/database/realtime/BrowserPollConnection.ts +++ b/src/database/realtime/BrowserPollConnection.ts @@ -14,29 +14,30 @@ import { PacketReceiver } from "./polling/PacketReceiver"; import { CONSTANTS } from "./Constants"; import { stringify } from "../../utils/json"; import { isNodeSdk } from "../../utils/environment"; +import { Transport } from './Transport'; +import { RepoInfo } from '../core/RepoInfo'; // URL query parameters associated with longpolling -// TODO: move more of these out of the global namespace -var FIREBASE_LONGPOLL_START_PARAM = 'start'; -var FIREBASE_LONGPOLL_CLOSE_COMMAND = 'close'; -var FIREBASE_LONGPOLL_COMMAND_CB_NAME = 'pLPCommand'; -var FIREBASE_LONGPOLL_DATA_CB_NAME = 'pRTLPCB'; -var FIREBASE_LONGPOLL_ID_PARAM = 'id'; -var FIREBASE_LONGPOLL_PW_PARAM = 'pw'; -var FIREBASE_LONGPOLL_SERIAL_PARAM = 'ser'; -var FIREBASE_LONGPOLL_CALLBACK_ID_PARAM = 'cb'; -var FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM = 'seg'; -var FIREBASE_LONGPOLL_SEGMENTS_IN_PACKET = 'ts'; -var FIREBASE_LONGPOLL_DATA_PARAM = 'd'; -var FIREBASE_LONGPOLL_DISCONN_FRAME_PARAM = 'disconn'; -var FIREBASE_LONGPOLL_DISCONN_FRAME_REQUEST_PARAM = 'dframe'; +const FIREBASE_LONGPOLL_START_PARAM = 'start'; +const FIREBASE_LONGPOLL_CLOSE_COMMAND = 'close'; +const FIREBASE_LONGPOLL_COMMAND_CB_NAME = 'pLPCommand'; +const FIREBASE_LONGPOLL_DATA_CB_NAME = 'pRTLPCB'; +const FIREBASE_LONGPOLL_ID_PARAM = 'id'; +const FIREBASE_LONGPOLL_PW_PARAM = 'pw'; +const FIREBASE_LONGPOLL_SERIAL_PARAM = 'ser'; +const FIREBASE_LONGPOLL_CALLBACK_ID_PARAM = 'cb'; +const FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM = 'seg'; +const FIREBASE_LONGPOLL_SEGMENTS_IN_PACKET = 'ts'; +const FIREBASE_LONGPOLL_DATA_PARAM = 'd'; +const FIREBASE_LONGPOLL_DISCONN_FRAME_PARAM = 'disconn'; +const FIREBASE_LONGPOLL_DISCONN_FRAME_REQUEST_PARAM = 'dframe'; //Data size constants. //TODO: Perf: the maximum length actually differs from browser to browser. // We should check what browser we're on and set accordingly. -var MAX_URL_DATA_SIZE = 1870; -var SEG_HEADER_SIZE = 30; //ie: &seg=8299234&ts=982389123&d= -var MAX_PAYLOAD_SIZE = MAX_URL_DATA_SIZE - SEG_HEADER_SIZE; +const MAX_URL_DATA_SIZE = 1870; +const SEG_HEADER_SIZE = 30; //ie: &seg=8299234&ts=982389123&d= +const MAX_PAYLOAD_SIZE = MAX_URL_DATA_SIZE - SEG_HEADER_SIZE; /** * Keepalive period @@ -45,146 +46,142 @@ var MAX_PAYLOAD_SIZE = MAX_URL_DATA_SIZE - SEG_HEADER_SIZE; * @const * @type {number} */ -var KEEPALIVE_REQUEST_INTERVAL = 25000; +const KEEPALIVE_REQUEST_INTERVAL = 25000; /** * How long to wait before aborting a long-polling connection attempt. * @const * @type {number} */ -var LP_CONNECT_TIMEOUT = 30000; +const LP_CONNECT_TIMEOUT = 30000; /** * This class manages a single long-polling connection. * * @constructor - * @implements {fb.realtime.Transport} + * @implements {Transport} * @param {string} connId An identifier for this connection, used for logging - * @param {fb.core.RepoInfo} repoInfo The info for the endpoint to send data to. + * @param {RepoInfo} repoInfo The info for the endpoint to send data to. * @param {string=} opt_transportSessionId Optional transportSessionid if we are reconnecting for an existing * transport session * @param {string=} opt_lastSessionId Optional lastSessionId if the PersistentConnection has already created a * connection previously */ -export class BrowserPollConnection { - connId; - log_; +export class BrowserPollConnection implements Transport { repoInfo; bytesSent; bytesReceived; - stats_; transportSessionId; - everConnected_; lastSessionId; urlFn; scriptTagHolder; myDisconnFrame; - connectTimeoutTimer_; curSegmentNum; - onDisconnect_; myPacketOrderer; - isClosed_; id; password; - - constructor(connId, repoInfo, opt_transportSessionId, opt_lastSessionId) { - this.connId = connId; + private log_; + private stats_; + private everConnected_; + private connectTimeoutTimer_; + private onDisconnect_; + private isClosed_; + + constructor(public connId: string, repoInfo: RepoInfo, transportSessionId?: string, lastSessionId?: string) { this.log_ = logWrapper(connId); this.repoInfo = repoInfo; this.bytesSent = 0; this.bytesReceived = 0; this.stats_ = StatsManager.getCollection(repoInfo); - this.transportSessionId = opt_transportSessionId; + this.transportSessionId = transportSessionId; this.everConnected_ = false; - this.lastSessionId = opt_lastSessionId; - this.urlFn = function(params) { - return repoInfo.connectionURL(CONSTANTS.LONG_POLLING, params); - }; + this.lastSessionId = lastSessionId; + this.urlFn = (params) => repoInfo.connectionURL(CONSTANTS.LONG_POLLING, params); }; + /** * * @param {function(Object)} onMessage Callback when messages arrive * @param {function()} onDisconnect Callback with connection lost. */ - open(onMessage, onDisconnect) { + open(onMessage: (msg: Object) => any, onDisconnect: () => any) { this.curSegmentNum = 0; this.onDisconnect_ = onDisconnect; this.myPacketOrderer = new PacketReceiver(onMessage); this.isClosed_ = false; - var self = this; - this.connectTimeoutTimer_ = setTimeout(function() { - self.log_('Timed out trying to connect.'); + this.connectTimeoutTimer_ = setTimeout(() => { + this.log_('Timed out trying to connect.'); // Make sure we clear the host cache - self.onClosed_(); - self.connectTimeoutTimer_ = null; + this.onClosed_(); + this.connectTimeoutTimer_ = null; }, Math.floor(LP_CONNECT_TIMEOUT)); // Ensure we delay the creation of the iframe until the DOM is loaded. - executeWhenDOMReady(function() { - if (self.isClosed_) + executeWhenDOMReady(() => { + if (this.isClosed_) return; //Set up a callback that gets triggered once a connection is set up. - self.scriptTagHolder = new FirebaseIFrameScriptHolder(function(command, arg1, arg2, arg3, arg4) { - self.incrementIncomingBytes_(arguments); - if (!self.scriptTagHolder) + this.scriptTagHolder = new FirebaseIFrameScriptHolder((command, arg1, arg2, arg3, arg4) => { + this.incrementIncomingBytes_(arguments); + if (!this.scriptTagHolder) return; // we closed the connection. - if (self.connectTimeoutTimer_) { - clearTimeout(self.connectTimeoutTimer_); - self.connectTimeoutTimer_ = null; + if (this.connectTimeoutTimer_) { + clearTimeout(this.connectTimeoutTimer_); + this.connectTimeoutTimer_ = null; } - self.everConnected_ = true; + this.everConnected_ = true; if (command == FIREBASE_LONGPOLL_START_PARAM) { - self.id = arg1; - self.password = arg2; + this.id = arg1; + this.password = arg2; } else if (command === FIREBASE_LONGPOLL_CLOSE_COMMAND) { // Don't clear the host cache. We got a response from the server, so we know it's reachable if (arg1) { // We aren't expecting any more data (other than what the server's already in the process of sending us // through our already open polls), so don't send any more. - self.scriptTagHolder.sendNewPolls = false; + this.scriptTagHolder.sendNewPolls = false; // arg1 in this case is the last response number sent by the server. We should try to receive // all of the responses up to this one before closing - self.myPacketOrderer.closeAfter(arg1, function() { self.onClosed_(); }); + this.myPacketOrderer.closeAfter(arg1, () => { this.onClosed_(); }); } else { - self.onClosed_(); + this.onClosed_(); } } else { throw new Error('Unrecognized command received: ' + command); } - }, function(pN, data) { - self.incrementIncomingBytes_(arguments); - self.myPacketOrderer.handleResponse(pN, data); - }, function() { - self.onClosed_(); - }, self.urlFn); + }, (pN, data) => { + this.incrementIncomingBytes_(arguments); + this.myPacketOrderer.handleResponse(pN, data); + }, () => { + this.onClosed_(); + }, this.urlFn); //Send the initial request to connect. The serial number is simply to keep the browser from pulling previous results //from cache. - var urlParams = {}; + const urlParams = {}; urlParams[FIREBASE_LONGPOLL_START_PARAM] = 't'; urlParams[FIREBASE_LONGPOLL_SERIAL_PARAM] = Math.floor(Math.random() * 100000000); - if (self.scriptTagHolder.uniqueCallbackIdentifier) - urlParams[FIREBASE_LONGPOLL_CALLBACK_ID_PARAM] = self.scriptTagHolder.uniqueCallbackIdentifier; + if (this.scriptTagHolder.uniqueCallbackIdentifier) + urlParams[FIREBASE_LONGPOLL_CALLBACK_ID_PARAM] = this.scriptTagHolder.uniqueCallbackIdentifier; urlParams[CONSTANTS.VERSION_PARAM] = CONSTANTS.PROTOCOL_VERSION; - if (self.transportSessionId) { - urlParams[CONSTANTS.TRANSPORT_SESSION_PARAM] = self.transportSessionId; + if (this.transportSessionId) { + urlParams[CONSTANTS.TRANSPORT_SESSION_PARAM] = this.transportSessionId; } - if (self.lastSessionId) { - urlParams[CONSTANTS.LAST_SESSION_PARAM] = self.lastSessionId; + if (this.lastSessionId) { + urlParams[CONSTANTS.LAST_SESSION_PARAM] = this.lastSessionId; } - if (!isNodeSdk() && + if (!isNodeSdk() && typeof location !== 'undefined' && location.href && location.href.indexOf(CONSTANTS.FORGE_DOMAIN) !== -1) { urlParams[CONSTANTS.REFERER_PARAM] = CONSTANTS.FORGE_REF; } - var connectURL = self.urlFn(urlParams); - self.log_('Connecting via long-poll to ' + connectURL); - self.scriptTagHolder.addTag(connectURL, function() { /* do nothing */ }); + const connectURL = this.urlFn(urlParams); + this.log_('Connecting via long-poll to ' + connectURL); + this.scriptTagHolder.addTag(connectURL, () => { /* do nothing */ }); }); }; @@ -196,7 +193,7 @@ export class BrowserPollConnection { this.addDisconnectPingFrame(this.id, this.password); }; - static forceAllow_ + private static forceAllow_; /** * Forces long polling to be considered as a potential transport @@ -205,7 +202,7 @@ export class BrowserPollConnection { BrowserPollConnection.forceAllow_ = true; }; - static forceDisallow_; + private static forceDisallow_; /** * Forces longpolling to not be considered as a potential transport @@ -236,7 +233,7 @@ export class BrowserPollConnection { * Stops polling and cleans up the iframe * @private */ - shutdown_() { + private shutdown_() { this.isClosed_ = true; if (this.scriptTagHolder) { @@ -260,7 +257,7 @@ export class BrowserPollConnection { * Triggered when this transport is closed * @private */ - onClosed_() { + private onClosed_() { if (!this.isClosed_) { this.log_('Longpoll is closing itself'); this.shutdown_(); @@ -288,21 +285,21 @@ export class BrowserPollConnection { * broken into chunks (since URLs have a small maximum length). * @param {!Object} data The JSON data to transmit. */ - send(data) { - var dataStr = stringify(data); + send(data: Object) { + const dataStr = stringify(data); this.bytesSent += dataStr.length; this.stats_.incrementCounter('bytes_sent', dataStr.length); //first, lets get the base64-encoded data - var base64data = base64Encode(dataStr); + const base64data = base64Encode(dataStr); //We can only fit a certain amount in each URL, so we need to split this request //up into multiple pieces if it doesn't fit in one request. - var dataSegs = splitStringBySize(base64data, MAX_PAYLOAD_SIZE); + const dataSegs = splitStringBySize(base64data, MAX_PAYLOAD_SIZE); //Enqueue each segment for transmission. We assign each chunk a sequential ID and a total number //of segments so that we can reassemble the packet on the server. - for (var i = 0; i < dataSegs.length; i++) { + for (let i = 0; i < dataSegs.length; i++) { this.scriptTagHolder.enqueueSegment(this.curSegmentNum, dataSegs.length, dataSegs[i]); this.curSegmentNum++; } @@ -315,10 +312,10 @@ export class BrowserPollConnection { * @param {!string} id * @param {!string} pw */ - addDisconnectPingFrame(id, pw) { + addDisconnectPingFrame(id: string, pw: string) { if (isNodeSdk()) return; this.myDisconnFrame = document.createElement('iframe'); - var urlParams = {}; + const urlParams = {}; urlParams[FIREBASE_LONGPOLL_DISCONN_FRAME_REQUEST_PARAM] = 't'; urlParams[FIREBASE_LONGPOLL_ID_PARAM] = id; urlParams[FIREBASE_LONGPOLL_PW_PARAM] = pw; @@ -333,49 +330,58 @@ export class BrowserPollConnection { * @param {*} args * @private */ - incrementIncomingBytes_(args) { + private incrementIncomingBytes_(args: any) { // TODO: This is an annoying perf hit just to track the number of incoming bytes. Maybe it should be opt-in. - var bytesReceived = stringify(args).length; + const bytesReceived = stringify(args).length; this.bytesReceived += bytesReceived; this.stats_.incrementCounter('bytes_received', bytesReceived); }; } +interface IFrameElement extends HTMLIFrameElement { + doc: Document; +} + /********************************************************************************************* * A wrapper around an iframe that is used as a long-polling script holder. * @constructor * @param commandCB - The callback to be called when control commands are recevied from the server. * @param onMessageCB - The callback to be triggered when responses arrive from the server. - * @param onDisconnectCB - The callback to be triggered when this tag holder is closed + * @param onDisconnect - The callback to be triggered when this tag holder is closed * @param urlFn - A function that provides the URL of the endpoint to send data to. *********************************************************************************************/ -function FirebaseIFrameScriptHolder(commandCB, onMessageCB, onDisconnectCB, urlFn) { - this.urlFn = urlFn; - this.onDisconnect = onDisconnectCB; - +class FirebaseIFrameScriptHolder { //We maintain a count of all of the outstanding requests, because if we have too many active at once it can cause //problems in some browsers. /** * @type {CountedSet.} */ - this.outstandingRequests = new CountedSet(); + outstandingRequests = new CountedSet(); //A queue of the pending segments waiting for transmission to the server. - this.pendingSegs = []; + pendingSegs = []; //A serial number. We use this for two things: // 1) A way to ensure the browser doesn't cache responses to polls // 2) A way to make the server aware when long-polls arrive in a different order than we started them. The // server needs to release both polls in this case or it will cause problems in Opera since Opera can only execute // JSONP code in the order it was added to the iframe. - this.currentSerial = Math.floor(Math.random() * 100000000); + currentSerial = Math.floor(Math.random() * 100000000); // This gets set to false when we're "closing down" the connection (e.g. we're switching transports but there's still // incoming data from the server that we're waiting for). - this.sendNewPolls = true; - - - if (!isNodeSdk()) { + sendNewPolls = true; + + uniqueCallbackIdentifier: number; + myIFrame: IFrameElement; + alive: boolean; + myID: string; + myPW: string; + commandCB; + onMessageCB; + + constructor(commandCB, onMessageCB, public onDisconnect, public urlFn) { + if (!isNodeSdk()) { //Each script holder registers a couple of uniquely named callbacks with the window. These are called from the //iframes where we put the long-polling script tags. We have two callbacks: // 1) Command Callback - Triggered for control issues, like starting a connection. @@ -385,17 +391,17 @@ function FirebaseIFrameScriptHolder(commandCB, onMessageCB, onDisconnectCB, urlF window[FIREBASE_LONGPOLL_DATA_CB_NAME + this.uniqueCallbackIdentifier] = onMessageCB; //Create an iframe for us to add script tags to. - this.myIFrame = this.createIFrame_(); + this.myIFrame = FirebaseIFrameScriptHolder.createIFrame_(); // Set the iframe's contents. - var script = ''; + let script = ''; // if we set a javascript url, it's IE and we need to set the document domain. The javascript url is sufficient // for ie9, but ie8 needs to do it again in the document itself. if (this.myIFrame.src && this.myIFrame.src.substr(0, 'javascript:'.length) === 'javascript:') { - var currentDomain = document.domain; + const currentDomain = document.domain; script = ''; } - var iframeContents = '' + script + ''; + const iframeContents = '' + script + ''; try { this.myIFrame.doc.open(); this.myIFrame.doc.write(iframeContents); @@ -407,242 +413,241 @@ function FirebaseIFrameScriptHolder(commandCB, onMessageCB, onDisconnectCB, urlF } log(e); } - } else { - this.commandCB = commandCB; - this.onMessageCB = onMessageCB; + } else { + this.commandCB = commandCB; + this.onMessageCB = onMessageCB; + } } -} -/** - * Each browser has its own funny way to handle iframes. Here we mush them all together into one object that I can - * actually use. - * @private - * @return {Element} - */ -FirebaseIFrameScriptHolder.prototype.createIFrame_ = function() { - var iframe = document.createElement('iframe'); - iframe.style.display = 'none'; + /** + * Each browser has its own funny way to handle iframes. Here we mush them all together into one object that I can + * actually use. + * @private + * @return {Element} + */ + private static createIFrame_(): IFrameElement { + const iframe = document.createElement('iframe'); + iframe.style.display = 'none'; - // This is necessary in order to initialize the document inside the iframe - if (document.body) { - document.body.appendChild(iframe); - try { - // If document.domain has been modified in IE, this will throw an error, and we need to set the - // domain of the iframe's document manually. We can do this via a javascript: url as the src attribute - // Also note that we must do this *after* the iframe has been appended to the page. Otherwise it doesn't work. - var a = iframe.contentWindow.document; - if (!a) { - // Apologies for the log-spam, I need to do something to keep closure from optimizing out the assignment above. - log('No IE domain setting required'); + // This is necessary in order to initialize the document inside the iframe + if (document.body) { + document.body.appendChild(iframe); + try { + // If document.domain has been modified in IE, this will throw an error, and we need to set the + // domain of the iframe's document manually. We can do this via a javascript: url as the src attribute + // Also note that we must do this *after* the iframe has been appended to the page. Otherwise it doesn't work. + const a = iframe.contentWindow.document; + if (!a) { + // Apologies for the log-spam, I need to do something to keep closure from optimizing out the assignment above. + log('No IE domain setting required'); + } + } catch (e) { + const domain = document.domain; + iframe.src = 'javascript:void((function(){document.open();document.domain=\'' + domain + + '\';document.close();})())'; } - } catch (e) { - var domain = document.domain; - iframe.src = 'javascript:void((function(){document.open();document.domain=\'' + domain + - '\';document.close();})())'; + } else { + // LongPollConnection attempts to delay initialization until the document is ready, so hopefully this + // never gets hit. + throw 'Document body has not initialized. Wait to initialize Firebase until after the document is ready.'; } - } else { - // LongPollConnection attempts to delay initialization until the document is ready, so hopefully this - // never gets hit. - throw 'Document body has not initialized. Wait to initialize Firebase until after the document is ready.'; - } - // Get the document of the iframe in a browser-specific way. - if (iframe.contentDocument) { - (iframe as any).doc = iframe.contentDocument; // Firefox, Opera, Safari - } else if (iframe.contentWindow) { - (iframe as any).doc = iframe.contentWindow.document; // Internet Explorer - } else if ((iframe as any).document) { - (iframe as any).doc = (iframe as any).document; //others? - } - return iframe; -}; + // Get the document of the iframe in a browser-specific way. + if (iframe.contentDocument) { + (iframe as any).doc = iframe.contentDocument; // Firefox, Opera, Safari + } else if (iframe.contentWindow) { + (iframe as any).doc = iframe.contentWindow.document; // Internet Explorer + } else if ((iframe as any).document) { + (iframe as any).doc = (iframe as any).document; //others? + } -/** - * Cancel all outstanding queries and remove the frame. - */ -FirebaseIFrameScriptHolder.prototype.close = function() { - //Mark this iframe as dead, so no new requests are sent. - this.alive = false; - - if (this.myIFrame) { - //We have to actually remove all of the html inside this iframe before removing it from the - //window, or IE will continue loading and executing the script tags we've already added, which - //can lead to some errors being thrown. Setting innerHTML seems to be the easiest way to do this. - this.myIFrame.doc.body.innerHTML = ''; - var self = this; - setTimeout(function() { - if (self.myIFrame !== null) { - document.body.removeChild(self.myIFrame); - self.myIFrame = null; - } - }, Math.floor(0)); + return iframe; } - if (isNodeSdk() && this.myID) { - var urlParams = {}; - urlParams[FIREBASE_LONGPOLL_DISCONN_FRAME_PARAM] = 't'; - urlParams[FIREBASE_LONGPOLL_ID_PARAM] = this.myID; - urlParams[FIREBASE_LONGPOLL_PW_PARAM] = this.myPW; - var theURL = this.urlFn(urlParams); - FirebaseIFrameScriptHolder.nodeRestRequest(theURL); - } + /** + * Cancel all outstanding queries and remove the frame. + */ + close() { + //Mark this iframe as dead, so no new requests are sent. + this.alive = false; + + if (this.myIFrame) { + //We have to actually remove all of the html inside this iframe before removing it from the + //window, or IE will continue loading and executing the script tags we've already added, which + //can lead to some errors being thrown. Setting innerHTML seems to be the easiest way to do this. + this.myIFrame.doc.body.innerHTML = ''; + setTimeout(() => { + if (this.myIFrame !== null) { + document.body.removeChild(this.myIFrame); + this.myIFrame = null; + } + }, Math.floor(0)); + } + + if (isNodeSdk() && this.myID) { + var urlParams = {}; + urlParams[FIREBASE_LONGPOLL_DISCONN_FRAME_PARAM] = 't'; + urlParams[FIREBASE_LONGPOLL_ID_PARAM] = this.myID; + urlParams[FIREBASE_LONGPOLL_PW_PARAM] = this.myPW; + var theURL = this.urlFn(urlParams); + (FirebaseIFrameScriptHolder).nodeRestRequest(theURL); + } - // Protect from being called recursively. - var onDisconnect = this.onDisconnect; - if (onDisconnect) { - this.onDisconnect = null; - onDisconnect(); + // Protect from being called recursively. + const onDisconnect = this.onDisconnect; + if (onDisconnect) { + this.onDisconnect = null; + onDisconnect(); + } } -}; -/** - * Actually start the long-polling session by adding the first script tag(s) to the iframe. - * @param {!string} id - The ID of this connection - * @param {!string} pw - The password for this connection - */ -FirebaseIFrameScriptHolder.prototype.startLongPoll = function(id, pw) { - this.myID = id; - this.myPW = pw; - this.alive = true; + /** + * Actually start the long-polling session by adding the first script tag(s) to the iframe. + * @param {!string} id - The ID of this connection + * @param {!string} pw - The password for this connection + */ + startLongPoll(id: string, pw: string) { + this.myID = id; + this.myPW = pw; + this.alive = true; - //send the initial request. If there are requests queued, make sure that we transmit as many as we are currently able to. - while (this.newRequest_()) {} -}; + //send the initial request. If there are requests queued, make sure that we transmit as many as we are currently able to. + while (this.newRequest_()) {} + }; -/** - * This is called any time someone might want a script tag to be added. It adds a script tag when there aren't - * too many outstanding requests and we are still alive. - * - * If there are outstanding packet segments to send, it sends one. If there aren't, it sends a long-poll anyways if - * needed. - */ -FirebaseIFrameScriptHolder.prototype.newRequest_ = function() { - // We keep one outstanding request open all the time to receive data, but if we need to send data - // (pendingSegs.length > 0) then we create a new request to send the data. The server will automatically - // close the old request. - if (this.alive && this.sendNewPolls && this.outstandingRequests.count() < (this.pendingSegs.length > 0 ? 2 : 1)) { - //construct our url - this.currentSerial++; - var urlParams = {}; - urlParams[FIREBASE_LONGPOLL_ID_PARAM] = this.myID; - urlParams[FIREBASE_LONGPOLL_PW_PARAM] = this.myPW; - urlParams[FIREBASE_LONGPOLL_SERIAL_PARAM] = this.currentSerial; - var theURL = this.urlFn(urlParams); - //Now add as much data as we can. - var curDataString = ''; - var i = 0; - - while (this.pendingSegs.length > 0) { - //first, lets see if the next segment will fit. - var nextSeg = this.pendingSegs[0]; - if (nextSeg.d.length + SEG_HEADER_SIZE + curDataString.length <= MAX_URL_DATA_SIZE) { - //great, the segment will fit. Lets append it. - var theSeg = this.pendingSegs.shift(); - curDataString = curDataString + '&' + FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM + i + '=' + theSeg.seg + + /** + * This is called any time someone might want a script tag to be added. It adds a script tag when there aren't + * too many outstanding requests and we are still alive. + * + * If there are outstanding packet segments to send, it sends one. If there aren't, it sends a long-poll anyways if + * needed. + */ + private newRequest_() { + // We keep one outstanding request open all the time to receive data, but if we need to send data + // (pendingSegs.length > 0) then we create a new request to send the data. The server will automatically + // close the old request. + if (this.alive && this.sendNewPolls && this.outstandingRequests.count() < (this.pendingSegs.length > 0 ? 2 : 1)) { + //construct our url + this.currentSerial++; + const urlParams = {}; + urlParams[FIREBASE_LONGPOLL_ID_PARAM] = this.myID; + urlParams[FIREBASE_LONGPOLL_PW_PARAM] = this.myPW; + urlParams[FIREBASE_LONGPOLL_SERIAL_PARAM] = this.currentSerial; + let theURL = this.urlFn(urlParams); + //Now add as much data as we can. + let curDataString = ''; + let i = 0; + + while (this.pendingSegs.length > 0) { + //first, lets see if the next segment will fit. + const nextSeg = this.pendingSegs[0]; + if (nextSeg.d.length + SEG_HEADER_SIZE + curDataString.length <= MAX_URL_DATA_SIZE) { + //great, the segment will fit. Lets append it. + const theSeg = this.pendingSegs.shift(); + curDataString = curDataString + '&' + FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM + i + '=' + theSeg.seg + '&' + FIREBASE_LONGPOLL_SEGMENTS_IN_PACKET + i + '=' + theSeg.ts + '&' + FIREBASE_LONGPOLL_DATA_PARAM + i + '=' + theSeg.d; - i++; - } else { - break; + i++; + } else { + break; + } } + + theURL = theURL + curDataString; + this.addLongPollTag_(theURL, this.currentSerial); + + return true; + } else { + return false; } + }; - theURL = theURL + curDataString; - this.addLongPollTag_(theURL, this.currentSerial); + /** + * Queue a packet for transmission to the server. + * @param segnum - A sequential id for this packet segment used for reassembly + * @param totalsegs - The total number of segments in this packet + * @param data - The data for this segment. + */ + enqueueSegment(segnum, totalsegs, data) { + //add this to the queue of segments to send. + this.pendingSegs.push({seg: segnum, ts: totalsegs, d: data}); + + //send the data immediately if there isn't already data being transmitted, unless + //startLongPoll hasn't been called yet. + if (this.alive) { + this.newRequest_(); + } + }; - return true; - } else { - return false; - } -}; + /** + * Add a script tag for a regular long-poll request. + * @param {!string} url - The URL of the script tag. + * @param {!number} serial - The serial number of the request. + * @private + */ + private addLongPollTag_(url: string, serial: number) { + //remember that we sent this request. + this.outstandingRequests.add(serial, 1); -/** - * Queue a packet for transmission to the server. - * @param segnum - A sequential id for this packet segment used for reassembly - * @param totalsegs - The total number of segments in this packet - * @param data - The data for this segment. - */ -FirebaseIFrameScriptHolder.prototype.enqueueSegment = function(segnum, totalsegs, data) { - //add this to the queue of segments to send. - this.pendingSegs.push({seg: segnum, ts: totalsegs, d: data}); - - //send the data immediately if there isn't already data being transmitted, unless - //startLongPoll hasn't been called yet. - if (this.alive) { - this.newRequest_(); - } -}; + const doNewRequest = () => { + this.outstandingRequests.remove(serial); + this.newRequest_(); + }; -/** - * Add a script tag for a regular long-poll request. - * @param {!string} url - The URL of the script tag. - * @param {!number} serial - The serial number of the request. - * @private - */ -FirebaseIFrameScriptHolder.prototype.addLongPollTag_ = function(url, serial) { - var self = this; - //remember that we sent this request. - self.outstandingRequests.add(serial, 1); - - var doNewRequest = function() { - self.outstandingRequests.remove(serial); - self.newRequest_(); - }; + // If this request doesn't return on its own accord (by the server sending us some data), we'll + // create a new one after the KEEPALIVE interval to make sure we always keep a fresh request open. + const keepaliveTimeout = setTimeout(doNewRequest, Math.floor(KEEPALIVE_REQUEST_INTERVAL)); - // If this request doesn't return on its own accord (by the server sending us some data), we'll - // create a new one after the KEEPALIVE interval to make sure we always keep a fresh request open. - var keepaliveTimeout = setTimeout(doNewRequest, Math.floor(KEEPALIVE_REQUEST_INTERVAL)); + const readyStateCB = () => { + // Request completed. Cancel the keepalive. + clearTimeout(keepaliveTimeout); - var readyStateCB = function() { - // Request completed. Cancel the keepalive. - clearTimeout(keepaliveTimeout); + // Trigger a new request so we can continue receiving data. + doNewRequest(); + }; - // Trigger a new request so we can continue receiving data. - doNewRequest(); + this.addTag(url, readyStateCB); }; - this.addTag(url, readyStateCB); -}; - -/** - * Add an arbitrary script tag to the iframe. - * @param {!string} url - The URL for the script tag source. - * @param {!function()} loadCB - A callback to be triggered once the script has loaded. - */ -FirebaseIFrameScriptHolder.prototype.addTag = function(url, loadCB) { - if (isNodeSdk()) { - this.doNodeLongPoll(url, loadCB); - } else { - var self = this; - setTimeout(function() { - try { - // if we're already closed, don't add this poll - if (!self.sendNewPolls) return; - var newScript = self.myIFrame.doc.createElement('script'); - newScript.type = 'text/javascript'; - newScript.async = true; - newScript.src = url; - newScript.onload = newScript.onreadystatechange = function() { - var rstate = newScript.readyState; - if (!rstate || rstate === 'loaded' || rstate === 'complete') { - newScript.onload = newScript.onreadystatechange = null; - if (newScript.parentNode) { - newScript.parentNode.removeChild(newScript); + /** + * Add an arbitrary script tag to the iframe. + * @param {!string} url - The URL for the script tag source. + * @param {!function()} loadCB - A callback to be triggered once the script has loaded. + */ + addTag(url: string, loadCB: () => any) { + if (isNodeSdk()) { + (this).doNodeLongPoll(url, loadCB); + } else { + setTimeout(() => { + try { + // if we're already closed, don't add this poll + if (!this.sendNewPolls) return; + const newScript = this.myIFrame.doc.createElement('script'); + newScript.type = 'text/javascript'; + newScript.async = true; + newScript.src = url; + newScript.onload = (newScript).onreadystatechange = function () { + const rstate = (newScript).readyState; + if (!rstate || rstate === 'loaded' || rstate === 'complete') { + newScript.onload = (newScript).onreadystatechange = null; + if (newScript.parentNode) { + newScript.parentNode.removeChild(newScript); + } + loadCB(); } - loadCB(); - } - }; - newScript.onerror = function() { - log('Long-poll script failed to load: ' + url); - self.sendNewPolls = false; - self.close(); - }; - self.myIFrame.doc.body.appendChild(newScript); - } catch (e) { - // TODO: we should make this error visible somehow - } - }, Math.floor(1)); + }; + newScript.onerror = () => { + log('Long-poll script failed to load: ' + url); + this.sendNewPolls = false; + this.close(); + }; + this.myIFrame.doc.body.appendChild(newScript); + } catch (e) { + // TODO: we should make this error visible somehow + } + }, Math.floor(1)); + } } -}; +} if (isNodeSdk()) { /** @@ -672,7 +677,7 @@ if (isNodeSdk()) { * @param {!string} url * @param {function()} loadCB */ - FirebaseIFrameScriptHolder.prototype.doNodeLongPoll = function(url, loadCB) { + (FirebaseIFrameScriptHolder.prototype).doNodeLongPoll = function(url, loadCB) { var self = this; (FirebaseIFrameScriptHolder as any).nodeRestRequest({ url: url, forever: true }, function(body) { self.evalBody(body); @@ -684,12 +689,12 @@ if (isNodeSdk()) { * Evaluates the string contents of a jsonp response. * @param {!string} body */ - FirebaseIFrameScriptHolder.prototype.evalBody = function(body) { + (FirebaseIFrameScriptHolder.prototype).evalBody = function(body) { var jsonpCB; //jsonpCB is externed in firebase-extern.js eval('jsonpCB = function(' + FIREBASE_LONGPOLL_COMMAND_CB_NAME + ', ' + FIREBASE_LONGPOLL_DATA_CB_NAME + ') {' + - body + - '}'); + body + + '}'); jsonpCB(this.commandCB, this.onMessageCB); }; } \ No newline at end of file diff --git a/src/database/realtime/Connection.ts b/src/database/realtime/Connection.ts index 6bac8e96f66..1896ed2946a 100644 --- a/src/database/realtime/Connection.ts +++ b/src/database/realtime/Connection.ts @@ -1,43 +1,44 @@ -import { - error, +import { + error, logWrapper, - requireKey, - setTimeoutNonBlocking, - warn , -} from "../core/util/util"; -import { PersistentStorage } from "../core/storage/storage"; -import { CONSTANTS } from "./Constants"; -import { TransportManager } from "./TransportManager"; + requireKey, + setTimeoutNonBlocking, + warn, +} from '../core/util/util'; +import { PersistentStorage } from '../core/storage/storage'; +import { CONSTANTS } from './Constants'; +import { TransportManager } from './TransportManager'; +import { RepoInfo } from '../core/RepoInfo'; // Abort upgrade attempt if it takes longer than 60s. -var UPGRADE_TIMEOUT = 60000; +const UPGRADE_TIMEOUT = 60000; // For some transports (WebSockets), we need to "validate" the transport by exchanging a few requests and responses. // If we haven't sent enough requests within 5s, we'll start sending noop ping requests. -var DELAY_BEFORE_SENDING_EXTRA_REQUESTS = 5000; +const DELAY_BEFORE_SENDING_EXTRA_REQUESTS = 5000; // If the initial data sent triggers a lot of bandwidth (i.e. it's a large put or a listen for a large amount of data) // then we may not be able to exchange our ping/pong requests within the healthy timeout. So if we reach the timeout // but we've sent/received enough bytes, we don't cancel the connection. -var BYTES_SENT_HEALTHY_OVERRIDE = 10 * 1024; -var BYTES_RECEIVED_HEALTHY_OVERRIDE = 100 * 1024; +const BYTES_SENT_HEALTHY_OVERRIDE = 10 * 1024; +const BYTES_RECEIVED_HEALTHY_OVERRIDE = 100 * 1024; -var REALTIME_STATE_CONNECTING = 0; -var REALTIME_STATE_CONNECTED = 1; -var REALTIME_STATE_DISCONNECTED = 2; +const REALTIME_STATE_CONNECTING = 0; +const REALTIME_STATE_CONNECTED = 1; +const REALTIME_STATE_DISCONNECTED = 2; -var MESSAGE_TYPE = 't'; -var MESSAGE_DATA = 'd'; -var CONTROL_SHUTDOWN = 's'; -var CONTROL_RESET = 'r'; -var CONTROL_ERROR = 'e'; -var CONTROL_PONG = 'o'; -var SWITCH_ACK = 'a'; -var END_TRANSMISSION = 'n'; -var PING = 'p'; +const MESSAGE_TYPE = 't'; +const MESSAGE_DATA = 'd'; +const CONTROL_SHUTDOWN = 's'; +const CONTROL_RESET = 'r'; +const CONTROL_ERROR = 'e'; +const CONTROL_PONG = 'o'; +const SWITCH_ACK = 'a'; +const END_TRANSMISSION = 'n'; +const PING = 'p'; -var SERVER_HELLO = 'h'; +const SERVER_HELLO = 'h'; /** * Creates a new real-time connection to the server using whichever method works @@ -45,7 +46,7 @@ var SERVER_HELLO = 'h'; * * @constructor * @param {!string} connId - an id for this connection - * @param {!fb.core.RepoInfo} repoInfo - the info for the endpoint to connect to + * @param {!RepoInfo} repoInfo - the info for the endpoint to connect to * @param {function(Object)} onMessage - the callback to be triggered when a server-push message arrives * @param {function(number, string)} onReady - the callback to be triggered when this connection is ready to send messages. * @param {function()} onDisconnect - the callback to be triggered when a connection was lost @@ -54,29 +55,36 @@ var SERVER_HELLO = 'h'; */ export class Connection { - conn_; connectionCount; - healthyTimeout_; id; - isHealthy_; lastSessionId; - log_; - onDisconnect_; - onKill_; - onMessage_; - onReady_; pendingDataMessages; - primaryResponsesRequired_ - repoInfo_; - rx_; - secondaryConn_; - secondaryResponsesRequired_; sessionId; - state_; - transportManager_; - tx_; - constructor(connId, repoInfo, onMessage, onReady, onDisconnect, onKill, lastSessionId) { + private conn_; + private healthyTimeout_; + private isHealthy_; + private log_; + private onDisconnect_; + private onKill_; + private onMessage_; + private onReady_; + private primaryResponsesRequired_; + private repoInfo_; + private rx_; + private secondaryConn_; + private secondaryResponsesRequired_; + private state_; + private transportManager_; + private tx_; + + constructor(connId: string, + repoInfo: RepoInfo, + onMessage: (a: Object) => any, + onReady: (a: number, b: string) => any, + onDisconnect: () => any, + onKill: (a: string) => any, + lastSessionId?: string) { this.id = connId; this.log_ = logWrapper('c:' + this.id + ':'); this.onMessage_ = onMessage; @@ -92,51 +100,52 @@ export class Connection { this.log_('Connection created'); this.start_(); } + /** * Starts a connection attempt * @private */ - start_() { - var conn = this.transportManager_.initialTransport(); + private start_() { + const conn = this.transportManager_.initialTransport(); this.conn_ = new conn(this.nextTransportId_(), this.repoInfo_, /*transportSessionId=*/undefined, this.lastSessionId); // For certain transports (WebSockets), we need to send and receive several messages back and forth before we // can consider the transport healthy. this.primaryResponsesRequired_ = conn['responsesRequiredToBeHealthy'] || 0; - var onMessageReceived = this.connReceiver_(this.conn_); - var onConnectionLost = this.disconnReceiver_(this.conn_); + const onMessageReceived = this.connReceiver_(this.conn_); + const onConnectionLost = this.disconnReceiver_(this.conn_); this.tx_ = this.conn_; this.rx_ = this.conn_; this.secondaryConn_ = null; this.isHealthy_ = false; - var self = this; + const self = this; /* - * Firefox doesn't like when code from one iframe tries to create another iframe by way of the parent frame. - * This can occur in the case of a redirect, i.e. we guessed wrong on what server to connect to and received a reset. - * Somehow, setTimeout seems to make this ok. That doesn't make sense from a security perspective, since you should - * still have the context of your originating frame. - */ - setTimeout(function() { + * Firefox doesn't like when code from one iframe tries to create another iframe by way of the parent frame. + * This can occur in the case of a redirect, i.e. we guessed wrong on what server to connect to and received a reset. + * Somehow, setTimeout seems to make this ok. That doesn't make sense from a security perspective, since you should + * still have the context of your originating frame. + */ + setTimeout(function () { // self.conn_ gets set to null in some of the tests. Check to make sure it still exists before using it self.conn_ && self.conn_.open(onMessageReceived, onConnectionLost); }, Math.floor(0)); - var healthyTimeout_ms = conn['healthyTimeout'] || 0; + const healthyTimeout_ms = conn['healthyTimeout'] || 0; if (healthyTimeout_ms > 0) { - this.healthyTimeout_ = setTimeoutNonBlocking(function() { + this.healthyTimeout_ = setTimeoutNonBlocking(function () { self.healthyTimeout_ = null; if (!self.isHealthy_) { if (self.conn_ && self.conn_.bytesReceived > BYTES_RECEIVED_HEALTHY_OVERRIDE) { self.log_('Connection exceeded healthy timeout but has received ' + self.conn_.bytesReceived + - ' bytes. Marking connection healthy.'); + ' bytes. Marking connection healthy.'); self.isHealthy_ = true; self.conn_.markConnectionHealthy(); } else if (self.conn_ && self.conn_.bytesSent > BYTES_SENT_HEALTHY_OVERRIDE) { self.log_('Connection exceeded healthy timeout but has sent ' + self.conn_.bytesSent + - ' bytes. Leaving connection alive.'); + ' bytes. Leaving connection alive.'); // NOTE: We don't want to mark it healthy, since we have no guarantee that the bytes have made it to // the server. } else { @@ -152,13 +161,13 @@ export class Connection { * @return {!string} * @private */ - nextTransportId_() { + private nextTransportId_() { return 'c:' + this.id + ':' + this.connectionCount++; }; - disconnReceiver_(conn) { - var self = this; - return function(everConnected) { + private disconnReceiver_(conn) { + const self = this; + return function (everConnected) { if (conn === self.conn_) { self.onConnectionLost_(everConnected); } else if (conn === self.secondaryConn_) { @@ -170,9 +179,9 @@ export class Connection { } }; - connReceiver_(conn) { - var self = this; - return function(message) { + private connReceiver_(conn) { + const self = this; + return function (message) { if (self.state_ != REALTIME_STATE_DISCONNECTED) { if (conn === self.rx_) { self.onPrimaryMessageReceived_(message); @@ -191,7 +200,7 @@ export class Connection { */ sendRequest(dataMsg) { // wrap in a data message envelope and send it on - var msg = {'t': 'd', 'd': dataMsg}; + const msg = {'t': 'd', 'd': dataMsg}; this.sendData_(msg); }; @@ -204,9 +213,9 @@ export class Connection { } }; - onSecondaryControl_(controlData) { + private onSecondaryControl_(controlData) { if (MESSAGE_TYPE in controlData) { - var cmd = controlData[MESSAGE_TYPE]; + const cmd = controlData[MESSAGE_TYPE]; if (cmd === SWITCH_ACK) { this.upgradeIfSecondaryHealthy_(); } else if (cmd === CONTROL_RESET) { @@ -225,9 +234,9 @@ export class Connection { } }; - onSecondaryMessageReceived_(parsedData) { - var layer = requireKey('t', parsedData); - var data = requireKey('d', parsedData); + private onSecondaryMessageReceived_(parsedData) { + const layer = requireKey('t', parsedData); + const data = requireKey('d', parsedData); if (layer == 'c') { this.onSecondaryControl_(data); } else if (layer == 'd') { @@ -238,7 +247,7 @@ export class Connection { } }; - upgradeIfSecondaryHealthy_() { + private upgradeIfSecondaryHealthy_() { if (this.secondaryResponsesRequired_ <= 0) { this.log_('Secondary connection is healthy.'); this.isHealthy_ = true; @@ -247,11 +256,11 @@ export class Connection { } else { // Send a ping to make sure the connection is healthy. this.log_('sending ping on secondary.'); - this.secondaryConn_.send({'t': 'c', 'd': {'t': PING, 'd': { } }}); + this.secondaryConn_.send({'t': 'c', 'd': {'t': PING, 'd': {}}}); } }; - proceedWithUpgrade_() { + private proceedWithUpgrade_() { // tell this connection to consider itself open this.secondaryConn_.start(); // send ack @@ -267,10 +276,10 @@ export class Connection { this.tryCleanupConnection(); }; - onPrimaryMessageReceived_(parsedData) { + private onPrimaryMessageReceived_(parsedData) { // Must refer to parsedData properties in quotes, so closure doesn't touch them. - var layer = requireKey('t', parsedData); - var data = requireKey('d', parsedData); + const layer = requireKey('t', parsedData); + const data = requireKey('d', parsedData); if (layer == 'c') { this.onControl_(data); } else if (layer == 'd') { @@ -278,14 +287,14 @@ export class Connection { } }; - onDataMessage_(message) { + private onDataMessage_(message) { this.onPrimaryResponse_(); // We don't do anything with data messages, just kick them up a level this.onMessage_(message); }; - onPrimaryResponse_() { + private onPrimaryResponse_() { if (!this.isHealthy_) { this.primaryResponsesRequired_--; if (this.primaryResponsesRequired_ <= 0) { @@ -296,16 +305,16 @@ export class Connection { } }; - onControl_(controlData) { - var cmd = requireKey(MESSAGE_TYPE, controlData); + private onControl_(controlData) { + const cmd = requireKey(MESSAGE_TYPE, controlData); if (MESSAGE_DATA in controlData) { - var payload = controlData[MESSAGE_DATA]; + const payload = controlData[MESSAGE_DATA]; if (cmd === SERVER_HELLO) { this.onHandshake_(payload); } else if (cmd === END_TRANSMISSION) { this.log_('recvd end transmission on primary'); this.rx_ = this.secondaryConn_; - for (var i = 0; i < this.pendingDataMessages.length; ++i) { + for (let i = 0; i < this.pendingDataMessages.length; ++i) { this.onDataMessage_(this.pendingDataMessages[i]); } this.pendingDataMessages = []; @@ -334,10 +343,10 @@ export class Connection { * @param {Object} handshake The handshake data returned from the server * @private */ - onHandshake_(handshake) { - var timestamp = handshake['ts']; - var version = handshake['v']; - var host = handshake['h']; + private onHandshake_(handshake) { + const timestamp = handshake['ts']; + const version = handshake['v']; + const host = handshake['h']; this.sessionId = handshake['s']; this.repoInfo_.updateHost(host); // if we've already closed the connection, then don't bother trying to progress further @@ -352,27 +361,27 @@ export class Connection { } }; - tryStartUpgrade_() { - var conn = this.transportManager_.upgradeTransport(); + private tryStartUpgrade_() { + const conn = this.transportManager_.upgradeTransport(); if (conn) { this.startUpgrade_(conn); } }; - startUpgrade_(conn) { + private startUpgrade_(conn) { this.secondaryConn_ = new conn(this.nextTransportId_(), this.repoInfo_, this.sessionId); // For certain transports (WebSockets), we need to send and receive several messages back and forth before we // can consider the transport healthy. this.secondaryResponsesRequired_ = conn['responsesRequiredToBeHealthy'] || 0; - var onMessage = this.connReceiver_(this.secondaryConn_); - var onDisconnect = this.disconnReceiver_(this.secondaryConn_); + const onMessage = this.connReceiver_(this.secondaryConn_); + const onDisconnect = this.disconnReceiver_(this.secondaryConn_); this.secondaryConn_.open(onMessage, onDisconnect); // If we haven't successfully upgraded after UPGRADE_TIMEOUT, give up and kill the secondary. - var self = this; - setTimeoutNonBlocking(function() { + const self = this; + setTimeoutNonBlocking(function () { if (self.secondaryConn_) { self.log_('Timed out trying to upgrade.'); self.secondaryConn_.close(); @@ -380,7 +389,7 @@ export class Connection { }, Math.floor(UPGRADE_TIMEOUT)); }; - onReset_(host) { + private onReset_(host) { this.log_('Reset packet received. New host: ' + host); this.repoInfo_.updateHost(host); // TODO: if we're already "connected", we need to trigger a disconnect at the next layer up. @@ -394,7 +403,7 @@ export class Connection { } }; - onConnectionEstablished_(conn, timestamp) { + private onConnectionEstablished_(conn, timestamp) { this.log_('Realtime connection established.'); this.conn_ = conn; this.state_ = REALTIME_STATE_CONNECTED; @@ -404,30 +413,29 @@ export class Connection { this.onReady_ = null; } - var self = this; + const self = this; // If after 5 seconds we haven't sent enough requests to the server to get the connection healthy, // send some pings. if (this.primaryResponsesRequired_ === 0) { this.log_('Primary connection is healthy.'); this.isHealthy_ = true; } else { - setTimeoutNonBlocking(function() { + setTimeoutNonBlocking(function () { self.sendPingOnPrimaryIfNecessary_(); }, Math.floor(DELAY_BEFORE_SENDING_EXTRA_REQUESTS)); } }; - sendPingOnPrimaryIfNecessary_() { + private sendPingOnPrimaryIfNecessary_() { // If the connection isn't considered healthy yet, we'll send a noop ping packet request. - if (!this.isHealthy_ && this.state_ === REALTIME_STATE_CONNECTED) - { - this.log_('sending ping on primary.'); - this.sendData_({'t': 'c', 'd': {'t': PING, 'd': {} }}); + if (!this.isHealthy_ && this.state_ === REALTIME_STATE_CONNECTED) { + this.log_('sending ping on primary.'); + this.sendData_({'t': 'c', 'd': {'t': PING, 'd': {}}}); } }; - onSecondaryConnectionLost_() { - var conn = this.secondaryConn_; + private onSecondaryConnectionLost_() { + const conn = this.secondaryConn_; this.secondaryConn_ = null; if (this.tx_ === conn || this.rx_ === conn) { // we are relying on this connection already in some capacity. Therefore, a failure is real @@ -441,7 +449,7 @@ export class Connection { * we should flush the host cache * @private */ - onConnectionLost_(everConnected) { + private onConnectionLost_(everConnected) { this.conn_ = null; // NOTE: IF you're seeing a Firefox error for this line, I think it might be because it's getting @@ -466,7 +474,7 @@ export class Connection { * @param {string} reason * @private */ - onConnectionShutdown_(reason) { + private onConnectionShutdown_(reason) { this.log_('Connection shutdown command received. Shutting down...'); if (this.onKill_) { @@ -482,7 +490,7 @@ export class Connection { }; - sendData_(data) { + private sendData_(data) { if (this.state_ !== REALTIME_STATE_CONNECTED) { throw 'Connection is not connected'; } else { @@ -511,7 +519,7 @@ export class Connection { * * @private */ - closeConnections_() { + private closeConnections_() { this.log_('Shutting down all connections'); if (this.conn_) { this.conn_.close(); @@ -528,5 +536,6 @@ export class Connection { this.healthyTimeout_ = null; } }; -}; +} + diff --git a/src/database/realtime/Transport.ts b/src/database/realtime/Transport.ts index 50b39528f97..b0d0b066738 100644 --- a/src/database/realtime/Transport.ts +++ b/src/database/realtime/Transport.ts @@ -1,35 +1,40 @@ -/** - * - * @param {string} connId An identifier for this connection, used for logging - * @param {fb.core.RepoInfo} repoInfo The info for the endpoint to send data to. - * @param {string=} sessionId Optional sessionId if we're connecting to an existing session - * @interface - */ -export const Transport = function(connId, repoInfo, sessionId) {}; +import { RepoInfo } from '../core/RepoInfo'; -/** - * @param {function(Object)} onMessage Callback when messages arrive - * @param {function()} onDisconnect Callback with connection lost. - */ -Transport.prototype.open = function(onMessage, onDisconnect) {}; +export abstract class Transport { + /** + * Bytes received since connection started. + * @type {number} + */ + abstract bytesReceived: number; -Transport.prototype.start = function() {}; + /** + * Bytes sent since connection started. + * @type {number} + */ + abstract bytesSent: number; -Transport.prototype.close = function() {}; + /** + * + * @param {string} connId An identifier for this connection, used for logging + * @param {RepoInfo} repoInfo The info for the endpoint to send data to. + * @param {string=} transportSessionId Optional transportSessionId if this is connecting to an existing transport session + * @param {string=} lastSessionId Optional lastSessionId if there was a previous connection + * @interface + */ + constructor(connId: string, repoInfo: RepoInfo, transportSessionId?: string, lastSessionId?: string) {} -/** - * @param {!Object} data The JSON data to transmit - */ -Transport.prototype.send = function(data) {}; + /** + * @param {function(Object)} onMessage Callback when messages arrive + * @param {function()} onDisconnect Callback with connection lost. + */ + abstract open(onMessage: (a: Object) => any, onDisconnect: () => any); -/** - * Bytes received since connection started. - * @type {number} - */ -Transport.prototype.bytesReceived; + abstract start(); -/** - * Bytes sent since connection started. - * @type {number} - */ -Transport.prototype.bytesSent; + abstract close(); + + /** + * @param {!Object} data The JSON data to transmit + */ + abstract send(data: Object); +} \ No newline at end of file diff --git a/src/database/realtime/TransportManager.ts b/src/database/realtime/TransportManager.ts index 5bde64581a1..f9c7096488e 100644 --- a/src/database/realtime/TransportManager.ts +++ b/src/database/realtime/TransportManager.ts @@ -9,13 +9,13 @@ import { warn, each } from "../core/util/util"; * It starts with longpolling in a browser, and httppolling on node. It then upgrades to websockets if * they are available. * @constructor - * @param {!fb.core.RepoInfo} repoInfo Metadata around the namespace we're connecting to + * @param {!RepoInfo} repoInfo Metadata around the namespace we're connecting to */ export class TransportManager { transports_: Array; /** * @const - * @type {!Array.} + * @type {!Array.} */ static get ALL_TRANSPORTS() { return [ @@ -28,12 +28,12 @@ export class TransportManager { }; /** - * @param {!fb.core.RepoInfo} repoInfo + * @param {!RepoInfo} repoInfo * @private */ initTransports_(repoInfo) { - var isWebSocketsAvailable = WebSocketConnection && WebSocketConnection['isAvailable'](); - var isSkipPollConnection = isWebSocketsAvailable && !WebSocketConnection.previouslyFailed(); + const isWebSocketsAvailable = WebSocketConnection && WebSocketConnection['isAvailable'](); + let isSkipPollConnection = isWebSocketsAvailable && !WebSocketConnection.previouslyFailed(); if (repoInfo.webSocketOnly) { if (!isWebSocketsAvailable) @@ -45,7 +45,7 @@ export class TransportManager { if (isSkipPollConnection) { this.transports_ = [WebSocketConnection]; } else { - var transports = this.transports_ = []; + const transports = this.transports_ = []; each(TransportManager.ALL_TRANSPORTS, function(i, transport) { if (transport && transport['isAvailable']()) { transports.push(transport); @@ -55,7 +55,7 @@ export class TransportManager { } /** - * @return {function(new:Transport, !string, !fb.core.RepoInfo, string=, string=)} The constructor for the + * @return {function(new:Transport, !string, !RepoInfo, string=, string=)} The constructor for the * initial transport to use */ initialTransport() { diff --git a/src/database/realtime/WebSocketConnection.ts b/src/database/realtime/WebSocketConnection.ts index 402bafdf487..009c643af65 100644 --- a/src/database/realtime/WebSocketConnection.ts +++ b/src/database/realtime/WebSocketConnection.ts @@ -1,14 +1,16 @@ +import { RepoInfo } from '../core/RepoInfo'; declare const MozWebSocket; import firebase from "../../app"; -import { assert } from "../../utils/assert"; -import { logWrapper, splitStringBySize } from "../core/util/util"; -import { StatsManager } from "../core/stats/StatsManager"; -import { CONSTANTS } from "./Constants"; +import { assert } from '../../utils/assert'; +import { logWrapper, splitStringBySize } from '../core/util/util'; +import { StatsManager } from '../core/stats/StatsManager'; +import { CONSTANTS } from './Constants'; import { CONSTANTS as ENV_CONSTANTS } from "../../utils/constants"; -import { PersistentStorage } from "../core/storage/storage"; -import { jsonEval, stringify } from "../../utils/json"; +import { PersistentStorage } from '../core/storage/storage'; +import { jsonEval, stringify } from '../../utils/json'; import { isNodeSdk } from "../../utils/environment"; +import { Transport } from './Transport'; const WEBSOCKET_MAX_FRAME_SIZE = 16384; const WEBSOCKET_KEEPALIVE_INTERVAL = 45000; @@ -25,31 +27,29 @@ if (isNodeSdk()) { /** * Create a new websocket connection with the given callbacks. * @constructor - * @implements {fb.realtime.Transport} + * @implements {Transport} * @param {string} connId identifier for this transport - * @param {fb.core.RepoInfo} repoInfo The info for the websocket endpoint. + * @param {RepoInfo} repoInfo The info for the websocket endpoint. * @param {string=} opt_transportSessionId Optional transportSessionId if this is connecting to an existing transport * session * @param {string=} opt_lastSessionId Optional lastSessionId if there was a previous connection */ -export class WebSocketConnection { - connId; - private log_; +export class WebSocketConnection implements Transport { keepaliveTimer; frames; totalFrames: number; bytesSent: number; bytesReceived: number; - private stats_; connURL; onDisconnect; onMessage; - everConnected_: boolean; mySock; - isClosed_; + private log_; + private stats_; + private everConnected_: boolean; + private isClosed_: boolean; - constructor(connId, repoInfo, opt_transportSessionId, opt_lastSessionId) { - this.connId = connId; + constructor(public connId: string, repoInfo: RepoInfo, transportSessionId?: string, lastSessionId?: string) { this.log_ = logWrapper(this.connId); this.keepaliveTimer = null; this.frames = null; @@ -57,44 +57,44 @@ export class WebSocketConnection { this.bytesSent = 0; this.bytesReceived = 0; this.stats_ = StatsManager.getCollection(repoInfo); - this.connURL = this.connectionURL_(repoInfo, opt_transportSessionId, opt_lastSessionId); + this.connURL = WebSocketConnection.connectionURL_(repoInfo, transportSessionId, lastSessionId); } /** - * @param {fb.core.RepoInfo} repoInfo The info for the websocket endpoint. - * @param {string=} opt_transportSessionId Optional transportSessionId if this is connecting to an existing transport + * @param {RepoInfo} repoInfo The info for the websocket endpoint. + * @param {string=} transportSessionId Optional transportSessionId if this is connecting to an existing transport * session - * @param {string=} opt_lastSessionId Optional lastSessionId if there was a previous connection + * @param {string=} lastSessionId Optional lastSessionId if there was a previous connection * @return {string} connection url * @private */ - connectionURL_(repoInfo, opt_transportSessionId, opt_lastSessionId) { - var urlParams = {}; + private static connectionURL_(repoInfo: RepoInfo, transportSessionId?: string, lastSessionId?: string): string { + const urlParams = {}; urlParams[CONSTANTS.VERSION_PARAM] = CONSTANTS.PROTOCOL_VERSION; if (!isNodeSdk() && - typeof location !== 'undefined' && - location.href && - location.href.indexOf(CONSTANTS.FORGE_DOMAIN) !== -1) { + typeof location !== 'undefined' && + location.href && + location.href.indexOf(CONSTANTS.FORGE_DOMAIN) !== -1) { urlParams[CONSTANTS.REFERER_PARAM] = CONSTANTS.FORGE_REF; } - if (opt_transportSessionId) { - urlParams[CONSTANTS.TRANSPORT_SESSION_PARAM] = opt_transportSessionId; + if (transportSessionId) { + urlParams[CONSTANTS.TRANSPORT_SESSION_PARAM] = transportSessionId; } - if (opt_lastSessionId) { - urlParams[CONSTANTS.LAST_SESSION_PARAM] = opt_lastSessionId; + if (lastSessionId) { + urlParams[CONSTANTS.LAST_SESSION_PARAM] = lastSessionId; } return repoInfo.connectionURL(CONSTANTS.WEBSOCKET, urlParams); } /** * - * @param onMess Callback when messages arrive - * @param onDisconn Callback with connection lost. + * @param onMessage Callback when messages arrive + * @param onDisconnect Callback with connection lost. */ - open(onMess, onDisconn) { - this.onDisconnect = onDisconn; - this.onMessage = onMess; + open(onMessage: (msg: Object) => any, onDisconnect: () => any) { + this.onDisconnect = onDisconnect; + this.onMessage = onMessage; this.log_('Websocket connecting to ' + this.connURL); @@ -104,16 +104,16 @@ export class WebSocketConnection { try { if (isNodeSdk()) { - var device = ENV_CONSTANTS.NODE_ADMIN ? 'AdminNode' : 'Node'; + const device = ENV_CONSTANTS.NODE_ADMIN ? 'AdminNode' : 'Node'; // UA Format: Firebase//// - var options = { + const options = { 'headers': { 'User-Agent': 'Firebase/' + CONSTANTS.PROTOCOL_VERSION + '/' + firebase.SDK_VERSION + '/' + process.platform + '/' + device }}; // Plumb appropriate http_proxy environment variable into faye-websocket if it exists. - var env = process['env']; - var proxy = (this.connURL.indexOf("wss://") == 0) + const env = process['env']; + const proxy = (this.connURL.indexOf("wss://") == 0) ? (env['HTTPS_PROXY'] || env['https_proxy']) : (env['HTTP_PROXY'] || env['http_proxy']); @@ -129,7 +129,7 @@ export class WebSocketConnection { this.mySock = new WebSocketImpl(this.connURL); } catch (e) { this.log_('Error instantiating WebSocket.'); - var error = e.message || e.data; + const error = e.message || e.data; if (error) { this.log_(error); } @@ -137,30 +137,28 @@ export class WebSocketConnection { return; } - var self = this; - - this.mySock.onopen = function() { - self.log_('Websocket connected.'); - self.everConnected_ = true; + this.mySock.onopen = () => { + this.log_('Websocket connected.'); + this.everConnected_ = true; }; - this.mySock.onclose = function() { - self.log_('Websocket connection was disconnected.'); - self.mySock = null; - self.onClosed_(); + this.mySock.onclose = () => { + this.log_('Websocket connection was disconnected.'); + this.mySock = null; + this.onClosed_(); }; - this.mySock.onmessage = function(m) { - self.handleIncomingFrame(m); + this.mySock.onmessage = (m) => { + this.handleIncomingFrame(m); }; - this.mySock.onerror = function(e) { - self.log_('WebSocket error. Closing connection.'); - var error = e.message || e.data; + this.mySock.onerror = (e) => { + this.log_('WebSocket error. Closing connection.'); + const error = e.message || e.data; if (error) { - self.log_(error); + this.log_(error); } - self.onClosed_(); + this.onClosed_(); }; } @@ -170,16 +168,16 @@ export class WebSocketConnection { start() {}; static forceDisallow_: Boolean; + static forceDisallow() { WebSocketConnection.forceDisallow_ = true; } - - static isAvailable() { - var isOldAndroid = false; + static isAvailable(): boolean { + let isOldAndroid = false; if (typeof navigator !== 'undefined' && navigator.userAgent) { - var oldAndroidRegex = /Android ([0-9]{0,}\.[0-9]{0,})/; - var oldAndroidMatch = navigator.userAgent.match(oldAndroidRegex); + const oldAndroidRegex = /Android ([0-9]{0,}\.[0-9]{0,})/; + const oldAndroidMatch = navigator.userAgent.match(oldAndroidRegex); if (oldAndroidMatch && oldAndroidMatch.length > 1) { if (parseFloat(oldAndroidMatch[1]) < 4.4) { isOldAndroid = true; @@ -210,23 +208,23 @@ export class WebSocketConnection { * Returns true if we previously failed to connect with this transport. * @return {boolean} */ - static previouslyFailed() { + static previouslyFailed(): boolean { // If our persistent storage is actually only in-memory storage, // we default to assuming that it previously failed to be safe. return PersistentStorage.isInMemoryStorage || - PersistentStorage.get('previous_websocket_failure') === true; + PersistentStorage.get('previous_websocket_failure') === true; }; markConnectionHealthy() { PersistentStorage.remove('previous_websocket_failure'); }; - appendFrame_(data) { + private appendFrame_(data) { this.frames.push(data); if (this.frames.length == this.totalFrames) { - var fullMess = this.frames.join(''); + const fullMess = this.frames.join(''); this.frames = null; - var jsonMess = jsonEval(fullMess); + const jsonMess = jsonEval(fullMess); //handle the message this.onMessage(jsonMess); @@ -237,7 +235,7 @@ export class WebSocketConnection { * @param {number} frameCount The number of frames we are expecting from the server * @private */ - handleNewFrameCount_(frameCount) { + private handleNewFrameCount_(frameCount: number) { this.totalFrames = frameCount; this.frames = []; } @@ -248,12 +246,12 @@ export class WebSocketConnection { * @return {?String} Any remaining data to be process, or null if there is none * @private */ - extractFrameCount_(data) { + private extractFrameCount_(data: string): string | null { assert(this.frames === null, 'We already have a frame buffer'); // TODO: The server is only supposed to send up to 9999 frames (i.e. length <= 4), but that isn't being enforced // currently. So allowing larger frame counts (length <= 6). See https://app.asana.com/0/search/8688598998380/8237608042508 if (data.length <= 6) { - var frameCount = Number(data); + const frameCount = Number(data); if (!isNaN(frameCount)) { this.handleNewFrameCount_(frameCount); return null; @@ -270,7 +268,7 @@ export class WebSocketConnection { handleIncomingFrame(mess) { if (this.mySock === null) return; // Chrome apparently delivers incoming packets even after we .close() the connection sometimes. - var data = mess['data']; + const data = mess['data']; this.bytesReceived += data.length; this.stats_.incrementCounter('bytes_received', data.length); @@ -281,7 +279,7 @@ export class WebSocketConnection { this.appendFrame_(data); } else { // try to parse out a frame count, otherwise, assume 1 and process it - var remainingData = this.extractFrameCount_(data); + const remainingData = this.extractFrameCount_(data); if (remainingData !== null) { this.appendFrame_(remainingData); } @@ -292,18 +290,18 @@ export class WebSocketConnection { * Send a message to the server * @param {Object} data The JSON object to transmit */ - send(data) { + send(data: Object) { this.resetKeepAlive(); - var dataStr = stringify(data); + const dataStr = stringify(data); this.bytesSent += dataStr.length; this.stats_.incrementCounter('bytes_sent', dataStr.length); //We can only fit a certain amount in each websocket frame, so we need to split this request //up into multiple pieces if it doesn't fit in one request. - var dataSegs = splitStringBySize(dataStr, WEBSOCKET_MAX_FRAME_SIZE); + const dataSegs = splitStringBySize(dataStr, WEBSOCKET_MAX_FRAME_SIZE); //Send the length header if (dataSegs.length > 1) { @@ -311,12 +309,12 @@ export class WebSocketConnection { } //Send the actual data in segments. - for (var i = 0; i < dataSegs.length; i++) { + for (let i = 0; i < dataSegs.length; i++) { this.sendString_(dataSegs[i]); } }; - shutdown_() { + private shutdown_() { this.isClosed_ = true; if (this.keepaliveTimer) { clearInterval(this.keepaliveTimer); @@ -329,7 +327,7 @@ export class WebSocketConnection { } }; - onClosed_() { + private onClosed_() { if (!this.isClosed_) { this.log_('WebSocket is closing itself'); this.shutdown_(); @@ -358,14 +356,13 @@ export class WebSocketConnection { * the last activity. */ resetKeepAlive() { - var self = this; clearInterval(this.keepaliveTimer); - this.keepaliveTimer = setInterval(function() { + this.keepaliveTimer = setInterval(() => { //If there has been no websocket activity for a while, send a no-op - if (self.mySock) { - self.sendString_('0'); + if (this.mySock) { + this.sendString_('0'); } - self.resetKeepAlive(); + this.resetKeepAlive(); }, Math.floor(WEBSOCKET_KEEPALIVE_INTERVAL)); }; @@ -375,7 +372,7 @@ export class WebSocketConnection { * @param {string} str String to send. * @private */ - sendString_(str) { + private sendString_(str: string) { // Firefox seems to sometimes throw exceptions (NS_ERROR_UNEXPECTED) from websocket .send() // calls for some unknown reason. We treat these as an error and disconnect. // See https://app.asana.com/0/58926111402292/68021340250410 diff --git a/src/database/realtime/polling/PacketReceiver.ts b/src/database/realtime/polling/PacketReceiver.ts index a3ccb1072a1..ab23a83a967 100644 --- a/src/database/realtime/polling/PacketReceiver.ts +++ b/src/database/realtime/polling/PacketReceiver.ts @@ -1,4 +1,4 @@ -import { exceptionGuard } from "../../core/util/util"; +import { exceptionGuard } from '../../core/util/util'; /** * This class ensures the packets from the server arrive in order @@ -6,52 +6,53 @@ import { exceptionGuard } from "../../core/util/util"; * @param onMessage * @constructor */ -export const PacketReceiver = function(onMessage) { - this.onMessage_ = onMessage; - this.pendingResponses = []; - this.currentResponseNum = 0; - this.closeAfterResponse = -1; - this.onClose = null; -}; +export class PacketReceiver { + pendingResponses = []; + currentResponseNum = 0; + closeAfterResponse = -1; + onClose = null; -PacketReceiver.prototype.closeAfter = function(responseNum, callback) { - this.closeAfterResponse = responseNum; - this.onClose = callback; - if (this.closeAfterResponse < this.currentResponseNum) { - this.onClose(); - this.onClose = null; + constructor(private onMessage_: any) { } -}; -/** - * Each message from the server comes with a response number, and an array of data. The responseNumber - * allows us to ensure that we process them in the right order, since we can't be guaranteed that all - * browsers will respond in the same order as the requests we sent - * @param {number} requestNum - * @param {Array} data - */ -PacketReceiver.prototype.handleResponse = function(requestNum, data) { - this.pendingResponses[requestNum] = data; - while (this.pendingResponses[this.currentResponseNum]) { - var toProcess = this.pendingResponses[this.currentResponseNum]; - delete this.pendingResponses[this.currentResponseNum]; - for (var i = 0; i < toProcess.length; ++i) { - if (toProcess[i]) { - var self = this; - exceptionGuard(function() { - self.onMessage_(toProcess[i]); - }); - } + closeAfter(responseNum, callback) { + this.closeAfterResponse = responseNum; + this.onClose = callback; + if (this.closeAfterResponse < this.currentResponseNum) { + this.onClose(); + this.onClose = null; } - if (this.currentResponseNum === this.closeAfterResponse) { - if (this.onClose) { - clearTimeout(this.onClose); - this.onClose(); - this.onClose = null; + }; + + /** + * Each message from the server comes with a response number, and an array of data. The responseNumber + * allows us to ensure that we process them in the right order, since we can't be guaranteed that all + * browsers will respond in the same order as the requests we sent + * @param {number} requestNum + * @param {Array} data + */ + handleResponse(requestNum, data) { + this.pendingResponses[requestNum] = data; + while (this.pendingResponses[this.currentResponseNum]) { + const toProcess = this.pendingResponses[this.currentResponseNum]; + delete this.pendingResponses[this.currentResponseNum]; + for (let i = 0; i < toProcess.length; ++i) { + if (toProcess[i]) { + exceptionGuard(() => { + this.onMessage_(toProcess[i]); + }); + } + } + if (this.currentResponseNum === this.closeAfterResponse) { + if (this.onClose) { + clearTimeout(this.onClose); + this.onClose(); + this.onClose = null; + } + break; } - break; + this.currentResponseNum++; } - this.currentResponseNum++; } -}; +} diff --git a/src/utils/obj.ts b/src/utils/obj.ts index 78a48add08d..3019cf945f5 100644 --- a/src/utils/obj.ts +++ b/src/utils/obj.ts @@ -110,4 +110,23 @@ export const getValues = function(obj) { res[i++] = obj[key]; } return res; -}; \ No newline at end of file +}; + +/** + * Tests whether every key/value pair in an object pass the test implemented + * by the provided function + * + * @param {?Object.} obj Object to test. + * @param {!function(K, V)} fn Function to call for each key and value. + * @template K,V + */ +export const every = function(obj: Object, fn: (k: string, v?: V) => boolean): boolean { + for (let key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + if (!fn(key, obj[key])) { + return false; + } + } + } + return true; +};