From 975da072b450e15fbeaaffd4f86eb4363e33d42f Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Thu, 11 Mar 2021 19:10:10 -0700 Subject: [PATCH 01/15] Add database@exp API (#4614) --- packages/database/exp/index.ts | 44 ++- packages/database/src/exp/DataSnapshot.ts | 68 ++++ packages/database/src/exp/Database.ts | 112 +++---- packages/database/src/exp/Query.ts | 315 ++++++++++++++++++ packages/database/src/exp/Reference.ts | 90 +++++ packages/database/src/exp/ServerValue.ts | 26 ++ packages/database/src/exp/Transaction.ts | 32 ++ .../database/test/exp/integration.test.ts | 29 +- 8 files changed, 635 insertions(+), 81 deletions(-) create mode 100644 packages/database/src/exp/DataSnapshot.ts create mode 100644 packages/database/src/exp/Query.ts create mode 100644 packages/database/src/exp/Reference.ts create mode 100644 packages/database/src/exp/ServerValue.ts create mode 100644 packages/database/src/exp/Transaction.ts diff --git a/packages/database/exp/index.ts b/packages/database/exp/index.ts index 49084283906..d7dd48db5d0 100644 --- a/packages/database/exp/index.ts +++ b/packages/database/exp/index.ts @@ -22,8 +22,48 @@ import { Component, ComponentType } from '@firebase/component'; import { version } from '../package.json'; import { FirebaseDatabase } from '../src/exp/Database'; -export { getDatabase, ServerValue } from '../src/exp/Database'; -export { enableLogging } from '../src/core/util/util'; +export { + enableLogging, + getDatabase, + goOffline, + goOnline, + ref, + refFromURL, + useDatabaseEmulator +} from '../src/exp/Database'; +export { + OnDisconnect, + Reference, + ThenableReference +} from '../src/exp/Reference'; +export { DataSnapshot } from '../src/exp/DataSnapshot'; +export { + ListenOptions, + Query, + QueryConstraint, + Unsubscribe, + endAt, + endBefore, + equalTo, + get, + limitToFirst, + limitToLast, + off, + onChildAdded, + onChildChanged, + onChildMoved, + onChildRemoved, + onValue, + orderByChild, + orderByKey, + orderByPriority, + orderByValue, + query, + startAfter, + startAt +} from '../src/exp/Query'; +export { increment, serverTimestamp } from '../src/exp/ServerValue'; +export { runTransaction, TransactionOptions } from '../src/exp/Transaction'; declare module '@firebase/component' { interface NameServiceMapping { diff --git a/packages/database/src/exp/DataSnapshot.ts b/packages/database/src/exp/DataSnapshot.ts new file mode 100644 index 00000000000..b2a3ea9dd60 --- /dev/null +++ b/packages/database/src/exp/DataSnapshot.ts @@ -0,0 +1,68 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Reference } from './Reference'; + +export class DataSnapshot { + private constructor() {} + priority: string | number | null; + size: number; + key: string | null; + ref: Reference; + + child(path: string): DataSnapshot { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; + } + + exists(): boolean { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + exportVal(): any { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; + } + + forEach(action: (child: DataSnapshot) => boolean | void): boolean { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; + } + + hasChild(path: string): boolean { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; + } + + hasChildren(): boolean { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; + } + + toJSON(): object | null { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + val(): any { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; + } +} diff --git a/packages/database/src/exp/Database.ts b/packages/database/src/exp/Database.ts index 25bc3ae3693..8ad71116a18 100644 --- a/packages/database/src/exp/Database.ts +++ b/packages/database/src/exp/Database.ts @@ -18,91 +18,27 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { _FirebaseService, _getProvider, FirebaseApp } from '@firebase/app-exp'; import { Reference } from '../api/Reference'; -import { repoManagerDatabaseFromApp } from '../core/RepoManager'; import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; -import { Database } from '../api/Database'; import { Provider } from '@firebase/component'; /** * Class representing a Firebase Realtime Database. */ export class FirebaseDatabase implements _FirebaseService { - static readonly ServerValue = Database.ServerValue; - - private _delegate: Database; + readonly 'type' = 'database'; constructor( readonly app: FirebaseApp, authProvider: Provider, databaseUrl?: string - ) { - this._delegate = repoManagerDatabaseFromApp( - this.app, - authProvider, - databaseUrl, - undefined - ); - } - - /** - * Modify this instance to communicate with the Realtime Database emulator. - * - *

Note: This method must be called before performing any other operation. - * - * @param host - the emulator host (ex: localhost) - * @param port - the emulator port (ex: 8080) - */ - useEmulator(host: string, port: number): void { - this._delegate.useEmulator(host, port); - } - - /** - * Returns a reference to the root or to the path specified in the provided - * argument. - * - * @param path - The relative string path or an existing Reference to a - * database location. - * @throws If a Reference is provided, throws if it does not belong to the - * same project. - * @returns Firebase reference. - */ - ref(path?: string): Reference; - ref(path?: Reference): Reference; - ref(path?: string | Reference): Reference { - return typeof path === 'string' - ? this._delegate.ref(path) - : this._delegate.ref(path); - } - - /** - * Returns a reference to the root or the path specified in url. - * We throw a exception if the url is not in the same domain as the - * current repo. - * @param url - A URL that refers to a database location. - * @returns A Firebase reference. - */ - refFromURL(url: string): Reference { - return this._delegate.refFromURL(url); - } - - goOffline(): void { - this._delegate.goOffline(); - } - - goOnline(): void { - this._delegate.goOnline(); - } + ) {} _delete(): Promise { - return this._delegate.INTERNAL.delete(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; } - - _setDatabaseUrl(url: string) {} } -const ServerValue = Database.ServerValue; -export { ServerValue }; - /** * Returns the instance of the Realtime Database SDK that is associated * with the provided {@link FirebaseApp}. Initializes a new instance with @@ -120,3 +56,43 @@ export function getDatabase(app: FirebaseApp, url?: string): FirebaseDatabase { identifier: url }) as FirebaseDatabase; } + +export function useDatabaseEmulator( + db: FirebaseDatabase, + host: string, + port: number +): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function goOffline(db: FirebaseDatabase): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function goOnline(db: FirebaseDatabase): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function ref( + db: FirebaseDatabase, + path?: string | Reference +): Reference { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function refFromURL(db: FirebaseDatabase, url: string): Reference { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function enableLogging( + logger?: boolean | ((message: string) => unknown), + persistent?: boolean +): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} diff --git a/packages/database/src/exp/Query.ts b/packages/database/src/exp/Query.ts new file mode 100644 index 00000000000..57f23d5350b --- /dev/null +++ b/packages/database/src/exp/Query.ts @@ -0,0 +1,315 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Reference } from './Reference'; +import { DataSnapshot } from './DataSnapshot'; + +export class Query { + protected constructor() {} + ref: Reference; + + isEqual(other: Query | null): boolean { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; + } + + toJSON(): object { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; + } + + toString(): string { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; + } +} + +export function get(query: Query): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export type Unsubscribe = () => {}; +export interface ListenOptions { + readonly onlyOnce?: boolean; +} + +export function onValue( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + cancelCallback?: (error: Error) => unknown +): Unsubscribe; +export function onValue( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + options: ListenOptions +): Unsubscribe; +export function onValue( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + cancelCallback: (error: Error) => unknown, + options: ListenOptions +): Unsubscribe; +export function onValue( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, + options?: ListenOptions +): Unsubscribe { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function onChildAdded( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName?: string | null + ) => unknown, + cancelCallback?: (error: Error) => unknown +): Unsubscribe; +export function onChildAdded( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildAdded( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallback: (error: Error) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildAdded( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, + options?: ListenOptions +): Unsubscribe { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function onChildChanged( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallback?: (error: Error) => unknown +): Unsubscribe; +export function onChildChanged( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildChanged( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallback: (error: Error) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildChanged( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, + options?: ListenOptions +): Unsubscribe { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function onChildMoved( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallback?: (error: Error) => unknown +): Unsubscribe; +export function onChildMoved( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildMoved( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallback: (error: Error) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildMoved( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, + options?: ListenOptions +): Unsubscribe { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function onChildRemoved( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + cancelCallback?: (error: Error) => unknown +): Unsubscribe; +export function onChildRemoved( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildRemoved( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + cancelCallback: (error: Error) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildRemoved( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, + options?: ListenOptions +): Unsubscribe { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function off( + query: Query, + callback?: ( + snapshot: DataSnapshot, + previousChildName?: string | null + ) => unknown +): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export interface QueryConstraint { + type: + | 'endAt' + | 'endBefore' + | 'startAt' + | 'startAfter' + | 'limitToFirst' + | 'limitToLast' + | 'orderByChild' + | 'orderByKey' + | 'orderByPriority' + | 'orderByValue' + | 'equalTo'; +} +export function endAt( + value: number | string | boolean | null, + key?: string +): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function endBefore( + value: number | string | boolean | null, + key?: string +): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function startAt( + value: number | string | boolean | null, + key?: string +): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function startAfter( + value: number | string | boolean | null, + key?: string +): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function limitToFirst(limit: number): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function limitToLast(limit: number): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function orderByChild(path: string): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function orderByKey(): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function orderByPriority(): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function orderByValue(): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function equalTo( + value: number | string | boolean | null, + key?: string +): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function query(query: Query, ...constraints: QueryConstraint[]): Query { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} diff --git a/packages/database/src/exp/Reference.ts b/packages/database/src/exp/Reference.ts new file mode 100644 index 00000000000..278e0cce361 --- /dev/null +++ b/packages/database/src/exp/Reference.ts @@ -0,0 +1,90 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Query } from './Query'; + +export class Reference extends Query { + private constructor() { + super(); + } + + key: string | null; + parent: Reference | null; + root: Reference; +} + +export interface OnDisconnect { + cancel(): Promise; + remove(): Promise; + set(value: unknown): Promise; + setWithPriority( + value: unknown, + priority: number | string | null + ): Promise; + update(values: object): Promise; +} + +export interface ThenableReference + extends Reference, + Pick, 'then' | 'catch'> {} + +export function child(ref: Reference, path: string): Reference { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function onDisconnect(ref: Reference): OnDisconnect { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function push(ref: Reference, value?: unknown): ThenableReference { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function remove(ref: Reference): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function set(ref: Reference, value: unknown): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function setPriority( + ref: Reference, + priority: string | number | null +): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function setWithPriority( + ref: Reference, + newVal: unknown, + newPriority: string | number | null +): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function update(ref: Reference, values: object): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} diff --git a/packages/database/src/exp/ServerValue.ts b/packages/database/src/exp/ServerValue.ts new file mode 100644 index 00000000000..fff9f29b52f --- /dev/null +++ b/packages/database/src/exp/ServerValue.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function serverTimestamp(): object { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function increment(delta: number): object { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} diff --git a/packages/database/src/exp/Transaction.ts b/packages/database/src/exp/Transaction.ts new file mode 100644 index 00000000000..a848c3eb173 --- /dev/null +++ b/packages/database/src/exp/Transaction.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Reference } from './Reference'; + +export interface TransactionOptions { + readonly applyLocally?: boolean; +} + +export function runTransaction( + ref: Reference, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + transactionUpdate: (currentData: any) => unknown, + options?: TransactionOptions +): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} diff --git a/packages/database/test/exp/integration.test.ts b/packages/database/test/exp/integration.test.ts index b446870cbe2..8c13e2348e1 100644 --- a/packages/database/test/exp/integration.test.ts +++ b/packages/database/test/exp/integration.test.ts @@ -20,13 +20,20 @@ import { initializeApp, deleteApp } from '@firebase/app-exp'; import { expect } from 'chai'; import { DATABASE_ADDRESS, DATABASE_URL } from '../helpers/util'; -import { getDatabase } from '../../exp/index'; +import { + getDatabase, + goOffline, + goOnline, + ref, + refFromURL +} from '../../exp/index'; export function createTestApp() { return initializeApp({ databaseURL: DATABASE_URL }); } -describe('Database Tests', () => { +// TODO(database-exp): Re-enable these tests +describe.skip('Database Tests', () => { let defaultApp; beforeEach(() => { @@ -48,7 +55,7 @@ describe('Database Tests', () => { const db = getDatabase(defaultApp, 'http://foo.bar.com'); expect(db).to.be.ok; // The URL is assumed to be secure if no port is specified. - expect(db.ref().toString()).to.equal('https://foo.bar.com/'); + expect(ref(db).toString()).to.equal('https://foo.bar.com/'); }); it('Can get app', () => { @@ -58,33 +65,33 @@ describe('Database Tests', () => { it('Can set and ge tref', async () => { const db = getDatabase(defaultApp); - await db.ref('foo/bar').set('foobar'); - const snap = await db.ref('foo/bar').get(); + await ref(db, 'foo/bar').set('foobar'); + const snap = await ref(db, 'foo/bar').get(); expect(snap.val()).to.equal('foobar'); }); it('Can get refFromUrl', async () => { const db = getDatabase(defaultApp); - await db.refFromURL(`${DATABASE_ADDRESS}/foo/bar`).get(); + await refFromURL(db, `${DATABASE_ADDRESS}/foo/bar`).get(); }); it('Can goOffline/goOnline', async () => { const db = getDatabase(defaultApp); - db.goOffline(); + goOffline(db); try { - await db.ref('foo/bar').get(); + await ref(db, 'foo/bar').get(); expect.fail('Should have failed since we are offline'); } catch (e) { expect(e.message).to.equal('Error: Client is offline.'); } - db.goOnline(); - await db.ref('foo/bar').get(); + goOnline(db); + await ref(db, 'foo/bar').get(); }); it('Can delete app', async () => { const db = getDatabase(defaultApp); await deleteApp(defaultApp); - expect(() => db.ref()).to.throw('Cannot call ref on a deleted database.'); + expect(() => ref(db)).to.throw('Cannot call ref on a deleted database.'); defaultApp = undefined; }); }); From fe0469fd52af42c3a6883b85c11ff99190170daf Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Tue, 30 Mar 2021 14:55:17 -0600 Subject: [PATCH 02/15] Compat and @exp class for DataSnapshot (#4686) --- packages/database/src/api/DataSnapshot.ts | 81 ++++++------------- packages/database/src/api/Query.ts | 39 +++++---- packages/database/src/api/Reference.ts | 28 +++++-- packages/database/src/core/Repo.ts | 46 +++-------- packages/database/src/core/view/Event.ts | 10 +-- .../src/core/view/EventRegistration.ts | 56 ++++++++++--- packages/database/src/exp/DataSnapshot.ts | 80 +++++++++++++----- packages/database/src/exp/Reference.ts | 31 +++++-- packages/database/test/datasnapshot.test.ts | 10 ++- 9 files changed, 225 insertions(+), 156 deletions(-) diff --git a/packages/database/src/api/DataSnapshot.ts b/packages/database/src/api/DataSnapshot.ts index 61b5558d7e0..44ea483a239 100644 --- a/packages/database/src/api/DataSnapshot.ts +++ b/packages/database/src/api/DataSnapshot.ts @@ -17,28 +17,20 @@ import { validateArgCount, validateCallback } from '@firebase/util'; 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'; +import { DataSnapshot as ExpDataSnapshot } from '../exp/DataSnapshot'; + +// TODO(databaseexp): Import Compat from @firebase/util +export interface Compat { + readonly _delegate: T; +} /** * Class representing a firebase data snapshot. It wraps a SnapshotNode and * surfaces the public methods (val, forEach, etc.) we want to expose. */ -export class DataSnapshot { - /** - * @param node_ A SnapshotNode to wrap. - * @param ref_ The ref of the location this snapshot came from. - * @param index_ The iteration order for this snapshot - */ - constructor( - private readonly node_: Node, - private readonly ref_: Reference, - private readonly index_: Index - ) {} +export class DataSnapshot implements Compat { + constructor(readonly _delegate: ExpDataSnapshot) {} /** * Retrieves the snapshot contents as JSON. Returns null if the snapshot is @@ -48,7 +40,7 @@ export class DataSnapshot { */ val(): unknown { validateArgCount('DataSnapshot.val', 0, 0, arguments.length); - return this.node_.val(); + return this._delegate.val(); } /** @@ -58,7 +50,7 @@ export class DataSnapshot { */ exportVal(): unknown { validateArgCount('DataSnapshot.exportVal', 0, 0, arguments.length); - return this.node_.val(true); + return this._delegate.exportVal(); } // Do not create public documentation. This is intended to make JSON serialization work but is otherwise unnecessary @@ -66,7 +58,7 @@ export class DataSnapshot { toJSON(): unknown { // 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(); + return this._delegate.toJSON(); } /** @@ -76,7 +68,7 @@ export class DataSnapshot { */ exists(): boolean { validateArgCount('DataSnapshot.exists', 0, 0, arguments.length); - return !this.node_.isEmpty(); + return this._delegate.exists(); } /** @@ -90,14 +82,7 @@ export class DataSnapshot { // Ensure the childPath is a string (can be a number) childPathString = String(childPathString); validatePathString('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 - ); + return new DataSnapshot(this._delegate.child(childPathString)); } /** @@ -109,9 +94,7 @@ export class DataSnapshot { hasChild(childPathString: string): boolean { validateArgCount('DataSnapshot.hasChild', 1, 1, arguments.length); validatePathString('DataSnapshot.hasChild', 1, childPathString, false); - - const childPath = new Path(childPathString); - return !this.node_.getChild(childPath).isEmpty(); + return this._delegate.hasChild(childPathString); } /** @@ -121,9 +104,7 @@ export class DataSnapshot { */ getPriority(): string | number | null { validateArgCount('DataSnapshot.getPriority', 0, 0, arguments.length); - - // typecast here because we never return deferred values or internal priorities (MAX_PRIORITY) - return this.node_.getPriority().val() as string | number | null; + return this._delegate.priority; } /** @@ -134,21 +115,12 @@ export class DataSnapshot { * @return True if forEach was canceled by action returning true for * one of the child nodes. */ - forEach(action: (d: DataSnapshot) => boolean | void): boolean { + forEach(action: (snapshot: DataSnapshot) => boolean | void): boolean { validateArgCount('DataSnapshot.forEach', 1, 1, arguments.length); validateCallback('DataSnapshot.forEach', 1, action, false); - - if (this.node_.isLeafNode()) { - return false; - } - - const childrenNode = this.node_ as ChildrenNode; - // 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) - ); - }); + return this._delegate.forEach(expDataSnapshot => + action(new DataSnapshot(expDataSnapshot)) + ); } /** @@ -157,16 +129,11 @@ export class DataSnapshot { */ hasChildren(): boolean { validateArgCount('DataSnapshot.hasChildren', 0, 0, arguments.length); - - if (this.node_.isLeafNode()) { - return false; - } else { - return !this.node_.isEmpty(); - } + return this._delegate.hasChildren(); } get key() { - return this.ref_.getKey(); + return this._delegate.key; } /** @@ -175,8 +142,7 @@ export class DataSnapshot { */ numChildren(): number { validateArgCount('DataSnapshot.numChildren', 0, 0, arguments.length); - - return this.node_.numChildren(); + return this._delegate.size; } /** @@ -185,8 +151,7 @@ export class DataSnapshot { */ getRef(): Reference { validateArgCount('DataSnapshot.ref', 0, 0, arguments.length); - - return this.ref_; + return new Reference(this._delegate.ref._repo, this._delegate.ref._path); } get ref() { diff --git a/packages/database/src/api/Query.ts b/packages/database/src/api/Query.ts index fd9164555d9..986c7778991 100644 --- a/packages/database/src/api/Query.ts +++ b/packages/database/src/api/Query.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import { DataSnapshot as ExpDataSnapshot } from '../exp/DataSnapshot'; +import { Reference as ExpReference } from '../exp/Reference'; import { assert, Deferred, @@ -45,6 +47,7 @@ import { import { ChildEventRegistration, EventRegistration, + ExpSnapshotCallback, ValueEventRegistration } from '../core/view/EventRegistration'; @@ -71,7 +74,7 @@ import { DataSnapshot } from './DataSnapshot'; let __referenceConstructor: new (repo: Repo, path: Path) => Query; export interface SnapshotCallback { - (a: DataSnapshot, b?: string | null): unknown; + (dataSnapshot: DataSnapshot, previousChildName?: string | null): unknown; } /** @@ -215,19 +218,19 @@ export class Query { cancelCallbackOrContext, context ); - + const expCallback = new ExpSnapshotCallback(callback); if (eventType === 'value') { - this.onValueEvent(callback, ret.cancel, ret.context); + this.onValueEvent(expCallback, ret.cancel, ret.context); } else { - const callbacks: { [k: string]: typeof callback } = {}; - callbacks[eventType] = callback; + const callbacks: { [k: string]: ExpSnapshotCallback } = {}; + callbacks[eventType] = expCallback; this.onChildEvent(callbacks, ret.cancel, ret.context); } return callback; } protected onValueEvent( - callback: (a: DataSnapshot) => void, + callback: ExpSnapshotCallback, cancelCallback: ((a: Error) => void) | null, context: object | null ) { @@ -239,8 +242,8 @@ export class Query { repoAddEventCallbackForQuery(this.repo, this, container); } - onChildEvent( - callbacks: { [k: string]: SnapshotCallback }, + protected onChildEvent( + callbacks: { [k: string]: ExpSnapshotCallback }, cancelCallback: ((a: Error) => unknown) | null, context: object | null ) { @@ -261,20 +264,20 @@ export class Query { validateEventType('Query.off', 1, eventType, true); validateCallback('Query.off', 2, callback, true); validateContextObject('Query.off', 3, context, true); - let container: EventRegistration | null = null; - let callbacks: { [k: string]: typeof callback } | null = null; + let callbacks: { [k: string]: ExpSnapshotCallback } | null = null; + + const expCallback = callback ? new ExpSnapshotCallback(callback) : null; if (eventType === 'value') { - const valueCallback = callback || null; container = new ValueEventRegistration( - valueCallback, + expCallback, null, context || null ); } else if (eventType) { if (callback) { callbacks = {}; - callbacks[eventType] = callback; + callbacks[eventType] = expCallback; } container = new ChildEventRegistration(callbacks, null, context || null); } @@ -285,7 +288,15 @@ export class Query { * Get the server-value for this query, or return a cached value if not connected. */ get(): Promise { - return repoGetValue(this.repo, this); + return repoGetValue(this.repo, this).then(node => { + return new DataSnapshot( + new ExpDataSnapshot( + node, + new ExpReference(this.getRef().repo, this.getRef().path), + this.getQueryParams().getIndex() + ) + ); + }); } /** diff --git a/packages/database/src/api/Reference.ts b/packages/database/src/api/Reference.ts index 09258089b4d..5d734b55bfd 100644 --- a/packages/database/src/api/Reference.ts +++ b/packages/database/src/api/Reference.ts @@ -15,6 +15,9 @@ * limitations under the License. */ +import { DataSnapshot as ExpDataSnapshot } from '../exp/DataSnapshot'; +import { Node } from '../core/snap/Node'; +import { Reference as ExpReference } from '../exp/Reference'; import { OnDisconnect } from './onDisconnect'; import { TransactionResult } from './TransactionResult'; import { warn } from '../core/util/util'; @@ -51,6 +54,7 @@ import { Deferred, validateArgCount, validateCallback } from '@firebase/util'; import { syncPointSetReferenceConstructor } from '../core/SyncPoint'; import { Database } from './Database'; import { DataSnapshot } from './DataSnapshot'; +import { PRIORITY_INDEX } from '../core/snap/indexes/PriorityIndex'; export interface ReferenceConstructor { new (repo: Repo, path: Path): Reference; @@ -232,7 +236,11 @@ export class Reference extends Query { transaction( transactionUpdate: (a: unknown) => unknown, - onComplete?: (a: Error | null, b: boolean, c: DataSnapshot | null) => void, + onComplete?: ( + error: Error | null, + committed: boolean, + dataSnapshot: DataSnapshot | null + ) => void, applyLocally?: boolean ): Promise { validateArgCount('Reference.transaction', 1, 3, arguments.length); @@ -260,18 +268,26 @@ export class Reference extends Query { deferred.promise.catch(() => {}); } - const promiseComplete = function ( + const promiseComplete = ( error: Error, committed: boolean, - snapshot: DataSnapshot - ) { + node: Node | null + ) => { + let dataSnapshot: DataSnapshot | null = null; if (error) { deferred.reject(error); } else { - deferred.resolve(new TransactionResult(committed, snapshot)); + dataSnapshot = new DataSnapshot( + new ExpDataSnapshot( + node, + new ExpReference(this.repo, this.path), + PRIORITY_INDEX + ) + ); + deferred.resolve(new TransactionResult(committed, dataSnapshot)); } if (typeof onComplete === 'function') { - onComplete(error, committed, snapshot); + onComplete(error, committed, dataSnapshot); } }; repoStartTransaction( diff --git a/packages/database/src/core/Repo.ts b/packages/database/src/core/Repo.ts index 99f719a7f4b..8dc4ebe79b0 100644 --- a/packages/database/src/core/Repo.ts +++ b/packages/database/src/core/Repo.ts @@ -105,7 +105,6 @@ import { } from './util/Tree'; import { isValidPriority, validateFirebaseData } from './util/validation'; import { ChildrenNode } from './snap/ChildrenNode'; -import { PRIORITY_INDEX } from './snap/indexes/PriorityIndex'; import { Reference } from '../api/Reference'; import { FirebaseAppLike } from './RepoManager'; @@ -143,7 +142,7 @@ const enum TransactionStatus { interface Transaction { path: Path; update: (a: unknown) => unknown; - onComplete: (a: Error | null, b: boolean, c: DataSnapshot | null) => void; + onComplete: (error: Error, committed: boolean, node: Node | null) => void; status: TransactionStatus; order: number; applyLocally: boolean; @@ -450,17 +449,11 @@ function repoGetNextWriteId(repo: Repo): number { * * @param query - The query to surface a value for. */ -export function repoGetValue(repo: Repo, query: Query): Promise { +export function repoGetValue(repo: Repo, query: Query): Promise { // Only active queries are cached. There is no persisted cache. const cached = syncTreeGetServerValue(repo.serverSyncTree_, query); if (cached != null) { - return Promise.resolve( - new DataSnapshot( - cached, - query.getRef(), - query.getQueryParams().getIndex() - ) - ); + return Promise.resolve(cached); } return repo.server_.get(query).then( payload => { @@ -471,13 +464,7 @@ export function repoGetValue(repo: Repo, query: Query): Promise { node ); eventQueueRaiseEventsAtPath(repo.eventQueue_, query.path, events); - return Promise.resolve( - new DataSnapshot( - node, - query.getRef(), - query.getQueryParams().getIndex() - ) - ); + return Promise.resolve(node); }, err => { repoLog(repo, 'get for query ' + stringify(query) + ' failed: ' + err); @@ -883,7 +870,9 @@ export function repoStartTransaction( repo: Repo, path: Path, transactionUpdate: (a: unknown) => unknown, - onComplete: ((a: Error, b: boolean, c: DataSnapshot) => void) | null, + onComplete: + | ((error: Error, committed: boolean, node: Node | null) => void) + | null, applyLocally: boolean ): void { repoLog(repo, 'transaction on ' + path); @@ -930,13 +919,7 @@ export function repoStartTransaction( transaction.currentOutputSnapshotRaw = null; transaction.currentOutputSnapshotResolved = null; if (transaction.onComplete) { - // We just set the input snapshot, so this cast should be safe - const snapshot = new DataSnapshot( - transaction.currentInputSnapshot, - new Reference(repo, transaction.path), - PRIORITY_INDEX - ); - transaction.onComplete(null, false, snapshot); + transaction.onComplete(null, false, transaction.currentInputSnapshot); } } else { validateFirebaseData( @@ -1115,11 +1098,7 @@ function repoSendTransactionQueue( // We never unset the output snapshot, and given that this // transaction is complete, it should be set const node = queue[i].currentOutputSnapshotResolved as Node; - const ref = new Reference(repo, queue[i].path); - const snapshot = new DataSnapshot(node, ref, PRIORITY_INDEX); - callbacks.push( - queue[i].onComplete.bind(null, null, true, snapshot) - ); + callbacks.push(() => queue[i].onComplete(null, true, node)); } queue[i].unwatcher(); } @@ -1329,11 +1308,10 @@ function repoRerunTransactionQueue( const ref = new Reference(repo, queue[i].path); // We set this field immediately, so it's safe to cast to an actual snapshot const lastInput /** @type {!Node} */ = queue[i].currentInputSnapshot; - const snapshot = new DataSnapshot(lastInput, ref, PRIORITY_INDEX); - callbacks.push(queue[i].onComplete.bind(null, null, false, snapshot)); + callbacks.push(() => queue[i].onComplete(null, false, lastInput)); } else { - callbacks.push( - queue[i].onComplete.bind(null, new Error(abortReason), false, null) + callbacks.push(() => + queue[i].onComplete(new Error(abortReason), false, null) ); } } diff --git a/packages/database/src/core/view/Event.ts b/packages/database/src/core/view/Event.ts index 1c9b8f70f8c..82bd1350de2 100644 --- a/packages/database/src/core/view/Event.ts +++ b/packages/database/src/core/view/Event.ts @@ -18,7 +18,7 @@ import { stringify } from '@firebase/util'; import { Path } from '../util/Path'; import { EventRegistration } from './EventRegistration'; -import { DataSnapshot } from '../../api/DataSnapshot'; +import { DataSnapshot as ExpDataSnapshot } from '../../exp/DataSnapshot'; /** * Encapsulates the data needed to raise an event @@ -54,7 +54,7 @@ export class DataEvent implements Event { constructor( public eventType: EventType, public eventRegistration: EventRegistration, - public snapshot: DataSnapshot, + public snapshot: ExpDataSnapshot, public prevName?: string | null ) {} @@ -62,11 +62,11 @@ export class DataEvent implements Event { * @inheritDoc */ getPath(): Path { - const ref = this.snapshot.getRef(); + const ref = this.snapshot.ref; if (this.eventType === 'value') { - return ref.path; + return ref._path; } else { - return ref.getParent().path; + return ref.parent._path; } } diff --git a/packages/database/src/core/view/EventRegistration.ts b/packages/database/src/core/view/EventRegistration.ts index 646019ba0c4..106b7dc29ec 100644 --- a/packages/database/src/core/view/EventRegistration.ts +++ b/packages/database/src/core/view/EventRegistration.ts @@ -15,13 +15,40 @@ * limitations under the License. */ -import { DataSnapshot } from '../../api/DataSnapshot'; +import { DataSnapshot as ExpDataSnapshot } from '../../exp/DataSnapshot'; +import { Reference as ExpReference } from '../../exp/Reference'; import { DataEvent, CancelEvent, Event, EventType } from './Event'; import { contains, assert } from '@firebase/util'; import { Path } from '../util/Path'; import { Change } from './Change'; -import { Query } from '../../api/Query'; +import { Query, SnapshotCallback } from '../../api/Query'; +import { DataSnapshot } from '../../api/DataSnapshot'; + +/** + * A wrapper class that converts events from the database@exp SDK to the legacy + * Database SDK. Events are not converted directly as event registration relies + * on reference comparison of the original user callback (see `matches()`). + */ +export class ExpSnapshotCallback { + constructor(private readonly _userCallback: SnapshotCallback) {} + + callback( + thisArg: unknown, + expDataSnapshot: ExpDataSnapshot, + previousChildName?: string | null + ): unknown { + return this._userCallback.call( + thisArg, + new DataSnapshot(expDataSnapshot), + previousChildName + ); + } + + matches(exp: ExpSnapshotCallback): boolean { + return this._userCallback === exp._userCallback; + } +} /** * An EventRegistration is basically an event type ('value', 'child_added', etc.) and a callback @@ -64,7 +91,7 @@ export interface EventRegistration { */ export class ValueEventRegistration implements EventRegistration { constructor( - private callback_: ((d: DataSnapshot) => void) | null, + private callback_: ExpSnapshotCallback | null, private cancelCallback_: ((e: Error) => void) | null, private context_: {} | null ) {} @@ -84,7 +111,11 @@ export class ValueEventRegistration implements EventRegistration { return new DataEvent( 'value', this, - new DataSnapshot(change.snapshotNode, query.getRef(), index) + new ExpDataSnapshot( + change.snapshotNode, + new ExpReference(query.getRef().repo, query.getRef().path), + index + ) ); } @@ -106,7 +137,7 @@ export class ValueEventRegistration implements EventRegistration { } else { const cb = this.callback_; return function () { - cb.call(ctx, (eventData as DataEvent).snapshot); + cb.callback(ctx, (eventData as DataEvent).snapshot); }; } } @@ -133,7 +164,8 @@ export class ValueEventRegistration implements EventRegistration { return true; } else { return ( - other.callback_ === this.callback_ && other.context_ === this.context_ + other.callback_.matches(this.callback_) && + other.context_ === this.context_ ); } } @@ -155,7 +187,7 @@ export class ValueEventRegistration implements EventRegistration { export class ChildEventRegistration implements EventRegistration { constructor( private callbacks_: { - [k: string]: (d: DataSnapshot, s?: string | null) => void; + [child: string]: ExpSnapshotCallback; } | null, private cancelCallback_: ((e: Error) => void) | null, private context_?: {} @@ -193,7 +225,11 @@ export class ChildEventRegistration implements EventRegistration { return new DataEvent( change.type as EventType, this, - new DataSnapshot(change.snapshotNode, ref, index), + new ExpDataSnapshot( + change.snapshotNode, + new ExpReference(ref.repo, ref.path), + index + ), change.prevName ); } @@ -216,7 +252,7 @@ export class ChildEventRegistration implements EventRegistration { } else { const cb = this.callbacks_[(eventData as DataEvent).eventType]; return function () { - cb.call( + cb.callback( ctx, (eventData as DataEvent).snapshot, (eventData as DataEvent).prevName @@ -249,7 +285,7 @@ export class ChildEventRegistration implements EventRegistration { thisKey === otherKey && (!other.callbacks_[otherKey] || !this.callbacks_[thisKey] || - other.callbacks_[otherKey] === this.callbacks_[thisKey]) + other.callbacks_[otherKey].matches(this.callbacks_[thisKey])) ); } else { // Exact match on each key. diff --git a/packages/database/src/exp/DataSnapshot.ts b/packages/database/src/exp/DataSnapshot.ts index b2a3ea9dd60..f696aa264d3 100644 --- a/packages/database/src/exp/DataSnapshot.ts +++ b/packages/database/src/exp/DataSnapshot.ts @@ -15,54 +15,90 @@ * limitations under the License. */ -import { Reference } from './Reference'; +import { child, Reference } from './Reference'; +import { Node } from '../core/snap/Node'; +import { Index } from '../core/snap/indexes/Index'; +import { Path } from '../core/util/Path'; +import { PRIORITY_INDEX } from '../core/snap/indexes/PriorityIndex'; +import { ChildrenNode } from '../core/snap/ChildrenNode'; export class DataSnapshot { - private constructor() {} - priority: string | number | null; - size: number; - key: string | null; - ref: Reference; + /** + * @param _node A SnapshotNode to wrap. + * @param ref The ref of the location this snapshot came from. + * @param _index The iteration order for this snapshot + */ + constructor( + readonly _node: Node, + readonly ref: Reference, + readonly _index: Index + ) {} + + get priority(): string | number | null { + // typecast here because we never return deferred values or internal priorities (MAX_PRIORITY) + return this._node.getPriority().val() as string | number | null; + } + + get key(): string | null { + return this.ref.key; + } + + get size(): number { + return this._node.numChildren(); + } child(path: string): DataSnapshot { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + const childPath = new Path(path); + const childRef = child(this.ref, path); + return new DataSnapshot( + this._node.getChild(childPath), + childRef, + PRIORITY_INDEX + ); } exists(): boolean { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + return !this._node.isEmpty(); } // eslint-disable-next-line @typescript-eslint/no-explicit-any exportVal(): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + return this._node.val(true); } forEach(action: (child: DataSnapshot) => boolean | void): boolean { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + if (this._node.isLeafNode()) { + return false; + } + + const childrenNode = this._node as ChildrenNode; + // 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, child(this.ref, key), PRIORITY_INDEX) + ); + }); } hasChild(path: string): boolean { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + const childPath = new Path(path); + return !this._node.getChild(childPath).isEmpty(); } hasChildren(): boolean { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + if (this._node.isLeafNode()) { + return false; + } else { + return !this._node.isEmpty(); + } } toJSON(): object | null { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + return this.exportVal(); } // eslint-disable-next-line @typescript-eslint/no-explicit-any val(): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + return this._node.val(); } } diff --git a/packages/database/src/exp/Reference.ts b/packages/database/src/exp/Reference.ts index 278e0cce361..88d751b5fb8 100644 --- a/packages/database/src/exp/Reference.ts +++ b/packages/database/src/exp/Reference.ts @@ -16,15 +16,34 @@ */ import { Query } from './Query'; +import { Repo } from '../core/Repo'; +import { + Path, + pathChild, + pathGetBack, + pathIsEmpty, + pathParent +} from '../core/util/Path'; export class Reference extends Query { - private constructor() { + root: Reference; + + constructor(readonly _repo: Repo, readonly _path: Path) { super(); } - key: string | null; - parent: Reference | null; - root: Reference; + get key(): string | null { + if (pathIsEmpty(this._path)) { + return null; + } else { + return pathGetBack(this._path); + } + } + + get parent(): Reference | null { + const parentPath = pathParent(this._path); + return parentPath === null ? null : new Reference(this._repo, parentPath); + } } export interface OnDisconnect { @@ -43,8 +62,8 @@ export interface ThenableReference Pick, 'then' | 'catch'> {} export function child(ref: Reference, path: string): Reference { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + // TODO: Accept Compat class + return new Reference(ref._repo, pathChild(ref._path, path)); } export function onDisconnect(ref: Reference): OnDisconnect { diff --git a/packages/database/test/datasnapshot.test.ts b/packages/database/test/datasnapshot.test.ts index e19602dbb41..98ffe093bbe 100644 --- a/packages/database/test/datasnapshot.test.ts +++ b/packages/database/test/datasnapshot.test.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import { DataSnapshot as ExpDataSnapshot } from '../src/exp/DataSnapshot'; +import { Reference as ExpReference } from '../src/exp/Reference'; import { expect } from 'chai'; import { nodeFromJSON } from '../src/core/snap/nodeFromJSON'; import { PRIORITY_INDEX } from '../src/core/snap/indexes/PriorityIndex'; @@ -26,7 +28,13 @@ describe('DataSnapshot Tests', () => { /** @return {!DataSnapshot} */ const snapshotForJSON = function (json) { const dummyRef = getRandomNode() as Reference; - return new DataSnapshot(nodeFromJSON(json), dummyRef, PRIORITY_INDEX); + return new DataSnapshot( + new ExpDataSnapshot( + nodeFromJSON(json), + new ExpReference(dummyRef.repo, dummyRef.path), + PRIORITY_INDEX + ) + ); }; it('DataSnapshot.hasChildren() works.', () => { From f4410b7045afffae1b45d0e8b05f77ab316b3a03 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 2 Apr 2021 15:26:21 -0600 Subject: [PATCH 03/15] Add events to database@exp (#4694) --- .../.idea/runConfigurations/All_Tests.xml | 2 +- packages/database/exp/index.ts | 21 +- packages/database/src/api/Reference.ts | 478 ++--------- packages/database/src/api/test_access.ts | 2 +- .../database/src/core/PersistentConnection.ts | 42 +- .../database/src/core/ReadonlyRestClient.ts | 27 +- packages/database/src/core/Repo.ts | 26 +- packages/database/src/core/ServerActions.ts | 8 +- packages/database/src/core/SyncPoint.ts | 43 +- packages/database/src/core/SyncTree.ts | 82 +- packages/database/src/core/util/util.ts | 8 +- packages/database/src/core/view/Event.ts | 13 +- .../database/src/core/view/EventGenerator.ts | 6 +- .../src/core/view/EventRegistration.ts | 107 +++ .../database/src/core/view/QueryParams.ts | 2 +- packages/database/src/core/view/View.ts | 12 +- packages/database/src/exp/DataSnapshot.ts | 105 --- packages/database/src/exp/Database.ts | 16 +- packages/database/src/exp/Query.ts | 315 ------- packages/database/src/exp/Reference.ts | 91 +- packages/database/src/exp/Reference_impl.ts | 810 ++++++++++++++++++ packages/database/test/datasnapshot.test.ts | 8 +- .../database/test/exp/integration.test.ts | 12 +- packages/database/test/query.test.ts | 2 +- 24 files changed, 1182 insertions(+), 1056 deletions(-) delete mode 100644 packages/database/src/exp/DataSnapshot.ts delete mode 100644 packages/database/src/exp/Query.ts create mode 100644 packages/database/src/exp/Reference_impl.ts diff --git a/packages/database/.idea/runConfigurations/All_Tests.xml b/packages/database/.idea/runConfigurations/All_Tests.xml index 9a5747271e7..08aebabf99e 100644 --- a/packages/database/.idea/runConfigurations/All_Tests.xml +++ b/packages/database/.idea/runConfigurations/All_Tests.xml @@ -14,4 +14,4 @@ test/{,!(browser)/**/}*.test.ts - \ No newline at end of file + diff --git a/packages/database/exp/index.ts b/packages/database/exp/index.ts index ea34f7402fb..37dddea0055 100644 --- a/packages/database/exp/index.ts +++ b/packages/database/exp/index.ts @@ -27,21 +27,20 @@ export { getDatabase, goOffline, goOnline, - ref, - refFromURL, useDatabaseEmulator } from '../src/exp/Database'; export { - OnDisconnect, + Query, Reference, - ThenableReference + ListenOptions, + Unsubscribe, + ThenableReference, + OnDisconnect } from '../src/exp/Reference'; -export { DataSnapshot } from '../src/exp/DataSnapshot'; export { - ListenOptions, - Query, QueryConstraint, - Unsubscribe, + DataSnapshot, + EventType, endAt, endBefore, equalTo, @@ -60,8 +59,10 @@ export { orderByValue, query, startAfter, - startAt -} from '../src/exp/Query'; + startAt, + ref, + refFromURL +} from '../src/exp/Reference_impl'; export { increment, serverTimestamp } from '../src/exp/ServerValue'; export { runTransaction, TransactionOptions } from '../src/exp/Transaction'; diff --git a/packages/database/src/api/Reference.ts b/packages/database/src/api/Reference.ts index 27f8d7d3e9f..a6a75bf1c0e 100644 --- a/packages/database/src/api/Reference.ts +++ b/packages/database/src/api/Reference.ts @@ -16,21 +16,18 @@ */ import { + assert, + Compat, Deferred, + errorPrefix, validateArgCount, validateCallback, - contains, - validateContextObject, - errorPrefix, - assert, - Compat + validateContextObject } from '@firebase/util'; import { Repo, - repoAddEventCallbackForQuery, repoGetValue, - repoRemoveEventCallbackForQuery, repoServerTime, repoSetWithPriority, repoStartTransaction, @@ -41,7 +38,6 @@ import { PathIndex } from '../core/snap/indexes/PathIndex'; import { PRIORITY_INDEX } from '../core/snap/indexes/PriorityIndex'; import { VALUE_INDEX } from '../core/snap/indexes/ValueIndex'; import { Node } from '../core/snap/Node'; -import { syncPointSetReferenceConstructor } from '../core/SyncPoint'; import { nextPushId } from '../core/util/NextPushId'; import { Path, @@ -53,7 +49,7 @@ import { pathParent, pathToUrlEncodedString } from '../core/util/Path'; -import { MAX_NAME, MIN_NAME, ObjectToUniqueKey, warn } from '../core/util/util'; +import { MAX_NAME, MIN_NAME, warn } from '../core/util/util'; import { isValidPriority, validateBoolean, @@ -66,30 +62,33 @@ import { validateRootPathString, validateWritablePath } from '../core/util/validation'; -import { Change } from '../core/view/Change'; -import { CancelEvent, DataEvent, Event, EventType } from '../core/view/Event'; +import { UserCallback } from '../core/view/EventRegistration'; import { QueryParams, queryParamsEndAt, queryParamsEndBefore, - queryParamsGetQueryObject, queryParamsLimitToFirst, queryParamsLimitToLast, queryParamsOrderBy, queryParamsStartAfter, queryParamsStartAt } from '../core/view/QueryParams'; -import { DataSnapshot as ExpDataSnapshot } from '../exp/DataSnapshot'; -import { Reference as ExpReference } from '../exp/Reference'; +import { + DataSnapshot as ExpDataSnapshot, + off, + onChildAdded, + onChildChanged, + onChildMoved, + onChildRemoved, + onValue, + QueryImpl, + ReferenceImpl, + EventType +} from '../exp/Reference_impl'; import { Database } from './Database'; import { OnDisconnect } from './onDisconnect'; import { TransactionResult } from './TransactionResult'; - -export interface ReferenceConstructor { - new (database: Database, path: Path): Reference; -} - /** * Class representing a firebase data snapshot. It wraps a SnapshotNode and * surfaces the public methods (val, forEach, etc.) we want to expose. @@ -234,300 +233,14 @@ export interface SnapshotCallback { (dataSnapshot: DataSnapshot, previousChildName?: string | null): unknown; } -/** - * A wrapper class that converts events from the database@exp SDK to the legacy - * Database SDK. Events are not converted directly as event registration relies - * on reference comparison of the original user callback (see `matches()`). - */ -export class ExpSnapshotCallback { - constructor( - private readonly _database: Database, - private readonly _userCallback: SnapshotCallback - ) {} - - callback( - thisArg: unknown, - expDataSnapshot: ExpDataSnapshot, - previousChildName?: string | null - ): unknown { - return this._userCallback.call( - thisArg, - new DataSnapshot(this._database, expDataSnapshot), - previousChildName - ); - } - - matches(exp: ExpSnapshotCallback): boolean { - return this._userCallback === exp._userCallback; - } -} - -/** - * An EventRegistration is basically an event type ('value', 'child_added', etc.) and a callback - * to be notified of that type of event. - * - * 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. - */ -export interface EventRegistration { - /** - * True if this container has a callback to trigger for this event type - */ - respondsTo(eventType: string): boolean; - - createEvent(change: Change, query: Query): Event; - - /** - * Given event data, return a function to trigger the user's callback - */ - getEventRunner(eventData: Event): () => void; - - createCancelEvent(error: Error, path: Path): CancelEvent | null; - - 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) - * - */ - hasAnyCallback(): boolean; -} - -/** - * Represents registration for 'value' events. - */ -export class ValueEventRegistration implements EventRegistration { - constructor( - private callback_: ExpSnapshotCallback | null, - private cancelCallback_: ((e: Error) => void) | null, - private context_: {} | null - ) {} - - /** - * @inheritDoc - */ - respondsTo(eventType: string): boolean { - return eventType === 'value'; - } - - /** - * @inheritDoc - */ - createEvent(change: Change, query: Query): DataEvent { - const index = query.getQueryParams().getIndex(); - return new DataEvent( - 'value', - this, - new ExpDataSnapshot( - change.snapshotNode, - new ExpReference(query.getRef().database.repo_, query.getRef().path), - 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 as CancelEvent).error); - }; - } else { - const cb = this.callback_; - return function () { - cb.callback(ctx, (eventData as DataEvent).snapshot); - }; - } - } - - /** - * @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_.matches(this.callback_) && - other.context_ === this.context_ - ); - } - } - - /** - * @inheritDoc - */ - hasAnyCallback(): boolean { - return this.callback_ !== null; - } -} - -/** - * Represents the registration of 1 or more child_xxx events. - * - * 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. - */ -export class ChildEventRegistration implements EventRegistration { - constructor( - private callbacks_: { - [child: string]: ExpSnapshotCallback; - } | null, - private cancelCallback_: ((e: Error) => void) | null, - private context_?: {} - ) {} - - /** - * @inheritDoc - */ - respondsTo(eventType: string): 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 - */ - createEvent(change: Change, query: Query): DataEvent { - assert(change.childName != null, 'Child events should have a childName.'); - const ref = query.getRef().child(change.childName); - const index = query.getQueryParams().getIndex(); - return new DataEvent( - change.type as EventType, - this, - new ExpDataSnapshot( - change.snapshotNode, - new ExpReference(ref.repo, ref.path), - index - ), - change.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 as CancelEvent).error); - }; - } else { - const cb = this.callbacks_[(eventData as DataEvent).eventType]; - return function () { - cb.callback( - ctx, - (eventData as DataEvent).snapshot, - (eventData as DataEvent).prevName - ); - }; - } - } - - /** - * @inheritDoc - */ - matches(other: EventRegistration): boolean { - if (other instanceof ChildEventRegistration) { - if (!this.callbacks_ || !other.callbacks_) { - return true; - } else if (this.context_ === other.context_) { - const otherKeys = Object.keys(other.callbacks_); - const thisKeys = Object.keys(this.callbacks_); - const otherCount = otherKeys.length; - const thisCount = thisKeys.length; - 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 = otherKeys[0]; - const thisKey = thisKeys[0]; - return ( - thisKey === otherKey && - (!other.callbacks_[otherKey] || - !this.callbacks_[thisKey] || - other.callbacks_[otherKey].matches(this.callbacks_[thisKey])) - ); - } else { - // Exact match on each key. - return thisKeys.every( - eventType => - other.callbacks_[eventType] === this.callbacks_[eventType] - ); - } - } - } - } - - return false; - } - - /** - * @inheritDoc - */ - hasAnyCallback(): boolean { - return this.callbacks_ !== null; - } -} - /** * A Query represents a filter to be applied to a firebase location. This object purely represents the * query expression (and exposes our public API to build the query). The actual query logic is in ViewBase.js. * * Since every Firebase reference is a query, Firebase inherits from this object. */ -export class Query { +export class Query implements Compat { + readonly _delegate: QueryImpl; readonly repo: Repo; constructor( @@ -537,6 +250,12 @@ export class Query { private orderByCalled_: boolean ) { this.repo = database.repo_; + this._delegate = new QueryImpl( + this.repo, + path, + queryParams_, + orderByCalled_ + ); } /** @@ -630,10 +349,6 @@ export class Query { } } - getQueryParams(): QueryParams { - return this.queryParams_; - } - 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. @@ -649,7 +364,6 @@ export class Query { context?: object | null ): SnapshotCallback { validateArgCount('Query.on', 2, 4, arguments.length); - validateEventType('Query.on', 1, eventType, false); validateCallback('Query.on', 2, callback, false); const ret = Query.getCancelAndContextArgs_( @@ -657,41 +371,40 @@ export class Query { cancelCallbackOrContext, context ); - const expCallback = new ExpSnapshotCallback(this.database, callback); - if (eventType === 'value') { - this.onValueEvent(expCallback, ret.cancel, ret.context); - } else { - const callbacks: { [k: string]: ExpSnapshotCallback } = {}; - callbacks[eventType] = expCallback; - this.onChildEvent(callbacks, ret.cancel, ret.context); + const valueCallback: UserCallback = (expSnapshot, previousChildName?) => { + callback.call( + ret.context, + new DataSnapshot(this.database, expSnapshot), + previousChildName + ); + }; + valueCallback.userCallback = callback; + valueCallback.context = ret.context; + const cancelCallback = ret.cancel?.bind(ret.context); + + switch (eventType) { + case 'value': + onValue(this._delegate, valueCallback, cancelCallback); + return callback; + case 'child_added': + onChildAdded(this._delegate, valueCallback, cancelCallback); + return callback; + case 'child_removed': + onChildRemoved(this._delegate, valueCallback, cancelCallback); + return callback; + case 'child_changed': + onChildChanged(this._delegate, valueCallback, cancelCallback); + return callback; + case 'child_moved': + onChildMoved(this._delegate, valueCallback, cancelCallback); + return callback; + default: + throw new Error( + errorPrefix('Query.on', 1, false) + + 'must be a valid event type = "value", "child_added", "child_removed", ' + + '"child_changed", or "child_moved".' + ); } - return callback; - } - - protected onValueEvent( - callback: ExpSnapshotCallback, - cancelCallback: ((a: Error) => void) | null, - context: object | null - ) { - const container = new ValueEventRegistration( - callback, - cancelCallback || null, - context || null - ); - repoAddEventCallbackForQuery(this.database.repo_, this, container); - } - - protected onChildEvent( - callbacks: { [k: string]: ExpSnapshotCallback }, - cancelCallback: ((a: Error) => unknown) | null, - context: object | null - ) { - const container = new ChildEventRegistration( - callbacks, - cancelCallback, - context - ); - repoAddEventCallbackForQuery(this.database.repo_, this, container); } off( @@ -703,39 +416,27 @@ export class Query { validateEventType('Query.off', 1, eventType, true); validateCallback('Query.off', 2, callback, true); validateContextObject('Query.off', 3, context, true); - let container: EventRegistration | null = null; - let callbacks: { [k: string]: ExpSnapshotCallback } | null = null; - - const expCallback = callback - ? new ExpSnapshotCallback(this.database, callback) - : null; - if (eventType === 'value') { - container = new ValueEventRegistration( - expCallback, - null, - context || null - ); - } else if (eventType) { - if (callback) { - callbacks = {}; - callbacks[eventType] = expCallback; - } - container = new ChildEventRegistration(callbacks, null, context || null); + if (callback) { + const valueCallback: UserCallback = () => {}; + valueCallback.userCallback = callback; + valueCallback.context = context; + off(this._delegate, eventType as EventType, valueCallback); + } else { + off(this._delegate, eventType as EventType | undefined); } - repoRemoveEventCallbackForQuery(this.database.repo_, this, container); } /** * Get the server-value for this query, or return a cached value if not connected. */ get(): Promise { - return repoGetValue(this.database.repo_, this).then(node => { + return repoGetValue(this.database.repo_, this._delegate).then(node => { return new DataSnapshot( this.database, new ExpDataSnapshot( node, - new ExpReference(this.getRef().database.repo_, this.getRef().path), - this.getQueryParams().getIndex() + new ReferenceImpl(this.getRef().database.repo_, this.getRef().path), + this._delegate._queryParams.getIndex() ) ); }); @@ -1073,19 +774,6 @@ export class Query { return this.toString(); } - /** - * An object representation of the query parameters used by this Query. - */ - queryObject(): object { - return queryParamsGetQueryObject(this.queryParams_); - } - - queryIdentifier(): string { - const obj = this.queryObject(); - const id = ObjectToUniqueKey(obj); - return id === '{}' ? 'default' : id; - } - /** * Return true if this query and the provided query are equivalent; otherwise, return false. */ @@ -1100,7 +788,7 @@ export class Query { const sameRepo = this.database.repo_ === other.database.repo_; const samePath = pathEquals(this.path, other.path); const sameQueryIdentifier = - this.queryIdentifier() === other.queryIdentifier(); + this._delegate._queryIdentifier === other._delegate._queryIdentifier; return sameRepo && samePath && sameQueryIdentifier; } @@ -1114,11 +802,11 @@ export class Query { fnName: string, cancelOrContext?: ((a: Error) => void) | object | null, context?: object | null - ): { cancel: ((a: Error) => void) | null; context: object | null } { + ): { cancel: ((a: Error) => void) | undefined; context: object | undefined } { const ret: { cancel: ((a: Error) => void) | null; context: object | null; - } = { cancel: null, context: null }; + } = { cancel: undefined, context: undefined }; if (cancelOrContext && context) { ret.cancel = cancelOrContext as (a: Error) => void; validateCallback(fnName, 3, ret.cancel, true); @@ -1147,7 +835,9 @@ export class Query { } } -export class Reference extends Query { +export class Reference extends Query implements Compat { + readonly _delegate: ReferenceImpl; + then: Promise['then']; catch: Promise['catch']; @@ -1220,7 +910,7 @@ export class Reference extends Query { const deferred = new Deferred(); repoSetWithPriority( - this.database.repo_, + this.repo, this.path, newVal, /*priority=*/ null, @@ -1259,7 +949,7 @@ export class Reference extends Query { validateCallback('Reference.update', 2, onComplete, true); const deferred = new Deferred(); repoUpdate( - this.database.repo_, + this.repo, this.path, objectToMerge as { [k: string]: unknown }, deferred.wrapCallback(onComplete) @@ -1294,7 +984,7 @@ export class Reference extends Query { const deferred = new Deferred(); repoSetWithPriority( - this.database.repo_, + this.repo, this.path, newVal, newPriority, @@ -1361,7 +1051,7 @@ export class Reference extends Query { this.database, new ExpDataSnapshot( node, - new ExpReference(this.database.repo_, this.path), + new ReferenceImpl(this.database.repo_, this.path), PRIORITY_INDEX ) ); @@ -1448,7 +1138,7 @@ export class Reference extends Query { onDisconnect(): OnDisconnect { validateWritablePath('Reference.onDisconnect', this.path); - return new OnDisconnect(this.database.repo_, this.path); + return new OnDisconnect(this.repo, this.path); } get key(): string | null { @@ -1463,11 +1153,3 @@ export class Reference extends Query { return this.getRoot(); } } - -/** - * Define reference constructor in various modules - * - * We are doing this here to avoid several circular - * dependency issues - */ -syncPointSetReferenceConstructor(Reference); diff --git a/packages/database/src/api/test_access.ts b/packages/database/src/api/test_access.ts index d9792b6d7d8..bd67b2ab9e5 100644 --- a/packages/database/src/api/test_access.ts +++ b/packages/database/src/api/test_access.ts @@ -64,7 +64,7 @@ export const hijackHash = function (newHash: () => string) { export const ConnectionTarget = RepoInfo; export const queryIdentifier = function (query: Query) { - return query.queryIdentifier(); + return query._delegate._queryIdentifier; }; /** diff --git a/packages/database/src/core/PersistentConnection.ts b/packages/database/src/core/PersistentConnection.ts index 233fce612bf..2cae60fcfec 100644 --- a/packages/database/src/core/PersistentConnection.ts +++ b/packages/database/src/core/PersistentConnection.ts @@ -29,7 +29,6 @@ import { Deferred } from '@firebase/util'; -import { Query } from '../api/Reference'; import { Connection } from '../realtime/Connection'; import { AuthTokenProvider } from './AuthTokenProvider'; @@ -40,6 +39,7 @@ import { Path } from './util/Path'; import { error, log, logWrapper, warn, ObjectToUniqueKey } from './util/util'; import { VisibilityMonitor } from './util/VisibilityMonitor'; import { SDK_VERSION } from './version'; +import { QueryContext } from './view/EventRegistration'; const RECONNECT_MIN_DELAY = 1000; const RECONNECT_MAX_DELAY_DEFAULT = 60 * 5 * 1000; // 5 minutes in milliseconds (Case: 1858) @@ -57,7 +57,7 @@ interface ListenSpec { hashFn(): string; - query: Query; + query: QueryContext; tag: number | null; } @@ -190,11 +190,11 @@ export class PersistentConnection extends ServerActions { } } - get(query: Query): Promise { + get(query: QueryContext): Promise { const deferred = new Deferred(); const request = { - p: query.path.toString(), - q: query.queryObject() + p: query._path.toString(), + q: query._queryObject }; const outstandingGet = { action: 'g', @@ -245,20 +245,19 @@ export class PersistentConnection extends ServerActions { * @inheritDoc */ listen( - query: Query, + query: QueryContext, currentHashFn: () => string, tag: number | null, onComplete: (a: string, b: unknown) => void ) { - const queryId = query.queryIdentifier(); - const pathString = query.path.toString(); + const queryId = query._queryIdentifier; + const pathString = query._path.toString(); this.log_('Listen called for ' + pathString + ' ' + queryId); if (!this.listens.has(pathString)) { this.listens.set(pathString, new Map()); } assert( - query.getQueryParams().isDefault() || - !query.getQueryParams().loadsAllData(), + query._queryParams.isDefault() || !query._queryParams.loadsAllData(), 'listen() called for non-default but complete query' ); assert( @@ -294,8 +293,8 @@ export class PersistentConnection extends ServerActions { private sendListen_(listenSpec: ListenSpec) { const query = listenSpec.query; - const pathString = query.path.toString(); - const queryId = query.queryIdentifier(); + const pathString = query._path.toString(); + const queryId = query._queryIdentifier; this.log_('Listen on ' + pathString + ' for ' + queryId); const req: { [k: string]: unknown } = { /*path*/ p: pathString }; @@ -303,7 +302,7 @@ export class PersistentConnection extends ServerActions { // Only bother to send query if it's non-default. if (listenSpec.tag) { - req['q'] = query.queryObject(); + req['q'] = query._queryObject; req['t'] = listenSpec.tag; } @@ -334,14 +333,14 @@ export class PersistentConnection extends ServerActions { }); } - private static warnOnListenWarnings_(payload: unknown, query: Query) { + private static warnOnListenWarnings_(payload: unknown, query: QueryContext) { if (payload && typeof payload === 'object' && contains(payload, 'w')) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const warnings = safeGet(payload as any, 'w'); if (Array.isArray(warnings) && ~warnings.indexOf('no_index')) { const indexSpec = - '".indexOn": "' + query.getQueryParams().getIndex().toString() + '"'; - const indexPath = query.path.toString(); + '".indexOn": "' + query._queryParams.getIndex().toString() + '"'; + const indexPath = query._path.toString(); warn( `Using an unspecified index. Your data will be downloaded and ` + `filtered on the client. Consider adding ${indexSpec} at ` + @@ -419,20 +418,19 @@ export class PersistentConnection extends ServerActions { /** * @inheritDoc */ - unlisten(query: Query, tag: number | null) { - const pathString = query.path.toString(); - const queryId = query.queryIdentifier(); + unlisten(query: QueryContext, tag: number | null) { + const pathString = query._path.toString(); + const queryId = query._queryIdentifier; this.log_('Unlisten called for ' + pathString + ' ' + queryId); assert( - query.getQueryParams().isDefault() || - !query.getQueryParams().loadsAllData(), + query._queryParams.isDefault() || !query._queryParams.loadsAllData(), 'unlisten() called for non-default but complete query' ); const listen = this.removeListen_(pathString, queryId); if (listen && this.connected_) { - this.sendUnlisten_(pathString, queryId, query.queryObject(), tag); + this.sendUnlisten_(pathString, queryId, query._queryObject, tag); } } diff --git a/packages/database/src/core/ReadonlyRestClient.ts b/packages/database/src/core/ReadonlyRestClient.ts index 1609c8708ed..d9cfceac113 100644 --- a/packages/database/src/core/ReadonlyRestClient.ts +++ b/packages/database/src/core/ReadonlyRestClient.ts @@ -23,12 +23,11 @@ import { Deferred } from '@firebase/util'; -import { Query } from '../api/Reference'; - import { AuthTokenProvider } from './AuthTokenProvider'; import { RepoInfo } from './RepoInfo'; import { ServerActions } from './ServerActions'; import { logWrapper, warn } from './util/util'; +import { QueryContext } from './view/EventRegistration'; import { queryParamsToRestQueryStringParameters } from './view/QueryParams'; /** @@ -50,15 +49,15 @@ export class ReadonlyRestClient extends ServerActions { */ private listens_: { [k: string]: object } = {}; - static getListenId_(query: Query, tag?: number | null): string { + static getListenId_(query: QueryContext, tag?: number | null): string { if (tag !== undefined) { return 'tag$' + tag; } else { assert( - query.getQueryParams().isDefault(), + query._queryParams.isDefault(), "should have a tag if it's not a default query." ); - return query.path.toString(); + return query._path.toString(); } } @@ -81,15 +80,13 @@ export class ReadonlyRestClient extends ServerActions { /** @inheritDoc */ listen( - query: Query, + query: QueryContext, currentHashFn: () => string, tag: number | null, onComplete: (a: string, b: unknown) => void ) { - const pathString = query.path.toString(); - this.log_( - 'Listen called for ' + pathString + ' ' + query.queryIdentifier() - ); + const pathString = query._path.toString(); + this.log_('Listen called for ' + pathString + ' ' + query._queryIdentifier); // Mark this listener so we can tell if it's removed. const listenId = ReadonlyRestClient.getListenId_(query, tag); @@ -97,7 +94,7 @@ export class ReadonlyRestClient extends ServerActions { this.listens_[listenId] = thisListen; const queryStringParameters = queryParamsToRestQueryStringParameters( - query.getQueryParams() + query._queryParams ); this.restRequest_( @@ -132,17 +129,17 @@ export class ReadonlyRestClient extends ServerActions { } /** @inheritDoc */ - unlisten(query: Query, tag: number | null) { + unlisten(query: QueryContext, tag: number | null) { const listenId = ReadonlyRestClient.getListenId_(query, tag); delete this.listens_[listenId]; } - get(query: Query): Promise { + get(query: QueryContext): Promise { const queryStringParameters = queryParamsToRestQueryStringParameters( - query.getQueryParams() + query._queryParams ); - const pathString = query.path.toString(); + const pathString = query._path.toString(); const deferred = new Deferred(); diff --git a/packages/database/src/core/Repo.ts b/packages/database/src/core/Repo.ts index 8b325ef96a7..bf47d50bb71 100644 --- a/packages/database/src/core/Repo.ts +++ b/packages/database/src/core/Repo.ts @@ -25,7 +25,6 @@ import { } from '@firebase/util'; import { FirebaseAppLike } from '../api/Database'; -import { EventRegistration, Query } from '../api/Reference'; import { AuthTokenProvider } from './AuthTokenProvider'; import { PersistentConnection } from './PersistentConnection'; @@ -104,6 +103,7 @@ import { eventQueueRaiseEventsAtPath, eventQueueRaiseEventsForChangedPath } from './view/EventQueue'; +import { EventRegistration, QueryContext } from './view/EventRegistration'; const INTERRUPT_REASON = 'repo_interrupt'; @@ -280,13 +280,13 @@ export function repoStart(repo: Repo): void { repo.infoSyncTree_ = new SyncTree({ startListening: (query, tag, currentHashFn, onComplete) => { let infoEvents: Event[] = []; - const node = repo.infoData_.getNode(query.path); + const node = repo.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 = syncTreeApplyServerOverwrite( repo.infoSyncTree_, - query.path, + query._path, node ); setTimeout(() => { @@ -305,7 +305,7 @@ export function repoStart(repo: Repo): void { const events = onComplete(status, data); eventQueueRaiseEventsForChangedPath( repo.eventQueue_, - query.path, + query._path, events ); }); @@ -449,7 +449,7 @@ function repoGetNextWriteId(repo: Repo): number { * * @param query - The query to surface a value for. */ -export function repoGetValue(repo: Repo, query: Query): Promise { +export function repoGetValue(repo: Repo, query: QueryContext): Promise { // Only active queries are cached. There is no persisted cache. const cached = syncTreeGetServerValue(repo.serverSyncTree_, query); if (cached != null) { @@ -460,10 +460,10 @@ export function repoGetValue(repo: Repo, query: Query): Promise { const node = nodeFromJSON(payload as string); const events = syncTreeApplyServerOverwrite( repo.serverSyncTree_, - query.path, + query._path, node ); - eventQueueRaiseEventsAtPath(repo.eventQueue_, query.path, events); + eventQueueRaiseEventsAtPath(repo.eventQueue_, query._path, events); return Promise.resolve(node); }, err => { @@ -726,11 +726,11 @@ export function repoOnDisconnectUpdate( export function repoAddEventCallbackForQuery( repo: Repo, - query: Query, + query: QueryContext, eventRegistration: EventRegistration ): void { let events; - if (pathGetFront(query.path) === '.info') { + if (pathGetFront(query._path) === '.info') { events = syncTreeAddEventRegistration( repo.infoSyncTree_, query, @@ -743,18 +743,18 @@ export function repoAddEventCallbackForQuery( eventRegistration ); } - eventQueueRaiseEventsAtPath(repo.eventQueue_, query.path, events); + eventQueueRaiseEventsAtPath(repo.eventQueue_, query._path, events); } export function repoRemoveEventCallbackForQuery( repo: Repo, - query: Query, + query: QueryContext, eventRegistration: EventRegistration ): void { // 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. let events; - if (pathGetFront(query.path) === '.info') { + if (pathGetFront(query._path) === '.info') { events = syncTreeRemoveEventRegistration( repo.infoSyncTree_, query, @@ -767,7 +767,7 @@ export function repoRemoveEventCallbackForQuery( eventRegistration ); } - eventQueueRaiseEventsAtPath(repo.eventQueue_, query.path, events); + eventQueueRaiseEventsAtPath(repo.eventQueue_, query._path, events); } export function repoInterrupt(repo: Repo): void { diff --git a/packages/database/src/core/ServerActions.ts b/packages/database/src/core/ServerActions.ts index 0f707b67b83..7a8d3fa1225 100644 --- a/packages/database/src/core/ServerActions.ts +++ b/packages/database/src/core/ServerActions.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Query } from '../api/Reference'; +import { QueryContext } from './view/EventRegistration'; /** * Interface defining the set of actions that can be performed against the Firebase server @@ -25,7 +25,7 @@ import { Query } from '../api/Reference'; */ export abstract class ServerActions { abstract listen( - query: Query, + query: QueryContext, currentHashFn: () => string, tag: number | null, onComplete: (a: string, b: unknown) => void @@ -34,12 +34,12 @@ export abstract class ServerActions { /** * Remove a listen. */ - abstract unlisten(query: Query, tag: number | null): void; + abstract unlisten(query: QueryContext, tag: number | null): void; /** * Get the server value satisfying this query. */ - abstract get(query: Query): Promise; + abstract get(query: QueryContext): Promise; put( pathString: string, diff --git a/packages/database/src/core/SyncPoint.ts b/packages/database/src/core/SyncPoint.ts index ed9a124f542..b001865084f 100644 --- a/packages/database/src/core/SyncPoint.ts +++ b/packages/database/src/core/SyncPoint.ts @@ -17,11 +17,7 @@ import { assert } from '@firebase/util'; -import { - EventRegistration, - Query, - ReferenceConstructor -} from '../api/Reference'; +import { ReferenceConstructor } from '../exp/Reference'; import { Operation } from './operation/Operation'; import { ChildrenNode } from './snap/ChildrenNode'; @@ -29,6 +25,7 @@ import { Node } from './snap/Node'; import { Path } from './util/Path'; import { CacheNode } from './view/CacheNode'; import { Event } from './view/Event'; +import { EventRegistration, QueryContext } from './view/EventRegistration'; import { View, viewAddEventRegistration, @@ -126,12 +123,12 @@ export function syncPointApplyOperation( */ export function syncPointGetView( syncPoint: SyncPoint, - query: Query, + query: QueryContext, writesCache: WriteTreeRef, serverCache: Node | null, serverCacheComplete: boolean ): View { - const queryId = query.queryIdentifier(); + const queryId = query._queryIdentifier; const view = syncPoint.views.get(queryId); if (!view) { // TODO: make writesCache take flag for complete server node @@ -173,7 +170,7 @@ export function syncPointGetView( */ export function syncPointAddEventRegistration( syncPoint: SyncPoint, - query: Query, + query: QueryContext, eventRegistration: EventRegistration, writesCache: WriteTreeRef, serverCache: Node | null, @@ -186,8 +183,8 @@ export function syncPointAddEventRegistration( serverCache, serverCacheComplete ); - if (!syncPoint.views.has(query.queryIdentifier())) { - syncPoint.views.set(query.queryIdentifier(), view); + if (!syncPoint.views.has(query._queryIdentifier)) { + syncPoint.views.set(query._queryIdentifier, view); } // This is guaranteed to exist now, we just created anything that was missing viewAddEventRegistration(view, eventRegistration); @@ -206,12 +203,12 @@ export function syncPointAddEventRegistration( */ export function syncPointRemoveEventRegistration( syncPoint: SyncPoint, - query: Query, + query: QueryContext, eventRegistration: EventRegistration | null, cancelError?: Error -): { removed: Query[]; events: Event[] } { - const queryId = query.queryIdentifier(); - const removed: Query[] = []; +): { removed: QueryContext[]; events: Event[] } { + const queryId = query._queryIdentifier; + const removed: QueryContext[] = []; let cancelEvents: Event[] = []; const hadCompleteView = syncPointHasCompleteView(syncPoint); if (queryId === 'default') { @@ -224,7 +221,7 @@ export function syncPointRemoveEventRegistration( syncPoint.views.delete(viewQueryId); // We'll deal with complete views later. - if (!view.query.getQueryParams().loadsAllData()) { + if (!view.query._queryParams.loadsAllData()) { removed.push(view.query); } } @@ -240,7 +237,7 @@ export function syncPointRemoveEventRegistration( syncPoint.views.delete(queryId); // We'll deal with complete views later. - if (!view.query.getQueryParams().loadsAllData()) { + if (!view.query._queryParams.loadsAllData()) { removed.push(view.query); } } @@ -250,7 +247,7 @@ export function syncPointRemoveEventRegistration( if (hadCompleteView && !syncPointHasCompleteView(syncPoint)) { // We removed our last complete view. removed.push( - new (syncPointGetReferenceConstructor())(query.database, query.path) + new (syncPointGetReferenceConstructor())(query._repo, query._path) ); } @@ -260,7 +257,7 @@ export function syncPointRemoveEventRegistration( export function syncPointGetQueryViews(syncPoint: SyncPoint): View[] { const result = []; for (const view of syncPoint.views.values()) { - if (!view.query.getQueryParams().loadsAllData()) { + if (!view.query._queryParams.loadsAllData()) { result.push(view); } } @@ -284,20 +281,20 @@ export function syncPointGetCompleteServerCache( export function syncPointViewForQuery( syncPoint: SyncPoint, - query: Query + query: QueryContext ): View | null { - const params = query.getQueryParams(); + const params = query._queryParams; if (params.loadsAllData()) { return syncPointGetCompleteView(syncPoint); } else { - const queryId = query.queryIdentifier(); + const queryId = query._queryIdentifier; return syncPoint.views.get(queryId); } } export function syncPointViewExistsForQuery( syncPoint: SyncPoint, - query: Query + query: QueryContext ): boolean { return syncPointViewForQuery(syncPoint, query) != null; } @@ -308,7 +305,7 @@ export function syncPointHasCompleteView(syncPoint: SyncPoint): boolean { export function syncPointGetCompleteView(syncPoint: SyncPoint): View | null { for (const view of syncPoint.views.values()) { - if (view.query.getQueryParams().loadsAllData()) { + if (view.query._queryParams.loadsAllData()) { return view; } } diff --git a/packages/database/src/core/SyncTree.ts b/packages/database/src/core/SyncTree.ts index 948e713be52..6d7c0da8526 100644 --- a/packages/database/src/core/SyncTree.ts +++ b/packages/database/src/core/SyncTree.ts @@ -17,7 +17,7 @@ import { assert } from '@firebase/util'; -import { EventRegistration, Query } from '../api/Reference'; +import { ReferenceConstructor } from '../exp/Reference'; import { AckUserWrite } from './operation/AckUserWrite'; import { ListenComplete } from './operation/ListenComplete'; @@ -56,6 +56,7 @@ import { import { each, errorForServerCode } from './util/util'; import { CacheNode } from './view/CacheNode'; import { Event } from './view/Event'; +import { EventRegistration, QueryContext } from './view/EventRegistration'; import { View, viewGetCompleteNode, viewGetServerCache } from './view/View'; import { newWriteTree, @@ -69,6 +70,23 @@ import { writeTreeRemoveWrite } from './WriteTree'; +let referenceConstructor: ReferenceConstructor; + +export function syncTreeSetReferenceConstructor( + val: ReferenceConstructor +): void { + assert( + !referenceConstructor, + '__referenceConstructor has already been defined' + ); + referenceConstructor = val; +} + +function syncTreeGetReferenceConstructor(): ReferenceConstructor { + assert(referenceConstructor, 'Reference.ts has not been loaded'); + return referenceConstructor; +} + /** * @typedef {{ * startListening: function( @@ -83,13 +101,13 @@ import { */ export interface ListenProvider { startListening( - query: Query, + query: QueryContext, tag: number | null, hashFn: () => string, onComplete: (a: string, b?: unknown) => Event[] ): Event[]; - stopListening(a: Query, b: number | null): void; + stopListening(a: QueryContext, b: number | null): void; } /** @@ -315,12 +333,12 @@ export function syncTreeApplyTaggedListenComplete( */ export function syncTreeRemoveEventRegistration( syncTree: SyncTree, - query: Query, + query: QueryContext, eventRegistration: EventRegistration | null, cancelError?: Error ): Event[] { // Find the syncPoint first. Then deal with whether or not it has matching listeners - const path = query.path; + const path = query._path; const maybeSyncPoint = syncTree.syncPointTree_.get(path); let cancelEvents: Event[] = []; // A removal on a default query affects all queries at that location. A removal on an indexed query, even one without @@ -328,7 +346,7 @@ export function syncTreeRemoveEventRegistration( // not loadsAllData(). if ( maybeSyncPoint && - (query.queryIdentifier() === 'default' || + (query._queryIdentifier === 'default' || syncPointViewExistsForQuery(maybeSyncPoint, query)) ) { const removedAndEvents = syncPointRemoveEventRegistration( @@ -351,7 +369,7 @@ export function syncTreeRemoveEventRegistration( const removingDefault = -1 !== removed.findIndex(query => { - return query.getQueryParams().loadsAllData(); + return query._queryParams.loadsAllData(); }); const covered = syncTree.syncPointTree_.findOnPath( path, @@ -397,7 +415,7 @@ export function syncTreeRemoveEventRegistration( defaultTag ); } else { - removed.forEach((queryToRemove: Query) => { + removed.forEach((queryToRemove: QueryContext) => { const tagToRemove = syncTree.queryToTagMap.get( syncTreeMakeQueryKey_(queryToRemove) ); @@ -482,10 +500,10 @@ export function syncTreeApplyTaggedQueryMerge( */ export function syncTreeAddEventRegistration( syncTree: SyncTree, - query: Query, + query: QueryContext, eventRegistration: EventRegistration ): Event[] { - const path = query.path; + const path = query._path; let serverCache: Node | null = null; let foundAncestorDefaultView = false; @@ -531,7 +549,7 @@ export function syncTreeAddEventRegistration( } const viewAlreadyExists = syncPointViewExistsForQuery(syncPoint, query); - if (!viewAlreadyExists && !query.getQueryParams().loadsAllData()) { + if (!viewAlreadyExists && !query._queryParams.loadsAllData()) { // We need to track a tag for this query const queryKey = syncTreeMakeQueryKey_(query); assert( @@ -600,9 +618,9 @@ export function syncTreeCalcCompleteEventCache( export function syncTreeGetServerValue( syncTree: SyncTree, - query: Query + query: QueryContext ): Node | null { - const path = query.path; + const path = query._path; let serverCache: Node | null = null; // Any covering writes will necessarily be at the root, so really all we need to find is the server cache. // Consider optimizing this once there's a better understanding of what actual behavior will be. @@ -625,7 +643,7 @@ export function syncTreeGetServerValue( : null; const writesCache: WriteTreeRef | null = writeTreeChildWrites( syncTree.pendingWriteTree_, - query.path + query._path ); const view: View = syncPointGetView( syncPoint, @@ -774,9 +792,9 @@ function syncTreeCreateListenerForView_( onComplete: (status: string): Event[] => { if (status === 'ok') { if (tag) { - return syncTreeApplyTaggedListenComplete(syncTree, query.path, tag); + return syncTreeApplyTaggedListenComplete(syncTree, query._path, tag); } else { - return syncTreeApplyListenComplete(syncTree, query.path); + return syncTreeApplyListenComplete(syncTree, query._path); } } else { // If a listen failed, kill all of the listeners here, not just the one that triggered the error. @@ -796,7 +814,10 @@ function syncTreeCreateListenerForView_( /** * Return the tag associated with the given query. */ -function syncTreeTagForQuery_(syncTree: SyncTree, query: Query): number | null { +function syncTreeTagForQuery_( + syncTree: SyncTree, + query: QueryContext +): number | null { const queryKey = syncTreeMakeQueryKey_(query); return syncTree.queryToTagMap.get(queryKey); } @@ -804,8 +825,8 @@ function syncTreeTagForQuery_(syncTree: SyncTree, query: Query): number | null { /** * Given a query, computes a "queryKey" suitable for use in our queryToTagMap_. */ -function syncTreeMakeQueryKey_(query: Query): string { - return query.path.toString() + '$' + query.queryIdentifier(); +function syncTreeMakeQueryKey_(query: QueryContext): string { + return query._path.toString() + '$' + query._queryIdentifier; } /** @@ -882,24 +903,21 @@ function syncTreeCollectDistinctViewsForSubTree_( * * @return The normalized query */ -function syncTreeQueryForListening_(query: Query): Query { - if ( - query.getQueryParams().loadsAllData() && - !query.getQueryParams().isDefault() - ) { +function syncTreeQueryForListening_(query: QueryContext): QueryContext { + if (query._queryParams.loadsAllData() && !query._queryParams.isDefault()) { // We treat queries that load all data as default queries // Cast is necessary because ref() technically returns Firebase which is actually fb.api.Firebase which inherits // from Query - return query.getRef()!; + return new (syncTreeGetReferenceConstructor())(query._repo, query._path); } else { return query; } } -function syncTreeRemoveTags_(syncTree: SyncTree, queries: Query[]) { +function syncTreeRemoveTags_(syncTree: SyncTree, queries: QueryContext[]) { for (let j = 0; j < queries.length; ++j) { const removedQuery = queries[j]; - if (!removedQuery.getQueryParams().loadsAllData()) { + if (!removedQuery._queryParams.loadsAllData()) { // We should have a tag for this const removedQueryKey = syncTreeMakeQueryKey_(removedQuery); const removedQueryTag = syncTree.queryToTagMap.get(removedQueryKey); @@ -923,10 +941,10 @@ function syncTreeGetNextQueryTag_(): number { */ function syncTreeSetupListener_( syncTree: SyncTree, - query: Query, + query: QueryContext, view: View ): Event[] { - const path = query.path; + const path = query._path; const tag = syncTreeTagForQuery_(syncTree, query); const listener = syncTreeCreateListenerForView_(syncTree, view); @@ -947,7 +965,7 @@ function syncTreeSetupListener_( ); } else { // Shadow everything at or below this location, this is a default listener. - const queriesToStop = subtree.fold( + const queriesToStop = subtree.fold( (relativePath, maybeChildSyncPoint, childMap) => { if ( !pathIsEmpty(relativePath) && @@ -957,7 +975,7 @@ function syncTreeSetupListener_( return [syncPointGetCompleteView(maybeChildSyncPoint).query]; } else { // No default listener here, flatten any deeper queries into an array - let queries: Query[] = []; + let queries: QueryContext[] = []; if (maybeChildSyncPoint) { queries = queries.concat( syncPointGetQueryViews(maybeChildSyncPoint).map( @@ -965,7 +983,7 @@ function syncTreeSetupListener_( ) ); } - each(childMap, (_key: string, childQueries: Query[]) => { + each(childMap, (_key: string, childQueries: QueryContext[]) => { queries = queries.concat(childQueries); }); return queries; diff --git a/packages/database/src/core/util/util.ts b/packages/database/src/core/util/util.ts index 57fad9d0ae8..4208b0529d9 100644 --- a/packages/database/src/core/util/util.ts +++ b/packages/database/src/core/util/util.ts @@ -25,8 +25,8 @@ import { isNodeSdk } from '@firebase/util'; -import { Query } from '../../api/Reference'; import { SessionStorage } from '../storage/storage'; +import { QueryContext } from '../view/EventRegistration'; declare const window: Window; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -470,7 +470,7 @@ export const isWindowsStoreApp = function (): boolean { /** * Converts a server error code to a Javascript Error */ -export const errorForServerCode = function (code: string, query: Query): Error { +export function errorForServerCode(code: string, query: QueryContext): Error { let reason = 'Unknown Error'; if (code === 'too_big') { reason = @@ -483,12 +483,12 @@ export const errorForServerCode = function (code: string, query: Query): Error { } const error = new Error( - code + ' at ' + query.path.toString() + ': ' + reason + code + ' at ' + query._path.toString() + ': ' + reason ); // eslint-disable-next-line @typescript-eslint/no-explicit-any (error as any).code = code.toUpperCase(); return error; -}; +} /** * Used to test for integer-looking strings diff --git a/packages/database/src/core/view/Event.ts b/packages/database/src/core/view/Event.ts index cba342eda6a..307510b27e6 100644 --- a/packages/database/src/core/view/Event.ts +++ b/packages/database/src/core/view/Event.ts @@ -17,10 +17,11 @@ import { stringify } from '@firebase/util'; -import { EventRegistration } from '../../api/Reference'; -import { DataSnapshot as ExpDataSnapshot } from '../../exp/DataSnapshot'; +import { DataSnapshot as ExpDataSnapshot } from '../../exp/Reference_impl'; import { Path } from '../util/Path'; +import { EventRegistration } from './EventRegistration'; + /** * Encapsulates the data needed to raise an event * @interface @@ -37,10 +38,10 @@ export interface Event { export type EventType = | 'value' - | ' child_added' - | ' child_changed' - | ' child_moved' - | ' child_removed'; + | 'child_added' + | 'child_changed' + | 'child_moved' + | 'child_removed'; /** * Encapsulates the data needed to raise an event diff --git a/packages/database/src/core/view/EventGenerator.ts b/packages/database/src/core/view/EventGenerator.ts index cfc0cbb0c49..e78fb14ef19 100644 --- a/packages/database/src/core/view/EventGenerator.ts +++ b/packages/database/src/core/view/EventGenerator.ts @@ -17,12 +17,12 @@ import { assertionError } from '@firebase/util'; -import { EventRegistration, Query } from '../../api/Reference'; import { Index } from '../snap/indexes/Index'; import { NamedNode, Node } from '../snap/Node'; import { Change, ChangeType, changeChildMoved } from './Change'; import { Event } from './Event'; +import { EventRegistration, QueryContext } from './EventRegistration'; /** * An EventGenerator is used to convert "raw" changes (Change) as computed by the @@ -33,8 +33,8 @@ import { Event } from './Event'; export class EventGenerator { index_: Index; - constructor(public query_: Query) { - this.index_ = this.query_.getQueryParams().getIndex(); + constructor(public query_: QueryContext) { + this.index_ = this.query_._queryParams.getIndex(); } } diff --git a/packages/database/src/core/view/EventRegistration.ts b/packages/database/src/core/view/EventRegistration.ts index b53b848f574..cd9ff28985a 100644 --- a/packages/database/src/core/view/EventRegistration.ts +++ b/packages/database/src/core/view/EventRegistration.ts @@ -14,3 +14,110 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { assert } from '@firebase/util'; + +import { DataSnapshot } from '../../exp/Reference_impl'; +import { Repo } from '../Repo'; +import { Path } from '../util/Path'; + +import { Change } from './Change'; +import { CancelEvent, Event } from './Event'; +import { QueryParams } from './QueryParams'; + +/** + * A user callback. Callbacks issues from the Legacy SDK maintain references + * to the original user-issued callbacks, which allows equality + * comparison by reference even though this callbacks are wrapped before + * they can be passed to the firebase@exp SDK. + */ +export interface UserCallback { + (dataSnapshot: DataSnapshot, previousChildName?: string | null): unknown; + userCallback?: unknown; + context?: object | null; +} + +/** + * A wrapper class that converts events from the database@exp SDK to the legacy + * Database SDK. Events are not converted directly as event registration relies + * on reference comparison of the original user callback (see `matches()`) and + * relies on equality of the legacy SDK's `context` object. + */ +export class CallbackContext { + constructor( + private readonly snapshotCallback: UserCallback, + private readonly cancelCallback?: (error: Error) => unknown + ) {} + + onValue( + expDataSnapshot: DataSnapshot, + previousChildName?: string | null + ): void { + this.snapshotCallback.call(null, expDataSnapshot, previousChildName); + } + + onCancel(error: Error): void { + assert( + this.hasCancelCallback, + 'Raising a cancel event on a listener with no cancel callback' + ); + return this.cancelCallback.call(null, error); + } + + get hasCancelCallback(): boolean { + return !!this.cancelCallback; + } + + matches(other: CallbackContext): boolean { + return ( + this.snapshotCallback === other.snapshotCallback || + (this.snapshotCallback.userCallback === + other.snapshotCallback.userCallback && + this.snapshotCallback.context === other.snapshotCallback.context) + ); + } +} + +export interface QueryContext { + readonly _queryIdentifier: string; + readonly _queryObject: object; + readonly _repo: Repo; + readonly _path: Path; + readonly _queryParams: QueryParams; +} + +/** + * An EventRegistration is basically an event type ('value', 'child_added', etc.) and a callback + * to be notified of that type of event. + * + * 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. + */ +export interface EventRegistration { + /** + * True if this container has a callback to trigger for this event type + */ + respondsTo(eventType: string): boolean; + + createEvent(change: Change, query: QueryContext): Event; + + /** + * Given event data, return a function to trigger the user's callback + */ + getEventRunner(eventData: Event): () => void; + + createCancelEvent(error: Error, path: Path): CancelEvent | null; + + 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) + * + */ + hasAnyCallback(): boolean; +} diff --git a/packages/database/src/core/view/QueryParams.ts b/packages/database/src/core/view/QueryParams.ts index 6812e4d56b6..33477032fb0 100644 --- a/packages/database/src/core/view/QueryParams.ts +++ b/packages/database/src/core/view/QueryParams.ts @@ -23,7 +23,7 @@ import { PathIndex } from '../snap/indexes/PathIndex'; import { PRIORITY_INDEX } from '../snap/indexes/PriorityIndex'; import { VALUE_INDEX } from '../snap/indexes/ValueIndex'; import { predecessor, successor } from '../util/NextPushId'; -import { MIN_NAME, MAX_NAME } from '../util/util'; +import { MAX_NAME, MIN_NAME } from '../util/util'; import { IndexedFilter } from './filter/IndexedFilter'; import { LimitedFilter } from './filter/LimitedFilter'; diff --git a/packages/database/src/core/view/View.ts b/packages/database/src/core/view/View.ts index 2eec1bc90bc..1c5804a0eb1 100644 --- a/packages/database/src/core/view/View.ts +++ b/packages/database/src/core/view/View.ts @@ -17,7 +17,6 @@ import { assert } from '@firebase/util'; -import { EventRegistration, Query } from '../../api/Reference'; import { Operation, OperationType } from '../operation/Operation'; import { ChildrenNode } from '../snap/ChildrenNode'; import { PRIORITY_INDEX } from '../snap/indexes/PriorityIndex'; @@ -32,6 +31,7 @@ import { EventGenerator, eventGeneratorGenerateEventsForChanges } from './EventGenerator'; +import { EventRegistration, QueryContext } from './EventRegistration'; import { IndexedFilter } from './filter/IndexedFilter'; import { queryParamsGetNodeFilter } from './QueryParams'; import { @@ -62,8 +62,8 @@ export class View { eventRegistrations_: EventRegistration[] = []; eventGenerator_: EventGenerator; - constructor(private query_: Query, initialViewCache: ViewCache) { - const params = this.query_.getQueryParams(); + constructor(private query_: QueryContext, initialViewCache: ViewCache) { + const params = this.query_._queryParams; const indexFilter = new IndexedFilter(params.getIndex()); const filter = queryParamsGetNodeFilter(params); @@ -99,7 +99,7 @@ export class View { this.eventGenerator_ = new EventGenerator(this.query_); } - get query(): Query { + get query(): QueryContext { return this.query_; } } @@ -121,7 +121,7 @@ export function viewGetCompleteServerCache( // If this isn't a "loadsAllData" view, then cache isn't actually a complete cache and // we need to see if it contains the child we're interested in. if ( - view.query.getQueryParams().loadsAllData() || + view.query._queryParams.loadsAllData() || (!pathIsEmpty(path) && !cache.getImmediateChild(pathGetFront(path)).isEmpty()) ) { @@ -158,7 +158,7 @@ export function viewRemoveEventRegistration( eventRegistration == null, 'A cancel should cancel all event registrations.' ); - const path = view.query.path; + const path = view.query._path; view.eventRegistrations_.forEach(registration => { const maybeEvent = registration.createCancelEvent(cancelError, path); if (maybeEvent) { diff --git a/packages/database/src/exp/DataSnapshot.ts b/packages/database/src/exp/DataSnapshot.ts deleted file mode 100644 index f41b885e6fa..00000000000 --- a/packages/database/src/exp/DataSnapshot.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ChildrenNode } from '../core/snap/ChildrenNode'; -import { Index } from '../core/snap/indexes/Index'; -import { PRIORITY_INDEX } from '../core/snap/indexes/PriorityIndex'; -import { Node } from '../core/snap/Node'; -import { Path } from '../core/util/Path'; - -import { child, Reference } from './Reference'; - -export class DataSnapshot { - /** - * @param _node A SnapshotNode to wrap. - * @param ref The ref of the location this snapshot came from. - * @param _index The iteration order for this snapshot - */ - constructor( - readonly _node: Node, - readonly ref: Reference, - readonly _index: Index - ) {} - - get priority(): string | number | null { - // typecast here because we never return deferred values or internal priorities (MAX_PRIORITY) - return this._node.getPriority().val() as string | number | null; - } - - get key(): string | null { - return this.ref.key; - } - - get size(): number { - return this._node.numChildren(); - } - - child(path: string): DataSnapshot { - const childPath = new Path(path); - const childRef = child(this.ref, path); - return new DataSnapshot( - this._node.getChild(childPath), - childRef, - PRIORITY_INDEX - ); - } - - exists(): boolean { - return !this._node.isEmpty(); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - exportVal(): any { - return this._node.val(true); - } - - forEach(action: (child: DataSnapshot) => boolean | void): boolean { - if (this._node.isLeafNode()) { - return false; - } - - const childrenNode = this._node as ChildrenNode; - // 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, child(this.ref, key), PRIORITY_INDEX) - ); - }); - } - - hasChild(path: string): boolean { - const childPath = new Path(path); - return !this._node.getChild(childPath).isEmpty(); - } - - hasChildren(): boolean { - if (this._node.isLeafNode()) { - return false; - } else { - return !this._node.isEmpty(); - } - } - - toJSON(): object | null { - return this.exportVal(); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - val(): any { - return this._node.val(); - } -} diff --git a/packages/database/src/exp/Database.ts b/packages/database/src/exp/Database.ts index e2624adecb4..a53dc7b4cef 100644 --- a/packages/database/src/exp/Database.ts +++ b/packages/database/src/exp/Database.ts @@ -20,13 +20,14 @@ import { _FirebaseService, _getProvider, FirebaseApp } from '@firebase/app-exp'; import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { Provider } from '@firebase/component'; -import { Reference } from '../api/Reference'; +import { Repo } from '../core/Repo'; /** * Class representing a Firebase Realtime Database. */ export class FirebaseDatabase implements _FirebaseService { readonly 'type' = 'database'; + _repo: Repo; constructor( readonly app: FirebaseApp, @@ -77,19 +78,6 @@ export function goOnline(db: FirebaseDatabase): void { return {} as any; } -export function ref( - db: FirebaseDatabase, - path?: string | Reference -): Reference { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function refFromURL(db: FirebaseDatabase, url: string): Reference { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - export function enableLogging( logger?: boolean | ((message: string) => unknown), persistent?: boolean diff --git a/packages/database/src/exp/Query.ts b/packages/database/src/exp/Query.ts deleted file mode 100644 index 4d1c01e1484..00000000000 --- a/packages/database/src/exp/Query.ts +++ /dev/null @@ -1,315 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { DataSnapshot } from './DataSnapshot'; -import { Reference } from './Reference'; - -export class Query { - protected constructor() {} - ref: Reference; - - isEqual(other: Query | null): boolean { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; - } - - toJSON(): object { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; - } - - toString(): string { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; - } -} - -export function get(query: Query): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export type Unsubscribe = () => {}; -export interface ListenOptions { - readonly onlyOnce?: boolean; -} - -export function onValue( - query: Query, - callback: (snapshot: DataSnapshot) => unknown, - cancelCallback?: (error: Error) => unknown -): Unsubscribe; -export function onValue( - query: Query, - callback: (snapshot: DataSnapshot) => unknown, - options: ListenOptions -): Unsubscribe; -export function onValue( - query: Query, - callback: (snapshot: DataSnapshot) => unknown, - cancelCallback: (error: Error) => unknown, - options: ListenOptions -): Unsubscribe; -export function onValue( - query: Query, - callback: (snapshot: DataSnapshot) => unknown, - cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, - options?: ListenOptions -): Unsubscribe { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function onChildAdded( - query: Query, - callback: ( - snapshot: DataSnapshot, - previousChildName?: string | null - ) => unknown, - cancelCallback?: (error: Error) => unknown -): Unsubscribe; -export function onChildAdded( - query: Query, - callback: ( - snapshot: DataSnapshot, - previousChildName: string | null - ) => unknown, - options: ListenOptions -): Unsubscribe; -export function onChildAdded( - query: Query, - callback: ( - snapshot: DataSnapshot, - previousChildName: string | null - ) => unknown, - cancelCallback: (error: Error) => unknown, - options: ListenOptions -): Unsubscribe; -export function onChildAdded( - query: Query, - callback: ( - snapshot: DataSnapshot, - previousChildName: string | null - ) => unknown, - cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, - options?: ListenOptions -): Unsubscribe { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function onChildChanged( - query: Query, - callback: ( - snapshot: DataSnapshot, - previousChildName: string | null - ) => unknown, - cancelCallback?: (error: Error) => unknown -): Unsubscribe; -export function onChildChanged( - query: Query, - callback: ( - snapshot: DataSnapshot, - previousChildName: string | null - ) => unknown, - options: ListenOptions -): Unsubscribe; -export function onChildChanged( - query: Query, - callback: ( - snapshot: DataSnapshot, - previousChildName: string | null - ) => unknown, - cancelCallback: (error: Error) => unknown, - options: ListenOptions -): Unsubscribe; -export function onChildChanged( - query: Query, - callback: ( - snapshot: DataSnapshot, - previousChildName: string | null - ) => unknown, - cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, - options?: ListenOptions -): Unsubscribe { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function onChildMoved( - query: Query, - callback: ( - snapshot: DataSnapshot, - previousChildName: string | null - ) => unknown, - cancelCallback?: (error: Error) => unknown -): Unsubscribe; -export function onChildMoved( - query: Query, - callback: ( - snapshot: DataSnapshot, - previousChildName: string | null - ) => unknown, - options: ListenOptions -): Unsubscribe; -export function onChildMoved( - query: Query, - callback: ( - snapshot: DataSnapshot, - previousChildName: string | null - ) => unknown, - cancelCallback: (error: Error) => unknown, - options: ListenOptions -): Unsubscribe; -export function onChildMoved( - query: Query, - callback: ( - snapshot: DataSnapshot, - previousChildName: string | null - ) => unknown, - cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, - options?: ListenOptions -): Unsubscribe { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function onChildRemoved( - query: Query, - callback: (snapshot: DataSnapshot) => unknown, - cancelCallback?: (error: Error) => unknown -): Unsubscribe; -export function onChildRemoved( - query: Query, - callback: (snapshot: DataSnapshot) => unknown, - options: ListenOptions -): Unsubscribe; -export function onChildRemoved( - query: Query, - callback: (snapshot: DataSnapshot) => unknown, - cancelCallback: (error: Error) => unknown, - options: ListenOptions -): Unsubscribe; -export function onChildRemoved( - query: Query, - callback: (snapshot: DataSnapshot) => unknown, - cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, - options?: ListenOptions -): Unsubscribe { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function off( - query: Query, - callback?: ( - snapshot: DataSnapshot, - previousChildName?: string | null - ) => unknown -): void { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export interface QueryConstraint { - type: - | 'endAt' - | 'endBefore' - | 'startAt' - | 'startAfter' - | 'limitToFirst' - | 'limitToLast' - | 'orderByChild' - | 'orderByKey' - | 'orderByPriority' - | 'orderByValue' - | 'equalTo'; -} -export function endAt( - value: number | string | boolean | null, - key?: string -): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function endBefore( - value: number | string | boolean | null, - key?: string -): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function startAt( - value: number | string | boolean | null, - key?: string -): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function startAfter( - value: number | string | boolean | null, - key?: string -): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function limitToFirst(limit: number): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function limitToLast(limit: number): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function orderByChild(path: string): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function orderByKey(): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function orderByPriority(): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function orderByValue(): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function equalTo( - value: number | string | boolean | null, - key?: string -): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function query(query: Query, ...constraints: QueryConstraint[]): Query { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} diff --git a/packages/database/src/exp/Reference.ts b/packages/database/src/exp/Reference.ts index 887800b3f8d..0d518a677ab 100644 --- a/packages/database/src/exp/Reference.ts +++ b/packages/database/src/exp/Reference.ts @@ -1,6 +1,10 @@ +import { Repo } from '../core/Repo'; +import { Path } from '../core/util/Path'; +import { QueryContext } from '../core/view/EventRegistration'; + /** * @license - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,36 +19,16 @@ * limitations under the License. */ -import { Repo } from '../core/Repo'; -import { - Path, - pathChild, - pathGetBack, - pathIsEmpty, - pathParent -} from '../core/util/Path'; - -import { Query } from './Query'; - -export class Reference extends Query { - root: Reference; - - constructor(readonly _repo: Repo, readonly _path: Path) { - super(); - } - - get key(): string | null { - if (pathIsEmpty(this._path)) { - return null; - } else { - return pathGetBack(this._path); - } - } +export interface Query extends QueryContext { + readonly ref: Reference; + isEqual(other: Query | null): boolean; + toJSON(): object; + toString(): string; +} - get parent(): Reference | null { - const parentPath = pathParent(this._path); - return parentPath === null ? null : new Reference(this._repo, parentPath); - } +export interface Reference extends Query { + readonly key: string | null; + readonly parent: Reference | null; } export interface OnDisconnect { @@ -62,49 +46,12 @@ export interface ThenableReference extends Reference, Pick, 'then' | 'catch'> {} -export function child(ref: Reference, path: string): Reference { - // TODO: Accept Compat class - return new Reference(ref._repo, pathChild(ref._path, path)); -} - -export function onDisconnect(ref: Reference): OnDisconnect { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function push(ref: Reference, value?: unknown): ThenableReference { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function remove(ref: Reference): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function set(ref: Reference, value: unknown): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} - -export function setPriority( - ref: Reference, - priority: string | number | null -): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; -} +export type Unsubscribe = () => void; -export function setWithPriority( - ref: Reference, - newVal: unknown, - newPriority: string | number | null -): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; +export interface ListenOptions { + readonly onlyOnce?: boolean; } -export function update(ref: Reference, values: object): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; +export interface ReferenceConstructor { + new (repo: Repo, path: Path): Reference; } diff --git a/packages/database/src/exp/Reference_impl.ts b/packages/database/src/exp/Reference_impl.ts new file mode 100644 index 00000000000..242324a81a3 --- /dev/null +++ b/packages/database/src/exp/Reference_impl.ts @@ -0,0 +1,810 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assert, contains } from '@firebase/util'; + +import { + Repo, + repoAddEventCallbackForQuery, + repoRemoveEventCallbackForQuery +} from '../core/Repo'; +import { ChildrenNode } from '../core/snap/ChildrenNode'; +import { Index } from '../core/snap/indexes/Index'; +import { PRIORITY_INDEX } from '../core/snap/indexes/PriorityIndex'; +import { Node } from '../core/snap/Node'; +import { syncPointSetReferenceConstructor } from '../core/SyncPoint'; +import { syncTreeSetReferenceConstructor } from '../core/SyncTree'; +import { + Path, + pathChild, + pathGetBack, + pathIsEmpty, + pathParent +} from '../core/util/Path'; +import { ObjectToUniqueKey } from '../core/util/util'; +import { Change } from '../core/view/Change'; +import { CancelEvent, DataEvent, EventType } from '../core/view/Event'; +import { + CallbackContext, + EventRegistration, + QueryContext +} from '../core/view/EventRegistration'; +import { + QueryParams, + queryParamsGetQueryObject +} from '../core/view/QueryParams'; + +import { FirebaseDatabase } from './Database'; +import { + ListenOptions, + OnDisconnect, + Query as Query, + Reference as Reference, + ThenableReference, + Unsubscribe +} from './Reference'; + +export class QueryImpl implements Query, QueryContext { + /** + * @hideconstructor + */ + constructor( + readonly _repo: Repo, + readonly _path: Path, + readonly _queryParams: QueryParams, + private readonly _orderByCalled: boolean + ) {} + + get key(): string | null { + if (pathIsEmpty(this._path)) { + return null; + } else { + return pathGetBack(this._path); + } + } + + get ref(): Reference { + return new ReferenceImpl(this._repo, this._path); + } + + get _queryIdentifier(): string { + const obj = queryParamsGetQueryObject(this._queryParams); + const id = ObjectToUniqueKey(obj); + return id === '{}' ? 'default' : id; + } + + /** + * An object representation of the query parameters used by this Query. + */ + get _queryObject(): object { + return queryParamsGetQueryObject(this._queryParams); + } + + isEqual(other: QueryImpl | null): boolean { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; + } + + toJSON(): object { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; + } + + toString(): string { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; + } +} + +export class ReferenceImpl extends QueryImpl implements Reference { + root: Reference; + + /** @hideconstructor */ + constructor(repo: Repo, path: Path) { + super(repo, path, new QueryParams(), false); + } + + get parent(): Reference | null { + const parentPath = pathParent(this._path); + return parentPath === null + ? null + : new ReferenceImpl(this._repo, parentPath); + } +} + +export class DataSnapshot { + /** + * @param _node A SnapshotNode to wrap. + * @param ref The location this snapshot came from. + * @param _index The iteration order for this snapshot + * @hideconstructor + */ + constructor( + readonly _node: Node, + readonly ref: Reference, + readonly _index: Index + ) {} + + get priority(): string | number | null { + // typecast here because we never return deferred values or internal priorities (MAX_PRIORITY) + return this._node.getPriority().val() as string | number | null; + } + + get key(): string | null { + return this.ref.key; + } + + get size(): number { + return this._node.numChildren(); + } + + child(path: string): DataSnapshot { + const childPath = new Path(path); + const childRef = child(this.ref, path); + return new DataSnapshot( + this._node.getChild(childPath), + childRef, + PRIORITY_INDEX + ); + } + + exists(): boolean { + return !this._node.isEmpty(); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + exportVal(): any { + return this._node.val(true); + } + + forEach(action: (child: DataSnapshot) => boolean | void): boolean { + if (this._node.isLeafNode()) { + return false; + } + + const childrenNode = this._node as ChildrenNode; + // 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, child(this.ref, key), PRIORITY_INDEX) + ); + }); + } + + hasChild(path: string): boolean { + const childPath = new Path(path); + return !this._node.getChild(childPath).isEmpty(); + } + + hasChildren(): boolean { + if (this._node.isLeafNode()) { + return false; + } else { + return !this._node.isEmpty(); + } + } + + toJSON(): object | null { + return this.exportVal(); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + val(): any { + return this._node.val(); + } +} + +export function ref(db: FirebaseDatabase, path?: string): Reference { + return new ReferenceImpl(db._repo, new Path(path || '')); +} + +export function refFromURL(db: FirebaseDatabase, url: string): Reference { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function child(ref: Reference, path: string): Reference { + // TODO: Accept Compat class + return new ReferenceImpl(ref._repo, pathChild(ref._path, path)); +} + +export function onDisconnect(ref: Reference): OnDisconnect { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function push(ref: Reference, value?: unknown): ThenableReference { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function remove(ref: Reference): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function set(ref: Reference, value: unknown): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function setPriority( + ref: Reference, + priority: string | number | null +): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function setWithPriority( + ref: Reference, + newVal: unknown, + newPriority: string | number | null +): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function update(ref: Reference, values: object): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function get(query: Query): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +/** + * Represents registration for 'value' events. + */ +export class ValueEventRegistration implements EventRegistration { + constructor(private callbackContext: CallbackContext) {} + + /** + * @inheritDoc + */ + respondsTo(eventType: string): boolean { + return eventType === 'value'; + } + + /** + * @inheritDoc + */ + createEvent(change: Change, query: QueryContext): DataEvent { + const index = query._queryParams.getIndex(); + return new DataEvent( + 'value', + this, + new DataSnapshot( + change.snapshotNode, + new ReferenceImpl(query._repo, query._path), + index + ) + ); + } + + /** + * @inheritDoc + */ + getEventRunner(eventData: CancelEvent | DataEvent): () => void { + if (eventData.getEventType() === 'cancel') { + return () => + this.callbackContext.onCancel((eventData as CancelEvent).error); + } else { + return () => + this.callbackContext.onValue((eventData as DataEvent).snapshot, null); + } + } + + /** + * @inheritDoc + */ + createCancelEvent(error: Error, path: Path): CancelEvent | null { + if (this.callbackContext.hasCancelCallback) { + return new CancelEvent(this, error, path); + } else { + return null; + } + } + + /** + * @inheritDoc + */ + matches(other: EventRegistration): boolean { + if (!(other instanceof ValueEventRegistration)) { + return false; + } else if (!other.callbackContext || !this.callbackContext) { + // If no callback specified, we consider it to match any callback. + return true; + } else { + return other.callbackContext.matches(this.callbackContext); + } + } + + /** + * @inheritDoc + */ + hasAnyCallback(): boolean { + return this.callbackContext !== null; + } +} + +/** + * Represents the registration of 1 or more child_xxx events. + * + * 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. + */ +export class ChildEventRegistration implements EventRegistration { + constructor( + private callbacks: { + [child: string]: CallbackContext; + } | null + ) {} + + /** + * @inheritDoc + */ + respondsTo(eventType: string): 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.callbacks['cancel'].hasCancelCallback) { + return new CancelEvent(this, error, path); + } else { + return null; + } + } + + /** + * @inheritDoc + */ + createEvent(change: Change, query: QueryContext): DataEvent { + assert(change.childName != null, 'Child events should have a childName.'); + const childRef = child( + new ReferenceImpl(query._repo, query._path), + change.childName + ); + const index = query._queryParams.getIndex(); + return new DataEvent( + change.type as EventType, + this, + new DataSnapshot(change.snapshotNode, childRef, index), + change.prevName + ); + } + + /** + * @inheritDoc + */ + getEventRunner(eventData: CancelEvent | DataEvent): () => void { + if (eventData.getEventType() === 'cancel') { + const cancelCB = this.callbacks['cancel']; + return () => cancelCB.onCancel((eventData as CancelEvent).error); + } else { + const cb = this.callbacks[(eventData as DataEvent).eventType]; + return () => + cb.onValue( + (eventData as DataEvent).snapshot, + (eventData as DataEvent).prevName + ); + } + } + + /** + * @inheritDoc + */ + matches(other: EventRegistration): boolean { + if (other instanceof ChildEventRegistration) { + if (!this.callbacks || !other.callbacks) { + return true; + } else { + const otherKeys = Object.keys(other.callbacks); + const thisKeys = Object.keys(this.callbacks); + const otherCount = otherKeys.length; + const thisCount = thisKeys.length; + 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 = otherKeys[0]; + const thisKey = thisKeys[0]; + return ( + thisKey === otherKey && + (!other.callbacks[otherKey] || + !this.callbacks[thisKey] || + other.callbacks[otherKey].matches(this.callbacks[thisKey])) + ); + } else { + // Exact match on each key. + return thisKeys.every(eventType => + other.callbacks[eventType].matches(this.callbacks[eventType]) + ); + } + } + } + } + + return false; + } + + /** + * @inheritDoc + */ + hasAnyCallback(): boolean { + return this.callbacks !== null; + } +} + +function addEventListener( + query: Query, + eventType: EventType, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallbackOrListenOptions: ((error: Error) => unknown) | ListenOptions, + options: ListenOptions +) { + let cancelCallback: ((error: Error) => unknown) | undefined; + if (typeof cancelCallbackOrListenOptions === 'object') { + cancelCallback = undefined; + options = cancelCallbackOrListenOptions; + } + if (typeof cancelCallbackOrListenOptions === 'function') { + cancelCallback = cancelCallbackOrListenOptions; + } + + const callbackContext = new CallbackContext( + callback, + cancelCallback || undefined + ); + const container = + eventType === 'value' + ? new ValueEventRegistration(callbackContext) + : new ChildEventRegistration({ + [eventType]: callbackContext + }); + repoAddEventCallbackForQuery(query._repo, query, container); + return () => repoRemoveEventCallbackForQuery(query._repo, query, container); +} + +export function onValue( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + cancelCallback?: (error: Error) => unknown +): Unsubscribe; +export function onValue( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + options: ListenOptions +): Unsubscribe; +export function onValue( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + cancelCallback: (error: Error) => unknown, + options: ListenOptions +): Unsubscribe; +export function onValue( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, + options?: ListenOptions +): Unsubscribe { + return addEventListener( + query, + 'value', + callback, + cancelCallbackOrListenOptions, + options + ); +} + +export function onChildAdded( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName?: string | null + ) => unknown, + cancelCallback?: (error: Error) => unknown +): Unsubscribe; +export function onChildAdded( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildAdded( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallback: (error: Error) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildAdded( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, + options?: ListenOptions +): Unsubscribe { + return addEventListener( + query, + 'child_added', + callback, + cancelCallbackOrListenOptions, + options + ); +} + +export function onChildChanged( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallback?: (error: Error) => unknown +): Unsubscribe; +export function onChildChanged( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildChanged( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallback: (error: Error) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildChanged( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, + options?: ListenOptions +): Unsubscribe { + return addEventListener( + query, + 'child_changed', + callback, + cancelCallbackOrListenOptions, + options + ); +} + +export function onChildMoved( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallback?: (error: Error) => unknown +): Unsubscribe; +export function onChildMoved( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildMoved( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallback: (error: Error) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildMoved( + query: Query, + callback: ( + snapshot: DataSnapshot, + previousChildName: string | null + ) => unknown, + cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, + options?: ListenOptions +): Unsubscribe { + return addEventListener( + query, + 'child_moved', + callback, + cancelCallbackOrListenOptions, + options + ); +} + +export function onChildRemoved( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + cancelCallback?: (error: Error) => unknown +): Unsubscribe; +export function onChildRemoved( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildRemoved( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + cancelCallback: (error: Error) => unknown, + options: ListenOptions +): Unsubscribe; +export function onChildRemoved( + query: Query, + callback: (snapshot: DataSnapshot) => unknown, + cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, + options?: ListenOptions +): Unsubscribe { + return addEventListener( + query, + 'child_removed', + callback, + cancelCallbackOrListenOptions, + options + ); +} + +export { EventType }; + +export function off( + query: Query, + eventType?: EventType, + callback?: ( + snapshot: DataSnapshot, + previousChildName?: string | null + ) => unknown +): void { + let container: EventRegistration | null = null; + let callbacks: { [k: string]: CallbackContext } | null = null; + const expCallback = callback ? new CallbackContext(callback) : null; + if (eventType === 'value') { + container = new ValueEventRegistration(expCallback); + } else if (eventType) { + if (callback) { + callbacks = {}; + callbacks[eventType] = expCallback; + } + container = new ChildEventRegistration(callbacks); + } + repoRemoveEventCallbackForQuery(query._repo, query, container); +} + +export interface QueryConstraint { + type: + | 'endAt' + | 'endBefore' + | 'startAt' + | 'startAfter' + | 'limitToFirst' + | 'limitToLast' + | 'orderByChild' + | 'orderByKey' + | 'orderByPriority' + | 'orderByValue' + | 'equalTo'; +} + +export function endAt( + value: number | string | boolean | null, + key?: string +): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function endBefore( + value: number | string | boolean | null, + key?: string +): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function startAt( + value: number | string | boolean | null, + key?: string +): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function startAfter( + value: number | string | boolean | null, + key?: string +): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function limitToFirst(limit: number): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function limitToLast(limit: number): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function orderByChild(path: string): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function orderByKey(): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function orderByPriority(): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function orderByValue(): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function equalTo( + value: number | string | boolean | null, + key?: string +): QueryConstraint { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +export function query(query: Query, ...constraints: QueryConstraint[]): Query { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; +} + +/** + * Define reference constructor in various modules + * + * We are doing this here to avoid several circular + * dependency issues + */ +syncPointSetReferenceConstructor(ReferenceImpl); +syncTreeSetReferenceConstructor(ReferenceImpl); diff --git a/packages/database/test/datasnapshot.test.ts b/packages/database/test/datasnapshot.test.ts index 7b57397286d..3afdfde8ddf 100644 --- a/packages/database/test/datasnapshot.test.ts +++ b/packages/database/test/datasnapshot.test.ts @@ -20,8 +20,7 @@ import { expect } from 'chai'; import { DataSnapshot, Reference } from '../src/api/Reference'; import { PRIORITY_INDEX } from '../src/core/snap/indexes/PriorityIndex'; import { nodeFromJSON } from '../src/core/snap/nodeFromJSON'; -import { DataSnapshot as ExpDataSnapshot } from '../src/exp/DataSnapshot'; -import { Reference as ExpReference } from '../src/exp/Reference'; +import { DataSnapshot as ExpDataSnapshot } from '../src/exp/Reference_impl'; import { getRandomNode } from './helpers/util'; @@ -30,11 +29,10 @@ describe('DataSnapshot Tests', () => { const snapshotForJSON = function (json) { const dummyRef = getRandomNode() as Reference; return new DataSnapshot( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, + dummyRef.database, new ExpDataSnapshot( nodeFromJSON(json), - new ExpReference(dummyRef.repo, dummyRef.path), + dummyRef._delegate, PRIORITY_INDEX ) ); diff --git a/packages/database/test/exp/integration.test.ts b/packages/database/test/exp/integration.test.ts index c602ee24b4b..36f90e971d3 100644 --- a/packages/database/test/exp/integration.test.ts +++ b/packages/database/test/exp/integration.test.ts @@ -20,12 +20,14 @@ import { initializeApp, deleteApp } from '@firebase/app-exp'; import { expect } from 'chai'; import { + get, getDatabase, goOffline, goOnline, ref, refFromURL } from '../../exp/index'; +import { set } from '../../src/exp/Reference_impl'; import { DATABASE_ADDRESS, DATABASE_URL } from '../helpers/util'; export function createTestApp() { @@ -65,27 +67,27 @@ describe.skip('Database Tests', () => { it('Can set and ge tref', async () => { const db = getDatabase(defaultApp); - await ref(db, 'foo/bar').set('foobar'); - const snap = await ref(db, 'foo/bar').get(); + await set(ref(db, 'foo/bar'), 'foobar'); + const snap = await get(ref(db, 'foo/bar')); expect(snap.val()).to.equal('foobar'); }); it('Can get refFromUrl', async () => { const db = getDatabase(defaultApp); - await refFromURL(db, `${DATABASE_ADDRESS}/foo/bar`).get(); + await get(refFromURL(db, `${DATABASE_ADDRESS}/foo/bar`)); }); it('Can goOffline/goOnline', async () => { const db = getDatabase(defaultApp); goOffline(db); try { - await ref(db, 'foo/bar').get(); + await get(ref(db, 'foo/bar')); expect.fail('Should have failed since we are offline'); } catch (e) { expect(e.message).to.equal('Error: Client is offline.'); } goOnline(db); - await ref(db, 'foo/bar').get(); + await get(ref(db, 'foo/bar')); }); it('Can delete app', async () => { diff --git a/packages/database/test/query.test.ts b/packages/database/test/query.test.ts index 8bcd6138cee..76fd4f7b940 100644 --- a/packages/database/test/query.test.ts +++ b/packages/database/test/query.test.ts @@ -365,7 +365,7 @@ describe('Query Tests', () => { it('Query.queryIdentifier works.', () => { const path = getRandomNode() as Reference; const queryId = function (query) { - return query.queryIdentifier(query); + return query._delegate._queryIdentifier; }; expect(queryId(path)).to.equal('default'); From 81ea6afca7665f4a3011bc6145adc9ce3bbc4e2c Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 2 Apr 2021 15:34:02 -0600 Subject: [PATCH 04/15] Compat and @exp class for Database (#4705) --- .../.idea/runConfigurations/All_Tests.xml | 2 + packages/database/exp/index.ts | 7 +- packages/database/index.node.ts | 9 +- packages/database/index.ts | 9 +- packages/database/src/api/Database.ts | 287 ++---------------- packages/database/src/api/Reference.ts | 43 +-- packages/database/src/api/internal.ts | 11 +- packages/database/src/api/test_access.ts | 2 +- .../database/src/core/AuthTokenProvider.ts | 11 +- packages/database/src/core/Repo.ts | 12 +- packages/database/src/exp/Database.ts | 239 ++++++++++++++- packages/database/src/exp/Reference_impl.ts | 34 ++- packages/database/test/database.test.ts | 24 +- 13 files changed, 345 insertions(+), 345 deletions(-) diff --git a/packages/database/.idea/runConfigurations/All_Tests.xml b/packages/database/.idea/runConfigurations/All_Tests.xml index 08aebabf99e..ec3e3acee6e 100644 --- a/packages/database/.idea/runConfigurations/All_Tests.xml +++ b/packages/database/.idea/runConfigurations/All_Tests.xml @@ -7,6 +7,8 @@ true + + bdd --require ts-node/register/type-check --require index.node.ts --timeout 5000 diff --git a/packages/database/exp/index.ts b/packages/database/exp/index.ts index 37dddea0055..22e55e7e584 100644 --- a/packages/database/exp/index.ts +++ b/packages/database/exp/index.ts @@ -20,7 +20,10 @@ import { _registerComponent, registerVersion } from '@firebase/app-exp'; import { Component, ComponentType } from '@firebase/component'; import { version } from '../package.json'; -import { FirebaseDatabase } from '../src/exp/Database'; +import { + FirebaseDatabase, + repoManagerDatabaseFromApp +} from '../src/exp/Database'; export { enableLogging, @@ -79,7 +82,7 @@ function registerDatabase(): void { (container, { instanceIdentifier: url }) => { const app = container.getProvider('app-exp').getImmediate()!; const authProvider = container.getProvider('auth-internal'); - return new FirebaseDatabase(app, authProvider, url); + return repoManagerDatabaseFromApp(app, authProvider, url); }, ComponentType.PUBLIC ).setMultipleInstances(true) diff --git a/packages/database/index.node.ts b/packages/database/index.node.ts index a1214d8abc0..de1a4c37d97 100644 --- a/packages/database/index.node.ts +++ b/packages/database/index.node.ts @@ -24,12 +24,13 @@ import { CONSTANTS, isNodeSdk } from '@firebase/util'; import { Client } from 'faye-websocket'; import { name, version } from './package.json'; -import { Database, repoManagerDatabaseFromApp } from './src/api/Database'; +import { Database } from './src/api/Database'; import * as INTERNAL from './src/api/internal'; import { DataSnapshot, Query, Reference } from './src/api/Reference'; import * as TEST_ACCESS from './src/api/test_access'; import { enableLogging } from './src/core/util/util'; import { setSDKVersion } from './src/core/version'; +import { repoManagerDatabaseFromApp } from './src/exp/Database'; import { setWebSocketImpl } from './src/realtime/WebSocketConnection'; setWebSocketImpl(Client); @@ -86,8 +87,10 @@ export function registerDatabase(instance: FirebaseNamespace) { // getImmediate for FirebaseApp will always succeed const app = container.getProvider('app').getImmediate(); const authProvider = container.getProvider('auth-internal'); - - return repoManagerDatabaseFromApp(app, authProvider, url, undefined); + return new Database( + repoManagerDatabaseFromApp(app, authProvider, url), + app + ); }, ComponentType.PUBLIC ) diff --git a/packages/database/index.ts b/packages/database/index.ts index c6c06675d80..442be97313b 100644 --- a/packages/database/index.ts +++ b/packages/database/index.ts @@ -24,12 +24,13 @@ import * as types from '@firebase/database-types'; import { isNodeSdk } from '@firebase/util'; import { name, version } from './package.json'; -import { Database, repoManagerDatabaseFromApp } from './src/api/Database'; +import { Database } from './src/api/Database'; import * as INTERNAL from './src/api/internal'; import { DataSnapshot, Query, Reference } from './src/api/Reference'; import * as TEST_ACCESS from './src/api/test_access'; import { enableLogging } from './src/core/util/util'; import { setSDKVersion } from './src/core/version'; +import { repoManagerDatabaseFromApp } from './src/exp/Database'; const ServerValue = Database.ServerValue; @@ -46,8 +47,10 @@ export function registerDatabase(instance: FirebaseNamespace) { // getImmediate for FirebaseApp will always succeed const app = container.getProvider('app').getImmediate(); const authProvider = container.getProvider('auth-internal'); - - return repoManagerDatabaseFromApp(app, authProvider, url, undefined); + return new Database( + repoManagerDatabaseFromApp(app, authProvider, url), + app + ); }, ComponentType.PUBLIC ) diff --git a/packages/database/src/api/Database.ts b/packages/database/src/api/Database.ts index c922f00f1b3..7ba48480ac6 100644 --- a/packages/database/src/api/Database.ts +++ b/packages/database/src/api/Database.ts @@ -15,202 +15,25 @@ * limitations under the License. */ // eslint-disable-next-line import/no-extraneous-dependencies -import { FirebaseApp as FirebaseAppExp } from '@firebase/app-exp'; + import { FirebaseApp } from '@firebase/app-types'; import { FirebaseService } from '@firebase/app-types/private'; -import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; -import { Provider } from '@firebase/component'; -import { safeGet, validateArgCount } from '@firebase/util'; +import { validateArgCount, Compat } from '@firebase/util'; import { - AuthTokenProvider, - EmulatorAdminTokenProvider, - FirebaseAuthTokenProvider -} from '../core/AuthTokenProvider'; -import { Repo, repoInterrupt, repoResume, repoStart } from '../core/Repo'; -import { RepoInfo } from '../core/RepoInfo'; -import { parseRepoInfo } from '../core/util/libs/parser'; -import { pathIsEmpty, newEmptyPath } from '../core/util/Path'; -import { fatal, log } from '../core/util/util'; -import { validateUrl } from '../core/util/validation'; + FirebaseDatabase as ExpDatabase, + goOnline, + useDatabaseEmulator, + goOffline +} from '../exp/Database'; +import { ref, refFromURL } from '../exp/Reference_impl'; import { Reference } from './Reference'; -/** - * This variable is also defined in the firebase node.js admin SDK. Before - * modifying this definition, consult the definition in: - * - * https://github.com/firebase/firebase-admin-node - * - * and make sure the two are consistent. - */ -const FIREBASE_DATABASE_EMULATOR_HOST_VAR = 'FIREBASE_DATABASE_EMULATOR_HOST'; - -/** - * Intersection type that allows the SDK to be used from firebase-exp and - * firebase v8. - */ -export type FirebaseAppLike = FirebaseApp | FirebaseAppExp; - -/** - * Creates and caches Repo instances. - */ -const repos: { - [appName: string]: { - [dbUrl: string]: Repo; - }; -} = {}; - -/** - * If true, new Repos will be created to use ReadonlyRestClient (for testing purposes). - */ -let useRestClient = false; - -/** - * Update an existing repo in place to point to a new host/port. - */ -export function repoManagerApplyEmulatorSettings( - repo: Repo, - host: string, - port: number -): void { - repo.repoInfo_ = new RepoInfo( - `${host}:${port}`, - /* secure= */ false, - repo.repoInfo_.namespace, - repo.repoInfo_.webSocketOnly, - repo.repoInfo_.nodeAdmin, - repo.repoInfo_.persistenceKey, - repo.repoInfo_.includeNamespaceInQueryParams - ); - - if (repo.repoInfo_.nodeAdmin) { - repo.authTokenProvider_ = new EmulatorAdminTokenProvider(); - } -} - -/** - * This function should only ever be called to CREATE a new database instance. - */ -export function repoManagerDatabaseFromApp( - app: FirebaseAppLike, - authProvider: Provider, - url?: string, - nodeAdmin?: boolean -): Database { - let dbUrl: string | undefined = url || app.options.databaseURL; - if (dbUrl === undefined) { - if (!app.options.projectId) { - fatal( - "Can't determine Firebase Database URL. Be sure to include " + - ' a Project ID when calling firebase.initializeApp().' - ); - } - - log('Using default host for project ', app.options.projectId); - dbUrl = `${app.options.projectId}-default-rtdb.firebaseio.com`; - } - - let parsedUrl = parseRepoInfo(dbUrl, nodeAdmin); - let repoInfo = parsedUrl.repoInfo; - - let isEmulator: boolean; - - let dbEmulatorHost: string | undefined = undefined; - if (typeof process !== 'undefined') { - dbEmulatorHost = process.env[FIREBASE_DATABASE_EMULATOR_HOST_VAR]; - } - - if (dbEmulatorHost) { - isEmulator = true; - dbUrl = `http://${dbEmulatorHost}?ns=${repoInfo.namespace}`; - parsedUrl = parseRepoInfo(dbUrl, nodeAdmin); - repoInfo = parsedUrl.repoInfo; - } else { - isEmulator = !parsedUrl.repoInfo.secure; - } - - const authTokenProvider = - nodeAdmin && isEmulator - ? new EmulatorAdminTokenProvider() - : new FirebaseAuthTokenProvider(app, authProvider); - - validateUrl('Invalid Firebase Database URL', 1, parsedUrl); - if (!pathIsEmpty(parsedUrl.path)) { - fatal( - 'Database URL must point to the root of a Firebase Database ' + - '(not including a child path).' - ); - } - - const repo = repoManagerCreateRepo(repoInfo, app, authTokenProvider); - return new Database(repo); -} - -/** - * Remove the repo and make sure it is disconnected. - * - */ -export function repoManagerDeleteRepo(repo: Repo): void { - const appRepos = safeGet(repos, repo.app.name); - // This should never happen... - if (!appRepos || safeGet(appRepos, repo.key) !== repo) { - fatal( - `Database ${repo.app.name}(${repo.repoInfo_}) has already been deleted.` - ); - } - repoInterrupt(repo); - delete appRepos[repo.key]; -} - -/** - * Ensures a repo doesn't already exist and then creates one using the - * provided app. - * - * @param repoInfo The metadata about the Repo - * @return The Repo object for the specified server / repoName. - */ -export function repoManagerCreateRepo( - repoInfo: RepoInfo, - app: FirebaseAppLike, - authTokenProvider: AuthTokenProvider -): Repo { - let appRepos = safeGet(repos, app.name); - - if (!appRepos) { - appRepos = {}; - repos[app.name] = appRepos; - } - - let repo = safeGet(appRepos, repoInfo.toURLString()); - if (repo) { - fatal( - 'Database initialized multiple times. Please make sure the format of the database URL matches with each database() call.' - ); - } - repo = new Repo(repoInfo, useRestClient, app, authTokenProvider); - appRepos[repoInfo.toURLString()] = repo; - - return repo; -} - -/** - * Forces us to use ReadonlyRestClient instead of PersistentConnection for new Repos. - */ -export function repoManagerForceRestClient(forceRestClient: boolean): void { - useRestClient = forceRestClient; -} - /** * Class representing a firebase database. */ -export class Database implements FirebaseService { - /** Track if the instance has been used (root or repo accessed) */ - private instanceStarted_: boolean = false; - - /** Backing state for root_ */ - private rootInternal_?: Reference; - +export class Database implements FirebaseService, Compat { static readonly ServerValue = { TIMESTAMP: { '.sv': 'timestamp' @@ -227,43 +50,12 @@ export class Database implements FirebaseService { /** * The constructor should not be called by users of our public API. */ - constructor(private repoInternal_: Repo) { - if (!(repoInternal_ instanceof Repo)) { - fatal( - "Don't call new Database() directly - please use firebase.database()." - ); - } - } + constructor(readonly _delegate: ExpDatabase, readonly app: FirebaseApp) {} INTERNAL = { - delete: async () => { - this.checkDeleted_('delete'); - repoManagerDeleteRepo(this.repo_); - this.repoInternal_ = null; - this.rootInternal_ = null; - } + delete: () => this._delegate._delete() }; - get repo_(): Repo { - if (!this.instanceStarted_) { - repoStart(this.repoInternal_); - this.instanceStarted_ = true; - } - return this.repoInternal_; - } - - get root_(): Reference { - if (!this.rootInternal_) { - this.rootInternal_ = new Reference(this, newEmptyPath()); - } - - return this.rootInternal_; - } - - get app(): FirebaseApp { - return this.repo_.app as FirebaseApp; - } - /** * Modify this instance to communicate with the Realtime Database emulator. * @@ -273,16 +65,7 @@ export class Database implements FirebaseService { * @param port the emulator port (ex: 8080) */ useEmulator(host: string, port: number): void { - this.checkDeleted_('useEmulator'); - if (this.instanceStarted_) { - fatal( - 'Cannot call useEmulator() after instance has already been initialized.' - ); - return; - } - - // Modify the repo to apply emulator settings - repoManagerApplyEmulatorSettings(this.repoInternal_, host, port); + useDatabaseEmulator(this._delegate, host, port); } /** @@ -298,14 +81,14 @@ export class Database implements FirebaseService { ref(path?: string): Reference; ref(path?: Reference): Reference; ref(path?: string | Reference): Reference { - this.checkDeleted_('ref'); validateArgCount('database.ref', 0, 1, arguments.length); - if (path instanceof Reference) { - return this.refFromURL(path.toString()); + const childRef = refFromURL(this._delegate, path.toString()); + return new Reference(this, childRef._path); + } else { + const childRef = ref(this._delegate, path); + return new Reference(this, childRef._path); } - - return path !== undefined ? this.root_.child(path) : this.root_; } /** @@ -315,48 +98,20 @@ export class Database implements FirebaseService { * @return Firebase reference. */ refFromURL(url: string): Reference { - /** @const {string} */ const apiName = 'database.refFromURL'; - this.checkDeleted_(apiName); validateArgCount(apiName, 1, 1, arguments.length); - const parsedURL = parseRepoInfo(url, this.repo_.repoInfo_.nodeAdmin); - validateUrl(apiName, 1, parsedURL); - - const repoInfo = parsedURL.repoInfo; - if ( - !this.repo_.repoInfo_.isCustomHost() && - repoInfo.host !== this.repo_.repoInfo_.host - ) { - fatal( - apiName + - ': Host name does not match the current database: ' + - '(found ' + - repoInfo.host + - ' but expected ' + - this.repo_.repoInfo_.host + - ')' - ); - } - - return this.ref(parsedURL.path.toString()); - } - - private checkDeleted_(apiName: string) { - if (this.repoInternal_ === null) { - fatal('Cannot call ' + apiName + ' on a deleted database.'); - } + const childRef = refFromURL(this._delegate, url); + return new Reference(this, childRef._path); } // Make individual repo go offline. goOffline(): void { validateArgCount('database.goOffline', 0, 0, arguments.length); - this.checkDeleted_('goOffline'); - repoInterrupt(this.repo_); + return goOffline(this._delegate); } goOnline(): void { validateArgCount('database.goOnline', 0, 0, arguments.length); - this.checkDeleted_('goOnline'); - repoResume(this.repo_); + return goOnline(this._delegate); } } diff --git a/packages/database/src/api/Reference.ts b/packages/database/src/api/Reference.ts index a6a75bf1c0e..b0c194f5255 100644 --- a/packages/database/src/api/Reference.ts +++ b/packages/database/src/api/Reference.ts @@ -249,7 +249,7 @@ export class Query implements Compat { private queryParams_: QueryParams, private orderByCalled_: boolean ) { - this.repo = database.repo_; + this.repo = database._delegate._repo; this._delegate = new QueryImpl( this.repo, path, @@ -430,16 +430,21 @@ export class Query implements Compat { * Get the server-value for this query, or return a cached value if not connected. */ get(): Promise { - return repoGetValue(this.database.repo_, this._delegate).then(node => { - return new DataSnapshot( - this.database, - new ExpDataSnapshot( - node, - new ReferenceImpl(this.getRef().database.repo_, this.getRef().path), - this._delegate._queryParams.getIndex() - ) - ); - }); + return repoGetValue(this.database._delegate._repo, this._delegate).then( + node => { + return new DataSnapshot( + this.database, + new ExpDataSnapshot( + node, + new ReferenceImpl( + this.getRef().database._delegate._repo, + this.getRef().path + ), + this._delegate._queryParams.getIndex() + ) + ); + } + ); } /** @@ -763,7 +768,10 @@ export class Query implements Compat { toString(): string { validateArgCount('Query.toString', 0, 0, arguments.length); - return this.database.repo_.toString() + pathToUrlEncodedString(this.path); + return ( + this.database._delegate._repo.toString() + + pathToUrlEncodedString(this.path) + ); } // Do not create public documentation. This is intended to make JSON serialization work but is otherwise unnecessary @@ -785,7 +793,8 @@ export class Query implements Compat { throw new Error(error); } - const sameRepo = this.database.repo_ === other.database.repo_; + const sameRepo = + this.database._delegate._repo === other.database._delegate._repo; const samePath = pathEquals(this.path, other.path); const sameQueryIdentifier = this._delegate._queryIdentifier === other._delegate._queryIdentifier; @@ -1051,7 +1060,7 @@ export class Reference extends Query implements Compat { this.database, new ExpDataSnapshot( node, - new ReferenceImpl(this.database.repo_, this.path), + new ReferenceImpl(this.database._delegate._repo, this.path), PRIORITY_INDEX ) ); @@ -1071,7 +1080,7 @@ export class Reference extends Query implements Compat { }; repoStartTransaction( - this.database.repo_, + this.database._delegate._repo, this.path, transactionUpdate, promiseComplete, @@ -1093,7 +1102,7 @@ export class Reference extends Query implements Compat { const deferred = new Deferred(); repoSetWithPriority( - this.database.repo_, + this.database._delegate._repo, pathChild(this.path, '.priority'), priority, null, @@ -1108,7 +1117,7 @@ export class Reference extends Query implements Compat { validateFirebaseDataArg('Reference.push', 1, value, this.path, true); validateCallback('Reference.push', 2, onComplete, true); - const now = repoServerTime(this.database.repo_); + const now = repoServerTime(this.database._delegate._repo); const name = nextPushId(now); // push() returns a ThennableReference whose promise is fulfilled with a regular Reference. diff --git a/packages/database/src/api/internal.ts b/packages/database/src/api/internal.ts index d64a2c5318f..4400a0fe23b 100644 --- a/packages/database/src/api/internal.ts +++ b/packages/database/src/api/internal.ts @@ -34,10 +34,11 @@ import { repoStatsIncrementCounter } from '../core/Repo'; import { setSDKVersion } from '../core/version'; +import { repoManagerDatabaseFromApp } from '../exp/Database'; import { BrowserPollConnection } from '../realtime/BrowserPollConnection'; import { WebSocketConnection } from '../realtime/WebSocketConnection'; -import { repoManagerDatabaseFromApp } from './Database'; +import { Database } from './Database'; import { Reference } from './Reference'; /** @@ -129,11 +130,9 @@ export function initStandalone({ ); return { - instance: repoManagerDatabaseFromApp( - app, - authProvider, - url, - nodeAdmin + instance: new Database( + repoManagerDatabaseFromApp(app, authProvider, url, nodeAdmin), + app ) as types.Database, namespace }; diff --git a/packages/database/src/api/test_access.ts b/packages/database/src/api/test_access.ts index bd67b2ab9e5..9e523158e10 100644 --- a/packages/database/src/api/test_access.ts +++ b/packages/database/src/api/test_access.ts @@ -17,9 +17,9 @@ import { PersistentConnection } from '../core/PersistentConnection'; import { RepoInfo } from '../core/RepoInfo'; +import { repoManagerForceRestClient } from '../exp/Database'; import { Connection } from '../realtime/Connection'; -import { repoManagerForceRestClient } from './Database'; import { Query } from './Reference'; export const DataConnection = PersistentConnection; diff --git a/packages/database/src/core/AuthTokenProvider.ts b/packages/database/src/core/AuthTokenProvider.ts index 27912fe9224..1662eea2770 100644 --- a/packages/database/src/core/AuthTokenProvider.ts +++ b/packages/database/src/core/AuthTokenProvider.ts @@ -22,8 +22,6 @@ import { } from '@firebase/auth-interop-types'; import { Provider } from '@firebase/component'; -import { FirebaseAppLike } from '../api/Database'; - import { log, warn } from './util/util'; export interface AuthTokenProvider { @@ -39,7 +37,8 @@ export interface AuthTokenProvider { export class FirebaseAuthTokenProvider implements AuthTokenProvider { private auth_: FirebaseAuthInternal | null = null; constructor( - private app_: FirebaseAppLike, + private appName_: string, + private firebaseOptions_: object, private authProvider_: Provider ) { this.auth_ = authProvider_.getImmediate({ optional: true }); @@ -87,15 +86,15 @@ export class FirebaseAuthTokenProvider implements AuthTokenProvider { notifyForInvalidToken(): void { let errorMessage = 'Provided authentication credentials for the app named "' + - this.app_.name + + this.appName_ + '" are invalid. This usually indicates your app was not ' + 'initialized correctly. '; - if ('credential' in this.app_.options) { + if ('credential' in this.firebaseOptions_) { errorMessage += 'Make sure the "credential" property provided to initializeApp() ' + 'is authorized to access the specified "databaseURL" and is from the correct ' + 'project.'; - } else if ('serviceAccount' in this.app_.options) { + } else if ('serviceAccount' in this.firebaseOptions_) { errorMessage += 'Make sure the "serviceAccount" property provided to initializeApp() ' + 'is authorized to access the specified "databaseURL" and is from the correct ' + diff --git a/packages/database/src/core/Repo.ts b/packages/database/src/core/Repo.ts index bf47d50bb71..ab5ff3f5933 100644 --- a/packages/database/src/core/Repo.ts +++ b/packages/database/src/core/Repo.ts @@ -24,8 +24,6 @@ import { stringify } from '@firebase/util'; -import { FirebaseAppLike } from '../api/Database'; - import { AuthTokenProvider } from './AuthTokenProvider'; import { PersistentConnection } from './PersistentConnection'; import { ReadonlyRestClient } from './ReadonlyRestClient'; @@ -188,7 +186,6 @@ export class Repo { constructor( public repoInfo_: RepoInfo, public forceRestClient_: boolean, - public app: FirebaseAppLike, public authTokenProvider_: AuthTokenProvider ) { // This key is intentionally not updated if RepoInfo is later changed or replaced @@ -205,7 +202,11 @@ export class Repo { } } -export function repoStart(repo: Repo): void { +export function repoStart( + repo: Repo, + appId: string, + authOverride?: object +): void { repo.stats_ = statsManagerGetCollection(repo.repoInfo_); if (repo.forceRestClient_ || beingCrawled()) { @@ -225,7 +226,6 @@ export function repoStart(repo: Repo): void { // Minor hack: Fire onConnect immediately, since there's no actual connection. setTimeout(() => repoOnConnectStatus(repo, /* connectStatus= */ true), 0); } else { - const authOverride = repo.app.options['databaseAuthVariableOverride']; // Validate authOverride if (typeof authOverride !== 'undefined' && authOverride !== null) { if (typeof authOverride !== 'object') { @@ -242,7 +242,7 @@ export function repoStart(repo: Repo): void { repo.persistentConnection_ = new PersistentConnection( repo.repoInfo_, - repo.app.options.appId, + appId, ( pathString: string, data: unknown, diff --git a/packages/database/src/exp/Database.ts b/packages/database/src/exp/Database.ts index a53dc7b4cef..f9fd114b1c5 100644 --- a/packages/database/src/exp/Database.ts +++ b/packages/database/src/exp/Database.ts @@ -19,25 +19,225 @@ import { _FirebaseService, _getProvider, FirebaseApp } from '@firebase/app-exp'; import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { Provider } from '@firebase/component'; +import { getModularInstance } from '@firebase/util'; -import { Repo } from '../core/Repo'; +import { + AuthTokenProvider, + EmulatorAdminTokenProvider, + FirebaseAuthTokenProvider +} from '../core/AuthTokenProvider'; +import { Repo, repoInterrupt, repoResume, repoStart } from '../core/Repo'; +import { RepoInfo } from '../core/RepoInfo'; +import { parseRepoInfo } from '../core/util/libs/parser'; +import { newEmptyPath, pathIsEmpty } from '../core/util/Path'; +import { fatal, log } from '../core/util/util'; +import { validateUrl } from '../core/util/validation'; + +import { Reference } from './Reference'; +import { ReferenceImpl } from './Reference_impl'; + +/** + * This variable is also defined in the firebase node.js admin SDK. Before + * modifying this definition, consult the definition in: + * + * https://github.com/firebase/firebase-admin-node + * + * and make sure the two are consistent. + */ +const FIREBASE_DATABASE_EMULATOR_HOST_VAR = 'FIREBASE_DATABASE_EMULATOR_HOST'; + +/** + * Creates and caches Repo instances. + */ +const repos: { + [appName: string]: { + [dbUrl: string]: Repo; + }; +} = {}; + +/** + * If true, new Repos will be created to use ReadonlyRestClient (for testing purposes). + */ +let useRestClient = false; + +/** + * Update an existing repo in place to point to a new host/port. + */ +function repoManagerApplyEmulatorSettings( + repo: Repo, + host: string, + port: number +): void { + repo.repoInfo_ = new RepoInfo( + `${host}:${port}`, + /* secure= */ false, + repo.repoInfo_.namespace, + repo.repoInfo_.webSocketOnly, + repo.repoInfo_.nodeAdmin, + repo.repoInfo_.persistenceKey, + repo.repoInfo_.includeNamespaceInQueryParams + ); + + if (repo.repoInfo_.nodeAdmin) { + repo.authTokenProvider_ = new EmulatorAdminTokenProvider(); + } +} + +/** + * This function should only ever be called to CREATE a new database instance. + */ +export function repoManagerDatabaseFromApp( + app: FirebaseApp, + authProvider: Provider, + url?: string, + nodeAdmin?: boolean +): FirebaseDatabase { + let dbUrl: string | undefined = url || app.options.databaseURL; + if (dbUrl === undefined) { + if (!app.options.projectId) { + fatal( + "Can't determine Firebase Database URL. Be sure to include " + + ' a Project ID when calling firebase.initializeApp().' + ); + } + + log('Using default host for project ', app.options.projectId); + dbUrl = `${app.options.projectId}-default-rtdb.firebaseio.com`; + } + + let parsedUrl = parseRepoInfo(dbUrl, nodeAdmin); + let repoInfo = parsedUrl.repoInfo; + + let isEmulator: boolean; + + let dbEmulatorHost: string | undefined = undefined; + if (typeof process !== 'undefined') { + dbEmulatorHost = process.env[FIREBASE_DATABASE_EMULATOR_HOST_VAR]; + } + + if (dbEmulatorHost) { + isEmulator = true; + dbUrl = `http://${dbEmulatorHost}?ns=${repoInfo.namespace}`; + parsedUrl = parseRepoInfo(dbUrl, nodeAdmin); + repoInfo = parsedUrl.repoInfo; + } else { + isEmulator = !parsedUrl.repoInfo.secure; + } + + const authTokenProvider = + nodeAdmin && isEmulator + ? new EmulatorAdminTokenProvider() + : new FirebaseAuthTokenProvider(app.name, app.options, authProvider); + + validateUrl('Invalid Firebase Database URL', 1, parsedUrl); + if (!pathIsEmpty(parsedUrl.path)) { + fatal( + 'Database URL must point to the root of a Firebase Database ' + + '(not including a child path).' + ); + } + + const repo = repoManagerCreateRepo(repoInfo, app, authTokenProvider); + return new FirebaseDatabase(repo, app); +} + +/** + * Remove the repo and make sure it is disconnected. + * + */ +function repoManagerDeleteRepo(repo: Repo, appName: string): void { + const appRepos = repos[appName]; + // This should never happen... + if (!appRepos || appRepos[repo.key] !== repo) { + fatal(`Database ${appName}(${repo.repoInfo_}) has already been deleted.`); + } + repoInterrupt(repo); + delete appRepos[repo.key]; +} + +/** + * Ensures a repo doesn't already exist and then creates one using the + * provided app. + * + * @param repoInfo The metadata about the Repo + * @return The Repo object for the specified server / repoName. + */ +function repoManagerCreateRepo( + repoInfo: RepoInfo, + app: FirebaseApp, + authTokenProvider: AuthTokenProvider +): Repo { + let appRepos = repos[app.name]; + + if (!appRepos) { + appRepos = {}; + repos[app.name] = appRepos; + } + + let repo = appRepos[repoInfo.toURLString()]; + if (repo) { + fatal( + 'Database initialized multiple times. Please make sure the format of the database URL matches with each database() call.' + ); + } + repo = new Repo(repoInfo, useRestClient, authTokenProvider); + appRepos[repoInfo.toURLString()] = repo; + + return repo; +} + +/** + * Forces us to use ReadonlyRestClient instead of PersistentConnection for new Repos. + */ +export function repoManagerForceRestClient(forceRestClient: boolean): void { + useRestClient = forceRestClient; +} /** * Class representing a Firebase Realtime Database. */ export class FirebaseDatabase implements _FirebaseService { readonly 'type' = 'database'; - _repo: Repo; - constructor( - readonly app: FirebaseApp, - authProvider: Provider, - databaseUrl?: string - ) {} + /** Track if the instance has been used (root or repo accessed) */ + _instanceStarted: boolean = false; + + /** Backing state for root_ */ + private _rootInternal?: Reference; + + constructor(private _repoInternal: Repo, readonly app: FirebaseApp) {} + + get _repo(): Repo { + if (!this._instanceStarted) { + repoStart( + this._repoInternal, + this.app.options.appId, + this.app.options['databaseAuthVariableOverride'] + ); + this._instanceStarted = true; + } + return this._repoInternal; + } + + get _root(): Reference { + if (!this._rootInternal) { + this._rootInternal = new ReferenceImpl(this._repo, newEmptyPath()); + } + return this._rootInternal; + } _delete(): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + this._checkNotDeleted('delete'); + repoManagerDeleteRepo(this._repo, this.app.name); + this._repoInternal = null; + this._rootInternal = null; + return Promise.resolve(); + } + + _checkNotDeleted(apiName: string) { + if (this._rootInternal === null) { + fatal('Cannot call ' + apiName + ' on a deleted database.'); + } } } @@ -64,18 +264,27 @@ export function useDatabaseEmulator( host: string, port: number ): void { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + db = getModularInstance(db); + db._checkNotDeleted('useEmulator'); + if (db._instanceStarted) { + fatal( + 'Cannot call useEmulator() after instance has already been initialized.' + ); + } + // Modify the repo to apply emulator settings + repoManagerApplyEmulatorSettings(db._repo, host, port); } export function goOffline(db: FirebaseDatabase): void { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + db = getModularInstance(db); + db._checkNotDeleted('goOffline'); + repoInterrupt(db._repo); } export function goOnline(db: FirebaseDatabase): void { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + db = getModularInstance(db); + db._checkNotDeleted('goOnline'); + repoResume(db._repo); } export function enableLogging( diff --git a/packages/database/src/exp/Reference_impl.ts b/packages/database/src/exp/Reference_impl.ts index 242324a81a3..2064d74ca6d 100644 --- a/packages/database/src/exp/Reference_impl.ts +++ b/packages/database/src/exp/Reference_impl.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { assert, contains } from '@firebase/util'; +import { assert, contains, getModularInstance } from '@firebase/util'; import { Repo, @@ -28,6 +28,7 @@ import { PRIORITY_INDEX } from '../core/snap/indexes/PriorityIndex'; import { Node } from '../core/snap/Node'; import { syncPointSetReferenceConstructor } from '../core/SyncPoint'; import { syncTreeSetReferenceConstructor } from '../core/SyncTree'; +import { parseRepoInfo } from '../core/util/libs/parser'; import { Path, pathChild, @@ -35,7 +36,8 @@ import { pathIsEmpty, pathParent } from '../core/util/Path'; -import { ObjectToUniqueKey } from '../core/util/util'; +import { fatal, ObjectToUniqueKey } from '../core/util/util'; +import { validateUrl } from '../core/util/validation'; import { Change } from '../core/view/Change'; import { CancelEvent, DataEvent, EventType } from '../core/view/Event'; import { @@ -209,12 +211,34 @@ export class DataSnapshot { } export function ref(db: FirebaseDatabase, path?: string): Reference { - return new ReferenceImpl(db._repo, new Path(path || '')); + db = getModularInstance(db); + db._checkNotDeleted('ref'); + return path !== undefined ? child(db._root, path) : db._root; } export function refFromURL(db: FirebaseDatabase, url: string): Reference { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + db = getModularInstance(db); + db._checkNotDeleted('refFromURL'); + const parsedURL = parseRepoInfo(url, db._repo.repoInfo_.nodeAdmin); + validateUrl('refFromURL', 1, parsedURL); + + const repoInfo = parsedURL.repoInfo; + if ( + !db._repo.repoInfo_.isCustomHost() && + repoInfo.host !== db._repo.repoInfo_.host + ) { + fatal( + 'refFromURL' + + ': Host name does not match the current database: ' + + '(found ' + + repoInfo.host + + ' but expected ' + + db._repo.repoInfo_.host + + ')' + ); + } + + return ref(db, parsedURL.path.toString()); } export function child(ref: Reference, path: string): Reference { diff --git a/packages/database/test/database.test.ts b/packages/database/test/database.test.ts index c909eccfee9..e89d98e373f 100644 --- a/packages/database/test/database.test.ts +++ b/packages/database/test/database.test.ts @@ -38,12 +38,6 @@ describe('Database Tests', () => { expect(db).not.to.be.null; }); - it('Illegal to call constructor', () => { - expect(() => { - const db = new (firebase as any).database.Database('url'); - }).to.throw(/don't call new Database/i); - }); - it('Can get database with custom URL', () => { const db = defaultApp.database('http://foo.bar.com'); expect(db).to.be.ok; @@ -66,7 +60,7 @@ describe('Database Tests', () => { it('Can get database with multi-region URL', () => { const db = defaultApp.database('http://foo.euw1.firebasedatabase.app'); expect(db).to.be.ok; - expect(db.repo_.repoInfo_.namespace).to.equal('foo'); + expect(db._delegate._repo.repoInfo_.namespace).to.equal('foo'); expect(db.ref().toString()).to.equal( 'https://foo.euw1.firebasedatabase.app/' ); @@ -75,7 +69,7 @@ describe('Database Tests', () => { it('Can get database with upper case URL', () => { const db = defaultApp.database('http://fOO.EUW1.firebaseDATABASE.app'); expect(db).to.be.ok; - expect(db.repo_.repoInfo_.namespace).to.equal('foo'); + expect(db._delegate._repo.repoInfo_.namespace).to.equal('foo'); expect(db.ref().toString()).to.equal( 'https://foo.euw1.firebasedatabase.app/' ); @@ -102,7 +96,7 @@ describe('Database Tests', () => { it('Can get database with a upper case localhost URL and ns', () => { const db = defaultApp.database('http://LOCALHOST?ns=foo'); expect(db).to.be.ok; - expect(db.repo_.repoInfo_.namespace).to.equal('foo'); + expect(db._delegate._repo.repoInfo_.namespace).to.equal('foo'); expect(db.ref().toString()).to.equal('https://localhost/'); }); @@ -123,14 +117,14 @@ describe('Database Tests', () => { it('Can read ns query param', () => { const db = defaultApp.database('http://localhost:80/?ns=foo&unused=true'); expect(db).to.be.ok; - expect(db.repo_.repoInfo_.namespace).to.equal('foo'); + expect(db._delegate._repo.repoInfo_.namespace).to.equal('foo'); expect(db.ref().toString()).to.equal('http://localhost:80/'); }); it('Reads ns query param even when subdomain is set', () => { const db = defaultApp.database('http://bar.firebaseio.com?ns=foo'); expect(db).to.be.ok; - expect(db.repo_.repoInfo_.namespace).to.equal('foo'); + expect(db._delegate._repo.repoInfo_.namespace).to.equal('foo'); expect(db.ref().toString()).to.equal('https://bar.firebaseio.com/'); }); @@ -138,8 +132,8 @@ describe('Database Tests', () => { process.env['FIREBASE_DATABASE_EMULATOR_HOST'] = 'localhost:9000'; const db = defaultApp.database('https://bar.firebaseio.com'); expect(db).to.be.ok; - expect(db.repo_.repoInfo_.namespace).to.equal('bar'); - expect(db.repo_.repoInfo_.host).to.equal('localhost:9000'); + expect(db._delegate._repo.repoInfo_.namespace).to.equal('bar'); + expect(db._delegate._repo.repoInfo_.host).to.equal('localhost:9000'); delete process.env['FIREBASE_DATABASE_EMULATOR_HOST']; }); @@ -154,10 +148,10 @@ describe('Database Tests', () => { process.env['FIREBASE_DATABASE_EMULATOR_HOST'] = 'localhost:9000'; const db1 = defaultApp.database('http://foo1.bar.com'); const db2 = defaultApp.database('http://foo2.bar.com'); - expect(db1.repo_.repoInfo_.toURLString()).to.equal( + expect(db1._delegate._repo.repoInfo_.toURLString()).to.equal( 'http://localhost:9000/?ns=foo1' ); - expect(db2.repo_.repoInfo_.toURLString()).to.equal( + expect(db2._delegate._repo.repoInfo_.toURLString()).to.equal( 'http://localhost:9000/?ns=foo2' ); delete process.env['FIREBASE_DATABASE_EMULATOR_HOST']; From cae7ea537d519276b7482185d9b283140eaf7c12 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 2 Apr 2021 15:37:19 -0600 Subject: [PATCH 05/15] Implement Query Compat and query@exp (#4709) --- packages/database/src/api/Reference.ts | 559 +++++------------- packages/database/src/api/internal.ts | 11 +- packages/database/src/exp/Reference.ts | 2 +- packages/database/src/exp/Reference_impl.ts | 595 ++++++++++++++++++-- packages/database/test/query.test.ts | 16 +- packages/firestore/src/lite/query.ts | 4 +- 6 files changed, 697 insertions(+), 490 deletions(-) diff --git a/packages/database/src/api/Reference.ts b/packages/database/src/api/Reference.ts index b0c194f5255..a7f0bee7eb5 100644 --- a/packages/database/src/api/Reference.ts +++ b/packages/database/src/api/Reference.ts @@ -16,7 +16,6 @@ */ import { - assert, Compat, Deferred, errorPrefix, @@ -26,53 +25,35 @@ import { } from '@firebase/util'; import { - Repo, - repoGetValue, repoServerTime, repoSetWithPriority, repoStartTransaction, repoUpdate } from '../core/Repo'; -import { KEY_INDEX } from '../core/snap/indexes/KeyIndex'; -import { PathIndex } from '../core/snap/indexes/PathIndex'; import { PRIORITY_INDEX } from '../core/snap/indexes/PriorityIndex'; -import { VALUE_INDEX } from '../core/snap/indexes/ValueIndex'; import { Node } from '../core/snap/Node'; import { nextPushId } from '../core/util/NextPushId'; import { Path, pathChild, - pathEquals, pathGetBack, pathGetFront, pathIsEmpty, - pathParent, - pathToUrlEncodedString + pathParent } from '../core/util/Path'; -import { MAX_NAME, MIN_NAME, warn } from '../core/util/util'; +import { warn } from '../core/util/util'; import { - isValidPriority, validateBoolean, validateEventType, validateFirebaseDataArg, validateFirebaseMergeDataArg, - validateKey, validatePathString, validatePriority, validateRootPathString, validateWritablePath } from '../core/util/validation'; import { UserCallback } from '../core/view/EventRegistration'; -import { - QueryParams, - queryParamsEndAt, - queryParamsEndBefore, - queryParamsLimitToFirst, - queryParamsLimitToLast, - queryParamsOrderBy, - queryParamsStartAfter, - queryParamsStartAt -} from '../core/view/QueryParams'; +import { QueryParams } from '../core/view/QueryParams'; import { DataSnapshot as ExpDataSnapshot, off, @@ -83,7 +64,20 @@ import { onValue, QueryImpl, ReferenceImpl, - EventType + EventType, + limitToFirst, + query, + limitToLast, + orderByChild, + orderByKey, + orderByValue, + orderByPriority, + startAt, + startAfter, + endAt, + endBefore, + equalTo, + get } from '../exp/Reference_impl'; import { Database } from './Database'; @@ -240,122 +234,7 @@ export interface SnapshotCallback { * Since every Firebase reference is a query, Firebase inherits from this object. */ export class Query implements Compat { - readonly _delegate: QueryImpl; - readonly repo: Repo; - - constructor( - public database: Database, - public path: Path, - private queryParams_: QueryParams, - private orderByCalled_: boolean - ) { - this.repo = database._delegate._repo; - this._delegate = new QueryImpl( - this.repo, - path, - queryParams_, - orderByCalled_ - ); - } - - /** - * Validates start/end values for queries. - */ - private static validateQueryEndpoints_(params: QueryParams) { - let startNode = null; - let endNode = null; - if (params.hasStart()) { - startNode = params.getIndexStartValue(); - } - if (params.hasEnd()) { - endNode = params.getIndexEndValue(); - } - - if (params.getIndex() === KEY_INDEX) { - 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(), startAfter(), ' + - 'endAt(), endBefore(), or equalTo() must be a string.'; - if (params.hasStart()) { - const startName = params.getIndexStartName(); - if (startName !== MIN_NAME) { - throw new Error(tooManyArgsError); - } else if (typeof startNode !== 'string') { - throw new Error(wrongArgTypeError); - } - } - if (params.hasEnd()) { - const endName = params.getIndexEndName(); - if (endName !== MAX_NAME) { - throw new Error(tooManyArgsError); - } else if (typeof endNode !== 'string') { - throw new Error(wrongArgTypeError); - } - } - } else if (params.getIndex() === PRIORITY_INDEX) { - if ( - (startNode != null && !isValidPriority(startNode)) || - (endNode != null && !isValidPriority(endNode)) - ) { - throw new Error( - 'Query: When ordering by priority, the first argument passed to startAt(), ' + - 'startAfter() endAt(), endBefore(), 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.' - ); - if ( - (startNode != null && typeof startNode === 'object') || - (endNode != null && typeof endNode === 'object') - ) { - throw new Error( - 'Query: First argument passed to startAt(), startAfter(), endAt(), endBefore(), or ' + - 'equalTo() cannot be an object.' - ); - } - } - } - - /** - * Validates that limit* has been called with the correct combination of parameters - */ - private static validateLimit_(params: QueryParams) { - if ( - params.hasStart() && - params.hasEnd() && - params.hasLimit() && - !params.hasAnchoredLimit() - ) { - throw new Error( - "Query: Can't combine startAt(), startAfter(), endAt(), endBefore(), and limit(). Use " + - 'limitToFirst() or limitToLast() instead.' - ); - } - } - - /** - * Validates that no other order by call has been made - */ - private validateNoPreviousOrderByCall_(fnName: string) { - if (this.orderByCalled_ === true) { - throw new Error(fnName + ": You can't combine multiple orderBy calls."); - } - } - - 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 Reference(this.database, this.path); - } + constructor(readonly database: Database, readonly _delegate: QueryImpl) {} on( eventType: string, @@ -430,21 +309,9 @@ export class Query implements Compat { * Get the server-value for this query, or return a cached value if not connected. */ get(): Promise { - return repoGetValue(this.database._delegate._repo, this._delegate).then( - node => { - return new DataSnapshot( - this.database, - new ExpDataSnapshot( - node, - new ReferenceImpl( - this.getRef().database._delegate._repo, - this.getRef().path - ), - this._delegate._queryParams.getIndex() - ) - ); - } - ); + return get(this._delegate).then(expSnapshot => { + return new DataSnapshot(this.database, expSnapshot); + }); } /** @@ -457,51 +324,64 @@ export class Query implements Compat { context?: object | null ): Promise { validateArgCount('Query.once', 1, 4, arguments.length); - validateEventType('Query.once', 1, eventType, false); validateCallback('Query.once', 2, userCallback, true); const ret = Query.getCancelAndContextArgs_( - 'Query.once', + 'Query.on', failureCallbackOrContext, 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. - let firstCall = true; const deferred = new Deferred(); - - // A dummy error handler in case a user wasn't expecting promises - deferred.promise.catch(() => {}); - - const onceCallback = (snapshot: DataSnapshot) => { - // 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; - this.off(eventType, onceCallback); - - if (userCallback) { - userCallback.bind(ret.context)(snapshot); - } - deferred.resolve(snapshot); + const valueCallback: UserCallback = (expSnapshot, previousChildName?) => { + const result = new DataSnapshot(this.database, expSnapshot); + if (userCallback) { + userCallback.call(ret.context, result, previousChildName); } + deferred.resolve(result); + }; + valueCallback.userCallback = userCallback; + valueCallback.context = ret.context; + const cancelCallback = (error: Error) => { + if (ret.cancel) { + ret.cancel.call(ret.context, error); + } + deferred.reject(error); }; - this.on( - eventType, - onceCallback, - /*cancel=*/ err => { - this.off(eventType, onceCallback); + switch (eventType) { + case 'value': + onValue(this._delegate, valueCallback, cancelCallback, { + onlyOnce: true + }); + break; + case 'child_added': + onChildAdded(this._delegate, valueCallback, cancelCallback, { + onlyOnce: true + }); + break; + case 'child_removed': + onChildRemoved(this._delegate, valueCallback, cancelCallback, { + onlyOnce: true + }); + break; + case 'child_changed': + onChildChanged(this._delegate, valueCallback, cancelCallback, { + onlyOnce: true + }); + break; + case 'child_moved': + onChildMoved(this._delegate, valueCallback, cancelCallback, { + onlyOnce: true + }); + break; + default: + throw new Error( + errorPrefix('Query.once', 1, false) + + 'must be a valid event type = "value", "child_added", "child_removed", ' + + '"child_changed", or "child_moved".' + ); + } - if (ret.cancel) { - ret.cancel.bind(ret.context)(err); - } - deferred.reject(err); - } - ); return deferred.promise; } @@ -510,28 +390,7 @@ export class Query implements Compat { */ 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).' - ); - } - - return new Query( - this.database, - this.path, - queryParamsLimitToFirst(this.queryParams_, limit), - this.orderByCalled_ - ); + return new Query(this.database, query(this._delegate, limitToFirst(limit))); } /** @@ -539,28 +398,7 @@ export class Query implements Compat { */ 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).' - ); - } - - return new Query( - this.database, - this.path, - queryParamsLimitToLast(this.queryParams_, limit), - this.orderByCalled_ - ); + return new Query(this.database, query(this._delegate, limitToLast(limit))); } /** @@ -568,37 +406,7 @@ export class Query implements Compat { */ 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.' - ); - } else if (path === '$priority') { - throw new Error( - 'Query.orderByChild: "$priority" is invalid. Use Query.orderByPriority() instead.' - ); - } else if (path === '$value') { - throw new Error( - 'Query.orderByChild: "$value" is invalid. Use Query.orderByValue() instead.' - ); - } - validatePathString('Query.orderByChild', 1, path, false); - this.validateNoPreviousOrderByCall_('Query.orderByChild'); - const parsedPath = new Path(path); - if (pathIsEmpty(parsedPath)) { - throw new Error( - 'Query.orderByChild: cannot pass in empty path. Use Query.orderByValue() instead.' - ); - } - const index = new PathIndex(parsedPath); - const newParams = queryParamsOrderBy(this.queryParams_, index); - Query.validateQueryEndpoints_(newParams); - - return new Query( - this.database, - this.path, - newParams, - /*orderByCalled=*/ true - ); + return new Query(this.database, query(this._delegate, orderByChild(path))); } /** @@ -606,15 +414,7 @@ export class Query implements Compat { */ orderByKey(): Query { validateArgCount('Query.orderByKey', 0, 0, arguments.length); - this.validateNoPreviousOrderByCall_('Query.orderByKey'); - const newParams = queryParamsOrderBy(this.queryParams_, KEY_INDEX); - Query.validateQueryEndpoints_(newParams); - return new Query( - this.database, - this.path, - newParams, - /*orderByCalled=*/ true - ); + return new Query(this.database, query(this._delegate, orderByKey())); } /** @@ -622,15 +422,7 @@ export class Query implements Compat { */ orderByPriority(): Query { validateArgCount('Query.orderByPriority', 0, 0, arguments.length); - this.validateNoPreviousOrderByCall_('Query.orderByPriority'); - const newParams = queryParamsOrderBy(this.queryParams_, PRIORITY_INDEX); - Query.validateQueryEndpoints_(newParams); - return new Query( - this.database, - this.path, - newParams, - /*orderByCalled=*/ true - ); + return new Query(this.database, query(this._delegate, orderByPriority())); } /** @@ -638,15 +430,7 @@ export class Query implements Compat { */ orderByValue(): Query { validateArgCount('Query.orderByValue', 0, 0, arguments.length); - this.validateNoPreviousOrderByCall_('Query.orderByValue'); - const newParams = queryParamsOrderBy(this.queryParams_, VALUE_INDEX); - Query.validateQueryEndpoints_(newParams); - return new Query( - this.database, - this.path, - newParams, - /*orderByCalled=*/ true - ); + return new Query(this.database, query(this._delegate, orderByValue())); } startAt( @@ -654,26 +438,10 @@ export class Query implements Compat { 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); - - const newParams = queryParamsStartAt(this.queryParams_, 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).' - ); - } - - // Calling with no params tells us to start at the beginning. - if (value === undefined) { - value = null; - name = null; - } - - return new Query(this.database, this.path, newParams, this.orderByCalled_); + return new Query( + this.database, + query(this._delegate, startAt(value, name)) + ); } startAfter( @@ -681,20 +449,10 @@ export class Query implements Compat { name?: string | null ): Query { validateArgCount('Query.startAfter', 0, 2, arguments.length); - validateFirebaseDataArg('Query.startAfter', 1, value, this.path, false); - validateKey('Query.startAfter', 2, name, true); - - const newParams = queryParamsStartAfter(this.queryParams_, value, name); - Query.validateLimit_(newParams); - Query.validateQueryEndpoints_(newParams); - if (this.queryParams_.hasStart()) { - throw new Error( - 'Query.startAfter: Starting point was already set (by another call to startAt, startAfter ' + - 'or equalTo).' - ); - } - - return new Query(this.database, this.path, newParams, this.orderByCalled_); + return new Query( + this.database, + query(this._delegate, startAfter(value, name)) + ); } endAt( @@ -702,20 +460,7 @@ export class Query implements Compat { 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); - - const newParams = queryParamsEndAt(this.queryParams_, 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, endBefore, or ' + - 'equalTo).' - ); - } - - return new Query(this.database, this.path, newParams, this.orderByCalled_); + return new Query(this.database, query(this._delegate, endAt(value, name))); } endBefore( @@ -723,20 +468,10 @@ export class Query implements Compat { name?: string | null ): Query { validateArgCount('Query.endBefore', 0, 2, arguments.length); - validateFirebaseDataArg('Query.endBefore', 1, value, this.path, false); - validateKey('Query.endBefore', 2, name, true); - - const newParams = queryParamsEndBefore(this.queryParams_, value, name); - Query.validateLimit_(newParams); - Query.validateQueryEndpoints_(newParams); - if (this.queryParams_.hasEnd()) { - throw new Error( - 'Query.endBefore: Ending point was already set (by another call to endAt, endBefore, or ' + - 'equalTo).' - ); - } - - return new Query(this.database, this.path, newParams, this.orderByCalled_); + return new Query( + this.database, + query(this._delegate, endBefore(value, name)) + ); } /** @@ -745,21 +480,10 @@ export class Query implements Compat { */ 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/startAfter or ' + - 'equalTo).' - ); - } - if (this.queryParams_.hasEnd()) { - throw new Error( - 'Query.equalTo: Ending point was already set (by another call to endAt/endBefore or ' + - 'equalTo).' - ); - } - return this.startAt(value, name).endAt(value, name); + return new Query( + this.database, + query(this._delegate, equalTo(value, name)) + ); } /** @@ -767,11 +491,7 @@ export class Query implements Compat { */ toString(): string { validateArgCount('Query.toString', 0, 0, arguments.length); - - return ( - this.database._delegate._repo.toString() + - pathToUrlEncodedString(this.path) - ); + return this._delegate.toString(); } // Do not create public documentation. This is intended to make JSON serialization work but is otherwise unnecessary @@ -779,7 +499,7 @@ export class Query implements Compat { toJSON() { // An optional spacer argument is unnecessary for a string. validateArgCount('Query.toJSON', 0, 1, arguments.length); - return this.toString(); + return this._delegate.toJSON(); } /** @@ -792,14 +512,7 @@ export class Query implements Compat { 'Query.isEqual failed: First argument must be an instance of firebase.database.Query.'; throw new Error(error); } - - const sameRepo = - this.database._delegate._repo === other.database._delegate._repo; - const samePath = pathEquals(this.path, other.path); - const sameQueryIdentifier = - this._delegate._queryIdentifier === other._delegate._queryIdentifier; - - return sameRepo && samePath && sameQueryIdentifier; + return this._delegate.isEqual(other._delegate); } /** @@ -840,7 +553,7 @@ export class Query implements Compat { } get ref(): Reference { - return this.getRef(); + return new Reference(this.database, this._delegate._path); } } @@ -858,17 +571,20 @@ export class Reference extends Query implements Compat { * Externally - this is the firebase.database.Reference type. */ constructor(database: Database, path: Path) { - super(database, path, new QueryParams(), false); + super( + database, + new QueryImpl(database._delegate._repo, path, new QueryParams(), false) + ); } /** @return {?string} */ getKey(): string | null { validateArgCount('Reference.key', 0, 0, arguments.length); - if (pathIsEmpty(this.path)) { + if (pathIsEmpty(this._delegate._path)) { return null; } else { - return pathGetBack(this.path); + return pathGetBack(this._delegate._path); } } @@ -877,21 +593,24 @@ export class Reference extends Query implements Compat { if (typeof pathString === 'number') { pathString = String(pathString); } else if (!(pathString instanceof Path)) { - if (pathGetFront(this.path) === null) { + if (pathGetFront(this._delegate._path) === null) { validateRootPathString('Reference.child', 1, pathString, false); } else { validatePathString('Reference.child', 1, pathString, false); } } - return new Reference(this.database, pathChild(this.path, pathString)); + return new Reference( + this.database, + pathChild(this._delegate._path, pathString) + ); } /** @return {?Reference} */ getParent(): Reference | null { validateArgCount('Reference.parent', 0, 0, arguments.length); - const parentPath = pathParent(this.path); + const parentPath = pathParent(this._delegate._path); return parentPath === null ? null : new Reference(this.database, parentPath); @@ -913,14 +632,20 @@ export class Reference extends Query implements Compat { onComplete?: (a: Error | null) => void ): Promise { validateArgCount('Reference.set', 1, 2, arguments.length); - validateWritablePath('Reference.set', this.path); - validateFirebaseDataArg('Reference.set', 1, newVal, this.path, false); + validateWritablePath('Reference.set', this._delegate._path); + validateFirebaseDataArg( + 'Reference.set', + 1, + newVal, + this._delegate._path, + false + ); validateCallback('Reference.set', 2, onComplete, true); const deferred = new Deferred(); repoSetWithPriority( - this.repo, - this.path, + this._delegate._repo, + this._delegate._path, newVal, /*priority=*/ null, deferred.wrapCallback(onComplete) @@ -933,7 +658,7 @@ export class Reference extends Query implements Compat { onComplete?: (a: Error | null) => void ): Promise { validateArgCount('Reference.update', 1, 2, arguments.length); - validateWritablePath('Reference.update', this.path); + validateWritablePath('Reference.update', this._delegate._path); if (Array.isArray(objectToMerge)) { const newObjectToMerge: { [k: string]: unknown } = {}; @@ -952,14 +677,14 @@ export class Reference extends Query implements Compat { 'Reference.update', 1, objectToMerge, - this.path, + this._delegate._path, false ); validateCallback('Reference.update', 2, onComplete, true); const deferred = new Deferred(); repoUpdate( - this.repo, - this.path, + this._delegate._repo, + this._delegate._path, objectToMerge as { [k: string]: unknown }, deferred.wrapCallback(onComplete) ); @@ -972,12 +697,12 @@ export class Reference extends Query implements Compat { onComplete?: (a: Error | null) => void ): Promise { validateArgCount('Reference.setWithPriority', 2, 3, arguments.length); - validateWritablePath('Reference.setWithPriority', this.path); + validateWritablePath('Reference.setWithPriority', this._delegate._path); validateFirebaseDataArg( 'Reference.setWithPriority', 1, newVal, - this.path, + this._delegate._path, false ); validatePriority('Reference.setWithPriority', 2, newPriority, false); @@ -993,8 +718,8 @@ export class Reference extends Query implements Compat { const deferred = new Deferred(); repoSetWithPriority( - this.repo, - this.path, + this._delegate._repo, + this._delegate._path, newVal, newPriority, deferred.wrapCallback(onComplete) @@ -1004,7 +729,7 @@ export class Reference extends Query implements Compat { remove(onComplete?: (a: Error | null) => void): Promise { validateArgCount('Reference.remove', 0, 1, arguments.length); - validateWritablePath('Reference.remove', this.path); + validateWritablePath('Reference.remove', this._delegate._path); validateCallback('Reference.remove', 1, onComplete, true); return this.set(null, onComplete); @@ -1020,7 +745,7 @@ export class Reference extends Query implements Compat { applyLocally?: boolean ): Promise { validateArgCount('Reference.transaction', 1, 3, arguments.length); - validateWritablePath('Reference.transaction', this.path); + validateWritablePath('Reference.transaction', this._delegate._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 @@ -1060,7 +785,7 @@ export class Reference extends Query implements Compat { this.database, new ExpDataSnapshot( node, - new ReferenceImpl(this.database._delegate._repo, this.path), + new ReferenceImpl(this._delegate._repo, this._delegate._path), PRIORITY_INDEX ) ); @@ -1073,15 +798,15 @@ export class Reference extends Query implements Compat { // Add a watch to make sure we get server updates. const valueCallback = function () {}; - const watchRef = new Reference(this.database, this.path); + const watchRef = new Reference(this.database, this._delegate._path); watchRef.on('value', valueCallback); const unwatcher = function () { watchRef.off('value', valueCallback); }; repoStartTransaction( - this.database._delegate._repo, - this.path, + this._delegate._repo, + this._delegate._path, transactionUpdate, promiseComplete, unwatcher, @@ -1096,14 +821,14 @@ export class Reference extends Query implements Compat { onComplete?: (a: Error | null) => void ): Promise { validateArgCount('Reference.setPriority', 1, 2, arguments.length); - validateWritablePath('Reference.setPriority', this.path); + validateWritablePath('Reference.setPriority', this._delegate._path); validatePriority('Reference.setPriority', 1, priority, false); validateCallback('Reference.setPriority', 2, onComplete, true); const deferred = new Deferred(); repoSetWithPriority( - this.database._delegate._repo, - pathChild(this.path, '.priority'), + this._delegate._repo, + pathChild(this._delegate._path, '.priority'), priority, null, deferred.wrapCallback(onComplete) @@ -1113,11 +838,17 @@ export class Reference extends Query implements Compat { push(value?: unknown, onComplete?: (a: Error | null) => void): Reference { validateArgCount('Reference.push', 0, 2, arguments.length); - validateWritablePath('Reference.push', this.path); - validateFirebaseDataArg('Reference.push', 1, value, this.path, true); + validateWritablePath('Reference.push', this._delegate._path); + validateFirebaseDataArg( + 'Reference.push', + 1, + value, + this._delegate._path, + true + ); validateCallback('Reference.push', 2, onComplete, true); - const now = repoServerTime(this.database._delegate._repo); + const now = repoServerTime(this._delegate._repo); const name = nextPushId(now); // push() returns a ThennableReference whose promise is fulfilled with a regular Reference. @@ -1146,8 +877,8 @@ export class Reference extends Query implements Compat { } onDisconnect(): OnDisconnect { - validateWritablePath('Reference.onDisconnect', this.path); - return new OnDisconnect(this.repo, this.path); + validateWritablePath('Reference.onDisconnect', this._delegate._path); + return new OnDisconnect(this._delegate._repo, this._delegate._path); } get key(): string | null { diff --git a/packages/database/src/api/internal.ts b/packages/database/src/api/internal.ts index 4400a0fe23b..773bf0fd0a2 100644 --- a/packages/database/src/api/internal.ts +++ b/packages/database/src/api/internal.ts @@ -65,27 +65,28 @@ export const setSecurityDebugCallback = function ( ref: Reference, callback: (a: object) => void ) { + const connection = ref._delegate._repo.persistentConnection_; // eslint-disable-next-line @typescript-eslint/no-explicit-any - (ref.repo.persistentConnection_ as any).securityDebugCallback_ = callback; + (connection as any).securityDebugCallback_ = callback; }; export const stats = function (ref: Reference, showDelta?: boolean) { - repoStats(ref.repo, showDelta); + repoStats(ref._delegate._repo, showDelta); }; export const statsIncrementCounter = function (ref: Reference, metric: string) { - repoStatsIncrementCounter(ref.repo, metric); + repoStatsIncrementCounter(ref._delegate._repo, metric); }; export const dataUpdateCount = function (ref: Reference): number { - return ref.repo.dataUpdateCount; + return ref._delegate._repo.dataUpdateCount; }; export const interceptServerData = function ( ref: Reference, callback: ((a: string, b: unknown) => void) | null ) { - return repoInterceptServerData(ref.repo, callback); + return repoInterceptServerData(ref._delegate._repo, callback); }; /** diff --git a/packages/database/src/exp/Reference.ts b/packages/database/src/exp/Reference.ts index 0d518a677ab..056660c93ed 100644 --- a/packages/database/src/exp/Reference.ts +++ b/packages/database/src/exp/Reference.ts @@ -22,7 +22,7 @@ import { QueryContext } from '../core/view/EventRegistration'; export interface Query extends QueryContext { readonly ref: Reference; isEqual(other: Query | null): boolean; - toJSON(): object; + toJSON(): string; toString(): string; } diff --git a/packages/database/src/exp/Reference_impl.ts b/packages/database/src/exp/Reference_impl.ts index 2064d74ca6d..7a7b0a68a9e 100644 --- a/packages/database/src/exp/Reference_impl.ts +++ b/packages/database/src/exp/Reference_impl.ts @@ -20,11 +20,15 @@ import { assert, contains, getModularInstance } from '@firebase/util'; import { Repo, repoAddEventCallbackForQuery, + repoGetValue, repoRemoveEventCallbackForQuery } from '../core/Repo'; import { ChildrenNode } from '../core/snap/ChildrenNode'; import { Index } from '../core/snap/indexes/Index'; +import { KEY_INDEX } from '../core/snap/indexes/KeyIndex'; +import { PathIndex } from '../core/snap/indexes/PathIndex'; import { PRIORITY_INDEX } from '../core/snap/indexes/PriorityIndex'; +import { VALUE_INDEX } from '../core/snap/indexes/ValueIndex'; import { Node } from '../core/snap/Node'; import { syncPointSetReferenceConstructor } from '../core/SyncPoint'; import { syncTreeSetReferenceConstructor } from '../core/SyncTree'; @@ -32,22 +36,43 @@ import { parseRepoInfo } from '../core/util/libs/parser'; import { Path, pathChild, + pathEquals, pathGetBack, pathIsEmpty, - pathParent + pathParent, + pathToUrlEncodedString } from '../core/util/Path'; -import { fatal, ObjectToUniqueKey } from '../core/util/util'; -import { validateUrl } from '../core/util/validation'; +import { + fatal, + MAX_NAME, + MIN_NAME, + ObjectToUniqueKey +} from '../core/util/util'; +import { + isValidPriority, + validateFirebaseDataArg, + validateKey, + validatePathString, + validateUrl +} from '../core/util/validation'; import { Change } from '../core/view/Change'; import { CancelEvent, DataEvent, EventType } from '../core/view/Event'; import { CallbackContext, EventRegistration, - QueryContext + QueryContext, + UserCallback } from '../core/view/EventRegistration'; import { QueryParams, - queryParamsGetQueryObject + queryParamsEndAt, + queryParamsEndBefore, + queryParamsGetQueryObject, + queryParamsLimitToFirst, + queryParamsLimitToLast, + queryParamsOrderBy, + queryParamsStartAfter, + queryParamsStartAt } from '../core/view/QueryParams'; import { FirebaseDatabase } from './Database'; @@ -68,7 +93,7 @@ export class QueryImpl implements Query, QueryContext { readonly _repo: Repo, readonly _path: Path, readonly _queryParams: QueryParams, - private readonly _orderByCalled: boolean + readonly _orderByCalled: boolean ) {} get key(): string | null { @@ -97,18 +122,116 @@ export class QueryImpl implements Query, QueryContext { } isEqual(other: QueryImpl | null): boolean { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + other = getModularInstance(other); + if (!(other instanceof QueryImpl)) { + return false; + } + + const sameRepo = this._repo === other._repo; + const samePath = pathEquals(this._path, other._path); + const sameQueryIdentifier = + this._queryIdentifier === other._queryIdentifier; + + return sameRepo && samePath && sameQueryIdentifier; } - toJSON(): object { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + toJSON(): string { + return this.toString(); } toString(): string { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + return this._repo.toString() + pathToUrlEncodedString(this._path); + } +} + +/** + * Validates that no other order by call has been made + */ +function validateNoPreviousOrderByCall(query: QueryImpl, fnName: string) { + if (query._orderByCalled === true) { + throw new Error(fnName + ": You can't combine multiple orderBy calls."); + } +} + +/** + * Validates start/end values for queries. + */ +function validateQueryEndpoints(params: QueryParams) { + let startNode = null; + let endNode = null; + if (params.hasStart()) { + startNode = params.getIndexStartValue(); + } + if (params.hasEnd()) { + endNode = params.getIndexEndValue(); + } + + if (params.getIndex() === KEY_INDEX) { + 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(), startAfter(), ' + + 'endAt(), endBefore(), or equalTo() must be a string.'; + if (params.hasStart()) { + const startName = params.getIndexStartName(); + if (startName !== MIN_NAME) { + throw new Error(tooManyArgsError); + } else if (typeof startNode !== 'string') { + throw new Error(wrongArgTypeError); + } + } + if (params.hasEnd()) { + const endName = params.getIndexEndName(); + if (endName !== MAX_NAME) { + throw new Error(tooManyArgsError); + } else if (typeof endNode !== 'string') { + throw new Error(wrongArgTypeError); + } + } + } else if (params.getIndex() === PRIORITY_INDEX) { + if ( + (startNode != null && !isValidPriority(startNode)) || + (endNode != null && !isValidPriority(endNode)) + ) { + throw new Error( + 'Query: When ordering by priority, the first argument passed to startAt(), ' + + 'startAfter() endAt(), endBefore(), 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.' + ); + if ( + (startNode != null && typeof startNode === 'object') || + (endNode != null && typeof endNode === 'object') + ) { + throw new Error( + 'Query: First argument passed to startAt(), startAfter(), endAt(), endBefore(), or ' + + 'equalTo() cannot be an object.' + ); + } + } +} + +/** + * Validates that limit* has been called with the correct combination of parameters + */ +function validateLimit(params: QueryParams) { + if ( + params.hasStart() && + params.hasEnd() && + params.hasLimit() && + !params.hasAnchoredLimit() + ) { + throw new Error( + "Query: Can't combine startAt(), startAfter(), endAt(), endBefore(), and limit(). Use " + + 'limitToFirst() or limitToLast() instead.' + ); } } @@ -289,8 +412,14 @@ export function update(ref: Reference, values: object): Promise { } export function get(query: Query): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + const queryImpl = getModularInstance(query) as QueryImpl; + return repoGetValue(query._repo, queryImpl).then(node => { + return new DataSnapshot( + node, + new ReferenceImpl(query._repo, query._path), + query._queryParams.getIndex() + ); + }); } /** @@ -488,12 +617,9 @@ export class ChildEventRegistration implements EventRegistration { function addEventListener( query: Query, eventType: EventType, - callback: ( - snapshot: DataSnapshot, - previousChildName: string | null - ) => unknown, - cancelCallbackOrListenOptions: ((error: Error) => unknown) | ListenOptions, - options: ListenOptions + callback: UserCallback, + cancelCallbackOrListenOptions?: ((error: Error) => unknown) | ListenOptions, + options?: ListenOptions ) { let cancelCallback: ((error: Error) => unknown) | undefined; if (typeof cancelCallbackOrListenOptions === 'object') { @@ -504,6 +630,17 @@ function addEventListener( cancelCallback = cancelCallbackOrListenOptions; } + if (options && options.onlyOnce) { + const userCallback = callback; + const onceCallback: UserCallback = (dataSnapshot, previousChildName) => { + userCallback(dataSnapshot, previousChildName); + repoRemoveEventCallbackForQuery(query._repo, query, container); + }; + onceCallback.userCallback = callback.userCallback; + onceCallback.context = callback.context; + callback = onceCallback; + } + const callbackContext = new CallbackContext( callback, cancelCallback || undefined @@ -734,94 +871,432 @@ export function off( repoRemoveEventCallbackForQuery(query._repo, query, container); } -export interface QueryConstraint { - type: - | 'endAt' - | 'endBefore' - | 'startAt' - | 'startAfter' - | 'limitToFirst' - | 'limitToLast' - | 'orderByChild' - | 'orderByKey' - | 'orderByPriority' - | 'orderByValue' - | 'equalTo'; +/** Describes the different query constraints available in this SDK. */ +export type QueryConstraintType = + | 'endAt' + | 'endBefore' + | 'startAt' + | 'startAfter' + | 'limitToFirst' + | 'limitToLast' + | 'orderByChild' + | 'orderByKey' + | 'orderByPriority' + | 'orderByValue' + | 'equalTo'; + +/** + * A `QueryConstraint` is used to narrow the set of documents returned by a + * Database query. `QueryConstraint`s are created by invoking {@link endAt}, + * {@link endBefore}, {@link startAt}, {@link startAfter}, {@link + * limitToFirst}, {@link limitToLast}, {@link orderByChild}, + * {@link orderByChild}, {@link orderByKey} , {@link orderByPriority} , + * {@link orderByValue} or {@link equalTo} and + * can then be passed to {@link query} to create a new query instance that + * also contains this `QueryConstraint`. + */ +export abstract class QueryConstraint { + /** The type of this query constraints */ + abstract readonly type: QueryConstraintType; + + /** + * Takes the provided `Query` and returns a copy of the `Query` with this + * `QueryConstraint` applied. + */ + abstract _apply(query: QueryImpl): QueryImpl; +} + +class QueryEndAtConstraint extends QueryConstraint { + readonly type: 'endAt'; + + constructor( + private readonly _value: number | string | boolean | null, + private readonly _key?: string + ) { + super(); + } + + _apply(query: QueryImpl): QueryImpl { + validateFirebaseDataArg('endAt', 1, this._value, query._path, true); + const newParams = queryParamsEndAt( + query._queryParams, + this._value, + this._key + ); + validateLimit(newParams); + validateQueryEndpoints(newParams); + if (query._queryParams.hasEnd()) { + throw new Error( + 'endAt: Starting point was already set (by another call to endAt, ' + + 'endBefore or equalTo).' + ); + } + return new QueryImpl( + query._repo, + query._path, + newParams, + query._orderByCalled + ); + } } export function endAt( value: number | string | boolean | null, key?: string ): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + validateKey('endAt', 2, key, true); + return new QueryEndAtConstraint(value, key); +} + +class QueryEndBeforeConstraint extends QueryConstraint { + readonly type: 'endBefore'; + + constructor( + private readonly _value: number | string | boolean | null, + private readonly _key?: string + ) { + super(); + } + + _apply(query: QueryImpl): QueryImpl { + validateFirebaseDataArg('endBefore', 1, this._value, query._path, false); + const newParams = queryParamsEndBefore( + query._queryParams, + this._value, + this._key + ); + validateLimit(newParams); + validateQueryEndpoints(newParams); + if (query._queryParams.hasEnd()) { + throw new Error( + 'endBefore: Starting point was already set (by another call to endAt, ' + + 'endBefore or equalTo).' + ); + } + return new QueryImpl( + query._repo, + query._path, + newParams, + query._orderByCalled + ); + } } export function endBefore( value: number | string | boolean | null, key?: string ): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + validateKey('endBefore', 2, key, true); + return new QueryEndBeforeConstraint(value, key); +} + +class QueryStartAtConstraint extends QueryConstraint { + readonly type: 'startAt'; + + constructor( + private readonly _value: number | string | boolean | null, + private readonly _key?: string + ) { + super(); + } + + _apply(query: QueryImpl): QueryImpl { + validateFirebaseDataArg('startAt', 1, this._value, query._path, true); + const newParams = queryParamsStartAt( + query._queryParams, + this._value, + this._key + ); + validateLimit(newParams); + validateQueryEndpoints(newParams); + if (query._queryParams.hasStart()) { + throw new Error( + 'startAt: Starting point was already set (by another call to startAt, ' + + 'startBefore or equalTo).' + ); + } + return new QueryImpl( + query._repo, + query._path, + newParams, + query._orderByCalled + ); + } } export function startAt( - value: number | string | boolean | null, + value: number | string | boolean | null = null, key?: string ): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + validateKey('startAt', 2, key, true); + return new QueryStartAtConstraint(value, key); +} + +class QueryStartAfterConstraint extends QueryConstraint { + readonly type: 'startAfter'; + + constructor( + private readonly _value: number | string | boolean | null, + private readonly _key?: string + ) { + super(); + } + + _apply(query: QueryImpl): QueryImpl { + validateFirebaseDataArg('startAfter', 1, this._value, query._path, false); + const newParams = queryParamsStartAfter( + query._queryParams, + this._value, + this._key + ); + validateLimit(newParams); + validateQueryEndpoints(newParams); + if (query._queryParams.hasStart()) { + throw new Error( + 'startAfter: Starting point was already set (by another call to startAt, ' + + 'startAfter, or equalTo).' + ); + } + return new QueryImpl( + query._repo, + query._path, + newParams, + query._orderByCalled + ); + } } export function startAfter( value: number | string | boolean | null, key?: string ): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + validateKey('startAfter', 2, key, true); + return new QueryStartAfterConstraint(value, key); +} + +class QueryLimitToFirstConstraint extends QueryConstraint { + readonly type: 'limitToFirst'; + + constructor(private readonly _limit: number) { + super(); + } + + _apply(query: QueryImpl): QueryImpl { + if (query._queryParams.hasLimit()) { + throw new Error( + 'limitToFirst: Limit was already set (by another call to limitToFirst ' + + 'or limitToLast).' + ); + } + return new QueryImpl( + query._repo, + query._path, + queryParamsLimitToFirst(query._queryParams, this._limit), + query._orderByCalled + ); + } } export function limitToFirst(limit: number): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + if (typeof limit !== 'number' || Math.floor(limit) !== limit || limit <= 0) { + throw new Error('limitToFirst: First argument must be a positive integer.'); + } + return new QueryLimitToFirstConstraint(limit); +} + +class QueryLimitToLastConstraint extends QueryConstraint { + readonly type: 'limitToLast'; + + constructor(private readonly _limit: number) { + super(); + } + + _apply(query: QueryImpl): QueryImpl { + if (query._queryParams.hasLimit()) { + throw new Error( + 'limitToLast: Limit was already set (by another call to limitToFirst ' + + 'or limitToLast).' + ); + } + return new QueryImpl( + query._repo, + query._path, + queryParamsLimitToLast(query._queryParams, this._limit), + query._orderByCalled + ); + } } export function limitToLast(limit: number): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + if (typeof limit !== 'number' || Math.floor(limit) !== limit || limit <= 0) { + throw new Error('limitToLast: First argument must be a positive integer.'); + } + + return new QueryLimitToLastConstraint(limit); +} + +class QueryOrderByChildConstraint extends QueryConstraint { + readonly type: 'orderByChild'; + + constructor(private readonly _path: string) { + super(); + } + + _apply(query: QueryImpl): QueryImpl { + validateNoPreviousOrderByCall(query, 'orderByChild'); + const parsedPath = new Path(this._path); + if (pathIsEmpty(parsedPath)) { + throw new Error( + 'orderByChild: cannot pass in empty path. Use orderByValue() instead.' + ); + } + const index = new PathIndex(parsedPath); + const newParams = queryParamsOrderBy(query._queryParams, index); + validateQueryEndpoints(newParams); + + return new QueryImpl( + query._repo, + query._path, + newParams, + /*orderByCalled=*/ true + ); + } } export function orderByChild(path: string): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + if (path === '$key') { + throw new Error( + 'orderByChild: "$key" is invalid. Use orderByKey() instead.' + ); + } else if (path === '$priority') { + throw new Error( + 'orderByChild: "$priority" is invalid. Use orderByPriority() instead.' + ); + } else if (path === '$value') { + throw new Error( + 'orderByChild: "$value" is invalid. Use orderByValue() instead.' + ); + } + validatePathString('orderByChild', 1, path, false); + return new QueryOrderByChildConstraint(path); +} + +class QueryOrderByKeyConstraint extends QueryConstraint { + readonly type: 'orderByKey'; + + _apply(query: QueryImpl): QueryImpl { + validateNoPreviousOrderByCall(query, 'orderByKey'); + const newParams = queryParamsOrderBy(query._queryParams, KEY_INDEX); + validateQueryEndpoints(newParams); + return new QueryImpl( + query._repo, + query._path, + newParams, + /*orderByCalled=*/ true + ); + } } export function orderByKey(): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + return new QueryOrderByKeyConstraint(); +} + +class QueryOrderByPriorityConstraint extends QueryConstraint { + readonly type: 'orderByPriority'; + + _apply(query: QueryImpl): QueryImpl { + validateNoPreviousOrderByCall(query, 'orderByPriority'); + const newParams = queryParamsOrderBy(query._queryParams, PRIORITY_INDEX); + validateQueryEndpoints(newParams); + return new QueryImpl( + query._repo, + query._path, + newParams, + /*orderByCalled=*/ true + ); + } } export function orderByPriority(): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + return new QueryOrderByPriorityConstraint(); +} + +class QueryOrderByValueConstraint extends QueryConstraint { + readonly type: 'orderByValue'; + + _apply(query: QueryImpl): QueryImpl { + validateNoPreviousOrderByCall(query, 'orderByValue'); + const newParams = queryParamsOrderBy(query._queryParams, VALUE_INDEX); + validateQueryEndpoints(newParams); + return new QueryImpl( + query._repo, + query._path, + newParams, + /*orderByCalled=*/ true + ); + } } export function orderByValue(): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + return new QueryOrderByValueConstraint(); +} + +class QueryEqualToValueConstraint extends QueryConstraint { + readonly type: 'equalTo'; + + constructor( + private readonly _value: number | string | boolean | null, + private readonly _key?: string + ) { + super(); + } + + _apply(query: QueryImpl): QueryImpl { + validateFirebaseDataArg('equalTo', 1, this._value, query._path, false); + if (query._queryParams.hasStart()) { + throw new Error( + 'equalTo: Starting point was already set (by another call to startAt/startAfter or ' + + 'equalTo).' + ); + } + if (query._queryParams.hasEnd()) { + throw new Error( + 'equalTo: Ending point was already set (by another call to endAt/endBefore or ' + + 'equalTo).' + ); + } + return new QueryEndAtConstraint(this._value, this._key)._apply( + new QueryStartAtConstraint(this._value, this._key)._apply(query) + ); + } } export function equalTo( value: number | string | boolean | null, key?: string ): QueryConstraint { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + validateKey('equalTo', 2, key, true); + return new QueryEqualToValueConstraint(value, key); } -export function query(query: Query, ...constraints: QueryConstraint[]): Query { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; +/** + * Creates a new immutable instance of `Query` that is extended to also include + * additional query constraints. + * + * @param query - The Query instance to use as a base for the new constraints. + * @param queryConstraints - The list of `QueryConstraint`s to apply. + * @throws if any of the provided query constraints cannot be combined with the + * existing or new constraints. + */ +export function query( + query: Query, + ...queryConstraints: QueryConstraint[] +): QueryImpl { + let queryImpl = getModularInstance(query) as QueryImpl; + for (const constraint of queryConstraints) { + queryImpl = constraint._apply(queryImpl); + } + return queryImpl; } /** diff --git a/packages/database/test/query.test.ts b/packages/database/test/query.test.ts index 76fd4f7b940..877a0b40bf3 100644 --- a/packages/database/test/query.test.ts +++ b/packages/database/test/query.test.ts @@ -2475,7 +2475,7 @@ describe('Query Tests', () => { }); function dumpListens(node: Query) { - const listens: Map> = (node.repo + const listens: Map> = (node._delegate._repo .persistentConnection_ as any).listens; const nodePath = getPath(node); const listenPaths = []; @@ -3194,7 +3194,7 @@ describe('Query Tests', () => { it('get reads node from cache when not connected', async () => { const node = getRandomNode() as Reference; - const node2 = getFreshRepo(node.path); + const node2 = getFreshRepo(node._delegate._path); try { await node2.set({ foo: 'bar' }); const onSnapshot = await new Promise((resolve, _) => { @@ -3216,7 +3216,7 @@ describe('Query Tests', () => { it('get reads child node from cache when not connected', async () => { const node = getRandomNode() as Reference; - const node2 = getFreshRepo(node.path); + const node2 = getFreshRepo(node._delegate._path); try { await node2.set({ foo: 'bar' }); const onSnapshot = await new Promise((resolve, _) => { @@ -3236,7 +3236,7 @@ describe('Query Tests', () => { it('get reads parent node from cache when not connected', async () => { const node = getRandomNode() as Reference; - const node2 = getFreshRepo(node.path); + const node2 = getFreshRepo(node._delegate._path); try { await node2.set({ foo: 'bar' }); await node2.child('baz').set(1); @@ -3257,7 +3257,7 @@ describe('Query Tests', () => { it('get with pending node writes when not connected', async () => { const node = getRandomNode() as Reference; - const node2 = getFreshRepo(node.path); + const node2 = getFreshRepo(node._delegate._path); try { await node2.set({ foo: 'bar' }); const onSnapshot = await new Promise((resolve, _) => { @@ -3278,7 +3278,7 @@ describe('Query Tests', () => { it('get with pending child writes when not connected', async () => { const node = getRandomNode() as Reference; - const node2 = getFreshRepo(node.path); + const node2 = getFreshRepo(node._delegate._path); try { await node2.set({ foo: 'bar' }); const onSnapshot = await new Promise((resolve, _) => { @@ -3299,7 +3299,7 @@ describe('Query Tests', () => { it('get with pending parent writes when not connected', async () => { const node = getRandomNode() as Reference; - const node2 = getFreshRepo(node.path); + const node2 = getFreshRepo(node._delegate._path); try { await node2.set({ foo: 'bar' }); const onSnapshot = await new Promise((resolve, _) => { @@ -3344,7 +3344,7 @@ describe('Query Tests', () => { it('get does not cache sibling data', async () => { const reader = getRandomNode() as Reference; - const writer = getFreshRepo(reader.path); + const writer = getFreshRepo(reader._delegate._path); await writer.set({ foo: { cached: { data: '1' }, notCached: { data: '2' } } }); diff --git a/packages/firestore/src/lite/query.ts b/packages/firestore/src/lite/query.ts index f162be5ed46..4d9e27dc967 100644 --- a/packages/firestore/src/lite/query.ts +++ b/packages/firestore/src/lite/query.ts @@ -105,10 +105,10 @@ export abstract class QueryConstraint { } /** - * Creates a new immutable instance of `query` that is extended to also include + * Creates a new immutable instance of `Query` that is extended to also include * additional query constraints. * - * @param query - The query instance to use as a base for the new constraints. + * @param query - The Query instance to use as a base for the new constraints. * @param queryConstraints - The list of `QueryConstraint`s to apply. * @throws if any of the provided query constraints cannot be combined with the * existing or new constraints. From f51cc6e3677f2a747daa5c7e52380949706f5b0c Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Mon, 5 Apr 2021 13:51:51 -0600 Subject: [PATCH 06/15] Compat class for Reference (#4719) --- packages/database/exp/index.ts | 6 +- packages/database/src/api/Database.ts | 6 +- packages/database/src/api/Reference.ts | 320 ++++++-------------- packages/database/src/exp/Database.ts | 5 +- packages/database/src/exp/Reference_impl.ts | 143 +++++++-- packages/database/src/exp/Transaction.ts | 67 +++- packages/database/test/helpers/util.ts | 5 +- packages/database/test/query.test.ts | 10 + packages/database/test/transaction.test.ts | 18 +- 9 files changed, 311 insertions(+), 269 deletions(-) diff --git a/packages/database/exp/index.ts b/packages/database/exp/index.ts index 22e55e7e584..30c9ed0db32 100644 --- a/packages/database/exp/index.ts +++ b/packages/database/exp/index.ts @@ -67,7 +67,11 @@ export { refFromURL } from '../src/exp/Reference_impl'; export { increment, serverTimestamp } from '../src/exp/ServerValue'; -export { runTransaction, TransactionOptions } from '../src/exp/Transaction'; +export { + runTransaction, + TransactionOptions, + TransactionResult +} from '../src/exp/Transaction'; declare module '@firebase/component' { interface NameServiceMapping { diff --git a/packages/database/src/api/Database.ts b/packages/database/src/api/Database.ts index 7ba48480ac6..817346602c7 100644 --- a/packages/database/src/api/Database.ts +++ b/packages/database/src/api/Database.ts @@ -84,10 +84,10 @@ export class Database implements FirebaseService, Compat { validateArgCount('database.ref', 0, 1, arguments.length); if (path instanceof Reference) { const childRef = refFromURL(this._delegate, path.toString()); - return new Reference(this, childRef._path); + return new Reference(this, childRef); } else { const childRef = ref(this._delegate, path); - return new Reference(this, childRef._path); + return new Reference(this, childRef); } } @@ -101,7 +101,7 @@ export class Database implements FirebaseService, Compat { const apiName = 'database.refFromURL'; validateArgCount(apiName, 1, 1, arguments.length); const childRef = refFromURL(this._delegate, url); - return new Reference(this, childRef._path); + return new Reference(this, childRef); } // Make individual repo go offline. diff --git a/packages/database/src/api/Reference.ts b/packages/database/src/api/Reference.ts index a7f0bee7eb5..2498cbbf6fd 100644 --- a/packages/database/src/api/Reference.ts +++ b/packages/database/src/api/Reference.ts @@ -24,32 +24,11 @@ import { validateContextObject } from '@firebase/util'; -import { - repoServerTime, - repoSetWithPriority, - repoStartTransaction, - repoUpdate -} from '../core/Repo'; -import { PRIORITY_INDEX } from '../core/snap/indexes/PriorityIndex'; -import { Node } from '../core/snap/Node'; -import { nextPushId } from '../core/util/NextPushId'; -import { - Path, - pathChild, - pathGetBack, - pathGetFront, - pathIsEmpty, - pathParent -} from '../core/util/Path'; import { warn } from '../core/util/util'; import { validateBoolean, validateEventType, - validateFirebaseDataArg, - validateFirebaseMergeDataArg, validatePathString, - validatePriority, - validateRootPathString, validateWritablePath } from '../core/util/validation'; import { UserCallback } from '../core/view/EventRegistration'; @@ -77,8 +56,16 @@ import { endAt, endBefore, equalTo, - get + get, + child, + set, + update, + setWithPriority, + remove, + setPriority, + push } from '../exp/Reference_impl'; +import { runTransaction } from '../exp/Transaction'; import { Database } from './Database'; import { OnDisconnect } from './onDisconnect'; @@ -215,7 +202,7 @@ export class DataSnapshot implements Compat { */ getRef(): Reference { validateArgCount('DataSnapshot.ref', 0, 0, arguments.length); - return new Reference(this._database, this._delegate.ref._path); + return new Reference(this._database, this._delegate.ref); } get ref(): Reference { @@ -553,13 +540,14 @@ export class Query implements Compat { } get ref(): Reference { - return new Reference(this.database, this._delegate._path); + return new Reference( + this.database, + new ReferenceImpl(this._delegate._repo, this._delegate._path) + ); } } export class Reference extends Query implements Compat { - readonly _delegate: ReferenceImpl; - then: Promise['then']; catch: Promise['catch']; @@ -570,87 +558,54 @@ export class Reference extends Query implements Compat { * * Externally - this is the firebase.database.Reference type. */ - constructor(database: Database, path: Path) { + constructor(readonly database: Database, readonly _delegate: ReferenceImpl) { super( database, - new QueryImpl(database._delegate._repo, path, new QueryParams(), false) + new QueryImpl(_delegate._repo, _delegate._path, new QueryParams(), false) ); } /** @return {?string} */ getKey(): string | null { validateArgCount('Reference.key', 0, 0, arguments.length); - - if (pathIsEmpty(this._delegate._path)) { - return null; - } else { - return pathGetBack(this._delegate._path); - } + return this._delegate.key; } - child(pathString: string | Path): Reference { + child(pathString: string): Reference { validateArgCount('Reference.child', 1, 1, arguments.length); if (typeof pathString === 'number') { pathString = String(pathString); - } else if (!(pathString instanceof Path)) { - if (pathGetFront(this._delegate._path) === null) { - validateRootPathString('Reference.child', 1, pathString, false); - } else { - validatePathString('Reference.child', 1, pathString, false); - } } - - return new Reference( - this.database, - pathChild(this._delegate._path, pathString) - ); + return new Reference(this.database, child(this._delegate, pathString)); } /** @return {?Reference} */ getParent(): Reference | null { validateArgCount('Reference.parent', 0, 0, arguments.length); - - const parentPath = pathParent(this._delegate._path); - return parentPath === null - ? null - : new Reference(this.database, parentPath); + const parent = this._delegate.parent; + return parent ? new Reference(this.database, parent) : null; } /** @return {!Reference} */ getRoot(): Reference { validateArgCount('Reference.root', 0, 0, arguments.length); - - let ref: Reference = this; - while (ref.getParent() !== null) { - ref = ref.getParent(); - } - return ref; + return new Reference(this.database, this._delegate.root); } set( newVal: unknown, - onComplete?: (a: Error | null) => void + onComplete?: (error: Error | null) => void ): Promise { validateArgCount('Reference.set', 1, 2, arguments.length); - validateWritablePath('Reference.set', this._delegate._path); - validateFirebaseDataArg( - 'Reference.set', - 1, - newVal, - this._delegate._path, - false - ); validateCallback('Reference.set', 2, onComplete, true); - - const deferred = new Deferred(); - repoSetWithPriority( - this._delegate._repo, - this._delegate._path, - newVal, - /*priority=*/ null, - deferred.wrapCallback(onComplete) - ); - return deferred.promise; + const result = set(this._delegate, newVal); + if (onComplete) { + result.then( + () => onComplete(null), + error => onComplete(error) + ); + } + return result; } update( @@ -658,7 +613,6 @@ export class Reference extends Query implements Compat { onComplete?: (a: Error | null) => void ): Promise { validateArgCount('Reference.update', 1, 2, arguments.length); - validateWritablePath('Reference.update', this._delegate._path); if (Array.isArray(objectToMerge)) { const newObjectToMerge: { [k: string]: unknown } = {}; @@ -673,22 +627,17 @@ export class Reference extends Query implements Compat { 'only update some of the children.' ); } - validateFirebaseMergeDataArg( - 'Reference.update', - 1, - objectToMerge, - this._delegate._path, - false - ); + validateWritablePath('Reference.update', this._delegate._path); validateCallback('Reference.update', 2, onComplete, true); - const deferred = new Deferred(); - repoUpdate( - this._delegate._repo, - this._delegate._path, - objectToMerge as { [k: string]: unknown }, - deferred.wrapCallback(onComplete) - ); - return deferred.promise; + + const result = update(this._delegate, objectToMerge); + if (onComplete) { + result.then( + () => onComplete(null), + error => onComplete(error) + ); + } + return result; } setWithPriority( @@ -697,46 +646,34 @@ export class Reference extends Query implements Compat { onComplete?: (a: Error | null) => void ): Promise { validateArgCount('Reference.setWithPriority', 2, 3, arguments.length); - validateWritablePath('Reference.setWithPriority', this._delegate._path); - validateFirebaseDataArg( - 'Reference.setWithPriority', - 1, - newVal, - this._delegate._path, - false - ); - validatePriority('Reference.setWithPriority', 2, newPriority, false); validateCallback('Reference.setWithPriority', 3, onComplete, true); - if (this.getKey() === '.length' || this.getKey() === '.keys') { - throw ( - 'Reference.setWithPriority failed: ' + - this.getKey() + - ' is a read-only object.' + const result = setWithPriority(this._delegate, newVal, newPriority); + if (onComplete) { + result.then( + () => onComplete(null), + error => onComplete(error) ); } - - const deferred = new Deferred(); - repoSetWithPriority( - this._delegate._repo, - this._delegate._path, - newVal, - newPriority, - deferred.wrapCallback(onComplete) - ); - return deferred.promise; + return result; } remove(onComplete?: (a: Error | null) => void): Promise { validateArgCount('Reference.remove', 0, 1, arguments.length); - validateWritablePath('Reference.remove', this._delegate._path); validateCallback('Reference.remove', 1, onComplete, true); - return this.set(null, onComplete); + const result = remove(this._delegate); + if (onComplete) { + result.then( + () => onComplete(null), + error => onComplete(error) + ); + } + return result; } transaction( - transactionUpdate: (a: unknown) => unknown, + transactionUpdate: (currentData: unknown) => unknown, onComplete?: ( error: Error | null, committed: boolean, @@ -745,75 +682,31 @@ export class Reference extends Query implements Compat { applyLocally?: boolean ): Promise { validateArgCount('Reference.transaction', 1, 3, arguments.length); - validateWritablePath('Reference.transaction', this._delegate._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('Reference.transaction', 3, applyLocally, true); - if (this.getKey() === '.length' || this.getKey() === '.keys') { - throw ( - 'Reference.transaction failed: ' + - this.getKey() + - ' is a read-only object.' - ); - } - - if (applyLocally === undefined) { - applyLocally = true; - } - - const deferred = new Deferred(); - if (typeof onComplete === 'function') { - deferred.promise.catch(() => {}); - } - - const promiseComplete = ( - error: Error | null, - committed: boolean, - node: Node | null - ) => { - let dataSnapshot: DataSnapshot | null = null; - if (error) { - deferred.reject(error); - if (typeof onComplete === 'function') { - onComplete(error, committed, null); - } - } else { - dataSnapshot = new DataSnapshot( - this.database, - new ExpDataSnapshot( - node, - new ReferenceImpl(this._delegate._repo, this._delegate._path), - PRIORITY_INDEX - ) - ); - deferred.resolve(new TransactionResult(committed, dataSnapshot)); - if (typeof onComplete === 'function') { - onComplete(error, committed, dataSnapshot); - } - } - }; - - // Add a watch to make sure we get server updates. - const valueCallback = function () {}; - const watchRef = new Reference(this.database, this._delegate._path); - watchRef.on('value', valueCallback); - const unwatcher = function () { - watchRef.off('value', valueCallback); - }; - - repoStartTransaction( - this._delegate._repo, - this._delegate._path, - transactionUpdate, - promiseComplete, - unwatcher, + const result = runTransaction(this._delegate, transactionUpdate, { applyLocally + }).then( + transactionResult => + new TransactionResult( + transactionResult.committed, + new DataSnapshot(this.database, transactionResult.snapshot) + ) ); - - return deferred.promise; + if (onComplete) { + result.then( + transactionResult => + onComplete( + null, + transactionResult.committed, + transactionResult.snapshot + ), + error => onComplete(error, false, null) + ); + } + return result; } setPriority( @@ -821,59 +714,38 @@ export class Reference extends Query implements Compat { onComplete?: (a: Error | null) => void ): Promise { validateArgCount('Reference.setPriority', 1, 2, arguments.length); - validateWritablePath('Reference.setPriority', this._delegate._path); - validatePriority('Reference.setPriority', 1, priority, false); validateCallback('Reference.setPriority', 2, onComplete, true); - const deferred = new Deferred(); - repoSetWithPriority( - this._delegate._repo, - pathChild(this._delegate._path, '.priority'), - priority, - null, - deferred.wrapCallback(onComplete) - ); - return deferred.promise; + const result = setPriority(this._delegate, priority); + if (onComplete) { + result.then( + () => onComplete(null), + error => onComplete(error) + ); + } + return result; } push(value?: unknown, onComplete?: (a: Error | null) => void): Reference { validateArgCount('Reference.push', 0, 2, arguments.length); - validateWritablePath('Reference.push', this._delegate._path); - validateFirebaseDataArg( - 'Reference.push', - 1, - value, - this._delegate._path, - true - ); validateCallback('Reference.push', 2, onComplete, true); - const now = repoServerTime(this._delegate._repo); - 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. - const thennablePushRef = this.child(name); - const pushRef = this.child(name); - - let promise; - if (value != null) { - promise = thennablePushRef.set(value, onComplete).then(() => pushRef); - } else { - promise = Promise.resolve(pushRef); - } - - thennablePushRef.then = promise.then.bind(promise); - thennablePushRef.catch = promise.then.bind(promise, undefined); + const expPromise = push(this._delegate, value); + const promise = expPromise.then( + expRef => new Reference(this.database, expRef) + ); - if (typeof onComplete === 'function') { - promise.catch(() => {}); + if (onComplete) { + promise.then( + () => onComplete(null), + error => onComplete(error) + ); } - return thennablePushRef; + const result = new Reference(this.database, expPromise); + result.then = promise.then.bind(promise); + result.catch = promise.catch.bind(promise, undefined); + return result; } onDisconnect(): OnDisconnect { diff --git a/packages/database/src/exp/Database.ts b/packages/database/src/exp/Database.ts index f9fd114b1c5..d62f293a45a 100644 --- a/packages/database/src/exp/Database.ts +++ b/packages/database/src/exp/Database.ts @@ -33,7 +33,6 @@ import { newEmptyPath, pathIsEmpty } from '../core/util/Path'; import { fatal, log } from '../core/util/util'; import { validateUrl } from '../core/util/validation'; -import { Reference } from './Reference'; import { ReferenceImpl } from './Reference_impl'; /** @@ -203,7 +202,7 @@ export class FirebaseDatabase implements _FirebaseService { _instanceStarted: boolean = false; /** Backing state for root_ */ - private _rootInternal?: Reference; + private _rootInternal?: ReferenceImpl; constructor(private _repoInternal: Repo, readonly app: FirebaseApp) {} @@ -219,7 +218,7 @@ export class FirebaseDatabase implements _FirebaseService { return this._repoInternal; } - get _root(): Reference { + get _root(): ReferenceImpl { if (!this._rootInternal) { this._rootInternal = new ReferenceImpl(this._repo, newEmptyPath()); } diff --git a/packages/database/src/exp/Reference_impl.ts b/packages/database/src/exp/Reference_impl.ts index 7a7b0a68a9e..1549fc26973 100644 --- a/packages/database/src/exp/Reference_impl.ts +++ b/packages/database/src/exp/Reference_impl.ts @@ -15,13 +15,16 @@ * limitations under the License. */ -import { assert, contains, getModularInstance } from '@firebase/util'; +import { assert, contains, getModularInstance, Deferred } from '@firebase/util'; import { Repo, repoAddEventCallbackForQuery, repoGetValue, - repoRemoveEventCallbackForQuery + repoRemoveEventCallbackForQuery, + repoServerTime, + repoSetWithPriority, + repoUpdate } from '../core/Repo'; import { ChildrenNode } from '../core/snap/ChildrenNode'; import { Index } from '../core/snap/indexes/Index'; @@ -33,11 +36,13 @@ import { Node } from '../core/snap/Node'; import { syncPointSetReferenceConstructor } from '../core/SyncPoint'; import { syncTreeSetReferenceConstructor } from '../core/SyncTree'; import { parseRepoInfo } from '../core/util/libs/parser'; +import { nextPushId } from '../core/util/NextPushId'; import { Path, pathChild, pathEquals, pathGetBack, + pathGetFront, pathIsEmpty, pathParent, pathToUrlEncodedString @@ -51,9 +56,13 @@ import { import { isValidPriority, validateFirebaseDataArg, + validateFirebaseMergeDataArg, validateKey, validatePathString, - validateUrl + validatePriority, + validateRootPathString, + validateUrl, + validateWritablePath } from '../core/util/validation'; import { Change } from '../core/view/Change'; import { CancelEvent, DataEvent, EventType } from '../core/view/Event'; @@ -81,7 +90,6 @@ import { OnDisconnect, Query as Query, Reference as Reference, - ThenableReference, Unsubscribe } from './Reference'; @@ -236,19 +244,25 @@ function validateLimit(params: QueryParams) { } export class ReferenceImpl extends QueryImpl implements Reference { - root: Reference; - /** @hideconstructor */ constructor(repo: Repo, path: Path) { super(repo, path, new QueryParams(), false); } - get parent(): Reference | null { + get parent(): ReferenceImpl | null { const parentPath = pathParent(this._path); return parentPath === null ? null : new ReferenceImpl(this._repo, parentPath); } + + get root(): ReferenceImpl { + let ref: ReferenceImpl = this; + while (ref.parent !== null) { + ref = ref.parent; + } + return ref; + } } export class DataSnapshot { @@ -260,7 +274,7 @@ export class DataSnapshot { */ constructor( readonly _node: Node, - readonly ref: Reference, + readonly ref: ReferenceImpl, readonly _index: Index ) {} @@ -333,13 +347,13 @@ export class DataSnapshot { } } -export function ref(db: FirebaseDatabase, path?: string): Reference { +export function ref(db: FirebaseDatabase, path?: string): ReferenceImpl { db = getModularInstance(db); db._checkNotDeleted('ref'); return path !== undefined ? child(db._root, path) : db._root; } -export function refFromURL(db: FirebaseDatabase, url: string): Reference { +export function refFromURL(db: FirebaseDatabase, url: string): ReferenceImpl { db = getModularInstance(db); db._checkNotDeleted('refFromURL'); const parsedURL = parseRepoInfo(url, db._repo.repoInfo_.nodeAdmin); @@ -364,8 +378,14 @@ export function refFromURL(db: FirebaseDatabase, url: string): Reference { return ref(db, parsedURL.path.toString()); } -export function child(ref: Reference, path: string): Reference { - // TODO: Accept Compat class +export function child(ref: Reference, path: string): ReferenceImpl { + ref = getModularInstance(ref); + if (pathGetFront(ref._path) === null) { + // TODO(database-exp): Remove argument numbers from all error messages + validateRootPathString('child', 1, path, false); + } else { + validatePathString('child', 1, path, false); + } return new ReferenceImpl(ref._repo, pathChild(ref._path, path)); } @@ -374,41 +394,110 @@ export function onDisconnect(ref: Reference): OnDisconnect { return {} as any; } -export function push(ref: Reference, value?: unknown): ThenableReference { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; +export interface ThenableReferenceImpl + extends ReferenceImpl, + Pick, 'then' | 'catch'> {} + +export function push(ref: Reference, value?: unknown): ThenableReferenceImpl { + ref = getModularInstance(ref); + validateWritablePath('push', ref._path); + validateFirebaseDataArg('push', 1, value, ref._path, true); + const now = repoServerTime(ref._repo); + 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. + const thennablePushRef: Partial = child(ref, name); + const pushRef = child(ref, name); + + let promise: Promise; + if (value != null) { + promise = set(pushRef, value).then(() => pushRef); + } else { + promise = Promise.resolve(pushRef); + } + + thennablePushRef.then = promise.then.bind(promise); + thennablePushRef.catch = promise.then.bind(promise, undefined); + return thennablePushRef as ThenableReferenceImpl; } export function remove(ref: Reference): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + validateWritablePath('remove', ref._path); + return set(ref, null); } export function set(ref: Reference, value: unknown): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + ref = getModularInstance(ref); + validateWritablePath('set', ref._path); + validateFirebaseDataArg('set', 1, value, ref._path, false); + const deferred = new Deferred(); + repoSetWithPriority( + ref._repo, + ref._path, + value, + /*priority=*/ null, + deferred.wrapCallback(() => {}) + ); + return deferred.promise; } export function setPriority( ref: Reference, priority: string | number | null ): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + ref = getModularInstance(ref); + validateWritablePath('setPriority', ref._path); + validatePriority('setPriority', 1, priority, false); + const deferred = new Deferred(); + repoSetWithPriority( + ref._repo, + pathChild(ref._path, '.priority'), + priority, + null, + deferred.wrapCallback(() => {}) + ); + return deferred.promise; } export function setWithPriority( ref: Reference, - newVal: unknown, - newPriority: string | number | null + value: unknown, + priority: string | number | null ): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + validateWritablePath('setWithPriority', ref._path); + validateFirebaseDataArg('setWithPriority', 1, value, ref._path, false); + validatePriority('setWithPriority', 2, priority, false); + + if (ref.key === '.length' || ref.key === '.keys') { + throw 'setWithPriority failed: ' + ref.key + ' is a read-only object.'; + } + + const deferred = new Deferred(); + repoSetWithPriority( + ref._repo, + ref._path, + value, + priority, + deferred.wrapCallback(() => {}) + ); + return deferred.promise; } export function update(ref: Reference, values: object): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + validateFirebaseMergeDataArg('update', 1, values, ref._path, false); + const deferred = new Deferred(); + repoUpdate( + ref._repo, + ref._path, + values as Record, + deferred.wrapCallback(() => {}) + ); + return deferred.promise; } export function get(query: Query): Promise { diff --git a/packages/database/src/exp/Transaction.ts b/packages/database/src/exp/Transaction.ts index a848c3eb173..d20723cdec8 100644 --- a/packages/database/src/exp/Transaction.ts +++ b/packages/database/src/exp/Transaction.ts @@ -15,18 +15,79 @@ * limitations under the License. */ +import { getModularInstance, Deferred } from '@firebase/util'; + +import { repoStartTransaction } from '../core/Repo'; +import { PRIORITY_INDEX } from '../core/snap/indexes/PriorityIndex'; +import { Node } from '../core/snap/Node'; +import { validateWritablePath } from '../core/util/validation'; + import { Reference } from './Reference'; +import { DataSnapshot, onValue, ReferenceImpl } from './Reference_impl'; export interface TransactionOptions { readonly applyLocally?: boolean; } +export class TransactionResult { + /** + * A type for the resolve value of Firebase.transaction. + */ + constructor(readonly committed: boolean, readonly snapshot: DataSnapshot) {} + + toJSON(): object { + return { committed: this.committed, snapshot: this.snapshot.toJSON() }; + } +} + export function runTransaction( ref: Reference, // eslint-disable-next-line @typescript-eslint/no-explicit-any transactionUpdate: (currentData: any) => unknown, options?: TransactionOptions -): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; +): Promise { + ref = getModularInstance(ref); + + validateWritablePath('Reference.transaction', ref._path); + + if (ref.key === '.length' || ref.key === '.keys') { + throw ( + 'Reference.transaction failed: ' + ref.key + ' is a read-only object.' + ); + } + + const applyLocally = options?.applyLocally ?? true; + const deferred = new Deferred(); + + const promiseComplete = ( + error: Error | null, + committed: boolean, + node: Node | null + ) => { + let dataSnapshot: DataSnapshot | null = null; + if (error) { + deferred.reject(error); + } else { + dataSnapshot = new DataSnapshot( + node, + new ReferenceImpl(ref._repo, ref._path), + PRIORITY_INDEX + ); + deferred.resolve(new TransactionResult(committed, dataSnapshot)); + } + }; + + // Add a watch to make sure we get server updates. + const unwatcher = onValue(ref, () => {}); + + repoStartTransaction( + ref._repo, + ref._path, + transactionUpdate, + promiseComplete, + unwatcher, + applyLocally + ); + + return deferred.promise; } diff --git a/packages/database/test/helpers/util.ts b/packages/database/test/helpers/util.ts index e63ddfea8f1..ec2a0a8f733 100644 --- a/packages/database/test/helpers/util.ts +++ b/packages/database/test/helpers/util.ts @@ -25,6 +25,7 @@ import { Component, ComponentType } from '@firebase/component'; import { Query, Reference } from '../../src/api/Reference'; import { ConnectionTarget } from '../../src/api/test_access'; +import { Path } from '../../src/core/util/Path'; // eslint-disable-next-line @typescript-eslint/no-require-imports export const TEST_PROJECT = require('../../../../config/project.json'); @@ -140,13 +141,13 @@ export function shuffle(arr, randFn = Math.random) { let freshRepoId = 1; const activeFreshApps = []; -export function getFreshRepo(path) { +export function getFreshRepo(path: Path) { const app = firebase.initializeApp( { databaseURL: DATABASE_URL }, 'ISOLATED_REPO_' + freshRepoId++ ); activeFreshApps.push(app); - return (app as any).database().ref(path); + return (app as any).database().ref(path.toString()); } export function getFreshRepoFromReference(ref) { diff --git a/packages/database/test/query.test.ts b/packages/database/test/query.test.ts index 877a0b40bf3..f8b432a7a47 100644 --- a/packages/database/test/query.test.ts +++ b/packages/database/test/query.test.ts @@ -767,6 +767,16 @@ describe('Query Tests', () => { expect(expected).to.equal(10); }); + it('Raises snapshots synchronously', () => { + const node = getRandomNode() as Reference; + let newValue; + node.on('value', v => { + newValue = v.val(); + }); + node.set('foo'); + expect(newValue).to.equal('foo'); + }); + it('Set a limit of 5, add a bunch of nodes, ensure only last 5 items are sent from server.', async () => { const node = getRandomNode() as Reference; await node.set({}); diff --git a/packages/database/test/transaction.test.ts b/packages/database/test/transaction.test.ts index 7ab88d55ae3..530c41361b3 100644 --- a/packages/database/test/transaction.test.ts +++ b/packages/database/test/transaction.test.ts @@ -16,6 +16,7 @@ */ import firebase from '@firebase/app'; +import { Deferred } from '@firebase/util'; import { expect } from 'chai'; import { Reference } from '../src/api/Reference'; @@ -381,13 +382,16 @@ describe('Transaction Tests', () => { node.child('foo').set(0); expect(firstDone).to.equal(false); + + // Wait for the `onComplete` callbacks to be invoked. This is no longer + // happening synchronously, as the underlying database@exp implementation + // uses promises. + await ea.promise; + expect(secondDone).to.equal(true); expect(thirdRunCount).to.equal(2); // Note that the set actually raises two events, one overlaid on top of the original transaction value, and a // second one with the re-run value from the third transaction - - await ea.promise; - expect(nodeSnap.val()).to.deep.equal({ foo: 0, bar: 'second' }); }); @@ -550,7 +554,7 @@ describe('Transaction Tests', () => { it('Update should not cancel unrelated transactions', async () => { const node = getRandomNode() as Reference; - let fooTransactionDone = false; + const fooTransactionDone = false; let barTransactionDone = false; restoreHash = hijackHash(() => { return 'foobar'; @@ -558,6 +562,8 @@ describe('Transaction Tests', () => { await node.child('foo').set(5); + const deferred = new Deferred(); + // 'foo' gets overwritten in the update so the transaction gets cancelled. node.child('foo').transaction( old => { @@ -566,7 +572,7 @@ describe('Transaction Tests', () => { (error, committed, snapshot) => { expect(error.message).to.equal('set'); expect(committed).to.equal(false); - fooTransactionDone = true; + deferred.resolve(); } ); @@ -592,7 +598,7 @@ describe('Transaction Tests', () => { } }); - expect(fooTransactionDone).to.equal(true); + await deferred.promise; expect(barTransactionDone).to.equal(false); restoreHash(); restoreHash = null; From 84940981d3849c902ea4ea317c42953b16e7ce5f Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Mon, 5 Apr 2021 18:41:15 -0600 Subject: [PATCH 07/15] OnDisconnect Compat (#4727) --- packages/database/exp/index.ts | 4 +- packages/database/src/api/Reference.ts | 5 +- packages/database/src/api/onDisconnect.ts | 122 +++++++------------- packages/database/src/exp/OnDisconnect.ts | 117 +++++++++++++++++++ packages/database/src/exp/Reference.ts | 11 -- packages/database/src/exp/Reference_impl.ts | 6 +- 6 files changed, 170 insertions(+), 95 deletions(-) create mode 100644 packages/database/src/exp/OnDisconnect.ts diff --git a/packages/database/exp/index.ts b/packages/database/exp/index.ts index 30c9ed0db32..074202a22de 100644 --- a/packages/database/exp/index.ts +++ b/packages/database/exp/index.ts @@ -37,9 +37,9 @@ export { Reference, ListenOptions, Unsubscribe, - ThenableReference, - OnDisconnect + ThenableReference } from '../src/exp/Reference'; +export { OnDisconnect } from '../src/exp/OnDisconnect'; export { QueryConstraint, DataSnapshot, diff --git a/packages/database/src/api/Reference.ts b/packages/database/src/api/Reference.ts index 2498cbbf6fd..d5862154df3 100644 --- a/packages/database/src/api/Reference.ts +++ b/packages/database/src/api/Reference.ts @@ -33,6 +33,7 @@ import { } from '../core/util/validation'; import { UserCallback } from '../core/view/EventRegistration'; import { QueryParams } from '../core/view/QueryParams'; +import { OnDisconnect as ExpOnDisconnect } from '../exp/OnDisconnect'; import { DataSnapshot as ExpDataSnapshot, off, @@ -750,7 +751,9 @@ export class Reference extends Query implements Compat { onDisconnect(): OnDisconnect { validateWritablePath('Reference.onDisconnect', this._delegate._path); - return new OnDisconnect(this._delegate._repo, this._delegate._path); + return new OnDisconnect( + new ExpOnDisconnect(this._delegate._repo, this._delegate._path) + ); } get key(): string | null { diff --git a/packages/database/src/api/onDisconnect.ts b/packages/database/src/api/onDisconnect.ts index bc8190e5f18..fbeb27674cc 100644 --- a/packages/database/src/api/onDisconnect.ts +++ b/packages/database/src/api/onDisconnect.ts @@ -15,67 +15,52 @@ * limitations under the License. */ -import { Deferred, validateArgCount, validateCallback } from '@firebase/util'; +import { validateArgCount, validateCallback, Compat } from '@firebase/util'; -import { - Repo, - repoOnDisconnectCancel, - repoOnDisconnectSet, - repoOnDisconnectSetWithPriority, - repoOnDisconnectUpdate -} from '../core/Repo'; import { Indexable } from '../core/util/misc'; -import { Path } from '../core/util/Path'; import { warn } from '../core/util/util'; -import { - validateWritablePath, - validateFirebaseDataArg, - validatePriority, - validateFirebaseMergeDataArg -} from '../core/util/validation'; +import { OnDisconnect as ExpOnDisconnect } from '../exp/OnDisconnect'; -export class OnDisconnect { - constructor(private repo_: Repo, private path_: Path) {} +export class OnDisconnect implements Compat { + constructor(readonly _delegate: ExpOnDisconnect) {} cancel(onComplete?: (a: Error | null) => void): Promise { validateArgCount('OnDisconnect.cancel', 0, 1, arguments.length); validateCallback('OnDisconnect.cancel', 1, onComplete, true); - const deferred = new Deferred(); - repoOnDisconnectCancel( - this.repo_, - this.path_, - deferred.wrapCallback(onComplete) - ); - return deferred.promise; + const result = this._delegate.cancel(); + if (onComplete) { + result.then( + () => onComplete(null), + error => onComplete(error) + ); + } + return result; } remove(onComplete?: (a: Error | null) => void): Promise { validateArgCount('OnDisconnect.remove', 0, 1, arguments.length); - validateWritablePath('OnDisconnect.remove', this.path_); validateCallback('OnDisconnect.remove', 1, onComplete, true); - const deferred = new Deferred(); - repoOnDisconnectSet( - this.repo_, - this.path_, - null, - deferred.wrapCallback(onComplete) - ); - return deferred.promise; + const result = this._delegate.remove(); + if (onComplete) { + result.then( + () => onComplete(null), + error => onComplete(error) + ); + } + return result; } set(value: unknown, onComplete?: (a: Error | null) => void): Promise { validateArgCount('OnDisconnect.set', 1, 2, arguments.length); - validateWritablePath('OnDisconnect.set', this.path_); - validateFirebaseDataArg('OnDisconnect.set', 1, value, this.path_, false); validateCallback('OnDisconnect.set', 2, onComplete, true); - const deferred = new Deferred(); - repoOnDisconnectSet( - this.repo_, - this.path_, - value, - deferred.wrapCallback(onComplete) - ); - return deferred.promise; + const result = this._delegate.set(value); + if (onComplete) { + result.then( + () => onComplete(null), + error => onComplete(error) + ); + } + return result; } setWithPriority( @@ -84,26 +69,15 @@ export class OnDisconnect { onComplete?: (a: Error | null) => void ): Promise { 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, onComplete, true); - - const deferred = new Deferred(); - repoOnDisconnectSetWithPriority( - this.repo_, - this.path_, - value, - priority, - deferred.wrapCallback(onComplete) - ); - return deferred.promise; + const result = this._delegate.setWithPriority(value, priority); + if (onComplete) { + result.then( + () => onComplete(null), + error => onComplete(error) + ); + } + return result; } update( @@ -111,7 +85,6 @@ export class OnDisconnect { onComplete?: (a: Error | null) => void ): Promise { validateArgCount('OnDisconnect.update', 1, 2, arguments.length); - validateWritablePath('OnDisconnect.update', this.path_); if (Array.isArray(objectToMerge)) { const newObjectToMerge: { [k: string]: unknown } = {}; for (let i = 0; i < objectToMerge.length; ++i) { @@ -123,21 +96,14 @@ export class OnDisconnect { 'existing data, or an Object with integer keys if you really do want to only update some of the children.' ); } - validateFirebaseMergeDataArg( - 'OnDisconnect.update', - 1, - objectToMerge, - this.path_, - false - ); validateCallback('OnDisconnect.update', 2, onComplete, true); - const deferred = new Deferred(); - repoOnDisconnectUpdate( - this.repo_, - this.path_, - objectToMerge, - deferred.wrapCallback(onComplete) - ); - return deferred.promise; + const result = this._delegate.update(objectToMerge); + if (onComplete) { + result.then( + () => onComplete(null), + error => onComplete(error) + ); + } + return result; } } diff --git a/packages/database/src/exp/OnDisconnect.ts b/packages/database/src/exp/OnDisconnect.ts new file mode 100644 index 00000000000..8441cae5aab --- /dev/null +++ b/packages/database/src/exp/OnDisconnect.ts @@ -0,0 +1,117 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Deferred } from '@firebase/util'; + +import { + Repo, + repoOnDisconnectCancel, + repoOnDisconnectSet, + repoOnDisconnectSetWithPriority, + repoOnDisconnectUpdate +} from '../core/Repo'; +import { Indexable } from '../core/util/misc'; +import { Path } from '../core/util/Path'; +import { + validateFirebaseDataArg, + validateFirebaseMergeDataArg, + validatePriority, + validateWritablePath +} from '../core/util/validation'; + +export class OnDisconnect { + constructor(private _repo: Repo, private _path: Path) {} + + cancel(): Promise { + const deferred = new Deferred(); + repoOnDisconnectCancel( + this._repo, + this._path, + deferred.wrapCallback(() => {}) + ); + return deferred.promise; + } + + remove(): Promise { + validateWritablePath('OnDisconnect.remove', this._path); + const deferred = new Deferred(); + repoOnDisconnectSet( + this._repo, + this._path, + null, + deferred.wrapCallback(() => {}) + ); + return deferred.promise; + } + + set(value: unknown): Promise { + validateWritablePath('OnDisconnect.set', this._path); + validateFirebaseDataArg('OnDisconnect.set', 1, value, this._path, false); + const deferred = new Deferred(); + repoOnDisconnectSet( + this._repo, + this._path, + value, + deferred.wrapCallback(() => {}) + ); + return deferred.promise; + } + + setWithPriority( + value: unknown, + priority: number | string | null + ): Promise { + validateWritablePath('OnDisconnect.setWithPriority', this._path); + validateFirebaseDataArg( + 'OnDisconnect.setWithPriority', + 1, + value, + this._path, + false + ); + validatePriority('OnDisconnect.setWithPriority', 2, priority, false); + + const deferred = new Deferred(); + repoOnDisconnectSetWithPriority( + this._repo, + this._path, + value, + priority, + deferred.wrapCallback(() => {}) + ); + return deferred.promise; + } + + update(objectToMerge: Indexable): Promise { + validateWritablePath('OnDisconnect.update', this._path); + validateFirebaseMergeDataArg( + 'OnDisconnect.update', + 1, + objectToMerge, + this._path, + false + ); + const deferred = new Deferred(); + repoOnDisconnectUpdate( + this._repo, + this._path, + objectToMerge, + deferred.wrapCallback(() => {}) + ); + return deferred.promise; + } +} diff --git a/packages/database/src/exp/Reference.ts b/packages/database/src/exp/Reference.ts index 056660c93ed..0335d5ff2c2 100644 --- a/packages/database/src/exp/Reference.ts +++ b/packages/database/src/exp/Reference.ts @@ -31,17 +31,6 @@ export interface Reference extends Query { readonly parent: Reference | null; } -export interface OnDisconnect { - cancel(): Promise; - remove(): Promise; - set(value: unknown): Promise; - setWithPriority( - value: unknown, - priority: number | string | null - ): Promise; - update(values: object): Promise; -} - export interface ThenableReference extends Reference, Pick, 'then' | 'catch'> {} diff --git a/packages/database/src/exp/Reference_impl.ts b/packages/database/src/exp/Reference_impl.ts index 1549fc26973..35bec7fe164 100644 --- a/packages/database/src/exp/Reference_impl.ts +++ b/packages/database/src/exp/Reference_impl.ts @@ -85,9 +85,9 @@ import { } from '../core/view/QueryParams'; import { FirebaseDatabase } from './Database'; +import { OnDisconnect } from './OnDisconnect'; import { ListenOptions, - OnDisconnect, Query as Query, Reference as Reference, Unsubscribe @@ -390,8 +390,8 @@ export function child(ref: Reference, path: string): ReferenceImpl { } export function onDisconnect(ref: Reference): OnDisconnect { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + ref = getModularInstance(ref) as ReferenceImpl; + return new OnDisconnect(ref._repo, ref._path); } export interface ThenableReferenceImpl From e121c6022ff57a7ff0a231161c634522ebc06b98 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Mon, 5 Apr 2021 18:41:34 -0600 Subject: [PATCH 08/15] ServerValues compat (#4728) --- packages/database/src/api/Database.ts | 13 +++---------- packages/database/src/exp/ServerValue.ts | 14 ++++++++++---- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/database/src/api/Database.ts b/packages/database/src/api/Database.ts index 817346602c7..468bf9153b1 100644 --- a/packages/database/src/api/Database.ts +++ b/packages/database/src/api/Database.ts @@ -27,6 +27,7 @@ import { goOffline } from '../exp/Database'; import { ref, refFromURL } from '../exp/Reference_impl'; +import { increment, serverTimestamp } from '../exp/ServerValue'; import { Reference } from './Reference'; @@ -35,16 +36,8 @@ import { Reference } from './Reference'; */ export class Database implements FirebaseService, Compat { static readonly ServerValue = { - TIMESTAMP: { - '.sv': 'timestamp' - }, - increment: (delta: number) => { - return { - '.sv': { - 'increment': delta - } - }; - } + TIMESTAMP: serverTimestamp(), + increment: (delta: number) => increment(delta) }; /** diff --git a/packages/database/src/exp/ServerValue.ts b/packages/database/src/exp/ServerValue.ts index fff9f29b52f..6fdecee1b2a 100644 --- a/packages/database/src/exp/ServerValue.ts +++ b/packages/database/src/exp/ServerValue.ts @@ -15,12 +15,18 @@ * limitations under the License. */ +const SERVER_TIMESTAMP = { + '.sv': 'timestamp' +}; + export function serverTimestamp(): object { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + return SERVER_TIMESTAMP; } export function increment(delta: number): object { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + return { + '.sv': { + 'increment': delta + } + }; } From 137b21bb9f0844c4991a6963feb64d85cdd49c06 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Mon, 5 Apr 2021 18:42:05 -0600 Subject: [PATCH 09/15] EnableLogging Compat (#4730) --- packages/database/index.node.ts | 3 +-- packages/database/index.ts | 3 +-- packages/database/src/exp/Database.ts | 9 ++++++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/database/index.node.ts b/packages/database/index.node.ts index de1a4c37d97..30a8a7bb0e5 100644 --- a/packages/database/index.node.ts +++ b/packages/database/index.node.ts @@ -28,9 +28,8 @@ import { Database } from './src/api/Database'; import * as INTERNAL from './src/api/internal'; import { DataSnapshot, Query, Reference } from './src/api/Reference'; import * as TEST_ACCESS from './src/api/test_access'; -import { enableLogging } from './src/core/util/util'; import { setSDKVersion } from './src/core/version'; -import { repoManagerDatabaseFromApp } from './src/exp/Database'; +import { enableLogging, repoManagerDatabaseFromApp } from './src/exp/Database'; import { setWebSocketImpl } from './src/realtime/WebSocketConnection'; setWebSocketImpl(Client); diff --git a/packages/database/index.ts b/packages/database/index.ts index 442be97313b..ee0d0363b44 100644 --- a/packages/database/index.ts +++ b/packages/database/index.ts @@ -28,9 +28,8 @@ import { Database } from './src/api/Database'; import * as INTERNAL from './src/api/internal'; import { DataSnapshot, Query, Reference } from './src/api/Reference'; import * as TEST_ACCESS from './src/api/test_access'; -import { enableLogging } from './src/core/util/util'; import { setSDKVersion } from './src/core/version'; -import { repoManagerDatabaseFromApp } from './src/exp/Database'; +import { enableLogging, repoManagerDatabaseFromApp } from './src/exp/Database'; const ServerValue = Database.ServerValue; diff --git a/packages/database/src/exp/Database.ts b/packages/database/src/exp/Database.ts index d62f293a45a..a8b86bdc183 100644 --- a/packages/database/src/exp/Database.ts +++ b/packages/database/src/exp/Database.ts @@ -30,7 +30,11 @@ import { Repo, repoInterrupt, repoResume, repoStart } from '../core/Repo'; import { RepoInfo } from '../core/RepoInfo'; import { parseRepoInfo } from '../core/util/libs/parser'; import { newEmptyPath, pathIsEmpty } from '../core/util/Path'; -import { fatal, log } from '../core/util/util'; +import { + fatal, + log, + enableLogging as enableLoggingImpl +} from '../core/util/util'; import { validateUrl } from '../core/util/validation'; import { ReferenceImpl } from './Reference_impl'; @@ -290,6 +294,5 @@ export function enableLogging( logger?: boolean | ((message: string) => unknown), persistent?: boolean ): void { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return {} as any; + enableLoggingImpl(logger, persistent); } From b2e25e92acd1198b8a198b5b4b8637a7323bbf88 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Mon, 5 Apr 2021 19:04:50 -0600 Subject: [PATCH 10/15] Remove argument numbers from validation (#4729) --- .changeset/lets-go-travel.md | 5 + packages/database/src/api/Reference.ts | 96 +++++++++++-------- packages/database/src/api/onDisconnect.ts | 15 ++- packages/database/src/core/util/validation.ts | 76 +++++---------- packages/database/src/exp/Database.ts | 2 +- packages/database/src/exp/OnDisconnect.ts | 12 +-- packages/database/src/exp/Reference_impl.ts | 42 ++++---- .../database/test/exp/integration.test.ts | 62 +++++++++++- packages/util/src/validation.ts | 48 ++-------- 9 files changed, 185 insertions(+), 173 deletions(-) create mode 100644 .changeset/lets-go-travel.md diff --git a/.changeset/lets-go-travel.md b/.changeset/lets-go-travel.md new file mode 100644 index 00000000000..1731f975ff3 --- /dev/null +++ b/.changeset/lets-go-travel.md @@ -0,0 +1,5 @@ +--- +"@firebase/util": major +--- + +Internal changes to validation APIs. diff --git a/packages/database/src/api/Reference.ts b/packages/database/src/api/Reference.ts index d5862154df3..e918b3020f9 100644 --- a/packages/database/src/api/Reference.ts +++ b/packages/database/src/api/Reference.ts @@ -123,30 +123,27 @@ export class DataSnapshot implements Compat { /** * Returns a DataSnapshot of the specified child node's contents. * - * @param childPathString Path to a child. + * @param path Path to a child. * @return DataSnapshot for child node. */ - child(childPathString: string): DataSnapshot { + child(path: 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); - return new DataSnapshot( - this._database, - this._delegate.child(childPathString) - ); + path = String(path); + validatePathString('DataSnapshot.child', 'path', path, false); + return new DataSnapshot(this._database, this._delegate.child(path)); } /** * Returns whether the snapshot contains a child at the specified path. * - * @param childPathString Path to a child. + * @param path Path to a child. * @return Whether the child exists. */ - hasChild(childPathString: string): boolean { + hasChild(path: string): boolean { validateArgCount('DataSnapshot.hasChild', 1, 1, arguments.length); - validatePathString('DataSnapshot.hasChild', 1, childPathString, false); - return this._delegate.hasChild(childPathString); + validatePathString('DataSnapshot.hasChild', 'path', path, false); + return this._delegate.hasChild(path); } /** @@ -169,7 +166,7 @@ export class DataSnapshot implements Compat { */ forEach(action: (snapshot: DataSnapshot) => boolean | void): boolean { validateArgCount('DataSnapshot.forEach', 1, 1, arguments.length); - validateCallback('DataSnapshot.forEach', 1, action, false); + validateCallback('DataSnapshot.forEach', 'action', action, false); return this._delegate.forEach(expDataSnapshot => action(new DataSnapshot(this._database, expDataSnapshot)) ); @@ -231,7 +228,7 @@ export class Query implements Compat { context?: object | null ): SnapshotCallback { validateArgCount('Query.on', 2, 4, arguments.length); - validateCallback('Query.on', 2, callback, false); + validateCallback('Query.on', 'callback', callback, false); const ret = Query.getCancelAndContextArgs_( 'Query.on', @@ -267,7 +264,7 @@ export class Query implements Compat { return callback; default: throw new Error( - errorPrefix('Query.on', 1, false) + + errorPrefix('Query.on', 'eventType') + 'must be a valid event type = "value", "child_added", "child_removed", ' + '"child_changed", or "child_moved".' ); @@ -280,9 +277,9 @@ export class Query implements Compat { context?: object | null ): void { validateArgCount('Query.off', 0, 3, arguments.length); - validateEventType('Query.off', 1, eventType, true); - validateCallback('Query.off', 2, callback, true); - validateContextObject('Query.off', 3, context, true); + validateEventType('Query.off', eventType, true); + validateCallback('Query.off', 'callback', callback, true); + validateContextObject('Query.off', 'context', context, true); if (callback) { const valueCallback: UserCallback = () => {}; valueCallback.userCallback = callback; @@ -307,12 +304,12 @@ export class Query implements Compat { */ once( eventType: string, - userCallback?: SnapshotCallback, + callback?: SnapshotCallback, failureCallbackOrContext?: ((a: Error) => void) | object | null, context?: object | null ): Promise { validateArgCount('Query.once', 1, 4, arguments.length); - validateCallback('Query.once', 2, userCallback, true); + validateCallback('Query.once', 'callback', callback, true); const ret = Query.getCancelAndContextArgs_( 'Query.on', @@ -322,12 +319,12 @@ export class Query implements Compat { const deferred = new Deferred(); const valueCallback: UserCallback = (expSnapshot, previousChildName?) => { const result = new DataSnapshot(this.database, expSnapshot); - if (userCallback) { - userCallback.call(ret.context, result, previousChildName); + if (callback) { + callback.call(ret.context, result, previousChildName); } deferred.resolve(result); }; - valueCallback.userCallback = userCallback; + valueCallback.userCallback = callback; valueCallback.context = ret.context; const cancelCallback = (error: Error) => { if (ret.cancel) { @@ -364,7 +361,7 @@ export class Query implements Compat { break; default: throw new Error( - errorPrefix('Query.once', 1, false) + + errorPrefix('Query.once', 'eventType') + 'must be a valid event type = "value", "child_added", "child_removed", ' + '"child_changed", or "child_moved".' ); @@ -519,10 +516,10 @@ export class Query implements Compat { } = { cancel: undefined, context: undefined }; if (cancelOrContext && context) { ret.cancel = cancelOrContext as (a: Error) => void; - validateCallback(fnName, 3, ret.cancel, true); + validateCallback(fnName, 'cancel', ret.cancel, true); ret.context = context; - validateContextObject(fnName, 4, ret.context, true); + validateContextObject(fnName, 'context', ret.context, true); } else if (cancelOrContext) { // we have either a cancel callback or a context. if (typeof cancelOrContext === 'object' && cancelOrContext !== null) { @@ -532,7 +529,7 @@ export class Query implements Compat { ret.cancel = cancelOrContext as (a: Error) => void; } else { throw new Error( - errorPrefix(fnName, 3, true) + + errorPrefix(fnName, 'cancelOrContext') + ' must either be a cancel callback or a context object.' ); } @@ -598,7 +595,7 @@ export class Reference extends Query implements Compat { onComplete?: (error: Error | null) => void ): Promise { validateArgCount('Reference.set', 1, 2, arguments.length); - validateCallback('Reference.set', 2, onComplete, true); + validateCallback('Reference.set', 'onComplete', onComplete, true); const result = set(this._delegate, newVal); if (onComplete) { result.then( @@ -610,17 +607,17 @@ export class Reference extends Query implements Compat { } update( - objectToMerge: object, + values: object, onComplete?: (a: Error | null) => void ): Promise { validateArgCount('Reference.update', 1, 2, arguments.length); - if (Array.isArray(objectToMerge)) { + if (Array.isArray(values)) { const newObjectToMerge: { [k: string]: unknown } = {}; - for (let i = 0; i < objectToMerge.length; ++i) { - newObjectToMerge['' + i] = objectToMerge[i]; + for (let i = 0; i < values.length; ++i) { + newObjectToMerge['' + i] = values[i]; } - objectToMerge = newObjectToMerge; + values = newObjectToMerge; warn( 'Passing an Array to Firebase.update() is deprecated. ' + 'Use set() if you want to overwrite the existing data, or ' + @@ -629,9 +626,9 @@ export class Reference extends Query implements Compat { ); } validateWritablePath('Reference.update', this._delegate._path); - validateCallback('Reference.update', 2, onComplete, true); + validateCallback('Reference.update', 'onComplete', onComplete, true); - const result = update(this._delegate, objectToMerge); + const result = update(this._delegate, values); if (onComplete) { result.then( () => onComplete(null), @@ -647,7 +644,12 @@ export class Reference extends Query implements Compat { onComplete?: (a: Error | null) => void ): Promise { validateArgCount('Reference.setWithPriority', 2, 3, arguments.length); - validateCallback('Reference.setWithPriority', 3, onComplete, true); + validateCallback( + 'Reference.setWithPriority', + 'onComplete', + onComplete, + true + ); const result = setWithPriority(this._delegate, newVal, newPriority); if (onComplete) { @@ -661,7 +663,7 @@ export class Reference extends Query implements Compat { remove(onComplete?: (a: Error | null) => void): Promise { validateArgCount('Reference.remove', 0, 1, arguments.length); - validateCallback('Reference.remove', 1, onComplete, true); + validateCallback('Reference.remove', 'onComplete', onComplete, true); const result = remove(this._delegate); if (onComplete) { @@ -683,9 +685,19 @@ export class Reference extends Query implements Compat { applyLocally?: boolean ): Promise { validateArgCount('Reference.transaction', 1, 3, arguments.length); - validateCallback('Reference.transaction', 1, transactionUpdate, false); - validateCallback('Reference.transaction', 2, onComplete, true); - validateBoolean('Reference.transaction', 3, applyLocally, true); + validateCallback( + 'Reference.transaction', + 'transactionUpdate', + transactionUpdate, + false + ); + validateCallback('Reference.transaction', 'onComplete', onComplete, true); + validateBoolean( + 'Reference.transaction', + 'applyLocally', + applyLocally, + true + ); const result = runTransaction(this._delegate, transactionUpdate, { applyLocally @@ -715,7 +727,7 @@ export class Reference extends Query implements Compat { onComplete?: (a: Error | null) => void ): Promise { validateArgCount('Reference.setPriority', 1, 2, arguments.length); - validateCallback('Reference.setPriority', 2, onComplete, true); + validateCallback('Reference.setPriority', 'onComplete', onComplete, true); const result = setPriority(this._delegate, priority); if (onComplete) { @@ -729,7 +741,7 @@ export class Reference extends Query implements Compat { push(value?: unknown, onComplete?: (a: Error | null) => void): Reference { validateArgCount('Reference.push', 0, 2, arguments.length); - validateCallback('Reference.push', 2, onComplete, true); + validateCallback('Reference.push', 'onComplete', onComplete, true); const expPromise = push(this._delegate, value); const promise = expPromise.then( diff --git a/packages/database/src/api/onDisconnect.ts b/packages/database/src/api/onDisconnect.ts index fbeb27674cc..82ea5858e43 100644 --- a/packages/database/src/api/onDisconnect.ts +++ b/packages/database/src/api/onDisconnect.ts @@ -26,7 +26,7 @@ export class OnDisconnect implements Compat { cancel(onComplete?: (a: Error | null) => void): Promise { validateArgCount('OnDisconnect.cancel', 0, 1, arguments.length); - validateCallback('OnDisconnect.cancel', 1, onComplete, true); + validateCallback('OnDisconnect.cancel', 'onComplete', onComplete, true); const result = this._delegate.cancel(); if (onComplete) { result.then( @@ -39,7 +39,7 @@ export class OnDisconnect implements Compat { remove(onComplete?: (a: Error | null) => void): Promise { validateArgCount('OnDisconnect.remove', 0, 1, arguments.length); - validateCallback('OnDisconnect.remove', 1, onComplete, true); + validateCallback('OnDisconnect.remove', 'onComplete', onComplete, true); const result = this._delegate.remove(); if (onComplete) { result.then( @@ -52,7 +52,7 @@ export class OnDisconnect implements Compat { set(value: unknown, onComplete?: (a: Error | null) => void): Promise { validateArgCount('OnDisconnect.set', 1, 2, arguments.length); - validateCallback('OnDisconnect.set', 2, onComplete, true); + validateCallback('OnDisconnect.set', 'onComplete', onComplete, true); const result = this._delegate.set(value); if (onComplete) { result.then( @@ -69,7 +69,12 @@ export class OnDisconnect implements Compat { onComplete?: (a: Error | null) => void ): Promise { validateArgCount('OnDisconnect.setWithPriority', 2, 3, arguments.length); - validateCallback('OnDisconnect.setWithPriority', 3, onComplete, true); + validateCallback( + 'OnDisconnect.setWithPriority', + 'onComplete', + onComplete, + true + ); const result = this._delegate.setWithPriority(value, priority); if (onComplete) { result.then( @@ -96,7 +101,7 @@ export class OnDisconnect implements Compat { 'existing data, or an Object with integer keys if you really do want to only update some of the children.' ); } - validateCallback('OnDisconnect.update', 2, onComplete, true); + validateCallback('OnDisconnect.update', 'onComplete', onComplete, true); const result = this._delegate.update(objectToMerge); if (onComplete) { result.then( diff --git a/packages/database/src/core/util/validation.ts b/packages/database/src/core/util/validation.ts index 2a50b13d0f7..356f03f3ac2 100644 --- a/packages/database/src/core/util/validation.ts +++ b/packages/database/src/core/util/validation.ts @@ -95,20 +95,15 @@ export const isValidPriority = function (priority: unknown): boolean { */ export const validateFirebaseDataArg = function ( fnName: string, - argumentNumber: number, - data: unknown, + value: unknown, path: Path, optional: boolean ) { - if (optional && data === undefined) { + if (optional && value === undefined) { return; } - validateFirebaseData( - errorPrefixFxn(fnName, argumentNumber, optional), - data, - path - ); + validateFirebaseData(errorPrefixFxn(fnName, 'value'), value, path); }; /** @@ -257,7 +252,6 @@ export const validateFirebaseMergePaths = function ( */ export const validateFirebaseMergeDataArg = function ( fnName: string, - argumentNumber: number, data: unknown, path: Path, optional: boolean @@ -266,7 +260,7 @@ export const validateFirebaseMergeDataArg = function ( return; } - const errorPrefix = errorPrefixFxn(fnName, argumentNumber, optional); + const errorPrefix = errorPrefixFxn(fnName, 'values'); if (!(data && typeof data === 'object') || Array.isArray(data)) { throw new Error( @@ -296,7 +290,6 @@ export const validateFirebaseMergeDataArg = function ( export const validatePriority = function ( fnName: string, - argumentNumber: number, priority: unknown, optional: boolean ) { @@ -305,7 +298,7 @@ export const validatePriority = function ( } if (isInvalidJSONNumber(priority)) { throw new Error( - errorPrefixFxn(fnName, argumentNumber, optional) + + errorPrefixFxn(fnName, 'priority') + 'is ' + priority.toString() + ', but must be a valid Firebase priority (a string, finite number, ' + @@ -315,7 +308,7 @@ export const validatePriority = function ( // Special case to allow importing data with a .sv. if (!isValidPriority(priority)) { throw new Error( - errorPrefixFxn(fnName, argumentNumber, optional) + + errorPrefixFxn(fnName, 'priority') + 'must be a valid Firebase priority ' + '(a string, finite number, server value, or null).' ); @@ -324,7 +317,6 @@ export const validatePriority = function ( export const validateEventType = function ( fnName: string, - argumentNumber: number, eventType: string, optional: boolean ) { @@ -341,7 +333,7 @@ export const validateEventType = function ( break; default: throw new Error( - errorPrefixFxn(fnName, argumentNumber, optional) + + errorPrefixFxn(fnName, 'eventType') + 'must be a valid event type = "value", "child_added", "child_removed", ' + '"child_changed", or "child_moved".' ); @@ -350,7 +342,7 @@ export const validateEventType = function ( export const validateKey = function ( fnName: string, - argumentNumber: number, + argumentName: string, key: string, optional: boolean ) { @@ -359,7 +351,7 @@ export const validateKey = function ( } if (!isValidKey(key)) { throw new Error( - errorPrefixFxn(fnName, argumentNumber, optional) + + errorPrefixFxn(fnName, argumentName) + 'was an invalid key = "' + key + '". Firebase keys must be non-empty strings and ' + @@ -370,7 +362,7 @@ export const validateKey = function ( export const validatePathString = function ( fnName: string, - argumentNumber: number, + argumentName: string, pathString: string, optional: boolean ) { @@ -380,7 +372,7 @@ export const validatePathString = function ( if (!isValidPathString(pathString)) { throw new Error( - errorPrefixFxn(fnName, argumentNumber, optional) + + errorPrefixFxn(fnName, argumentName) + 'was an invalid path = "' + pathString + '". Paths must be non-empty strings and ' + @@ -391,7 +383,7 @@ export const validatePathString = function ( export const validateRootPathString = function ( fnName: string, - argumentNumber: number, + argumentName: string, pathString: string, optional: boolean ) { @@ -400,7 +392,7 @@ export const validateRootPathString = function ( pathString = pathString.replace(/^\/*\.info(\/|$)/, '/'); } - validatePathString(fnName, argumentNumber, pathString, optional); + validatePathString(fnName, argumentName, pathString, optional); }; export const validateWritablePath = function (fnName: string, path: Path) { @@ -411,7 +403,6 @@ export const validateWritablePath = function (fnName: string, path: Path) { export const validateUrl = function ( fnName: string, - argumentNumber: number, parsedUrl: { repoInfo: RepoInfo; path: Path } ) { // TODO = Validate server better. @@ -424,33 +415,16 @@ export const validateUrl = function ( (pathString.length !== 0 && !isValidRootPathString(pathString)) ) { throw new Error( - errorPrefixFxn(fnName, argumentNumber, false) + + errorPrefixFxn(fnName, 'url') + 'must be a valid firebase URL and ' + 'the path can\'t contain ".", "#", "$", "[", or "]".' ); } }; -export const validateCredential = function ( - fnName: string, - argumentNumber: number, - cred: unknown, - optional: boolean -) { - if (optional && cred === undefined) { - return; - } - if (!(typeof cred === 'string')) { - throw new Error( - errorPrefixFxn(fnName, argumentNumber, optional) + - 'must be a valid credential (a string).' - ); - } -}; - export const validateBoolean = function ( fnName: string, - argumentNumber: number, + argumentName: string, bool: unknown, optional: boolean ) { @@ -459,14 +433,14 @@ export const validateBoolean = function ( } if (typeof bool !== 'boolean') { throw new Error( - errorPrefixFxn(fnName, argumentNumber, optional) + 'must be a boolean.' + errorPrefixFxn(fnName, argumentName) + 'must be a boolean.' ); } }; export const validateString = function ( fnName: string, - argumentNumber: number, + argumentName: string, string: unknown, optional: boolean ) { @@ -475,15 +449,14 @@ export const validateString = function ( } if (!(typeof string === 'string')) { throw new Error( - errorPrefixFxn(fnName, argumentNumber, optional) + - 'must be a valid string.' + errorPrefixFxn(fnName, argumentName) + 'must be a valid string.' ); } }; export const validateObject = function ( fnName: string, - argumentNumber: number, + argumentName: string, obj: unknown, optional: boolean ) { @@ -492,15 +465,14 @@ export const validateObject = function ( } if (!(obj && typeof obj === 'object') || obj === null) { throw new Error( - errorPrefixFxn(fnName, argumentNumber, optional) + - 'must be a valid object.' + errorPrefixFxn(fnName, argumentName) + 'must be a valid object.' ); } }; export const validateObjectContainsKey = function ( fnName: string, - argumentNumber: number, + argumentName: string, obj: unknown, key: string, optional: boolean, @@ -515,7 +487,7 @@ export const validateObjectContainsKey = function ( return; } else { throw new Error( - errorPrefixFxn(fnName, argumentNumber, optional) + + errorPrefixFxn(fnName, argumentName) + 'must contain the key "' + key + '"' @@ -535,7 +507,7 @@ export const validateObjectContainsKey = function ( ) { if (optional) { throw new Error( - errorPrefixFxn(fnName, argumentNumber, optional) + + errorPrefixFxn(fnName, argumentName) + 'contains invalid value for key "' + key + '" (must be of type "' + @@ -544,7 +516,7 @@ export const validateObjectContainsKey = function ( ); } else { throw new Error( - errorPrefixFxn(fnName, argumentNumber, optional) + + errorPrefixFxn(fnName, argumentName) + 'must contain the key "' + key + '" with type "' + diff --git a/packages/database/src/exp/Database.ts b/packages/database/src/exp/Database.ts index a8b86bdc183..db01fe5bbb3 100644 --- a/packages/database/src/exp/Database.ts +++ b/packages/database/src/exp/Database.ts @@ -132,7 +132,7 @@ export function repoManagerDatabaseFromApp( ? new EmulatorAdminTokenProvider() : new FirebaseAuthTokenProvider(app.name, app.options, authProvider); - validateUrl('Invalid Firebase Database URL', 1, parsedUrl); + validateUrl('Invalid Firebase Database URL', parsedUrl); if (!pathIsEmpty(parsedUrl.path)) { fatal( 'Database URL must point to the root of a Firebase Database ' + diff --git a/packages/database/src/exp/OnDisconnect.ts b/packages/database/src/exp/OnDisconnect.ts index 8441cae5aab..efb57f9eda9 100644 --- a/packages/database/src/exp/OnDisconnect.ts +++ b/packages/database/src/exp/OnDisconnect.ts @@ -60,7 +60,7 @@ export class OnDisconnect { set(value: unknown): Promise { validateWritablePath('OnDisconnect.set', this._path); - validateFirebaseDataArg('OnDisconnect.set', 1, value, this._path, false); + validateFirebaseDataArg('OnDisconnect.set', value, this._path, false); const deferred = new Deferred(); repoOnDisconnectSet( this._repo, @@ -78,12 +78,11 @@ export class OnDisconnect { validateWritablePath('OnDisconnect.setWithPriority', this._path); validateFirebaseDataArg( 'OnDisconnect.setWithPriority', - 1, value, this._path, false ); - validatePriority('OnDisconnect.setWithPriority', 2, priority, false); + validatePriority('OnDisconnect.setWithPriority', priority, false); const deferred = new Deferred(); repoOnDisconnectSetWithPriority( @@ -96,12 +95,11 @@ export class OnDisconnect { return deferred.promise; } - update(objectToMerge: Indexable): Promise { + update(values: Indexable): Promise { validateWritablePath('OnDisconnect.update', this._path); validateFirebaseMergeDataArg( 'OnDisconnect.update', - 1, - objectToMerge, + values, this._path, false ); @@ -109,7 +107,7 @@ export class OnDisconnect { repoOnDisconnectUpdate( this._repo, this._path, - objectToMerge, + values, deferred.wrapCallback(() => {}) ); return deferred.promise; diff --git a/packages/database/src/exp/Reference_impl.ts b/packages/database/src/exp/Reference_impl.ts index 35bec7fe164..948ad2e3cc7 100644 --- a/packages/database/src/exp/Reference_impl.ts +++ b/packages/database/src/exp/Reference_impl.ts @@ -357,7 +357,7 @@ export function refFromURL(db: FirebaseDatabase, url: string): ReferenceImpl { db = getModularInstance(db); db._checkNotDeleted('refFromURL'); const parsedURL = parseRepoInfo(url, db._repo.repoInfo_.nodeAdmin); - validateUrl('refFromURL', 1, parsedURL); + validateUrl('refFromURL', parsedURL); const repoInfo = parsedURL.repoInfo; if ( @@ -381,10 +381,9 @@ export function refFromURL(db: FirebaseDatabase, url: string): ReferenceImpl { export function child(ref: Reference, path: string): ReferenceImpl { ref = getModularInstance(ref); if (pathGetFront(ref._path) === null) { - // TODO(database-exp): Remove argument numbers from all error messages - validateRootPathString('child', 1, path, false); + validateRootPathString('child', 'path', path, false); } else { - validatePathString('child', 1, path, false); + validatePathString('child', 'path', path, false); } return new ReferenceImpl(ref._repo, pathChild(ref._path, path)); } @@ -401,7 +400,7 @@ export interface ThenableReferenceImpl export function push(ref: Reference, value?: unknown): ThenableReferenceImpl { ref = getModularInstance(ref); validateWritablePath('push', ref._path); - validateFirebaseDataArg('push', 1, value, ref._path, true); + validateFirebaseDataArg('push', value, ref._path, true); const now = repoServerTime(ref._repo); const name = nextPushId(now); @@ -434,7 +433,7 @@ export function remove(ref: Reference): Promise { export function set(ref: Reference, value: unknown): Promise { ref = getModularInstance(ref); validateWritablePath('set', ref._path); - validateFirebaseDataArg('set', 1, value, ref._path, false); + validateFirebaseDataArg('set', value, ref._path, false); const deferred = new Deferred(); repoSetWithPriority( ref._repo, @@ -452,7 +451,7 @@ export function setPriority( ): Promise { ref = getModularInstance(ref); validateWritablePath('setPriority', ref._path); - validatePriority('setPriority', 1, priority, false); + validatePriority('setPriority', priority, false); const deferred = new Deferred(); repoSetWithPriority( ref._repo, @@ -470,9 +469,8 @@ export function setWithPriority( priority: string | number | null ): Promise { validateWritablePath('setWithPriority', ref._path); - validateFirebaseDataArg('setWithPriority', 1, value, ref._path, false); - validatePriority('setWithPriority', 2, priority, false); - + validateFirebaseDataArg('setWithPriority', value, ref._path, false); + validatePriority('setWithPriority', priority, false); if (ref.key === '.length' || ref.key === '.keys') { throw 'setWithPriority failed: ' + ref.key + ' is a read-only object.'; } @@ -489,7 +487,7 @@ export function setWithPriority( } export function update(ref: Reference, values: object): Promise { - validateFirebaseMergeDataArg('update', 1, values, ref._path, false); + validateFirebaseMergeDataArg('update', values, ref._path, false); const deferred = new Deferred(); repoUpdate( ref._repo, @@ -1006,7 +1004,7 @@ class QueryEndAtConstraint extends QueryConstraint { } _apply(query: QueryImpl): QueryImpl { - validateFirebaseDataArg('endAt', 1, this._value, query._path, true); + validateFirebaseDataArg('endAt', this._value, query._path, true); const newParams = queryParamsEndAt( query._queryParams, this._value, @@ -1033,7 +1031,7 @@ export function endAt( value: number | string | boolean | null, key?: string ): QueryConstraint { - validateKey('endAt', 2, key, true); + validateKey('endAt', 'key', key, true); return new QueryEndAtConstraint(value, key); } @@ -1048,7 +1046,7 @@ class QueryEndBeforeConstraint extends QueryConstraint { } _apply(query: QueryImpl): QueryImpl { - validateFirebaseDataArg('endBefore', 1, this._value, query._path, false); + validateFirebaseDataArg('endBefore', this._value, query._path, false); const newParams = queryParamsEndBefore( query._queryParams, this._value, @@ -1075,7 +1073,7 @@ export function endBefore( value: number | string | boolean | null, key?: string ): QueryConstraint { - validateKey('endBefore', 2, key, true); + validateKey('endBefore', 'key', key, true); return new QueryEndBeforeConstraint(value, key); } @@ -1090,7 +1088,7 @@ class QueryStartAtConstraint extends QueryConstraint { } _apply(query: QueryImpl): QueryImpl { - validateFirebaseDataArg('startAt', 1, this._value, query._path, true); + validateFirebaseDataArg('startAt', this._value, query._path, true); const newParams = queryParamsStartAt( query._queryParams, this._value, @@ -1117,7 +1115,7 @@ export function startAt( value: number | string | boolean | null = null, key?: string ): QueryConstraint { - validateKey('startAt', 2, key, true); + validateKey('startAt', 'key', key, true); return new QueryStartAtConstraint(value, key); } @@ -1132,7 +1130,7 @@ class QueryStartAfterConstraint extends QueryConstraint { } _apply(query: QueryImpl): QueryImpl { - validateFirebaseDataArg('startAfter', 1, this._value, query._path, false); + validateFirebaseDataArg('startAfter', this._value, query._path, false); const newParams = queryParamsStartAfter( query._queryParams, this._value, @@ -1159,7 +1157,7 @@ export function startAfter( value: number | string | boolean | null, key?: string ): QueryConstraint { - validateKey('startAfter', 2, key, true); + validateKey('startAfter', 'key', key, true); return new QueryStartAfterConstraint(value, key); } @@ -1266,7 +1264,7 @@ export function orderByChild(path: string): QueryConstraint { 'orderByChild: "$value" is invalid. Use orderByValue() instead.' ); } - validatePathString('orderByChild', 1, path, false); + validatePathString('orderByChild', 'path', path, false); return new QueryOrderByChildConstraint(path); } @@ -1341,7 +1339,7 @@ class QueryEqualToValueConstraint extends QueryConstraint { } _apply(query: QueryImpl): QueryImpl { - validateFirebaseDataArg('equalTo', 1, this._value, query._path, false); + validateFirebaseDataArg('equalTo', this._value, query._path, false); if (query._queryParams.hasStart()) { throw new Error( 'equalTo: Starting point was already set (by another call to startAt/startAfter or ' + @@ -1364,7 +1362,7 @@ export function equalTo( value: number | string | boolean | null, key?: string ): QueryConstraint { - validateKey('equalTo', 2, key, true); + validateKey('equalTo', 'key', key, true); return new QueryEqualToValueConstraint(value, key); } diff --git a/packages/database/test/exp/integration.test.ts b/packages/database/test/exp/integration.test.ts index 36f90e971d3..9790c88d24c 100644 --- a/packages/database/test/exp/integration.test.ts +++ b/packages/database/test/exp/integration.test.ts @@ -27,15 +27,15 @@ import { ref, refFromURL } from '../../exp/index'; -import { set } from '../../src/exp/Reference_impl'; +import { onValue, set } from '../../src/exp/Reference_impl'; +import { EventAccumulatorFactory } from '../helpers/EventAccumulator'; import { DATABASE_ADDRESS, DATABASE_URL } from '../helpers/util'; export function createTestApp() { return initializeApp({ databaseURL: DATABASE_URL }); } -// TODO(database-exp): Re-enable these tests -describe.skip('Database Tests', () => { +describe('Database@exp Tests', () => { let defaultApp; beforeEach(() => { @@ -65,7 +65,7 @@ describe.skip('Database Tests', () => { expect(db.app).to.equal(defaultApp); }); - it('Can set and ge tref', async () => { + it('Can set and get ref', async () => { const db = getDatabase(defaultApp); await set(ref(db, 'foo/bar'), 'foobar'); const snap = await get(ref(db, 'foo/bar')); @@ -77,6 +77,60 @@ describe.skip('Database Tests', () => { await get(refFromURL(db, `${DATABASE_ADDRESS}/foo/bar`)); }); + it('Can get updates', async () => { + const db = getDatabase(defaultApp); + const fooRef = ref(db, 'foo'); + + const ea = EventAccumulatorFactory.waitsForCount(2); + onValue(fooRef, snap => { + ea.addEvent(snap.val()); + }); + + await set(fooRef, 'a'); + await set(fooRef, 'b'); + + const [snap1, snap2] = await ea.promise; + expect(snap1).to.equal('a'); + expect(snap2).to.equal('b'); + }); + + it('Can use onlyOnce', async () => { + const db = getDatabase(defaultApp); + const fooRef = ref(db, 'foo'); + + const ea = EventAccumulatorFactory.waitsForCount(1); + onValue( + fooRef, + snap => { + ea.addEvent(snap.val()); + }, + { onlyOnce: true } + ); + + await set(fooRef, 'a'); + await set(fooRef, 'b'); + + const [snap1] = await ea.promise; + expect(snap1).to.equal('a'); + }); + + it('Can unsubscribe', async () => { + const db = getDatabase(defaultApp); + const fooRef = ref(db, 'foo'); + + const ea = EventAccumulatorFactory.waitsForCount(1); + const unsubscribe = onValue(fooRef, snap => { + ea.addEvent(snap.val()); + }); + + await set(fooRef, 'a'); + unsubscribe(); + await set(fooRef, 'b'); + + const [snap1] = await ea.promise; + expect(snap1).to.equal('a'); + }); + it('Can goOffline/goOnline', async () => { const db = getDatabase(defaultApp); goOffline(db); diff --git a/packages/util/src/validation.ts b/packages/util/src/validation.ts index a4751801ff2..372144b6645 100644 --- a/packages/util/src/validation.ts +++ b/packages/util/src/validation.ts @@ -53,39 +53,11 @@ export const validateArgCount = function ( * Generates a string to prefix an error message about failed argument validation * * @param fnName The function name - * @param argumentNumber The index of the argument - * @param optional Whether or not the argument is optional + * @param argName The name of the argument * @return The prefix to add to the error thrown for validation. */ -export function errorPrefix( - fnName: string, - argumentNumber: number, - optional: boolean -): string { - let argName = ''; - switch (argumentNumber) { - case 1: - argName = optional ? 'first' : 'First'; - break; - case 2: - argName = optional ? 'second' : 'Second'; - break; - case 3: - argName = optional ? 'third' : 'Third'; - break; - case 4: - argName = optional ? 'fourth' : 'Fourth'; - break; - default: - throw new Error( - 'errorPrefix called with argumentNumber > 4. Need to update it?' - ); - } - - let error = fnName + ' failed: '; - - error += argName + ' argument '; - return error; +export function errorPrefix(fnName: string, argName: string): string { + return `${fnName} failed: ${argName} argument `; } /** @@ -96,7 +68,6 @@ export function errorPrefix( */ export function validateNamespace( fnName: string, - argumentNumber: number, namespace: string, optional: boolean ): void { @@ -106,15 +77,14 @@ export function validateNamespace( if (typeof namespace !== 'string') { //TODO: I should do more validation here. We only allow certain chars in namespaces. throw new Error( - errorPrefix(fnName, argumentNumber, optional) + - 'must be a valid firebase namespace.' + errorPrefix(fnName, 'namespace') + 'must be a valid firebase namespace.' ); } } export function validateCallback( fnName: string, - argumentNumber: number, + argumentName: string, // eslint-disable-next-line @typescript-eslint/ban-types callback: Function, optional: boolean @@ -124,15 +94,14 @@ export function validateCallback( } if (typeof callback !== 'function') { throw new Error( - errorPrefix(fnName, argumentNumber, optional) + - 'must be a valid function.' + errorPrefix(fnName, argumentName) + 'must be a valid function.' ); } } export function validateContextObject( fnName: string, - argumentNumber: number, + argumentName: string, context: unknown, optional: boolean ): void { @@ -141,8 +110,7 @@ export function validateContextObject( } if (typeof context !== 'object' || context === null) { throw new Error( - errorPrefix(fnName, argumentNumber, optional) + - 'must be a valid context object.' + errorPrefix(fnName, argumentName) + 'must be a valid context object.' ); } } From dd562b0335c9a17d34d51d39dabce5527a705beb Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Mon, 5 Apr 2021 19:22:14 -0600 Subject: [PATCH 11/15] Update changeset --- .changeset/lets-go-travel.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/lets-go-travel.md b/.changeset/lets-go-travel.md index 1731f975ff3..59f341aa35d 100644 --- a/.changeset/lets-go-travel.md +++ b/.changeset/lets-go-travel.md @@ -1,5 +1,7 @@ --- +"firebase": minor "@firebase/util": major +"@firebase/database": patch --- -Internal changes to validation APIs. +Internal changes to Database and Validation APIs. From b1d42b9c1222095a061d1767eab57e14b59bd4ff Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Tue, 6 Apr 2021 13:12:23 -0600 Subject: [PATCH 12/15] Add database@exp API docs (#4738) --- packages/database/src/api/Database.ts | 4 +- packages/database/src/api/Reference.ts | 28 +- packages/database/src/core/CompoundWrite.ts | 12 +- packages/database/src/core/Repo.ts | 10 +- .../database/src/core/SparseSnapshotTree.ts | 4 +- packages/database/src/core/SyncPoint.ts | 8 +- packages/database/src/core/SyncTree.ts | 26 +- packages/database/src/core/WriteTree.ts | 2 +- packages/database/src/core/snap/LeafNode.ts | 2 +- packages/database/src/core/snap/Node.ts | 32 +- .../database/src/core/snap/indexes/Index.ts | 10 +- .../src/core/snap/indexes/KeyIndex.ts | 2 +- .../src/core/snap/indexes/PriorityIndex.ts | 2 +- .../src/core/snap/indexes/ValueIndex.ts | 2 +- .../src/core/storage/DOMStorageWrapper.ts | 2 +- packages/database/src/core/storage/storage.ts | 2 +- .../database/src/core/util/EventEmitter.ts | 2 +- .../database/src/core/util/ImmutableTree.ts | 10 +- packages/database/src/core/util/Path.ts | 12 +- packages/database/src/core/util/SortedMap.ts | 76 +- packages/database/src/core/util/Tree.ts | 12 +- .../database/src/core/util/libs/parser.ts | 2 +- packages/database/src/core/util/util.ts | 10 +- packages/database/src/core/view/Event.ts | 4 + .../database/src/core/view/QueryParams.ts | 6 +- packages/database/src/core/view/View.ts | 2 +- packages/database/src/exp/Database.ts | 67 +- packages/database/src/exp/OnDisconnect.ts | 81 ++ packages/database/src/exp/Reference.ts | 91 ++ packages/database/src/exp/Reference_impl.ts | 1002 ++++++++++++++++- packages/database/src/exp/ServerValue.ts | 12 + packages/database/src/exp/Transaction.ts | 44 + .../database/src/realtime/TransportManager.ts | 4 +- .../src/realtime/WebSocketConnection.ts | 4 +- packages/database/test/datasnapshot.test.ts | 2 +- 35 files changed, 1393 insertions(+), 198 deletions(-) diff --git a/packages/database/src/api/Database.ts b/packages/database/src/api/Database.ts index 468bf9153b1..2649cad1b47 100644 --- a/packages/database/src/api/Database.ts +++ b/packages/database/src/api/Database.ts @@ -69,7 +69,7 @@ export class Database implements FirebaseService, Compat { * location. * @throws If a Reference is provided, throws if it does not belong to the * same project. - * @return Firebase reference. + * @returns Firebase reference. */ ref(path?: string): Reference; ref(path?: Reference): Reference; @@ -88,7 +88,7 @@ export class Database implements FirebaseService, Compat { * Returns a reference to the root or the path specified in url. * We throw a exception if the url is not in the same domain as the * current repo. - * @return Firebase reference. + * @returns Firebase reference. */ refFromURL(url: string): Reference { const apiName = 'database.refFromURL'; diff --git a/packages/database/src/api/Reference.ts b/packages/database/src/api/Reference.ts index e918b3020f9..1d1dd3ba1b6 100644 --- a/packages/database/src/api/Reference.ts +++ b/packages/database/src/api/Reference.ts @@ -85,7 +85,7 @@ export class DataSnapshot implements Compat { * Retrieves the snapshot contents as JSON. Returns null if the snapshot is * empty. * - * @return JSON representation of the DataSnapshot contents, or null if empty. + * @returns JSON representation of the DataSnapshot contents, or null if empty. */ val(): unknown { validateArgCount('DataSnapshot.val', 0, 0, arguments.length); @@ -95,7 +95,7 @@ export class DataSnapshot implements Compat { /** * 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. + * @returns JSON representation of the DataSnapshot contents, or null if empty. */ exportVal(): unknown { validateArgCount('DataSnapshot.exportVal', 0, 0, arguments.length); @@ -113,7 +113,7 @@ export class DataSnapshot implements Compat { /** * Returns whether the snapshot contains a non-null value. * - * @return Whether the snapshot contains a non-null value, or is empty. + * @returns Whether the snapshot contains a non-null value, or is empty. */ exists(): boolean { validateArgCount('DataSnapshot.exists', 0, 0, arguments.length); @@ -124,7 +124,7 @@ export class DataSnapshot implements Compat { * Returns a DataSnapshot of the specified child node's contents. * * @param path Path to a child. - * @return DataSnapshot for child node. + * @returns DataSnapshot for child node. */ child(path: string): DataSnapshot { validateArgCount('DataSnapshot.child', 0, 1, arguments.length); @@ -138,7 +138,7 @@ export class DataSnapshot implements Compat { * Returns whether the snapshot contains a child at the specified path. * * @param path Path to a child. - * @return Whether the child exists. + * @returns Whether the child exists. */ hasChild(path: string): boolean { validateArgCount('DataSnapshot.hasChild', 1, 1, arguments.length); @@ -149,7 +149,7 @@ export class DataSnapshot implements Compat { /** * Returns the priority of the object, or null if no priority was set. * - * @return The priority. + * @returns The priority. */ getPriority(): string | number | null { validateArgCount('DataSnapshot.getPriority', 0, 0, arguments.length); @@ -161,7 +161,7 @@ export class DataSnapshot implements Compat { * * @param action Callback function to be called * for each child. - * @return True if forEach was canceled by action returning true for + * @returns True if forEach was canceled by action returning true for * one of the child nodes. */ forEach(action: (snapshot: DataSnapshot) => boolean | void): boolean { @@ -174,7 +174,7 @@ export class DataSnapshot implements Compat { /** * Returns whether this DataSnapshot has children. - * @return True if the DataSnapshot contains 1 or more child nodes. + * @returns True if the DataSnapshot contains 1 or more child nodes. */ hasChildren(): boolean { validateArgCount('DataSnapshot.hasChildren', 0, 0, arguments.length); @@ -187,7 +187,7 @@ export class DataSnapshot implements Compat { /** * Returns the number of children for this DataSnapshot. - * @return The number of children that this DataSnapshot contains. + * @returns The number of children that this DataSnapshot contains. */ numChildren(): number { validateArgCount('DataSnapshot.numChildren', 0, 0, arguments.length); @@ -195,7 +195,7 @@ export class DataSnapshot implements Compat { } /** - * @return The Firebase reference for the location this snapshot's data came + * @returns The Firebase reference for the location this snapshot's data came * from. */ getRef(): Reference { @@ -472,7 +472,7 @@ export class Query implements Compat { } /** - * @return URL for this location. + * @returns URL for this location. */ toString(): string { validateArgCount('Query.toString', 0, 0, arguments.length); @@ -563,7 +563,7 @@ export class Reference extends Query implements Compat { ); } - /** @return {?string} */ + /** @returns {?string} */ getKey(): string | null { validateArgCount('Reference.key', 0, 0, arguments.length); return this._delegate.key; @@ -577,14 +577,14 @@ export class Reference extends Query implements Compat { return new Reference(this.database, child(this._delegate, pathString)); } - /** @return {?Reference} */ + /** @returns {?Reference} */ getParent(): Reference | null { validateArgCount('Reference.parent', 0, 0, arguments.length); const parent = this._delegate.parent; return parent ? new Reference(this.database, parent) : null; } - /** @return {!Reference} */ + /** @returns {!Reference} */ getRoot(): Reference { validateArgCount('Reference.root', 0, 0, arguments.length); return new Reference(this.database, this._delegate.root); diff --git a/packages/database/src/core/CompoundWrite.ts b/packages/database/src/core/CompoundWrite.ts index dffc92a98f6..f19577bd6e0 100644 --- a/packages/database/src/core/CompoundWrite.ts +++ b/packages/database/src/core/CompoundWrite.ts @@ -87,7 +87,7 @@ export function compoundWriteAddWrites( * * @param compoundWrite The CompoundWrite to remove. * @param path The path at which a write and all deeper writes should be removed - * @return The new CompoundWrite with the removed path + * @returns The new CompoundWrite with the removed path */ export function compoundWriteRemoveWrite( compoundWrite: CompoundWrite, @@ -110,7 +110,7 @@ export function compoundWriteRemoveWrite( * * @param compoundWrite The CompoundWrite to check. * @param path The path to check for - * @return Whether there is a complete write at that path + * @returns Whether there is a complete write at that path */ export function compoundWriteHasCompleteWrite( compoundWrite: CompoundWrite, @@ -125,7 +125,7 @@ export function compoundWriteHasCompleteWrite( * * @param compoundWrite The CompoundWrite to get the node from. * @param path The path to get a complete write - * @return The node if complete at that path, or null otherwise. + * @returns The node if complete at that path, or null otherwise. */ export function compoundWriteGetCompleteNode( compoundWrite: CompoundWrite, @@ -145,7 +145,7 @@ export function compoundWriteGetCompleteNode( * Returns all children that are guaranteed to be a complete overwrite. * * @param compoundWrite The CompoundWrite to get children from. - * @return A list of all complete children. + * @returns A list of all complete children. */ export function compoundWriteGetCompleteChildren( compoundWrite: CompoundWrite @@ -192,7 +192,7 @@ export function compoundWriteChildCompoundWrite( /** * Returns true if this CompoundWrite is empty and therefore does not modify any nodes. - * @return Whether this CompoundWrite is empty + * @returns Whether this CompoundWrite is empty */ export function compoundWriteIsEmpty(compoundWrite: CompoundWrite): boolean { return compoundWrite.writeTree_.isEmpty(); @@ -202,7 +202,7 @@ export function compoundWriteIsEmpty(compoundWrite: CompoundWrite): boolean { * Applies this CompoundWrite to a node. The node is returned with all writes from this CompoundWrite applied to the * node * @param node The node to apply this CompoundWrite to - * @return The node with all writes applied + * @returns The node with all writes applied */ export function compoundWriteApply( compoundWrite: CompoundWrite, diff --git a/packages/database/src/core/Repo.ts b/packages/database/src/core/Repo.ts index ab5ff3f5933..13f3d1db196 100644 --- a/packages/database/src/core/Repo.ts +++ b/packages/database/src/core/Repo.ts @@ -193,7 +193,7 @@ export class Repo { } /** - * @return The URL corresponding to the root of this Firebase. + * @returns The URL corresponding to the root of this Firebase. */ toString(): string { return ( @@ -319,7 +319,7 @@ export function repoStart( } /** - * @return The time in milliseconds, taking the server offset into account if we have one. + * @returns The time in milliseconds, taking the server offset into account if we have one. */ export function repoServerTime(repo: Repo): number { const offsetNode = repo.infoData_.getNode(new Path('.info/serverTimeOffset')); @@ -1147,7 +1147,7 @@ function repoSendTransactionQueue( * is the path at which events need to be raised for. * * @param changedPath The path in mergedData that changed. - * @return The rootmost path that was affected by rerunning transactions. + * @returns The rootmost path that was affected by rerunning transactions. */ function repoRerunTransactions(repo: Repo, changedPath: Path): Path { const rootMostTransactionNode = repoGetAncestorTransactionNode( @@ -1329,7 +1329,7 @@ function repoRerunTransactionQueue( * no pending transactions on any ancestor. * * @param path The location to start at. - * @return The rootmost node with a transaction. + * @returns The rootmost node with a transaction. */ function repoGetAncestorTransactionNode( repo: Repo, @@ -1355,7 +1355,7 @@ function repoGetAncestorTransactionNode( * transactionNode. * * @param transactionNode - * @return The generated queue. + * @returns The generated queue. */ function repoBuildTransactionQueue( repo: Repo, diff --git a/packages/database/src/core/SparseSnapshotTree.ts b/packages/database/src/core/SparseSnapshotTree.ts index 6286ff512cb..27a9be9a85a 100644 --- a/packages/database/src/core/SparseSnapshotTree.ts +++ b/packages/database/src/core/SparseSnapshotTree.ts @@ -39,7 +39,7 @@ export function newSparseSnapshotTree(): SparseSnapshotTree { * Only seems to be used in tests. * * @param path Path to look up snapshot for. - * @return The retrieved node, or null. + * @returns The retrieved node, or null. */ export function sparseSnapshotTreeFind( sparseSnapshotTree: SparseSnapshotTree, @@ -94,7 +94,7 @@ export function sparseSnapshotTreeRemember( * Purge the data at path from the cache. * * @param path Path to look up snapshot for. - * @return True if this node should now be removed. + * @returns True if this node should now be removed. */ export function sparseSnapshotTreeForget( sparseSnapshotTree: SparseSnapshotTree, diff --git a/packages/database/src/core/SyncPoint.ts b/packages/database/src/core/SyncPoint.ts index b001865084f..99c0efc5dd4 100644 --- a/packages/database/src/core/SyncPoint.ts +++ b/packages/database/src/core/SyncPoint.ts @@ -119,7 +119,7 @@ export function syncPointApplyOperation( * @param writesCache * @param serverCache * @param serverCacheComplete - * @return Events to raise. + * @returns Events to raise. */ export function syncPointGetView( syncPoint: SyncPoint, @@ -166,7 +166,7 @@ export function syncPointGetView( * @param writesCache * @param serverCache Complete server cache, if we have it. * @param serverCacheComplete - * @return Events to raise. + * @returns Events to raise. */ export function syncPointAddEventRegistration( syncPoint: SyncPoint, @@ -199,7 +199,7 @@ export function syncPointAddEventRegistration( * * @param eventRegistration If null, remove all callbacks. * @param cancelError If a cancelError is provided, appropriate cancel events will be returned. - * @return removed queries and any cancel events + * @returns removed queries and any cancel events */ export function syncPointRemoveEventRegistration( syncPoint: SyncPoint, @@ -266,7 +266,7 @@ export function syncPointGetQueryViews(syncPoint: SyncPoint): View[] { /** * @param path The path to the desired complete snapshot - * @return A complete cache, if it exists + * @returns A complete cache, if it exists */ export function syncPointGetCompleteServerCache( syncPoint: SyncPoint, diff --git a/packages/database/src/core/SyncTree.ts b/packages/database/src/core/SyncTree.ts index 6d7c0da8526..700a6fe126c 100644 --- a/packages/database/src/core/SyncTree.ts +++ b/packages/database/src/core/SyncTree.ts @@ -160,7 +160,7 @@ export class SyncTree { /** * Apply the data changes for a user-generated set() or transaction() call. * - * @return Events to raise. + * @returns Events to raise. */ export function syncTreeApplyUserOverwrite( syncTree: SyncTree, @@ -191,7 +191,7 @@ export function syncTreeApplyUserOverwrite( /** * Apply the data from a user-generated update() call * - * @return Events to raise. + * @returns Events to raise. */ export function syncTreeApplyUserMerge( syncTree: SyncTree, @@ -214,7 +214,7 @@ export function syncTreeApplyUserMerge( * Acknowledge a pending user write that was previously registered with applyUserOverwrite() or applyUserMerge(). * * @param revert True if the given write failed and needs to be reverted - * @return Events to raise. + * @returns Events to raise. */ export function syncTreeAckUserWrite( syncTree: SyncTree, @@ -248,7 +248,7 @@ export function syncTreeAckUserWrite( /** * Apply new server data for the specified path.. * - * @return Events to raise. + * @returns Events to raise. */ export function syncTreeApplyServerOverwrite( syncTree: SyncTree, @@ -264,7 +264,7 @@ export function syncTreeApplyServerOverwrite( /** * Apply new server data to be merged in at the specified path. * - * @return Events to raise. + * @returns Events to raise. */ export function syncTreeApplyServerMerge( syncTree: SyncTree, @@ -282,7 +282,7 @@ export function syncTreeApplyServerMerge( /** * Apply a listen complete for a query * - * @return Events to raise. + * @returns Events to raise. */ export function syncTreeApplyListenComplete( syncTree: SyncTree, @@ -297,7 +297,7 @@ export function syncTreeApplyListenComplete( /** * Apply a listen complete for a tagged query * - * @return Events to raise. + * @returns Events to raise. */ export function syncTreeApplyTaggedListenComplete( syncTree: SyncTree, @@ -329,7 +329,7 @@ export function syncTreeApplyTaggedListenComplete( * * @param eventRegistration If null, all callbacks are removed. * @param cancelError If a cancelError is provided, appropriate cancel events will be returned. - * @return Cancel events, if cancelError was provided. + * @returns Cancel events, if cancelError was provided. */ export function syncTreeRemoveEventRegistration( syncTree: SyncTree, @@ -437,7 +437,7 @@ export function syncTreeRemoveEventRegistration( /** * Apply new server data for the specified tagged query. * - * @return Events to raise. + * @returns Events to raise. */ export function syncTreeApplyTaggedQueryOverwrite( syncTree: SyncTree, @@ -466,7 +466,7 @@ export function syncTreeApplyTaggedQueryOverwrite( /** * Apply server data to be merged in for the specified tagged query. * - * @return Events to raise. + * @returns Events to raise. */ export function syncTreeApplyTaggedQueryMerge( syncTree: SyncTree, @@ -496,7 +496,7 @@ export function syncTreeApplyTaggedQueryMerge( /** * Add an event callback for the specified query. * - * @return Events to raise. + * @returns Events to raise. */ export function syncTreeAddEventRegistration( syncTree: SyncTree, @@ -901,7 +901,7 @@ function syncTreeCollectDistinctViewsForSubTree_( /** * Normalizes a query to a query we send the server for listening * - * @return The normalized query + * @returns The normalized query */ function syncTreeQueryForListening_(query: QueryContext): QueryContext { if (query._queryParams.loadsAllData() && !query._queryParams.isDefault()) { @@ -937,7 +937,7 @@ function syncTreeGetNextQueryTag_(): number { /** * For a given new listen, manage the de-duplication of outstanding subscriptions. * - * @return This method can return events to support synchronous data sources + * @returns This method can return events to support synchronous data sources */ function syncTreeSetupListener_( syncTree: SyncTree, diff --git a/packages/database/src/core/WriteTree.ts b/packages/database/src/core/WriteTree.ts index b6e9972aba4..5003dad68a9 100644 --- a/packages/database/src/core/WriteTree.ts +++ b/packages/database/src/core/WriteTree.ts @@ -150,7 +150,7 @@ export function writeTreeGetWrite( * Remove a write (either an overwrite or merge) that has been successfully acknowledge by the server. Recalculates * the tree if necessary. We return true if it may have been visible, meaning views need to reevaluate. * - * @return true if the write may have been visible (meaning we'll need to reevaluate / raise + * @returns true if the write may have been visible (meaning we'll need to reevaluate / raise * events as a result). */ export function writeTreeRemoveWrite( diff --git a/packages/database/src/core/snap/LeafNode.ts b/packages/database/src/core/snap/LeafNode.ts index 9e244e28fd6..e280de54cf7 100644 --- a/packages/database/src/core/snap/LeafNode.ts +++ b/packages/database/src/core/snap/LeafNode.ts @@ -212,7 +212,7 @@ export class LeafNode implements Node { /** * Returns the value of the leaf node. - * @return The value of the node. + * @returns The value of the node. */ getValue(): Indexable | string | number | boolean { return this.value_; diff --git a/packages/database/src/core/snap/Node.ts b/packages/database/src/core/snap/Node.ts index eeaa38b5e0d..0bf423e8af6 100644 --- a/packages/database/src/core/snap/Node.ts +++ b/packages/database/src/core/snap/Node.ts @@ -28,34 +28,34 @@ import { Index } from './indexes/Index'; export interface Node { /** * Whether this node is a leaf node. - * @return Whether this is a leaf node. + * @returns Whether this is a leaf node. */ isLeafNode(): boolean; /** * Gets the priority of the node. - * @return The priority of the node. + * @returns The priority of the node. */ getPriority(): Node; /** * Returns a duplicate node with the new priority. * @param newPriorityNode New priority to set for the node. - * @return Node with new priority. + * @returns Node with new priority. */ updatePriority(newPriorityNode: Node): Node; /** * Returns the specified immediate child, or null if it doesn't exist. * @param childName The name of the child to retrieve. - * @return The retrieved child, or an empty node. + * @returns The retrieved child, or an empty node. */ getImmediateChild(childName: string): Node; /** * Returns a child by path, or null if it doesn't exist. * @param path The path of the child to retrieve. - * @return The retrieved child or an empty node. + * @returns The retrieved child or an empty node. */ getChild(path: Path): Node; @@ -64,7 +64,7 @@ export interface Node { * @param childName The name of the child to find the predecessor of. * @param childNode The node to find the predecessor of. * @param index The index to use to determine the predecessor - * @return The name of the predecessor child, or null if childNode is the first child. + * @returns The name of the predecessor child, or null if childNode is the first child. */ getPredecessorChildName( childName: string, @@ -77,7 +77,7 @@ export interface Node { * Any value in the node will be removed. * @param childName The name of the child to update. * @param newChildNode The new child node - * @return The updated node. + * @returns The updated node. */ updateImmediateChild(childName: string, newChildNode: Node): Node; @@ -86,7 +86,7 @@ export interface Node { * be removed. * @param path The path of the child to update. * @param newChildNode The new child node, which may be an empty node - * @return The updated node. + * @returns The updated node. */ updateChild(path: Path, newChildNode: Node): Node; @@ -96,12 +96,12 @@ export interface Node { hasChild(childName: string): boolean; /** - * @return True if this node has no value or children. + * @returns True if this node has no value or children. */ isEmpty(): boolean; /** - * @return The number of children of this node. + * @returns The number of children of this node. */ numChildren(): number; @@ -109,34 +109,34 @@ export interface Node { * Calls action for each child. * @param action Action to be called for * each child. It's passed the child name and the child node. - * @return The first truthy value return by action, or the last falsey one + * @returns The first truthy value return by action, or the last falsey one */ forEachChild(index: Index, action: (a: string, b: Node) => void): unknown; /** * @param exportFormat True for export format (also wire protocol format). - * @return Value of this node as JSON. + * @returns Value of this node as JSON. */ val(exportFormat?: boolean): unknown; /** - * @return hash representing the node contents. + * @returns hash representing the node contents. */ hash(): string; /** * @param other Another node - * @return -1 for less than, 0 for equal, 1 for greater than other + * @returns -1 for less than, 0 for equal, 1 for greater than other */ compareTo(other: Node): number; /** - * @return Whether or not this snapshot equals other + * @returns Whether or not this snapshot equals other */ equals(other: Node): boolean; /** - * @return This node, with the specified index now available + * @returns This node, with the specified index now available */ withIndex(indexDefinition: Index): Node; diff --git a/packages/database/src/core/snap/indexes/Index.ts b/packages/database/src/core/snap/indexes/Index.ts index 8c768b64926..ee4f796582f 100644 --- a/packages/database/src/core/snap/indexes/Index.ts +++ b/packages/database/src/core/snap/indexes/Index.ts @@ -25,7 +25,7 @@ export abstract class Index { abstract isDefinedOn(node: Node): boolean; /** - * @return A standalone comparison function for + * @returns A standalone comparison function for * this index */ getCompare(): Comparator { @@ -37,7 +37,7 @@ export abstract class Index { * it's possible that the changes are isolated to parts of the snapshot that are not indexed. * * - * @return True if the portion of the snapshot being indexed changed between oldNode and newNode + * @returns True if the portion of the snapshot being indexed changed between oldNode and newNode */ indexedValueChanged(oldNode: Node, newNode: Node): boolean { const oldWrapped = new NamedNode(MIN_NAME, oldNode); @@ -46,7 +46,7 @@ export abstract class Index { } /** - * @return a node wrapper that will sort equal to or less than + * @returns a node wrapper that will sort equal to or less than * any other node wrapper, using this index */ minPost(): NamedNode { @@ -55,7 +55,7 @@ export abstract class Index { } /** - * @return a node wrapper that will sort greater than or equal to + * @returns a node wrapper that will sort greater than or equal to * any other node wrapper, using this index */ abstract maxPost(): NamedNode; @@ -63,7 +63,7 @@ export abstract class Index { abstract makePost(indexValue: unknown, name: string): NamedNode; /** - * @return String representation for inclusion in a query spec + * @returns String representation for inclusion in a query spec */ abstract toString(): string; } diff --git a/packages/database/src/core/snap/indexes/KeyIndex.ts b/packages/database/src/core/snap/indexes/KeyIndex.ts index c3c47aa6cc4..05d4e7e79ac 100644 --- a/packages/database/src/core/snap/indexes/KeyIndex.ts +++ b/packages/database/src/core/snap/indexes/KeyIndex.ts @@ -84,7 +84,7 @@ export class KeyIndex extends Index { } /** - * @return String representation for inclusion in a query spec + * @returns String representation for inclusion in a query spec */ toString(): string { return '.key'; diff --git a/packages/database/src/core/snap/indexes/PriorityIndex.ts b/packages/database/src/core/snap/indexes/PriorityIndex.ts index 5b9e7d3d76c..117abd2e60d 100644 --- a/packages/database/src/core/snap/indexes/PriorityIndex.ts +++ b/packages/database/src/core/snap/indexes/PriorityIndex.ts @@ -82,7 +82,7 @@ export class PriorityIndex extends Index { } /** - * @return String representation for inclusion in a query spec + * @returns String representation for inclusion in a query spec */ toString(): string { return '.priority'; diff --git a/packages/database/src/core/snap/indexes/ValueIndex.ts b/packages/database/src/core/snap/indexes/ValueIndex.ts index 8f87aa10101..576bc84f961 100644 --- a/packages/database/src/core/snap/indexes/ValueIndex.ts +++ b/packages/database/src/core/snap/indexes/ValueIndex.ts @@ -70,7 +70,7 @@ export class ValueIndex extends Index { } /** - * @return String representation for inclusion in a query spec + * @returns String representation for inclusion in a query spec */ toString(): string { return '.value'; diff --git a/packages/database/src/core/storage/DOMStorageWrapper.ts b/packages/database/src/core/storage/DOMStorageWrapper.ts index 5f3fe008fa8..8d31ba5df53 100644 --- a/packages/database/src/core/storage/DOMStorageWrapper.ts +++ b/packages/database/src/core/storage/DOMStorageWrapper.ts @@ -48,7 +48,7 @@ export class DOMStorageWrapper { } /** - * @return The value that was stored under this key, or null + * @returns The value that was stored under this key, or null */ get(key: string): unknown { const storedVal = this.domStorage_.getItem(this.prefixedName_(key)); diff --git a/packages/database/src/core/storage/storage.ts b/packages/database/src/core/storage/storage.ts index d590146948e..99d62a82f95 100644 --- a/packages/database/src/core/storage/storage.ts +++ b/packages/database/src/core/storage/storage.ts @@ -27,7 +27,7 @@ declare const window: Window; * * @param domStorageName Name of the underlying storage object * (e.g. 'localStorage' or 'sessionStorage'). - * @return Turning off type information until a common interface is defined. + * @returns Turning off type information until a common interface is defined. */ const createStoragefor = function ( domStorageName: string diff --git a/packages/database/src/core/util/EventEmitter.ts b/packages/database/src/core/util/EventEmitter.ts index 8be5e6cab55..0c87df607b4 100644 --- a/packages/database/src/core/util/EventEmitter.ts +++ b/packages/database/src/core/util/EventEmitter.ts @@ -40,7 +40,7 @@ export abstract class EventEmitter { * To be overridden by derived classes in order to fire an initial event when * somebody subscribes for data. * - * @return {Array.<*>} Array of parameters to trigger initial event with. + * @returns {Array.<*>} Array of parameters to trigger initial event with. */ abstract getInitialEvent(eventType: string): unknown[]; diff --git a/packages/database/src/core/util/ImmutableTree.ts b/packages/database/src/core/util/ImmutableTree.ts index 435549293c2..5191150588d 100644 --- a/packages/database/src/core/util/ImmutableTree.ts +++ b/packages/database/src/core/util/ImmutableTree.ts @@ -122,7 +122,7 @@ export class ImmutableTree { } /** - * @return The subtree at the given path + * @returns The subtree at the given path */ subtree(relativePath: Path): ImmutableTree { if (pathIsEmpty(relativePath)) { @@ -143,7 +143,7 @@ export class ImmutableTree { * * @param relativePath Path to set value at. * @param toSet Value to set. - * @return Resulting tree. + * @returns Resulting tree. */ set(relativePath: Path, toSet: T | null): ImmutableTree { if (pathIsEmpty(relativePath)) { @@ -161,7 +161,7 @@ export class ImmutableTree { * Removes the value at the specified path. * * @param relativePath Path to value to remove. - * @return Resulting tree. + * @returns Resulting tree. */ remove(relativePath: Path): ImmutableTree { if (pathIsEmpty(relativePath)) { @@ -196,7 +196,7 @@ export class ImmutableTree { * Gets a value from the tree. * * @param relativePath Path to get value for. - * @return Value at path, or null. + * @returns Value at path, or null. */ get(relativePath: Path): T | null { if (pathIsEmpty(relativePath)) { @@ -217,7 +217,7 @@ export class ImmutableTree { * * @param relativePath Path to replace subtree for. * @param newTree New tree. - * @return Resulting tree. + * @returns Resulting tree. */ setTree(relativePath: Path, newTree: ImmutableTree): ImmutableTree { if (pathIsEmpty(relativePath)) { diff --git a/packages/database/src/core/util/Path.ts b/packages/database/src/core/util/Path.ts index f4d1d3f25c2..b409f9783bd 100644 --- a/packages/database/src/core/util/Path.ts +++ b/packages/database/src/core/util/Path.ts @@ -85,7 +85,7 @@ export function pathGetFront(path: Path): string | null { } /** - * @return The number of segments in this path + * @returns The number of segments in this path */ export function pathGetLength(path: Path): number { return path.pieces_.length - path.pieceNum_; @@ -162,14 +162,14 @@ export function pathChild(path: Path, childPathObj: string | Path): Path { } /** - * @return True if there are no segments in this path + * @returns True if there are no segments in this path */ export function pathIsEmpty(path: Path): boolean { return path.pieceNum_ >= path.pieces_.length; } /** - * @return The path from outerPath to innerPath + * @returns The path from outerPath to innerPath */ export function newRelativePath(outerPath: Path, innerPath: Path): Path { const outer = pathGetFront(outerPath), @@ -191,7 +191,7 @@ export function newRelativePath(outerPath: Path, innerPath: Path): Path { } /** - * @return -1, 0, 1 if left is less, equal, or greater than the right. + * @returns -1, 0, 1 if left is less, equal, or greater than the right. */ export function pathCompare(left: Path, right: Path): number { const leftKeys = pathSlice(left, 0); @@ -209,7 +209,7 @@ export function pathCompare(left: Path, right: Path): number { } /** - * @return true if paths are the same. + * @returns true if paths are the same. */ export function pathEquals(path: Path, other: Path): boolean { if (pathGetLength(path) !== pathGetLength(other)) { @@ -230,7 +230,7 @@ export function pathEquals(path: Path, other: Path): boolean { } /** - * @return True if this path is a parent (or the same as) other + * @returns True if this path is a parent (or the same as) other */ export function pathContains(path: Path, other: Path): boolean { let i = path.pieceNum_; diff --git a/packages/database/src/core/util/SortedMap.ts b/packages/database/src/core/util/SortedMap.ts index 6b8bf0aeb02..38328d2b043 100644 --- a/packages/database/src/core/util/SortedMap.ts +++ b/packages/database/src/core/util/SortedMap.ts @@ -175,7 +175,7 @@ export class LLRBNode { * @param color New color for the node, or null. * @param left New left child for the node, or null. * @param right New right child for the node, or null. - * @return The node copy. + * @returns The node copy. */ copy( key: K | null, @@ -194,14 +194,14 @@ export class LLRBNode { } /** - * @return The total number of nodes in the tree. + * @returns The total number of nodes in the tree. */ count(): number { return this.left.count() + 1 + this.right.count(); } /** - * @return True if the tree is empty. + * @returns True if the tree is empty. */ isEmpty(): boolean { return false; @@ -213,7 +213,7 @@ export class LLRBNode { * * @param action Callback function to be called for each * node. If it returns true, traversal is aborted. - * @return The first truthy value returned by action, or the last falsey + * @returns The first truthy value returned by action, or the last falsey * value returned by action */ inorderTraversal(action: (k: K, v: V) => unknown): boolean { @@ -230,7 +230,7 @@ export class LLRBNode { * * @param action Callback function to be called for each * node. If it returns true, traversal is aborted. - * @return True if traversal was aborted. + * @returns True if traversal was aborted. */ reverseTraversal(action: (k: K, v: V) => void): boolean { return ( @@ -241,7 +241,7 @@ export class LLRBNode { } /** - * @return The minimum node in the tree. + * @returns The minimum node in the tree. */ private min_(): LLRBNode { if (this.left.isEmpty()) { @@ -252,14 +252,14 @@ export class LLRBNode { } /** - * @return The maximum key in the tree. + * @returns The maximum key in the tree. */ minKey(): K { return this.min_().key; } /** - * @return The maximum key in the tree. + * @returns The maximum key in the tree. */ maxKey(): K { if (this.right.isEmpty()) { @@ -273,7 +273,7 @@ export class LLRBNode { * @param key Key to insert. * @param value Value to insert. * @param comparator Comparator. - * @return New tree, with the key/value added. + * @returns New tree, with the key/value added. */ insert(key: K, value: V, comparator: Comparator): LLRBNode { let n: LLRBNode = this; @@ -295,7 +295,7 @@ export class LLRBNode { } /** - * @return New tree, with the minimum key removed. + * @returns New tree, with the minimum key removed. */ private removeMin_(): LLRBNode | LLRBEmptyNode { if (this.left.isEmpty()) { @@ -312,7 +312,7 @@ export class LLRBNode { /** * @param key The key of the item to remove. * @param comparator Comparator. - * @return New tree, with the specified item removed. + * @returns New tree, with the specified item removed. */ remove( key: K, @@ -352,14 +352,14 @@ export class LLRBNode { } /** - * @return Whether this is a RED node. + * @returns Whether this is a RED node. */ isRed_(): boolean { return this.color; } /** - * @return New tree after performing any needed rotations. + * @returns New tree after performing any needed rotations. */ private fixUp_(): LLRBNode { let n: LLRBNode = this; @@ -376,7 +376,7 @@ export class LLRBNode { } /** - * @return New tree, after moveRedLeft. + * @returns New tree, after moveRedLeft. */ private moveRedLeft_(): LLRBNode { let n = this.colorFlip_(); @@ -395,7 +395,7 @@ export class LLRBNode { } /** - * @return New tree, after moveRedRight. + * @returns New tree, after moveRedRight. */ private moveRedRight_(): LLRBNode { let n = this.colorFlip_(); @@ -407,7 +407,7 @@ export class LLRBNode { } /** - * @return New tree, after rotateLeft. + * @returns New tree, after rotateLeft. */ private rotateLeft_(): LLRBNode { const nl = this.copy(null, null, LLRBNode.RED, null, this.right.left); @@ -415,7 +415,7 @@ export class LLRBNode { } /** - * @return New tree, after rotateRight. + * @returns New tree, after rotateRight. */ private rotateRight_(): LLRBNode { const nr = this.copy(null, null, LLRBNode.RED, this.left.right, null); @@ -423,7 +423,7 @@ export class LLRBNode { } /** - * @return Newt ree, after colorFlip. + * @returns Newt ree, after colorFlip. */ private colorFlip_(): LLRBNode { const left = this.left.copy(null, null, !this.left.color, null, null); @@ -434,7 +434,7 @@ export class LLRBNode { /** * For testing. * - * @return True if all is well. + * @returns True if all is well. */ private checkMaxDepth_(): boolean { const blackDepth = this.check_(); @@ -474,7 +474,7 @@ export class LLRBEmptyNode { /** * Returns a copy of the current node. * - * @return The node copy. + * @returns The node copy. */ copy( key: K | null, @@ -492,7 +492,7 @@ export class LLRBEmptyNode { * @param key Key to be added. * @param value Value to be added. * @param comparator Comparator. - * @return New tree, with item added. + * @returns New tree, with item added. */ insert(key: K, value: V, comparator: Comparator): LLRBNode { return new LLRBNode(key, value, null); @@ -503,21 +503,21 @@ export class LLRBEmptyNode { * * @param key The key to remove. * @param comparator Comparator. - * @return New tree, with item removed. + * @returns New tree, with item removed. */ remove(key: K, comparator: Comparator): LLRBEmptyNode { return this; } /** - * @return The total number of nodes in the tree. + * @returns The total number of nodes in the tree. */ count(): number { return 0; } /** - * @return True if the tree is empty. + * @returns True if the tree is empty. */ isEmpty(): boolean { return true; @@ -529,7 +529,7 @@ export class LLRBEmptyNode { * * @param action Callback function to be called for each * node. If it returns true, traversal is aborted. - * @return True if traversal was aborted. + * @returns True if traversal was aborted. */ inorderTraversal(action: (k: K, v: V) => unknown): boolean { return false; @@ -541,7 +541,7 @@ export class LLRBEmptyNode { * * @param action Callback function to be called for each * node. If it returns true, traversal is aborted. - * @return True if traversal was aborted. + * @returns True if traversal was aborted. */ reverseTraversal(action: (k: K, v: V) => void): boolean { return false; @@ -560,7 +560,7 @@ export class LLRBEmptyNode { } /** - * @return Whether this node is red. + * @returns Whether this node is red. */ isRed_() { return false; @@ -594,7 +594,7 @@ export class SortedMap { * * @param key Key to be added. * @param value Value to be added. - * @return New map, with item added. + * @returns New map, with item added. */ insert(key: K, value: V): SortedMap { return new SortedMap( @@ -609,7 +609,7 @@ export class SortedMap { * Returns a copy of the map, with the specified key removed. * * @param key The key to remove. - * @return New map, with item removed. + * @returns New map, with item removed. */ remove(key: K): SortedMap { return new SortedMap( @@ -624,7 +624,7 @@ export class SortedMap { * Returns the value of the node with the given key, or null. * * @param key The key to look up. - * @return The value of the node with the given key, or null if the + * @returns The value of the node with the given key, or null if the * key doesn't exist. */ get(key: K): V | null { @@ -646,7 +646,7 @@ export class SortedMap { /** * Returns the key of the item *before* the specified key, or null if key is the first item. * @param key The key to find the predecessor of - * @return The predecessor key. + * @returns The predecessor key. */ getPredecessorKey(key: K): K | null { let cmp, @@ -680,28 +680,28 @@ export class SortedMap { } /** - * @return True if the map is empty. + * @returns True if the map is empty. */ isEmpty(): boolean { return this.root_.isEmpty(); } /** - * @return The total number of nodes in the map. + * @returns The total number of nodes in the map. */ count(): number { return this.root_.count(); } /** - * @return The minimum key in the map. + * @returns The minimum key in the map. */ minKey(): K | null { return this.root_.minKey(); } /** - * @return The maximum key in the map. + * @returns The maximum key in the map. */ maxKey(): K | null { return this.root_.maxKey(); @@ -713,7 +713,7 @@ export class SortedMap { * * @param action Callback function to be called * for each key/value pair. If action returns true, traversal is aborted. - * @return The first truthy value returned by action, or the last falsey + * @returns The first truthy value returned by action, or the last falsey * value returned by action */ inorderTraversal(action: (k: K, v: V) => unknown): boolean { @@ -726,7 +726,7 @@ export class SortedMap { * * @param action Callback function to be called * for each key/value pair. If action returns true, traversal is aborted. - * @return True if the traversal was aborted. + * @returns True if the traversal was aborted. */ reverseTraversal(action: (k: K, v: V) => void): boolean { return this.root_.reverseTraversal(action); @@ -734,7 +734,7 @@ export class SortedMap { /** * Returns an iterator over the SortedMap. - * @return The iterator. + * @returns The iterator. */ getIterator( resultGenerator?: (k: K, v: V) => T diff --git a/packages/database/src/core/util/Tree.ts b/packages/database/src/core/util/Tree.ts index 4b1ba798341..fd8f8ebcb52 100644 --- a/packages/database/src/core/util/Tree.ts +++ b/packages/database/src/core/util/Tree.ts @@ -53,7 +53,7 @@ export class Tree { * Returns a sub-Tree for the given path. * * @param pathObj Path to look up. - * @return Tree for path. + * @returns Tree for path. */ export function treeSubTree(tree: Tree, pathObj: string | Path): Tree { // TODO: Require pathObj to be Path? @@ -76,7 +76,7 @@ export function treeSubTree(tree: Tree, pathObj: string | Path): Tree { /** * Returns the data associated with this tree node. * - * @return The data or null if no data exists. + * @returns The data or null if no data exists. */ export function treeGetValue(tree: Tree): T | undefined { return tree.node.value; @@ -93,14 +93,14 @@ export function treeSetValue(tree: Tree, value: T | undefined): void { } /** - * @return Whether the tree has any children. + * @returns Whether the tree has any children. */ export function treeHasChildren(tree: Tree): boolean { return tree.node.childCount > 0; } /** - * @return Whethe rthe tree is empty (no value or children). + * @returns Whethe rthe tree is empty (no value or children). */ export function treeIsEmpty(tree: Tree): boolean { return treeGetValue(tree) === undefined && !treeHasChildren(tree); @@ -154,7 +154,7 @@ export function treeForEachDescendant( * @param action Action to be called on each parent; return * true to abort. * @param includeSelf Whether to call action on this node as well. - * @return true if the action callback returned true. + * @returns true if the action callback returned true. */ export function treeForEachAncestor( tree: Tree, @@ -192,7 +192,7 @@ export function treeForEachImmediateDescendantWithValue( } /** - * @return The path of this tree node, as a Path. + * @returns The path of this tree node, as a Path. */ export function treeGetPath(tree: Tree) { return new Path( diff --git a/packages/database/src/core/util/libs/parser.ts b/packages/database/src/core/util/libs/parser.ts index 69c3b4e1db7..e4f006a635a 100644 --- a/packages/database/src/core/util/libs/parser.ts +++ b/packages/database/src/core/util/libs/parser.ts @@ -35,7 +35,7 @@ function decodePath(pathString: string): string { } /** - * @return key value hash + * @returns key value hash */ function decodeQuery(queryString: string): { [key: string]: string } { const results = {}; diff --git a/packages/database/src/core/util/util.ts b/packages/database/src/core/util/util.ts index 4208b0529d9..c63010d15b8 100644 --- a/packages/database/src/core/util/util.ts +++ b/packages/database/src/core/util/util.ts @@ -47,7 +47,7 @@ export const LUIDGenerator: () => number = (function () { /** * Sha1 hash of the input string * @param str The string to hash - * @return {!string} The resulting hash + * @returns {!string} The resulting hash */ export const sha1 = function (str: string): string { const utf8Bytes = stringToByteArray(str); @@ -277,7 +277,7 @@ export const nameCompare = function (a: string, b: string): number { }; /** - * @return {!number} comparison result. + * @returns {!number} comparison result. */ export const stringCompare = function (a: string, b: string): number { if (a === b) { @@ -333,7 +333,7 @@ export const ObjectToUniqueKey = function (obj: unknown): string { * Splits a string into a number of smaller segments of maximum size * @param str The string * @param segsize The maximum number of chars in the string. - * @return The string, split into appropriately-sized chunks + * @returns The string, split into appropriately-sized chunks */ export const splitStringBySize = function ( str: string, @@ -573,7 +573,7 @@ export const callUserCallback = function ( }; /** - * @return {boolean} true if we think we're currently being crawled. + * @returns {boolean} true if we think we're currently being crawled. */ export const beingCrawled = function (): boolean { const userAgent = @@ -611,7 +611,7 @@ export const exportPropGetter = function ( * * @param fn Function to run. * @param time Milliseconds to wait before running. - * @return The setTimeout() return value. + * @returns The setTimeout() return value. */ export const setTimeoutNonBlocking = function ( fn: () => void, diff --git a/packages/database/src/core/view/Event.ts b/packages/database/src/core/view/Event.ts index 307510b27e6..ef18a9045a0 100644 --- a/packages/database/src/core/view/Event.ts +++ b/packages/database/src/core/view/Event.ts @@ -36,6 +36,10 @@ export interface Event { toString(): string; } +/** + * One of the following strings: "value", "child_added", "child_changed", + * "child_removed", or "child_moved." + */ export type EventType = | 'value' | 'child_added' diff --git a/packages/database/src/core/view/QueryParams.ts b/packages/database/src/core/view/QueryParams.ts index 33477032fb0..a8443e90521 100644 --- a/packages/database/src/core/view/QueryParams.ts +++ b/packages/database/src/core/view/QueryParams.ts @@ -93,7 +93,7 @@ export class QueryParams { } /** - * @return True if it would return from left. + * @returns True if it would return from left. */ isViewFromLeft(): boolean { if (this.viewFrom_ === '') { @@ -158,7 +158,7 @@ export class QueryParams { } /** - * @return True if a limit has been set and it has been explicitly anchored + * @returns True if a limit has been set and it has been explicitly anchored */ hasAnchoredLimit(): boolean { return this.limitSet_ && this.viewFrom_ !== ''; @@ -347,7 +347,7 @@ export function queryParamsOrderBy( /** * Returns a set of REST query string parameters representing this query. * - * @return query string parameters + * @returns query string parameters */ export function queryParamsToRestQueryStringParameters( queryParams: QueryParams diff --git a/packages/database/src/core/view/View.ts b/packages/database/src/core/view/View.ts index 1c5804a0eb1..032fd1daaa4 100644 --- a/packages/database/src/core/view/View.ts +++ b/packages/database/src/core/view/View.ts @@ -145,7 +145,7 @@ export function viewAddEventRegistration( /** * @param eventRegistration If null, remove all callbacks. * @param cancelError If a cancelError is provided, appropriate cancel events will be returned. - * @return Cancel events, if cancelError was provided. + * @returns Cancel events, if cancelError was provided. */ export function viewRemoveEventRegistration( view: View, diff --git a/packages/database/src/exp/Database.ts b/packages/database/src/exp/Database.ts index db01fe5bbb3..557840cc004 100644 --- a/packages/database/src/exp/Database.ts +++ b/packages/database/src/exp/Database.ts @@ -163,7 +163,7 @@ function repoManagerDeleteRepo(repo: Repo, appName: string): void { * provided app. * * @param repoInfo The metadata about the Repo - * @return The Repo object for the specified server / repoName. + * @returns The Repo object for the specified server / repoName. */ function repoManagerCreateRepo( repoInfo: RepoInfo, @@ -262,6 +262,16 @@ export function getDatabase(app: FirebaseApp, url?: string): FirebaseDatabase { }) as FirebaseDatabase; } +/** + * Modify the provided instance to communicate with the Realtime Database + * emulator. + * + *

Note: This method must be called before performing any other operation. + * + * @param db - The instance to modify. + * @param host - The emulator host (ex: localhost) + * @param port - The emulator port (ex: 8080) + */ export function useDatabaseEmulator( db: FirebaseDatabase, host: string, @@ -278,20 +288,73 @@ export function useDatabaseEmulator( repoManagerApplyEmulatorSettings(db._repo, host, port); } +/** + * Disconnects from the server (all Database operations will be completed + * offline). + * + * The client automatically maintains a persistent connection to the Database + * server, which will remain active indefinitely and reconnect when + * disconnected. However, the `goOffline()` and `goOnline()` methods may be used + * to control the client connection in cases where a persistent connection is + * undesirable. + * + * While offline, the client will no longer receive data updates from the + * Database. However, all Database operations performed locally will continue to + * immediately fire events, allowing your application to continue behaving + * normally. Additionally, each operation performed locally will automatically + * be queued and retried upon reconnection to the Database server. + * + * To reconnect to the Database and begin receiving remote events, see + * `goOnline()`. + * + * @param db - The instance to disconnect. + */ export function goOffline(db: FirebaseDatabase): void { db = getModularInstance(db); db._checkNotDeleted('goOffline'); repoInterrupt(db._repo); } +/** + * Reconnects to the server and synchronizes the offline Database state + * with the server state. + * + * This method should be used after disabling the active connection with + * `goOffline()`. Once reconnected, the client will transmit the proper data + * and fire the appropriate events so that your client "catches up" + * automatically. + * + * @param db - The instance to reconnect. + */ export function goOnline(db: FirebaseDatabase): void { db = getModularInstance(db); db._checkNotDeleted('goOnline'); repoResume(db._repo); } +/** + * Logs debugging information to the console. + * + * @param enabled Enables logging if `true`, disables logging if `false`. + * @param persistent Remembers the logging state between page refreshes if + * `true`. + */ +export function enableLogging(enabled: boolean, persistent?: boolean); + +/** + * Logs debugging information to the console. + * + * @param logger A custom logger function to control how things get logged. + * @param persistent Remembers the logging state between page refreshes if + * `true`. + */ +export function enableLogging( + logger?: (message: string) => unknown, + persistent?: boolean +); + export function enableLogging( - logger?: boolean | ((message: string) => unknown), + logger: boolean | ((message: string) => unknown), persistent?: boolean ): void { enableLoggingImpl(logger, persistent); diff --git a/packages/database/src/exp/OnDisconnect.ts b/packages/database/src/exp/OnDisconnect.ts index efb57f9eda9..cf7998cc2bd 100644 --- a/packages/database/src/exp/OnDisconnect.ts +++ b/packages/database/src/exp/OnDisconnect.ts @@ -33,9 +33,39 @@ import { validateWritablePath } from '../core/util/validation'; +/** + * The `onDisconnect` class allows you to write or clear data when your client + * disconnects from the Database server. These updates occur whether your + * client disconnects cleanly or not, so you can rely on them to clean up data + * even if a connection is dropped or a client crashes. + * + * The `onDisconnect` class is most commonly used to manage presence in + * applications where it is useful to detect how many clients are connected and + * when other clients disconnect. See {@link + * https://firebase.google.com/docs/database/web/offline-capabilities Enabling + * Offline Capabilities in JavaScript} for more information. + * + * To avoid problems when a connection is dropped before the requests can be + * transferred to the Database server, these functions should be called before + * writing any data. + * + * Note that `onDisconnect` operations are only triggered once. If you want an + * operation to occur each time a disconnect occurs, you'll need to re-establish + * the `onDisconnect` operations each time you reconnect. + */ export class OnDisconnect { constructor(private _repo: Repo, private _path: Path) {} + /** + * Cancels all previously queued `onDisconnect()` set or update events for this + * location and all children. + * + * If a write has been queued for this location via a `set()` or `update()` at a + * parent location, the write at this location will be canceled, though writes + * to sibling locations will still occur. + * + * @returns Resolves when synchronization to the server is complete. + */ cancel(): Promise { const deferred = new Deferred(); repoOnDisconnectCancel( @@ -46,6 +76,12 @@ export class OnDisconnect { return deferred.promise; } + /** + * Ensures the data at this location is deleted when the client is disconnected + * (due to closing the browser, navigating to a new page, or network issues). + * + * @returns Resolves when synchronization to the server is complete. + */ remove(): Promise { validateWritablePath('OnDisconnect.remove', this._path); const deferred = new Deferred(); @@ -58,6 +94,25 @@ export class OnDisconnect { return deferred.promise; } + /** + * Ensures the data at this location is set to the specified value when the + * client is disconnected (due to closing the browser, navigating to a new page, + * or network issues). + * + * `set()` is especially useful for implementing "presence" systems, where a + * value should be changed or cleared when a user disconnects so that they + * appear "offline" to other users. See {@link + * https://firebase.google.com/docs/database/web/offline-capabilities Enabling + * Offline Capabilities in JavaScript} for more information. + * + * Note that `onDisconnect` operations are only triggered once. If you want an + * operation to occur each time a disconnect occurs, you'll need to re-establish + * the `onDisconnect` operations each time. + * + * @param value - The value to be written to this location on disconnect (can + * be an object, array, string, number, boolean, or null). + * @returns Resolves when synchronization to the Database is complete. + */ set(value: unknown): Promise { validateWritablePath('OnDisconnect.set', this._path); validateFirebaseDataArg('OnDisconnect.set', value, this._path, false); @@ -71,6 +126,16 @@ export class OnDisconnect { return deferred.promise; } + /** + * Ensures the data at this location is set to the specified value and priority + * when the client is disconnected (due to closing the browser, navigating to a + * new page, or network issues). + * + * @param value - The value to be written to this location on disconnect (can + * be an object, array, string, number, boolean, or null). + * @param priority - The priority to be written (string, number, or null). + * @returns Resolves when synchronization to the Database is complete. + */ setWithPriority( value: unknown, priority: number | string | null @@ -95,6 +160,22 @@ export class OnDisconnect { return deferred.promise; } + /** + * Writes multiple values at this location when the client is disconnected (due + * to closing the browser, navigating to a new page, or network issues). + * + * The `values` argument contains multiple property-value pairs that will be + * written to the Database together. Each child property can either be a simple + * property (for example, "name") or a relative path (for example, "name/first") + * from the current location to the data to update. + * + * As opposed to the `set()` method, `update()` can be use to selectively update + * only the referenced properties at the current location (instead of replacing + * all the child properties at the current location). + * + * @param values Object containing multiple values. + * @returns Resolves when synchronization to the Database is complete. + */ update(values: Indexable): Promise { validateWritablePath('OnDisconnect.update', this._path); validateFirebaseMergeDataArg( diff --git a/packages/database/src/exp/Reference.ts b/packages/database/src/exp/Reference.ts index 0335d5ff2c2..6598f983c74 100644 --- a/packages/database/src/exp/Reference.ts +++ b/packages/database/src/exp/Reference.ts @@ -19,25 +19,116 @@ import { QueryContext } from '../core/view/EventRegistration'; * limitations under the License. */ +/** + * A `Query` sorts and filters the data at a Database location so only a subset + * of the child data is included. This can be used to order a collection of + * data by some attribute (for example, height of dinosaurs) as well as to + * restrict a large list of items (for example, chat messages) down to a number + * suitable for synchronizing to the client. Queries are created by chaining + * together one or more of the filter methods defined here. + * + * Just as with a `Reference`, you can receive data from a `Query` by using the + * `on*()` methods. You will only receive events and `DataSnapshot`s for the + * subset of the data that matches your query. + * + * Read our documentation on {@link + * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data + * Sorting and filtering data} for more information. + */ export interface Query extends QueryContext { + /** The `Reference` for the `Query`'s location. */ readonly ref: Reference; + + /** + * Returns whether or not the current and provided queries represent the same + * location, have the same query parameters, and are from the same instance of + * `FirebaseApp`. + * + * Two `Reference` objects are equivalent if they represent the same location + * and are from the same instance of `FirebaseApp`. + * + * Two `Query` objects are equivalent if they represent the same location, + * have the same query parameters, and are from the same instance of + * `FirebaseApp`. Equivalent queries share the same sort order, limits, and + * starting and ending points. + * + * @param other - The query to compare against. + * @returns Whether or not the current and provided queries are equivalent. + */ isEqual(other: Query | null): boolean; + + /** + * Returns a JSON-serializable representation of this object. + * + * @returns A JSON-serializable representation of this object. + */ toJSON(): string; + + /** + * Gets the absolute URL for this location. + * + * The `toString()` method returns a URL that is ready to be put into a + * browser, curl command, or a `refFromURL()` call. Since all of those expect + * the URL to be url-encoded, `toString()` returns an encoded URL. + * + * Append '.json' to the returned URL when typed into a browser to download + * JSON-formatted data. If the location is secured (that is, not publicly + * readable), you will get a permission-denied error. + * + * @returns The absolute URL for this location. + */ toString(): string; } +/** + * A `Reference` represents a specific location in your Database and can be used + * for reading or writing data to that Database location. + * + * You can reference the root or child location in your Database by calling + * `ref()` or `ref("child/path")`. + * + * Writing is done with the `set()` method and reading can be done with the + * `on*()` method. See {@link + * https://firebase.google.com/docs/database/web/read-and-write Read and Write + * Data on the Web} + */ export interface Reference extends Query { + /** + * The last part of the `Reference`'s path. + * + * For example, `"ada"` is the key for + * `https://.firebaseio.com/users/ada`. + * + * The key of a root `Reference` is `null`. + */ readonly key: string | null; + + /** + * The parent location of a `Reference`. + * + * The parent of a root `Reference` is `null`. + */ readonly parent: Reference | null; + + /** The root `Reference` of the Database. */ + readonly root: Reference; } +/** + * A `Promise` that can also act as a `Reference` when returned by + * {@link push}. The reference is available immediately and the Promise resolves + * as the write to the backend completes. + */ export interface ThenableReference extends Reference, Pick, 'then' | 'catch'> {} +/** A callback that can invoked to remove a listener. */ export type Unsubscribe = () => void; +/** An options objects that can be used to customize a listener. */ export interface ListenOptions { + /** Whether to remove the listener after its first invocation. */ readonly onlyOnce?: boolean; } diff --git a/packages/database/src/exp/Reference_impl.ts b/packages/database/src/exp/Reference_impl.ts index 948ad2e3cc7..d29628bd5e4 100644 --- a/packages/database/src/exp/Reference_impl.ts +++ b/packages/database/src/exp/Reference_impl.ts @@ -265,6 +265,20 @@ export class ReferenceImpl extends QueryImpl implements Reference { } } +/** + * A `DataSnapshot` contains data from a Database location. + * + * Any time you read data from the Database, you receive the data as a + * `DataSnapshot`. A `DataSnapshot` is passed to the event callbacks you attach + * with `on()` or `once()`. You can extract the contents of the snapshot as a + * JavaScript object by calling the `val()` method. Alternatively, you can + * traverse into the snapshot by calling `child()` to return child snapshots + * (which you could then call `val()` on). + * + * A `DataSnapshot` is an efficiently generated, immutable copy of the data at + * a Database location. It cannot be modified and will never change (to modify + * data, you always call the `set()` method on a `Reference` directly). + */ export class DataSnapshot { /** * @param _node A SnapshotNode to wrap. @@ -278,19 +292,50 @@ export class DataSnapshot { readonly _index: Index ) {} + /** + * Gets the priority value of the data in this `DataSnapshot`. + * + * Applications need not use priority but can order collections by + * ordinary properties (see + * {@link + * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data + * Sorting and filtering data}). + */ get priority(): string | number | null { // typecast here because we never return deferred values or internal priorities (MAX_PRIORITY) return this._node.getPriority().val() as string | number | null; } + /** + * The key (last part of the path) of the location of this `DataSnapshot`. + * + * The last token in a Database location is considered its key. For example, + * "ada" is the key for the /users/ada/ node. Accessing the key on any + * `DataSnapshot` will return the key for the location that generated it. + * However, accessing the key on the root URL of a Database will return + * `null`. + */ get key(): string | null { return this.ref.key; } + /** Returns the number of child properties of this `DataSnapshot`. */ get size(): number { return this._node.numChildren(); } + /** + * Gets another `DataSnapshot` for the location at the specified relative path. + * + * Passing a relative path to the `child()` method of a DataSnapshot returns + * another `DataSnapshot` for the location at the specified relative path. The + * relative path can either be a simple child name (for example, "ada") or a + * deeper, slash-separated path (for example, "ada/name/first"). If the child + * location has no data, an empty `DataSnapshot` (that is, a `DataSnapshot` + * whose value is `null`) is returned. + * + * @param path - A relative path to the location of child data. + */ child(path: string): DataSnapshot { const childPath = new Path(path); const childRef = child(this.ref, path); @@ -300,16 +345,46 @@ export class DataSnapshot { PRIORITY_INDEX ); } - + /** + * Returns true if this `DataSnapshot` contains any data. It is slightly more + * efficient than using `snapshot.val() !== null`. + */ exists(): boolean { return !this._node.isEmpty(); } + /** + * Exports the entire contents of the DataSnapshot as a JavaScript object. + * + * The `exportVal()` method is similar to `val()`, except priority information + * is included (if available), making it suitable for backing up your data. + * + * @returns The DataSnapshot's contents as a JavaScript value (Object, + * Array, string, number, boolean, or `null`). + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any exportVal(): any { return this._node.val(true); } + /** + * Enumerates the top-level children in the `DataSnapshot`. + * + * Because of the way JavaScript objects work, the ordering of data in the + * JavaScript object returned by `val()` is not guaranteed to match the + * ordering on the server nor the ordering of `onChildAdded()` events. That is + * where `forEach()` comes in handy. It guarantees the children of a + * `DataSnapshot` will be iterated in their query order. + * + * If no explicit `orderBy*()` method is used, results are returned + * ordered by key (unless priorities are used, in which case, results are + * returned by priority). + * + * @param action - A function that will be called for each child DataSnapshot. + * The callback can return true to cancel further enumeration. + * @returns true if enumeration was canceled due to your callback returning + * true. + */ forEach(action: (child: DataSnapshot) => boolean | void): boolean { if (this._node.isLeafNode()) { return false; @@ -324,11 +399,30 @@ export class DataSnapshot { }); } + /** + * Returns true if the specified child path has (non-null) data. + * + * @param path - A relative path to the location of a potential child. + * @returns `true` if data exists at the specified child path; else + * `false`. + */ hasChild(path: string): boolean { const childPath = new Path(path); return !this._node.getChild(childPath).isEmpty(); } + /** + * Returns whether or not the `DataSnapshot` has any non-`null` child + * properties. + * + * You can use `hasChildren()` to determine if a `DataSnapshot` has any + * children. If it does, you can enumerate them using `forEach()`. If it + * doesn't, then either this snapshot contains a primitive value (which can be + * retrieved with `val()`) or it is empty (in which case, `val()` will return + * `null`). + * + * @returns true if this snapshot has any children; else false. + */ hasChildren(): boolean { if (this._node.isLeafNode()) { return false; @@ -337,22 +431,65 @@ export class DataSnapshot { } } + /** + * Returns a JSON-serializable representation of this object. + */ toJSON(): object | null { return this.exportVal(); } + /** + * Extracts a JavaScript value from a `DataSnapshot`. + * + * Depending on the data in a `DataSnapshot`, the `val()` method may return a + * scalar type (string, number, or boolean), an array, or an object. It may + * also return null, indicating that the `DataSnapshot` is empty (contains no + * data). + * + * @returns The DataSnapshot's contents as a JavaScript value (Object, + * Array, string, number, boolean, or `null`). + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any val(): any { return this._node.val(); } } - +/** + * + * Returns a `Reference` representing the location in the Database + * corresponding to the provided path. If no path is provided, the `Reference` + * will point to the root of the Database. + * + * @param db - The database instance to obtain a reference for. + * @param path - Optional path representing the location the returned + * `Reference` will point. If not provided, the returned `Reference` will + * point to the root of the Database. + * @returns If a path is provided, a `Reference` + * pointing to the provided path. Otherwise, a `Reference` pointing to the + * root of the Database. + */ export function ref(db: FirebaseDatabase, path?: string): ReferenceImpl { db = getModularInstance(db); db._checkNotDeleted('ref'); return path !== undefined ? child(db._root, path) : db._root; } +/** + * Returns a `Reference` representing the location in the Database + * corresponding to the provided Firebase URL. + * + * An exception is thrown if the URL is not a valid Firebase Database URL or it + * has a different domain than the current `Database` instance. + * + * Note that all query parameters (`orderBy`, `limitToLast`, etc.) are ignored + * and are not applied to the returned `Reference`. + * + * @param db - The database instance to obtain a reference for. + * @param url - The Firebase URL at which the returned `Reference` will + * point. + * @returns A `Reference` pointing to the provided + * Firebase URL. + */ export function refFromURL(db: FirebaseDatabase, url: string): ReferenceImpl { db = getModularInstance(db); db._checkNotDeleted('refFromURL'); @@ -378,16 +515,35 @@ export function refFromURL(db: FirebaseDatabase, url: string): ReferenceImpl { return ref(db, parsedURL.path.toString()); } -export function child(ref: Reference, path: string): ReferenceImpl { - ref = getModularInstance(ref); - if (pathGetFront(ref._path) === null) { +/** + * Gets a `Reference` for the location at the specified relative path. + * + * The relative path can either be a simple child name (for example, "ada") or + * a deeper slash-separated path (for example, "ada/name/first"). + * + * @param parent - The parent location. + * @param path - A relative path from this location to the desired child + * location. + * @returns The specified child location. + */ +export function child(parent: Reference, path: string): ReferenceImpl { + parent = getModularInstance(parent); + if (pathGetFront(parent._path) === null) { validateRootPathString('child', 'path', path, false); } else { validatePathString('child', 'path', path, false); } - return new ReferenceImpl(ref._repo, pathChild(ref._path, path)); + return new ReferenceImpl(parent._repo, pathChild(parent._path, path)); } +/** + * Returns an `OnDisconnect` object - see {@link + * https://firebase.google.com/docs/database/web/offline-capabilities + * Enabling Offline Capabilities in JavaScript} for more information on how + * to use it. + * + * @param ref - The reference to add OnDisconnect triggers for. + */ export function onDisconnect(ref: Reference): OnDisconnect { ref = getModularInstance(ref) as ReferenceImpl; return new OnDisconnect(ref._repo, ref._path); @@ -397,11 +553,41 @@ export interface ThenableReferenceImpl extends ReferenceImpl, Pick, 'then' | 'catch'> {} -export function push(ref: Reference, value?: unknown): ThenableReferenceImpl { - ref = getModularInstance(ref); - validateWritablePath('push', ref._path); - validateFirebaseDataArg('push', value, ref._path, true); - const now = repoServerTime(ref._repo); +/** + * Generates a new child location using a unique key and returns its + * `Reference`. + * + * This is the most common pattern for adding data to a collection of items. + * + * If you provide a value to `push()`, the value is written to the + * generated location. If you don't pass a value, nothing is written to the + * database and the child remains empty (but you can use the `Reference` + * elsewhere). + * + * The unique keys generated by `push()` are ordered by the current time, so the + * resulting list of items is chronologically sorted. The keys are also + * designed to be unguessable (they contain 72 random bits of entropy). + * + * See {@link + * https://firebase.google.com/docs/database/web/lists-of-data#append_to_a_list_of_data + * Append to a list of data} + *
See {@link + * https://firebase.googleblog.com/2015/02/the-2120-ways-to-ensure-unique_68.html + * The 2^120 Ways to Ensure Unique Identifiers} + * + * @param parent - The parent location. + * @param value - Optional value to be written at the generated location. + * @returns Combined `Promise` and `Reference`; resolves when write is complete, + * but can be used immediately as the `Reference` to the child location. + */ +export function push( + parent: Reference, + value?: unknown +): ThenableReferenceImpl { + parent = getModularInstance(parent); + validateWritablePath('push', parent._path); + validateFirebaseDataArg('push', value, parent._path, true); + const now = repoServerTime(parent._repo); const name = nextPushId(now); // push() returns a ThennableReference whose promise is fulfilled with a @@ -410,8 +596,8 @@ export function push(ref: Reference, value?: unknown): ThenableReferenceImpl { // 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. - const thennablePushRef: Partial = child(ref, name); - const pushRef = child(ref, name); + const thennablePushRef: Partial = child(parent, name); + const pushRef = child(parent, name); let promise: Promise; if (value != null) { @@ -425,11 +611,54 @@ export function push(ref: Reference, value?: unknown): ThenableReferenceImpl { return thennablePushRef as ThenableReferenceImpl; } +/** + * Removes the data at this Database location. + * + * Any data at child locations will also be deleted. + * + * The effect of the remove will be visible immediately and the corresponding + * event 'value' will be triggered. Synchronization of the remove to the + * Firebase servers will also be started, and the returned Promise will resolve + * when complete. If provided, the onComplete callback will be called + * asynchronously after synchronization has finished. + * + * @param ref - The location to remove. + * @returns Resolves when remove on server is complete. + */ export function remove(ref: Reference): Promise { validateWritablePath('remove', ref._path); return set(ref, null); } +/** + * Writes data to this Database location. + * + * This will overwrite any data at this location and all child locations. + * + * The effect of the write will be visible immediately, and the corresponding + * events ("value", "child_added", etc.) will be triggered. Synchronization of + * the data to the Firebase servers will also be started, and the returned + * Promise will resolve when complete. If provided, the `onComplete` callback + * will be called asynchronously after synchronization has finished. + * + * Passing `null` for the new value is equivalent to calling `remove()`; namely, + * all data at this location and all child locations will be deleted. + * + * `set()` will remove any priority stored at this location, so if priority is + * meant to be preserved, you need to use `setWithPriority()` instead. + * + * Note that modifying data with `set()` will cancel any pending transactions + * at that location, so extreme care should be taken if mixing `set()` and + * `transaction()` to modify the same data. + * + * A single `set()` will generate a single "value" event at the location where + * the `set()` was performed. + * + * @param ref - The location to write to. + * @param value - The value to be written (string, number, boolean, object, + * array, or null). + * @returns Resolves when write to server is complete. + */ export function set(ref: Reference, value: unknown): Promise { ref = getModularInstance(ref); validateWritablePath('set', ref._path); @@ -445,6 +674,18 @@ export function set(ref: Reference, value: unknown): Promise { return deferred.promise; } +/** + * Sets a priority for the data at this Database location. + * + * Applications need not use priority but can order collections by + * ordinary properties (see {@link + * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data + * Sorting and filtering data}). + * + * @param ref - The location to write to. + * @param priority - The priority to be written (string, number, or null). + * @returns Resolves when write to server is complete. + */ export function setPriority( ref: Reference, priority: string | number | null @@ -463,6 +704,21 @@ export function setPriority( return deferred.promise; } +/** + * Writes data the Database location. Like `set()` but also specifies the + * priority for that data. + * + * Applications need not use priority but can order collections by + * ordinary properties (see {@link + * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data + * Sorting and filtering data}). + * + * @param ref - The location to write to. + * @param value - The value to be written (string, number, boolean, object, + * array, or null). + * @param priority - The priority to be written (string, number, or null). + * @returns Resolves when write to server is complete. + */ export function setWithPriority( ref: Reference, value: unknown, @@ -486,6 +742,42 @@ export function setWithPriority( return deferred.promise; } +/** + * Writes multiple values to the Database at once. + * + * The `values` argument contains multiple property-value pairs that will be + * written to the Database together. Each child property can either be a simple + * property (for example, "name") or a relative path (for example, + * "name/first") from the current location to the data to update. + * + * As opposed to the `set()` method, `update()` can be use to selectively update + * only the referenced properties at the current location (instead of replacing + * all the child properties at the current location). + * + * The effect of the write will be visible immediately, and the corresponding + * events ('value', 'child_added', etc.) will be triggered. Synchronization of + * the data to the Firebase servers will also be started, and the returned + * Promise will resolve when complete. If provided, the `onComplete` callback + * will be called asynchronously after synchronization has finished. + * + * A single `update()` will generate a single "value" event at the location + * where the `update()` was performed, regardless of how many children were + * modified. + * + * Note that modifying data with `update()` will cancel any pending + * transactions at that location, so extreme care should be taken if mixing + * `update()` and `transaction()` to modify the same data. + * + * Passing `null` to `update()` will remove the data at this location. + * + * See {@link + * https://firebase.googleblog.com/2015/09/introducing-multi-location-updates-and_86.html + * Introducing multi-location updates and more}. + * + * @param ref - The location to write to. + * @param values - Object containing multiple values. + * @returns Resolves when update on server is complete. + */ export function update(ref: Reference, values: object): Promise { validateFirebaseMergeDataArg('update', values, ref._path, false); const deferred = new Deferred(); @@ -498,6 +790,14 @@ export function update(ref: Reference, values: object): Promise { return deferred.promise; } +/** + * Gets the most up-to-date result for this query. + * + * @param query - The query to run. + * @returns A promise which resolves to the resulting DataSnapshot if a value is + * available, or rejects if the client is unable to return a value (e.g., if the + * server is unreachable and there is nothing cached). + */ export function get(query: Query): Promise { const queryImpl = getModularInstance(query) as QueryImpl; return repoGetValue(query._repo, queryImpl).then(node => { @@ -515,16 +815,10 @@ export function get(query: Query): Promise { export class ValueEventRegistration implements EventRegistration { constructor(private callbackContext: CallbackContext) {} - /** - * @inheritDoc - */ respondsTo(eventType: string): boolean { return eventType === 'value'; } - /** - * @inheritDoc - */ createEvent(change: Change, query: QueryContext): DataEvent { const index = query._queryParams.getIndex(); return new DataEvent( @@ -538,9 +832,6 @@ export class ValueEventRegistration implements EventRegistration { ); } - /** - * @inheritDoc - */ getEventRunner(eventData: CancelEvent | DataEvent): () => void { if (eventData.getEventType() === 'cancel') { return () => @@ -551,9 +842,6 @@ export class ValueEventRegistration implements EventRegistration { } } - /** - * @inheritDoc - */ createCancelEvent(error: Error, path: Path): CancelEvent | null { if (this.callbackContext.hasCancelCallback) { return new CancelEvent(this, error, path); @@ -562,9 +850,6 @@ export class ValueEventRegistration implements EventRegistration { } } - /** - * @inheritDoc - */ matches(other: EventRegistration): boolean { if (!(other instanceof ValueEventRegistration)) { return false; @@ -576,9 +861,6 @@ export class ValueEventRegistration implements EventRegistration { } } - /** - * @inheritDoc - */ hasAnyCallback(): boolean { return this.callbackContext !== null; } @@ -587,8 +869,8 @@ export class ValueEventRegistration implements EventRegistration { /** * Represents the registration of 1 or more child_xxx events. * - * 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. + * 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. */ export class ChildEventRegistration implements EventRegistration { constructor( @@ -597,9 +879,6 @@ export class ChildEventRegistration implements EventRegistration { } | null ) {} - /** - * @inheritDoc - */ respondsTo(eventType: string): boolean { let eventToCheck = eventType === 'children_added' ? 'child_added' : eventType; @@ -608,9 +887,6 @@ export class ChildEventRegistration implements EventRegistration { return contains(this.callbacks, eventToCheck); } - /** - * @inheritDoc - */ createCancelEvent(error: Error, path: Path): CancelEvent | null { if (this.callbacks['cancel'].hasCancelCallback) { return new CancelEvent(this, error, path); @@ -619,9 +895,6 @@ export class ChildEventRegistration implements EventRegistration { } } - /** - * @inheritDoc - */ createEvent(change: Change, query: QueryContext): DataEvent { assert(change.childName != null, 'Child events should have a childName.'); const childRef = child( @@ -637,9 +910,6 @@ export class ChildEventRegistration implements EventRegistration { ); } - /** - * @inheritDoc - */ getEventRunner(eventData: CancelEvent | DataEvent): () => void { if (eventData.getEventType() === 'cancel') { const cancelCB = this.callbacks['cancel']; @@ -654,9 +924,6 @@ export class ChildEventRegistration implements EventRegistration { } } - /** - * @inheritDoc - */ matches(other: EventRegistration): boolean { if (other instanceof ChildEventRegistration) { if (!this.callbacks || !other.callbacks) { @@ -693,9 +960,6 @@ export class ChildEventRegistration implements EventRegistration { return false; } - /** - * @inheritDoc - */ hasAnyCallback(): boolean { return this.callbacks !== null; } @@ -742,22 +1006,102 @@ function addEventListener( return () => repoRemoveEventCallbackForQuery(query._repo, query, container); } +/** + * Listens for data changes at a particular location. + * + * This is the primary way to read data from a Database. Your callback + * will be triggered for the initial data and again whenever the data changes. + * Invoke the returned unsubscribe callback to stop receiving updates. See + * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve + * Data on the Web} for more details. + * + * An `onValue` event will trigger once with the initial data stored at this + * location, and then trigger again each time the data changes. The + * `DataSnapshot` passed to the callback will be for the location at which + * `on()` was called. It won't trigger until the entire contents has been + * synchronized. If the location has no data, it will be triggered with an empty + * `DataSnapshot` (`val()` will return `null`). + * + * @param query - The query to run. + * @param callback - A callback that fires when the specified event occurs. The + * callback will be passed a DataSnapshot. + * @param cancelCallback - An optional callback that will be notified if your + * event subscription is ever canceled because your client does not have + * permission to read this data (or it had permission but has now lost it). + * This callback will be passed an `Error` object indicating why the failure + * occurred. + * @returns A function that can be invoked to remove the listener. + */ export function onValue( query: Query, callback: (snapshot: DataSnapshot) => unknown, cancelCallback?: (error: Error) => unknown ): Unsubscribe; + +/** + * Listens for data changes at a particular location. + * + * This is the primary way to read data from a Database. Your callback + * will be triggered for the initial data and again whenever the data changes. + * Invoke the returned unsubscribe callback to stop receiving updates. See + * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve + * Data on the Web} for more details. + * + * An `onValue` event will trigger once with the initial data stored at this + * location, and then trigger again each time the data changes. The + * `DataSnapshot` passed to the callback will be for the location at which + * `on()` was called. It won't trigger until the entire contents has been + * synchronized. If the location has no data, it will be triggered with an empty + * `DataSnapshot` (`val()` will return `null`). + * + * @param query - The query to run. + * @param callback - A callback that fires when the specified event occurs. The + * callback will be passed a DataSnapshot. + * @param options - An object that can be used to configure `onlyOnce`, which + * then removes the listener after its first invocation. + * @returns A function that can be invoked to remove the listener. + */ export function onValue( query: Query, callback: (snapshot: DataSnapshot) => unknown, options: ListenOptions ): Unsubscribe; + +/** + * Listens for data changes at a particular location. + * + * This is the primary way to read data from a Database. Your callback + * will be triggered for the initial data and again whenever the data changes. + * Invoke the returned unsubscribe callback to stop receiving updates. See + * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve + * Data on the Web} for more details. + * + * An `onValue` event will trigger once with the initial data stored at this + * location, and then trigger again each time the data changes. The + * `DataSnapshot` passed to the callback will be for the location at which + * `on()` was called. It won't trigger until the entire contents has been + * synchronized. If the location has no data, it will be triggered with an empty + * `DataSnapshot` (`val()` will return `null`). + * + * @param query - The query to run. + * @param callback - A callback that fires when the specified event occurs. The + * callback will be passed a DataSnapshot. + * @param cancelCallback - An optional callback that will be notified if your + * event subscription is ever canceled because your client does not have + * permission to read this data (or it had permission but has now lost it). + * This callback will be passed an `Error` object indicating why the failure + * occurred. + * @param options - An object that can be used to configure `onlyOnce`, which + * then removes the listener after its first invocation. + * @returns A function that can be invoked to remove the listener. + */ export function onValue( query: Query, callback: (snapshot: DataSnapshot) => unknown, cancelCallback: (error: Error) => unknown, options: ListenOptions ): Unsubscribe; + export function onValue( query: Query, callback: (snapshot: DataSnapshot) => unknown, @@ -773,6 +1117,33 @@ export function onValue( ); } +/** + * Listens for data changes at a particular location. + * + * This is the primary way to read data from a Database. Your callback + * will be triggered for the initial data and again whenever the data changes. + * Invoke the returned unsubscribe callback to stop receiving updates. See + * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve + * Data on the Web} for more details. + * + * An `onChildAdded` event will be triggered once for each initial child at this + * location, and it will be triggered again every time a new child is added. The + * `DataSnapshot` passed into the callback will reflect the data for the + * relevant child. For ordering purposes, it is passed a second argument which + * is a string containing the key of the previous sibling child by sort order, + * or `null` if it is the first child. + * + * @param query - The query to run. + * @param callback - A callback that fires when the specified event occurs. + * The callback will be passed a DataSnapshot and a string containing the key of + * the previous child, by sort order, or `null` if it is the first child. + * @param cancelCallback - An optional callback that will be notified if your + * event subscription is ever canceled because your client does not have + * permission to read this data (or it had permission but has now lost it). + * This callback will be passed an `Error` object indicating why the failure + * occurred. + * @returns A function that can be invoked to remove the listener. + */ export function onChildAdded( query: Query, callback: ( @@ -781,6 +1152,31 @@ export function onChildAdded( ) => unknown, cancelCallback?: (error: Error) => unknown ): Unsubscribe; + +/** + * Listens for data changes at a particular location. + * + * This is the primary way to read data from a Database. Your callback + * will be triggered for the initial data and again whenever the data changes. + * Invoke the returned unsubscribe callback to stop receiving updates. See + * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve + * Data on the Web} for more details. + * + * An `onChildAdded` event will be triggered once for each initial child at this + * location, and it will be triggered again every time a new child is added. The + * `DataSnapshot` passed into the callback will reflect the data for the + * relevant child. For ordering purposes, it is passed a second argument which + * is a string containing the key of the previous sibling child by sort order, + * or `null` if it is the first child. + * + * @param query - The query to run. + * @param callback - A callback that fires when the specified event occurs. + * The callback will be passed a DataSnapshot and a string containing the key of + * the previous child, by sort order, or `null` if it is the first child. + * @param options - An object that can be used to configure `onlyOnce`, which + * then removes the listener after its first invocation. + * @returns A function that can be invoked to remove the listener. + */ export function onChildAdded( query: Query, callback: ( @@ -789,6 +1185,36 @@ export function onChildAdded( ) => unknown, options: ListenOptions ): Unsubscribe; + +/** + * Listens for data changes at a particular location. + * + * This is the primary way to read data from a Database. Your callback + * will be triggered for the initial data and again whenever the data changes. + * Invoke the returned unsubscribe callback to stop receiving updates. See + * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve + * Data on the Web} for more details. + * + * An `onChildAdded` event will be triggered once for each initial child at this + * location, and it will be triggered again every time a new child is added. The + * `DataSnapshot` passed into the callback will reflect the data for the + * relevant child. For ordering purposes, it is passed a second argument which + * is a string containing the key of the previous sibling child by sort order, + * or `null` if it is the first child. + * + * @param query - The query to run. + * @param callback - A callback that fires when the specified event occurs. + * The callback will be passed a DataSnapshot and a string containing the key of + * the previous child, by sort order, or `null` if it is the first child. + * @param cancelCallback - An optional callback that will be notified if your + * event subscription is ever canceled because your client does not have + * permission to read this data (or it had permission but has now lost it). + * This callback will be passed an `Error` object indicating why the failure + * occurred. + * @param options - An object that can be used to configure `onlyOnce`, which + * then removes the listener after its first invocation. + * @returns A function that can be invoked to remove the listener. + */ export function onChildAdded( query: Query, callback: ( @@ -798,6 +1224,7 @@ export function onChildAdded( cancelCallback: (error: Error) => unknown, options: ListenOptions ): Unsubscribe; + export function onChildAdded( query: Query, callback: ( @@ -816,6 +1243,34 @@ export function onChildAdded( ); } +/** + * Listens for data changes at a particular location. + * + * This is the primary way to read data from a Database. Your callback + * will be triggered for the initial data and again whenever the data changes. + * Invoke the returned unsubscribe callback to stop receiving updates. See + * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve + * Data on the Web} for more details. + * + * An `onChildChanged` event will be triggered when the data stored in a child + * (or any of its descendants) changes. Note that a single `child_changed` event + * may represent multiple changes to the child. The `DataSnapshot` passed to the + * callback will contain the new child contents. For ordering purposes, the + * callback is also passed a second argument which is a string containing the + * key of the previous sibling child by sort order, or `null` if it is the first + * child. + * + * @param query - The query to run. + * @param callback - A callback that fires when the specified event occurs. + * The callback will be passed a DataSnapshot and a string containing the key of + * the previous child, by sort order, or `null` if it is the first child. + * @param cancelCallback - An optional callback that will be notified if your + * event subscription is ever canceled because your client does not have + * permission to read this data (or it had permission but has now lost it). + * This callback will be passed an `Error` object indicating why the failure + * occurred. + * @returns A function that can be invoked to remove the listener. + */ export function onChildChanged( query: Query, callback: ( @@ -824,6 +1279,32 @@ export function onChildChanged( ) => unknown, cancelCallback?: (error: Error) => unknown ): Unsubscribe; + +/** + * Listens for data changes at a particular location. + * + * This is the primary way to read data from a Database. Your callback + * will be triggered for the initial data and again whenever the data changes. + * Invoke the returned unsubscribe callback to stop receiving updates. See + * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve + * Data on the Web} for more details. + * + * An `onChildChanged` event will be triggered when the data stored in a child + * (or any of its descendants) changes. Note that a single `child_changed` event + * may represent multiple changes to the child. The `DataSnapshot` passed to the + * callback will contain the new child contents. For ordering purposes, the + * callback is also passed a second argument which is a string containing the + * key of the previous sibling child by sort order, or `null` if it is the first + * child. + * + * @param query - The query to run. + * @param callback - A callback that fires when the specified event occurs. + * The callback will be passed a DataSnapshot and a string containing the key of + * the previous child, by sort order, or `null` if it is the first child. + * @param options - An object that can be used to configure `onlyOnce`, which + * then removes the listener after its first invocation. + * @returns A function that can be invoked to remove the listener. + */ export function onChildChanged( query: Query, callback: ( @@ -832,6 +1313,37 @@ export function onChildChanged( ) => unknown, options: ListenOptions ): Unsubscribe; + +/** + * Listens for data changes at a particular location. + * + * This is the primary way to read data from a Database. Your callback + * will be triggered for the initial data and again whenever the data changes. + * Invoke the returned unsubscribe callback to stop receiving updates. See + * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve + * Data on the Web} for more details. + * + * An `onChildChanged` event will be triggered when the data stored in a child + * (or any of its descendants) changes. Note that a single `child_changed` event + * may represent multiple changes to the child. The `DataSnapshot` passed to the + * callback will contain the new child contents. For ordering purposes, the + * callback is also passed a second argument which is a string containing the + * key of the previous sibling child by sort order, or `null` if it is the first + * child. + * + * @param query - The query to run. + * @param callback - A callback that fires when the specified event occurs. + * The callback will be passed a DataSnapshot and a string containing the key of + * the previous child, by sort order, or `null` if it is the first child. + * @param cancelCallback - An optional callback that will be notified if your + * event subscription is ever canceled because your client does not have + * permission to read this data (or it had permission but has now lost it). + * This callback will be passed an `Error` object indicating why the failure + * occurred. + * @param options - An object that can be used to configure `onlyOnce`, which + * then removes the listener after its first invocation. + * @returns A function that can be invoked to remove the listener. + */ export function onChildChanged( query: Query, callback: ( @@ -841,6 +1353,7 @@ export function onChildChanged( cancelCallback: (error: Error) => unknown, options: ListenOptions ): Unsubscribe; + export function onChildChanged( query: Query, callback: ( @@ -859,6 +1372,32 @@ export function onChildChanged( ); } +/** + * Listens for data changes at a particular location. + * + * This is the primary way to read data from a Database. Your callback + * will be triggered for the initial data and again whenever the data changes. + * Invoke the returned unsubscribe callback to stop receiving updates. See + * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve + * Data on the Web} for more details. + * + * An `onChildMoved` event will be triggered when a child's sort order changes + * such that its position relative to its siblings changes. The `DataSnapshot` + * passed to the callback will be for the data of the child that has moved. It + * is also passed a second argument which is a string containing the key of the + * previous sibling child by sort order, or `null` if it is the first child. + * + * @param query - The query to run. + * @param callback - A callback that fires when the specified event occurs. + * The callback will be passed a DataSnapshot and a string containing the key of + * the previous child, by sort order, or `null` if it is the first child. + * @param cancelCallback - An optional callback that will be notified if your + * event subscription is ever canceled because your client does not have + * permission to read this data (or it had permission but has now lost it). + * This callback will be passed an `Error` object indicating why the failure + * occurred. + * @returns A function that can be invoked to remove the listener. + */ export function onChildMoved( query: Query, callback: ( @@ -867,6 +1406,30 @@ export function onChildMoved( ) => unknown, cancelCallback?: (error: Error) => unknown ): Unsubscribe; + +/** + * Listens for data changes at a particular location. + * + * This is the primary way to read data from a Database. Your callback + * will be triggered for the initial data and again whenever the data changes. + * Invoke the returned unsubscribe callback to stop receiving updates. See + * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve + * Data on the Web} for more details. + * + * An `onChildMoved` event will be triggered when a child's sort order changes + * such that its position relative to its siblings changes. The `DataSnapshot` + * passed to the callback will be for the data of the child that has moved. It + * is also passed a second argument which is a string containing the key of the + * previous sibling child by sort order, or `null` if it is the first child. + * + * @param query - The query to run. + * @param callback - A callback that fires when the specified event occurs. + * The callback will be passed a DataSnapshot and a string containing the key of + * the previous child, by sort order, or `null` if it is the first child. + * @param options - An object that can be used to configure `onlyOnce`, which + * then removes the listener after its first invocation. + * @returns A function that can be invoked to remove the listener. + */ export function onChildMoved( query: Query, callback: ( @@ -875,6 +1438,35 @@ export function onChildMoved( ) => unknown, options: ListenOptions ): Unsubscribe; + +/** + * Listens for data changes at a particular location. + * + * This is the primary way to read data from a Database. Your callback + * will be triggered for the initial data and again whenever the data changes. + * Invoke the returned unsubscribe callback to stop receiving updates. See + * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve + * Data on the Web} for more details. + * + * An `onChildMoved` event will be triggered when a child's sort order changes + * such that its position relative to its siblings changes. The `DataSnapshot` + * passed to the callback will be for the data of the child that has moved. It + * is also passed a second argument which is a string containing the key of the + * previous sibling child by sort order, or `null` if it is the first child. + * + * @param query - The query to run. + * @param callback - A callback that fires when the specified event occurs. + * The callback will be passed a DataSnapshot and a string containing the key of + * the previous child, by sort order, or `null` if it is the first child. + * @param cancelCallback - An optional callback that will be notified if your + * event subscription is ever canceled because your client does not have + * permission to read this data (or it had permission but has now lost it). + * This callback will be passed an `Error` object indicating why the failure + * occurred. + * @param options - An object that can be used to configure `onlyOnce`, which + * then removes the listener after its first invocation. + * @returns A function that can be invoked to remove the listener. + */ export function onChildMoved( query: Query, callback: ( @@ -884,6 +1476,7 @@ export function onChildMoved( cancelCallback: (error: Error) => unknown, options: ListenOptions ): Unsubscribe; + export function onChildMoved( query: Query, callback: ( @@ -902,22 +1495,114 @@ export function onChildMoved( ); } +/** + * Listens for data changes at a particular location. + * + * This is the primary way to read data from a Database. Your callback + * will be triggered for the initial data and again whenever the data changes. + * Invoke the returned unsubscribe callback to stop receiving updates. See + * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve + * Data on the Web} for more details. + * + * An `onChildRemoved` event will be triggered once every time a child is + * removed. The `DataSnapshot` passed into the callback will be the old data for + * the child that was removed. A child will get removed when either: + * + * - a client explicitly calls `remove()` on that child or one of its ancestors + * - a client calls `set(null)` on that child or one of its ancestors + * - that child has all of its children removed + * - there is a query in effect which now filters out the child (because it's + * sort order changed or the max limit was hit) + * + * @param query - The query to run. + * @param callback - A callback that fires when the specified event occurs. + * The callback will be passed a DataSnapshot and a string containing the key of + * the previous child, by sort order, or `null` if it is the first child. + * @param cancelCallback - An optional callback that will be notified if your + * event subscription is ever canceled because your client does not have + * permission to read this data (or it had permission but has now lost it). + * This callback will be passed an `Error` object indicating why the failure + * occurred. + * @returns A function that can be invoked to remove the listener. + */ export function onChildRemoved( query: Query, callback: (snapshot: DataSnapshot) => unknown, cancelCallback?: (error: Error) => unknown ): Unsubscribe; + +/** + * Listens for data changes at a particular location. + * + * This is the primary way to read data from a Database. Your callback + * will be triggered for the initial data and again whenever the data changes. + * Invoke the returned unsubscribe callback to stop receiving updates. See + * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve + * Data on the Web} for more details. + * + * An `onChildRemoved` event will be triggered once every time a child is + * removed. The `DataSnapshot` passed into the callback will be the old data for + * the child that was removed. A child will get removed when either: + * + * - a client explicitly calls `remove()` on that child or one of its ancestors + * - a client calls `set(null)` on that child or one of its ancestors + * - that child has all of its children removed + * - there is a query in effect which now filters out the child (because it's + * sort order changed or the max limit was hit) + * + * @param query - The query to run. + * @param callback - A callback that fires when the specified event occurs. + * The callback will be passed a DataSnapshot and a string containing the key of + * the previous child, by sort order, or `null` if it is the first child. + * @param options - An object that can be used to configure `onlyOnce`, which + * then removes the listener after its first invocation. + * @returns A function that can be invoked to remove the listener. + */ export function onChildRemoved( query: Query, callback: (snapshot: DataSnapshot) => unknown, options: ListenOptions ): Unsubscribe; + +/** + * Listens for data changes at a particular location. + * + * This is the primary way to read data from a Database. Your callback + * will be triggered for the initial data and again whenever the data changes. + * Invoke the returned unsubscribe callback to stop receiving updates. See + * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve + * Data on the Web} for more details. + * + * An `onChildRemoved` event will be triggered once every time a child is + * removed. The `DataSnapshot` passed into the callback will be the old data for + * the child that was removed. A child will get removed when either: + * + * - a client explicitly calls `remove()` on that child or one of its ancestors + * - a client calls `set(null)` on that child or one of its ancestors + * - that child has all of its children removed + * - there is a query in effect which now filters out the child (because it's + * sort order changed or the max limit was hit) + * + * @param query - The query to run. + * @param callback - A callback that fires when the specified event occurs. + * The callback will be passed a DataSnapshot and a string containing the key of + * the previous child, by sort order, or `null` if it is the first child. + * @param cancelCallback - An optional callback that will be notified if your + * event subscription is ever canceled because your client does not have + * permission to read this data (or it had permission but has now lost it). + * This callback will be passed an `Error` object indicating why the failure + * occurred. + * @param options - An object that can be used to configure `onlyOnce`, which + * then removes the listener after its first invocation. + * @returns A function that can be invoked to remove the listener. + */ export function onChildRemoved( query: Query, callback: (snapshot: DataSnapshot) => unknown, cancelCallback: (error: Error) => unknown, options: ListenOptions ): Unsubscribe; + export function onChildRemoved( query: Query, callback: (snapshot: DataSnapshot) => unknown, @@ -935,6 +1620,30 @@ export function onChildRemoved( export { EventType }; +/** + * Detaches a callback previously attached with `on()`. + * + * Detach a callback previously attached with `on()`. Note that if `on()` was + * called multiple times with the same eventType and callback, the callback + * will be called multiple times for each event, and `off()` must be called + * multiple times to remove the callback. Calling `off()` on a parent listener + * will not automatically remove listeners registered on child nodes, `off()` + * must also be called on any child listeners to remove the callback. + * + * If a callback is not specified, all callbacks for the specified eventType + * will be removed. Similarly, if no eventType is specified, all callbacks + * for the `Reference` will be removed. + * + * Individual listeners can also be removed by invoking their unsubscribe + * callbacks. + * + * @param query - The query that the listener was registered with. + * @param eventType One of the following strings: "value", "child_added", + * "child_changed", "child_removed", or "child_moved." If omitted, all callbacks + * for the `Reference` will be removed. + * @param callback The callback function that was passed to `on()` or + * `undefined` to remove all callbacks. + */ export function off( query: Query, eventType?: EventType, @@ -1027,6 +1736,30 @@ class QueryEndAtConstraint extends QueryConstraint { } } +/** + * Creates a `QueryConstraint` with the specified ending point. + * + * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()` + * allows you to choose arbitrary starting and ending points for your queries. + * + * The ending point is inclusive, so children with exactly the specified value + * will be included in the query. The optional key argument can be used to + * further limit the range of the query. If it is specified, then children that + * have exactly the specified value must also have a key name less than or equal + * to the specified key. + * + * You can read more about `endAt()` in{@link + * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data + * Filtering data}. + * + * @param value - The value to end at. The argument type depends on which + * `orderBy*()` function was used in this query. Specify a value that matches + * the `orderBy*()` type. When used in combination with `orderByKey()`, the + * value must be a string. + * @param key - The child key to end at, among the children with the previously + * specified priority. This argument is only allowed if ordering by child, + * value, or priority. + */ export function endAt( value: number | string | boolean | null, key?: string @@ -1069,6 +1802,25 @@ class QueryEndBeforeConstraint extends QueryConstraint { } } +/** + * Creates a `QueryConstraint` with the specified ending point (exclusive). + * + * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()` + * allows you to choose arbitrary starting and ending points for your queries. + * + * The ending point is exclusive. If only a value is provided, children + * with a value less than the specified value will be included in the query. + * If a key is specified, then children must have a value lesss than or equal + * to the specified value and a a key name less than the specified key. + * + * @param value - The value to end before. The argument type depends on which + * `orderBy*()` function was used in this query. Specify a value that matches + * the `orderBy*()` type. When used in combination with `orderByKey()`, the + * value must be a string. + * @param key - The child key to end before, among the children with the + * previously specified priority. This argument is only allowed if ordering by + * child, value, or priority. + */ export function endBefore( value: number | string | boolean | null, key?: string @@ -1111,6 +1863,29 @@ class QueryStartAtConstraint extends QueryConstraint { } } +/** + * Creates a `QueryConstraint` with the specified starting point. + * + * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()` + * allows you to choose arbitrary starting and ending points for your queries. + * + * The starting point is inclusive, so children with exactly the specified value + * will be included in the query. The optional key argument can be used to + * further limit the range of the query. If it is specified, then children that + * have exactly the specified value must also have a key name greater than or + * equal to the specified key. + * + * You can read more about `startAt()` in {@link + * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data + * Filtering data}. + * + * @param value - The value to start at. The argument type depends on which + * `orderBy*()` function was used in this query. Specify a value that matches + * the `orderBy*()` type. When used in combination with `orderByKey()`, the + * value must be a string. + * @param key - The child key to start at. This argument is only allowed if + * ordering by child, value, or priority. + */ export function startAt( value: number | string | boolean | null = null, key?: string @@ -1153,6 +1928,24 @@ class QueryStartAfterConstraint extends QueryConstraint { } } +/** + * Creates a `QueryConstraint` with the specified starting point (exclusive). + * + * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()` + * allows you to choose arbitrary starting and ending points for your queries. + * + * The starting point is exclusive. If only a value is provided, children + * with a value greater than the specified value will be included in the query. + * If a key is specified, then children must have a value greater than or equal + * to the specified value and a a key name greater than the specified key. + * + * @param value The value to start after. The argument type depends on which + * `orderBy*()` function was used in this query. Specify a value that matches + * the `orderBy*()` type. When used in combination with `orderByKey()`, the + * value must be a string. + * @param key - The child key to start after. This argument is only allowed if + * ordering by child, value, or priority. + */ export function startAfter( value: number | string | boolean | null, key?: string @@ -1184,6 +1977,25 @@ class QueryLimitToFirstConstraint extends QueryConstraint { } } +/** + * Creates a new `QueryConstraint` that if limited to the first specific number + * of children. + * + * The `limitToFirst()` method is used to set a maximum number of children to be + * synced for a given callback. If we set a limit of 100, we will initially only + * receive up to 100 `child_added` events. If we have fewer than 100 messages + * stored in our Database, a `child_added` event will fire for each message. + * However, if we have over 100 messages, we will only receive a `child_added` + * event for the first 100 ordered messages. As items change, we will receive + * `child_removed` events for each item that drops out of the active list so + * that the total number stays at 100. + * + * You can read more about `limitToFirst()` in {@link + * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data + * Filtering data}. + * + * @param limit - The maximum number of nodes to include in this query. + */ export function limitToFirst(limit: number): QueryConstraint { if (typeof limit !== 'number' || Math.floor(limit) !== limit || limit <= 0) { throw new Error('limitToFirst: First argument must be a positive integer.'); @@ -1214,6 +2026,25 @@ class QueryLimitToLastConstraint extends QueryConstraint { } } +/** + * Creates a new `QueryConstraint` that is limited to return only the last + * specified number of children. + * + * The `limitToLast()` method is used to set a maximum number of children to be + * synced for a given callback. If we set a limit of 100, we will initially only + * receive up to 100 `child_added` events. If we have fewer than 100 messages + * stored in our Database, a `child_added` event will fire for each message. + * However, if we have over 100 messages, we will only receive a `child_added` + * event for the last 100 ordered messages. As items change, we will receive + * `child_removed` events for each item that drops out of the active list so + * that the total number stays at 100. + * + * You can read more about `limitToLast()` in {@link + * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data + * Filtering data}. + * + * @param limit - The maximum number of nodes to include in this query. + */ export function limitToLast(limit: number): QueryConstraint { if (typeof limit !== 'number' || Math.floor(limit) !== limit || limit <= 0) { throw new Error('limitToLast: First argument must be a positive integer.'); @@ -1250,6 +2081,24 @@ class QueryOrderByChildConstraint extends QueryConstraint { } } +/** + * Creates a new `QueryConstraint` that orders by the specified child key. + * + * Queries can only order by one key at a time. Calling `orderByChild()` + * multiple times on the same query is an error. + * + * Firebase queries allow you to order your data by any child key on the fly. + * However, if you know in advance what your indexes will be, you can define + * them via the .indexOn rule in your Security Rules for better performance. See + * the {@link https://firebase.google.com/docs/database/security/indexing-data + * .indexOn} rule for more information. + * + * You can read more about `orderByChild()` in {@link + * https://firebase.google.com/docs/database/web/lists-of-data#sort_data + * Sort data}. + * + * @param path - The path to order by. + */ export function orderByChild(path: string): QueryConstraint { if (path === '$key') { throw new Error( @@ -1284,6 +2133,15 @@ class QueryOrderByKeyConstraint extends QueryConstraint { } } +/** + * Creates a new `QueryConstraint` that orders by the key. + * + * Sorts the results of a query by their (ascending) key values. + * + * You can read more about `orderByKey()` in {@link + * https://firebase.google.com/docs/database/web/lists-of-data#sort_data + * Sort data}. + */ export function orderByKey(): QueryConstraint { return new QueryOrderByKeyConstraint(); } @@ -1304,6 +2162,14 @@ class QueryOrderByPriorityConstraint extends QueryConstraint { } } +/** + * Creates a new `QueryConstraint` that orders by priority. + * + * Applications need not use priority but can order collections by + * ordinary properties (see {@link + * https://firebase.google.com/docs/database/web/lists-of-data#sort_data + * Sort data} for alternatives to priority. + */ export function orderByPriority(): QueryConstraint { return new QueryOrderByPriorityConstraint(); } @@ -1324,6 +2190,16 @@ class QueryOrderByValueConstraint extends QueryConstraint { } } +/** + * Creates a new `QueryConstraint` that orders by value. + * + * If the children of a query are all scalar values (string, number, or + * boolean), you can order the results by their (ascending) values. + * + * You can read more about `orderByValue()` in {@link + * https://firebase.google.com/docs/database/web/lists-of-data#sort_data + * Sort data}. + */ export function orderByValue(): QueryConstraint { return new QueryOrderByValueConstraint(); } @@ -1358,6 +2234,30 @@ class QueryEqualToValueConstraint extends QueryConstraint { } } +/** + * Creates a `QueryConstraint` that includes children that match the specified + * value. + * + * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()` + * allows you to choose arbitrary starting and ending points for your queries. + * + * The optional key argument can be used to further limit the range of the + * query. If it is specified, then children that have exactly the specified + * value must also have exactly the specified key as their key name. This can be + * used to filter result sets with many matches for the same value. + * + * You can read more about `equalTo()` in {@link + * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data + * Filtering data}. + * + * @param value - The value to match for. The argument type depends on which + * `orderBy*()` function was used in this query. Specify a value that matches + * the `orderBy*()` type. When used in combination with `orderByKey()`, the + * value must be a string. + * @param key - The child key to start at, among the children with the + * previously specified priority. This argument is only allowed if ordering by + * child, value, or priority. + */ export function equalTo( value: number | string | boolean | null, key?: string diff --git a/packages/database/src/exp/ServerValue.ts b/packages/database/src/exp/ServerValue.ts index 6fdecee1b2a..30cdffdd509 100644 --- a/packages/database/src/exp/ServerValue.ts +++ b/packages/database/src/exp/ServerValue.ts @@ -19,10 +19,22 @@ const SERVER_TIMESTAMP = { '.sv': 'timestamp' }; +/** + * Returns a placeholder value for auto-populating the current timestamp (time + * since the Unix epoch, in milliseconds) as determined by the Firebase + * servers. + */ export function serverTimestamp(): object { return SERVER_TIMESTAMP; } +/** + * Returns a placeholder value that can be used to atomically increment the + * current database value by the provided delta. + * + * @param delta the amount to modify the current value atomically. + * @returns A placeholder value for modifying data atomically server-side. + */ export function increment(delta: number): object { return { '.sv': { diff --git a/packages/database/src/exp/Transaction.ts b/packages/database/src/exp/Transaction.ts index d20723cdec8..a7555ec3f99 100644 --- a/packages/database/src/exp/Transaction.ts +++ b/packages/database/src/exp/Transaction.ts @@ -25,7 +25,14 @@ import { validateWritablePath } from '../core/util/validation'; import { Reference } from './Reference'; import { DataSnapshot, onValue, ReferenceImpl } from './Reference_impl'; +/** An options object to configure transactions. */ export interface TransactionOptions { + /** + * By default, events are raised each time the transaction update function + * runs. So if it is run multiple times, you may see intermediate states. You + * can set this to false to suppress these intermediate states and instead + * wait until the transaction has completed before events are raised. + */ readonly applyLocally?: boolean; } @@ -40,6 +47,43 @@ export class TransactionResult { } } +/** + * Atomically modifies the data at this location. + * + * Atomically modify the data at this location. Unlike a normal `set()`, which + * just overwrites the data regardless of its previous value, `transaction()` is + * used to modify the existing value to a new value, ensuring there are no + * conflicts with other clients writing to the same location at the same time. + * + * To accomplish this, you pass `runTransaction()` an update function which is + * used to transform the current value into a new value. If another client + * writes to the location before your new value is successfully written, your + * update function will be called again with the new current value, and the + * write will be retried. This will happen repeatedly until your write succeeds + * without conflict or you abort the transaction by not returning a value from + * your update function. + * + * Note: Modifying data with `set()` will cancel any pending transactions at + * that location, so extreme care should be taken if mixing `set()` and + * `transaction()` to update the same data. + * + * Note: When using transactions with Security and Firebase Rules in place, be + * aware that a client needs `.read` access in addition to `.write` access in + * order to perform a transaction. This is because the client-side nature of + * transactions requires the client to read the data in order to transactionally + * update it. + * + * @param ref - The location to atomically modify. + * @param transactionUpdate - A developer-supplied function which will be passed + * the current data stored at this location (as a JavaScript object). The + * function should return the new value it would like written (as a JavaScript + * object). If `undefined` is returned (i.e. you return with no arguments) the + * transaction will be aborted and the data at this location will not be + * modified. + * @param options - An options object to configure transactions. + * @returns A Promise that can optionally be used instead of the onComplete + * callback to handle success and failure. + */ export function runTransaction( ref: Reference, // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/database/src/realtime/TransportManager.ts b/packages/database/src/realtime/TransportManager.ts index 787761c34fd..19ec62dc2e4 100644 --- a/packages/database/src/realtime/TransportManager.ts +++ b/packages/database/src/realtime/TransportManager.ts @@ -72,7 +72,7 @@ export class TransportManager { } /** - * @return The constructor for the initial transport to use + * @returns The constructor for the initial transport to use */ initialTransport(): TransportConstructor { if (this.transports_.length > 0) { @@ -83,7 +83,7 @@ export class TransportManager { } /** - * @return The constructor for the next transport, or null + * @returns The constructor for the next transport, or null */ upgradeTransport(): TransportConstructor | null { if (this.transports_.length > 1) { diff --git a/packages/database/src/realtime/WebSocketConnection.ts b/packages/database/src/realtime/WebSocketConnection.ts index 707c7c5f940..929f2f799d1 100644 --- a/packages/database/src/realtime/WebSocketConnection.ts +++ b/packages/database/src/realtime/WebSocketConnection.ts @@ -102,7 +102,7 @@ export class WebSocketConnection implements Transport { * @param transportSessionId Optional transportSessionId if this is connecting to an existing transport * session * @param lastSessionId Optional lastSessionId if there was a previous connection - * @return connection url + * @returns connection url */ private static connectionURL_( repoInfo: RepoInfo, @@ -288,7 +288,7 @@ export class WebSocketConnection implements Transport { /** * Attempts to parse a frame count out of some text. If it can't, assumes a value of 1 - * @return Any remaining data to be process, or null if there is none + * @returns Any remaining data to be process, or null if there is none */ private extractFrameCount_(data: string): string | null { assert(this.frames === null, 'We already have a frame buffer'); diff --git a/packages/database/test/datasnapshot.test.ts b/packages/database/test/datasnapshot.test.ts index 3afdfde8ddf..d6114b39ff1 100644 --- a/packages/database/test/datasnapshot.test.ts +++ b/packages/database/test/datasnapshot.test.ts @@ -25,7 +25,7 @@ import { DataSnapshot as ExpDataSnapshot } from '../src/exp/Reference_impl'; import { getRandomNode } from './helpers/util'; describe('DataSnapshot Tests', () => { - /** @return {!DataSnapshot} */ + /** @returns {!DataSnapshot} */ const snapshotForJSON = function (json) { const dummyRef = getRandomNode() as Reference; return new DataSnapshot( From d78deae3591bf6fdc445ce205cd59e5646a29a0d Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Wed, 7 Apr 2021 14:51:10 -0600 Subject: [PATCH 13/15] Add Support for API report (#4741) --- common/api-review/database-exp.api.md | 234 ++++++++++++++++++ packages/database/.eslintrc.js | 6 + packages/database/exp/index.ts | 15 +- packages/database/exp/package.json | 2 +- packages/database/index.node.ts | 8 +- packages/database/package.json | 5 +- packages/database/scripts/api-report.ts | 178 +++++++++++++ packages/database/src/api/Database.ts | 6 +- packages/database/src/api/Reference.ts | 14 +- packages/database/src/api/internal.ts | 8 +- packages/database/src/core/CompoundWrite.ts | 16 +- .../database/src/core/PersistentConnection.ts | 43 +--- .../database/src/core/ReadonlyRestClient.ts | 4 +- packages/database/src/core/Repo.ts | 33 +-- packages/database/src/core/RepoInfo.ts | 12 +- packages/database/src/core/ServerActions.ts | 2 +- .../database/src/core/SparseSnapshotTree.ts | 14 +- packages/database/src/core/SyncPoint.ts | 10 +- packages/database/src/core/SyncTree.ts | 27 +- packages/database/src/core/WriteTree.ts | 10 +- .../src/core/operation/AckUserWrite.ts | 6 +- packages/database/src/core/operation/Merge.ts | 8 - .../database/src/core/snap/ChildrenNode.ts | 24 +- packages/database/src/core/snap/LeafNode.ts | 31 +-- packages/database/src/core/snap/Node.ts | 26 +- packages/database/src/core/snap/childSet.ts | 8 +- .../src/core/snap/indexes/KeyIndex.ts | 20 -- .../src/core/snap/indexes/PathIndex.ts | 20 -- .../src/core/snap/indexes/PriorityIndex.ts | 19 -- .../src/core/snap/indexes/ValueIndex.ts | 19 -- .../database/src/core/snap/nodeFromJSON.ts | 4 +- .../database/src/core/stats/StatsListener.ts | 2 +- .../database/src/core/stats/StatsReporter.ts | 4 - .../src/core/storage/DOMStorageWrapper.ts | 6 +- packages/database/src/core/storage/storage.ts | 2 +- .../database/src/core/util/ImmutableTree.ts | 22 +- packages/database/src/core/util/Path.ts | 6 +- .../database/src/core/util/ServerValues.ts | 6 +- packages/database/src/core/util/SortedMap.ts | 73 +++--- packages/database/src/core/util/Tree.ts | 28 +-- packages/database/src/core/util/util.ts | 30 +-- packages/database/src/core/view/Change.ts | 10 +- .../src/core/view/CompleteChildSource.ts | 15 -- packages/database/src/core/view/Event.ts | 40 +-- packages/database/src/core/view/EventQueue.ts | 10 +- .../database/src/core/view/QueryParams.ts | 4 +- packages/database/src/core/view/View.ts | 4 +- .../src/core/view/filter/IndexedFilter.ts | 20 -- .../src/core/view/filter/LimitedFilter.ts | 24 -- .../src/core/view/filter/RangedFilter.ts | 24 -- packages/database/src/exp/Database.ts | 18 +- packages/database/src/exp/OnDisconnect.ts | 20 +- packages/database/src/exp/Reference.ts | 8 +- packages/database/src/exp/Reference_impl.ts | 168 ++++++------- packages/database/src/exp/ServerValue.ts | 2 +- packages/database/src/exp/Transaction.ts | 15 +- .../src/realtime/BrowserPollConnection.ts | 16 +- packages/database/src/realtime/Connection.ts | 6 +- packages/database/src/realtime/Transport.ts | 14 +- .../database/src/realtime/TransportManager.ts | 2 +- .../src/realtime/WebSocketConnection.ts | 28 +-- packages/database/test/helpers/events.ts | 2 +- repo-scripts/prune-dts/prune-dts.ts | 2 +- 63 files changed, 792 insertions(+), 671 deletions(-) create mode 100644 common/api-review/database-exp.api.md create mode 100644 packages/database/scripts/api-report.ts diff --git a/common/api-review/database-exp.api.md b/common/api-review/database-exp.api.md new file mode 100644 index 00000000000..7a36c6f43b1 --- /dev/null +++ b/common/api-review/database-exp.api.md @@ -0,0 +1,234 @@ +## API Report File for "@firebase/database-exp" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { FirebaseApp } from '@firebase/app'; + +// @public (undocumented) +export class DataSnapshot { + child(path: string): DataSnapshot; + exists(): boolean; + exportVal(): any; + forEach(action: (child: DataSnapshot) => boolean | void): boolean; + hasChild(path: string): boolean; + hasChildren(): boolean; + get key(): string | null; + get priority(): string | number | null; + readonly ref: Reference; + get size(): number; + toJSON(): object | null; + val(): any; +} + +// @public +export function enableLogging(enabled: boolean, persistent?: boolean): any; + +// @public +export function enableLogging(logger?: (message: string) => unknown, persistent?: boolean): any; + +// @public +export function endAt(value: number | string | boolean | null, key?: string): QueryConstraint; + +// @public +export function endBefore(value: number | string | boolean | null, key?: string): QueryConstraint; + +// @public +export function equalTo(value: number | string | boolean | null, key?: string): QueryConstraint; + +// @public +export type EventType = 'value' | 'child_added' | 'child_changed' | 'child_moved' | 'child_removed'; + +// @public +export class FirebaseDatabase { + readonly app: FirebaseApp; + readonly 'type' = "database"; +} + +// @public +export function get(query: Query): Promise; + +// @public +export function getDatabase(app: FirebaseApp, url?: string): FirebaseDatabase; + +// @public +export function goOffline(db: FirebaseDatabase): void; + +// @public +export function goOnline(db: FirebaseDatabase): void; + +// @public +export function increment(delta: number): object; + +// @public +export function limitToFirst(limit: number): QueryConstraint; + +// @public +export function limitToLast(limit: number): QueryConstraint; + +// @public +export interface ListenOptions { + readonly onlyOnce?: boolean; +} + +// @public +export function off(query: Query, eventType?: EventType, callback?: (snapshot: DataSnapshot, previousChildName?: string | null) => unknown): void; + +// @public +export function onChildAdded(query: Query, callback: (snapshot: DataSnapshot, previousChildName?: string | null) => unknown, cancelCallback?: (error: Error) => unknown): Unsubscribe; + +// @public +export function onChildAdded(query: Query, callback: (snapshot: DataSnapshot, previousChildName: string | null) => unknown, options: ListenOptions): Unsubscribe; + +// @public +export function onChildAdded(query: Query, callback: (snapshot: DataSnapshot, previousChildName: string | null) => unknown, cancelCallback: (error: Error) => unknown, options: ListenOptions): Unsubscribe; + +// @public +export function onChildChanged(query: Query, callback: (snapshot: DataSnapshot, previousChildName: string | null) => unknown, cancelCallback?: (error: Error) => unknown): Unsubscribe; + +// @public +export function onChildChanged(query: Query, callback: (snapshot: DataSnapshot, previousChildName: string | null) => unknown, options: ListenOptions): Unsubscribe; + +// @public +export function onChildChanged(query: Query, callback: (snapshot: DataSnapshot, previousChildName: string | null) => unknown, cancelCallback: (error: Error) => unknown, options: ListenOptions): Unsubscribe; + +// @public +export function onChildMoved(query: Query, callback: (snapshot: DataSnapshot, previousChildName: string | null) => unknown, cancelCallback?: (error: Error) => unknown): Unsubscribe; + +// @public +export function onChildMoved(query: Query, callback: (snapshot: DataSnapshot, previousChildName: string | null) => unknown, options: ListenOptions): Unsubscribe; + +// @public +export function onChildMoved(query: Query, callback: (snapshot: DataSnapshot, previousChildName: string | null) => unknown, cancelCallback: (error: Error) => unknown, options: ListenOptions): Unsubscribe; + +// @public +export function onChildRemoved(query: Query, callback: (snapshot: DataSnapshot) => unknown, cancelCallback?: (error: Error) => unknown): Unsubscribe; + +// @public +export function onChildRemoved(query: Query, callback: (snapshot: DataSnapshot) => unknown, options: ListenOptions): Unsubscribe; + +// @public +export function onChildRemoved(query: Query, callback: (snapshot: DataSnapshot) => unknown, cancelCallback: (error: Error) => unknown, options: ListenOptions): Unsubscribe; + +// @public +export class OnDisconnect { + cancel(): Promise; + remove(): Promise; + set(value: unknown): Promise; + setWithPriority(value: unknown, priority: number | string | null): Promise; + update(values: object): Promise; +} + +// @public +export function onDisconnect(ref: Reference): OnDisconnect; + +// @public +export function onValue(query: Query, callback: (snapshot: DataSnapshot) => unknown, cancelCallback?: (error: Error) => unknown): Unsubscribe; + +// @public +export function onValue(query: Query, callback: (snapshot: DataSnapshot) => unknown, options: ListenOptions): Unsubscribe; + +// @public +export function onValue(query: Query, callback: (snapshot: DataSnapshot) => unknown, cancelCallback: (error: Error) => unknown, options: ListenOptions): Unsubscribe; + +// @public +export function orderByChild(path: string): QueryConstraint; + +// @public +export function orderByKey(): QueryConstraint; + +// @public +export function orderByPriority(): QueryConstraint; + +// @public +export function orderByValue(): QueryConstraint; + +// @public +export function push(parent: Reference, value?: unknown): ThenableReference; + +// @public +export interface Query { + isEqual(other: Query | null): boolean; + readonly ref: Reference; + toJSON(): string; + toString(): string; +} + +// @public +export function query(query: Query, ...queryConstraints: QueryConstraint[]): Query; + +// @public +export abstract class QueryConstraint { + abstract readonly type: QueryConstraintType; +} + +// @public +export type QueryConstraintType = 'endAt' | 'endBefore' | 'startAt' | 'startAfter' | 'limitToFirst' | 'limitToLast' | 'orderByChild' | 'orderByKey' | 'orderByPriority' | 'orderByValue' | 'equalTo'; + +// @public +export function ref(db: FirebaseDatabase, path?: string): Reference; + +// @public +export interface Reference extends Query { + readonly key: string | null; + readonly parent: Reference | null; + readonly root: Reference; +} + +// @public +export function refFromURL(db: FirebaseDatabase, url: string): Reference; + +// @public +export function remove(ref: Reference): Promise; + +// @public +export function runTransaction(ref: Reference, transactionUpdate: (currentData: any) => unknown, options?: TransactionOptions): Promise; + +// @public +export function serverTimestamp(): object; + +// @public +export function set(ref: Reference, value: unknown): Promise; + +// @public +export function setPriority(ref: Reference, priority: string | number | null): Promise; + +// @public +export function setWithPriority(ref: Reference, value: unknown, priority: string | number | null): Promise; + +// @public +export function startAfter(value: number | string | boolean | null, key?: string): QueryConstraint; + +// @public +export function startAt(value?: number | string | boolean | null, key?: string): QueryConstraint; + +// @public +export interface ThenableReference extends Reference, Pick, 'then' | 'catch'> { +} + +// @public +export interface TransactionOptions { + readonly applyLocally?: boolean; +} + +// @public +export class TransactionResult { + readonly committed: boolean; + readonly snapshot: DataSnapshot; + toJSON(): object; +} + +// @public +export type Unsubscribe = () => void; + +// @public +export function update(ref: Reference, values: object): Promise; + +// @public +export function useDatabaseEmulator(db: FirebaseDatabase, host: string, port: number): void; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/database/.eslintrc.js b/packages/database/.eslintrc.js index d569aa23a76..3a04a986c41 100644 --- a/packages/database/.eslintrc.js +++ b/packages/database/.eslintrc.js @@ -53,6 +53,12 @@ module.exports = { rules: { '@typescript-eslint/no-explicit-any': 'off' } + }, + { + files: ['scripts/*.ts'], + rules: { + 'import/no-extraneous-dependencies': 'off' + } } ] }; diff --git a/packages/database/exp/index.ts b/packages/database/exp/index.ts index 074202a22de..05f704e3149 100644 --- a/packages/database/exp/index.ts +++ b/packages/database/exp/index.ts @@ -26,6 +26,7 @@ import { } from '../src/exp/Database'; export { + FirebaseDatabase, enableLogging, getDatabase, goOffline, @@ -41,9 +42,10 @@ export { } from '../src/exp/Reference'; export { OnDisconnect } from '../src/exp/OnDisconnect'; export { - QueryConstraint, DataSnapshot, EventType, + QueryConstraint, + QueryConstraintType, endAt, endBefore, equalTo, @@ -55,16 +57,23 @@ export { onChildChanged, onChildMoved, onChildRemoved, + onDisconnect, onValue, orderByChild, orderByKey, orderByPriority, orderByValue, + push, query, + ref, + refFromURL, + remove, + set, + setPriority, + setWithPriority, startAfter, startAt, - ref, - refFromURL + update } from '../src/exp/Reference_impl'; export { increment, serverTimestamp } from '../src/exp/ServerValue'; export { diff --git a/packages/database/exp/package.json b/packages/database/exp/package.json index b6cf2b6b028..916f8918e64 100644 --- a/packages/database/exp/package.json +++ b/packages/database/exp/package.json @@ -5,5 +5,5 @@ "browser": "../dist/exp/index.esm.js", "module": "../dist/exp/index.esm.js", "esm2017": "../dist/exp/index.esm2017.js", - "typings": "../exp-types/index.d.ts" + "typings": "../dist/exp/index.d.ts" } diff --git a/packages/database/index.node.ts b/packages/database/index.node.ts index 30a8a7bb0e5..277365da490 100644 --- a/packages/database/index.node.ts +++ b/packages/database/index.node.ts @@ -40,10 +40,10 @@ const ServerValue = Database.ServerValue; * A one off register function which returns a database based on the app and * passed database URL. (Used by the Admin SDK) * - * @param app A valid FirebaseApp-like object - * @param url A valid Firebase databaseURL - * @param version custom version e.g. firebase-admin version - * @param nodeAdmin true if the SDK is being initialized from Firebase Admin. + * @param app - A valid FirebaseApp-like object + * @param url - A valid Firebase databaseURL + * @param version - custom version e.g. firebase-admin version + * @param nodeAdmin - true if the SDK is being initialized from Firebase Admin. */ export function initStandalone( app: FirebaseApp, diff --git a/packages/database/package.json b/packages/database/package.json index c57504a49f5..4893e8ef2bd 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -14,7 +14,7 @@ "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "prettier": "prettier --write '*.js' '*.ts' '@(exp|src|test)/**/*.ts'", - "build": "run-p build:classic build:exp && yarn api-report", + "build": "run-p build:classic build:exp", "build:classic": "rollup -c rollup.config.js", "build:exp": "rollup -c rollup.config.exp.js && yarn api-report", "build:deps": "lerna run --scope @firebase/'{app,database}' --include-dependencies build", @@ -25,7 +25,8 @@ "test:browser": "karma start --single-run", "test:node": "TS_NODE_FILES=true TS_NODE_CACHE=NO TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'test/{,!(browser)/**/}*.test.ts' --file index.node.ts --config ../../config/mocharc.node.js", "test:emulator": "ts-node --compiler-options='{\"module\":\"commonjs\"}' ../../scripts/emulator-testing/database-test-runner.ts", - "api-report": "api-extractor run --local --verbose", + "api-report": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' ts-node ./scripts/api-report.ts && yarn api-report:api-json", + "api-report:api-json": "rm -rf temp && api-extractor run --local --verbose", "predoc": "node ../../scripts/exp/remove-exp.js temp", "doc": "api-documenter markdown --input temp --output docs" }, diff --git a/packages/database/scripts/api-report.ts b/packages/database/scripts/api-report.ts new file mode 100644 index 00000000000..873721b4df8 --- /dev/null +++ b/packages/database/scripts/api-report.ts @@ -0,0 +1,178 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +import { Extractor, ExtractorConfig } from 'api-extractor-me'; +import * as tmp from 'tmp'; + +import { + pruneDts, + removeUnusedImports +} from '../../../repo-scripts/prune-dts/prune-dts'; + +// TODOD(mrschmidt): Combine this script with the Firestore version. + +/* eslint-disable no-console */ + +// This script takes the output of the API Extractor, post-processes it using +// the pruned-dts script and then invokes API report to generate a report +// that only includes exported symbols. This is all done in temporary folders, +// all configuration is auto-generated for each run. + +const baseApiExtractorConfigFile: string = path.resolve( + __dirname, + '../../../config/api-extractor.json' +); +const reportFolder = path.resolve(__dirname, '../../../common/api-review'); +const tmpDir = tmp.dirSync().name; + +function writeTypescriptConfig(): void { + const tsConfigJson = { + extends: path.resolve(__dirname, '../tsconfig.json'), + include: [path.resolve(__dirname, '../src')], + compilerOptions: { + downlevelIteration: true // Needed for FirebaseApp + } + }; + fs.writeFileSync( + path.resolve(tmpDir, 'tsconfig.json'), + JSON.stringify(tsConfigJson), + { encoding: 'utf-8' } + ); +} + +function writePackageJson(): void { + const packageJson = { + 'name': `@firebase/database-exp` + }; + const packageJsonPath = path.resolve(tmpDir, 'package.json'); + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson), { + encoding: 'utf-8' + }); +} + +function loadApiExtractorConfig( + typescriptDtsPath: string, + rollupDtsPath: string, + untrimmedRollupDtsPath: string, + dtsRollupEnabled: boolean, + apiReportEnabled: boolean +): ExtractorConfig { + const apiExtractorJsonPath = path.resolve(tmpDir, 'api-extractor.json'); + const apiExtractorJson = { + extends: baseApiExtractorConfigFile, + mainEntryPointFilePath: typescriptDtsPath, + 'dtsRollup': { + 'enabled': dtsRollupEnabled, + publicTrimmedFilePath: rollupDtsPath, + untrimmedFilePath: untrimmedRollupDtsPath + }, + 'tsdocMetadata': { + 'enabled': false + }, + 'apiReport': { + 'enabled': apiReportEnabled, + reportFolder + }, + 'messages': { + 'extractorMessageReporting': { + 'ae-missing-release-tag': { + 'logLevel': 'none' + }, + 'ae-unresolved-link': { + 'logLevel': 'none' + }, + 'ae-forgotten-export': { + 'logLevel': apiReportEnabled ? 'error' : 'none' + } + }, + 'tsdocMessageReporting': { + 'tsdoc-undefined-tag': { + 'logLevel': 'none' + } + } + } + }; + fs.writeFileSync(apiExtractorJsonPath, JSON.stringify(apiExtractorJson), { + encoding: 'utf-8' + }); + return ExtractorConfig.loadFileAndPrepare(apiExtractorJsonPath); +} + +export async function generateApi(): Promise { + // The .d.ts file generated by the Typescript compiler as we transpile our + // sources + const typescriptDtsPath: string = path.resolve( + __dirname, + `../dist/exp/packages/database/exp/index.d.ts` + ); + // A "bundled" version of our d.ts files that includes all public and private + // types + const rollupDtsPath: string = path.resolve( + __dirname, + `../dist/exp/private.d.ts` + ); + + // A "bundled" version of our d.ts files that includes all public and private + // types, but also include exports marked as @internal + // This file is used by @firebase/database-compat to use internal exports + const untrimmedRollupDtsPath: string = path.resolve( + __dirname, + `../dist/exp/internal.d.ts` + ); + + // A customer-facing d.ts file that only include the public APIs + const publicDtsPath: string = path.resolve( + __dirname, + `../dist/exp/index.d.ts` + ); + + console.log(`Running API Extractor configuration for RTDB...`); + writeTypescriptConfig(); + writePackageJson(); + + let extractorConfig = loadApiExtractorConfig( + typescriptDtsPath, + rollupDtsPath, + untrimmedRollupDtsPath, + /* dtsRollupEnabled= */ true, + /* apiReportEnabled= */ false + ); + Extractor.invoke(extractorConfig, { + localBuild: true + }); + + console.log('Generated rollup DTS'); + pruneDts(rollupDtsPath, publicDtsPath); + console.log('Pruned DTS file'); + await removeUnusedImports(publicDtsPath); + console.log('Removed unused imports'); + + extractorConfig = loadApiExtractorConfig( + publicDtsPath, + rollupDtsPath, + untrimmedRollupDtsPath, + /* dtsRollupEnabled= */ false, + /* apiReportEnabled= */ true + ); + Extractor.invoke(extractorConfig, { localBuild: true }); + console.log(`API report for RTDB written to ${reportFolder}`); +} + +void generateApi(); diff --git a/packages/database/src/api/Database.ts b/packages/database/src/api/Database.ts index 2649cad1b47..43766045bce 100644 --- a/packages/database/src/api/Database.ts +++ b/packages/database/src/api/Database.ts @@ -54,8 +54,8 @@ export class Database implements FirebaseService, Compat { * *

Note: This method must be called before performing any other operation. * - * @param host the emulator host (ex: localhost) - * @param port the emulator port (ex: 8080) + * @param host - the emulator host (ex: localhost) + * @param port - the emulator port (ex: 8080) */ useEmulator(host: string, port: number): void { useDatabaseEmulator(this._delegate, host, port); @@ -65,7 +65,7 @@ export class Database implements FirebaseService, Compat { * Returns a reference to the root or to the path specified in the provided * argument. * - * @param path The relative string path or an existing Reference to a database + * @param path - The relative string path or an existing Reference to a database * location. * @throws If a Reference is provided, throws if it does not belong to the * same project. diff --git a/packages/database/src/api/Reference.ts b/packages/database/src/api/Reference.ts index 1d1dd3ba1b6..7e1328d9dca 100644 --- a/packages/database/src/api/Reference.ts +++ b/packages/database/src/api/Reference.ts @@ -64,13 +64,15 @@ import { setWithPriority, remove, setPriority, - push + push, + ThenableReferenceImpl } from '../exp/Reference_impl'; import { runTransaction } from '../exp/Transaction'; import { Database } from './Database'; import { OnDisconnect } from './onDisconnect'; import { TransactionResult } from './TransactionResult'; + /** * Class representing a firebase data snapshot. It wraps a SnapshotNode and * surfaces the public methods (val, forEach, etc.) we want to expose. @@ -123,7 +125,7 @@ export class DataSnapshot implements Compat { /** * Returns a DataSnapshot of the specified child node's contents. * - * @param path Path to a child. + * @param path - Path to a child. * @returns DataSnapshot for child node. */ child(path: string): DataSnapshot { @@ -137,7 +139,7 @@ export class DataSnapshot implements Compat { /** * Returns whether the snapshot contains a child at the specified path. * - * @param path Path to a child. + * @param path - Path to a child. * @returns Whether the child exists. */ hasChild(path: string): boolean { @@ -159,7 +161,7 @@ export class DataSnapshot implements Compat { /** * Iterates through child nodes and calls the specified action for each one. * - * @param action Callback function to be called + * @param action - Callback function to be called * for each child. * @returns True if forEach was canceled by action returning true for * one of the child nodes. @@ -502,7 +504,7 @@ export class Query implements Compat { /** * Helper used by .on and .once to extract the context and or cancel arguments. - * @param fnName The function name (on or once) + * @param fnName - The function name (on or once) * */ private static getCancelAndContextArgs_( @@ -743,7 +745,7 @@ export class Reference extends Query implements Compat { validateArgCount('Reference.push', 0, 2, arguments.length); validateCallback('Reference.push', 'onComplete', onComplete, true); - const expPromise = push(this._delegate, value); + const expPromise = push(this._delegate, value) as ThenableReferenceImpl; const promise = expPromise.then( expRef => new Reference(this.database, expRef) ); diff --git a/packages/database/src/api/internal.ts b/packages/database/src/api/internal.ts index 773bf0fd0a2..aad24d65cff 100644 --- a/packages/database/src/api/internal.ts +++ b/packages/database/src/api/internal.ts @@ -93,10 +93,10 @@ export const interceptServerData = function ( * Used by console to create a database based on the app, * passed database URL and a custom auth implementation. * - * @param app A valid FirebaseApp-like object - * @param url A valid Firebase databaseURL - * @param version custom version e.g. firebase-admin version - * @param customAuthImpl custom auth implementation + * @param app - A valid FirebaseApp-like object + * @param url - A valid Firebase databaseURL + * @param version - custom version e.g. firebase-admin version + * @param customAuthImpl - custom auth implementation */ export function initStandalone({ app, diff --git a/packages/database/src/core/CompoundWrite.ts b/packages/database/src/core/CompoundWrite.ts index f19577bd6e0..1ebee8a4e18 100644 --- a/packages/database/src/core/CompoundWrite.ts +++ b/packages/database/src/core/CompoundWrite.ts @@ -85,8 +85,8 @@ export function compoundWriteAddWrites( * Will remove a write at the given path and deeper paths. This will not modify a write at a higher * location, which must be removed by calling this method with that path. * - * @param compoundWrite The CompoundWrite to remove. - * @param path The path at which a write and all deeper writes should be removed + * @param compoundWrite - The CompoundWrite to remove. + * @param path - The path at which a write and all deeper writes should be removed * @returns The new CompoundWrite with the removed path */ export function compoundWriteRemoveWrite( @@ -108,8 +108,8 @@ export function compoundWriteRemoveWrite( * Returns whether this CompoundWrite will fully overwrite a node at a given location and can therefore be * considered "complete". * - * @param compoundWrite The CompoundWrite to check. - * @param path The path to check for + * @param compoundWrite - The CompoundWrite to check. + * @param path - The path to check for * @returns Whether there is a complete write at that path */ export function compoundWriteHasCompleteWrite( @@ -123,8 +123,8 @@ export function compoundWriteHasCompleteWrite( * Returns a node for a path if and only if the node is a "complete" overwrite at that path. This will not aggregate * writes from deeper paths, but will return child nodes from a more shallow path. * - * @param compoundWrite The CompoundWrite to get the node from. - * @param path The path to get a complete write + * @param compoundWrite - The CompoundWrite to get the node from. + * @param path - The path to get a complete write * @returns The node if complete at that path, or null otherwise. */ export function compoundWriteGetCompleteNode( @@ -144,7 +144,7 @@ export function compoundWriteGetCompleteNode( /** * Returns all children that are guaranteed to be a complete overwrite. * - * @param compoundWrite The CompoundWrite to get children from. + * @param compoundWrite - The CompoundWrite to get children from. * @returns A list of all complete children. */ export function compoundWriteGetCompleteChildren( @@ -201,7 +201,7 @@ export function compoundWriteIsEmpty(compoundWrite: CompoundWrite): boolean { /** * Applies this CompoundWrite to a node. The node is returned with all writes from this CompoundWrite applied to the * node - * @param node The node to apply this CompoundWrite to + * @param node - The node to apply this CompoundWrite to * @returns The node with all writes applied */ export function compoundWriteApply( diff --git a/packages/database/src/core/PersistentConnection.ts b/packages/database/src/core/PersistentConnection.ts index 2cae60fcfec..e3cfa4c5322 100644 --- a/packages/database/src/core/PersistentConnection.ts +++ b/packages/database/src/core/PersistentConnection.ts @@ -92,7 +92,6 @@ export class PersistentConnection extends ServerActions { private log_ = logWrapper('p:' + this.id + ':'); private interruptReasons_: { [reason: string]: boolean } = {}; - /** Map> */ private readonly listens: Map< /* path */ string, Map @@ -137,9 +136,9 @@ export class PersistentConnection extends ServerActions { private static nextConnectionId_ = 0; /** - * @param repoInfo_ Data about the namespace we are connecting to - * @param applicationId_ The Firebase App ID for this project - * @param onDataUpdate_ A callback for new data from the server + * @param repoInfo_ - Data about the namespace we are connecting to + * @param applicationId_ - The Firebase App ID for this project + * @param onDataUpdate_ - A callback for new data from the server */ constructor( private repoInfo_: RepoInfo, @@ -240,10 +239,6 @@ export class PersistentConnection extends ServerActions { return deferred.promise; } - - /** - * @inheritDoc - */ listen( query: QueryContext, currentHashFn: () => string, @@ -349,10 +344,6 @@ export class PersistentConnection extends ServerActions { } } } - - /** - * @inheritDoc - */ refreshAuthToken(token: string) { this.authToken_ = token; this.log_('Auth token refreshed'); @@ -414,10 +405,6 @@ export class PersistentConnection extends ServerActions { ); } } - - /** - * @inheritDoc - */ unlisten(query: QueryContext, tag: number | null) { const pathString = query._path.toString(); const queryId = query._queryIdentifier; @@ -452,10 +439,6 @@ export class PersistentConnection extends ServerActions { this.sendRequest(action, req); } - - /** - * @inheritDoc - */ onDisconnectPut( pathString: string, data: unknown, @@ -472,10 +455,6 @@ export class PersistentConnection extends ServerActions { }); } } - - /** - * @inheritDoc - */ onDisconnectMerge( pathString: string, data: unknown, @@ -492,10 +471,6 @@ export class PersistentConnection extends ServerActions { }); } } - - /** - * @inheritDoc - */ onDisconnectCancel( pathString: string, onComplete?: (a: string, b: string) => void @@ -531,10 +506,6 @@ export class PersistentConnection extends ServerActions { } }); } - - /** - * @inheritDoc - */ put( pathString: string, data: unknown, @@ -543,10 +514,6 @@ export class PersistentConnection extends ServerActions { ) { this.putInternal('p', pathString, data, onComplete, hash); } - - /** - * @inheritDoc - */ merge( pathString: string, data: unknown, @@ -614,10 +581,6 @@ export class PersistentConnection extends ServerActions { } }); } - - /** - * @inheritDoc - */ reportStats(stats: { [k: string]: unknown }) { // If we're not connected, we just drop the stats. if (this.connected_) { diff --git a/packages/database/src/core/ReadonlyRestClient.ts b/packages/database/src/core/ReadonlyRestClient.ts index d9cfceac113..63615b6b003 100644 --- a/packages/database/src/core/ReadonlyRestClient.ts +++ b/packages/database/src/core/ReadonlyRestClient.ts @@ -62,8 +62,8 @@ export class ReadonlyRestClient extends ServerActions { } /** - * @param repoInfo_ Data about the namespace we are connecting to - * @param onDataUpdate_ A callback for new data from the server + * @param repoInfo_ - Data about the namespace we are connecting to + * @param onDataUpdate_ - A callback for new data from the server */ constructor( private repoInfo_: RepoInfo, diff --git a/packages/database/src/core/Repo.ts b/packages/database/src/core/Repo.ts index 13f3d1db196..005c663f74c 100644 --- a/packages/database/src/core/Repo.ts +++ b/packages/database/src/core/Repo.ts @@ -35,6 +35,7 @@ import { nodeFromJSON } from './snap/nodeFromJSON'; import { SnapshotHolder } from './SnapshotHolder'; import { newSparseSnapshotTree, + SparseSnapshotTree, sparseSnapshotTreeForEachTree, sparseSnapshotTreeForget, sparseSnapshotTreeRemember @@ -175,7 +176,7 @@ export class Repo { interceptServerDataCallback_: ((a: string, b: unknown) => void) | null = null; /** A list of data pieces and paths to be set when this client disconnects. */ - onDisconnect_ = newSparseSnapshotTree(); + onDisconnect_: SparseSnapshotTree = newSparseSnapshotTree(); /** Stores queues of outstanding transactions for Firebase locations. */ transactionQueueTree_ = new Tree(); @@ -857,12 +858,12 @@ export function repoCallOnCompleteCallback( * Creates a new transaction, adds it to the transactions we're tracking, and * sends it to the server if possible. * - * @param path Path at which to do transaction. - * @param transactionUpdate Update callback. - * @param onComplete Completion callback. - * @param unwatcher Function that will be called when the transaction no longer + * @param path - Path at which to do transaction. + * @param transactionUpdate - Update callback. + * @param onComplete - Completion callback. + * @param unwatcher - Function that will be called when the transaction no longer * need data updates for `path`. - * @param applyLocally Whether or not to make intermediate results visible + * @param applyLocally - Whether or not to make intermediate results visible */ export function repoStartTransaction( repo: Repo, @@ -974,7 +975,7 @@ export function repoStartTransaction( } /** - * @param excludeSets A specific set to exclude + * @param excludeSets - A specific set to exclude */ function repoGetLatestState( repo: Repo, @@ -994,7 +995,7 @@ function repoGetLatestState( * Externally it's called with no arguments, but it calls itself recursively * with a particular transactionQueueTree node to recurse through the tree. * - * @param node transactionQueueTree node to start at. + * @param node - transactionQueueTree node to start at. */ function repoSendReadyTransactions( repo: Repo, @@ -1028,8 +1029,8 @@ function repoSendReadyTransactions( * Given a list of run transactions, send them to the server and then handle * the result (success or failure). * - * @param path The location of the queue. - * @param queue Queue of transactions under the specified location. + * @param path - The location of the queue. + * @param queue - Queue of transactions under the specified location. */ function repoSendTransactionQueue( repo: Repo, @@ -1146,7 +1147,7 @@ function repoSendTransactionQueue( * Return the highest path that was affected by rerunning transactions. This * is the path at which events need to be raised for. * - * @param changedPath The path in mergedData that changed. + * @param changedPath - The path in mergedData that changed. * @returns The rootmost path that was affected by rerunning transactions. */ function repoRerunTransactions(repo: Repo, changedPath: Path): Path { @@ -1166,8 +1167,8 @@ function repoRerunTransactions(repo: Repo, changedPath: Path): Path { * Does all the work of rerunning transactions (as well as cleans up aborted * transactions and whatnot). * - * @param queue The queue of transactions to run. - * @param path The path the queue is for. + * @param queue - The queue of transactions to run. + * @param path - The path the queue is for. */ function repoRerunTransactionQueue( repo: Repo, @@ -1328,7 +1329,7 @@ function repoRerunTransactionQueue( * transaction on it, or just returns the node for the given path if there are * no pending transactions on any ancestor. * - * @param path The location to start at. + * @param path - The location to start at. * @returns The rootmost node with a transaction. */ function repoGetAncestorTransactionNode( @@ -1422,7 +1423,7 @@ function repoPruneCompletedTransactionsBelowNode( * Called when doing a set() or update() since we consider them incompatible * with transactions. * - * @param path Path for which we want to abort related transactions. + * @param path - Path for which we want to abort related transactions. */ function repoAbortTransactions(repo: Repo, path: Path): Path { const affectedPath = treeGetPath(repoGetAncestorTransactionNode(repo, path)); @@ -1445,7 +1446,7 @@ function repoAbortTransactions(repo: Repo, path: Path): Path { /** * Abort transactions stored in this transaction queue node. * - * @param node Node to abort transactions for. + * @param node - Node to abort transactions for. */ function repoAbortTransactionsOnNode( repo: Repo, diff --git a/packages/database/src/core/RepoInfo.ts b/packages/database/src/core/RepoInfo.ts index 4302ea86581..de0e4603be1 100644 --- a/packages/database/src/core/RepoInfo.ts +++ b/packages/database/src/core/RepoInfo.ts @@ -31,12 +31,12 @@ export class RepoInfo { internalHost: string; /** - * @param host Hostname portion of the url for the repo - * @param secure Whether or not this repo is accessed over ssl - * @param namespace The namespace represented by the repo - * @param webSocketOnly Whether to prefer websockets over all other transports (used by Nest). - * @param nodeAdmin Whether this instance uses Admin SDK credentials - * @param persistenceKey Override the default session persistence storage key + * @param host - Hostname portion of the url for the repo + * @param secure - Whether or not this repo is accessed over ssl + * @param namespace - The namespace represented by the repo + * @param webSocketOnly - Whether to prefer websockets over all other transports (used by Nest). + * @param nodeAdmin - Whether this instance uses Admin SDK credentials + * @param persistenceKey - Override the default session persistence storage key */ constructor( host: string, diff --git a/packages/database/src/core/ServerActions.ts b/packages/database/src/core/ServerActions.ts index 7a8d3fa1225..ee5b3f32ff9 100644 --- a/packages/database/src/core/ServerActions.ts +++ b/packages/database/src/core/ServerActions.ts @@ -57,7 +57,7 @@ export abstract class ServerActions { /** * Refreshes the auth token for the current connection. - * @param token The authentication token + * @param token - The authentication token */ refreshAuthToken(token: string) {} diff --git a/packages/database/src/core/SparseSnapshotTree.ts b/packages/database/src/core/SparseSnapshotTree.ts index 27a9be9a85a..e578cd2d11d 100644 --- a/packages/database/src/core/SparseSnapshotTree.ts +++ b/packages/database/src/core/SparseSnapshotTree.ts @@ -38,7 +38,7 @@ export function newSparseSnapshotTree(): SparseSnapshotTree { * Gets the node stored at the given path if one exists. * Only seems to be used in tests. * - * @param path Path to look up snapshot for. + * @param path - Path to look up snapshot for. * @returns The retrieved node, or null. */ export function sparseSnapshotTreeFind( @@ -65,8 +65,8 @@ export function sparseSnapshotTreeFind( * Stores the given node at the specified path. If there is already a node * at a shallower path, it merges the new data into that snapshot node. * - * @param path Path to look up snapshot for. - * @param data The new data, or null. + * @param path - Path to look up snapshot for. + * @param data - The new data, or null. */ export function sparseSnapshotTreeRemember( sparseSnapshotTree: SparseSnapshotTree, @@ -93,7 +93,7 @@ export function sparseSnapshotTreeRemember( /** * Purge the data at path from the cache. * - * @param path Path to look up snapshot for. + * @param path - Path to look up snapshot for. * @returns True if this node should now be removed. */ export function sparseSnapshotTreeForget( @@ -143,8 +143,8 @@ export function sparseSnapshotTreeForget( * Recursively iterates through all of the stored tree and calls the * callback on each one. * - * @param prefixPath Path to look up node for. - * @param func The function to invoke for each tree. + * @param prefixPath - Path to look up node for. + * @param func - The function to invoke for each tree. */ export function sparseSnapshotTreeForEachTree( sparseSnapshotTree: SparseSnapshotTree, @@ -165,7 +165,7 @@ export function sparseSnapshotTreeForEachTree( * Iterates through each immediate child and triggers the callback. * Only seems to be used in tests. * - * @param func The function to invoke for each child. + * @param func - The function to invoke for each child. */ export function sparseSnapshotTreeForEachChild( sparseSnapshotTree: SparseSnapshotTree, diff --git a/packages/database/src/core/SyncPoint.ts b/packages/database/src/core/SyncPoint.ts index 99c0efc5dd4..6751b2242a8 100644 --- a/packages/database/src/core/SyncPoint.ts +++ b/packages/database/src/core/SyncPoint.ts @@ -115,7 +115,7 @@ export function syncPointApplyOperation( /** * Get a view for the specified query. * - * @param query The query to return a view for + * @param query - The query to return a view for * @param writesCache * @param serverCache * @param serverCacheComplete @@ -164,7 +164,7 @@ export function syncPointGetView( * @param query * @param eventRegistration * @param writesCache - * @param serverCache Complete server cache, if we have it. + * @param serverCache - Complete server cache, if we have it. * @param serverCacheComplete * @returns Events to raise. */ @@ -197,8 +197,8 @@ export function syncPointAddEventRegistration( * If query is the default query, we'll check all views for the specified eventRegistration. * If eventRegistration is null, we'll remove all callbacks for the specified view(s). * - * @param eventRegistration If null, remove all callbacks. - * @param cancelError If a cancelError is provided, appropriate cancel events will be returned. + * @param eventRegistration - If null, remove all callbacks. + * @param cancelError - If a cancelError is provided, appropriate cancel events will be returned. * @returns removed queries and any cancel events */ export function syncPointRemoveEventRegistration( @@ -265,7 +265,7 @@ export function syncPointGetQueryViews(syncPoint: SyncPoint): View[] { } /** - * @param path The path to the desired complete snapshot + * @param path - The path to the desired complete snapshot * @returns A complete cache, if it exists */ export function syncPointGetCompleteServerCache( diff --git a/packages/database/src/core/SyncTree.ts b/packages/database/src/core/SyncTree.ts index 700a6fe126c..0fad43f5976 100644 --- a/packages/database/src/core/SyncTree.ts +++ b/packages/database/src/core/SyncTree.ts @@ -60,6 +60,7 @@ import { EventRegistration, QueryContext } from './view/EventRegistration'; import { View, viewGetCompleteNode, viewGetServerCache } from './view/View'; import { newWriteTree, + WriteTree, writeTreeAddMerge, writeTreeAddOverwrite, writeTreeCalcCompleteEventCache, @@ -87,18 +88,6 @@ function syncTreeGetReferenceConstructor(): ReferenceConstructor { return referenceConstructor; } -/** - * @typedef {{ - * startListening: function( - * !Query, - * ?number, - * function():string, - * function(!string, *):!Array. - * ):!Array., - * - * stopListening: function(!Query, ?number) - * }} - */ export interface ListenProvider { startListening( query: QueryContext, @@ -145,13 +134,13 @@ export class SyncTree { /** * A tree of all pending user writes (user-initiated set()'s, transaction()'s, update()'s, etc.). */ - pendingWriteTree_ = newWriteTree(); + pendingWriteTree_: WriteTree = newWriteTree(); readonly tagToQueryMap: Map = new Map(); readonly queryToTagMap: Map = new Map(); /** - * @param listenProvider_ Used by SyncTree to start / stop listening + * @param listenProvider_ - Used by SyncTree to start / stop listening * to server data. */ constructor(public listenProvider_: ListenProvider) {} @@ -213,7 +202,7 @@ export function syncTreeApplyUserMerge( /** * Acknowledge a pending user write that was previously registered with applyUserOverwrite() or applyUserMerge(). * - * @param revert True if the given write failed and needs to be reverted + * @param revert - True if the given write failed and needs to be reverted * @returns Events to raise. */ export function syncTreeAckUserWrite( @@ -327,8 +316,8 @@ export function syncTreeApplyTaggedListenComplete( * If query is the default query, we'll check all queries for the specified eventRegistration. * If eventRegistration is null, we'll remove all callbacks for the specified query/queries. * - * @param eventRegistration If null, all callbacks are removed. - * @param cancelError If a cancelError is provided, appropriate cancel events will be returned. + * @param eventRegistration - If null, all callbacks are removed. + * @param cancelError - If a cancelError is provided, appropriate cancel events will be returned. * @returns Cancel events, if cancelError was provided. */ export function syncTreeRemoveEventRegistration( @@ -584,8 +573,8 @@ export function syncTreeAddEventRegistration( * * Note: this method will *include* hidden writes from transaction with applyLocally set to false. * - * @param path The path to the data we want - * @param writeIdsToExclude A specific set to be excluded + * @param path - The path to the data we want + * @param writeIdsToExclude - A specific set to be excluded */ export function syncTreeCalcCompleteEventCache( syncTree: SyncTree, diff --git a/packages/database/src/core/WriteTree.ts b/packages/database/src/core/WriteTree.ts index 5003dad68a9..f586f194441 100644 --- a/packages/database/src/core/WriteTree.ts +++ b/packages/database/src/core/WriteTree.ts @@ -72,7 +72,7 @@ export function writeTreeChildWrites( /** * Record a new overwrite from user code. * - * @param visible This is set to false by some transactions. It should be excluded from event caches + * @param visible - This is set to false by some transactions. It should be excluded from event caches */ export function writeTreeAddOverwrite( writeTree: WriteTree, @@ -351,8 +351,8 @@ export function writeTreeGetCompleteWriteData( * Given optional, underlying server data, and an optional set of constraints (exclude some sets, include hidden * writes), attempt to calculate a complete snapshot for the given path * - * @param writeIdsToExclude An optional set to be excluded - * @param includeHiddenWrites Defaults to false, whether or not to layer on writes with visible set to false + * @param writeIdsToExclude - An optional set to be excluded + * @param includeHiddenWrites - Defaults to false, whether or not to layer on writes with visible set to false */ export function writeTreeCalcCompleteEventCache( writeTree: WriteTree, @@ -674,8 +674,8 @@ export interface WriteTree { * to get a cache that includes hidden writes, and excludes arbitrary writes. Note that customizing the returned node * can lead to a more expensive calculation. * - * @param writeIdsToExclude Optional writes to exclude. - * @param includeHiddenWrites Defaults to false, whether or not to layer on writes with visible set to false + * @param writeIdsToExclude - Optional writes to exclude. + * @param includeHiddenWrites - Defaults to false, whether or not to layer on writes with visible set to false */ export function writeTreeRefCalcCompleteEventCache( writeTreeRef: WriteTreeRef, diff --git a/packages/database/src/core/operation/AckUserWrite.ts b/packages/database/src/core/operation/AckUserWrite.ts index 41f0067526f..8bd98ae6745 100644 --- a/packages/database/src/core/operation/AckUserWrite.ts +++ b/packages/database/src/core/operation/AckUserWrite.ts @@ -36,17 +36,13 @@ export class AckUserWrite implements Operation { source = newOperationSourceUser(); /** - * @param affectedTree A tree containing true for each affected path. Affected paths can't overlap. + * @param affectedTree - A tree containing true for each affected path. Affected paths can't overlap. */ constructor( /** @inheritDoc */ public path: Path, /** @inheritDoc */ public affectedTree: ImmutableTree, /** @inheritDoc */ public revert: boolean ) {} - - /** - * @inheritDoc - */ operationForChild(childName: string): AckUserWrite { if (!pathIsEmpty(this.path)) { assert( diff --git a/packages/database/src/core/operation/Merge.ts b/packages/database/src/core/operation/Merge.ts index c72587a84c8..eeded9d96c6 100644 --- a/packages/database/src/core/operation/Merge.ts +++ b/packages/database/src/core/operation/Merge.ts @@ -39,10 +39,6 @@ export class Merge implements Operation { /** @inheritDoc */ public path: Path, /** @inheritDoc */ public children: ImmutableTree ) {} - - /** - * @inheritDoc - */ operationForChild(childName: string): Operation { if (pathIsEmpty(this.path)) { const childTree = this.children.subtree(new Path(childName)); @@ -64,10 +60,6 @@ export class Merge implements Operation { return new Merge(this.source, pathPopFront(this.path), this.children); } } - - /** - * @inheritDoc - */ toString(): string { return ( 'Operation(' + diff --git a/packages/database/src/core/snap/ChildrenNode.ts b/packages/database/src/core/snap/ChildrenNode.ts index 0989925f65c..fce34e484c3 100644 --- a/packages/database/src/core/snap/ChildrenNode.ts +++ b/packages/database/src/core/snap/ChildrenNode.ts @@ -66,8 +66,8 @@ export class ChildrenNode implements Node { } /** - * @param children_ List of children of this node.. - * @param priorityNode_ The priority of this node (as a snapshot node). + * @param children_ - List of children of this node.. + * @param priorityNode_ - The priority of this node (as a snapshot node). */ constructor( private readonly children_: SortedMap, @@ -311,10 +311,6 @@ export class ChildrenNode implements Node { return null; } } - - /** - * @inheritDoc - */ forEachChild( index: Index, action: (key: string, node: Node) => boolean | void @@ -387,10 +383,6 @@ export class ChildrenNode implements Node { return iterator; } } - - /** - * @inheritDoc - */ compareTo(other: ChildrenNode): number { if (this.isEmpty()) { if (other.isEmpty()) { @@ -407,10 +399,6 @@ export class ChildrenNode implements Node { return 0; } } - - /** - * @inheritDoc - */ withIndex(indexDefinition: Index): Node { if ( indexDefinition === KEY_INDEX || @@ -425,17 +413,9 @@ export class ChildrenNode implements Node { return new ChildrenNode(this.children_, this.priorityNode_, newIndexMap); } } - - /** - * @inheritDoc - */ isIndexed(index: Index): boolean { return index === KEY_INDEX || this.indexMap_.hasIndex(index); } - - /** - * @inheritDoc - */ equals(other: Node): boolean { if (other === this) { return true; diff --git a/packages/database/src/core/snap/LeafNode.ts b/packages/database/src/core/snap/LeafNode.ts index e280de54cf7..3b3316326a7 100644 --- a/packages/database/src/core/snap/LeafNode.ts +++ b/packages/database/src/core/snap/LeafNode.ts @@ -57,9 +57,9 @@ export class LeafNode implements Node { private lazyHash_: string | null = null; /** - * @param value_ The value to store in this leaf node. The object type is + * @param value_ - The value to store in this leaf node. The object type is * possible in the event of a deferred value - * @param priorityNode_ The priority of this node. + * @param priorityNode_ - The priority of this node. */ constructor( private readonly value_: string | number | boolean | Indexable, @@ -108,10 +108,6 @@ export class LeafNode implements Node { return LeafNode.__childrenNodeConstructor.EMPTY_NODE; } } - - /** - * @inheritDoc - */ hasChild(): boolean { return false; } @@ -172,10 +168,6 @@ export class LeafNode implements Node { forEachChild(index: Index, action: (s: string, n: Node) => void): boolean { return false; } - - /** - * @inheritDoc - */ val(exportFormat?: boolean): {} { if (exportFormat && !this.getPriority().isEmpty()) { return { @@ -217,10 +209,6 @@ export class LeafNode implements Node { getValue(): Indexable | string | number | boolean { return this.value_; } - - /** - * @inheritDoc - */ compareTo(other: Node): number { if (other === LeafNode.__childrenNodeConstructor.EMPTY_NODE) { return 1; @@ -261,28 +249,13 @@ export class LeafNode implements Node { return thisIndex - otherIndex; } } - - /** - * @inheritDoc - */ withIndex(): Node { return this; } - - /** - * @inheritDoc - */ isIndexed(): boolean { return true; } - - /** - * @inheritDoc - */ equals(other: Node): boolean { - /** - * @inheritDoc - */ if (other === this) { return true; } else if (other.isLeafNode()) { diff --git a/packages/database/src/core/snap/Node.ts b/packages/database/src/core/snap/Node.ts index 0bf423e8af6..9d4afca79dc 100644 --- a/packages/database/src/core/snap/Node.ts +++ b/packages/database/src/core/snap/Node.ts @@ -40,30 +40,30 @@ export interface Node { /** * Returns a duplicate node with the new priority. - * @param newPriorityNode New priority to set for the node. + * @param newPriorityNode - New priority to set for the node. * @returns Node with new priority. */ updatePriority(newPriorityNode: Node): Node; /** * Returns the specified immediate child, or null if it doesn't exist. - * @param childName The name of the child to retrieve. + * @param childName - The name of the child to retrieve. * @returns The retrieved child, or an empty node. */ getImmediateChild(childName: string): Node; /** * Returns a child by path, or null if it doesn't exist. - * @param path The path of the child to retrieve. + * @param path - The path of the child to retrieve. * @returns The retrieved child or an empty node. */ getChild(path: Path): Node; /** * Returns the name of the child immediately prior to the specified childNode, or null. - * @param childName The name of the child to find the predecessor of. - * @param childNode The node to find the predecessor of. - * @param index The index to use to determine the predecessor + * @param childName - The name of the child to find the predecessor of. + * @param childNode - The node to find the predecessor of. + * @param index - The index to use to determine the predecessor * @returns The name of the predecessor child, or null if childNode is the first child. */ getPredecessorChildName( @@ -75,8 +75,8 @@ export interface Node { /** * Returns a duplicate node, with the specified immediate child updated. * Any value in the node will be removed. - * @param childName The name of the child to update. - * @param newChildNode The new child node + * @param childName - The name of the child to update. + * @param newChildNode - The new child node * @returns The updated node. */ updateImmediateChild(childName: string, newChildNode: Node): Node; @@ -84,8 +84,8 @@ export interface Node { /** * Returns a duplicate node, with the specified child updated. Any value will * be removed. - * @param path The path of the child to update. - * @param newChildNode The new child node, which may be an empty node + * @param path - The path of the child to update. + * @param newChildNode - The new child node, which may be an empty node * @returns The updated node. */ updateChild(path: Path, newChildNode: Node): Node; @@ -107,14 +107,14 @@ export interface Node { /** * Calls action for each child. - * @param action Action to be called for + * @param action - Action to be called for * each child. It's passed the child name and the child node. * @returns The first truthy value return by action, or the last falsey one */ forEachChild(index: Index, action: (a: string, b: Node) => void): unknown; /** - * @param exportFormat True for export format (also wire protocol format). + * @param exportFormat - True for export format (also wire protocol format). * @returns Value of this node as JSON. */ val(exportFormat?: boolean): unknown; @@ -125,7 +125,7 @@ export interface Node { hash(): string; /** - * @param other Another node + * @param other - Another node * @returns -1 for less than, 0 for equal, 1 for greater than other */ compareTo(other: Node): number; diff --git a/packages/database/src/core/snap/childSet.ts b/packages/database/src/core/snap/childSet.ts index de4f01569be..bef2b193f1f 100644 --- a/packages/database/src/core/snap/childSet.ts +++ b/packages/database/src/core/snap/childSet.ts @@ -52,11 +52,11 @@ class Base12Num { * Uses the algorithm described in the paper linked here: * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.46.1458 * - * @param childList Unsorted list of children - * @param cmp The comparison method to be used - * @param keyFn An optional function to extract K from a node wrapper, if K's + * @param childList - Unsorted list of children + * @param cmp - The comparison method to be used + * @param keyFn - An optional function to extract K from a node wrapper, if K's * type is not NamedNode - * @param mapSortFn An optional override for comparator used by the generated sorted map + * @param mapSortFn - An optional override for comparator used by the generated sorted map */ export const buildChildSet = function ( childList: NamedNode[], diff --git a/packages/database/src/core/snap/indexes/KeyIndex.ts b/packages/database/src/core/snap/indexes/KeyIndex.ts index 05d4e7e79ac..b2678c39d5a 100644 --- a/packages/database/src/core/snap/indexes/KeyIndex.ts +++ b/packages/database/src/core/snap/indexes/KeyIndex.ts @@ -33,41 +33,21 @@ export class KeyIndex extends Index { static set __EMPTY_NODE(val) { __EMPTY_NODE = val; } - - /** - * @inheritDoc - */ compare(a: NamedNode, b: NamedNode): number { return nameCompare(a.name, b.name); } - - /** - * @inheritDoc - */ isDefinedOn(node: Node): boolean { // We could probably return true here (since every node has a key), but it's never called // so just leaving unimplemented for now. throw assertionError('KeyIndex.isDefinedOn not expected to be called.'); } - - /** - * @inheritDoc - */ indexedValueChanged(oldNode: Node, newNode: Node): boolean { return false; // The key for a node never changes. } - - /** - * @inheritDoc - */ minPost() { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (NamedNode as any).MIN; } - - /** - * @inheritDoc - */ maxPost(): NamedNode { // TODO: This should really be created once and cached in a static property, but // NamedNode isn't defined yet, so I can't use it in a static. Bleh. diff --git a/packages/database/src/core/snap/indexes/PathIndex.ts b/packages/database/src/core/snap/indexes/PathIndex.ts index e7808864d80..dfe3ef1bdc5 100644 --- a/packages/database/src/core/snap/indexes/PathIndex.ts +++ b/packages/database/src/core/snap/indexes/PathIndex.ts @@ -38,17 +38,9 @@ export class PathIndex extends Index { protected extractChild(snap: Node): Node { return snap.getChild(this.indexPath_); } - - /** - * @inheritDoc - */ isDefinedOn(node: Node): boolean { return !node.getChild(this.indexPath_).isEmpty(); } - - /** - * @inheritDoc - */ compare(a: NamedNode, b: NamedNode): number { const aChild = this.extractChild(a.node); const bChild = this.extractChild(b.node); @@ -59,10 +51,6 @@ export class PathIndex extends Index { return indexCmp; } } - - /** - * @inheritDoc - */ makePost(indexValue: object, name: string): NamedNode { const valueNode = nodeFromJSON(indexValue); const node = ChildrenNode.EMPTY_NODE.updateChild( @@ -71,18 +59,10 @@ export class PathIndex extends Index { ); return new NamedNode(name, node); } - - /** - * @inheritDoc - */ maxPost(): NamedNode { const node = ChildrenNode.EMPTY_NODE.updateChild(this.indexPath_, MAX_NODE); return new NamedNode(MAX_NAME, node); } - - /** - * @inheritDoc - */ toString(): string { return pathSlice(this.indexPath_, 0).join('/'); } diff --git a/packages/database/src/core/snap/indexes/PriorityIndex.ts b/packages/database/src/core/snap/indexes/PriorityIndex.ts index 117abd2e60d..9eda74bbc29 100644 --- a/packages/database/src/core/snap/indexes/PriorityIndex.ts +++ b/packages/database/src/core/snap/indexes/PriorityIndex.ts @@ -33,9 +33,6 @@ export function setMaxNode(val: Node) { } export class PriorityIndex extends Index { - /** - * @inheritDoc - */ compare(a: NamedNode, b: NamedNode): number { const aPriority = a.node.getPriority(); const bPriority = b.node.getPriority(); @@ -46,32 +43,16 @@ export class PriorityIndex extends Index { return indexCmp; } } - - /** - * @inheritDoc - */ isDefinedOn(node: Node): boolean { return !node.getPriority().isEmpty(); } - - /** - * @inheritDoc - */ indexedValueChanged(oldNode: Node, newNode: Node): boolean { return !oldNode.getPriority().equals(newNode.getPriority()); } - - /** - * @inheritDoc - */ minPost(): NamedNode { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (NamedNode as any).MIN; } - - /** - * @inheritDoc - */ maxPost(): NamedNode { return new NamedNode(MAX_NAME, new LeafNode('[PRIORITY-POST]', MAX_NODE)); } diff --git a/packages/database/src/core/snap/indexes/ValueIndex.ts b/packages/database/src/core/snap/indexes/ValueIndex.ts index 576bc84f961..855b31c0e9e 100644 --- a/packages/database/src/core/snap/indexes/ValueIndex.ts +++ b/packages/database/src/core/snap/indexes/ValueIndex.ts @@ -22,9 +22,6 @@ import { nodeFromJSON } from '../nodeFromJSON'; import { Index } from './Index'; export class ValueIndex extends Index { - /** - * @inheritDoc - */ compare(a: NamedNode, b: NamedNode): number { const indexCmp = a.node.compareTo(b.node); if (indexCmp === 0) { @@ -33,32 +30,16 @@ export class ValueIndex extends Index { return indexCmp; } } - - /** - * @inheritDoc - */ isDefinedOn(node: Node): boolean { return true; } - - /** - * @inheritDoc - */ indexedValueChanged(oldNode: Node, newNode: Node): boolean { return !oldNode.equals(newNode); } - - /** - * @inheritDoc - */ minPost(): NamedNode { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (NamedNode as any).MIN; } - - /** - * @inheritDoc - */ maxPost(): NamedNode { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (NamedNode as any).MAX; diff --git a/packages/database/src/core/snap/nodeFromJSON.ts b/packages/database/src/core/snap/nodeFromJSON.ts index a497ed57377..b6df6bb4ed0 100644 --- a/packages/database/src/core/snap/nodeFromJSON.ts +++ b/packages/database/src/core/snap/nodeFromJSON.ts @@ -33,8 +33,8 @@ const USE_HINZE = true; /** * Constructs a snapshot node representing the passed JSON and returns it. - * @param json JSON to create a node for. - * @param priority Optional priority to use. This will be ignored if the + * @param json - JSON to create a node for. + * @param priority - Optional priority to use. This will be ignored if the * passed JSON contains a .priority property. */ export function nodeFromJSON( diff --git a/packages/database/src/core/stats/StatsListener.ts b/packages/database/src/core/stats/StatsListener.ts index 4e30c6ca9fa..969ce4f3cb2 100644 --- a/packages/database/src/core/stats/StatsListener.ts +++ b/packages/database/src/core/stats/StatsListener.ts @@ -22,7 +22,7 @@ import { StatsCollection } from './StatsCollection'; /** * Returns the delta from the previous call to get stats. * - * @param collection_ The collection to "listen" to. + * @param collection_ - The collection to "listen" to. */ export class StatsListener { private last_: { [k: string]: number } | null = null; diff --git a/packages/database/src/core/stats/StatsReporter.ts b/packages/database/src/core/stats/StatsReporter.ts index 2f5dd06ff77..f10db738111 100644 --- a/packages/database/src/core/stats/StatsReporter.ts +++ b/packages/database/src/core/stats/StatsReporter.ts @@ -36,10 +36,6 @@ export class StatsReporter { private statsListener_: StatsListener; statsToReport_: { [k: string]: boolean } = {}; - /** - * @param collection - * @param server_ - */ constructor(collection: StatsCollection, private server_: ServerActions) { this.statsListener_ = new StatsListener(collection); diff --git a/packages/database/src/core/storage/DOMStorageWrapper.ts b/packages/database/src/core/storage/DOMStorageWrapper.ts index 8d31ba5df53..f4255f50a23 100644 --- a/packages/database/src/core/storage/DOMStorageWrapper.ts +++ b/packages/database/src/core/storage/DOMStorageWrapper.ts @@ -31,13 +31,13 @@ export class DOMStorageWrapper { private prefix_ = 'firebase:'; /** - * @param domStorage_ The underlying storage object (e.g. localStorage or sessionStorage) + * @param domStorage_ - The underlying storage object (e.g. localStorage or sessionStorage) */ constructor(private domStorage_: Storage) {} /** - * @param key The key to save the value under - * @param value The value being stored, or null to remove the key. + * @param key - The key to save the value under + * @param value - The value being stored, or null to remove the key. */ set(key: string, value: unknown | null) { if (value == null) { diff --git a/packages/database/src/core/storage/storage.ts b/packages/database/src/core/storage/storage.ts index 99d62a82f95..c47ad1c482c 100644 --- a/packages/database/src/core/storage/storage.ts +++ b/packages/database/src/core/storage/storage.ts @@ -25,7 +25,7 @@ declare const window: Window; * TODO: Once MemoryStorage and DOMStorageWrapper have a shared interface this method annotation should change * to reflect this type * - * @param domStorageName Name of the underlying storage object + * @param domStorageName - Name of the underlying storage object * (e.g. 'localStorage' or 'sessionStorage'). * @returns Turning off type information until a common interface is defined. */ diff --git a/packages/database/src/core/util/ImmutableTree.ts b/packages/database/src/core/util/ImmutableTree.ts index 5191150588d..a6449878dad 100644 --- a/packages/database/src/core/util/ImmutableTree.ts +++ b/packages/database/src/core/util/ImmutableTree.ts @@ -72,11 +72,11 @@ export class ImmutableTree { * Given a path and predicate, return the first node and the path to that node * where the predicate returns true. * - * TODO Do a perf test -- If we're creating a bunch of {path: value:} objects - * on the way back out, it may be better to pass down a pathSoFar obj. + * TODO Do a perf test -- If we're creating a bunch of `{path: value:}` + * objects on the way back out, it may be better to pass down a pathSoFar obj. * - * @param relativePath The remainder of the path - * @param predicate The predicate to satisfy to return a node + * @param relativePath - The remainder of the path + * @param predicate - The predicate to satisfy to return a node */ findRootMostMatchingPathAndValue( relativePath: Path, @@ -141,8 +141,8 @@ export class ImmutableTree { /** * Sets a value at the specified path. * - * @param relativePath Path to set value at. - * @param toSet Value to set. + * @param relativePath - Path to set value at. + * @param toSet - Value to set. * @returns Resulting tree. */ set(relativePath: Path, toSet: T | null): ImmutableTree { @@ -160,7 +160,7 @@ export class ImmutableTree { /** * Removes the value at the specified path. * - * @param relativePath Path to value to remove. + * @param relativePath - Path to value to remove. * @returns Resulting tree. */ remove(relativePath: Path): ImmutableTree { @@ -195,7 +195,7 @@ export class ImmutableTree { /** * Gets a value from the tree. * - * @param relativePath Path to get value for. + * @param relativePath - Path to get value for. * @returns Value at path, or null. */ get(relativePath: Path): T | null { @@ -215,8 +215,8 @@ export class ImmutableTree { /** * Replace the subtree at the specified path with the given new tree. * - * @param relativePath Path to replace subtree for. - * @param newTree New tree. + * @param relativePath - Path to replace subtree for. + * @param newTree - New tree. * @returns Resulting tree. */ setTree(relativePath: Path, newTree: ImmutableTree): ImmutableTree { @@ -330,7 +330,7 @@ export class ImmutableTree { /** * Calls the given function for each node in the tree that has a value. * - * @param f A function to be called with the path from the root of the tree to + * @param f - A function to be called with the path from the root of the tree to * a node, and the value at that node. Called in depth-first order. */ foreach(f: (path: Path, value: T) => void) { diff --git a/packages/database/src/core/util/Path.ts b/packages/database/src/core/util/Path.ts index b409f9783bd..c0ddcf5061b 100644 --- a/packages/database/src/core/util/Path.ts +++ b/packages/database/src/core/util/Path.ts @@ -36,7 +36,7 @@ export class Path { pieceNum_: number; /** - * @param pathOrString Path string to parse, or another path, or the raw + * @param pathOrString - Path string to parse, or another path, or the raw * tokens array */ constructor(pathOrString: string | string[], pieceNum?: number) { @@ -264,8 +264,8 @@ export class ValidationPath { byteLength_: number; /** - * @param path Initial Path. - * @param errorPrefix_ Prefix for any error messages. + * @param path - Initial Path. + * @param errorPrefix_ - Prefix for any error messages. */ constructor(path: Path, public errorPrefix_: string) { this.parts_ = pathSlice(path, 0); diff --git a/packages/database/src/core/util/ServerValues.ts b/packages/database/src/core/util/ServerValues.ts index 8995e24bbae..e3b8cbb7b19 100644 --- a/packages/database/src/core/util/ServerValues.ts +++ b/packages/database/src/core/util/ServerValues.ts @@ -157,9 +157,9 @@ const resolveComplexDeferredValue = function ( /** * Recursively replace all deferred values and priorities in the tree with the * specified generated replacement values. - * @param path path to which write is relative - * @param node new data written at path - * @param syncTree current data + * @param path - path to which write is relative + * @param node - new data written at path + * @param syncTree - current data */ export const resolveDeferredValueTree = function ( path: Path, diff --git a/packages/database/src/core/util/SortedMap.ts b/packages/database/src/core/util/SortedMap.ts index 38328d2b043..c27cd7d5226 100644 --- a/packages/database/src/core/util/SortedMap.ts +++ b/packages/database/src/core/util/SortedMap.ts @@ -19,7 +19,7 @@ * @fileoverview Implementation of an immutable SortedMap using a Left-leaning * Red-Black Tree, adapted from the implementation in Mugs * (http://mads379.github.com/mugs/) by Mads Hartmann Jensen - * (mads379@gmail.com). + * (mads379\@gmail.com). * * Original paper on Left-leaning Red-Black Trees: * http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf @@ -44,9 +44,8 @@ export class SortedMapIterator { private nodeStack_: Array | LLRBEmptyNode> = []; /** - * @param node Node to iterate. - * @param isReverse_ Whether or not to iterate in reverse - * @param resultGenerator_ + * @param node - Node to iterate. + * @param isReverse_ - Whether or not to iterate in reverse */ constructor( node: LLRBNode | LLRBEmptyNode, @@ -144,11 +143,11 @@ export class LLRBNode { right: LLRBNode | LLRBEmptyNode; /** - * @param key Key associated with this node. - * @param value Value associated with this node. - * @param color Whether this node is red. - * @param left Left child. - * @param right Right child. + * @param key - Key associated with this node. + * @param value - Value associated with this node. + * @param color - Whether this node is red. + * @param left - Left child. + * @param right - Right child. */ constructor( public key: K, @@ -170,11 +169,11 @@ export class LLRBNode { /** * Returns a copy of the current node, optionally replacing pieces of it. * - * @param key New key for the node, or null. - * @param value New value for the node, or null. - * @param color New color for the node, or null. - * @param left New left child for the node, or null. - * @param right New right child for the node, or null. + * @param key - New key for the node, or null. + * @param value - New value for the node, or null. + * @param color - New color for the node, or null. + * @param left - New left child for the node, or null. + * @param right - New right child for the node, or null. * @returns The node copy. */ copy( @@ -211,7 +210,7 @@ export class LLRBNode { * Traverses the tree in key order and calls the specified action function * for each node. * - * @param action Callback function to be called for each + * @param action - Callback function to be called for each * node. If it returns true, traversal is aborted. * @returns The first truthy value returned by action, or the last falsey * value returned by action @@ -228,7 +227,7 @@ export class LLRBNode { * Traverses the tree in reverse key order and calls the specified action function * for each node. * - * @param action Callback function to be called for each + * @param action - Callback function to be called for each * node. If it returns true, traversal is aborted. * @returns True if traversal was aborted. */ @@ -270,9 +269,9 @@ export class LLRBNode { } /** - * @param key Key to insert. - * @param value Value to insert. - * @param comparator Comparator. + * @param key - Key to insert. + * @param value - Value to insert. + * @param comparator - Comparator. * @returns New tree, with the key/value added. */ insert(key: K, value: V, comparator: Comparator): LLRBNode { @@ -310,8 +309,8 @@ export class LLRBNode { } /** - * @param key The key of the item to remove. - * @param comparator Comparator. + * @param key - The key of the item to remove. + * @param comparator - Comparator. * @returns New tree, with the specified item removed. */ remove( @@ -489,9 +488,9 @@ export class LLRBEmptyNode { /** * Returns a copy of the tree, with the specified key/value added. * - * @param key Key to be added. - * @param value Value to be added. - * @param comparator Comparator. + * @param key - Key to be added. + * @param value - Value to be added. + * @param comparator - Comparator. * @returns New tree, with item added. */ insert(key: K, value: V, comparator: Comparator): LLRBNode { @@ -501,8 +500,8 @@ export class LLRBEmptyNode { /** * Returns a copy of the tree, with the specified key removed. * - * @param key The key to remove. - * @param comparator Comparator. + * @param key - The key to remove. + * @param comparator - Comparator. * @returns New tree, with item removed. */ remove(key: K, comparator: Comparator): LLRBEmptyNode { @@ -527,7 +526,7 @@ export class LLRBEmptyNode { * Traverses the tree in key order and calls the specified action function * for each node. * - * @param action Callback function to be called for each + * @param action - Callback function to be called for each * node. If it returns true, traversal is aborted. * @returns True if traversal was aborted. */ @@ -539,7 +538,7 @@ export class LLRBEmptyNode { * Traverses the tree in reverse key order and calls the specified action function * for each node. * - * @param action Callback function to be called for each + * @param action - Callback function to be called for each * node. If it returns true, traversal is aborted. * @returns True if traversal was aborted. */ @@ -578,8 +577,8 @@ export class SortedMap { static EMPTY_NODE = new LLRBEmptyNode(); /** - * @param comparator_ Key comparator. - * @param root_ (Optional) Root node for the map. + * @param comparator_ - Key comparator. + * @param root_ - Optional root node for the map. */ constructor( private comparator_: Comparator, @@ -592,8 +591,8 @@ export class SortedMap { * Returns a copy of the map, with the specified key/value added or replaced. * (TODO: We should perhaps rename this method to 'put') * - * @param key Key to be added. - * @param value Value to be added. + * @param key - Key to be added. + * @param value - Value to be added. * @returns New map, with item added. */ insert(key: K, value: V): SortedMap { @@ -608,7 +607,7 @@ export class SortedMap { /** * Returns a copy of the map, with the specified key removed. * - * @param key The key to remove. + * @param key - The key to remove. * @returns New map, with item removed. */ remove(key: K): SortedMap { @@ -623,7 +622,7 @@ export class SortedMap { /** * Returns the value of the node with the given key, or null. * - * @param key The key to look up. + * @param key - The key to look up. * @returns The value of the node with the given key, or null if the * key doesn't exist. */ @@ -645,7 +644,7 @@ export class SortedMap { /** * Returns the key of the item *before* the specified key, or null if key is the first item. - * @param key The key to find the predecessor of + * @param key - The key to find the predecessor of * @returns The predecessor key. */ getPredecessorKey(key: K): K | null { @@ -711,7 +710,7 @@ export class SortedMap { * Traverses the map in key order and calls the specified action function * for each key/value pair. * - * @param action Callback function to be called + * @param action - Callback function to be called * for each key/value pair. If action returns true, traversal is aborted. * @returns The first truthy value returned by action, or the last falsey * value returned by action @@ -724,7 +723,7 @@ export class SortedMap { * Traverses the map in reverse key order and calls the specified action function * for each key/value pair. * - * @param action Callback function to be called + * @param action - Callback function to be called * for each key/value pair. If action returns true, traversal is aborted. * @returns True if the traversal was aborted. */ diff --git a/packages/database/src/core/util/Tree.ts b/packages/database/src/core/util/Tree.ts index fd8f8ebcb52..7233b9ed948 100644 --- a/packages/database/src/core/util/Tree.ts +++ b/packages/database/src/core/util/Tree.ts @@ -38,9 +38,9 @@ export interface TreeNode { */ export class Tree { /** - * @param name Optional name of the node. - * @param parent Optional parent node. - * @param node Optional node to wrap. + * @param name - Optional name of the node. + * @param parent - Optional parent node. + * @param node - Optional node to wrap. */ constructor( readonly name: string = '', @@ -52,7 +52,7 @@ export class Tree { /** * Returns a sub-Tree for the given path. * - * @param pathObj Path to look up. + * @param pathObj - Path to look up. * @returns Tree for path. */ export function treeSubTree(tree: Tree, pathObj: string | Path): Tree { @@ -85,7 +85,7 @@ export function treeGetValue(tree: Tree): T | undefined { /** * Sets data to this tree node. * - * @param value Value to set. + * @param value - Value to set. */ export function treeSetValue(tree: Tree, value: T | undefined): void { tree.node.value = value; @@ -109,7 +109,7 @@ export function treeIsEmpty(tree: Tree): boolean { /** * Calls action for each child of this tree node. * - * @param action Action to be called for each child. + * @param action - Action to be called for each child. */ export function treeForEachChild( tree: Tree, @@ -123,10 +123,10 @@ export function treeForEachChild( /** * Does a depth-first traversal of this node's descendants, calling action for each one. * - * @param action Action to be called for each child. - * @param includeSelf Whether to call action on this node as well. Defaults to + * @param action - Action to be called for each child. + * @param includeSelf - Whether to call action on this node as well. Defaults to * false. - * @param childrenFirst Whether to call action on children before calling it on + * @param childrenFirst - Whether to call action on children before calling it on * parent. */ export function treeForEachDescendant( @@ -151,9 +151,9 @@ export function treeForEachDescendant( /** * Calls action on each ancestor node. * - * @param action Action to be called on each parent; return + * @param action - Action to be called on each parent; return * true to abort. - * @param includeSelf Whether to call action on this node as well. + * @param includeSelf - Whether to call action on this node as well. * @returns true if the action callback returned true. */ export function treeForEachAncestor( @@ -176,7 +176,7 @@ export function treeForEachAncestor( * is found, action is called on it and traversal does not continue inside the node. * Action is *not* called on this node. * - * @param action Action to be called for each child. + * @param action - Action to be called for each child. */ export function treeForEachImmediateDescendantWithValue( tree: Tree, @@ -214,8 +214,8 @@ function treeUpdateParents(tree: Tree) { /** * Adds or removes the passed child to this tree node, depending on whether it's empty. * - * @param childName The name of the child to update. - * @param child The child to update. + * @param childName - The name of the child to update. + * @param child - The child to update. */ function treeUpdateChild(tree: Tree, childName: string, child: Tree) { const childEmpty = treeIsEmpty(child); diff --git a/packages/database/src/core/util/util.ts b/packages/database/src/core/util/util.ts index c63010d15b8..4ce187b755c 100644 --- a/packages/database/src/core/util/util.ts +++ b/packages/database/src/core/util/util.ts @@ -46,7 +46,7 @@ export const LUIDGenerator: () => number = (function () { /** * Sha1 hash of the input string - * @param str The string to hash + * @param str - The string to hash * @returns {!string} The resulting hash */ export const sha1 = function (str: string): string { @@ -92,8 +92,8 @@ let firstLog_ = true; /** * The implementation of Firebase.enableLogging (defined here to break dependencies) - * @param logger_ A flag to turn on logging, or a custom logger - * @param persistent Whether or not to persist logging settings across refreshes + * @param logger_ - A flag to turn on logging, or a custom logger + * @param persistent - Whether or not to persist logging settings across refreshes */ export const enableLogging = function ( logger_?: boolean | ((a: string) => void) | null, @@ -331,8 +331,8 @@ export const ObjectToUniqueKey = function (obj: unknown): string { /** * Splits a string into a number of smaller segments of maximum size - * @param str The string - * @param segsize The maximum number of chars in the string. + * @param str - The string + * @param segsize - The maximum number of chars in the string. * @returns The string, split into appropriately-sized chunks */ export const splitStringBySize = function ( @@ -359,8 +359,8 @@ export const splitStringBySize = function ( /** * Apply a function to each (key, value) pair in an object or * apply a function to each (index, value) pair in an array - * @param obj The object or array to iterate over - * @param fn The function to apply + * @param obj - The object or array to iterate over + * @param fn - The function to apply */ export function each(obj: object, fn: (k: string, v: unknown) => void) { for (const key in obj) { @@ -372,8 +372,8 @@ export function each(obj: object, fn: (k: string, v: unknown) => void) { /** * Like goog.bind, but doesn't bother to create a closure if opt_context is null/undefined. - * @param callback Callback function. - * @param context Optional context to bind to. + * @param callback - Callback function. + * @param context - Optional context to bind to. * */ export const bindCallback = function ( @@ -387,7 +387,7 @@ export const bindCallback = function ( * Borrowed from http://hg.secondlife.com/llsd/src/tip/js/typedarray.js (MIT License) * I made one modification at the end and removed the NaN / Infinity * handling (since it seemed broken [caused an overflow] and we don't need it). See MJL comments. - * @param v A double + * @param v - A double * */ export const doubleToIEEE754String = function (v: number): string { @@ -533,7 +533,7 @@ export const tryParseInt = function (str: string): number | null { * * If you're only pausing on uncaught exceptions, the debugger will only pause * on us re-throwing it. * - * @param fn The code to guard. + * @param fn - The code to guard. */ export const exceptionGuard = function (fn: () => void) { try { @@ -557,8 +557,8 @@ export const exceptionGuard = function (fn: () => void) { * 1. Turns into a no-op if opt_callback is null or undefined. * 2. Wraps the call inside exceptionGuard to prevent exceptions from breaking our state. * - * @param callback Optional onComplete callback. - * @param varArgs Arbitrary args to be passed to opt_onComplete + * @param callback - Optional onComplete callback. + * @param varArgs - Arbitrary args to be passed to opt_onComplete */ export const callUserCallback = function ( // eslint-disable-next-line @typescript-eslint/ban-types @@ -609,8 +609,8 @@ export const exportPropGetter = function ( * * It is removed with clearTimeout() as normal. * - * @param fn Function to run. - * @param time Milliseconds to wait before running. + * @param fn - Function to run. + * @param time - Milliseconds to wait before running. * @returns The setTimeout() return value. */ export const setTimeoutNonBlocking = function ( diff --git a/packages/database/src/core/view/Change.ts b/packages/database/src/core/view/Change.ts index c2a57e556ec..30ffaa53649 100644 --- a/packages/database/src/core/view/Change.ts +++ b/packages/database/src/core/view/Change.ts @@ -31,15 +31,15 @@ export const enum ChangeType { } export interface Change { - /** @param type The event type */ + /** @param type - The event type */ type: ChangeType; - /** @param snapshotNode The data */ + /** @param snapshotNode - The data */ snapshotNode: Node; - /** @param childName The name for this child, if it's a child even */ + /** @param childName - The name for this child, if it's a child even */ childName?: string; - /** @param oldSnap Used for intermediate processing of child changed events */ + /** @param oldSnap - Used for intermediate processing of child changed events */ oldSnap?: Node; - /** * @param prevName The name for the previous child, if applicable */ + /** * @param prevName - The name for the previous child, if applicable */ prevName?: string | null; } diff --git a/packages/database/src/core/view/CompleteChildSource.ts b/packages/database/src/core/view/CompleteChildSource.ts index 36f2e6239df..6887ebbe442 100644 --- a/packages/database/src/core/view/CompleteChildSource.ts +++ b/packages/database/src/core/view/CompleteChildSource.ts @@ -49,16 +49,9 @@ export interface CompleteChildSource { */ // eslint-disable-next-line @typescript-eslint/naming-convention export class NoCompleteChildSource_ implements CompleteChildSource { - /** - * @inheritDoc - */ getCompleteChild(childKey?: string): Node | null { return null; } - - /** - * @inheritDoc - */ getChildAfterChild( index?: Index, child?: NamedNode, @@ -83,10 +76,6 @@ export class WriteTreeCompleteChildSource implements CompleteChildSource { private viewCache_: ViewCache, private optCompleteServerCache_: Node | null = null ) {} - - /** - * @inheritDoc - */ getCompleteChild(childKey: string): Node | null { const node = this.viewCache_.eventCache; if (node.isCompleteForChild(childKey)) { @@ -99,10 +88,6 @@ export class WriteTreeCompleteChildSource implements CompleteChildSource { return writeTreeRefCalcCompleteChild(this.writes_, childKey, serverNode); } } - - /** - * @inheritDoc - */ getChildAfterChild( index: Index, child: NamedNode, diff --git a/packages/database/src/core/view/Event.ts b/packages/database/src/core/view/Event.ts index ef18a9045a0..81d5d3b5d2e 100644 --- a/packages/database/src/core/view/Event.ts +++ b/packages/database/src/core/view/Event.ts @@ -52,10 +52,10 @@ export type EventType = */ export class DataEvent implements Event { /** - * @param eventType One of: value, child_added, child_changed, child_moved, child_removed - * @param eventRegistration The function to call to with the event data. User provided - * @param snapshot The data backing the event - * @param prevName Optional, the name of the previous child for child_* events. + * @param eventType - One of: value, child_added, child_changed, child_moved, child_removed + * @param eventRegistration - The function to call to with the event data. User provided + * @param snapshot - The data backing the event + * @param prevName - Optional, the name of the previous child for child_* events. */ constructor( public eventType: EventType, @@ -63,10 +63,6 @@ export class DataEvent implements Event { public snapshot: ExpDataSnapshot, public prevName?: string | null ) {} - - /** - * @inheritDoc - */ getPath(): Path { const ref = this.snapshot.ref; if (this.eventType === 'value') { @@ -75,24 +71,12 @@ export class DataEvent implements Event { return ref.parent._path; } } - - /** - * @inheritDoc - */ getEventType(): string { return this.eventType; } - - /** - * @inheritDoc - */ getEventRunner(): () => void { return this.eventRegistration.getEventRunner(this); } - - /** - * @inheritDoc - */ toString(): string { return ( this.getPath().toString() + @@ -110,31 +94,15 @@ export class CancelEvent implements Event { public error: Error, public path: Path ) {} - - /** - * @inheritDoc - */ getPath(): Path { return this.path; } - - /** - * @inheritDoc - */ getEventType(): string { return 'cancel'; } - - /** - * @inheritDoc - */ getEventRunner(): () => void { return this.eventRegistration.getEventRunner(this); } - - /** - * @inheritDoc - */ toString(): string { return this.path.toString() + ':cancel'; } diff --git a/packages/database/src/core/view/EventQueue.ts b/packages/database/src/core/view/EventQueue.ts index 9b5ba573843..dc62b5d1c51 100644 --- a/packages/database/src/core/view/EventQueue.ts +++ b/packages/database/src/core/view/EventQueue.ts @@ -43,7 +43,7 @@ export class EventQueue { } /** - * @param eventDataList The new events to queue. + * @param eventDataList - The new events to queue. */ export function eventQueueQueueEvents( eventQueue: EventQueue, @@ -76,8 +76,8 @@ export function eventQueueQueueEvents( * * It is assumed that the new events are all for the specified path. * - * @param path The path to raise events for. - * @param eventDataList The new events to raise. + * @param path - The path to raise events for. + * @param eventDataList - The new events to raise. */ export function eventQueueRaiseEventsAtPath( eventQueue: EventQueue, @@ -96,8 +96,8 @@ export function eventQueueRaiseEventsAtPath( * * It is assumed that the new events are all related (ancestor or descendant) to the specified path. * - * @param changedPath The path to raise events for. - * @param eventDataList The events to raise + * @param changedPath - The path to raise events for. + * @param eventDataList - The events to raise */ export function eventQueueRaiseEventsForChangedPath( eventQueue: EventQueue, diff --git a/packages/database/src/core/view/QueryParams.ts b/packages/database/src/core/view/QueryParams.ts index a8443e90521..577970fb255 100644 --- a/packages/database/src/core/view/QueryParams.ts +++ b/packages/database/src/core/view/QueryParams.ts @@ -20,7 +20,7 @@ import { assert, stringify } from '@firebase/util'; import { Index } from '../snap/indexes/Index'; import { KEY_INDEX } from '../snap/indexes/KeyIndex'; import { PathIndex } from '../snap/indexes/PathIndex'; -import { PRIORITY_INDEX } from '../snap/indexes/PriorityIndex'; +import { PRIORITY_INDEX, PriorityIndex } from '../snap/indexes/PriorityIndex'; import { VALUE_INDEX } from '../snap/indexes/ValueIndex'; import { predecessor, successor } from '../util/NextPushId'; import { MAX_NAME, MIN_NAME } from '../util/util'; @@ -78,7 +78,7 @@ export class QueryParams { indexStartName_ = ''; indexEndValue_: unknown | null = null; indexEndName_ = ''; - index_ = PRIORITY_INDEX; + index_: PriorityIndex = PRIORITY_INDEX; hasStart(): boolean { return this.startSet_; diff --git a/packages/database/src/core/view/View.ts b/packages/database/src/core/view/View.ts index 032fd1daaa4..2bb9e1d52e1 100644 --- a/packages/database/src/core/view/View.ts +++ b/packages/database/src/core/view/View.ts @@ -143,8 +143,8 @@ export function viewAddEventRegistration( } /** - * @param eventRegistration If null, remove all callbacks. - * @param cancelError If a cancelError is provided, appropriate cancel events will be returned. + * @param eventRegistration - If null, remove all callbacks. + * @param cancelError - If a cancelError is provided, appropriate cancel events will be returned. * @returns Cancel events, if cancelError was provided. */ export function viewRemoveEventRegistration( diff --git a/packages/database/src/core/view/filter/IndexedFilter.ts b/packages/database/src/core/view/filter/IndexedFilter.ts index f31c6607d62..3db8560d28e 100644 --- a/packages/database/src/core/view/filter/IndexedFilter.ts +++ b/packages/database/src/core/view/filter/IndexedFilter.ts @@ -94,10 +94,6 @@ export class IndexedFilter implements NodeFilter { return snap.updateImmediateChild(key, newChild).withIndex(this.index_); } } - - /** - * @inheritDoc - */ updateFullNode( oldSnap: Node, newSnap: Node, @@ -132,10 +128,6 @@ export class IndexedFilter implements NodeFilter { } return newSnap.withIndex(this.index_); } - - /** - * @inheritDoc - */ updatePriority(oldSnap: Node, newPriority: Node): Node { if (oldSnap.isEmpty()) { return ChildrenNode.EMPTY_NODE; @@ -143,24 +135,12 @@ export class IndexedFilter implements NodeFilter { return oldSnap.updatePriority(newPriority); } } - - /** - * @inheritDoc - */ filtersNodes(): boolean { return false; } - - /** - * @inheritDoc - */ getIndexedFilter(): IndexedFilter { return this; } - - /** - * @inheritDoc - */ getIndex(): Index { return this.index_; } diff --git a/packages/database/src/core/view/filter/LimitedFilter.ts b/packages/database/src/core/view/filter/LimitedFilter.ts index 40e003072cc..9fb74b4027d 100644 --- a/packages/database/src/core/view/filter/LimitedFilter.ts +++ b/packages/database/src/core/view/filter/LimitedFilter.ts @@ -52,10 +52,6 @@ export class LimitedFilter implements NodeFilter { this.limit_ = params.getLimit(); this.reverse_ = !params.isViewFromLeft(); } - - /** - * @inheritDoc - */ updateChild( snap: Node, key: string, @@ -91,10 +87,6 @@ export class LimitedFilter implements NodeFilter { ); } } - - /** - * @inheritDoc - */ updateFullNode( oldSnap: Node, newSnap: Node, @@ -192,32 +184,16 @@ export class LimitedFilter implements NodeFilter { .getIndexedFilter() .updateFullNode(oldSnap, filtered, optChangeAccumulator); } - - /** - * @inheritDoc - */ updatePriority(oldSnap: Node, newPriority: Node): Node { // Don't support priorities on queries return oldSnap; } - - /** - * @inheritDoc - */ filtersNodes(): boolean { return true; } - - /** - * @inheritDoc - */ getIndexedFilter(): IndexedFilter { return this.rangedFilter_.getIndexedFilter(); } - - /** - * @inheritDoc - */ getIndex(): Index { return this.index_; } diff --git a/packages/database/src/core/view/filter/RangedFilter.ts b/packages/database/src/core/view/filter/RangedFilter.ts index 3c86be93ba4..7311f1cc826 100644 --- a/packages/database/src/core/view/filter/RangedFilter.ts +++ b/packages/database/src/core/view/filter/RangedFilter.ts @@ -60,10 +60,6 @@ export class RangedFilter implements NodeFilter { this.index_.compare(node, this.getEndPost()) <= 0 ); } - - /** - * @inheritDoc - */ updateChild( snap: Node, key: string, @@ -84,10 +80,6 @@ export class RangedFilter implements NodeFilter { optChangeAccumulator ); } - - /** - * @inheritDoc - */ updateFullNode( oldSnap: Node, newSnap: Node, @@ -112,32 +104,16 @@ export class RangedFilter implements NodeFilter { optChangeAccumulator ); } - - /** - * @inheritDoc - */ updatePriority(oldSnap: Node, newPriority: Node): Node { // Don't support priorities on queries return oldSnap; } - - /** - * @inheritDoc - */ filtersNodes(): boolean { return true; } - - /** - * @inheritDoc - */ getIndexedFilter(): IndexedFilter { return this.indexedFilter_; } - - /** - * @inheritDoc - */ getIndex(): Index { return this.index_; } diff --git a/packages/database/src/exp/Database.ts b/packages/database/src/exp/Database.ts index 557840cc004..77d083bc66b 100644 --- a/packages/database/src/exp/Database.ts +++ b/packages/database/src/exp/Database.ts @@ -162,7 +162,7 @@ function repoManagerDeleteRepo(repo: Repo, appName: string): void { * Ensures a repo doesn't already exist and then creates one using the * provided app. * - * @param repoInfo The metadata about the Repo + * @param repoInfo - The metadata about the Repo * @returns The Repo object for the specified server / repoName. */ function repoManagerCreateRepo( @@ -200,6 +200,7 @@ export function repoManagerForceRestClient(forceRestClient: boolean): void { * Class representing a Firebase Realtime Database. */ export class FirebaseDatabase implements _FirebaseService { + /** Represents a database instance. */ readonly 'type' = 'database'; /** Track if the instance has been used (root or repo accessed) */ @@ -208,7 +209,12 @@ export class FirebaseDatabase implements _FirebaseService { /** Backing state for root_ */ private _rootInternal?: ReferenceImpl; - constructor(private _repoInternal: Repo, readonly app: FirebaseApp) {} + /** @hideconstructor */ + constructor( + private _repoInternal: Repo, + /** The FirebaseApp associated with this Realtime Database instance. */ + readonly app: FirebaseApp + ) {} get _repo(): Repo { if (!this._instanceStarted) { @@ -335,8 +341,8 @@ export function goOnline(db: FirebaseDatabase): void { /** * Logs debugging information to the console. * - * @param enabled Enables logging if `true`, disables logging if `false`. - * @param persistent Remembers the logging state between page refreshes if + * @param enabled - Enables logging if `true`, disables logging if `false`. + * @param persistent - Remembers the logging state between page refreshes if * `true`. */ export function enableLogging(enabled: boolean, persistent?: boolean); @@ -344,8 +350,8 @@ export function enableLogging(enabled: boolean, persistent?: boolean); /** * Logs debugging information to the console. * - * @param logger A custom logger function to control how things get logged. - * @param persistent Remembers the logging state between page refreshes if + * @param logger - A custom logger function to control how things get logged. + * @param persistent - Remembers the logging state between page refreshes if * `true`. */ export function enableLogging( diff --git a/packages/database/src/exp/OnDisconnect.ts b/packages/database/src/exp/OnDisconnect.ts index cf7998cc2bd..f3b9507ab91 100644 --- a/packages/database/src/exp/OnDisconnect.ts +++ b/packages/database/src/exp/OnDisconnect.ts @@ -24,7 +24,6 @@ import { repoOnDisconnectSetWithPriority, repoOnDisconnectUpdate } from '../core/Repo'; -import { Indexable } from '../core/util/misc'; import { Path } from '../core/util/Path'; import { validateFirebaseDataArg, @@ -41,9 +40,9 @@ import { * * The `onDisconnect` class is most commonly used to manage presence in * applications where it is useful to detect how many clients are connected and - * when other clients disconnect. See {@link - * https://firebase.google.com/docs/database/web/offline-capabilities Enabling - * Offline Capabilities in JavaScript} for more information. + * when other clients disconnect. See + * {@link https://firebase.google.com/docs/database/web/offline-capabilities | Enabling Offline Capabilities in JavaScript} + * for more information. * * To avoid problems when a connection is dropped before the requests can be * transferred to the Database server, these functions should be called before @@ -54,6 +53,7 @@ import { * the `onDisconnect` operations each time you reconnect. */ export class OnDisconnect { + /** @hideconstructor */ constructor(private _repo: Repo, private _path: Path) {} /** @@ -101,9 +101,9 @@ export class OnDisconnect { * * `set()` is especially useful for implementing "presence" systems, where a * value should be changed or cleared when a user disconnects so that they - * appear "offline" to other users. See {@link - * https://firebase.google.com/docs/database/web/offline-capabilities Enabling - * Offline Capabilities in JavaScript} for more information. + * appear "offline" to other users. See + * {@link https://firebase.google.com/docs/database/web/offline-capabilities | Enabling Offline Capabilities in JavaScript} + * for more information. * * Note that `onDisconnect` operations are only triggered once. If you want an * operation to occur each time a disconnect occurs, you'll need to re-establish @@ -173,10 +173,10 @@ export class OnDisconnect { * only the referenced properties at the current location (instead of replacing * all the child properties at the current location). * - * @param values Object containing multiple values. + * @param values - Object containing multiple values. * @returns Resolves when synchronization to the Database is complete. */ - update(values: Indexable): Promise { + update(values: object): Promise { validateWritablePath('OnDisconnect.update', this._path); validateFirebaseMergeDataArg( 'OnDisconnect.update', @@ -188,7 +188,7 @@ export class OnDisconnect { repoOnDisconnectUpdate( this._repo, this._path, - values, + values as Record, deferred.wrapCallback(() => {}) ); return deferred.promise; diff --git a/packages/database/src/exp/Reference.ts b/packages/database/src/exp/Reference.ts index 6598f983c74..0934e8e183d 100644 --- a/packages/database/src/exp/Reference.ts +++ b/packages/database/src/exp/Reference.ts @@ -31,9 +31,8 @@ import { QueryContext } from '../core/view/EventRegistration'; * `on*()` methods. You will only receive events and `DataSnapshot`s for the * subset of the data that matches your query. * - * Read our documentation on {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data - * Sorting and filtering data} for more information. + * See {@link https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data} + * for more information. */ export interface Query extends QueryContext { /** The `Reference` for the `Query`'s location. */ @@ -89,8 +88,7 @@ export interface Query extends QueryContext { * * Writing is done with the `set()` method and reading can be done with the * `on*()` method. See {@link - * https://firebase.google.com/docs/database/web/read-and-write Read and Write - * Data on the Web} + * https://firebase.google.com/docs/database/web/read-and-write} */ export interface Reference extends Query { /** diff --git a/packages/database/src/exp/Reference_impl.ts b/packages/database/src/exp/Reference_impl.ts index d29628bd5e4..4f4d6b7e754 100644 --- a/packages/database/src/exp/Reference_impl.ts +++ b/packages/database/src/exp/Reference_impl.ts @@ -90,7 +90,8 @@ import { ListenOptions, Query as Query, Reference as Reference, - Unsubscribe + Unsubscribe, + ThenableReference } from './Reference'; export class QueryImpl implements Query, QueryContext { @@ -281,13 +282,14 @@ export class ReferenceImpl extends QueryImpl implements Reference { */ export class DataSnapshot { /** - * @param _node A SnapshotNode to wrap. - * @param ref The location this snapshot came from. - * @param _index The iteration order for this snapshot + * @param _node - A SnapshotNode to wrap. + * @param ref - The location this snapshot came from. + * @param _index - The iteration order for this snapshot * @hideconstructor */ constructor( readonly _node: Node, + /** The location of this DataSnapshot. */ readonly ref: ReferenceImpl, readonly _index: Index ) {} @@ -297,9 +299,8 @@ export class DataSnapshot { * * Applications need not use priority but can order collections by * ordinary properties (see - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data - * Sorting and filtering data}). + * {@link https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data |Sorting and filtering data} + * ). */ get priority(): string | number | null { // typecast here because we never return deferred values or internal priorities (MAX_PRIORITY) @@ -537,10 +538,9 @@ export function child(parent: Reference, path: string): ReferenceImpl { } /** - * Returns an `OnDisconnect` object - see {@link - * https://firebase.google.com/docs/database/web/offline-capabilities - * Enabling Offline Capabilities in JavaScript} for more information on how - * to use it. + * Returns an `OnDisconnect` object - see + * {@link https://firebase.google.com/docs/database/web/offline-capabilities | Enabling Offline Capabilities in JavaScript} + * for more information on how to use it. * * @param ref - The reference to add OnDisconnect triggers for. */ @@ -568,22 +568,15 @@ export interface ThenableReferenceImpl * resulting list of items is chronologically sorted. The keys are also * designed to be unguessable (they contain 72 random bits of entropy). * - * See {@link - * https://firebase.google.com/docs/database/web/lists-of-data#append_to_a_list_of_data - * Append to a list of data} - *
See {@link - * https://firebase.googleblog.com/2015/02/the-2120-ways-to-ensure-unique_68.html - * The 2^120 Ways to Ensure Unique Identifiers} + * See {@link https://firebase.google.com/docs/database/web/lists-of-data#append_to_a_list_of_data | Append to a list of data} + *
See {@link ttps://firebase.googleblog.com/2015/02/the-2120-ways-to-ensure-unique_68.html | The 2^120 Ways to Ensure Unique Identifiers} * * @param parent - The parent location. * @param value - Optional value to be written at the generated location. * @returns Combined `Promise` and `Reference`; resolves when write is complete, * but can be used immediately as the `Reference` to the child location. */ -export function push( - parent: Reference, - value?: unknown -): ThenableReferenceImpl { +export function push(parent: Reference, value?: unknown): ThenableReference { parent = getModularInstance(parent); validateWritablePath('push', parent._path); validateFirebaseDataArg('push', value, parent._path, true); @@ -678,9 +671,9 @@ export function set(ref: Reference, value: unknown): Promise { * Sets a priority for the data at this Database location. * * Applications need not use priority but can order collections by - * ordinary properties (see {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data - * Sorting and filtering data}). + * ordinary properties (see + * {@link https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data | Sorting and filtering data} + * ). * * @param ref - The location to write to. * @param priority - The priority to be written (string, number, or null). @@ -709,9 +702,9 @@ export function setPriority( * priority for that data. * * Applications need not use priority but can order collections by - * ordinary properties (see {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data - * Sorting and filtering data}). + * ordinary properties (see + * {@link https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data | Sorting and filtering data} + * ). * * @param ref - The location to write to. * @param value - The value to be written (string, number, boolean, object, @@ -770,9 +763,8 @@ export function setWithPriority( * * Passing `null` to `update()` will remove the data at this location. * - * See {@link - * https://firebase.googleblog.com/2015/09/introducing-multi-location-updates-and_86.html - * Introducing multi-location updates and more}. + * See + * {@link https://firebase.googleblog.com/2015/09/introducing-multi-location-updates-and_86.html | Introducing multi-location updates and more}. * * @param ref - The location to write to. * @param values - Object containing multiple values. @@ -1012,8 +1004,8 @@ function addEventListener( * This is the primary way to read data from a Database. Your callback * will be triggered for the initial data and again whenever the data changes. * Invoke the returned unsubscribe callback to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve - * Data on the Web} for more details. + * {@link https://firebase.google.com/docs/database/web/retrieve-data | Retrieve Data on the Web} + * for more details. * * An `onValue` event will trigger once with the initial data stored at this * location, and then trigger again each time the data changes. The @@ -1044,8 +1036,8 @@ export function onValue( * This is the primary way to read data from a Database. Your callback * will be triggered for the initial data and again whenever the data changes. * Invoke the returned unsubscribe callback to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve - * Data on the Web} for more details. + * {@link https://firebase.google.com/docs/database/web/retrieve-data | Retrieve Data on the Web} + * for more details. * * An `onValue` event will trigger once with the initial data stored at this * location, and then trigger again each time the data changes. The @@ -1073,8 +1065,8 @@ export function onValue( * This is the primary way to read data from a Database. Your callback * will be triggered for the initial data and again whenever the data changes. * Invoke the returned unsubscribe callback to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve - * Data on the Web} for more details. + * {@link https://firebase.google.com/docs/database/web/retrieve-data | Retrieve Data on the Web} + * for more details. * * An `onValue` event will trigger once with the initial data stored at this * location, and then trigger again each time the data changes. The @@ -1123,8 +1115,8 @@ export function onValue( * This is the primary way to read data from a Database. Your callback * will be triggered for the initial data and again whenever the data changes. * Invoke the returned unsubscribe callback to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve - * Data on the Web} for more details. + * {@link https://firebase.google.com/docs/database/web/retrieve-data | Retrieve Data on the Web} + * for more details. * * An `onChildAdded` event will be triggered once for each initial child at this * location, and it will be triggered again every time a new child is added. The @@ -1159,8 +1151,8 @@ export function onChildAdded( * This is the primary way to read data from a Database. Your callback * will be triggered for the initial data and again whenever the data changes. * Invoke the returned unsubscribe callback to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve - * Data on the Web} for more details. + * {@link https://firebase.google.com/docs/database/web/retrieve-data | Retrieve Data on the Web} + * for more details. * * An `onChildAdded` event will be triggered once for each initial child at this * location, and it will be triggered again every time a new child is added. The @@ -1192,8 +1184,8 @@ export function onChildAdded( * This is the primary way to read data from a Database. Your callback * will be triggered for the initial data and again whenever the data changes. * Invoke the returned unsubscribe callback to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve - * Data on the Web} for more details. + * {@link https://firebase.google.com/docs/database/web/retrieve-data | Retrieve Data on the Web} + * for more details. * * An `onChildAdded` event will be triggered once for each initial child at this * location, and it will be triggered again every time a new child is added. The @@ -1249,8 +1241,8 @@ export function onChildAdded( * This is the primary way to read data from a Database. Your callback * will be triggered for the initial data and again whenever the data changes. * Invoke the returned unsubscribe callback to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve - * Data on the Web} for more details. + * {@link https://firebase.google.com/docs/database/web/retrieve-data | Retrieve Data on the Web} + * for more details. * * An `onChildChanged` event will be triggered when the data stored in a child * (or any of its descendants) changes. Note that a single `child_changed` event @@ -1286,8 +1278,8 @@ export function onChildChanged( * This is the primary way to read data from a Database. Your callback * will be triggered for the initial data and again whenever the data changes. * Invoke the returned unsubscribe callback to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve - * Data on the Web} for more details. + * {@link https://firebase.google.com/docs/database/web/retrieve-data | Retrieve Data on the Web} + * for more details. * * An `onChildChanged` event will be triggered when the data stored in a child * (or any of its descendants) changes. Note that a single `child_changed` event @@ -1320,8 +1312,8 @@ export function onChildChanged( * This is the primary way to read data from a Database. Your callback * will be triggered for the initial data and again whenever the data changes. * Invoke the returned unsubscribe callback to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve - * Data on the Web} for more details. + * {@link https://firebase.google.com/docs/database/web/retrieve-data | Retrieve Data on the Web} + * for more details. * * An `onChildChanged` event will be triggered when the data stored in a child * (or any of its descendants) changes. Note that a single `child_changed` event @@ -1378,8 +1370,8 @@ export function onChildChanged( * This is the primary way to read data from a Database. Your callback * will be triggered for the initial data and again whenever the data changes. * Invoke the returned unsubscribe callback to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve - * Data on the Web} for more details. + * {@link https://firebase.google.com/docs/database/web/retrieve-data | Retrieve Data on the Web} + * for more details. * * An `onChildMoved` event will be triggered when a child's sort order changes * such that its position relative to its siblings changes. The `DataSnapshot` @@ -1413,8 +1405,8 @@ export function onChildMoved( * This is the primary way to read data from a Database. Your callback * will be triggered for the initial data and again whenever the data changes. * Invoke the returned unsubscribe callback to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve - * Data on the Web} for more details. + * {@link https://firebase.google.com/docs/database/web/retrieve-data | Retrieve Data on the Web} + * for more details. * * An `onChildMoved` event will be triggered when a child's sort order changes * such that its position relative to its siblings changes. The `DataSnapshot` @@ -1445,8 +1437,8 @@ export function onChildMoved( * This is the primary way to read data from a Database. Your callback * will be triggered for the initial data and again whenever the data changes. * Invoke the returned unsubscribe callback to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve - * Data on the Web} for more details. + * {@link https://firebase.google.com/docs/database/web/retrieve-data | Retrieve Data on the Web} + * for more details. * * An `onChildMoved` event will be triggered when a child's sort order changes * such that its position relative to its siblings changes. The `DataSnapshot` @@ -1501,8 +1493,8 @@ export function onChildMoved( * This is the primary way to read data from a Database. Your callback * will be triggered for the initial data and again whenever the data changes. * Invoke the returned unsubscribe callback to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve - * Data on the Web} for more details. + * {@link https://firebase.google.com/docs/database/web/retrieve-data | Retrieve Data on the Web} + * for more details. * * An `onChildRemoved` event will be triggered once every time a child is * removed. The `DataSnapshot` passed into the callback will be the old data for @@ -1537,8 +1529,8 @@ export function onChildRemoved( * This is the primary way to read data from a Database. Your callback * will be triggered for the initial data and again whenever the data changes. * Invoke the returned unsubscribe callback to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve - * Data on the Web} for more details. + * {@link https://firebase.google.com/docs/database/web/retrieve-data | Retrieve Data on the Web} + * for more details. * * An `onChildRemoved` event will be triggered once every time a child is * removed. The `DataSnapshot` passed into the callback will be the old data for @@ -1570,8 +1562,8 @@ export function onChildRemoved( * This is the primary way to read data from a Database. Your callback * will be triggered for the initial data and again whenever the data changes. * Invoke the returned unsubscribe callback to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data Retrieve - * Data on the Web} for more details. + * {@link https://firebase.google.com/docs/database/web/retrieve-data | Retrieve Data on the Web} + * for more details. * * An `onChildRemoved` event will be triggered once every time a child is * removed. The `DataSnapshot` passed into the callback will be the old data for @@ -1638,10 +1630,10 @@ export { EventType }; * callbacks. * * @param query - The query that the listener was registered with. - * @param eventType One of the following strings: "value", "child_added", + * @param eventType - One of the following strings: "value", "child_added", * "child_changed", "child_removed", or "child_moved." If omitted, all callbacks * for the `Reference` will be removed. - * @param callback The callback function that was passed to `on()` or + * @param callback - The callback function that was passed to `on()` or * `undefined` to remove all callbacks. */ export function off( @@ -1748,9 +1740,8 @@ class QueryEndAtConstraint extends QueryConstraint { * have exactly the specified value must also have a key name less than or equal * to the specified key. * - * You can read more about `endAt()` in{@link - * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data - * Filtering data}. + * You can read more about `endAt()` in + * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}. * * @param value - The value to end at. The argument type depends on which * `orderBy*()` function was used in this query. Specify a value that matches @@ -1875,9 +1866,8 @@ class QueryStartAtConstraint extends QueryConstraint { * have exactly the specified value must also have a key name greater than or * equal to the specified key. * - * You can read more about `startAt()` in {@link - * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data - * Filtering data}. + * You can read more about `startAt()` in + * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}. * * @param value - The value to start at. The argument type depends on which * `orderBy*()` function was used in this query. Specify a value that matches @@ -1939,7 +1929,7 @@ class QueryStartAfterConstraint extends QueryConstraint { * If a key is specified, then children must have a value greater than or equal * to the specified value and a a key name greater than the specified key. * - * @param value The value to start after. The argument type depends on which + * @param value - The value to start after. The argument type depends on which * `orderBy*()` function was used in this query. Specify a value that matches * the `orderBy*()` type. When used in combination with `orderByKey()`, the * value must be a string. @@ -1990,9 +1980,8 @@ class QueryLimitToFirstConstraint extends QueryConstraint { * `child_removed` events for each item that drops out of the active list so * that the total number stays at 100. * - * You can read more about `limitToFirst()` in {@link - * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data - * Filtering data}. + * You can read more about `limitToFirst()` in + * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}. * * @param limit - The maximum number of nodes to include in this query. */ @@ -2039,9 +2028,8 @@ class QueryLimitToLastConstraint extends QueryConstraint { * `child_removed` events for each item that drops out of the active list so * that the total number stays at 100. * - * You can read more about `limitToLast()` in {@link - * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data - * Filtering data}. + * You can read more about `limitToLast()` in + * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}. * * @param limit - The maximum number of nodes to include in this query. */ @@ -2090,12 +2078,11 @@ class QueryOrderByChildConstraint extends QueryConstraint { * Firebase queries allow you to order your data by any child key on the fly. * However, if you know in advance what your indexes will be, you can define * them via the .indexOn rule in your Security Rules for better performance. See - * the {@link https://firebase.google.com/docs/database/security/indexing-data - * .indexOn} rule for more information. + * the{@link https://firebase.google.com/docs/database/security/indexing-data} + * rule for more information. * - * You can read more about `orderByChild()` in {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sort_data - * Sort data}. + * You can read more about `orderByChild()` in + * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data}. * * @param path - The path to order by. */ @@ -2138,9 +2125,8 @@ class QueryOrderByKeyConstraint extends QueryConstraint { * * Sorts the results of a query by their (ascending) key values. * - * You can read more about `orderByKey()` in {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sort_data - * Sort data}. + * You can read more about `orderByKey()` in + * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data}. */ export function orderByKey(): QueryConstraint { return new QueryOrderByKeyConstraint(); @@ -2166,9 +2152,9 @@ class QueryOrderByPriorityConstraint extends QueryConstraint { * Creates a new `QueryConstraint` that orders by priority. * * Applications need not use priority but can order collections by - * ordinary properties (see {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sort_data - * Sort data} for alternatives to priority. + * ordinary properties (see + * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data} + * for alternatives to priority. */ export function orderByPriority(): QueryConstraint { return new QueryOrderByPriorityConstraint(); @@ -2196,9 +2182,8 @@ class QueryOrderByValueConstraint extends QueryConstraint { * If the children of a query are all scalar values (string, number, or * boolean), you can order the results by their (ascending) values. * - * You can read more about `orderByValue()` in {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sort_data - * Sort data}. + * You can read more about `orderByValue()` in + * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data}. */ export function orderByValue(): QueryConstraint { return new QueryOrderByValueConstraint(); @@ -2246,9 +2231,8 @@ class QueryEqualToValueConstraint extends QueryConstraint { * value must also have exactly the specified key as their key name. This can be * used to filter result sets with many matches for the same value. * - * You can read more about `equalTo()` in {@link - * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data - * Filtering data}. + * You can read more about `equalTo()` in + * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}. * * @param value - The value to match for. The argument type depends on which * `orderBy*()` function was used in this query. Specify a value that matches diff --git a/packages/database/src/exp/ServerValue.ts b/packages/database/src/exp/ServerValue.ts index 30cdffdd509..c8fa7825d42 100644 --- a/packages/database/src/exp/ServerValue.ts +++ b/packages/database/src/exp/ServerValue.ts @@ -32,7 +32,7 @@ export function serverTimestamp(): object { * Returns a placeholder value that can be used to atomically increment the * current database value by the provided delta. * - * @param delta the amount to modify the current value atomically. + * @param delta - the amount to modify the current value atomically. * @returns A placeholder value for modifying data atomically server-side. */ export function increment(delta: number): object { diff --git a/packages/database/src/exp/Transaction.ts b/packages/database/src/exp/Transaction.ts index a7555ec3f99..66304cc55c3 100644 --- a/packages/database/src/exp/Transaction.ts +++ b/packages/database/src/exp/Transaction.ts @@ -36,12 +36,19 @@ export interface TransactionOptions { readonly applyLocally?: boolean; } +/** + * A type for the resolve value of Firebase.transaction. + */ export class TransactionResult { - /** - * A type for the resolve value of Firebase.transaction. - */ - constructor(readonly committed: boolean, readonly snapshot: DataSnapshot) {} + /** @hideconstructor */ + constructor( + /** Whether the transaction was successfully committed. */ + readonly committed: boolean, + /** The resulting data snapshot. */ + readonly snapshot: DataSnapshot + ) {} + /** Returns a JSON-serializable representation of this object. */ toJSON(): object { return { committed: this.committed, snapshot: this.snapshot.toJSON() }; } diff --git a/packages/database/src/realtime/BrowserPollConnection.ts b/packages/database/src/realtime/BrowserPollConnection.ts index 5eb864972a8..4f60632d1f1 100644 --- a/packages/database/src/realtime/BrowserPollConnection.ts +++ b/packages/database/src/realtime/BrowserPollConnection.ts @@ -99,12 +99,12 @@ export class BrowserPollConnection implements Transport { private onDisconnect_: ((a?: boolean) => void) | null; /** - * @param connId An identifier for this connection, used for logging - * @param repoInfo The info for the endpoint to send data to. - * @param applicationId The Firebase App ID for this project. - * @param transportSessionId Optional transportSessionid if we are reconnecting for an existing + * @param connId - An identifier for this connection, used for logging + * @param repoInfo - The info for the endpoint to send data to. + * @param applicationId - The Firebase App ID for this project. + * @param transportSessionId - Optional transportSessionid if we are reconnecting for an existing * transport session - * @param lastSessionId Optional lastSessionId if the PersistentConnection has already created a + * @param lastSessionId - Optional lastSessionId if the PersistentConnection has already created a * connection previously */ constructor( @@ -121,8 +121,8 @@ export class BrowserPollConnection implements Transport { } /** - * @param onMessage Callback when messages arrive - * @param onDisconnect Callback with connection lost. + * @param onMessage - Callback when messages arrive + * @param onDisconnect - Callback with connection lost. */ open(onMessage: (msg: {}) => void, onDisconnect: (a?: boolean) => void) { this.curSegmentNum = 0; @@ -330,7 +330,7 @@ export class BrowserPollConnection implements Transport { /** * Send the JSON object down to the server. It will need to be stringified, base64 encoded, and then * broken into chunks (since URLs have a small maximum length). - * @param data The JSON data to transmit. + * @param data - The JSON data to transmit. */ send(data: {}) { const dataStr = stringify(data); diff --git a/packages/database/src/realtime/Connection.ts b/packages/database/src/realtime/Connection.ts index 25f86aeb411..6c2ef7967ec 100644 --- a/packages/database/src/realtime/Connection.ts +++ b/packages/database/src/realtime/Connection.ts @@ -212,7 +212,7 @@ export class Connection { } /** - * @param dataMsg An arbitrary data message to be sent to the server + * @param dataMsg - An arbitrary data message to be sent to the server */ sendRequest(dataMsg: object) { // wrap in a data message envelope and send it on @@ -367,7 +367,7 @@ export class Connection { } /** - * @param handshake The handshake data returned from the server + * @param handshake - The handshake data returned from the server */ private onHandshake_(handshake: { ts: number; @@ -478,7 +478,7 @@ export class Connection { } /** - * @param everConnected Whether or not the connection ever reached a server. Used to determine if + * @param everConnected - Whether or not the connection ever reached a server. Used to determine if * we should flush the host cache */ private onConnectionLost_(everConnected: boolean) { diff --git a/packages/database/src/realtime/Transport.ts b/packages/database/src/realtime/Transport.ts index c8d6839be1d..f3433c4c354 100644 --- a/packages/database/src/realtime/Transport.ts +++ b/packages/database/src/realtime/Transport.ts @@ -47,10 +47,10 @@ export abstract class Transport { abstract connId: string; /** - * @param connId An identifier for this connection, used for logging - * @param repoInfo The info for the endpoint to send data to. - * @param transportSessionId Optional transportSessionId if this is connecting to an existing transport session - * @param lastSessionId Optional lastSessionId if there was a previous connection + * @param connId - An identifier for this connection, used for logging + * @param repoInfo - The info for the endpoint to send data to. + * @param transportSessionId - Optional transportSessionId if this is connecting to an existing transport session + * @param lastSessionId - Optional lastSessionId if there was a previous connection * @interface */ // eslint-disable-next-line @typescript-eslint/no-useless-constructor @@ -62,8 +62,8 @@ export abstract class Transport { ) {} /** - * @param onMessage Callback when messages arrive - * @param onDisconnect Callback with connection lost. + * @param onMessage - Callback when messages arrive + * @param onDisconnect - Callback with connection lost. */ abstract open( onMessage: (a: {}) => void, @@ -75,7 +75,7 @@ export abstract class Transport { abstract close(): void; /** - * @param data The JSON data to transmit + * @param data - The JSON data to transmit */ abstract send(data: {}): void; diff --git a/packages/database/src/realtime/TransportManager.ts b/packages/database/src/realtime/TransportManager.ts index 19ec62dc2e4..36d732fb840 100644 --- a/packages/database/src/realtime/TransportManager.ts +++ b/packages/database/src/realtime/TransportManager.ts @@ -37,7 +37,7 @@ export class TransportManager { } /** - * @param repoInfo Metadata around the namespace we're connecting to + * @param repoInfo - Metadata around the namespace we're connecting to */ constructor(repoInfo: RepoInfo) { this.initTransports_(repoInfo); diff --git a/packages/database/src/realtime/WebSocketConnection.ts b/packages/database/src/realtime/WebSocketConnection.ts index 929f2f799d1..dad520c3bc0 100644 --- a/packages/database/src/realtime/WebSocketConnection.ts +++ b/packages/database/src/realtime/WebSocketConnection.ts @@ -73,12 +73,12 @@ export class WebSocketConnection implements Transport { private nodeAdmin: boolean; /** - * @param connId identifier for this transport - * @param repoInfo The info for the websocket endpoint. - * @param applicationId The Firebase App ID for this project. - * @param transportSessionId Optional transportSessionId if this is connecting to an existing transport + * @param connId - identifier for this transport + * @param repoInfo - The info for the websocket endpoint. + * @param applicationId - The Firebase App ID for this project. + * @param transportSessionId - Optional transportSessionId if this is connecting to an existing transport * session - * @param lastSessionId Optional lastSessionId if there was a previous connection + * @param lastSessionId - Optional lastSessionId if there was a previous connection */ constructor( public connId: string, @@ -98,10 +98,10 @@ export class WebSocketConnection implements Transport { } /** - * @param repoInfo The info for the websocket endpoint. - * @param transportSessionId Optional transportSessionId if this is connecting to an existing transport + * @param repoInfo - The info for the websocket endpoint. + * @param transportSessionId - Optional transportSessionId if this is connecting to an existing transport * session - * @param lastSessionId Optional lastSessionId if there was a previous connection + * @param lastSessionId - Optional lastSessionId if there was a previous connection * @returns connection url */ private static connectionURL_( @@ -130,8 +130,8 @@ export class WebSocketConnection implements Transport { } /** - * @param onMessage Callback when messages arrive - * @param onDisconnect Callback with connection lost. + * @param onMessage - Callback when messages arrive + * @param onDisconnect - Callback with connection lost. */ open(onMessage: (msg: {}) => void, onDisconnect: (a?: boolean) => void) { this.onDisconnect = onDisconnect; @@ -279,7 +279,7 @@ export class WebSocketConnection implements Transport { } /** - * @param frameCount The number of frames we are expecting from the server + * @param frameCount - The number of frames we are expecting from the server */ private handleNewFrameCount_(frameCount: number) { this.totalFrames = frameCount; @@ -307,7 +307,7 @@ export class WebSocketConnection implements Transport { /** * Process a websocket frame that has arrived from the server. - * @param mess The frame data + * @param mess - The frame data */ handleIncomingFrame(mess: { [k: string]: unknown }) { if (this.mySock === null) { @@ -333,7 +333,7 @@ export class WebSocketConnection implements Transport { /** * Send a message to the server - * @param data The JSON object to transmit + * @param data - The JSON object to transmit */ send(data: {}) { this.resetKeepAlive(); @@ -414,7 +414,7 @@ export class WebSocketConnection implements Transport { /** * Send a string over the websocket. * - * @param str String to send. + * @param str - String to send. */ private sendString_(str: string) { // Firefox seems to sometimes throw exceptions (NS_ERROR_UNEXPECTED) from websocket .send() diff --git a/packages/database/test/helpers/events.ts b/packages/database/test/helpers/events.ts index 20be8461d70..144ea2b10d5 100644 --- a/packages/database/test/helpers/events.ts +++ b/packages/database/test/helpers/events.ts @@ -42,7 +42,7 @@ function rawPath(firebaseRef: Reference) { /** * Creates a struct which waits for many events. - * @param pathAndEvents an array of tuples of [Firebase, [event type strings]] + * @param pathAndEvents - an array of tuples of [Firebase, [event type strings]] */ export function eventTestHelper(pathAndEvents, helperName?) { let resolve, reject; diff --git a/repo-scripts/prune-dts/prune-dts.ts b/repo-scripts/prune-dts/prune-dts.ts index a2ac2fbb101..745d0f8ab26 100644 --- a/repo-scripts/prune-dts/prune-dts.ts +++ b/repo-scripts/prune-dts/prune-dts.ts @@ -79,7 +79,7 @@ export async function removeUnusedImports( function hasPrivatePrefix(name: ts.Identifier): boolean { // Identifiers that are prefixed with an underscore are not not included in // the public API. - return name.escapedText.toString().startsWith('_'); + return !!name.escapedText?.toString().startsWith('_'); } /** Returns whether type identified by `name` is exported. */ From d2919940caf577632ab0aeca004d623ca9312056 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Wed, 7 Apr 2021 16:35:12 -0600 Subject: [PATCH 14/15] Update lets-go-travel.md --- .changeset/lets-go-travel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/lets-go-travel.md b/.changeset/lets-go-travel.md index 59f341aa35d..b1b21fe2320 100644 --- a/.changeset/lets-go-travel.md +++ b/.changeset/lets-go-travel.md @@ -1,5 +1,5 @@ --- -"firebase": minor +"firebase": patch "@firebase/util": major "@firebase/database": patch --- From d9bef493888fdce9e8f182df32cfbbf654f578d8 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Wed, 7 Apr 2021 16:45:19 -0600 Subject: [PATCH 15/15] Update enableLogging --- common/api-review/database-exp.api.md | 4 ++-- packages/database/src/exp/Database.ts | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/common/api-review/database-exp.api.md b/common/api-review/database-exp.api.md index 7a36c6f43b1..60965141148 100644 --- a/common/api-review/database-exp.api.md +++ b/common/api-review/database-exp.api.md @@ -26,7 +26,7 @@ export class DataSnapshot { export function enableLogging(enabled: boolean, persistent?: boolean): any; // @public -export function enableLogging(logger?: (message: string) => unknown, persistent?: boolean): any; +export function enableLogging(logger: (message: string) => unknown): any; // @public export function endAt(value: number | string | boolean | null, key?: string): QueryConstraint; @@ -50,7 +50,7 @@ export class FirebaseDatabase { export function get(query: Query): Promise; // @public -export function getDatabase(app: FirebaseApp, url?: string): FirebaseDatabase; +export function getDatabase(app?: FirebaseApp, url?: string): FirebaseDatabase; // @public export function goOffline(db: FirebaseDatabase): void; diff --git a/packages/database/src/exp/Database.ts b/packages/database/src/exp/Database.ts index 4dfdfa9a582..041f000872b 100644 --- a/packages/database/src/exp/Database.ts +++ b/packages/database/src/exp/Database.ts @@ -359,13 +359,8 @@ export function enableLogging(enabled: boolean, persistent?: boolean); * Logs debugging information to the console. * * @param logger - A custom logger function to control how things get logged. - * @param persistent - Remembers the logging state between page refreshes if - * `true`. */ -export function enableLogging( - logger?: (message: string) => unknown, - persistent?: boolean -); +export function enableLogging(logger: (message: string) => unknown); export function enableLogging( logger: boolean | ((message: string) => unknown),