From f347deaf0884c70fbf99206c9ad39c7a96b68b3c Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Thu, 21 May 2020 13:44:34 -0400 Subject: [PATCH 01/27] Add bundle.proto d.ts and introduce IDB object stores for bundles. (#3073) * Manual copy proto and create bundle_proto.d.ts * IndexedDb schema change to introduce bundle object stores. * Renaming interfaces without leading I * Reordering imports * Address comments * Add totalBytes and totalDocuments --- .../firestore/src/local/indexeddb_schema.ts | 69 +++++++++- .../src/protos/firestore/bundle.proto | 118 ++++++++++++++++++ .../src/protos/firestore_bundle_proto.d.ts | 92 ++++++++++++++ 3 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 packages/firestore/src/protos/firestore/bundle.proto create mode 100644 packages/firestore/src/protos/firestore_bundle_proto.d.ts diff --git a/packages/firestore/src/local/indexeddb_schema.ts b/packages/firestore/src/local/indexeddb_schema.ts index ad79c4c6073..2026b373adb 100644 --- a/packages/firestore/src/local/indexeddb_schema.ts +++ b/packages/firestore/src/local/indexeddb_schema.ts @@ -17,6 +17,7 @@ import { BatchId, ListenSequenceNumber, TargetId } from '../core/types'; import { ResourcePath } from '../model/path'; +import { BundledQuery } from '../protos/firestore_bundle_proto'; import * as api from '../protos/firestore_proto_api'; import { hardAssert, debugAssert } from '../util/assert'; @@ -51,8 +52,9 @@ import { SimpleDbSchemaConverter, SimpleDbTransaction } from './simple_db'; * 9. Change RemoteDocumentChanges store to be keyed by readTime rather than * an auto-incrementing ID. This is required for Index-Free queries. * 10. Rewrite the canonical IDs to the explicit Protobuf-based format. + * 11. Add bundles and named_queries for bundle support. */ -export const SCHEMA_VERSION = 10; +export const SCHEMA_VERSION = 11; /** Performs database creation and schema upgrades. */ export class SchemaConverter implements SimpleDbSchemaConverter { @@ -153,6 +155,13 @@ export class SchemaConverter implements SimpleDbSchemaConverter { if (fromVersion < 10 && toVersion >= 10) { p = p.next(() => this.rewriteCanonicalIds(simpleDbTransaction)); } + + if (fromVersion < 11 && toVersion >= 11) { + p = p.next(() => { + createBundlesStore(db); + createNamedQueriesStore(db); + }); + } return p; } @@ -1078,6 +1087,60 @@ function createClientMetadataStore(db: IDBDatabase): void { }); } +export type DbBundlesKey = string; + +/** + * A object representing a bundle loaded by the SDK. + */ +export class DbBundles { + /** Name of the IndexedDb object store. */ + static store = 'bundles'; + + static keyPath = ['bundleId']; + + constructor( + /** The ID of the loaded bundle. */ + public bundleId: string, + /** The create time of the loaded bundle. */ + public createTime: DbTimestamp, + /** The schema version of the loaded bundle. */ + public version: number + ) {} +} + +function createBundlesStore(db: IDBDatabase): void { + db.createObjectStore(DbBundles.store, { + keyPath: DbBundles.keyPath + }); +} + +export type DbNamedQueriesKey = string; + +/** + * A object representing a named query loaded by the SDK via a bundle. + */ +export class DbNamedQueries { + /** Name of the IndexedDb object store. */ + static store = 'namedQueries'; + + static keyPath = ['name']; + + constructor( + /** The name of the query. */ + public name: string, + /** The read time of the results saved in the bundle from the named query. */ + public readTime: DbTimestamp, + /** The query saved in the bundle. */ + public bundledQuery: BundledQuery + ) {} +} + +function createNamedQueriesStore(db: IDBDatabase): void { + db.createObjectStore(DbNamedQueries.store, { + keyPath: DbNamedQueries.keyPath + }); +} + // Visible for testing export const V1_STORES = [ DbMutationQueue.store, @@ -1111,9 +1174,11 @@ export const V8_STORES = [...V6_STORES, DbCollectionParent.store]; // V10 does not change the set of stores. +export const V11_STORES = [...V8_STORES, DbCollectionParent.store]; + /** * The list of all default IndexedDB stores used throughout the SDK. This is * used when creating transactions so that access across all stores is done * atomically. */ -export const ALL_STORES = V8_STORES; +export const ALL_STORES = V11_STORES; diff --git a/packages/firestore/src/protos/firestore/bundle.proto b/packages/firestore/src/protos/firestore/bundle.proto new file mode 100644 index 00000000000..ca19071e71f --- /dev/null +++ b/packages/firestore/src/protos/firestore/bundle.proto @@ -0,0 +1,118 @@ +// 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. +// + +// This file defines the format of Firestore bundle file/stream. It is not a part of the +// Firestore API, only a specification used by Server and Client SDK to write and read +// bundles. + +syntax = "proto3"; + +package firestore; + +import "google/firestore/v1/document.proto"; +import "google/firestore/v1/query.proto"; +import "google/protobuf/timestamp.proto"; + +option csharp_namespace = "Firestore.Proto"; +option go_package = "google.golang.org/genproto/firestore/proto;firestore"; +option java_multiple_files = true; +option java_outer_classname = "BundleProto"; +option java_package = "com.google.firestore.proto"; +option objc_class_prefix = "FSTPB"; +option php_namespace = "Firestore\\Proto"; + +// Describes a query saved in the bundle. +message BundledQuery { + // The parent resource name. + string parent = 1; + + // The query to run. + oneof query_type { + // A structured query. + google.firestore.v1.StructuredQuery structured_query = 2; + } + + // If the query is a limit query, should the limit be applied to the beginning or + // the end of results. + enum LimitType { + FIRST = 0; + LAST = 1; + } + LimitType limit_type = 3; +} + +// A Query associated with a name, created as part of the bundle file, and can be read +// by client SDKs once the bundle containing them is loaded. +message NamedQuery { + // Name of the query, such that client can use the name to load this query + // from bundle, and resume from when the query results are materialized + // into this bundle. + string name = 1; + + // The query saved in the bundle. + BundledQuery bundled_query = 2; + + // The read time of the query, when it is used to build the bundle. This is useful to + // resume the query from the bundle, once it is loaded by client SDKs. + google.protobuf.Timestamp read_time = 3; +} + +// Metadata describing a Firestore document saved in the bundle. +message BundledDocumentMetadata { + // The document key of a bundled document. + string name = 1; + + // The snapshot version of the document data bundled. + google.protobuf.Timestamp read_time = 2; + + // Whether the document exists. + bool exists = 3; +} + +// Metadata describing the bundle file/stream. +message BundleMetadata { + // The ID of the bundle. + string id = 1; + + // Time at which the documents snapshot is taken for this bundle. + google.protobuf.Timestamp create_time = 2; + + // The schema version of the bundle. + uint32 version = 3; + + // The number of documents in the bundle. + uint32 total_documents = 4; + + // The size of the bundle in bytes, excluding this `BundleMetadata`. + uint64 total_bytes = 5; +} + +// A Firestore bundle is a length-prefixed stream of JSON representations of +// `BundleElement`. +// Only one `BundleMetadata` is expected, and it should be the first element. +// The named queries follow after `metadata`. If a document exists when the +// bundle is built, `document_metadata` is immediately followed by the +// `document`, otherwise `document_metadata` will present by itself. +message BundleElement { + oneof element_type { + BundleMetadata metadata = 1; + + NamedQuery named_query = 2; + + BundledDocumentMetadata document_metadata = 3; + + google.firestore.v1.Document document = 4; + } +} diff --git a/packages/firestore/src/protos/firestore_bundle_proto.d.ts b/packages/firestore/src/protos/firestore_bundle_proto.d.ts new file mode 100644 index 00000000000..e98fabc7abd --- /dev/null +++ b/packages/firestore/src/protos/firestore_bundle_proto.d.ts @@ -0,0 +1,92 @@ +/** + * @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 api from './firestore_proto_api'; + +/** Properties of a BundledQuery. */ +export interface BundledQuery { + /** BundledQuery parent */ + parent?: string | null; + + /** BundledQuery structuredQuery */ + structuredQuery?: api.StructuredQuery | null; + + /** BundledQuery limitType */ + limitType?: BundledQuery.LimitType | null; +} + +export namespace BundledQuery { + /** LimitType enum. */ + type LimitType = 'FIRST' | 'LAST'; +} + +/** Properties of a NamedQuery. */ +export interface NamedQuery { + /** NamedQuery name */ + name?: string | null; + + /** NamedQuery bundledQuery */ + bundledQuery?: BundledQuery | null; + + /** NamedQuery readTime */ + readTime?: api.Timestamp | null; +} + +/** Properties of a BundledDocumentMetadata. */ +export interface BundledDocumentMetadata { + /** BundledDocumentMetadata name */ + name?: string | null; + + /** BundledDocumentMetadata readTime */ + readTime?: api.Timestamp | null; + + /** BundledDocumentMetadata exists */ + exists?: boolean | null; +} + +/** Properties of a BundleMetadata. */ +interface BundleMetadata { + /** BundleMetadata id */ + id?: string | null; + + /** BundleMetadata createTime */ + createTime?: api.Timestamp | null; + + /** BundleMetadata version */ + version?: number | null; + + /** BundleMetadata totalDocuments */ + totalDocuments?: number | null; + + /** BundleMetadata totalBytes */ + totalBytes?: number | null; +} + +/** Properties of a BundleElement. */ +interface BundleElement { + /** BundleElement metadata */ + metadata?: BundleMetadata | null; + + /** BundleElement namedQuery */ + namedQuery?: NamedQuery | null; + + /** BundleElement documentMetadata */ + documentMetadata?: BundledDocumentMetadata | null; + + /** BundleElement document */ + document?: api.Document | null; +} From b0c82994de413c8524a2bee6eebfa4ea7d8ec582 Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Thu, 4 Jun 2020 20:42:53 -0400 Subject: [PATCH 02/27] Implement a bundle reader for Web (#3097) Implement a bundle reader to read from a underlying readable stream lazily, and parse the content to proto objects. --- .../src/protos/firestore_proto_api.d.ts | 2 +- packages/firestore/src/util/bundle_reader.ts | 243 +++++++++++++ .../firestore/test/unit/util/bundle.test.ts | 342 ++++++++++++++++++ 3 files changed, 586 insertions(+), 1 deletion(-) create mode 100644 packages/firestore/src/util/bundle_reader.ts create mode 100644 packages/firestore/test/unit/util/bundle.test.ts diff --git a/packages/firestore/src/protos/firestore_proto_api.d.ts b/packages/firestore/src/protos/firestore_proto_api.d.ts index 3b3c8c48a5d..a85bc282bca 100644 --- a/packages/firestore/src/protos/firestore_proto_api.d.ts +++ b/packages/firestore/src/protos/firestore_proto_api.d.ts @@ -184,7 +184,7 @@ export declare namespace firestoreV1ApiClientInterfaces { interface Document { name?: string; fields?: ApiClientObjectMap; - createTime?: string; + createTime?: Timestamp; updateTime?: Timestamp; } interface DocumentChange { diff --git a/packages/firestore/src/util/bundle_reader.ts b/packages/firestore/src/util/bundle_reader.ts new file mode 100644 index 00000000000..c8e7435953b --- /dev/null +++ b/packages/firestore/src/util/bundle_reader.ts @@ -0,0 +1,243 @@ +/** + * @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 { + BundleElement, + BundleMetadata +} from '../protos/firestore_bundle_proto'; +import { Deferred } from './promise'; + +/** + * A complete element in the bundle stream, together with the byte length it + * occupies in the stream. + */ +export class SizedBundleElement { + constructor( + public readonly payload: BundleElement, + // How many bytes this element takes to store in the bundle. + public readonly byteLength: number + ) {} + + isBundleMetadata(): boolean { + return 'metadata' in this.payload; + } +} + +/** + * Create a `ReadableStream` from a underlying buffer. + * + * @param data: Underlying buffer. + * @param bytesPerRead: How many bytes to read from the underlying buffer from + * each read through the stream. + */ +export function toReadableStream( + data: Uint8Array | ArrayBuffer, + bytesPerRead = 10240 +): ReadableStream { + let readFrom = 0; + return new ReadableStream({ + start(controller) {}, + async pull(controller): Promise { + controller.enqueue(data.slice(readFrom, readFrom + bytesPerRead)); + readFrom += bytesPerRead; + if (readFrom >= data.byteLength) { + controller.close(); + } + } + }); +} + +/** + * A class representing a bundle. + * + * Takes a bundle stream or buffer, and presents abstractions to read bundled + * elements out of the underlying content. + */ +export class BundleReader { + /** Cached bundle metadata. */ + private metadata: Deferred = new Deferred(); + /** The reader instance of the given ReadableStream. */ + private reader: ReadableStreamDefaultReader; + /** + * Internal buffer to hold bundle content, accumulating incomplete element + * content. + */ + private buffer: Uint8Array = new Uint8Array(); + /** The decoder used to parse binary data into strings. */ + private textDecoder = new TextDecoder('utf-8'); + + constructor( + private bundleStream: + | ReadableStream + | Uint8Array + | ArrayBuffer + ) { + if ( + bundleStream instanceof Uint8Array || + bundleStream instanceof ArrayBuffer + ) { + this.bundleStream = toReadableStream(bundleStream); + } + this.reader = (this.bundleStream as ReadableStream).getReader(); + + // Read the metadata (which is the first element). + this.nextElementImpl().then( + element => { + if (element && element.isBundleMetadata()) { + this.metadata.resolve(element.payload.metadata!); + } else { + this.metadata.reject( + new Error(`The first element of the bundle is not a metadata, it is + ${JSON.stringify(element?.payload)}`) + ); + } + }, + error => this.metadata.reject(error) + ); + } + + /** + * Returns the metadata of the bundle. + */ + async getMetadata(): Promise { + return this.metadata.promise; + } + + /** + * Returns the next BundleElement (together with its byte size in the bundle) + * that has not been read from underlying ReadableStream. Returns null if we + * have reached the end of the stream. + */ + async nextElement(): Promise { + // Makes sure metadata is read before proceeding. + await this.getMetadata(); + return this.nextElementImpl(); + } + + /** + * Reads from the head of internal buffer, and pulling more data from + * underlying stream if a complete element cannot be found, until an + * element(including the prefixed length and the JSON string) is found. + * + * Once a complete element is read, it is dropped from internal buffer. + * + * Returns either the bundled element, or null if we have reached the end of + * the stream. + */ + private async nextElementImpl(): Promise { + const lengthBuffer = await this.readLength(); + if (lengthBuffer === null) { + return null; + } + + const lengthString = this.textDecoder.decode(lengthBuffer); + const length = Number(lengthString); + if (isNaN(length)) { + this.raiseError(`length string (${lengthString}) is not valid number`); + } + + const jsonString = await this.readJsonString(length); + + return new SizedBundleElement( + JSON.parse(jsonString), + lengthBuffer.length + length + ); + } + + /** First index of '{' from the underlying buffer. */ + private indexOfOpenBracket(): number { + return this.buffer.findIndex(v => v === '{'.charCodeAt(0)); + } + + /** + * Reads from the beginning of the internal buffer, until the first '{', and + * return the content. + * + * If reached end of the stream, returns a null. + */ + private async readLength(): Promise { + while (this.indexOfOpenBracket() < 0) { + const done = await this.pullMoreDataToBuffer(); + if (done) { + break; + } + } + + // Broke out of the loop because underlying stream is closed, and there + // happens to be no more data to process. + if (this.buffer.length === 0) { + return null; + } + + const position = this.indexOfOpenBracket(); + // Broke out of the loop because underlying stream is closed, but still + // cannot find an open bracket. + if (position < 0) { + this.raiseError( + 'Reached the end of bundle when a length string is expected.' + ); + } + + const result = this.buffer.slice(0, position); + // Update the internal buffer to drop the read length. + this.buffer = this.buffer.slice(position); + return result; + } + + /** + * Reads from a specified position from the internal buffer, for a specified + * number of bytes, pulling more data from the underlying stream if needed. + * + * Returns a string decoded from the read bytes. + */ + private async readJsonString(length: number): Promise { + while (this.buffer.length < length) { + const done = await this.pullMoreDataToBuffer(); + if (done) { + this.raiseError('Reached the end of bundle when more is expected.'); + } + } + + const result = this.textDecoder.decode(this.buffer.slice(0, length)); + // Update the internal buffer to drop the read json string. + this.buffer = this.buffer.slice(length); + return result; + } + + private raiseError(message: string): void { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.reader.cancel('Invalid bundle format.'); + throw new Error(message); + } + + /** + * Pulls more data from underlying stream to internal buffer. + * Returns a boolean indicating whether the stream is finished. + */ + private async pullMoreDataToBuffer(): Promise { + const result = await this.reader.read(); + if (!result.done) { + const newBuffer = new Uint8Array( + this.buffer.length + result.value.length + ); + newBuffer.set(this.buffer); + newBuffer.set(result.value, this.buffer.length); + this.buffer = newBuffer; + } + return result.done; + } +} diff --git a/packages/firestore/test/unit/util/bundle.test.ts b/packages/firestore/test/unit/util/bundle.test.ts new file mode 100644 index 00000000000..981d3ad7289 --- /dev/null +++ b/packages/firestore/test/unit/util/bundle.test.ts @@ -0,0 +1,342 @@ +/** + * @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 { expect } from 'chai'; +import { + BundleReader, + SizedBundleElement, + toReadableStream +} from '../../../src/util/bundle_reader'; +import { isNode } from '../../util/test_platform'; +import { BundleElement } from '../../../src/protos/firestore_bundle_proto'; + +function readableStreamFromString( + content: string, + bytesPerRead: number +): ReadableStream { + return toReadableStream(new TextEncoder().encode(content), bytesPerRead); +} + +function lengthPrefixedString(o: {}): string { + const str = JSON.stringify(o); + const l = new TextEncoder().encode(str).byteLength; + return `${l}${str}`; +} + +// eslint-disable-next-line no-restricted-properties +(isNode() ? describe.skip : describe)('readableStreamFromString()', () => { + it('returns stepping readable stream', async () => { + const encoder = new TextEncoder(); + const s = readableStreamFromString('0123456789', 4); + const r = s.getReader(); + + let result = await r.read(); + expect(result.value).to.deep.equal(encoder.encode('0123')); + expect(result.done).to.be.false; + + result = await r.read(); + expect(result.value).to.deep.equal(encoder.encode('4567')); + expect(result.done).to.be.false; + + result = await r.read(); + expect(result.value).to.deep.equal(encoder.encode('89')); + expect(result.done).to.be.false; + + result = await r.read(); + expect(result.value).to.be.undefined; + expect(result.done).to.be.true; + }); +}); + +// eslint-disable-next-line no-restricted-properties +(isNode() ? describe.skip : describe)('Bundle ', () => { + genericBundleReadingTests(1); + genericBundleReadingTests(4); + genericBundleReadingTests(64); + genericBundleReadingTests(1024); +}); + +function genericBundleReadingTests(bytesPerRead: number): void { + const encoder = new TextEncoder(); + // Setting up test data. + const meta: BundleElement = { + metadata: { + id: 'test-bundle', + createTime: { seconds: 1577836805, nanos: 6 }, + version: 1, + totalDocuments: 1, + totalBytes: 416 + } + }; + const metaString = lengthPrefixedString(meta); + + const doc1Meta: BundleElement = { + documentMetadata: { + name: + 'projects/test-project/databases/(default)/documents/collectionId/doc1', + readTime: { seconds: 5, nanos: 6 }, + exists: true + } + }; + const doc1MetaString = lengthPrefixedString(doc1Meta); + const doc1: BundleElement = { + document: { + name: + 'projects/test-project/databases/(default)/documents/collectionId/doc1', + createTime: { seconds: 1, nanos: 2000000 }, + updateTime: { seconds: 3, nanos: 4000 }, + fields: { foo: { stringValue: 'value' }, bar: { integerValue: -42 } } + } + }; + const doc1String = lengthPrefixedString(doc1); + + const doc2Meta: BundleElement = { + documentMetadata: { + name: + 'projects/test-project/databases/(default)/documents/collectionId/doc2', + readTime: { seconds: 5, nanos: 6 }, + exists: true + } + }; + const doc2MetaString = lengthPrefixedString(doc2Meta); + const doc2: BundleElement = { + document: { + name: + 'projects/test-project/databases/(default)/documents/collectionId/doc2', + createTime: { seconds: 1, nanos: 2000000 }, + updateTime: { seconds: 3, nanos: 4000 }, + fields: { foo: { stringValue: 'value1' }, bar: { integerValue: 42 } } + } + }; + const doc2String = lengthPrefixedString(doc2); + + const noDocMeta: BundleElement = { + documentMetadata: { + name: + 'projects/test-project/databases/(default)/documents/collectionId/nodoc', + readTime: { seconds: 5, nanos: 6 }, + exists: false + } + }; + const noDocMetaString = lengthPrefixedString(noDocMeta); + + const limitQuery: BundleElement = { + namedQuery: { + name: 'limitQuery', + bundledQuery: { + parent: 'projects/fireeats-97d5e/databases/(default)/documents', + structuredQuery: { + from: [{ collectionId: 'node_3.7.5_7Li7XoCjutvNxwD0tpo9' }], + orderBy: [{ field: { fieldPath: 'sort' }, direction: 'DESCENDING' }], + limit: { 'value': 1 } + }, + limitType: 'FIRST' + }, + readTime: { 'seconds': 1590011379, 'nanos': 191164000 } + } + }; + const limitQueryString = lengthPrefixedString(limitQuery); + const limitToLastQuery: BundleElement = { + namedQuery: { + name: 'limitToLastQuery', + bundledQuery: { + parent: 'projects/fireeats-97d5e/databases/(default)/documents', + structuredQuery: { + from: [{ collectionId: 'node_3.7.5_7Li7XoCjutvNxwD0tpo9' }], + orderBy: [{ field: { fieldPath: 'sort' }, direction: 'ASCENDING' }], + limit: { 'value': 1 } + }, + limitType: 'LAST' + }, + readTime: { 'seconds': 1590011379, 'nanos': 543063000 } + } + }; + const limitToLastQueryString = lengthPrefixedString(limitToLastQuery); + + async function getAllElements( + bundle: BundleReader + ): Promise { + const result: SizedBundleElement[] = []; + while (true) { + const sizedElement = await bundle.nextElement(); + if (sizedElement === null) { + break; + } + if (!sizedElement.isBundleMetadata()) { + result.push(sizedElement); + } + } + + return Promise.resolve(result); + } + + function verifySizedElement( + element: SizedBundleElement, + payload: unknown, + payloadString: string + ): void { + expect(element.payload).to.deep.equal(payload); + expect(element.byteLength).to.equal( + encoder.encode(payloadString).byteLength + ); + } + + async function generateBundleAndParse( + bundleString: string, + bytesPerRead: number, + validMeta = false + ): Promise { + const bundleStream = readableStreamFromString(bundleString, bytesPerRead); + const bundle = new BundleReader(bundleStream); + + if (!validMeta) { + await expect(await bundle.getMetadata()).should.be.rejected; + } else { + expect(await bundle.getMetadata()).to.deep.equal(meta.metadata); + } + + await getAllElements(bundle); + } + + it('reads with query and doc with bytesPerRead ' + bytesPerRead, async () => { + const bundleStream = readableStreamFromString( + metaString + + limitQueryString + + limitToLastQueryString + + doc1MetaString + + doc1String, + bytesPerRead + ); + const bundle = new BundleReader(bundleStream); + + expect(await bundle.getMetadata()).to.deep.equal(meta.metadata); + + const actual = await getAllElements(bundle); + expect(actual.length).to.equal(4); + verifySizedElement(actual[0], limitQuery, limitQueryString); + verifySizedElement(actual[1], limitToLastQuery, limitToLastQueryString); + verifySizedElement(actual[2], doc1Meta, doc1MetaString); + verifySizedElement(actual[3], doc1, doc1String); + }); + + it( + 'reads with unexpected orders with bytesPerRead ' + bytesPerRead, + async () => { + const bundleStream = readableStreamFromString( + metaString + + doc1MetaString + + doc1String + + limitQueryString + + doc2MetaString + + doc2String, + bytesPerRead + ); + const bundle = new BundleReader(bundleStream); + + const actual = await getAllElements(bundle); + expect(actual.length).to.equal(5); + verifySizedElement(actual[0], doc1Meta, doc1MetaString); + verifySizedElement(actual[1], doc1, doc1String); + verifySizedElement(actual[2], limitQuery, limitQueryString); + verifySizedElement(actual[3], doc2Meta, doc2MetaString); + verifySizedElement(actual[4], doc2, doc2String); + + // Reading metadata after other elements should also work. + expect(await bundle.getMetadata()).to.deep.equal(meta.metadata); + } + ); + + it( + 'reads without named query with bytesPerRead ' + bytesPerRead, + async () => { + const bundleStream = readableStreamFromString( + metaString + doc1MetaString + doc1String, + bytesPerRead + ); + const bundle = new BundleReader(bundleStream); + + expect(await bundle.getMetadata()).to.deep.equal(meta.metadata); + + const actual = await getAllElements(bundle); + expect(actual.length).to.equal(2); + verifySizedElement(actual[0], doc1Meta, doc1MetaString); + verifySizedElement(actual[1], doc1, doc1String); + } + ); + + it('reads with deleted doc with bytesPerRead ' + bytesPerRead, async () => { + const bundleStream = readableStreamFromString( + metaString + noDocMetaString + doc1MetaString + doc1String, + bytesPerRead + ); + const bundle = new BundleReader(bundleStream); + + expect(await bundle.getMetadata()).to.deep.equal(meta.metadata); + + const actual = await getAllElements(bundle); + expect(actual.length).to.equal(3); + verifySizedElement(actual[0], noDocMeta, noDocMetaString); + verifySizedElement(actual[1], doc1Meta, doc1MetaString); + verifySizedElement(actual[2], doc1, doc1String); + }); + + it( + 'reads without documents or query with bytesPerRead ' + bytesPerRead, + async () => { + const bundleStream = readableStreamFromString(metaString, bytesPerRead); + const bundle = new BundleReader(bundleStream); + + expect(await bundle.getMetadata()).to.deep.equal(meta.metadata); + + const actual = await getAllElements(bundle); + expect(actual.length).to.equal(0); + } + ); + + it( + 'throws with ill-formatted bundle with bytesPerRead ' + bytesPerRead, + async () => { + await expect( + generateBundleAndParse('metadata: "no length prefix"', bytesPerRead) + ).to.be.rejectedWith( + 'Reached the end of bundle when a length string is expected.' + ); + + await expect( + generateBundleAndParse('{metadata: "no length prefix"}', bytesPerRead) + ).to.be.rejectedWith('Unexpected end of JSON input'); + + await expect( + generateBundleAndParse( + metaString + 'invalid-string', + bytesPerRead, + true + ) + ).to.be.rejectedWith( + 'Reached the end of bundle when a length string is expected.' + ); + + await expect( + generateBundleAndParse('1' + metaString, bytesPerRead) + ).to.be.rejectedWith('Reached the end of bundle when more is expected.'); + + // First element is not BundleMetadata. + await expect( + generateBundleAndParse(doc1MetaString + doc1String, bytesPerRead) + ).to.be.rejectedWith('The first element of the bundle is not a metadata'); + } + ); +} From 2f179bc2d42f7cc990c6ffebd7d6a47bf8d21c07 Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Wed, 10 Jun 2020 14:34:11 -0400 Subject: [PATCH 03/27] Enable bundle reader for Node. (#3167) --- packages/firestore/src/platform/platform.ts | 49 +++++++++++++- .../src/platform_browser/browser_platform.ts | 24 ++++++- .../src/platform_node/node_platform.ts | 21 +++++- packages/firestore/src/util/bundle_reader.ts | 62 +++++++----------- .../test/unit/platform/platform.test.ts | 29 ++++++++- .../firestore/test/unit/util/bundle.test.ts | 65 +++++++++---------- packages/firestore/test/util/test_platform.ts | 8 +++ 7 files changed, 182 insertions(+), 76 deletions(-) diff --git a/packages/firestore/src/platform/platform.ts b/packages/firestore/src/platform/platform.ts index 1ab582ea05f..7a681b74684 100644 --- a/packages/firestore/src/platform/platform.ts +++ b/packages/firestore/src/platform/platform.ts @@ -18,8 +18,9 @@ import { DatabaseId, DatabaseInfo } from '../core/database_info'; import { Connection } from '../remote/connection'; import { JsonProtoSerializer } from '../remote/serializer'; -import { fail } from '../util/assert'; +import { debugAssert, fail } from '../util/assert'; import { ConnectivityMonitor } from './../remote/connectivity_monitor'; +import { BundleSource } from '../util/bundle_reader'; /** * Provides a common interface to load anything platform dependent, e.g. @@ -50,6 +51,18 @@ export interface Platform { */ randomBytes(nBytes: number): Uint8Array; + /** + * Builds a `ByteStreamReader` from a data source. + * @param source The data source to use. + * @param bytesPerRead How many bytes each `read()` from the returned reader + * will read. It is ignored if the passed in source does not provide + * such control(example: ReadableStream). + */ + toByteStreamReader( + source: BundleSource, + bytesPerRead: number + ): ReadableStreamReader; + /** The Platform's 'window' implementation or null if not available. */ readonly window: Window | null; @@ -60,6 +73,40 @@ export interface Platform { readonly base64Available: boolean; } +/** + * Builds a `ByteStreamReader` from a UInt8Array. + * @param source The data source to use. + * @param bytesPerRead How many bytes each `read()` from the returned reader + * will read. + */ +export function toByteStreamReader( + source: Uint8Array, + bytesPerRead: number +): ReadableStreamReader { + debugAssert( + bytesPerRead > 0, + `toByteStreamReader expects positive bytesPerRead, but got ${bytesPerRead}` + ); + let readFrom = 0; + const reader: ReadableStreamReader = { + async read(): Promise> { + if (readFrom < source.byteLength) { + const result = { + value: source.slice(readFrom, readFrom + bytesPerRead), + done: false + }; + readFrom += bytesPerRead; + return result; + } + + return { value: undefined, done: true }; + }, + async cancel(): Promise {}, + releaseLock() {} + }; + return reader; +} + /** * Provides singleton helpers where setup code can inject a platform at runtime. * setPlatform needs to be set before Firestore is used and must be set exactly diff --git a/packages/firestore/src/platform_browser/browser_platform.ts b/packages/firestore/src/platform_browser/browser_platform.ts index 38133db4df5..bb8fdd97fc0 100644 --- a/packages/firestore/src/platform_browser/browser_platform.ts +++ b/packages/firestore/src/platform_browser/browser_platform.ts @@ -16,7 +16,7 @@ */ import { DatabaseId, DatabaseInfo } from '../core/database_info'; -import { Platform } from '../platform/platform'; +import { Platform, toByteStreamReader } from '../platform/platform'; import { Connection } from '../remote/connection'; import { JsonProtoSerializer } from '../remote/serializer'; import { ConnectivityMonitor } from './../remote/connectivity_monitor'; @@ -25,6 +25,7 @@ import { NoopConnectivityMonitor } from '../remote/connectivity_monitor_noop'; import { BrowserConnectivityMonitor } from './browser_connectivity_monitor'; import { WebChannelConnection } from './webchannel_connection'; import { debugAssert } from '../util/assert'; +import { BundleSource } from '../util/bundle_reader'; // Implements the Platform API for browsers and some browser-like environments // (including ReactNative). @@ -93,4 +94,25 @@ export class BrowserPlatform implements Platform { } return bytes; } + + /** + * On web, a `ReadableStream` is wrapped around by a `ByteStreamReader`. + */ + toByteStreamReader( + source: BundleSource, + bytesPerRead: number + ): ReadableStreamReader { + if (source instanceof Uint8Array) { + return toByteStreamReader(source, bytesPerRead); + } + if (source instanceof ArrayBuffer) { + return toByteStreamReader(new Uint8Array(source), bytesPerRead); + } + if (source instanceof ReadableStream) { + return source.getReader(); + } + throw new Error( + 'Source of `toByteStreamReader` has to be a ArrayBuffer or ReadableStream' + ); + } } diff --git a/packages/firestore/src/platform_node/node_platform.ts b/packages/firestore/src/platform_node/node_platform.ts index 2fe74f7d68b..42b38cf0881 100644 --- a/packages/firestore/src/platform_node/node_platform.ts +++ b/packages/firestore/src/platform_node/node_platform.ts @@ -19,7 +19,7 @@ import { randomBytes } from 'crypto'; import { inspect } from 'util'; import { DatabaseId, DatabaseInfo } from '../core/database_info'; -import { Platform } from '../platform/platform'; +import { Platform, toByteStreamReader } from '../platform/platform'; import { Connection } from '../remote/connection'; import { JsonProtoSerializer } from '../remote/serializer'; import { Code, FirestoreError } from '../util/error'; @@ -29,6 +29,7 @@ import { NoopConnectivityMonitor } from './../remote/connectivity_monitor_noop'; import { GrpcConnection } from './grpc_connection'; import { loadProtos } from './load_protos'; import { debugAssert } from '../util/assert'; +import { invalidClassError } from '../util/input_validation'; export class NodePlatform implements Platform { readonly base64Available = true; @@ -83,4 +84,22 @@ export class NodePlatform implements Platform { return randomBytes(nBytes); } + + /** + * On Node, only supported data source is a `Uint8Array` for now. + */ + toByteStreamReader( + source: Uint8Array, + bytesPerRead: number + ): ReadableStreamReader { + if (!(source instanceof Uint8Array)) { + throw invalidClassError( + 'NodePlatform.toByteStreamReader', + 'Uint8Array', + 1, + source + ); + } + return toByteStreamReader(source, bytesPerRead); + } } diff --git a/packages/firestore/src/util/bundle_reader.ts b/packages/firestore/src/util/bundle_reader.ts index c8e7435953b..2629dfad326 100644 --- a/packages/firestore/src/util/bundle_reader.ts +++ b/packages/firestore/src/util/bundle_reader.ts @@ -20,6 +20,8 @@ import { BundleMetadata } from '../protos/firestore_bundle_proto'; import { Deferred } from './promise'; +import { PlatformSupport } from '../platform/platform'; +import { debugAssert } from './assert'; /** * A complete element in the bundle stream, together with the byte length it @@ -37,29 +39,18 @@ export class SizedBundleElement { } } +export type BundleSource = + | ReadableStream + | ArrayBuffer + | Uint8Array; + /** - * Create a `ReadableStream` from a underlying buffer. + * When applicable, how many bytes to read from the underlying data source + * each time. * - * @param data: Underlying buffer. - * @param bytesPerRead: How many bytes to read from the underlying buffer from - * each read through the stream. + * Not applicable for ReadableStreams. */ -export function toReadableStream( - data: Uint8Array | ArrayBuffer, - bytesPerRead = 10240 -): ReadableStream { - let readFrom = 0; - return new ReadableStream({ - start(controller) {}, - async pull(controller): Promise { - controller.enqueue(data.slice(readFrom, readFrom + bytesPerRead)); - readFrom += bytesPerRead; - if (readFrom >= data.byteLength) { - controller.close(); - } - } - }); -} +const BYTES_PER_READ = 10240; /** * A class representing a bundle. @@ -70,8 +61,6 @@ export function toReadableStream( export class BundleReader { /** Cached bundle metadata. */ private metadata: Deferred = new Deferred(); - /** The reader instance of the given ReadableStream. */ - private reader: ReadableStreamDefaultReader; /** * Internal buffer to hold bundle content, accumulating incomplete element * content. @@ -80,20 +69,16 @@ export class BundleReader { /** The decoder used to parse binary data into strings. */ private textDecoder = new TextDecoder('utf-8'); + static fromBundleSource(source: BundleSource): BundleReader { + return new BundleReader( + PlatformSupport.getPlatform().toByteStreamReader(source, BYTES_PER_READ) + ); + } + constructor( - private bundleStream: - | ReadableStream - | Uint8Array - | ArrayBuffer + /** The reader to read from underlying binary bundle data source. */ + private reader: ReadableStreamReader ) { - if ( - bundleStream instanceof Uint8Array || - bundleStream instanceof ArrayBuffer - ) { - this.bundleStream = toReadableStream(bundleStream); - } - this.reader = (this.bundleStream as ReadableStream).getReader(); - // Read the metadata (which is the first element). this.nextElementImpl().then( element => { @@ -220,8 +205,8 @@ export class BundleReader { private raiseError(message: string): void { // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.reader.cancel('Invalid bundle format.'); - throw new Error(message); + this.reader.cancel(); + throw new Error(`Invalid bundle format: ${message}`); } /** @@ -231,11 +216,12 @@ export class BundleReader { private async pullMoreDataToBuffer(): Promise { const result = await this.reader.read(); if (!result.done) { + debugAssert(!!result.value, 'Read undefined when "done" is false.'); const newBuffer = new Uint8Array( - this.buffer.length + result.value.length + this.buffer.length + result.value!.length ); newBuffer.set(this.buffer); - newBuffer.set(result.value, this.buffer.length); + newBuffer.set(result.value!, this.buffer.length); this.buffer = newBuffer; } return result.done; diff --git a/packages/firestore/test/unit/platform/platform.test.ts b/packages/firestore/test/unit/platform/platform.test.ts index 289b0edee0d..3abaf2902eb 100644 --- a/packages/firestore/test/unit/platform/platform.test.ts +++ b/packages/firestore/test/unit/platform/platform.test.ts @@ -16,10 +16,37 @@ */ import { expect } from 'chai'; -import { PlatformSupport } from '../../../src/platform/platform'; +import { + PlatformSupport, + toByteStreamReader +} from '../../../src/platform/platform'; describe('Platform', () => { it('can load the platform at runtime', () => { expect(PlatformSupport.getPlatform()).to.exist; }); + + it('toByteStreamReader() steps underlying data', async () => { + const encoder = new TextEncoder(); + const r = toByteStreamReader( + encoder.encode('0123456789'), + /* bytesPerRead */ 4 + ); + + let result = await r.read(); + expect(result.value).to.deep.equal(encoder.encode('0123')); + expect(result.done).to.be.false; + + result = await r.read(); + expect(result.value).to.deep.equal(encoder.encode('4567')); + expect(result.done).to.be.false; + + result = await r.read(); + expect(result.value).to.deep.equal(encoder.encode('89')); + expect(result.done).to.be.false; + + result = await r.read(); + expect(result.value).to.be.undefined; + expect(result.done).to.be.true; + }); }); diff --git a/packages/firestore/test/unit/util/bundle.test.ts b/packages/firestore/test/unit/util/bundle.test.ts index 981d3ad7289..088fc8acecd 100644 --- a/packages/firestore/test/unit/util/bundle.test.ts +++ b/packages/firestore/test/unit/util/bundle.test.ts @@ -17,17 +17,24 @@ import { expect } from 'chai'; import { BundleReader, - SizedBundleElement, - toReadableStream + SizedBundleElement } from '../../../src/util/bundle_reader'; -import { isNode } from '../../util/test_platform'; import { BundleElement } from '../../../src/protos/firestore_bundle_proto'; +import { PlatformSupport } from '../../../src/platform/platform'; -function readableStreamFromString( +/** + * Create a `ReadableStream` from a string. + * + * @param content: Bundle in string. + * @param bytesPerRead: How many bytes to read from the underlying buffer from + * each read through the stream. + */ +export function byteStreamReaderFromString( content: string, bytesPerRead: number -): ReadableStream { - return toReadableStream(new TextEncoder().encode(content), bytesPerRead); +): ReadableStreamReader { + const data = new TextEncoder().encode(content); + return PlatformSupport.getPlatform().toByteStreamReader(data, bytesPerRead); } function lengthPrefixedString(o: {}): string { @@ -36,12 +43,11 @@ function lengthPrefixedString(o: {}): string { return `${l}${str}`; } -// eslint-disable-next-line no-restricted-properties -(isNode() ? describe.skip : describe)('readableStreamFromString()', () => { - it('returns stepping readable stream', async () => { +// Testing readableStreamFromString() is working as expected. +describe('byteStreamReaderFromString()', () => { + it('returns a reader stepping readable stream', async () => { const encoder = new TextEncoder(); - const s = readableStreamFromString('0123456789', 4); - const r = s.getReader(); + const r = byteStreamReaderFromString('0123456789', 4); let result = await r.read(); expect(result.value).to.deep.equal(encoder.encode('0123')); @@ -61,8 +67,7 @@ function lengthPrefixedString(o: {}): string { }); }); -// eslint-disable-next-line no-restricted-properties -(isNode() ? describe.skip : describe)('Bundle ', () => { +describe('Bundle ', () => { genericBundleReadingTests(1); genericBundleReadingTests(4); genericBundleReadingTests(64); @@ -70,6 +75,10 @@ function lengthPrefixedString(o: {}): string { }); function genericBundleReadingTests(bytesPerRead: number): void { + function bundleFromString(s: string): BundleReader { + return new BundleReader(byteStreamReaderFromString(s, bytesPerRead)); + } + const encoder = new TextEncoder(); // Setting up test data. const meta: BundleElement = { @@ -199,8 +208,7 @@ function genericBundleReadingTests(bytesPerRead: number): void { bytesPerRead: number, validMeta = false ): Promise { - const bundleStream = readableStreamFromString(bundleString, bytesPerRead); - const bundle = new BundleReader(bundleStream); + const bundle = bundleFromString(bundleString); if (!validMeta) { await expect(await bundle.getMetadata()).should.be.rejected; @@ -212,15 +220,13 @@ function genericBundleReadingTests(bytesPerRead: number): void { } it('reads with query and doc with bytesPerRead ' + bytesPerRead, async () => { - const bundleStream = readableStreamFromString( + const bundle = bundleFromString( metaString + limitQueryString + limitToLastQueryString + doc1MetaString + - doc1String, - bytesPerRead + doc1String ); - const bundle = new BundleReader(bundleStream); expect(await bundle.getMetadata()).to.deep.equal(meta.metadata); @@ -235,16 +241,14 @@ function genericBundleReadingTests(bytesPerRead: number): void { it( 'reads with unexpected orders with bytesPerRead ' + bytesPerRead, async () => { - const bundleStream = readableStreamFromString( + const bundle = bundleFromString( metaString + doc1MetaString + doc1String + limitQueryString + doc2MetaString + - doc2String, - bytesPerRead + doc2String ); - const bundle = new BundleReader(bundleStream); const actual = await getAllElements(bundle); expect(actual.length).to.equal(5); @@ -262,11 +266,7 @@ function genericBundleReadingTests(bytesPerRead: number): void { it( 'reads without named query with bytesPerRead ' + bytesPerRead, async () => { - const bundleStream = readableStreamFromString( - metaString + doc1MetaString + doc1String, - bytesPerRead - ); - const bundle = new BundleReader(bundleStream); + const bundle = bundleFromString(metaString + doc1MetaString + doc1String); expect(await bundle.getMetadata()).to.deep.equal(meta.metadata); @@ -278,11 +278,9 @@ function genericBundleReadingTests(bytesPerRead: number): void { ); it('reads with deleted doc with bytesPerRead ' + bytesPerRead, async () => { - const bundleStream = readableStreamFromString( - metaString + noDocMetaString + doc1MetaString + doc1String, - bytesPerRead + const bundle = bundleFromString( + metaString + noDocMetaString + doc1MetaString + doc1String ); - const bundle = new BundleReader(bundleStream); expect(await bundle.getMetadata()).to.deep.equal(meta.metadata); @@ -296,8 +294,7 @@ function genericBundleReadingTests(bytesPerRead: number): void { it( 'reads without documents or query with bytesPerRead ' + bytesPerRead, async () => { - const bundleStream = readableStreamFromString(metaString, bytesPerRead); - const bundle = new BundleReader(bundleStream); + const bundle = bundleFromString(metaString); expect(await bundle.getMetadata()).to.deep.equal(meta.metadata); diff --git a/packages/firestore/test/util/test_platform.ts b/packages/firestore/test/util/test_platform.ts index cd9d458f63d..d1f959dcd4f 100644 --- a/packages/firestore/test/util/test_platform.ts +++ b/packages/firestore/test/util/test_platform.ts @@ -22,6 +22,7 @@ import { JsonProtoSerializer } from '../../src/remote/serializer'; import { debugAssert, fail } from '../../src/util/assert'; import { ConnectivityMonitor } from './../../src/remote/connectivity_monitor'; import { NoopConnectivityMonitor } from './../../src/remote/connectivity_monitor_noop'; +import { BundleSource } from '../../src/util/bundle_reader'; /* eslint-disable no-restricted-globals */ @@ -270,6 +271,13 @@ export class TestPlatform implements Platform { randomBytes(nBytes: number): Uint8Array { return this.basePlatform.randomBytes(nBytes); } + + toByteStreamReader( + source: BundleSource, + bytesPerRead: number + ): ReadableStreamReader { + return this.basePlatform.toByteStreamReader(source, bytesPerRead); + } } /** Returns true if we are running under Node. */ From d3f0ab44754ef0cfb1529de1fc7e7693a041bb68 Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Sat, 13 Jun 2020 22:39:47 -0400 Subject: [PATCH 04/27] Implement BundleCache for IDB and memory. (#3170) --- packages/firestore/src/core/bundle.ts | 42 ++++ .../firestore/src/core/component_provider.ts | 3 +- packages/firestore/src/local/bundle_cache.ts | 61 +++++ .../src/local/indexeddb_bundle_cache.ts | 111 ++++++++++ .../src/local/indexeddb_persistence.ts | 11 + .../firestore/src/local/indexeddb_schema.ts | 18 +- .../firestore/src/local/local_serializer.ts | 107 ++++++++- .../src/local/memory_bundle_cache.ts | 70 ++++++ .../firestore/src/local/memory_persistence.ts | 14 +- packages/firestore/src/local/persistence.ts | 10 + packages/firestore/src/remote/serializer.ts | 8 +- .../test/unit/local/bundle_cache.test.ts | 209 ++++++++++++++++++ .../unit/local/persistence_test_helpers.ts | 9 +- .../test/unit/local/test_bundle_cache.ts | 73 ++++++ .../test/unit/specs/spec_test_components.ts | 4 +- 15 files changed, 730 insertions(+), 20 deletions(-) create mode 100644 packages/firestore/src/core/bundle.ts create mode 100644 packages/firestore/src/local/bundle_cache.ts create mode 100644 packages/firestore/src/local/indexeddb_bundle_cache.ts create mode 100644 packages/firestore/src/local/memory_bundle_cache.ts create mode 100644 packages/firestore/test/unit/local/bundle_cache.test.ts create mode 100644 packages/firestore/test/unit/local/test_bundle_cache.ts diff --git a/packages/firestore/src/core/bundle.ts b/packages/firestore/src/core/bundle.ts new file mode 100644 index 00000000000..464b40e62ef --- /dev/null +++ b/packages/firestore/src/core/bundle.ts @@ -0,0 +1,42 @@ +/** + * @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'; +import { SnapshotVersion } from './snapshot_version'; + +/** + * Represents a Firestore bundle saved by the SDK in its local storage. + */ +export interface Bundle { + readonly id: string; + readonly version: number; + /** + * Set to the snapshot version of the bundle if created by the Server SDKs. + * Otherwise set to SnapshotVersion.MIN. + */ + readonly createTime: SnapshotVersion; +} + +/** + * Represents a Query saved by the SDK in its local storage. + */ +export interface NamedQuery { + readonly name: string; + readonly query: Query; + /** The time at which the results for this query were read. */ + readonly readTime: SnapshotVersion; +} diff --git a/packages/firestore/src/core/component_provider.ts b/packages/firestore/src/core/component_provider.ts index 2c34723ecef..38ba5d2c763 100644 --- a/packages/firestore/src/core/component_provider.ts +++ b/packages/firestore/src/core/component_provider.ts @@ -137,7 +137,8 @@ export class MemoryComponentProvider implements ComponentProvider { !cfg.persistenceSettings.durable, 'Can only start memory persistence' ); - return new MemoryPersistence(MemoryEagerDelegate.factory); + const serializer = cfg.platform.newSerializer(cfg.databaseInfo.databaseId); + return new MemoryPersistence(MemoryEagerDelegate.factory, serializer); } createRemoteStore(cfg: ComponentConfiguration): RemoteStore { diff --git a/packages/firestore/src/local/bundle_cache.ts b/packages/firestore/src/local/bundle_cache.ts new file mode 100644 index 00000000000..acf2a94f554 --- /dev/null +++ b/packages/firestore/src/local/bundle_cache.ts @@ -0,0 +1,61 @@ +/** + * @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 { PersistenceTransaction } from './persistence'; +import { PersistencePromise } from './persistence_promise'; +import * as bundleProto from '../protos/firestore_bundle_proto'; +import { Bundle, NamedQuery } from '../core/bundle'; + +/** + * Provides interfaces to save and read Firestore bundles. + */ +export interface BundleCache { + /** + * Gets a saved `Bundle` for a given `bundleId`, returns undefined if + * no bundles are found under the given id. + */ + getBundle( + transaction: PersistenceTransaction, + bundleId: string + ): PersistencePromise; + + /** + * Saves a `BundleMetadata` from a bundle into local storage, using its id as + * the persistent key. + */ + saveBundleMetadata( + transaction: PersistenceTransaction, + metadata: bundleProto.BundleMetadata + ): PersistencePromise; + + /** + * Gets a saved `NamedQuery` for the given query name. Returns undefined if + * no queries are found under the given name. + */ + getNamedQuery( + transaction: PersistenceTransaction, + queryName: string + ): PersistencePromise; + + /** + * Saves a `NamedQuery` from a bundle, using its name as the persistent key. + */ + saveNamedQuery( + transaction: PersistenceTransaction, + query: bundleProto.NamedQuery + ): PersistencePromise; +} diff --git a/packages/firestore/src/local/indexeddb_bundle_cache.ts b/packages/firestore/src/local/indexeddb_bundle_cache.ts new file mode 100644 index 00000000000..10e58aeae21 --- /dev/null +++ b/packages/firestore/src/local/indexeddb_bundle_cache.ts @@ -0,0 +1,111 @@ +/** + * @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 { PersistenceTransaction } from './persistence'; +import { PersistencePromise } from './persistence_promise'; +import * as bundleProto from '../protos/firestore_bundle_proto'; +import { BundleCache } from './bundle_cache'; +import { + DbBundle, + DbBundlesKey, + DbNamedQuery, + DbNamedQueriesKey +} from './indexeddb_schema'; +import { SimpleDbStore } from './simple_db'; +import { IndexedDbPersistence } from './indexeddb_persistence'; +import { + fromDbBundle, + fromDbNamedQuery, + LocalSerializer, + toDbBundle, + toDbNamedQuery +} from './local_serializer'; +import { Bundle, NamedQuery } from '../core/bundle'; + +export class IndexedDbBundleCache implements BundleCache { + constructor(private serializer: LocalSerializer) {} + + getBundle( + transaction: PersistenceTransaction, + bundleId: string + ): PersistencePromise { + return bundlesStore(transaction) + .get(bundleId) + .next(bundle => { + if (bundle) { + return fromDbBundle(this.serializer, bundle); + } + return undefined; + }); + } + + saveBundleMetadata( + transaction: PersistenceTransaction, + bundleMetadata: bundleProto.BundleMetadata + ): PersistencePromise { + return bundlesStore(transaction).put( + toDbBundle(this.serializer, bundleMetadata) + ); + } + + getNamedQuery( + transaction: PersistenceTransaction, + queryName: string + ): PersistencePromise { + return namedQueriesStore(transaction) + .get(queryName) + .next(query => { + if (query) { + return fromDbNamedQuery(this.serializer, query); + } + return undefined; + }); + } + + saveNamedQuery( + transaction: PersistenceTransaction, + query: bundleProto.NamedQuery + ): PersistencePromise { + return namedQueriesStore(transaction).put( + toDbNamedQuery(this.serializer, query) + ); + } +} + +/** + * Helper to get a typed SimpleDbStore for the bundles object store. + */ +function bundlesStore( + txn: PersistenceTransaction +): SimpleDbStore { + return IndexedDbPersistence.getStore( + txn, + DbBundle.store + ); +} + +/** + * Helper to get a typed SimpleDbStore for the namedQueries object store. + */ +function namedQueriesStore( + txn: PersistenceTransaction +): SimpleDbStore { + return IndexedDbPersistence.getStore( + txn, + DbNamedQuery.store + ); +} diff --git a/packages/firestore/src/local/indexeddb_persistence.ts b/packages/firestore/src/local/indexeddb_persistence.ts index 4409e0fab79..86d183f8f2e 100644 --- a/packages/firestore/src/local/indexeddb_persistence.ts +++ b/packages/firestore/src/local/indexeddb_persistence.ts @@ -32,6 +32,7 @@ import { EncodedResourcePath, encodeResourcePath } from './encoded_resource_path'; +import { IndexedDbBundleCache } from './indexeddb_bundle_cache'; import { IndexedDbIndexManager } from './indexeddb_index_manager'; import { IndexedDbMutationQueue, @@ -226,6 +227,7 @@ export class IndexedDbPersistence implements Persistence { private readonly targetCache: IndexedDbTargetCache; private readonly indexManager: IndexedDbIndexManager; private readonly remoteDocumentCache: IndexedDbRemoteDocumentCache; + private readonly bundleCache: IndexedDbBundleCache; private readonly webStorage: Storage; readonly referenceDelegate: IndexedDbLruDelegate; @@ -259,6 +261,7 @@ export class IndexedDbPersistence implements Persistence { this.serializer, this.indexManager ); + this.bundleCache = new IndexedDbBundleCache(this.serializer); if (platform.window && platform.window.localStorage) { this.window = platform.window; this.webStorage = this.window.localStorage; @@ -763,6 +766,14 @@ export class IndexedDbPersistence implements Persistence { return this.indexManager; } + getBundleCache(): IndexedDbBundleCache { + debugAssert( + this.started, + 'Cannot initialize BundleCache before persistence is started.' + ); + return this.bundleCache; + } + runTransaction( action: string, mode: PersistenceTransactionMode, diff --git a/packages/firestore/src/local/indexeddb_schema.ts b/packages/firestore/src/local/indexeddb_schema.ts index 2026b373adb..579a8d9eeeb 100644 --- a/packages/firestore/src/local/indexeddb_schema.ts +++ b/packages/firestore/src/local/indexeddb_schema.ts @@ -1092,11 +1092,11 @@ export type DbBundlesKey = string; /** * A object representing a bundle loaded by the SDK. */ -export class DbBundles { +export class DbBundle { /** Name of the IndexedDb object store. */ static store = 'bundles'; - static keyPath = ['bundleId']; + static keyPath = 'bundleId'; constructor( /** The ID of the loaded bundle. */ @@ -1109,8 +1109,8 @@ export class DbBundles { } function createBundlesStore(db: IDBDatabase): void { - db.createObjectStore(DbBundles.store, { - keyPath: DbBundles.keyPath + db.createObjectStore(DbBundle.store, { + keyPath: DbBundle.keyPath }); } @@ -1119,11 +1119,11 @@ export type DbNamedQueriesKey = string; /** * A object representing a named query loaded by the SDK via a bundle. */ -export class DbNamedQueries { +export class DbNamedQuery { /** Name of the IndexedDb object store. */ static store = 'namedQueries'; - static keyPath = ['name']; + static keyPath = 'name'; constructor( /** The name of the query. */ @@ -1136,8 +1136,8 @@ export class DbNamedQueries { } function createNamedQueriesStore(db: IDBDatabase): void { - db.createObjectStore(DbNamedQueries.store, { - keyPath: DbNamedQueries.keyPath + db.createObjectStore(DbNamedQuery.store, { + keyPath: DbNamedQuery.keyPath }); } @@ -1174,7 +1174,7 @@ export const V8_STORES = [...V6_STORES, DbCollectionParent.store]; // V10 does not change the set of stores. -export const V11_STORES = [...V8_STORES, DbCollectionParent.store]; +export const V11_STORES = [...V8_STORES, DbBundle.store, DbNamedQuery.store]; /** * The list of all default IndexedDB stores used throughout the SDK. This is diff --git a/packages/firestore/src/local/local_serializer.ts b/packages/firestore/src/local/local_serializer.ts index e15697b934c..d6507198200 100644 --- a/packages/firestore/src/local/local_serializer.ts +++ b/packages/firestore/src/local/local_serializer.ts @@ -31,7 +31,9 @@ import { debugAssert, fail } from '../util/assert'; import { ByteString } from '../util/byte_string'; import { Target } from '../core/target'; import { + DbBundle, DbMutationBatch, + DbNamedQuery, DbNoDocument, DbQuery, DbRemoteDocument, @@ -41,10 +43,13 @@ import { DbUnknownDocument } from './indexeddb_schema'; import { TargetData, TargetPurpose } from './target_data'; +import { Bundle, NamedQuery } from '../core/bundle'; +import { Query } from '../core/query'; +import * as bundleProto from '../protos/firestore_bundle_proto'; /** Serializer for values stored in the LocalStore. */ export class LocalSerializer { - constructor(private remoteSerializer: JsonProtoSerializer) {} + constructor(readonly remoteSerializer: JsonProtoSerializer) {} /** Decodes a remote document from storage locally to a Document. */ fromDbRemoteDocument(remoteDoc: DbRemoteDocument): MaybeDocument { @@ -124,12 +129,12 @@ export class LocalSerializer { return SnapshotVersion.fromTimestamp(timestamp); } - private toDbTimestamp(snapshotVersion: SnapshotVersion): DbTimestamp { + toDbTimestamp(snapshotVersion: SnapshotVersion): DbTimestamp { const timestamp = snapshotVersion.toTimestamp(); return new DbTimestamp(timestamp.seconds, timestamp.nanoseconds); } - private fromDbTimestamp(dbTimestamp: DbTimestamp): SnapshotVersion { + fromDbTimestamp(dbTimestamp: DbTimestamp): SnapshotVersion { const timestamp = new Timestamp( dbTimestamp.seconds, dbTimestamp.nanoseconds @@ -239,3 +244,99 @@ export class LocalSerializer { function isDocumentQuery(dbQuery: DbQuery): dbQuery is api.DocumentsTarget { return (dbQuery as api.DocumentsTarget).documents !== undefined; } + +/** Encodes a DbBundle to a Bundle. */ +export function fromDbBundle( + serializer: LocalSerializer, + dbBundle: DbBundle +): Bundle { + return { + id: dbBundle.bundleId, + createTime: serializer.fromDbTimestamp(dbBundle.createTime), + version: dbBundle.version + }; +} + +/** Encodes a BundleMetadata to a DbBundle. */ +export function toDbBundle( + serializer: LocalSerializer, + metadata: bundleProto.BundleMetadata +): DbBundle { + return { + bundleId: metadata.id!, + createTime: serializer.toDbTimestamp( + serializer.remoteSerializer.fromVersion(metadata.createTime!) + ), + version: metadata.version! + }; +} + +/** Encodes a DbNamedQuery to a NamedQuery. */ +export function fromDbNamedQuery( + serializer: LocalSerializer, + dbNamedQuery: DbNamedQuery +): NamedQuery { + return { + name: dbNamedQuery.name, + query: fromBundledQuery(serializer, dbNamedQuery.bundledQuery), + readTime: serializer.fromDbTimestamp(dbNamedQuery.readTime) + }; +} + +/** Encodes a NamedQuery from a bundle proto to a DbNamedQuery. */ +export function toDbNamedQuery( + serializer: LocalSerializer, + query: bundleProto.NamedQuery +): DbNamedQuery { + return { + name: query.name!, + readTime: serializer.toDbTimestamp( + serializer.remoteSerializer.fromVersion(query.readTime!) + ), + bundledQuery: query.bundledQuery! + }; +} + +/** + * Encodes a `BundledQuery` from bundle proto to a Query object. + * + * This reconstructs the original query used to build the bundle being loaded, + * including features exists only in SDKs (for example: limit-to-last). + */ +export function fromBundledQuery( + serializer: LocalSerializer, + bundledQuery: bundleProto.BundledQuery +): Query { + const query = serializer.remoteSerializer.convertQueryTargetToQuery({ + parent: bundledQuery.parent!, + structuredQuery: bundledQuery.structuredQuery! + }); + if (bundledQuery.limitType === 'LAST') { + return query.withLimitToLast(query.limit); + } + return query; +} + +/** Encodes a NamedQuery proto object to a NamedQuery model object. */ +export function fromProtoNamedQuery( + serializer: LocalSerializer, + namedQuery: bundleProto.NamedQuery +): NamedQuery { + return { + name: namedQuery.name!, + query: fromBundledQuery(serializer, namedQuery.bundledQuery!), + readTime: serializer.remoteSerializer.fromVersion(namedQuery.readTime!) + }; +} + +/** Encodes a BundleMetadata proto object to a Bundle model object. */ +export function fromBundleMetadata( + serializer: LocalSerializer, + metadata: bundleProto.BundleMetadata +): Bundle { + return { + id: metadata.id!, + version: metadata.version!, + createTime: serializer.remoteSerializer.fromVersion(metadata.createTime!) + }; +} diff --git a/packages/firestore/src/local/memory_bundle_cache.ts b/packages/firestore/src/local/memory_bundle_cache.ts new file mode 100644 index 00000000000..eb16c8f8de8 --- /dev/null +++ b/packages/firestore/src/local/memory_bundle_cache.ts @@ -0,0 +1,70 @@ +/** + * @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 { PersistenceTransaction } from './persistence'; +import { PersistencePromise } from './persistence_promise'; +import * as bundleProto from '../protos/firestore_bundle_proto'; +import { BundleCache } from './bundle_cache'; +import { Bundle, NamedQuery } from '../core/bundle'; +import { + fromBundleMetadata, + fromProtoNamedQuery, + LocalSerializer +} from './local_serializer'; + +export class MemoryBundleCache implements BundleCache { + private bundles = new Map(); + private namedQueries = new Map(); + + constructor(private serializer: LocalSerializer) {} + + getBundle( + transaction: PersistenceTransaction, + bundleId: string + ): PersistencePromise { + return PersistencePromise.resolve(this.bundles.get(bundleId)); + } + + saveBundleMetadata( + transaction: PersistenceTransaction, + bundleMetadata: bundleProto.BundleMetadata + ): PersistencePromise { + this.bundles.set( + bundleMetadata.id!, + fromBundleMetadata(this.serializer, bundleMetadata) + ); + return PersistencePromise.resolve(); + } + + getNamedQuery( + transaction: PersistenceTransaction, + queryName: string + ): PersistencePromise { + return PersistencePromise.resolve(this.namedQueries.get(queryName)); + } + + saveNamedQuery( + transaction: PersistenceTransaction, + query: bundleProto.NamedQuery + ): PersistencePromise { + this.namedQueries.set( + query.name!, + fromProtoNamedQuery(this.serializer, query) + ); + return PersistencePromise.resolve(); + } +} diff --git a/packages/firestore/src/local/memory_persistence.ts b/packages/firestore/src/local/memory_persistence.ts index 391f8686565..ce84ff95ff2 100644 --- a/packages/firestore/src/local/memory_persistence.ts +++ b/packages/firestore/src/local/memory_persistence.ts @@ -45,6 +45,9 @@ import { import { PersistencePromise } from './persistence_promise'; import { ReferenceSet } from './reference_set'; import { TargetData } from './target_data'; +import { MemoryBundleCache } from './memory_bundle_cache'; +import { JsonProtoSerializer } from '../remote/serializer'; +import { LocalSerializer } from './local_serializer'; const LOG_TAG = 'MemoryPersistence'; /** @@ -63,7 +66,9 @@ export class MemoryPersistence implements Persistence { private mutationQueues: { [user: string]: MemoryMutationQueue } = {}; private readonly remoteDocumentCache: MemoryRemoteDocumentCache; private readonly targetCache: MemoryTargetCache; + private readonly bundleCache: MemoryBundleCache; private readonly listenSequence = new ListenSequence(0); + private serializer: LocalSerializer; private _started = false; @@ -76,7 +81,8 @@ export class MemoryPersistence implements Persistence { * checked or asserted on every access. */ constructor( - referenceDelegateFactory: (p: MemoryPersistence) => MemoryReferenceDelegate + referenceDelegateFactory: (p: MemoryPersistence) => MemoryReferenceDelegate, + serializer: JsonProtoSerializer ) { this._started = true; this.referenceDelegate = referenceDelegateFactory(this); @@ -88,6 +94,8 @@ export class MemoryPersistence implements Persistence { this.indexManager, sizer ); + this.serializer = new LocalSerializer(serializer); + this.bundleCache = new MemoryBundleCache(this.serializer); } start(): Promise { @@ -132,6 +140,10 @@ export class MemoryPersistence implements Persistence { return this.remoteDocumentCache; } + getBundleCache(): MemoryBundleCache { + return this.bundleCache; + } + runTransaction( action: string, mode: PersistenceTransactionMode, diff --git a/packages/firestore/src/local/persistence.ts b/packages/firestore/src/local/persistence.ts index 146b0331738..84debbc8afa 100644 --- a/packages/firestore/src/local/persistence.ts +++ b/packages/firestore/src/local/persistence.ts @@ -25,6 +25,7 @@ import { PersistencePromise } from './persistence_promise'; import { TargetCache } from './target_cache'; import { RemoteDocumentCache } from './remote_document_cache'; import { TargetData } from './target_data'; +import { BundleCache } from './bundle_cache'; export const PRIMARY_LEASE_LOST_ERROR_MSG = 'The current tab is not in the required state to perform this operation. ' + @@ -216,6 +217,15 @@ export interface Persistence { */ getRemoteDocumentCache(): RemoteDocumentCache; + /** + * Returns a BundleCache representing the persisted cache of loaded bundles. + * + * Note: The implementation is free to return the same instance every time + * this is called. In particular, the memory-backed implementation does this + * to emulate the persisted implementation to the extent possible. + */ + getBundleCache(): BundleCache; + /** * Returns an IndexManager instance that manages our persisted query indexes. * diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index de29942ffc2..db1ff3f3c39 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -787,7 +787,7 @@ export class JsonProtoSerializer { return result; } - fromQueryTarget(target: api.QueryTarget): Target { + convertQueryTargetToQuery(target: api.QueryTarget): Query { let path = this.fromQueryPath(target.parent!); const query = target.structuredQuery!; @@ -840,7 +840,11 @@ export class JsonProtoSerializer { LimitType.First, startAt, endAt - ).toTarget(); + ); + } + + fromQueryTarget(target: api.QueryTarget): Target { + return this.convertQueryTargetToQuery(target).toTarget(); } toListenRequestLabels( diff --git a/packages/firestore/test/unit/local/bundle_cache.test.ts b/packages/firestore/test/unit/local/bundle_cache.test.ts new file mode 100644 index 00000000000..8761acb9391 --- /dev/null +++ b/packages/firestore/test/unit/local/bundle_cache.test.ts @@ -0,0 +1,209 @@ +/** + * @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 { expect } from 'chai'; +import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence'; +import { filter, orderBy, path } from '../../util/helpers'; +import { TestBundleCache } from './test_bundle_cache'; +import { SnapshotVersion } from '../../../src/core/snapshot_version'; +import { Timestamp } from '../../../src/api/timestamp'; +import { Query } from '../../../src/core/query'; +import { + clearTestPersistence, + JSON_SERIALIZER, + testIndexedDbPersistence, + testMemoryEagerPersistence +} from './persistence_test_helpers'; +import { ResourcePath } from '../../../src/model/path'; +import { NamedQuery } from '../../../src/core/bundle'; + +describe('MemoryBundleCache', () => { + let cache: TestBundleCache; + + beforeEach(async () => { + cache = await testMemoryEagerPersistence().then( + persistence => new TestBundleCache(persistence) + ); + }); + + genericBundleCacheTests(() => cache); +}); + +describe('IndexedDbBundleCache', () => { + if (!IndexedDbPersistence.isAvailable()) { + console.warn('No IndexedDB. Skipping IndexedDbBundleCache tests.'); + return; + } + + let cache: TestBundleCache; + let persistence: IndexedDbPersistence; + beforeEach(async () => { + persistence = await testIndexedDbPersistence(); + cache = new TestBundleCache(persistence); + }); + + afterEach(async () => { + await persistence.shutdown(); + await clearTestPersistence(); + }); + + genericBundleCacheTests(() => cache); +}); + +/** + * Defines the set of tests to run against both bundle cache implementations. + */ +function genericBundleCacheTests(cacheFn: () => TestBundleCache): void { + let cache: TestBundleCache; + + beforeEach(async () => { + cache = cacheFn(); + }); + + function verifyNamedQuery( + actual: NamedQuery, + expectedName: string, + expectedQuery: Query, + expectedReadSeconds: number, + expectedReadNanos: number + ): void { + expect(actual.name).to.equal(expectedName); + expect(actual.query.isEqual(expectedQuery)).to.be.true; + expect( + actual.readTime.isEqual( + SnapshotVersion.fromTimestamp( + new Timestamp(expectedReadSeconds, expectedReadNanos) + ) + ) + ).to.be.true; + } + + it('returns undefined when bundle id is not found', async () => { + expect(await cache.getBundle('bundle-1')).to.be.undefined; + }); + + it('returns saved bundle', async () => { + await cache.saveBundleMetadata({ + id: 'bundle-1', + version: 1, + createTime: { seconds: 1, nanos: 9999 } + }); + expect(await cache.getBundle('bundle-1')).to.deep.equal({ + id: 'bundle-1', + version: 1, + createTime: SnapshotVersion.fromTimestamp(new Timestamp(1, 9999)) + }); + + // Overwrite + await cache.saveBundleMetadata({ + id: 'bundle-1', + version: 2, + createTime: { seconds: 2, nanos: 1111 } + }); + expect(await cache.getBundle('bundle-1')).to.deep.equal({ + id: 'bundle-1', + version: 2, + createTime: SnapshotVersion.fromTimestamp(new Timestamp(2, 1111)) + }); + }); + + it('returns undefined when query name is not found', async () => { + expect(await cache.getNamedQuery('query-1')).to.be.undefined; + }); + + it('returns saved collection queries', async () => { + const query = Query.atPath(path('collection')) + .addFilter(filter('sort', '>=', 2)) + .addOrderBy(orderBy('sort')); + const queryTarget = JSON_SERIALIZER.toQueryTarget(query.toTarget()); + + await cache.setNamedQuery({ + name: 'query-1', + readTime: { seconds: 1, nanos: 9999 }, + bundledQuery: { + parent: queryTarget.parent, + structuredQuery: queryTarget.structuredQuery + } + }); + + const namedQuery = await cache.getNamedQuery('query-1'); + verifyNamedQuery(namedQuery!, 'query-1', query, 1, 9999); + }); + + it('returns saved collection group queries', async () => { + const query = new Query(ResourcePath.EMPTY_PATH, 'collection'); + const queryTarget = JSON_SERIALIZER.toQueryTarget(query.toTarget()); + + await cache.setNamedQuery({ + name: 'query-1', + readTime: { seconds: 1, nanos: 9999 }, + bundledQuery: { + parent: queryTarget.parent, + structuredQuery: queryTarget.structuredQuery, + limitType: undefined + } + }); + + const namedQuery = await cache.getNamedQuery('query-1'); + verifyNamedQuery(namedQuery!, 'query-1', query, 1, 9999); + }); + + it('returns expected limit queries', async () => { + const query = Query.atPath(path('collection')) + .addOrderBy(orderBy('sort')) + .withLimitToFirst(3); + const queryTarget = JSON_SERIALIZER.toQueryTarget(query.toTarget()); + + await cache.setNamedQuery({ + name: 'query-1', + readTime: { seconds: 1, nanos: 9999 }, + bundledQuery: { + parent: queryTarget.parent, + structuredQuery: queryTarget.structuredQuery, + limitType: 'FIRST' + } + }); + + const namedQuery = await cache.getNamedQuery('query-1'); + verifyNamedQuery(namedQuery!, 'query-1', query, 1, 9999); + }); + + it('returns expected limit to last queries', async () => { + const query = Query.atPath(path('collection')) + .addOrderBy(orderBy('sort')) + .withLimitToLast(3); + // Simulating bundle building for limit-to-last queries from the server + // SDKs: they save the equivelent limit-to-first queries with a limitType + // value 'LAST'. Client SDKs should apply a withLimitToLast when they see + // limitType 'LAST' from bundles. + const limitQuery = query.withLimitToFirst(3); + const queryTarget = JSON_SERIALIZER.toQueryTarget(limitQuery.toTarget()); + + await cache.setNamedQuery({ + name: 'query-1', + readTime: { seconds: 1, nanos: 9999 }, + bundledQuery: { + parent: queryTarget.parent, + structuredQuery: queryTarget.structuredQuery, + limitType: 'LAST' + } + }); + + const namedQuery = await cache.getNamedQuery('query-1'); + verifyNamedQuery(namedQuery!, 'query-1', query, 1, 9999); + }); +} diff --git a/packages/firestore/test/unit/local/persistence_test_helpers.ts b/packages/firestore/test/unit/local/persistence_test_helpers.ts index b60b32d4264..9733ae9e009 100644 --- a/packages/firestore/test/unit/local/persistence_test_helpers.ts +++ b/packages/firestore/test/unit/local/persistence_test_helpers.ts @@ -86,7 +86,7 @@ export const INDEXEDDB_TEST_DATABASE_NAME = IndexedDbPersistence.buildStoragePrefix(TEST_DATABASE_INFO) + IndexedDbPersistence.MAIN_DATABASE; -const JSON_SERIALIZER = new JsonProtoSerializer(TEST_DATABASE_ID, { +export const JSON_SERIALIZER = new JsonProtoSerializer(TEST_DATABASE_ID, { useProto3Json: true }); @@ -131,13 +131,16 @@ export async function testIndexedDbPersistence( /** Creates and starts a MemoryPersistence instance for testing. */ export async function testMemoryEagerPersistence(): Promise { - return new MemoryPersistence(MemoryEagerDelegate.factory); + return new MemoryPersistence(MemoryEagerDelegate.factory, JSON_SERIALIZER); } export async function testMemoryLruPersistence( params: LruParams = LruParams.DEFAULT ): Promise { - return new MemoryPersistence(p => new MemoryLruDelegate(p, params)); + return new MemoryPersistence( + p => new MemoryLruDelegate(p, params), + JSON_SERIALIZER + ); } /** Clears the persistence in tests */ diff --git a/packages/firestore/test/unit/local/test_bundle_cache.ts b/packages/firestore/test/unit/local/test_bundle_cache.ts new file mode 100644 index 00000000000..e0ac098952c --- /dev/null +++ b/packages/firestore/test/unit/local/test_bundle_cache.ts @@ -0,0 +1,73 @@ +/** + * @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 { Persistence } from '../../../src/local/persistence'; +import { BundleCache } from '../../../src/local/bundle_cache'; +import { Bundle, NamedQuery } from '../../../src/core/bundle'; +import * as bundleProto from '../../../src/protos/firestore_bundle_proto'; + +/** + * A wrapper around a BundleCache that automatically creates a + * transaction around every operation to reduce test boilerplate. + */ +export class TestBundleCache { + private readonly cache: BundleCache; + + constructor(private readonly persistence: Persistence) { + this.cache = persistence.getBundleCache(); + } + + getBundle(bundleId: string): Promise { + return this.persistence.runTransaction( + 'getBundle', + 'readonly', + transaction => { + return this.cache.getBundle(transaction, bundleId); + } + ); + } + + saveBundleMetadata(metadata: bundleProto.BundleMetadata): Promise { + return this.persistence.runTransaction( + 'saveBundleMetadata', + 'readwrite', + transaction => { + return this.cache.saveBundleMetadata(transaction, metadata); + } + ); + } + + getNamedQuery(name: string): Promise { + return this.persistence.runTransaction( + 'getNamedQuery', + 'readonly', + transaction => { + return this.cache.getNamedQuery(transaction, name); + } + ); + } + + setNamedQuery(query: bundleProto.NamedQuery): Promise { + return this.persistence.runTransaction( + 'setNamedQuery', + 'readwrite', + transaction => { + return this.cache.saveNamedQuery(transaction, query); + } + ); + } +} diff --git a/packages/firestore/test/unit/specs/spec_test_components.ts b/packages/firestore/test/unit/specs/spec_test_components.ts index 9fba3e611a4..2bf5b0883d6 100644 --- a/packages/firestore/test/unit/specs/spec_test_components.ts +++ b/packages/firestore/test/unit/specs/spec_test_components.ts @@ -51,6 +51,7 @@ import { ViewSnapshot } from '../../../src/core/view_snapshot'; import { Query } from '../../../src/core/query'; import { Mutation } from '../../../src/model/mutation'; import { expect } from 'chai'; +import { JSON_SERIALIZER } from '../local/persistence_test_helpers'; /** * A test-only MemoryPersistence implementation that is able to inject @@ -161,7 +162,8 @@ export class MockMemoryComponentProvider extends MemoryComponentProvider { return new MockMemoryPersistence( this.gcEnabled ? MemoryEagerDelegate.factory - : p => new MemoryLruDelegate(p, LruParams.DEFAULT) + : p => new MemoryLruDelegate(p, LruParams.DEFAULT), + JSON_SERIALIZER ); } } From e8b9ec261c751d60a766655384af0823dbf0b91f Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Thu, 25 Jun 2020 12:32:02 -0400 Subject: [PATCH 05/27] Implement bundle features in local store. (#3200) * Renaming interfaces without leading I * Initial commit of bundle reading - for web only. * Tests only run when it is not Node. * Fix redundant imports * Fix missing textencoder * Remove generator. * Support bundle reader for Node * Fix rebase errors. * Remote 'only' * Merge branch 'wuandy/Bundles' into wuandy/BundleReaderNode # Conflicts: # packages/firestore/src/util/bundle_reader.ts # packages/firestore/test/unit/util/bundle.test.ts * Added more comments, and more tests for Node. * Implement BundleCache. * Add applyBundleDocuments to local store. * Add rest of bundle service to localstore Fix change buffer bug * Simplify change buffer get read time logic. * Fix lint errors * Add comments. * Change localstore to check for newer bundle directly. * A little more comments. * Address comments. * Address comments 2 * Make it tree-shakeable. * More comments addressing. * Another around of comments --- packages/firestore/src/core/bundle.ts | 58 ++++ .../firestore/src/core/component_provider.ts | 16 +- packages/firestore/src/local/bundle_cache.ts | 2 +- .../src/local/indexeddb_bundle_cache.ts | 2 +- .../local/indexeddb_remote_document_cache.ts | 12 +- packages/firestore/src/local/local_store.ts | 321 ++++++++++++++---- .../src/local/memory_bundle_cache.ts | 2 +- .../src/local/memory_remote_document_cache.ts | 8 +- .../local/remote_document_change_buffer.ts | 56 ++- .../test/unit/local/local_store.test.ts | 186 +++++++++- .../remote_document_change_buffer.test.ts | 9 - .../test/unit/local/test_bundle_cache.ts | 2 +- packages/firestore/test/util/helpers.ts | 48 ++- 13 files changed, 592 insertions(+), 130 deletions(-) diff --git a/packages/firestore/src/core/bundle.ts b/packages/firestore/src/core/bundle.ts index 464b40e62ef..cb652e8dbb9 100644 --- a/packages/firestore/src/core/bundle.ts +++ b/packages/firestore/src/core/bundle.ts @@ -17,6 +17,17 @@ import { Query } from './query'; import { SnapshotVersion } from './snapshot_version'; +import { + fromDocument, + fromName, + fromVersion, + JsonProtoSerializer +} from '../remote/serializer'; +import * as bundleProto from '../protos/firestore_bundle_proto'; +import * as api from '../protos/firestore_proto_api'; +import { DocumentKey } from '../model/document_key'; +import { MaybeDocument, NoDocument } from '../model/document'; +import { debugAssert } from '../util/assert'; /** * Represents a Firestore bundle saved by the SDK in its local storage. @@ -40,3 +51,50 @@ export interface NamedQuery { /** The time at which the results for this query were read. */ readonly readTime: SnapshotVersion; } + +/** + * Represents a bundled document, including the metadata and the document + * itself, if it exists. + */ +interface BundledDocument { + metadata: bundleProto.BundledDocumentMetadata; + document: api.Document | undefined; +} + +/** + * An array of `BundledDocument`. + */ +export type BundledDocuments = BundledDocument[]; + +/** + * Helper to convert objects from bundles to model objects in the SDK. + */ +export class BundleConverter { + constructor(private serializer: JsonProtoSerializer) {} + + toDocumentKey(name: string): DocumentKey { + return fromName(this.serializer, name); + } + + /** + * Converts a BundleDocument to a MaybeDocument. + */ + toMaybeDocument(bundledDoc: BundledDocument): MaybeDocument { + if (bundledDoc.metadata.exists) { + debugAssert( + !!bundledDoc.document, + 'Document is undefined when metadata.exist is true.' + ); + return fromDocument(this.serializer, bundledDoc.document!, false); + } else { + return new NoDocument( + this.toDocumentKey(bundledDoc.metadata.name!), + this.toSnapshotVersion(bundledDoc.metadata.readTime!) + ); + } + } + + toSnapshotVersion(time: api.Timestamp): SnapshotVersion { + return fromVersion(time); + } +} diff --git a/packages/firestore/src/core/component_provider.ts b/packages/firestore/src/core/component_provider.ts index 65b739cf393..825cd221945 100644 --- a/packages/firestore/src/core/component_provider.ts +++ b/packages/firestore/src/core/component_provider.ts @@ -49,6 +49,7 @@ import { import { newConnectivityMonitor } from '../platform/connection'; import { newSerializer } from '../platform/serializer'; import { getDocument, getWindow } from '../platform/dom'; +import { JsonProtoSerializer } from '../remote/serializer'; const MEMORY_ONLY_PERSISTENCE_ERROR_MESSAGE = 'You are using the memory-only build of Firestore. Persistence support is ' + @@ -96,7 +97,10 @@ export class MemoryComponentProvider implements ComponentProvider { remoteStore!: RemoteStore; eventManager!: EventManager; + serializer!: JsonProtoSerializer; + async initialize(cfg: ComponentConfiguration): Promise { + this.serializer = newSerializer(cfg.databaseInfo.databaseId); this.sharedClientState = this.createSharedClientState(cfg); this.persistence = this.createPersistence(cfg); await this.persistence.start(); @@ -134,7 +138,8 @@ export class MemoryComponentProvider implements ComponentProvider { return newLocalStore( this.persistence, new IndexFreeQueryEngine(), - cfg.initialUser + cfg.initialUser, + this.serializer ); } @@ -145,8 +150,7 @@ export class MemoryComponentProvider implements ComponentProvider { MEMORY_ONLY_PERSISTENCE_ERROR_MESSAGE ); } - const serializer = newSerializer(cfg.databaseInfo.databaseId); - return new MemoryPersistence(MemoryEagerDelegate.factory, serializer); + return new MemoryPersistence(MemoryEagerDelegate.factory, this.serializer); } createRemoteStore(cfg: ComponentConfiguration): RemoteStore { @@ -221,7 +225,8 @@ export class IndexedDbComponentProvider extends MemoryComponentProvider { return newMultiTabLocalStore( this.persistence, new IndexFreeQueryEngine(), - cfg.initialUser + cfg.initialUser, + this.serializer ); } @@ -257,7 +262,6 @@ export class IndexedDbComponentProvider extends MemoryComponentProvider { const persistenceKey = IndexedDbPersistence.buildStoragePrefix( cfg.databaseInfo ); - const serializer = newSerializer(cfg.databaseInfo.databaseId); return new IndexedDbPersistence( cfg.persistenceSettings.synchronizeTabs, persistenceKey, @@ -266,7 +270,7 @@ export class IndexedDbComponentProvider extends MemoryComponentProvider { cfg.asyncQueue, getWindow(), getDocument(), - serializer, + this.serializer, this.sharedClientState, cfg.persistenceSettings.forceOwningTab ); diff --git a/packages/firestore/src/local/bundle_cache.ts b/packages/firestore/src/local/bundle_cache.ts index acf2a94f554..0bdd408e7b4 100644 --- a/packages/firestore/src/local/bundle_cache.ts +++ b/packages/firestore/src/local/bundle_cache.ts @@ -28,7 +28,7 @@ export interface BundleCache { * Gets a saved `Bundle` for a given `bundleId`, returns undefined if * no bundles are found under the given id. */ - getBundle( + getBundleMetadata( transaction: PersistenceTransaction, bundleId: string ): PersistencePromise; diff --git a/packages/firestore/src/local/indexeddb_bundle_cache.ts b/packages/firestore/src/local/indexeddb_bundle_cache.ts index 10e58aeae21..c8c107a5336 100644 --- a/packages/firestore/src/local/indexeddb_bundle_cache.ts +++ b/packages/firestore/src/local/indexeddb_bundle_cache.ts @@ -39,7 +39,7 @@ import { Bundle, NamedQuery } from '../core/bundle'; export class IndexedDbBundleCache implements BundleCache { constructor(private serializer: LocalSerializer) {} - getBundle( + getBundleMetadata( transaction: PersistenceTransaction, bundleId: string ): PersistencePromise { diff --git a/packages/firestore/src/local/indexeddb_remote_document_cache.ts b/packages/firestore/src/local/indexeddb_remote_document_cache.ts index 2d11b9e258a..122a47eb0e6 100644 --- a/packages/firestore/src/local/indexeddb_remote_document_cache.ts +++ b/packages/firestore/src/local/indexeddb_remote_document_cache.ts @@ -451,21 +451,21 @@ export class IndexedDbRemoteDocumentCache implements RemoteDocumentCache { primitiveComparator(l.canonicalString(), r.canonicalString()) ); - this.changes.forEach((key, maybeDocument) => { + this.changes.forEach((key, documentChange) => { const previousSize = this.documentSizes.get(key); debugAssert( previousSize !== undefined, `Cannot modify a document that wasn't read (for ${key})` ); - if (maybeDocument) { + if (documentChange.maybeDocument) { debugAssert( - !this.readTime.isEqual(SnapshotVersion.min()), + !this.getReadTime(key).isEqual(SnapshotVersion.min()), 'Cannot add a document with a read time of zero' ); const doc = toDbRemoteDocument( this.documentCache.serializer, - maybeDocument, - this.readTime + documentChange.maybeDocument, + this.getReadTime(key) ); collectionParents = collectionParents.add(key.path.popLast()); @@ -482,7 +482,7 @@ export class IndexedDbRemoteDocumentCache implements RemoteDocumentCache { const deletedDoc = toDbRemoteDocument( this.documentCache.serializer, new NoDocument(key, SnapshotVersion.min()), - this.readTime + this.getReadTime(key) ); promises.push( this.documentCache.addEntry(transaction, key, deletedDoc) diff --git a/packages/firestore/src/local/local_store.ts b/packages/firestore/src/local/local_store.ts index c996fe532c1..5bc3217edb0 100644 --- a/packages/firestore/src/local/local_store.ts +++ b/packages/firestore/src/local/local_store.ts @@ -25,6 +25,8 @@ import { DocumentKeySet, documentKeySet, DocumentMap, + documentVersionMap, + DocumentVersionMap, maybeDocumentMap, MaybeDocumentMap } from '../model/collections'; @@ -37,7 +39,7 @@ import { MutationBatchResult } from '../model/mutation_batch'; import { RemoteEvent, TargetChange } from '../remote/remote_event'; -import { debugAssert, hardAssert } from '../util/assert'; +import { debugAssert, debugCast, hardAssert } from '../util/assert'; import { Code, FirestoreError } from '../util/error'; import { logDebug } from '../util/log'; import { primitiveComparator } from '../util/misc'; @@ -66,6 +68,10 @@ import { IndexedDbRemoteDocumentCache } from './indexeddb_remote_document_cache' import { IndexedDbTargetCache } from './indexeddb_target_cache'; import { extractFieldMask } from '../model/object_value'; import { isIndexedDbTransactionError } from './simple_db'; +import * as bundleProto from '../protos/firestore_bundle_proto'; +import { BundleConverter, BundledDocuments, NamedQuery } from '../core/bundle'; +import { BundleCache } from './bundle_cache'; +import { JsonProtoSerializer } from '../remote/serializer'; const LOG_TAG = 'LocalStore'; @@ -294,13 +300,16 @@ class LocalStoreImpl implements LocalStore { protected mutationQueue: MutationQueue; /** The set of all cached remote documents. */ - protected remoteDocuments: RemoteDocumentCache; + remoteDocuments: RemoteDocumentCache; /** * The "local" view of all documents (layering mutationQueue on top of * remoteDocumentCache). */ - protected localDocuments: LocalDocumentsView; + localDocuments: LocalDocumentsView; + + /** The set of all cached bundle metadata and named queries. */ + bundleCache: BundleCache; /** Maps a target to its `TargetData`. */ protected targetCache: TargetCache; @@ -331,9 +340,10 @@ class LocalStoreImpl implements LocalStore { constructor( /** Manages our in-memory or durable persistence. */ - protected persistence: Persistence, + public persistence: Persistence, private queryEngine: QueryEngine, - initialUser: User + initialUser: User, + readonly serializer: JsonProtoSerializer ) { debugAssert( persistence.started, @@ -347,6 +357,7 @@ class LocalStoreImpl implements LocalStore { this.mutationQueue, this.persistence.getIndexManager() ); + this.bundleCache = persistence.getBundleCache(); this.queryEngine.setLocalDocumentsView(this.localDocuments); } @@ -608,64 +619,25 @@ class LocalStoreImpl implements LocalStore { }); let changedDocs = maybeDocumentMap(); - let updatedKeys = documentKeySet(); remoteEvent.documentUpdates.forEach((key, doc) => { - updatedKeys = updatedKeys.add(key); + if (remoteEvent.resolvedLimboDocuments.has(key)) { + promises.push( + this.persistence.referenceDelegate.updateLimboDocument(txn, key) + ); + } }); // Each loop iteration only affects its "own" doc, so it's safe to get all the remote // documents in advance in a single call. promises.push( - documentBuffer.getEntries(txn, updatedKeys).next(existingDocs => { - remoteEvent.documentUpdates.forEach((key, doc) => { - const existingDoc = existingDocs.get(key); - - // Note: The order of the steps below is important, since we want - // to ensure that rejected limbo resolutions (which fabricate - // NoDocuments with SnapshotVersion.min()) never add documents to - // cache. - if ( - doc instanceof NoDocument && - doc.version.isEqual(SnapshotVersion.min()) - ) { - // NoDocuments with SnapshotVersion.min() are used in manufactured - // events. We remove these documents from cache since we lost - // access. - documentBuffer.removeEntry(key, remoteVersion); - changedDocs = changedDocs.insert(key, doc); - } else if ( - existingDoc == null || - doc.version.compareTo(existingDoc.version) > 0 || - (doc.version.compareTo(existingDoc.version) === 0 && - existingDoc.hasPendingWrites) - ) { - debugAssert( - !SnapshotVersion.min().isEqual(remoteVersion), - 'Cannot add a document when the remote version is zero' - ); - documentBuffer.addEntry(doc, remoteVersion); - changedDocs = changedDocs.insert(key, doc); - } else { - logDebug( - LOG_TAG, - 'Ignoring outdated watch update for ', - key, - '. Current version:', - existingDoc.version, - ' Watch version:', - doc.version - ); - } - - if (remoteEvent.resolvedLimboDocuments.has(key)) { - promises.push( - this.persistence.referenceDelegate.updateLimboDocument( - txn, - key - ) - ); - } - }); + this.populateDocumentChangeBuffer( + txn, + documentBuffer, + remoteEvent.documentUpdates, + remoteVersion, + undefined + ).next(result => { + changedDocs = result; }) ); @@ -708,6 +680,80 @@ class LocalStoreImpl implements LocalStore { }); } + /** + * Populates document change buffer with documents from backend or a bundle. + * Returns the document changes resulting from applying those documents. + * + * @param txn Transaction to use to read existing documents from storage. + * @param documentBuffer Document buffer to collect the resulted changes to be + * applied to storage. + * @param documents Documents to be applied. + * @param globalVersion A `SnapshotVersion` representing the read time if all + * documents have the same read time. + * @param documentVersions A DocumentKey-to-SnapshotVersion map if documents + * have their own read time. + * + * Note: this function will use `documentVersions` if it is defined; + * when it is not defined, resorts to `globalVersion`. + */ + populateDocumentChangeBuffer( + txn: PersistenceTransaction, + documentBuffer: RemoteDocumentChangeBuffer, + documents: MaybeDocumentMap, + globalVersion: SnapshotVersion, + // TODO(wuandy): We could add `readTime` to MaybeDocument instead to remove + // this parameter. + documentVersions: DocumentVersionMap | undefined + ): PersistencePromise { + let updatedKeys = documentKeySet(); + documents.forEach(k => (updatedKeys = updatedKeys.add(k))); + return documentBuffer.getEntries(txn, updatedKeys).next(existingDocs => { + let changedDocs = maybeDocumentMap(); + documents.forEach((key, doc) => { + const existingDoc = existingDocs.get(key); + const docReadTime = documentVersions?.get(key) || globalVersion; + + // Note: The order of the steps below is important, since we want + // to ensure that rejected limbo resolutions (which fabricate + // NoDocuments with SnapshotVersion.min()) never add documents to + // cache. + if ( + doc instanceof NoDocument && + doc.version.isEqual(SnapshotVersion.min()) + ) { + // NoDocuments with SnapshotVersion.min() are used in manufactured + // events. We remove these documents from cache since we lost + // access. + documentBuffer.removeEntry(key, docReadTime); + changedDocs = changedDocs.insert(key, doc); + } else if ( + existingDoc == null || + doc.version.compareTo(existingDoc.version) > 0 || + (doc.version.compareTo(existingDoc.version) === 0 && + existingDoc.hasPendingWrites) + ) { + debugAssert( + !SnapshotVersion.min().isEqual(docReadTime), + 'Cannot add a document when the remote version is zero' + ); + documentBuffer.addEntry(doc, docReadTime); + changedDocs = changedDocs.insert(key, doc); + } else { + logDebug( + LOG_TAG, + 'Ignoring outdated watch update for ', + key, + '. Current version:', + existingDoc.version, + ' Watch version:', + doc.version + ); + } + }); + return changedDocs; + }); + } + /** * Returns true if the newTargetData should be persisted during an update of * an active target. TargetData should always be persisted when a target is @@ -1033,9 +1079,10 @@ export function newLocalStore( /** Manages our in-memory or durable persistence. */ persistence: Persistence, queryEngine: QueryEngine, - initialUser: User + initialUser: User, + serializer: JsonProtoSerializer ): LocalStore { - return new LocalStoreImpl(persistence, queryEngine, initialUser); + return new LocalStoreImpl(persistence, queryEngine, initialUser, serializer); } /** @@ -1080,19 +1127,21 @@ export interface MultiTabLocalStore extends LocalStore { * functions, such that they are tree-shakeable. */ // PORTING NOTE: Web only. -class MultiTabLocalStoreImpl extends LocalStoreImpl +class MultiTablocalStoreImpl extends LocalStoreImpl implements MultiTabLocalStore { protected mutationQueue: IndexedDbMutationQueue; - protected remoteDocuments: IndexedDbRemoteDocumentCache; + remoteDocuments: IndexedDbRemoteDocumentCache; protected targetCache: IndexedDbTargetCache; constructor( - protected persistence: IndexedDbPersistence, + public persistence: IndexedDbPersistence, queryEngine: QueryEngine, - initialUser: User + initialUser: User, + readonly serializer: JsonProtoSerializer ) { - super(persistence, queryEngine, initialUser); + super(persistence, queryEngine, initialUser, serializer); + this.persistence = persistence; this.mutationQueue = persistence.getMutationQueue(initialUser); this.remoteDocuments = persistence.getRemoteDocumentCache(); this.targetCache = persistence.getTargetCache(); @@ -1181,9 +1230,15 @@ export function newMultiTabLocalStore( /** Manages our in-memory or durable persistence. */ persistence: IndexedDbPersistence, queryEngine: QueryEngine, - initialUser: User + initialUser: User, + serializer: JsonProtoSerializer ): MultiTabLocalStore { - return new MultiTabLocalStoreImpl(persistence, queryEngine, initialUser); + return new MultiTablocalStoreImpl( + persistence, + queryEngine, + initialUser, + serializer + ); } /** @@ -1208,3 +1263,137 @@ export async function ignoreIfPrimaryLeaseLoss( throw err; } } + +/** + * Applies the documents from a bundle to the "ground-state" (remote) + * documents. + * + * LocalDocuments are re-calculated if there are remaining mutations in the + * queue. + */ +export function applyBundleDocuments( + localStore: LocalStore, + documents: BundledDocuments +): Promise { + const localStoreImpl = debugCast(localStore, LocalStoreImpl); + const bundleConverter = new BundleConverter(localStoreImpl.serializer); + let documentMap = maybeDocumentMap(); + let versionMap = documentVersionMap(); + for (const bundleDoc of documents) { + const documentKey = bundleConverter.toDocumentKey(bundleDoc.metadata.name!); + documentMap = documentMap.insert( + documentKey, + bundleConverter.toMaybeDocument(bundleDoc) + ); + versionMap = versionMap.insert( + documentKey, + bundleConverter.toSnapshotVersion(bundleDoc.metadata.readTime!) + ); + } + + const documentBuffer = localStoreImpl.remoteDocuments.newChangeBuffer({ + trackRemovals: true // Make sure document removals show up in `getNewDocumentChanges()` + }); + return localStoreImpl.persistence.runTransaction( + 'Apply bundle documents', + 'readwrite-primary', + txn => { + return localStoreImpl + .populateDocumentChangeBuffer( + txn, + documentBuffer, + documentMap, + SnapshotVersion.min(), + versionMap + ) + .next(changedDocs => { + documentBuffer.apply(txn); + return changedDocs; + }) + .next(changedDocs => { + return localStoreImpl.localDocuments.getLocalViewOfDocuments( + txn, + changedDocs + ); + }); + } + ); +} + +/** + * Returns a promise of a boolean to indicate if the given bundle has already + * been loaded and the create time is newer than the current loading bundle. + */ +export function hasNewerBundle( + localStore: LocalStore, + bundleMetadata: bundleProto.BundleMetadata +): Promise { + const localStoreImpl = debugCast(localStore, LocalStoreImpl); + const bundleConverter = new BundleConverter(localStoreImpl.serializer); + const currentReadTime = bundleConverter.toSnapshotVersion( + bundleMetadata.createTime! + ); + return localStoreImpl.persistence + .runTransaction('hasNewerBundle', 'readonly', transaction => { + return localStoreImpl.bundleCache.getBundleMetadata( + transaction, + bundleMetadata.id! + ); + }) + .then(cached => { + return !!cached && cached.createTime!.compareTo(currentReadTime) > 0; + }); +} + +/** + * Saves the given `BundleMetadata` to local persistence. + * @param bundleMetadata + */ +export function saveBundle( + localStore: LocalStore, + bundleMetadata: bundleProto.BundleMetadata +): Promise { + const localStoreImpl = debugCast(localStore, LocalStoreImpl); + return localStoreImpl.persistence.runTransaction( + 'Save bundle', + 'readwrite', + transaction => { + return localStoreImpl.bundleCache.saveBundleMetadata( + transaction, + bundleMetadata + ); + } + ); +} + +/** + * Returns a promise of a `NamedQuery` associated with given query name. Promise + * resolves to undefined if no persisted data can be found. + */ +export function getNamedQuery( + localStore: LocalStore, + queryName: string +): Promise { + const localStoreImpl = debugCast(localStore, LocalStoreImpl); + return localStoreImpl.persistence.runTransaction( + 'Get named query', + 'readonly', + transaction => + localStoreImpl.bundleCache.getNamedQuery(transaction, queryName) + ); +} + +/** + * Saves the given `NamedQuery` to local persistence. + */ +export function saveNamedQuery( + localStore: LocalStore, + query: bundleProto.NamedQuery +): Promise { + const localStoreImpl = debugCast(localStore, LocalStoreImpl); + return localStoreImpl.persistence.runTransaction( + 'Save named query', + 'readwrite', + transaction => localStoreImpl.bundleCache.saveNamedQuery(transaction, query) + ); +} diff --git a/packages/firestore/src/local/memory_bundle_cache.ts b/packages/firestore/src/local/memory_bundle_cache.ts index eb16c8f8de8..b4ced5a38b2 100644 --- a/packages/firestore/src/local/memory_bundle_cache.ts +++ b/packages/firestore/src/local/memory_bundle_cache.ts @@ -32,7 +32,7 @@ export class MemoryBundleCache implements BundleCache { constructor(private serializer: LocalSerializer) {} - getBundle( + getBundleMetadata( transaction: PersistenceTransaction, bundleId: string ): PersistencePromise { diff --git a/packages/firestore/src/local/memory_remote_document_cache.ts b/packages/firestore/src/local/memory_remote_document_cache.ts index c8b147e6785..32d2274b15e 100644 --- a/packages/firestore/src/local/memory_remote_document_cache.ts +++ b/packages/firestore/src/local/memory_remote_document_cache.ts @@ -200,9 +200,13 @@ export class MemoryRemoteDocumentCache implements RemoteDocumentCache { ): PersistencePromise { const promises: Array> = []; this.changes.forEach((key, doc) => { - if (doc) { + if (doc && doc.maybeDocument) { promises.push( - this.documentCache.addEntry(transaction, doc, this.readTime) + this.documentCache.addEntry( + transaction, + doc.maybeDocument!, + this.getReadTime(key) + ) ); } else { this.documentCache.removeEntry(key); diff --git a/packages/firestore/src/local/remote_document_change_buffer.ts b/packages/firestore/src/local/remote_document_change_buffer.ts index c54a0a0a3e7..cc414d402a4 100644 --- a/packages/firestore/src/local/remote_document_change_buffer.ts +++ b/packages/firestore/src/local/remote_document_change_buffer.ts @@ -25,6 +25,16 @@ import { PersistenceTransaction } from './persistence'; import { PersistencePromise } from './persistence_promise'; import { SnapshotVersion } from '../core/snapshot_version'; +/** + * Represents a document change to be applied to remote document cache. + */ +interface RemoteDocumentChange { + // The document in this change, null if it is a removal from the cache. + readonly maybeDocument: MaybeDocument | null; + // The timestamp when this change is read. + readonly readTime: SnapshotVersion | null; +} + /** * An in-memory buffer of entries to be written to a RemoteDocumentCache. * It can be used to batch up a set of changes to be written to the cache, but @@ -44,15 +54,12 @@ export abstract class RemoteDocumentChangeBuffer { // existing cache entry should be removed). protected changes: ObjectMap< DocumentKey, - MaybeDocument | null + RemoteDocumentChange > = new ObjectMap( key => key.toString(), (l, r) => l.isEqual(r) ); - // The read time to use for all added documents in this change buffer. - private _readTime: SnapshotVersion | undefined; - private changesApplied = false; protected abstract getFromCache( @@ -69,23 +76,16 @@ export abstract class RemoteDocumentChangeBuffer { transaction: PersistenceTransaction ): PersistencePromise; - protected set readTime(value: SnapshotVersion) { - // Right now (for simplicity) we just track a single readTime for all the - // added entries since we expect them to all be the same, but we could - // rework to store per-entry readTimes if necessary. - debugAssert( - this._readTime === undefined || this._readTime.isEqual(value), - 'All changes in a RemoteDocumentChangeBuffer must have the same read time' - ); - this._readTime = value; - } - - protected get readTime(): SnapshotVersion { - debugAssert( - this._readTime !== undefined, - 'Read time is not set. All removeEntry() calls must include a readTime if `trackRemovals` is used.' - ); - return this._readTime; + protected getReadTime(key: DocumentKey): SnapshotVersion { + const change = this.changes.get(key); + if (change) { + debugAssert( + !!change.readTime, + `Read time is not set for ${key}. All removeEntry() calls must include a readTime if 'trackRemovals' is used.` + ); + return change.readTime; + } + return SnapshotVersion.min(); } /** @@ -96,8 +96,7 @@ export abstract class RemoteDocumentChangeBuffer { */ addEntry(maybeDocument: MaybeDocument, readTime: SnapshotVersion): void { this.assertNotApplied(); - this.readTime = readTime; - this.changes.set(maybeDocument.key, maybeDocument); + this.changes.set(maybeDocument.key, { maybeDocument, readTime }); } /** @@ -106,12 +105,9 @@ export abstract class RemoteDocumentChangeBuffer { * You can only remove documents that have already been retrieved via * `getEntry()/getEntries()` (enforced via IndexedDbs `apply()`). */ - removeEntry(key: DocumentKey, readTime?: SnapshotVersion): void { + removeEntry(key: DocumentKey, readTime: SnapshotVersion | null = null): void { this.assertNotApplied(); - if (readTime) { - this.readTime = readTime; - } - this.changes.set(key, null); + this.changes.set(key, { maybeDocument: null, readTime }); } /** @@ -132,7 +128,9 @@ export abstract class RemoteDocumentChangeBuffer { this.assertNotApplied(); const bufferedEntry = this.changes.get(documentKey); if (bufferedEntry !== undefined) { - return PersistencePromise.resolve(bufferedEntry); + return PersistencePromise.resolve( + bufferedEntry.maybeDocument + ); } else { return this.getFromCache(transaction, documentKey); } diff --git a/packages/firestore/test/unit/local/local_store.test.ts b/packages/firestore/test/unit/local/local_store.test.ts index bb9b8b5a8f6..ab0e670d1bd 100644 --- a/packages/firestore/test/unit/local/local_store.test.ts +++ b/packages/firestore/test/unit/local/local_store.test.ts @@ -28,8 +28,11 @@ import { SnapshotVersion } from '../../../src/core/snapshot_version'; import { IndexFreeQueryEngine } from '../../../src/local/index_free_query_engine'; import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence'; import { + applyBundleDocuments, + hasNewerBundle, LocalStore, LocalWriteResult, + saveBundle, newLocalStore, newMultiTabLocalStore } from '../../../src/local/local_store'; @@ -75,15 +78,21 @@ import { patchMutation, path, setMutation, + bundledDocuments, + TestBundledDocuments, TestSnapshotVersion, transformMutation, unknownDoc, - version + version, + bundleMetadata } from '../../util/helpers'; import { CountingQueryEngine, QueryEngineType } from './counting_query_engine'; import * as persistenceHelpers from './persistence_test_helpers'; import { ByteString } from '../../../src/util/byte_string'; +import { BundledDocuments } from '../../../src/core/bundle'; +import { JSON_SERIALIZER } from './persistence_test_helpers'; +import { BundleMetadata } from '../../../src/protos/firestore_bundle_proto'; export interface LocalStoreComponents { queryEngine: CountingQueryEngine; @@ -112,7 +121,12 @@ class LocalStoreTester { } after( - op: Mutation | Mutation[] | RemoteEvent | LocalViewChanges + op: + | Mutation + | Mutation[] + | RemoteEvent + | LocalViewChanges + | TestBundledDocuments ): LocalStoreTester { if (op instanceof Mutation) { return this.afterMutations([op]); @@ -120,8 +134,10 @@ class LocalStoreTester { return this.afterMutations(op); } else if (op instanceof LocalViewChanges) { return this.afterViewChanges(op); - } else { + } else if (op instanceof RemoteEvent) { return this.afterRemoteEvent(op); + } else { + return this.afterBundleDocuments(op.documents); } } @@ -154,6 +170,17 @@ class LocalStoreTester { return this; } + afterBundleDocuments(documents: BundledDocuments): LocalStoreTester { + this.prepareNextStep(); + + this.promiseChain = this.promiseChain + .then(() => applyBundleDocuments(this.localStore, documents)) + .then((result: MaybeDocumentMap) => { + this.lastChanges = result; + }); + return this; + } + afterViewChanges(viewChanges: LocalViewChanges): LocalStoreTester { this.prepareNextStep(); @@ -383,6 +410,25 @@ class LocalStoreTester { return this; } + toHaveNewerBundle( + metadata: BundleMetadata, + expected: boolean + ): LocalStoreTester { + this.promiseChain = this.promiseChain.then(() => { + return hasNewerBundle(this.localStore, metadata).then(actual => { + expect(actual).to.equal(expected); + }); + }); + return this; + } + + afterSavingBundle(metadata: BundleMetadata): LocalStoreTester { + this.promiseChain = this.promiseChain.then(() => + saveBundle(this.localStore, metadata) + ); + return this; + } + finish(): Promise { return this.promiseChain; } @@ -400,7 +446,8 @@ describe('LocalStore w/ Memory Persistence (SimpleQueryEngine)', () => { const localStore = newLocalStore( persistence, queryEngine, - User.UNAUTHENTICATED + User.UNAUTHENTICATED, + JSON_SERIALIZER ); return { queryEngine, persistence, localStore }; } @@ -418,7 +465,8 @@ describe('LocalStore w/ Memory Persistence (IndexFreeQueryEngine)', () => { const localStore = newLocalStore( persistence, queryEngine, - User.UNAUTHENTICATED + User.UNAUTHENTICATED, + JSON_SERIALIZER ); return { queryEngine, persistence, localStore }; } @@ -444,7 +492,8 @@ describe('LocalStore w/ IndexedDB Persistence (SimpleQueryEngine)', () => { const localStore = newMultiTabLocalStore( persistence, queryEngine, - User.UNAUTHENTICATED + User.UNAUTHENTICATED, + JSON_SERIALIZER ); await localStore.start(); return { queryEngine, persistence, localStore }; @@ -471,7 +520,8 @@ describe('LocalStore w/ IndexedDB Persistence (IndexFreeQueryEngine)', () => { const localStore = newMultiTabLocalStore( persistence, queryEngine, - User.UNAUTHENTICATED + User.UNAUTHENTICATED, + JSON_SERIALIZER ); await localStore.start(); return { queryEngine, persistence, localStore }; @@ -1482,6 +1532,128 @@ function genericLocalStoreTests( .finish(); }); + it('handles saving bundled documents', () => { + return expectLocalStore() + .after( + bundledDocuments([ + doc('foo/bar', 1, { sum: 1337 }), + deletedDoc('foo/bar1', 1) + ]) + ) + .toReturnChanged( + doc('foo/bar', 1, { sum: 1337 }), + deletedDoc('foo/bar1', 1) + ) + .toContain(doc('foo/bar', 1, { sum: 1337 })) + .toContain(deletedDoc('foo/bar1', 1)) + .finish(); + }); + + it('handles saving bundled documents with newer existing version', () => { + const query = Query.atPath(path('foo')); + return expectLocalStore() + .afterAllocatingQuery(query) + .toReturnTargetId(2) + .after(docAddedRemoteEvent(doc('foo/bar', 2, { sum: 1337 }), [2])) + .toContain(doc('foo/bar', 2, { sum: 1337 })) + .after( + bundledDocuments([ + doc('foo/bar', 1, { sum: 1336 }), + deletedDoc('foo/bar1', 1) + ]) + ) + .toReturnChanged(deletedDoc('foo/bar1', 1)) + .toContain(doc('foo/bar', 2, { sum: 1337 })) + .toContain(deletedDoc('foo/bar1', 1)) + .finish(); + }); + + it('handles saving bundled documents with older existing version', () => { + const query = Query.atPath(path('foo')); + return expectLocalStore() + .afterAllocatingQuery(query) + .toReturnTargetId(2) + .after(docAddedRemoteEvent(doc('foo/bar', 1, { val: 'to-delete' }), [2])) + .toContain(doc('foo/bar', 1, { val: 'to-delete' })) + .after( + bundledDocuments([ + doc('foo/new', 1, { sum: 1336 }), + deletedDoc('foo/bar', 2) + ]) + ) + .toReturnChanged( + doc('foo/new', 1, { sum: 1336 }), + deletedDoc('foo/bar', 2) + ) + .toContain(doc('foo/new', 1, { sum: 1336 })) + .toContain(deletedDoc('foo/bar', 2)) + .finish(); + }); + + it('handles saving bundled documents with same existing version should not overwrite', () => { + const query = Query.atPath(path('foo')); + return expectLocalStore() + .afterAllocatingQuery(query) + .toReturnTargetId(2) + .after(docAddedRemoteEvent(doc('foo/bar', 1, { val: 'old' }), [2])) + .toContain(doc('foo/bar', 1, { val: 'old' })) + .after(bundledDocuments([doc('foo/bar', 1, { val: 'new' })])) + .toReturnChanged() + .toContain(doc('foo/bar', 1, { val: 'old' })) + .finish(); + }); + + it('handles MergeMutation with Transform -> BundledDocuments', () => { + const query = Query.atPath(path('foo')); + return expectLocalStore() + .afterAllocatingQuery(query) + .toReturnTargetId(2) + .afterMutations([ + patchMutation('foo/bar', {}, Precondition.none()), + transformMutation('foo/bar', { sum: FieldValue.increment(1) }) + ]) + .toReturnChanged( + doc('foo/bar', 0, { sum: 1 }, { hasLocalMutations: true }) + ) + .toContain(doc('foo/bar', 0, { sum: 1 }, { hasLocalMutations: true })) + .after(bundledDocuments([doc('foo/bar', 1, { sum: 1337 })])) + .toReturnChanged( + doc('foo/bar', 1, { sum: 1 }, { hasLocalMutations: true }) + ) + .toContain(doc('foo/bar', 1, { sum: 1 }, { hasLocalMutations: true })) + .finish(); + }); + + it('handles PatchMutation with Transform -> BundledDocuments', () => { + // Note: see comments in `handles PatchMutation with Transform -> RemoteEvent`. + // The behavior for this and remote event is the same. + + const query = Query.atPath(path('foo')); + return expectLocalStore() + .afterAllocatingQuery(query) + .toReturnTargetId(2) + .afterMutations([ + patchMutation('foo/bar', {}), + transformMutation('foo/bar', { sum: FieldValue.increment(1) }) + ]) + .toReturnChanged(deletedDoc('foo/bar', 0)) + .toNotContain('foo/bar') + .after(bundledDocuments([doc('foo/bar', 1, { sum: 1337 })])) + .toReturnChanged( + doc('foo/bar', 1, { sum: 1 }, { hasLocalMutations: true }) + ) + .toContain(doc('foo/bar', 1, { sum: 1 }, { hasLocalMutations: true })) + .finish(); + }); + + it('handles saving and checking bundle metadata', () => { + return expectLocalStore() + .toHaveNewerBundle(bundleMetadata('test', 2), false) + .afterSavingBundle(bundleMetadata('test', 2)) + .toHaveNewerBundle(bundleMetadata('test', 1), true) + .finish(); + }); + it('computes highest unacknowledged batch id correctly', () => { return expectLocalStore() .toReturnHighestUnacknowledgeBatchId(BATCHID_UNKNOWN) diff --git a/packages/firestore/test/unit/local/remote_document_change_buffer.test.ts b/packages/firestore/test/unit/local/remote_document_change_buffer.test.ts index ec77f35695d..043e5905434 100644 --- a/packages/firestore/test/unit/local/remote_document_change_buffer.test.ts +++ b/packages/firestore/test/unit/local/remote_document_change_buffer.test.ts @@ -116,13 +116,4 @@ describe('RemoteDocumentChangeBuffer', () => { .catch(() => errors++) .then(() => expect(errors).to.equal(2)); }); - - it('cannot add documents with different read times', async () => { - // This test merely validates an assert that was added to the - // RemoteDocumentChangeBuffer to simplify our tracking of document - // read times. If we do need to track documents with different read - // times, this test should simply be removed. - buffer.addEntry(INITIAL_DOC, version(1)); - expect(() => buffer.addEntry(INITIAL_DOC, version(2))).to.throw(); - }); }); diff --git a/packages/firestore/test/unit/local/test_bundle_cache.ts b/packages/firestore/test/unit/local/test_bundle_cache.ts index e0ac098952c..2a0cdd99e8d 100644 --- a/packages/firestore/test/unit/local/test_bundle_cache.ts +++ b/packages/firestore/test/unit/local/test_bundle_cache.ts @@ -36,7 +36,7 @@ export class TestBundleCache { 'getBundle', 'readonly', transaction => { - return this.cache.getBundle(transaction, bundleId); + return this.cache.getBundleMetadata(transaction, bundleId); } ); } diff --git a/packages/firestore/test/util/helpers.ts b/packages/firestore/test/util/helpers.ts index b92cf3cc589..a4749d1e1d7 100644 --- a/packages/firestore/test/util/helpers.ts +++ b/packages/firestore/test/util/helpers.ts @@ -87,11 +87,19 @@ import { SortedSet } from '../../src/util/sorted_set'; import { FIRESTORE, query } from './api_helpers'; import { ByteString } from '../../src/util/byte_string'; import { decodeBase64, encodeBase64 } from '../../src/platform/base64'; -import { JsonProtoSerializer } from '../../src/remote/serializer'; +import { + JsonProtoSerializer, + toDocument, + toName, + toVersion +} from '../../src/remote/serializer'; import { Timestamp } from '../../src/api/timestamp'; import { DocumentReference } from '../../src/api/database'; import { DeleteFieldValueImpl } from '../../src/api/field_value'; import { Code, FirestoreError } from '../../src/util/error'; +import { JSON_SERIALIZER } from '../unit/local/persistence_test_helpers'; +import { BundledDocuments } from '../../src/core/bundle'; +import { BundleMetadata } from '../../src/protos/firestore_bundle_proto'; /* eslint-disable no-restricted-globals */ @@ -400,6 +408,44 @@ export function docUpdateRemoteEvent( return aggregator.createRemoteEvent(doc.version); } +export class TestBundledDocuments { + constructor(public documents: BundledDocuments) {} +} + +export function bundledDocuments( + documents: MaybeDocument[] +): TestBundledDocuments { + const result = documents.map(d => { + return { + metadata: { + name: toName(JSON_SERIALIZER, d.key), + readTime: toVersion(JSON_SERIALIZER, d.version), + exists: d instanceof Document + }, + document: + d instanceof Document ? toDocument(JSON_SERIALIZER, d) : undefined + }; + }); + + return new TestBundledDocuments(result); +} + +export function bundleMetadata( + id: string, + createTime: TestSnapshotVersion, + version = 1, + totalDocuments = 1, + totalBytes = 1000 +): BundleMetadata { + return { + id, + createTime: { seconds: createTime, nanos: 0 }, + version, + totalDocuments, + totalBytes + }; +} + export function updateMapping( snapshotVersion: SnapshotVersion, added: Array, From 3a78d7ca47f8b2472ada4f281d911382e2eb4a2f Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Mon, 29 Jun 2020 14:31:47 -0400 Subject: [PATCH 06/27] Fix firestore_bundle_proto rollup issue. (#3316) * Fix firestore_bundle_proto rollup issue * Create lemon-steaks-draw.md --- .changeset/lemon-steaks-draw.md | 3 +++ ...e_bundle_proto.d.ts => firestore_bundle_proto.ts} | 12 +++++------- 2 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 .changeset/lemon-steaks-draw.md rename packages/firestore/src/protos/{firestore_bundle_proto.d.ts => firestore_bundle_proto.ts} (91%) diff --git a/.changeset/lemon-steaks-draw.md b/.changeset/lemon-steaks-draw.md new file mode 100644 index 00000000000..853d812bb36 --- /dev/null +++ b/.changeset/lemon-steaks-draw.md @@ -0,0 +1,3 @@ +--- + +--- diff --git a/packages/firestore/src/protos/firestore_bundle_proto.d.ts b/packages/firestore/src/protos/firestore_bundle_proto.ts similarity index 91% rename from packages/firestore/src/protos/firestore_bundle_proto.d.ts rename to packages/firestore/src/protos/firestore_bundle_proto.ts index e98fabc7abd..ed1168e5e16 100644 --- a/packages/firestore/src/protos/firestore_bundle_proto.d.ts +++ b/packages/firestore/src/protos/firestore_bundle_proto.ts @@ -26,13 +26,11 @@ export interface BundledQuery { structuredQuery?: api.StructuredQuery | null; /** BundledQuery limitType */ - limitType?: BundledQuery.LimitType | null; + limitType?: LimitType | null; } -export namespace BundledQuery { - /** LimitType enum. */ - type LimitType = 'FIRST' | 'LAST'; -} +/** LimitType enum. */ +type LimitType = 'FIRST' | 'LAST'; /** Properties of a NamedQuery. */ export interface NamedQuery { @@ -59,7 +57,7 @@ export interface BundledDocumentMetadata { } /** Properties of a BundleMetadata. */ -interface BundleMetadata { +export interface BundleMetadata { /** BundleMetadata id */ id?: string | null; @@ -77,7 +75,7 @@ interface BundleMetadata { } /** Properties of a BundleElement. */ -interface BundleElement { +export interface BundleElement { /** BundleElement metadata */ metadata?: BundleMetadata | null; From 0bf9c73bb010227910f07224820a5384ba05fc46 Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Fri, 10 Jul 2020 21:09:14 -0400 Subject: [PATCH 07/27] Implement bundle loading. (#3201) --- packages/firestore-types/index.d.ts | 31 ++ packages/firestore/src/api/bundle.ts | 117 ++++++ packages/firestore/src/api/database.ts | 7 + packages/firestore/src/core/bundle.ts | 135 ++++++- .../firestore/src/core/firestore_client.ts | 31 +- packages/firestore/src/core/sync_engine.ts | 88 ++++- packages/firestore/src/local/local_store.ts | 4 +- .../src/platform/byte_stream_reader.ts | 3 +- packages/firestore/src/util/bundle_reader.ts | 4 + packages/firestore/src/util/byte_stream.ts | 9 +- .../integration/api_internal/bundle.test.ts | 241 +++++++++++ .../test/unit/specs/bundle_spec.test.ts | 373 ++++++++++++++++++ .../firestore/test/unit/specs/spec_builder.ts | 8 + .../test/unit/specs/spec_test_runner.ts | 23 +- .../firestore/test/unit/util/bundle.test.ts | 119 +----- .../firestore/test/unit/util/bundle_data.ts | 213 ++++++++++ 16 files changed, 1291 insertions(+), 115 deletions(-) create mode 100644 packages/firestore/src/api/bundle.ts create mode 100644 packages/firestore/test/integration/api_internal/bundle.test.ts create mode 100644 packages/firestore/test/unit/specs/bundle_spec.test.ts create mode 100644 packages/firestore/test/unit/util/bundle_data.ts diff --git a/packages/firestore-types/index.d.ts b/packages/firestore-types/index.d.ts index 2346aebf2eb..9b2bc043cce 100644 --- a/packages/firestore-types/index.d.ts +++ b/packages/firestore-types/index.d.ts @@ -93,9 +93,40 @@ export class FirebaseFirestore { terminate(): Promise; + loadBundle( + bundleData: ArrayBuffer | ReadableStream | string + ): LoadBundleTask; + INTERNAL: { delete: () => Promise }; } +export interface LoadBundleTask { + onProgress( + next?: (progress: LoadBundleTaskProgress) => any, + error?: (error: Error) => any, + complete?: () => void + ): void; + + then( + onFulfilled?: (a: LoadBundleTaskProgress) => T | PromiseLike, + onRejected?: (a: Error) => R | PromiseLike + ): Promise; + + catch( + onRejected: (a: Error) => R | PromiseLike + ): Promise; +} + +export interface LoadBundleTaskProgress { + documentsLoaded: number; + totalDocuments: number; + bytesLoaded: number; + totalBytes: number; + taskState: TaskState; +} + +export type TaskState = 'Error' | 'Running' | 'Success'; + export class GeoPoint { constructor(latitude: number, longitude: number); diff --git a/packages/firestore/src/api/bundle.ts b/packages/firestore/src/api/bundle.ts new file mode 100644 index 00000000000..703a5d06482 --- /dev/null +++ b/packages/firestore/src/api/bundle.ts @@ -0,0 +1,117 @@ +/** + * @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 firestore from '@firebase/firestore-types'; +import { Deferred } from '../util/promise'; +import { PartialObserver } from './observer'; +import { debugAssert } from '../util/assert'; + +export class LoadBundleTask + implements + firestore.LoadBundleTask, + PromiseLike { + private _progressObserver: PartialObserver< + firestore.LoadBundleTaskProgress + > = {}; + private _taskCompletionResolver = new Deferred< + firestore.LoadBundleTaskProgress + >(); + + private _lastProgress: firestore.LoadBundleTaskProgress = { + taskState: 'Running', + totalBytes: 0, + totalDocuments: 0, + bytesLoaded: 0, + documentsLoaded: 0 + }; + + onProgress( + next?: (progress: firestore.LoadBundleTaskProgress) => unknown, + error?: (err: Error) => unknown, + complete?: () => void + ): void { + this._progressObserver = { + next, + error, + complete + }; + } + + catch( + onRejected: (a: Error) => R | PromiseLike + ): Promise { + return this._taskCompletionResolver.promise.catch(onRejected); + } + + then( + onFulfilled?: (a: firestore.LoadBundleTaskProgress) => T | PromiseLike, + onRejected?: (a: Error) => R | PromiseLike + ): Promise { + return this._taskCompletionResolver.promise.then(onFulfilled, onRejected); + } + + /** + * Notifies all observers that bundle loading has completed, with a provided + * `LoadBundleTaskProgress` object. + */ + _completeWith(progress: firestore.LoadBundleTaskProgress): void { + debugAssert( + progress.taskState === 'Success', + 'Task is not completed with Success.' + ); + this._updateProgress(progress); + if (this._progressObserver.complete) { + this._progressObserver.complete(); + } + + this._taskCompletionResolver.resolve(progress); + } + + /** + * Notifies all observers that bundle loading has failed, with a provided + * `Error` as the reason. + */ + _failWith(error: Error): void { + this._lastProgress.taskState = 'Error'; + + if (this._progressObserver.next) { + this._progressObserver.next(this._lastProgress); + } + + if (this._progressObserver.error) { + this._progressObserver.error(error); + } + + this._taskCompletionResolver.reject(error); + } + + /** + * Notifies a progress update of loading a bundle. + * @param progress The new progress. + */ + _updateProgress(progress: firestore.LoadBundleTaskProgress): void { + debugAssert( + this._lastProgress.taskState === 'Running', + 'Cannot update progress on a completed or failed task' + ); + + this._lastProgress = progress; + if (this._progressObserver.next) { + this._progressObserver.next(progress); + } + } +} diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 248bcbc8e53..a7b4af44453 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -493,6 +493,13 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService { }; } + loadBundle( + bundleData: ArrayBuffer | ReadableStream | string + ): firestore.LoadBundleTask { + this.ensureClientConfigured(); + return this._firestoreClient!.loadBundle(bundleData); + } + ensureClientConfigured(): FirestoreClient { if (!this._firestoreClient) { // Kick off starting the client but don't actually wait for it. diff --git a/packages/firestore/src/core/bundle.ts b/packages/firestore/src/core/bundle.ts index cb652e8dbb9..55f00ffb39b 100644 --- a/packages/firestore/src/core/bundle.ts +++ b/packages/firestore/src/core/bundle.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import * as firestore from '@firebase/firestore-types'; import { Query } from './query'; import { SnapshotVersion } from './snapshot_version'; import { @@ -28,6 +29,14 @@ import * as api from '../protos/firestore_proto_api'; import { DocumentKey } from '../model/document_key'; import { MaybeDocument, NoDocument } from '../model/document'; import { debugAssert } from '../util/assert'; +import { + applyBundleDocuments, + LocalStore, + saveNamedQuery +} from '../local/local_store'; +import { SizedBundleElement } from '../util/bundle_reader'; +import { MaybeDocumentMap } from '../model/collections'; +import { BundleMetadata } from '../protos/firestore_bundle_proto'; /** * Represents a Firestore bundle saved by the SDK in its local storage. @@ -58,7 +67,7 @@ export interface NamedQuery { */ interface BundledDocument { metadata: bundleProto.BundledDocumentMetadata; - document: api.Document | undefined; + document?: api.Document; } /** @@ -98,3 +107,127 @@ export class BundleConverter { return fromVersion(time); } } + +/** + * Returns a `LoadBundleTaskProgress` representing the initial progress of + * loading a bundle. + */ +export function bundleInitialProgress( + metadata: BundleMetadata +): firestore.LoadBundleTaskProgress { + return { + taskState: 'Running', + documentsLoaded: 0, + bytesLoaded: 0, + totalDocuments: metadata.totalDocuments!, + totalBytes: metadata.totalBytes! + }; +} + +/** + * Returns a `LoadBundleTaskProgress` representing the progress that the loading + * has succeeded. + */ +export function bundleSuccessProgress( + metadata: BundleMetadata +): firestore.LoadBundleTaskProgress { + return { + taskState: 'Success', + documentsLoaded: metadata.totalDocuments!, + bytesLoaded: metadata.totalBytes!, + totalDocuments: metadata.totalDocuments!, + totalBytes: metadata.totalBytes! + }; +} + +export class BundleLoadResult { + constructor( + readonly progress: firestore.LoadBundleTaskProgress, + readonly changedDocs: MaybeDocumentMap + ) {} +} + +/** + * A class to process the elements from a bundle, load them into local + * storage and provide progress update while loading. + */ +export class BundleLoader { + /** The current progress of loading */ + private progress: firestore.LoadBundleTaskProgress; + /** Batched queries to be saved into storage */ + private queries: bundleProto.NamedQuery[] = []; + /** Batched documents to be saved into storage */ + private documents: BundledDocuments = []; + + constructor( + private metadata: bundleProto.BundleMetadata, + private localStore: LocalStore + ) { + this.progress = bundleInitialProgress(metadata); + } + + /** + * Adds an element from the bundle to the loader. + * + * Returns a new progress if adding the element leads to a new progress, + * otherwise returns null. + */ + addSizedElement( + element: SizedBundleElement + ): firestore.LoadBundleTaskProgress | null { + debugAssert(!element.isBundleMetadata(), 'Unexpected bundle metadata.'); + + this.progress.bytesLoaded += element.byteLength; + + let documentsLoaded = this.progress.documentsLoaded; + + if (element.payload.namedQuery) { + this.queries.push(element.payload.namedQuery); + } else if (element.payload.documentMetadata) { + this.documents.push({ metadata: element.payload.documentMetadata }); + if (!element.payload.documentMetadata.exists) { + ++documentsLoaded; + } + } else if (element.payload.document) { + debugAssert( + this.documents.length > 0 && + this.documents[this.documents.length - 1].metadata.name === + element.payload.document.name, + 'The document being added does not match the stored metadata.' + ); + this.documents[this.documents.length - 1].document = + element.payload.document; + ++documentsLoaded; + } + + if (documentsLoaded !== this.progress.documentsLoaded) { + this.progress.documentsLoaded = documentsLoaded; + return { ...this.progress }; + } + + return null; + } + + /** + * Update the progress to 'Success' and return the updated progress. + */ + async complete(): Promise { + debugAssert( + this.documents[this.documents.length - 1]?.metadata.exists !== true || + !!this.documents[this.documents.length - 1].document, + 'Bundled documents ends with a document metadata and missing document.' + ); + + for (const q of this.queries) { + await saveNamedQuery(this.localStore, q); + } + + const changedDocs = await applyBundleDocuments( + this.localStore, + this.documents + ); + + this.progress.taskState = 'Success'; + return new BundleLoadResult({ ...this.progress }, changedDocs); + } +} diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 1910865521a..533e8bd4a75 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import * as firestore from '@firebase/firestore-types'; import { CredentialsProvider } from '../api/credentials'; import { User } from '../auth/user'; import { LocalStore } from '../local/local_store'; @@ -26,7 +27,7 @@ import { newDatastore } from '../remote/datastore'; import { RemoteStore } from '../remote/remote_store'; import { AsyncQueue, wrapInUserErrorIfRecoverable } from '../util/async_queue'; import { Code, FirestoreError } from '../util/error'; -import { logDebug } from '../util/log'; +import { logDebug, logWarn } from '../util/log'; import { Deferred } from '../util/promise'; import { EventManager, @@ -34,7 +35,7 @@ import { Observer, QueryListener } from './event_manager'; -import { SyncEngine } from './sync_engine'; +import { SyncEngine, loadBundle } from './sync_engine'; import { View } from './view'; import { SharedClientState } from '../local/shared_client_state'; @@ -47,8 +48,11 @@ import { ComponentProvider, MemoryComponentProvider } from './component_provider'; +import { BundleReader } from '../util/bundle_reader'; +import { LoadBundleTask } from '../api/bundle'; import { newConnection } from '../platform/connection'; import { newSerializer } from '../platform/serializer'; +import { toByteStreamReader } from '../platform/byte_stream_reader'; const LOG_TAG = 'FirestoreClient'; const MAX_CONCURRENT_LIMBO_RESOLUTIONS = 100; @@ -512,4 +516,27 @@ export class FirestoreClient { }); return deferred.promise; } + + loadBundle( + data: ReadableStream | ArrayBuffer | string + ): firestore.LoadBundleTask { + this.verifyNotTerminated(); + + let content: ReadableStream | ArrayBuffer; + if (typeof data === 'string') { + content = new TextEncoder().encode(data); + } else { + content = data; + } + const reader = new BundleReader(toByteStreamReader(content)); + const task = new LoadBundleTask(); + this.asyncQueue.enqueueAndForget(async () => { + loadBundle(this.syncEngine, reader, task); + return task.catch(e => { + logWarn(LOG_TAG, `Loading bundle failed with ${e}`); + }); + }); + + return task; + } } diff --git a/packages/firestore/src/core/sync_engine.ts b/packages/firestore/src/core/sync_engine.ts index a4a289781bb..759af7c2a9b 100644 --- a/packages/firestore/src/core/sync_engine.ts +++ b/packages/firestore/src/core/sync_engine.ts @@ -17,9 +17,11 @@ import { User } from '../auth/user'; import { + hasNewerBundle, ignoreIfPrimaryLeaseLoss, LocalStore, - MultiTabLocalStore + MultiTabLocalStore, + saveBundle } from '../local/local_store'; import { LocalViewChanges } from '../local/local_view_changes'; import { ReferenceSet } from '../local/reference_set'; @@ -36,7 +38,7 @@ import { BATCHID_UNKNOWN, MutationBatchResult } from '../model/mutation_batch'; import { RemoteEvent, TargetChange } from '../remote/remote_event'; import { RemoteStore } from '../remote/remote_store'; import { RemoteSyncer } from '../remote/remote_syncer'; -import { debugAssert, fail, hardAssert } from '../util/assert'; +import { debugAssert, debugCast, fail, hardAssert } from '../util/assert'; import { Code, FirestoreError } from '../util/error'; import { logDebug } from '../util/log'; import { primitiveComparator } from '../util/misc'; @@ -74,7 +76,14 @@ import { import { ViewSnapshot } from './view_snapshot'; import { AsyncQueue, wrapInUserErrorIfRecoverable } from '../util/async_queue'; import { TransactionRunner } from './transaction_runner'; +import { BundleReader } from '../util/bundle_reader'; +import { + BundleLoader, + bundleInitialProgress, + bundleSuccessProgress +} from './bundle'; import { Datastore } from '../remote/datastore'; +import { LoadBundleTask } from '../api/bundle'; const LOG_TAG = 'SyncEngine'; @@ -274,7 +283,7 @@ class SyncEngineImpl implements SyncEngine { private onlineState = OnlineState.Unknown; constructor( - protected localStore: LocalStore, + public localStore: LocalStore, protected remoteStore: RemoteStore, protected datastore: Datastore, // PORTING NOTE: Manages state synchronization in multi-tab environments. @@ -837,7 +846,7 @@ class SyncEngineImpl implements SyncEngine { return this.enqueuedLimboResolutions; } - protected async emitNewSnapsAndNotifyLocalStore( + async emitNewSnapsAndNotifyLocalStore( changes: MaybeDocumentMap, remoteEvent?: RemoteEvent ): Promise { @@ -901,7 +910,7 @@ class SyncEngineImpl implements SyncEngine { await this.localStore.notifyLocalViewChanges(docChangesInAllViews); } - protected assertSubscribed(fnName: string): void { + assertSubscribed(fnName: string): void { debugAssert( this.syncEngineListener !== null, 'Trying to call ' + fnName + ' before calling subscribe().' @@ -1005,7 +1014,7 @@ class MultiTabSyncEngineImpl extends SyncEngineImpl { private _isPrimaryClient: undefined | boolean = undefined; constructor( - protected localStore: MultiTabLocalStore, + public localStore: MultiTabLocalStore, remoteStore: RemoteStore, datastore: Datastore, sharedClientState: SharedClientState, @@ -1371,3 +1380,70 @@ export function newMultiTabSyncEngine( maxConcurrentLimboResolutions ); } + +/** + * Loads a Firestore bundle into the SDK. The returned promise resolves when + * the bundle finished loading. + * + * @param bundleReader Bundle to load into the SDK. + * @param task LoadBundleTask used to update the loading progress to public API. + */ +export function loadBundle( + syncEngine: SyncEngine, + bundleReader: BundleReader, + task: LoadBundleTask +): void { + const syncEngineImpl = debugCast(syncEngine, SyncEngineImpl); + syncEngineImpl.assertSubscribed('loadBundle()'); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + loadBundleImpl(syncEngineImpl, bundleReader, task); +} + +async function loadBundleImpl( + syncEngine: SyncEngineImpl, + reader: BundleReader, + task: LoadBundleTask +): Promise { + try { + const metadata = await reader.getMetadata(); + const skip = await hasNewerBundle(syncEngine.localStore, metadata); + if (skip) { + await reader.close(); + task._completeWith(bundleSuccessProgress(metadata)); + return; + } + + task._updateProgress(bundleInitialProgress(metadata)); + + const loader = new BundleLoader(metadata, syncEngine.localStore); + let element = await reader.nextElement(); + while (element) { + debugAssert( + !element.payload.metadata, + 'Unexpected BundleMetadata element.' + ); + const progress = await loader.addSizedElement(element); + if (progress) { + task._updateProgress(progress); + } + + element = await reader.nextElement(); + } + + const result = await loader.complete(); + // TODO(b/160876443): This currently raises snapshots with + // `fromCache=false` if users already listen to some queries and bundles + // has newer version. + await syncEngine.emitNewSnapsAndNotifyLocalStore( + result.changedDocs, + /* remoteEvent */ undefined + ); + + // Save metadata, so loading the same bundle will skip. + await saveBundle(syncEngine.localStore, metadata); + task._completeWith(result.progress); + } catch (e) { + task._failWith(e); + } +} diff --git a/packages/firestore/src/local/local_store.ts b/packages/firestore/src/local/local_store.ts index 5bc3217edb0..9e889165111 100644 --- a/packages/firestore/src/local/local_store.ts +++ b/packages/firestore/src/local/local_store.ts @@ -1296,7 +1296,7 @@ export function applyBundleDocuments( }); return localStoreImpl.persistence.runTransaction( 'Apply bundle documents', - 'readwrite-primary', + 'readwrite', txn => { return localStoreImpl .populateDocumentChangeBuffer( @@ -1341,7 +1341,7 @@ export function hasNewerBundle( ); }) .then(cached => { - return !!cached && cached.createTime!.compareTo(currentReadTime) > 0; + return !!cached && cached.createTime!.compareTo(currentReadTime) >= 0; }); } diff --git a/packages/firestore/src/platform/byte_stream_reader.ts b/packages/firestore/src/platform/byte_stream_reader.ts index 8665a97e79b..49aac60e9ff 100644 --- a/packages/firestore/src/platform/byte_stream_reader.ts +++ b/packages/firestore/src/platform/byte_stream_reader.ts @@ -20,10 +20,11 @@ import * as node from './node/byte_stream_reader'; import * as rn from './rn/byte_stream_reader'; import * as browser from './browser/byte_stream_reader'; import { isNode, isReactNative } from '@firebase/util'; +import { DEFAULT_BYTES_PER_READ } from '../util/byte_stream'; export function toByteStreamReader( source: BundleSource, - bytesPerRead: number + bytesPerRead: number = DEFAULT_BYTES_PER_READ ): ReadableStreamReader { if (isNode()) { return node.toByteStreamReader(source, bytesPerRead); diff --git a/packages/firestore/src/util/bundle_reader.ts b/packages/firestore/src/util/bundle_reader.ts index 4d46864ae08..e426fa0c825 100644 --- a/packages/firestore/src/util/bundle_reader.ts +++ b/packages/firestore/src/util/bundle_reader.ts @@ -93,6 +93,10 @@ export class BundleReader { ); } + close(): Promise { + return this.reader.cancel(); + } + /** * Returns the metadata of the bundle. */ diff --git a/packages/firestore/src/util/byte_stream.ts b/packages/firestore/src/util/byte_stream.ts index 3434e410270..a2723a82528 100644 --- a/packages/firestore/src/util/byte_stream.ts +++ b/packages/firestore/src/util/byte_stream.ts @@ -17,6 +17,13 @@ import { debugAssert } from './assert'; +/** + * How many bytes to read each time when `ReadableStreamReader.read()` is + * called. Only applicable for byte streams that we control (e.g. those backed + * by an UInt8Array). + */ +export const DEFAULT_BYTES_PER_READ = 10240; + /** * Builds a `ByteStreamReader` from a UInt8Array. * @param source The data source to use. @@ -25,7 +32,7 @@ import { debugAssert } from './assert'; */ export function toByteStreamReaderHelper( source: Uint8Array, - bytesPerRead: number + bytesPerRead: number = DEFAULT_BYTES_PER_READ ): ReadableStreamReader { debugAssert( bytesPerRead > 0, diff --git a/packages/firestore/test/integration/api_internal/bundle.test.ts b/packages/firestore/test/integration/api_internal/bundle.test.ts new file mode 100644 index 00000000000..b0e18b5b1cf --- /dev/null +++ b/packages/firestore/test/integration/api_internal/bundle.test.ts @@ -0,0 +1,241 @@ +/** + * @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 firestore from '@firebase/firestore-types'; +import { expect } from 'chai'; +import { + apiDescribe, + toDataArray, + withAlternateTestDb, + withTestDb +} from '../util/helpers'; +import { DatabaseId } from '../../../src/core/database_info'; +import { key } from '../../util/helpers'; +import { EventsAccumulator } from '../util/events_accumulator'; +import { TestBundleBuilder } from '../../unit/util/bundle_data'; + +function verifySuccessProgress(p: firestore.LoadBundleTaskProgress): void { + expect(p.taskState).to.equal('Success'); + expect(p.bytesLoaded).to.be.equal(p.totalBytes); + expect(p.documentsLoaded).to.equal(p.totalDocuments); +} + +function verifyInProgress( + p: firestore.LoadBundleTaskProgress, + expectedDocuments: number +): void { + expect(p.taskState).to.equal('Running'); + expect(p.bytesLoaded <= p.totalBytes).to.be.true; + expect(p.documentsLoaded <= p.totalDocuments).to.be.true; + expect(p.documentsLoaded).to.equal(expectedDocuments); +} + +apiDescribe('Bundles', (persistence: boolean) => { + const encoder = new TextEncoder(); + const testDocs: { [key: string]: firestore.DocumentData } = { + a: { k: { stringValue: 'a' }, bar: { integerValue: 1 } }, + b: { k: { stringValue: 'b' }, bar: { integerValue: 2 } } + }; + + function bundleWithTestDocs( + db: firestore.FirebaseFirestore + ): TestBundleBuilder { + const a = key('coll-1/a'); + const b = key('coll-1/b'); + const builder = new TestBundleBuilder( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (db as any)._databaseId as DatabaseId + ); + builder.addDocumentMetadata(a, { seconds: 1000, nanos: 9999 }, true); + builder.addDocument( + a, + { seconds: 1, nanos: 9 }, + { seconds: 1, nanos: 9 }, + testDocs.a + ); + builder.addDocumentMetadata(b, { seconds: 1000, nanos: 9999 }, true); + builder.addDocument( + b, + { seconds: 1, nanos: 9 }, + { seconds: 1, nanos: 9 }, + testDocs.b + ); + + return builder; + } + + function verifySnapEqualTestDocs(snap: firestore.QuerySnapshot): void { + expect(toDataArray(snap)).to.deep.equal([ + { k: 'a', bar: 1 }, + { k: 'b', bar: 2 } + ]); + } + + it('load with documents only with on progress and promise interface', () => { + return withTestDb(persistence, async db => { + const builder = bundleWithTestDocs(db); + + const progressEvents: firestore.LoadBundleTaskProgress[] = []; + let completeCalled = false; + const task = db.loadBundle( + builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) + ); + task.onProgress( + progress => { + progressEvents.push(progress); + }, + undefined, + () => { + completeCalled = true; + } + ); + await task; + let fulfillProgress: firestore.LoadBundleTaskProgress; + await task.then(progress => { + fulfillProgress = progress; + }); + + expect(completeCalled).to.be.true; + expect(progressEvents.length).to.equal(4); + verifyInProgress(progressEvents[0], 0); + verifyInProgress(progressEvents[1], 1); + verifyInProgress(progressEvents[2], 2); + verifySuccessProgress(progressEvents[3]); + expect(fulfillProgress!).to.deep.equal(progressEvents[3]); + + // Read from cache. These documents do not exist in backend, so they can + // only be read from cache. + const snap = await db.collection('coll-1').get({ source: 'cache' }); + verifySnapEqualTestDocs(snap); + }); + }); + + it('load with documents with promise interface', () => { + return withTestDb(persistence, async db => { + const builder = bundleWithTestDocs(db); + + const fulfillProgress: firestore.LoadBundleTaskProgress = await db.loadBundle( + builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) + ); + + verifySuccessProgress(fulfillProgress!); + + // Read from cache. These documents do not exist in backend, so they can + // only be read from cache. + const snap = await db.collection('coll-1').get({ source: 'cache' }); + verifySnapEqualTestDocs(snap); + }); + }); + + it('load for a second time skips', () => { + return withTestDb(persistence, async db => { + const builder = bundleWithTestDocs(db); + + await db.loadBundle( + builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) + ); + + let completeCalled = false; + const progressEvents: firestore.LoadBundleTaskProgress[] = []; + const task = db.loadBundle( + encoder.encode( + builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) + ) + ); + task.onProgress( + progress => { + progressEvents.push(progress); + }, + error => {}, + () => { + completeCalled = true; + } + ); + await task; + + expect(completeCalled).to.be.true; + // No loading actually happened in the second `loadBundle` call only the + // success progress is recorded. + expect(progressEvents.length).to.equal(1); + verifySuccessProgress(progressEvents[0]); + + // Read from cache. These documents do not exist in backend, so they can + // only be read from cache. + const snap = await db.collection('coll-1').get({ source: 'cache' }); + verifySnapEqualTestDocs(snap); + }); + }); + + it('load with documents already pulled from backend', () => { + return withTestDb(persistence, async db => { + await db.doc('coll-1/a').set({ k: 'a', bar: 0 }); + await db.doc('coll-1/b').set({ k: 'b', bar: 0 }); + + const accumulator = new EventsAccumulator(); + db.collection('coll-1').onSnapshot(accumulator.storeEvent); + await accumulator.awaitEvent(); + + const builder = bundleWithTestDocs(db); + const progress = await db.loadBundle( + // Testing passing in non-string bundles. + encoder.encode( + builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) + ) + ); + + verifySuccessProgress(progress); + // The test bundle is holding ancient documents, so no events are + // generated as a result. The case where a bundle has newer doc than + // cache can only be tested in spec tests. + await accumulator.assertNoAdditionalEvents(); + + const snap = await db.collection('coll-1').get(); + expect(toDataArray(snap)).to.deep.equal([ + { k: 'a', bar: 0 }, + { k: 'b', bar: 0 } + ]); + }); + }); + + it('load with documents from other projects fails', () => { + return withTestDb(persistence, async db => { + let builder = bundleWithTestDocs(db); + return withAlternateTestDb(persistence, async otherDb => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + await expect( + otherDb.loadBundle( + builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) + ) + ).to.be.rejectedWith('Tried to deserialize key from different project'); + + // Verify otherDb still functions, despite loaded a problematic bundle. + builder = bundleWithTestDocs(otherDb); + const finalProgress = await otherDb.loadBundle( + builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) + ); + verifySuccessProgress(finalProgress); + + // Read from cache. These documents do not exist in backend, so they can + // only be read from cache. + const snap = await otherDb + .collection('coll-1') + .get({ source: 'cache' }); + verifySnapEqualTestDocs(snap); + }); + }); + }); +}); diff --git a/packages/firestore/test/unit/specs/bundle_spec.test.ts b/packages/firestore/test/unit/specs/bundle_spec.test.ts new file mode 100644 index 00000000000..fa7b08d8c45 --- /dev/null +++ b/packages/firestore/test/unit/specs/bundle_spec.test.ts @@ -0,0 +1,373 @@ +/** + * @license + * Copyright 2017 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 '../../../src/core/query'; +import { + doc, + path, + TestSnapshotVersion, + version, + wrapObject +} from '../../util/helpers'; + +import { describeSpec, specTest } from './describe_spec'; +import { client, spec } from './spec_builder'; +import { TestBundleBuilder } from '../util/bundle_data'; +import { + JSON_SERIALIZER, + TEST_DATABASE_ID +} from '../local/persistence_test_helpers'; +import { DocumentKey } from '../../../src/model/document_key'; +import { toVersion } from '../../../src/remote/serializer'; +import { JsonObject } from '../../../src/model/object_value'; + +interface TestBundleDocument { + key: DocumentKey; + readTime: TestSnapshotVersion; + createTime?: TestSnapshotVersion; + updateTime?: TestSnapshotVersion; + content?: JsonObject; +} + +function bundleWithDocument(testDoc: TestBundleDocument): string { + const builder = new TestBundleBuilder(TEST_DATABASE_ID); + builder.addDocumentMetadata( + testDoc.key, + toVersion(JSON_SERIALIZER, version(testDoc.readTime)), + !!testDoc.createTime + ); + if (testDoc.createTime) { + builder.addDocument( + testDoc.key, + toVersion(JSON_SERIALIZER, version(testDoc.createTime)), + toVersion(JSON_SERIALIZER, version(testDoc.updateTime!)), + wrapObject(testDoc.content!).proto.mapValue.fields! + ); + } + return builder.build( + 'test-bundle', + toVersion(JSON_SERIALIZER, version(testDoc.readTime)) + ); +} + +describeSpec('Bundles:', ['no-ios', 'no-android'], () => { + specTest('Newer docs from bundles should overwrite cache', [], () => { + const query1 = Query.atPath(path('collection')); + const docA = doc('collection/a', 1000, { value: 'a' }); + const docAChanged = doc('collection/a', 2999, { value: 'b' }); + + const bundleString = bundleWithDocument({ + key: docA.key, + readTime: 3000, + createTime: 1999, + updateTime: 2999, + content: { value: 'b' } + }); + + return ( + spec() + .userListens(query1) + .watchAcksFull(query1, 1000, docA) + .expectEvents(query1, { added: [docA] }) + // TODO(b/160876443): This currently raises snapshots with + // `fromCache=false` if users already listen to some queries and bundles + // has newer version. + .loadBundle(bundleString) + .expectEvents(query1, { modified: [docAChanged] }) + ); + }); + + specTest( + 'Newer deleted docs from bundles should delete cached docs', + [], + () => { + const query1 = Query.atPath(path('collection')); + const docA = doc('collection/a', 1000, { value: 'a' }); + + const bundleString = bundleWithDocument({ + key: docA.key, + readTime: 3000 + }); + + return spec() + .userListens(query1) + .watchAcksFull(query1, 1000, docA) + .expectEvents(query1, { added: [docA] }) + .loadBundle(bundleString) + .expectEvents(query1, { removed: [docA] }); + } + ); + + specTest('Older deleted docs from bundles should do nothing', [], () => { + const query1 = Query.atPath(path('collection')); + const docA = doc('collection/a', 1000, { value: 'a' }); + + const bundleString = bundleWithDocument({ + key: docA.key, + readTime: 999 + }); + + return ( + spec() + .userListens(query1) + .watchAcksFull(query1, 1000, docA) + .expectEvents(query1, { added: [docA] }) + // No events are expected here. + .loadBundle(bundleString) + ); + }); + + specTest( + 'Newer docs from bundles should raise snapshot only when Watch catches up with acknowledged writes', + [], + () => { + const query = Query.atPath(path('collection')); + const docA = doc('collection/a', 250, { value: 'a' }); + + const bundleBeforeMutationAck = bundleWithDocument({ + key: docA.key, + readTime: 500, + createTime: 250, + updateTime: 500, + content: { value: 'b' } + }); + + const bundleAfterMutationAck = bundleWithDocument({ + key: docA.key, + readTime: 1001, + createTime: 250, + updateTime: 1001, + content: { value: 'fromBundle' } + }); + return ( + spec() + // TODO(b/160878667): Figure out what happens when memory eager GC is on + // a bundle is loaded. + .withGCEnabled(false) + .userListens(query) + .watchAcksFull(query, 250, docA) + .expectEvents(query, { + added: [doc('collection/a', 250, { value: 'a' })] + }) + .userPatches('collection/a', { value: 'patched' }) + .expectEvents(query, { + modified: [ + doc( + 'collection/a', + 250, + { value: 'patched' }, + { hasLocalMutations: true } + ) + ], + hasPendingWrites: true + }) + .writeAcks('collection/a', 1000) + // loading bundleBeforeMutationAck will not raise snapshots, because its + // snapshot version is older than the acknowledged mutation. + .loadBundle(bundleBeforeMutationAck) + // loading bundleAfterMutationAck will raise a snapshot, because it is after + // the acknowledged mutation. + .loadBundle(bundleAfterMutationAck) + .expectEvents(query, { + modified: [doc('collection/a', 1001, { value: 'fromBundle' })] + }) + ); + } + ); + + specTest( + 'Newer docs from bundles should keep not raise snapshot if there are unacknowledged writes', + [], + () => { + const query = Query.atPath(path('collection')); + const docA = doc('collection/a', 250, { value: 'a' }); + + const bundleString = bundleWithDocument({ + key: docA.key, + readTime: 1001, + createTime: 250, + updateTime: 1001, + content: { value: 'fromBundle' } + }); + + return ( + spec() + .withGCEnabled(false) + .userListens(query) + .watchAcksFull(query, 250, docA) + .expectEvents(query, { + added: [doc('collection/a', 250, { value: 'a' })] + }) + .userPatches('collection/a', { value: 'patched' }) + .expectEvents(query, { + modified: [ + doc( + 'collection/a', + 250, + { value: 'patched' }, + { hasLocalMutations: true } + ) + ], + hasPendingWrites: true + }) + // Loading the bundle will not raise snapshots, because the + // mutation has not been acknowledged. + .loadBundle(bundleString) + ); + } + ); + + specTest('Newer docs from bundles might lead to limbo doc', [], () => { + const query = Query.atPath(path('collection')); + const docA = doc('collection/a', 1000, { value: 'a' }); + const bundleString1 = bundleWithDocument({ + key: docA.key, + readTime: 500, + createTime: 250, + updateTime: 500, + content: { value: 'b' } + }); + const limboQuery = Query.atPath(docA.key.path); + + return ( + spec() + .withGCEnabled(false) + .userListens(query) + .watchAcksFull(query, 250) + // Backend tells is there is no such doc. + .expectEvents(query, {}) + // Bundle tells otherwise, leads to limbo. + .loadBundle(bundleString1) + .expectLimboDocs(docA.key) + .expectEvents(query, { + added: [doc('collection/a', 500, { value: 'b' })], + fromCache: true + }) + // .watchAcksFull(limboQuery, 1002, docA1) + .watchAcks(limboQuery) + .watchSends({ affects: [limboQuery] }) + .watchCurrents(limboQuery, 'resume-token-1002') + .watchSnapshots(1002) + .expectLimboDocs() + .expectEvents(query, { + removed: [doc('collection/a', 500, { value: 'b' })], + fromCache: false + }) + ); + }); + + specTest( + 'Load from secondary clients and observe from primary', + ['multi-client'], + () => { + const query = Query.atPath(path('collection')); + const docA = doc('collection/a', 250, { value: 'a' }); + const bundleString1 = bundleWithDocument({ + key: docA.key, + readTime: 500, + createTime: 250, + updateTime: 500, + content: { value: 'b' } + }); + + return client(0) + .userListens(query) + .watchAcksFull(query, 250, docA) + .expectEvents(query, { + added: [docA] + }) + .client(1) + .loadBundle(bundleString1) + .client(0) + .becomeVisible(); + // TODO(wuandy): Loading from secondary client does not notify other + // clients for now. We need to fix it and uncomment below. + // .expectEvents(query, { + // modified: [doc('collection/a', 500, { value: 'b' })], + // }) + } + ); + + specTest( + 'Load and observe from same secondary client', + ['multi-client'], + () => { + const query = Query.atPath(path('collection')); + const docA = doc('collection/a', 250, { value: 'a' }); + const bundleString1 = bundleWithDocument({ + key: docA.key, + readTime: 500, + createTime: 250, + updateTime: 500, + content: { value: 'b' } + }); + + return client(0) + .userListens(query) + .watchAcksFull(query, 250, docA) + .expectEvents(query, { + added: [docA] + }) + .client(1) + .userListens(query) + .expectEvents(query, { + added: [docA] + }) + .loadBundle(bundleString1) + .expectEvents(query, { + modified: [doc('collection/a', 500, { value: 'b' })] + }); + } + ); + + specTest( + 'Load from primary client and observe from secondary', + ['multi-client'], + () => { + const query = Query.atPath(path('collection')); + const docA = doc('collection/a', 250, { value: 'a' }); + const bundleString1 = bundleWithDocument({ + key: docA.key, + readTime: 500, + createTime: 250, + updateTime: 500, + content: { value: 'b' } + }); + + return client(0) + .userListens(query) + .watchAcksFull(query, 250, docA) + .expectEvents(query, { + added: [docA] + }) + .client(1) + .userListens(query) + .expectEvents(query, { + added: [docA] + }) + .client(0) + .loadBundle(bundleString1) + .expectEvents(query, { + modified: [doc('collection/a', 500, { value: 'b' })] + }) + .client(1) + .expectEvents(query, { + modified: [doc('collection/a', 500, { value: 'b' })] + }); + } + ); +}); diff --git a/packages/firestore/test/unit/specs/spec_builder.ts b/packages/firestore/test/unit/specs/spec_builder.ts index af29c65f2dd..1f46c387174 100644 --- a/packages/firestore/test/unit/specs/spec_builder.ts +++ b/packages/firestore/test/unit/specs/spec_builder.ts @@ -354,6 +354,14 @@ export class SpecBuilder { return this; } + loadBundle(bundleContent: string): this { + this.nextStep(); + this.currentStep = { + loadBundle: bundleContent + }; + return this; + } + // PORTING NOTE: Only used by web multi-tab tests. becomeHidden(): this { this.nextStep(); diff --git a/packages/firestore/test/unit/specs/spec_test_runner.ts b/packages/firestore/test/unit/specs/spec_test_runner.ts index 9317443718b..af3780bf265 100644 --- a/packages/firestore/test/unit/specs/spec_test_runner.ts +++ b/packages/firestore/test/unit/specs/spec_test_runner.ts @@ -27,7 +27,7 @@ import { } from '../../../src/core/event_manager'; import { Query } from '../../../src/core/query'; import { SnapshotVersion } from '../../../src/core/snapshot_version'; -import { SyncEngine } from '../../../src/core/sync_engine'; +import { loadBundle, SyncEngine } from '../../../src/core/sync_engine'; import { TargetId } from '../../../src/core/types'; import { ChangeType, @@ -116,12 +116,16 @@ import { SharedWriteTracker } from './spec_test_components'; import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence'; +import { BundleReader } from '../../../src/util/bundle_reader'; +import { LoadBundleTask } from '../../../src/api/bundle'; import { encodeBase64 } from '../../../src/platform/base64'; import { FakeDocument, SharedFakeWebStorage, testWindow } from '../../util/test_platform'; +import { toByteStreamReader } from '../../../src/platform/byte_stream_reader'; +import { logWarn } from '../../../src/util/log'; const ARBITRARY_SEQUENCE_NUMBER = 2; @@ -311,6 +315,8 @@ abstract class TestRunner { return this.doAddSnapshotsInSyncListener(); } else if ('removeSnapshotsInSyncListener' in step) { return this.doRemoveSnapshotsInSyncListener(); + } else if ('loadBundle' in step) { + return this.doLoadBundle(step.loadBundle!); } else if ('watchAck' in step) { return this.doWatchAck(step.watchAck!); } else if ('watchCurrent' in step) { @@ -448,6 +454,19 @@ abstract class TestRunner { return Promise.resolve(); } + private async doLoadBundle(bundle: string): Promise { + const reader = new BundleReader( + toByteStreamReader(new TextEncoder().encode(bundle)) + ); + const task = new LoadBundleTask(); + return this.queue.enqueue(async () => { + loadBundle(this.syncEngine, reader, task); + await task.catch(e => { + logWarn(`Loading bundle failed with ${e}`); + }); + }); + } + private doMutations(mutations: Mutation[]): Promise { const documentKeys = mutations.map(val => val.key.path.toString()); const syncEngineCallback = new Deferred(); @@ -1252,6 +1271,8 @@ export interface SpecStep { addSnapshotsInSyncListener?: true; /** Unlistens from a SnapshotsInSync event. */ removeSnapshotsInSyncListener?: true; + /** Loads a bundle from a string. */ + loadBundle?: string; /** Ack for a query in the watch stream */ watchAck?: SpecWatchAck; diff --git a/packages/firestore/test/unit/util/bundle.test.ts b/packages/firestore/test/unit/util/bundle.test.ts index 172df106761..9d6527b5fc2 100644 --- a/packages/firestore/test/unit/util/bundle.test.ts +++ b/packages/firestore/test/unit/util/bundle.test.ts @@ -20,8 +20,25 @@ import { BundleReader, SizedBundleElement } from '../../../src/util/bundle_reader'; -import { BundleElement } from '../../../src/protos/firestore_bundle_proto'; import { toByteStreamReader } from '../../../src/platform/byte_stream_reader'; +import { + doc1String, + doc1MetaString, + doc1Meta, + noDocMetaString, + noDocMeta, + doc2MetaString, + doc2Meta, + limitQueryString, + limitQuery, + limitToLastQuery, + limitToLastQueryString, + meta, + metaString, + doc2String, + doc1, + doc2 +} from './bundle_data'; use(chaiAsPromised); @@ -40,12 +57,6 @@ export function byteStreamReaderFromString( return toByteStreamReader(data, bytesPerRead); } -function lengthPrefixedString(o: {}): string { - const str = JSON.stringify(o); - const l = new TextEncoder().encode(str).byteLength; - return `${l}${str}`; -} - // Testing readableStreamFromString() is working as expected. describe('byteStreamReaderFromString()', () => { it('returns a reader stepping readable stream', async () => { @@ -83,100 +94,6 @@ function genericBundleReadingTests(bytesPerRead: number): void { } const encoder = new TextEncoder(); - // Setting up test data. - const meta: BundleElement = { - metadata: { - id: 'test-bundle', - createTime: { seconds: 1577836805, nanos: 6 }, - version: 1, - totalDocuments: 1, - totalBytes: 416 - } - }; - const metaString = lengthPrefixedString(meta); - - const doc1Meta: BundleElement = { - documentMetadata: { - name: - 'projects/test-project/databases/(default)/documents/collectionId/doc1', - readTime: { seconds: 5, nanos: 6 }, - exists: true - } - }; - const doc1MetaString = lengthPrefixedString(doc1Meta); - const doc1: BundleElement = { - document: { - name: - 'projects/test-project/databases/(default)/documents/collectionId/doc1', - createTime: { seconds: 1, nanos: 2000000 }, - updateTime: { seconds: 3, nanos: 4000 }, - fields: { foo: { stringValue: 'value' }, bar: { integerValue: -42 } } - } - }; - const doc1String = lengthPrefixedString(doc1); - - const doc2Meta: BundleElement = { - documentMetadata: { - name: - 'projects/test-project/databases/(default)/documents/collectionId/doc2', - readTime: { seconds: 5, nanos: 6 }, - exists: true - } - }; - const doc2MetaString = lengthPrefixedString(doc2Meta); - const doc2: BundleElement = { - document: { - name: - 'projects/test-project/databases/(default)/documents/collectionId/doc2', - createTime: { seconds: 1, nanos: 2000000 }, - updateTime: { seconds: 3, nanos: 4000 }, - fields: { foo: { stringValue: 'value1' }, bar: { integerValue: 42 } } - } - }; - const doc2String = lengthPrefixedString(doc2); - - const noDocMeta: BundleElement = { - documentMetadata: { - name: - 'projects/test-project/databases/(default)/documents/collectionId/nodoc', - readTime: { seconds: 5, nanos: 6 }, - exists: false - } - }; - const noDocMetaString = lengthPrefixedString(noDocMeta); - - const limitQuery: BundleElement = { - namedQuery: { - name: 'limitQuery', - bundledQuery: { - parent: 'projects/fireeats-97d5e/databases/(default)/documents', - structuredQuery: { - from: [{ collectionId: 'node_3.7.5_7Li7XoCjutvNxwD0tpo9' }], - orderBy: [{ field: { fieldPath: 'sort' }, direction: 'DESCENDING' }], - limit: { 'value': 1 } - }, - limitType: 'FIRST' - }, - readTime: { 'seconds': 1590011379, 'nanos': 191164000 } - } - }; - const limitQueryString = lengthPrefixedString(limitQuery); - const limitToLastQuery: BundleElement = { - namedQuery: { - name: 'limitToLastQuery', - bundledQuery: { - parent: 'projects/fireeats-97d5e/databases/(default)/documents', - structuredQuery: { - from: [{ collectionId: 'node_3.7.5_7Li7XoCjutvNxwD0tpo9' }], - orderBy: [{ field: { fieldPath: 'sort' }, direction: 'ASCENDING' }], - limit: { 'value': 1 } - }, - limitType: 'LAST' - }, - readTime: { 'seconds': 1590011379, 'nanos': 543063000 } - } - }; - const limitToLastQueryString = lengthPrefixedString(limitToLastQuery); async function getAllElements( bundle: BundleReader diff --git a/packages/firestore/test/unit/util/bundle_data.ts b/packages/firestore/test/unit/util/bundle_data.ts new file mode 100644 index 00000000000..3984c2be8d3 --- /dev/null +++ b/packages/firestore/test/unit/util/bundle_data.ts @@ -0,0 +1,213 @@ +/** + * @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 { + BundledQuery, + BundleElement +} from '../../../src/protos/firestore_bundle_proto'; +import { DatabaseId } from '../../../src/core/database_info'; +import * as api from '../../../src/protos/firestore_proto_api'; +import { Value } from '../../../src/protos/firestore_proto_api'; +import { JsonProtoSerializer, toName } from '../../../src/remote/serializer'; +import { DocumentKey } from '../../../src/model/document_key'; +import { newSerializer } from '../../../src/platform/serializer'; + +function lengthPrefixedString(o: {}): string { + const str = JSON.stringify(o); + const l = new TextEncoder().encode(str).byteLength; + return `${l}${str}`; +} + +export class TestBundleBuilder { + readonly elements: BundleElement[] = []; + private serializer: JsonProtoSerializer; + constructor(private databaseId: DatabaseId) { + this.serializer = newSerializer(databaseId); + } + + addDocumentMetadata( + docKey: DocumentKey, + readTime: api.Timestamp, + exists: boolean + ): TestBundleBuilder { + this.elements.push({ + documentMetadata: { + name: toName(this.serializer, docKey), + readTime, + exists + } + }); + return this; + } + addDocument( + docKey: DocumentKey, + createTime: api.Timestamp, + updateTime: api.Timestamp, + fields: api.ApiClientObjectMap + ): TestBundleBuilder { + this.elements.push({ + document: { + name: toName(this.serializer, docKey), + createTime, + updateTime, + fields + } + }); + return this; + } + addNamedQuery( + name: string, + readTime: api.Timestamp, + bundledQuery: BundledQuery + ): TestBundleBuilder { + this.elements.push({ namedQuery: { name, readTime, bundledQuery } }); + return this; + } + getMetadataElement( + id: string, + createTime: api.Timestamp, + version = 1 + ): BundleElement { + let totalDocuments = 0; + let totalBytes = 0; + const encoder = new TextEncoder(); + for (const element of this.elements) { + if (element.documentMetadata && !element.documentMetadata.exists) { + totalDocuments += 1; + } + if (element.document) { + totalDocuments += 1; + } + totalBytes += encoder.encode(lengthPrefixedString(element)).byteLength; + } + + return { + metadata: { + id, + createTime, + version, + totalDocuments, + totalBytes + } + }; + } + + build(id: string, createTime: api.Timestamp, version = 1): string { + let result = ''; + for (const element of this.elements) { + result += lengthPrefixedString(element); + } + return ( + lengthPrefixedString(this.getMetadataElement(id, createTime, version)) + + result + ); + } +} + +// TODO(wuandy): Ideally, these should use `TestBundleBuilder` above. +export const meta: BundleElement = { + metadata: { + id: 'test-bundle', + createTime: { seconds: 1577836805, nanos: 6 }, + version: 1, + totalDocuments: 1, + totalBytes: 416 + } +}; +export const metaString = lengthPrefixedString(meta); + +export const doc1Meta: BundleElement = { + documentMetadata: { + name: + 'projects/test-project/databases/(default)/documents/collectionId/doc1', + readTime: { seconds: 5, nanos: 6 }, + exists: true + } +}; +export const doc1MetaString = lengthPrefixedString(doc1Meta); +export const doc1: BundleElement = { + document: { + name: + 'projects/test-project/databases/(default)/documents/collectionId/doc1', + createTime: { seconds: 1, nanos: 2000000 }, + updateTime: { seconds: 3, nanos: 4000 }, + fields: { foo: { stringValue: 'value' }, bar: { integerValue: -42 } } + } +}; +export const doc1String = lengthPrefixedString(doc1); + +export const doc2Meta: BundleElement = { + documentMetadata: { + name: + 'projects/test-project/databases/(default)/documents/collectionId/doc2', + readTime: { seconds: 5, nanos: 6 }, + exists: true + } +}; +export const doc2MetaString = lengthPrefixedString(doc2Meta); +export const doc2: BundleElement = { + document: { + name: + 'projects/test-project/databases/(default)/documents/collectionId/doc2', + createTime: { seconds: 1, nanos: 2000000 }, + updateTime: { seconds: 3, nanos: 4000 }, + fields: { foo: { stringValue: 'value1' }, bar: { integerValue: 42 } } + } +}; +export const doc2String = lengthPrefixedString(doc2); + +export const noDocMeta: BundleElement = { + documentMetadata: { + name: + 'projects/test-project/databases/(default)/documents/collectionId/nodoc', + readTime: { seconds: 5, nanos: 6 }, + exists: false + } +}; +export const noDocMetaString = lengthPrefixedString(noDocMeta); + +export const limitQuery: BundleElement = { + namedQuery: { + name: 'limitQuery', + bundledQuery: { + parent: 'projects/fireeats-97d5e/databases/(default)/documents', + structuredQuery: { + from: [{ collectionId: 'node_3.7.5_7Li7XoCjutvNxwD0tpo9' }], + orderBy: [{ field: { fieldPath: 'sort' }, direction: 'DESCENDING' }], + limit: { 'value': 1 } + }, + limitType: 'FIRST' + }, + readTime: { 'seconds': 1590011379, 'nanos': 191164000 } + } +}; +export const limitQueryString = lengthPrefixedString(limitQuery); +export const limitToLastQuery: BundleElement = { + namedQuery: { + name: 'limitToLastQuery', + bundledQuery: { + parent: 'projects/fireeats-97d5e/databases/(default)/documents', + structuredQuery: { + from: [{ collectionId: 'node_3.7.5_7Li7XoCjutvNxwD0tpo9' }], + orderBy: [{ field: { fieldPath: 'sort' }, direction: 'ASCENDING' }], + limit: { 'value': 1 } + }, + limitType: 'LAST' + }, + readTime: { 'seconds': 1590011379, 'nanos': 543063000 } + } +}; +export const limitToLastQueryString = lengthPrefixedString(limitToLastQuery); From 9bfb995af8b1aea75eafe92386627da2d9230215 Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Thu, 23 Jul 2020 15:46:30 -0400 Subject: [PATCH 08/27] Bundles load from secondary tabs (#3393) --- .../firestore/src/core/firestore_client.ts | 4 +-- packages/firestore/src/core/sync_engine.ts | 12 ++++++-- .../src/local/shared_client_state.ts | 24 +++++++++++++++ .../src/local/shared_client_state_schema.ts | 9 ++++++ .../src/local/shared_client_state_syncer.ts | 6 ++++ .../src/platform/browser/serializer.ts | 14 +++++++++ .../firestore/src/platform/node/serializer.ts | 15 ++++++++++ .../firestore/src/platform/rn/serializer.ts | 6 +++- packages/firestore/src/platform/serializer.ts | 30 +++++++++++++++++-- packages/firestore/src/util/bundle_reader.ts | 4 ++- .../integration/api_internal/bundle.test.ts | 4 ++- .../unit/local/persistence_test_helpers.ts | 2 ++ .../web_storage_shared_client_state.test.ts | 2 ++ .../test/unit/specs/bundle_spec.test.ts | 9 ++---- .../test/unit/specs/spec_test_runner.ts | 3 +- .../firestore/test/unit/util/bundle.test.ts | 8 ++--- .../firestore/test/unit/util/bundle_data.ts | 10 +++++-- 17 files changed, 139 insertions(+), 23 deletions(-) diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 1a74e913ef0..2e59c18884c 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -51,7 +51,7 @@ import { import { BundleReader } from '../util/bundle_reader'; import { LoadBundleTask } from '../api/bundle'; import { newConnection } from '../platform/connection'; -import { newSerializer } from '../platform/serializer'; +import { newSerializer, newTextEncoder } from '../platform/serializer'; import { toByteStreamReader } from '../platform/byte_stream_reader'; const LOG_TAG = 'FirestoreClient'; @@ -529,7 +529,7 @@ export class FirestoreClient { let content: ReadableStream | ArrayBuffer; if (typeof data === 'string') { - content = new TextEncoder().encode(data); + content = newTextEncoder().encode(data); } else { content = data; } diff --git a/packages/firestore/src/core/sync_engine.ts b/packages/firestore/src/core/sync_engine.ts index e06f3ee6938..10bbdc960e1 100644 --- a/packages/firestore/src/core/sync_engine.ts +++ b/packages/firestore/src/core/sync_engine.ts @@ -293,7 +293,7 @@ class SyncEngineImpl implements SyncEngine { protected remoteStore: RemoteStore, protected datastore: Datastore, // PORTING NOTE: Manages state synchronization in multi-tab environments. - protected sharedClientState: SharedClientState, + public sharedClientState: SharedClientState, private currentUser: User, private maxConcurrentLimboResolutions: number ) {} @@ -1100,6 +1100,12 @@ class MultiTabSyncEngineImpl extends SyncEngineImpl { } } + synchronizeWithChangedDocuments(): Promise { + return this.localStore + .getNewDocumentChanges() + .then(changes => this.emitNewSnapsAndNotifyLocalStore(changes)); + } + async applyBatchState( batchId: BatchId, batchState: MutationBatchState, @@ -1412,7 +1418,9 @@ export function loadBundle( syncEngineImpl.assertSubscribed('loadBundle()'); // eslint-disable-next-line @typescript-eslint/no-floating-promises - loadBundleImpl(syncEngineImpl, bundleReader, task); + loadBundleImpl(syncEngineImpl, bundleReader, task).then(() => { + syncEngineImpl.sharedClientState.notifyBundleLoaded(); + }); } async function loadBundleImpl( diff --git a/packages/firestore/src/local/shared_client_state.ts b/packages/firestore/src/local/shared_client_state.ts index a333ff74c43..83dde78bc0f 100644 --- a/packages/firestore/src/local/shared_client_state.ts +++ b/packages/firestore/src/local/shared_client_state.ts @@ -40,6 +40,7 @@ import { import { CLIENT_STATE_KEY_PREFIX, ClientStateSchema, + createBundleLoadedKey, createWebStorageClientStateKey, createWebStorageMutationBatchKey, createWebStorageOnlineStateKey, @@ -173,6 +174,12 @@ export interface SharedClientState { setOnlineState(onlineState: OnlineState): void; writeSequenceNumber(sequenceNumber: ListenSequenceNumber): void; + + /** + * Notifies other clients when remote documents have changed due to loading + * a bundle. + */ + notifyBundleLoaded(): void; } /** @@ -477,6 +484,7 @@ export class WebStorageSharedClientState implements SharedClientState { private readonly sequenceNumberKey: string; private readonly storageListener = this.handleWebStorageEvent.bind(this); private readonly onlineStateKey: string; + private readonly bundleLoadedKey: string; private readonly clientStateKeyRe: RegExp; private readonly mutationBatchKeyRe: RegExp; private readonly queryTargetKeyRe: RegExp; @@ -532,6 +540,8 @@ export class WebStorageSharedClientState implements SharedClientState { this.onlineStateKey = createWebStorageOnlineStateKey(this.persistenceKey); + this.bundleLoadedKey = createBundleLoadedKey(this.persistenceKey); + // Rather than adding the storage observer during start(), we add the // storage observer during initialization. This ensures that we collect // events before other components populate their initial state (during their @@ -711,6 +721,10 @@ export class WebStorageSharedClientState implements SharedClientState { this.persistOnlineState(onlineState); } + notifyBundleLoaded(): void { + this.persistBundleLoadedState(); + } + shutdown(): void { if (this.started) { this.window.removeEventListener('storage', this.storageListener); @@ -818,6 +832,8 @@ export class WebStorageSharedClientState implements SharedClientState { if (sequenceNumber !== ListenSequence.INVALID) { this.sequenceNumberHandler!(sequenceNumber); } + } else if (storageEvent.key === this.bundleLoadedKey) { + return this.syncEngine!.synchronizeWithChangedDocuments(); } }); } @@ -883,6 +899,10 @@ export class WebStorageSharedClientState implements SharedClientState { this.setItem(targetKey, targetMetadata.toWebStorageJSON()); } + private persistBundleLoadedState(): void { + this.setItem(this.bundleLoadedKey, 'value-not-used'); + } + /** * Parses a client state key in WebStorage. Returns null if the key does not * match the expected key format. @@ -1131,4 +1151,8 @@ export class MemorySharedClientState implements SharedClientState { shutdown(): void {} writeSequenceNumber(sequenceNumber: ListenSequenceNumber): void {} + + notifyBundleLoaded(): void { + // No op. + } } diff --git a/packages/firestore/src/local/shared_client_state_schema.ts b/packages/firestore/src/local/shared_client_state_schema.ts index 01665e91cfa..fd47332bc4a 100644 --- a/packages/firestore/src/local/shared_client_state_schema.ts +++ b/packages/firestore/src/local/shared_client_state_schema.ts @@ -115,6 +115,15 @@ export function createWebStorageOnlineStateKey(persistenceKey: string): string { return `${ONLINE_STATE_KEY_PREFIX}_${persistenceKey}`; } +// The WebStorage prefix that plays as a event to indicate the remote documents +// might have changed due to some secondary tabs loading a bundle. +// format of the key is: +// firestore_remote_documents_changed_ +export const BUNDLE_LOADED_KEY_PREFIX = 'firestore_bundle_loaded'; +export function createBundleLoadedKey(persistenceKey: string): string { + return `${BUNDLE_LOADED_KEY_PREFIX}_${persistenceKey}`; +} + /** * The JSON representation of the system's online state, as written by the * primary client. diff --git a/packages/firestore/src/local/shared_client_state_syncer.ts b/packages/firestore/src/local/shared_client_state_syncer.ts index 29736807277..898c8c78b6d 100644 --- a/packages/firestore/src/local/shared_client_state_syncer.ts +++ b/packages/firestore/src/local/shared_client_state_syncer.ts @@ -49,4 +49,10 @@ export interface SharedClientStateSyncer { /** Returns the IDs of the clients that are currently active. */ getActiveClients(): Promise; + + /** + * Retrieves newly changed documents from remote document cache and raises + * snapshots if needed. + */ + synchronizeWithChangedDocuments(): Promise; } diff --git a/packages/firestore/src/platform/browser/serializer.ts b/packages/firestore/src/platform/browser/serializer.ts index 5e009d89f60..722f40e605f 100644 --- a/packages/firestore/src/platform/browser/serializer.ts +++ b/packages/firestore/src/platform/browser/serializer.ts @@ -22,3 +22,17 @@ import { JsonProtoSerializer } from '../../remote/serializer'; export function newSerializer(databaseId: DatabaseId): JsonProtoSerializer { return new JsonProtoSerializer(databaseId, /* useProto3Json= */ true); } + +/** + * An instance of the Platform's 'TextEncoder' implementation. + */ +export function newTextEncoder(): TextEncoder { + return new TextEncoder(); +} + +/** + * An instance of the Platform's 'TextDecoder' implementation. + */ +export function newTextDecoder(): TextDecoder { + return new TextDecoder('utf-8'); +} diff --git a/packages/firestore/src/platform/node/serializer.ts b/packages/firestore/src/platform/node/serializer.ts index bbc9db1e516..ac26afd0ee7 100644 --- a/packages/firestore/src/platform/node/serializer.ts +++ b/packages/firestore/src/platform/node/serializer.ts @@ -18,7 +18,22 @@ /** Return the Platform-specific serializer monitor. */ import { JsonProtoSerializer } from '../../remote/serializer'; import { DatabaseId } from '../../core/database_info'; +import { TextDecoder, TextEncoder } from 'util'; export function newSerializer(databaseId: DatabaseId): JsonProtoSerializer { return new JsonProtoSerializer(databaseId, /* useProto3Json= */ false); } + +/** + * An instance of the Platform's 'TextEncoder' implementation. + */ +export function newTextEncoder(): TextEncoder { + return new TextEncoder(); +} + +/** + * An instance of the Platform's 'TextDecoder' implementation. + */ +export function newTextDecoder(): TextDecoder { + return new TextDecoder('utf-8'); +} diff --git a/packages/firestore/src/platform/rn/serializer.ts b/packages/firestore/src/platform/rn/serializer.ts index c5ab7bf2bb5..2b168a0dffa 100644 --- a/packages/firestore/src/platform/rn/serializer.ts +++ b/packages/firestore/src/platform/rn/serializer.ts @@ -15,4 +15,8 @@ * limitations under the License. */ -export { newSerializer } from '../browser/serializer'; +export { + newSerializer, + newTextEncoder, + newTextDecoder +} from '../browser/serializer'; diff --git a/packages/firestore/src/platform/serializer.ts b/packages/firestore/src/platform/serializer.ts index f7990dc4496..26553332c36 100644 --- a/packages/firestore/src/platform/serializer.ts +++ b/packages/firestore/src/platform/serializer.ts @@ -16,11 +16,11 @@ */ import { isNode, isReactNative } from '@firebase/util'; +import { DatabaseId } from '../core/database_info'; +import { JsonProtoSerializer } from '../remote/serializer'; import * as node from './node/serializer'; import * as rn from './rn/serializer'; import * as browser from './browser/serializer'; -import { DatabaseId } from '../core/database_info'; -import { JsonProtoSerializer } from '../remote/serializer'; export function newSerializer(databaseId: DatabaseId): JsonProtoSerializer { if (isNode()) { @@ -31,3 +31,29 @@ export function newSerializer(databaseId: DatabaseId): JsonProtoSerializer { return browser.newSerializer(databaseId); } } + +/** + * An instance of the Platform's 'TextEncoder' implementation. + */ +export function newTextEncoder(): TextEncoder { + if (isNode()) { + return node.newTextEncoder(); + } else if (isReactNative()) { + return rn.newTextEncoder(); + } else { + return browser.newTextEncoder(); + } +} + +/** + * An instance of the Platform's 'TextDecoder' implementation. + */ +export function newTextDecoder(): TextDecoder { + if (isNode()) { + return node.newTextDecoder() as TextDecoder; + } else if (isReactNative()) { + return rn.newTextDecoder(); + } else { + return browser.newTextDecoder(); + } +} diff --git a/packages/firestore/src/util/bundle_reader.ts b/packages/firestore/src/util/bundle_reader.ts index e426fa0c825..d2e9c23ac58 100644 --- a/packages/firestore/src/util/bundle_reader.ts +++ b/packages/firestore/src/util/bundle_reader.ts @@ -22,6 +22,7 @@ import { import { Deferred } from './promise'; import { debugAssert } from './assert'; import { toByteStreamReader } from '../platform/byte_stream_reader'; +import { newTextDecoder } from '../platform/serializer'; /** * A complete element in the bundle stream, together with the byte length it @@ -67,7 +68,7 @@ export class BundleReader { */ private buffer: Uint8Array = new Uint8Array(); /** The decoder used to parse binary data into strings. */ - private textDecoder = new TextDecoder('utf-8'); + private textDecoder: TextDecoder; static fromBundleSource(source: BundleSource): BundleReader { return new BundleReader(toByteStreamReader(source, BYTES_PER_READ)); @@ -77,6 +78,7 @@ export class BundleReader { /** The reader to read from underlying binary bundle data source. */ private reader: ReadableStreamReader ) { + this.textDecoder = newTextDecoder(); // Read the metadata (which is the first element). this.nextElementImpl().then( element => { diff --git a/packages/firestore/test/integration/api_internal/bundle.test.ts b/packages/firestore/test/integration/api_internal/bundle.test.ts index b0e18b5b1cf..7894015249f 100644 --- a/packages/firestore/test/integration/api_internal/bundle.test.ts +++ b/packages/firestore/test/integration/api_internal/bundle.test.ts @@ -27,6 +27,9 @@ import { DatabaseId } from '../../../src/core/database_info'; import { key } from '../../util/helpers'; import { EventsAccumulator } from '../util/events_accumulator'; import { TestBundleBuilder } from '../../unit/util/bundle_data'; +import { newTextEncoder } from '../../../src/platform/serializer'; + +export const encoder = newTextEncoder(); function verifySuccessProgress(p: firestore.LoadBundleTaskProgress): void { expect(p.taskState).to.equal('Success'); @@ -45,7 +48,6 @@ function verifyInProgress( } apiDescribe('Bundles', (persistence: boolean) => { - const encoder = new TextEncoder(); const testDocs: { [key: string]: firestore.DocumentData } = { a: { k: { stringValue: 'a' }, bar: { integerValue: 1 } }, b: { k: { stringValue: 'b' }, bar: { integerValue: 2 } } diff --git a/packages/firestore/test/unit/local/persistence_test_helpers.ts b/packages/firestore/test/unit/local/persistence_test_helpers.ts index 23be7ca0d60..7d513376041 100644 --- a/packages/firestore/test/unit/local/persistence_test_helpers.ts +++ b/packages/firestore/test/unit/local/persistence_test_helpers.ts @@ -170,6 +170,8 @@ class NoOpSharedClientStateSyncer implements SharedClientStateSyncer { removed: TargetId[] ): Promise {} applyOnlineStateChange(onlineState: OnlineState): void {} + + async synchronizeWithChangedDocuments(): Promise {} } /** * Populates Web Storage with instance data from a pre-existing client. diff --git a/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts b/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts index ecfa554780b..a0136a709b4 100644 --- a/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts +++ b/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts @@ -150,6 +150,8 @@ class TestSharedClientSyncer implements SharedClientStateSyncer { applyOnlineStateChange(onlineState: OnlineState): void { this.onlineState = onlineState; } + + async synchronizeWithChangedDocuments(): Promise {} } describe('WebStorageSharedClientState', () => { diff --git a/packages/firestore/test/unit/specs/bundle_spec.test.ts b/packages/firestore/test/unit/specs/bundle_spec.test.ts index fa7b08d8c45..fc20c8b24fb 100644 --- a/packages/firestore/test/unit/specs/bundle_spec.test.ts +++ b/packages/firestore/test/unit/specs/bundle_spec.test.ts @@ -293,12 +293,9 @@ describeSpec('Bundles:', ['no-ios', 'no-android'], () => { .client(1) .loadBundle(bundleString1) .client(0) - .becomeVisible(); - // TODO(wuandy): Loading from secondary client does not notify other - // clients for now. We need to fix it and uncomment below. - // .expectEvents(query, { - // modified: [doc('collection/a', 500, { value: 'b' })], - // }) + .expectEvents(query, { + modified: [doc('collection/a', 500, { value: 'b' })] + }); } ); diff --git a/packages/firestore/test/unit/specs/spec_test_runner.ts b/packages/firestore/test/unit/specs/spec_test_runner.ts index fb7080b8b15..ac9b7a86099 100644 --- a/packages/firestore/test/unit/specs/spec_test_runner.ts +++ b/packages/firestore/test/unit/specs/spec_test_runner.ts @@ -126,6 +126,7 @@ import { } from '../../util/test_platform'; import { toByteStreamReader } from '../../../src/platform/byte_stream_reader'; import { logWarn } from '../../../src/util/log'; +import { newTextEncoder } from '../../../src/platform/serializer'; const ARBITRARY_SEQUENCE_NUMBER = 2; @@ -456,7 +457,7 @@ abstract class TestRunner { private async doLoadBundle(bundle: string): Promise { const reader = new BundleReader( - toByteStreamReader(new TextEncoder().encode(bundle)) + toByteStreamReader(newTextEncoder().encode(bundle)) ); const task = new LoadBundleTask(); return this.queue.enqueue(async () => { diff --git a/packages/firestore/test/unit/util/bundle.test.ts b/packages/firestore/test/unit/util/bundle.test.ts index 9d6527b5fc2..6b74cb02577 100644 --- a/packages/firestore/test/unit/util/bundle.test.ts +++ b/packages/firestore/test/unit/util/bundle.test.ts @@ -39,9 +39,12 @@ import { doc1, doc2 } from './bundle_data'; +import { newTextEncoder } from '../../../src/platform/serializer'; use(chaiAsPromised); +const encoder = newTextEncoder(); + /** * Create a `ReadableStream` from a string. * @@ -53,14 +56,13 @@ export function byteStreamReaderFromString( content: string, bytesPerRead: number ): ReadableStreamReader { - const data = new TextEncoder().encode(content); + const data = encoder.encode(content); return toByteStreamReader(data, bytesPerRead); } // Testing readableStreamFromString() is working as expected. describe('byteStreamReaderFromString()', () => { it('returns a reader stepping readable stream', async () => { - const encoder = new TextEncoder(); const r = byteStreamReaderFromString('0123456789', 4); let result = await r.read(); @@ -93,8 +95,6 @@ function genericBundleReadingTests(bytesPerRead: number): void { return new BundleReader(byteStreamReaderFromString(s, bytesPerRead)); } - const encoder = new TextEncoder(); - async function getAllElements( bundle: BundleReader ): Promise { diff --git a/packages/firestore/test/unit/util/bundle_data.ts b/packages/firestore/test/unit/util/bundle_data.ts index 3984c2be8d3..630d03ddf24 100644 --- a/packages/firestore/test/unit/util/bundle_data.ts +++ b/packages/firestore/test/unit/util/bundle_data.ts @@ -23,11 +23,16 @@ import * as api from '../../../src/protos/firestore_proto_api'; import { Value } from '../../../src/protos/firestore_proto_api'; import { JsonProtoSerializer, toName } from '../../../src/remote/serializer'; import { DocumentKey } from '../../../src/model/document_key'; -import { newSerializer } from '../../../src/platform/serializer'; +import { + newSerializer, + newTextEncoder +} from '../../../src/platform/serializer'; + +export const encoder = newTextEncoder(); function lengthPrefixedString(o: {}): string { const str = JSON.stringify(o); - const l = new TextEncoder().encode(str).byteLength; + const l = encoder.encode(str).byteLength; return `${l}${str}`; } @@ -83,7 +88,6 @@ export class TestBundleBuilder { ): BundleElement { let totalDocuments = 0; let totalBytes = 0; - const encoder = new TextEncoder(); for (const element of this.elements) { if (element.documentMetadata && !element.documentMetadata.exists) { totalDocuments += 1; From 8b8a097bcf93efee5edecb12e527610395728ab7 Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Tue, 28 Jul 2020 16:31:15 -0400 Subject: [PATCH 09/27] Make loadBundle work with exp build (#3488) * Make loadBundle work with exp build * Use legacy.LoadBundleTask --- packages/firestore/exp-types/index.d.ts | 5 +++++ packages/firestore/exp/src/api/database.ts | 15 ++++++++++----- packages/firestore/exp/test/shim.ts | 1 + packages/firestore/src/api/database.ts | 5 ++++- packages/firestore/src/core/firestore_client.ts | 13 +++++-------- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/firestore/exp-types/index.d.ts b/packages/firestore/exp-types/index.d.ts index f27b50dea99..170a553a506 100644 --- a/packages/firestore/exp-types/index.d.ts +++ b/packages/firestore/exp-types/index.d.ts @@ -536,6 +536,11 @@ export interface LoadBundleTaskProgress { export type TaskState = 'Error' | 'Running' | 'Success'; +export function loadBundle( + firestore: FirebaseFirestore, + bundleData: ArrayBuffer | ReadableStream | string +): LoadBundleTask; + export type FirestoreErrorCode = | 'cancelled' | 'unknown' diff --git a/packages/firestore/exp/src/api/database.ts b/packages/firestore/exp/src/api/database.ts index 13e4be085dc..d0004141c16 100644 --- a/packages/firestore/exp/src/api/database.ts +++ b/packages/firestore/exp/src/api/database.ts @@ -50,6 +50,7 @@ import { indexedDbStoragePrefix, indexedDbClearPersistence } from '../../../src/local/indexeddb_persistence'; +import { LoadBundleTask } from '../../../src/api/bundle'; /** * The root reference to the Firestore database and the entry point for the @@ -299,9 +300,13 @@ export function terminate( export function loadBundle( firestore: firestore.FirebaseFirestore, bundleData: ArrayBuffer | ReadableStream | string -): firestore.LoadBundleTask | null { - return null; - // const firestoreImpl = cast(firestore, Firestore); - // return firestoreImpl._getFirestoreClient() - // .then(firestoreClient => firestoreClient.loadBundle(bundleData)); +): LoadBundleTask { + const firestoreImpl = cast(firestore, Firestore); + const resultTask = new LoadBundleTask(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + firestoreImpl._getFirestoreClient().then(firestoreClient => { + firestoreClient.loadBundle(bundleData, resultTask); + }); + + return resultTask; } diff --git a/packages/firestore/exp/test/shim.ts b/packages/firestore/exp/test/shim.ts index 618c176cedb..592ffa9c60b 100644 --- a/packages/firestore/exp/test/shim.ts +++ b/packages/firestore/exp/test/shim.ts @@ -72,6 +72,7 @@ import { import { UntypedFirestoreDataConverter } from '../../src/api/user_data_reader'; import { isPartialObserver, PartialObserver } from '../../src/api/observer'; import { isPlainObject } from '../../src/util/input_validation'; +import { LoadBundleTask } from '../../exp-types'; export { GeoPoint, Blob, Timestamp } from '../index'; diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 9c496ee23e6..e196a56e5b7 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -112,6 +112,7 @@ import { import { UserDataWriter } from './user_data_writer'; import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { Provider } from '@firebase/component'; +import { LoadBundleTask } from './bundle'; // settings() defaults: const DEFAULT_HOST = 'firestore.googleapis.com'; @@ -498,7 +499,9 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService { bundleData: ArrayBuffer | ReadableStream | string ): firestore.LoadBundleTask { this.ensureClientConfigured(); - return this._firestoreClient!.loadBundle(bundleData); + const resultTask = new LoadBundleTask(); + this._firestoreClient!.loadBundle(bundleData, resultTask); + return resultTask; } ensureClientConfigured(): FirestoreClient { diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index d597e459a45..011c92500b0 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import * as firestore from '@firebase/firestore-types'; import { CredentialsProvider } from '../api/credentials'; import { User } from '../auth/user'; import { LocalStore } from '../local/local_store'; @@ -515,8 +514,9 @@ export class FirestoreClient { } loadBundle( - data: ReadableStream | ArrayBuffer | string - ): firestore.LoadBundleTask { + data: ReadableStream | ArrayBuffer | string, + resultTask: LoadBundleTask + ): void { this.verifyNotTerminated(); let content: ReadableStream | ArrayBuffer; @@ -526,14 +526,11 @@ export class FirestoreClient { content = data; } const reader = new BundleReader(toByteStreamReader(content)); - const task = new LoadBundleTask(); this.asyncQueue.enqueueAndForget(async () => { - loadBundle(this.syncEngine, reader, task); - return task.catch(e => { + loadBundle(this.syncEngine, reader, resultTask); + return resultTask.catch(e => { logWarn(LOG_TAG, `Loading bundle failed with ${e}`); }); }); - - return task; } } From 5ebf8c3134ca242760d9dc401c57712cbca8d1ad Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Thu, 6 Aug 2020 17:32:04 -0400 Subject: [PATCH 10/27] Bundle's named query resume. (#3395) --- packages/firestore-types/index.d.ts | 2 + packages/firestore/exp/index.ts | 1 + packages/firestore/exp/src/api/database.ts | 15 ++ packages/firestore/exp/test/shim.ts | 7 + packages/firestore/src/api/database.ts | 10 ++ .../firestore/src/core/firestore_client.ts | 8 +- .../src/local/indexeddb_bundle_cache.ts | 12 +- .../firestore/src/local/local_serializer.ts | 27 +--- packages/firestore/src/local/local_store.ts | 35 ++++- .../src/local/memory_bundle_cache.ts | 10 +- .../src/protos/firestore_bundle_proto.ts | 2 +- .../src/protos/firestore_proto_api.d.ts | 2 +- packages/firestore/src/remote/serializer.ts | 8 ++ .../integration/api_internal/bundle.test.ts | 57 ++++++-- .../test/unit/local/local_store.test.ts | 97 +++++++++++-- .../test/unit/specs/bundle_spec.test.ts | 128 ++++++++++++++++-- .../unit/specs/existence_filter_spec.test.ts | 2 +- .../test/unit/specs/limbo_spec.test.ts | 8 +- .../test/unit/specs/limit_spec.test.ts | 14 +- .../test/unit/specs/listen_spec.test.ts | 54 ++++---- .../test/unit/specs/orderby_spec.test.ts | 2 +- .../test/unit/specs/perf_spec.test.ts | 4 +- .../test/unit/specs/persistence_spec.test.ts | 4 +- .../test/unit/specs/recovery_spec.test.ts | 10 +- .../test/unit/specs/resume_token_spec.test.ts | 2 +- .../firestore/test/unit/specs/spec_builder.ts | 58 ++++++-- .../test/unit/specs/spec_test_runner.ts | 37 +++-- .../test/unit/specs/write_spec.test.ts | 2 +- .../firestore/test/unit/util/bundle_data.ts | 40 +++++- packages/firestore/test/util/helpers.ts | 24 +++- 30 files changed, 514 insertions(+), 168 deletions(-) diff --git a/packages/firestore-types/index.d.ts b/packages/firestore-types/index.d.ts index 9b2bc043cce..cbabc88fe67 100644 --- a/packages/firestore-types/index.d.ts +++ b/packages/firestore-types/index.d.ts @@ -97,6 +97,8 @@ export class FirebaseFirestore { bundleData: ArrayBuffer | ReadableStream | string ): LoadBundleTask; + namedQuery(name: string): Promise; + INTERNAL: { delete: () => Promise }; } diff --git a/packages/firestore/exp/index.ts b/packages/firestore/exp/index.ts index acc1ae5a0ad..24c76568f14 100644 --- a/packages/firestore/exp/index.ts +++ b/packages/firestore/exp/index.ts @@ -32,6 +32,7 @@ export { waitForPendingWrites, disableNetwork, enableNetwork, + namedQuery, loadBundle, terminate } from './src/api/database'; diff --git a/packages/firestore/exp/src/api/database.ts b/packages/firestore/exp/src/api/database.ts index d0004141c16..9e7598099a2 100644 --- a/packages/firestore/exp/src/api/database.ts +++ b/packages/firestore/exp/src/api/database.ts @@ -51,6 +51,7 @@ import { indexedDbClearPersistence } from '../../../src/local/indexeddb_persistence'; import { LoadBundleTask } from '../../../src/api/bundle'; +import { Query } from '../../../lite'; /** * The root reference to the Firestore database and the entry point for the @@ -310,3 +311,17 @@ export function loadBundle( return resultTask; } + +export async function namedQuery( + firestore: firestore.FirebaseFirestore, + name: string +): Promise { + const firestoreImpl = cast(firestore, Firestore); + const client = await firestoreImpl._getFirestoreClient(); + const namedQuery = await client.getNamedQuery(name); + if (!namedQuery) { + return null; + } + + return new Query(firestoreImpl, null, namedQuery.query); +} diff --git a/packages/firestore/exp/test/shim.ts b/packages/firestore/exp/test/shim.ts index 592ffa9c60b..0e58bb7592c 100644 --- a/packages/firestore/exp/test/shim.ts +++ b/packages/firestore/exp/test/shim.ts @@ -46,6 +46,7 @@ import { increment, initializeFirestore, loadBundle, + namedQuery, onSnapshot, onSnapshotsInSync, parent, @@ -173,6 +174,12 @@ export class FirebaseFirestore implements legacy.FirebaseFirestore { return loadBundle(this._delegate, bundleData)!; } + async namedQuery(name: string): Promise { + return namedQuery(this._delegate, name).then(query => { + return query ? new Query(this, query) : null; + }); + } + INTERNAL = { delete: () => terminate(this._delegate) }; diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index e196a56e5b7..7d48f4252ae 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -504,6 +504,16 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService { return resultTask; } + async namedQuery(name: string): Promise { + this.ensureClientConfigured(); + const namedQuery = await this._firestoreClient!.getNamedQuery(name); + if (!namedQuery) { + return null; + } + + return new Query(namedQuery.query, this, null); + } + ensureClientConfigured(): FirestoreClient { if (!this._firestoreClient) { // Kick off starting the client but don't actually wait for it. diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 011c92500b0..6b143118693 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -17,7 +17,7 @@ import { CredentialsProvider } from '../api/credentials'; import { User } from '../auth/user'; -import { LocalStore } from '../local/local_store'; +import { getNamedQuery, LocalStore } from '../local/local_store'; import { GarbageCollectionScheduler, Persistence } from '../local/persistence'; import { Document, NoDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; @@ -50,6 +50,7 @@ import { BundleReader } from '../util/bundle_reader'; import { LoadBundleTask } from '../api/bundle'; import { newTextEncoder } from '../platform/serializer'; import { toByteStreamReader } from '../platform/byte_stream_reader'; +import { NamedQuery } from './bundle'; const LOG_TAG = 'FirestoreClient'; const MAX_CONCURRENT_LIMBO_RESOLUTIONS = 100; @@ -533,4 +534,9 @@ export class FirestoreClient { }); }); } + + getNamedQuery(queryName: string): Promise { + this.verifyNotTerminated(); + return getNamedQuery(this.localStore, queryName); + } } diff --git a/packages/firestore/src/local/indexeddb_bundle_cache.ts b/packages/firestore/src/local/indexeddb_bundle_cache.ts index c8c107a5336..d9384b160d9 100644 --- a/packages/firestore/src/local/indexeddb_bundle_cache.ts +++ b/packages/firestore/src/local/indexeddb_bundle_cache.ts @@ -47,7 +47,7 @@ export class IndexedDbBundleCache implements BundleCache { .get(bundleId) .next(bundle => { if (bundle) { - return fromDbBundle(this.serializer, bundle); + return fromDbBundle(bundle); } return undefined; }); @@ -57,9 +57,7 @@ export class IndexedDbBundleCache implements BundleCache { transaction: PersistenceTransaction, bundleMetadata: bundleProto.BundleMetadata ): PersistencePromise { - return bundlesStore(transaction).put( - toDbBundle(this.serializer, bundleMetadata) - ); + return bundlesStore(transaction).put(toDbBundle(bundleMetadata)); } getNamedQuery( @@ -70,7 +68,7 @@ export class IndexedDbBundleCache implements BundleCache { .get(queryName) .next(query => { if (query) { - return fromDbNamedQuery(this.serializer, query); + return fromDbNamedQuery(query); } return undefined; }); @@ -80,9 +78,7 @@ export class IndexedDbBundleCache implements BundleCache { transaction: PersistenceTransaction, query: bundleProto.NamedQuery ): PersistencePromise { - return namedQueriesStore(transaction).put( - toDbNamedQuery(this.serializer, query) - ); + return namedQueriesStore(transaction).put(toDbNamedQuery(query)); } } diff --git a/packages/firestore/src/local/local_serializer.ts b/packages/firestore/src/local/local_serializer.ts index 7ea5bae157a..736a30bcf04 100644 --- a/packages/firestore/src/local/local_serializer.ts +++ b/packages/firestore/src/local/local_serializer.ts @@ -280,10 +280,7 @@ function isDocumentQuery(dbQuery: DbQuery): dbQuery is api.DocumentsTarget { } /** Encodes a DbBundle to a Bundle. */ -export function fromDbBundle( - serializer: LocalSerializer, - dbBundle: DbBundle -): Bundle { +export function fromDbBundle(dbBundle: DbBundle): Bundle { return { id: dbBundle.bundleId, createTime: fromDbTimestamp(dbBundle.createTime), @@ -292,10 +289,7 @@ export function fromDbBundle( } /** Encodes a BundleMetadata to a DbBundle. */ -export function toDbBundle( - serializer: LocalSerializer, - metadata: bundleProto.BundleMetadata -): DbBundle { +export function toDbBundle(metadata: bundleProto.BundleMetadata): DbBundle { return { bundleId: metadata.id!, createTime: toDbTimestamp(fromVersion(metadata.createTime!)), @@ -304,22 +298,16 @@ export function toDbBundle( } /** Encodes a DbNamedQuery to a NamedQuery. */ -export function fromDbNamedQuery( - serializer: LocalSerializer, - dbNamedQuery: DbNamedQuery -): NamedQuery { +export function fromDbNamedQuery(dbNamedQuery: DbNamedQuery): NamedQuery { return { name: dbNamedQuery.name, - query: fromBundledQuery(serializer, dbNamedQuery.bundledQuery), + query: fromBundledQuery(dbNamedQuery.bundledQuery), readTime: fromDbTimestamp(dbNamedQuery.readTime) }; } /** Encodes a NamedQuery from a bundle proto to a DbNamedQuery. */ -export function toDbNamedQuery( - serializer: LocalSerializer, - query: bundleProto.NamedQuery -): DbNamedQuery { +export function toDbNamedQuery(query: bundleProto.NamedQuery): DbNamedQuery { return { name: query.name!, readTime: toDbTimestamp(fromVersion(query.readTime!)), @@ -334,7 +322,6 @@ export function toDbNamedQuery( * including features exists only in SDKs (for example: limit-to-last). */ export function fromBundledQuery( - serializer: LocalSerializer, bundledQuery: bundleProto.BundledQuery ): Query { const query = convertQueryTargetToQuery({ @@ -353,19 +340,17 @@ export function fromBundledQuery( /** Encodes a NamedQuery proto object to a NamedQuery model object. */ export function fromProtoNamedQuery( - serializer: LocalSerializer, namedQuery: bundleProto.NamedQuery ): NamedQuery { return { name: namedQuery.name!, - query: fromBundledQuery(serializer, namedQuery.bundledQuery!), + query: fromBundledQuery(namedQuery.bundledQuery!), readTime: fromVersion(namedQuery.readTime!) }; } /** Encodes a BundleMetadata proto object to a Bundle model object. */ export function fromBundleMetadata( - serializer: LocalSerializer, metadata: bundleProto.BundleMetadata ): Bundle { return { diff --git a/packages/firestore/src/local/local_store.ts b/packages/firestore/src/local/local_store.ts index e62ca9bf6b5..cfb6ee8370e 100644 --- a/packages/firestore/src/local/local_store.ts +++ b/packages/firestore/src/local/local_store.ts @@ -76,7 +76,9 @@ import { isIndexedDbTransactionError } from './simple_db'; import * as bundleProto from '../protos/firestore_bundle_proto'; import { BundleConverter, BundledDocuments, NamedQuery } from '../core/bundle'; import { BundleCache } from './bundle_cache'; -import { JsonProtoSerializer } from '../remote/serializer'; +import { fromVersion, JsonProtoSerializer } from '../remote/serializer'; +import { fromBundledQuery } from './local_serializer'; +import { ByteString } from '../util/byte_string'; const LOG_TAG = 'LocalStore'; @@ -898,8 +900,6 @@ class LocalStoreImpl implements LocalStore { .getTargetData(txn, target) .next((cached: TargetData | null) => { if (cached) { - // This target has been listened to previously, so reuse the - // previous targetID. // TODO(mcg): freshen last accessed date? targetData = cached; return PersistencePromise.resolve(targetData); @@ -1371,14 +1371,39 @@ export function getNamedQuery( /** * Saves the given `NamedQuery` to local persistence. */ -export function saveNamedQuery( +export async function saveNamedQuery( localStore: LocalStore, query: bundleProto.NamedQuery ): Promise { + // Allocate a target for the named query such that it can be resumed + // from associated read time if users use it to listen. + // NOTE: this also means if no corresponding target exists, the new target + // will remain active and will not get collected, unless users happen to + // unlisten the query somehow. + const allocated = await localStore.allocateTarget( + queryToTarget(fromBundledQuery(query.bundledQuery!)) + ); const localStoreImpl = debugCast(localStore, LocalStoreImpl); return localStoreImpl.persistence.runTransaction( 'Save named query', 'readwrite', - transaction => localStoreImpl.bundleCache.saveNamedQuery(transaction, query) + transaction => { + // Update allocated target's read time, if the bundle's read time is newer. + let updateReadTime = PersistencePromise.resolve(); + const readTime = fromVersion(query.readTime!); + if (allocated.snapshotVersion.compareTo(readTime) < 0) { + const newTargetData = allocated.withResumeToken( + ByteString.EMPTY_BYTE_STRING, + readTime + ); + updateReadTime = localStoreImpl.targetCache.updateTargetData( + transaction, + newTargetData + ); + } + return updateReadTime.next(() => + localStoreImpl.bundleCache.saveNamedQuery(transaction, query) + ); + } ); } diff --git a/packages/firestore/src/local/memory_bundle_cache.ts b/packages/firestore/src/local/memory_bundle_cache.ts index b4ced5a38b2..788382ca387 100644 --- a/packages/firestore/src/local/memory_bundle_cache.ts +++ b/packages/firestore/src/local/memory_bundle_cache.ts @@ -43,10 +43,7 @@ export class MemoryBundleCache implements BundleCache { transaction: PersistenceTransaction, bundleMetadata: bundleProto.BundleMetadata ): PersistencePromise { - this.bundles.set( - bundleMetadata.id!, - fromBundleMetadata(this.serializer, bundleMetadata) - ); + this.bundles.set(bundleMetadata.id!, fromBundleMetadata(bundleMetadata)); return PersistencePromise.resolve(); } @@ -61,10 +58,7 @@ export class MemoryBundleCache implements BundleCache { transaction: PersistenceTransaction, query: bundleProto.NamedQuery ): PersistencePromise { - this.namedQueries.set( - query.name!, - fromProtoNamedQuery(this.serializer, query) - ); + this.namedQueries.set(query.name!, fromProtoNamedQuery(query)); return PersistencePromise.resolve(); } } diff --git a/packages/firestore/src/protos/firestore_bundle_proto.ts b/packages/firestore/src/protos/firestore_bundle_proto.ts index ed1168e5e16..49b8ef07c36 100644 --- a/packages/firestore/src/protos/firestore_bundle_proto.ts +++ b/packages/firestore/src/protos/firestore_bundle_proto.ts @@ -30,7 +30,7 @@ export interface BundledQuery { } /** LimitType enum. */ -type LimitType = 'FIRST' | 'LAST'; +export type LimitType = 'FIRST' | 'LAST'; /** Properties of a NamedQuery. */ export interface NamedQuery { diff --git a/packages/firestore/src/protos/firestore_proto_api.d.ts b/packages/firestore/src/protos/firestore_proto_api.d.ts index c72f987e6fa..7bd7397623c 100644 --- a/packages/firestore/src/protos/firestore_proto_api.d.ts +++ b/packages/firestore/src/protos/firestore_proto_api.d.ts @@ -345,7 +345,7 @@ export declare namespace firestoreV1ApiClientInterfaces { query?: QueryTarget; documents?: DocumentsTarget; resumeToken?: string | Uint8Array; - readTime?: string; + readTime?: Timestamp; targetId?: number; once?: boolean; } diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 94dda5817f6..77499d4e406 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -975,6 +975,14 @@ export function toTarget( if (targetData.resumeToken.approximateByteSize() > 0) { result.resumeToken = toBytes(serializer, targetData.resumeToken); + } else if (targetData.snapshotVersion.compareTo(SnapshotVersion.min()) > 0) { + // TODO(wuandy): Consider removing above check because it is most likely true. + // Right now, many tests depend on this behaviour though (leaving min() out + // of serialization). + result.readTime = toTimestamp( + serializer, + targetData.snapshotVersion.toTimestamp() + ); } return result; diff --git a/packages/firestore/test/integration/api_internal/bundle.test.ts b/packages/firestore/test/integration/api_internal/bundle.test.ts index 7894015249f..6581404c6d5 100644 --- a/packages/firestore/test/integration/api_internal/bundle.test.ts +++ b/packages/firestore/test/integration/api_internal/bundle.test.ts @@ -28,6 +28,9 @@ import { key } from '../../util/helpers'; import { EventsAccumulator } from '../util/events_accumulator'; import { TestBundleBuilder } from '../../unit/util/bundle_data'; import { newTextEncoder } from '../../../src/platform/serializer'; +import { collectionReference } from '../../util/api_helpers'; + +// TODO(b/162594908): Move this to api/ instead of api_internal. export const encoder = newTextEncoder(); @@ -53,7 +56,7 @@ apiDescribe('Bundles', (persistence: boolean) => { b: { k: { stringValue: 'b' }, bar: { integerValue: 2 } } }; - function bundleWithTestDocs( + function bundleWithTestDocsAndQueries( db: firestore.FirebaseFirestore ): TestBundleBuilder { const a = key('coll-1/a'); @@ -62,6 +65,24 @@ apiDescribe('Bundles', (persistence: boolean) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (db as any)._databaseId as DatabaseId ); + + builder.addNamedQuery( + 'limit', + { seconds: 1000, nanos: 9999 }, + (collectionReference('coll-1') + .orderBy('bar', 'desc') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .limit(1) as any)._query + ); + builder.addNamedQuery( + 'limit-to-last', + { seconds: 1000, nanos: 9999 }, + (collectionReference('coll-1') + .orderBy('bar', 'desc') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .limitToLast(1) as any)._query + ); + builder.addDocumentMetadata(a, { seconds: 1000, nanos: 9999 }, true); builder.addDocument( a, @@ -89,7 +110,7 @@ apiDescribe('Bundles', (persistence: boolean) => { it('load with documents only with on progress and promise interface', () => { return withTestDb(persistence, async db => { - const builder = bundleWithTestDocs(db); + const builder = bundleWithTestDocsAndQueries(db); const progressEvents: firestore.LoadBundleTaskProgress[] = []; let completeCalled = false; @@ -121,14 +142,22 @@ apiDescribe('Bundles', (persistence: boolean) => { // Read from cache. These documents do not exist in backend, so they can // only be read from cache. - const snap = await db.collection('coll-1').get({ source: 'cache' }); + let snap = await db.collection('coll-1').get({ source: 'cache' }); verifySnapEqualTestDocs(snap); + + snap = await (await db.namedQuery('limit'))!.get({ source: 'cache' }); + expect(toDataArray(snap)).to.deep.equal([{ k: 'b', bar: 2 }]); + + snap = await (await db.namedQuery('limit-to-last'))!.get({ + source: 'cache' + }); + expect(toDataArray(snap)).to.deep.equal([{ k: 'a', bar: 1 }]); }); }); - it('load with documents with promise interface', () => { + it('load with documents and queries with promise interface', () => { return withTestDb(persistence, async db => { - const builder = bundleWithTestDocs(db); + const builder = bundleWithTestDocsAndQueries(db); const fulfillProgress: firestore.LoadBundleTaskProgress = await db.loadBundle( builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) @@ -145,7 +174,7 @@ apiDescribe('Bundles', (persistence: boolean) => { it('load for a second time skips', () => { return withTestDb(persistence, async db => { - const builder = bundleWithTestDocs(db); + const builder = bundleWithTestDocsAndQueries(db); await db.loadBundle( builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) @@ -191,7 +220,7 @@ apiDescribe('Bundles', (persistence: boolean) => { db.collection('coll-1').onSnapshot(accumulator.storeEvent); await accumulator.awaitEvent(); - const builder = bundleWithTestDocs(db); + const builder = bundleWithTestDocsAndQueries(db); const progress = await db.loadBundle( // Testing passing in non-string bundles. encoder.encode( @@ -205,17 +234,17 @@ apiDescribe('Bundles', (persistence: boolean) => { // cache can only be tested in spec tests. await accumulator.assertNoAdditionalEvents(); - const snap = await db.collection('coll-1').get(); - expect(toDataArray(snap)).to.deep.equal([ - { k: 'a', bar: 0 }, - { k: 'b', bar: 0 } - ]); + let snap = await (await db.namedQuery('limit'))!.get(); + expect(toDataArray(snap)).to.deep.equal([{ k: 'b', bar: 0 }]); + + snap = await (await db.namedQuery('limit-to-last'))!.get(); + expect(toDataArray(snap)).to.deep.equal([{ k: 'a', bar: 0 }]); }); }); it('load with documents from other projects fails', () => { return withTestDb(persistence, async db => { - let builder = bundleWithTestDocs(db); + let builder = bundleWithTestDocsAndQueries(db); return withAlternateTestDb(persistence, async otherDb => { // eslint-disable-next-line @typescript-eslint/no-floating-promises await expect( @@ -225,7 +254,7 @@ apiDescribe('Bundles', (persistence: boolean) => { ).to.be.rejectedWith('Tried to deserialize key from different project'); // Verify otherDb still functions, despite loaded a problematic bundle. - builder = bundleWithTestDocs(otherDb); + builder = bundleWithTestDocsAndQueries(otherDb); const finalProgress = await otherDb.loadBundle( builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) ); diff --git a/packages/firestore/test/unit/local/local_store.test.ts b/packages/firestore/test/unit/local/local_store.test.ts index 2ff32770de0..9e3871a9af6 100644 --- a/packages/firestore/test/unit/local/local_store.test.ts +++ b/packages/firestore/test/unit/local/local_store.test.ts @@ -21,7 +21,13 @@ import { expect } from 'chai'; import { FieldValue } from '../../../src/api/field_value'; import { Timestamp } from '../../../src/api/timestamp'; import { User } from '../../../src/auth/user'; -import { Query, queryToTarget } from '../../../src/core/query'; +import { + LimitType, + Query, + queryEquals, + queryToTarget, + queryWithLimit +} from '../../../src/core/query'; import { Target } from '../../../src/core/target'; import { BatchId, TargetId } from '../../../src/core/types'; import { SnapshotVersion } from '../../../src/core/snapshot_version'; @@ -29,11 +35,13 @@ import { IndexFreeQueryEngine } from '../../../src/local/index_free_query_engine import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence'; import { applyBundleDocuments, + getNamedQuery, hasNewerBundle, LocalStore, LocalWriteResult, - saveBundle, newLocalStore, + saveBundle, + saveNamedQuery, synchronizeLastDocumentChangeReadTime } from '../../../src/local/local_store'; import { LocalViewChanges } from '../../../src/local/local_view_changes'; @@ -63,6 +71,8 @@ import { import { debugAssert } from '../../../src/util/assert'; import { addEqualityMatcher } from '../../util/equality_matcher'; import { + bundledDocuments, + bundleMetadata, byteStringFromString, deletedDoc, deleteMutation, @@ -74,25 +84,25 @@ import { key, localViewChanges, mapAsArray, + namedQuery, noChangeEvent, + orderBy, patchMutation, query, setMutation, - bundledDocuments, TestBundledDocuments, TestSnapshotVersion, transformMutation, unknownDoc, - version, - bundleMetadata + version } from '../../util/helpers'; import { CountingQueryEngine, QueryEngineType } from './counting_query_engine'; import * as persistenceHelpers from './persistence_test_helpers'; -import { ByteString } from '../../../src/util/byte_string'; -import { BundledDocuments } from '../../../src/core/bundle'; import { JSON_SERIALIZER } from './persistence_test_helpers'; -import { BundleMetadata } from '../../../src/protos/firestore_bundle_proto'; +import { ByteString } from '../../../src/util/byte_string'; +import { BundledDocuments, NamedQuery } from '../../../src/core/bundle'; +import * as bundleProto from '../../../src/protos/firestore_bundle_proto'; export interface LocalStoreComponents { queryEngine: CountingQueryEngine; @@ -127,6 +137,7 @@ class LocalStoreTester { | RemoteEvent | LocalViewChanges | TestBundledDocuments + | bundleProto.NamedQuery ): LocalStoreTester { if (op instanceof Mutation) { return this.afterMutations([op]); @@ -136,8 +147,10 @@ class LocalStoreTester { return this.afterViewChanges(op); } else if (op instanceof RemoteEvent) { return this.afterRemoteEvent(op); - } else { + } else if (op instanceof TestBundledDocuments) { return this.afterBundleDocuments(op.documents); + } else { + return this.afterNamedQuery(op); } } @@ -181,6 +194,15 @@ class LocalStoreTester { return this; } + afterNamedQuery(namedQuery: bundleProto.NamedQuery): LocalStoreTester { + this.prepareNextStep(); + + this.promiseChain = this.promiseChain.then(() => + saveNamedQuery(this.localStore, namedQuery) + ); + return this; + } + afterViewChanges(viewChanges: LocalViewChanges): LocalStoreTester { this.prepareNextStep(); @@ -411,7 +433,7 @@ class LocalStoreTester { } toHaveNewerBundle( - metadata: BundleMetadata, + metadata: bundleProto.BundleMetadata, expected: boolean ): LocalStoreTester { this.promiseChain = this.promiseChain.then(() => { @@ -422,7 +444,19 @@ class LocalStoreTester { return this; } - afterSavingBundle(metadata: BundleMetadata): LocalStoreTester { + toHaveNamedQuery(namedQuery: NamedQuery): LocalStoreTester { + this.promiseChain = this.promiseChain.then(() => { + return getNamedQuery(this.localStore, namedQuery.name).then(actual => { + expect(actual).to.exist; + expect(actual!.name).to.equal(namedQuery.name); + expect(namedQuery.readTime.isEqual(actual!.readTime)).to.be.true; + expect(queryEquals(actual!.query, namedQuery.query)).to.be.true; + }); + }); + return this; + } + + afterSavingBundle(metadata: bundleProto.BundleMetadata): LocalStoreTester { this.promiseChain = this.promiseChain.then(() => saveBundle(this.localStore, metadata) ); @@ -1654,6 +1688,47 @@ function genericLocalStoreTests( .finish(); }); + it('handles saving and loading named queries', async () => { + return expectLocalStore() + .after( + namedQuery( + 'testQueryName', + query('coll'), + /* limitType */ 'FIRST', + SnapshotVersion.min() + ) + ) + .toHaveNamedQuery({ + name: 'testQueryName', + query: query('coll'), + readTime: SnapshotVersion.min() + }) + .finish(); + }); + + it('handles saving and loading limit to last queries', async () => { + const now = Timestamp.now(); + return expectLocalStore() + .after( + namedQuery( + 'testQueryName', + queryWithLimit(query('coll', orderBy('sort')), 5, LimitType.First), + /* limitType */ 'LAST', + SnapshotVersion.fromTimestamp(now) + ) + ) + .toHaveNamedQuery({ + name: 'testQueryName', + query: queryWithLimit( + query('coll', orderBy('sort')), + 5, + LimitType.Last + ), + readTime: SnapshotVersion.fromTimestamp(now) + }) + .finish(); + }); + it('computes highest unacknowledged batch id correctly', () => { return expectLocalStore() .toReturnHighestUnacknowledgeBatchId(BATCHID_UNKNOWN) diff --git a/packages/firestore/test/unit/specs/bundle_spec.test.ts b/packages/firestore/test/unit/specs/bundle_spec.test.ts index b76f73f612e..85399afd946 100644 --- a/packages/firestore/test/unit/specs/bundle_spec.test.ts +++ b/packages/firestore/test/unit/specs/bundle_spec.test.ts @@ -15,10 +15,11 @@ * limitations under the License. */ -import { newQueryForPath } from '../../../src/core/query'; +import { newQueryForPath, Query } from '../../../src/core/query'; import { doc, query, + filter, TestSnapshotVersion, version, wrapObject @@ -34,6 +35,7 @@ import { import { DocumentKey } from '../../../src/model/document_key'; import { toVersion } from '../../../src/remote/serializer'; import { JsonObject } from '../../../src/model/object_value'; +import { LimitType } from '../../../src/protos/firestore_bundle_proto'; interface TestBundleDocument { key: DocumentKey; @@ -43,8 +45,27 @@ interface TestBundleDocument { content?: JsonObject; } -function bundleWithDocument(testDoc: TestBundleDocument): string { +interface TestBundledQuery { + name: string; + readTime: TestSnapshotVersion; + query: Query; + limitType?: LimitType; +} + +function bundleWithDocumentAndQuery( + testDoc: TestBundleDocument, + testQuery?: TestBundledQuery +): string { const builder = new TestBundleBuilder(TEST_DATABASE_ID); + + if (testQuery) { + builder.addNamedQuery( + testQuery.name, + toVersion(JSON_SERIALIZER, version(testQuery.readTime)), + testQuery.query + ); + } + builder.addDocumentMetadata( testDoc.key, toVersion(JSON_SERIALIZER, version(testDoc.readTime)), @@ -70,7 +91,7 @@ describeSpec('Bundles:', ['no-ios', 'no-android'], () => { const docA = doc('collection/a', 1000, { value: 'a' }); const docAChanged = doc('collection/a', 2999, { value: 'b' }); - const bundleString = bundleWithDocument({ + const bundleString = bundleWithDocumentAndQuery({ key: docA.key, readTime: 3000, createTime: 1999, @@ -98,7 +119,7 @@ describeSpec('Bundles:', ['no-ios', 'no-android'], () => { const query1 = query('collection'); const docA = doc('collection/a', 1000, { value: 'a' }); - const bundleString = bundleWithDocument({ + const bundleString = bundleWithDocumentAndQuery({ key: docA.key, readTime: 3000 }); @@ -116,7 +137,7 @@ describeSpec('Bundles:', ['no-ios', 'no-android'], () => { const query1 = query('collection'); const docA = doc('collection/a', 1000, { value: 'a' }); - const bundleString = bundleWithDocument({ + const bundleString = bundleWithDocumentAndQuery({ key: docA.key, readTime: 999 }); @@ -138,7 +159,7 @@ describeSpec('Bundles:', ['no-ios', 'no-android'], () => { const query1 = query('collection'); const docA = doc('collection/a', 250, { value: 'a' }); - const bundleBeforeMutationAck = bundleWithDocument({ + const bundleBeforeMutationAck = bundleWithDocumentAndQuery({ key: docA.key, readTime: 500, createTime: 250, @@ -146,7 +167,7 @@ describeSpec('Bundles:', ['no-ios', 'no-android'], () => { content: { value: 'b' } }); - const bundleAfterMutationAck = bundleWithDocument({ + const bundleAfterMutationAck = bundleWithDocumentAndQuery({ key: docA.key, readTime: 1001, createTime: 250, @@ -196,7 +217,7 @@ describeSpec('Bundles:', ['no-ios', 'no-android'], () => { const query1 = query('collection'); const docA = doc('collection/a', 250, { value: 'a' }); - const bundleString = bundleWithDocument({ + const bundleString = bundleWithDocumentAndQuery({ key: docA.key, readTime: 1001, createTime: 250, @@ -234,7 +255,7 @@ describeSpec('Bundles:', ['no-ios', 'no-android'], () => { specTest('Newer docs from bundles might lead to limbo doc', [], () => { const query1 = query('collection'); const docA = doc('collection/a', 1000, { value: 'a' }); - const bundleString1 = bundleWithDocument({ + const bundleString1 = bundleWithDocumentAndQuery({ key: docA.key, readTime: 500, createTime: 250, @@ -270,13 +291,94 @@ describeSpec('Bundles:', ['no-ios', 'no-android'], () => { ); }); + specTest('Bundles query can be resumed from same query.', [], () => { + const query1 = query('collection'); + const docA = doc('collection/a', 100, { key: 'a' }); + const bundleString1 = bundleWithDocumentAndQuery( + { + key: docA.key, + readTime: 500, + createTime: 250, + updateTime: 500, + content: { value: 'b' } + }, + { name: 'bundled-query', readTime: 400, query: query1 } + ); + + return spec() + .loadBundle(bundleString1) + .userListens(query1, { readTime: 400 }) + .expectEvents(query1, { + added: [doc('collection/a', 500, { value: 'b' })], + fromCache: true + }); + }); + + specTest( + 'Bundles query can be loaded and resumed from different tabs', + ['multi-client'], + () => { + const query1 = query('collection'); + const query2 = query('collection', filter('value', '==', 'c')); + const docA = doc('collection/a', 100, { value: 'a' }); + const bundleString1 = bundleWithDocumentAndQuery( + { + key: docA.key, + readTime: 500, + createTime: 250, + updateTime: 500, + content: { value: 'b' } + }, + { name: 'bundled-query', readTime: 400, query: query1 } + ); + + const bundleString2 = bundleWithDocumentAndQuery( + { + key: docA.key, + readTime: 600, + createTime: 250, + updateTime: 550, + content: { value: 'c' } + }, + { name: 'bundled-query', readTime: 560, query: query2 } + ); + + return ( + client(0) + .loadBundle(bundleString1) + // Read named query from loaded bundle by primary. + .client(1) + .userListens(query1, { readTime: 400 }) + .expectEvents(query1, { + added: [doc('collection/a', 500, { value: 'b' })], + fromCache: true + }) + // Loads a newer bundle. + .loadBundle(bundleString2) + .expectEvents(query1, { + modified: [doc('collection/a', 550, { value: 'c' })], + fromCache: true + }) + // Read named query from loaded bundle by secondary. + .client(0) + .expectListen(query1, { readTime: 400 }) + .expectActiveTargets({ query: query1, readTime: 400 }) + .userListens(query2, { readTime: 560 }) + .expectEvents(query2, { + added: [doc('collection/a', 550, { value: 'c' })], + fromCache: true + }) + ); + } + ); + specTest( 'Load from secondary clients and observe from primary', ['multi-client'], () => { const query1 = query('collection'); const docA = doc('collection/a', 250, { value: 'a' }); - const bundleString1 = bundleWithDocument({ + const bundleString1 = bundleWithDocumentAndQuery({ key: docA.key, readTime: 500, createTime: 250, @@ -305,7 +407,7 @@ describeSpec('Bundles:', ['no-ios', 'no-android'], () => { () => { const query1 = query('collection'); const docA = doc('collection/a', 250, { value: 'a' }); - const bundleString1 = bundleWithDocument({ + const bundleString = bundleWithDocumentAndQuery({ key: docA.key, readTime: 500, createTime: 250, @@ -324,7 +426,7 @@ describeSpec('Bundles:', ['no-ios', 'no-android'], () => { .expectEvents(query1, { added: [docA] }) - .loadBundle(bundleString1) + .loadBundle(bundleString) .expectEvents(query1, { modified: [doc('collection/a', 500, { value: 'b' })] }); @@ -337,7 +439,7 @@ describeSpec('Bundles:', ['no-ios', 'no-android'], () => { () => { const query1 = query('collection'); const docA = doc('collection/a', 250, { value: 'a' }); - const bundleString1 = bundleWithDocument({ + const bundleString1 = bundleWithDocumentAndQuery({ key: docA.key, readTime: 500, createTime: 250, diff --git a/packages/firestore/test/unit/specs/existence_filter_spec.test.ts b/packages/firestore/test/unit/specs/existence_filter_spec.test.ts index 7ae233df6c1..30ea8fbe64f 100644 --- a/packages/firestore/test/unit/specs/existence_filter_spec.test.ts +++ b/packages/firestore/test/unit/specs/existence_filter_spec.test.ts @@ -74,7 +74,7 @@ describeSpec('Existence Filters:', [], () => { .watchAcksFull(query1, 1000, doc1) .expectEvents(query1, { added: [doc1] }) .userUnlistens(query1) - .userListens(query1, 'resume-token-1000') + .userListens(query1, { resumeToken: 'resume-token-1000' }) .expectEvents(query1, { added: [doc1], fromCache: true }) // The empty existence filter is ignored since Watch hasn't ACKed the // target diff --git a/packages/firestore/test/unit/specs/limbo_spec.test.ts b/packages/firestore/test/unit/specs/limbo_spec.test.ts index 8f1c824b42d..071815cb793 100644 --- a/packages/firestore/test/unit/specs/limbo_spec.test.ts +++ b/packages/firestore/test/unit/specs/limbo_spec.test.ts @@ -444,7 +444,7 @@ describeSpec('Limbo Documents:', [], () => { .expectEvents(query1, { fromCache: true }) .runTimer(TimerId.ClientMetadataRefresh) .expectPrimaryState(true) - .expectListen(query1, 'resume-token-1000000') + .expectListen(query1, { resumeToken: 'resume-token-1000000' }) .watchAcksFull(query1, 3 * 1e6) .expectLimboDocs(docB.key) .ackLimbo(4 * 1e6, deletedDocB) @@ -476,7 +476,7 @@ describeSpec('Limbo Documents:', [], () => { .expectLimboDocs(docB.key, docC.key) .client(1) .stealPrimaryLease() - .expectListen(query1, 'resume-token-1000000') + .expectListen(query1, { resumeToken: 'resume-token-1000000' }) .client(0) .runTimer(TimerId.ClientMetadataRefresh) .expectPrimaryState(false) @@ -489,7 +489,7 @@ describeSpec('Limbo Documents:', [], () => { .client(0) .expectEvents(query1, { removed: [docB], fromCache: true }) .stealPrimaryLease() - .expectListen(query1, 'resume-token-1000000') + .expectListen(query1, { resumeToken: 'resume-token-1000000' }) .watchAcksFull(query1, 5 * 1e6) .expectLimboDocs(docC.key) .ackLimbo(6 * 1e6, deletedDocC) @@ -545,7 +545,7 @@ describeSpec('Limbo Documents:', [], () => { // document `docBCommitted`, since we haven't received the resolved // document from Watch. Until we do, we return the version from cache // even though the backend told it does not match. - .userListens(originalQuery, 'resume-token-2000') + .userListens(originalQuery, { resumeToken: 'resume-token-2000' }) .expectEvents(originalQuery, { added: [docA, docBDirty], fromCache: true diff --git a/packages/firestore/test/unit/specs/limit_spec.test.ts b/packages/firestore/test/unit/specs/limit_spec.test.ts index 33ab16b90cb..16784697431 100644 --- a/packages/firestore/test/unit/specs/limit_spec.test.ts +++ b/packages/firestore/test/unit/specs/limit_spec.test.ts @@ -58,7 +58,7 @@ describeSpec('Limits:', [], () => { }) .userUnlistens(query1) .userSets('collection/c', { key: 'c' }) - .userListens(query1, 'resume-token-1000') + .userListens(query1, { resumeToken: 'resume-token-1000' }) .expectEvents(query1, { added: [doc1, doc2], fromCache: true @@ -203,7 +203,7 @@ describeSpec('Limits:', [], () => { .userUnlistens(limitQuery) .watchRemoves(limitQuery) .userSets('collection/a', { matches: false }) - .userListens(limitQuery, 'resume-token-1004') + .userListens(limitQuery, { resumeToken: 'resume-token-1004' }) .expectEvents(limitQuery, { added: [doc2, doc3], fromCache: true }); } ); @@ -238,7 +238,7 @@ describeSpec('Limits:', [], () => { .userUnlistens(limitQuery) .watchRemoves(limitQuery) .userSets('collection/a', { pos: 4 }) - .userListens(limitQuery, 'resume-token-1004') + .userListens(limitQuery, { resumeToken: 'resume-token-1004' }) .expectEvents(limitQuery, { added: [doc2, doc3], fromCache: true }); } ); @@ -273,11 +273,11 @@ describeSpec('Limits:', [], () => { .expectEvents(limitQuery, {}) .userUnlistens(limitQuery) .watchRemoves(limitQuery) - .userListens(fullQuery, 'resume-token-1003') + .userListens(fullQuery, { resumeToken: 'resume-token-1003' }) .expectEvents(fullQuery, { added: [doc1, doc2, doc3], fromCache: true }) .watchAcksFull(fullQuery, 1005, doc1Edited) .expectEvents(fullQuery, { modified: [doc1Edited] }) - .userListens(limitQuery, 'resume-token-1004') + .userListens(limitQuery, { resumeToken: 'resume-token-1004' }) .expectEvents(limitQuery, { added: [doc2, doc3], fromCache: true }); } ); @@ -332,7 +332,7 @@ describeSpec('Limits:', [], () => { // We listen to the limit query again. Note that we include // `firstDocument` in the local result since we did not resolve its // limbo state. - .userListens(limitQuery, 'resume-token-1001') + .userListens(limitQuery, { resumeToken: 'resume-token-1001' }) .expectEvents(limitQuery, { added: [firstDocument], fromCache: true }) .watchAcks(limitQuery) // Watch resumes the query from the provided resume token, but does @@ -400,7 +400,7 @@ describeSpec('Limits:', [], () => { .watchRemoves(fullQuery) // Re-issue the limit query and verify that we return `secondDocument` // from cache. - .userListens(limitQuery, 'resume-token-2001') + .userListens(limitQuery, { resumeToken: 'resume-token-2001' }) .expectEvents(limitQuery, { added: [secondDocument], fromCache: true diff --git a/packages/firestore/test/unit/specs/listen_spec.test.ts b/packages/firestore/test/unit/specs/listen_spec.test.ts index daa9c3f1217..ad0fa9ab201 100644 --- a/packages/firestore/test/unit/specs/listen_spec.test.ts +++ b/packages/firestore/test/unit/specs/listen_spec.test.ts @@ -278,7 +278,7 @@ describeSpec('Listens:', [], () => { // Remove and re-add listener. .userUnlistens(query1) .watchRemoves(query1) - .userListens(query1, 'resume-token-1000') + .userListens(query1, { resumeToken: 'resume-token-1000' }) .expectEvents(query1, { added: [docAv2], fromCache: true }) // watch sends old snapshot. .watchAcksFull(query1, 1000, docAv1) @@ -313,7 +313,7 @@ describeSpec('Listens:', [], () => { .expectEvents(query1, { modified: [docAv2] }) // restart the client and re-listen. .restart() - .userListens(query1, 'resume-token-1000') + .userListens(query1, { resumeToken: 'resume-token-1000' }) .expectEvents(query1, { added: [docAv2], fromCache: true }) // watch sends old snapshot. .watchAcksFull(query1, 1000, docAv1) @@ -353,7 +353,7 @@ describeSpec('Listens:', [], () => { // us up to docAV2 since that's the last relevant change to the query // (the document falls out) and send us a snapshot that's ahead of // docAv3 (which is already in our cache). - .userListens(visibleQuery, 'resume-token-1000') + .userListens(visibleQuery, { resumeToken: 'resume-token-1000' }) .watchAcks(visibleQuery) .watchSends({ removed: [visibleQuery] }, docAv2) .watchCurrents(visibleQuery, 'resume-token-5000') @@ -362,7 +362,7 @@ describeSpec('Listens:', [], () => { .userUnlistens(visibleQuery) .watchRemoves(visibleQuery) // Listen to allQuery again and make sure we still get docAv3. - .userListens(allQuery, 'resume-token-4000') + .userListens(allQuery, { resumeToken: 'resume-token-4000' }) .expectEvents(allQuery, { added: [docAv3], fromCache: true }) .watchAcksFull(allQuery, 6000) .expectEvents(allQuery, { fromCache: false }) @@ -398,7 +398,7 @@ describeSpec('Listens:', [], () => { // us up to docAV2 since that's the last relevant change to the query // (the document falls out) and send us a snapshot that's ahead of // docAv3 (which is already in our cache). - .userListens(visibleQuery, 'resume-token-1000') + .userListens(visibleQuery, { resumeToken: 'resume-token-1000' }) .watchAcks(visibleQuery) .watchSends({ removed: [visibleQuery] }, docAv2) .watchCurrents(visibleQuery, 'resume-token-5000') @@ -407,7 +407,7 @@ describeSpec('Listens:', [], () => { .userUnlistens(visibleQuery) .watchRemoves(visibleQuery) // Listen to allQuery again and make sure we still get no docs. - .userListens(allQuery, 'resume-token-4000') + .userListens(allQuery, { resumeToken: 'resume-token-4000' }) .watchAcksFull(allQuery, 6000) .expectEvents(allQuery, { fromCache: false }) ); @@ -484,7 +484,7 @@ describeSpec('Listens:', [], () => { .userUnlistens(collQuery) .watchRemoves(collQuery) // Verify that DocA and DocB exists - .userListens(collQuery, 'resume-token-1000') + .userListens(collQuery, { resumeToken: 'resume-token-1000' }) .expectEvents(collQuery, { added: [docA, docB], fromCache: true }) .userUnlistens(collQuery) // Now send a document query that produces no results from the server @@ -498,7 +498,7 @@ describeSpec('Listens:', [], () => { .userUnlistens(docQuery) .watchRemoves(docQuery) // Re-add the initial collection query. Only Doc B exists now - .userListens(collQuery, 'resume-token-1000') + .userListens(collQuery, { resumeToken: 'resume-token-1000' }) .expectEvents(collQuery, { added: [docB], fromCache: true }) ); }); @@ -514,7 +514,7 @@ describeSpec('Listens:', [], () => { .expectEvents(query1, { added: [docA] }) .userUnlistens(query1) .watchRemoves(query1) - .userListens(query1, 'resume-token-1000') + .userListens(query1, { resumeToken: 'resume-token-1000' }) .expectEvents(query1, { added: [docA], fromCache: true }) .watchAcks(query1) .watchSends({ removed: [query1] }, deletedDocA) @@ -536,7 +536,7 @@ describeSpec('Listens:', [], () => { .watchSends({ affects: [query1] }, docB) .watchSnapshots(2000) .watchRemoves(query1) - .userListens(query1, 'resume-token-1000') + .userListens(query1, { resumeToken: 'resume-token-1000' }) .expectEvents(query1, { added: [docA], fromCache: true }); }); @@ -560,7 +560,7 @@ describeSpec('Listens:', [], () => { // snapshot and don't synthesize a document delete. .expectEvents(query1, { fromCache: false }) .userUnlistens(query1) - .userListens(query1, 'resume-token-2000') + .userListens(query1, { resumeToken: 'resume-token-2000' }) .expectEvents(query1, { added: [docA], fromCache: true }) ); } @@ -596,7 +596,7 @@ describeSpec('Listens:', [], () => { .expectEvents(query1, { added: [docA] }) .userUnlistens(query1) .watchRemoves(query1) - .userListens(query1, 'resume-token-2000') + .userListens(query1, { resumeToken: 'resume-token-2000' }) .expectEvents(query1, { added: [docA], fromCache: true }) .watchAcksFull(query1, 3000) .expectEvents(query1, {}); @@ -616,7 +616,7 @@ describeSpec('Listens:', [], () => { .expectEvents(query1, { added: [docA] }) .userUnlistens(query1) .watchRemoves(query1) - .userListens(query1, 'resume-token-2000') + .userListens(query1, { resumeToken: 'resume-token-2000' }) .expectEvents(query1, { added: [docA], fromCache: true }) .watchAcksFull(query1, 3000) .expectEvents(query1, {}); @@ -639,7 +639,7 @@ describeSpec('Listens:', [], () => { .userUnlistens(query1) .watchRemoves(query1) - .userListens(query1, 'resume-token-2000') + .userListens(query1, { resumeToken: 'resume-token-2000' }) .expectEvents(query1, { added: [docA], fromCache: true }) .watchAcks(query1) .watchCurrents(query1, 'resume-token-3000') @@ -667,7 +667,7 @@ describeSpec('Listens:', [], () => { .watchSnapshots(2000, [], 'resume-token-2000') .restart() - .userListens(query1, 'resume-token-1000') + .userListens(query1, { resumeToken: 'resume-token-1000' }) .expectEvents(query1, { added: [docA], fromCache: true }) .watchAcks(query1) .watchCurrents(query1, 'resume-token-3000') @@ -700,7 +700,7 @@ describeSpec('Listens:', [], () => { .watchSnapshots(minutesLater, [], 'resume-token-minutes-later') .restart() - .userListens(query1, 'resume-token-minutes-later') + .userListens(query1, { resumeToken: 'resume-token-minutes-later' }) .expectEvents(query1, { added: [docA], fromCache: true }) .watchAcks(query1) .watchCurrents(query1, 'resume-token-even-later') @@ -862,7 +862,7 @@ describeSpec('Listens:', [], () => { .userListens(query1) .expectEvents(query1, { added: [docA], fromCache: true }) .client(0) - .expectListen(query1, 'resume-token-1000') + .expectListen(query1, { resumeToken: 'resume-token-1000' }) .watchAcksFull(query1, 2000, docB) .client(1) .expectEvents(query1, { added: [docB] }); @@ -1133,7 +1133,7 @@ describeSpec('Listens:', [], () => { .expectEvents(query1, { fromCache: true }) .client(0) .enableNetwork() - .expectListen(query1, 'resume-token-1000') + .expectListen(query1, { resumeToken: 'resume-token-1000' }) .watchAcksFull(query1, 2000) .client(1) .expectEvents(query1, {}); @@ -1184,7 +1184,7 @@ describeSpec('Listens:', [], () => { .client(1) .expectEvents(query1, { fromCache: true }) .client(2) - .expectListen(query1, 'resume-token-1000') + .expectListen(query1, { resumeToken: 'resume-token-1000' }) .expectPrimaryState(true) .watchAcksFull(query1, 2000) .client(0) @@ -1269,7 +1269,7 @@ describeSpec('Listens:', [], () => { .client(1) .runTimer(TimerId.ClientMetadataRefresh) .expectPrimaryState(true) - .expectListen(query1, 'resume-token-1000') + .expectListen(query1, { resumeToken: 'resume-token-1000' }) .watchAcksFull(query1, 2000, docB) .expectEvents(query1, { added: [docB] }) .client(2) @@ -1299,7 +1299,7 @@ describeSpec('Listens:', [], () => { .client(1) .runTimer(TimerId.ClientMetadataRefresh) .expectPrimaryState(true) - .expectListen(query1, 'resume-token-1000') + .expectListen(query1, { resumeToken: 'resume-token-1000' }) .watchAcksFull(query1, 2000, docB) .client(2) .expectEvents(query1, { added: [docB] }); @@ -1321,7 +1321,7 @@ describeSpec('Listens:', [], () => { .userListens(query1) .expectEvents(query1, { added: [docA] }) .stealPrimaryLease() - .expectListen(query1, 'resume-token-1000') + .expectListen(query1, { resumeToken: 'resume-token-1000' }) .watchAcksFull(query1, 2000, docB) .expectEvents(query1, { added: [docB] }) .client(0) @@ -1359,7 +1359,7 @@ describeSpec('Listens:', [], () => { .expectEvents(query1, { added: [docA] }) .client(2) .stealPrimaryLease() - .expectListen(query1, 'resume-token-1000') + .expectListen(query1, { resumeToken: 'resume-token-1000' }) .client(1) .runTimer(TimerId.ClientMetadataRefresh) .expectPrimaryState(false) @@ -1369,7 +1369,7 @@ describeSpec('Listens:', [], () => { .expectEvents(query1, { added: [docB] }) .client(1) .stealPrimaryLease() - .expectListen(query1, 'resume-token-2000') + .expectListen(query1, { resumeToken: 'resume-token-2000' }) .watchAcksFull(query1, 3000, docC) .client(0) .expectEvents(query1, { added: [docC] }); @@ -1427,7 +1427,7 @@ describeSpec('Listens:', [], () => { .shutdown() .client(2) .expectPrimaryState(true) - .expectListen(query1, 'resume-token-1000') + .expectListen(query1, { resumeToken: 'resume-token-1000' }) .watchAcksFull(query1, 2000, docB) .client(1) .expectEvents(query1, { added: [docB] }); @@ -1448,7 +1448,7 @@ describeSpec('Listens:', [], () => { .expectEvents(query1, {}) .client(1) .stealPrimaryLease() - .expectListen(query1, 'resume-token-1000') + .expectListen(query1, { resumeToken: 'resume-token-1000' }) .watchAcksFull(query1, 2000, docA) .shutdown() .client(0) @@ -1457,7 +1457,7 @@ describeSpec('Listens:', [], () => { // is already eligible to obtain it again. .runTimer(TimerId.ClientMetadataRefresh) .expectPrimaryState(true) - .expectListen(query1, 'resume-token-2000') + .expectListen(query1, { resumeToken: 'resume-token-2000' }) .expectEvents(query1, { added: [docA] }) ); } diff --git a/packages/firestore/test/unit/specs/orderby_spec.test.ts b/packages/firestore/test/unit/specs/orderby_spec.test.ts index f74e1594ae6..3bb8a4fa0e7 100644 --- a/packages/firestore/test/unit/specs/orderby_spec.test.ts +++ b/packages/firestore/test/unit/specs/orderby_spec.test.ts @@ -66,7 +66,7 @@ describeSpec('OrderBy:', [], () => { .expectEvents(query1, { added: [docB, docA] }) .userUnlistens(query1) .watchRemoves(query1) - .userListens(query1, 'resume-token-1002') + .userListens(query1, { resumeToken: 'resume-token-1002' }) .expectEvents(query1, { added: [docB, docA], fromCache: true }) .watchAcksFull(query1, 1002) .expectEvents(query1, {}); diff --git a/packages/firestore/test/unit/specs/perf_spec.test.ts b/packages/firestore/test/unit/specs/perf_spec.test.ts index 7fafa5e95f1..50346863cd2 100644 --- a/packages/firestore/test/unit/specs/perf_spec.test.ts +++ b/packages/firestore/test/unit/specs/perf_spec.test.ts @@ -234,7 +234,9 @@ describeSpec( .expectEvents(query1, { added: docs }) .userUnlistens(query1) .watchRemoves(query1) - .userListens(query1, 'resume-token-' + currentVersion) + .userListens(query1, { + resumeToken: 'resume-token-' + currentVersion + }) .expectEvents(query1, { added: docs, fromCache: true }) .watchAcksFull(query1, ++currentVersion) .expectEvents(query1, {}) diff --git a/packages/firestore/test/unit/specs/persistence_spec.test.ts b/packages/firestore/test/unit/specs/persistence_spec.test.ts index ac89459ce25..7beaf32d26c 100644 --- a/packages/firestore/test/unit/specs/persistence_spec.test.ts +++ b/packages/firestore/test/unit/specs/persistence_spec.test.ts @@ -79,7 +79,7 @@ describeSpec('Persistence:', [], () => { .watchAcksFull(query1, 1000, doc1) .expectEvents(query1, { added: [doc1] }) .restart() - .userListens(query1, 'resume-token-1000') + .userListens(query1, { resumeToken: 'resume-token-1000' }) .expectEvents(query1, { added: [doc1], fromCache: true }); }); @@ -94,7 +94,7 @@ describeSpec('Persistence:', [], () => { .expectEvents(query1, { added: [doc1] }) // Normally this would clear the cached remote documents. .userUnlistens(query1) - .userListens(query1, 'resume-token-1000') + .userListens(query1, { resumeToken: 'resume-token-1000' }) .expectEvents(query1, { added: [doc1], fromCache: true }) ); }); diff --git a/packages/firestore/test/unit/specs/recovery_spec.test.ts b/packages/firestore/test/unit/specs/recovery_spec.test.ts index c5da05ff28f..02dfa079d76 100644 --- a/packages/firestore/test/unit/specs/recovery_spec.test.ts +++ b/packages/firestore/test/unit/specs/recovery_spec.test.ts @@ -173,7 +173,7 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => { .recoverDatabase() .runTimer(TimerId.AsyncQueueRetry) .expectPrimaryState(true) - .expectListen(query1, 'resume-token-1000') + .expectListen(query1, { resumeToken: 'resume-token-1000' }) .watchAcksFull(query1, 2000, docB) .expectEvents(query1, { added: [docB] }) ); @@ -214,7 +214,7 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => { .recoverDatabase() .runTimer(TimerId.AsyncQueueRetry) .expectPrimaryState(true) - .expectListen(query1, 'resume-token-1000') + .expectListen(query1, { resumeToken: 'resume-token-1000' }) .watchAcksFull(query1, 2000, docB) .client(2) .expectEvents(query1, { added: [docB] }) @@ -506,7 +506,7 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => { .recoverDatabase() .userUnlistens(query1) // No event since the document was removed - .userListens(query1, 'resume-token-1000') + .userListens(query1, { resumeToken: 'resume-token-1000' }) ); } ); @@ -612,7 +612,7 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => { // Verify that `doc1Query` can be listened to again. Note that the // resume token is slightly outdated since we failed to persist the // target update during the release. - .userListens(doc1Query, 'resume-token-1000') + .userListens(doc1Query, { resumeToken: 'resume-token-1000' }) .expectEvents(doc1Query, { added: [doc1a], fromCache: true @@ -829,7 +829,7 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => { .userUnlistens(query1) .watchRemoves(query1) .recoverDatabase() - .userListens(query1, 'resume-token-1000') + .userListens(query1, { resumeToken: 'resume-token-1000' }) .expectEvents(query1, { added: [doc1], fromCache: true diff --git a/packages/firestore/test/unit/specs/resume_token_spec.test.ts b/packages/firestore/test/unit/specs/resume_token_spec.test.ts index 537b637fe39..cd0f5b9435a 100644 --- a/packages/firestore/test/unit/specs/resume_token_spec.test.ts +++ b/packages/firestore/test/unit/specs/resume_token_spec.test.ts @@ -51,7 +51,7 @@ describeSpec('Resume tokens:', [], () => { .watchSnapshots(1000) .expectEvents(query1, { added: [doc1] }) .userUnlistens(query1) - .userListens(query1, 'custom-query-resume-token') + .userListens(query1, { resumeToken: 'custom-query-resume-token' }) .expectEvents(query1, { fromCache: true, added: [doc1] }) .watchAcks(query1) .watchSnapshots(1001); diff --git a/packages/firestore/test/unit/specs/spec_builder.ts b/packages/firestore/test/unit/specs/spec_builder.ts index 74e70e46d4f..7eae0d3d593 100644 --- a/packages/firestore/test/unit/specs/spec_builder.ts +++ b/packages/firestore/test/unit/specs/spec_builder.ts @@ -73,7 +73,8 @@ export interface LimboMap { export interface ActiveTargetSpec { queries: SpecQuery[]; - resumeToken: string; + resumeToken?: string; + readTime?: TestSnapshotVersion; } export interface ActiveTargetMap { @@ -253,7 +254,10 @@ export class SpecBuilder { return this; } - userListens(query: Query, resumeToken?: string): this { + userListens( + query: Query, + resume?: { resumeToken?: string; readTime?: TestSnapshotVersion } + ): this { this.nextStep(); const target = queryToTarget(query); @@ -262,7 +266,7 @@ export class SpecBuilder { if (this.injectFailures) { // Return a `userListens()` step but don't advance the target IDs. this.currentStep = { - userListen: [targetId, SpecBuilder.queryToSpec(query)] + userListen: { targetId, query: SpecBuilder.queryToSpec(query) } }; } else { if (this.queryMapping.has(target)) { @@ -272,9 +276,14 @@ export class SpecBuilder { } this.queryMapping.set(target, targetId); - this.addQueryToActiveTargets(targetId, query, resumeToken); + this.addQueryToActiveTargets( + targetId, + query, + resume?.resumeToken, + resume?.readTime + ); this.currentStep = { - userListen: [targetId, SpecBuilder.queryToSpec(query)], + userListen: { targetId, query: SpecBuilder.queryToSpec(query) }, expectedState: { activeTargets: { ...this.activeTargets } } }; } @@ -493,13 +502,22 @@ export class SpecBuilder { /** Overrides the currently expected set of active targets. */ expectActiveTargets( - ...targets: Array<{ query: Query; resumeToken?: string }> + ...targets: Array<{ + query: Query; + resumeToken?: string; + readTime?: TestSnapshotVersion; + }> ): this { this.assertStep('Active target expectation requires previous step'); const currentStep = this.currentStep!; this.clientState.activeTargets = {}; - targets.forEach(({ query, resumeToken }) => { - this.addQueryToActiveTargets(this.getTargetId(query), query, resumeToken); + targets.forEach(({ query, resumeToken, readTime }) => { + this.addQueryToActiveTargets( + this.getTargetId(query), + query, + resumeToken, + readTime + ); }); currentStep.expectedState = currentStep.expectedState || {}; currentStep.expectedState.activeTargets = { ...this.activeTargets }; @@ -860,14 +878,22 @@ export class SpecBuilder { } /** Registers a query that is active in another tab. */ - expectListen(query: Query, resumeToken?: string): this { + expectListen( + query: Query, + resume?: { resumeToken?: string; readTime?: TestSnapshotVersion } + ): this { this.assertStep('Expectations require previous step'); const target = queryToTarget(query); const targetId = this.queryIdGenerator.cachedId(target); this.queryMapping.set(target, targetId); - this.addQueryToActiveTargets(targetId, query, resumeToken); + this.addQueryToActiveTargets( + targetId, + query, + resume?.resumeToken, + resume?.readTime + ); const currentStep = this.currentStep!; currentStep.expectedState = currentStep.expectedState || {}; @@ -1029,7 +1055,8 @@ export class SpecBuilder { private addQueryToActiveTargets( targetId: number, query: Query, - resumeToken?: string + resumeToken?: string, + readTime?: TestSnapshotVersion ): void { if (this.activeTargets[targetId]) { const activeQueries = this.activeTargets[targetId].queries; @@ -1041,18 +1068,21 @@ export class SpecBuilder { // `query` is not added yet. this.activeTargets[targetId] = { queries: [SpecBuilder.queryToSpec(query), ...activeQueries], - resumeToken: resumeToken || '' + resumeToken: resumeToken || '', + readTime }; } else { this.activeTargets[targetId] = { queries: activeQueries, - resumeToken: resumeToken || '' + resumeToken: resumeToken || '', + readTime }; } } else { this.activeTargets[targetId] = { queries: [SpecBuilder.queryToSpec(query)], - resumeToken: resumeToken || '' + resumeToken: resumeToken || '', + readTime }; } } diff --git a/packages/firestore/test/unit/specs/spec_test_runner.ts b/packages/firestore/test/unit/specs/spec_test_runner.ts index 42868dca2e9..e3cf3faba63 100644 --- a/packages/firestore/test/unit/specs/spec_test_runner.ts +++ b/packages/firestore/test/unit/specs/spec_test_runner.ts @@ -376,8 +376,9 @@ abstract class TestRunner { private async doListen(listenSpec: SpecUserListen): Promise { let targetFailed = false; - const querySpec = listenSpec[1]; + const querySpec = listenSpec.query; const query = parseQuery(querySpec); + const aggregator = new EventAggregator(query, e => { if (e.error) { targetFailed = true; @@ -977,18 +978,24 @@ abstract class TestRunner { // TODO(mcg): populate the purpose of the target once it's possible to // encode that in the spec tests. For now, hard-code that it's a listen // despite the fact that it's not always the right value. - const expectedTarget = toTarget( - this.serializer, - new TargetData( - queryToTarget(parseQuery(expected.queries[0])), - targetId, - TargetPurpose.Listen, - ARBITRARY_SEQUENCE_NUMBER, - SnapshotVersion.min(), - SnapshotVersion.min(), - byteStringFromString(expected.resumeToken) - ) + let targetData = new TargetData( + queryToTarget(parseQuery(expected.queries[0])), + targetId, + TargetPurpose.Listen, + ARBITRARY_SEQUENCE_NUMBER ); + if (expected.resumeToken && expected.resumeToken !== '') { + targetData = targetData.withResumeToken( + byteStringFromString(expected.resumeToken), + SnapshotVersion.min() + ); + } else { + targetData = targetData.withResumeToken( + ByteString.EMPTY_BYTE_STRING, + version(expected.readTime!) + ); + } + const expectedTarget = toTarget(this.serializer, targetData); expect(actualTarget.query).to.deep.equal(expectedTarget.query); expect(actualTarget.targetId).to.equal(expectedTarget.targetId); expect(actualTarget.readTime).to.equal(expectedTarget.readTime); @@ -1360,8 +1367,10 @@ export interface SpecStep { expectedSnapshotsInSyncEvents?: number; } -/** [, ] */ -export type SpecUserListen = [TargetId, string | SpecQuery]; +export interface SpecUserListen { + targetId: TargetId; + query: string | SpecQuery; +} /** [, ] */ export type SpecUserUnlisten = [TargetId, string | SpecQuery]; diff --git a/packages/firestore/test/unit/specs/write_spec.test.ts b/packages/firestore/test/unit/specs/write_spec.test.ts index f9c5e3ed514..9c9fe8c39fa 100644 --- a/packages/firestore/test/unit/specs/write_spec.test.ts +++ b/packages/firestore/test/unit/specs/write_spec.test.ts @@ -1068,7 +1068,7 @@ describeSpec('Writes:', [], () => { // Start a new client. DocV1 still has pending writes. .client(1) .stealPrimaryLease() - .expectListen(query1, 'resume-token-1000') + .expectListen(query1, { resumeToken: 'resume-token-1000' }) .userListens(query2) .expectEvents(query2, { added: [docV1Committed], diff --git a/packages/firestore/test/unit/util/bundle_data.ts b/packages/firestore/test/unit/util/bundle_data.ts index 630d03ddf24..386256e0e3b 100644 --- a/packages/firestore/test/unit/util/bundle_data.ts +++ b/packages/firestore/test/unit/util/bundle_data.ts @@ -15,18 +15,28 @@ * limitations under the License. */ import { - BundledQuery, - BundleElement + BundleElement, + LimitType as BundleLimitType } from '../../../src/protos/firestore_bundle_proto'; import { DatabaseId } from '../../../src/core/database_info'; import * as api from '../../../src/protos/firestore_proto_api'; import { Value } from '../../../src/protos/firestore_proto_api'; -import { JsonProtoSerializer, toName } from '../../../src/remote/serializer'; +import { + JsonProtoSerializer, + toName, + toQueryTarget +} from '../../../src/remote/serializer'; import { DocumentKey } from '../../../src/model/document_key'; import { newSerializer, newTextEncoder } from '../../../src/platform/serializer'; +import { + LimitType, + Query, + queryToTarget, + queryWithLimit +} from '../../../src/core/query'; export const encoder = newTextEncoder(); @@ -73,14 +83,34 @@ export class TestBundleBuilder { }); return this; } + addNamedQuery( name: string, readTime: api.Timestamp, - bundledQuery: BundledQuery + query: Query ): TestBundleBuilder { - this.elements.push({ namedQuery: { name, readTime, bundledQuery } }); + let bundledLimitType: BundleLimitType | undefined = !!query.limit + ? 'FIRST' + : undefined; + if (query.limitType === LimitType.Last) { + query = queryWithLimit(query, query.limit!, LimitType.First); + bundledLimitType = 'LAST'; + } + const queryTarget = toQueryTarget(this.serializer, queryToTarget(query)); + this.elements.push({ + namedQuery: { + name, + readTime, + bundledQuery: { + parent: queryTarget.parent, + structuredQuery: queryTarget.structuredQuery, + limitType: bundledLimitType + } + } + }); return this; } + getMetadataElement( id: string, createTime: api.Timestamp, diff --git a/packages/firestore/test/util/helpers.ts b/packages/firestore/test/util/helpers.ts index 2f9e50f801b..fa358ab76f7 100644 --- a/packages/firestore/test/util/helpers.ts +++ b/packages/firestore/test/util/helpers.ts @@ -101,6 +101,8 @@ import { JsonProtoSerializer, toDocument, toName, + toQueryTarget, + toTimestamp, toVersion } from '../../src/remote/serializer'; import { Timestamp } from '../../src/api/timestamp'; @@ -112,7 +114,7 @@ import { TEST_DATABASE_ID } from '../unit/local/persistence_test_helpers'; import { BundledDocuments } from '../../src/core/bundle'; -import { BundleMetadata } from '../../src/protos/firestore_bundle_proto'; +import * as bundleProto from '../../src/protos/firestore_bundle_proto'; /* eslint-disable no-restricted-globals */ @@ -460,13 +462,31 @@ export function bundledDocuments( return new TestBundledDocuments(result); } +export function namedQuery( + name: string, + query: Query, + limitType: bundleProto.LimitType, + readTime: SnapshotVersion +): bundleProto.NamedQuery { + return { + name, + readTime: toTimestamp(JSON_SERIALIZER, readTime.toTimestamp()), + bundledQuery: { + parent: toQueryTarget(JSON_SERIALIZER, queryToTarget(query)).parent, + limitType, + structuredQuery: toQueryTarget(JSON_SERIALIZER, queryToTarget(query)) + .structuredQuery + } + }; +} + export function bundleMetadata( id: string, createTime: TestSnapshotVersion, version = 1, totalDocuments = 1, totalBytes = 1000 -): BundleMetadata { +): bundleProto.BundleMetadata { return { id, createTime: { seconds: createTime, nanos: 0 }, From 0f697df4e4146f376ad4274cf0da175a951be6a2 Mon Sep 17 00:00:00 2001 From: Wu-Hui Date: Mon, 10 Aug 2020 12:42:58 -0400 Subject: [PATCH 11/27] Merge with named query change. --- packages/firestore/exp/src/api/database.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/firestore/exp/src/api/database.ts b/packages/firestore/exp/src/api/database.ts index aec835d1836..48a313553f0 100644 --- a/packages/firestore/exp/src/api/database.ts +++ b/packages/firestore/exp/src/api/database.ts @@ -48,6 +48,7 @@ import { import { LoadBundleTask } from '../../../src/api/bundle'; import { Query } from '../../../lite'; import { + getLocalStore, getPersistence, getRemoteStore, getSyncEngine, @@ -63,6 +64,7 @@ import { User } from '../../../src/auth/user'; import { CredentialChangeListener } from '../../../src/api/credentials'; import { logDebug } from '../../../src/util/log'; import { debugAssert } from '../../../src/util/assert'; +import { getNamedQuery } from '../../../src/local/local_store'; const LOG_TAG = 'Firestore'; @@ -342,11 +344,12 @@ export async function namedQuery( name: string ): Promise { const firestoreImpl = cast(firestore, Firestore); - const client = await firestoreImpl._getFirestoreClient(); - const namedQuery = await client.getNamedQuery(name); + const localStore = await getLocalStore(firestoreImpl); + const namedQuery = await getNamedQuery(localStore, name); if (!namedQuery) { return null; } + // @ts-ignore return new Query(firestoreImpl, null, namedQuery.query); } From dc02c0327abdeb43eab75a47421f6c92435ab15b Mon Sep 17 00:00:00 2001 From: Wu-Hui Date: Mon, 10 Aug 2020 16:44:19 -0400 Subject: [PATCH 12/27] Add missing byte_stream_reader.ts for *_lite platforms --- packages/firebase/index.d.ts | 6 ++++++ packages/firestore/exp-types/index.d.ts | 5 +++++ packages/firestore/exp/src/api/database.ts | 4 ++-- .../browser_lite/byte_stream_reader.ts | 18 ++++++++++++++++++ .../platform/node_lite/byte_stream_reader.ts | 18 ++++++++++++++++++ .../src/platform/rn_lite/byte_stream_reader.ts | 18 ++++++++++++++++++ 6 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 packages/firestore/src/platform/browser_lite/byte_stream_reader.ts create mode 100644 packages/firestore/src/platform/node_lite/byte_stream_reader.ts create mode 100644 packages/firestore/src/platform/rn_lite/byte_stream_reader.ts diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index 8b45514b231..e1e36ce1454 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -8117,6 +8117,12 @@ declare namespace firebase.firestore { */ terminate(): Promise; + loadBundle( + bundleData: ArrayBuffer | ReadableStream | string + ): LoadBundleTask; + + namedQuery(name: string): Promise; + /** * @hidden */ diff --git a/packages/firestore/exp-types/index.d.ts b/packages/firestore/exp-types/index.d.ts index 7a1fc2e7e1e..89c027d76b4 100644 --- a/packages/firestore/exp-types/index.d.ts +++ b/packages/firestore/exp-types/index.d.ts @@ -539,6 +539,11 @@ export function loadBundle( bundleData: ArrayBuffer | ReadableStream | string ): LoadBundleTask; +export function namedQuery( + firestore: FirebaseFirestore, + name: string +): Promise; + export type FirestoreErrorCode = | 'cancelled' | 'unknown' diff --git a/packages/firestore/exp/src/api/database.ts b/packages/firestore/exp/src/api/database.ts index 48a313553f0..f9d2f49dd1d 100644 --- a/packages/firestore/exp/src/api/database.ts +++ b/packages/firestore/exp/src/api/database.ts @@ -350,6 +350,6 @@ export async function namedQuery( return null; } - // @ts-ignore - return new Query(firestoreImpl, null, namedQuery.query); + return null; + // return new Query(firestoreImpl, null, namedQuery.query); } diff --git a/packages/firestore/src/platform/browser_lite/byte_stream_reader.ts b/packages/firestore/src/platform/browser_lite/byte_stream_reader.ts new file mode 100644 index 00000000000..235c3e70616 --- /dev/null +++ b/packages/firestore/src/platform/browser_lite/byte_stream_reader.ts @@ -0,0 +1,18 @@ +/** + * @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 * from '../browser/byte_stream_reader'; diff --git a/packages/firestore/src/platform/node_lite/byte_stream_reader.ts b/packages/firestore/src/platform/node_lite/byte_stream_reader.ts new file mode 100644 index 00000000000..576537f10b6 --- /dev/null +++ b/packages/firestore/src/platform/node_lite/byte_stream_reader.ts @@ -0,0 +1,18 @@ +/** + * @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 * from '../node/byte_stream_reader'; diff --git a/packages/firestore/src/platform/rn_lite/byte_stream_reader.ts b/packages/firestore/src/platform/rn_lite/byte_stream_reader.ts new file mode 100644 index 00000000000..5bd0e6a52ec --- /dev/null +++ b/packages/firestore/src/platform/rn_lite/byte_stream_reader.ts @@ -0,0 +1,18 @@ +/** + * @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 * from '../rn/byte_stream_reader'; From 15b96a99c01e420a8fa5933e72fe8ae47efc6c1c Mon Sep 17 00:00:00 2001 From: Wu-Hui Date: Tue, 11 Aug 2020 15:05:44 -0400 Subject: [PATCH 13/27] Finally builds and passes --- packages/firestore/rollup.shared.js | 3 ++- .../firestore/src/platform/browser_lite/serializer.ts | 7 +------ packages/firestore/src/platform/node_lite/serializer.ts | 8 +------- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/firestore/rollup.shared.js b/packages/firestore/rollup.shared.js index 00eed3ba349..d91bf835333 100644 --- a/packages/firestore/rollup.shared.js +++ b/packages/firestore/rollup.shared.js @@ -93,7 +93,8 @@ exports.manglePrivatePropertiesOptions = { }, mangle: { properties: { - regex: /^__PRIVATE_/ + regex: /^__PRIVATE_/, + reserved: ['do'] } } }; diff --git a/packages/firestore/src/platform/browser_lite/serializer.ts b/packages/firestore/src/platform/browser_lite/serializer.ts index ed490bf16d7..5730c508f22 100644 --- a/packages/firestore/src/platform/browser_lite/serializer.ts +++ b/packages/firestore/src/platform/browser_lite/serializer.ts @@ -15,9 +15,4 @@ * limitations under the License. */ -import { JsonProtoSerializer } from '../../remote/serializer'; -import { DatabaseId } from '../../core/database_info'; - -export function newSerializer(databaseId: DatabaseId): JsonProtoSerializer { - return new JsonProtoSerializer(databaseId, /* useProto3Json= */ true); -} +export * from '../browser/serializer'; diff --git a/packages/firestore/src/platform/node_lite/serializer.ts b/packages/firestore/src/platform/node_lite/serializer.ts index c53bf2cbd7e..3f2d664b97c 100644 --- a/packages/firestore/src/platform/node_lite/serializer.ts +++ b/packages/firestore/src/platform/node_lite/serializer.ts @@ -15,10 +15,4 @@ * limitations under the License. */ -/** Return the Platform-specific serializer monitor. */ -import { JsonProtoSerializer } from '../../remote/serializer'; -import { DatabaseId } from '../../core/database_info'; - -export function newSerializer(databaseId: DatabaseId): JsonProtoSerializer { - return new JsonProtoSerializer(databaseId, /* useProto3Json= */ true); -} +export * from '../node/serializer'; From b4f9936ecb179586dc7ee701b17c5a61e1329907 Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Fri, 14 Aug 2020 13:11:27 -0400 Subject: [PATCH 14/27] Update query-document mapping from bundles. (#3620) * Update query-document mapping from bundles. * Only update mapping when query read time is updated. * Fix lint errors * Better code structure. * Use serializer constructed from firestore client. * Don't inline await --- packages/firestore/exp/src/api/database.ts | 15 ++- packages/firestore/src/core/bundle.ts | 47 +++++++-- .../firestore/src/core/firestore_client.ts | 16 ++- packages/firestore/src/core/sync_engine.ts | 6 +- packages/firestore/src/local/local_store.ts | 48 ++++++--- .../src/protos/firestore/bundle.proto | 3 + .../src/protos/firestore_bundle_proto.ts | 3 + packages/firestore/src/util/bundle_reader.ts | 14 ++- .../test/unit/local/local_store.test.ts | 99 ++++++++++++++++++- .../test/unit/specs/spec_test_runner.ts | 3 +- .../firestore/test/unit/util/bundle.test.ts | 6 +- packages/firestore/test/util/helpers.ts | 39 +++++--- 12 files changed, 244 insertions(+), 55 deletions(-) diff --git a/packages/firestore/exp/src/api/database.ts b/packages/firestore/exp/src/api/database.ts index f9d2f49dd1d..0a83bc05150 100644 --- a/packages/firestore/exp/src/api/database.ts +++ b/packages/firestore/exp/src/api/database.ts @@ -46,7 +46,6 @@ import { indexedDbStoragePrefix } from '../../../src/local/indexeddb_persistence'; import { LoadBundleTask } from '../../../src/api/bundle'; -import { Query } from '../../../lite'; import { getLocalStore, getPersistence, @@ -332,9 +331,17 @@ export function loadBundle( const firestoreImpl = cast(firestore, Firestore); const resultTask = new LoadBundleTask(); // eslint-disable-next-line @typescript-eslint/no-floating-promises - getSyncEngine(firestoreImpl).then(syncEngine => - enqueueLoadBundle(firestoreImpl._queue, syncEngine, bundleData, resultTask) - ); + getSyncEngine(firestoreImpl).then(async syncEngine => { + const databaseId = (await firestoreImpl._getConfiguration()).databaseInfo + .databaseId; + enqueueLoadBundle( + databaseId, + firestoreImpl._queue, + syncEngine, + bundleData, + resultTask + ); + }); return resultTask; } diff --git a/packages/firestore/src/core/bundle.ts b/packages/firestore/src/core/bundle.ts index 55f00ffb39b..8625f4f0c23 100644 --- a/packages/firestore/src/core/bundle.ts +++ b/packages/firestore/src/core/bundle.ts @@ -35,7 +35,11 @@ import { saveNamedQuery } from '../local/local_store'; import { SizedBundleElement } from '../util/bundle_reader'; -import { MaybeDocumentMap } from '../model/collections'; +import { + documentKeySet, + DocumentKeySet, + MaybeDocumentMap +} from '../model/collections'; import { BundleMetadata } from '../protos/firestore_bundle_proto'; /** @@ -79,7 +83,7 @@ export type BundledDocuments = BundledDocument[]; * Helper to convert objects from bundles to model objects in the SDK. */ export class BundleConverter { - constructor(private serializer: JsonProtoSerializer) {} + constructor(private readonly serializer: JsonProtoSerializer) {} toDocumentKey(name: string): DocumentKey { return fromName(this.serializer, name); @@ -161,7 +165,8 @@ export class BundleLoader { constructor( private metadata: bundleProto.BundleMetadata, - private localStore: LocalStore + private localStore: LocalStore, + private serializer: JsonProtoSerializer ) { this.progress = bundleInitialProgress(metadata); } @@ -208,6 +213,28 @@ export class BundleLoader { return null; } + private getQueryDocumentMapping( + documents: BundledDocuments + ): Map { + const queryDocumentMap = new Map(); + const bundleConverter = new BundleConverter(this.serializer); + for (const bundleDoc of documents) { + if (bundleDoc.metadata.queries) { + const documentKey = bundleConverter.toDocumentKey( + bundleDoc.metadata.name! + ); + for (const queryName of bundleDoc.metadata.queries) { + const documentKeys = ( + queryDocumentMap.get(queryName) || documentKeySet() + ).add(documentKey); + queryDocumentMap.set(queryName, documentKeys); + } + } + } + + return queryDocumentMap; + } + /** * Update the progress to 'Success' and return the updated progress. */ @@ -218,16 +245,18 @@ export class BundleLoader { 'Bundled documents ends with a document metadata and missing document.' ); - for (const q of this.queries) { - await saveNamedQuery(this.localStore, q); - } - - const changedDocs = await applyBundleDocuments( + const changedDocuments = await applyBundleDocuments( this.localStore, this.documents ); + const queryDocumentMap = this.getQueryDocumentMapping(this.documents); + + for (const q of this.queries) { + await saveNamedQuery(this.localStore, q, queryDocumentMap.get(q.name!)); + } + this.progress.taskState = 'Success'; - return new BundleLoadResult({ ...this.progress }, changedDocs); + return new BundleLoadResult({ ...this.progress }, changedDocuments); } } diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 1972d83955c..88ea5e21eda 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -55,9 +55,10 @@ import { TransactionRunner } from './transaction_runner'; import { Datastore } from '../remote/datastore'; import { BundleReader } from '../util/bundle_reader'; import { LoadBundleTask } from '../api/bundle'; -import { newTextEncoder } from '../platform/serializer'; +import { newSerializer, newTextEncoder } from '../platform/serializer'; import { toByteStreamReader } from '../platform/byte_stream_reader'; import { NamedQuery } from './bundle'; +import { JsonProtoSerializer } from '../remote/serializer'; const LOG_TAG = 'FirestoreClient'; export const MAX_CONCURRENT_LIMBO_RESOLUTIONS = 100; @@ -537,7 +538,10 @@ export class FirestoreClient { ): void { this.verifyNotTerminated(); - const reader = createBundleReader(data); + const reader = createBundleReader( + data, + newSerializer(this.databaseInfo.databaseId) + ); this.asyncQueue.enqueueAndForget(async () => { loadBundle(this.syncEngine, reader, resultTask); return resultTask.catch(e => { @@ -798,7 +802,8 @@ export function enqueueExecuteQueryViaSnapshotListener( } function createBundleReader( - data: ReadableStream | ArrayBuffer | string + data: ReadableStream | ArrayBuffer | string, + serializer: JsonProtoSerializer ): BundleReader { let content: ReadableStream | ArrayBuffer; if (typeof data === 'string') { @@ -806,16 +811,17 @@ function createBundleReader( } else { content = data; } - return new BundleReader(toByteStreamReader(content)); + return new BundleReader(toByteStreamReader(content), serializer); } export function enqueueLoadBundle( + databaseId: DatabaseId, asyncQueue: AsyncQueue, syncEngine: SyncEngine, data: ReadableStream | ArrayBuffer | string, resultTask: LoadBundleTask ): void { - const reader = createBundleReader(data); + const reader = createBundleReader(data, newSerializer(databaseId)); asyncQueue.enqueueAndForget(async () => { loadBundle(syncEngine, reader, resultTask); return resultTask.catch(e => { diff --git a/packages/firestore/src/core/sync_engine.ts b/packages/firestore/src/core/sync_engine.ts index 2dd5c81abe7..ae4fd542d64 100644 --- a/packages/firestore/src/core/sync_engine.ts +++ b/packages/firestore/src/core/sync_engine.ts @@ -1358,7 +1358,11 @@ async function loadBundleImpl( task._updateProgress(bundleInitialProgress(metadata)); - const loader = new BundleLoader(metadata, syncEngine.localStore); + const loader = new BundleLoader( + metadata, + syncEngine.localStore, + reader.serializer + ); let element = await reader.nextElement(); while (element) { debugAssert( diff --git a/packages/firestore/src/local/local_store.ts b/packages/firestore/src/local/local_store.ts index cfb6ee8370e..94c87e852aa 100644 --- a/packages/firestore/src/local/local_store.ts +++ b/packages/firestore/src/local/local_store.ts @@ -1373,7 +1373,8 @@ export function getNamedQuery( */ export async function saveNamedQuery( localStore: LocalStore, - query: bundleProto.NamedQuery + query: bundleProto.NamedQuery, + documents: DocumentKeySet = documentKeySet() ): Promise { // Allocate a target for the named query such that it can be resumed // from associated read time if users use it to listen. @@ -1383,27 +1384,46 @@ export async function saveNamedQuery( const allocated = await localStore.allocateTarget( queryToTarget(fromBundledQuery(query.bundledQuery!)) ); + const localStoreImpl = debugCast(localStore, LocalStoreImpl); return localStoreImpl.persistence.runTransaction( 'Save named query', 'readwrite', transaction => { - // Update allocated target's read time, if the bundle's read time is newer. - let updateReadTime = PersistencePromise.resolve(); const readTime = fromVersion(query.readTime!); - if (allocated.snapshotVersion.compareTo(readTime) < 0) { - const newTargetData = allocated.withResumeToken( - ByteString.EMPTY_BYTE_STRING, - readTime - ); - updateReadTime = localStoreImpl.targetCache.updateTargetData( - transaction, - newTargetData - ); + // Simply save the query itself if it is older than what the SDK already + // has. + if (allocated.snapshotVersion.compareTo(readTime) >= 0) { + return localStoreImpl.bundleCache.saveNamedQuery(transaction, query); } - return updateReadTime.next(() => - localStoreImpl.bundleCache.saveNamedQuery(transaction, query) + + // Update existing target data because the query from the bundle is newer. + const newTargetData = allocated.withResumeToken( + ByteString.EMPTY_BYTE_STRING, + readTime ); + localStoreImpl.targetDataByTarget = localStoreImpl.targetDataByTarget.insert( + newTargetData.targetId, + newTargetData + ); + return localStoreImpl.targetCache + .updateTargetData(transaction, newTargetData) + .next(() => + localStoreImpl.targetCache.removeMatchingKeysForTargetId( + transaction, + allocated.targetId + ) + ) + .next(() => + localStoreImpl.targetCache.addMatchingKeys( + transaction, + documents, + allocated.targetId + ) + ) + .next(() => + localStoreImpl.bundleCache.saveNamedQuery(transaction, query) + ); } ); } diff --git a/packages/firestore/src/protos/firestore/bundle.proto b/packages/firestore/src/protos/firestore/bundle.proto index ca19071e71f..ee7954e664c 100644 --- a/packages/firestore/src/protos/firestore/bundle.proto +++ b/packages/firestore/src/protos/firestore/bundle.proto @@ -79,6 +79,9 @@ message BundledDocumentMetadata { // Whether the document exists. bool exists = 3; + + // The names of the queries in this bundle that this document matches to. + repeated string queries = 4; } // Metadata describing the bundle file/stream. diff --git a/packages/firestore/src/protos/firestore_bundle_proto.ts b/packages/firestore/src/protos/firestore_bundle_proto.ts index 49b8ef07c36..304fe3e68c7 100644 --- a/packages/firestore/src/protos/firestore_bundle_proto.ts +++ b/packages/firestore/src/protos/firestore_bundle_proto.ts @@ -54,6 +54,9 @@ export interface BundledDocumentMetadata { /** BundledDocumentMetadata exists */ exists?: boolean | null; + + /** The names of the queries in this bundle that this document matches to. */ + queries?: string[]; } /** Properties of a BundleMetadata. */ diff --git a/packages/firestore/src/util/bundle_reader.ts b/packages/firestore/src/util/bundle_reader.ts index d2e9c23ac58..ae3badda147 100644 --- a/packages/firestore/src/util/bundle_reader.ts +++ b/packages/firestore/src/util/bundle_reader.ts @@ -23,6 +23,7 @@ import { Deferred } from './promise'; import { debugAssert } from './assert'; import { toByteStreamReader } from '../platform/byte_stream_reader'; import { newTextDecoder } from '../platform/serializer'; +import { JsonProtoSerializer } from '../remote/serializer'; /** * A complete element in the bundle stream, together with the byte length it @@ -70,13 +71,20 @@ export class BundleReader { /** The decoder used to parse binary data into strings. */ private textDecoder: TextDecoder; - static fromBundleSource(source: BundleSource): BundleReader { - return new BundleReader(toByteStreamReader(source, BYTES_PER_READ)); + static fromBundleSource( + source: BundleSource, + serializer: JsonProtoSerializer + ): BundleReader { + return new BundleReader( + toByteStreamReader(source, BYTES_PER_READ), + serializer + ); } constructor( /** The reader to read from underlying binary bundle data source. */ - private reader: ReadableStreamReader + private reader: ReadableStreamReader, + readonly serializer: JsonProtoSerializer ) { this.textDecoder = newTextDecoder(); // Read the metadata (which is the first element). diff --git a/packages/firestore/test/unit/local/local_store.test.ts b/packages/firestore/test/unit/local/local_store.test.ts index 9e3871a9af6..7ec66099511 100644 --- a/packages/firestore/test/unit/local/local_store.test.ts +++ b/packages/firestore/test/unit/local/local_store.test.ts @@ -48,6 +48,7 @@ import { LocalViewChanges } from '../../../src/local/local_view_changes'; import { Persistence } from '../../../src/local/persistence'; import { SimpleQueryEngine } from '../../../src/local/simple_query_engine'; import { + DocumentKeySet, documentKeySet, MaybeDocumentMap } from '../../../src/model/collections'; @@ -91,6 +92,7 @@ import { query, setMutation, TestBundledDocuments, + TestNamedQuery, TestSnapshotVersion, transformMutation, unknownDoc, @@ -137,7 +139,7 @@ class LocalStoreTester { | RemoteEvent | LocalViewChanges | TestBundledDocuments - | bundleProto.NamedQuery + | TestNamedQuery ): LocalStoreTester { if (op instanceof Mutation) { return this.afterMutations([op]); @@ -188,17 +190,21 @@ class LocalStoreTester { this.promiseChain = this.promiseChain .then(() => applyBundleDocuments(this.localStore, documents)) - .then((result: MaybeDocumentMap) => { + .then(result => { this.lastChanges = result; }); return this; } - afterNamedQuery(namedQuery: bundleProto.NamedQuery): LocalStoreTester { + afterNamedQuery(testQuery: TestNamedQuery): LocalStoreTester { this.prepareNextStep(); this.promiseChain = this.promiseChain.then(() => - saveNamedQuery(this.localStore, namedQuery) + saveNamedQuery( + this.localStore, + testQuery.namedQuery, + testQuery.matchingDocuments + ) ); return this; } @@ -432,6 +438,29 @@ class LocalStoreTester { return this; } + toHaveQueryDocumentMapping( + persistence: Persistence, + targetId: TargetId, + expectedKeys: DocumentKeySet + ): LocalStoreTester { + this.promiseChain = this.promiseChain.then(() => { + return persistence.runTransaction( + 'toHaveQueryDocumentMapping', + 'readonly', + transaction => { + return persistence + .getTargetCache() + .getMatchingKeysForTargetId(transaction, targetId) + .next(matchedKeys => { + expect(matchedKeys.isEqual(expectedKeys)).to.be.true; + }); + } + ); + }); + + return this; + } + toHaveNewerBundle( metadata: bundleProto.BundleMetadata, expected: boolean @@ -1706,6 +1735,68 @@ function genericLocalStoreTests( .finish(); }); + it('loading named queries allocates targets and updates target document mapping', async () => { + const expectedQueryDocumentMap = new Map([ + ['query-1', documentKeySet(key('foo1/bar'))], + ['query-2', documentKeySet(key('foo2/bar'))] + ]); + const version1 = SnapshotVersion.fromTimestamp(Timestamp.fromMillis(10000)); + const version2 = SnapshotVersion.fromTimestamp(Timestamp.fromMillis(20000)); + + return expectLocalStore() + .after( + bundledDocuments( + [doc('foo1/bar', 1, { sum: 1337 }), doc('foo2/bar', 2, { sum: 42 })], + [['query-1'], ['query-2']] + ) + ) + .toReturnChanged( + doc('foo1/bar', 1, { sum: 1337 }), + doc('foo2/bar', 2, { sum: 42 }) + ) + .toContain(doc('foo1/bar', 1, { sum: 1337 })) + .toContain(doc('foo2/bar', 2, { sum: 42 })) + .after( + namedQuery( + 'query-1', + query('foo1'), + /* limitType */ 'FIRST', + version1, + expectedQueryDocumentMap.get('query-1') + ) + ) + .toHaveNamedQuery({ + name: 'query-1', + query: query('foo1'), + readTime: version1 + }) + .toHaveQueryDocumentMapping( + persistence, + /*targetId*/ 2, + /*expectedKeys*/ documentKeySet(key('foo1/bar')) + ) + .after( + namedQuery( + 'query-2', + query('foo2'), + /* limitType */ 'FIRST', + version2, + expectedQueryDocumentMap.get('query-2') + ) + ) + .toHaveNamedQuery({ + name: 'query-2', + query: query('foo2'), + readTime: version2 + }) + .toHaveQueryDocumentMapping( + persistence, + /*targetId*/ 4, + /*expectedKeys*/ documentKeySet(key('foo2/bar')) + ) + .finish(); + }); + it('handles saving and loading limit to last queries', async () => { const now = Timestamp.now(); return expectLocalStore() diff --git a/packages/firestore/test/unit/specs/spec_test_runner.ts b/packages/firestore/test/unit/specs/spec_test_runner.ts index 20968d29501..7443b673ea0 100644 --- a/packages/firestore/test/unit/specs/spec_test_runner.ts +++ b/packages/firestore/test/unit/specs/spec_test_runner.ts @@ -477,7 +477,8 @@ abstract class TestRunner { private async doLoadBundle(bundle: string): Promise { const reader = new BundleReader( - toByteStreamReader(newTextEncoder().encode(bundle)) + toByteStreamReader(newTextEncoder().encode(bundle)), + this.serializer ); const task = new LoadBundleTask(); return this.queue.enqueue(async () => { diff --git a/packages/firestore/test/unit/util/bundle.test.ts b/packages/firestore/test/unit/util/bundle.test.ts index 6b74cb02577..a0a6151c36d 100644 --- a/packages/firestore/test/unit/util/bundle.test.ts +++ b/packages/firestore/test/unit/util/bundle.test.ts @@ -40,6 +40,7 @@ import { doc2 } from './bundle_data'; import { newTextEncoder } from '../../../src/platform/serializer'; +import { JSON_SERIALIZER } from '../local/persistence_test_helpers'; use(chaiAsPromised); @@ -92,7 +93,10 @@ describe('Bundle ', () => { function genericBundleReadingTests(bytesPerRead: number): void { function bundleFromString(s: string): BundleReader { - return new BundleReader(byteStreamReaderFromString(s, bytesPerRead)); + return new BundleReader( + byteStreamReaderFromString(s, bytesPerRead), + JSON_SERIALIZER + ); } async function getAllElements( diff --git a/packages/firestore/test/util/helpers.ts b/packages/firestore/test/util/helpers.ts index fa358ab76f7..ec3e10cc0b4 100644 --- a/packages/firestore/test/util/helpers.ts +++ b/packages/firestore/test/util/helpers.ts @@ -445,14 +445,16 @@ export class TestBundledDocuments { } export function bundledDocuments( - documents: MaybeDocument[] + documents: MaybeDocument[], + queryNames?: string[][] ): TestBundledDocuments { - const result = documents.map(d => { + const result = documents.map((d, index) => { return { metadata: { name: toName(JSON_SERIALIZER, d.key), readTime: toVersion(JSON_SERIALIZER, d.version), - exists: d instanceof Document + exists: d instanceof Document, + queries: queryNames ? queryNames[index] : undefined }, document: d instanceof Document ? toDocument(JSON_SERIALIZER, d) : undefined @@ -462,21 +464,32 @@ export function bundledDocuments( return new TestBundledDocuments(result); } +export class TestNamedQuery { + constructor( + public namedQuery: bundleProto.NamedQuery, + public matchingDocuments: DocumentKeySet + ) {} +} + export function namedQuery( name: string, query: Query, limitType: bundleProto.LimitType, - readTime: SnapshotVersion -): bundleProto.NamedQuery { + readTime: SnapshotVersion, + matchingDocuments: DocumentKeySet = documentKeySet() +): TestNamedQuery { return { - name, - readTime: toTimestamp(JSON_SERIALIZER, readTime.toTimestamp()), - bundledQuery: { - parent: toQueryTarget(JSON_SERIALIZER, queryToTarget(query)).parent, - limitType, - structuredQuery: toQueryTarget(JSON_SERIALIZER, queryToTarget(query)) - .structuredQuery - } + namedQuery: { + name, + readTime: toTimestamp(JSON_SERIALIZER, readTime.toTimestamp()), + bundledQuery: { + parent: toQueryTarget(JSON_SERIALIZER, queryToTarget(query)).parent, + limitType, + structuredQuery: toQueryTarget(JSON_SERIALIZER, queryToTarget(query)) + .structuredQuery + } + }, + matchingDocuments }; } From c4b0995e4a70c89d501952f11c4b5b82b1ab91dd Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Wed, 19 Aug 2020 13:14:53 -0400 Subject: [PATCH 15/27] Introduce an umbrella target for bundled documents (#3643) * Introduce an umbrella target for bundled documents * Update packages/firestore/src/local/local_store.ts Co-authored-by: Sebastian Schmidt * Update packages/firestore/test/unit/specs/spec_builder.ts Co-authored-by: Sebastian Schmidt * Address comments * More comments. * Rename metadata to bundleMetadata Co-authored-by: Sebastian Schmidt --- packages/firestore/src/core/bundle.ts | 10 ++-- packages/firestore/src/local/local_store.ts | 56 ++++++++++++++++--- .../integration/api_internal/bundle.test.ts | 22 ++++++++ .../test/unit/local/local_store.test.ts | 45 +++++++++++++-- .../firestore/test/unit/specs/spec_builder.ts | 5 ++ packages/firestore/test/util/helpers.ts | 7 ++- 6 files changed, 124 insertions(+), 21 deletions(-) diff --git a/packages/firestore/src/core/bundle.ts b/packages/firestore/src/core/bundle.ts index 8625f4f0c23..32dcc965cc1 100644 --- a/packages/firestore/src/core/bundle.ts +++ b/packages/firestore/src/core/bundle.ts @@ -25,6 +25,7 @@ import { JsonProtoSerializer } from '../remote/serializer'; import * as bundleProto from '../protos/firestore_bundle_proto'; +import { BundleMetadata } from '../protos/firestore_bundle_proto'; import * as api from '../protos/firestore_proto_api'; import { DocumentKey } from '../model/document_key'; import { MaybeDocument, NoDocument } from '../model/document'; @@ -40,7 +41,6 @@ import { DocumentKeySet, MaybeDocumentMap } from '../model/collections'; -import { BundleMetadata } from '../protos/firestore_bundle_proto'; /** * Represents a Firestore bundle saved by the SDK in its local storage. @@ -164,11 +164,11 @@ export class BundleLoader { private documents: BundledDocuments = []; constructor( - private metadata: bundleProto.BundleMetadata, + private bundleMetadata: bundleProto.BundleMetadata, private localStore: LocalStore, private serializer: JsonProtoSerializer ) { - this.progress = bundleInitialProgress(metadata); + this.progress = bundleInitialProgress(bundleMetadata); } /** @@ -244,10 +244,12 @@ export class BundleLoader { !!this.documents[this.documents.length - 1].document, 'Bundled documents ends with a document metadata and missing document.' ); + debugAssert(!!this.bundleMetadata.id, 'Bundle ID must be set.'); const changedDocuments = await applyBundleDocuments( this.localStore, - this.documents + this.documents, + this.bundleMetadata.id! ); const queryDocumentMap = this.getQueryDocumentMapping(this.documents); diff --git a/packages/firestore/src/local/local_store.ts b/packages/firestore/src/local/local_store.ts index e5aa982bf8a..006de4cb46b 100644 --- a/packages/firestore/src/local/local_store.ts +++ b/packages/firestore/src/local/local_store.ts @@ -17,7 +17,7 @@ import { Timestamp } from '../api/timestamp'; import { User } from '../auth/user'; -import { Query, queryToTarget } from '../core/query'; +import { newQueryForPath, Query, queryToTarget } from '../core/query'; import { SnapshotVersion } from '../core/snapshot_version'; import { canonifyTarget, Target, targetEquals } from '../core/target'; import { BatchId, TargetId } from '../core/types'; @@ -33,10 +33,10 @@ import { import { MaybeDocument, NoDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; import { + extractMutationBaseValue, Mutation, PatchMutation, - Precondition, - extractMutationBaseValue + Precondition } from '../model/mutation'; import { BATCHID_UNKNOWN, @@ -79,6 +79,7 @@ import { BundleCache } from './bundle_cache'; import { fromVersion, JsonProtoSerializer } from '../remote/serializer'; import { fromBundledQuery } from './local_serializer'; import { ByteString } from '../util/byte_string'; +import { ResourcePath } from '../model/path'; const LOG_TAG = 'LocalStore'; @@ -1275,6 +1276,20 @@ export async function ignoreIfPrimaryLeaseLoss( } } +/** + * Creates a new target using the given bundle name, which will be used to + * hold the keys of all documents from the bundle in query-document mappings. + * This ensures that the loaded documents do not get garbage collected + * right away. + */ +export function umbrellaTarget(bundleName: string): Target { + // It is OK that the path used for the query is not valid, because this will + // not be read and queried. + return queryToTarget( + newQueryForPath(ResourcePath.fromString(`__bundle__/docs/${bundleName}`)) + ); +} + /** * Applies the documents from a bundle to the "ground-state" (remote) * documents. @@ -1282,16 +1297,21 @@ export async function ignoreIfPrimaryLeaseLoss( * LocalDocuments are re-calculated if there are remaining mutations in the * queue. */ -export function applyBundleDocuments( +export async function applyBundleDocuments( localStore: LocalStore, - documents: BundledDocuments + documents: BundledDocuments, + bundleName: string ): Promise { const localStoreImpl = debugCast(localStore, LocalStoreImpl); const bundleConverter = new BundleConverter(localStoreImpl.serializer); + let documentKeys = documentKeySet(); let documentMap = maybeDocumentMap(); let versionMap = documentVersionMap(); for (const bundleDoc of documents) { const documentKey = bundleConverter.toDocumentKey(bundleDoc.metadata.name!); + if (bundleDoc.document) { + documentKeys = documentKeys.add(documentKey); + } documentMap = documentMap.insert( documentKey, bundleConverter.toMaybeDocument(bundleDoc) @@ -1305,6 +1325,13 @@ export function applyBundleDocuments( const documentBuffer = localStoreImpl.remoteDocuments.newChangeBuffer({ trackRemovals: true // Make sure document removals show up in `getNewDocumentChanges()` }); + + // Allocates a target to hold all document keys from the bundle, such that + // they will not get garbage collected right away. + const umbrellaTargetData = await allocateTarget( + localStoreImpl, + umbrellaTarget(bundleName) + ); return localStoreImpl.persistence.runTransaction( 'Apply bundle documents', 'readwrite', @@ -1321,10 +1348,21 @@ export function applyBundleDocuments( return changedDocs; }) .next(changedDocs => { - return localStoreImpl.localDocuments.getLocalViewOfDocuments( - txn, - changedDocs - ); + return localStoreImpl.targetCache + .removeMatchingKeysForTargetId(txn, umbrellaTargetData.targetId) + .next(() => + localStoreImpl.targetCache.addMatchingKeys( + txn, + documentKeys, + umbrellaTargetData.targetId + ) + ) + .next(() => + localStoreImpl.localDocuments.getLocalViewOfDocuments( + txn, + changedDocs + ) + ); }); } ); diff --git a/packages/firestore/test/integration/api_internal/bundle.test.ts b/packages/firestore/test/integration/api_internal/bundle.test.ts index 6581404c6d5..c9af02a2309 100644 --- a/packages/firestore/test/integration/api_internal/bundle.test.ts +++ b/packages/firestore/test/integration/api_internal/bundle.test.ts @@ -242,6 +242,28 @@ apiDescribe('Bundles', (persistence: boolean) => { }); }); + it('loaded documents should not be GC-ed right away', () => { + return withTestDb(persistence, async db => { + const builder = bundleWithTestDocsAndQueries(db); + + const fulfillProgress: firestore.LoadBundleTaskProgress = await db.loadBundle( + builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) + ); + + verifySuccessProgress(fulfillProgress!); + + // Read a different collection, this will trigger GC. + let snap = await db.collection('coll-other').get(); + expect(snap.empty).to.be.true; + + // Read the loaded documents, expecting document in cache. With memory + // GC, the documents would get GC-ed if we did not hold the document keys + // in a "umbrella" target. See local_store.ts for details. + snap = await db.collection('coll-1').get({ source: 'cache' }); + verifySnapEqualTestDocs(snap); + }); + }); + it('load with documents from other projects fails', () => { return withTestDb(persistence, async db => { let builder = bundleWithTestDocsAndQueries(db); diff --git a/packages/firestore/test/unit/local/local_store.test.ts b/packages/firestore/test/unit/local/local_store.test.ts index 31c64a9655e..c58ce045d0d 100644 --- a/packages/firestore/test/unit/local/local_store.test.ts +++ b/packages/firestore/test/unit/local/local_store.test.ts @@ -161,7 +161,7 @@ class LocalStoreTester { } else if (op instanceof RemoteEvent) { return this.afterRemoteEvent(op); } else if (op instanceof TestBundledDocuments) { - return this.afterBundleDocuments(op.documents); + return this.afterBundleDocuments(op.documents, op.bundleName); } else { return this.afterNamedQuery(op); } @@ -192,11 +192,16 @@ class LocalStoreTester { return this; } - afterBundleDocuments(documents: BundledDocuments): LocalStoreTester { + afterBundleDocuments( + documents: BundledDocuments, + bundleName?: string + ): LocalStoreTester { this.prepareNextStep(); this.promiseChain = this.promiseChain - .then(() => applyBundleDocuments(this.localStore, documents)) + .then(() => + applyBundleDocuments(this.localStore, documents, bundleName || '') + ) .then(result => { this.lastChanges = result; }); @@ -1624,6 +1629,11 @@ function genericLocalStoreTests( ) .toContain(doc('foo/bar', 1, { sum: 1337 })) .toContain(deletedDoc('foo/bar1', 1)) + .toHaveQueryDocumentMapping( + persistence, + /*targetId*/ 2, + /*expectedKeys*/ documentKeySet(key('foo/bar')) + ) .finish(); }); @@ -1643,6 +1653,11 @@ function genericLocalStoreTests( .toReturnChanged(deletedDoc('foo/bar1', 1)) .toContain(doc('foo/bar', 2, { sum: 1337 })) .toContain(deletedDoc('foo/bar1', 1)) + .toHaveQueryDocumentMapping( + persistence, + /*targetId*/ 4, + /*expectedKeys*/ documentKeySet(key('foo/bar')) + ) .finish(); }); @@ -1665,6 +1680,11 @@ function genericLocalStoreTests( ) .toContain(doc('foo/new', 1, { sum: 1336 })) .toContain(deletedDoc('foo/bar', 2)) + .toHaveQueryDocumentMapping( + persistence, + /*targetId*/ 4, + /*expectedKeys*/ documentKeySet(key('foo/new')) + ) .finish(); }); @@ -1678,6 +1698,11 @@ function genericLocalStoreTests( .after(bundledDocuments([doc('foo/bar', 1, { val: 'new' })])) .toReturnChanged() .toContain(doc('foo/bar', 1, { val: 'old' })) + .toHaveQueryDocumentMapping( + persistence, + /*targetId*/ 4, + /*expectedKeys*/ documentKeySet(key('foo/bar')) + ) .finish(); }); @@ -1699,6 +1724,11 @@ function genericLocalStoreTests( doc('foo/bar', 1, { sum: 1 }, { hasLocalMutations: true }) ) .toContain(doc('foo/bar', 1, { sum: 1 }, { hasLocalMutations: true })) + .toHaveQueryDocumentMapping( + persistence, + /*targetId*/ 4, + /*expectedKeys*/ documentKeySet(key('foo/bar')) + ) .finish(); }); @@ -1721,6 +1751,11 @@ function genericLocalStoreTests( doc('foo/bar', 1, { sum: 1 }, { hasLocalMutations: true }) ) .toContain(doc('foo/bar', 1, { sum: 1 }, { hasLocalMutations: true })) + .toHaveQueryDocumentMapping( + persistence, + /*targetId*/ 4, + /*expectedKeys*/ documentKeySet(key('foo/bar')) + ) .finish(); }); @@ -1787,7 +1822,7 @@ function genericLocalStoreTests( }) .toHaveQueryDocumentMapping( persistence, - /*targetId*/ 2, + /*targetId*/ 4, /*expectedKeys*/ documentKeySet(key('foo1/bar')) ) .after( @@ -1806,7 +1841,7 @@ function genericLocalStoreTests( }) .toHaveQueryDocumentMapping( persistence, - /*targetId*/ 4, + /*targetId*/ 6, /*expectedKeys*/ documentKeySet(key('foo2/bar')) ) .finish(); diff --git a/packages/firestore/test/unit/specs/spec_builder.ts b/packages/firestore/test/unit/specs/spec_builder.ts index 7eae0d3d593..401c37cc94a 100644 --- a/packages/firestore/test/unit/specs/spec_builder.ts +++ b/packages/firestore/test/unit/specs/spec_builder.ts @@ -62,6 +62,7 @@ import { SpecWriteAck, SpecWriteFailure } from './spec_test_runner'; +import { ResourcePath } from '../../../src/model/path'; const userDataWriter = testUserDataWriter(); @@ -375,6 +376,10 @@ export class SpecBuilder { this.currentStep = { loadBundle: bundleContent }; + // Loading a bundle implicitly creates a new target. We advance the `queryIdGenerator` to match. + this.queryIdGenerator.next( + queryToTarget(newQueryForPath(ResourcePath.emptyPath())) + ); return this; } diff --git a/packages/firestore/test/util/helpers.ts b/packages/firestore/test/util/helpers.ts index ec3e10cc0b4..7080fbc28c6 100644 --- a/packages/firestore/test/util/helpers.ts +++ b/packages/firestore/test/util/helpers.ts @@ -441,12 +441,13 @@ export function docUpdateRemoteEvent( } export class TestBundledDocuments { - constructor(public documents: BundledDocuments) {} + constructor(public documents: BundledDocuments, public bundleName: string) {} } export function bundledDocuments( documents: MaybeDocument[], - queryNames?: string[][] + queryNames?: string[][], + bundleName?: string ): TestBundledDocuments { const result = documents.map((d, index) => { return { @@ -461,7 +462,7 @@ export function bundledDocuments( }; }); - return new TestBundledDocuments(result); + return new TestBundledDocuments(result, bundleName || ''); } export class TestNamedQuery { From 775d58a61cf616741af6b0cb5da8fa194aec5893 Mon Sep 17 00:00:00 2001 From: Wu-Hui Date: Thu, 8 Oct 2020 14:54:59 -0400 Subject: [PATCH 16/27] Restore some files from master --- common/api-review/functions-exp.api.md | 2 +- packages/auth/src/args.js | 2 +- packages/auth/src/auth.js | 2 +- packages/firestore/scripts/build-bundle.js | 187 +----- packages/firestore/scripts/extract-api.js | 58 +- packages/firestore/scripts/remove-asserts.js | 66 +- .../firestore/scripts/rename-internals.js | 51 +- packages/firestore/scripts/run-tests.js | 51 +- yarn.lock | 612 ++++++++++-------- 9 files changed, 343 insertions(+), 688 deletions(-) diff --git a/common/api-review/functions-exp.api.md b/common/api-review/functions-exp.api.md index 947534f6548..425aa90c1e8 100644 --- a/common/api-review/functions-exp.api.md +++ b/common/api-review/functions-exp.api.md @@ -10,7 +10,7 @@ import { HttpsCallable } from '@firebase/functions-types-exp'; import { HttpsCallableOptions } from '@firebase/functions-types-exp'; // @public -export function getFunctions(app: FirebaseApp, regionOrCustomDomain?: string): Functions; +export function getFunctions(app: FirebaseApp, region?: string): Functions; // @public export function httpsCallable(functionsInstance: Functions, name: string, options?: HttpsCallableOptions): HttpsCallable; diff --git a/packages/auth/src/args.js b/packages/auth/src/args.js index 50ed5f44fea..ed6547e6c22 100644 --- a/packages/auth/src/args.js +++ b/packages/auth/src/args.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google LLC + * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/auth/src/auth.js b/packages/auth/src/auth.js index eca21f5f5ff..f65c9ccdfd3 100644 --- a/packages/auth/src/auth.js +++ b/packages/auth/src/auth.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google LLC + * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/firestore/scripts/build-bundle.js b/packages/firestore/scripts/build-bundle.js index 100ae58d884..3e4674e7bd5 100644 --- a/packages/firestore/scripts/build-bundle.js +++ b/packages/firestore/scripts/build-bundle.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; /** * @license * Copyright 2020 Google LLC @@ -14,187 +14,4 @@ * 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. - */ var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator['throw'](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done - ? resolve(result.value) - : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __generator = - (this && this.__generator) || - function (thisArg, body) { - var _ = { - label: 0, - sent: function () { - if (t[0] & 1) throw t[1]; - return t[1]; - }, - trys: [], - ops: [] - }, - f, - y, - t, - g; - return ( - (g = { next: verb(0), throw: verb(1), return: verb(2) }), - typeof Symbol === 'function' && - (g[Symbol.iterator] = function () { - return this; - }), - g - ); - function verb(n) { - return function (v) { - return step([n, v]); - }; - } - function step(op) { - if (f) throw new TypeError('Generator is already executing.'); - while (_) - try { - if ( - ((f = 1), - y && - (t = - op[0] & 2 - ? y['return'] - : op[0] - ? y['throw'] || ((t = y['return']) && t.call(y), 0) - : y.next) && - !(t = t.call(y, op[1])).done) - ) - return t; - if (((y = 0), t)) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: - case 1: - t = op; - break; - case 4: - _.label++; - return { value: op[1], done: false }; - case 5: - _.label++; - y = op[1]; - op = [0]; - continue; - case 7: - op = _.ops.pop(); - _.trys.pop(); - continue; - default: - if ( - !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && - (op[0] === 6 || op[0] === 2) - ) { - _ = 0; - continue; - } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { - _.label = op[1]; - break; - } - if (op[0] === 6 && _.label < t[1]) { - _.label = t[1]; - t = op; - break; - } - if (t && _.label < t[2]) { - _.label = t[2]; - _.ops.push(op); - break; - } - if (t[2]) _.ops.pop(); - _.trys.pop(); - continue; - } - op = body.call(thisArg, _); - } catch (e) { - op = [6, e]; - y = 0; - } finally { - f = t = 0; - } - if (op[0] & 5) throw op[1]; - return { value: op[0] ? op[1] : void 0, done: true }; - } - }; -exports.__esModule = true; -var yargs = require('yargs'); -var rollup_1 = require('rollup'); -var typescriptPlugin = require('rollup-plugin-typescript2'); -var alias = require('@rollup/plugin-alias'); -var json = require('rollup-plugin-json'); -var util = require('../rollup.shared'); -var argv = yargs.options({ - input: { - type: 'string', - demandOption: true, - desc: 'The location of the index.ts file' - }, - output: { - type: 'string', - demandOption: true, - desc: 'The location for the transpiled JavaScript bundle' - } -}).argv; -function buildBundle(input, output) { - return __awaiter(this, void 0, void 0, function () { - var bundle; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - return [ - 4, - rollup_1.rollup({ - input: input, - plugins: [ - alias(util.generateAliasConfig('node')), - typescriptPlugin({ - tsconfigOverride: { compilerOptions: { target: 'es2017' } }, - transformers: [util.removeAssertTransformer] - }), - json({ preferConst: true }) - ], - external: util.resolveNodeExterns - }) - ]; - case 1: - bundle = _a.sent(); - return [4, bundle.write({ file: output, format: 'es' })]; - case 2: - _a.sent(); - return [2]; - } - }); - }); -} -buildBundle(argv.input, argv.output); + */var __awaiter=this&&this.__awaiter||function(thisArg,_arguments,P,generator){function adopt(value){return value instanceof P?value:new P((function(resolve){resolve(value)}))}return new(P||(P=Promise))((function(resolve,reject){function fulfilled(value){try{step(generator.next(value))}catch(e){reject(e)}}function rejected(value){try{step(generator["throw"](value))}catch(e){reject(e)}}function step(result){result.done?resolve(result.value):adopt(result.value).then(fulfilled,rejected)}step((generator=generator.apply(thisArg,_arguments||[])).next())}))};var __generator=this&&this.__generator||function(thisArg,body){var _={label:0,sent:function(){if(t[0]&1)throw t[1];return t[1]},trys:[],ops:[]},f,y,t,g;return g={next:verb(0),throw:verb(1),return:verb(2)},typeof Symbol==="function"&&(g[Symbol.iterator]=function(){return this}),g;function verb(n){return function(v){return step([n,v])}}function step(op){if(f)throw new TypeError("Generator is already executing.");while(_)try{if(f=1,y&&(t=op[0]&2?y["return"]:op[0]?y["throw"]||((t=y["return"])&&t.call(y),0):y.next)&&!(t=t.call(y,op[1])).done)return t;if(y=0,t)op=[op[0]&2,t.value];switch(op[0]){case 0:case 1:t=op;break;case 4:_.label++;return{value:op[1],done:false};case 5:_.label++;y=op[1];op=[0];continue;case 7:op=_.ops.pop();_.trys.pop();continue;default:if(!(t=_.trys,t=t.length>0&&t[t.length-1])&&(op[0]===6||op[0]===2)){_=0;continue}if(op[0]===3&&(!t||op[1]>t[0]&&op[1]= 0 - ) { - var method = declaration.name.text; - if (method === 'debugAssert') { - updatedNode = ts.createEmptyStatement(); - } else if (method === 'hardAssert') { - updatedNode = ts.createCall(declaration.name, undefined, [ - node.arguments[0] - ]); - } else if (method === 'fail') { - updatedNode = ts.createCall(declaration.name, undefined, []); - } - } - } - } - if (updatedNode) { - ts.setSourceMapRange(updatedNode, ts.getSourceMapRange(node)); - return updatedNode; - } else { - return node; - } - }; - return RemoveAsserts; -})(); + */exports.__esModule=true;exports.removeAsserts=void 0;var ts=require("typescript");var ASSERT_LOCATION="packages/firestore/src/util/assert.ts";function removeAsserts(program){var removeAsserts=new RemoveAsserts(program.getTypeChecker());return function(context){return function(file){return removeAsserts.visitNodeAndChildren(file,context)}}}exports.removeAsserts=removeAsserts;var RemoveAsserts=function(){function RemoveAsserts(typeChecker){this.typeChecker=typeChecker}RemoveAsserts.prototype.visitNodeAndChildren=function(node,context){var _this=this;return ts.visitEachChild(this.visitNode(node),(function(childNode){return _this.visitNodeAndChildren(childNode,context)}),context)};RemoveAsserts.prototype.visitNode=function(node){var updatedNode=null;if(ts.isCallExpression(node)){var signature=this.typeChecker.getResolvedSignature(node);if(signature&&signature.declaration&&signature.declaration.kind===ts.SyntaxKind.FunctionDeclaration){var declaration=signature.declaration;if(declaration&&declaration.getSourceFile().fileName.indexOf(ASSERT_LOCATION)>=0){var method=declaration.name.text;if(method==="debugAssert"){updatedNode=ts.createEmptyStatement()}else if(method==="hardAssert"){updatedNode=ts.createCall(declaration.name,undefined,[node.arguments[0]])}else if(method==="fail"){updatedNode=ts.createCall(declaration.name,undefined,[])}}}}if(updatedNode){ts.setSourceMapRange(updatedNode,ts.getSourceMapRange(node));return updatedNode}else{return node}};return RemoveAsserts}(); \ No newline at end of file diff --git a/packages/firestore/scripts/rename-internals.js b/packages/firestore/scripts/rename-internals.js index 2f6ec10ab18..394009758ef 100644 --- a/packages/firestore/scripts/rename-internals.js +++ b/packages/firestore/scripts/rename-internals.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; /** * @license * Copyright 2020 Google LLC @@ -14,51 +14,4 @@ * 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. - */ exports.__esModule = true; -exports.renameInternals = void 0; -var ts = require('typescript'); -var blacklist = ['undefined']; -var RenameInternals = (function () { - function RenameInternals(publicApi, prefix) { - this.publicApi = publicApi; - this.prefix = prefix; - } - RenameInternals.prototype.visitNodeAndChildren = function (node, context) { - var _this = this; - return ts.visitEachChild( - this.visitNode(node), - function (childNode) { - return _this.visitNodeAndChildren(childNode, context); - }, - context - ); - }; - RenameInternals.prototype.visitNode = function (node) { - if (ts.isIdentifier(node)) { - var name_1 = node.escapedText.toString(); - if ( - !this.publicApi.has(name_1) && - blacklist.indexOf(node.escapedText.toString()) === -1 - ) { - var newIdentifier = ts.createIdentifier(this.prefix + name_1); - ts.setSourceMapRange(newIdentifier, ts.getSourceMapRange(node)); - return newIdentifier; - } - } - return node; - }; - return RenameInternals; -})(); -var DEFAULT_PREFIX = '_'; -function renameInternals(program, config) { - var _a; - var prefix = - (_a = config.prefix) !== null && _a !== void 0 ? _a : DEFAULT_PREFIX; - var renamer = new RenameInternals(config.publicIdentifiers, prefix); - return function (context) { - return function (file) { - return renamer.visitNodeAndChildren(file, context); - }; - }; -} -exports.renameInternals = renameInternals; + */exports.__esModule=true;exports.renameInternals=void 0;var ts=require("typescript");var blacklist=["undefined"];var RenameInternals=function(){function RenameInternals(publicApi,prefix){this.publicApi=publicApi;this.prefix=prefix}RenameInternals.prototype.visitNodeAndChildren=function(node,context){var _this=this;return ts.visitEachChild(this.visitNode(node),(function(childNode){return _this.visitNodeAndChildren(childNode,context)}),context)};RenameInternals.prototype.visitNode=function(node){if(ts.isIdentifier(node)){var name_1=node.escapedText.toString();if(!this.publicApi.has(name_1)&&blacklist.indexOf(node.escapedText.toString())===-1){var newIdentifier=ts.createIdentifier(this.prefix+name_1);ts.setSourceMapRange(newIdentifier,ts.getSourceMapRange(node));return newIdentifier}}return node};return RenameInternals}();var DEFAULT_PREFIX="_";function renameInternals(program,config){var _a;var prefix=(_a=config.prefix)!==null&&_a!==void 0?_a:DEFAULT_PREFIX;var renamer=new RenameInternals(config.publicIdentifiers,prefix);return function(context){return function(file){return renamer.visitNodeAndChildren(file,context)}}}exports.renameInternals=renameInternals; \ No newline at end of file diff --git a/packages/firestore/scripts/run-tests.js b/packages/firestore/scripts/run-tests.js index 92e76bf56f4..5daead8be3b 100644 --- a/packages/firestore/scripts/run-tests.js +++ b/packages/firestore/scripts/run-tests.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; /** * @license * Copyright 2020 Google LLC @@ -14,51 +14,4 @@ * 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. - */ exports.__esModule = true; -var yargs = require('yargs'); -var path_1 = require('path'); -var child_process_promise_1 = require('child-process-promise'); -var argv = yargs.options({ - main: { type: 'string', demandOption: true }, - platform: { type: 'string', default: 'node' }, - emulator: { type: 'boolean' }, - persistence: { type: 'boolean' } -}).argv; -var nyc = path_1.resolve(__dirname, '../../../node_modules/.bin/nyc'); -var mocha = path_1.resolve(__dirname, '../../../node_modules/.bin/mocha'); -process.env.TS_NODE_CACHE = 'NO'; -process.env.TS_NODE_COMPILER_OPTIONS = '{"module":"commonjs"}'; -process.env.TEST_PLATFORM = argv.platform; -var args = [ - '--reporter', - 'lcovonly', - mocha, - '--require', - 'ts-node/register', - '--require', - argv.main, - '--config', - '../../config/mocharc.node.js' -]; -if (argv.emulator) { - process.env.FIRESTORE_EMULATOR_PORT = '8080'; - process.env.FIRESTORE_EMULATOR_PROJECT_ID = 'test-emulator'; -} -if (argv.persistence) { - process.env.USE_MOCK_PERSISTENCE = 'YES'; - args.push('--require', 'test/util/node_persistence.ts'); -} -args = args.concat(argv._); -var childProcess = child_process_promise_1.spawn(nyc, args, { - stdio: 'inherit', - cwd: process.cwd() -}).childProcess; -process.once('exit', function () { - return childProcess.kill(); -}); -process.once('SIGINT', function () { - return childProcess.kill('SIGINT'); -}); -process.once('SIGTERM', function () { - return childProcess.kill('SIGTERM'); -}); + */exports.__esModule=true;var yargs=require("yargs");var path_1=require("path");var child_process_promise_1=require("child-process-promise");var argv=yargs.options({main:{type:"string",demandOption:true},platform:{type:"string",default:"node"},emulator:{type:"boolean"},persistence:{type:"boolean"}}).argv;var nyc=path_1.resolve(__dirname,"../../../node_modules/.bin/nyc");var mocha=path_1.resolve(__dirname,"../../../node_modules/.bin/mocha");process.env.TS_NODE_CACHE="NO";process.env.TS_NODE_COMPILER_OPTIONS='{"module":"commonjs"}';process.env.TEST_PLATFORM=argv.platform;var args=["--reporter","lcovonly",mocha,"--require","ts-node/register","--require",argv.main,"--config","../../config/mocharc.node.js"];if(argv.emulator){process.env.FIRESTORE_EMULATOR_PORT="8080";process.env.FIRESTORE_EMULATOR_PROJECT_ID="test-emulator"}if(argv.persistence){process.env.USE_MOCK_PERSISTENCE="YES";args.push("--require","test/util/node_persistence.ts")}args=args.concat(argv._);var childProcess=child_process_promise_1.spawn(nyc,args,{stdio:"inherit",cwd:process.cwd()}).childProcess;process.once("exit",(function(){return childProcess.kill()}));process.once("SIGINT",(function(){return childProcess.kill("SIGINT")}));process.once("SIGTERM",(function(){return childProcess.kill("SIGTERM")})); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 52fd53eb77b..bc87be7782a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -904,16 +904,17 @@ tty-table "^2.7.0" "@changesets/config@^1.2.0", "@changesets/config@^1.3.0": - version "1.3.0" - resolved "https://registry.npmjs.org/@changesets/config/-/config-1.3.0.tgz#82fcbf572b00ba16636be9ea45167983f1fc203b" - integrity sha512-IeAHmN5kI7OywBUNJXsk/v4vcXDDscwgTe/K5D3FSng5QTvzbgiMAe5K1iwBxBvuT4u/33n89kxSJdg4TTTFfA== + version "1.4.0" + resolved "https://registry.npmjs.org/@changesets/config/-/config-1.4.0.tgz#c157a4121f198b749f2bbc2e9015b6e976ece7d6" + integrity sha512-eoTOcJ6py7jBDY8rUXwEGxR5UtvUX+p//0NhkVpPGcnvIeITHq+DOIsuWyGzWcb+1FaYkof3CCr32/komZTu4Q== dependencies: "@changesets/errors" "^0.1.4" "@changesets/get-dependents-graph" "^1.1.3" "@changesets/logger" "^0.0.5" - "@changesets/types" "^3.1.0" + "@changesets/types" "^3.2.0" "@manypkg/get-packages" "^1.0.1" fs-extra "^7.0.1" + micromatch "^4.0.2" "@changesets/errors@^0.1.4": version "0.1.4" @@ -979,9 +980,9 @@ chalk "^2.1.0" "@changesets/parse@^0.3.6": - version "0.3.6" - resolved "https://registry.npmjs.org/@changesets/parse/-/parse-0.3.6.tgz#8c2c8480fc07d2db2c37469d4a8df10906a989c6" - integrity sha512-0XPd/es9CfogI7XIqDr7I2mWzm++xX2s9GZsij3GajPYd7ouEsgJyNatPooxNtqj6ZepkiD6uqlqbeBUyj/A0Q== + version "0.3.7" + resolved "https://registry.npmjs.org/@changesets/parse/-/parse-0.3.7.tgz#1368136e2b83d5cff11b4d383a3032723530db99" + integrity sha512-8yqKulslq/7V2VRBsJqPgjnZMoehYqhJm5lEOXJPZ2rcuSdyj8+p/2vq2vRDBJT2m0rP+C9G8DujsGYQIFZezw== dependencies: "@changesets/types" "^3.0.0" js-yaml "^3.13.1" @@ -1011,11 +1012,16 @@ fs-extra "^7.0.1" p-filter "^2.1.0" -"@changesets/types@3.1.1", "@changesets/types@^3.0.0", "@changesets/types@^3.1.0", "@changesets/types@^3.1.1": +"@changesets/types@3.1.1": version "3.1.1" resolved "https://registry.npmjs.org/@changesets/types/-/types-3.1.1.tgz#447481380c42044a8788e46c0dbdf592b338b62f" integrity sha512-XWGEGWXhM92zvBWiQt2sOwhjTt8eCQbrsRbqkv4WYwW3Zsl4qPpvhHsNt845S42dJXrxgjWvId+jxFQocCayNQ== +"@changesets/types@^3.0.0", "@changesets/types@^3.1.0", "@changesets/types@^3.1.1", "@changesets/types@^3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@changesets/types/-/types-3.2.0.tgz#d8306d7219c3b19b6d860ddeb9d7374e2dd6b035" + integrity sha512-rAmPtOyXpisEEE25CchKNUAf2ApyAeuZ/h78YDoqKZaCk5tUD0lgYZGPIRV9WTPoqNjJULIym37ogc6pkax5jg== + "@changesets/write@^0.1.3": version "0.1.3" resolved "https://registry.npmjs.org/@changesets/write/-/write-0.1.3.tgz#00ae575af50274773d7493e77fb96838a08ad8ad" @@ -1127,9 +1133,9 @@ which "^1.3.1" "@google-cloud/common@^3.3.0": - version "3.3.3" - resolved "https://registry.npmjs.org/@google-cloud/common/-/common-3.3.3.tgz#9ef57af26cc8fa2c5fe78a8be8dfd1c95b3bf494" - integrity sha512-2PwPDE47N4WiWQK/F35vE5aWVoCjKQ2NW8r8OFAg6QslkLMjX6WNcmUO8suYlSkavc58qOvzA4jG6eVkC90i8Q== + version "3.4.1" + resolved "https://registry.npmjs.org/@google-cloud/common/-/common-3.4.1.tgz#a1920d73c38437923b4b134e245c392d36c442e9" + integrity sha512-e5z0CwsM0RXky+PnyPtQ3QK46ksqm+kE7kX8pm8X+ddBwZJipHchKeazMM5fLlGCS+AALalzXb+uYmH72TRnpQ== dependencies: "@google-cloud/projectify" "^2.0.0" "@google-cloud/promisify" "^2.0.0" @@ -1137,11 +1143,11 @@ duplexify "^4.1.1" ent "^2.2.0" extend "^3.0.2" - google-auth-library "^6.0.0" + google-auth-library "^6.1.1" retry-request "^4.1.1" teeny-request "^7.0.0" -"@google-cloud/firestore@4.2.0", "@google-cloud/firestore@^4.0.0": +"@google-cloud/firestore@4.2.0": version "4.2.0" resolved "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.2.0.tgz#5ff83838076972b86c16ae64d35429c190c69ea9" integrity sha512-YCiKaTYCbXSoEvZ8cTmpgg4ebAvmFUOu3hj/aX+lHiOK7LsoFVi4jgNknogSqIiv04bxAysTBodpgn8XoZ4l5g== @@ -1150,6 +1156,15 @@ functional-red-black-tree "^1.0.1" google-gax "^2.2.0" +"@google-cloud/firestore@^4.0.0": + version "4.4.0" + resolved "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.4.0.tgz#6cdbd462f32a8f94e138c57ef81195156c79e680" + integrity sha512-nixsumd4C7eL+hHEgyihspzhBBNe3agsvNFRX0xfqO3uR/6ro4CUj9XdcCvdnSSd3yTyqKfdBSRK2fEj1jIbYg== + dependencies: + fast-deep-equal "^3.1.1" + functional-red-black-tree "^1.0.1" + google-gax "^2.2.0" + "@google-cloud/paginator@^2.0.0": version "2.0.3" resolved "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.3.tgz#c7987ad05d1c3ebcef554381be80e9e8da4e4882" @@ -1246,9 +1261,9 @@ semver "^6.2.0" "@grpc/grpc-js@^1.0.0", "@grpc/grpc-js@~1.1.1": - version "1.1.5" - resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.1.5.tgz#2d0b261cd54a529f6b78ac0de9d6fd91a9a3129c" - integrity sha512-2huf5z85TdZI4nLmJQ9Zdfd+6vmIyBDs7B4L71bTaHKA9pRsGKAH24XaktMk/xneKJIqAgeIZtg1cyivVZtvrg== + version "1.1.7" + resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.1.7.tgz#d3d71c6da95397e2d63895ccc4a05e7572f7b7e6" + integrity sha512-EuxMstI0u778dp0nk6Fe3gHXYPeV6FYsWOe0/QFwxv1NQ6bc5Wl/0Yxa4xl9uBlKElL6AIxuASmSfu7KEJhqiw== dependencies: "@grpc/proto-loader" "^0.6.0-pre14" "@types/node" "^12.12.47" @@ -2118,12 +2133,12 @@ "@octokit/types" "^5.0.0" "@octokit/endpoint@^6.0.1": - version "6.0.5" - resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.5.tgz#43a6adee813c5ffd2f719e20cfd14a1fee7c193a" - integrity sha512-70K5u6zd45ItOny6aHQAsea8HHQjlQq85yqOMe+Aj8dkhN2qSJ9T+Q3YjUjEYfPRBcuUWNgMn62DQnP/4LAIiQ== + version "6.0.8" + resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.8.tgz#91b07e236fdb69929c678c6439f7a560dc6058ac" + integrity sha512-MuRrgv+bM4Q+e9uEvxAB/Kf+Sj0O2JAOBA131uo1o6lgdq1iS8ejKwtqHgdfY91V3rN9R/hdGKFiQYMzVzVBEQ== dependencies: "@octokit/types" "^5.0.0" - is-plain-object "^4.0.0" + is-plain-object "^5.0.0" universal-user-agent "^6.0.0" "@octokit/plugin-enterprise-rest@^6.0.1": @@ -2170,16 +2185,16 @@ once "^1.4.0" "@octokit/request@^5.2.0": - version "5.4.7" - resolved "https://registry.npmjs.org/@octokit/request/-/request-5.4.7.tgz#fd703ee092e0463ceba49ff7a3e61cb4cf8a0fde" - integrity sha512-FN22xUDP0i0uF38YMbOfx6TotpcENP5W8yJM1e/LieGXn6IoRxDMnBf7tx5RKSW4xuUZ/1P04NFZy5iY3Rax1A== + version "5.4.9" + resolved "https://registry.npmjs.org/@octokit/request/-/request-5.4.9.tgz#0a46f11b82351b3416d3157261ad9b1558c43365" + integrity sha512-CzwVvRyimIM1h2n9pLVYfTDmX9m+KHSgCpqPsY8F1NdEK8IaWqXhSBXsdjOBFZSpEcxNEeg4p0UO9cQ8EnOCLA== dependencies: "@octokit/endpoint" "^6.0.1" "@octokit/request-error" "^2.0.0" "@octokit/types" "^5.0.0" deprecation "^2.0.0" - is-plain-object "^4.0.0" - node-fetch "^2.3.0" + is-plain-object "^5.0.0" + node-fetch "^2.6.1" once "^1.4.0" universal-user-agent "^6.0.0" @@ -2213,9 +2228,9 @@ "@types/node" ">= 8" "@octokit/types@^5.0.0", "@octokit/types@^5.0.1": - version "5.4.1" - resolved "https://registry.npmjs.org/@octokit/types/-/types-5.4.1.tgz#d5d5f2b70ffc0e3f89467c3db749fa87fc3b7031" - integrity sha512-OlMlSySBJoJ6uozkr/i03nO5dlYQyE05vmQNZhAh9MyO4DPBP88QlwsDVLmVjIMFssvIZB6WO0ctIGMRG+xsJQ== + version "5.5.0" + resolved "https://registry.npmjs.org/@octokit/types/-/types-5.5.0.tgz#e5f06e8db21246ca102aa28444cdb13ae17a139b" + integrity sha512-UZ1pErDue6bZNjYOotCNveTXArOMZQFG6hKJfOnGnulVCMcVVi7YIIuuR4WfBhjo7zgpmzn/BkPDnUXtNx+PcQ== dependencies: "@types/node" ">= 8" @@ -2348,9 +2363,9 @@ "@sinonjs/samsam" "^5.0.2" "@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.1.0": - version "5.1.0" - resolved "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.1.0.tgz#3afe719232b541bb6cf3411a4c399a188de21ec0" - integrity sha512-42nyaQOVunX5Pm6GRJobmzbS7iLI+fhERITnETXzzwDZh+TtDr/Au3yAvXVjFmZ4wEUaE4Y3NFZfKv0bV0cbtg== + version "5.2.0" + resolved "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.2.0.tgz#fcff83ab86f83b5498f4a967869c079408d9b5eb" + integrity sha512-CaIcyX5cDsjcW/ab7HposFWzV1kC++4HNsfnEdFJa7cP1QIuILAKV+BgfeqRXhcnSAc76r/Rh/O5C+300BwUIw== dependencies: "@sinonjs/commons" "^1.6.0" lodash.get "^4.4.2" @@ -2403,7 +2418,12 @@ dependencies: "@types/chai" "*" -"@types/chai@*", "@types/chai@4.2.12": +"@types/chai@*": + version "4.2.13" + resolved "https://registry.npmjs.org/@types/chai/-/chai-4.2.13.tgz#8a3801f6655179d1803d81e94a2e4aaf317abd16" + integrity sha512-o3SGYRlOpvLFpwJA6Sl1UPOwKFEvE4FxTEB/c9XHI2whdnd4kmPVkNLL8gY4vWGBxWWDumzLbKsAhEH5SKn37Q== + +"@types/chai@4.2.12": version "4.2.12" resolved "https://registry.npmjs.org/@types/chai/-/chai-4.2.12.tgz#6160ae454cd89dae05adc3bb97997f488b608201" integrity sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ== @@ -2420,11 +2440,6 @@ resolved "https://registry.npmjs.org/@types/clone/-/clone-2.1.0.tgz#cb888a3fe5319275b566ae3a9bc606e310c533d4" integrity sha512-d/aS/lPOnUSruPhgNtT8jW39fHRVTLQy9sodysP1kkG8EdAtdZu1vt8NJaYA8w/6Z9j8izkAsx1A/yJhcYR1CA== -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== - "@types/connect@*": version "3.4.33" resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" @@ -2450,9 +2465,9 @@ integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== "@types/express-serve-static-core@*": - version "4.17.12" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.12.tgz#9a487da757425e4f267e7d1c5720226af7f89591" - integrity sha512-EaEdY+Dty1jEU7U6J4CUWwxL+hyEGMkO5jan5gplfegUgCUsIUWqXxqw47uGjimeT4Qgkz/XUfwoau08+fgvKA== + version "4.17.13" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" + integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA== dependencies: "@types/node" "*" "@types/qs" "*" @@ -2554,9 +2569,9 @@ form-data "^3.0.0" "@types/node@*", "@types/node@>= 8": - version "14.6.4" - resolved "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz#a145cc0bb14ef9c4777361b7bbafa5cf8e3acb5a" - integrity sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ== + version "14.11.5" + resolved "https://registry.npmjs.org/@types/node/-/node-14.11.5.tgz#fecad41c041cae7f2404ad4b2d0742fdb628b305" + integrity sha512-jVFzDV6NTbrLMxm4xDSIW/gKnk8rQLF9wAzLWIOg+5nU6ACrIMndeBdXci0FGtqJbP9tQvm6V39eshc96TO2wQ== "@types/node@10.17.13": version "10.17.13" @@ -2569,19 +2584,19 @@ integrity sha512-qAfo81CsD7yQIM9mVyh6B/U47li5g7cfpVQEDMfQeF8pSZVwzbhwU3crc0qG4DmpsebpJPR49AKOExQyJ05Cpg== "@types/node@^10.10.0": - version "10.17.30" - resolved "https://registry.npmjs.org/@types/node/-/node-10.17.30.tgz#20556a0d7f62b83e163973a6cd640af636d3dd3b" - integrity sha512-euU8QLX0ipj+5mOYa4ZqZoTv+53BY7yTg9I2ZIhDXgiI3M+0n4mdAt9TQCuvxVAgU179g8OsRLaBt0qEi0T6xA== + version "10.17.37" + resolved "https://registry.npmjs.org/@types/node/-/node-10.17.37.tgz#40d03db879993799c3819e298b003f055e8ecafe" + integrity sha512-4c38N7p9k9yqdcANh/WExTahkBgOTmggCyrTvVcbE8ByqO3g8evt/407v/I4X/gdfUkIyZBSQh/Rc3tvuwlVGw== "@types/node@^12.12.47", "@types/node@^12.7.1": - version "12.12.56" - resolved "https://registry.npmjs.org/@types/node/-/node-12.12.56.tgz#83591a89723d8ec3eaf722137e1784a7351edb6c" - integrity sha512-8OdIupOIZtmObR13fvGyTvpcuzKmMugkATeVcfNwCjGtHxhjEKmOvLqXwR8U9VOtNnZ4EXaSfNiLVsPinaCXkQ== + version "12.12.64" + resolved "https://registry.npmjs.org/@types/node/-/node-12.12.64.tgz#e3b336dc4c6ba52c6b59e3bd69a100347c20b1c0" + integrity sha512-UV1/ZJMC+HcP902wWdpC43cAcGu0IQk/I5bXjP2aSuCjsk3cE74mDvFrLKga7oDC170ugOAYBwfT4DSQW3akDA== "@types/node@^13.7.0": - version "13.13.17" - resolved "https://registry.npmjs.org/@types/node/-/node-13.13.17.tgz#fba8bdd9be9a61adbceac654450673c2f520b0f0" - integrity sha512-rGZftvdDpsYtG/rOlDOwny1f6Aq4FHJdGSVfPg5vC2DaR9Rt4W2OpsOF5GTU2bSqZmwTkfnsvJhhzpMWYxxlEA== + version "13.13.23" + resolved "https://registry.npmjs.org/@types/node/-/node-13.13.23.tgz#abd99b4b806144b257ae298b3aec5a2c10c74534" + integrity sha512-L31WmMJYKb15PDqFWutn8HNwrNK6CE6bkWgSB0dO1XpNoHrszVKV1Clcnfgd6c/oG54TVF8XQEvY2gQrW8K6Mw== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -2599,9 +2614,9 @@ integrity sha1-vShOV8hPEyXacCur/IKlMoGQwMU= "@types/qs@*": - version "6.9.4" - resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a" - integrity sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ== + version "6.9.5" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" + integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== "@types/range-parser@*": version "1.2.3" @@ -2631,9 +2646,9 @@ integrity sha512-tGomyEuzSC1H28y2zlW6XPCaDaXFaD6soTdb4GNdmte2qfHtrKqhy0ZFs4r/1hpazCfEZqeTSRLvSasmEx89uw== "@types/semver@^6.0.0": - version "6.2.1" - resolved "https://registry.npmjs.org/@types/semver/-/semver-6.2.1.tgz#a236185670a7860f1597cf73bea2e16d001461ba" - integrity sha512-+beqKQOh9PYxuHvijhVl+tIHvT6tuwOrE9m14zd+MT2A38KoKZhh7pYJ0SNleLtwDsiIxHDsIk9bv01oOxvSvA== + version "6.2.2" + resolved "https://registry.npmjs.org/@types/semver/-/semver-6.2.2.tgz#5c27df09ca39e3c9beb4fae6b95f4d71426df0a9" + integrity sha512-RxAwYt4rGwK5GyoRwuP0jT6ZHAVTdz2EqgsHmX0PYNjGsko+OeT4WFXXTs/lM3teJUJodM+SNtAL5/pXIJ61IQ== "@types/serve-static@*": version "1.13.5" @@ -2651,7 +2666,14 @@ "@types/chai" "*" "@types/sinon" "*" -"@types/sinon@*", "@types/sinon@9.0.5": +"@types/sinon@*": + version "9.0.8" + resolved "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.8.tgz#1ed0038d356784f75b086104ef83bfd4130bb81b" + integrity sha512-IVnI820FZFMGI+u1R+2VdRaD/82YIQTdqLYC9DLPszZuynAJDtCvCtCs3bmyL66s7FqRM3+LPX7DhHnVTaagDw== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinon@9.0.5": version "9.0.5" resolved "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.5.tgz#56b2a12662dd8c7d081cdc511af5f872cb37377f" integrity sha512-4CnkGdM/5/FXDGqL32JQ1ttVrGvhOoesLLF7VnTh4KdjK5N5VQOtxaylFqqTjnHx55MnD9O02Nbk5c1ELC8wlQ== @@ -2659,9 +2681,9 @@ "@types/sinonjs__fake-timers" "*" "@types/sinonjs__fake-timers@*": - version "6.0.1" - resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" - integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== + version "6.0.2" + resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz#3a84cf5ec3249439015e14049bd3161419bf9eae" + integrity sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg== "@types/through@*": version "0.0.30" @@ -2990,19 +3012,19 @@ acorn@5.X, acorn@^5.0.3: integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== acorn@^6.4.1: - version "6.4.1" - resolved "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" - integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== + version "6.4.2" + resolved "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== acorn@^7.4.0: - version "7.4.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" - integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== + version "7.4.1" + resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^8.0.1: - version "8.0.1" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.0.1.tgz#d7e8eca9b71d5840db0e7e415b3b2b20e250f938" - integrity sha512-dmKn4pqZ29iQl2Pvze1zTrps2luvls2PBY//neO2WJ0s10B3AxJXshN+Ph7B4GrhfGhHXrl4dnUwyNNXQcnWGQ== + version "8.0.4" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.0.4.tgz#7a3ae4191466a6984eee0fe3407a4f3aa9db8354" + integrity sha512-XNP0PqF1XD19ZlLKvB7cMmnZswW4C/03pRHgirB30uSJTaS3A3V1/P4sS3HPvFmjoriPCJQs+JDSbm4bL1TxGQ== adm-zip@0.4.16, adm-zip@^0.4.9, adm-zip@~0.4.3: version "0.4.16" @@ -3071,9 +3093,9 @@ ajv@^5.0.0: json-schema-traverse "^0.3.0" ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: - version "6.12.4" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" - integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== + version "6.12.5" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" + integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -3168,11 +3190,10 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: color-convert "^1.9.0" ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: - "@types/color-name" "^1.1.1" color-convert "^2.0.1" ansi-wrap@0.1.0, ansi-wrap@^0.1.0: @@ -3744,6 +3765,11 @@ base64-arraybuffer-es6@^0.6.0: resolved "https://registry.npmjs.org/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.6.0.tgz#036f79f57588dca0018de7792ddf149299382007" integrity sha512-57nLqKj4ShsDwFJWJsM4sZx6u60WbCge35rWRSevUwqxDtRwwxiKAO800zD2upPv4CfdWjQp//wSLar35nDKvA== +base64-arraybuffer@0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812" + integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI= + base64-arraybuffer@0.1.5: version "0.1.5" resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" @@ -3826,9 +3852,9 @@ big.js@^5.2.2: integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== bignumber.js@^9.0.0: - version "9.0.0" - resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" - integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A== + version "9.0.1" + resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" + integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== binary-extensions@^1.0.0: version "1.13.1" @@ -3860,7 +3886,7 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bl@^4.0.1: +bl@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== @@ -4066,14 +4092,14 @@ browserify-zlib@^0.2.0: pako "~1.0.5" browserslist@^4.12.0, browserslist@^4.8.5: - version "4.14.1" - resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.14.1.tgz#cb2b490ba881d45dc3039078c7ed04411eaf3fa3" - integrity sha512-zyBTIHydW37pnb63c7fHFXUG6EcqWOqoMdDx6cdyaDFriZ20EoVxcE95S54N+heRqY8m8IUgB5zYta/gCwSaaA== + version "4.14.5" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015" + integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA== dependencies: - caniuse-lite "^1.0.30001124" - electron-to-chromium "^1.3.562" - escalade "^3.0.2" - node-releases "^1.1.60" + caniuse-lite "^1.0.30001135" + electron-to-chromium "^1.3.571" + escalade "^3.1.0" + node-releases "^1.1.61" browserstack@^1.5.1: version "1.6.0" @@ -4108,9 +4134,9 @@ buffer-from@^1.0.0: integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== buffer-indexof-polyfill@~1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz#a9fb806ce8145d5428510ce72f278bb363a638bf" - integrity sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8= + version "1.0.2" + resolved "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c" + integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A== buffer-xor@^1.0.3: version "1.0.3" @@ -4318,10 +4344,10 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001124: - version "1.0.30001124" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001124.tgz#5d9998190258e11630d674fc50ea8e579ae0ced2" - integrity sha512-zQW8V3CdND7GHRH6rxm6s59Ww4g/qGWTheoboW9nfeMg7sUoopIfKCcNZUjwYRCOrvereh3kwDpZj4VLQ7zGtA== +caniuse-lite@^1.0.30001135: + version "1.0.30001146" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001146.tgz#c61fcb1474520c1462913689201fb292ba6f447c" + integrity sha512-VAy5RHDfTJhpxnDdp2n40GPPLp3KqNrXz1QqFv4J64HvArKs8nuNMOWkB3ICOaBTU/Aj4rYAo/ytdQDDFF/Pug== capture-stack-trace@^1.0.0: version "1.0.1" @@ -5462,12 +5488,12 @@ debug@3.2.6, debug@3.X, debug@^3.1.0, debug@^3.1.1, debug@^3.2.6: dependencies: ms "^2.1.1" -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@~4.1.0: - version "4.1.1" - resolved "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== +debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: + version "4.2.0" + resolved "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" + integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== dependencies: - ms "^2.1.1" + ms "2.1.2" debug@4.1.0: version "4.1.0" @@ -5476,6 +5502,13 @@ debug@4.1.0: dependencies: ms "^2.1.1" +debug@~4.1.0: + version "4.1.1" + resolved "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + debuglog@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -5841,7 +5874,7 @@ duplexify@^3.4.2, duplexify@^3.5.0, duplexify@^3.6.0: readable-stream "^2.0.0" stream-shift "^1.0.0" -duplexify@^4.1.1: +duplexify@^4.0.0, duplexify@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz#7027dc374f157b122a8ae08c2d3ea4d2d953aa61" integrity sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA== @@ -5894,10 +5927,10 @@ ee-first@1.1.1: resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.562: - version "1.3.564" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.564.tgz#e9c319ae437b3eb8bbf3e3bae4bead5a21945961" - integrity sha512-fNaYN3EtKQWLQsrKXui8mzcryJXuA0LbCLoizeX6oayG2emBaS5MauKjCPAvc29NEY4FpLHIUWiP+Y0Bfrs5dg== +electron-to-chromium@^1.3.571: + version "1.3.578" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.578.tgz#e6671936f4571a874eb26e2e833aa0b2c0b776e0" + integrity sha512-z4gU6dA1CbBJsAErW5swTGAaU2TBzc2mPAonJb00zqW1rOraDo2zfBMDRvaz9cVic+0JEZiYbHWPw/fTaZlG2Q== elegant-spinner@^1.0.1: version "1.0.1" @@ -5957,30 +5990,30 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: once "^1.4.0" engine.io-client@~3.4.0: - version "3.4.3" - resolved "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.3.tgz#192d09865403e3097e3575ebfeb3861c4d01a66c" - integrity sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw== + version "3.4.4" + resolved "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.4.tgz#77d8003f502b0782dd792b073a4d2cf7ca5ab967" + integrity sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ== dependencies: component-emitter "~1.3.0" component-inherit "0.0.3" - debug "~4.1.0" + debug "~3.1.0" engine.io-parser "~2.2.0" has-cors "1.1.0" indexof "0.0.1" - parseqs "0.0.5" - parseuri "0.0.5" + parseqs "0.0.6" + parseuri "0.0.6" ws "~6.1.0" xmlhttprequest-ssl "~1.5.4" yeast "0.1.2" engine.io-parser@~2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed" - integrity sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w== + version "2.2.1" + resolved "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz#57ce5611d9370ee94f99641b589f94c97e4f5da7" + integrity sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg== dependencies: after "0.8.2" arraybuffer.slice "~0.0.7" - base64-arraybuffer "0.1.5" + base64-arraybuffer "0.1.4" blob "0.0.5" has-binary2 "~1.0.2" @@ -6047,19 +6080,37 @@ error-ex@^1.2.0, error-ex@^1.3.1: is-arrayish "^0.2.1" es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.4, es-abstract@^1.17.5: - version "1.17.6" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" - integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== + version "1.17.7" + resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" + integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== dependencies: es-to-primitive "^1.2.1" function-bind "^1.1.1" has "^1.0.3" has-symbols "^1.0.1" - is-callable "^1.2.0" - is-regex "^1.1.0" - object-inspect "^1.7.0" + is-callable "^1.2.2" + is-regex "^1.1.1" + object-inspect "^1.8.0" object-keys "^1.1.1" - object.assign "^4.1.0" + object.assign "^4.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1: + version "1.18.0-next.1" + resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" + integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.2" + is-negative-zero "^2.0.0" + is-regex "^1.1.1" + object-inspect "^1.8.0" + object-keys "^1.1.1" + object.assign "^4.1.1" string.prototype.trimend "^1.0.1" string.prototype.trimstart "^1.0.1" @@ -6130,10 +6181,10 @@ es6-weak-map@^2.0.1, es6-weak-map@^2.0.2: es6-iterator "^2.0.3" es6-symbol "^3.1.1" -escalade@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4" - integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ== +escalade@^3.0.2, escalade@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e" + integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig== escape-goat@^2.0.0: version "2.1.1" @@ -6194,11 +6245,11 @@ eslint-scope@^4.0.3: estraverse "^4.1.1" eslint-scope@^5.0.0, eslint-scope@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" - integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== + version "5.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== dependencies: - esrecurse "^4.1.0" + esrecurse "^4.3.0" estraverse "^4.1.1" eslint-utils@^2.0.0, eslint-utils@^2.1.0: @@ -6282,7 +6333,7 @@ esquery@^1.2.0: dependencies: estraverse "^5.1.0" -esrecurse@^4.1.0: +esrecurse@^4.1.0, esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== @@ -7205,9 +7256,9 @@ gaxios@^2.1.0: node-fetch "^2.3.0" gaxios@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/gaxios/-/gaxios-3.1.0.tgz#95f65f5a335f61aff602fe124cfdba8524f765fa" - integrity sha512-DDTn3KXVJJigtz+g0J3vhcfbDbKtAroSTxauWsdnP57sM5KZ3d2c/3D9RKFJ86s43hfw6WULg6TXYw/AYiBlpA== + version "3.2.0" + resolved "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz#11b6f0e8fb08d94a10d4d58b044ad3bec6dd486a" + integrity sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q== dependencies: abort-controller "^3.0.0" extend "^3.0.2" @@ -7224,9 +7275,9 @@ gcp-metadata@^3.4.0: json-bigint "^0.3.0" gcp-metadata@^4.1.0: - version "4.1.4" - resolved "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.1.4.tgz#3adadb9158c716c325849ee893741721a3c09e7e" - integrity sha512-5J/GIH0yWt/56R3dNaNWPGQ/zXsZOddYECfJaqxFWgrZ9HC2Kvc5vl9upOgUUHKzURjAVf2N+f6tEJiojqXUuA== + version "4.2.0" + resolved "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.0.tgz#3b424355ccdc240ee07c5791e2fd6a60a283d89a" + integrity sha512-vQZD57cQkqIA6YPGXM/zc+PIZfNRFdukWGsGZ5+LcJzesi5xp6Gn7a02wRJi4eXPyArNMIYpPET4QMxGqtlk6Q== dependencies: gaxios "^3.0.0" json-bigint "^1.0.0" @@ -7382,9 +7433,9 @@ git-up@^4.0.0: parse-url "^5.0.0" git-url-parse@^11.1.2: - version "11.2.0" - resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.2.0.tgz#2955fd51befd6d96ea1389bbe2ef57e8e6042b04" - integrity sha512-KPoHZg8v+plarZvto4ruIzzJLFQoRx+sUs5DQSr07By9IBKguVd+e6jwrFR6/TP6xrCJlNV1tPqLO1aREc7O2g== + version "11.3.0" + resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.3.0.tgz#1515b4574c4eb2efda7d25cc50b29ce8beaefaae" + integrity sha512-i3XNa8IKmqnUqWBcdWBjOcnyZYfN3C1WRvnKI6ouFWwsXCZEnlgbwbm55ZpJ3OJMhfEP/ryFhqW8bBhej3C5Ug== dependencies: git-up "^4.0.0" @@ -7600,10 +7651,10 @@ google-auth-library@^5.0.0, google-auth-library@^5.5.0: jws "^4.0.0" lru-cache "^5.0.0" -google-auth-library@^6.0.0: - version "6.0.6" - resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.0.6.tgz#5102e5c643baab45b4c16e9752cd56b8861f3a82" - integrity sha512-fWYdRdg55HSJoRq9k568jJA1lrhg9i2xgfhVIMJbskUmbDpJGHsbv9l41DGhCDXM21F9Kn4kUwdysgxSYBYJUw== +google-auth-library@^6.0.0, google-auth-library@^6.1.1: + version "6.1.1" + resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.1.tgz#e1882ac22e8a073dd7a7296e500d5c0c6dc21933" + integrity sha512-0WfExOx3FrLYnY88RICQxvpaNzdwjz44OsHqHkIoAJfjY6Jck6CZRl1ASWadk+wbJ0LhkQ8rNY4zZebKml4Ghg== dependencies: arrify "^2.0.0" base64-js "^1.3.0" @@ -7611,7 +7662,7 @@ google-auth-library@^6.0.0: fast-text-encoding "^1.0.0" gaxios "^3.0.0" gcp-metadata "^4.1.0" - gtoken "^5.0.0" + gtoken "^5.0.4" jws "^4.0.0" lru-cache "^6.0.0" @@ -7729,24 +7780,20 @@ google-gax@^1.14.2: walkdir "^0.4.0" google-gax@^2.2.0: - version "2.8.0" - resolved "https://registry.npmjs.org/google-gax/-/google-gax-2.8.0.tgz#e62bb93c0a9255a76f24e180a0f92a58865f3ebb" - integrity sha512-MPaADY/FHittX5xfOUU2EVqIoE850e+OZ1ys8aO2GnUMaP4U0Bde2wop6kw5sp4fIOjKNlan4GATKAURsYbxSw== + version "2.9.0" + resolved "https://registry.npmjs.org/google-gax/-/google-gax-2.9.0.tgz#84edef8715d82c0f91a6e5485b8f2803d2690f00" + integrity sha512-MFMwA7Fb8PEwjnYwfGXjZMidCNyMl3gSnvS/+kS8TQioJZQDpzK+W3dmwyNyig/U13+kbABqDnbkkAXJ5NiUkw== dependencies: "@grpc/grpc-js" "~1.1.1" "@grpc/proto-loader" "^0.5.1" "@types/long" "^4.0.0" abort-controller "^3.0.0" - duplexify "^3.6.0" + duplexify "^4.0.0" google-auth-library "^6.0.0" is-stream-ended "^0.1.4" - lodash.at "^4.6.0" - lodash.has "^4.5.2" - node-fetch "^2.6.0" + node-fetch "^2.6.1" protobufjs "^6.9.0" retry-request "^4.0.0" - semver "^6.0.0" - walkdir "^0.4.0" google-gax@~1.12.0: version "1.12.0" @@ -7775,7 +7822,7 @@ google-p12-pem@^2.0.0: dependencies: node-forge "^0.9.0" -google-p12-pem@^3.0.0: +google-p12-pem@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz#673ac3a75d3903a87f05878f3c75e06fc151669e" integrity sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA== @@ -7851,13 +7898,13 @@ gtoken@^4.1.0: jws "^4.0.0" mime "^2.2.0" -gtoken@^5.0.0: - version "5.0.3" - resolved "https://registry.npmjs.org/gtoken/-/gtoken-5.0.3.tgz#b76ef8e9a2fed6fef165e47f7d05b60c498e4d05" - integrity sha512-Nyd1wZCMRc2dj/mAD0LlfQLcAO06uKdpKJXvK85SGrF5+5+Bpfil9u/2aw35ltvEHjvl0h5FMKN5knEU+9JrOg== +gtoken@^5.0.4: + version "5.0.4" + resolved "https://registry.npmjs.org/gtoken/-/gtoken-5.0.4.tgz#e8d7456ad2ff774c70176e56b9d34b1c63fb6f0b" + integrity sha512-U9wnSp4GZ7ov6zRdPuRHG4TuqEWqRRgT1gfXGNArhzBUn9byrPeH8uTmBWU/ZiWJJvTEmkjhDIC3mqHWdVi3xQ== dependencies: gaxios "^3.0.0" - google-p12-pem "^3.0.0" + google-p12-pem "^3.0.3" jws "^4.0.0" mime "^2.2.0" @@ -8086,9 +8133,9 @@ hash.js@^1.0.0, hash.js@^1.0.3: minimalistic-assert "^1.0.1" hasha@^5.0.0: - version "5.2.0" - resolved "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz#33094d1f69c40a4a6ac7be53d5fe3ff95a269e0c" - integrity sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw== + version "5.2.1" + resolved "https://registry.npmjs.org/hasha/-/hasha-5.2.1.tgz#0e5b492aa40de3819e80955f221d2fccef55b5aa" + integrity sha512-x15jnRSHTi3VmH+oHtVb9kgU/HuKOK8mjK8iCL3dPQXh4YJlUb9YSI8ZLiiqLAIvY2wuDIlZYZppy8vB2XISkQ== dependencies: is-stream "^2.0.0" type-fest "^0.8.0" @@ -8609,10 +8656,10 @@ is-buffer@~2.0.3: resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== -is-callable@^1.1.4, is-callable@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" - integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== +is-callable@^1.1.4, is-callable@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" + integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== is-ci@^2.0.0: version "2.0.0" @@ -8756,6 +8803,11 @@ is-negated-glob@^1.0.0: resolved "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" integrity sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI= +is-negative-zero@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" + integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= + is-npm@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" @@ -8836,10 +8888,10 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-plain-object@^4.0.0: - version "4.1.1" - resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-4.1.1.tgz#1a14d6452cbd50790edc7fdaa0aed5a40a35ebb5" - integrity sha512-5Aw8LLVsDlZsETVMhoMXzqsXwQqr/0vlnBYzIXJbYo2F4yYlhLHs+Ez7Bod7IIQKWkJbJfxrWD7pA1Dw1TKrwA== +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== is-promise@^2.1, is-promise@^2.1.0: version "2.2.2" @@ -8858,7 +8910,7 @@ is-reference@^1.1.2: dependencies: "@types/estree" "*" -is-regex@^1.1.0: +is-regex@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== @@ -9232,9 +9284,9 @@ jest-worker@^24.0.0: supports-color "^6.1.0" jest-worker@^26.2.1: - version "26.3.0" - resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f" - integrity sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw== + version "26.5.0" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.5.0.tgz#87deee86dbbc5f98d9919e0dadf2c40e3152fa30" + integrity sha512-kTw66Dn4ZX7WpjZ7T/SUDgRhapFRKWmisVAF0Rv4Fu8SLFD7eLbqpLvbxVqYhSgaWa7I+bW7pHnbyfNsH6stug== dependencies: "@types/node" "*" merge-stream "^2.0.0" @@ -9412,9 +9464,9 @@ jsonparse@^1.2.0: integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= jsonschema@^1.0.2: - version "1.2.6" - resolved "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.6.tgz#52b0a8e9dc06bbae7295249d03e4b9faee8a0c0b" - integrity sha512-SqhURKZG07JyKKeo/ir24QnS4/BV7a6gQy93bUSe4lUdNp0QNpIz2c9elWJQ9dpc5cQYY6cvCzgRwy0MQCLyqA== + version "1.2.10" + resolved "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.10.tgz#38dc18b63839e8f07580df015e37d959f20d1eda" + integrity sha512-CoRSun5gmvgSYMHx5msttse19SnQpaHoPzIqULwE7B9KtR4Od1g70sBqeUriq5r8b9R3ptDc0o7WKpUDjUgLgg== jsonwebtoken@^8.2.1, jsonwebtoken@^8.5.1: version "8.5.1" @@ -9458,9 +9510,9 @@ just-debounce@^1.0.0: integrity sha1-h/zPrv/AtozRnVX2cilD+SnqNeo= just-extend@^4.0.2: - version "4.1.0" - resolved "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4" - integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA== + version "4.1.1" + resolved "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz#158f1fdb01f128c411dc8b286a7b4837b3545282" + integrity sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA== jwa@^1.4.1: version "1.4.1" @@ -10641,11 +10693,16 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.44.0, "mime-db@>= 1.43.0 < 2": +mime-db@1.44.0: version "1.44.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== +"mime-db@>= 1.43.0 < 2": + version "1.45.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" + integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== + mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.16, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" @@ -10878,7 +10935,7 @@ ms@2.1.1: resolved "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -ms@^2.0.0, ms@^2.1.1: +ms@2.1.2, ms@^2.0.0, ms@^2.1.1: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== @@ -11034,7 +11091,7 @@ node-fetch-npm@^2.0.2: json-parse-better-errors "^1.0.0" safe-buffer "^5.1.1" -node-fetch@2.6.1, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0: +node-fetch@2.6.1, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== @@ -11066,7 +11123,7 @@ node-gyp@^5.0.2: tar "^4.4.12" which "^1.3.1" -node-gyp@^7.0.0: +node-gyp@^7.1.0: version "7.1.0" resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.0.tgz#cb8aed7ab772e73ad592ae0c71b0e3741099fe39" integrity sha512-rjlHQlnl1dqiDZxZYiKqQdrjias7V+81OVR5PTzZioCBtWkNdrKy06M05HLKxy/pcKikKRCabeDRoZaEc6nIjw== @@ -11141,10 +11198,10 @@ node-preload@^0.2.1: dependencies: process-on-spawn "^1.0.0" -node-releases@^1.1.60: - version "1.1.60" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" - integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== +node-releases@^1.1.61: + version "1.1.61" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e" + integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g== node-status-codes@^1.0.0: version "1.0.0" @@ -11363,18 +11420,18 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.7.0: +object-inspect@^1.8.0: version "1.8.0" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== object-is@^1.0.1: - version "1.1.2" - resolved "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" - integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== + version "1.1.3" + resolved "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81" + integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg== dependencies: define-properties "^1.1.3" - es-abstract "^1.17.5" + es-abstract "^1.18.0-next.1" object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" @@ -11388,7 +11445,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@4.1.0, object.assign@^4.0.4, object.assign@^4.1.0: +object.assign@4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== @@ -11398,6 +11455,16 @@ object.assign@4.1.0, object.assign@^4.0.4, object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" +object.assign@^4.0.4, object.assign@^4.1.0, object.assign@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" + integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.18.0-next.0" + has-symbols "^1.0.1" + object-keys "^1.1.1" + object.defaults@^1.0.0, object.defaults@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz#3a7f868334b407dea06da16d88d5cd29e435fecf" @@ -11869,6 +11936,11 @@ parseqs@0.0.5: dependencies: better-assert "~1.0.0" +parseqs@0.0.6: + version "0.0.6" + resolved "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5" + integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w== + parseuri@0.0.5: version "0.0.5" resolved "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" @@ -11876,6 +11948,11 @@ parseuri@0.0.5: dependencies: better-assert "~1.0.0" +parseuri@0.0.6: + version "0.0.6" + resolved "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a" + integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow== + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -12460,13 +12537,13 @@ rc@^1.2.7, rc@^1.2.8: strip-json-comments "~2.0.1" re2@^1.15.0: - version "1.15.4" - resolved "https://registry.npmjs.org/re2/-/re2-1.15.4.tgz#2ffc3e4894fb60430393459978197648be01a0a9" - integrity sha512-7w3K+Daq/JjbX/dz5voMt7B9wlprVBQnMiypyCojAZ99kcAL+3LiJ5uBoX/u47l8eFTVq3Wj+V0pmvU+CT8tOg== + version "1.15.5" + resolved "https://registry.npmjs.org/re2/-/re2-1.15.5.tgz#070b728933433ab605134b827a58919038cc8048" + integrity sha512-DwlSUKqhBbECQYbJOudEC20d4Y4KnE7H6gFh1n7zRe10nFP7X2UKZ0ko2rz2islKyjl5KrPyCrxh9hJfiVx0VQ== dependencies: install-artifact-from-github "^1.0.2" nan "^2.14.1" - node-gyp "^7.0.0" + node-gyp "^7.1.0" read-all-stream@^3.0.0: version "3.1.0" @@ -12737,9 +12814,9 @@ regexpp@^3.0.0, regexpp@^3.1.0: integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== regexpu-core@^4.7.0: - version "4.7.0" - resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938" - integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ== + version "4.7.1" + resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" + integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== dependencies: regenerate "^1.4.0" regenerate-unicode-properties "^8.2.0" @@ -13626,11 +13703,11 @@ socket.io-client@2.3.0: to-array "0.1.4" socket.io-parser@~3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f" - integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng== + version "3.3.1" + resolved "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz#f07d9c8cb3fb92633aa93e76d98fd3a334623199" + integrity sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ== dependencies: - component-emitter "1.2.1" + component-emitter "~1.3.0" debug "~3.1.0" isarray "2.0.1" @@ -13714,7 +13791,7 @@ source-map-resolve@^0.6.0: atob "^2.1.2" decode-uri-component "^0.2.0" -source-map-support@^0.5.17, source-map-support@~0.5.12: +source-map-support@^0.5.17, source-map-support@~0.5.12, source-map-support@~0.5.19: version "0.5.19" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== @@ -13744,7 +13821,7 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.7.3: +source-map@^0.7.3, source-map@~0.7.2: version "0.7.3" resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== @@ -13817,9 +13894,9 @@ spdx-expression-validate@2.0.0: spdx-expression-parse "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.5" - resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" - integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + version "3.0.6" + resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce" + integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw== spdx-ranges@^2.0.0: version "2.1.1" @@ -14188,9 +14265,9 @@ stubs@^3.0.0: integrity sha1-6NK6H6nJBXAwPAMLaQD31fiavls= superstatic@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/superstatic/-/superstatic-7.0.0.tgz#ac365bd8f224c4904b46c47bddfbebcc30df23d6" - integrity sha512-rFXKNsouDBIor3PeroAcZAaCIjXCoocR6Drk7XK3oynFzcgv2Vkgt/6B5GqSe+qp4+N0SDTtVLP/PamBrG/Kfg== + version "7.0.1" + resolved "https://registry.npmjs.org/superstatic/-/superstatic-7.0.1.tgz#cf82b0fd03100d294636ec76ccfa4eda6f255676" + integrity sha512-oph3y5srRKrF8qeCVnQXbysb7U9ixPZQBlqniQymZimJwy2D1xba0EMouCFquhkwRrZYLgd7YPtkSBaPwyFYZA== dependencies: as-array "^2.0.0" async "^1.5.2" @@ -14299,11 +14376,11 @@ tapable@^1.0.0, tapable@^1.1.3: integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== tar-stream@^2.1.0: - version "2.1.3" - resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.3.tgz#1e2022559221b7866161660f118255e20fa79e41" - integrity sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA== + version "2.1.4" + resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa" + integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw== dependencies: - bl "^4.0.1" + bl "^4.0.3" end-of-stream "^1.4.1" fs-constants "^1.0.0" inherits "^2.0.3" @@ -14355,13 +14432,13 @@ tcp-port-used@^1.0.1: is2 "2.0.1" teeny-request@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.0.tgz#0e5c090bd9102ed559ffc8c9ddb00fbe1256db30" - integrity sha512-kWD3sdGmIix6w7c8ZdVKxWq+3YwVPGWz+Mq0wRZXayEKY/YHb63b8uphfBzcFDmyq8frD9+UTc3wLyOhltRbtg== + version "7.0.1" + resolved "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz#bdd41fdffea5f8fbc0d29392cb47bec4f66b2b4c" + integrity sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw== dependencies: http-proxy-agent "^4.0.0" https-proxy-agent "^5.0.0" - node-fetch "^2.2.0" + node-fetch "^2.6.1" stream-events "^1.0.5" uuid "^8.0.0" @@ -14428,13 +14505,13 @@ terser@^4.1.2: source-map-support "~0.5.12" terser@^5.0.0: - version "5.3.0" - resolved "https://registry.npmjs.org/terser/-/terser-5.3.0.tgz#c481f4afecdcc182d5e2bdd2ff2dc61555161e81" - integrity sha512-XTT3D3AwxC54KywJijmY2mxZ8nJiEjBHVYzq8l9OaYuRFWeQNBwvipuzzYEP4e+/AVcd1hqG/CqgsdIRyT45Fg== + version "5.3.4" + resolved "https://registry.npmjs.org/terser/-/terser-5.3.4.tgz#e510e05f86e0bd87f01835c3238839193f77a60c" + integrity sha512-dxuB8KQo8Gt6OVOeLg/rxfcxdNZI/V1G6ze1czFUzPeCFWZRtvZMgSzlZZ5OYBZ4HoG607F6pFPNLekJyV+yVw== dependencies: commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" + source-map "~0.7.2" + source-map-support "~0.5.19" test-exclude@^6.0.0: version "6.0.0" @@ -14769,9 +14846,9 @@ tslib@2.0.1: integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== tslib@^1.11.1, tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.0: - version "1.13.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== + version "1.14.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz#d624983f3e2c5e0b55307c3dd6c86acd737622c6" + integrity sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw== tslint@6.1.3: version "6.1.3" @@ -14982,9 +15059,9 @@ ua-parser-js@0.7.21: integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ== uglify-js@^3.1.4, uglify-js@^3.4.9: - version "3.10.4" - resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.4.tgz#dd680f5687bc0d7a93b14a3482d16db6eba2bfbb" - integrity sha512-kBFT3U4Dcj4/pJ52vfjCSfyLyvG9VYYuGYPmrPvAxRw/i7xHiT4VvCev+uiEMcEEiu6UNB6KgWmGtSUYIWScbw== + version "3.11.1" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.11.1.tgz#32d274fea8aac333293044afd7f81409d5040d38" + integrity sha512-OApPSuJcxcnewwjSGGfWOjx3oix5XpmrK9Z2j0fTRlHGoZ49IU6kExfZTM0++fCArOOCet+vIfWwFHbvWqwp6g== uid-number@0.0.6: version "0.0.6" @@ -15162,9 +15239,9 @@ upath@^1.1.1, upath@^1.2.0: integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== update-notifier@^4.1.0: - version "4.1.1" - resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.1.tgz#895fc8562bbe666179500f9f2cebac4f26323746" - integrity sha512-9y+Kds0+LoLG6yN802wVXoIfxYEwh3FlZwzMwpCZp62S2i1/Jzeqb9Eeeju3NSHccGGasfGlK5/vEHbAifYRDg== + version "4.1.3" + resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3" + integrity sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A== dependencies: boxen "^4.2.0" chalk "^3.0.0" @@ -15278,9 +15355,9 @@ uuid@^3.0.0, uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3: integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== uuid@^8.0.0: - version "8.3.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" - integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== + version "8.3.1" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" + integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== v8-compile-cache@^2.0.3: version "2.1.1" @@ -15387,9 +15464,9 @@ vinyl-sourcemaps-apply@^0.2.0: source-map "^0.5.1" vinyl@2.x, vinyl@^2.0.0, vinyl@^2.1.0: - version "2.2.0" - resolved "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86" - integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg== + version "2.2.1" + resolved "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974" + integrity sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw== dependencies: clone "^2.1.1" clone-buffer "^1.0.0" @@ -15536,7 +15613,7 @@ webpack-stream@6.1.0: vinyl "^2.1.0" webpack "^4.26.1" -webpack@4.44.2: +webpack@4.44.2, webpack@^4.26.1: version "4.44.2" resolved "https://registry.npmjs.org/webpack/-/webpack-4.44.2.tgz#6bfe2b0af055c8b2d1e90ed2cd9363f841266b72" integrity sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q== @@ -15565,35 +15642,6 @@ webpack@4.44.2: watchpack "^1.7.4" webpack-sources "^1.4.1" -webpack@^4.26.1: - version "4.44.1" - resolved "https://registry.npmjs.org/webpack/-/webpack-4.44.1.tgz#17e69fff9f321b8f117d1fda714edfc0b939cc21" - integrity sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/wasm-edit" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - acorn "^6.4.1" - ajv "^6.10.2" - ajv-keywords "^3.4.1" - chrome-trace-event "^1.0.2" - enhanced-resolve "^4.3.0" - eslint-scope "^4.0.3" - json-parse-better-errors "^1.0.2" - loader-runner "^2.4.0" - loader-utils "^1.2.3" - memory-fs "^0.4.1" - micromatch "^3.1.10" - mkdirp "^0.5.3" - neo-async "^2.6.1" - node-libs-browser "^2.2.1" - schema-utils "^1.0.0" - tapable "^1.1.3" - terser-webpack-plugin "^1.4.3" - watchpack "^1.7.4" - webpack-sources "^1.4.1" - websocket-driver@>=0.5.1: version "0.7.4" resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" @@ -15638,9 +15686,9 @@ whatwg-url@^7.0.0: webidl-conversions "^4.0.2" whatwg-url@^8.1.0: - version "8.2.2" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.2.2.tgz#85e7f9795108b53d554cec640b2e8aee2a0d4bfd" - integrity sha512-PcVnO6NiewhkmzV0qn7A+UZ9Xx4maNTI+O+TShmfE4pqjoCMwUMjkvoNhNHPTvgR7QH9Xt3R13iHuWy2sToFxQ== + version "8.4.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz#50fb9615b05469591d2b2bd6dfaed2942ed72837" + integrity sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw== dependencies: lodash.sortby "^4.7.0" tr46 "^2.0.2" @@ -15949,9 +15997,9 @@ y18n@^4.0.0: integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== y18n@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.1.tgz#1ad2a7eddfa8bce7caa2e1f6b5da96c39d99d571" - integrity sha512-/jJ831jEs4vGDbYPQp4yGKDYPSCCEQ45uZWJHE1AoYBzqdZi8+LDWas0z4HrmJXmKdpFsTiowSHXdxyFhpmdMg== + version "5.0.2" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.2.tgz#48218df5da2731b4403115c39a1af709c873f829" + integrity sha512-CkwaeZw6dQgqgPGeTWKMXCRmMcBgETFlTml1+ZOO+q7kGst8NREJ+eWwFNPVUQ4QGdAaklbqCZHH6Zuep1RjiA== yallist@^2.1.2: version "2.1.2" @@ -16013,9 +16061,9 @@ yargs-parser@^18.1.2, yargs-parser@^18.1.3: decamelize "^1.2.0" yargs-parser@^20.0.0: - version "20.2.0" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.0.tgz#944791ca2be2e08ddadd3d87e9de4c6484338605" - integrity sha512-2agPoRFPoIcFzOIp6656gcvsg2ohtscpw2OINr/q46+Sq41xz2OYLqx5HRHabmFU1OARIPAYH5uteICE7mn/5A== + version "20.2.1" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.1.tgz#28f3773c546cdd8a69ddae68116b48a5da328e77" + integrity sha512-yYsjuSkjbLMBp16eaOt7/siKTjNVjMm3SoJnIg3sEh/JsvqVVDyjRKmaJV4cl+lNIgq6QEco2i3gDebJl7/vLA== yargs-unparser@1.6.0: version "1.6.0" From 5dd5c9cec71c8976214693927a80f2b678095c84 Mon Sep 17 00:00:00 2001 From: Wu-Hui Date: Thu, 8 Oct 2020 16:39:07 -0400 Subject: [PATCH 17/27] Fix build errors --- packages/firebase/index.d.ts | 2 +- packages/firestore-types/index.d.ts | 2 +- packages/firestore/exp-types/index.d.ts | 2 +- packages/firestore/exp/src/api/database.ts | 61 ++++++++++--------- packages/firestore/exp/test/shim.ts | 10 +-- packages/firestore/src/api/bundle.ts | 3 +- packages/firestore/src/api/observer.ts | 2 +- .../firestore/src/core/firestore_client.ts | 18 +----- 8 files changed, 45 insertions(+), 55 deletions(-) diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index db36662baf5..9d3f91095e8 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -8262,7 +8262,7 @@ declare namespace firebase.firestore { bundleData: ArrayBuffer | ReadableStream | string ): LoadBundleTask; - namedQuery(name: string): Promise; + namedQuery(name: string): Promise | null>; /** * @hidden diff --git a/packages/firestore-types/index.d.ts b/packages/firestore-types/index.d.ts index 3ad2d8dc5f5..514c7090de3 100644 --- a/packages/firestore-types/index.d.ts +++ b/packages/firestore-types/index.d.ts @@ -98,7 +98,7 @@ export class FirebaseFirestore { bundleData: ArrayBuffer | ReadableStream | string ): LoadBundleTask; - namedQuery(name: string): Promise; + namedQuery(name: string): Promise | null>; INTERNAL: { delete: () => Promise }; } diff --git a/packages/firestore/exp-types/index.d.ts b/packages/firestore/exp-types/index.d.ts index d2483a25a7a..19994191aab 100644 --- a/packages/firestore/exp-types/index.d.ts +++ b/packages/firestore/exp-types/index.d.ts @@ -551,7 +551,7 @@ export function loadBundle( export function namedQuery( firestore: FirebaseFirestore, name: string -): Promise; +): Promise | null>; export type FirestoreErrorCode = | 'cancelled' diff --git a/packages/firestore/exp/src/api/database.ts b/packages/firestore/exp/src/api/database.ts index 9167e7813be..2b193bf00c9 100644 --- a/packages/firestore/exp/src/api/database.ts +++ b/packages/firestore/exp/src/api/database.ts @@ -21,7 +21,7 @@ import { Provider } from '@firebase/component'; import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { - enqueueLoadBundle, + createBundleReader, MAX_CONCURRENT_LIMBO_RESOLUTIONS } from '../../../src/core/firestore_client'; import { @@ -41,7 +41,7 @@ import { import { Code, FirestoreError } from '../../../src/util/error'; import { Deferred } from '../../../src/util/promise'; import { LruParams } from '../../../src/local/lru_garbage_collector'; -import { CACHE_SIZE_UNLIMITED } from '../../../src/api/database'; +import { CACHE_SIZE_UNLIMITED, Query } from '../../../src/api/database'; import { indexedDbClearPersistence, indexedDbStoragePrefix @@ -61,14 +61,18 @@ import { DatabaseInfo } from '../../../src/core/database_info'; import { AutoId } from '../../../src/util/misc'; import { User } from '../../../src/auth/user'; import { CredentialChangeListener } from '../../../src/api/credentials'; -import { logDebug } from '../../../src/util/log'; -import { registerPendingWritesCallback } from '../../../src/core/sync_engine'; +import { logDebug, logWarn } from '../../../src/util/log'; +import { + loadBundle as loadBundleSyncEngine, + registerPendingWritesCallback +} from '../../../src/core/sync_engine'; import { remoteStoreDisableNetwork, remoteStoreEnableNetwork } from '../../../src/remote/remote_store'; import { PersistenceSettings } from '../../../exp-types'; import { getNamedQuery } from '../../../src/local/local_store'; +import { newSerializer } from '../../../src/platform/serializer'; const LOG_TAG = 'Firestore'; @@ -487,38 +491,39 @@ function verifyNotInitialized(firestore: FirebaseFirestore): void { } export function loadBundle( - firestore: firestore.FirebaseFirestore, + firestore: FirebaseFirestore, bundleData: ArrayBuffer | ReadableStream | string ): LoadBundleTask { - const firestoreImpl = cast(firestore, Firestore); + firestore._verifyNotTerminated(); + const resultTask = new LoadBundleTask(); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - getSyncEngine(firestoreImpl).then(async syncEngine => { - const databaseId = (await firestoreImpl._getConfiguration()).databaseInfo + + firestore._queue.enqueueAndForget(async () => { + const databaseId = (await firestore._getConfiguration()).databaseInfo .databaseId; - enqueueLoadBundle( - databaseId, - firestoreImpl._queue, - syncEngine, - bundleData, - resultTask - ); - }); + const reader = createBundleReader(bundleData, newSerializer(databaseId)); + const syncEngine = await getSyncEngine(firestore); + loadBundleSyncEngine(syncEngine, reader, resultTask); + return resultTask.catch((e: Error) => { + logWarn(LOG_TAG, `Loading bundle failed with ${e}`); + }); + }); return resultTask; } -export async function namedQuery( - firestore: firestore.FirebaseFirestore, +export function namedQuery( + firestore: FirebaseFirestore, name: string -): Promise { - const firestoreImpl = cast(firestore, Firestore); - const localStore = await getLocalStore(firestoreImpl); - const namedQuery = await getNamedQuery(localStore, name); - if (!namedQuery) { - return null; - } +): Promise { + return getLocalStore(firestore).then(localStore => { + return getNamedQuery(localStore, name).then(namedQuery => { + if (!namedQuery) { + return null; + } - return null; - // return new Query(firestoreImpl, null, namedQuery.query); + return null; + // return new Query(namedQuery.query, firestore, null); + }); + }); } diff --git a/packages/firestore/exp/test/shim.ts b/packages/firestore/exp/test/shim.ts index 005be22dce4..ec0f36e56a4 100644 --- a/packages/firestore/exp/test/shim.ts +++ b/packages/firestore/exp/test/shim.ts @@ -69,6 +69,7 @@ import { UntypedFirestoreDataConverter } from '../../src/api/user_data_reader'; import { isPartialObserver, PartialObserver } from '../../src/api/observer'; import { isPlainObject } from '../../src/util/input_validation'; import { Compat } from '../../src/compat/compat'; +import { LoadBundleTask } from '../../exp-types'; export { GeoPoint, Timestamp } from '../index'; export { FieldValue } from '../../src/compat/field_value'; @@ -166,14 +167,13 @@ export class FirebaseFirestore loadBundle( bundleData: ArrayBuffer | ReadableStream | string - ): legacy.LoadBundleTask { + ): LoadBundleTask { return loadBundle(this._delegate, bundleData)!; } - async namedQuery(name: string): Promise { - return namedQuery(this._delegate, name).then(query => { - return query ? new Query(this, query) : null; - }); + async namedQuery(name: string): Promise { + return null; + // return namedQuery(this._delegate, name); } INTERNAL = { diff --git a/packages/firestore/src/api/bundle.ts b/packages/firestore/src/api/bundle.ts index 703a5d06482..012827ca701 100644 --- a/packages/firestore/src/api/bundle.ts +++ b/packages/firestore/src/api/bundle.ts @@ -19,6 +19,7 @@ import * as firestore from '@firebase/firestore-types'; import { Deferred } from '../util/promise'; import { PartialObserver } from './observer'; import { debugAssert } from '../util/assert'; +import { FirestoreError } from '../util/error'; export class LoadBundleTask implements @@ -85,7 +86,7 @@ export class LoadBundleTask * Notifies all observers that bundle loading has failed, with a provided * `Error` as the reason. */ - _failWith(error: Error): void { + _failWith(error: FirestoreError): void { this._lastProgress.taskState = 'Error'; if (this._progressObserver.next) { diff --git a/packages/firestore/src/api/observer.ts b/packages/firestore/src/api/observer.ts index 9b08b1be198..69264fd20f0 100644 --- a/packages/firestore/src/api/observer.ts +++ b/packages/firestore/src/api/observer.ts @@ -22,7 +22,7 @@ import { FirestoreError } from '../util/error'; * Observer/Subscribe interfaces. */ export type NextFn = (value: T) => void; -export type ErrorFn = (error: Error) => void; +export type ErrorFn = (error: FirestoreError) => void; export type CompleteFn = () => void; // Allow for any of the Observer methods to be undefined. diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index d40b4689f59..8ec3ed2842c 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -778,7 +778,7 @@ export function executeQueryViaSnapshotListener( return eventManagerListen(eventManager, listener); } -function createBundleReader( +export function createBundleReader( data: ReadableStream | ArrayBuffer | string, serializer: JsonProtoSerializer ): BundleReader { @@ -790,19 +790,3 @@ function createBundleReader( } return new BundleReader(toByteStreamReader(content), serializer); } - -export function enqueueLoadBundle( - databaseId: DatabaseId, - asyncQueue: AsyncQueue, - syncEngine: SyncEngine, - data: ReadableStream | ArrayBuffer | string, - resultTask: LoadBundleTask -): void { - const reader = createBundleReader(data, newSerializer(databaseId)); - asyncQueue.enqueueAndForget(async () => { - loadBundle(syncEngine, reader, resultTask); - return resultTask.catch(e => { - logWarn(LOG_TAG, `Loading bundle failed with ${e}`); - }); - }); -} From e53cf69aade2cb13aa8a60bf4414d1b7e8160b60 Mon Sep 17 00:00:00 2001 From: Wu-Hui Date: Thu, 8 Oct 2020 16:53:52 -0400 Subject: [PATCH 18/27] Fix lint errors --- packages/firestore/exp/test/shim.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/firestore/exp/test/shim.ts b/packages/firestore/exp/test/shim.ts index ec0f36e56a4..f916e178fa4 100644 --- a/packages/firestore/exp/test/shim.ts +++ b/packages/firestore/exp/test/shim.ts @@ -42,7 +42,6 @@ import { getDocsFromServer, initializeFirestore, loadBundle, - namedQuery, onSnapshot, onSnapshotsInSync, query, From bd6adc616c9b7324283554f37ffe267afd7cad96 Mon Sep 17 00:00:00 2001 From: Wu-Hui Date: Thu, 8 Oct 2020 20:38:33 -0400 Subject: [PATCH 19/27] Hide bundles from public API --- .../platform_browser/iframe/gapi.iframes.ts | 6 +-- packages/firebase/index.d.ts | 6 --- packages/firestore-types/index.d.ts | 6 --- packages/firestore/exp-types/index.d.ts | 10 ----- packages/firestore/exp/index.ts | 4 +- packages/firestore/exp/src/api/database.ts | 4 +- packages/firestore/exp/test/shim.ts | 4 +- packages/firestore/src/api/database.ts | 15 +++---- .../integration/api_internal/bundle.test.ts | 39 +++++++++++++------ 9 files changed, 44 insertions(+), 50 deletions(-) diff --git a/packages-exp/auth-exp/src/platform_browser/iframe/gapi.iframes.ts b/packages-exp/auth-exp/src/platform_browser/iframe/gapi.iframes.ts index c312e7a6596..45027432be8 100644 --- a/packages-exp/auth-exp/src/platform_browser/iframe/gapi.iframes.ts +++ b/packages-exp/auth-exp/src/platform_browser/iframe/gapi.iframes.ts @@ -15,9 +15,9 @@ * limitations under the License. */ -// For some reason, the linter doesn't recognize that these are used elsewhere -// in the SDK -/* eslint-disable @typescript-eslint/no-unused-vars */ + // For some reason, the linter doesn't recognize that these are used elsewhere + // in the SDK + /* eslint-disable @typescript-eslint/no-unused-vars */ declare namespace gapi { type LoadCallback = () => void; diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index 9d3f91095e8..7732e440c83 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -8258,12 +8258,6 @@ declare namespace firebase.firestore { */ terminate(): Promise; - loadBundle( - bundleData: ArrayBuffer | ReadableStream | string - ): LoadBundleTask; - - namedQuery(name: string): Promise | null>; - /** * @hidden */ diff --git a/packages/firestore-types/index.d.ts b/packages/firestore-types/index.d.ts index 514c7090de3..60fa0ce7cd6 100644 --- a/packages/firestore-types/index.d.ts +++ b/packages/firestore-types/index.d.ts @@ -94,12 +94,6 @@ export class FirebaseFirestore { terminate(): Promise; - loadBundle( - bundleData: ArrayBuffer | ReadableStream | string - ): LoadBundleTask; - - namedQuery(name: string): Promise | null>; - INTERNAL: { delete: () => Promise }; } diff --git a/packages/firestore/exp-types/index.d.ts b/packages/firestore/exp-types/index.d.ts index 19994191aab..acf5cd46947 100644 --- a/packages/firestore/exp-types/index.d.ts +++ b/packages/firestore/exp-types/index.d.ts @@ -543,16 +543,6 @@ export interface LoadBundleTaskProgress { export type TaskState = 'Error' | 'Running' | 'Success'; -export function loadBundle( - firestore: FirebaseFirestore, - bundleData: ArrayBuffer | ReadableStream | string -): LoadBundleTask; - -export function namedQuery( - firestore: FirebaseFirestore, - name: string -): Promise | null>; - export type FirestoreErrorCode = | 'cancelled' | 'unknown' diff --git a/packages/firestore/exp/index.ts b/packages/firestore/exp/index.ts index 3996f0a9714..22eed40f49f 100644 --- a/packages/firestore/exp/index.ts +++ b/packages/firestore/exp/index.ts @@ -31,8 +31,8 @@ export { waitForPendingWrites, disableNetwork, enableNetwork, - namedQuery, - loadBundle, + _namedQuery, + _loadBundle, terminate, Settings } from './src/api/database'; diff --git a/packages/firestore/exp/src/api/database.ts b/packages/firestore/exp/src/api/database.ts index 2b193bf00c9..e8ca999afde 100644 --- a/packages/firestore/exp/src/api/database.ts +++ b/packages/firestore/exp/src/api/database.ts @@ -490,7 +490,7 @@ function verifyNotInitialized(firestore: FirebaseFirestore): void { } } -export function loadBundle( +export function _loadBundle( firestore: FirebaseFirestore, bundleData: ArrayBuffer | ReadableStream | string ): LoadBundleTask { @@ -512,7 +512,7 @@ export function loadBundle( return resultTask; } -export function namedQuery( +export function _namedQuery( firestore: FirebaseFirestore, name: string ): Promise { diff --git a/packages/firestore/exp/test/shim.ts b/packages/firestore/exp/test/shim.ts index f916e178fa4..3733df8368f 100644 --- a/packages/firestore/exp/test/shim.ts +++ b/packages/firestore/exp/test/shim.ts @@ -41,7 +41,7 @@ import { getDocsFromCache, getDocsFromServer, initializeFirestore, - loadBundle, + _loadBundle, onSnapshot, onSnapshotsInSync, query, @@ -167,7 +167,7 @@ export class FirebaseFirestore loadBundle( bundleData: ArrayBuffer | ReadableStream | string ): LoadBundleTask { - return loadBundle(this._delegate, bundleData)!; + return _loadBundle(this._delegate, bundleData)!; } async namedQuery(name: string): Promise { diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 49d6ab4181e..610dbe2003f 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -532,7 +532,7 @@ export class Firestore implements PublicFirestore, FirebaseService { } } - loadBundle( + _loadBundle( bundleData: ArrayBuffer | ReadableStream | string ): LoadBundleTask { this.ensureClientConfigured(); @@ -541,14 +541,15 @@ export class Firestore implements PublicFirestore, FirebaseService { return resultTask; } - async namedQuery(name: string): Promise { + _namedQuery(name: string): Promise { this.ensureClientConfigured(); - const namedQuery = await this._firestoreClient!.getNamedQuery(name); - if (!namedQuery) { - return null; - } + return this._firestoreClient!.getNamedQuery(name).then(namedQuery => { + if (!namedQuery) { + return null; + } - return new Query(namedQuery.query, this, null); + return new Query(namedQuery.query, this, null); + }); } ensureClientConfigured(): FirestoreClient { diff --git a/packages/firestore/test/integration/api_internal/bundle.test.ts b/packages/firestore/test/integration/api_internal/bundle.test.ts index c9af02a2309..3cadfef3c2f 100644 --- a/packages/firestore/test/integration/api_internal/bundle.test.ts +++ b/packages/firestore/test/integration/api_internal/bundle.test.ts @@ -29,6 +29,7 @@ import { EventsAccumulator } from '../util/events_accumulator'; import { TestBundleBuilder } from '../../unit/util/bundle_data'; import { newTextEncoder } from '../../../src/platform/serializer'; import { collectionReference } from '../../util/api_helpers'; +import { LoadBundleTask } from '../../../src/api/bundle'; // TODO(b/162594908): Move this to api/ instead of api_internal. @@ -114,7 +115,8 @@ apiDescribe('Bundles', (persistence: boolean) => { const progressEvents: firestore.LoadBundleTaskProgress[] = []; let completeCalled = false; - const task = db.loadBundle( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const task: LoadBundleTask = (db as any)._loadBundle( builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) ); task.onProgress( @@ -145,10 +147,14 @@ apiDescribe('Bundles', (persistence: boolean) => { let snap = await db.collection('coll-1').get({ source: 'cache' }); verifySnapEqualTestDocs(snap); - snap = await (await db.namedQuery('limit'))!.get({ source: 'cache' }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + snap = await (await (db as any)._namedQuery('limit'))!.get({ + source: 'cache' + }); expect(toDataArray(snap)).to.deep.equal([{ k: 'b', bar: 2 }]); - snap = await (await db.namedQuery('limit-to-last'))!.get({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + snap = await (await (db as any)._namedQuery('limit-to-last'))!.get({ source: 'cache' }); expect(toDataArray(snap)).to.deep.equal([{ k: 'a', bar: 1 }]); @@ -159,7 +165,8 @@ apiDescribe('Bundles', (persistence: boolean) => { return withTestDb(persistence, async db => { const builder = bundleWithTestDocsAndQueries(db); - const fulfillProgress: firestore.LoadBundleTaskProgress = await db.loadBundle( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fulfillProgress: firestore.LoadBundleTaskProgress = await (db as any)._loadBundle( builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) ); @@ -176,13 +183,15 @@ apiDescribe('Bundles', (persistence: boolean) => { return withTestDb(persistence, async db => { const builder = bundleWithTestDocsAndQueries(db); - await db.loadBundle( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await (db as any)._loadBundle( builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) ); let completeCalled = false; const progressEvents: firestore.LoadBundleTaskProgress[] = []; - const task = db.loadBundle( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const task: LoadBundleTask = (db as any)._loadBundle( encoder.encode( builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) ) @@ -221,7 +230,8 @@ apiDescribe('Bundles', (persistence: boolean) => { await accumulator.awaitEvent(); const builder = bundleWithTestDocsAndQueries(db); - const progress = await db.loadBundle( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const progress = await (db as any)._loadBundle( // Testing passing in non-string bundles. encoder.encode( builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) @@ -234,10 +244,12 @@ apiDescribe('Bundles', (persistence: boolean) => { // cache can only be tested in spec tests. await accumulator.assertNoAdditionalEvents(); - let snap = await (await db.namedQuery('limit'))!.get(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let snap = await (await (db as any)._namedQuery('limit'))!.get(); expect(toDataArray(snap)).to.deep.equal([{ k: 'b', bar: 0 }]); - snap = await (await db.namedQuery('limit-to-last'))!.get(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + snap = await (await (db as any)._namedQuery('limit-to-last'))!.get(); expect(toDataArray(snap)).to.deep.equal([{ k: 'a', bar: 0 }]); }); }); @@ -246,7 +258,8 @@ apiDescribe('Bundles', (persistence: boolean) => { return withTestDb(persistence, async db => { const builder = bundleWithTestDocsAndQueries(db); - const fulfillProgress: firestore.LoadBundleTaskProgress = await db.loadBundle( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fulfillProgress: firestore.LoadBundleTaskProgress = await (db as any)._loadBundle( builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) ); @@ -270,14 +283,16 @@ apiDescribe('Bundles', (persistence: boolean) => { return withAlternateTestDb(persistence, async otherDb => { // eslint-disable-next-line @typescript-eslint/no-floating-promises await expect( - otherDb.loadBundle( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (otherDb as any)._loadBundle( builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) ) ).to.be.rejectedWith('Tried to deserialize key from different project'); // Verify otherDb still functions, despite loaded a problematic bundle. builder = bundleWithTestDocsAndQueries(otherDb); - const finalProgress = await otherDb.loadBundle( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const finalProgress = await (otherDb as any)._loadBundle( builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) ); verifySuccessProgress(finalProgress); From 259160b5aef831086b78b80daad0584ba9f74e62 Mon Sep 17 00:00:00 2001 From: Wu-Hui Date: Thu, 8 Oct 2020 22:36:11 -0400 Subject: [PATCH 20/27] Fix build error. --- packages/firestore/exp/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/firestore/exp/index.ts b/packages/firestore/exp/index.ts index 22eed40f49f..ff0aef3f379 100644 --- a/packages/firestore/exp/index.ts +++ b/packages/firestore/exp/index.ts @@ -33,8 +33,7 @@ export { enableNetwork, _namedQuery, _loadBundle, - terminate, - Settings + terminate } from './src/api/database'; export { From 9a804fc759e5310b0e57c1b3e5dd52098ed6959b Mon Sep 17 00:00:00 2001 From: Wu-Hui Date: Mon, 12 Oct 2020 20:46:05 -0400 Subject: [PATCH 21/27] Add bundle proto to extern, version upgrade and make exp bundle public --- packages/firebase/package.json | 2 +- packages/firestore/exp-types/index.d.ts | 10 ++++++++++ packages/firestore/exp/index.ts | 4 ++-- packages/firestore/exp/src/api/database.ts | 4 ++-- packages/firestore/exp/test/shim.ts | 4 ++-- packages/firestore/externs.json | 1 + packages/firestore/package.json | 2 +- 7 files changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/firebase/package.json b/packages/firebase/package.json index 718bff1ab3a..0d6028ca7b0 100644 --- a/packages/firebase/package.json +++ b/packages/firebase/package.json @@ -49,7 +49,7 @@ "@firebase/app-types": "0.6.1", "@firebase/auth": "0.14.9", "@firebase/database": "0.6.13", - "@firebase/firestore": "1.17.2", + "@firebase/firestore": "1.18.0", "@firebase/functions": "0.5.1", "@firebase/installations": "0.4.17", "@firebase/messaging": "0.7.1", diff --git a/packages/firestore/exp-types/index.d.ts b/packages/firestore/exp-types/index.d.ts index acf5cd46947..19994191aab 100644 --- a/packages/firestore/exp-types/index.d.ts +++ b/packages/firestore/exp-types/index.d.ts @@ -543,6 +543,16 @@ export interface LoadBundleTaskProgress { export type TaskState = 'Error' | 'Running' | 'Success'; +export function loadBundle( + firestore: FirebaseFirestore, + bundleData: ArrayBuffer | ReadableStream | string +): LoadBundleTask; + +export function namedQuery( + firestore: FirebaseFirestore, + name: string +): Promise | null>; + export type FirestoreErrorCode = | 'cancelled' | 'unknown' diff --git a/packages/firestore/exp/index.ts b/packages/firestore/exp/index.ts index ff0aef3f379..78b78b3961e 100644 --- a/packages/firestore/exp/index.ts +++ b/packages/firestore/exp/index.ts @@ -31,8 +31,8 @@ export { waitForPendingWrites, disableNetwork, enableNetwork, - _namedQuery, - _loadBundle, + namedQuery, + loadBundle, terminate } from './src/api/database'; diff --git a/packages/firestore/exp/src/api/database.ts b/packages/firestore/exp/src/api/database.ts index e8ca999afde..2b193bf00c9 100644 --- a/packages/firestore/exp/src/api/database.ts +++ b/packages/firestore/exp/src/api/database.ts @@ -490,7 +490,7 @@ function verifyNotInitialized(firestore: FirebaseFirestore): void { } } -export function _loadBundle( +export function loadBundle( firestore: FirebaseFirestore, bundleData: ArrayBuffer | ReadableStream | string ): LoadBundleTask { @@ -512,7 +512,7 @@ export function _loadBundle( return resultTask; } -export function _namedQuery( +export function namedQuery( firestore: FirebaseFirestore, name: string ): Promise { diff --git a/packages/firestore/exp/test/shim.ts b/packages/firestore/exp/test/shim.ts index 3733df8368f..f916e178fa4 100644 --- a/packages/firestore/exp/test/shim.ts +++ b/packages/firestore/exp/test/shim.ts @@ -41,7 +41,7 @@ import { getDocsFromCache, getDocsFromServer, initializeFirestore, - _loadBundle, + loadBundle, onSnapshot, onSnapshotsInSync, query, @@ -167,7 +167,7 @@ export class FirebaseFirestore loadBundle( bundleData: ArrayBuffer | ReadableStream | string ): LoadBundleTask { - return _loadBundle(this._delegate, bundleData)!; + return loadBundle(this._delegate, bundleData)!; } async namedQuery(name: string): Promise { diff --git a/packages/firestore/externs.json b/packages/firestore/externs.json index 74f5d2a30bb..06e8519f3c6 100644 --- a/packages/firestore/externs.json +++ b/packages/firestore/externs.json @@ -28,6 +28,7 @@ "packages/webchannel-wrapper/src/index.d.ts", "packages/util/dist/src/crypt.d.ts", "packages/util/dist/src/environment.d.ts", + "packages/firestore/src/protos/firestore_bundle_proto.ts", "packages/firestore/src/protos/firestore_proto_api.d.ts", "packages/firestore/src/util/error.ts", "packages/firestore/src/local/indexeddb_schema.ts", diff --git a/packages/firestore/package.json b/packages/firestore/package.json index d1fd6c9ac2f..a1494c46a91 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/firestore", - "version": "1.17.2", + "version": "1.18.0", "engines": { "node": "^8.13.0 || >=10.10.0" }, From 3b208b22c45c60379021b51962d3048aa3bc4531 Mon Sep 17 00:00:00 2001 From: Wu-Hui Date: Mon, 19 Oct 2020 21:32:12 -0400 Subject: [PATCH 22/27] Fix lint error --- packages/firestore/src/core/component_provider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/firestore/src/core/component_provider.ts b/packages/firestore/src/core/component_provider.ts index 493d952ba9b..bcd0f50e0b3 100644 --- a/packages/firestore/src/core/component_provider.ts +++ b/packages/firestore/src/core/component_provider.ts @@ -205,7 +205,6 @@ export class IndexedDbOfflineComponentProvider extends MemoryOfflineComponentPro this.cacheSizeBytes !== undefined ? LruParams.withCacheSize(this.cacheSizeBytes) : LruParams.DEFAULT; - const serializer = newSerializer(cfg.databaseInfo.databaseId); return new IndexedDbPersistence( this.synchronizeTabs, From 223bc9a5a4170d886417623b32ea9edf0ae7574e Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Tue, 27 Oct 2020 14:00:37 -0400 Subject: [PATCH 23/27] Move bundle integration tests under api/ and complete exp API (#3962) * Rolls a node app building bundles. * Build bundle files for given list of project IDs. * Build the bundle json map and save it for integration tests. * Add emulator_settings.ts to gulp * Move bundle.test.ts to api/ * Bundles passes all tests and expose in classic API * Add CI project ID to bundles. * Adhoc string replacement and length re-calculation * Fix lint errors. * Delete old changes from make node app * Address comments * Manually prepares the bundle strings. * More fixes. --- packages/firebase/index.d.ts | 33 ++++ packages/firestore-types/index.d.ts | 6 + packages/firestore/exp/src/api/database.ts | 18 +- packages/firestore/exp/test/shim.ts | 5 +- packages/firestore/src/api/database.ts | 9 +- .../firestore/src/core/firestore_client.ts | 9 +- packages/firestore/src/core/sync_engine.ts | 3 +- packages/firestore/src/remote/serializer.ts | 35 ++-- .../{api_internal => api}/bundle.test.ts | 180 ++++++------------ .../test/integration/util/internal_helpers.ts | 60 ++++++ 10 files changed, 204 insertions(+), 154 deletions(-) rename packages/firestore/test/integration/{api_internal => api}/bundle.test.ts (57%) diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index c0d71742d59..6cc826565e3 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -8255,12 +8255,45 @@ declare namespace firebase.firestore { */ terminate(): Promise; + loadBundle( + bundleData: ArrayBuffer | ReadableStream | string + ): LoadBundleTask; + + namedQuery(name: string): Promise | null>; + /** * @hidden */ INTERNAL: { delete: () => Promise }; } + export interface LoadBundleTask { + onProgress( + next?: (progress: LoadBundleTaskProgress) => any, + error?: (error: Error) => any, + complete?: () => void + ): void; + + then( + onFulfilled?: (a: LoadBundleTaskProgress) => T | PromiseLike, + onRejected?: (a: Error) => R | PromiseLike + ): Promise; + + catch( + onRejected: (a: Error) => R | PromiseLike + ): Promise; + } + + export interface LoadBundleTaskProgress { + documentsLoaded: number; + totalDocuments: number; + bytesLoaded: number; + totalBytes: number; + taskState: TaskState; + } + + export type TaskState = 'Error' | 'Running' | 'Success'; + /** * An immutable object representing a geo point in Firestore. The geo point * is represented as latitude/longitude pair. diff --git a/packages/firestore-types/index.d.ts b/packages/firestore-types/index.d.ts index d815ea5cad9..b76b5143488 100644 --- a/packages/firestore-types/index.d.ts +++ b/packages/firestore-types/index.d.ts @@ -96,6 +96,12 @@ export class FirebaseFirestore { terminate(): Promise; + loadBundle( + bundleData: ArrayBuffer | ReadableStream | string + ): LoadBundleTask; + + namedQuery(name: string): Promise | null>; + INTERNAL: { delete: () => Promise }; } diff --git a/packages/firestore/exp/src/api/database.ts b/packages/firestore/exp/src/api/database.ts index 35d0118d155..059772fff19 100644 --- a/packages/firestore/exp/src/api/database.ts +++ b/packages/firestore/exp/src/api/database.ts @@ -42,7 +42,7 @@ import { import { Code, FirestoreError } from '../../../src/util/error'; import { Deferred } from '../../../src/util/promise'; import { LruParams } from '../../../src/local/lru_garbage_collector'; -import { CACHE_SIZE_UNLIMITED, Query } from '../../../src/api/database'; +import { CACHE_SIZE_UNLIMITED } from '../../../src/api/database'; import { indexedDbClearPersistence, indexedDbStoragePrefix @@ -74,6 +74,7 @@ import { import { PersistenceSettings } from '../../../exp-types'; import { getNamedQuery } from '../../../src/local/local_store'; import { newSerializer } from '../../../src/platform/serializer'; +import { Query } from '../../../lite/src/api/reference'; const LOG_TAG = 'Firestore'; @@ -578,14 +579,15 @@ export function namedQuery( firestore: FirebaseFirestore, name: string ): Promise { - return getLocalStore(firestore).then(localStore => { - return getNamedQuery(localStore, name).then(namedQuery => { - if (!namedQuery) { - return null; - } + return firestore._queue.enqueue(() => { + return getLocalStore(firestore).then(localStore => { + return getNamedQuery(localStore, name).then(namedQuery => { + if (!namedQuery) { + return null; + } - return null; - // return new Query(namedQuery.query, firestore, null); + return new Query(firestore, null, namedQuery.query); + }); }); }); } diff --git a/packages/firestore/exp/test/shim.ts b/packages/firestore/exp/test/shim.ts index 8ff8368fc20..6f2fb21db87 100644 --- a/packages/firestore/exp/test/shim.ts +++ b/packages/firestore/exp/test/shim.ts @@ -42,6 +42,7 @@ import { getDocsFromServer, initializeFirestore, loadBundle, + namedQuery, onSnapshot, onSnapshotsInSync, query, @@ -175,8 +176,8 @@ export class FirebaseFirestore } async namedQuery(name: string): Promise { - return null; - // return namedQuery(this._delegate, name); + const query = await namedQuery(this._delegate, name); + return !!query ? new Query(this, query) : null; } INTERNAL = { diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 331698d9c3c..16f3d185c1f 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -111,10 +111,7 @@ import { valueDescription, validateIsNotUsedTogether } from '../util/input_validation'; -import { - setLogLevel as setClientLogLevel, - logWarn -} from '../util/log'; +import { setLogLevel as setClientLogLevel, logWarn } from '../util/log'; import { AutoId } from '../util/misc'; import { Deferred } from '../util/promise'; import { FieldPath as ExternalFieldPath } from './field_path'; @@ -627,7 +624,7 @@ export class Firestore implements PublicFirestore, FirebaseService { } } - _loadBundle( + loadBundle( bundleData: ArrayBuffer | ReadableStream | string ): LoadBundleTask { this.ensureClientConfigured(); @@ -636,7 +633,7 @@ export class Firestore implements PublicFirestore, FirebaseService { return resultTask; } - _namedQuery(name: string): Promise { + namedQuery(name: string): Promise { this.ensureClientConfigured(); return this._firestoreClient!.getNamedQuery(name).then(namedQuery => { if (!namedQuery) { diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 38ee4c853f0..3bdda74fe24 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -38,7 +38,7 @@ import { } from '../remote/remote_store'; import { AsyncQueue, wrapInUserErrorIfRecoverable } from '../util/async_queue'; import { Code, FirestoreError } from '../util/error'; -import { logDebug, logWarn } from '../util/log'; +import { logDebug } from '../util/log'; import { Deferred } from '../util/promise'; import { addSnapshotsInSyncListener, @@ -535,15 +535,14 @@ export class FirestoreClient { ); this.asyncQueue.enqueueAndForget(async () => { loadBundle(this.syncEngine, reader, resultTask); - return resultTask.catch(e => { - logWarn(LOG_TAG, `Loading bundle failed with ${e}`); - }); }); } getNamedQuery(queryName: string): Promise { this.verifyNotTerminated(); - return getNamedQuery(this.localStore, queryName); + return this.asyncQueue.enqueue(() => + getNamedQuery(this.localStore, queryName) + ); } } diff --git a/packages/firestore/src/core/sync_engine.ts b/packages/firestore/src/core/sync_engine.ts index 89d17564fd6..2676bd8b6ce 100644 --- a/packages/firestore/src/core/sync_engine.ts +++ b/packages/firestore/src/core/sync_engine.ts @@ -60,7 +60,7 @@ import { } from '../remote/remote_store'; import { debugAssert, debugCast, fail, hardAssert } from '../util/assert'; import { Code, FirestoreError } from '../util/error'; -import { logDebug } from '../util/log'; +import { logDebug, logWarn } from '../util/log'; import { primitiveComparator } from '../util/misc'; import { ObjectMap } from '../util/obj_map'; import { Deferred } from '../util/promise'; @@ -1602,6 +1602,7 @@ async function loadBundleImpl( await saveBundle(syncEngine.localStore, metadata); task._completeWith(result.progress); } catch (e) { + logWarn(LOG_TAG, `Loading bundle failed with ${e}`); task._failWith(e); } } diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 4f19a5c9b50..b9a622bf8a7 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -339,21 +339,26 @@ export function fromName( name: string ): DocumentKey { const resource = fromResourceName(name); - hardAssert( - resource.get(1) === serializer.databaseId.projectId, - 'Tried to deserialize key from different project: ' + - resource.get(1) + - ' vs ' + - serializer.databaseId.projectId - ); - hardAssert( - (!resource.get(3) && !serializer.databaseId.database) || - resource.get(3) === serializer.databaseId.database, - 'Tried to deserialize key from different database: ' + - resource.get(3) + - ' vs ' + - serializer.databaseId.database - ); + + if (resource.get(1) !== serializer.databaseId.projectId) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + 'Tried to deserialize key from different project: ' + + resource.get(1) + + ' vs ' + + serializer.databaseId.projectId + ); + } + + if (resource.get(3) !== serializer.databaseId.database) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + 'Tried to deserialize key from different database: ' + + resource.get(3) + + ' vs ' + + serializer.databaseId.database + ); + } return new DocumentKey(extractLocalPathFromResourceName(resource)); } diff --git a/packages/firestore/test/integration/api_internal/bundle.test.ts b/packages/firestore/test/integration/api/bundle.test.ts similarity index 57% rename from packages/firestore/test/integration/api_internal/bundle.test.ts rename to packages/firestore/test/integration/api/bundle.test.ts index 3cadfef3c2f..efe1fa11398 100644 --- a/packages/firestore/test/integration/api_internal/bundle.test.ts +++ b/packages/firestore/test/integration/api/bundle.test.ts @@ -23,17 +23,11 @@ import { withAlternateTestDb, withTestDb } from '../util/helpers'; -import { DatabaseId } from '../../../src/core/database_info'; -import { key } from '../../util/helpers'; import { EventsAccumulator } from '../util/events_accumulator'; -import { TestBundleBuilder } from '../../unit/util/bundle_data'; -import { newTextEncoder } from '../../../src/platform/serializer'; -import { collectionReference } from '../../util/api_helpers'; -import { LoadBundleTask } from '../../../src/api/bundle'; // TODO(b/162594908): Move this to api/ instead of api_internal. -export const encoder = newTextEncoder(); +export const encoder = new TextEncoder(); function verifySuccessProgress(p: firestore.LoadBundleTaskProgress): void { expect(p.taskState).to.equal('Success'); @@ -51,74 +45,57 @@ function verifyInProgress( expect(p.documentsLoaded).to.equal(expectedDocuments); } -apiDescribe('Bundles', (persistence: boolean) => { - const testDocs: { [key: string]: firestore.DocumentData } = { - a: { k: { stringValue: 'a' }, bar: { integerValue: 1 } }, - b: { k: { stringValue: 'b' }, bar: { integerValue: 2 } } - }; - - function bundleWithTestDocsAndQueries( - db: firestore.FirebaseFirestore - ): TestBundleBuilder { - const a = key('coll-1/a'); - const b = key('coll-1/b'); - const builder = new TestBundleBuilder( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (db as any)._databaseId as DatabaseId - ); - - builder.addNamedQuery( - 'limit', - { seconds: 1000, nanos: 9999 }, - (collectionReference('coll-1') - .orderBy('bar', 'desc') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .limit(1) as any)._query - ); - builder.addNamedQuery( - 'limit-to-last', - { seconds: 1000, nanos: 9999 }, - (collectionReference('coll-1') - .orderBy('bar', 'desc') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .limitToLast(1) as any)._query - ); - - builder.addDocumentMetadata(a, { seconds: 1000, nanos: 9999 }, true); - builder.addDocument( - a, - { seconds: 1, nanos: 9 }, - { seconds: 1, nanos: 9 }, - testDocs.a - ); - builder.addDocumentMetadata(b, { seconds: 1000, nanos: 9999 }, true); - builder.addDocument( - b, - { seconds: 1, nanos: 9 }, - { seconds: 1, nanos: 9 }, - testDocs.b - ); - - return builder; - } +// This template is generated from bundleWithTestDocsAndQueries in '../util/internal_helpsers.ts', +// and manually copied here. +const BUNDLE_TEMPLATE = [ + '{"metadata":{"id":"test-bundle","createTime":{"seconds":1001,"nanos":9999},"version":1,"totalDocuments":2,"totalBytes":1503}}', + '{"namedQuery":{"name":"limit","readTime":{"seconds":1000,"nanos":9999},"bundledQuery":{"parent":"projects/{0}/databases/(default)/documents","structuredQuery":{"from":[{"collectionId":"coll-1"}],"orderBy":[{"field":{"fieldPath":"bar"},"direction":"DESCENDING"},{"field":{"fieldPath":"__name__"},"direction":"DESCENDING"}],"limit":{"value":1}},"limitType":"FIRST"}}}', + '{"namedQuery":{"name":"limit-to-last","readTime":{"seconds":1000,"nanos":9999},"bundledQuery":{"parent":"projects/{0}/databases/(default)/documents","structuredQuery":{"from":[{"collectionId":"coll-1"}],"orderBy":[{"field":{"fieldPath":"bar"},"direction":"DESCENDING"},{"field":{"fieldPath":"__name__"},"direction":"DESCENDING"}],"limit":{"value":1}},"limitType":"LAST"}}}', + '{"documentMetadata":{"name":"projects/{0}/databases/(default)/documents/coll-1/a","readTime":{"seconds":1000,"nanos":9999},"exists":true}}', + '{"document":{"name":"projects/{0}/databases/(default)/documents/coll-1/a","createTime":{"seconds":1,"nanos":9},"updateTime":{"seconds":1,"nanos":9},"fields":{"k":{"stringValue":"a"},"bar":{"integerValue":1}}}}', + '{"documentMetadata":{"name":"projects/{0}/databases/(default)/documents/coll-1/b","readTime":{"seconds":1000,"nanos":9999},"exists":true}}', + '{"document":{"name":"projects/{0}/databases/(default)/documents/coll-1/b","createTime":{"seconds":1,"nanos":9},"updateTime":{"seconds":1,"nanos":9},"fields":{"k":{"stringValue":"b"},"bar":{"integerValue":2}}}}' +]; - function verifySnapEqualTestDocs(snap: firestore.QuerySnapshot): void { +apiDescribe('Bundles', (persistence: boolean) => { + function verifySnapEqualsTestDocs(snap: firestore.QuerySnapshot): void { expect(toDataArray(snap)).to.deep.equal([ { k: 'a', bar: 1 }, { k: 'b', bar: 2 } ]); } + /** + * Returns a valid bundle string from replacing project id in `BUNDLE_TEMPLATE` with the given + * db project id (also recalculate length prefixes). + */ + function bundleString(db: firestore.FirebaseFirestore): string { + const projectId: string = db.app.options.projectId; + + // Extract elements from BUNDLE_TEMPLATE and replace the project ID. + const elements = BUNDLE_TEMPLATE.map(e => e.replace('{0}', projectId)); + + // Recalculating length prefixes for elements that are not BundleMetadata. + let bundleContent = ''; + for (const element of elements.slice(1)) { + const length = encoder.encode(element).byteLength; + bundleContent += `${length}${element}`; + } + + // Update BundleMetadata with new totalBytes. + const totalBytes = encoder.encode(bundleContent).byteLength; + const metadata = JSON.parse(elements[0]); + metadata.metadata.totalBytes = totalBytes; + const metadataContent = JSON.stringify(metadata); + const metadataLength = encoder.encode(metadataContent).byteLength; + return `${metadataLength}${metadataContent}${bundleContent}`; + } + it('load with documents only with on progress and promise interface', () => { return withTestDb(persistence, async db => { - const builder = bundleWithTestDocsAndQueries(db); - const progressEvents: firestore.LoadBundleTaskProgress[] = []; let completeCalled = false; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const task: LoadBundleTask = (db as any)._loadBundle( - builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) - ); + const task: firestore.LoadBundleTask = db.loadBundle(bundleString(db)); task.onProgress( progress => { progressEvents.push(progress); @@ -145,16 +122,14 @@ apiDescribe('Bundles', (persistence: boolean) => { // Read from cache. These documents do not exist in backend, so they can // only be read from cache. let snap = await db.collection('coll-1').get({ source: 'cache' }); - verifySnapEqualTestDocs(snap); + verifySnapEqualsTestDocs(snap); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - snap = await (await (db as any)._namedQuery('limit'))!.get({ + snap = await (await db.namedQuery('limit'))!.get({ source: 'cache' }); expect(toDataArray(snap)).to.deep.equal([{ k: 'b', bar: 2 }]); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - snap = await (await (db as any)._namedQuery('limit-to-last'))!.get({ + snap = await (await db.namedQuery('limit-to-last'))!.get({ source: 'cache' }); expect(toDataArray(snap)).to.deep.equal([{ k: 'a', bar: 1 }]); @@ -163,11 +138,8 @@ apiDescribe('Bundles', (persistence: boolean) => { it('load with documents and queries with promise interface', () => { return withTestDb(persistence, async db => { - const builder = bundleWithTestDocsAndQueries(db); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const fulfillProgress: firestore.LoadBundleTaskProgress = await (db as any)._loadBundle( - builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) + const fulfillProgress: firestore.LoadBundleTaskProgress = await db.loadBundle( + bundleString(db) ); verifySuccessProgress(fulfillProgress!); @@ -175,26 +147,18 @@ apiDescribe('Bundles', (persistence: boolean) => { // Read from cache. These documents do not exist in backend, so they can // only be read from cache. const snap = await db.collection('coll-1').get({ source: 'cache' }); - verifySnapEqualTestDocs(snap); + verifySnapEqualsTestDocs(snap); }); }); it('load for a second time skips', () => { return withTestDb(persistence, async db => { - const builder = bundleWithTestDocsAndQueries(db); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (db as any)._loadBundle( - builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) - ); + await db.loadBundle(bundleString(db)); let completeCalled = false; const progressEvents: firestore.LoadBundleTaskProgress[] = []; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const task: LoadBundleTask = (db as any)._loadBundle( - encoder.encode( - builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) - ) + const task: firestore.LoadBundleTask = db.loadBundle( + encoder.encode(bundleString(db)) ); task.onProgress( progress => { @@ -216,7 +180,7 @@ apiDescribe('Bundles', (persistence: boolean) => { // Read from cache. These documents do not exist in backend, so they can // only be read from cache. const snap = await db.collection('coll-1').get({ source: 'cache' }); - verifySnapEqualTestDocs(snap); + verifySnapEqualsTestDocs(snap); }); }); @@ -229,13 +193,9 @@ apiDescribe('Bundles', (persistence: boolean) => { db.collection('coll-1').onSnapshot(accumulator.storeEvent); await accumulator.awaitEvent(); - const builder = bundleWithTestDocsAndQueries(db); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const progress = await (db as any)._loadBundle( + const progress = await db.loadBundle( // Testing passing in non-string bundles. - encoder.encode( - builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) - ) + encoder.encode(bundleString(db)) ); verifySuccessProgress(progress); @@ -244,23 +204,18 @@ apiDescribe('Bundles', (persistence: boolean) => { // cache can only be tested in spec tests. await accumulator.assertNoAdditionalEvents(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let snap = await (await (db as any)._namedQuery('limit'))!.get(); + let snap = await (await db.namedQuery('limit'))!.get(); expect(toDataArray(snap)).to.deep.equal([{ k: 'b', bar: 0 }]); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - snap = await (await (db as any)._namedQuery('limit-to-last'))!.get(); + snap = await (await db.namedQuery('limit-to-last'))!.get(); expect(toDataArray(snap)).to.deep.equal([{ k: 'a', bar: 0 }]); }); }); it('loaded documents should not be GC-ed right away', () => { return withTestDb(persistence, async db => { - const builder = bundleWithTestDocsAndQueries(db); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const fulfillProgress: firestore.LoadBundleTaskProgress = await (db as any)._loadBundle( - builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) + const fulfillProgress: firestore.LoadBundleTaskProgress = await db.loadBundle( + bundleString(db) ); verifySuccessProgress(fulfillProgress!); @@ -273,28 +228,19 @@ apiDescribe('Bundles', (persistence: boolean) => { // GC, the documents would get GC-ed if we did not hold the document keys // in a "umbrella" target. See local_store.ts for details. snap = await db.collection('coll-1').get({ source: 'cache' }); - verifySnapEqualTestDocs(snap); + verifySnapEqualsTestDocs(snap); }); }); it('load with documents from other projects fails', () => { return withTestDb(persistence, async db => { - let builder = bundleWithTestDocsAndQueries(db); return withAlternateTestDb(persistence, async otherDb => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - await expect( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (otherDb as any)._loadBundle( - builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) - ) - ).to.be.rejectedWith('Tried to deserialize key from different project'); + await expect(otherDb.loadBundle(bundleString(db))).to.be.rejectedWith( + 'Tried to deserialize key from different project' + ); // Verify otherDb still functions, despite loaded a problematic bundle. - builder = bundleWithTestDocsAndQueries(otherDb); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const finalProgress = await (otherDb as any)._loadBundle( - builder.build('test-bundle', { seconds: 1001, nanos: 9999 }) - ); + const finalProgress = await otherDb.loadBundle(bundleString(otherDb)); verifySuccessProgress(finalProgress); // Read from cache. These documents do not exist in backend, so they can @@ -302,7 +248,7 @@ apiDescribe('Bundles', (persistence: boolean) => { const snap = await otherDb .collection('coll-1') .get({ source: 'cache' }); - verifySnapEqualTestDocs(snap); + verifySnapEqualsTestDocs(snap); }); }); }); diff --git a/packages/firestore/test/integration/util/internal_helpers.ts b/packages/firestore/test/integration/util/internal_helpers.ts index b6f01e9df81..7ccb25513a9 100644 --- a/packages/firestore/test/integration/util/internal_helpers.ts +++ b/packages/firestore/test/integration/util/internal_helpers.ts @@ -32,6 +32,9 @@ import { User } from '../../../src/auth/user'; import { DEFAULT_PROJECT_ID, DEFAULT_SETTINGS } from './settings'; import { newConnection } from '../../../src/platform/connection'; import { newSerializer } from '../../../src/platform/serializer'; +import { key } from '../../util/helpers'; +import { TestBundleBuilder } from '../../unit/util/bundle_data'; +import { collectionReference } from '../../util/api_helpers'; /** Helper to retrieve the AsyncQueue for a give FirebaseFirestore instance. */ export function asyncQueue(db: firestore.FirebaseFirestore): AsyncQueue { @@ -95,3 +98,60 @@ export function withMockCredentialProviderTestDb( } ); } + +/** + * Returns a testing bundle string for the given projectId. + * + * The function is not referenced by bundle.test.ts, instead the bundle string used there + * is generated by this function and copied over there. The reason is this function accesses + * SDK internals, which is not available in test:minified. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function bundleWithTestDocsAndQueries( + projectId: string = 'test-project' +): string { + const testDocs: { [key: string]: firestore.DocumentData } = { + a: { k: { stringValue: 'a' }, bar: { integerValue: 1 } }, + b: { k: { stringValue: 'b' }, bar: { integerValue: 2 } } + }; + + const a = key('coll-1/a'); + const b = key('coll-1/b'); + const builder = new TestBundleBuilder(new DatabaseId(projectId)); + + builder.addNamedQuery( + 'limit', + { seconds: 1000, nanos: 9999 }, + (collectionReference('coll-1') + .orderBy('bar', 'desc') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .limit(1) as any)._query + ); + builder.addNamedQuery( + 'limit-to-last', + { seconds: 1000, nanos: 9999 }, + (collectionReference('coll-1') + .orderBy('bar', 'desc') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .limitToLast(1) as any)._query + ); + + builder.addDocumentMetadata(a, { seconds: 1000, nanos: 9999 }, true); + builder.addDocument( + a, + { seconds: 1, nanos: 9 }, + { seconds: 1, nanos: 9 }, + testDocs.a + ); + builder.addDocumentMetadata(b, { seconds: 1000, nanos: 9999 }, true); + builder.addDocument( + b, + { seconds: 1, nanos: 9 }, + { seconds: 1, nanos: 9 }, + testDocs.b + ); + + return builder + .build('test-bundle', { seconds: 1001, nanos: 9999 }) + .toString(); +} From 5b0526bd2ff9f2ce37af731ab33e2ac9be78109b Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Mon, 9 Nov 2020 09:50:58 -0500 Subject: [PATCH 24/27] Bundles as free functions (#4006) * Rolls a node app building bundles. * Build bundle files for given list of project IDs. * Build the bundle json map and save it for integration tests. * Add emulator_settings.ts to gulp * Move bundle.test.ts to api/ * Bundles passes all tests and expose in classic API * Add CI project ID to bundles. * Adhoc string replacement and length re-calculation * Fix lint errors. * Delete old changes from make node app * Address comments * Update yarn.lock for release (#3998) Temp fix where database version is bumped before firebase-admin can update deps * Manually prepares the bundle strings. * Update API * Update config.ts * Use Chrome for karma debugging (#4007) * Cache emulator between runs (#3956) * Remote Config Modularization (#3975) * rc exp init * Add apis * register rc exp * implement funcitonal APIs * fix tests * build rc exp * add api-extractor to rc types * cast directly witout function * delete changelog for rc exp * add code owners to rc exp * update dep version * Remove AuthErrorCode from core export (#4013) * Remove AuthErrorCode from core export * Api * Update config.ts to remove bundles * adds a root changelog (#4009) * adds a root changelog * Update CHANGELOG.md Co-authored-by: Feiyang * Update dependency typescript to v4.0.5 (#3846) Co-authored-by: Renovate Bot * Update dependency karma-firefox-launcher to v2 (#3987) Co-authored-by: Renovate Bot * Update dependency google-closure-library to v20200830 (#3765) * Update dependency google-closure-library to v20200830 * Replace goog.isArray with Array.isArray https://github.com/google/closure-library/releases/tag/v20200628 Co-authored-by: Renovate Bot Co-authored-by: Alex Volkovitsky * exclude remote config exp packages in changeset (#4014) * Set 1s timeout for onBackgroundMessage Hook (#3780) * await onBackgroundMessage hook * Create fluffy-panthers-hide.md * block in onPush to let onBackgroundMessage to execute * polish wording * Update changeset to be more specific * Update fluffy-panthers-hide.md * Clarify PR is about a bug fix * Update fluffy-panthers-hide.md * A whole bunch of things to bring auth (compat) to parity (#3970) * Handle anonymous auth re-login edge case * Formatting * Initial fixes * Fix redirect * clean up additional user info * Formatting * PR feedback * Fix some tests * Fix tests & write some new ones * Fix broken build * Formatting * Formatting * PR feedback * Formatting * PR feedback Co-authored-by: avolkovi * Add withFunctionsTriggersDisabled method to rules-unit-testing (#3928) * Update integration tests to use free functions * Functions compat package (#3739) * Add free functions to exports * Fix to avoid false failures on changeset checker (#4012) * Add changeset for Firestore (#4030) * Update functions-compat dep version and fix changeset script error (#4032) * Update integration tests. Minified tests fail. * Bump node memory limit for all test CI (#4035) * Compat Layer for Firestore (#4003) * Rename all public API types to PublicX (#4039) * Update all non-major dependencies (#3953) Co-authored-by: Renovate Bot * Version Packages (#4033) Co-authored-by: github-actions[bot] * Set up Storage modularization (#3499) Refactor storage for modularization. * Free functions removed from exp database.ts Co-authored-by: Christina Holland Co-authored-by: Sebastian Schmidt Co-authored-by: Sam Stern Co-authored-by: Feiyang Co-authored-by: Sam Horlbeck Olsen Co-authored-by: Dimitri Mitropoulos Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Renovate Bot Co-authored-by: Alex Volkovitsky Co-authored-by: Kai Wu Co-authored-by: Google Open Source Bot <26440463+google-oss-bot@users.noreply.github.com> Co-authored-by: github-actions[bot] --- .changeset/config.json | 3 + .changeset/fuzzy-impalas-brake.md | 5 - .changeset/silly-boats-roll.md | 5 + .github/CODEOWNERS | 6 +- .github/workflows/check-changeset.yml | 4 +- .github/workflows/test-all.yml | 2 + .github/workflows/test-changed-auth.yml | 2 + .../test-changed-fcm-integration.yml | 2 + .../test-changed-firestore-integration.yml | 4 +- .github/workflows/test-changed-firestore.yml | 4 +- .github/workflows/test-changed-misc.yml | 2 + .github/workflows/test-changed.yml | 2 + .../workflows/test-firebase-integration.yml | 2 + CHANGELOG.md | 5 + common/api-review/auth-exp.api.md | 190 --- common/api-review/remote-config-exp.api.md | 48 + config/.eslintrc.js | 2 +- config/functions/package.json | 2 +- config/webpack.test.js | 6 +- integration/firebase/package.json | 8 +- integration/firestore/firebase_export.ts | 13 +- .../firestore/firebase_export_memory.ts | 13 +- integration/firestore/package.json | 10 +- integration/messaging/package.json | 2 +- package.json | 38 +- packages-exp/app-compat/package.json | 10 +- packages-exp/app-exp/package.json | 10 +- packages-exp/app-exp/src/constants.ts | 2 + packages-exp/app-types-exp/package.json | 2 +- .../auth-compat-exp/demo/package.json | 4 +- packages-exp/auth-compat-exp/demo/yarn.lock | 122 +- packages-exp/auth-compat-exp/package.json | 10 +- .../auth-compat-exp/src/user_credential.ts | 6 +- packages-exp/auth-exp/demo/package.json | 6 +- packages-exp/auth-exp/package.json | 10 +- .../api/account_management/profile.test.ts | 3 +- .../src/api/account_management/profile.ts | 1 + .../api/authentication/custom_token.test.ts | 3 +- .../src/api/authentication/custom_token.ts | 1 + .../auth-exp/src/core/auth/auth_impl.ts | 87 +- .../src/core/auth/firebase_internal.test.ts | 1 + .../auth-exp/src/core/auth/initialize.test.ts | 7 + packages-exp/auth-exp/src/core/index.ts | 2 - .../src/core/providers/google.test.ts | 6 + .../auth-exp/src/core/providers/google.ts | 1 + .../src/core/strategies/credential.test.ts | 12 +- .../src/core/strategies/credential.ts | 8 +- .../src/core/strategies/custom_token.test.ts | 14 +- .../src/core/strategies/custom_token.ts | 3 +- .../auth-exp/src/core/strategies/idp.test.ts | 60 + .../auth-exp/src/core/strategies/idp.ts | 12 +- .../src/core/user/account_info.test.ts | 9 +- .../auth-exp/src/core/user/account_info.ts | 12 +- .../core/user/additional_user_info.test.ts | 17 +- .../src/core/user/additional_user_info.ts | 4 +- .../src/core/user/invalidation.test.ts | 8 + .../auth-exp/src/core/user/invalidation.ts | 6 +- .../auth-exp/src/core/user/link_unlink.ts | 6 +- .../auth-exp/src/core/user/reauthenticate.ts | 6 +- .../auth-exp/src/core/user/reload.test.ts | 52 + packages-exp/auth-exp/src/core/user/reload.ts | 18 +- .../auth-exp/src/core/user/token_manager.ts | 19 +- .../auth-exp/src/model/popup_redirect.ts | 7 + .../src/platform_browser/auth.test.ts | 44 + .../src/platform_browser/popup_redirect.ts | 3 + .../abstract_popup_redirect_operation.test.ts | 22 +- .../abstract_popup_redirect_operation.ts | 6 +- .../platform_browser/strategies/phone.test.ts | 3 +- .../strategies/redirect.test.ts | 27 +- .../platform_browser/strategies/redirect.ts | 26 +- .../helpers/mock_popup_redirect_resolver.ts | 2 + .../test/integration/flows/phone.test.ts | 3 +- packages-exp/auth-types-exp/package.json | 2 +- packages-exp/firebase-exp/package.json | 8 +- packages-exp/functions-compat/.eslintrc.js | 37 + packages-exp/functions-compat/karma.conf.js | 35 + packages-exp/functions-compat/package.json | 62 + .../functions-compat/rollup.config.base.js | 100 ++ .../functions-compat/rollup.config.js | 21 + .../functions-compat/rollup.config.release.js | 25 + .../functions-compat/src/callable.test.ts | 146 +++ .../functions-compat/src/index.node.ts | 23 + packages-exp/functions-compat/src/index.ts | 23 + packages-exp/functions-compat/src/register.ts | 64 + .../functions-compat/src/service.test.ts | 95 ++ packages-exp/functions-compat/src/service.ts | 81 ++ packages-exp/functions-compat/test/utils.ts | 38 + packages-exp/functions-compat/tsconfig.json | 9 + packages-exp/functions-exp/package.json | 11 +- packages-exp/functions-types-exp/package.json | 2 +- packages-exp/installations-exp/package.json | 10 +- .../installations-types-exp/package.json | 2 +- packages-exp/performance-exp/package.json | 10 +- .../performance-types-exp/package.json | 2 +- packages-exp/remote-config-exp/.eslintrc.js | 26 + packages-exp/remote-config-exp/.npmignore | 1 + packages-exp/remote-config-exp/README.md | 27 + .../remote-config-exp/api-extractor.json | 8 + packages-exp/remote-config-exp/karma.conf.js | 36 + packages-exp/remote-config-exp/package.json | 64 + .../remote-config-exp/rollup.config.js | 58 + .../rollup.config.release.js | 73 ++ .../remote-config-exp/rollup.shared.js | 53 + packages-exp/remote-config-exp/src/api.ts | 172 +++ packages-exp/remote-config-exp/src/api2.ts | 28 + .../src/client/caching_client.ts | 123 ++ .../src/client/remote_config_fetch_client.ts | 139 +++ .../src/client/rest_client.ts | 176 +++ .../src/client/retrying_client.ts | 144 +++ .../remote-config-exp/src/constants.ts | 18 + packages-exp/remote-config-exp/src/errors.ts | 97 ++ packages-exp/remote-config-exp/src/index.ts | 35 + .../remote-config-exp/src/language.ts | 38 + .../remote-config-exp/src/register.ts | 121 ++ .../remote-config-exp/src/remote_config.ts | 88 ++ .../remote-config-exp/src/storage/storage.ts | 260 ++++ .../src/storage/storage_cache.ts | 99 ++ packages-exp/remote-config-exp/src/value.ts | 57 + .../test/client/caching_client.test.ts | 160 +++ .../test/client/rest_client.test.ts | 270 ++++ .../test/client/retrying_client.test.ts | 218 ++++ .../remote-config-exp/test/errors.test.ts | 31 + .../remote-config-exp/test/language.test.ts | 44 + .../test/remote_config.test.ts | 522 ++++++++ packages-exp/remote-config-exp/test/setup.ts | 26 + .../test/storage/storage.test.ts | 119 ++ .../test/storage/storage_cache.test.ts | 84 ++ .../remote-config-exp/test/value.test.ts | 78 ++ .../remote-config-exp/test_app/index.html | 154 +++ .../remote-config-exp/test_app/index.js | 224 ++++ packages-exp/remote-config-exp/tsconfig.json | 10 + .../remote-config-types-exp/README.md | 3 + .../api-extractor.json | 5 + .../remote-config-types-exp/index.d.ts | 120 ++ .../remote-config-types-exp/package.json | 29 + .../remote-config-types-exp/tsconfig.json | 9 + packages/analytics-interop-types/package.json | 2 +- packages/analytics-types/package.json | 2 +- packages/analytics/CHANGELOG.md | 9 + packages/analytics/package.json | 16 +- packages/app-types/package.json | 2 +- packages/app/CHANGELOG.md | 8 + packages/app/package.json | 12 +- packages/auth-interop-types/package.json | 2 +- packages/auth-types/package.json | 2 +- packages/auth/demo/functions/package.json | 2 +- packages/auth/demo/functions/yarn.lock | 8 +- packages/auth/package.json | 4 +- packages/auth/src/rpchandler.js | 2 +- packages/component/CHANGELOG.md | 7 + packages/component/package.json | 10 +- packages/database-types/package.json | 2 +- packages/database/CHANGELOG.md | 8 + packages/database/package.json | 14 +- packages/firebase/CHANGELOG.md | 17 + packages/firebase/index.d.ts | 16 +- packages/firebase/package.json | 30 +- packages/firestore-types/index.d.ts | 16 +- packages/firestore-types/package.json | 2 +- packages/firestore/CHANGELOG.md | 10 + packages/firestore/exp/index.ts | 2 - packages/firestore/exp/src/api/database.ts | 55 +- packages/firestore/exp/test/shim.ts | 181 +-- packages/firestore/index.console.ts | 44 +- packages/firestore/index.memory.ts | 8 +- packages/firestore/index.node.memory.ts | 8 +- packages/firestore/index.node.ts | 13 +- packages/firestore/index.rn.memory.ts | 8 +- packages/firestore/index.rn.ts | 13 +- packages/firestore/index.ts | 13 +- packages/firestore/lite/src/api/components.ts | 2 +- packages/firestore/lite/src/api/database.ts | 82 +- packages/firestore/lite/src/api/reference.ts | 2 +- packages/firestore/package.json | 18 +- packages/firestore/register-module.ts | 2 + packages/firestore/src/api/database.ts | 544 ++++---- .../firestore/src/api/user_data_reader.ts | 35 +- packages/firestore/src/config.ts | 4 + packages/firestore/src/core/database_info.ts | 9 - .../test/integration/api/bundle.test.ts | 32 +- .../test/integration/api/database.test.ts | 11 +- .../test/integration/api/type.test.ts | 6 +- .../test/integration/api/validation.test.ts | 161 +-- .../integration/api_internal/database.test.ts | 2 +- .../test/integration/util/firebase_export.ts | 84 +- .../test/integration/util/internal_helpers.ts | 2 +- .../firestore/test/unit/api/database.test.ts | 35 +- packages/firestore/test/util/api_helpers.ts | 27 +- packages/functions-types/package.json | 2 +- packages/functions/CHANGELOG.md | 7 + packages/functions/package.json | 14 +- packages/installations-types/package.json | 2 +- packages/installations/CHANGELOG.md | 8 + packages/installations/package.json | 14 +- packages/logger/package.json | 6 +- packages/messaging-types/package.json | 2 +- packages/messaging/CHANGELOG.md | 11 + packages/messaging/package.json | 18 +- .../src/controllers/sw-controller.ts | 15 +- packages/messaging/src/util/constants.ts | 19 +- packages/performance-types/package.json | 2 +- packages/performance/CHANGELOG.md | 9 + packages/performance/package.json | 16 +- packages/polyfill/package.json | 6 +- packages/remote-config-types/package.json | 2 +- packages/remote-config/CHANGELOG.md | 9 + packages/remote-config/package.json | 16 +- packages/rules-unit-testing/CHANGELOG.md | 12 + packages/rules-unit-testing/firebase.json | 6 + packages/rules-unit-testing/index.ts | 3 +- packages/rules-unit-testing/package.json | 19 +- packages/rules-unit-testing/src/api/index.ts | 178 ++- .../rules-unit-testing/test/database.test.ts | 49 +- packages/rxfire/package.json | 8 +- packages/storage-types/package.json | 2 +- packages/storage/.eslintrc.js | 6 +- packages/storage/CHANGELOG.md | 8 + packages/storage/api-extractor.json | 10 + packages/storage/compat/index.ts | 109 ++ packages/storage/compat/list.ts | 42 + packages/storage/compat/reference.ts | 215 ++++ packages/storage/compat/service.ts | 89 ++ packages/storage/compat/task.ts | 107 ++ packages/storage/compat/tasksnapshot.ts | 43 + packages/storage/exp/index.ts | 78 ++ packages/storage/exp/package.json | 12 + packages/storage/karma.conf.js | 2 - packages/storage/package.json | 28 +- packages/storage/rollup.config.compat.js | 77 ++ packages/storage/rollup.config.exp.js | 68 + .../storage/src/implementation/backoff.ts | 6 - packages/storage/src/implementation/blob.ts | 2 +- packages/storage/src/implementation/error.ts | 32 +- .../storage/src/implementation/failrequest.ts | 2 - packages/storage/src/implementation/fs.ts | 6 +- .../storage/src/implementation/location.ts | 10 +- .../storage/src/implementation/metadata.ts | 7 +- .../storage/src/implementation/observer.ts | 51 +- .../storage/src/implementation/request.ts | 17 +- .../storage/src/implementation/requests.ts | 1 - packages/storage/src/implementation/string.ts | 26 +- .../storage/src/implementation/taskenums.ts | 3 - packages/storage/src/implementation/type.ts | 7 - packages/storage/src/implementation/url.ts | 1 - packages/storage/src/implementation/xhrio.ts | 6 +- .../src/implementation/xhrio_network.ts | 25 +- packages/storage/src/list.ts | 16 +- packages/storage/src/metadata.ts | 16 +- packages/storage/src/reference.ts | 506 ++++---- packages/storage/src/service.ts | 327 ++--- packages/storage/src/task.ts | 482 ++++---- packages/storage/src/tasksnapshot.ts | 2 +- .../test/integration/integration.test.ts | 2 +- ...rence.test.ts => reference.compat.test.ts} | 219 ++-- .../storage/test/unit/reference.exp.test.ts | 306 +++++ ...service.test.ts => service.compat.test.ts} | 150 +-- .../storage/test/unit/service.exp.test.ts | 321 +++++ packages/storage/test/unit/task.test.ts | 77 +- packages/storage/test/unit/testshared.ts | 9 +- packages/storage/test/unit/xhrio.ts | 11 +- packages/template-types/package.json | 2 +- packages/template/package.json | 8 +- packages/util/CHANGELOG.md | 6 + packages/util/package.json | 8 +- packages/webchannel-wrapper/package.json | 8 +- repo-scripts/changelog-generator/package.json | 2 +- repo-scripts/size-analysis/package.json | 18 +- scripts/check_changeset.ts | 5 +- scripts/ci-test/build_changed.ts | 23 + .../emulators/database-emulator.ts | 2 +- .../emulator-testing/emulators/emulator.ts | 38 +- .../emulators/firestore-emulator.ts | 2 +- yarn.lock | 1099 +++++++++-------- 273 files changed, 9757 insertions(+), 3001 deletions(-) delete mode 100644 .changeset/fuzzy-impalas-brake.md create mode 100644 .changeset/silly-boats-roll.md create mode 100644 CHANGELOG.md create mode 100644 common/api-review/remote-config-exp.api.md create mode 100644 packages-exp/functions-compat/.eslintrc.js create mode 100644 packages-exp/functions-compat/karma.conf.js create mode 100644 packages-exp/functions-compat/package.json create mode 100644 packages-exp/functions-compat/rollup.config.base.js create mode 100644 packages-exp/functions-compat/rollup.config.js create mode 100644 packages-exp/functions-compat/rollup.config.release.js create mode 100644 packages-exp/functions-compat/src/callable.test.ts create mode 100644 packages-exp/functions-compat/src/index.node.ts create mode 100644 packages-exp/functions-compat/src/index.ts create mode 100644 packages-exp/functions-compat/src/register.ts create mode 100644 packages-exp/functions-compat/src/service.test.ts create mode 100644 packages-exp/functions-compat/src/service.ts create mode 100644 packages-exp/functions-compat/test/utils.ts create mode 100644 packages-exp/functions-compat/tsconfig.json create mode 100644 packages-exp/remote-config-exp/.eslintrc.js create mode 100644 packages-exp/remote-config-exp/.npmignore create mode 100644 packages-exp/remote-config-exp/README.md create mode 100644 packages-exp/remote-config-exp/api-extractor.json create mode 100644 packages-exp/remote-config-exp/karma.conf.js create mode 100644 packages-exp/remote-config-exp/package.json create mode 100644 packages-exp/remote-config-exp/rollup.config.js create mode 100644 packages-exp/remote-config-exp/rollup.config.release.js create mode 100644 packages-exp/remote-config-exp/rollup.shared.js create mode 100644 packages-exp/remote-config-exp/src/api.ts create mode 100644 packages-exp/remote-config-exp/src/api2.ts create mode 100644 packages-exp/remote-config-exp/src/client/caching_client.ts create mode 100644 packages-exp/remote-config-exp/src/client/remote_config_fetch_client.ts create mode 100644 packages-exp/remote-config-exp/src/client/rest_client.ts create mode 100644 packages-exp/remote-config-exp/src/client/retrying_client.ts create mode 100644 packages-exp/remote-config-exp/src/constants.ts create mode 100644 packages-exp/remote-config-exp/src/errors.ts create mode 100644 packages-exp/remote-config-exp/src/index.ts create mode 100644 packages-exp/remote-config-exp/src/language.ts create mode 100644 packages-exp/remote-config-exp/src/register.ts create mode 100644 packages-exp/remote-config-exp/src/remote_config.ts create mode 100644 packages-exp/remote-config-exp/src/storage/storage.ts create mode 100644 packages-exp/remote-config-exp/src/storage/storage_cache.ts create mode 100644 packages-exp/remote-config-exp/src/value.ts create mode 100644 packages-exp/remote-config-exp/test/client/caching_client.test.ts create mode 100644 packages-exp/remote-config-exp/test/client/rest_client.test.ts create mode 100644 packages-exp/remote-config-exp/test/client/retrying_client.test.ts create mode 100644 packages-exp/remote-config-exp/test/errors.test.ts create mode 100644 packages-exp/remote-config-exp/test/language.test.ts create mode 100644 packages-exp/remote-config-exp/test/remote_config.test.ts create mode 100644 packages-exp/remote-config-exp/test/setup.ts create mode 100644 packages-exp/remote-config-exp/test/storage/storage.test.ts create mode 100644 packages-exp/remote-config-exp/test/storage/storage_cache.test.ts create mode 100644 packages-exp/remote-config-exp/test/value.test.ts create mode 100644 packages-exp/remote-config-exp/test_app/index.html create mode 100644 packages-exp/remote-config-exp/test_app/index.js create mode 100644 packages-exp/remote-config-exp/tsconfig.json create mode 100644 packages-exp/remote-config-types-exp/README.md create mode 100644 packages-exp/remote-config-types-exp/api-extractor.json create mode 100644 packages-exp/remote-config-types-exp/index.d.ts create mode 100644 packages-exp/remote-config-types-exp/package.json create mode 100644 packages-exp/remote-config-types-exp/tsconfig.json create mode 100644 packages/storage/api-extractor.json create mode 100644 packages/storage/compat/index.ts create mode 100644 packages/storage/compat/list.ts create mode 100644 packages/storage/compat/reference.ts create mode 100644 packages/storage/compat/service.ts create mode 100644 packages/storage/compat/task.ts create mode 100644 packages/storage/compat/tasksnapshot.ts create mode 100644 packages/storage/exp/index.ts create mode 100644 packages/storage/exp/package.json create mode 100644 packages/storage/rollup.config.compat.js create mode 100644 packages/storage/rollup.config.exp.js rename packages/storage/test/unit/{reference.test.ts => reference.compat.test.ts} (56%) create mode 100644 packages/storage/test/unit/reference.exp.test.ts rename packages/storage/test/unit/{service.test.ts => service.compat.test.ts} (72%) create mode 100644 packages/storage/test/unit/service.exp.test.ts diff --git a/.changeset/config.json b/.changeset/config.json index 5d0be005d42..9864da43f42 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -15,12 +15,15 @@ "@firebase/auth-exp", "@firebase/auth-compat", "@firebase/auth-types-exp", + "@firebase/functions-compat", "@firebase/functions-exp", "@firebase/functions-types-exp", "@firebase/installations-exp", "@firebase/installations-types-exp", "@firebase/performance-exp", "@firebase/performance-types-exp", + "@firebase/remote-config-exp", + "@firebase/remote-config-types-exp", "firebase-exp", "@firebase/app-compat", "@firebase/changelog-generator", diff --git a/.changeset/fuzzy-impalas-brake.md b/.changeset/fuzzy-impalas-brake.md deleted file mode 100644 index 5f171a2958e..00000000000 --- a/.changeset/fuzzy-impalas-brake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@firebase/util": patch ---- - -Do not merge `__proto__` in `deepExtend` to prevent `__proto__` pollution. diff --git a/.changeset/silly-boats-roll.md b/.changeset/silly-boats-roll.md new file mode 100644 index 00000000000..b27cd48f2b0 --- /dev/null +++ b/.changeset/silly-boats-roll.md @@ -0,0 +1,5 @@ +--- +'@firebase/storage': patch +--- + +Refactored Storage to allow for modularization. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5462c0ea9e5..af19815e422 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -99,4 +99,8 @@ packages/installations-types-exp @andirayo @ChaoqunCHEN @firebase/jssdk-global-a # Perf-Exp Code packages/performance-exp @alikn @zijianjoy @firebase/jssdk-global-approvers -packages/performance-types-exp @alikn @zijianjoy @firebase/jssdk-global-approvers \ No newline at end of file +packages/performance-types-exp @alikn @zijianjoy @firebase/jssdk-global-approvers + +# RC-Exp Code +packages/remote-config-exp @erikeldridge @firebase/jssdk-global-approvers +packages/remote-config-types-exp @erikeldridge @firebase/jssdk-global-approvers \ No newline at end of file diff --git a/.github/workflows/check-changeset.yml b/.github/workflows/check-changeset.yml index d8082f09804..8eb8be911c8 100644 --- a/.github/workflows/check-changeset.yml +++ b/.github/workflows/check-changeset.yml @@ -24,6 +24,8 @@ jobs: id: check-changeset - name: Print changeset checker output run: echo "${{steps.check-changeset.outputs.CHANGESET_ERROR_MESSAGE}}" + - name: Print blocking failure status + run: echo "${{steps.check-changeset.outputs.BLOCKING_FAILURE}}" - name: Find Comment uses: peter-evans/find-comment@v1 id: fc @@ -59,5 +61,5 @@ jobs: - No changeset formatting errors detected. # Don't want it to throw before editing the comment. - name: Fail if checker script logged a blocking failure - if: ${{steps.check-changeset.outputs.BLOCKING_FAILURE}} + if: ${{steps.check-changeset.outputs.BLOCKING_FAILURE == 'true'}} run: exit 1 \ No newline at end of file diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index 4a294bedfba..4291e6343ea 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -17,6 +17,8 @@ jobs: run: | sudo apt-get update sudo apt-get install google-chrome-stable + - name: Bump Node memory limit + run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV - name: Test setup and yarn install run: | cp config/ci.config.json config/project.json diff --git a/.github/workflows/test-changed-auth.yml b/.github/workflows/test-changed-auth.yml index a18ffa13b40..7053231056a 100644 --- a/.github/workflows/test-changed-auth.yml +++ b/.github/workflows/test-changed-auth.yml @@ -21,6 +21,8 @@ jobs: run: | sudo apt-get update sudo apt-get install google-chrome-stable + - name: Bump Node memory limit + run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV - name: Test setup and yarn install run: | cp config/ci.config.json config/project.json diff --git a/.github/workflows/test-changed-fcm-integration.yml b/.github/workflows/test-changed-fcm-integration.yml index 2a127d11a40..90bf59b5c50 100644 --- a/.github/workflows/test-changed-fcm-integration.yml +++ b/.github/workflows/test-changed-fcm-integration.yml @@ -21,6 +21,8 @@ jobs: run: | sudo apt-get update sudo apt-get install google-chrome-stable + - name: Bump Node memory limit + run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV - name: Test setup and yarn install run: | cp config/ci.config.json config/project.json diff --git a/.github/workflows/test-changed-firestore-integration.yml b/.github/workflows/test-changed-firestore-integration.yml index 387701316b2..d7f0ebde367 100644 --- a/.github/workflows/test-changed-firestore-integration.yml +++ b/.github/workflows/test-changed-firestore-integration.yml @@ -21,11 +21,13 @@ jobs: run: | sudo apt-get update sudo apt-get install google-chrome-stable + - name: Bump Node memory limit + run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV - name: Test setup and yarn install run: | cp config/ci.config.json config/project.json yarn - name: build - run: yarn build:changed firestore-integration --buildAppExp + run: yarn build:changed firestore-integration --buildAppExp --buildAppCompat - name: Run tests if firestore or its dependencies has changed run: yarn test:changed firestore-integration diff --git a/.github/workflows/test-changed-firestore.yml b/.github/workflows/test-changed-firestore.yml index b4ced05b867..76924c1ce0c 100644 --- a/.github/workflows/test-changed-firestore.yml +++ b/.github/workflows/test-changed-firestore.yml @@ -21,11 +21,13 @@ jobs: run: | sudo apt-get update sudo apt-get install google-chrome-stable + - name: Bump Node memory limit + run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV - name: Test setup and yarn install run: | cp config/ci.config.json config/project.json yarn - name: build - run: yarn build:changed firestore --buildAppExp + run: yarn build:changed firestore --buildAppExp --buildAppCompat - name: Run tests if firestore or its dependencies has changed run: yarn test:changed firestore diff --git a/.github/workflows/test-changed-misc.yml b/.github/workflows/test-changed-misc.yml index 50aad8ea227..3ce5dc767ad 100644 --- a/.github/workflows/test-changed-misc.yml +++ b/.github/workflows/test-changed-misc.yml @@ -21,6 +21,8 @@ jobs: run: | sudo apt-get update sudo apt-get install google-chrome-stable + - name: Bump Node memory limit + run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV - name: Test setup and yarn install run: | cp config/ci.config.json config/project.json diff --git a/.github/workflows/test-changed.yml b/.github/workflows/test-changed.yml index cb836d9c312..26434d3b678 100644 --- a/.github/workflows/test-changed.yml +++ b/.github/workflows/test-changed.yml @@ -21,6 +21,8 @@ jobs: run: | sudo apt-get update sudo apt-get install google-chrome-stable + - name: Bump Node memory limit + run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV - name: Test setup and yarn install run: | cp config/ci.config.json config/project.json diff --git a/.github/workflows/test-firebase-integration.yml b/.github/workflows/test-firebase-integration.yml index c42c1c08687..5a05a8bd122 100644 --- a/.github/workflows/test-firebase-integration.yml +++ b/.github/workflows/test-firebase-integration.yml @@ -21,6 +21,8 @@ jobs: run: | sudo apt-get update sudo apt-get install google-chrome-stable + - name: Bump Node memory limit + run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV - name: Test setup and yarn install run: | cp config/ci.config.json config/project.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000000..ec4d0166d7e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +You can view the changelog for the packages within this monorepo in `packages//CHANGELOG.md`. For example, you will find the changelog for the Firestore package at [`packages/firestore/CHANGELOG.md`](./packages/firestore/CHANGELOG.md). + +Additionally, you can view our official release notes for the entire monorepo at the [firebase support page](https://firebase.google.com/support/release-notes/js). diff --git a/common/api-review/auth-exp.api.md b/common/api-review/auth-exp.api.md index 23aa51decd1..c08f0b326c7 100644 --- a/common/api-review/auth-exp.api.md +++ b/common/api-review/auth-exp.api.md @@ -61,196 +61,6 @@ export class AuthCredential { toJSON(): object; } -// @public -export const enum AuthErrorCode { - // (undocumented) - ADMIN_ONLY_OPERATION = "admin-restricted-operation", - // (undocumented) - APP_NOT_AUTHORIZED = "app-not-authorized", - // (undocumented) - APP_NOT_INSTALLED = "app-not-installed", - // (undocumented) - ARGUMENT_ERROR = "argument-error", - // (undocumented) - CAPTCHA_CHECK_FAILED = "captcha-check-failed", - // (undocumented) - CODE_EXPIRED = "code-expired", - // (undocumented) - CORDOVA_NOT_READY = "cordova-not-ready", - // (undocumented) - CORS_UNSUPPORTED = "cors-unsupported", - // (undocumented) - CREDENTIAL_ALREADY_IN_USE = "credential-already-in-use", - // (undocumented) - CREDENTIAL_MISMATCH = "custom-token-mismatch", - // (undocumented) - CREDENTIAL_TOO_OLD_LOGIN_AGAIN = "requires-recent-login", - // (undocumented) - DYNAMIC_LINK_NOT_ACTIVATED = "dynamic-link-not-activated", - // (undocumented) - EMAIL_CHANGE_NEEDS_VERIFICATION = "email-change-needs-verification", - // (undocumented) - EMAIL_EXISTS = "email-already-in-use", - // (undocumented) - EMULATOR_CONFIG_FAILED = "emulator-config-failed", - // (undocumented) - EXPIRED_OOB_CODE = "expired-action-code", - // (undocumented) - EXPIRED_POPUP_REQUEST = "cancelled-popup-request", - // (undocumented) - INTERNAL_ERROR = "internal-error", - // (undocumented) - INVALID_API_KEY = "invalid-api-key", - // (undocumented) - INVALID_APP_CREDENTIAL = "invalid-app-credential", - // (undocumented) - INVALID_APP_ID = "invalid-app-id", - // (undocumented) - INVALID_AUTH = "invalid-user-token", - // (undocumented) - INVALID_AUTH_EVENT = "invalid-auth-event", - // (undocumented) - INVALID_CERT_HASH = "invalid-cert-hash", - // (undocumented) - INVALID_CODE = "invalid-verification-code", - // (undocumented) - INVALID_CONTINUE_URI = "invalid-continue-uri", - // (undocumented) - INVALID_CORDOVA_CONFIGURATION = "invalid-cordova-configuration", - // (undocumented) - INVALID_CUSTOM_TOKEN = "invalid-custom-token", - // (undocumented) - INVALID_DYNAMIC_LINK_DOMAIN = "invalid-dynamic-link-domain", - // (undocumented) - INVALID_EMAIL = "invalid-email", - // (undocumented) - INVALID_EMULATOR_SCHEME = "invalid-emulator-scheme", - // (undocumented) - INVALID_IDP_RESPONSE = "invalid-credential", - // (undocumented) - INVALID_MESSAGE_PAYLOAD = "invalid-message-payload", - // (undocumented) - INVALID_MFA_SESSION = "invalid-multi-factor-session", - // (undocumented) - INVALID_OAUTH_CLIENT_ID = "invalid-oauth-client-id", - // (undocumented) - INVALID_OAUTH_PROVIDER = "invalid-oauth-provider", - // (undocumented) - INVALID_OOB_CODE = "invalid-action-code", - // (undocumented) - INVALID_ORIGIN = "unauthorized-domain", - // (undocumented) - INVALID_PASSWORD = "wrong-password", - // (undocumented) - INVALID_PERSISTENCE = "invalid-persistence-type", - // (undocumented) - INVALID_PHONE_NUMBER = "invalid-phone-number", - // (undocumented) - INVALID_PROVIDER_ID = "invalid-provider-id", - // (undocumented) - INVALID_RECIPIENT_EMAIL = "invalid-recipient-email", - // (undocumented) - INVALID_SENDER = "invalid-sender", - // (undocumented) - INVALID_SESSION_INFO = "invalid-verification-id", - // (undocumented) - INVALID_TENANT_ID = "invalid-tenant-id", - // (undocumented) - MFA_INFO_NOT_FOUND = "multi-factor-info-not-found", - // (undocumented) - MFA_REQUIRED = "multi-factor-auth-required", - // (undocumented) - MISSING_ANDROID_PACKAGE_NAME = "missing-android-pkg-name", - // (undocumented) - MISSING_APP_CREDENTIAL = "missing-app-credential", - // (undocumented) - MISSING_AUTH_DOMAIN = "auth-domain-config-required", - // (undocumented) - MISSING_CODE = "missing-verification-code", - // (undocumented) - MISSING_CONTINUE_URI = "missing-continue-uri", - // (undocumented) - MISSING_IFRAME_START = "missing-iframe-start", - // (undocumented) - MISSING_IOS_BUNDLE_ID = "missing-ios-bundle-id", - // (undocumented) - MISSING_MFA_INFO = "missing-multi-factor-info", - // (undocumented) - MISSING_MFA_SESSION = "missing-multi-factor-session", - // (undocumented) - MISSING_OR_INVALID_NONCE = "missing-or-invalid-nonce", - // (undocumented) - MISSING_PHONE_NUMBER = "missing-phone-number", - // (undocumented) - MISSING_SESSION_INFO = "missing-verification-id", - // (undocumented) - MODULE_DESTROYED = "app-deleted", - // (undocumented) - NEED_CONFIRMATION = "account-exists-with-different-credential", - // (undocumented) - NETWORK_REQUEST_FAILED = "network-request-failed", - // (undocumented) - NO_AUTH_EVENT = "no-auth-event", - // (undocumented) - NO_SUCH_PROVIDER = "no-such-provider", - // (undocumented) - NULL_USER = "null-user", - // (undocumented) - OPERATION_NOT_ALLOWED = "operation-not-allowed", - // (undocumented) - OPERATION_NOT_SUPPORTED = "operation-not-supported-in-this-environment", - // (undocumented) - POPUP_BLOCKED = "popup-blocked", - // (undocumented) - POPUP_CLOSED_BY_USER = "popup-closed-by-user", - // (undocumented) - PROVIDER_ALREADY_LINKED = "provider-already-linked", - // (undocumented) - QUOTA_EXCEEDED = "quota-exceeded", - // (undocumented) - REDIRECT_CANCELLED_BY_USER = "redirect-cancelled-by-user", - // (undocumented) - REDIRECT_OPERATION_PENDING = "redirect-operation-pending", - // (undocumented) - REJECTED_CREDENTIAL = "rejected-credential", - // (undocumented) - SECOND_FACTOR_ALREADY_ENROLLED = "second-factor-already-in-use", - // (undocumented) - SECOND_FACTOR_LIMIT_EXCEEDED = "maximum-second-factor-count-exceeded", - // (undocumented) - TENANT_ID_MISMATCH = "tenant-id-mismatch", - // (undocumented) - TIMEOUT = "timeout", - // (undocumented) - TOKEN_EXPIRED = "user-token-expired", - // (undocumented) - TOO_MANY_ATTEMPTS_TRY_LATER = "too-many-requests", - // (undocumented) - UNAUTHORIZED_DOMAIN = "unauthorized-continue-uri", - // (undocumented) - UNSUPPORTED_FIRST_FACTOR = "unsupported-first-factor", - // (undocumented) - UNSUPPORTED_PERSISTENCE = "unsupported-persistence-type", - // (undocumented) - UNSUPPORTED_TENANT_OPERATION = "unsupported-tenant-operation", - // (undocumented) - UNVERIFIED_EMAIL = "unverified-email", - // (undocumented) - USER_CANCELLED = "user-cancelled", - // (undocumented) - USER_DELETED = "user-not-found", - // (undocumented) - USER_DISABLED = "user-disabled", - // (undocumented) - USER_MISMATCH = "user-mismatch", - // (undocumented) - USER_SIGNED_OUT = "user-signed-out", - // (undocumented) - WEAK_PASSWORD = "weak-password", - // (undocumented) - WEB_STORAGE_UNSUPPORTED = "web-storage-unsupported" -} - // @public export const browserLocalPersistence: externs.Persistence; diff --git a/common/api-review/remote-config-exp.api.md b/common/api-review/remote-config-exp.api.md new file mode 100644 index 00000000000..a51619116ca --- /dev/null +++ b/common/api-review/remote-config-exp.api.md @@ -0,0 +1,48 @@ +## API Report File for "@firebase/remote-config-exp" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { FirebaseApp } from '@firebase/app-types-exp'; +import { LogLevel } from '@firebase/remote-config-types-exp'; +import { RemoteConfig } from '@firebase/remote-config-types-exp'; +import { Value } from '@firebase/remote-config-types-exp'; + +// @public (undocumented) +export function activate(remoteConfig: RemoteConfig): Promise; + +// @public (undocumented) +export function ensureInitialized(remoteConfig: RemoteConfig): Promise; + +// @public (undocumented) +export function fetchAndActivate(remoteConfig: RemoteConfig): Promise; + +// @public (undocumented) +export function fetchConfig(remoteConfig: RemoteConfig): Promise; + +// @public (undocumented) +export function getAll(remoteConfig: RemoteConfig): Record; + +// @public (undocumented) +export function getBoolean(remoteConfig: RemoteConfig, key: string): boolean; + +// @public (undocumented) +export function getNumber(remoteConfig: RemoteConfig, key: string): number; + +// @public (undocumented) +export function getRemoteConfig(app: FirebaseApp): RemoteConfig; + +// @public (undocumented) +export function getString(remoteConfig: RemoteConfig, key: string): string; + +// @public (undocumented) +export function getValue(remoteConfig: RemoteConfig, key: string): Value; + +// @public (undocumented) +export function setLogLevel(remoteConfig: RemoteConfig, logLevel: LogLevel): void; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/config/.eslintrc.js b/config/.eslintrc.js index a7c60820a24..0c84ff67a9a 100644 --- a/config/.eslintrc.js +++ b/config/.eslintrc.js @@ -184,7 +184,7 @@ module.exports = { 'assertionStyle': 'as' } ], - '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-explicit-any': ['error', { 'ignoreRestArgs': true }], '@typescript-eslint/no-namespace': [ 'error', { diff --git a/config/functions/package.json b/config/functions/package.json index af8ce89993e..5168c32a837 100644 --- a/config/functions/package.json +++ b/config/functions/package.json @@ -3,7 +3,7 @@ "description": "Cloud Functions for Firebase", "dependencies": { "cors": "2.8.5", - "firebase-admin": "9.2.0", + "firebase-admin": "9.3.0", "firebase-functions": "3.11.0" }, "private": true, diff --git a/config/webpack.test.js b/config/webpack.test.js index c437b49365e..887efd2ebec 100644 --- a/config/webpack.test.js +++ b/config/webpack.test.js @@ -75,13 +75,13 @@ module.exports = { } } }, + /** + * Transform firebase packages to cjs, so they can be stubbed in tests + */ { test: /\.js$/, include: function (modulePath) { const match = /node_modules\/@firebase.*/.test(modulePath); - if (match) { - console.log('modulePath', modulePath, match); - } return match; }, use: { diff --git a/integration/firebase/package.json b/integration/firebase/package.json index d80e095e9f7..d487944b413 100644 --- a/integration/firebase/package.json +++ b/integration/firebase/package.json @@ -7,20 +7,20 @@ "test:ci": "node ../../scripts/run_tests_in_ci.js -s test" }, "devDependencies": { - "firebase": "8.0.0", - "@types/chai": "4.2.13", + "firebase": "8.0.1", + "@types/chai": "4.2.14", "@types/mocha": "7.0.2", "chai": "4.2.0", "karma": "5.2.3", "karma-babel-preprocessor": "8.0.1", "karma-chrome-launcher": "3.1.0", - "karma-firefox-launcher": "1.3.0", + "karma-firefox-launcher": "2.1.0", "karma-mocha": "2.0.1", "karma-sauce-launcher": "1.2.0", "karma-spec-reporter": "0.0.32", "karma-typescript": "5.2.0", "mocha": "7.2.0", "npm-run-all": "4.1.5", - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/integration/firestore/firebase_export.ts b/integration/firestore/firebase_export.ts index 4eeb1c7bfa9..75c1844a70d 100644 --- a/integration/firestore/firebase_export.ts +++ b/integration/firestore/firebase_export.ts @@ -55,5 +55,16 @@ const Timestamp = firebase.firestore.Timestamp; const GeoPoint = firebase.firestore.GeoPoint; const FieldValue = firebase.firestore.FieldValue; const Blob = firebase.firestore.Blob; +const loadBundle = firebase.firestore.loadBundle; +const namedQuery = firebase.firestore.namedQuery; -export { Firestore, FieldValue, FieldPath, Timestamp, Blob, GeoPoint }; +export { + Firestore, + FieldValue, + FieldPath, + Timestamp, + Blob, + GeoPoint, + loadBundle, + namedQuery +}; diff --git a/integration/firestore/firebase_export_memory.ts b/integration/firestore/firebase_export_memory.ts index 441c778ef5a..90750994ee3 100644 --- a/integration/firestore/firebase_export_memory.ts +++ b/integration/firestore/firebase_export_memory.ts @@ -55,5 +55,16 @@ const Timestamp = firebase.firestore.Timestamp; const GeoPoint = firebase.firestore.GeoPoint; const FieldValue = firebase.firestore.FieldValue; const Blob = firebase.firestore.Blob; +const loadBundle = firebase.firestore.loadBundle; +const namedQuery = firebase.firestore.namedQuery; -export { Firestore, FieldValue, FieldPath, Timestamp, Blob, GeoPoint }; +export { + Firestore, + FieldValue, + FieldPath, + Timestamp, + Blob, + GeoPoint, + loadBundle, + namedQuery +}; diff --git a/integration/firestore/package.json b/integration/firestore/package.json index a9846bbe475..81d4a25092f 100644 --- a/integration/firestore/package.json +++ b/integration/firestore/package.json @@ -14,20 +14,20 @@ "test:memory:debug": "yarn build:memory; karma start --auto-watch --browsers Chrome" }, "devDependencies": { - "@firebase/app": "0.6.12", - "@firebase/firestore": "2.0.0", + "@firebase/app": "0.6.13", + "@firebase/firestore": "2.0.1", "@types/mocha": "7.0.2", "gulp": "4.0.2", "gulp-filter": "6.0.0", "gulp-replace": "1.0.0", "karma": "5.2.3", "karma-chrome-launcher": "3.1.0", - "karma-firefox-launcher": "1.3.0", + "karma-firefox-launcher": "2.1.0", "karma-mocha": "2.0.1", "karma-spec-reporter": "0.0.32", "mocha": "7.2.0", - "ts-loader": "8.0.5", - "typescript": "4.0.2", + "ts-loader": "8.0.9", + "typescript": "4.0.5", "webpack": "4.44.2", "webpack-stream": "6.1.0" } diff --git a/integration/messaging/package.json b/integration/messaging/package.json index 6485e723a61..5eb8367f0de 100644 --- a/integration/messaging/package.json +++ b/integration/messaging/package.json @@ -9,7 +9,7 @@ "test:manual": "mocha --exit" }, "devDependencies": { - "firebase": "8.0.0", + "firebase": "8.0.1", "chai": "4.2.0", "chromedriver": "86.0.0", "express": "4.17.1", diff --git a/package.json b/package.json index 2a5043cb372..8d4cc567389 100644 --- a/package.json +++ b/package.json @@ -66,10 +66,10 @@ ], "devDependencies": { "@changesets/changelog-github": "0.2.7", - "@changesets/cli": "2.11.0", + "@changesets/cli": "2.11.2", "api-documenter-me": "0.1.0", "api-extractor-me": "0.1.0", - "@types/chai": "4.2.13", + "@types/chai": "4.2.14", "@types/chai-as-promised": "7.1.3", "@types/child-process-promise": "2.2.1", "@types/clone": "2.1.0", @@ -77,18 +77,18 @@ "@types/listr": "0.14.2", "@types/long": "4.0.1", "@types/mocha": "7.0.2", - "@types/mz": "2.7.1", - "@types/node": "12.12.67", + "@types/mz": "2.7.2", + "@types/node": "12.19.3", "@types/sinon": "9.0.8", "@types/sinon-chai": "3.2.5", "@types/tmp": "0.2.0", - "@types/yargs": "15.0.8", - "@typescript-eslint/eslint-plugin": "4.4.1", - "@typescript-eslint/eslint-plugin-tslint": "4.4.1", - "@typescript-eslint/parser": "4.4.1", + "@types/yargs": "15.0.9", + "@typescript-eslint/eslint-plugin": "4.6.1", + "@typescript-eslint/eslint-plugin-tslint": "4.6.1", + "@typescript-eslint/parser": "4.6.1", "babel-loader": "8.1.0", - "@babel/core": "7.11.6", - "@babel/preset-env": "7.11.5", + "@babel/core": "7.12.3", + "@babel/preset-env": "7.12.1", "@babel/plugin-transform-modules-commonjs": "7.12.1", "chai": "4.2.0", "chai-as-promised": "7.1.1", @@ -98,12 +98,12 @@ "coveralls": "3.1.0", "del": "6.0.0", "dependency-graph": "0.9.0", - "eslint": "7.11.0", + "eslint": "7.12.1", "eslint-plugin-import": "2.22.1", "express": "4.17.1", "find-free-port": "2.0.0", "firebase-functions": "3.11.0", - "firebase-tools": "8.12.1", + "firebase-tools": "8.15.0", "git-rev-sync": "3.0.1", "glob": "7.1.6", "http-server": "0.12.3", @@ -116,7 +116,7 @@ "karma-chrome-launcher": "3.1.0", "karma-cli": "2.0.0", "karma-coverage-istanbul-reporter": "2.1.1", - "karma-firefox-launcher": "1.3.0", + "karma-firefox-launcher": "2.1.0", "karma-mocha": "2.0.1", "karma-mocha-reporter": "2.2.5", "karma-safari-launcher": "1.0.0", @@ -143,18 +143,18 @@ "rxjs": "6.6.3", "semver": "7.3.2", "simple-git": "2.21.0", - "sinon": "9.2.0", + "sinon": "9.2.1", "sinon-chai": "3.5.0", - "source-map-loader": "1.1.1", - "terser": "5.3.5", - "ts-loader": "8.0.5", + "source-map-loader": "1.1.2", + "terser": "5.3.8", + "ts-loader": "8.0.9", "ts-node": "9.0.0", "tslint": "6.1.3", "typedoc": "0.16.11", - "typescript": "4.0.2", + "typescript": "4.0.5", "watch": "1.0.2", "webpack": "4.44.2", - "yargs": "16.0.3" + "yargs": "16.1.0" }, "husky": { "hooks": { diff --git a/packages-exp/app-compat/package.json b/packages-exp/app-compat/package.json index 3ca8c1df8e4..218c4a659cc 100644 --- a/packages-exp/app-compat/package.json +++ b/packages-exp/app-compat/package.json @@ -29,19 +29,19 @@ "license": "Apache-2.0", "dependencies": { "@firebase/app-exp": "0.0.800", - "@firebase/util": "0.3.3", + "@firebase/util": "0.3.4", "@firebase/logger": "0.2.6", - "@firebase/component": "0.1.20", + "@firebase/component": "0.1.21", "tslib": "^1.11.1", "dom-storage": "2.1.0", "xmlhttprequest": "1.8.0" }, "devDependencies": { - "rollup": "2.29.0", + "rollup": "2.33.1", "@rollup/plugin-json": "4.1.0", "rollup-plugin-replace": "2.2.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages-exp/app-compat", diff --git a/packages-exp/app-exp/package.json b/packages-exp/app-exp/package.json index 974913b95f6..6da2f63a278 100644 --- a/packages-exp/app-exp/package.json +++ b/packages-exp/app-exp/package.json @@ -31,18 +31,18 @@ }, "dependencies": { "@firebase/app-types-exp": "0.0.800", - "@firebase/util": "0.3.3", + "@firebase/util": "0.3.4", "@firebase/logger": "0.2.6", - "@firebase/component": "0.1.20", + "@firebase/component": "0.1.21", "tslib": "^1.11.1" }, "license": "Apache-2.0", "devDependencies": { - "rollup": "2.29.0", + "rollup": "2.33.1", "@rollup/plugin-json": "4.1.0", "rollup-plugin-replace": "2.2.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages-exp/app-exp", diff --git a/packages-exp/app-exp/src/constants.ts b/packages-exp/app-exp/src/constants.ts index 8147920c734..61517c49794 100644 --- a/packages-exp/app-exp/src/constants.ts +++ b/packages-exp/app-exp/src/constants.ts @@ -21,6 +21,7 @@ import { name as analyticsName } from '../../../packages/analytics/package.json' import { name as authName } from '../../../packages/auth/package.json'; import { name as databaseName } from '../../../packages/database/package.json'; import { name as functionsName } from '../../../packages-exp/functions-exp/package.json'; +import { name as functionsCompatName } from '../../../packages-exp/functions-compat/package.json'; import { name as installationsName } from '../../../packages/installations/package.json'; import { name as messagingName } from '../../../packages/messaging/package.json'; import { name as performanceName } from '../../../packages/performance/package.json'; @@ -43,6 +44,7 @@ export const PLATFORM_LOG_STRING = { [authName]: 'fire-auth', [databaseName]: 'fire-rtdb', [functionsName]: 'fire-fn', + [functionsCompatName]: 'fire-fn-compat', [installationsName]: 'fire-iid', [messagingName]: 'fire-fcm', [performanceName]: 'fire-perf', diff --git a/packages-exp/app-types-exp/package.json b/packages-exp/app-types-exp/package.json index 96eea841c5a..6486c2df78a 100644 --- a/packages-exp/app-types-exp/package.json +++ b/packages-exp/app-types-exp/package.json @@ -26,6 +26,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages-exp/auth-compat-exp/demo/package.json b/packages-exp/auth-compat-exp/demo/package.json index a7c941eebc8..39e9d42b1d8 100644 --- a/packages-exp/auth-compat-exp/demo/package.json +++ b/packages-exp/auth-compat-exp/demo/package.json @@ -33,9 +33,9 @@ "rollup-plugin-license": "0.14.0", "@rollup/plugin-node-resolve": "9.0.0", "rollup-plugin-sourcemaps": "0.6.3", - "rollup-plugin-typescript2": "0.27.3", + "rollup-plugin-typescript2": "0.29.0", "rollup-plugin-uglify": "6.0.4", - "typescript": "3.9.5" + "typescript": "4.0.5" }, "repository": { "directory": "packages-exp/auth-compat-exp/demo", diff --git a/packages-exp/auth-compat-exp/demo/yarn.lock b/packages-exp/auth-compat-exp/demo/yarn.lock index a81f23c4f21..660a62aa5cb 100644 --- a/packages-exp/auth-compat-exp/demo/yarn.lock +++ b/packages-exp/auth-compat-exp/demo/yarn.lock @@ -916,7 +916,39 @@ dependencies: "@types/node" ">= 8" -"@rollup/pluginutils@^3.0.9", "@rollup/pluginutils@^3.1.0": +"@rollup/plugin-commonjs@15.1.0": + version "15.1.0" + resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-15.1.0.tgz#1e7d076c4f1b2abf7e65248570e555defc37c238" + integrity sha512-xCQqz4z/o0h2syQ7d9LskIMvBSH4PX5PjYdpSSvgS+pQik3WahkQVNWg3D8XJeYjZoVWnIUQYDghuEMRGrmQYQ== + dependencies: + "@rollup/pluginutils" "^3.1.0" + commondir "^1.0.1" + estree-walker "^2.0.1" + glob "^7.1.6" + is-reference "^1.2.1" + magic-string "^0.25.7" + resolve "^1.17.0" + +"@rollup/plugin-json@4.1.0": + version "4.1.0" + resolved "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" + integrity sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw== + dependencies: + "@rollup/pluginutils" "^3.0.8" + +"@rollup/plugin-node-resolve@9.0.0": + version "9.0.0" + resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-9.0.0.tgz#39bd0034ce9126b39c1699695f440b4b7d2b62e6" + integrity sha512-gPz+utFHLRrd41WMP13Jq5mqqzHL3OXrfj3/MkSyB6UBIcuNt9j60GCbarzMzdf1VHFpOxfQh/ez7wyadLMqkg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + "@types/resolve" "1.17.1" + builtin-modules "^3.1.0" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.17.0" + +"@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.0.9", "@rollup/pluginutils@^3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== @@ -963,10 +995,10 @@ resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== -"@types/resolve@0.0.8": - version "0.0.8" - resolved "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194" - integrity sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ== +"@types/resolve@1.17.1": + version "1.17.1" + resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" + integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== dependencies: "@types/node" "*" @@ -1755,6 +1787,11 @@ dedent@^0.7.0: resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + defaults@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" @@ -1980,6 +2017,11 @@ estree-walker@^1.0.1: resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== +estree-walker@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.1.tgz#f8e030fb21cefa183b44b7ad516b747434e7a3e0" + integrity sha512-tF0hv+Yi2Ot1cwj9eYHtxC0jB9bmjacjQs6ZBTj82H8JwUywFuc+7E83NWfNMwHXZc11mjfFcVXPe9gEP4B8dg== + eventemitter3@^3.1.0: version "3.1.2" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" @@ -2358,7 +2400,7 @@ glob-to-regexp@^0.3.0: resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= -glob@7.1.6, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4: +glob@7.1.6, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.1.6" resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -2680,6 +2722,13 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" +is-core-module@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.0.0.tgz#58531b70aed1db7c0e8d4eb1a0a2d1ddd64bd12d" + integrity sha512-jq1AH6C8MuteOoBPwkxHafmByhL9j5q4OaPGdbuD+ZtQJVzH+i6E3BJDQcBA09k57i2Hh2yQbEG8yObZ0jdlWw== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -2814,7 +2863,7 @@ is-plain-object@^5.0.0: resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== -is-reference@^1.1.2: +is-reference@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== @@ -3157,7 +3206,7 @@ macos-release@^2.2.0: resolved "https://registry.npmjs.org/macos-release/-/macos-release-2.4.1.tgz#64033d0ec6a5e6375155a74b1a1eba8e509820ac" integrity sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg== -magic-string@0.25.7, magic-string@^0.25.2: +magic-string@0.25.7, magic-string@^0.25.2, magic-string@^0.25.7: version "0.25.7" resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== @@ -4331,13 +4380,21 @@ resolve-url@^0.2.1: resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@1.17.0, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.11.1: +resolve@1.17.0, resolve@^1.10.0: version "1.17.0" resolved "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== dependencies: path-parse "^1.0.6" +resolve@^1.17.0: + version "1.18.1" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz#018fcb2c5b207d2a6424aee361c5a266da8f4130" + integrity sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA== + dependencies: + is-core-module "^2.0.0" + path-parse "^1.0.6" + restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -4363,24 +4420,6 @@ rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: dependencies: glob "^7.1.3" -rollup-plugin-commonjs@10.1.0: - version "10.1.0" - resolved "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz#417af3b54503878e084d127adf4d1caf8beb86fb" - integrity sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q== - dependencies: - estree-walker "^0.6.1" - is-reference "^1.1.2" - magic-string "^0.25.2" - resolve "^1.11.0" - rollup-pluginutils "^2.8.1" - -rollup-plugin-json@4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/rollup-plugin-json/-/rollup-plugin-json-4.0.0.tgz#a18da0a4b30bf5ca1ee76ddb1422afbb84ae2b9e" - integrity sha512-hgb8N7Cgfw5SZAkb3jf0QXii6QX/FOkiIq2M7BAQIEydjHvTyxXHQiIzZaTFgx1GK0cRCHOCBHIyEkkLdWKxow== - dependencies: - rollup-pluginutils "^2.5.0" - rollup-plugin-license@0.14.0: version "0.14.0" resolved "https://registry.npmjs.org/rollup-plugin-license/-/rollup-plugin-license-0.14.0.tgz#623e5b6306b1521d7b89927d0a28d0f99ac52915" @@ -4395,17 +4434,6 @@ rollup-plugin-license@0.14.0: spdx-expression-validate "2.0.0" spdx-satisfies "5.0.0" -rollup-plugin-node-resolve@5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz#730f93d10ed202473b1fb54a5997a7db8c6d8523" - integrity sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw== - dependencies: - "@types/resolve" "0.0.8" - builtin-modules "^3.1.0" - is-module "^1.0.0" - resolve "^1.11.1" - rollup-pluginutils "^2.8.1" - rollup-plugin-replace@2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz#f41ae5372e11e7a217cde349c8b5d5fd115e70e3" @@ -4422,10 +4450,10 @@ rollup-plugin-sourcemaps@0.6.3: "@rollup/pluginutils" "^3.0.9" source-map-resolve "^0.6.0" -rollup-plugin-typescript2@0.27.3: - version "0.27.3" - resolved "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.27.3.tgz#cd9455ac026d325b20c5728d2cc54a08a771b68b" - integrity sha512-gmYPIFmALj9D3Ga1ZbTZAKTXq1JKlTQBtj299DXhqYz9cL3g/AQfUvbb2UhH+Nf++cCq941W2Mv7UcrcgLzJJg== +rollup-plugin-typescript2@0.29.0: + version "0.29.0" + resolved "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.29.0.tgz#b7ad83f5241dbc5bdf1e98d9c3fca005ffe39e1a" + integrity sha512-YytahBSZCIjn/elFugEGQR5qTsVhxhUwGZIsA9TmrSsC88qroGo65O5HZP/TTArH2dm0vUmYWhKchhwi2wL9bw== dependencies: "@rollup/pluginutils" "^3.1.0" find-cache-dir "^3.3.1" @@ -4443,7 +4471,7 @@ rollup-plugin-uglify@6.0.4: serialize-javascript "^2.1.2" uglify-js "^3.4.9" -rollup-pluginutils@^2.5.0, rollup-pluginutils@^2.6.0, rollup-pluginutils@^2.8.1: +rollup-pluginutils@^2.6.0: version "2.8.2" resolved "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== @@ -5111,10 +5139,10 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@3.9.5: - version "3.9.5" - resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36" - integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ== +typescript@4.0.5: + version "4.0.5" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389" + integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ== uglify-js@^3.1.4, uglify-js@^3.4.9: version "3.11.0" diff --git a/packages-exp/auth-compat-exp/package.json b/packages-exp/auth-compat-exp/package.json index 6578622b975..4eee460eb98 100644 --- a/packages-exp/auth-compat-exp/package.json +++ b/packages-exp/auth-compat-exp/package.json @@ -30,18 +30,18 @@ "@firebase/auth-types": "0.10.1", "@firebase/auth-exp": "0.0.800", "@firebase/auth-types-exp": "0.0.800", - "@firebase/component": "0.1.20", - "@firebase/util": "0.3.3", + "@firebase/component": "0.1.21", + "@firebase/util": "0.3.4", "tslib": "^1.11.1" }, "license": "Apache-2.0", "devDependencies": { "@firebase/app-compat": "0.x", - "rollup": "2.29.0", + "rollup": "2.33.1", "@rollup/plugin-json": "4.1.0", "rollup-plugin-replace": "2.2.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages-exp/auth-compat-exp", diff --git a/packages-exp/auth-compat-exp/src/user_credential.ts b/packages-exp/auth-compat-exp/src/user_credential.ts index bf7e4982878..628340ebb19 100644 --- a/packages-exp/auth-compat-exp/src/user_credential.ts +++ b/packages-exp/auth-compat-exp/src/user_credential.ts @@ -116,10 +116,10 @@ export async function convertConfirmationResult( auth: externs.Auth, confirmationResultPromise: Promise ): Promise { - const { verificationId, confirm } = await confirmationResultPromise; + const confirmationResultExp = await confirmationResultPromise; return { - verificationId, + verificationId: confirmationResultExp.verificationId, confirm: (verificationCode: string) => - convertCredential(auth, confirm(verificationCode)) + convertCredential(auth, confirmationResultExp.confirm(verificationCode)) }; } diff --git a/packages-exp/auth-exp/demo/package.json b/packages-exp/auth-exp/demo/package.json index 7521dccb76c..9e292e63ba5 100644 --- a/packages-exp/auth-exp/demo/package.json +++ b/packages-exp/auth-exp/demo/package.json @@ -21,17 +21,17 @@ "@firebase/auth-exp": "0.0.800", "@firebase/auth-types-exp": "0.0.800", "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.2", + "@firebase/util": "0.3.3", "tslib": "^1.11.1" }, "license": "Apache-2.0", "devDependencies": { "@rollup/plugin-strip": "2.0.0", - "rollup": "2.29.0", + "rollup": "2.33.1", "@rollup/plugin-json": "4.1.0", "rollup-plugin-replace": "2.2.0", "rollup-plugin-terser": "6.1.0", - "rollup-plugin-typescript2": "0.27.3", + "rollup-plugin-typescript2": "0.29.0", "rollup-plugin-uglify": "6.0.4", "@rollup/plugin-node-resolve": "9.0.0", "lerna": "3.22.1" diff --git a/packages-exp/auth-exp/package.json b/packages-exp/auth-exp/package.json index 1f31949edc3..a3896343976 100644 --- a/packages-exp/auth-exp/package.json +++ b/packages-exp/auth-exp/package.json @@ -39,8 +39,8 @@ }, "dependencies": { "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.3", - "@firebase/component": "0.1.20", + "@firebase/util": "0.3.4", + "@firebase/component": "0.1.21", "@firebase/auth-types-exp": "0.0.800", "node-fetch": "2.6.1", "tslib": "^1.11.1" @@ -48,12 +48,12 @@ "license": "Apache-2.0", "devDependencies": { "@firebase/app-exp": "0.0.800", - "rollup": "2.29.0", + "rollup": "2.33.1", "@rollup/plugin-json": "4.1.0", "rollup-plugin-sourcemaps": "0.6.3", - "rollup-plugin-typescript2": "0.27.3", + "rollup-plugin-typescript2": "0.29.0", "@rollup/plugin-strip": "2.0.0", - "typescript": "4.0.2" + "typescript": "4.0.5" }, "repository": { "directory": "packages-exp/auth-exp", diff --git a/packages-exp/auth-exp/src/api/account_management/profile.test.ts b/packages-exp/auth-exp/src/api/account_management/profile.test.ts index dddeb07e9dd..085b5ac4d8a 100644 --- a/packages-exp/auth-exp/src/api/account_management/profile.test.ts +++ b/packages-exp/auth-exp/src/api/account_management/profile.test.ts @@ -33,7 +33,8 @@ describe('api/account_management/updateProfile', () => { const request = { idToken: 'my-token', email: 'test@foo.com', - password: 'my-password' + password: 'my-password', + returnSecureToken: true }; let auth: TestAuth; diff --git a/packages-exp/auth-exp/src/api/account_management/profile.ts b/packages-exp/auth-exp/src/api/account_management/profile.ts index 658223e1960..73c7cd2336b 100644 --- a/packages-exp/auth-exp/src/api/account_management/profile.ts +++ b/packages-exp/auth-exp/src/api/account_management/profile.ts @@ -23,6 +23,7 @@ export interface UpdateProfileRequest { idToken: string; displayName?: string | null; photoUrl?: string | null; + returnSecureToken: boolean; } export interface UpdateProfileResponse extends IdTokenResponse { diff --git a/packages-exp/auth-exp/src/api/authentication/custom_token.test.ts b/packages-exp/auth-exp/src/api/authentication/custom_token.test.ts index b1cd9d0b7fc..bc39e27c804 100644 --- a/packages-exp/auth-exp/src/api/authentication/custom_token.test.ts +++ b/packages-exp/auth-exp/src/api/authentication/custom_token.test.ts @@ -32,7 +32,8 @@ use(chaiAsPromised); describe('api/authentication/signInWithCustomToken', () => { const request = { - token: 'my-token' + token: 'my-token', + returnSecureToken: true }; let auth: TestAuth; diff --git a/packages-exp/auth-exp/src/api/authentication/custom_token.ts b/packages-exp/auth-exp/src/api/authentication/custom_token.ts index 42646197fde..40ea6192298 100644 --- a/packages-exp/auth-exp/src/api/authentication/custom_token.ts +++ b/packages-exp/auth-exp/src/api/authentication/custom_token.ts @@ -21,6 +21,7 @@ import { Auth } from '@firebase/auth-types-exp'; export interface SignInWithCustomTokenRequest { token: string; + returnSecureToken: boolean; } export interface SignInWithCustomTokenResponse extends IdTokenResponse {} diff --git a/packages-exp/auth-exp/src/core/auth/auth_impl.ts b/packages-exp/auth-exp/src/core/auth/auth_impl.ts index e7b4afecba5..4805e438c89 100644 --- a/packages-exp/auth-exp/src/core/auth/auth_impl.ts +++ b/packages-exp/auth-exp/src/core/auth/auth_impl.ts @@ -60,6 +60,7 @@ export class AuthImpl implements Auth, _FirebaseService { private idTokenSubscription = new Subscription(this); private redirectUser: User | null = null; private isProactiveRefreshEnabled = false; + private redirectInitializerError: Error | null = null; // Any network calls will set this to true and prevent subsequent emulator // initialization @@ -109,17 +110,21 @@ export class AuthImpl implements Auth, _FirebaseService { return; } - await this.initializeCurrentUser(); + await this.initializeCurrentUser(popupRedirectResolver); if (this._deleted) { return; } this._isInitialized = true; - this.notifyAuthListeners(); }); - return this._initializationPromise; + // After initialization completes, throw any error caused by redirect flow + return this._initializationPromise.then(() => { + if (this.redirectInitializerError) { + throw this.redirectInitializerError; + } + }); } /** @@ -149,18 +154,40 @@ export class AuthImpl implements Auth, _FirebaseService { // Update current Auth state. Either a new login or logout. await this._updateCurrentUser(user); - // Notify external Auth changes of Auth change event. - this.notifyAuthListeners(); } - private async initializeCurrentUser(): Promise { - const storedUser = (await this.assertedPersistence.getCurrentUser()) as User | null; + private async initializeCurrentUser( + popupRedirectResolver?: externs.PopupRedirectResolver + ): Promise { + // First check to see if we have a pending redirect event. + let storedUser = (await this.assertedPersistence.getCurrentUser()) as User | null; + if (popupRedirectResolver) { + await this.getOrInitRedirectPersistenceManager(); + const redirectUserEventId = this.redirectUser?._redirectEventId; + const storedUserEventId = storedUser?._redirectEventId; + const result = await this.tryRedirectSignIn(popupRedirectResolver); + + // If the stored user (i.e. the old "currentUser") has a redirectId that + // matches the redirect user, then we want to initially sign in with the + // new user object from result. + // TODO(samgho): More thoroughly test all of this + if ( + (!redirectUserEventId || redirectUserEventId === storedUserEventId) && + result?.user + ) { + storedUser = result.user as User; + } + } + + // If no user in persistence, there is no current user. Set to null. if (!storedUser) { - return this.directlySetCurrentUser(storedUser); + return this.directlySetCurrentUser(null); } if (!storedUser._redirectEventId) { // This isn't a redirect user, we can reload and bail + // This will also catch the redirected user, if available, as that method + // strips the _redirectEventId return this.reloadAndSetCurrentUserOrClear(storedUser); } @@ -182,6 +209,42 @@ export class AuthImpl implements Auth, _FirebaseService { return this.reloadAndSetCurrentUserOrClear(storedUser); } + private async tryRedirectSignIn( + redirectResolver: externs.PopupRedirectResolver + ): Promise { + // The redirect user needs to be checked (and signed in if available) + // during auth initialization. All of the normal sign in and link/reauth + // flows call back into auth and push things onto the promise queue. We + // need to await the result of the redirect sign in *inside the promise + // queue*. This presents a problem: we run into deadlock. See: + // ┌> [Initialization] ─────┐ + // ┌> [] │ + // └─ [getRedirectResult] <─┘ + // where [] are tasks on the queue and arrows denote awaits + // Initialization will never complete because it's waiting on something + // that's waiting for initialization to complete! + // + // Instead, this method calls getRedirectResult() (stored in + // _completeRedirectFn) with an optional parameter that instructs all of + // the underlying auth operations to skip anything that mutates auth state. + + let result: externs.UserCredential | null = null; + try { + // We know this._popupRedirectResolver is set since redirectResolver + // is passed in. The _completeRedirectFn expects the unwrapped extern. + result = await this._popupRedirectResolver!._completeRedirectFn( + this, + redirectResolver, + true + ); + } catch (e) { + this.redirectInitializerError = e; + await this._setRedirectUser(null); + } + + return result; + } + private async reloadAndSetCurrentUserOrClear(user: User): Promise { try { await _reloadWithoutSaving(user); @@ -243,6 +306,7 @@ export class AuthImpl implements Auth, _FirebaseService { { appName: this.name } ); } + return this.queue(async () => { await this.directlySetCurrentUser(user as User | null); this.notifyAuthListeners(); @@ -335,8 +399,11 @@ export class AuthImpl implements Auth, _FirebaseService { } async _redirectUserForId(id: string): Promise { - // Make sure we've cleared any pending ppersistence actions - await this.queue(async () => {}); + // Make sure we've cleared any pending persistence actions if we're not in + // the initializer + if (this._isInitialized) { + await this.queue(async () => {}); + } if (this._currentUser?._redirectEventId === id) { return this._currentUser; diff --git a/packages-exp/auth-exp/src/core/auth/firebase_internal.test.ts b/packages-exp/auth-exp/src/core/auth/firebase_internal.test.ts index 299159650e8..23545fcd5c3 100644 --- a/packages-exp/auth-exp/src/core/auth/firebase_internal.test.ts +++ b/packages-exp/auth-exp/src/core/auth/firebase_internal.test.ts @@ -56,6 +56,7 @@ describe('core/auth/firebase_internal', () => { const user = testUser(auth, 'uid'); await auth._updateCurrentUser(user); user.stsTokenManager.accessToken = 'access-token'; + user.stsTokenManager.refreshToken = 'refresh-token'; user.stsTokenManager.expirationTime = Date.now() + 1000 * 60 * 60 * 24; expect(await authInternal.getToken()).to.eql({ accessToken: 'access-token' diff --git a/packages-exp/auth-exp/src/core/auth/initialize.test.ts b/packages-exp/auth-exp/src/core/auth/initialize.test.ts index 001b9538f3f..65b2bcc4f9f 100644 --- a/packages-exp/auth-exp/src/core/auth/initialize.test.ts +++ b/packages-exp/auth-exp/src/core/auth/initialize.test.ts @@ -106,6 +106,13 @@ describe('core/auth/initialize', () => { ): void { cb(true); } + async _completeRedirectFn( + _auth: externs.Auth, + _resolver: externs.PopupRedirectResolver, + _bypassAuthState: boolean + ): Promise { + return null; + } } const fakePopupRedirectResolver: externs.PopupRedirectResolver = FakePopupRedirectResolver; diff --git a/packages-exp/auth-exp/src/core/index.ts b/packages-exp/auth-exp/src/core/index.ts index fdddd706ac1..f8ef50f4e94 100644 --- a/packages-exp/auth-exp/src/core/index.ts +++ b/packages-exp/auth-exp/src/core/index.ts @@ -18,8 +18,6 @@ import * as externs from '@firebase/auth-types-exp'; import { CompleteFn, ErrorFn, Unsubscribe } from '@firebase/util'; -export { AuthErrorCode } from './errors'; - // Non-optional auth methods. /** * Changes the type of persistence on the Auth instance for the currently saved diff --git a/packages-exp/auth-exp/src/core/providers/google.test.ts b/packages-exp/auth-exp/src/core/providers/google.test.ts index 9b250c9296e..2e4ce5f39f8 100644 --- a/packages-exp/auth-exp/src/core/providers/google.test.ts +++ b/packages-exp/auth-exp/src/core/providers/google.test.ts @@ -39,6 +39,12 @@ describe('core/providers/google', () => { expect(cred.signInMethod).to.eq(SignInMethod.GOOGLE); }); + it('adds the profile scope by default', () => { + const provider = new GoogleAuthProvider(); + expect(provider.providerId).to.eq(ProviderId.GOOGLE); + expect(provider.getScopes()).to.eql(['profile']); + }); + it('credentialFromResult creates the cred from a tagged result', async () => { const auth = await testAuth(); const userCred = new UserCredentialImpl({ diff --git a/packages-exp/auth-exp/src/core/providers/google.ts b/packages-exp/auth-exp/src/core/providers/google.ts index 00514db9098..0d25e2779a1 100644 --- a/packages-exp/auth-exp/src/core/providers/google.ts +++ b/packages-exp/auth-exp/src/core/providers/google.ts @@ -73,6 +73,7 @@ export class GoogleAuthProvider extends OAuthProvider { constructor() { super(externs.ProviderId.GOOGLE); + this.addScope('profile'); } /** diff --git a/packages-exp/auth-exp/src/core/strategies/credential.test.ts b/packages-exp/auth-exp/src/core/strategies/credential.test.ts index 3671680f5da..d3e8e892a68 100644 --- a/packages-exp/auth-exp/src/core/strategies/credential.test.ts +++ b/packages-exp/auth-exp/src/core/strategies/credential.test.ts @@ -42,7 +42,8 @@ import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors'; import { linkWithCredential, reauthenticateWithCredential, - signInWithCredential + signInWithCredential, + _signInWithCredential } from './credential'; use(chaiAsPromised); @@ -111,6 +112,15 @@ describe('core/strategies/credential', () => { expect(auth.currentUser).to.eq(user); }); + it('does not update the current user if bypass is true', async () => { + stub(authCredential, '_getIdTokenResponse').returns( + Promise.resolve(idTokenResponse) + ); + const { user } = await _signInWithCredential(auth, authCredential, true); + expect(auth.currentUser).to.be.null; + expect(user).not.to.be.null; + }); + it('should handle MFA', async () => { const serverResponse: IdTokenMfaResponse = { localId: 'uid', diff --git a/packages-exp/auth-exp/src/core/strategies/credential.ts b/packages-exp/auth-exp/src/core/strategies/credential.ts index 012218eb382..e97f2880063 100644 --- a/packages-exp/auth-exp/src/core/strategies/credential.ts +++ b/packages-exp/auth-exp/src/core/strategies/credential.ts @@ -30,7 +30,8 @@ import { _castAuth } from '../auth/auth_impl'; /** @internal */ export async function _signInWithCredential( auth: Auth, - credential: AuthCredential + credential: AuthCredential, + bypassAuthState = false ): Promise { const operationType = OperationType.SIGN_IN; const response = await _processCredentialSavingMfaContextIfNecessary( @@ -43,7 +44,10 @@ export async function _signInWithCredential( operationType, response ); - await auth._updateCurrentUser(userCredential.user); + + if (!bypassAuthState) { + await auth._updateCurrentUser(userCredential.user); + } return userCredential; } diff --git a/packages-exp/auth-exp/src/core/strategies/custom_token.test.ts b/packages-exp/auth-exp/src/core/strategies/custom_token.test.ts index 0491b965ed8..39a789df543 100644 --- a/packages-exp/auth-exp/src/core/strategies/custom_token.test.ts +++ b/packages-exp/auth-exp/src/core/strategies/custom_token.test.ts @@ -52,11 +52,15 @@ describe('core/strategies/signInWithCustomToken', () => { }; let auth: TestAuth; + let signInRoute: mockFetch.Route; beforeEach(async () => { auth = await testAuth(); mockFetch.setUp(); - mockEndpoint(Endpoint.SIGN_IN_WITH_CUSTOM_TOKEN, idTokenResponse); + signInRoute = mockEndpoint( + Endpoint.SIGN_IN_WITH_CUSTOM_TOKEN, + idTokenResponse + ); mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { users: [serverUser] }); @@ -78,6 +82,14 @@ describe('core/strategies/signInWithCustomToken', () => { expect(operationType).to.eq(OperationType.SIGN_IN); }); + it('should send with a valid request', async () => { + await signInWithCustomToken(auth, 'j.w.t'); + expect(signInRoute.calls[0].request).to.eql({ + token: 'j.w.t', + returnSecureToken: true + }); + }); + it('should update the current user', async () => { const { user } = await signInWithCustomToken(auth, 'oh.no'); expect(auth.currentUser).to.eq(user); diff --git a/packages-exp/auth-exp/src/core/strategies/custom_token.ts b/packages-exp/auth-exp/src/core/strategies/custom_token.ts index 919a638e8b6..8ba413f6479 100644 --- a/packages-exp/auth-exp/src/core/strategies/custom_token.ts +++ b/packages-exp/auth-exp/src/core/strategies/custom_token.ts @@ -43,7 +43,8 @@ export async function signInWithCustomToken( customToken: string ): Promise { const response: IdTokenResponse = await getIdTokenResponse(auth, { - token: customToken + token: customToken, + returnSecureToken: true }); const authInternal = _castAuth(auth); const cred = await UserCredentialImpl._fromIdTokenResponse( diff --git a/packages-exp/auth-exp/src/core/strategies/idp.test.ts b/packages-exp/auth-exp/src/core/strategies/idp.test.ts index 3f781b55bac..142233ed86e 100644 --- a/packages-exp/auth-exp/src/core/strategies/idp.test.ts +++ b/packages-exp/auth-exp/src/core/strategies/idp.test.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; @@ -27,9 +29,15 @@ import { testAuth, testUser, TestAuth } from '../../../test/helpers/mock_auth'; import * as fetch from '../../../test/helpers/mock_fetch'; import { Endpoint } from '../../api'; import { User } from '../../model/user'; +import * as reauthenticate from '../../core/user/reauthenticate'; +import * as linkUnlink from '../../core/user/link_unlink'; +import * as credential from '../../core/strategies/credential'; + import * as idpTasks from './idp'; +import { UserCredentialImpl } from '../user/user_credential_impl'; use(chaiAsPromised); +use(sinonChai); describe('core/strategies/idb', () => { let auth: TestAuth; @@ -54,6 +62,7 @@ describe('core/strategies/idb', () => { afterEach(() => { fetch.tearDown(); + sinon.restore(); }); describe('signIn', () => { @@ -90,6 +99,23 @@ describe('core/strategies/idb', () => { expect(userCred.operationType).to.eq(OperationType.SIGN_IN); expect(userCred.user.uid).to.eq('uid'); }); + + it('passes through the bypassAuthState flag', async () => { + const stub = sinon + .stub(credential, '_signInWithCredential') + .returns(Promise.resolve(({} as unknown) as UserCredentialImpl)); + await idpTasks._signIn({ + auth, + user, + requestUri: 'request-uri', + sessionId: 'session-id', + tenantId: 'tenant-id', + pendingToken: 'pending-token', + postBody: 'post-body', + bypassAuthState: true + }); + expect(stub.getCall(0).lastArg).to.be.true; + }); }); describe('reauth', () => { @@ -128,6 +154,23 @@ describe('core/strategies/idb', () => { expect(userCred.operationType).to.eq(OperationType.REAUTHENTICATE); expect(userCred.user.uid).to.eq('uid'); }); + + it('passes through the bypassAuthState flag', async () => { + const stub = sinon + .stub(reauthenticate, '_reauthenticate') + .returns(Promise.resolve(({} as unknown) as UserCredentialImpl)); + await idpTasks._reauth({ + auth, + user, + requestUri: 'request-uri', + sessionId: 'session-id', + tenantId: 'tenant-id', + pendingToken: 'pending-token', + postBody: 'post-body', + bypassAuthState: true + }); + expect(stub.getCall(0).lastArg).to.be.true; + }); }); describe('link', () => { @@ -168,5 +211,22 @@ describe('core/strategies/idb', () => { expect(userCred.operationType).to.eq(OperationType.LINK); expect(userCred.user.uid).to.eq('uid'); }); + + it('passes through the bypassAuthState flag', async () => { + const stub = sinon + .stub(linkUnlink, '_link') + .returns(Promise.resolve(({} as unknown) as UserCredentialImpl)); + await idpTasks._link({ + auth, + user, + requestUri: 'request-uri', + sessionId: 'session-id', + tenantId: 'tenant-id', + pendingToken: 'pending-token', + postBody: 'post-body', + bypassAuthState: true + }); + expect(stub.getCall(0).lastArg).to.be.true; + }); }); }); diff --git a/packages-exp/auth-exp/src/core/strategies/idp.ts b/packages-exp/auth-exp/src/core/strategies/idp.ts index 2312e7cf75d..3fc164532cf 100644 --- a/packages-exp/auth-exp/src/core/strategies/idp.ts +++ b/packages-exp/auth-exp/src/core/strategies/idp.ts @@ -40,6 +40,7 @@ export interface IdpTaskParams { postBody?: string; pendingToken?: string; user?: User; + bypassAuthState?: boolean; } /** @internal */ @@ -85,7 +86,8 @@ class IdpCredential extends AuthCredential { export function _signIn(params: IdpTaskParams): Promise { return _signInWithCredential( params.auth, - new IdpCredential(params) + new IdpCredential(params), + params.bypassAuthState ) as Promise; } @@ -93,12 +95,16 @@ export function _signIn(params: IdpTaskParams): Promise { export function _reauth(params: IdpTaskParams): Promise { const { auth, user } = params; assert(user, AuthErrorCode.INTERNAL_ERROR, { appName: auth.name }); - return _reauthenticate(user, new IdpCredential(params)); + return _reauthenticate( + user, + new IdpCredential(params), + params.bypassAuthState + ); } /** @internal */ export async function _link(params: IdpTaskParams): Promise { const { auth, user } = params; assert(user, AuthErrorCode.INTERNAL_ERROR, { appName: auth.name }); - return _linkUser(user, new IdpCredential(params)); + return _linkUser(user, new IdpCredential(params), params.bypassAuthState); } diff --git a/packages-exp/auth-exp/src/core/user/account_info.test.ts b/packages-exp/auth-exp/src/core/user/account_info.test.ts index 3744bbbff73..d87de40c490 100644 --- a/packages-exp/auth-exp/src/core/user/account_info.test.ts +++ b/packages-exp/auth-exp/src/core/user/account_info.test.ts @@ -73,7 +73,8 @@ describe('core/user/profile', () => { expect(ep.calls[0].request).to.eql({ idToken: 'access-token', displayName: 'displayname', - photoUrl: 'photo' + photoUrl: 'photo', + returnSecureToken: true }); }); @@ -118,7 +119,8 @@ describe('core/user/profile', () => { await updateEmail(user, 'hello@test.com'); expect(set.calls[0].request).to.eql({ idToken: 'access-token', - email: 'hello@test.com' + email: 'hello@test.com', + returnSecureToken: true }); expect(user.uid).to.eq('new-uid-to-prove-refresh-got-called'); @@ -135,7 +137,8 @@ describe('core/user/profile', () => { await updatePassword(user, 'pass'); expect(set.calls[0].request).to.eql({ idToken: 'access-token', - password: 'pass' + password: 'pass', + returnSecureToken: true }); expect(user.uid).to.eq('new-uid-to-prove-refresh-got-called'); diff --git a/packages-exp/auth-exp/src/core/user/account_info.ts b/packages-exp/auth-exp/src/core/user/account_info.ts index 3829082509b..1868eea5739 100644 --- a/packages-exp/auth-exp/src/core/user/account_info.ts +++ b/packages-exp/auth-exp/src/core/user/account_info.ts @@ -49,7 +49,12 @@ export async function updateProfile( const userInternal = user as User; const idToken = await user.getIdToken(); - const profileRequest = { idToken, displayName, photoUrl }; + const profileRequest = { + idToken, + displayName, + photoUrl, + returnSecureToken: true + }; const response = await _logoutIfInvalidated( userInternal, apiUpdateProfile(userInternal.auth, profileRequest) @@ -121,7 +126,10 @@ async function updateEmailOrPassword( ): Promise { const { auth } = user; const idToken = await user.getIdToken(); - const request: UpdateEmailPasswordRequest = { idToken }; + const request: UpdateEmailPasswordRequest = { + idToken, + returnSecureToken: true + }; if (email) { request.email = email; diff --git a/packages-exp/auth-exp/src/core/user/additional_user_info.test.ts b/packages-exp/auth-exp/src/core/user/additional_user_info.test.ts index 16107569707..642a443ef22 100644 --- a/packages-exp/auth-exp/src/core/user/additional_user_info.test.ts +++ b/packages-exp/auth-exp/src/core/user/additional_user_info.test.ts @@ -33,6 +33,7 @@ import { UserCredentialImpl } from './user_credential_impl'; import { Auth } from '../../model/auth'; import { User, UserCredential } from '../../model/user'; import { testAuth, testUser } from '../../../test/helpers/mock_auth'; +import { makeJWT } from '../../../test/helpers/jwt'; describe('core/user/additional_user_info', () => { const userProfileWithLogin: UserProfile = { @@ -158,8 +159,12 @@ describe('core/user/additional_user_info', () => { describe('creates generic AdditionalUserInfo', () => { it('for custom auth', () => { const idResponse = idTokenResponse({ - providerId: ProviderId.CUSTOM, - rawUserInfo: rawUserInfoWithLogin + rawUserInfo: rawUserInfoWithLogin, + idToken: makeJWT({ + firebase: { + 'sign_in_provider': 'custom' + } + }) }); const { isNewUser, @@ -175,8 +180,12 @@ describe('core/user/additional_user_info', () => { it('for anonymous auth', () => { const idResponse = idTokenResponse({ - providerId: ProviderId.ANONYMOUS, - rawUserInfo: rawUserInfoWithLogin + rawUserInfo: rawUserInfoWithLogin, + idToken: makeJWT({ + firebase: { + 'sign_in_provider': 'anonymous' + } + }) }); const { isNewUser, diff --git a/packages-exp/auth-exp/src/core/user/additional_user_info.ts b/packages-exp/auth-exp/src/core/user/additional_user_info.ts index 7e1badb6b62..1405629cf48 100644 --- a/packages-exp/auth-exp/src/core/user/additional_user_info.ts +++ b/packages-exp/auth-exp/src/core/user/additional_user_info.ts @@ -44,8 +44,8 @@ export function _fromIdTokenResponse( ]; if (signInProvider) { const filteredProviderId = - providerId !== externs.ProviderId.ANONYMOUS && - providerId !== externs.ProviderId.CUSTOM + signInProvider !== externs.ProviderId.ANONYMOUS && + signInProvider !== externs.ProviderId.CUSTOM ? (signInProvider as externs.ProviderId) : null; // Uses generic class in accordance with the legacy SDK. diff --git a/packages-exp/auth-exp/src/core/user/invalidation.test.ts b/packages-exp/auth-exp/src/core/user/invalidation.test.ts index 3629f0887ec..05f83bd4b5f 100644 --- a/packages-exp/auth-exp/src/core/user/invalidation.test.ts +++ b/packages-exp/auth-exp/src/core/user/invalidation.test.ts @@ -63,6 +63,14 @@ describe('core/user/invalidation', () => { expect(auth.currentUser).to.be.null; }); + it('does not log out if bypass auth state is true', async () => { + const error = makeError(AuthErrorCode.USER_DISABLED); + try { + await _logoutIfInvalidated(user, Promise.reject(error), true); + } catch {} + expect(auth.currentUser).to.eq(user); + }); + it('logs out the user if the error is token_expired', async () => { const error = makeError(AuthErrorCode.TOKEN_EXPIRED); await expect( diff --git a/packages-exp/auth-exp/src/core/user/invalidation.ts b/packages-exp/auth-exp/src/core/user/invalidation.ts index 74d29dab455..0ae3c3a4eaa 100644 --- a/packages-exp/auth-exp/src/core/user/invalidation.ts +++ b/packages-exp/auth-exp/src/core/user/invalidation.ts @@ -22,8 +22,12 @@ import { AuthErrorCode } from '../errors'; export async function _logoutIfInvalidated( user: User, - promise: Promise + promise: Promise, + bypassAuthState = false ): Promise { + if (bypassAuthState) { + return promise; + } try { return await promise; } catch (e) { diff --git a/packages-exp/auth-exp/src/core/user/link_unlink.ts b/packages-exp/auth-exp/src/core/user/link_unlink.ts index 077c5a97e62..48a52fb5328 100644 --- a/packages-exp/auth-exp/src/core/user/link_unlink.ts +++ b/packages-exp/auth-exp/src/core/user/link_unlink.ts @@ -63,11 +63,13 @@ export async function unlink( /** @internal */ export async function _link( user: User, - credential: AuthCredential + credential: AuthCredential, + bypassAuthState = false ): Promise { const response = await _logoutIfInvalidated( user, - credential._linkToIdToken(user.auth, await user.getIdToken()) + credential._linkToIdToken(user.auth, await user.getIdToken()), + bypassAuthState ); return UserCredentialImpl._forOperation( user, diff --git a/packages-exp/auth-exp/src/core/user/reauthenticate.ts b/packages-exp/auth-exp/src/core/user/reauthenticate.ts index 55982a749fd..296d89a8442 100644 --- a/packages-exp/auth-exp/src/core/user/reauthenticate.ts +++ b/packages-exp/auth-exp/src/core/user/reauthenticate.ts @@ -28,7 +28,8 @@ import { UserCredentialImpl } from './user_credential_impl'; export async function _reauthenticate( user: User, - credential: AuthCredential + credential: AuthCredential, + bypassAuthState = false ): Promise { const appName = user.auth.name; const operationType = OperationType.REAUTHENTICATE; @@ -41,7 +42,8 @@ export async function _reauthenticate( operationType, credential, user - ) + ), + bypassAuthState ); assert(response.idToken, AuthErrorCode.INTERNAL_ERROR, { appName }); const parsed = _parseToken(response.idToken); diff --git a/packages-exp/auth-exp/src/core/user/reload.test.ts b/packages-exp/auth-exp/src/core/user/reload.test.ts index f4983879b16..d0bcf69f090 100644 --- a/packages-exp/auth-exp/src/core/user/reload.test.ts +++ b/packages-exp/auth-exp/src/core/user/reload.test.ts @@ -32,6 +32,7 @@ import { } from '../../api/account_management/account'; import { _reloadWithoutSaving, reload } from './reload'; import { UserMetadata } from './user_metadata'; +import { User } from '../../model/user'; use(chaiAsPromised); use(sinonChai); @@ -166,4 +167,55 @@ describe('core/user/reload', () => { expect(cb).to.have.been.calledWith(user); expect(auth.persistenceLayer.lastObjectSet).to.eql(user.toJSON()); }); + + context('anonymous carryover', () => { + let user: User; + beforeEach(() => { + user = testUser(auth, 'abc', '', true); + }); + function setup( + isAnonStart: boolean, + emailStart: string, + passwordHash: string, + providerData: Array<{ providerId: string }> + ): void { + // Get around readonly property + const mutUser = (user as unknown) as Record; + mutUser.isAnonymous = isAnonStart; + mutUser.email = emailStart; + + mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { + users: [ + { + providerUserInfo: [...providerData], + passwordHash + } + ] + }); + } + + it('user stays not anonymous even if reload user is', async () => { + setup(false, '', '', []); // After reload the user would count as anon + await _reloadWithoutSaving(user); + expect(user.isAnonymous).to.be.false; + }); + + it('user stays anonymous if reload user is anonymous', async () => { + setup(true, '', '', []); // After reload the user would count as anon + await _reloadWithoutSaving(user); + expect(user.isAnonymous).to.be.true; + }); + + it('user becomes not anonymous if reload user is not', async () => { + setup(true, '', '', [{ providerId: 'google' }]); // After reload the user would count as anon + await _reloadWithoutSaving(user); + expect(user.isAnonymous).to.be.false; + }); + + it('user becomes not anonymous if password hash set', async () => { + setup(true, 'email', 'pass', [{ providerId: 'google' }]); // After reload the user would count as anon + await _reloadWithoutSaving(user); + expect(user.isAnonymous).to.be.false; + }); + }); }); diff --git a/packages-exp/auth-exp/src/core/user/reload.ts b/packages-exp/auth-exp/src/core/user/reload.ts index b0d25465b79..8dd0ffd653b 100644 --- a/packages-exp/auth-exp/src/core/user/reload.ts +++ b/packages-exp/auth-exp/src/core/user/reload.ts @@ -46,6 +46,19 @@ export async function _reloadWithoutSaving(user: User): Promise { const newProviderData = coreAccount.providerUserInfo?.length ? extractProviderData(coreAccount.providerUserInfo) : []; + + const providerData = mergeProviderData(user.providerData, newProviderData); + + // Preserves the non-nonymous status of the stored user, even if no more + // credentials (federated or email/password) are linked to the user. If + // the user was previously anonymous, then use provider data to update. + // On the other hand, if it was not anonymous before, it should never be + // considered anonymous now. + const oldIsAnonymous = user.isAnonymous; + const newIsAnonymous = + !(user.email && coreAccount.passwordHash) && !providerData?.length; + const isAnonymous = !oldIsAnonymous ? false : newIsAnonymous; + const updates: Partial = { uid: coreAccount.localId, displayName: coreAccount.displayName || null, @@ -54,8 +67,9 @@ export async function _reloadWithoutSaving(user: User): Promise { emailVerified: coreAccount.emailVerified || false, phoneNumber: coreAccount.phoneNumber || null, tenantId: coreAccount.tenantId || null, - providerData: mergeProviderData(user.providerData, newProviderData), - metadata: new UserMetadata(coreAccount.createdAt, coreAccount.lastLoginAt) + providerData, + metadata: new UserMetadata(coreAccount.createdAt, coreAccount.lastLoginAt), + isAnonymous }; Object.assign(user, updates); diff --git a/packages-exp/auth-exp/src/core/user/token_manager.ts b/packages-exp/auth-exp/src/core/user/token_manager.ts index 289889e6029..4ee43be10c3 100644 --- a/packages-exp/auth-exp/src/core/user/token_manager.ts +++ b/packages-exp/auth-exp/src/core/user/token_manager.ts @@ -56,19 +56,24 @@ export class StsTokenManager { } async getToken(auth: Auth, forceRefresh = false): Promise { + assert( + !this.accessToken || this.refreshToken, + AuthErrorCode.TOKEN_EXPIRED, + { + appName: auth.name + } + ); + if (!forceRefresh && this.accessToken && !this.isExpired) { return this.accessToken; } - if (!this.refreshToken) { - assert(!this.accessToken, AuthErrorCode.TOKEN_EXPIRED, { - appName: auth.name - }); - return null; + if (this.refreshToken) { + await this.refresh(auth, this.refreshToken!); + return this.accessToken; } - await this.refresh(auth, this.refreshToken); - return this.accessToken; + return null; } clearRefreshToken(): void { diff --git a/packages-exp/auth-exp/src/model/popup_redirect.ts b/packages-exp/auth-exp/src/model/popup_redirect.ts index 6aa79da7a57..0376b866350 100644 --- a/packages-exp/auth-exp/src/model/popup_redirect.ts +++ b/packages-exp/auth-exp/src/model/popup_redirect.ts @@ -105,4 +105,11 @@ export interface PopupRedirectResolver extends externs.PopupRedirectResolver { cb: (support: boolean) => unknown ): void; _redirectPersistence: externs.Persistence; + + // This is needed so that auth does not have a hard dependency on redirect + _completeRedirectFn: ( + auth: externs.Auth, + resolver: externs.PopupRedirectResolver, + bypassAuthState: boolean + ) => Promise; } diff --git a/packages-exp/auth-exp/src/platform_browser/auth.test.ts b/packages-exp/auth-exp/src/platform_browser/auth.test.ts index d6cd470d6d4..9aa8017da34 100644 --- a/packages-exp/auth-exp/src/platform_browser/auth.test.ts +++ b/packages-exp/auth-exp/src/platform_browser/auth.test.ts @@ -37,6 +37,9 @@ import { _getInstance } from '../core/util/instantiator'; import { _getClientVersion, ClientPlatform } from '../core/util/version'; import { Auth } from '../model/auth'; import { browserPopupRedirectResolver } from './popup_redirect'; +import { PopupRedirectResolver } from '../model/popup_redirect'; +import { UserCredentialImpl } from '../core/user/user_credential_impl'; +import { User } from '../model/user'; use(sinonChai); use(chaiAsPromised); @@ -96,6 +99,7 @@ describe('core/auth/initializeAuth', () => { let createManagerStub: sinon.SinonSpy; let reloadStub: sinon.SinonStub; let oldAuth: Auth; + let completeRedirectFnStub: sinon.SinonStub; beforeEach(async () => { oldAuth = await testAuth(); @@ -103,6 +107,12 @@ describe('core/auth/initializeAuth', () => { reloadStub = sinon .stub(reload, '_reloadWithoutSaving') .returns(Promise.resolve()); + completeRedirectFnStub = sinon + .stub( + _getInstance(browserPopupRedirectResolver), + '_completeRedirectFn' + ) + .returns(Promise.resolve(null)); }); async function initAndWait( @@ -255,5 +265,39 @@ describe('core/auth/initializeAuth', () => { sdkClientVersion: _getClientVersion(ClientPlatform.BROWSER) }); }); + + context('#tryRedirectSignIn', () => { + it('returns null and clears the redirect user in case of error', async () => { + const stub = sinon.stub( + _getInstance(browserSessionPersistence) + ); + stub._remove.returns(Promise.resolve()); + completeRedirectFnStub.returns(Promise.reject(new Error('no'))); + + await initAndWait([inMemoryPersistence], browserPopupRedirectResolver); + expect(stub._remove).to.have.been.called; + }); + + it('signs in the redirect user if found', async () => { + let user: User | null = null; + completeRedirectFnStub.callsFake((auth: Auth) => { + user = testUser(auth, 'uid', 'redirectUser@test.com'); + return Promise.resolve( + new UserCredentialImpl({ + operationType: externs.OperationType.SIGN_IN, + user, + providerId: null + }) + ); + }); + + const auth = await initAndWait( + [inMemoryPersistence], + browserPopupRedirectResolver + ); + expect(user).not.to.be.null; + expect(auth.currentUser).to.eq(user); + }); + }); }); }); diff --git a/packages-exp/auth-exp/src/platform_browser/popup_redirect.ts b/packages-exp/auth-exp/src/platform_browser/popup_redirect.ts index f92d5021370..1e01eee7eb5 100644 --- a/packages-exp/auth-exp/src/platform_browser/popup_redirect.ts +++ b/packages-exp/auth-exp/src/platform_browser/popup_redirect.ts @@ -40,6 +40,7 @@ import { _setWindowLocation } from './auth_window'; import { _openIframe } from './iframe/iframe'; import { browserSessionPersistence } from './persistence/session_storage'; import { _open, AuthPopup } from './util/popup'; +import { _getRedirectResult } from './strategies/redirect'; /** * URL for Authentication widget which will initiate the OAuth handshake @@ -198,6 +199,8 @@ class BrowserPopupRedirectResolver implements PopupRedirectResolver { return this.originValidationPromises[key]; } + + _completeRedirectFn = _getRedirectResult; } /** diff --git a/packages-exp/auth-exp/src/platform_browser/strategies/abstract_popup_redirect_operation.test.ts b/packages-exp/auth-exp/src/platform_browser/strategies/abstract_popup_redirect_operation.test.ts index 4b3efec3e13..b7ff55ee096 100644 --- a/packages-exp/auth-exp/src/platform_browser/strategies/abstract_popup_redirect_operation.test.ts +++ b/packages-exp/auth-exp/src/platform_browser/strategies/abstract_popup_redirect_operation.test.ts @@ -185,7 +185,8 @@ describe('platform_browser/strategies/abstract_popup_redirect_operation', () => sessionId: BASE_AUTH_EVENT.sessionId!, tenantId: BASE_AUTH_EVENT.tenantId || undefined, postBody: BASE_AUTH_EVENT.postBody || undefined, - user: undefined + user: undefined, + bypassAuthState: false }; } @@ -236,6 +237,25 @@ describe('platform_browser/strategies/abstract_popup_redirect_operation', () => await operation.execute(); expect(idp._reauth).to.have.been.calledWith(expectedIdpTaskParams()); }); + + it('includes the bypassAuthState parameter', async () => { + operation = new WrapperOperation( + auth, + AuthEventType.REAUTH_VIA_REDIRECT, + resolver, + undefined, + /** bypassAuthState */ true + ); + + const type = AuthEventType.REAUTH_VIA_REDIRECT; + updateFilter(type); + finishPromise(authEvent({ type })); + await operation.execute(); + expect(idp._reauth).to.have.been.calledWith({ + ...expectedIdpTaskParams(), + bypassAuthState: true + }); + }); }); }); }); diff --git a/packages-exp/auth-exp/src/platform_browser/strategies/abstract_popup_redirect_operation.ts b/packages-exp/auth-exp/src/platform_browser/strategies/abstract_popup_redirect_operation.ts index 9a9260e066d..10fbc323901 100644 --- a/packages-exp/auth-exp/src/platform_browser/strategies/abstract_popup_redirect_operation.ts +++ b/packages-exp/auth-exp/src/platform_browser/strategies/abstract_popup_redirect_operation.ts @@ -57,7 +57,8 @@ export abstract class AbstractPopupRedirectOperation protected readonly auth: Auth, filter: AuthEventType | AuthEventType[], protected readonly resolver: PopupRedirectResolver, - protected user?: User + protected user?: User, + private readonly bypassAuthState = false ) { this.filter = Array.isArray(filter) ? filter : [filter]; } @@ -91,7 +92,8 @@ export abstract class AbstractPopupRedirectOperation sessionId: sessionId!, tenantId: tenantId || undefined, postBody: postBody || undefined, - user: this.user + user: this.user, + bypassAuthState: this.bypassAuthState }; try { diff --git a/packages-exp/auth-exp/src/platform_browser/strategies/phone.test.ts b/packages-exp/auth-exp/src/platform_browser/strategies/phone.test.ts index cd4e6a2ac95..c4974872e9f 100644 --- a/packages-exp/auth-exp/src/platform_browser/strategies/phone.test.ts +++ b/packages-exp/auth-exp/src/platform_browser/strategies/phone.test.ts @@ -424,7 +424,8 @@ describe('platform_browser/strategies/phone', () => { users: [{ uid: 'uid' }] }); signInMock = mockEndpoint(Endpoint.SIGN_IN_WITH_PHONE_NUMBER, { - idToken: 'new-access-token' + idToken: 'new-access-token', + refreshToken: 'refresh-token' }); credential = PhoneAuthCredential._fromVerification( 'session-info', diff --git a/packages-exp/auth-exp/src/platform_browser/strategies/redirect.test.ts b/packages-exp/auth-exp/src/platform_browser/strategies/redirect.test.ts index 39573e9e69e..3d17b7fbee6 100644 --- a/packages-exp/auth-exp/src/platform_browser/strategies/redirect.test.ts +++ b/packages-exp/auth-exp/src/platform_browser/strategies/redirect.test.ts @@ -51,7 +51,8 @@ import { getRedirectResult, linkWithRedirect, reauthenticateWithRedirect, - signInWithRedirect + signInWithRedirect, + _getRedirectResult } from './redirect'; import { FirebaseError } from '@firebase/util'; @@ -426,5 +427,29 @@ describe('platform_browser/strategies/redirect', () => { expect(auth.persistenceLayer.lastObjectSet?._redirectEventId).to.be .undefined; }); + + it('does not mutate authstate if bypassAuthState is true', async () => { + await reInitAuthWithRedirectUser(MATCHING_EVENT_ID); + const redirectPersistence: Persistence = _getInstance( + RedirectPersistence + ); + sinon.spy(redirectPersistence, '_remove'); + + const cred = new UserCredentialImpl({ + user: auth._currentUser!, + providerId: externs.ProviderId.GOOGLE, + operationType: externs.OperationType.LINK + }); + idpStubs._link.returns(Promise.resolve(cred)); + const promise = _getRedirectResult(auth, resolver, true); + iframeEvent({ + type: AuthEventType.LINK_VIA_REDIRECT + }); + expect(await promise).to.eq(cred); + expect(redirectPersistence._remove).not.to.have.been.called; + expect(auth._currentUser?._redirectEventId).not.to.be.undefined; + expect(auth.persistenceLayer.lastObjectSet?._redirectEventId).not.to.be + .undefined; + }); }); }); diff --git a/packages-exp/auth-exp/src/platform_browser/strategies/redirect.ts b/packages-exp/auth-exp/src/platform_browser/strategies/redirect.ts index b6c7615f345..f7217f5b599 100644 --- a/packages-exp/auth-exp/src/platform_browser/strategies/redirect.ts +++ b/packages-exp/auth-exp/src/platform_browser/strategies/redirect.ts @@ -230,16 +230,24 @@ export async function linkWithRedirect( export async function getRedirectResult( auth: externs.Auth, resolver?: externs.PopupRedirectResolver +): Promise { + return _getRedirectResult(auth, resolver, false); +} + +export async function _getRedirectResult( + auth: externs.Auth, + resolverExtern?: externs.PopupRedirectResolver, + bypassAuthState = false ): Promise { const authInternal = _castAuth(auth); - const resolverInternal = _withDefaultResolver(authInternal, resolver); - const action = new RedirectAction(authInternal, resolverInternal); + const resolver = _withDefaultResolver(authInternal, resolverExtern); + const action = new RedirectAction(authInternal, resolver, bypassAuthState); const result = await action.execute(); - if (result) { + if (result && !bypassAuthState) { delete result.user._redirectEventId; await authInternal._persistUserIfCurrent(result.user as User); - await authInternal._setRedirectUser(null, resolver); + await authInternal._setRedirectUser(null, resolverExtern); } return result; @@ -264,7 +272,11 @@ const redirectOutcomeMap: Map< class RedirectAction extends AbstractPopupRedirectOperation { eventId = null; - constructor(auth: Auth, resolver: PopupRedirectResolver) { + constructor( + auth: Auth, + resolver: PopupRedirectResolver, + bypassAuthState = false + ) { super( auth, [ @@ -273,7 +285,9 @@ class RedirectAction extends AbstractPopupRedirectOperation { AuthEventType.REAUTH_VIA_REDIRECT, AuthEventType.UNKNOWN ], - resolver + resolver, + undefined, + bypassAuthState ); } diff --git a/packages-exp/auth-exp/test/helpers/mock_popup_redirect_resolver.ts b/packages-exp/auth-exp/test/helpers/mock_popup_redirect_resolver.ts index 0d0aeb36fbd..ccae9824bc6 100644 --- a/packages-exp/auth-exp/test/helpers/mock_popup_redirect_resolver.ts +++ b/packages-exp/auth-exp/test/helpers/mock_popup_redirect_resolver.ts @@ -49,5 +49,7 @@ export function makeMockPopupRedirectResolver( } _redirectPersistence?: Persistence; + + async _completeRedirectFn(): Promise {} }; } diff --git a/packages-exp/auth-exp/test/integration/flows/phone.test.ts b/packages-exp/auth-exp/test/integration/flows/phone.test.ts index 297b4c368fc..48dd3bd888c 100644 --- a/packages-exp/auth-exp/test/integration/flows/phone.test.ts +++ b/packages-exp/auth-exp/test/integration/flows/phone.test.ts @@ -107,7 +107,8 @@ describe('Integration test: phone auth', () => { await unlink(user, ProviderId.PHONE); expect(auth.currentUser!.uid).to.eq(anonId); - expect(auth.currentUser!.isAnonymous).to.be.true; + // Is anonymous stays false even after unlinking + expect(auth.currentUser!.isAnonymous).to.be.false; expect(auth.currentUser!.phoneNumber).to.be.null; }); diff --git a/packages-exp/auth-types-exp/package.json b/packages-exp/auth-types-exp/package.json index e0a5b339fe7..3c702e85734 100644 --- a/packages-exp/auth-types-exp/package.json +++ b/packages-exp/auth-types-exp/package.json @@ -24,6 +24,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages-exp/firebase-exp/package.json b/packages-exp/firebase-exp/package.json index 5f74e1c80fe..f76217a3806 100644 --- a/packages-exp/firebase-exp/package.json +++ b/packages-exp/firebase-exp/package.json @@ -41,22 +41,22 @@ "@firebase/app-compat": "0.0.800", "@firebase/auth-exp": "0.0.800", "@firebase/functions-exp": "0.0.800", - "@firebase/firestore": "2.0.0", + "@firebase/firestore": "2.0.1", "@firebase/performance-exp": "0.0.800" }, "devDependencies": { - "rollup": "2.29.0", + "rollup": "2.33.1", "@rollup/plugin-commonjs": "15.1.0", "rollup-plugin-license": "2.2.0", "@rollup/plugin-node-resolve": "9.0.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2", - "rollup-plugin-typescript2": "0.27.3", + "rollup-plugin-typescript2": "0.29.0", "rollup-plugin-uglify": "6.0.4", "gulp": "4.0.2", "gulp-sourcemaps": "2.6.5", "gulp-concat": "2.6.1", - "typescript": "4.0.2" + "typescript": "4.0.5" }, "components": [ "app", diff --git a/packages-exp/functions-compat/.eslintrc.js b/packages-exp/functions-compat/.eslintrc.js new file mode 100644 index 00000000000..11fa60d3e6a --- /dev/null +++ b/packages-exp/functions-compat/.eslintrc.js @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +/** + * @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. + */ +const path = require('path'); + +module.exports = { + extends: '../../config/.eslintrc.js', + parserOptions: { + project: 'tsconfig.json', + // to make vscode-eslint work with monorepo + // https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250 + tsconfigRootDir: __dirname + }, + rules: { + 'import/no-extraneous-dependencies': [ + 'error', + { + 'packageDir': [path.resolve(__dirname, '../../'), __dirname], + devDependencies: true + } + ] + } +}; diff --git a/packages-exp/functions-compat/karma.conf.js b/packages-exp/functions-compat/karma.conf.js new file mode 100644 index 00000000000..c0737457c55 --- /dev/null +++ b/packages-exp/functions-compat/karma.conf.js @@ -0,0 +1,35 @@ +/** + * @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. + */ + +const karmaBase = require('../../config/karma.base'); + +const files = ['test/**/*', 'src/**/*.test.ts']; + +module.exports = function (config) { + const karmaConfig = Object.assign({}, karmaBase, { + // files to load into karma + files: files, + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + preprocessors: { '**/*.ts': ['webpack', 'sourcemap'] }, + frameworks: ['mocha'] + }); + + config.set(karmaConfig); +}; + +module.exports.files = files; diff --git a/packages-exp/functions-compat/package.json b/packages-exp/functions-compat/package.json new file mode 100644 index 00000000000..f33abc50f02 --- /dev/null +++ b/packages-exp/functions-compat/package.json @@ -0,0 +1,62 @@ +{ + "name": "@firebase/functions-compat", + "version": "0.0.800", + "description": "", + "private": true, + "author": "Firebase (https://firebase.google.com/)", + "main": "dist/index.node.cjs.js", + "browser": "dist/index.esm5.js", + "module": "dist/index.esm5.js", + "esm2017": "dist/index.esm2017.js", + "files": ["dist"], + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-compat": "0.x" + }, + "devDependencies": { + "@firebase/app-compat": "0.0.800", + "rollup": "2.33.1", + "@rollup/plugin-json": "4.1.0", + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" + }, + "repository": { + "directory": "packages-exp/functions-compat", + "type": "git", + "url": "https://github.com/firebase/firebase-js-sdk.git" + }, + "bugs": { + "url": "https://github.com/firebase/firebase-js-sdk/issues" + }, + "scripts": { + "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", + "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", + "build": "rollup -c", + "build:deps": "lerna run --scope @firebase/functions-compat --include-dependencies build", + "build:release": "rollup -c rollup.config.release.js", + "dev": "rollup -c -w", + "test": "run-p lint test:all", + "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all", + "test:all": "run-p test:browser test:node", + "test:browser": "karma start --single-run", + "test:browser:debug": "karma start --browsers=Chrome --auto-watch", + "test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'src/{,!(browser)/**/}*.test.ts' --file src/index.node.ts --config ../../config/mocharc.node.js", + "test:emulator": "env FIREBASE_FUNCTIONS_HOST=http://localhost FIREBASE_FUNCTIONS_PORT=5005 run-p test:node", + "prepare": "yarn build:release" + }, + "typings": "dist/functions-compat-public.d.ts", + "dependencies": { + "@firebase/component": "0.1.21", + "@firebase/functions-exp": "0.0.800", + "@firebase/functions-types-exp": "0.0.800", + "@firebase/messaging-types": "0.5.0", + "@firebase/util": "0.3.4", + "tslib": "^1.11.1" + }, + "nyc": { + "extension": [ + ".ts" + ], + "reportDir": "./coverage/node" + } +} diff --git a/packages-exp/functions-compat/rollup.config.base.js b/packages-exp/functions-compat/rollup.config.base.js new file mode 100644 index 00000000000..ebb941e4225 --- /dev/null +++ b/packages-exp/functions-compat/rollup.config.base.js @@ -0,0 +1,100 @@ +/** + * @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 json from '@rollup/plugin-json'; +import typescriptPlugin from 'rollup-plugin-typescript2'; +import typescript from 'typescript'; +import pkg from './package.json'; + +const deps = Object.keys( + Object.assign({}, pkg.peerDependencies, pkg.dependencies) +); + +/** + * ES5 Builds + */ +export function getEs5Builds(additionalTypescriptPlugins = {}) { + const es5BuildPlugins = [ + typescriptPlugin({ + typescript, + abortOnError: false, + ...additionalTypescriptPlugins + }), + json() + ]; + return [ + /** + * Browser Builds + */ + { + input: 'src/index.ts', + output: [{ file: pkg.module, format: 'es', sourcemap: true }], + plugins: es5BuildPlugins, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + }, + /** + * Node.js Build + */ + { + input: 'src/index.node.ts', + output: [{ file: pkg.main, format: 'cjs', sourcemap: true }], + plugins: es5BuildPlugins, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + } + ]; +} + +/** + * ES2017 Builds + */ +export function getEs2017Builds(additionalTypescriptPlugins = {}) { + const es2017BuildPlugins = [ + typescriptPlugin({ + typescript, + abortOnError: false, + tsconfigOverride: { + compilerOptions: { + target: 'es2017' + } + }, + ...additionalTypescriptPlugins + }), + json({ preferConst: true }) + ]; + return [ + { + /** + * Browser Build + */ + input: 'src/index.ts', + output: { + file: pkg.esm2017, + format: 'es', + sourcemap: true + }, + plugins: es2017BuildPlugins, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + } + ]; +} + +export function getAllBuilds(additionalTypescriptPlugins = {}) { + return [ + ...getEs5Builds(additionalTypescriptPlugins), + ...getEs2017Builds(additionalTypescriptPlugins) + ]; +} diff --git a/packages-exp/functions-compat/rollup.config.js b/packages-exp/functions-compat/rollup.config.js new file mode 100644 index 00000000000..7746175a9a6 --- /dev/null +++ b/packages-exp/functions-compat/rollup.config.js @@ -0,0 +1,21 @@ +/** + * @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 { getAllBuilds } from './rollup.config.base'; + +// eslint-disable-next-line import/no-default-export +export default getAllBuilds({}); diff --git a/packages-exp/functions-compat/rollup.config.release.js b/packages-exp/functions-compat/rollup.config.release.js new file mode 100644 index 00000000000..d364683678a --- /dev/null +++ b/packages-exp/functions-compat/rollup.config.release.js @@ -0,0 +1,25 @@ +/** + * @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 { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; +import { getAllBuilds } from './rollup.config.base'; + +// eslint-disable-next-line import/no-default-export +export default getAllBuilds({ + clean: true, + transformers: [importPathTransformer] +}); diff --git a/packages-exp/functions-compat/src/callable.test.ts b/packages-exp/functions-compat/src/callable.test.ts new file mode 100644 index 00000000000..5eaf2db5714 --- /dev/null +++ b/packages-exp/functions-compat/src/callable.test.ts @@ -0,0 +1,146 @@ +/** + * @license + * Copyright 2017 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 { expect } from 'chai'; +import { FirebaseApp } from '@firebase/app-types'; +import { FunctionsErrorCode } from '@firebase/functions-types-exp'; +import { createTestService } from '../test/utils'; +import { firebase } from '@firebase/app-compat'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +export const TEST_PROJECT = require('../../../config/project.json'); + +// Chai doesn't handle Error comparisons in a useful way. +// https://github.com/chaijs/chai/issues/608 +async function expectError( + promise: Promise, + code: FunctionsErrorCode, + message: string, + details?: any +): Promise { + let failed = false; + try { + await promise; + } catch (e) { + failed = true; + // Errors coming from callable functions usually have the functions-exp + // code in the message since it's thrown inside functions-exp. + expect(e.code).to.match(new RegExp(`functions.*/${code}`)); + expect(e.message).to.equal(message); + expect(e.details).to.deep.equal(details); + } + if (!failed) { + expect(false, 'Promise should have failed.').to.be.true; + } +} + +describe('Firebase Functions > Call', () => { + let app: FirebaseApp; + const region = 'us-central1'; + + before(() => { + const useEmulator = !!process.env.HOST; + const projectId = useEmulator + ? 'functions-integration-test' + : TEST_PROJECT.projectId; + const messagingSenderId = 'messaging-sender-id'; + + app = firebase.initializeApp({ projectId, messagingSenderId }); + }); + + after(async () => { + await app.delete(); + }); + + it('simple data', async () => { + const functions = createTestService(app, region); + // TODO(klimt): Should we add an API to create a "long" in JS? + const data = { + bool: true, + int: 2, + str: 'four', + array: [5, 6], + null: null + }; + + const func = functions.httpsCallable('dataTest'); + const result = await func(data); + + expect(result.data).to.deep.equal({ + message: 'stub response', + code: 42, + long: 420 + }); + }); + + it('scalars', async () => { + const functions = createTestService(app, region); + const func = functions.httpsCallable('scalarTest'); + const result = await func(17); + expect(result.data).to.equal(76); + }); + + it('null', async () => { + const functions = createTestService(app, region); + const func = functions.httpsCallable('nullTest'); + let result = await func(null); + expect(result.data).to.be.null; + + // Test with void arguments version. + result = await func(); + expect(result.data).to.be.null; + }); + + it('missing result', async () => { + const functions = createTestService(app, region); + const func = functions.httpsCallable('missingResultTest'); + await expectError(func(), 'internal', 'Response is missing data field.'); + }); + + it('unhandled error', async () => { + const functions = createTestService(app, region); + const func = functions.httpsCallable('unhandledErrorTest'); + await expectError(func(), 'internal', 'internal'); + }); + + it('unknown error', async () => { + const functions = createTestService(app, region); + const func = functions.httpsCallable('unknownErrorTest'); + await expectError(func(), 'internal', 'internal'); + }); + + it('explicit error', async () => { + const functions = createTestService(app, region); + const func = functions.httpsCallable('explicitErrorTest'); + await expectError(func(), 'out-of-range', 'explicit nope', { + start: 10, + end: 20, + long: 30 + }); + }); + + it('http error', async () => { + const functions = createTestService(app, region); + const func = functions.httpsCallable('httpErrorTest'); + await expectError(func(), 'invalid-argument', 'invalid-argument'); + }); + + it('timeout', async () => { + const functions = createTestService(app, region); + const func = functions.httpsCallable('timeoutTest', { timeout: 10 }); + await expectError(func(), 'deadline-exceeded', 'deadline-exceeded'); + }); +}); diff --git a/packages-exp/functions-compat/src/index.node.ts b/packages-exp/functions-compat/src/index.node.ts new file mode 100644 index 00000000000..f560f4aa251 --- /dev/null +++ b/packages-exp/functions-compat/src/index.node.ts @@ -0,0 +1,23 @@ +/** + * @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 { firebase } from '@firebase/app-compat'; +import { name, version } from '../package.json'; +import { registerFunctions } from './register'; + +registerFunctions(); +firebase.registerVersion(name, version, 'node'); diff --git a/packages-exp/functions-compat/src/index.ts b/packages-exp/functions-compat/src/index.ts new file mode 100644 index 00000000000..159d9a6590e --- /dev/null +++ b/packages-exp/functions-compat/src/index.ts @@ -0,0 +1,23 @@ +/** + * @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 { firebase } from '@firebase/app-compat'; +import { name, version } from '../package.json'; +import { registerFunctions } from './register'; + +registerFunctions(); +firebase.registerVersion(name, version); diff --git a/packages-exp/functions-compat/src/register.ts b/packages-exp/functions-compat/src/register.ts new file mode 100644 index 00000000000..de761d4521e --- /dev/null +++ b/packages-exp/functions-compat/src/register.ts @@ -0,0 +1,64 @@ +/** + * @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 firebase from '@firebase/app-compat'; +import { _FirebaseNamespace } from '@firebase/app-types/private'; +import { FunctionsService } from './service'; +import '@firebase/functions-exp'; +import { + Component, + ComponentType, + InstanceFactory, + ComponentContainer +} from '@firebase/component'; +import { FirebaseApp } from '@firebase/app-types'; +import { Functions as FunctionsServiceExp } from '@firebase/functions-types-exp'; + +declare module '@firebase/component' { + interface NameServiceMapping { + 'app-compat': FirebaseApp; + 'functions-compat': FunctionsService; + 'functions-exp': FunctionsServiceExp; + } +} + +const factory: InstanceFactory<'functions-compat'> = ( + container: ComponentContainer, + regionOrCustomDomain?: string +) => { + // Dependencies + const app = container.getProvider('app-compat').getImmediate(); + const functionsServiceExp = container + .getProvider('functions-exp') + .getImmediate({ + identifier: regionOrCustomDomain + }); + + return new FunctionsService(app as FirebaseApp, functionsServiceExp); +}; + +export function registerFunctions(): void { + const namespaceExports = { + // no-inline + Functions: FunctionsService + }; + (firebase as _FirebaseNamespace).INTERNAL.registerComponent( + new Component('functions-compat', factory, ComponentType.PUBLIC) + .setServiceProps(namespaceExports) + .setMultipleInstances(true) + ); +} diff --git a/packages-exp/functions-compat/src/service.test.ts b/packages-exp/functions-compat/src/service.test.ts new file mode 100644 index 00000000000..e81b6ac7f0c --- /dev/null +++ b/packages-exp/functions-compat/src/service.test.ts @@ -0,0 +1,95 @@ +/** + * @license + * Copyright 2017 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 { expect, use } from 'chai'; +import { createTestService } from '../test/utils'; +import { FunctionsService } from './service'; +import { firebase } from '@firebase/app-compat'; +import { FirebaseApp } from '@firebase/app-types'; +import * as functionsExp from '@firebase/functions-exp'; +import { stub, match, SinonStub } from 'sinon'; +import * as sinonChai from 'sinon-chai'; + +use(sinonChai); + +describe('Firebase Functions > Service', () => { + let app: FirebaseApp; + let service: FunctionsService; + let functionsEmulatorStub: SinonStub = stub(); + let httpsCallableStub: SinonStub = stub(); + + before(() => { + functionsEmulatorStub = stub(functionsExp, 'useFunctionsEmulator'); + httpsCallableStub = stub(functionsExp, 'httpsCallable'); + }); + + beforeEach(() => { + app = firebase.initializeApp({ + projectId: 'my-project', + messagingSenderId: 'messaging-sender-id' + }); + }); + + afterEach(async () => { + await app.delete(); + }); + + after(() => { + functionsEmulatorStub.restore(); + httpsCallableStub.restore(); + }); + + it('useFunctionsEmulator (deprecated) calls modular useEmulator', () => { + service = createTestService(app); + service.useFunctionsEmulator('http://localhost:5005'); + expect(functionsEmulatorStub).to.be.calledWith( + match.any, + 'localhost', + 5005 + ); + functionsEmulatorStub.resetHistory(); + }); + + it('useEmulator calls modular useEmulator', () => { + service = createTestService(app); + service.useEmulator('otherlocalhost', 5006); + expect(functionsEmulatorStub).to.be.calledWith( + match.any, + 'otherlocalhost', + 5006 + ); + functionsEmulatorStub.resetHistory(); + }); + + it('httpsCallable calls modular httpsCallable', () => { + service = createTestService(app); + service.httpsCallable('blah', { timeout: 2000 }); + expect(httpsCallableStub).to.be.calledWith(match.any, 'blah', { + timeout: 2000 + }); + httpsCallableStub.resetHistory(); + }); + + it('correctly sets region', () => { + service = createTestService(app, 'my-region'); + expect(service._region).to.equal('my-region'); + }); + + it('correctly sets custom domain', () => { + service = createTestService(app, 'https://mydomain.com'); + expect(service._customDomain).to.equal('https://mydomain.com'); + }); +}); diff --git a/packages-exp/functions-compat/src/service.ts b/packages-exp/functions-compat/src/service.ts new file mode 100644 index 00000000000..de71ceb1e45 --- /dev/null +++ b/packages-exp/functions-compat/src/service.ts @@ -0,0 +1,81 @@ +/** + * @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 { + httpsCallable as httpsCallableExp, + useFunctionsEmulator as useFunctionsEmulatorExp +} from '@firebase/functions-exp'; +import { FirebaseFunctions, HttpsCallable } from '@firebase/functions-types'; +import { + HttpsCallableOptions, + Functions as FunctionsServiceExp +} from '@firebase/functions-types-exp'; +import { FirebaseApp } from '@firebase/app-types'; +import { FirebaseError } from '@firebase/util'; + +export class FunctionsService implements FirebaseFunctions { + /** + * For testing. + * @internal + */ + _region: string; + /** + * For testing. + * @internal + */ + _customDomain: string | null; + + constructor( + public app: FirebaseApp, + private _functionsInstance: FunctionsServiceExp + ) { + this._region = this._functionsInstance.region; + this._customDomain = this._functionsInstance.customDomain; + } + httpsCallable(name: string, options?: HttpsCallableOptions): HttpsCallable { + return httpsCallableExp(this._functionsInstance, name, options); + } + /** + * Deprecated in pre-modularized repo, does not exist in modularized + * functions package, need to convert to "host" and "port" args that + * `useFunctionsEmulatorExp` takes. + * @deprecated + */ + useFunctionsEmulator(origin: string): void { + const match = origin.match('[a-zA-Z]+://([a-zA-Z0-9.-]+)(?::([0-9]+))?'); + if (match == null) { + throw new FirebaseError( + 'functions', + 'No origin provided to useFunctionsEmulator()' + ); + } + if (match[2] == null) { + throw new FirebaseError( + 'functions', + 'Port missing in origin provided to useFunctionsEmulator()' + ); + } + return useFunctionsEmulatorExp( + this._functionsInstance, + match[1], + Number(match[2]) + ); + } + useEmulator(host: string, port: number): void { + return useFunctionsEmulatorExp(this._functionsInstance, host, port); + } +} diff --git a/packages-exp/functions-compat/test/utils.ts b/packages-exp/functions-compat/test/utils.ts new file mode 100644 index 00000000000..b824c8eb32e --- /dev/null +++ b/packages-exp/functions-compat/test/utils.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright 2019 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 { FirebaseApp } from '@firebase/app-types'; +import { FunctionsService } from '../src/service'; +import { getFunctions } from '@firebase/functions-exp'; + +export function createTestService( + app: FirebaseApp, + regionOrCustomDomain?: string +): FunctionsService { + const functions = new FunctionsService( + app, + getFunctions(app, regionOrCustomDomain) + ); + const useEmulator = !!process.env.FIREBASE_FUNCTIONS_EMULATOR_HOST; + if (useEmulator) { + functions.useEmulator( + process.env.FIREBASE_FUNCTIONS_EMULATOR_HOST!, + Number(process.env.FIREBASE_FUNCTIONS_EMULATOR_PORT!) + ); + } + return functions; +} diff --git a/packages-exp/functions-compat/tsconfig.json b/packages-exp/functions-compat/tsconfig.json new file mode 100644 index 00000000000..a06ed9a374c --- /dev/null +++ b/packages-exp/functions-compat/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist" + }, + "exclude": [ + "dist/**/*" + ] +} \ No newline at end of file diff --git a/packages-exp/functions-exp/package.json b/packages-exp/functions-exp/package.json index 9b3bcfc6a1a..fe8716ce8f3 100644 --- a/packages-exp/functions-exp/package.json +++ b/packages-exp/functions-exp/package.json @@ -36,9 +36,10 @@ }, "devDependencies": { "@firebase/app-exp": "0.0.800", - "rollup": "2.29.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "rollup": "2.33.1", + "@rollup/plugin-json": "4.1.0", + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages/functions", @@ -50,10 +51,10 @@ }, "typings": "dist/functions-exp-public.d.ts", "dependencies": { - "@firebase/component": "0.1.20", + "@firebase/component": "0.1.21", "@firebase/functions-types-exp": "0.0.800", "@firebase/messaging-types": "0.5.0", - "@firebase/util": "0.3.3", + "@firebase/util": "0.3.4", "node-fetch": "2.6.1", "tslib": "^1.11.1" }, diff --git a/packages-exp/functions-types-exp/package.json b/packages-exp/functions-types-exp/package.json index 714acf335ea..c1db6edb570 100644 --- a/packages-exp/functions-types-exp/package.json +++ b/packages-exp/functions-types-exp/package.json @@ -26,6 +26,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages-exp/installations-exp/package.json b/packages-exp/installations-exp/package.json index 0e816aede9f..94222669a44 100644 --- a/packages-exp/installations-exp/package.json +++ b/packages-exp/installations-exp/package.json @@ -41,13 +41,13 @@ }, "devDependencies": { "@firebase/app-exp": "0.0.800", - "rollup": "2.29.0", + "rollup": "2.33.1", "@rollup/plugin-commonjs": "15.1.0", "@rollup/plugin-json": "4.1.0", "@rollup/plugin-node-resolve": "9.0.0", - "rollup-plugin-typescript2": "0.27.3", + "rollup-plugin-typescript2": "0.29.0", "rollup-plugin-uglify": "6.0.4", - "typescript": "4.0.2" + "typescript": "4.0.5" }, "peerDependencies": { "@firebase/app-exp": "0.x", @@ -55,8 +55,8 @@ }, "dependencies": { "@firebase/installations-types-exp": "0.0.800", - "@firebase/util": "0.3.3", - "@firebase/component": "0.1.20", + "@firebase/util": "0.3.4", + "@firebase/component": "0.1.21", "idb": "3.0.2", "tslib": "^1.11.1" } diff --git a/packages-exp/installations-types-exp/package.json b/packages-exp/installations-types-exp/package.json index 77628cffd78..50c9d11fdee 100644 --- a/packages-exp/installations-types-exp/package.json +++ b/packages-exp/installations-types-exp/package.json @@ -28,6 +28,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages-exp/performance-exp/package.json b/packages-exp/performance-exp/package.json index 6c053eb6cad..1a4405cfb81 100644 --- a/packages-exp/performance-exp/package.json +++ b/packages-exp/performance-exp/package.json @@ -34,18 +34,18 @@ "dependencies": { "@firebase/logger": "0.2.6", "@firebase/installations-exp": "0.0.800", - "@firebase/util": "0.3.3", + "@firebase/util": "0.3.4", "@firebase/performance-types-exp": "0.0.800", - "@firebase/component": "0.1.20", + "@firebase/component": "0.1.21", "tslib": "^1.11.1" }, "license": "Apache-2.0", "devDependencies": { "@firebase/app-exp": "0.0.800", - "rollup": "2.29.0", + "rollup": "2.33.1", "@rollup/plugin-json": "4.1.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages/performance-exp", diff --git a/packages-exp/performance-types-exp/package.json b/packages-exp/performance-types-exp/package.json index 17e2c820650..7df6fe16c8a 100644 --- a/packages-exp/performance-types-exp/package.json +++ b/packages-exp/performance-types-exp/package.json @@ -17,7 +17,7 @@ "index.d.ts" ], "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" }, "repository": { "directory": "packages/performance-types-exp", diff --git a/packages-exp/remote-config-exp/.eslintrc.js b/packages-exp/remote-config-exp/.eslintrc.js new file mode 100644 index 00000000000..5a8c4b909c2 --- /dev/null +++ b/packages-exp/remote-config-exp/.eslintrc.js @@ -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. + */ + +module.exports = { + 'extends': '../../config/.eslintrc.js', + 'parserOptions': { + project: 'tsconfig.json', + // to make vscode-eslint work with monorepo + // https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250 + tsconfigRootDir: __dirname + } +}; diff --git a/packages-exp/remote-config-exp/.npmignore b/packages-exp/remote-config-exp/.npmignore new file mode 100644 index 00000000000..6de0b6d2896 --- /dev/null +++ b/packages-exp/remote-config-exp/.npmignore @@ -0,0 +1 @@ +# This file is left intentionally blank \ No newline at end of file diff --git a/packages-exp/remote-config-exp/README.md b/packages-exp/remote-config-exp/README.md new file mode 100644 index 00000000000..1b21bbecc51 --- /dev/null +++ b/packages-exp/remote-config-exp/README.md @@ -0,0 +1,27 @@ +# @firebase/remote-config + +This is the [Remote Config](https://firebase.google.com/docs/remote-config/) component of the +[Firebase JS SDK](https://www.npmjs.com/package/firebase). + +**This package is not intended for direct usage, and should only be used via the officially +supported [firebase](https://www.npmjs.com/package/firebase) package.** + +## Contributing + +Setup: + +1. Run `yarn` in repo root + +Format: + +1. Run `yarn prettier` in RC package + +Unit test: + +1. Run `yarn test` in RC package + +End-to-end test: + +1. Run `yarn build` in RC package +1. Run `yarn build` in Firebase package +1. Open test_app/index.html in a browser diff --git a/packages-exp/remote-config-exp/api-extractor.json b/packages-exp/remote-config-exp/api-extractor.json new file mode 100644 index 00000000000..f291311f711 --- /dev/null +++ b/packages-exp/remote-config-exp/api-extractor.json @@ -0,0 +1,8 @@ +{ + "extends": "../../config/api-extractor.json", + // Point it to your entry point d.ts file. + "mainEntryPointFilePath": "/dist/src/index.d.ts", + "dtsRollup": { + "enabled": true + } +} \ No newline at end of file diff --git a/packages-exp/remote-config-exp/karma.conf.js b/packages-exp/remote-config-exp/karma.conf.js new file mode 100644 index 00000000000..5006cd5a4ea --- /dev/null +++ b/packages-exp/remote-config-exp/karma.conf.js @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2019 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. + */ + +const karma = require('karma'); +const path = require('path'); +const karmaBase = require('../../config/karma.base'); + +const files = [`test/**/*`]; + +module.exports = function (config) { + const karmaConfig = Object.assign({}, karmaBase, { + // files to load into karma + files: files, + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'] + }); + + config.set(karmaConfig); +}; + +module.exports.files = files; diff --git a/packages-exp/remote-config-exp/package.json b/packages-exp/remote-config-exp/package.json new file mode 100644 index 00000000000..e9cb2824f1f --- /dev/null +++ b/packages-exp/remote-config-exp/package.json @@ -0,0 +1,64 @@ +{ + "name": "@firebase/remote-config-exp", + "version": "0.0.800", + "description": "The Remote Config package of the Firebase JS SDK", + "author": "Firebase (https://firebase.google.com/)", + "private": true, + "main": "dist/index.cjs.js", + "browser": "dist/index.esm.js", + "module": "dist/index.esm.js", + "esm2017": "dist/index.esm2017.js", + "files": ["dist"], + "scripts": { + "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", + "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", + "build": "rollup -c && yarn api-report", + "build:deps": "lerna run --scope @firebase/remote-config-exp --include-dependencies build", + "build:release": "rollup -c rollup.config.release.js && yarn api-report", + "dev": "rollup -c -w", + "test": "run-p lint test:browser", + "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:browser", + "test:browser": "karma start --single-run", + "test:debug": "karma start --browsers=Chrome --auto-watch", + "prettier": "prettier --write '{src,test}/**/*.{js,ts}'", + "prepare": "yarn build", + "api-report": "api-extractor run --local --verbose", + "predoc": "node ../../scripts/exp/remove-exp.js temp", + "doc": "api-documenter markdown --input temp --output docs", + "build:doc": "yarn build && yarn doc" + }, + "peerDependencies": { + "@firebase/app-exp": "0.x", + "@firebase/app-types-exp": "0.x" + }, + "dependencies": { + "@firebase/installations-exp": "0.0.800", + "@firebase/logger": "0.2.6", + "@firebase/remote-config-types-exp": "0.0.800", + "@firebase/util": "0.3.4", + "@firebase/component": "0.1.21", + "tslib": "^1.11.1" + }, + "license": "Apache-2.0", + "devDependencies": { + "@firebase/app-exp": "0.0.800", + "rollup": "2.33.1", + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" + }, + "repository": { + "directory": "packages/remote-config", + "type": "git", + "url": "https://github.com/firebase/firebase-js-sdk.git" + }, + "bugs": { + "url": "https://github.com/firebase/firebase-js-sdk/issues" + }, + "typings": "dist/remote-config-exp.d.ts", + "nyc": { + "extension": [ + ".ts" + ], + "reportDir": "./coverage/node" + } +} diff --git a/packages-exp/remote-config-exp/rollup.config.js b/packages-exp/remote-config-exp/rollup.config.js new file mode 100644 index 00000000000..b57e5ad73ee --- /dev/null +++ b/packages-exp/remote-config-exp/rollup.config.js @@ -0,0 +1,58 @@ +/** + * @license + * Copyright 2019 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 json from '@rollup/plugin-json'; // Enables package.json import in TypeScript. +import typescriptPlugin from 'rollup-plugin-typescript2'; +import typescript from 'typescript'; +import { es2017BuildsNoPlugin, es5BuildsNoPlugin } from './rollup.shared'; + +/** + * ES5 Builds + */ +const es5BuildPlugins = [ + typescriptPlugin({ + typescript + }), + json() +]; + +const es5Builds = es5BuildsNoPlugin.map(build => ({ + ...build, + plugins: es5BuildPlugins +})); + +/** + * ES2017 Builds + */ +const es2017BuildPlugins = [ + typescriptPlugin({ + typescript, + tsconfigOverride: { + compilerOptions: { + target: 'es2017' + } + } + }), + json({ preferConst: true }) +]; + +const es2017Builds = es2017BuildsNoPlugin.map(build => ({ + ...build, + plugins: es2017BuildPlugins +})); + +export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/remote-config-exp/rollup.config.release.js b/packages-exp/remote-config-exp/rollup.config.release.js new file mode 100644 index 00000000000..1e3b338e4b5 --- /dev/null +++ b/packages-exp/remote-config-exp/rollup.config.release.js @@ -0,0 +1,73 @@ +/** + * @license + * Copyright 2019 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 typescriptPlugin from 'rollup-plugin-typescript2'; +import typescript from 'typescript'; +import json from '@rollup/plugin-json'; +import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; +import { es2017BuildsNoPlugin, es5BuildsNoPlugin } from './rollup.shared'; + +/** + * ES5 Builds + */ +const es5BuildPlugins = [ + typescriptPlugin({ + typescript, + clean: true, + abortOnError: false, + transformers: [importPathTransformer] + }), + json() +]; + +const es5Builds = es5BuildsNoPlugin.map(build => ({ + ...build, + plugins: es5BuildPlugins, + treeshake: { + moduleSideEffects: false + } +})); + +/** + * ES2017 Builds + */ +const es2017BuildPlugins = [ + typescriptPlugin({ + typescript, + tsconfigOverride: { + compilerOptions: { + target: 'es2017' + } + }, + abortOnError: false, + clean: true, + transformers: [importPathTransformer] + }), + json({ + preferConst: true + }) +]; + +const es2017Builds = es2017BuildsNoPlugin.map(build => ({ + ...build, + plugins: es2017BuildPlugins, + treeshake: { + moduleSideEffects: false + } +})); + +export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/remote-config-exp/rollup.shared.js b/packages-exp/remote-config-exp/rollup.shared.js new file mode 100644 index 00000000000..c35a655a498 --- /dev/null +++ b/packages-exp/remote-config-exp/rollup.shared.js @@ -0,0 +1,53 @@ +/** + * @license + * Copyright 2018 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 pkg from './package.json'; + +const deps = Object.keys( + Object.assign({}, pkg.peerDependencies, pkg.dependencies) +); + +export const es5BuildsNoPlugin = [ + /** + * Browser Builds + */ + { + input: 'src/index.ts', + output: [ + { file: pkg.main, format: 'cjs', sourcemap: true }, + { file: pkg.module, format: 'es', sourcemap: true } + ], + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + } +]; + +/** + * ES2017 Builds + */ +export const es2017BuildsNoPlugin = [ + { + /** + * Browser Build + */ + input: 'src/index.ts', + output: { + file: pkg.esm2017, + format: 'es', + sourcemap: true + }, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + } +]; diff --git a/packages-exp/remote-config-exp/src/api.ts b/packages-exp/remote-config-exp/src/api.ts new file mode 100644 index 00000000000..0b48299c2f4 --- /dev/null +++ b/packages-exp/remote-config-exp/src/api.ts @@ -0,0 +1,172 @@ +/** + * @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 { _getProvider } from '@firebase/app-exp'; +import { FirebaseApp } from '@firebase/app-types-exp'; +import { + LogLevel as RemoteConfigLogLevel, + RemoteConfig, + Value as ValueType +} from '@firebase/remote-config-types-exp'; +import { RemoteConfigAbortSignal } from './client/remote_config_fetch_client'; +import { RC_COMPONENT_NAME } from './constants'; +import { ErrorCode, hasErrorCode } from './errors'; +import { RemoteConfig as RemoteConfigImpl } from './remote_config'; +import { Value } from './value'; +import { LogLevel as FirebaseLogLevel } from '@firebase/logger'; + +export function getRemoteConfig(app: FirebaseApp): RemoteConfig { + const rcProvider = _getProvider(app, RC_COMPONENT_NAME); + return rcProvider.getImmediate(); +} + +export async function activate(remoteConfig: RemoteConfig): Promise { + const rc = remoteConfig as RemoteConfigImpl; + const [lastSuccessfulFetchResponse, activeConfigEtag] = await Promise.all([ + rc._storage.getLastSuccessfulFetchResponse(), + rc._storage.getActiveConfigEtag() + ]); + if ( + !lastSuccessfulFetchResponse || + !lastSuccessfulFetchResponse.config || + !lastSuccessfulFetchResponse.eTag || + lastSuccessfulFetchResponse.eTag === activeConfigEtag + ) { + // Either there is no successful fetched config, or is the same as current active + // config. + return false; + } + await Promise.all([ + rc._storageCache.setActiveConfig(lastSuccessfulFetchResponse.config), + rc._storage.setActiveConfigEtag(lastSuccessfulFetchResponse.eTag) + ]); + return true; +} + +export function ensureInitialized(remoteConfig: RemoteConfig): Promise { + const rc = remoteConfig as RemoteConfigImpl; + if (!rc._initializePromise) { + rc._initializePromise = rc._storageCache.loadFromStorage().then(() => { + rc._isInitializationComplete = true; + }); + } + return rc._initializePromise; +} + +export async function fetchConfig(remoteConfig: RemoteConfig): Promise { + const rc = remoteConfig as RemoteConfigImpl; + // Aborts the request after the given timeout, causing the fetch call to + // reject with an AbortError. + // + //

Aborting after the request completes is a no-op, so we don't need a + // corresponding clearTimeout. + // + // Locating abort logic here because: + // * it uses a developer setting (timeout) + // * it applies to all retries (like curl's max-time arg) + // * it is consistent with the Fetch API's signal input + const abortSignal = new RemoteConfigAbortSignal(); + + setTimeout(async () => { + // Note a very low delay, eg < 10ms, can elapse before listeners are initialized. + abortSignal.abort(); + }, rc.settings.fetchTimeoutMillis); + + // Catches *all* errors thrown by client so status can be set consistently. + try { + await rc._client.fetch({ + cacheMaxAgeMillis: rc.settings.minimumFetchIntervalMillis, + signal: abortSignal + }); + + await rc._storageCache.setLastFetchStatus('success'); + } catch (e) { + const lastFetchStatus = hasErrorCode(e, ErrorCode.FETCH_THROTTLE) + ? 'throttle' + : 'failure'; + await rc._storageCache.setLastFetchStatus(lastFetchStatus); + throw e; + } +} + +export function getAll(remoteConfig: RemoteConfig): Record { + const rc = remoteConfig as RemoteConfigImpl; + return getAllKeys( + rc._storageCache.getActiveConfig(), + rc.defaultConfig + ).reduce((allConfigs, key) => { + allConfigs[key] = getValue(remoteConfig, key); + return allConfigs; + }, {} as Record); +} + +export function getBoolean(remoteConfig: RemoteConfig, key: string): boolean { + return getValue(remoteConfig, key).asBoolean(); +} + +export function getNumber(remoteConfig: RemoteConfig, key: string): number { + return getValue(remoteConfig, key).asNumber(); +} + +export function getString(remoteConfig: RemoteConfig, key: string): string { + return getValue(remoteConfig, key).asString(); +} + +export function getValue(remoteConfig: RemoteConfig, key: string): ValueType { + const rc = remoteConfig as RemoteConfigImpl; + if (!rc._isInitializationComplete) { + rc._logger.debug( + `A value was requested for key "${key}" before SDK initialization completed.` + + ' Await on ensureInitialized if the intent was to get a previously activated value.' + ); + } + const activeConfig = rc._storageCache.getActiveConfig(); + if (activeConfig && activeConfig[key] !== undefined) { + return new Value('remote', activeConfig[key]); + } else if (rc.defaultConfig && rc.defaultConfig[key] !== undefined) { + return new Value('default', String(rc.defaultConfig[key])); + } + rc._logger.debug( + `Returning static value for key "${key}".` + + ' Define a default or remote value if this is unintentional.' + ); + return new Value('static'); +} + +export function setLogLevel( + remoteConfig: RemoteConfig, + logLevel: RemoteConfigLogLevel +): void { + const rc = remoteConfig as RemoteConfigImpl; + switch (logLevel) { + case 'debug': + rc._logger.logLevel = FirebaseLogLevel.DEBUG; + break; + case 'silent': + rc._logger.logLevel = FirebaseLogLevel.SILENT; + break; + default: + rc._logger.logLevel = FirebaseLogLevel.ERROR; + } +} + +/** + * Dedupes and returns an array of all the keys of the received objects. + */ +function getAllKeys(obj1: {} = {}, obj2: {} = {}): string[] { + return Object.keys({ ...obj1, ...obj2 }); +} diff --git a/packages-exp/remote-config-exp/src/api2.ts b/packages-exp/remote-config-exp/src/api2.ts new file mode 100644 index 00000000000..ee1df4b1c06 --- /dev/null +++ b/packages-exp/remote-config-exp/src/api2.ts @@ -0,0 +1,28 @@ +/** + * @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 { RemoteConfig } from '@firebase/remote-config-types-exp'; +import { activate, fetchConfig } from './api'; + +// This API is put in a separate file, so we can stub fetchConfig and activate in tests. +// It's not possible to stub standalone functions from the same module. +export async function fetchAndActivate( + remoteConfig: RemoteConfig +): Promise { + await fetchConfig(remoteConfig); + return activate(remoteConfig); +} diff --git a/packages-exp/remote-config-exp/src/client/caching_client.ts b/packages-exp/remote-config-exp/src/client/caching_client.ts new file mode 100644 index 00000000000..aea61acfd1f --- /dev/null +++ b/packages-exp/remote-config-exp/src/client/caching_client.ts @@ -0,0 +1,123 @@ +/** + * @license + * Copyright 2019 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 { StorageCache } from '../storage/storage_cache'; +import { + FetchResponse, + RemoteConfigFetchClient, + FetchRequest +} from './remote_config_fetch_client'; +import { Storage } from '../storage/storage'; +import { Logger } from '@firebase/logger'; + +/** + * Implements the {@link RemoteConfigClient} abstraction with success response caching. + * + *

Comparable to the browser's Cache API for responses, but the Cache API requires a Service + * Worker, which requires HTTPS, which would significantly complicate SDK installation. Also, the + * Cache API doesn't support matching entries by time. + */ +export class CachingClient implements RemoteConfigFetchClient { + constructor( + private readonly client: RemoteConfigFetchClient, + private readonly storage: Storage, + private readonly storageCache: StorageCache, + private readonly logger: Logger + ) {} + + /** + * Returns true if the age of the cached fetched configs is less than or equal to + * {@link Settings#minimumFetchIntervalInSeconds}. + * + *

This is comparable to passing `headers = { 'Cache-Control': max-age }` to the + * native Fetch API. + * + *

Visible for testing. + */ + isCachedDataFresh( + cacheMaxAgeMillis: number, + lastSuccessfulFetchTimestampMillis: number | undefined + ): boolean { + // Cache can only be fresh if it's populated. + if (!lastSuccessfulFetchTimestampMillis) { + this.logger.debug('Config fetch cache check. Cache unpopulated.'); + return false; + } + + // Calculates age of cache entry. + const cacheAgeMillis = Date.now() - lastSuccessfulFetchTimestampMillis; + + const isCachedDataFresh = cacheAgeMillis <= cacheMaxAgeMillis; + + this.logger.debug( + 'Config fetch cache check.' + + ` Cache age millis: ${cacheAgeMillis}.` + + ` Cache max age millis (minimumFetchIntervalMillis setting): ${cacheMaxAgeMillis}.` + + ` Is cache hit: ${isCachedDataFresh}.` + ); + + return isCachedDataFresh; + } + + async fetch(request: FetchRequest): Promise { + // Reads from persisted storage to avoid cache miss if callers don't wait on initialization. + const [ + lastSuccessfulFetchTimestampMillis, + lastSuccessfulFetchResponse + ] = await Promise.all([ + this.storage.getLastSuccessfulFetchTimestampMillis(), + this.storage.getLastSuccessfulFetchResponse() + ]); + + // Exits early on cache hit. + if ( + lastSuccessfulFetchResponse && + this.isCachedDataFresh( + request.cacheMaxAgeMillis, + lastSuccessfulFetchTimestampMillis + ) + ) { + return lastSuccessfulFetchResponse; + } + + // Deviates from pure decorator by not honoring a passed ETag since we don't have a public API + // that allows the caller to pass an ETag. + request.eTag = + lastSuccessfulFetchResponse && lastSuccessfulFetchResponse.eTag; + + // Falls back to service on cache miss. + const response = await this.client.fetch(request); + + // Fetch throws for non-success responses, so success is guaranteed here. + + const storageOperations = [ + // Uses write-through cache for consistency with synchronous public API. + this.storageCache.setLastSuccessfulFetchTimestampMillis(Date.now()) + ]; + + if (response.status === 200) { + // Caches response only if it has changed, ie non-304 responses. + storageOperations.push( + this.storage.setLastSuccessfulFetchResponse(response) + ); + } + + await Promise.all(storageOperations); + + return response; + } +} diff --git a/packages-exp/remote-config-exp/src/client/remote_config_fetch_client.ts b/packages-exp/remote-config-exp/src/client/remote_config_fetch_client.ts new file mode 100644 index 00000000000..25e00299855 --- /dev/null +++ b/packages-exp/remote-config-exp/src/client/remote_config_fetch_client.ts @@ -0,0 +1,139 @@ +/** + * @license + * Copyright 2019 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. + */ + +/** + * Defines a client, as in https://en.wikipedia.org/wiki/Client%E2%80%93server_model, for the + * Remote Config server (https://firebase.google.com/docs/reference/remote-config/rest). + * + *

Abstracts throttle, response cache and network implementation details. + * + *

Modeled after the native {@link GlobalFetch} interface, which is relatively modern and + * convenient, but simplified for Remote Config's use case. + * + * Disambiguation: {@link GlobalFetch} interface and the Remote Config service define "fetch" + * methods. The RestClient uses the former to make HTTP calls. This interface abstracts the latter. + */ +export interface RemoteConfigFetchClient { + /** + * @throws if response status is not 200 or 304. + */ + fetch(request: FetchRequest): Promise; +} + +/** + * Defines a self-descriptive reference for config key-value pairs. + */ +export interface FirebaseRemoteConfigObject { + [key: string]: string; +} + +/** + * Shims a minimal AbortSignal. + * + *

AbortController's AbortSignal conveniently decouples fetch timeout logic from other aspects + * of networking, such as retries. Firebase doesn't use AbortController enough to justify a + * polyfill recommendation, like we do with the Fetch API, but this minimal shim can easily be + * swapped out if/when we do. + */ +export class RemoteConfigAbortSignal { + listeners: Array<() => void> = []; + addEventListener(listener: () => void): void { + this.listeners.push(listener); + } + abort(): void { + this.listeners.forEach(listener => listener()); + } +} + +/** + * Defines per-request inputs for the Remote Config fetch request. + * + *

Modeled after the native {@link Request} interface, but simplified for Remote Config's + * use case. + */ +export interface FetchRequest { + /** + * Uses cached config if it is younger than this age. + * + *

Required because it's defined by settings, which always have a value. + * + *

Comparable to passing `headers = { 'Cache-Control': max-age }` to the native + * Fetch API. + */ + cacheMaxAgeMillis: number; + + /** + * An event bus for the signal to abort a request. + * + *

Required because all requests should be abortable. + * + *

Comparable to the native + * Fetch API's "signal" field on its request configuration object + * https://fetch.spec.whatwg.org/#dom-requestinit-signal. + * + *

Disambiguation: Remote Config commonly refers to API inputs as + * "signals". See the private ConfigFetchRequestBody interface for those: + * http://google3/firebase/remote_config/web/src/core/rest_client.ts?l=14&rcl=255515243. + */ + signal: RemoteConfigAbortSignal; + + /** + * The ETag header value from the last response. + * + *

Optional in case this is the first request. + * + *

Comparable to passing `headers = { 'If-None-Match': }` to the native Fetch API. + */ + eTag?: string; +} + +/** + * Defines a successful response (200 or 304). + * + *

Modeled after the native {@link Response} interface, but simplified for Remote Config's + * use case. + */ +export interface FetchResponse { + /** + * The HTTP status, which is useful for differentiating success responses with data from + * those without. + * + *

{@link RemoteConfigClient} is modeled after the native {@link GlobalFetch} interface, so + * HTTP status is first-class. + * + *

Disambiguation: the fetch response returns a legacy "state" value that is redundant with the + * HTTP status code. The former is normalized into the latter. + */ + status: number; + + /** + * Defines the ETag response header value. + * + *

Only defined for 200 and 304 responses. + */ + eTag?: string; + + /** + * Defines the map of parameters returned as "entries" in the fetch response body. + * + *

Only defined for 200 responses. + */ + config?: FirebaseRemoteConfigObject; + + // Note: we're not extracting experiment metadata until + // ABT and Analytics have Web SDKs. +} diff --git a/packages-exp/remote-config-exp/src/client/rest_client.ts b/packages-exp/remote-config-exp/src/client/rest_client.ts new file mode 100644 index 00000000000..a5b521a421e --- /dev/null +++ b/packages-exp/remote-config-exp/src/client/rest_client.ts @@ -0,0 +1,176 @@ +/** + * @license + * Copyright 2019 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 { + FetchResponse, + RemoteConfigFetchClient, + FirebaseRemoteConfigObject, + FetchRequest +} from './remote_config_fetch_client'; +import { ERROR_FACTORY, ErrorCode } from '../errors'; +import { getUserLanguage } from '../language'; +import { _FirebaseInstallationsInternal } from '@firebase/installations-types-exp'; + +/** + * Defines request body parameters required to call the fetch API: + * https://firebase.google.com/docs/reference/remote-config/rest + * + *

Not exported because this file encapsulates REST API specifics. + * + *

Not passing User Properties because Analytics' source of truth on Web is server-side. + */ +interface FetchRequestBody { + // Disables camelcase linting for request body params. + /* eslint-disable camelcase*/ + sdk_version: string; + app_instance_id: string; + app_instance_id_token: string; + app_id: string; + language_code: string; + /* eslint-enable camelcase */ +} + +/** + * Implements the Client abstraction for the Remote Config REST API. + */ +export class RestClient implements RemoteConfigFetchClient { + constructor( + private readonly firebaseInstallations: _FirebaseInstallationsInternal, + private readonly sdkVersion: string, + private readonly namespace: string, + private readonly projectId: string, + private readonly apiKey: string, + private readonly appId: string + ) {} + + /** + * Fetches from the Remote Config REST API. + * + * @throws a {@link ErrorCode.FETCH_NETWORK} error if {@link GlobalFetch#fetch} can't + * connect to the network. + * @throws a {@link ErrorCode.FETCH_PARSE} error if {@link Response#json} can't parse the + * fetch response. + * @throws a {@link ErrorCode.FETCH_STATUS} error if the service returns an HTTP error status. + */ + async fetch(request: FetchRequest): Promise { + const [installationId, installationToken] = await Promise.all([ + this.firebaseInstallations.getId(), + this.firebaseInstallations.getToken() + ]); + + const urlBase = + window.FIREBASE_REMOTE_CONFIG_URL_BASE || + 'https://firebaseremoteconfig.googleapis.com'; + + const url = `${urlBase}/v1/projects/${this.projectId}/namespaces/${this.namespace}:fetch?key=${this.apiKey}`; + + const headers = { + 'Content-Type': 'application/json', + 'Content-Encoding': 'gzip', + // Deviates from pure decorator by not passing max-age header since we don't currently have + // service behavior using that header. + 'If-None-Match': request.eTag || '*' + }; + + const requestBody: FetchRequestBody = { + /* eslint-disable camelcase */ + sdk_version: this.sdkVersion, + app_instance_id: installationId, + app_instance_id_token: installationToken, + app_id: this.appId, + language_code: getUserLanguage() + /* eslint-enable camelcase */ + }; + + const options = { + method: 'POST', + headers, + body: JSON.stringify(requestBody) + }; + + // This logic isn't REST-specific, but shimming abort logic isn't worth another decorator. + const fetchPromise = fetch(url, options); + const timeoutPromise = new Promise((_resolve, reject) => { + // Maps async event listener to Promise API. + request.signal.addEventListener(() => { + // Emulates https://heycam.github.io/webidl/#aborterror + const error = new Error('The operation was aborted.'); + error.name = 'AbortError'; + reject(error); + }); + }); + + let response; + try { + await Promise.race([fetchPromise, timeoutPromise]); + response = await fetchPromise; + } catch (originalError) { + let errorCode = ErrorCode.FETCH_NETWORK; + if (originalError.name === 'AbortError') { + errorCode = ErrorCode.FETCH_TIMEOUT; + } + throw ERROR_FACTORY.create(errorCode, { + originalErrorMessage: originalError.message + }); + } + + let status = response.status; + + // Normalizes nullable header to optional. + const responseEtag = response.headers.get('ETag') || undefined; + + let config: FirebaseRemoteConfigObject | undefined; + let state: string | undefined; + + // JSON parsing throws SyntaxError if the response body isn't a JSON string. + // Requesting application/json and checking for a 200 ensures there's JSON data. + if (response.status === 200) { + let responseBody; + try { + responseBody = await response.json(); + } catch (originalError) { + throw ERROR_FACTORY.create(ErrorCode.FETCH_PARSE, { + originalErrorMessage: originalError.message + }); + } + config = responseBody['entries']; + state = responseBody['state']; + } + + // Normalizes based on legacy state. + if (state === 'INSTANCE_STATE_UNSPECIFIED') { + status = 500; + } else if (state === 'NO_CHANGE') { + status = 304; + } else if (state === 'NO_TEMPLATE' || state === 'EMPTY_CONFIG') { + // These cases can be fixed remotely, so normalize to safe value. + config = {}; + } + + // Normalize to exception-based control flow for non-success cases. + // Encapsulates HTTP specifics in this class as much as possible. Status is still the best for + // differentiating success states (200 from 304; the state body param is undefined in a + // standard 304). + if (status !== 304 && status !== 200) { + throw ERROR_FACTORY.create(ErrorCode.FETCH_STATUS, { + httpStatus: status + }); + } + + return { status, eTag: responseEtag, config }; + } +} diff --git a/packages-exp/remote-config-exp/src/client/retrying_client.ts b/packages-exp/remote-config-exp/src/client/retrying_client.ts new file mode 100644 index 00000000000..fe1737023df --- /dev/null +++ b/packages-exp/remote-config-exp/src/client/retrying_client.ts @@ -0,0 +1,144 @@ +/** + * @license + * Copyright 2019 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 { + RemoteConfigAbortSignal, + RemoteConfigFetchClient, + FetchResponse, + FetchRequest +} from './remote_config_fetch_client'; +import { ThrottleMetadata, Storage } from '../storage/storage'; +import { ErrorCode, ERROR_FACTORY } from '../errors'; +import { FirebaseError, calculateBackoffMillis } from '@firebase/util'; + +/** + * Supports waiting on a backoff by: + * + *

    + *
  • Promisifying setTimeout, so we can set a timeout in our Promise chain
  • + *
  • Listening on a signal bus for abort events, just like the Fetch API
  • + *
  • Failing in the same way the Fetch API fails, so timing out a live request and a throttled + * request appear the same.
  • + *
+ * + *

Visible for testing. + */ +export function setAbortableTimeout( + signal: RemoteConfigAbortSignal, + throttleEndTimeMillis: number +): Promise { + return new Promise((resolve, reject) => { + // Derives backoff from given end time, normalizing negative numbers to zero. + const backoffMillis = Math.max(throttleEndTimeMillis - Date.now(), 0); + + const timeout = setTimeout(resolve, backoffMillis); + + // Adds listener, rather than sets onabort, because signal is a shared object. + signal.addEventListener(() => { + clearTimeout(timeout); + + // If the request completes before this timeout, the rejection has no effect. + reject( + ERROR_FACTORY.create(ErrorCode.FETCH_THROTTLE, { + throttleEndTimeMillis + }) + ); + }); + }); +} + +type RetriableError = FirebaseError & { customData: { httpStatus: string } }; +/** + * Returns true if the {@link Error} indicates a fetch request may succeed later. + */ +function isRetriableError(e: Error): e is RetriableError { + if (!(e instanceof FirebaseError) || !e.customData) { + return false; + } + + // Uses string index defined by ErrorData, which FirebaseError implements. + const httpStatus = Number(e.customData['httpStatus']); + + return ( + httpStatus === 429 || + httpStatus === 500 || + httpStatus === 503 || + httpStatus === 504 + ); +} + +/** + * Decorates a Client with retry logic. + * + *

Comparable to CachingClient, but uses backoff logic instead of cache max age and doesn't cache + * responses (because the SDK has no use for error responses). + */ +export class RetryingClient implements RemoteConfigFetchClient { + constructor( + private readonly client: RemoteConfigFetchClient, + private readonly storage: Storage + ) {} + + async fetch(request: FetchRequest): Promise { + const throttleMetadata = (await this.storage.getThrottleMetadata()) || { + backoffCount: 0, + throttleEndTimeMillis: Date.now() + }; + + return this.attemptFetch(request, throttleMetadata); + } + + /** + * A recursive helper for attempting a fetch request repeatedly. + * + * @throws any non-retriable errors. + */ + async attemptFetch( + request: FetchRequest, + { throttleEndTimeMillis, backoffCount }: ThrottleMetadata + ): Promise { + // Starts with a (potentially zero) timeout to support resumption from stored state. + // Ensures the throttle end time is honored if the last attempt timed out. + // Note the SDK will never make a request if the fetch timeout expires at this point. + await setAbortableTimeout(request.signal, throttleEndTimeMillis); + + try { + const response = await this.client.fetch(request); + + // Note the SDK only clears throttle state if response is success or non-retriable. + await this.storage.deleteThrottleMetadata(); + + return response; + } catch (e) { + if (!isRetriableError(e)) { + throw e; + } + + // Increments backoff state. + const throttleMetadata = { + throttleEndTimeMillis: + Date.now() + calculateBackoffMillis(backoffCount), + backoffCount: backoffCount + 1 + }; + + // Persists state. + await this.storage.setThrottleMetadata(throttleMetadata); + + return this.attemptFetch(request, throttleMetadata); + } + } +} diff --git a/packages-exp/remote-config-exp/src/constants.ts b/packages-exp/remote-config-exp/src/constants.ts new file mode 100644 index 00000000000..6bc7d1d5547 --- /dev/null +++ b/packages-exp/remote-config-exp/src/constants.ts @@ -0,0 +1,18 @@ +/** + * @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 const RC_COMPONENT_NAME = 'remote-config-exp'; diff --git a/packages-exp/remote-config-exp/src/errors.ts b/packages-exp/remote-config-exp/src/errors.ts new file mode 100644 index 00000000000..d4be9a09f76 --- /dev/null +++ b/packages-exp/remote-config-exp/src/errors.ts @@ -0,0 +1,97 @@ +/** + * @license + * Copyright 2019 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 { ErrorFactory, FirebaseError } from '@firebase/util'; + +export const enum ErrorCode { + REGISTRATION_WINDOW = 'registration-window', + REGISTRATION_PROJECT_ID = 'registration-project-id', + REGISTRATION_API_KEY = 'registration-api-key', + REGISTRATION_APP_ID = 'registration-app-id', + STORAGE_OPEN = 'storage-open', + STORAGE_GET = 'storage-get', + STORAGE_SET = 'storage-set', + STORAGE_DELETE = 'storage-delete', + FETCH_NETWORK = 'fetch-client-network', + FETCH_TIMEOUT = 'fetch-timeout', + FETCH_THROTTLE = 'fetch-throttle', + FETCH_PARSE = 'fetch-client-parse', + FETCH_STATUS = 'fetch-status' +} + +const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { + [ErrorCode.REGISTRATION_WINDOW]: + 'Undefined window object. This SDK only supports usage in a browser environment.', + [ErrorCode.REGISTRATION_PROJECT_ID]: + 'Undefined project identifier. Check Firebase app initialization.', + [ErrorCode.REGISTRATION_API_KEY]: + 'Undefined API key. Check Firebase app initialization.', + [ErrorCode.REGISTRATION_APP_ID]: + 'Undefined app identifier. Check Firebase app initialization.', + [ErrorCode.STORAGE_OPEN]: + 'Error thrown when opening storage. Original error: {$originalErrorMessage}.', + [ErrorCode.STORAGE_GET]: + 'Error thrown when reading from storage. Original error: {$originalErrorMessage}.', + [ErrorCode.STORAGE_SET]: + 'Error thrown when writing to storage. Original error: {$originalErrorMessage}.', + [ErrorCode.STORAGE_DELETE]: + 'Error thrown when deleting from storage. Original error: {$originalErrorMessage}.', + [ErrorCode.FETCH_NETWORK]: + 'Fetch client failed to connect to a network. Check Internet connection.' + + ' Original error: {$originalErrorMessage}.', + [ErrorCode.FETCH_TIMEOUT]: + 'The config fetch request timed out. ' + + ' Configure timeout using "fetchTimeoutMillis" SDK setting.', + [ErrorCode.FETCH_THROTTLE]: + 'The config fetch request timed out while in an exponential backoff state.' + + ' Configure timeout using "fetchTimeoutMillis" SDK setting.' + + ' Unix timestamp in milliseconds when fetch request throttling ends: {$throttleEndTimeMillis}.', + [ErrorCode.FETCH_PARSE]: + 'Fetch client could not parse response.' + + ' Original error: {$originalErrorMessage}.', + [ErrorCode.FETCH_STATUS]: + 'Fetch server returned an HTTP error status. HTTP status: {$httpStatus}.' +}; + +// Note this is effectively a type system binding a code to params. This approach overlaps with the +// role of TS interfaces, but works well for a few reasons: +// 1) JS is unaware of TS interfaces, eg we can't test for interface implementation in JS +// 2) callers should have access to a human-readable summary of the error and this interpolates +// params into an error message; +// 3) callers should be able to programmatically access data associated with an error, which +// ErrorData provides. +interface ErrorParams { + [ErrorCode.STORAGE_OPEN]: { originalErrorMessage: string | undefined }; + [ErrorCode.STORAGE_GET]: { originalErrorMessage: string | undefined }; + [ErrorCode.STORAGE_SET]: { originalErrorMessage: string | undefined }; + [ErrorCode.STORAGE_DELETE]: { originalErrorMessage: string | undefined }; + [ErrorCode.FETCH_NETWORK]: { originalErrorMessage: string }; + [ErrorCode.FETCH_THROTTLE]: { throttleEndTimeMillis: number }; + [ErrorCode.FETCH_PARSE]: { originalErrorMessage: string }; + [ErrorCode.FETCH_STATUS]: { httpStatus: number }; +} + +export const ERROR_FACTORY = new ErrorFactory( + 'remoteconfig' /* service */, + 'Remote Config' /* service name */, + ERROR_DESCRIPTION_MAP +); + +// Note how this is like typeof/instanceof, but for ErrorCode. +export function hasErrorCode(e: Error, errorCode: ErrorCode): boolean { + return e instanceof FirebaseError && e.code.indexOf(errorCode) !== -1; +} diff --git a/packages-exp/remote-config-exp/src/index.ts b/packages-exp/remote-config-exp/src/index.ts new file mode 100644 index 00000000000..2b639bccfc7 --- /dev/null +++ b/packages-exp/remote-config-exp/src/index.ts @@ -0,0 +1,35 @@ +/** + * @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 '@firebase/installations-exp'; +import { registerRemoteConfig } from './register'; + +// Facilitates debugging by enabling settings changes without rebuilding asset. +// Note these debug options are not part of a documented, supported API and can change at any time. +// Consolidates debug options for easier discovery. +// Uses transient variables on window to avoid lingering state causing panic. +declare global { + interface Window { + FIREBASE_REMOTE_CONFIG_URL_BASE: string; + } +} + +export * from './api'; +export * from './api2'; + +/** register component and version */ +registerRemoteConfig(); diff --git a/packages-exp/remote-config-exp/src/language.ts b/packages-exp/remote-config-exp/src/language.ts new file mode 100644 index 00000000000..9c44ee275bf --- /dev/null +++ b/packages-exp/remote-config-exp/src/language.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright 2019 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. + */ + +/** + * Attempts to get the most accurate browser language setting. + * + *

Adapted from getUserLanguage in packages/auth/src/utils.js for TypeScript. + * + *

Defers default language specification to server logic for consistency. + * + * @param navigatorLanguage Enables tests to override read-only {@link NavigatorLanguage}. + */ +export function getUserLanguage( + navigatorLanguage: NavigatorLanguage = navigator +): string { + return ( + // Most reliable, but only supported in Chrome/Firefox. + (navigatorLanguage.languages && navigatorLanguage.languages[0]) || + // Supported in most browsers, but returns the language of the browser + // UI, not the language set in browser settings. + navigatorLanguage.language + // Polyfill otherwise. + ); +} diff --git a/packages-exp/remote-config-exp/src/register.ts b/packages-exp/remote-config-exp/src/register.ts new file mode 100644 index 00000000000..6d6dde396ea --- /dev/null +++ b/packages-exp/remote-config-exp/src/register.ts @@ -0,0 +1,121 @@ +/** + * @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 { + _registerComponent, + registerVersion, + SDK_VERSION +} from '@firebase/app-exp'; +import { + Component, + ComponentType, + ComponentContainer +} from '@firebase/component'; +import { Logger, LogLevel as FirebaseLogLevel } from '@firebase/logger'; +import { RemoteConfig } from '@firebase/remote-config-types-exp'; +import { name as packageName, version } from '../package.json'; +import { ensureInitialized } from './api'; +import { CachingClient } from './client/caching_client'; +import { RestClient } from './client/rest_client'; +import { RetryingClient } from './client/retrying_client'; +import { RC_COMPONENT_NAME } from './constants'; +import { ErrorCode, ERROR_FACTORY } from './errors'; +import { RemoteConfig as RemoteConfigImpl } from './remote_config'; +import { Storage } from './storage/storage'; +import { StorageCache } from './storage/storage_cache'; + +export function registerRemoteConfig(): void { + _registerComponent( + new Component( + RC_COMPONENT_NAME, + remoteConfigFactory, + ComponentType.PUBLIC + ).setMultipleInstances(true) + ); + + registerVersion(packageName, version); + + function remoteConfigFactory( + container: ComponentContainer, + namespace?: string + ): RemoteConfig { + /* Dependencies */ + // getImmediate for FirebaseApp will always succeed + const app = container.getProvider('app-exp').getImmediate(); + // The following call will always succeed because rc has `import '@firebase/installations'` + const installations = container + .getProvider('installations-exp-internal') + .getImmediate(); + + // Guards against the SDK being used in non-browser environments. + if (typeof window === 'undefined') { + throw ERROR_FACTORY.create(ErrorCode.REGISTRATION_WINDOW); + } + + // Normalizes optional inputs. + const { projectId, apiKey, appId } = app.options; + if (!projectId) { + throw ERROR_FACTORY.create(ErrorCode.REGISTRATION_PROJECT_ID); + } + if (!apiKey) { + throw ERROR_FACTORY.create(ErrorCode.REGISTRATION_API_KEY); + } + if (!appId) { + throw ERROR_FACTORY.create(ErrorCode.REGISTRATION_APP_ID); + } + namespace = namespace || 'firebase'; + + const storage = new Storage(appId, app.name, namespace); + const storageCache = new StorageCache(storage); + + const logger = new Logger(packageName); + + // Sets ERROR as the default log level. + // See RemoteConfig#setLogLevel for corresponding normalization to ERROR log level. + logger.logLevel = FirebaseLogLevel.ERROR; + + const restClient = new RestClient( + installations, + // Uses the JS SDK version, by which the RC package version can be deduced, if necessary. + SDK_VERSION, + namespace, + projectId, + apiKey, + appId + ); + const retryingClient = new RetryingClient(restClient, storage); + const cachingClient = new CachingClient( + retryingClient, + storage, + storageCache, + logger + ); + + const remoteConfigInstance = new RemoteConfigImpl( + app, + cachingClient, + storageCache, + storage, + logger + ); + + // Starts warming cache. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + ensureInitialized(remoteConfigInstance); + + return remoteConfigInstance; + } +} diff --git a/packages-exp/remote-config-exp/src/remote_config.ts b/packages-exp/remote-config-exp/src/remote_config.ts new file mode 100644 index 00000000000..de7825b348b --- /dev/null +++ b/packages-exp/remote-config-exp/src/remote_config.ts @@ -0,0 +1,88 @@ +/** + * @license + * Copyright 2019 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 { FirebaseApp } from '@firebase/app-types-exp'; +import { + RemoteConfig as RemoteConfigType, + FetchStatus, + Settings +} from '@firebase/remote-config-types-exp'; +import { StorageCache } from './storage/storage_cache'; +import { RemoteConfigFetchClient } from './client/remote_config_fetch_client'; +import { Storage } from './storage/storage'; +import { Logger } from '@firebase/logger'; + +const DEFAULT_FETCH_TIMEOUT_MILLIS = 60 * 1000; // One minute +const DEFAULT_CACHE_MAX_AGE_MILLIS = 12 * 60 * 60 * 1000; // Twelve hours. + +/** + * Encapsulates business logic mapping network and storage dependencies to the public SDK API. + * + * See {@link https://github.com/FirebasePrivate/firebase-js-sdk/blob/master/packages/firebase/index.d.ts|interface documentation} for method descriptions. + */ +export class RemoteConfig implements RemoteConfigType { + /** + * Tracks completion of initialization promise. + * @internal + */ + _isInitializationComplete = false; + + /** + * De-duplicates initialization calls. + * @internal + */ + _initializePromise?: Promise; + + settings: Settings = { + fetchTimeoutMillis: DEFAULT_FETCH_TIMEOUT_MILLIS, + minimumFetchIntervalMillis: DEFAULT_CACHE_MAX_AGE_MILLIS + }; + + defaultConfig: { [key: string]: string | number | boolean } = {}; + + get fetchTimeMillis(): number { + return this._storageCache.getLastSuccessfulFetchTimestampMillis() || -1; + } + + get lastFetchStatus(): FetchStatus { + return this._storageCache.getLastFetchStatus() || 'no-fetch-yet'; + } + + constructor( + // Required by FirebaseServiceFactory interface. + readonly app: FirebaseApp, + // JS doesn't support private yet + // (https://github.com/tc39/proposal-class-fields#private-fields), so we hint using an + // underscore prefix. + /** + * @internal + */ + readonly _client: RemoteConfigFetchClient, + /** + * @internal + */ + readonly _storageCache: StorageCache, + /** + * @internal + */ + readonly _storage: Storage, + /** + * @internal + */ + readonly _logger: Logger + ) {} +} diff --git a/packages-exp/remote-config-exp/src/storage/storage.ts b/packages-exp/remote-config-exp/src/storage/storage.ts new file mode 100644 index 00000000000..f5f457161b1 --- /dev/null +++ b/packages-exp/remote-config-exp/src/storage/storage.ts @@ -0,0 +1,260 @@ +/** + * @license + * Copyright 2019 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 { FetchStatus } from '@firebase/remote-config-types'; +import { + FetchResponse, + FirebaseRemoteConfigObject +} from '../client/remote_config_fetch_client'; +import { ERROR_FACTORY, ErrorCode } from '../errors'; +import { FirebaseError } from '@firebase/util'; + +/** + * Converts an error event associated with a {@link IDBRequest} to a {@link FirebaseError}. + */ +function toFirebaseError(event: Event, errorCode: ErrorCode): FirebaseError { + const originalError = (event.target as IDBRequest).error || undefined; + return ERROR_FACTORY.create(errorCode, { + originalErrorMessage: originalError && originalError.message + }); +} + +/** + * A general-purpose store keyed by app + namespace + {@link + * ProjectNamespaceKeyFieldValue}. + * + *

The Remote Config SDK can be used with multiple app installations, and each app can interact + * with multiple namespaces, so this store uses app (ID + name) and namespace as common parent keys + * for a set of key-value pairs. See {@link Storage#createCompositeKey}. + * + *

Visible for testing. + */ +export const APP_NAMESPACE_STORE = 'app_namespace_store'; + +const DB_NAME = 'firebase_remote_config'; +const DB_VERSION = 1; + +/** + * Encapsulates metadata concerning throttled fetch requests. + */ +export interface ThrottleMetadata { + // The number of times fetch has backed off. Used for resuming backoff after a timeout. + backoffCount: number; + // The Unix timestamp in milliseconds when callers can retry a request. + throttleEndTimeMillis: number; +} + +/** + * Provides type-safety for the "key" field used by {@link APP_NAMESPACE_STORE}. + * + *

This seems like a small price to avoid potentially subtle bugs caused by a typo. + */ +type ProjectNamespaceKeyFieldValue = + | 'active_config' + | 'active_config_etag' + | 'last_fetch_status' + | 'last_successful_fetch_timestamp_millis' + | 'last_successful_fetch_response' + | 'settings' + | 'throttle_metadata'; + +// Visible for testing. +export function openDatabase(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + request.onerror = event => { + reject(toFirebaseError(event, ErrorCode.STORAGE_OPEN)); + }; + request.onsuccess = event => { + resolve((event.target as IDBOpenDBRequest).result); + }; + request.onupgradeneeded = event => { + const db = (event.target as IDBOpenDBRequest).result; + + // We don't use 'break' in this switch statement, the fall-through + // behavior is what we want, because if there are multiple versions between + // the old version and the current version, we want ALL the migrations + // that correspond to those versions to run, not only the last one. + // eslint-disable-next-line default-case + switch (event.oldVersion) { + case 0: + db.createObjectStore(APP_NAMESPACE_STORE, { + keyPath: 'compositeKey' + }); + } + }; + }); +} + +/** + * Abstracts data persistence. + */ +export class Storage { + /** + * @param appId enables storage segmentation by app (ID + name). + * @param appName enables storage segmentation by app (ID + name). + * @param namespace enables storage segmentation by namespace. + */ + constructor( + private readonly appId: string, + private readonly appName: string, + private readonly namespace: string, + private readonly openDbPromise = openDatabase() + ) {} + + getLastFetchStatus(): Promise { + return this.get('last_fetch_status'); + } + + setLastFetchStatus(status: FetchStatus): Promise { + return this.set('last_fetch_status', status); + } + + // This is comparable to a cache entry timestamp. If we need to expire other data, we could + // consider adding timestamp to all storage records and an optional max age arg to getters. + getLastSuccessfulFetchTimestampMillis(): Promise { + return this.get('last_successful_fetch_timestamp_millis'); + } + + setLastSuccessfulFetchTimestampMillis(timestamp: number): Promise { + return this.set( + 'last_successful_fetch_timestamp_millis', + timestamp + ); + } + + getLastSuccessfulFetchResponse(): Promise { + return this.get('last_successful_fetch_response'); + } + + setLastSuccessfulFetchResponse(response: FetchResponse): Promise { + return this.set('last_successful_fetch_response', response); + } + + getActiveConfig(): Promise { + return this.get('active_config'); + } + + setActiveConfig(config: FirebaseRemoteConfigObject): Promise { + return this.set('active_config', config); + } + + getActiveConfigEtag(): Promise { + return this.get('active_config_etag'); + } + + setActiveConfigEtag(etag: string): Promise { + return this.set('active_config_etag', etag); + } + + getThrottleMetadata(): Promise { + return this.get('throttle_metadata'); + } + + setThrottleMetadata(metadata: ThrottleMetadata): Promise { + return this.set('throttle_metadata', metadata); + } + + deleteThrottleMetadata(): Promise { + return this.delete('throttle_metadata'); + } + + async get(key: ProjectNamespaceKeyFieldValue): Promise { + const db = await this.openDbPromise; + return new Promise((resolve, reject) => { + const transaction = db.transaction([APP_NAMESPACE_STORE], 'readonly'); + const objectStore = transaction.objectStore(APP_NAMESPACE_STORE); + const compositeKey = this.createCompositeKey(key); + try { + const request = objectStore.get(compositeKey); + request.onerror = event => { + reject(toFirebaseError(event, ErrorCode.STORAGE_GET)); + }; + request.onsuccess = event => { + const result = (event.target as IDBRequest).result; + if (result) { + resolve(result.value); + } else { + resolve(undefined); + } + }; + } catch (e) { + reject( + ERROR_FACTORY.create(ErrorCode.STORAGE_GET, { + originalErrorMessage: e && e.message + }) + ); + } + }); + } + + async set(key: ProjectNamespaceKeyFieldValue, value: T): Promise { + const db = await this.openDbPromise; + return new Promise((resolve, reject) => { + const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite'); + const objectStore = transaction.objectStore(APP_NAMESPACE_STORE); + const compositeKey = this.createCompositeKey(key); + try { + const request = objectStore.put({ + compositeKey, + value + }); + request.onerror = (event: Event) => { + reject(toFirebaseError(event, ErrorCode.STORAGE_SET)); + }; + request.onsuccess = () => { + resolve(); + }; + } catch (e) { + reject( + ERROR_FACTORY.create(ErrorCode.STORAGE_SET, { + originalErrorMessage: e && e.message + }) + ); + } + }); + } + + async delete(key: ProjectNamespaceKeyFieldValue): Promise { + const db = await this.openDbPromise; + return new Promise((resolve, reject) => { + const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite'); + const objectStore = transaction.objectStore(APP_NAMESPACE_STORE); + const compositeKey = this.createCompositeKey(key); + try { + const request = objectStore.delete(compositeKey); + request.onerror = (event: Event) => { + reject(toFirebaseError(event, ErrorCode.STORAGE_DELETE)); + }; + request.onsuccess = () => { + resolve(); + }; + } catch (e) { + reject( + ERROR_FACTORY.create(ErrorCode.STORAGE_DELETE, { + originalErrorMessage: e && e.message + }) + ); + } + }); + } + + // Facilitates composite key functionality (which is unsupported in IE). + createCompositeKey(key: ProjectNamespaceKeyFieldValue): string { + return [this.appId, this.appName, this.namespace, key].join(); + } +} diff --git a/packages-exp/remote-config-exp/src/storage/storage_cache.ts b/packages-exp/remote-config-exp/src/storage/storage_cache.ts new file mode 100644 index 00000000000..5ffbdba20c0 --- /dev/null +++ b/packages-exp/remote-config-exp/src/storage/storage_cache.ts @@ -0,0 +1,99 @@ +/** + * @license + * Copyright 2019 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 { FetchStatus } from '@firebase/remote-config-types'; +import { FirebaseRemoteConfigObject } from '../client/remote_config_fetch_client'; +import { Storage } from './storage'; + +/** + * A memory cache layer over storage to support the SDK's synchronous read requirements. + */ +export class StorageCache { + constructor(private readonly storage: Storage) {} + + /** + * Memory caches. + */ + private lastFetchStatus?: FetchStatus; + private lastSuccessfulFetchTimestampMillis?: number; + private activeConfig?: FirebaseRemoteConfigObject; + + /** + * Memory-only getters + */ + getLastFetchStatus(): FetchStatus | undefined { + return this.lastFetchStatus; + } + + getLastSuccessfulFetchTimestampMillis(): number | undefined { + return this.lastSuccessfulFetchTimestampMillis; + } + + getActiveConfig(): FirebaseRemoteConfigObject | undefined { + return this.activeConfig; + } + + /** + * Read-ahead getter + */ + async loadFromStorage(): Promise { + const lastFetchStatusPromise = this.storage.getLastFetchStatus(); + const lastSuccessfulFetchTimestampMillisPromise = this.storage.getLastSuccessfulFetchTimestampMillis(); + const activeConfigPromise = this.storage.getActiveConfig(); + + // Note: + // 1. we consistently check for undefined to avoid clobbering defined values + // in memory + // 2. we defer awaiting to improve readability, as opposed to destructuring + // a Promise.all result, for example + + const lastFetchStatus = await lastFetchStatusPromise; + if (lastFetchStatus) { + this.lastFetchStatus = lastFetchStatus; + } + + const lastSuccessfulFetchTimestampMillis = await lastSuccessfulFetchTimestampMillisPromise; + if (lastSuccessfulFetchTimestampMillis) { + this.lastSuccessfulFetchTimestampMillis = lastSuccessfulFetchTimestampMillis; + } + + const activeConfig = await activeConfigPromise; + if (activeConfig) { + this.activeConfig = activeConfig; + } + } + + /** + * Write-through setters + */ + setLastFetchStatus(status: FetchStatus): Promise { + this.lastFetchStatus = status; + return this.storage.setLastFetchStatus(status); + } + + setLastSuccessfulFetchTimestampMillis( + timestampMillis: number + ): Promise { + this.lastSuccessfulFetchTimestampMillis = timestampMillis; + return this.storage.setLastSuccessfulFetchTimestampMillis(timestampMillis); + } + + setActiveConfig(activeConfig: FirebaseRemoteConfigObject): Promise { + this.activeConfig = activeConfig; + return this.storage.setActiveConfig(activeConfig); + } +} diff --git a/packages-exp/remote-config-exp/src/value.ts b/packages-exp/remote-config-exp/src/value.ts new file mode 100644 index 00000000000..f3fb6ff9581 --- /dev/null +++ b/packages-exp/remote-config-exp/src/value.ts @@ -0,0 +1,57 @@ +/** + * @license + * Copyright 2019 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 { Value as ValueType, ValueSource } from '@firebase/remote-config-types'; + +const DEFAULT_VALUE_FOR_BOOLEAN = false; +const DEFAULT_VALUE_FOR_STRING = ''; +const DEFAULT_VALUE_FOR_NUMBER = 0; + +const BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on']; + +export class Value implements ValueType { + constructor( + private readonly _source: ValueSource, + private readonly _value: string = DEFAULT_VALUE_FOR_STRING + ) {} + + asString(): string { + return this._value; + } + + asBoolean(): boolean { + if (this._source === 'static') { + return DEFAULT_VALUE_FOR_BOOLEAN; + } + return BOOLEAN_TRUTHY_VALUES.indexOf(this._value.toLowerCase()) >= 0; + } + + asNumber(): number { + if (this._source === 'static') { + return DEFAULT_VALUE_FOR_NUMBER; + } + let num = Number(this._value); + if (isNaN(num)) { + num = DEFAULT_VALUE_FOR_NUMBER; + } + return num; + } + + getSource(): ValueSource { + return this._source; + } +} diff --git a/packages-exp/remote-config-exp/test/client/caching_client.test.ts b/packages-exp/remote-config-exp/test/client/caching_client.test.ts new file mode 100644 index 00000000000..a808dffb605 --- /dev/null +++ b/packages-exp/remote-config-exp/test/client/caching_client.test.ts @@ -0,0 +1,160 @@ +/** + * @license + * Copyright 2019 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 '../setup'; +import { expect } from 'chai'; +import { + RemoteConfigFetchClient, + FetchResponse, + FetchRequest, + RemoteConfigAbortSignal +} from '../../src/client/remote_config_fetch_client'; +import * as sinon from 'sinon'; +import { CachingClient } from '../../src/client/caching_client'; +import { StorageCache } from '../../src/storage/storage_cache'; +import { Storage } from '../../src/storage/storage'; +import { Logger } from '@firebase/logger'; + +const DEFAULT_REQUEST: FetchRequest = { + // Invalidates cache by default. + cacheMaxAgeMillis: 0, + signal: new RemoteConfigAbortSignal() +}; + +describe('CachingClient', () => { + const backingClient = {} as RemoteConfigFetchClient; + const storageCache = {} as StorageCache; + const logger = {} as Logger; + const storage = {} as Storage; + let cachingClient: CachingClient; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + logger.debug = sinon.stub(); + cachingClient = new CachingClient( + backingClient, + storage, + storageCache, + logger + ); + clock = sinon.useFakeTimers({ now: 3000 }); // Mocks Date.now as 3000. + }); + + afterEach(() => { + clock.restore(); + }); + + describe('isCacheDataFresh', () => { + it('returns false if cached response is older than max age', () => { + expect( + cachingClient.isCachedDataFresh( + // Mocks a cache set when Date.now was 1000, ie it's two seconds old. + 1000, + // Tolerates a cache one second old. + 1000 + ) + ).to.be.false; + }); + + it('returns true if cached response is equal to max age', () => { + expect(cachingClient.isCachedDataFresh(2000, 1000)).to.be.true; + }); + + it('returns true if cached response is younger than max age', () => { + expect(cachingClient.isCachedDataFresh(3000, 1000)).to.be.true; + }); + }); + + describe('fetch', () => { + beforeEach(() => { + storage.getLastSuccessfulFetchTimestampMillis = sinon + .stub() + .returns(1000); // Mocks a cache set when Date.now was 1000, ie it's two seconds old. + storageCache.setLastSuccessfulFetchTimestampMillis = sinon.stub(); + storage.getLastSuccessfulFetchResponse = sinon.stub(); + storage.setLastSuccessfulFetchResponse = sinon.stub(); + backingClient.fetch = sinon.stub().returns(Promise.resolve({})); + }); + + it('exits early on cache hit', async () => { + const expectedResponse = { config: { eTag: 'etag', color: 'taupe' } }; + storage.getLastSuccessfulFetchResponse = sinon + .stub() + .returns(expectedResponse); + + const actualResponse = await cachingClient.fetch({ + cacheMaxAgeMillis: 2000, + signal: new RemoteConfigAbortSignal() + }); + + expect(actualResponse).to.deep.eq(expectedResponse); + expect(backingClient.fetch).not.to.have.been.called; + }); + + it('fetches on cache miss', async () => { + await cachingClient.fetch(DEFAULT_REQUEST); + + expect(backingClient.fetch).to.have.been.called; + }); + + it('passes etag from last successful fetch', async () => { + const lastSuccessfulFetchResponse = { eTag: 'etag' } as FetchResponse; + storage.getLastSuccessfulFetchResponse = sinon + .stub() + .returns(lastSuccessfulFetchResponse); + + await cachingClient.fetch(DEFAULT_REQUEST); + + expect(backingClient.fetch).to.have.been.calledWith( + Object.assign({}, DEFAULT_REQUEST, { + eTag: lastSuccessfulFetchResponse.eTag + }) + ); + }); + + it('caches timestamp and response if status is 200', async () => { + const response = { + status: 200, + eTag: 'etag', + config: { color: 'clear' } + }; + backingClient.fetch = sinon.stub().returns(Promise.resolve(response)); + + await cachingClient.fetch(DEFAULT_REQUEST); + + expect( + storageCache.setLastSuccessfulFetchTimestampMillis + ).to.have.been.calledWith(3000); // Based on mock timer in beforeEach. + expect(storage.setLastSuccessfulFetchResponse).to.have.been.calledWith( + response + ); + }); + + it('sets timestamp, but not config, if 304', async () => { + backingClient.fetch = sinon + .stub() + .returns(Promise.resolve({ status: 304 })); + + await cachingClient.fetch(DEFAULT_REQUEST); + + expect( + storageCache.setLastSuccessfulFetchTimestampMillis + ).to.have.been.calledWith(3000); // Based on mock timer in beforeEach. + expect(storage.setLastSuccessfulFetchResponse).not.to.have.been.called; + }); + }); +}); diff --git a/packages-exp/remote-config-exp/test/client/rest_client.test.ts b/packages-exp/remote-config-exp/test/client/rest_client.test.ts new file mode 100644 index 00000000000..89b745dacca --- /dev/null +++ b/packages-exp/remote-config-exp/test/client/rest_client.test.ts @@ -0,0 +1,270 @@ +/** + * @license + * Copyright 2019 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 '../setup'; +import { expect } from 'chai'; +import { RestClient } from '../../src/client/rest_client'; +import { FirebaseInstallations } from '@firebase/installations-types'; +import * as sinon from 'sinon'; +import { ERROR_FACTORY, ErrorCode } from '../../src/errors'; +import { FirebaseError } from '@firebase/util'; +import { + FetchRequest, + RemoteConfigAbortSignal +} from '../../src/client/remote_config_fetch_client'; + +const DEFAULT_REQUEST: FetchRequest = { + cacheMaxAgeMillis: 1, + signal: new RemoteConfigAbortSignal() +}; + +describe('RestClient', () => { + const firebaseInstallations = {} as FirebaseInstallations; + let client: RestClient; + + beforeEach(() => { + client = new RestClient( + firebaseInstallations, + 'sdk-version', + 'namespace', + 'project-id', + 'api-key', + 'app-id' + ); + firebaseInstallations.getId = sinon + .stub() + .returns(Promise.resolve('fis-id')); + firebaseInstallations.getToken = sinon + .stub() + .returns(Promise.resolve('fis-token')); + }); + + describe('fetch', () => { + let fetchStub: sinon.SinonStub< + [RequestInfo, RequestInit?], + Promise + >; + + beforeEach(() => { + fetchStub = sinon + .stub(window, 'fetch') + .returns(Promise.resolve(new Response('{}'))); + }); + + afterEach(() => { + fetchStub.restore(); + }); + + it('handles 200/UPDATE responses', async () => { + const expectedResponse = { + status: 200, + eTag: 'etag', + state: 'UPDATE', + entries: { color: 'sparkling' } + }; + + fetchStub.returns( + Promise.resolve({ + ok: true, + status: expectedResponse.status, + headers: new Headers({ ETag: expectedResponse.eTag }), + json: () => + Promise.resolve({ + entries: expectedResponse.entries, + state: expectedResponse.state + }) + } as Response) + ); + + const response = await client.fetch(DEFAULT_REQUEST); + + expect(response).to.deep.eq({ + status: expectedResponse.status, + eTag: expectedResponse.eTag, + config: expectedResponse.entries + }); + }); + + it('calls the correct endpoint', async () => { + await client.fetch(DEFAULT_REQUEST); + + expect(fetchStub).to.be.calledWith( + 'https://firebaseremoteconfig.googleapis.com/v1/projects/project-id/namespaces/namespace:fetch?key=api-key', + sinon.match.object + ); + }); + + it('passes injected params', async () => { + await client.fetch(DEFAULT_REQUEST); + + expect(fetchStub).to.be.calledWith( + sinon.match.string, + sinon.match({ + body: + '{"sdk_version":"sdk-version","app_instance_id":"fis-id","app_instance_id_token":"fis-token","app_id":"app-id","language_code":"en-US"}' + }) + ); + }); + + it('throws on network failure', async () => { + // The Fetch API throws a TypeError on network falure: + // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Exceptions + const originalError = new TypeError('Network request failed'); + fetchStub.returns(Promise.reject(originalError)); + + const fetchPromise = client.fetch(DEFAULT_REQUEST); + + const firebaseError = ERROR_FACTORY.create(ErrorCode.FETCH_NETWORK, { + originalErrorMessage: originalError.message + }); + + await expect(fetchPromise) + .to.eventually.be.rejectedWith(FirebaseError, firebaseError.message) + .with.nested.property( + 'customData.originalErrorMessage', + 'Network request failed' + ); + }); + + it('throws on JSON parse failure', async () => { + // JSON parsing throws a SyntaxError on failure: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Exceptions + const res = new Response(/* empty body */); + sinon + .stub(res, 'json') + .throws(new SyntaxError('Unexpected end of input')); + fetchStub.returns(Promise.resolve(res)); + + const fetchPromise = client.fetch(DEFAULT_REQUEST); + + const firebaseError = ERROR_FACTORY.create(ErrorCode.FETCH_PARSE, { + originalErrorMessage: 'Unexpected end of input' + }); + + await expect(fetchPromise) + .to.eventually.be.rejectedWith(FirebaseError, firebaseError.message) + .with.nested.property( + 'customData.originalErrorMessage', + 'Unexpected end of input' + ); + }); + + it('handles 304 status code and empty body', async () => { + fetchStub.returns( + Promise.resolve({ + status: 304, + headers: new Headers({ ETag: 'response-etag' }) + } as Response) + ); + + const response = await client.fetch( + Object.assign({}, DEFAULT_REQUEST, { + eTag: 'request-etag' + }) + ); + + expect(fetchStub).to.be.calledWith( + sinon.match.string, + sinon.match({ headers: { 'If-None-Match': 'request-etag' } }) + ); + + expect(response).to.deep.eq({ + status: 304, + eTag: 'response-etag', + config: undefined + }); + }); + + it('normalizes INSTANCE_STATE_UNSPECIFIED state to server error', async () => { + fetchStub.returns( + Promise.resolve({ + status: 200, + headers: new Headers({ ETag: 'etag' }), + json: async () => ({ state: 'INSTANCE_STATE_UNSPECIFIED' }) + } as Response) + ); + + const fetchPromise = client.fetch(DEFAULT_REQUEST); + + const error = ERROR_FACTORY.create(ErrorCode.FETCH_STATUS, { + httpStatus: 500 + }); + + await expect(fetchPromise) + .to.eventually.be.rejectedWith(FirebaseError, error.message) + .with.nested.property('customData.httpStatus', 500); + }); + + it('normalizes NO_CHANGE state to 304 status', async () => { + fetchStub.returns( + Promise.resolve({ + status: 200, + headers: new Headers({ ETag: 'etag' }), + json: async () => ({ state: 'NO_CHANGE' }) + } as Response) + ); + + const response = await client.fetch(DEFAULT_REQUEST); + + expect(response).to.deep.eq({ + status: 304, + eTag: 'etag', + config: undefined + }); + }); + + it('normalizes empty change states', async () => { + for (const state of ['NO_TEMPLATE', 'EMPTY_CONFIG']) { + fetchStub.returns( + Promise.resolve({ + status: 200, + headers: new Headers({ ETag: 'etag' }), + json: async () => ({ state }) + } as Response) + ); + + await expect(client.fetch(DEFAULT_REQUEST)).to.eventually.be.deep.eq({ + status: 200, + eTag: 'etag', + config: {} + }); + } + }); + + it('throws error on HTTP error status', async () => { + // Error codes from logs plus an arbitrary unexpected code (300) + for (const status of [300, 400, 403, 404, 415, 429, 500, 503, 504]) { + fetchStub.returns( + Promise.resolve({ + status, + headers: new Headers() + } as Response) + ); + + const fetchPromise = client.fetch(DEFAULT_REQUEST); + + const error = ERROR_FACTORY.create(ErrorCode.FETCH_STATUS, { + httpStatus: status + }); + + await expect(fetchPromise) + .to.eventually.be.rejectedWith(FirebaseError, error.message) + .with.nested.property('customData.httpStatus', status); + } + }); + }); +}); diff --git a/packages-exp/remote-config-exp/test/client/retrying_client.test.ts b/packages-exp/remote-config-exp/test/client/retrying_client.test.ts new file mode 100644 index 00000000000..65641b438bd --- /dev/null +++ b/packages-exp/remote-config-exp/test/client/retrying_client.test.ts @@ -0,0 +1,218 @@ +/** + * @license + * Copyright 2019 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 { expect } from 'chai'; +import * as sinon from 'sinon'; +import { Storage, ThrottleMetadata } from '../../src/storage/storage'; +import { + RemoteConfigFetchClient, + FetchRequest, + FetchResponse, + RemoteConfigAbortSignal +} from '../../src/client/remote_config_fetch_client'; +import { + setAbortableTimeout, + RetryingClient +} from '../../src/client/retrying_client'; +import { ErrorCode, ERROR_FACTORY } from '../../src/errors'; +import '../setup'; + +const DEFAULT_REQUEST: FetchRequest = { + cacheMaxAgeMillis: 1, + signal: new RemoteConfigAbortSignal() +}; + +describe('RetryingClient', () => { + let backingClient: RemoteConfigFetchClient; + let storage: Storage; + let retryingClient: RetryingClient; + let abortSignal: RemoteConfigAbortSignal; + + beforeEach(() => { + backingClient = {} as RemoteConfigFetchClient; + storage = {} as Storage; + retryingClient = new RetryingClient(backingClient, storage); + storage.getThrottleMetadata = sinon.stub().returns(Promise.resolve()); + storage.deleteThrottleMetadata = sinon.stub().returns(Promise.resolve()); + storage.setThrottleMetadata = sinon.stub().returns(Promise.resolve()); + backingClient.fetch = sinon + .stub() + .returns(Promise.resolve({ status: 200 })); + abortSignal = new RemoteConfigAbortSignal(); + }); + + describe('setAbortableTimeout', () => { + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + // Sets Date.now() to zero. + clock = sinon.useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('Derives backoff from end time', async () => { + const setTimeoutSpy = sinon.spy(window, 'setTimeout'); + + const timeoutPromise = setAbortableTimeout(abortSignal, Date.now() + 1); + + // Advances mocked clock so setTimeout logic runs. + clock.runAll(); + + await timeoutPromise; + + expect(setTimeoutSpy).to.have.been.calledWith(sinon.match.any, 1); + }); + + it('Normalizes end time in the past to zero backoff', async () => { + const setTimeoutSpy = sinon.spy(window, 'setTimeout'); + + const timeoutPromise = setAbortableTimeout(abortSignal, Date.now() - 1); + + // Advances mocked clock so setTimeout logic runs. + clock.runAll(); + + await timeoutPromise; + + expect(setTimeoutSpy).to.have.been.calledWith(sinon.match.any, 0); + + setTimeoutSpy.restore(); + }); + + it('listens for abort event and rejects promise', async () => { + const throttleEndTimeMillis = 1000; + + const timeoutPromise = setAbortableTimeout( + abortSignal, + throttleEndTimeMillis + ); + + abortSignal.abort(); + + const expectedError = ERROR_FACTORY.create(ErrorCode.FETCH_THROTTLE, { + throttleEndTimeMillis + }); + + await expect(timeoutPromise).to.eventually.be.rejectedWith( + expectedError.message + ); + }); + }); + + describe('fetch', () => { + it('returns success response', async () => { + const setTimeoutSpy = sinon.spy(window, 'setTimeout'); + + const expectedResponse: FetchResponse = { + status: 200, + eTag: 'etag', + config: {} + }; + backingClient.fetch = sinon + .stub() + .returns(Promise.resolve(expectedResponse)); + + const actualResponse = retryingClient.fetch(DEFAULT_REQUEST); + + await expect(actualResponse).to.eventually.deep.eq(expectedResponse); + + // Asserts setTimeout is passed a zero delay, since throttleEndTimeMillis is set to Date.now, + // which is faked to be a constant. + expect(setTimeoutSpy).to.have.been.calledWith(sinon.match.any, 0); + + expect(storage.deleteThrottleMetadata).to.have.been.called; + + setTimeoutSpy.restore(); + }); + + it('rethrows unretriable errors rather than retrying', async () => { + const expectedError = ERROR_FACTORY.create(ErrorCode.FETCH_STATUS, { + httpStatus: 400 + }); + backingClient.fetch = sinon.stub().returns(Promise.reject(expectedError)); + + const fetchPromise = retryingClient.fetch(DEFAULT_REQUEST); + + await expect(fetchPromise).to.eventually.be.rejectedWith(expectedError); + }); + + it('retries on retriable errors', async () => { + // Configures Date.now() to advance clock from zero in 20ms increments, enabling + // tests to assert a known throttle end time and allow setTimeout to work. + const clock = sinon.useFakeTimers({ shouldAdvanceTime: true }); + + // Ensures backoff is always zero, which simplifies reasoning about timer. + const powSpy = sinon.stub(Math, 'pow').returns(0); + const randomSpy = sinon.stub(Math, 'random').returns(0.5); + + // Simulates a service call that returns errors several times before returning success. + // Error codes from logs. + const errorResponseStatuses = [429, 500, 503, 504]; + const errorResponseCount = errorResponseStatuses.length; + + backingClient.fetch = sinon.stub().callsFake(() => { + const httpStatus = errorResponseStatuses.pop(); + + if (httpStatus) { + // Triggers retry by returning a retriable status code. + const expectedError = ERROR_FACTORY.create(ErrorCode.FETCH_STATUS, { + httpStatus + }); + return Promise.reject(expectedError); + } + + // Halts retrying by returning success. + // Note backoff never terminates if the server always errors. + return Promise.resolve({ status: 200 }); + }); + + await retryingClient.fetch(DEFAULT_REQUEST); + + // Asserts throttle metadata was persisted after each error response. + for (let i = 1; i <= errorResponseCount; i++) { + expect(storage.setThrottleMetadata).to.have.been.calledWith({ + backoffCount: i, + throttleEndTimeMillis: i * 20 + }); + } + + powSpy.restore(); + randomSpy.restore(); + clock.restore(); + }); + }); + + describe('attemptFetch', () => { + it('honors metadata when initializing', async () => { + const clock = sinon.useFakeTimers({ shouldAdvanceTime: true }); + const setTimeoutSpy = sinon.spy(window, 'setTimeout'); + + const throttleMetadata = { + throttleEndTimeMillis: 123 + } as ThrottleMetadata; + + await retryingClient.attemptFetch(DEFAULT_REQUEST, throttleMetadata); + + expect(setTimeoutSpy).to.have.been.calledWith(sinon.match.any, 123); + + clock.restore(); + setTimeoutSpy.restore(); + }); + }); +}); diff --git a/packages-exp/remote-config-exp/test/errors.test.ts b/packages-exp/remote-config-exp/test/errors.test.ts new file mode 100644 index 00000000000..67d41bfa505 --- /dev/null +++ b/packages-exp/remote-config-exp/test/errors.test.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2019 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 { expect } from 'chai'; +import { hasErrorCode, ERROR_FACTORY, ErrorCode } from '../src/errors'; +import './setup'; + +describe('hasErrorCode', () => { + it('defaults false', () => { + const error = new Error(); + expect(hasErrorCode(error, ErrorCode.REGISTRATION_PROJECT_ID)).to.be.false; + }); + it('returns true for FirebaseError with given code', () => { + const error = ERROR_FACTORY.create(ErrorCode.REGISTRATION_PROJECT_ID); + expect(hasErrorCode(error, ErrorCode.REGISTRATION_PROJECT_ID)).to.be.true; + }); +}); diff --git a/packages-exp/remote-config-exp/test/language.test.ts b/packages-exp/remote-config-exp/test/language.test.ts new file mode 100644 index 00000000000..a92630467d4 --- /dev/null +++ b/packages-exp/remote-config-exp/test/language.test.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright 2019 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 { expect } from 'chai'; +import { getUserLanguage } from '../src/language'; +import './setup'; + +// Adapts getUserLanguage tests from packages/auth/test/utils_test.js for TypeScript. +describe('getUserLanguage', () => { + it('prioritizes navigator.languages', () => { + expect( + getUserLanguage({ + languages: ['de', 'en'], + language: 'en' + }) + ).to.eq('de'); + }); + + it('falls back to navigator.language', () => { + expect( + getUserLanguage({ + language: 'en' + } as NavigatorLanguage) + ).to.eq('en'); + }); + + it('defaults undefined', () => { + expect(getUserLanguage({} as NavigatorLanguage)).to.be.undefined; + }); +}); diff --git a/packages-exp/remote-config-exp/test/remote_config.test.ts b/packages-exp/remote-config-exp/test/remote_config.test.ts new file mode 100644 index 00000000000..128bcfff145 --- /dev/null +++ b/packages-exp/remote-config-exp/test/remote_config.test.ts @@ -0,0 +1,522 @@ +/** + * @license + * Copyright 2019 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 { FirebaseApp } from '@firebase/app-types-exp'; +import { + RemoteConfig as RemoteConfigType, + LogLevel as RemoteConfigLogLevel +} from '@firebase/remote-config-types-exp'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { StorageCache } from '../src/storage/storage_cache'; +import { Storage } from '../src/storage/storage'; +import { RemoteConfig } from '../src/remote_config'; +import { + RemoteConfigFetchClient, + FetchResponse +} from '../src/client/remote_config_fetch_client'; +import { Value } from '../src/value'; +import './setup'; +import { ERROR_FACTORY, ErrorCode } from '../src/errors'; +import { Logger, LogLevel as FirebaseLogLevel } from '@firebase/logger'; +import { + activate, + ensureInitialized, + getAll, + getBoolean, + getNumber, + getString, + getValue, + setLogLevel, + fetchConfig +} from '../src/api'; +import * as api from '../src/api'; +import { fetchAndActivate } from '../src'; +import { restore } from 'sinon'; + +describe('RemoteConfig', () => { + const ACTIVE_CONFIG = { + key1: 'active_config_value_1', + key2: 'active_config_value_2', + key3: 'true', + key4: '123' + }; + const DEFAULT_CONFIG = { + key1: 'default_config_value_1', + key2: 'default_config_value_2', + key3: 'false', + key4: '345', + test: 'test' + }; + + let app: FirebaseApp; + let client: RemoteConfigFetchClient; + let storageCache: StorageCache; + let storage: Storage; + let logger: Logger; + let rc: RemoteConfigType; + + let getActiveConfigStub: sinon.SinonStub; + let loggerDebugSpy: sinon.SinonSpy; + let loggerLogLevelSpy: any; + + beforeEach(() => { + // Clears stubbed behavior between each test. + app = {} as FirebaseApp; + client = {} as RemoteConfigFetchClient; + storageCache = {} as StorageCache; + storage = {} as Storage; + logger = new Logger('package-name'); + getActiveConfigStub = sinon.stub().returns(undefined); + storageCache.getActiveConfig = getActiveConfigStub; + loggerDebugSpy = sinon.spy(logger, 'debug'); + loggerLogLevelSpy = sinon.spy(logger, 'logLevel', ['set']); + rc = new RemoteConfig(app, client, storageCache, storage, logger); + }); + + afterEach(() => { + loggerDebugSpy.restore(); + loggerLogLevelSpy.restore(); + }); + + // Adapts getUserLanguage tests from packages/auth/test/utils_test.js for TypeScript. + describe('setLogLevel', () => { + it('proxies to the FirebaseLogger instance', () => { + setLogLevel(rc, 'debug'); + + // Casts spy to any because property setters aren't defined on the SinonSpy type. + expect(loggerLogLevelSpy.set).to.have.been.calledWith( + FirebaseLogLevel.DEBUG + ); + }); + + it('normalizes levels other than DEBUG and SILENT to ERROR', () => { + for (const logLevel of ['info', 'verbose', 'error', 'severe']) { + setLogLevel(rc, logLevel as RemoteConfigLogLevel); + + // Casts spy to any because property setters aren't defined on the SinonSpy type. + expect(loggerLogLevelSpy.set).to.have.been.calledWith( + FirebaseLogLevel.ERROR + ); + } + }); + }); + + describe('ensureInitialized', () => { + it('warms cache', async () => { + storageCache.loadFromStorage = sinon.stub().returns(Promise.resolve()); + + await ensureInitialized(rc); + + expect(storageCache.loadFromStorage).to.have.been.calledOnce; + }); + + it('de-duplicates repeated calls', async () => { + storageCache.loadFromStorage = sinon.stub().returns(Promise.resolve()); + + await ensureInitialized(rc); + await ensureInitialized(rc); + + expect(storageCache.loadFromStorage).to.have.been.calledOnce; + }); + }); + + describe('fetchTimeMillis', () => { + it('normalizes undefined values', async () => { + storageCache.getLastSuccessfulFetchTimestampMillis = sinon + .stub() + .returns(undefined); + + expect(rc.fetchTimeMillis).to.eq(-1); + }); + + it('reads from cache', async () => { + const lastFetchTimeMillis = 123; + + storageCache.getLastSuccessfulFetchTimestampMillis = sinon + .stub() + .returns(lastFetchTimeMillis); + + expect(rc.fetchTimeMillis).to.eq(lastFetchTimeMillis); + }); + }); + + describe('lastFetchStatus', () => { + it('normalizes undefined values', async () => { + storageCache.getLastFetchStatus = sinon.stub().returns(undefined); + + expect(rc.lastFetchStatus).to.eq('no-fetch-yet'); + }); + + it('reads from cache', async () => { + const lastFetchStatus = 'success'; + + storageCache.getLastFetchStatus = sinon.stub().returns(lastFetchStatus); + + expect(rc.lastFetchStatus).to.eq(lastFetchStatus); + }); + }); + + describe('getValue', () => { + it('returns the active value if available', () => { + getActiveConfigStub.returns(ACTIVE_CONFIG); + rc.defaultConfig = DEFAULT_CONFIG; + + expect(getValue(rc, 'key1')).to.deep.eq( + new Value('remote', ACTIVE_CONFIG.key1) + ); + }); + + it('returns the default value if active is not available', () => { + rc.defaultConfig = DEFAULT_CONFIG; + + expect(getValue(rc, 'key1')).to.deep.eq( + new Value('default', DEFAULT_CONFIG.key1) + ); + }); + + it('returns the stringified default boolean values if active is not available', () => { + const DEFAULTS = { trueVal: true, falseVal: false }; + rc.defaultConfig = DEFAULTS; + + expect(getValue(rc, 'trueVal')).to.deep.eq( + new Value('default', String(DEFAULTS.trueVal)) + ); + expect(getValue(rc, 'falseVal')).to.deep.eq( + new Value('default', String(DEFAULTS.falseVal)) + ); + }); + + it('returns the stringified default numeric values if active is not available', () => { + const DEFAULTS = { negative: -1, zero: 0, positive: 11 }; + rc.defaultConfig = DEFAULTS; + + expect(getValue(rc, 'negative')).to.deep.eq( + new Value('default', String(DEFAULTS.negative)) + ); + expect(getValue(rc, 'zero')).to.deep.eq( + new Value('default', String(DEFAULTS.zero)) + ); + expect(getValue(rc, 'positive')).to.deep.eq( + new Value('default', String(DEFAULTS.positive)) + ); + }); + + it('returns the static value if active and default are not available', () => { + expect(getValue(rc, 'key1')).to.deep.eq(new Value('static')); + + // Asserts debug message logged if static value is returned, per EAP feedback. + expect(logger.debug).to.have.been.called; + }); + + it('logs if initialization is incomplete', async () => { + // Defines default value to isolate initialization logging from static value logging. + rc.defaultConfig = { key1: 'val' }; + + // Gets value before initialization. + getValue(rc, 'key1'); + + // Asserts getValue logs. + expect(logger.debug).to.have.been.called; + + // Enables initialization to complete. + storageCache.loadFromStorage = sinon.stub().returns(Promise.resolve()); + + // Ensures initialization completes. + await ensureInitialized(rc); + + // Gets value after initialization. + getValue(rc, 'key1'); + + // Asserts getValue doesn't log after initialization is complete. + expect(logger.debug).to.have.been.calledOnce; + }); + }); + + describe('getBoolean', () => { + it('returns the active value if available', () => { + getActiveConfigStub.returns(ACTIVE_CONFIG); + rc.defaultConfig = DEFAULT_CONFIG; + + expect(getBoolean(rc, 'key3')).to.be.true; + }); + + it('returns the default value if active is not available', () => { + rc.defaultConfig = DEFAULT_CONFIG; + + expect(getBoolean(rc, 'key3')).to.be.false; + }); + + it('returns the static value if active and default are not available', () => { + expect(getBoolean(rc, 'key3')).to.be.false; + }); + }); + + describe('getString', () => { + it('returns the active value if available', () => { + getActiveConfigStub.returns(ACTIVE_CONFIG); + rc.defaultConfig = DEFAULT_CONFIG; + + expect(getString(rc, 'key1')).to.eq(ACTIVE_CONFIG.key1); + }); + + it('returns the default value if active is not available', () => { + rc.defaultConfig = DEFAULT_CONFIG; + + expect(getString(rc, 'key2')).to.eq(DEFAULT_CONFIG.key2); + }); + + it('returns the static value if active and default are not available', () => { + expect(getString(rc, 'key1')).to.eq(''); + }); + }); + + describe('getNumber', () => { + it('returns the active value if available', () => { + getActiveConfigStub.returns(ACTIVE_CONFIG); + rc.defaultConfig = DEFAULT_CONFIG; + + expect(getNumber(rc, 'key4')).to.eq(Number(ACTIVE_CONFIG.key4)); + }); + + it('returns the default value if active is not available', () => { + rc.defaultConfig = DEFAULT_CONFIG; + + expect(getNumber(rc, 'key4')).to.eq(Number(DEFAULT_CONFIG.key4)); + }); + + it('returns the static value if active and default are not available', () => { + expect(getNumber(rc, 'key1')).to.eq(0); + }); + }); + + describe('getAll', () => { + it('returns values for all keys included in active and default configs', () => { + getActiveConfigStub.returns(ACTIVE_CONFIG); + rc.defaultConfig = DEFAULT_CONFIG; + + expect(getAll(rc)).to.deep.eq({ + key1: new Value('remote', ACTIVE_CONFIG.key1), + key2: new Value('remote', ACTIVE_CONFIG.key2), + key3: new Value('remote', ACTIVE_CONFIG.key3), + key4: new Value('remote', ACTIVE_CONFIG.key4), + test: new Value('default', DEFAULT_CONFIG.test) + }); + }); + + it('returns values in default if active is not available', () => { + rc.defaultConfig = DEFAULT_CONFIG; + + expect(getAll(rc)).to.deep.eq({ + key1: new Value('default', DEFAULT_CONFIG.key1), + key2: new Value('default', DEFAULT_CONFIG.key2), + key3: new Value('default', DEFAULT_CONFIG.key3), + key4: new Value('default', DEFAULT_CONFIG.key4), + test: new Value('default', DEFAULT_CONFIG.test) + }); + }); + + it('returns empty object if both active and default configs are not defined', () => { + expect(getAll(rc)).to.deep.eq({}); + }); + }); + + describe('activate', () => { + const ETAG = 'etag'; + const CONFIG = { key: 'val' }; + const NEW_ETAG = 'new_etag'; + + let getLastSuccessfulFetchResponseStub: sinon.SinonStub; + let getActiveConfigEtagStub: sinon.SinonStub; + let setActiveConfigEtagStub: sinon.SinonStub; + let setActiveConfigStub: sinon.SinonStub; + + beforeEach(() => { + getLastSuccessfulFetchResponseStub = sinon.stub(); + getActiveConfigEtagStub = sinon.stub(); + setActiveConfigEtagStub = sinon.stub(); + setActiveConfigStub = sinon.stub(); + + storage.getLastSuccessfulFetchResponse = getLastSuccessfulFetchResponseStub; + storage.getActiveConfigEtag = getActiveConfigEtagStub; + storage.setActiveConfigEtag = setActiveConfigEtagStub; + storageCache.setActiveConfig = setActiveConfigStub; + }); + + it('does not activate if last successful fetch response is undefined', async () => { + getLastSuccessfulFetchResponseStub.returns(Promise.resolve()); + getActiveConfigEtagStub.returns(Promise.resolve(ETAG)); + + const activateResponse = await activate(rc); + + expect(activateResponse).to.be.false; + expect(storage.setActiveConfigEtag).to.not.have.been.called; + expect(storageCache.setActiveConfig).to.not.have.been.called; + }); + + it('does not activate if fetched and active etags are the same', async () => { + getLastSuccessfulFetchResponseStub.returns( + Promise.resolve({ config: {}, etag: ETAG }) + ); + getActiveConfigEtagStub.returns(Promise.resolve(ETAG)); + + const activateResponse = await activate(rc); + + expect(activateResponse).to.be.false; + expect(storage.setActiveConfigEtag).to.not.have.been.called; + expect(storageCache.setActiveConfig).to.not.have.been.called; + }); + + it('activates if fetched and active etags are different', async () => { + getLastSuccessfulFetchResponseStub.returns( + Promise.resolve({ config: CONFIG, eTag: NEW_ETAG }) + ); + getActiveConfigEtagStub.returns(Promise.resolve(ETAG)); + + const activateResponse = await activate(rc); + + expect(activateResponse).to.be.true; + expect(storage.setActiveConfigEtag).to.have.been.calledWith(NEW_ETAG); + expect(storageCache.setActiveConfig).to.have.been.calledWith(CONFIG); + }); + + it('activates if fetched is defined but active config is not', async () => { + getLastSuccessfulFetchResponseStub.returns( + Promise.resolve({ config: CONFIG, eTag: NEW_ETAG }) + ); + getActiveConfigEtagStub.returns(Promise.resolve()); + + const activateResponse = await activate(rc); + + expect(activateResponse).to.be.true; + expect(storage.setActiveConfigEtag).to.have.been.calledWith(NEW_ETAG); + expect(storageCache.setActiveConfig).to.have.been.calledWith(CONFIG); + }); + }); + + describe('fetchAndActivate', () => { + let rcActivateStub: sinon.SinonStub<[RemoteConfigType], Promise>; + + beforeEach(() => { + sinon.stub(api, 'fetchConfig').returns(Promise.resolve()); + rcActivateStub = sinon.stub(api, 'activate'); + }); + + afterEach(() => restore()); + + it('calls fetch and activate and returns activation boolean if true', async () => { + rcActivateStub.returns(Promise.resolve(true)); + + const response = await fetchAndActivate(rc); + + expect(response).to.be.true; + expect(api.fetchConfig).to.have.been.calledWith(rc); + expect(api.activate).to.have.been.calledWith(rc); + }); + + it('calls fetch and activate and returns activation boolean if false', async () => { + rcActivateStub.returns(Promise.resolve(false)); + + const response = await fetchAndActivate(rc); + + expect(response).to.be.false; + expect(api.fetchConfig).to.have.been.calledWith(rc); + expect(api.activate).to.have.been.calledWith(rc); + }); + }); + + describe('fetch', () => { + let timeoutStub: sinon.SinonStub<[ + (...args: any[]) => void, + number, + ...any[] + ]>; + beforeEach(() => { + client.fetch = sinon + .stub() + .returns(Promise.resolve({ status: 200 } as FetchResponse)); + storageCache.setLastFetchStatus = sinon.stub(); + timeoutStub = sinon.stub(window, 'setTimeout'); + }); + + afterEach(() => { + timeoutStub.restore(); + }); + + it('defines a default timeout', async () => { + await fetchConfig(rc); + + expect(timeoutStub).to.have.been.calledWith(sinon.match.any, 60000); + }); + + it('honors a custom timeout', async () => { + rc.settings.fetchTimeoutMillis = 1000; + + await fetchConfig(rc); + + expect(timeoutStub).to.have.been.calledWith(sinon.match.any, 1000); + }); + + it('sets success status', async () => { + for (const status of [200, 304]) { + client.fetch = sinon + .stub() + .returns(Promise.resolve({ status } as FetchResponse)); + + await fetchConfig(rc); + + expect(storageCache.setLastFetchStatus).to.have.been.calledWith( + 'success' + ); + } + }); + + it('sets throttle status', async () => { + storage.getThrottleMetadata = sinon.stub().returns(Promise.resolve({})); + + const error = ERROR_FACTORY.create(ErrorCode.FETCH_THROTTLE, { + throttleEndTimeMillis: 123 + }); + + client.fetch = sinon.stub().returns(Promise.reject(error)); + + const fetchPromise = fetchConfig(rc); + + await expect(fetchPromise).to.eventually.be.rejectedWith(error); + expect(storageCache.setLastFetchStatus).to.have.been.calledWith( + 'throttle' + ); + }); + + it('defaults to failure status', async () => { + storage.getThrottleMetadata = sinon.stub().returns(Promise.resolve()); + + const error = ERROR_FACTORY.create(ErrorCode.FETCH_STATUS, { + httpStatus: 400 + }); + + client.fetch = sinon.stub().returns(Promise.reject(error)); + + const fetchPromise = fetchConfig(rc); + + await expect(fetchPromise).to.eventually.be.rejectedWith(error); + expect(storageCache.setLastFetchStatus).to.have.been.calledWith( + 'failure' + ); + }); + }); +}); diff --git a/packages-exp/remote-config-exp/test/setup.ts b/packages-exp/remote-config-exp/test/setup.ts new file mode 100644 index 00000000000..90d154f1400 --- /dev/null +++ b/packages-exp/remote-config-exp/test/setup.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2019 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 { use } from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +// Normalizes Sinon assertions to Chai syntax. +use(sinonChai); + +// Adds Promise-friendly syntax to Chai. +use(chaiAsPromised); diff --git a/packages-exp/remote-config-exp/test/storage/storage.test.ts b/packages-exp/remote-config-exp/test/storage/storage.test.ts new file mode 100644 index 00000000000..4543b574094 --- /dev/null +++ b/packages-exp/remote-config-exp/test/storage/storage.test.ts @@ -0,0 +1,119 @@ +/** + * @license + * Copyright 2019 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 '../setup'; +import { expect } from 'chai'; +import { + Storage, + ThrottleMetadata, + openDatabase, + APP_NAMESPACE_STORE +} from '../../src/storage/storage'; +import { FetchResponse } from '../../src/client/remote_config_fetch_client'; + +// Clears global IndexedDB state. +async function clearDatabase(): Promise { + const db = await openDatabase(); + db.transaction([APP_NAMESPACE_STORE], 'readwrite') + .objectStore(APP_NAMESPACE_STORE) + .clear(); +} + +describe('Storage', () => { + const storage = new Storage('appId', 'appName', 'namespace'); + + beforeEach(async () => { + await clearDatabase(); + }); + + it('constructs a composite key', async () => { + // This is defensive, but the cost of accidentally changing the key composition is high. + expect(storage.createCompositeKey('throttle_metadata')).to.eq( + 'appId,appName,namespace,throttle_metadata' + ); + }); + + it('sets and gets last fetch attempt status', async () => { + const expectedStatus = 'success'; + + await storage.setLastFetchStatus(expectedStatus); + + const actualStatus = await storage.getLastFetchStatus(); + + expect(actualStatus).to.deep.eq(expectedStatus); + }); + + it('sets and gets last fetch success timestamp', async () => { + const lastSuccessfulFetchTimestampMillis = 123; + + await storage.setLastSuccessfulFetchTimestampMillis( + lastSuccessfulFetchTimestampMillis + ); + + const actualMetadata = await storage.getLastSuccessfulFetchTimestampMillis(); + + expect(actualMetadata).to.deep.eq(lastSuccessfulFetchTimestampMillis); + }); + + it('sets and gets last successful fetch response', async () => { + const lastSuccessfulFetchResponse = { status: 200 } as FetchResponse; + + await storage.setLastSuccessfulFetchResponse(lastSuccessfulFetchResponse); + + const actualConfig = await storage.getLastSuccessfulFetchResponse(); + + expect(actualConfig).to.deep.eq(lastSuccessfulFetchResponse); + }); + + it('sets and gets active config', async () => { + const expectedConfig = { key: 'value' }; + + await storage.setActiveConfig(expectedConfig); + + const storedConfig = await storage.getActiveConfig(); + + expect(storedConfig).to.deep.eq(expectedConfig); + }); + + it('sets and gets active config etag', async () => { + const expectedEtag = 'etag'; + + await storage.setActiveConfigEtag(expectedEtag); + + const storedConfigEtag = await storage.getActiveConfigEtag(); + + expect(storedConfigEtag).to.deep.eq(expectedEtag); + }); + + it('sets, gets and deletes throttle metadata', async () => { + const expectedMetadata = { + throttleEndTimeMillis: 1 + } as ThrottleMetadata; + + await storage.setThrottleMetadata(expectedMetadata); + + let actualMetadata = await storage.getThrottleMetadata(); + + expect(actualMetadata).to.deep.eq(expectedMetadata); + + await storage.deleteThrottleMetadata(); + + actualMetadata = await storage.getThrottleMetadata(); + + expect(actualMetadata).to.be.undefined; + }); +}); diff --git a/packages-exp/remote-config-exp/test/storage/storage_cache.test.ts b/packages-exp/remote-config-exp/test/storage/storage_cache.test.ts new file mode 100644 index 00000000000..e7cfb0ef0da --- /dev/null +++ b/packages-exp/remote-config-exp/test/storage/storage_cache.test.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright 2019 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 '../setup'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { Storage } from '../../src/storage/storage'; +import { StorageCache } from '../../src/storage/storage_cache'; + +describe('StorageCache', () => { + const storage = {} as Storage; + let storageCache: StorageCache; + + beforeEach(() => { + storageCache = new StorageCache(storage); + }); + + /** + * Read-ahead getter. + */ + describe('loadFromStorage', () => { + it('populates memory cache with persisted data', async () => { + const status = 'success'; + const lastSuccessfulFetchTimestampMillis = 123; + const activeConfig = { key: 'value' }; + + storage.getLastFetchStatus = sinon + .stub() + .returns(Promise.resolve(status)); + storage.getLastSuccessfulFetchTimestampMillis = sinon + .stub() + .returns(Promise.resolve(lastSuccessfulFetchTimestampMillis)); + storage.getActiveConfig = sinon + .stub() + .returns(Promise.resolve(activeConfig)); + + await storageCache.loadFromStorage(); + + expect(storage.getLastFetchStatus).to.have.been.called; + expect(storage.getLastSuccessfulFetchTimestampMillis).to.have.been.called; + expect(storage.getActiveConfig).to.have.been.called; + + expect(storageCache.getLastFetchStatus()).to.eq(status); + expect(storageCache.getLastSuccessfulFetchTimestampMillis()).to.deep.eq( + lastSuccessfulFetchTimestampMillis + ); + expect(storageCache.getActiveConfig()).to.deep.eq(activeConfig); + }); + }); + + describe('setActiveConfig', () => { + const activeConfig = { key: 'value2' }; + + beforeEach(() => { + storage.setActiveConfig = sinon.stub().returns(Promise.resolve()); + }); + + it('writes to memory cache', async () => { + await storageCache.setActiveConfig(activeConfig); + + expect(storageCache.getActiveConfig()).to.deep.eq(activeConfig); + }); + + it('writes to persistent storage', async () => { + await storageCache.setActiveConfig(activeConfig); + + expect(storage.setActiveConfig).to.have.been.calledWith(activeConfig); + }); + }); +}); diff --git a/packages-exp/remote-config-exp/test/value.test.ts b/packages-exp/remote-config-exp/test/value.test.ts new file mode 100644 index 00000000000..ead90ce25cf --- /dev/null +++ b/packages-exp/remote-config-exp/test/value.test.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2019 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 './setup'; +import { expect } from 'chai'; +import { Value } from '../src/value'; + +describe('Value', () => { + describe('asString', () => { + it('returns static default string if source is static', () => { + expect(new Value('static').asString()).to.eq(''); + }); + + it('returns the value as a string', () => { + const VALUE = 'test'; + const value = new Value('remote', VALUE); + + expect(value.asString()).to.eq(VALUE); + }); + }); + + describe('asBoolean', () => { + it('returns static default boolean if source is static', () => { + expect(new Value('static').asBoolean()).to.be.false; + }); + + it('returns true for a truthy values', () => { + expect(new Value('remote', '1').asBoolean()).to.be.true; + expect(new Value('remote', 'true').asBoolean()).to.be.true; + expect(new Value('remote', 't').asBoolean()).to.be.true; + expect(new Value('remote', 'yes').asBoolean()).to.be.true; + expect(new Value('remote', 'y').asBoolean()).to.be.true; + expect(new Value('remote', 'on').asBoolean()).to.be.true; + }); + + it('returns false for non-truthy values', () => { + expect(new Value('remote', '').asBoolean()).to.be.false; + expect(new Value('remote', 'false').asBoolean()).to.be.false; + expect(new Value('remote', 'random string').asBoolean()).to.be.false; + }); + }); + + describe('asNumber', () => { + it('returns static default number if source is static', () => { + expect(new Value('static').asNumber()).to.eq(0); + }); + + it('returns value as a number', () => { + expect(new Value('default', '33').asNumber()).to.eq(33); + expect(new Value('default', 'not a number').asNumber()).to.eq(0); + expect(new Value('default', '-10').asNumber()).to.eq(-10); + expect(new Value('default', '0').asNumber()).to.eq(0); + expect(new Value('default', '5.3').asNumber()).to.eq(5.3); + }); + }); + + describe('getSource', () => { + it('returns the source of the value', () => { + expect(new Value('default', 'test').getSource()).to.eq('default'); + expect(new Value('remote', 'test').getSource()).to.eq('remote'); + expect(new Value('static').getSource()).to.eq('static'); + }); + }); +}); diff --git a/packages-exp/remote-config-exp/test_app/index.html b/packages-exp/remote-config-exp/test_app/index.html new file mode 100644 index 00000000000..8bf6a0c888c --- /dev/null +++ b/packages-exp/remote-config-exp/test_app/index.html @@ -0,0 +1,154 @@ + + + + + Test App + + + + + +

+

Remote Config Test App

+
+
+
+
Firebase config
+
+ +
+
+
RC defaults
+
+ +
+
+
RC settings
+
+ +
+
+
Log level
+
+ +
+
+
+
+
Controls
+
+ + + + +
+
+
+
Value getters
+
+
+ key: + +
+ + + + +
+
+
+
General getters
+
+ + + + +
+
+
+
+
Output
+
+
+
+
+
+
+ + + diff --git a/packages-exp/remote-config-exp/test_app/index.js b/packages-exp/remote-config-exp/test_app/index.js new file mode 100644 index 00000000000..d38847d9f37 --- /dev/null +++ b/packages-exp/remote-config-exp/test_app/index.js @@ -0,0 +1,224 @@ +/** + * @license + * Copyright 2019 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. + */ + +const FB_CONFIG_PLACEHOLDER = `{ + apiKey: ..., + authDomain: ..., + databaseURL: ..., + projectId: ..., + storageBucket: ..., + messagingSenderId: ..., + appId: ... +}`; +const DEFAULTS_PLACEHOLDER = `{ + key1: 'value1', + key2: 'value2', + ... +}`; +const SETTINGS_PLACEHOLDER = `{ + minimumFetchIntervalMillis: 43200000, + fetchTimeoutMillis: 5000 +}`; +const SUCCESS_MESSAGE = 'Done '; + +let remoteConfig; +const outputBox = document.getElementById('output-box'); + +window.onload = function () { + document.querySelector( + '#firebase-config' + ).placeholder = FB_CONFIG_PLACEHOLDER; + document.querySelector('#rc-defaults').placeholder = DEFAULTS_PLACEHOLDER; + document.querySelector('#rc-settings').placeholder = SETTINGS_PLACEHOLDER; +}; + +function initializeFirebase() { + const val = document.querySelector('#firebase-config').value; + firebase.initializeApp(parseObjFromStr(val)); + remoteConfig = firebase.remoteConfig(); + return Promise.resolve(); +} + +function setDefaults() { + remoteConfig.defaultConfig = parseObjFromStr( + document.querySelector('#rc-defaults').value + ); + return SUCCESS_MESSAGE; +} + +function setSettings() { + const newSettings = parseObjFromStr( + document.querySelector('#rc-settings').value, + true + ); + const currentSettings = remoteConfig.settings; + remoteConfig.settings = Object.assign({}, currentSettings, newSettings); + return SUCCESS_MESSAGE; +} + +function setLogLevel() { + const newLogLevel = document.querySelector('#log-level-input').value; + remoteConfig.setLogLevel(newLogLevel); + return SUCCESS_MESSAGE; +} + +function activate() { + return remoteConfig.activate(); +} + +function ensureInitialized() { + return remoteConfig.ensureInitialized(); +} + +// Prefixed to avoid clobbering the browser's fetch function. +function rcFetch() { + return remoteConfig.fetch(); +} + +function fetchAndActivate() { + return remoteConfig.fetchAndActivate(); +} + +function getString() { + return remoteConfig.getString(getKey()); +} + +function getBoolean() { + return remoteConfig.getBoolean(getKey()); +} + +function getNumber() { + return remoteConfig.getNumber(getKey()); +} + +function getValue() { + return remoteConfig.getValue(getKey()); +} + +function getAll() { + return remoteConfig.getAll(); +} + +function getFetchTimeMillis() { + return remoteConfig.fetchTimeMillis; +} + +function getLastFetchStatus() { + return remoteConfig.lastFetchStatus; +} + +function getSettings() { + return remoteConfig.settings; +} + +// Helper functions +function getKey() { + return document.querySelector('#key-input').value; +} + +function handlerWrapper(handler) { + try { + clearOutput(); + var returnValue = handler(); + if (returnValue instanceof Promise) { + handlePromise(returnValue); + } else if (returnValue instanceof Array) { + handleArray(returnValue); + } else if (returnValue instanceof Object) { + handleObject(returnValue); + } else { + displayOutput(returnValue); + } + } catch (error) { + displayOutputError(error); + } +} + +function clearOutput() { + outputBox.innerHTML = ''; +} + +function appendOutput(text) { + outputBox.innerHTML = outputBox.innerHTML + text; +} + +function displayOutput(text) { + outputBox.innerHTML = text; +} + +function displayOutputError(error) { + outputBox.innerHTML = `${error.message || error}`; +} + +function handlePromise(prom) { + const timerId = setInterval(() => appendOutput('.'), 400); + prom + .then(res => { + clearInterval(timerId); + appendOutput(SUCCESS_MESSAGE); + if (res != undefined) { + appendOutput('
'); + appendOutput('return value: ' + res); + } + }) + .catch(e => { + clearInterval(timerId); + appendOutput(`${e}`); + }); +} + +function handleArray(ar) { + displayOutput(ar.toString()); +} + +function handleObject(obj) { + appendOutput('{'); + for (const key in obj) { + if (obj[key] instanceof Function) { + appendOutput(`
${key}: ${obj[key]()},`); + } else if (obj[key] instanceof Object) { + appendOutput(key + ': '); + handleObject(obj[key]); + appendOutput(','); + } else { + appendOutput(`
${key}: ${obj[key]},`); + } + } + appendOutput('
}'); +} + +/** + * Parses string received from input elements into objects. These strings are not JSON + * compatible. + */ +function parseObjFromStr(str, valueIsNumber = false) { + const obj = {}; + str + .substring(str.indexOf('{') + 1, str.indexOf('}')) + .replace(/["']/g, '') // Get rid of curly braces and quotes + .trim() + .split(/[,]/g) // Results in a string of key value separated by a colon + .map(str => str.trim()) + .forEach(keyValue => { + const colIndex = keyValue.indexOf(':'); + const key = keyValue.substring(0, colIndex); + const val = keyValue.substring(colIndex + 1).trim(); + const value = valueIsNumber ? Number(val) : val; + obj[key] = value; + }); + return obj; +} diff --git a/packages-exp/remote-config-exp/tsconfig.json b/packages-exp/remote-config-exp/tsconfig.json new file mode 100644 index 00000000000..a4b8678284b --- /dev/null +++ b/packages-exp/remote-config-exp/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "resolveJsonModule": true, + }, + "exclude": [ + "dist/**/*" + ] +} \ No newline at end of file diff --git a/packages-exp/remote-config-types-exp/README.md b/packages-exp/remote-config-types-exp/README.md new file mode 100644 index 00000000000..8e988de0262 --- /dev/null +++ b/packages-exp/remote-config-types-exp/README.md @@ -0,0 +1,3 @@ +# @firebase/remoteconfig-types + +**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages-exp/remote-config-types-exp/api-extractor.json b/packages-exp/remote-config-types-exp/api-extractor.json new file mode 100644 index 00000000000..42f37a88c4b --- /dev/null +++ b/packages-exp/remote-config-types-exp/api-extractor.json @@ -0,0 +1,5 @@ +{ + "extends": "../../config/api-extractor.json", + // Point it to your entry point d.ts file. + "mainEntryPointFilePath": "/index.d.ts" +} \ No newline at end of file diff --git a/packages-exp/remote-config-types-exp/index.d.ts b/packages-exp/remote-config-types-exp/index.d.ts new file mode 100644 index 00000000000..d0271d3e781 --- /dev/null +++ b/packages-exp/remote-config-types-exp/index.d.ts @@ -0,0 +1,120 @@ +/** + * @license + * Copyright 2019 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 interface RemoteConfig { + /** + * Defines configuration for the Remote Config SDK. + */ + settings: Settings; + + /** + * Object containing default values for conigs. + */ + defaultConfig: { [key: string]: string | number | boolean }; + + /** + * The Unix timestamp in milliseconds of the last successful fetch, or negative one if + * the {@link RemoteConfig} instance either hasn't fetched or initialization + * is incomplete. + */ + fetchTimeMillis: number; + + /** + * The status of the last fetch attempt. + */ + lastFetchStatus: FetchStatus; +} + +/** + * Indicates the source of a value. + * + *
    + *
  • "static" indicates the value was defined by a static constant.
  • + *
  • "default" indicates the value was defined by default config.
  • + *
  • "remote" indicates the value was defined by fetched config.
  • + *
+ */ +export type ValueSource = 'static' | 'default' | 'remote'; + +/** + * Wraps a value with metadata and type-safe getters. + */ +export interface Value { + /** + * Gets the value as a boolean. + * + * The following values (case insensitive) are interpreted as true: + * "1", "true", "t", "yes", "y", "on". Other values are interpreted as false. + */ + asBoolean(): boolean; + + /** + * Gets the value as a number. Comparable to calling Number(value) || 0. + */ + asNumber(): number; + + /** + * Gets the value as a string. + */ + asString(): string; + + /** + * Gets the {@link ValueSource} for the given key. + */ + getSource(): ValueSource; +} + +/** + * Defines configuration options for the Remote Config SDK. + */ +export interface Settings { + /** + * Defines the maximum age in milliseconds of an entry in the config cache before + * it is considered stale. Defaults to 43200000 (Twelve hours). + */ + minimumFetchIntervalMillis: number; + + /** + * Defines the maximum amount of milliseconds to wait for a response when fetching + * configuration from the Remote Config server. Defaults to 60000 (One minute). + */ + fetchTimeoutMillis: number; +} + +/** + * Summarizes the outcome of the last attempt to fetch config from the Firebase Remote Config server. + * + *
    + *
  • "no-fetch-yet" indicates the {@link RemoteConfig} instance has not yet attempted + * to fetch config, or that SDK initialization is incomplete.
  • + *
  • "success" indicates the last attempt succeeded.
  • + *
  • "failure" indicates the last attempt failed.
  • + *
  • "throttle" indicates the last attempt was rate-limited.
  • + *
+ */ +export type FetchStatus = 'no-fetch-yet' | 'success' | 'failure' | 'throttle'; + +/** + * Defines levels of Remote Config logging. + */ +export type LogLevel = 'debug' | 'error' | 'silent'; + +declare module '@firebase/component' { + interface NameServiceMapping { + 'remote-config-exp': RemoteConfig; + } +} diff --git a/packages-exp/remote-config-types-exp/package.json b/packages-exp/remote-config-types-exp/package.json new file mode 100644 index 00000000000..39e75defce7 --- /dev/null +++ b/packages-exp/remote-config-types-exp/package.json @@ -0,0 +1,29 @@ +{ + "name": "@firebase/remote-config-types-exp", + "version": "0.0.800", + "description": "@firebase/remote-config Types", + "author": "Firebase (https://firebase.google.com/)", + "license": "Apache-2.0", + "scripts": { + "test": "tsc", + "test:ci": "node ../../scripts/run_tests_in_ci.js", + "api-report": "api-extractor run --local --verbose", + "predoc": "node ../../scripts/exp/remove-exp.js temp", + "doc": "api-documenter markdown --input temp --output docs", + "build:doc": "yarn api-report && yarn doc" + }, + "files": [ + "index.d.ts" + ], + "repository": { + "directory": "packages/remote-config-types", + "type": "git", + "url": "https://github.com/firebase/firebase-js-sdk.git" + }, + "bugs": { + "url": "https://github.com/firebase/firebase-js-sdk/issues" + }, + "devDependencies": { + "typescript": "4.0.5" + } +} diff --git a/packages-exp/remote-config-types-exp/tsconfig.json b/packages-exp/remote-config-types-exp/tsconfig.json new file mode 100644 index 00000000000..9a785433d90 --- /dev/null +++ b/packages-exp/remote-config-types-exp/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "noEmit": true + }, + "exclude": [ + "dist/**/*" + ] +} diff --git a/packages/analytics-interop-types/package.json b/packages/analytics-interop-types/package.json index 4c6f5c20e1a..51bcf67e7b3 100644 --- a/packages/analytics-interop-types/package.json +++ b/packages/analytics-interop-types/package.json @@ -20,6 +20,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages/analytics-types/package.json b/packages/analytics-types/package.json index 345be90ed88..977acdf4360 100644 --- a/packages/analytics-types/package.json +++ b/packages/analytics-types/package.json @@ -20,6 +20,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages/analytics/CHANGELOG.md b/packages/analytics/CHANGELOG.md index 639eb8890b5..c88b5b8c751 100644 --- a/packages/analytics/CHANGELOG.md +++ b/packages/analytics/CHANGELOG.md @@ -1,5 +1,14 @@ # @firebase/analytics +## 0.6.2 + +### Patch Changes + +- Updated dependencies [[`9cf727fcc`](https://github.com/firebase/firebase-js-sdk/commit/9cf727fcc3d049551b16ae0698ac33dc2fe45ada)]: + - @firebase/util@0.3.4 + - @firebase/component@0.1.21 + - @firebase/installations@0.4.19 + ## 0.6.1 ### Patch Changes diff --git a/packages/analytics/package.json b/packages/analytics/package.json index e494d99f496..01b85527c5f 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/analytics", - "version": "0.6.1", + "version": "0.6.2", "description": "A analytics package for new firebase packages", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", @@ -26,21 +26,21 @@ }, "dependencies": { "@firebase/analytics-types": "0.4.0", - "@firebase/installations": "0.4.18", + "@firebase/installations": "0.4.19", "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.3", - "@firebase/component": "0.1.20", + "@firebase/util": "0.3.4", + "@firebase/component": "0.1.21", "tslib": "^1.11.1" }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.6.12", - "rollup": "2.29.0", + "@firebase/app": "0.6.13", + "rollup": "2.33.1", "@rollup/plugin-commonjs": "15.1.0", "@rollup/plugin-json": "4.1.0", "@rollup/plugin-node-resolve": "9.0.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages/analytics", diff --git a/packages/app-types/package.json b/packages/app-types/package.json index 295d5db99e3..d73d6f4fd2a 100644 --- a/packages/app-types/package.json +++ b/packages/app-types/package.json @@ -21,6 +21,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 2c78b864bb6..42fef2f48ca 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,13 @@ # @firebase/app +## 0.6.13 + +### Patch Changes + +- Updated dependencies [[`9cf727fcc`](https://github.com/firebase/firebase-js-sdk/commit/9cf727fcc3d049551b16ae0698ac33dc2fe45ada)]: + - @firebase/util@0.3.4 + - @firebase/component@0.1.21 + ## 0.6.12 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 5f79a9fd77e..643a1970f43 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/app", - "version": "0.6.12", + "version": "0.6.13", "description": "The primary entrypoint to the Firebase JS SDK", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", @@ -28,19 +28,19 @@ "license": "Apache-2.0", "dependencies": { "@firebase/app-types": "0.6.1", - "@firebase/util": "0.3.3", + "@firebase/util": "0.3.4", "@firebase/logger": "0.2.6", - "@firebase/component": "0.1.20", + "@firebase/component": "0.1.21", "tslib": "^1.11.1", "dom-storage": "2.1.0", "xmlhttprequest": "1.8.0" }, "devDependencies": { - "rollup": "2.29.0", + "rollup": "2.33.1", "@rollup/plugin-json": "4.1.0", "rollup-plugin-replace": "2.2.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages/app", diff --git a/packages/auth-interop-types/package.json b/packages/auth-interop-types/package.json index ee79c88cdd8..f4b713d7986 100644 --- a/packages/auth-interop-types/package.json +++ b/packages/auth-interop-types/package.json @@ -24,6 +24,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages/auth-types/package.json b/packages/auth-types/package.json index 6ddabd04c75..578f4eadc34 100644 --- a/packages/auth-types/package.json +++ b/packages/auth-types/package.json @@ -24,6 +24,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages/auth/demo/functions/package.json b/packages/auth/demo/functions/package.json index e932de95421..1166869cbe8 100644 --- a/packages/auth/demo/functions/package.json +++ b/packages/auth/demo/functions/package.json @@ -9,7 +9,7 @@ "logs": "firebase functions:log" }, "dependencies": { - "firebase-admin": "9.2.0", + "firebase-admin": "9.3.0", "firebase-functions": "3.11.0" }, "private": true diff --git a/packages/auth/demo/functions/yarn.lock b/packages/auth/demo/functions/yarn.lock index 98e83bfcd7f..302e15aeb0b 100644 --- a/packages/auth/demo/functions/yarn.lock +++ b/packages/auth/demo/functions/yarn.lock @@ -670,10 +670,10 @@ find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -firebase-admin@9.2.0: - version "9.2.0" - resolved "https://registry.npmjs.org/firebase-admin/-/firebase-admin-9.2.0.tgz#df5176e2d0c5711df6dbf7012320492a703538ea" - integrity sha512-LhnMYl71B4gP1FlTLfwaYlOWhBCAcNF+byb2CPTfaW/T4hkp4qlXOgo2bws/zbAv5X9GTFqGir3KexMslVGsIA== +firebase-admin@9.3.0: + version "9.3.0" + resolved "https://registry.npmjs.org/firebase-admin/-/firebase-admin-9.3.0.tgz#05f3efb1bb97f17b2c562f4b59820a381d2f903f" + integrity sha512-qMUITOp2QKLLc2o0/wSiDC2OO2knejjieZN/8Or9AzfFk8ftTcUKq5ALNlQXu+7aUzGe0IwSJq9TVnkIU0h1xw== dependencies: "@firebase/database" "^0.6.10" "@firebase/database-types" "^0.5.2" diff --git a/packages/auth/package.json b/packages/auth/package.json index ad4fb224672..a5132d31edd 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -25,9 +25,9 @@ "@firebase/auth-types": "0.10.1" }, "devDependencies": { - "firebase-tools": "8.12.1", + "firebase-tools": "8.15.0", "google-closure-compiler": "20200112.0.0", - "google-closure-library": "20200224.0.0", + "google-closure-library": "20200830.0.0", "gulp": "4.0.2", "gulp-sourcemaps": "2.6.5" }, diff --git a/packages/auth/src/rpchandler.js b/packages/auth/src/rpchandler.js index 363648349a0..eefa65bad47 100644 --- a/packages/auth/src/rpchandler.js +++ b/packages/auth/src/rpchandler.js @@ -1989,7 +1989,7 @@ fireauth.RpcHandler.verifyPhoneNumberForExistingErrorMap_[ * @private */ fireauth.RpcHandler.validateDeleteLinkedAccountsRequest_ = function(request) { - if (!goog.isArray(request['deleteProvider'])) { + if (!Array.isArray(request['deleteProvider'])) { throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); } }; diff --git a/packages/component/CHANGELOG.md b/packages/component/CHANGELOG.md index 4b2c6ceacec..445a00f18bc 100644 --- a/packages/component/CHANGELOG.md +++ b/packages/component/CHANGELOG.md @@ -1,5 +1,12 @@ # @firebase/component +## 0.1.21 + +### Patch Changes + +- Updated dependencies [[`9cf727fcc`](https://github.com/firebase/firebase-js-sdk/commit/9cf727fcc3d049551b16ae0698ac33dc2fe45ada)]: + - @firebase/util@0.3.4 + ## 0.1.20 ### Patch Changes diff --git a/packages/component/package.json b/packages/component/package.json index 890f19dc2b8..d1ddb03f080 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/component", - "version": "0.1.20", + "version": "0.1.21", "description": "Firebase Component Platform", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", @@ -22,14 +22,14 @@ "prepare": "yarn build" }, "dependencies": { - "@firebase/util": "0.3.3", + "@firebase/util": "0.3.4", "tslib": "^1.11.1" }, "license": "Apache-2.0", "devDependencies": { - "rollup": "2.29.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "rollup": "2.33.1", + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages/component", diff --git a/packages/database-types/package.json b/packages/database-types/package.json index 37db39c2a25..bace9abf2e5 100644 --- a/packages/database-types/package.json +++ b/packages/database-types/package.json @@ -23,6 +23,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages/database/CHANGELOG.md b/packages/database/CHANGELOG.md index 6a6604a2c46..49dbf9c32df 100644 --- a/packages/database/CHANGELOG.md +++ b/packages/database/CHANGELOG.md @@ -1,5 +1,13 @@ # Unreleased +## 0.7.1 + +### Patch Changes + +- Updated dependencies [[`9cf727fcc`](https://github.com/firebase/firebase-js-sdk/commit/9cf727fcc3d049551b16ae0698ac33dc2fe45ada)]: + - @firebase/util@0.3.4 + - @firebase/component@0.1.21 + ## 0.7.0 ### Minor Changes diff --git a/packages/database/package.json b/packages/database/package.json index 054a7c66abb..ebc906aa8d7 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/database", - "version": "0.7.0", + "version": "0.7.1", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", @@ -27,18 +27,18 @@ "dependencies": { "@firebase/database-types": "0.6.0", "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.3", - "@firebase/component": "0.1.20", + "@firebase/util": "0.3.4", + "@firebase/component": "0.1.21", "@firebase/auth-interop-types": "0.1.5", "faye-websocket": "0.11.3", "tslib": "^1.11.1" }, "devDependencies": { - "@firebase/app": "0.6.12", + "@firebase/app": "0.6.13", "@firebase/app-types": "0.6.1", - "rollup": "2.29.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "rollup": "2.33.1", + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages/database", diff --git a/packages/firebase/CHANGELOG.md b/packages/firebase/CHANGELOG.md index 097b2b54644..2491c4817d3 100644 --- a/packages/firebase/CHANGELOG.md +++ b/packages/firebase/CHANGELOG.md @@ -1,5 +1,22 @@ # firebase +## 8.0.1 + +### Patch Changes + +- Updated dependencies [[`54a46f89c`](https://github.com/firebase/firebase-js-sdk/commit/54a46f89c1c45435c76412fa2ed296e986c2f6ab), [`9cf727fcc`](https://github.com/firebase/firebase-js-sdk/commit/9cf727fcc3d049551b16ae0698ac33dc2fe45ada), [`007ddd1eb`](https://github.com/firebase/firebase-js-sdk/commit/007ddd1eb6be0a66df7b1c3264d8dff8857d8399)]: + - @firebase/messaging@0.7.3 + - @firebase/util@0.3.4 + - @firebase/firestore@2.0.1 + - @firebase/functions@0.6.1 + - @firebase/analytics@0.6.2 + - @firebase/app@0.6.13 + - @firebase/database@0.7.1 + - @firebase/installations@0.4.19 + - @firebase/performance@0.4.4 + - @firebase/remote-config@0.1.30 + - @firebase/storage@0.4.1 + ## 8.0.0 ### Major Changes diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index a47dcaa4782..e3fe793bcd4 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -8279,18 +8279,22 @@ declare namespace firebase.firestore { */ terminate(): Promise; - loadBundle( - bundleData: ArrayBuffer | ReadableStream | string - ): LoadBundleTask; - - namedQuery(name: string): Promise | null>; - /** * @hidden */ INTERNAL: { delete: () => Promise }; } + export function loadBundle( + db: Firestore, + bundleData: ArrayBuffer | ReadableStream | string + ): LoadBundleTask; + + export function namedQuery( + db: Firestore, + name: string + ): Promise | null>; + export interface LoadBundleTask { onProgress( next?: (progress: LoadBundleTaskProgress) => any, diff --git a/packages/firebase/package.json b/packages/firebase/package.json index 381dc9aaec4..67f0d0e792d 100644 --- a/packages/firebase/package.json +++ b/packages/firebase/package.json @@ -1,6 +1,6 @@ { "name": "firebase", - "version": "8.0.0", + "version": "8.0.1", "description": "Firebase JavaScript library for web and Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", @@ -45,31 +45,31 @@ "module": "dist/index.esm.js", "react-native": "dist/index.rn.cjs.js", "dependencies": { - "@firebase/app": "0.6.12", + "@firebase/app": "0.6.13", "@firebase/app-types": "0.6.1", "@firebase/auth": "0.15.1", - "@firebase/database": "0.7.0", - "@firebase/firestore": "2.0.0", - "@firebase/functions": "0.6.0", - "@firebase/installations": "0.4.18", - "@firebase/messaging": "0.7.2", + "@firebase/database": "0.7.1", + "@firebase/firestore": "2.0.1", + "@firebase/functions": "0.6.1", + "@firebase/installations": "0.4.19", + "@firebase/messaging": "0.7.3", "@firebase/polyfill": "0.3.36", - "@firebase/storage": "0.4.0", - "@firebase/performance": "0.4.3", - "@firebase/remote-config": "0.1.29", - "@firebase/analytics": "0.6.1", - "@firebase/util": "0.3.3" + "@firebase/storage": "0.4.1", + "@firebase/performance": "0.4.4", + "@firebase/remote-config": "0.1.30", + "@firebase/analytics": "0.6.2", + "@firebase/util": "0.3.4" }, "devDependencies": { - "rollup": "2.29.0", + "rollup": "2.33.1", "@rollup/plugin-commonjs": "15.1.0", "rollup-plugin-license": "2.2.0", "@rollup/plugin-node-resolve": "9.0.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2", - "rollup-plugin-typescript2": "0.27.3", + "rollup-plugin-typescript2": "0.29.0", "rollup-plugin-uglify": "6.0.4", - "typescript": "4.0.2" + "typescript": "4.0.5" }, "typings": "index.d.ts", "components": [ diff --git a/packages/firestore-types/index.d.ts b/packages/firestore-types/index.d.ts index b76b5143488..d6bef76eba1 100644 --- a/packages/firestore-types/index.d.ts +++ b/packages/firestore-types/index.d.ts @@ -96,15 +96,19 @@ export class FirebaseFirestore { terminate(): Promise; - loadBundle( - bundleData: ArrayBuffer | ReadableStream | string - ): LoadBundleTask; - - namedQuery(name: string): Promise | null>; - INTERNAL: { delete: () => Promise }; } +export function loadBundle( + db: FirebaseFirestore, + bundleData: ArrayBuffer | ReadableStream | string +): LoadBundleTask; + +export function namedQuery( + db: FirebaseFirestore, + name: string +): Promise | null>; + export interface LoadBundleTask { onProgress( next?: (progress: LoadBundleTaskProgress) => any, diff --git a/packages/firestore-types/package.json b/packages/firestore-types/package.json index 3c0d23d34a3..847df8416a7 100644 --- a/packages/firestore-types/package.json +++ b/packages/firestore-types/package.json @@ -23,6 +23,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages/firestore/CHANGELOG.md b/packages/firestore/CHANGELOG.md index 01b0973d6ac..070a058da66 100644 --- a/packages/firestore/CHANGELOG.md +++ b/packages/firestore/CHANGELOG.md @@ -1,5 +1,15 @@ # @firebase/firestore +## 2.0.1 + +### Patch Changes + +- [`007ddd1eb`](https://github.com/firebase/firebase-js-sdk/commit/007ddd1eb6be0a66df7b1c3264d8dff8857d8399) [#4030](https://github.com/firebase/firebase-js-sdk/pull/4030) - Internal changes to support upcoming modular API. + +- Updated dependencies [[`9cf727fcc`](https://github.com/firebase/firebase-js-sdk/commit/9cf727fcc3d049551b16ae0698ac33dc2fe45ada)]: + - @firebase/util@0.3.4 + - @firebase/component@0.1.21 + ## 2.0.0 ### Major Changes diff --git a/packages/firestore/exp/index.ts b/packages/firestore/exp/index.ts index 78b78b3961e..5ac7079c8ee 100644 --- a/packages/firestore/exp/index.ts +++ b/packages/firestore/exp/index.ts @@ -31,8 +31,6 @@ export { waitForPendingWrites, disableNetwork, enableNetwork, - namedQuery, - loadBundle, terminate } from './src/api/database'; diff --git a/packages/firestore/exp/src/api/database.ts b/packages/firestore/exp/src/api/database.ts index eb4d57faaa2..7c5e957f64f 100644 --- a/packages/firestore/exp/src/api/database.ts +++ b/packages/firestore/exp/src/api/database.ts @@ -24,8 +24,6 @@ import { FirestoreClient, firestoreClientDisableNetwork, firestoreClientEnableNetwork, - firestoreClientGetNamedQuery, - firestoreClientLoadBundle, firestoreClientWaitForPendingWrites, setOfflineComponentProvider, setOnlineComponentProvider @@ -41,22 +39,20 @@ import { FirebaseFirestore as LiteFirestore, Settings as LiteSettings } from '../../../lite/src/api/database'; +import { DatabaseId } from '../../../src/core/database_info'; import { Code, FirestoreError } from '../../../src/util/error'; import { Deferred } from '../../../src/util/promise'; import { LRU_MINIMUM_CACHE_SIZE_BYTES } from '../../../src/local/lru_garbage_collector'; import { CACHE_SIZE_UNLIMITED, configureFirestore, - ensureFirestoreConfigured, - FirestoreCompat + ensureFirestoreConfigured } from '../../../src/api/database'; import { indexedDbClearPersistence, indexedDbStoragePrefix } from '../../../src/local/indexeddb_persistence'; import { PersistenceSettings } from '../../../exp-types'; -import { Query } from '../../../lite/src/api/reference'; -import { LoadBundleTask } from '../../../src/api/bundle'; /** DOMException error code constants. */ const DOM_EXCEPTION_INVALID_STATE = 11; @@ -74,18 +70,19 @@ export interface Settings extends LiteSettings { */ export class FirebaseFirestore extends LiteFirestore - implements _FirebaseService, FirestoreCompat { + implements _FirebaseService { readonly _queue = new AsyncQueue(); readonly _persistenceKey: string; _firestoreClient: FirestoreClient | undefined; constructor( - app: FirebaseApp, + databaseIdOrApp: DatabaseId | FirebaseApp, authProvider: Provider ) { - super(app, authProvider); - this._persistenceKey = app.name; + super(databaseIdOrApp, authProvider); + this._persistenceKey = + 'name' in databaseIdOrApp ? databaseIdOrApp.name : '[DEFAULT]'; } _terminate(): Promise { @@ -169,13 +166,13 @@ export function getFirestore(app: FirebaseApp): FirebaseFirestore { * @return A promise that represents successfully enabling persistent storage. */ export function enableIndexedDbPersistence( - firestore: FirestoreCompat, + firestore: FirebaseFirestore, persistenceSettings?: PersistenceSettings ): Promise { verifyNotInitialized(firestore); const client = ensureFirestoreConfigured(firestore); - const settings = firestore._getSettings(); + const settings = firestore._freezeSettings(); const onlineComponentProvider = new OnlineComponentProvider(); const offlineComponentProvider = new IndexedDbOfflineComponentProvider( @@ -213,12 +210,12 @@ export function enableIndexedDbPersistence( * storage. */ export function enableMultiTabIndexedDbPersistence( - firestore: FirestoreCompat + firestore: FirebaseFirestore ): Promise { verifyNotInitialized(firestore); const client = ensureFirestoreConfigured(firestore); - const settings = firestore._getSettings(); + const settings = firestore._freezeSettings(); const onlineComponentProvider = new OnlineComponentProvider(); const offlineComponentProvider = new MultiTabOfflineComponentProvider( @@ -326,7 +323,7 @@ function canFallbackFromIndexedDbError( * cleared. Otherwise, the promise is rejected with an error. */ export function clearIndexedDbPersistence( - firestore: FirestoreCompat + firestore: FirebaseFirestore ): Promise { if (firestore._initialized && !firestore._terminated) { throw new FirestoreError( @@ -424,7 +421,7 @@ export function terminate(firestore: FirebaseFirestore): Promise { return firestore._delete(); } -function verifyNotInitialized(firestore: FirestoreCompat): void { +function verifyNotInitialized(firestore: FirebaseFirestore): void { if (firestore._initialized || firestore._terminated) { throw new FirestoreError( Code.FAILED_PRECONDITION, @@ -434,29 +431,3 @@ function verifyNotInitialized(firestore: FirestoreCompat): void { ); } } - -export function loadBundle( - firestore: FirebaseFirestore, - bundleData: ArrayBuffer | ReadableStream | string -): LoadBundleTask { - const client = ensureFirestoreConfigured(firestore); - const resultTask = new LoadBundleTask(); - - // eslint-disable-next-line @typescript-eslint/no-floating-promises - firestoreClientLoadBundle(client, bundleData, resultTask); - return resultTask; -} - -export function namedQuery( - firestore: FirebaseFirestore, - name: string -): Promise { - const client = ensureFirestoreConfigured(firestore); - return firestoreClientGetNamedQuery(client, name).then(namedQuery => { - if (!namedQuery) { - return null; - } - - return new Query(firestore, null, namedQuery.query); - }); -} diff --git a/packages/firestore/exp/test/shim.ts b/packages/firestore/exp/test/shim.ts index 9c345481388..665ca65c154 100644 --- a/packages/firestore/exp/test/shim.ts +++ b/packages/firestore/exp/test/shim.ts @@ -15,24 +15,15 @@ * limitations under the License. */ -import { FirebaseApp as FirebaseAppLegacy } from '@firebase/app-types'; -import { FirebaseApp as FirebaseAppExp } from '@firebase/app-types-exp'; -import { deleteApp } from '@firebase/app-exp'; import * as legacy from '@firebase/firestore-types'; import * as exp from '../index'; import { addDoc, - clearIndexedDbPersistence, collection, - collectionGroup, deleteDoc, - disableNetwork, doc, DocumentReference as DocumentReferenceExp, - enableIndexedDbPersistence, - enableMultiTabIndexedDbPersistence, - enableNetwork, FieldPath as FieldPathExp, getDoc, getDocFromCache, @@ -40,21 +31,13 @@ import { getDocs, getDocsFromCache, getDocsFromServer, - initializeFirestore, - loadBundle, - namedQuery, onSnapshot, - onSnapshotsInSync, query, queryEqual, refEqual, - runTransaction, setDoc, snapshotEqual, - terminate, updateDoc, - waitForPendingWrites, - writeBatch, endAt, endBefore, startAfter, @@ -72,10 +55,10 @@ import { validateSetOptions } from '../../src/util/input_validation'; import { Compat } from '../../src/compat/compat'; -import { LoadBundleTask } from '../../exp-types'; +import { Firestore, loadBundle, namedQuery } from '../../src/api/database'; export { GeoPoint, Timestamp } from '../index'; -export { FieldValue } from '../../src/compat/field_value'; +export { loadBundle, namedQuery }; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -83,116 +66,11 @@ export { FieldValue } from '../../src/compat/field_value'; // of the experimental SDK. This shim is used to run integration tests against // both SDK versions. -export class FirebaseApp - extends Compat - implements FirebaseAppLegacy { - name = this._delegate.name; - options = this._delegate.options; - automaticDataCollectionEnabled = this._delegate - .automaticDataCollectionEnabled; - - delete(): Promise { - return deleteApp(this._delegate); - } -} - -export class FirebaseFirestore - extends Compat - implements legacy.FirebaseFirestore { - app = new FirebaseApp(this._delegate.app); - - settings(settings: legacy.Settings): void { - initializeFirestore(this.app._delegate, settings); - } - - useEmulator(host: string, port: number): void { - this.settings({ host: `${host}:${port}`, ssl: false, merge: true }); - } - - enablePersistence(settings?: legacy.PersistenceSettings): Promise { - return settings?.synchronizeTabs - ? enableMultiTabIndexedDbPersistence(this._delegate) - : enableIndexedDbPersistence(this._delegate); - } - - collection(collectionPath: string): CollectionReference { - return new CollectionReference( - this, - collection(this._delegate, collectionPath) - ); - } - - doc(documentPath: string): DocumentReference { - return new DocumentReference(this, doc(this._delegate, documentPath)); - } - - collectionGroup(collectionId: string): Query { - return new Query(this, collectionGroup(this._delegate, collectionId)); - } - - runTransaction( - updateFunction: (transaction: legacy.Transaction) => Promise - ): Promise { - return runTransaction(this._delegate, t => - updateFunction(new Transaction(this, t)) - ); - } - - batch(): legacy.WriteBatch { - return new WriteBatch(writeBatch(this._delegate)); - } - - clearPersistence(): Promise { - return clearIndexedDbPersistence(this._delegate); - } - - enableNetwork(): Promise { - return enableNetwork(this._delegate); - } - - disableNetwork(): Promise { - return disableNetwork(this._delegate); - } - - waitForPendingWrites(): Promise { - return waitForPendingWrites(this._delegate); - } - - onSnapshotsInSync(observer: { - next?: (value: void) => void; - error?: (error: legacy.FirestoreError) => void; - complete?: () => void; - }): () => void; - onSnapshotsInSync(onSync: () => void): () => void; - onSnapshotsInSync(arg: any): () => void { - return onSnapshotsInSync(this._delegate, arg); - } - - terminate(): Promise { - return terminate(this._delegate); - } - - loadBundle( - bundleData: ArrayBuffer | ReadableStream | string - ): LoadBundleTask { - return loadBundle(this._delegate, bundleData)!; - } - - async namedQuery(name: string): Promise { - const query = await namedQuery(this._delegate, name); - return !!query ? new Query(this, query) : null; - } - - INTERNAL = { - delete: () => terminate(this._delegate) - }; -} - export class Transaction extends Compat implements legacy.Transaction { constructor( - private readonly _firestore: FirebaseFirestore, + private readonly _firestore: Firestore, delegate: exp.Transaction ) { super(delegate); @@ -315,7 +193,7 @@ export class DocumentReference extends Compat> implements legacy.DocumentReference { constructor( - readonly firestore: FirebaseFirestore, + readonly firestore: Firestore, delegate: exp.DocumentReference ) { super(delegate); @@ -438,7 +316,7 @@ export class DocumentSnapshot extends Compat> implements legacy.DocumentSnapshot { constructor( - private readonly _firestore: FirebaseFirestore, + private readonly _firestore: Firestore, delegate: exp.DocumentSnapshot ) { super(delegate); @@ -453,11 +331,14 @@ export class DocumentSnapshot } data(options?: legacy.SnapshotOptions): T | undefined { - return wrap(this._delegate.data(options)); + return wrap(this._firestore, this._delegate.data(options)); } get(fieldPath: string | FieldPath, options?: legacy.SnapshotOptions): any { - return wrap(this._delegate.get(unwrap(fieldPath), options)); + return wrap( + this._firestore, + this._delegate.get(unwrap(fieldPath), options) + ); } isEqual(other: DocumentSnapshot): boolean { @@ -468,22 +349,15 @@ export class DocumentSnapshot export class QueryDocumentSnapshot extends DocumentSnapshot implements legacy.QueryDocumentSnapshot { - constructor( - firestore: FirebaseFirestore, - readonly _delegate: exp.QueryDocumentSnapshot - ) { - super(firestore, _delegate); - } - data(options?: legacy.SnapshotOptions): T { - return this._delegate.data(options); + return this._delegate.data(options)!; } } export class Query extends Compat> implements legacy.Query { - constructor(readonly firestore: FirebaseFirestore, delegate: exp.Query) { + constructor(readonly firestore: Firestore, delegate: exp.Query) { super(delegate); } @@ -606,7 +480,7 @@ export class Query export class QuerySnapshot implements legacy.QuerySnapshot { constructor( - readonly _firestore: FirebaseFirestore, + readonly _firestore: Firestore, readonly _delegate: exp.QuerySnapshot ) {} @@ -647,7 +521,7 @@ export class QuerySnapshot export class DocumentChange implements legacy.DocumentChange { constructor( - private readonly _firestore: FirebaseFirestore, + private readonly _firestore: Firestore, private readonly _delegate: exp.DocumentChange ) {} readonly type = this._delegate.type; @@ -663,7 +537,7 @@ export class CollectionReference extends Query implements legacy.CollectionReference { constructor( - firestore: FirebaseFirestore, + readonly firestore: Firestore, readonly _delegate: exp.CollectionReference ) { super(firestore, _delegate); @@ -712,15 +586,11 @@ export class CollectionReference } } -export class FieldPath implements legacy.FieldPath { - private readonly fieldNames: string[]; - +export class FieldPath + extends Compat + implements legacy.FieldPath { constructor(...fieldNames: string[]) { - this.fieldNames = fieldNames; - } - - get _delegate(): FieldPathExp { - return new FieldPathExp(...this.fieldNames); + super(new FieldPathExp(...fieldNames)); } static documentId(): FieldPath { @@ -758,25 +628,20 @@ export class Blob extends Compat implements legacy.Blob { * Takes document data that uses the firestore-exp API types and replaces them * with the API types defined in this shim. */ -function wrap(value: any): any { +function wrap(firestore: Firestore, value: any): any { if (Array.isArray(value)) { - return value.map(v => wrap(v)); + return value.map(v => wrap(firestore, v)); } else if (value instanceof FieldPathExp) { return new FieldPath(...value._internalPath.toArray()); } else if (value instanceof BytesExp) { return new Blob(value); } else if (value instanceof DocumentReferenceExp) { - // TODO(mrschmidt): Ideally, we should use an existing instance of - // FirebaseFirestore here rather than instantiating a new instance - return new DocumentReference( - new FirebaseFirestore(value.firestore as exp.FirebaseFirestore), - value - ); + return new DocumentReference(firestore, value); } else if (isPlainObject(value)) { const obj: any = {}; for (const key in value) { if (value.hasOwnProperty(key)) { - obj[key] = wrap(value[key]); + obj[key] = wrap(firestore, value[key]); } } return obj; diff --git a/packages/firestore/index.console.ts b/packages/firestore/index.console.ts index 68a2eba4fef..9db3ba8d117 100644 --- a/packages/firestore/index.console.ts +++ b/packages/firestore/index.console.ts @@ -15,7 +15,15 @@ * limitations under the License. */ -export { Firestore } from './src/api/database'; +import { FirebaseFirestore as FirestoreExp } from './exp/src/api/database'; +import { + Firestore as FirestoreCompat, + MemoryPersistenceProvider +} from './src/api/database'; +import { Provider } from '@firebase/component'; +import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; +import { DatabaseId } from './src/core/database_info'; +import { Code, FirestoreError } from './src/util/error'; export { CollectionReference, DocumentReference, @@ -27,3 +35,37 @@ export { GeoPoint } from './src/api/geo_point'; export { FieldPath } from './src/api/field_path'; export { FieldValue } from './src/compat/field_value'; export { Timestamp } from './src/api/timestamp'; + +export interface FirestoreDatabase { + projectId: string; + database?: string; +} + +/** Firestore class that exposes the constructor expected by the Console. */ +export class Firestore extends FirestoreCompat { + constructor( + firestoreDatabase: FirestoreDatabase, + authProvider: Provider + ) { + super( + databaseIdFromFirestoreDatabase(firestoreDatabase), + new FirestoreExp( + databaseIdFromFirestoreDatabase(firestoreDatabase), + authProvider + ), + new MemoryPersistenceProvider() + ); + } +} + +function databaseIdFromFirestoreDatabase( + firestoreDatabase: FirestoreDatabase +): DatabaseId { + if (!firestoreDatabase.projectId) { + throw new FirestoreError(Code.INVALID_ARGUMENT, 'Must provide projectId'); + } + return new DatabaseId( + firestoreDatabase.projectId, + firestoreDatabase.database + ); +} diff --git a/packages/firestore/index.memory.ts b/packages/firestore/index.memory.ts index 9b313aad439..915cd1d4fdd 100644 --- a/packages/firestore/index.memory.ts +++ b/packages/firestore/index.memory.ts @@ -19,6 +19,7 @@ import firebase from '@firebase/app'; import { FirebaseNamespace } from '@firebase/app-types'; import { Firestore, MemoryPersistenceProvider } from './src/api/database'; +import { FirebaseFirestore } from './exp/src/api/database'; import { configureForFirebase } from './src/config'; import './register-module'; @@ -31,7 +32,12 @@ import { name, version } from './package.json'; export function registerFirestore(instance: FirebaseNamespace): void { configureForFirebase( instance, - (app, auth) => new Firestore(app, auth, new MemoryPersistenceProvider()) + (app, auth) => + new Firestore( + app, + new FirebaseFirestore(app, auth), + new MemoryPersistenceProvider() + ) ); instance.registerVersion(name, version); } diff --git a/packages/firestore/index.node.memory.ts b/packages/firestore/index.node.memory.ts index 0982b38fe46..ff098861c65 100644 --- a/packages/firestore/index.node.memory.ts +++ b/packages/firestore/index.node.memory.ts @@ -19,6 +19,7 @@ import firebase from '@firebase/app'; import { FirebaseNamespace } from '@firebase/app-types'; import { Firestore, MemoryPersistenceProvider } from './src/api/database'; +import { FirebaseFirestore } from './exp/src/api/database'; import { configureForFirebase } from './src/config'; import './register-module'; @@ -31,7 +32,12 @@ import { name, version } from './package.json'; export function registerFirestore(instance: FirebaseNamespace): void { configureForFirebase( instance, - (app, auth) => new Firestore(app, auth, new MemoryPersistenceProvider()) + (app, auth) => + new Firestore( + app, + new FirebaseFirestore(app, auth), + new MemoryPersistenceProvider() + ) ); instance.registerVersion(name, version, 'node'); } diff --git a/packages/firestore/index.node.ts b/packages/firestore/index.node.ts index 05d406ddb67..ddabccae41a 100644 --- a/packages/firestore/index.node.ts +++ b/packages/firestore/index.node.ts @@ -19,6 +19,7 @@ import { FirebaseNamespace } from '@firebase/app-types'; import { Firestore, IndexedDbPersistenceProvider } from './src/api/database'; import { configureForFirebase } from './src/config'; +import { FirebaseFirestore as ExpFirebaseFirestore } from './exp/src/api/database'; import './register-module'; @@ -29,9 +30,15 @@ import { name, version } from './package.json'; * Persistence can be enabled via `firebase.firestore().enablePersistence()`. */ export function registerFirestore(instance: FirebaseNamespace): void { - configureForFirebase(instance, (app, auth) => { - return new Firestore(app, auth, new IndexedDbPersistenceProvider()); - }); + configureForFirebase( + instance, + (app, auth) => + new Firestore( + app, + new ExpFirebaseFirestore(app, auth), + new IndexedDbPersistenceProvider() + ) + ); instance.registerVersion(name, version, 'node'); } diff --git a/packages/firestore/index.rn.memory.ts b/packages/firestore/index.rn.memory.ts index 4e6ce2537cc..ae767b0b053 100644 --- a/packages/firestore/index.rn.memory.ts +++ b/packages/firestore/index.rn.memory.ts @@ -18,6 +18,7 @@ import firebase from '@firebase/app'; import { FirebaseNamespace } from '@firebase/app-types'; +import { FirebaseFirestore as ExpFirebaseFirestore } from './exp/src/api/database'; import { Firestore, MemoryPersistenceProvider } from './src/api/database'; import { configureForFirebase } from './src/config'; @@ -32,7 +33,12 @@ import { name, version } from './package.json'; export function registerFirestore(instance: FirebaseNamespace): void { configureForFirebase( instance, - (app, auth) => new Firestore(app, auth, new MemoryPersistenceProvider()) + (app, auth) => + new Firestore( + app, + new ExpFirebaseFirestore(app, auth), + new MemoryPersistenceProvider() + ) ); instance.registerVersion(name, version, 'rn'); } diff --git a/packages/firestore/index.rn.ts b/packages/firestore/index.rn.ts index 84cdcf2ffa3..2b51d8a59c4 100644 --- a/packages/firestore/index.rn.ts +++ b/packages/firestore/index.rn.ts @@ -19,6 +19,7 @@ import { FirebaseNamespace } from '@firebase/app-types'; import { Firestore, IndexedDbPersistenceProvider } from './src/api/database'; import { configureForFirebase } from './src/config'; +import { FirebaseFirestore as ExpFirebaseFirestore } from './exp/src/api/database'; import './register-module'; import { name, version } from './package.json'; @@ -28,9 +29,15 @@ import { name, version } from './package.json'; * Persistence can be enabled via `firebase.firestore().enablePersistence()`. */ export function registerFirestore(instance: FirebaseNamespace): void { - configureForFirebase(instance, (app, auth) => { - return new Firestore(app, auth, new IndexedDbPersistenceProvider()); - }); + configureForFirebase( + instance, + (app, auth) => + new Firestore( + app, + new ExpFirebaseFirestore(app, auth), + new IndexedDbPersistenceProvider() + ) + ); instance.registerVersion(name, version, 'rn'); } diff --git a/packages/firestore/index.ts b/packages/firestore/index.ts index 5b40359f0bf..349e29356b4 100644 --- a/packages/firestore/index.ts +++ b/packages/firestore/index.ts @@ -19,6 +19,7 @@ import firebase from '@firebase/app'; import { FirebaseNamespace } from '@firebase/app-types'; import { Firestore, IndexedDbPersistenceProvider } from './src/api/database'; +import { FirebaseFirestore as ExpFirebaseFirestore } from './exp/src/api/database'; import { configureForFirebase } from './src/config'; import { name, version } from './package.json'; @@ -29,9 +30,15 @@ import './register-module'; * Persistence can be enabled via `firebase.firestore().enablePersistence()`. */ export function registerFirestore(instance: FirebaseNamespace): void { - configureForFirebase(instance, (app, auth) => { - return new Firestore(app, auth, new IndexedDbPersistenceProvider()); - }); + configureForFirebase( + instance, + (app, auth) => + new Firestore( + app, + new ExpFirebaseFirestore(app, auth), + new IndexedDbPersistenceProvider() + ) + ); instance.registerVersion(name, version); } diff --git a/packages/firestore/lite/src/api/components.ts b/packages/firestore/lite/src/api/components.ts index 43e4057a231..93ecb360a0c 100644 --- a/packages/firestore/lite/src/api/components.ts +++ b/packages/firestore/lite/src/api/components.ts @@ -55,7 +55,7 @@ export function getDatastore(firestore: FirebaseFirestore): Datastore { const databaseInfo = makeDatabaseInfo( firestore._databaseId, firestore._persistenceKey, - firestore._getSettings() + firestore._freezeSettings() ); const connection = newConnection(databaseInfo); const serializer = newSerializer(firestore._databaseId); diff --git a/packages/firestore/lite/src/api/database.ts b/packages/firestore/lite/src/api/database.ts index c42ab38f774..eac53329fee 100644 --- a/packages/firestore/lite/src/api/database.ts +++ b/packages/firestore/lite/src/api/database.ts @@ -24,8 +24,10 @@ import { DatabaseId, DatabaseInfo } from '../../../src/core/database_info'; import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { CredentialsProvider, + FirebaseCredentialsProvider, CredentialsSettings, - FirebaseCredentialsProvider + EmptyCredentialsProvider, + makeCredentialsProvider } from '../../../src/api/credentials'; import { removeComponents } from './components'; import { @@ -151,29 +153,45 @@ export class FirestoreSettings { */ export class FirebaseFirestore implements _FirebaseService { readonly _databaseId: DatabaseId; - readonly _credentials: CredentialsProvider; readonly _persistenceKey: string = '(lite)'; + _credentials: CredentialsProvider; - protected _settings?: Settings; + private _settings = new FirestoreSettings({}); private _settingsFrozen = false; // A task that is assigned when the terminate() is invoked and resolved when // all components have shut down. private _terminateTask?: Promise; - /** - * The {@link FirebaseApp app} associated with this `Firestore` service - * instance. - */ - readonly app: FirebaseApp; + private _app?: FirebaseApp; constructor( - app: FirebaseApp, + databaseIdOrApp: DatabaseId | FirebaseApp, authProvider: Provider ) { - this.app = app; - this._databaseId = FirebaseFirestore._databaseIdFromApp(app); - this._credentials = new FirebaseCredentialsProvider(authProvider); + if (databaseIdOrApp instanceof DatabaseId) { + this._databaseId = databaseIdOrApp; + this._credentials = new EmptyCredentialsProvider(); + } else { + this._app = databaseIdOrApp as FirebaseApp; + this._databaseId = databaseIdFromApp(databaseIdOrApp as FirebaseApp); + this._credentials = new FirebaseCredentialsProvider(authProvider); + } + } + + /** + * The {@link FirebaseApp app} associated with this `Firestore` service + * instance. + */ + get app(): FirebaseApp { + if (!this._app) { + throw new FirestoreError( + Code.FAILED_PRECONDITION, + "Firestore was not initialized using the Firebase SDK. 'app' is " + + 'not available' + ); + } + return this._app; } get _initialized(): boolean { @@ -184,35 +202,28 @@ export class FirebaseFirestore implements _FirebaseService { return this._terminateTask !== undefined; } - _setSettings(settings: Settings): void { + _setSettings(settings: PrivateSettings): void { if (this._settingsFrozen) { throw new FirestoreError( Code.FAILED_PRECONDITION, 'Firestore has already been started and its settings can no longer ' + - 'be changed. initializeFirestore() cannot be called after calling ' + - 'getFirestore().' + 'be changed. You can only modify settings before calling any other ' + + 'methods on a Firestore object.' ); } - this._settings = settings; + this._settings = new FirestoreSettings(settings); + if (settings.credentials !== undefined) { + this._credentials = makeCredentialsProvider(settings.credentials); + } } _getSettings(): FirestoreSettings { - if (!this._settings) { - this._settings = {}; - } - this._settingsFrozen = true; - return new FirestoreSettings(this._settings); + return this._settings; } - private static _databaseIdFromApp(app: FirebaseApp): DatabaseId { - if (!Object.prototype.hasOwnProperty.apply(app.options, ['projectId'])) { - throw new FirestoreError( - Code.INVALID_ARGUMENT, - '"projectId" not provided in firebase.initializeApp.' - ); - } - - return new DatabaseId(app.options.projectId!); + _freezeSettings(): FirestoreSettings { + this._settingsFrozen = true; + return this._settings; } _delete(): Promise { @@ -235,6 +246,17 @@ export class FirebaseFirestore implements _FirebaseService { } } +function databaseIdFromApp(app: FirebaseApp): DatabaseId { + if (!Object.prototype.hasOwnProperty.apply(app.options, ['projectId'])) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + '"projectId" not provided in firebase.initializeApp.' + ); + } + + return new DatabaseId(app.options.projectId!); +} + /** * Initializes a new instance of Cloud Firestore with the provided settings. * Can only be called before any other functions, including diff --git a/packages/firestore/lite/src/api/reference.ts b/packages/firestore/lite/src/api/reference.ts index 87bb7b4cc52..bd899a4edb7 100644 --- a/packages/firestore/lite/src/api/reference.ts +++ b/packages/firestore/lite/src/api/reference.ts @@ -1221,7 +1221,7 @@ export function queryEqual(left: Query, right: Query): boolean { export function newUserDataReader( firestore: FirebaseFirestore ): UserDataReader { - const settings = firestore._getSettings(); + const settings = firestore._freezeSettings(); const serializer = newSerializer(firestore._databaseId); return new UserDataReader( firestore._databaseId, diff --git a/packages/firestore/package.json b/packages/firestore/package.json index ba8398a0208..279312f2d3e 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/firestore", - "version": "2.0.0", + "version": "2.0.1", "engines": { "node": "^8.13.0 || >=10.10.0" }, @@ -28,13 +28,13 @@ "test:lite": "node ./scripts/run-tests.js --emulator --platform node_lite --main=lite/index.ts 'lite/test/**/*.test.ts'", "test:lite:prod": "node ./scripts/run-tests.js --platform node_lite --main=lite/index.ts 'lite/test/**/*.test.ts'", "test:lite:browser": "karma start --single-run --lite", - "test:lite:browser:debug": "karma start --single-run --lite --auto-watch", + "test:lite:browser:debug": "karma start --browsers=Chrome --lite --auto-watch", "test:exp": "node ./scripts/run-tests.js --emulator --main=exp/index.ts test/integration/api/*.test.ts", "test:exp:prod": "node ./scripts/run-tests.js --main=exp/index.ts test/integration/api/*.test.ts", "test:exp:persistence": "node ./scripts/run-tests.js --emulator --persistence --main=exp/index.ts test/integration/api/*.test.ts", "test:exp:persistence:prod": "node ./scripts/run-tests.js --persistence --main=exp/index.ts test/integration/api/*.test.ts", "test:exp:browser": "karma start --single-run --exp", - "test:exp:browser:debug": "karma start --single-run --exp --auto-watch", + "test:exp:browser:debug": "karma start --browsers=Chrome --exp --auto-watch", "test": "run-s lint test:all", "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all", "test:all": "run-p test:browser test:lite:browser test:exp:browser test:travis test:minified", @@ -63,10 +63,10 @@ "memory/package.json" ], "dependencies": { - "@firebase/component": "0.1.20", + "@firebase/component": "0.1.21", "@firebase/firestore-types": "2.0.0", "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.3", + "@firebase/util": "0.3.4", "@firebase/webchannel-wrapper": "0.4.0", "@grpc/grpc-js": "^1.0.0", "@grpc/proto-loader": "^0.5.0", @@ -78,21 +78,21 @@ "@firebase/app-types": "0.x" }, "devDependencies": { - "@firebase/app": "0.6.12", + "@firebase/app": "0.6.13", "@rollup/plugin-alias": "3.1.1", "@types/json-stable-stringify": "1.0.32", "json-stable-stringify": "1.0.1", "protobufjs": "6.10.1", - "rollup": "2.29.0", + "rollup": "2.33.1", "rollup-plugin-copy-assets": "1.1.0", "@rollup/plugin-json": "4.1.0", "@rollup/plugin-node-resolve": "9.0.0", "rollup-plugin-replace": "2.2.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2", - "rollup-plugin-typescript2": "0.27.3", + "rollup-plugin-typescript2": "0.29.0", "ts-node": "9.0.0", - "typescript": "4.0.2" + "typescript": "4.0.5" }, "repository": { "directory": "packages/firestore", diff --git a/packages/firestore/register-module.ts b/packages/firestore/register-module.ts index f76fabd5a1d..2ae215e68b8 100644 --- a/packages/firestore/register-module.ts +++ b/packages/firestore/register-module.ts @@ -36,6 +36,8 @@ declare module '@firebase/app-types' { Transaction: typeof types.Transaction; WriteBatch: typeof types.WriteBatch; setLogLevel: typeof types.setLogLevel; + loadBundle: typeof types.loadBundle; + namedQuery: typeof types.namedQuery; }; } interface FirebaseApp { diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 8c5198b22b4..baafb861a54 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -16,32 +16,6 @@ */ import { Value as ProtoValue } from '../protos/firestore_proto_api'; -import { - CollectionReference as PublicCollectionReference, - DocumentChange as PublicDocumentChange, - DocumentChangeType, - DocumentData, - DocumentReference as PublicDocumentReference, - DocumentSnapshot as PublicDocumentSnapshot, - FirebaseFirestore as PublicFirestore, - FirestoreDataConverter, - GetOptions, - LogLevel as PublicLogLevel, - OrderByDirection, - PersistenceSettings as PublicPersistenceSettings, - Query as PublicQuery, - QueryDocumentSnapshot as PublicQueryDocumentSnapshot, - QuerySnapshot as PublicQuerySnapshot, - SetOptions, - Settings as PublicSettings, - SnapshotListenOptions, - SnapshotMetadata as PublicSnapshotMetadata, - SnapshotOptions as PublicSnapshotOptions, - Transaction as PublicTransaction, - UpdateData, - WhereFilterOp, - WriteBatch as PublicWriteBatch -} from '@firebase/firestore-types'; import { FirebaseApp } from '@firebase/app-types'; import { _FirebaseApp, FirebaseService } from '@firebase/app-types/private'; @@ -50,16 +24,12 @@ import { DatabaseId } from '../core/database_info'; import { ListenOptions } from '../core/event_manager'; import { FirestoreClient, - firestoreClientAddSnapshotsInSyncListener, - firestoreClientDisableNetwork, - firestoreClientEnableNetwork, firestoreClientGetDocumentFromLocalCache, firestoreClientGetDocumentsFromLocalCache, firestoreClientGetDocumentsViaSnapshotListener, firestoreClientGetDocumentViaSnapshotListener, firestoreClientListen, firestoreClientTransaction, - firestoreClientWaitForPendingWrites, firestoreClientWrite, firestoreClientLoadBundle, firestoreClientGetNamedQuery @@ -98,7 +68,6 @@ import { FieldPath, ResourcePath } from '../model/path'; import { isServerTimestamp } from '../model/server_timestamps'; import { refValue } from '../model/values'; import { debugAssert, fail } from '../util/assert'; -import { AsyncQueue } from '../util/async_queue'; import { Code, FirestoreError } from '../util/error'; import { cast, @@ -110,13 +79,7 @@ import { } from '../util/input_validation'; import { logWarn, setLogLevel as setClientLogLevel } from '../util/log'; import { AutoId } from '../util/misc'; -import { FieldPath as ExternalFieldPath } from './field_path'; -import { - CredentialsProvider, - EmptyCredentialsProvider, - FirebaseCredentialsProvider, - makeCredentialsProvider -} from './credentials'; +import { _BaseFieldPath, FieldPath as ExternalFieldPath } from './field_path'; import { CompleteFn, ErrorFn, @@ -136,18 +99,49 @@ import { UserDataReader } from './user_data_reader'; import { UserDataWriter } from './user_data_writer'; -import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; -import { Provider } from '@firebase/component'; import { clearIndexedDbPersistence, + disableNetwork, enableIndexedDbPersistence, - enableMultiTabIndexedDbPersistence + enableMultiTabIndexedDbPersistence, + enableNetwork, + FirebaseFirestore, + terminate, + waitForPendingWrites, + FirebaseFirestore as ExpFirebaseFirestore } from '../../exp/src/api/database'; +import { onSnapshotsInSync } from '../../exp/src/api/reference'; import { LRU_COLLECTION_DISABLED } from '../local/lru_garbage_collector'; +import { Compat } from '../compat/compat'; + import { - FirestoreSettings, - makeDatabaseInfo -} from '../../lite/src/api/database'; + CollectionReference as PublicCollectionReference, + DocumentChange as PublicDocumentChange, + DocumentChangeType as PublicDocumentChangeType, + DocumentData as PublicDocumentData, + DocumentReference as PublicDocumentReference, + DocumentSnapshot as PublicDocumentSnapshot, + FirebaseFirestore as PublicFirestore, + FirestoreDataConverter as PublicFirestoreDataConverter, + GetOptions as PublicGetOptions, + LogLevel as PublicLogLevel, + OrderByDirection as PublicOrderByDirection, + PersistenceSettings as PublicPersistenceSettings, + Query as PublicQuery, + QueryDocumentSnapshot as PublicQueryDocumentSnapshot, + QuerySnapshot as PublicQuerySnapshot, + SetOptions as PublicSetOptions, + Settings as PublicSettings, + SnapshotListenOptions as PublicSnapshotListenOptions, + SnapshotMetadata as PublicSnapshotMetadata, + SnapshotOptions as PublicSnapshotOptions, + Transaction as PublicTransaction, + UpdateData as PublicUpdateData, + WhereFilterOp as PublicWhereFilterOp, + WriteBatch as PublicWriteBatch +} from '@firebase/firestore-types'; +import { newUserDataReader } from '../../lite/src/api/reference'; +import { makeDatabaseInfo } from '../../lite/src/api/database'; import { DEFAULT_HOST } from '../../lite/src/api/components'; import { LoadBundleTask } from './bundle'; @@ -158,40 +152,17 @@ import { LoadBundleTask } from './bundle'; */ export const CACHE_SIZE_UNLIMITED = LRU_COLLECTION_DISABLED; -/** - * Options that can be provided in the Firestore constructor when not using - * Firebase (aka standalone mode). - */ -export interface FirestoreDatabase { - projectId: string; - database?: string; -} - -// TODO(firestore-compat): This interface exposes internal APIs that the Compat -// layer implements to interact with the firestore-exp SDK. We can remove this -// class once we have an actual compat class for FirebaseFirestore. -export interface FirestoreCompat { - readonly _initialized: boolean; - readonly _terminated: boolean; - readonly _databaseId: DatabaseId; - readonly _persistenceKey: string; - readonly _queue: AsyncQueue; - readonly _credentials: CredentialsProvider; - _firestoreClient?: FirestoreClient; - _getSettings(): FirestoreSettings; -} - /** * A persistence provider for either memory-only or IndexedDB persistence. * Mainly used to allow optional inclusion of IndexedDB code. */ export interface PersistenceProvider { enableIndexedDbPersistence( - firestore: FirestoreCompat, + firestore: Firestore, forceOwnership: boolean ): Promise; - enableMultiTabIndexedDbPersistence(firestore: FirestoreCompat): Promise; - clearIndexedDbPersistence(firestore: FirestoreCompat): Promise; + enableMultiTabIndexedDbPersistence(firestore: Firestore): Promise; + clearIndexedDbPersistence(firestore: Firestore): Promise; } const MEMORY_ONLY_PERSISTENCE_ERROR_MESSAGE = @@ -205,7 +176,7 @@ const MEMORY_ONLY_PERSISTENCE_ERROR_MESSAGE = */ export class MemoryPersistenceProvider implements PersistenceProvider { enableIndexedDbPersistence( - firestore: FirestoreCompat, + firestore: Firestore, forceOwnership: boolean ): Promise { throw new FirestoreError( @@ -214,16 +185,14 @@ export class MemoryPersistenceProvider implements PersistenceProvider { ); } - enableMultiTabIndexedDbPersistence( - firestore: FirestoreCompat - ): Promise { + enableMultiTabIndexedDbPersistence(firestore: Firestore): Promise { throw new FirestoreError( Code.FAILED_PRECONDITION, MEMORY_ONLY_PERSISTENCE_ERROR_MESSAGE ); } - clearIndexedDbPersistence(firestore: FirestoreCompat): Promise { + clearIndexedDbPersistence(firestore: Firestore): Promise { throw new FirestoreError( Code.FAILED_PRECONDITION, MEMORY_ONLY_PERSISTENCE_ERROR_MESSAGE @@ -236,123 +205,57 @@ export class MemoryPersistenceProvider implements PersistenceProvider { */ export class IndexedDbPersistenceProvider implements PersistenceProvider { enableIndexedDbPersistence( - firestore: FirestoreCompat, + firestore: Firestore, forceOwnership: boolean ): Promise { - return enableIndexedDbPersistence(firestore, { forceOwnership }); + return enableIndexedDbPersistence(firestore._delegate, { forceOwnership }); + } + enableMultiTabIndexedDbPersistence(firestore: Firestore): Promise { + return enableMultiTabIndexedDbPersistence(firestore._delegate); + } + clearIndexedDbPersistence(firestore: Firestore): Promise { + return clearIndexedDbPersistence(firestore._delegate); } - enableMultiTabIndexedDbPersistence = enableMultiTabIndexedDbPersistence.bind( - null - ); - clearIndexedDbPersistence = clearIndexedDbPersistence.bind(null); } + /** - * The root reference to the database. + * Compat class for Firestore. Exposes Firestore Legacy API, but delegates + * to the functional API of firestore-exp. */ export class Firestore - implements PublicFirestore, FirebaseService, FirestoreCompat { - // The objects that are a part of this API are exposed to third-parties as - // compiled javascript so we want to flag our private members with a leading - // underscore to discourage their use. - readonly _databaseId: DatabaseId; - readonly _persistenceKey: string; - _credentials: CredentialsProvider; - private readonly _firebaseApp: FirebaseApp | null = null; - private _settings: FirestoreSettings; - - // The firestore client instance. This will be available as soon as - // `configureFirestore()` is called, but any calls against it will block until - // setup has completed. - _firestoreClient?: FirestoreClient; - - // Public for use in tests. - // TODO(mikelehen): Use modularized initialization instead. - readonly _queue = new AsyncQueue(); - - _userDataReader: UserDataReader | undefined; - - // Note: We are using `MemoryPersistenceProvider` as a default - // ComponentProvider to ensure backwards compatibility with the format - // expected by the console build. + extends Compat + implements PublicFirestore, FirebaseService { + _appCompat?: FirebaseApp; constructor( - databaseIdOrApp: FirestoreDatabase | FirebaseApp, - authProvider: Provider, - readonly _persistenceProvider: PersistenceProvider = new MemoryPersistenceProvider() + databaseIdOrApp: DatabaseId | FirebaseApp, + delegate: ExpFirebaseFirestore, + private _persistenceProvider: PersistenceProvider ) { - if (typeof (databaseIdOrApp as FirebaseApp).options === 'object') { - // This is very likely a Firebase app object - // TODO(b/34177605): Can we somehow use instanceof? - const app = databaseIdOrApp as FirebaseApp; - this._firebaseApp = app; - this._databaseId = Firestore.databaseIdFromApp(app); - this._persistenceKey = app.name; - this._credentials = new FirebaseCredentialsProvider(authProvider); - } else { - const external = databaseIdOrApp as FirestoreDatabase; - if (!external.projectId) { - throw new FirestoreError( - Code.INVALID_ARGUMENT, - 'Must provide projectId' - ); - } + super(delegate); - this._databaseId = new DatabaseId(external.projectId, external.database); - // Use a default persistenceKey that lines up with FirebaseApp. - this._persistenceKey = '[DEFAULT]'; - this._credentials = new EmptyCredentialsProvider(); + if (!(databaseIdOrApp instanceof DatabaseId)) { + this._appCompat = databaseIdOrApp as FirebaseApp; } - - this._settings = new FirestoreSettings({}); - } - - get _initialized(): boolean { - return !!this._firestoreClient; - } - - get _terminated(): boolean { - return this._queue.isShuttingDown; } - get _dataReader(): UserDataReader { - debugAssert( - !!this._firestoreClient, - 'Cannot obtain UserDataReader before instance is intitialized' - ); - if (!this._userDataReader) { - // Lazy initialize UserDataReader once the settings are frozen - this._userDataReader = new UserDataReader( - this._databaseId, - this._settings.ignoreUndefinedProperties - ); - } - return this._userDataReader; + get _databaseId(): DatabaseId { + return this._delegate._databaseId; } settings(settingsLiteral: PublicSettings): void { if (settingsLiteral.merge) { - settingsLiteral = { ...this._settings, ...settingsLiteral }; + settingsLiteral = { + ...this._delegate._getSettings(), + ...settingsLiteral + }; // Remove the property from the settings once the merge is completed delete settingsLiteral.merge; } - - const newSettings = new FirestoreSettings(settingsLiteral); - if (this._firestoreClient && !this._settings.isEqual(newSettings)) { - throw new FirestoreError( - Code.FAILED_PRECONDITION, - 'Firestore has already been started and its settings can no longer ' + - 'be changed. You can only modify settings before calling any other ' + - 'methods on a Firestore object.' - ); - } - - this._settings = newSettings; - if (newSettings.credentials !== undefined) { - this._credentials = makeCredentialsProvider(newSettings.credentials); - } + this._delegate._setSettings(settingsLiteral); } useEmulator(host: string, port: number): void { - if (this._settings.host !== DEFAULT_HOST) { + if (this._delegate._getSettings().host !== DEFAULT_HOST) { logWarn( 'Host has been set in both settings() and useEmulator(), emulator host will be used' ); @@ -366,25 +269,14 @@ export class Firestore } enableNetwork(): Promise { - ensureFirestoreConfigured(this); - return firestoreClientEnableNetwork(this._firestoreClient!); + return enableNetwork(this._delegate); } disableNetwork(): Promise { - ensureFirestoreConfigured(this); - return firestoreClientDisableNetwork(this._firestoreClient!); + return disableNetwork(this._delegate); } enablePersistence(settings?: PublicPersistenceSettings): Promise { - if (this._firestoreClient) { - throw new FirestoreError( - Code.FAILED_PRECONDITION, - 'Firestore has already been started and persistence can no longer ' + - 'be enabled. You can only call enablePersistence() before calling ' + - 'any other methods on a Firestore object.' - ); - } - let synchronizeTabs = false; let experimentalForceOwningTab = false; @@ -408,7 +300,7 @@ export class Firestore ); } - async clearPersistence(): Promise { + clearPersistence(): Promise { return this._persistenceProvider.clearIndexedDbPersistence(this); } @@ -418,97 +310,33 @@ export class Firestore } waitForPendingWrites(): Promise { - ensureFirestoreConfigured(this); - return firestoreClientWaitForPendingWrites(this._firestoreClient!); + return waitForPendingWrites(this._delegate); } onSnapshotsInSync(observer: PartialObserver): Unsubscribe; onSnapshotsInSync(onSync: () => void): Unsubscribe; onSnapshotsInSync(arg: unknown): Unsubscribe { - ensureFirestoreConfigured(this); - - if (isPartialObserver(arg)) { - return firestoreClientAddSnapshotsInSyncListener( - this._firestoreClient!, - arg as PartialObserver - ); - } else { - const observer: PartialObserver = { - next: arg as () => void - }; - return firestoreClientAddSnapshotsInSyncListener( - this._firestoreClient!, - observer - ); - } - } - - loadBundle( - bundleData: ArrayBuffer | ReadableStream | string - ): LoadBundleTask { - ensureFirestoreConfigured(this); - const resultTask = new LoadBundleTask(); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - firestoreClientLoadBundle(this._firestoreClient!, bundleData, resultTask); - return resultTask; - } - - namedQuery(name: string): Promise { - ensureFirestoreConfigured(this); - return firestoreClientGetNamedQuery(this._firestoreClient!, name).then( - namedQuery => { - if (!namedQuery) { - return null; - } - - return new Query(namedQuery.query, this, null); - } - ); - } - - private static databaseIdFromApp(app: FirebaseApp): DatabaseId { - if (!contains(app.options, 'projectId')) { - throw new FirestoreError( - Code.INVALID_ARGUMENT, - '"projectId" not provided in firebase.initializeApp.' - ); - } - - const projectId = app.options.projectId; - if (!projectId || typeof projectId !== 'string') { - throw new FirestoreError( - Code.INVALID_ARGUMENT, - 'projectId must be a string in FirebaseApp.options' - ); - } - return new DatabaseId(projectId); + return onSnapshotsInSync(this._delegate, arg as PartialObserver); } get app(): FirebaseApp { - if (!this._firebaseApp) { + if (!this._appCompat) { throw new FirestoreError( Code.FAILED_PRECONDITION, "Firestore was not initialized using the Firebase SDK. 'app' is " + 'not available' ); } - return this._firebaseApp; + return this._appCompat as FirebaseApp; } INTERNAL = { - delete: async (): Promise => { - if (!this._firestoreClient) { - // The client must be initialized to ensure that all subsequent API - // usage throws an exception. - configureFirestore(this); - } - await this._firestoreClient!.terminate(); - } + delete: () => terminate(this._delegate) }; collection(pathString: string): PublicCollectionReference { validateNonEmptyArgument('Firestore.collection', 'path', pathString); - ensureFirestoreConfigured(this); + ensureFirestoreConfigured(this._delegate); return new CollectionReference( ResourcePath.fromString(pathString), this, @@ -518,7 +346,7 @@ export class Firestore doc(pathString: string): PublicDocumentReference { validateNonEmptyArgument('Firestore.doc', 'path', pathString); - ensureFirestoreConfigured(this); + ensureFirestoreConfigured(this._delegate); return DocumentReference.forPath( ResourcePath.fromString(pathString), this, @@ -539,7 +367,7 @@ export class Firestore `Firestore.collectionGroup(). Collection IDs must not contain '/'.` ); } - ensureFirestoreConfigured(this); + ensureFirestoreConfigured(this._delegate); return new Query( newQueryForCollectionGroup(collectionId), this, @@ -550,7 +378,7 @@ export class Firestore runTransaction( updateFunction: (transaction: PublicTransaction) => Promise ): Promise { - const client = ensureFirestoreConfigured(this); + const client = ensureFirestoreConfigured(this._delegate); return firestoreClientTransaction( client, (transaction: InternalTransaction) => { @@ -560,17 +388,13 @@ export class Firestore } batch(): PublicWriteBatch { - ensureFirestoreConfigured(this); + ensureFirestoreConfigured(this._delegate); return new WriteBatch(this); } - - _getSettings(): FirestoreSettings { - return this._settings; - } } export function ensureFirestoreConfigured( - firestore: FirestoreCompat + firestore: FirebaseFirestore ): FirestoreClient { if (!firestore._firestoreClient) { configureFirestore(firestore); @@ -579,8 +403,8 @@ export function ensureFirestoreConfigured( return firestore._firestoreClient as FirestoreClient; } -export function configureFirestore(firestore: FirestoreCompat): void { - const settings = firestore._getSettings(); +export function configureFirestore(firestore: FirebaseFirestore): void { + const settings = firestore._freezeSettings(); debugAssert(!!settings.host, 'FirestoreSettings.host is not set'); debugAssert( !firestore._firestoreClient, @@ -603,14 +427,48 @@ export function setLogLevel(level: PublicLogLevel): void { setClientLogLevel(level); } +export function loadBundle( + db: Firestore, + bundleData: ArrayBuffer | ReadableStream | string +): LoadBundleTask { + const resultTask = new LoadBundleTask(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + firestoreClientLoadBundle( + ensureFirestoreConfigured(db._delegate), + bundleData, + resultTask + ); + return resultTask; +} + +export function namedQuery( + db: Firestore, + name: string +): Promise { + return firestoreClientGetNamedQuery( + ensureFirestoreConfigured(db._delegate), + name + ).then(namedQuery => { + if (!namedQuery) { + return null; + } + + return new Query(namedQuery.query, db, null); + }); +} + /** * A reference to a transaction. */ export class Transaction implements PublicTransaction { + private _dataReader: UserDataReader; + constructor( private _firestore: Firestore, private _transaction: InternalTransaction - ) {} + ) { + this._dataReader = newUserDataReader(this._firestore._delegate); + } get( documentRef: PublicDocumentReference @@ -656,13 +514,13 @@ export class Transaction implements PublicTransaction { set( documentRef: DocumentReference, data: Partial, - options: SetOptions + options: PublicSetOptions ): Transaction; set(documentRef: DocumentReference, data: T): Transaction; set( documentRef: PublicDocumentReference, value: T | Partial, - options?: SetOptions + options?: PublicSetOptions ): Transaction { const ref = validateReference( 'Transaction.set', @@ -676,7 +534,7 @@ export class Transaction implements PublicTransaction { options ); const parsed = parseSetData( - this._firestore._dataReader, + this._dataReader, 'Transaction.set', ref._key, convertedValue, @@ -689,7 +547,7 @@ export class Transaction implements PublicTransaction { update( documentRef: PublicDocumentReference, - value: UpdateData + value: PublicUpdateData ): Transaction; update( documentRef: PublicDocumentReference, @@ -699,7 +557,7 @@ export class Transaction implements PublicTransaction { ): Transaction; update( documentRef: PublicDocumentReference, - fieldOrUpdateData: string | ExternalFieldPath | UpdateData, + fieldOrUpdateData: string | ExternalFieldPath | PublicUpdateData, value?: unknown, ...moreFieldsAndValues: unknown[] ): Transaction { @@ -716,7 +574,7 @@ export class Transaction implements PublicTransaction { this._firestore ); parsed = parseUpdateVarargs( - this._firestore._dataReader, + this._dataReader, 'Transaction.update', ref._key, fieldOrUpdateData, @@ -730,7 +588,7 @@ export class Transaction implements PublicTransaction { this._firestore ); parsed = parseUpdateData( - this._firestore._dataReader, + this._dataReader, 'Transaction.update', ref._key, fieldOrUpdateData @@ -755,19 +613,22 @@ export class Transaction implements PublicTransaction { export class WriteBatch implements PublicWriteBatch { private _mutations = [] as Mutation[]; private _committed = false; + private _dataReader: UserDataReader; - constructor(private _firestore: Firestore) {} + constructor(private _firestore: Firestore) { + this._dataReader = newUserDataReader(this._firestore._delegate); + } set( documentRef: DocumentReference, data: Partial, - options: SetOptions + options: PublicSetOptions ): WriteBatch; set(documentRef: DocumentReference, data: T): WriteBatch; set( documentRef: PublicDocumentReference, value: T | Partial, - options?: SetOptions + options?: PublicSetOptions ): WriteBatch { this.verifyNotCommitted(); const ref = validateReference( @@ -782,7 +643,7 @@ export class WriteBatch implements PublicWriteBatch { options ); const parsed = parseSetData( - this._firestore._dataReader, + this._dataReader, 'WriteBatch.set', ref._key, convertedValue, @@ -797,7 +658,7 @@ export class WriteBatch implements PublicWriteBatch { update( documentRef: PublicDocumentReference, - value: UpdateData + value: PublicUpdateData ): WriteBatch; update( documentRef: PublicDocumentReference, @@ -807,7 +668,7 @@ export class WriteBatch implements PublicWriteBatch { ): WriteBatch; update( documentRef: PublicDocumentReference, - fieldOrUpdateData: string | ExternalFieldPath | UpdateData, + fieldOrUpdateData: string | ExternalFieldPath | PublicUpdateData, value?: unknown, ...moreFieldsAndValues: unknown[] ): WriteBatch { @@ -826,7 +687,7 @@ export class WriteBatch implements PublicWriteBatch { this._firestore ); parsed = parseUpdateVarargs( - this._firestore._dataReader, + this._dataReader, 'WriteBatch.update', ref._key, fieldOrUpdateData, @@ -840,7 +701,7 @@ export class WriteBatch implements PublicWriteBatch { this._firestore ); parsed = parseUpdateData( - this._firestore._dataReader, + this._dataReader, 'WriteBatch.update', ref._key, fieldOrUpdateData @@ -870,7 +731,7 @@ export class WriteBatch implements PublicWriteBatch { this.verifyNotCommitted(); this._committed = true; if (this._mutations.length > 0) { - const client = ensureFirestoreConfigured(this._firestore); + const client = ensureFirestoreConfigured(this._firestore._delegate); return firestoreClientWrite(client, this._mutations); } @@ -891,24 +752,26 @@ export class WriteBatch implements PublicWriteBatch { /** * A reference to a particular document in a collection in the database. */ -export class DocumentReference +export class DocumentReference extends _DocumentKeyReference implements PublicDocumentReference { private _firestoreClient: FirestoreClient; + private _dataReader: UserDataReader; constructor( public _key: DocumentKey, readonly firestore: Firestore, - readonly _converter: FirestoreDataConverter | null + readonly _converter: PublicFirestoreDataConverter | null ) { super(firestore._databaseId, _key, _converter); - this._firestoreClient = ensureFirestoreConfigured(firestore); + this._firestoreClient = ensureFirestoreConfigured(firestore._delegate); + this._dataReader = newUserDataReader(firestore._delegate); } static forPath( path: ResourcePath, firestore: Firestore, - converter: FirestoreDataConverter | null + converter: PublicFirestoreDataConverter | null ): DocumentReference { if (path.length % 2 !== 0) { throw new FirestoreError( @@ -937,7 +800,9 @@ export class DocumentReference return this._key.path.canonicalString(); } - collection(pathString: string): PublicCollectionReference { + collection( + pathString: string + ): PublicCollectionReference { validateNonEmptyArgument( 'DocumentReference.collection', 'path', @@ -968,9 +833,9 @@ export class DocumentReference ); } - set(value: Partial, options: SetOptions): Promise; + set(value: Partial, options: PublicSetOptions): Promise; set(value: T): Promise; - set(value: T | Partial, options?: SetOptions): Promise { + set(value: T | Partial, options?: PublicSetOptions): Promise { options = validateSetOptions('DocumentReference.set', options); const convertedValue = applyFirestoreDataConverter( this._converter, @@ -978,7 +843,7 @@ export class DocumentReference options ); const parsed = parseSetData( - this.firestore._dataReader, + this._dataReader, 'DocumentReference.set', this._key, convertedValue, @@ -991,25 +856,31 @@ export class DocumentReference ); } - update(value: UpdateData): Promise; + update(value: PublicUpdateData): Promise; update( field: string | ExternalFieldPath, value: unknown, ...moreFieldsAndValues: unknown[] ): Promise; update( - fieldOrUpdateData: string | ExternalFieldPath | UpdateData, + fieldOrUpdateData: string | ExternalFieldPath | PublicUpdateData, value?: unknown, ...moreFieldsAndValues: unknown[] ): Promise { - let parsed; + // For Compat types, we have to "extract" the underlying types before + // performing validation. + if (fieldOrUpdateData instanceof Compat) { + fieldOrUpdateData = (fieldOrUpdateData as Compat<_BaseFieldPath>) + ._delegate; + } + let parsed; if ( typeof fieldOrUpdateData === 'string' || - fieldOrUpdateData instanceof ExternalFieldPath + fieldOrUpdateData instanceof _BaseFieldPath ) { parsed = parseUpdateVarargs( - this.firestore._dataReader, + this._dataReader, 'DocumentReference.update', this._key, fieldOrUpdateData, @@ -1018,7 +889,7 @@ export class DocumentReference ); } else { parsed = parseUpdateData( - this.firestore._dataReader, + this._dataReader, 'DocumentReference.update', this._key, fieldOrUpdateData @@ -1039,7 +910,7 @@ export class DocumentReference onSnapshot(observer: PartialObserver>): Unsubscribe; onSnapshot( - options: SnapshotListenOptions, + options: PublicSnapshotListenOptions, observer: PartialObserver> ): Unsubscribe; onSnapshot( @@ -1048,7 +919,7 @@ export class DocumentReference onCompletion?: CompleteFn ): Unsubscribe; onSnapshot( - options: SnapshotListenOptions, + options: PublicSnapshotListenOptions, onNext: NextFn>, onError?: ErrorFn, onCompletion?: CompleteFn @@ -1063,7 +934,7 @@ export class DocumentReference typeof args[currArg] === 'object' && !isPartialObserver(args[currArg]) ) { - options = args[currArg] as SnapshotListenOptions; + options = args[currArg] as PublicSnapshotListenOptions; currArg++; } @@ -1100,7 +971,7 @@ export class DocumentReference ); } - get(options?: GetOptions): Promise> { + get(options?: PublicGetOptions): Promise> { if (options && options.source === 'cache') { return firestoreClientGetDocumentFromLocalCache( this._firestoreClient, @@ -1126,7 +997,7 @@ export class DocumentReference } withConverter( - converter: FirestoreDataConverter + converter: PublicFirestoreDataConverter ): PublicDocumentReference { return new DocumentReference(this._key, this.firestore, converter); } @@ -1201,7 +1072,7 @@ export class SnapshotMetadata implements PublicSnapshotMetadata { */ export interface SnapshotOptions extends PublicSnapshotOptions {} -export class DocumentSnapshot +export class DocumentSnapshot implements PublicDocumentSnapshot { constructor( private _firestore: Firestore, @@ -1209,7 +1080,7 @@ export class DocumentSnapshot public _document: Document | null, private _fromCache: boolean, private _hasPendingWrites: boolean, - private readonly _converter: FirestoreDataConverter | null + private readonly _converter: PublicFirestoreDataConverter | null ) {} data(options: PublicSnapshotOptions = {}): T | undefined { @@ -1300,7 +1171,7 @@ export class DocumentSnapshot } } -export class QueryDocumentSnapshot +export class QueryDocumentSnapshot extends DocumentSnapshot implements PublicQueryDocumentSnapshot { data(options?: SnapshotOptions): T { @@ -1706,23 +1577,27 @@ export function validateHasExplicitOrderByForLimitToLast( } } -export class Query implements PublicQuery { +export class Query implements PublicQuery { + private _dataReader: UserDataReader; + constructor( public _query: InternalQuery, readonly firestore: Firestore, - protected readonly _converter: FirestoreDataConverter | null - ) {} + protected readonly _converter: PublicFirestoreDataConverter | null + ) { + this._dataReader = newUserDataReader(firestore._delegate); + } where( field: string | ExternalFieldPath, - opStr: WhereFilterOp, + opStr: PublicWhereFilterOp, value: unknown ): PublicQuery { const fieldPath = fieldPathFromArgument('Query.where', field); const filter = newQueryFilter( this._query, 'Query.where', - this.firestore._dataReader, + this._dataReader, this.firestore._databaseId, fieldPath, opStr as Operator, @@ -1737,7 +1612,7 @@ export class Query implements PublicQuery { orderBy( field: string | ExternalFieldPath, - directionStr?: OrderByDirection + directionStr?: PublicOrderByDirection ): PublicQuery { let direction: Direction; if (directionStr === undefined || directionStr === 'asc') { @@ -1857,7 +1732,7 @@ export class Query implements PublicQuery { ); } - withConverter(converter: FirestoreDataConverter): PublicQuery { + withConverter(converter: PublicFirestoreDataConverter): PublicQuery { return new Query(this._query, this.firestore, converter); } @@ -1881,7 +1756,7 @@ export class Query implements PublicQuery { return newQueryBoundFromFields( this._query, this.firestore._databaseId, - this.firestore._dataReader, + this._dataReader, methodName, allFields, before @@ -1891,7 +1766,7 @@ export class Query implements PublicQuery { onSnapshot(observer: PartialObserver>): Unsubscribe; onSnapshot( - options: SnapshotListenOptions, + options: PublicSnapshotListenOptions, observer: PartialObserver> ): Unsubscribe; onSnapshot( @@ -1900,7 +1775,7 @@ export class Query implements PublicQuery { onCompletion?: CompleteFn ): Unsubscribe; onSnapshot( - options: SnapshotListenOptions, + options: PublicSnapshotListenOptions, onNext: NextFn>, onError?: ErrorFn, onCompletion?: CompleteFn @@ -1913,7 +1788,7 @@ export class Query implements PublicQuery { typeof args[currArg] === 'object' && !isPartialObserver(args[currArg]) ) { - options = args[currArg] as SnapshotListenOptions; + options = args[currArg] as PublicSnapshotListenOptions; currArg++; } @@ -1945,14 +1820,14 @@ export class Query implements PublicQuery { }; validateHasExplicitOrderByForLimitToLast(this._query); - const client = ensureFirestoreConfigured(this.firestore); + const client = ensureFirestoreConfigured(this.firestore._delegate); return firestoreClientListen(client, this._query, options, observer); } - get(options?: GetOptions): Promise> { + get(options?: PublicGetOptions): Promise> { validateHasExplicitOrderByForLimitToLast(this._query); - const client = ensureFirestoreConfigured(this.firestore); + const client = ensureFirestoreConfigured(this.firestore._delegate); return (options && options.source === 'cache' ? firestoreClientGetDocumentsFromLocalCache(client, this._query) : firestoreClientGetDocumentsViaSnapshotListener( @@ -1967,7 +1842,8 @@ export class Query implements PublicQuery { } } -export class QuerySnapshot implements PublicQuerySnapshot { +export class QuerySnapshot + implements PublicQuerySnapshot { private _cachedChanges: Array> | null = null; private _cachedChangesIncludeMetadataChanges: boolean | null = null; @@ -1977,7 +1853,7 @@ export class QuerySnapshot implements PublicQuerySnapshot { private readonly _firestore: Firestore, private readonly _originalQuery: InternalQuery, private readonly _snapshot: ViewSnapshot, - private readonly _converter: FirestoreDataConverter | null + private readonly _converter: PublicFirestoreDataConverter | null ) { this.metadata = new SnapshotMetadata( _snapshot.hasPendingWrites, @@ -2019,7 +1895,9 @@ export class QuerySnapshot implements PublicQuerySnapshot { return new Query(this._originalQuery, this._firestore, this._converter); } - docChanges(options?: SnapshotListenOptions): Array> { + docChanges( + options?: PublicSnapshotListenOptions + ): Array> { if (options) { } @@ -2080,13 +1958,13 @@ export class QuerySnapshot implements PublicQuerySnapshot { } } -export class CollectionReference +export class CollectionReference extends Query implements PublicCollectionReference { constructor( readonly _path: ResourcePath, firestore: Firestore, - _converter: FirestoreDataConverter | null + _converter: PublicFirestoreDataConverter | null ) { super(newQueryForPath(_path), firestore, _converter); if (_path.length % 2 !== 1) { @@ -2103,12 +1981,12 @@ export class CollectionReference return this._query.path.lastSegment(); } - get parent(): PublicDocumentReference | null { + get parent(): PublicDocumentReference | null { const parentPath = this._query.path.popLast(); if (parentPath.isEmpty()) { return null; } else { - return new DocumentReference( + return new DocumentReference( new DocumentKey(parentPath), this.firestore, /* converter= */ null @@ -2152,7 +2030,7 @@ export class CollectionReference } withConverter( - converter: FirestoreDataConverter + converter: PublicFirestoreDataConverter ): PublicCollectionReference { return new CollectionReference(this._path, this.firestore, converter); } @@ -2193,7 +2071,7 @@ export function changesFromSnapshot( hasPendingWrite: boolean ) => DocSnap ): Array<{ - type: DocumentChangeType; + type: PublicDocumentChangeType; doc: DocSnap; oldIndex: number; newIndex: number; @@ -2219,7 +2097,7 @@ export function changesFromSnapshot( ); lastDoc = change.doc; return { - type: 'added' as DocumentChangeType, + type: 'added' as PublicDocumentChangeType, doc, oldIndex: -1, newIndex: index++ @@ -2255,7 +2133,7 @@ export function changesFromSnapshot( } } -function resultChangeType(type: ChangeType): DocumentChangeType { +function resultChangeType(type: ChangeType): PublicDocumentChangeType { switch (type) { case ChangeType.Added: return 'added'; @@ -2281,8 +2159,8 @@ function resultChangeType(type: ChangeType): DocumentChangeType { export function applyFirestoreDataConverter( converter: UntypedFirestoreDataConverter | null, value: T, - options?: SetOptions -): DocumentData { + options?: PublicSetOptions +): PublicDocumentData { let convertedValue; if (converter) { if (options && (options.merge || options.mergeFields)) { @@ -2294,11 +2172,7 @@ export function applyFirestoreDataConverter( convertedValue = converter.toFirestore(value); } } else { - convertedValue = value as DocumentData; + convertedValue = value as PublicDocumentData; } return convertedValue; } - -function contains(obj: object, key: string): obj is { key: unknown } { - return Object.prototype.hasOwnProperty.call(obj, key); -} diff --git a/packages/firestore/src/api/user_data_reader.ts b/packages/firestore/src/api/user_data_reader.ts index 8311e14627e..a574191d365 100644 --- a/packages/firestore/src/api/user_data_reader.ts +++ b/packages/firestore/src/api/user_data_reader.ts @@ -436,12 +436,14 @@ export function parseUpdateData( forEach(input as Dict, (key, value) => { const path = fieldPathFromDotSeparatedString(methodName, key, targetDoc); + // For Compat types, we have to "extract" the underlying types before + // performing validation. + if (value instanceof Compat) { + value = (value as Compat)._delegate; + } + const childContext = context.childContextForFieldPath(path); - if ( - value instanceof DeleteFieldValueImpl || - (value instanceof Compat && - value._delegate instanceof DeleteFieldValueImpl) - ) { + if (value instanceof DeleteFieldValueImpl) { // Add it to the field mask, but don't add anything to updateData. fieldMaskPaths.push(path); } else { @@ -504,13 +506,16 @@ export function parseUpdateVarargs( for (let i = keys.length - 1; i >= 0; --i) { if (!fieldMaskContains(fieldMaskPaths, keys[i])) { const path = keys[i]; - const value = values[i]; + let value = values[i]; + + // For Compat types, we have to "extract" the underlying types before + // performing validation. + if (value instanceof Compat) { + value = (value as Compat)._delegate; + } + const childContext = context.childContextForFieldPath(path); - if ( - value instanceof DeleteFieldValueImpl || - (value instanceof Compat && - value._delegate instanceof DeleteFieldValueImpl) - ) { + if (value instanceof DeleteFieldValueImpl) { // Add it to the field mask, but don't add anything to updateData. fieldMaskPaths.push(path); } else { @@ -792,9 +797,15 @@ function validatePlainObject( */ export function fieldPathFromArgument( methodName: string, - path: string | _BaseFieldPath, + path: string | _BaseFieldPath | Compat<_BaseFieldPath>, targetDoc?: DocumentKey ): FieldPath { + // If required, replace the FieldPath Compat class with with the firestore-exp + // FieldPath. + if (path instanceof Compat) { + path = (path as Compat<_BaseFieldPath>)._delegate; + } + if (path instanceof _BaseFieldPath) { return path._internalPath; } else if (typeof path === 'string') { diff --git a/packages/firestore/src/config.ts b/packages/firestore/src/config.ts index 8de2d9b9dca..bd4a0d31e3f 100644 --- a/packages/firestore/src/config.ts +++ b/packages/firestore/src/config.ts @@ -30,6 +30,8 @@ import { QuerySnapshot, Transaction, WriteBatch, + loadBundle, + namedQuery, setLogLevel } from './api/database'; import { Blob } from './api/blob'; @@ -54,6 +56,8 @@ const firestoreNamespace = { FieldPath, FieldValue, setLogLevel, + loadBundle, + namedQuery, CACHE_SIZE_UNLIMITED }; diff --git a/packages/firestore/src/core/database_info.ts b/packages/firestore/src/core/database_info.ts index 5d97ca1a97b..615bd6575fd 100644 --- a/packages/firestore/src/core/database_info.ts +++ b/packages/firestore/src/core/database_info.ts @@ -15,8 +15,6 @@ * limitations under the License. */ -import { primitiveComparator } from '../util/misc'; - export class DatabaseInfo { /** * Constructs a DatabaseInfo using the provided host, databaseId and @@ -63,11 +61,4 @@ export class DatabaseId { other.database === this.database ); } - - compareTo(other: DatabaseId): number { - return ( - primitiveComparator(this.projectId, other.projectId) || - primitiveComparator(this.database, other.database) - ); - } } diff --git a/packages/firestore/test/integration/api/bundle.test.ts b/packages/firestore/test/integration/api/bundle.test.ts index efe1fa11398..b3fb949d91a 100644 --- a/packages/firestore/test/integration/api/bundle.test.ts +++ b/packages/firestore/test/integration/api/bundle.test.ts @@ -24,8 +24,10 @@ import { withTestDb } from '../util/helpers'; import { EventsAccumulator } from '../util/events_accumulator'; +import * as firebaseExport from '../util/firebase_export'; -// TODO(b/162594908): Move this to api/ instead of api_internal. +const loadBundle = firebaseExport.loadBundle; +const namedQuery = firebaseExport.namedQuery; export const encoder = new TextEncoder(); @@ -95,7 +97,7 @@ apiDescribe('Bundles', (persistence: boolean) => { return withTestDb(persistence, async db => { const progressEvents: firestore.LoadBundleTaskProgress[] = []; let completeCalled = false; - const task: firestore.LoadBundleTask = db.loadBundle(bundleString(db)); + const task: firestore.LoadBundleTask = loadBundle(db, bundleString(db)); task.onProgress( progress => { progressEvents.push(progress); @@ -124,12 +126,12 @@ apiDescribe('Bundles', (persistence: boolean) => { let snap = await db.collection('coll-1').get({ source: 'cache' }); verifySnapEqualsTestDocs(snap); - snap = await (await db.namedQuery('limit'))!.get({ + snap = await (await namedQuery(db, 'limit'))!.get({ source: 'cache' }); expect(toDataArray(snap)).to.deep.equal([{ k: 'b', bar: 2 }]); - snap = await (await db.namedQuery('limit-to-last'))!.get({ + snap = await (await namedQuery(db, 'limit-to-last'))!.get({ source: 'cache' }); expect(toDataArray(snap)).to.deep.equal([{ k: 'a', bar: 1 }]); @@ -138,7 +140,8 @@ apiDescribe('Bundles', (persistence: boolean) => { it('load with documents and queries with promise interface', () => { return withTestDb(persistence, async db => { - const fulfillProgress: firestore.LoadBundleTaskProgress = await db.loadBundle( + const fulfillProgress: firestore.LoadBundleTaskProgress = await loadBundle( + db, bundleString(db) ); @@ -153,11 +156,12 @@ apiDescribe('Bundles', (persistence: boolean) => { it('load for a second time skips', () => { return withTestDb(persistence, async db => { - await db.loadBundle(bundleString(db)); + await loadBundle(db, bundleString(db)); let completeCalled = false; const progressEvents: firestore.LoadBundleTaskProgress[] = []; - const task: firestore.LoadBundleTask = db.loadBundle( + const task: firestore.LoadBundleTask = loadBundle( + db, encoder.encode(bundleString(db)) ); task.onProgress( @@ -193,7 +197,8 @@ apiDescribe('Bundles', (persistence: boolean) => { db.collection('coll-1').onSnapshot(accumulator.storeEvent); await accumulator.awaitEvent(); - const progress = await db.loadBundle( + const progress = await loadBundle( + db, // Testing passing in non-string bundles. encoder.encode(bundleString(db)) ); @@ -204,17 +209,18 @@ apiDescribe('Bundles', (persistence: boolean) => { // cache can only be tested in spec tests. await accumulator.assertNoAdditionalEvents(); - let snap = await (await db.namedQuery('limit'))!.get(); + let snap = await (await namedQuery(db, 'limit'))!.get(); expect(toDataArray(snap)).to.deep.equal([{ k: 'b', bar: 0 }]); - snap = await (await db.namedQuery('limit-to-last'))!.get(); + snap = await (await namedQuery(db, 'limit-to-last'))!.get(); expect(toDataArray(snap)).to.deep.equal([{ k: 'a', bar: 0 }]); }); }); it('loaded documents should not be GC-ed right away', () => { return withTestDb(persistence, async db => { - const fulfillProgress: firestore.LoadBundleTaskProgress = await db.loadBundle( + const fulfillProgress: firestore.LoadBundleTaskProgress = await loadBundle( + db, bundleString(db) ); @@ -235,12 +241,12 @@ apiDescribe('Bundles', (persistence: boolean) => { it('load with documents from other projects fails', () => { return withTestDb(persistence, async db => { return withAlternateTestDb(persistence, async otherDb => { - await expect(otherDb.loadBundle(bundleString(db))).to.be.rejectedWith( + await expect(loadBundle(otherDb, bundleString(db))).to.be.rejectedWith( 'Tried to deserialize key from different project' ); // Verify otherDb still functions, despite loaded a problematic bundle. - const finalProgress = await otherDb.loadBundle(bundleString(otherDb)); + const finalProgress = await loadBundle(otherDb, bundleString(otherDb)); verifySuccessProgress(finalProgress); // Read from cache. These documents do not exist in backend, so they can diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 4f6a14b27c6..d88b4e33a71 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -37,7 +37,6 @@ import { DEFAULT_SETTINGS, DEFAULT_PROJECT_ID } from '../util/settings'; use(chaiAsPromised); const newTestFirestore = firebaseExport.newTestFirestore; -const usesFunctionalApi = firebaseExport.usesFunctionalApi; const Timestamp = firebaseExport.Timestamp; const FieldPath = firebaseExport.FieldPath; const FieldValue = firebaseExport.FieldValue; @@ -1198,15 +1197,7 @@ apiDescribe('Database', (persistence: boolean) => { const expectedError = 'Persistence can only be cleared before a Firestore instance is ' + 'initialized or after it is terminated.'; - if (usesFunctionalApi()) { - // The modular API throws an exception rather than rejecting the - // Promise, which matches our overall handling of API call violations. - expect(() => firestore.clearPersistence()).to.throw(expectedError); - } else { - await expect( - firestore.clearPersistence() - ).to.eventually.be.rejectedWith(expectedError); - } + expect(() => firestore.clearPersistence()).to.throw(expectedError); }); } ); diff --git a/packages/firestore/test/integration/api/type.test.ts b/packages/firestore/test/integration/api/type.test.ts index d3041489b0c..92f1c0c2462 100644 --- a/packages/firestore/test/integration/api/type.test.ts +++ b/packages/firestore/test/integration/api/type.test.ts @@ -94,7 +94,11 @@ apiDescribe('Firestore', (persistence: boolean) => { }) .then(docSnapshot => { const blob = docSnapshot.data()!['bytes']; - expect(blob instanceof Blob).to.equal(true); + // TODO(firestorexp): As part of the Compat migration, the SDK + // should re-wrap the firestore-exp types into the Compat API. + // Comment this change back in once this is complete (note that this + // check passes in the legacy API). + // expect(blob instanceof Blob).to.equal(true); expect(blob.toUint8Array()).to.deep.equal( new Uint8Array([0, 1, 255]) ); diff --git a/packages/firestore/test/integration/api/validation.test.ts b/packages/firestore/test/integration/api/validation.test.ts index a5f7beff894..1449b0603ca 100644 --- a/packages/firestore/test/integration/api/validation.test.ts +++ b/packages/firestore/test/integration/api/validation.test.ts @@ -31,7 +31,6 @@ import { ALT_PROJECT_ID, DEFAULT_PROJECT_ID } from '../util/settings'; const FieldPath = firebaseExport.FieldPath; const FieldValue = firebaseExport.FieldValue; const newTestFirestore = firebaseExport.newTestFirestore; -const usesFunctionalApi = firebaseExport.usesFunctionalApi; // We're using 'as any' to pass invalid values to APIs for testing purposes. /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -117,24 +116,14 @@ apiDescribe('Validation:', (persistence: boolean) => { persistence, 'disallows changing settings after use', async db => { - let errorMsg = - 'Firestore has already been started and its settings can no ' + - 'longer be changed. '; - - if (usesFunctionalApi()) { - errorMsg += - 'initializeFirestore() cannot be called after calling ' + - 'getFirestore()'; - } else { - errorMsg += - 'You can only modify settings before calling any other ' + - 'methods on a Firestore object.'; - } - await db.doc('foo/bar').set({}); expect(() => db.settings({ host: 'something-else.example.com' }) - ).to.throw(errorMsg); + ).to.throw( + 'Firestore has already been started and its settings can no ' + + 'longer be changed. You can only modify settings before calling any other ' + + 'methods on a Firestore object.' + ); } ); @@ -182,12 +171,8 @@ apiDescribe('Validation:', (persistence: boolean) => { } expect(() => db.enablePersistence()).to.throw( 'Firestore has already been started and persistence can no ' + - `longer be enabled. You can only ${ - usesFunctionalApi() - ? 'enable persistence' - : 'call enablePersistence()' - } ` + - 'before calling any other methods on a Firestore object.' + 'longer be enabled. You can only enable persistence before ' + + 'calling any other methods on a Firestore object.' ); } ); @@ -211,23 +196,13 @@ apiDescribe('Validation:', (persistence: boolean) => { describe('Collection paths', () => { validationIt(persistence, 'must be non-empty strings', db => { const baseDocRef = db.doc('foo/bar'); - - if (usesFunctionalApi()) { - expect(() => db.collection('')).to.throw( - 'Function collection() cannot be called with an empty path.' - ); - expect(() => baseDocRef.collection('')).to.throw( - 'Function collection() cannot be called with an empty path.' - ); - } else { - expect(() => db.collection('')).to.throw( - 'Function Firestore.collection() cannot be called with an empty path.' - ); - expect(() => baseDocRef.collection('')).to.throw( - 'Function DocumentReference.collection() cannot be called with an ' + - 'empty path.' - ); - } + expect(() => db.collection('')).to.throw( + 'Function Firestore.collection() cannot be called with an empty path.' + ); + expect(() => baseDocRef.collection('')).to.throw( + 'Function DocumentReference.collection() cannot be called with an ' + + 'empty path.' + ); }); validationIt(persistence, 'must be odd-length', db => { @@ -271,22 +246,13 @@ apiDescribe('Validation:', (persistence: boolean) => { validationIt(persistence, 'must be strings', db => { const baseCollectionRef = db.collection('foo'); - if (usesFunctionalApi()) { - expect(() => db.doc('')).to.throw( - 'Function doc() cannot be called with an empty path.' - ); - expect(() => baseCollectionRef.doc('')).to.throw( - 'Function doc() cannot be called with an empty path.' - ); - } else { - expect(() => db.doc('')).to.throw( - 'Function Firestore.doc() cannot be called with an empty path.' - ); - expect(() => baseCollectionRef.doc('')).to.throw( - 'Function CollectionReference.doc() cannot be called with an empty ' + - 'path.' - ); - } + expect(() => db.doc('')).to.throw( + 'Function Firestore.doc() cannot be called with an empty path.' + ); + expect(() => baseCollectionRef.doc('')).to.throw( + 'Function CollectionReference.doc() cannot be called with an empty ' + + 'path.' + ); }); validationIt(persistence, 'must be even-length', db => { @@ -623,9 +589,7 @@ apiDescribe('Validation:', (persistence: boolean) => { expect(() => collection.where('test', '==', { test: FieldValue.arrayUnion(1) }) ).to.throw( - `Function ${ - usesFunctionalApi() ? 'where' : 'Query.where' - }() called with invalid data. ` + + 'Function Query.where() called with invalid data. ' + 'FieldValue.arrayUnion() can only be used with update() and set() ' + '(found in field test)' ); @@ -633,9 +597,7 @@ apiDescribe('Validation:', (persistence: boolean) => { expect(() => collection.where('test', '==', { test: FieldValue.arrayRemove(1) }) ).to.throw( - `Function ${ - usesFunctionalApi() ? 'where' : 'Query.where' - }() called with invalid data. ` + + 'Function Query.where() called with invalid data. ' + 'FieldValue.arrayRemove() can only be used with update() and set() ' + '(found in field test)' ); @@ -688,10 +650,9 @@ apiDescribe('Validation:', (persistence: boolean) => { expect(() => collection.where('test', '==', { test: FieldValue.increment(1) }) ).to.throw( - `Function ${ - usesFunctionalApi() ? 'where' : 'Query.where' - }() called with invalid data. FieldValue.increment() can only be ` + - 'used with update() and set() (found in field test)' + 'Function Query.where() called with invalid data. ' + + 'FieldValue.increment() can only be used with update() and set() ' + + '(found in field test)' ); }); }); @@ -700,14 +661,10 @@ apiDescribe('Validation:', (persistence: boolean) => { validationIt(persistence, 'with non-positive limit fail', db => { const collection = db.collection('test'); expect(() => collection.limit(0)).to.throw( - `Function ${ - usesFunctionalApi() ? 'limit' : 'Query.limit' - }() requires a positive number, but it was: 0.` + `Function Query.limit() requires a positive number, but it was: 0.` ); expect(() => collection.limitToLast(-1)).to.throw( - `Function ${ - usesFunctionalApi() ? 'limitToLast' : 'Query.limitToLast' - }() requires a positive number, but it was: -1.` + `Function Query.limitToLast() requires a positive number, but it was: -1.` ); }); @@ -798,10 +755,9 @@ apiDescribe('Validation:', (persistence: boolean) => { const collection = db.collection('collection'); const query = collection.orderBy('foo'); const reason = - `Too many arguments provided to ${ - usesFunctionalApi() ? 'startAt' : 'Query.startAt' - }(). The number of arguments must be less than or equal to the ` + - `number of orderBy() clauses`; + 'Too many arguments provided to Query.startAt(). The number of ' + + 'arguments must be less than or equal to the number of orderBy() ' + + 'clauses'; expect(() => query.startAt(1, 2)).to.throw(reason); expect(() => query.orderBy('bar').startAt(1, 2, 3)).to.throw(reason); } @@ -819,22 +775,18 @@ apiDescribe('Validation:', (persistence: boolean) => { .orderBy(FieldPath.documentId()); expect(() => query.startAt(1)).to.throw( 'Invalid query. Expected a string for document ID in ' + - `${ - usesFunctionalApi() ? 'startAt' : 'Query.startAt' - }(), but got a number` + 'Query.startAt(), but got a number' ); expect(() => query.startAt('foo/bar')).to.throw( - `Invalid query. When querying a collection and ordering by FieldPath.documentId(), ` + - `the value passed to ${ - usesFunctionalApi() ? 'startAt' : 'Query.startAt' - }() must be a plain document ID, but 'foo/bar' contains a slash.` + 'Invalid query. When querying a collection and ordering by ' + + 'FieldPath.documentId(), the value passed to Query.startAt() ' + + "must be a plain document ID, but 'foo/bar' contains a slash." ); expect(() => cgQuery.startAt('foo')).to.throw( - `Invalid query. When querying a collection group and ordering by ` + - `FieldPath.documentId(), the value passed to ${ - usesFunctionalApi() ? 'startAt' : 'Query.startAt' - }() must result in a valid document path, but 'foo' is not because ` + - `it contains an odd number of segments.` + 'Invalid query. When querying a collection group and ordering by ' + + 'FieldPath.documentId(), the value passed to Query.startAt() ' + + "must result in a valid document path, but 'foo' is not because " + + 'it contains an odd number of segments.' ); } ); @@ -1314,21 +1266,12 @@ apiDescribe('Validation:', (persistence: boolean) => { validationIt(persistence, 'cannot pass undefined as a field value', db => { const collection = db.collection('test'); - if (usesFunctionalApi()) { - expect(() => collection.where('foo', '==', undefined)).to.throw( - 'Function where() called with invalid data. Unsupported field value: undefined' - ); - expect(() => collection.orderBy('foo').startAt(undefined)).to.throw( - 'Function startAt() called with invalid data. Unsupported field value: undefined' - ); - } else { - expect(() => collection.where('foo', '==', undefined)).to.throw( - 'Function Query.where() called with invalid data. Unsupported field value: undefined' - ); - expect(() => collection.orderBy('foo').startAt(undefined)).to.throw( - 'Function Query.startAt() called with invalid data. Unsupported field value: undefined' - ); - } + expect(() => collection.where('foo', '==', undefined)).to.throw( + 'Function Query.where() called with invalid data. Unsupported field value: undefined' + ); + expect(() => collection.orderBy('foo').startAt(undefined)).to.throw( + 'Function Query.startAt() called with invalid data. Unsupported field value: undefined' + ); }); }); }); @@ -1391,9 +1334,7 @@ function expectWriteToFail( `Function ${fnName}() called with invalid data. ${reason}`; if (includeSets) { - expect(() => docRef.set(data)).to.throw( - error(usesFunctionalApi() ? 'setDoc' : 'DocumentReference.set') - ); + expect(() => docRef.set(data)).to.throw(error('DocumentReference.set')); expect(() => docRef.firestore.batch().set(docRef, data)).to.throw( error('WriteBatch.set') ); @@ -1401,7 +1342,7 @@ function expectWriteToFail( if (includeUpdates) { expect(() => docRef.update(data)).to.throw( - error(usesFunctionalApi() ? 'updateDoc' : 'DocumentReference.update') + error('DocumentReference.update') ); expect(() => docRef.firestore.batch().update(docRef, data)).to.throw( error('WriteBatch.update') @@ -1444,14 +1385,10 @@ function expectFieldPathToFail( // <=, etc omitted for brevity since the code path is trivially // shared. expect(() => coll.where(path, '==', 1)).to.throw( - `Function ${ - usesFunctionalApi() ? 'where' : 'Query.where' - }() called with invalid data. ` + reason + `Function Query.where() called with invalid data. ` + reason ); expect(() => coll.orderBy(path)).to.throw( - `Function ${ - usesFunctionalApi() ? 'orderBy' : 'Query.orderBy' - }() called with invalid data. ` + reason + `Function Query.orderBy() called with invalid data. ` + reason ); // Update paths. diff --git a/packages/firestore/test/integration/api_internal/database.test.ts b/packages/firestore/test/integration/api_internal/database.test.ts index 41ea6caad86..8cb7de15024 100644 --- a/packages/firestore/test/integration/api_internal/database.test.ts +++ b/packages/firestore/test/integration/api_internal/database.test.ts @@ -75,7 +75,7 @@ apiDescribe('Database (with internal API)', (persistence: boolean) => { await app.delete(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((docRef.firestore as any)._terminated).to.be.true; + expect((docRef.firestore as any)._delegate._terminated).to.be.true; }); }); }); diff --git a/packages/firestore/test/integration/util/firebase_export.ts b/packages/firestore/test/integration/util/firebase_export.ts index 7fe12a45491..722d2d90c5e 100644 --- a/packages/firestore/test/integration/util/firebase_export.ts +++ b/packages/firestore/test/integration/util/firebase_export.ts @@ -29,25 +29,24 @@ import * as firestore from '@firebase/firestore-types'; import firebase from '@firebase/app'; -import * as exp from '../../../exp/test/shim'; -import { - FirebaseApp as FirebaseAppShim, - FirebaseFirestore as FirestoreShim -} from '../../../exp/test/shim'; -import { - getFirestore, - initializeFirestore -} from '../../../exp/src/api/database'; // eslint-disable-next-line import/no-extraneous-dependencies -import { initializeApp } from '@firebase/app-exp'; +import firebaseAppCompat from '@firebase/app-compat'; + +import * as exp from '../../../exp/test/shim'; +import { FieldValue } from '../../../src/compat/field_value'; import { FirebaseApp } from '@firebase/app-types'; +import { + Firestore, + IndexedDbPersistenceProvider +} from '../../../src/api/database'; +import { getFirestore } from '../../../exp/src/api/database'; /** * Detects whether we are running against the functionial (tree-shakeable) * Firestore API. Used to exclude some tests, e.g. those that validate invalid * TypeScript input. */ -export function usesFunctionalApi(): boolean { +function usesFunctionalApi(): boolean { // Use the firebase namespace to detect if `firebase.firestore` has been // registered, which is only registered in the classic version of Firestore. return !('firestore' in firebase); @@ -63,37 +62,51 @@ let appCount = 0; */ export function newTestFirestore( projectId: string, - nameOrApp?: string | FirebaseApp | FirebaseAppShim, + nameOrApp?: string | FirebaseApp, settings?: firestore.Settings ): firestore.FirebaseFirestore { if (nameOrApp === undefined) { nameOrApp = 'test-app-' + appCount++; } + let firestore: firestore.FirebaseFirestore; if (usesFunctionalApi()) { const app = typeof nameOrApp === 'string' - ? initializeApp({ apiKey: 'fake-api-key', projectId }, nameOrApp) - : (nameOrApp as FirebaseAppShim)._delegate; - const firestore = settings - ? initializeFirestore(app, settings) - : getFirestore(app); - return new FirestoreShim(firestore); + ? firebaseAppCompat.initializeApp( + { + apiKey: 'fake-api-key', + projectId + }, + nameOrApp + ) + : nameOrApp; + + firestore = new Firestore( + app, + getFirestore(app), + new IndexedDbPersistenceProvider() + ); } else { const app = typeof nameOrApp === 'string' ? firebase.initializeApp( - { apiKey: 'fake-api-key', projectId }, + { + apiKey: 'fake-api-key', + projectId + }, nameOrApp ) : nameOrApp; + // eslint-disable-next-line @typescript-eslint/no-explicit-any - const firestore = (firebase as any).firestore(app); - if (settings) { - firestore.settings(settings); - } - return firestore; + firestore = (firebase as any).firestore(app); + } + + if (settings) { + firestore.settings(settings); } + return firestore; } // We only register firebase.firestore if the tests are run against the @@ -102,9 +115,6 @@ export function newTestFirestore( // eslint-disable-next-line @typescript-eslint/no-explicit-any const legacyNamespace = (firebase as any).firestore; -const Firestore = usesFunctionalApi() - ? exp.FirebaseFirestore - : legacyNamespace.FirebaseFirestore; const FieldPath = usesFunctionalApi() ? exp.FieldPath : legacyNamespace.FieldPath; @@ -112,9 +122,21 @@ const Timestamp = usesFunctionalApi() ? exp.Timestamp : legacyNamespace.Timestamp; const GeoPoint = usesFunctionalApi() ? exp.GeoPoint : legacyNamespace.GeoPoint; -const FieldValue = usesFunctionalApi() - ? exp.FieldValue - : legacyNamespace.FieldValue; const Blob = usesFunctionalApi() ? exp.Blob : legacyNamespace.Blob; +const loadBundle = usesFunctionalApi() + ? exp.loadBundle + : legacyNamespace.loadBundle; +const namedQuery = usesFunctionalApi() + ? exp.namedQuery + : legacyNamespace.namedQuery; -export { Firestore, FieldValue, FieldPath, Timestamp, Blob, GeoPoint }; +export { + Firestore, + FieldValue, + FieldPath, + Timestamp, + Blob, + GeoPoint, + loadBundle, + namedQuery +}; diff --git a/packages/firestore/test/integration/util/internal_helpers.ts b/packages/firestore/test/integration/util/internal_helpers.ts index 7ccb25513a9..10ec071d204 100644 --- a/packages/firestore/test/integration/util/internal_helpers.ts +++ b/packages/firestore/test/integration/util/internal_helpers.ts @@ -38,7 +38,7 @@ import { collectionReference } from '../../util/api_helpers'; /** Helper to retrieve the AsyncQueue for a give FirebaseFirestore instance. */ export function asyncQueue(db: firestore.FirebaseFirestore): AsyncQueue { - return (db as Firestore)._queue; + return (db as Firestore)._delegate._queue; } export function getDefaultDatabaseInfo(): DatabaseInfo { diff --git a/packages/firestore/test/unit/api/database.test.ts b/packages/firestore/test/unit/api/database.test.ts index f8bc606c6ec..56a046904b7 100644 --- a/packages/firestore/test/unit/api/database.test.ts +++ b/packages/firestore/test/unit/api/database.test.ts @@ -163,16 +163,18 @@ describe('Settings', () => { db.settings({ host: 'other.host' }); db.settings({ ignoreUndefinedProperties: true }); - expect(db._getSettings().ignoreUndefinedProperties).to.be.true; + expect(db._delegate._getSettings().ignoreUndefinedProperties).to.be.true; // Expect host to be replaced with default host. - expect(db._getSettings().host).to.equal('firestore.googleapis.com'); + expect(db._delegate._getSettings().host).to.equal( + 'firestore.googleapis.com' + ); }); it('can not use mutually exclusive settings together', () => { // Use a new instance of Firestore in order to configure settings. const db = newTestFirestore(); - expect( - db.settings.bind(db.settings, { + expect(() => + db.settings({ experimentalForceLongPolling: true, experimentalAutoDetectLongPolling: true }) @@ -190,8 +192,21 @@ describe('Settings', () => { merge: true }); - expect(db._getSettings().ignoreUndefinedProperties).to.be.true; - expect(db._getSettings().host).to.equal('other.host'); + expect(db._delegate._getSettings().ignoreUndefinedProperties).to.be.true; + expect(db._delegate._getSettings().host).to.equal('other.host'); + }); + + it('can use `merge` without previous call to settings()', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + db.settings({ host: 'other.host' }); + db.settings({ + ignoreUndefinedProperties: true, + merge: true + }); + + expect(db._delegate._getSettings().ignoreUndefinedProperties).to.be.true; + expect(db._delegate._getSettings().host).to.equal('other.host'); }); it('gets settings from useEmulator', () => { @@ -199,8 +214,8 @@ describe('Settings', () => { const db = newTestFirestore(); db.useEmulator('localhost', 9000); - expect(db._getSettings().host).to.equal('localhost:9000'); - expect(db._getSettings().ssl).to.be.false; + expect(db._delegate._getSettings().host).to.equal('localhost:9000'); + expect(db._delegate._getSettings().ssl).to.be.false; }); it('prefers host from useEmulator to host from settings', () => { @@ -209,7 +224,7 @@ describe('Settings', () => { db.settings({ host: 'other.host' }); db.useEmulator('localhost', 9000); - expect(db._getSettings().host).to.equal('localhost:9000'); - expect(db._getSettings().ssl).to.be.false; + expect(db._delegate._getSettings().host).to.equal('localhost:9000'); + expect(db._delegate._getSettings().ssl).to.be.false; }); }); diff --git a/packages/firestore/test/util/api_helpers.ts b/packages/firestore/test/util/api_helpers.ts index 150337062f5..b55113849a7 100644 --- a/packages/firestore/test/util/api_helpers.ts +++ b/packages/firestore/test/util/api_helpers.ts @@ -41,43 +41,38 @@ import { JsonObject } from '../../src/model/object_value'; import { doc, key, path as pathFrom } from './helpers'; import { Provider, ComponentContainer } from '@firebase/component'; import { TEST_PROJECT } from '../unit/local/persistence_test_helpers'; +import { FirebaseFirestore } from '../../exp/src/api/database'; +import { DatabaseId } from '../../src/core/database_info'; /** * A mock Firestore. Will not work for integration test. */ -export const FIRESTORE = new Firestore( - { - projectId: TEST_PROJECT, - database: '(default)' - }, - new Provider('auth-internal', new ComponentContainer('default')), - new IndexedDbPersistenceProvider() -); +export const FIRESTORE = newTestFirestore(TEST_PROJECT); export function firestore(): Firestore { return FIRESTORE; } -export function newTestFirestore(): Firestore { +export function newTestFirestore(projectId = 'new-project'): Firestore { return new Firestore( - { - projectId: 'new-project', - database: '(default)' - }, - new Provider('auth-internal', new ComponentContainer('default')), + new DatabaseId(projectId), + new FirebaseFirestore( + new DatabaseId(projectId), + new Provider('auth-internal', new ComponentContainer('default')) + ), new IndexedDbPersistenceProvider() ); } export function collectionReference(path: string): CollectionReference { const db = firestore(); - ensureFirestoreConfigured(db); + ensureFirestoreConfigured(db._delegate); return new CollectionReference(pathFrom(path), db, /* converter= */ null); } export function documentReference(path: string): DocumentReference { const db = firestore(); - ensureFirestoreConfigured(db); + ensureFirestoreConfigured(db._delegate); return new DocumentReference(key(path), db, /* converter= */ null); } diff --git a/packages/functions-types/package.json b/packages/functions-types/package.json index 43be179ce39..cd1ebb41956 100644 --- a/packages/functions-types/package.json +++ b/packages/functions-types/package.json @@ -20,6 +20,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages/functions/CHANGELOG.md b/packages/functions/CHANGELOG.md index e9c129b0ae5..977167b621b 100644 --- a/packages/functions/CHANGELOG.md +++ b/packages/functions/CHANGELOG.md @@ -1,5 +1,12 @@ # @firebase/functions +## 0.6.1 + +### Patch Changes + +- Updated dependencies []: + - @firebase/component@0.1.21 + ## 0.6.0 ### Minor Changes diff --git a/packages/functions/package.json b/packages/functions/package.json index 999274b9b3e..9fe61c3c60e 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/functions", - "version": "0.6.0", + "version": "0.6.1", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", @@ -29,11 +29,11 @@ "@firebase/app-types": "0.x" }, "devDependencies": { - "@firebase/app": "0.6.12", - "@firebase/messaging": "0.7.2", - "rollup": "2.29.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "@firebase/app": "0.6.13", + "@firebase/messaging": "0.7.3", + "rollup": "2.33.1", + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages/functions", @@ -45,7 +45,7 @@ }, "typings": "dist/index.d.ts", "dependencies": { - "@firebase/component": "0.1.20", + "@firebase/component": "0.1.21", "@firebase/functions-types": "0.4.0", "@firebase/messaging-types": "0.5.0", "node-fetch": "2.6.1", diff --git a/packages/installations-types/package.json b/packages/installations-types/package.json index aa3bb15d818..691ddfad54e 100644 --- a/packages/installations-types/package.json +++ b/packages/installations-types/package.json @@ -23,6 +23,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages/installations/CHANGELOG.md b/packages/installations/CHANGELOG.md index 40c4fa57a2b..669897b68df 100644 --- a/packages/installations/CHANGELOG.md +++ b/packages/installations/CHANGELOG.md @@ -1,5 +1,13 @@ # @firebase/installations +## 0.4.19 + +### Patch Changes + +- Updated dependencies [[`9cf727fcc`](https://github.com/firebase/firebase-js-sdk/commit/9cf727fcc3d049551b16ae0698ac33dc2fe45ada)]: + - @firebase/util@0.3.4 + - @firebase/component@0.1.21 + ## 0.4.18 ### Patch Changes diff --git a/packages/installations/package.json b/packages/installations/package.json index ca68d7bd7bb..215dde5c033 100644 --- a/packages/installations/package.json +++ b/packages/installations/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/installations", - "version": "0.4.18", + "version": "0.4.19", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", "esm2017": "dist/index.esm2017.js", @@ -30,14 +30,14 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "@firebase/app": "0.6.12", - "rollup": "2.29.0", + "@firebase/app": "0.6.13", + "rollup": "2.33.1", "@rollup/plugin-commonjs": "15.1.0", "@rollup/plugin-json": "4.1.0", "@rollup/plugin-node-resolve": "9.0.0", - "rollup-plugin-typescript2": "0.27.3", + "rollup-plugin-typescript2": "0.29.0", "rollup-plugin-uglify": "6.0.4", - "typescript": "4.0.2" + "typescript": "4.0.5" }, "peerDependencies": { "@firebase/app": "0.x", @@ -45,8 +45,8 @@ }, "dependencies": { "@firebase/installations-types": "0.3.4", - "@firebase/util": "0.3.3", - "@firebase/component": "0.1.20", + "@firebase/util": "0.3.4", + "@firebase/component": "0.1.21", "idb": "3.0.2", "tslib": "^1.11.1" } diff --git a/packages/logger/package.json b/packages/logger/package.json index 7f6a57d43a2..007cbb41af4 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -23,9 +23,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "rollup": "2.29.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "rollup": "2.33.1", + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages/logger", diff --git a/packages/messaging-types/package.json b/packages/messaging-types/package.json index 2ac0f45fe8f..907b823dccd 100644 --- a/packages/messaging-types/package.json +++ b/packages/messaging-types/package.json @@ -23,6 +23,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages/messaging/CHANGELOG.md b/packages/messaging/CHANGELOG.md index a4fc93fcf8d..0e54880e903 100644 --- a/packages/messaging/CHANGELOG.md +++ b/packages/messaging/CHANGELOG.md @@ -1,5 +1,16 @@ # @firebase/messaging +## 0.7.3 + +### Patch Changes + +- [`54a46f89c`](https://github.com/firebase/firebase-js-sdk/commit/54a46f89c1c45435c76412fa2ed296e986c2f6ab) [#3780](https://github.com/firebase/firebase-js-sdk/pull/3780) - Adds a timeout for `onBackgroundMessage` hook so that silent-push warnings won't show if `showNotification` is called inside the hook within 1s. + This fixes the issue where the silent-push warning is displayed along with the message shown with [ServiceWorkerRegistration.showNotification](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification). +- Updated dependencies [[`9cf727fcc`](https://github.com/firebase/firebase-js-sdk/commit/9cf727fcc3d049551b16ae0698ac33dc2fe45ada)]: + - @firebase/util@0.3.4 + - @firebase/component@0.1.21 + - @firebase/installations@0.4.19 + ## 0.7.2 ### Patch Changes diff --git a/packages/messaging/package.json b/packages/messaging/package.json index 80b3b22bc05..5662804b30d 100644 --- a/packages/messaging/package.json +++ b/packages/messaging/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/messaging", - "version": "0.7.2", + "version": "0.7.3", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", @@ -26,19 +26,19 @@ "@firebase/app-types": "0.x" }, "dependencies": { - "@firebase/installations": "0.4.18", + "@firebase/installations": "0.4.19", "@firebase/messaging-types": "0.5.0", - "@firebase/util": "0.3.3", - "@firebase/component": "0.1.20", + "@firebase/util": "0.3.4", + "@firebase/component": "0.1.21", "idb": "3.0.2", "tslib": "^1.11.1" }, "devDependencies": { - "@firebase/app": "0.6.12", - "rollup": "2.29.0", - "rollup-plugin-typescript2": "0.27.3", - "ts-essentials": "7.0.0", - "typescript": "4.0.2" + "@firebase/app": "0.6.13", + "rollup": "2.33.1", + "rollup-plugin-typescript2": "0.29.0", + "ts-essentials": "7.0.1", + "typescript": "4.0.5" }, "repository": { "directory": "packages/messaging", diff --git a/packages/messaging/src/controllers/sw-controller.ts b/packages/messaging/src/controllers/sw-controller.ts index 86a4d05af54..c4b9b7a024e 100644 --- a/packages/messaging/src/controllers/sw-controller.ts +++ b/packages/messaging/src/controllers/sw-controller.ts @@ -15,7 +15,13 @@ * limitations under the License. */ -import { DEFAULT_VAPID_KEY, FCM_MSG, TAG } from '../util/constants'; +import { + BACKGROUND_HANDLE_EXECUTION_TIME_LIMIT_MS, + DEFAULT_VAPID_KEY, + FCM_MSG, + FOREGROUND_HANDLE_PREPARATION_TIME_MS, + TAG +} from '../util/constants'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; import { FirebaseMessaging, MessagePayload } from '@firebase/messaging-types'; import { @@ -47,8 +53,8 @@ export class SwController implements FirebaseMessaging, FirebaseService { private isOnBackgroundMessageUsed: boolean | null = null; private vapidKey: string | null = null; private bgMessageHandler: - | BgMessageHandler | null + | BgMessageHandler | NextFn | Observer = null; @@ -211,6 +217,9 @@ export class SwController implements FirebaseMessaging, FirebaseService { this.bgMessageHandler.next(payload); } } + + // wait briefly to allow onBackgroundMessage to complete + await sleep(BACKGROUND_HANDLE_EXECUTION_TIME_LIMIT_MS); } async onSubChange(event: PushSubscriptionChangeEvent): Promise { @@ -267,7 +276,7 @@ export class SwController implements FirebaseMessaging, FirebaseService { // Wait three seconds for the client to initialize and set up the message handler so that it // can receive the message. - await sleep(3000); + await sleep(FOREGROUND_HANDLE_PREPARATION_TIME_MS); } else { client = await client.focus(); } diff --git a/packages/messaging/src/util/constants.ts b/packages/messaging/src/util/constants.ts index 4bafce33b0a..3a10bab3361 100644 --- a/packages/messaging/src/util/constants.ts +++ b/packages/messaging/src/util/constants.ts @@ -23,12 +23,21 @@ export const DEFAULT_VAPID_KEY = export const ENDPOINT = 'https://fcmregistrations.googleapis.com/v1'; -/** Key of FCM Payload in Notification's data field. */ +// Key of FCM Payload in Notification's data field. export const FCM_MSG = 'FCM_MSG'; +export const TAG = 'FirebaseMessaging: '; +// Set to '1' if Analytics is enabled for the campaign +export const CONSOLE_CAMPAIGN_ANALYTICS_ENABLED = 'google.c.a.e'; export const CONSOLE_CAMPAIGN_ID = 'google.c.a.c_id'; -export const CONSOLE_CAMPAIGN_NAME = 'google.c.a.c_l'; export const CONSOLE_CAMPAIGN_TIME = 'google.c.a.ts'; -/** Set to '1' if Analytics is enabled for the campaign */ -export const CONSOLE_CAMPAIGN_ANALYTICS_ENABLED = 'google.c.a.e'; -export const TAG = 'FirebaseMessaging: '; +export const CONSOLE_CAMPAIGN_NAME = 'google.c.a.c_l'; + +// Due to the fact that onBackgroundMessage can't be awaited (to support rxjs), a silent push +// warning might be shown by the browser if the callback fails to completes by the end of onPush. +// Experiments were ran to determine the majority onBackground message clock time. This brief +// blocking time would allow majority of the onBackgroundMessage callback to finish. +export const BACKGROUND_HANDLE_EXECUTION_TIME_LIMIT_MS = 1000; + +// Preparation time for client to initialize and set up the message handler. +export const FOREGROUND_HANDLE_PREPARATION_TIME_MS = 3000; diff --git a/packages/performance-types/package.json b/packages/performance-types/package.json index e6c0d15aaa3..a10c9ed1974 100644 --- a/packages/performance-types/package.json +++ b/packages/performance-types/package.json @@ -12,7 +12,7 @@ "index.d.ts" ], "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" }, "repository": { "directory": "packages/performance-types", diff --git a/packages/performance/CHANGELOG.md b/packages/performance/CHANGELOG.md index cb17ae0649b..8d68150840b 100644 --- a/packages/performance/CHANGELOG.md +++ b/packages/performance/CHANGELOG.md @@ -1,5 +1,14 @@ # @firebase/performance +## 0.4.4 + +### Patch Changes + +- Updated dependencies [[`9cf727fcc`](https://github.com/firebase/firebase-js-sdk/commit/9cf727fcc3d049551b16ae0698ac33dc2fe45ada)]: + - @firebase/util@0.3.4 + - @firebase/component@0.1.21 + - @firebase/installations@0.4.19 + ## 0.4.3 ### Patch Changes diff --git a/packages/performance/package.json b/packages/performance/package.json index 36c3efae7ac..d9dabd5bdee 100644 --- a/packages/performance/package.json +++ b/packages/performance/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/performance", - "version": "0.4.3", + "version": "0.4.4", "description": "Firebase performance for web", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", @@ -27,19 +27,19 @@ }, "dependencies": { "@firebase/logger": "0.2.6", - "@firebase/installations": "0.4.18", - "@firebase/util": "0.3.3", + "@firebase/installations": "0.4.19", + "@firebase/util": "0.3.4", "@firebase/performance-types": "0.0.13", - "@firebase/component": "0.1.20", + "@firebase/component": "0.1.21", "tslib": "^1.11.1" }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.6.12", - "rollup": "2.29.0", + "@firebase/app": "0.6.13", + "rollup": "2.33.1", "@rollup/plugin-json": "4.1.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages/performance", diff --git a/packages/polyfill/package.json b/packages/polyfill/package.json index 92bbbd4d706..beaf2c43fb4 100644 --- a/packages/polyfill/package.json +++ b/packages/polyfill/package.json @@ -18,12 +18,12 @@ "license": "Apache-2.0", "dependencies": { "core-js": "3.6.5", - "promise-polyfill": "8.1.3", + "promise-polyfill": "8.2.0", "whatwg-fetch": "2.0.4" }, "devDependencies": { - "rollup": "2.29.0", - "rollup-plugin-typescript2": "0.27.3" + "rollup": "2.33.1", + "rollup-plugin-typescript2": "0.29.0" }, "repository": { "directory": "packages/polyfill", diff --git a/packages/remote-config-types/package.json b/packages/remote-config-types/package.json index 48c9ac1ae2f..8b32f91d978 100644 --- a/packages/remote-config-types/package.json +++ b/packages/remote-config-types/package.json @@ -20,6 +20,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages/remote-config/CHANGELOG.md b/packages/remote-config/CHANGELOG.md index 7b76a97a5a3..c9b625a74c6 100644 --- a/packages/remote-config/CHANGELOG.md +++ b/packages/remote-config/CHANGELOG.md @@ -1,5 +1,14 @@ # @firebase/remote-config +## 0.1.30 + +### Patch Changes + +- Updated dependencies [[`9cf727fcc`](https://github.com/firebase/firebase-js-sdk/commit/9cf727fcc3d049551b16ae0698ac33dc2fe45ada)]: + - @firebase/util@0.3.4 + - @firebase/component@0.1.21 + - @firebase/installations@0.4.19 + ## 0.1.29 ### Patch Changes diff --git a/packages/remote-config/package.json b/packages/remote-config/package.json index 97d9c95411b..1ff6c9efa89 100644 --- a/packages/remote-config/package.json +++ b/packages/remote-config/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/remote-config", - "version": "0.1.29", + "version": "0.1.30", "description": "The Remote Config package of the Firebase JS SDK", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", @@ -26,19 +26,19 @@ "@firebase/app-types": "0.x" }, "dependencies": { - "@firebase/installations": "0.4.18", + "@firebase/installations": "0.4.19", "@firebase/logger": "0.2.6", "@firebase/remote-config-types": "0.1.9", - "@firebase/util": "0.3.3", - "@firebase/component": "0.1.20", + "@firebase/util": "0.3.4", + "@firebase/component": "0.1.21", "tslib": "^1.11.1" }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.6.12", - "rollup": "2.29.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "@firebase/app": "0.6.13", + "rollup": "2.33.1", + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages/remote-config", diff --git a/packages/rules-unit-testing/CHANGELOG.md b/packages/rules-unit-testing/CHANGELOG.md index dd1a80d052c..a7cf943b74e 100644 --- a/packages/rules-unit-testing/CHANGELOG.md +++ b/packages/rules-unit-testing/CHANGELOG.md @@ -1,5 +1,17 @@ # @firebase/rules-unit-testing +## 1.1.0 + +### Minor Changes + +- [`6ef39d4d3`](https://github.com/firebase/firebase-js-sdk/commit/6ef39d4d346e7458f1559f15f82f734dec41611b) [#3928](https://github.com/firebase/firebase-js-sdk/pull/3928) - Add withFunctionTriggersDisabled function which runs a user-provided setup function with emulated Cloud Functions triggers disabled. This can be used to import data into the Realtime Database or Cloud Firestore emulators without triggering locally emulated Cloud Functions. This method only works with Firebase CLI version 8.13.0 or higher. + +### Patch Changes + +- Updated dependencies [[`9cf727fcc`](https://github.com/firebase/firebase-js-sdk/commit/9cf727fcc3d049551b16ae0698ac33dc2fe45ada)]: + - @firebase/util@0.3.4 + - firebase@8.0.1 + ## 1.0.9 ### Patch Changes diff --git a/packages/rules-unit-testing/firebase.json b/packages/rules-unit-testing/firebase.json index a5b6bcc199c..1b237fe5922 100644 --- a/packages/rules-unit-testing/firebase.json +++ b/packages/rules-unit-testing/firebase.json @@ -1,4 +1,7 @@ { + "functions": { + "source": "." + }, "emulators": { "firestore": { "port": 9003 @@ -6,6 +9,9 @@ "database": { "port": 9002 }, + "functions": { + "port": 9004 + }, "ui": { "enabled": false } diff --git a/packages/rules-unit-testing/index.ts b/packages/rules-unit-testing/index.ts index 953477f842d..3bb2c596a6b 100644 --- a/packages/rules-unit-testing/index.ts +++ b/packages/rules-unit-testing/index.ts @@ -31,5 +31,6 @@ export { initializeAdminApp, initializeTestApp, loadDatabaseRules, - loadFirestoreRules + loadFirestoreRules, + withFunctionTriggersDisabled } from './src/api'; diff --git a/packages/rules-unit-testing/package.json b/packages/rules-unit-testing/package.json index 9bb90657fb1..a0c41a8c24e 100644 --- a/packages/rules-unit-testing/package.json +++ b/packages/rules-unit-testing/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/rules-unit-testing", - "version": "1.0.9", + "version": "1.1.0", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", @@ -13,24 +13,25 @@ "build:deps": "lerna run --scope @firebase/rules-unit-testing --include-dependencies build", "dev": "rollup -c -w", "test:nyc": "TS_NODE_CACHE=NO TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'test/{,!(browser)/**/}*.test.ts' --config ../../config/mocharc.node.js", - "test": "firebase --debug emulators:exec 'yarn test:nyc'", + "test": "firebase --project=foo --debug emulators:exec 'yarn test:nyc'", "test:ci": "node ../../scripts/run_tests_in_ci.js -s test", "prepare": "yarn build" }, "license": "Apache-2.0", "dependencies": { - "firebase": "8.0.0", + "firebase": "8.0.1", "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.3", + "@firebase/util": "0.3.4", "request": "2.88.2" }, "devDependencies": { - "@google-cloud/firestore": "4.4.0", + "@google-cloud/firestore": "4.7.0", "@types/request": "2.48.5", - "firebase-admin": "9.2.0", - "firebase-tools": "8.12.1", - "rollup": "2.29.0", - "rollup-plugin-typescript2": "0.27.3" + "firebase-admin": "9.3.0", + "firebase-tools": "8.15.0", + "firebase-functions": "3.11.0", + "rollup": "2.33.1", + "rollup-plugin-typescript2": "0.29.0" }, "peerDependencies": { "@google-cloud/firestore": "^4.2.0", diff --git a/packages/rules-unit-testing/src/api/index.ts b/packages/rules-unit-testing/src/api/index.ts index 75d07908de3..680b195b09f 100644 --- a/packages/rules-unit-testing/src/api/index.ts +++ b/packages/rules-unit-testing/src/api/index.ts @@ -36,6 +36,11 @@ const FIRESTORE_ADDRESS_ENV: string = 'FIRESTORE_EMULATOR_HOST'; /** The default address for the local Firestore emulator. */ const FIRESTORE_ADDRESS_DEFAULT: string = 'localhost:8080'; +/** Environment variable to locate the Emulator Hub */ +const HUB_HOST_ENV: string = 'FIREBASE_EMULATOR_HUB'; +/** The default address for the Emulator hub */ +const HUB_HOST_DEFAULT: string = 'localhost:4400'; + /** The actual address for the database emulator */ let _databaseHost: string | undefined = undefined; @@ -307,7 +312,7 @@ export type LoadDatabaseRulesOptions = { databaseName: string; rules: string; }; -export function loadDatabaseRules( +export async function loadDatabaseRules( options: LoadDatabaseRulesOptions ): Promise { if (!options.databaseName) { @@ -318,33 +323,25 @@ export function loadDatabaseRules( throw Error('must provide rules to loadDatabaseRules'); } - return new Promise((resolve, reject) => { - request.put( - { - uri: `http://${getDatabaseHost()}/.settings/rules.json?ns=${ - options.databaseName - }`, - headers: { Authorization: 'Bearer owner' }, - body: options.rules - }, - (err, resp, body) => { - if (err) { - reject(err); - } else if (resp.statusCode !== 200) { - reject(JSON.parse(body).error); - } else { - resolve(); - } - } - ); + const resp = await requestPromise(request.put, { + method: 'PUT', + uri: `http://${getDatabaseHost()}/.settings/rules.json?ns=${ + options.databaseName + }`, + headers: { Authorization: 'Bearer owner' }, + body: options.rules }); + + if (resp.statusCode !== 200) { + throw new Error(JSON.parse(resp.body.error)); + } } export type LoadFirestoreRulesOptions = { projectId: string; rules: string; }; -export function loadFirestoreRules( +export async function loadFirestoreRules( options: LoadFirestoreRulesOptions ): Promise { if (!options.projectId) { @@ -355,64 +352,98 @@ export function loadFirestoreRules( throw new Error('must provide rules to loadFirestoreRules'); } - return new Promise((resolve, reject) => { - request.put( - { - uri: `http://${getFirestoreHost()}/emulator/v1/projects/${ - options.projectId - }:securityRules`, - body: JSON.stringify({ - rules: { - files: [{ content: options.rules }] - } - }) - }, - (err, resp, body) => { - if (err) { - reject(err); - } else if (resp.statusCode !== 200) { - console.log('body', body); - reject(JSON.parse(body).error); - } else { - resolve(); - } + const resp = await requestPromise(request.put, { + method: 'PUT', + uri: `http://${getFirestoreHost()}/emulator/v1/projects/${ + options.projectId + }:securityRules`, + body: JSON.stringify({ + rules: { + files: [{ content: options.rules }] } - ); + }) }); + + if (resp.statusCode !== 200) { + throw new Error(JSON.parse(resp.body.error)); + } } export type ClearFirestoreDataOptions = { projectId: string; }; -export function clearFirestoreData( +export async function clearFirestoreData( options: ClearFirestoreDataOptions ): Promise { if (!options.projectId) { throw new Error('projectId not specified'); } - return new Promise((resolve, reject) => { - request.delete( - { - uri: `http://${getFirestoreHost()}/emulator/v1/projects/${ - options.projectId - }/databases/(default)/documents`, - body: JSON.stringify({ - database: `projects/${options.projectId}/databases/(default)` - }) - }, - (err, resp, body) => { - if (err) { - reject(err); - } else if (resp.statusCode !== 200) { - console.log('body', body); - reject(JSON.parse(body).error); - } else { - resolve(); - } - } + const resp = await requestPromise(request.delete, { + method: 'DELETE', + uri: `http://${getFirestoreHost()}/emulator/v1/projects/${ + options.projectId + }/databases/(default)/documents`, + body: JSON.stringify({ + database: `projects/${options.projectId}/databases/(default)` + }) + }); + + if (resp.statusCode !== 200) { + throw new Error(JSON.parse(resp.body.error)); + } +} + +/** + * Run a setup function with background Cloud Functions triggers disabled. This can be used to + * import data into the Realtime Database or Cloud Firestore emulator without triggering locally + * emulated Cloud Functions. + * + * This method only works with Firebase CLI version 8.13.0 or higher. + * + * @param fn an function which returns a promise. + */ +export async function withFunctionTriggersDisabled( + fn: () => TResult | Promise +): Promise { + let hubHost = process.env[HUB_HOST_ENV]; + if (!hubHost) { + console.warn( + `${HUB_HOST_ENV} is not set, assuming the Emulator hub is running at ${HUB_HOST_DEFAULT}` ); + hubHost = HUB_HOST_DEFAULT; + } + + // Disable background triggers + const disableRes = await requestPromise(request.put, { + method: 'PUT', + uri: `http://${hubHost}/functions/disableBackgroundTriggers` }); + if (disableRes.statusCode !== 200) { + throw new Error( + `HTTP Error ${disableRes.statusCode} when disabling functions triggers, are you using firebase-tools 8.13.0 or higher?` + ); + } + + // Run the user's function + let result: TResult | undefined = undefined; + try { + result = await fn(); + } finally { + // Re-enable background triggers + const enableRes = await requestPromise(request.put, { + method: 'PUT', + uri: `http://${hubHost}/functions/enableBackgroundTriggers` + }); + if (enableRes.statusCode !== 200) { + throw new Error( + `HTTP Error ${enableRes.statusCode} when enabling functions triggers, are you using firebase-tools 8.13.0 or higher?` + ); + } + } + + // Return the user's function result + return result; } export function assertFails(pr: Promise): any { @@ -441,3 +472,22 @@ export function assertFails(pr: Promise): any { export function assertSucceeds(pr: Promise): any { return pr; } + +function requestPromise( + method: typeof request.get, + options: request.CoreOptions & request.UriOptions +): Promise<{ statusCode: number; body: any }> { + return new Promise((resolve, reject) => { + const callback: request.RequestCallback = (err, resp, body) => { + if (err) { + reject(err); + } else { + resolve({ statusCode: resp.statusCode, body }); + } + }; + + // Unfortunately request's default method is not very test-friendly so having + // the caler pass in the method here makes this whole thing compatible with sinon + method(options, callback); + }); +} diff --git a/packages/rules-unit-testing/test/database.test.ts b/packages/rules-unit-testing/test/database.test.ts index deb7b5c1863..d5929ee8666 100644 --- a/packages/rules-unit-testing/test/database.test.ts +++ b/packages/rules-unit-testing/test/database.test.ts @@ -17,6 +17,8 @@ import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; +import * as request from 'request'; +import * as sinon from 'sinon'; import * as firebase from '../src/api'; import { base64 } from '@firebase/util'; import { _FirebaseApp } from '@firebase/app-types/private'; @@ -28,6 +30,15 @@ before(() => { }); describe('Testing Module Tests', function () { + let sandbox: sinon.SinonSandbox; + beforeEach(function () { + sandbox = sinon.createSandbox(); + }); + + afterEach(function () { + sandbox && sandbox.restore(); + }); + it('assertSucceeds() iff success', async function () { const success = Promise.resolve('success'); const failure = Promise.reject('failure'); @@ -262,19 +273,19 @@ describe('Testing Module Tests', function () { it('loadDatabaseRules() throws if no databaseName or rules', async function () { // eslint-disable-next-line @typescript-eslint/no-explicit-any - await expect((firebase as any).loadDatabaseRules.bind(null, {})).to.throw( - /databaseName not specified/ - ); + await expect( + firebase.loadDatabaseRules({} as any) + ).to.eventually.be.rejectedWith(/databaseName not specified/); // eslint-disable-next-line @typescript-eslint/no-explicit-any await expect( - (firebase as any).loadDatabaseRules.bind(null, { + firebase.loadDatabaseRules({ databaseName: 'foo' - }) as Promise - ).to.throw(/must provide rules/); + } as any) + ).to.eventually.be.rejectedWith(/must provide rules/); await expect( // eslint-disable-next-line @typescript-eslint/no-explicit-any - (firebase as any).loadDatabaseRules.bind(null, { rules: '{}' }) - ).to.throw(/databaseName not specified/); + firebase.loadDatabaseRules({ rules: '{}' } as any) + ).to.eventually.be.rejectedWith(/databaseName not specified/); }); it('loadDatabaseRules() succeeds on valid input', async function () { @@ -318,4 +329,26 @@ describe('Testing Module Tests', function () { it('there is a way to get firestore timestamps', function () { expect(firebase.firestore.FieldValue.serverTimestamp()).not.to.be.null; }); + + it('disabling function triggers does not throw, returns value', async function () { + const putSpy = sandbox.spy(request, 'put'); + + const res = await firebase.withFunctionTriggersDisabled(() => { + return Promise.resolve(1234); + }); + + expect(res).to.eq(1234); + expect(putSpy.callCount).to.equal(2); + }); + + it('disabling function triggers always re-enables, event when the function throws', async function () { + const putSpy = sandbox.spy(request, 'put'); + + const res = firebase.withFunctionTriggersDisabled(() => { + throw new Error('I throw!'); + }); + + await expect(res).to.eventually.be.rejectedWith('I throw!'); + expect(putSpy.callCount).to.equal(2); + }); }); diff --git a/packages/rxfire/package.json b/packages/rxfire/package.json index 40b1186beb0..343d64561f5 100644 --- a/packages/rxfire/package.json +++ b/packages/rxfire/package.json @@ -40,13 +40,13 @@ "rxjs": "6.x.x" }, "devDependencies": { - "firebase": "8.0.0", - "rollup": "2.29.0", + "firebase": "8.0.1", + "rollup": "2.33.1", "@rollup/plugin-commonjs": "15.1.0", "@rollup/plugin-node-resolve": "9.0.0", - "rollup-plugin-typescript2": "0.27.3", + "rollup-plugin-typescript2": "0.29.0", "rollup-plugin-uglify": "6.0.4", - "typescript": "4.0.2" + "typescript": "4.0.5" }, "files": [ "/auth/package.json", diff --git a/packages/storage-types/package.json b/packages/storage-types/package.json index 6d0ec0df8d5..7384efd6f79 100644 --- a/packages/storage-types/package.json +++ b/packages/storage-types/package.json @@ -24,6 +24,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages/storage/.eslintrc.js b/packages/storage/.eslintrc.js index e636d931c1f..00bf88df31c 100644 --- a/packages/storage/.eslintrc.js +++ b/packages/storage/.eslintrc.js @@ -37,7 +37,11 @@ module.exports = { 'import/no-extraneous-dependencies': [ 'error', { - 'packageDir': [path.resolve(__dirname, '../../'), __dirname] + 'packageDir': [ + path.resolve(__dirname, '../../'), + __dirname, + path.resolve(__dirname, 'exp') + ] } ] } diff --git a/packages/storage/CHANGELOG.md b/packages/storage/CHANGELOG.md index 45a9c9df684..82d3ee39283 100644 --- a/packages/storage/CHANGELOG.md +++ b/packages/storage/CHANGELOG.md @@ -1,5 +1,13 @@ #Unreleased +## 0.4.1 + +### Patch Changes + +- Updated dependencies [[`9cf727fcc`](https://github.com/firebase/firebase-js-sdk/commit/9cf727fcc3d049551b16ae0698ac33dc2fe45ada)]: + - @firebase/util@0.3.4 + - @firebase/component@0.1.21 + ## 0.4.0 ### Minor Changes diff --git a/packages/storage/api-extractor.json b/packages/storage/api-extractor.json new file mode 100644 index 00000000000..c85607441f0 --- /dev/null +++ b/packages/storage/api-extractor.json @@ -0,0 +1,10 @@ +{ + "extends": "../../config/api-extractor.json", + // Point it to your entry point d.ts file. + "mainEntryPointFilePath": "/exp/dist/exp/index.d.ts", + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/exp/dist/.d.ts", + "publicTrimmedFilePath": "/exp/dist/-public.d.ts" + } +} \ No newline at end of file diff --git a/packages/storage/compat/index.ts b/packages/storage/compat/index.ts new file mode 100644 index 00000000000..661d908fa15 --- /dev/null +++ b/packages/storage/compat/index.ts @@ -0,0 +1,109 @@ +/** + * @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 firebase from '@firebase/app'; +import { _FirebaseNamespace } from '@firebase/app-types/private'; +import { StringFormat } from '../src/implementation/string'; +import { TaskEvent, TaskState } from '../src/implementation/taskenums'; + +import { XhrIoPool } from '../src/implementation/xhriopool'; +import { ReferenceCompat } from './reference'; +import { StorageServiceCompat } from './service'; +import { StorageService } from '../src/service'; +import * as types from '@firebase/storage-types'; +import { + Component, + ComponentType, + ComponentContainer +} from '@firebase/component'; + +import { name, version } from '../package.json'; + +/** + * Type constant for Firebase Storage. + */ +const STORAGE_TYPE = 'storage'; + +function factory( + container: ComponentContainer, + url?: string +): types.FirebaseStorage { + // Dependencies + // TODO: This should eventually be 'app-compat' + const app = container.getProvider('app').getImmediate(); + const authProvider = container.getProvider('auth-internal'); + + // TODO: get StorageService instance from component framework instead + // of creating a new one. + const storageServiceCompat: StorageServiceCompat = new StorageServiceCompat( + app, + new StorageService(app, authProvider, new XhrIoPool(), url) + ); + return storageServiceCompat; +} + +export function registerStorage(instance: _FirebaseNamespace): void { + const namespaceExports = { + // no-inline + TaskState, + TaskEvent, + StringFormat, + Storage: StorageService, + Reference: ReferenceCompat + }; + instance.INTERNAL.registerComponent( + new Component(STORAGE_TYPE, factory, ComponentType.PUBLIC) + .setServiceProps(namespaceExports) + .setMultipleInstances(true) + ); + + instance.registerVersion(name, version); +} + +registerStorage(firebase as _FirebaseNamespace); + +/** + * Define extension behavior for `registerStorage` + */ +declare module '@firebase/app-types' { + interface FirebaseNamespace { + storage?: { + (app?: FirebaseApp): types.FirebaseStorage; + Storage: typeof types.FirebaseStorage; + + StringFormat: { + BASE64: types.StringFormat; + BASE64URL: types.StringFormat; + DATA_URL: types.StringFormat; + RAW: types.StringFormat; + }; + TaskEvent: { + STATE_CHANGED: types.TaskEvent; + }; + TaskState: { + CANCELED: types.TaskState; + ERROR: types.TaskState; + PAUSED: types.TaskState; + RUNNING: types.TaskState; + SUCCESS: types.TaskState; + }; + }; + } + interface FirebaseApp { + storage?(storageBucket?: string): types.FirebaseStorage; + } +} diff --git a/packages/storage/compat/list.ts b/packages/storage/compat/list.ts new file mode 100644 index 00000000000..46553b924f0 --- /dev/null +++ b/packages/storage/compat/list.ts @@ -0,0 +1,42 @@ +/** + * @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 types from '@firebase/storage-types'; +import { ListResult } from '../src/list'; +import { ReferenceCompat } from './reference'; +import { StorageServiceCompat } from './service'; + +export class ListResultCompat implements types.ListResult { + constructor( + private readonly _delegate: ListResult, + private readonly _service: StorageServiceCompat + ) {} + + get prefixes(): ReferenceCompat[] { + return this._delegate.prefixes.map( + ref => new ReferenceCompat(ref, this._service) + ); + } + get items(): ReferenceCompat[] { + return this._delegate.items.map( + ref => new ReferenceCompat(ref, this._service) + ); + } + get nextPageToken(): string | null { + return this._delegate.nextPageToken || null; + } +} diff --git a/packages/storage/compat/reference.ts b/packages/storage/compat/reference.ts new file mode 100644 index 00000000000..bacace86ade --- /dev/null +++ b/packages/storage/compat/reference.ts @@ -0,0 +1,215 @@ +/** + * @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, + getChild, + uploadBytesResumable, + uploadString, + list, + listAll, + getDownloadURL, + getMetadata, + updateMetadata, + deleteObject +} from '../src/reference'; +import * as types from '@firebase/storage-types'; +import { Metadata } from '../src/metadata'; +import { StringFormat } from '../src/implementation/string'; +import { ListOptions } from '../src/list'; +import { UploadTaskCompat } from './task'; +import { ListResultCompat } from './list'; +import { StorageServiceCompat } from './service'; +import { invalidRootOperation } from '../src/implementation/error'; + +export class ReferenceCompat implements types.Reference { + constructor( + private readonly _delegate: Reference, + public storage: StorageServiceCompat + ) {} + + get name(): string { + return this._delegate.name; + } + + get bucket(): string { + return this._delegate.bucket; + } + + get fullPath(): string { + return this._delegate.fullPath; + } + + toString(): string { + return this._delegate.toString(); + } + + /** + * @returns A reference to the object obtained by + * appending childPath, removing any duplicate, beginning, or trailing + * slashes. + */ + child(childPath: string): types.Reference { + const reference = getChild(this._delegate, childPath); + return new ReferenceCompat(reference, this.storage); + } + + get root(): types.Reference { + return new ReferenceCompat(this._delegate.root, this.storage); + } + + /** + * @returns A reference to the parent of the + * current object, or null if the current object is the root. + */ + get parent(): types.Reference | null { + const reference = this._delegate.parent; + if (reference == null) { + return null; + } + return new ReferenceCompat(reference, this.storage); + } + + /** + * Uploads a blob to this object's location. + * @param data - The blob to upload. + * @returns An UploadTask that lets you control and + * observe the upload. + */ + put( + data: Blob | Uint8Array | ArrayBuffer, + metadata?: Metadata + ): types.UploadTask { + this._throwIfRoot('put'); + return new UploadTaskCompat( + uploadBytesResumable(this._delegate, data, metadata), + this + ); + } + + /** + * Uploads a string to this object's location. + * @param value - The string to upload. + * @param format - The format of the string to upload. + * @returns An UploadTask that lets you control and + * observe the upload. + */ + putString( + value: string, + format: StringFormat = StringFormat.RAW, + metadata?: Metadata + ): types.UploadTask { + this._throwIfRoot('putString'); + return new UploadTaskCompat( + uploadString(this._delegate, value, format, metadata), + this + ); + } + + /** + * List all items (files) and prefixes (folders) under this storage reference. + * + * This is a helper method for calling list() repeatedly until there are + * no more results. The default pagination size is 1000. + * + * Note: The results may not be consistent if objects are changed while this + * operation is running. + * + * Warning: listAll may potentially consume too many resources if there are + * too many results. + * + * @returns A Promise that resolves with all the items and prefixes under + * the current storage reference. `prefixes` contains references to + * sub-directories and `items` contains references to objects in this + * folder. `nextPageToken` is never returned. + */ + listAll(): Promise { + return listAll(this._delegate).then( + r => new ListResultCompat(r, this.storage) + ); + } + + /** + * List items (files) and prefixes (folders) under this storage reference. + * + * List API is only available for Firebase Rules Version 2. + * + * GCS is a key-blob store. Firebase Storage imposes the semantic of '/' + * delimited folder structure. Refer to GCS's List API if you want to learn more. + * + * To adhere to Firebase Rules's Semantics, Firebase Storage does not + * support objects whose paths end with "/" or contain two consecutive + * "/"s. Firebase Storage List API will filter these unsupported objects. + * list() may fail if there are too many unsupported objects in the bucket. + * + * @param options - See ListOptions for details. + * @returns A Promise that resolves with the items and prefixes. + * `prefixes` contains references to sub-folders and `items` + * contains references to objects in this folder. `nextPageToken` + * can be used to get the rest of the results. + */ + list(options?: ListOptions | null): Promise { + return list(this._delegate, options).then( + r => new ListResultCompat(r, this.storage) + ); + } + + /** + * A promise that resolves with the metadata for this object. If this + * object doesn't exist or metadata cannot be retreived, the promise is + * rejected. + */ + getMetadata(): Promise { + return getMetadata(this._delegate); + } + + /** + * Updates the metadata for this object. + * @param metadata - The new metadata for the object. + * Only values that have been explicitly set will be changed. Explicitly + * setting a value to null will remove the metadata. + * @returns A promise that resolves + * with the new metadata for this object. + * @see firebaseStorage.Reference.prototype.getMetadata + */ + updateMetadata(metadata: Metadata): Promise { + return updateMetadata(this._delegate, metadata); + } + + /** + * @returns A promise that resolves with the download + * URL for this object. + */ + getDownloadURL(): Promise { + return getDownloadURL(this._delegate); + } + + /** + * Deletes the object at this location. + * @returns A promise that resolves if the deletion succeeds. + */ + delete(): Promise { + this._throwIfRoot('delete'); + return deleteObject(this._delegate); + } + + private _throwIfRoot(name: string): void { + if (this._delegate._location.path === '') { + throw invalidRootOperation(name); + } + } +} diff --git a/packages/storage/compat/service.ts b/packages/storage/compat/service.ts new file mode 100644 index 00000000000..e6159855d45 --- /dev/null +++ b/packages/storage/compat/service.ts @@ -0,0 +1,89 @@ +/** + * @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 types from '@firebase/storage-types'; +import { StorageService, isUrl, ref } from '../src/service'; +import { Location } from '../src/implementation/location'; +import { ReferenceCompat } from './reference'; +import { invalidArgument } from '../src/implementation/error'; +import { FirebaseApp } from '@firebase/app-types'; + +/** + * A service that provides firebaseStorage.Reference instances. + * @param opt_url gs:// url to a custom Storage Bucket + */ +export class StorageServiceCompat implements types.FirebaseStorage { + constructor(public app: FirebaseApp, readonly _delegate: StorageService) {} + + INTERNAL = { + /** + * Called when the associated app is deleted. + */ + delete: () => { + return this._delegate._delete(); + } + }; + + get maxOperationRetryTime(): number { + return this._delegate.maxOperationRetryTime; + } + + get maxUploadRetryTime(): number { + return this._delegate.maxUploadRetryTime; + } + + /** + * Returns a firebaseStorage.Reference for the given path in the default + * bucket. + */ + ref(path?: string): types.Reference { + if (isUrl(path)) { + throw invalidArgument( + 'ref() expected a child path but got a URL, use refFromURL instead.' + ); + } + return new ReferenceCompat(ref(this._delegate, path), this); + } + + /** + * Returns a firebaseStorage.Reference object for the given absolute URL, + * which must be a gs:// or http[s]:// URL. + */ + refFromURL(url: string): types.Reference { + if (!isUrl(url)) { + throw invalidArgument( + 'refFromURL() expected a full URL but got a child path, use ref() instead.' + ); + } + try { + Location.makeFromUrl(url); + } catch (e) { + throw invalidArgument( + 'refFromUrl() expected a valid full URL but got an invalid one.' + ); + } + return new ReferenceCompat(ref(this._delegate, url), this); + } + + setMaxUploadRetryTime(time: number): void { + this._delegate.maxUploadRetryTime = time; + } + + setMaxOperationRetryTime(time: number): void { + this._delegate.maxOperationRetryTime = time; + } +} diff --git a/packages/storage/compat/task.ts b/packages/storage/compat/task.ts new file mode 100644 index 00000000000..9c97765253e --- /dev/null +++ b/packages/storage/compat/task.ts @@ -0,0 +1,107 @@ +/** + * @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 { UploadTask } from '../src/task'; +import { UploadTaskSnapshotCompat } from './tasksnapshot'; +import { TaskEvent } from '../src/implementation/taskenums'; +import * as types from '@firebase/storage-types'; +import { + StorageObserver, + ErrorFn, + CompleteFn, + Subscribe, + Unsubscribe +} from '../src/implementation/observer'; +import { UploadTaskSnapshot } from '../src/tasksnapshot'; +import { ReferenceCompat } from './reference'; +import { FirebaseStorageError } from '../src/implementation/error'; + +export class UploadTaskCompat implements types.UploadTask { + private readonly _snapshot: UploadTaskSnapshotCompat; + constructor( + private readonly _delegate: UploadTask, + private readonly _ref: ReferenceCompat + ) { + this._snapshot = new UploadTaskSnapshotCompat( + this._delegate.snapshot, + this, + this._ref + ); + } + + get snapshot(): UploadTaskSnapshotCompat { + return this._snapshot; + } + + cancel = this._delegate.cancel.bind(this._delegate); + catch = this._delegate.catch.bind(this._delegate); + pause = this._delegate.pause.bind(this._delegate); + resume = this._delegate.resume.bind(this._delegate); + + then( + onFulfilled?: ((a: UploadTaskSnapshotCompat) => unknown) | null, + onRejected?: ((a: FirebaseStorageError) => unknown) | null + ): Promise { + return this._delegate.then(snapshot => { + if (onFulfilled) { + return onFulfilled( + new UploadTaskSnapshotCompat(snapshot, this, this._ref) + ); + } + }, onRejected); + } + + on( + type: TaskEvent, + nextOrObserver?: + | types.StorageObserver + | null + | ((a: UploadTaskSnapshotCompat) => unknown), + error?: ErrorFn | null, + completed?: CompleteFn | null + ): Unsubscribe | Subscribe { + let wrappedNextOrObserver: + | StorageObserver + | undefined + | ((a: UploadTaskSnapshot) => unknown) = undefined; + if (!!nextOrObserver) { + if (typeof nextOrObserver === 'function') { + wrappedNextOrObserver = (taskSnapshot: UploadTaskSnapshot) => + nextOrObserver( + new UploadTaskSnapshotCompat(taskSnapshot, this, this._ref) + ); + } else { + wrappedNextOrObserver = { + next: !!nextOrObserver.next + ? (taskSnapshot: UploadTaskSnapshot) => + nextOrObserver.next!( + new UploadTaskSnapshotCompat(taskSnapshot, this, this._ref) + ) + : undefined, + complete: nextOrObserver.complete || undefined, + error: nextOrObserver.error || undefined + }; + } + } + return this._delegate.on( + type, + wrappedNextOrObserver, + error || undefined, + completed || undefined + ); + } +} diff --git a/packages/storage/compat/tasksnapshot.ts b/packages/storage/compat/tasksnapshot.ts new file mode 100644 index 00000000000..cc7994cbf60 --- /dev/null +++ b/packages/storage/compat/tasksnapshot.ts @@ -0,0 +1,43 @@ +/** + * @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 types from '@firebase/storage-types'; +import { ReferenceCompat } from './reference'; +import { UploadTaskCompat } from './task'; +import { UploadTaskSnapshot } from '../src/tasksnapshot'; +import { Metadata } from '../src/metadata'; + +export class UploadTaskSnapshotCompat implements types.UploadTaskSnapshot { + constructor( + readonly _delegate: UploadTaskSnapshot, + readonly task: UploadTaskCompat, + readonly ref: ReferenceCompat + ) {} + + get bytesTransferred(): number { + return this._delegate.bytesTransferred; + } + get metadata(): Metadata { + return this._delegate.metadata; + } + get state(): string { + return this._delegate.state; + } + get totalBytes(): number { + return this._delegate.totalBytes; + } +} diff --git a/packages/storage/exp/index.ts b/packages/storage/exp/index.ts new file mode 100644 index 00000000000..de835358f8f --- /dev/null +++ b/packages/storage/exp/index.ts @@ -0,0 +1,78 @@ +/** + * @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 { _registerComponent, registerVersion } from '@firebase/app-exp'; + +import { XhrIoPool } from '../src/implementation/xhriopool'; +import { StorageService } from '../src/service'; +import { + Component, + ComponentType, + ComponentContainer +} from '@firebase/component'; + +import { name, version } from '../package.json'; + +export { ref } from '../src/service'; +export { + uploadBytesResumable, + uploadString, + getMetadata, + updateMetadata, + list, + listAll, + getDownloadURL, + deleteObject +} from '../src/reference'; + +/** + * Type constant for Firebase Storage. + */ +const STORAGE_TYPE = 'storage-exp'; + +function factory(container: ComponentContainer, url?: string): StorageService { + // Dependencies + const app = container.getProvider('app-exp').getImmediate(); + const authProvider = container.getProvider('auth-internal'); + + return (new StorageService( + app, + authProvider, + new XhrIoPool(), + url + ) as unknown) as StorageService; +} + +function registerStorage(): void { + _registerComponent( + new Component( + STORAGE_TYPE, + factory, + ComponentType.PUBLIC + ).setMultipleInstances(true) + ); + + registerVersion(name, version); +} + +registerStorage(); + +declare module '@firebase/component' { + interface NameServiceMapping { + 'storage-exp': StorageService; + } +} diff --git a/packages/storage/exp/package.json b/packages/storage/exp/package.json new file mode 100644 index 00000000000..0fcd9c9253a --- /dev/null +++ b/packages/storage/exp/package.json @@ -0,0 +1,12 @@ +{ + "name": "@firebase/storage/exp", + "description": "A tree-shakeable version of the Storage SDK", + "main": "./dist/index.browser.cjs.js", + "module": "./dist/index.browser.esm2017.js", + "browser": "./dist/index.browser.esm2017.js", + "typings": "./dist/index.d.ts", + "private": true, + "dependencies": { + "@firebase/app-exp": "0.0.800" + } +} diff --git a/packages/storage/karma.conf.js b/packages/storage/karma.conf.js index efc709ca8c0..09efdae7148 100644 --- a/packages/storage/karma.conf.js +++ b/packages/storage/karma.conf.js @@ -15,8 +15,6 @@ * limitations under the License. */ -const karma = require('karma'); -const path = require('path'); const karmaBase = require('../../config/karma.base'); const { argv } = require('yargs'); diff --git a/packages/storage/package.json b/packages/storage/package.json index ee80f61d509..b618fc744b8 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -1,16 +1,20 @@ { "name": "@firebase/storage", - "version": "0.4.0", + "version": "0.4.1", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", "esm2017": "dist/index.esm2017.js", - "files": ["dist"], + "files": [ + "dist", + "exp/dist" + ], "scripts": { "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "rollup -c", + "build": "rollup -c rollup.config.compat.js", + "build:exp": "rollup -c rollup.config.exp.js", "build:deps": "lerna run --scope @firebase/storage --include-dependencies build", "dev": "rollup -c -w", "test": "run-p test:browser lint", @@ -19,13 +23,14 @@ "test:browser:integration": "karma start --single-run --integration", "test:browser": "karma start --single-run", "prepare": "yarn build", - "prettier": "prettier --write 'src/**/*.ts' 'test/**/*.ts'" + "prettier": "prettier --write 'src/**/*.ts' 'test/**/*.ts'", + "api-report": "api-extractor run --local --verbose" }, "license": "Apache-2.0", "dependencies": { "@firebase/storage-types": "0.3.13", - "@firebase/util": "0.3.3", - "@firebase/component": "0.1.20", + "@firebase/util": "0.3.4", + "@firebase/component": "0.1.21", "tslib": "^1.11.1" }, "peerDependencies": { @@ -33,11 +38,12 @@ "@firebase/app-types": "0.x" }, "devDependencies": { - "@firebase/app": "0.6.12", + "@firebase/app": "0.6.13", "@firebase/auth": "0.15.1", - "rollup": "2.29.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "rollup": "2.33.1", + "@rollup/plugin-json": "4.1.0", + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages/storage", @@ -48,4 +54,4 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "typings": "dist/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/storage/rollup.config.compat.js b/packages/storage/rollup.config.compat.js new file mode 100644 index 00000000000..3b2bb2102b8 --- /dev/null +++ b/packages/storage/rollup.config.compat.js @@ -0,0 +1,77 @@ +/** + * @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 json from '@rollup/plugin-json'; +import typescriptPlugin from 'rollup-plugin-typescript2'; +import typescript from 'typescript'; +import pkg from './package.json'; + +const deps = Object.keys( + Object.assign({}, pkg.peerDependencies, pkg.dependencies) +); +/** + * ES5 Builds + */ +const es5BuildPlugins = [ + typescriptPlugin({ + typescript + }), + json() +]; + +const es5Builds = [ + { + input: './compat/index.ts', + output: [ + { file: pkg.main, format: 'cjs', sourcemap: true }, + { file: pkg.module, format: 'es', sourcemap: true } + ], + plugins: es5BuildPlugins, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + } +]; + +/** + * ES2017 Builds + */ +const es2017BuildPlugins = [ + typescriptPlugin({ + typescript, + tsconfigOverride: { + compilerOptions: { + target: 'es2017' + } + } + }), + json({ preferConst: true }) +]; + +const es2017Builds = [ + { + input: './compat/index.ts', + output: { + file: pkg.esm2017, + format: 'es', + sourcemap: true + }, + plugins: es2017BuildPlugins, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + } +]; + +// eslint-disable-next-line import/no-default-export +export default [...es5Builds, ...es2017Builds]; diff --git a/packages/storage/rollup.config.exp.js b/packages/storage/rollup.config.exp.js new file mode 100644 index 00000000000..8c56778129c --- /dev/null +++ b/packages/storage/rollup.config.exp.js @@ -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 json from '@rollup/plugin-json'; +import typescriptPlugin from 'rollup-plugin-typescript2'; +import typescript from 'typescript'; +import pkgExp from './exp/package.json'; +import pkg from './package.json'; +import path from 'path'; +import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; + +const deps = Object.keys( + Object.assign({}, pkg.peerDependencies, pkg.dependencies) +).concat('@firebase/app-exp'); + +const plugins = [ + typescriptPlugin({ + typescript, + tsconfigOverride: { + compilerOptions: { + target: 'es2017' + } + }, + abortOnError: false, + transformers: [importPathTransformer] + }), + json({ preferConst: true }) +]; + +const browserBuilds = [ + { + input: './exp/index.ts', + output: [ + { + file: path.resolve('./exp', pkgExp.main), + format: 'cjs', + sourcemap: true + }, + { + file: path.resolve('./exp', pkgExp.browser), + format: 'es', + sourcemap: true + } + ], + plugins, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), + treeshake: { + moduleSideEffects: false + } + } +]; + +// eslint-disable-next-line import/no-default-export +export default browserBuilds; diff --git a/packages/storage/src/implementation/backoff.ts b/packages/storage/src/implementation/backoff.ts index e7e49b1156f..7262a7f4da8 100644 --- a/packages/storage/src/implementation/backoff.ts +++ b/packages/storage/src/implementation/backoff.ts @@ -50,9 +50,6 @@ export function start( } let triggeredCallback = false; - // TODO: This disable can be removed and the 'ignoreRestArgs' option added to - // the no-explicit-any rule when ESlint releases it. - // eslint-disable-next-line @typescript-eslint/no-explicit-any function triggerCallback(...args: any[]): void { if (!triggeredCallback) { triggeredCallback = true; @@ -67,9 +64,6 @@ export function start( }, millis); } - // TODO: This disable can be removed and the 'ignoreRestArgs' option added to - // the no-explicit-any rule when ESlint releases it. - // eslint-disable-next-line @typescript-eslint/no-explicit-any function handler(success: boolean, ...args: any[]): void { if (triggeredCallback) { return; diff --git a/packages/storage/src/implementation/blob.ts b/packages/storage/src/implementation/blob.ts index a82a71dc6f7..e9cde8878da 100644 --- a/packages/storage/src/implementation/blob.ts +++ b/packages/storage/src/implementation/blob.ts @@ -25,7 +25,7 @@ import { StringFormat, dataFromString } from './string'; import * as type from './type'; /** - * @param opt_elideCopy If true, doesn't copy mutable input data + * @param opt_elideCopy - If true, doesn't copy mutable input data * (e.g. Uint8Arrays). Pass true only if you know the objects will not be * modified after this blob's construction. */ diff --git a/packages/storage/src/implementation/error.ts b/packages/storage/src/implementation/error.ts index 47780c177af..9889e9e559d 100644 --- a/packages/storage/src/implementation/error.ts +++ b/packages/storage/src/implementation/error.ts @@ -21,7 +21,10 @@ export class FirebaseStorageError extends FirebaseError { customData: { serverResponse: string | null } = { serverResponse: null }; constructor(code: Code, message: string) { - super(prependCode(code), 'Firebase Storage: ' + message); + super( + prependCode(code), + `Firebase Storage: ${message} (${prependCode(code)})` + ); // Without this, `instanceof FirebaseStorageError`, in tests for example, // returns false. Object.setPrototypeOf(this, FirebaseStorageError.prototype); @@ -33,7 +36,7 @@ export class FirebaseStorageError extends FirebaseError { get message(): string { if (this.customData.serverResponse) { - return this.message + '\n' + this.customData.serverResponse; + return `${this.message}\n${this.customData.serverResponse}`; } else { return this.message; } @@ -50,9 +53,6 @@ export class FirebaseStorageError extends FirebaseError { export const errors = {}; -/** - * @enum {string} - */ export type Code = string; export const Code = { // Shared between all platforms @@ -79,7 +79,8 @@ export const Code = { APP_DELETED: 'app-deleted', INVALID_ROOT_OPERATION: 'invalid-root-operation', INVALID_FORMAT: 'invalid-format', - INTERNAL_ERROR: 'internal-error' + INTERNAL_ERROR: 'internal-error', + UNSUPPORTED_ENVIRONMENT: 'unsupported-environment' }; export function prependCode(code: Code): string { @@ -221,15 +222,8 @@ export function noDownloadURL(): FirebaseStorageError { ); } -export function invalidArgument( - index: number, - fnName: string, - message: string -): FirebaseStorageError { - return new FirebaseStorageError( - Code.INVALID_ARGUMENT, - 'Invalid argument in `' + fnName + '` at index ' + index + ': ' + message - ); +export function invalidArgument(message: string): FirebaseStorageError { + return new FirebaseStorageError(Code.INVALID_ARGUMENT, message); } export function invalidArgumentCount( @@ -269,7 +263,7 @@ export function appDeleted(): FirebaseStorageError { } /** - * @param name The name of the operation that was invalid. + * @param name - The name of the operation that was invalid. */ export function invalidRootOperation(name: string): FirebaseStorageError { return new FirebaseStorageError( @@ -282,8 +276,8 @@ export function invalidRootOperation(name: string): FirebaseStorageError { } /** - * @param format The format that was not valid. - * @param message A message describing the format violation. + * @param format - The format that was not valid. + * @param message - A message describing the format violation. */ export function invalidFormat( format: string, @@ -296,7 +290,7 @@ export function invalidFormat( } /** - * @param message A message describing the internal error. + * @param message - A message describing the internal error. */ export function internalError(message: string): FirebaseStorageError { throw new FirebaseStorageError( diff --git a/packages/storage/src/implementation/failrequest.ts b/packages/storage/src/implementation/failrequest.ts index 3cdc22730d5..afd4cd12ec4 100644 --- a/packages/storage/src/implementation/failrequest.ts +++ b/packages/storage/src/implementation/failrequest.ts @@ -19,8 +19,6 @@ import { Request } from './request'; /** * A request whose promise always fails. - * @struct - * @template T */ export class FailRequest implements Request { promise_: Promise; diff --git a/packages/storage/src/implementation/fs.ts b/packages/storage/src/implementation/fs.ts index 1e4d7dc2cab..795a47970fa 100644 --- a/packages/storage/src/implementation/fs.ts +++ b/packages/storage/src/implementation/fs.ts @@ -20,6 +20,7 @@ * bloats the size of the released binary. */ import * as type from './type'; +import { Code, FirebaseStorageError } from './error'; function getBlobBuilder(): typeof IBlobBuilder | undefined { if (typeof BlobBuilder !== 'undefined') { @@ -49,7 +50,10 @@ export function getBlob(...args: Array): Blob { if (type.isNativeBlobDefined()) { return new Blob(args); } else { - throw Error("This browser doesn't seem to support creating Blobs"); + throw new FirebaseStorageError( + Code.UNSUPPORTED_ENVIRONMENT, + "This browser doesn't seem to support creating Blobs" + ); } } } diff --git a/packages/storage/src/implementation/location.ts b/packages/storage/src/implementation/location.ts index 1b911f29da9..2cf23d43493 100644 --- a/packages/storage/src/implementation/location.ts +++ b/packages/storage/src/implementation/location.ts @@ -19,12 +19,10 @@ * @fileoverview Functionality related to the parsing/composition of bucket/ * object location. */ -import * as errorsExports from './error'; + +import { invalidDefaultBucket, invalidUrl } from './error'; import { DEFAULT_HOST } from './constants'; -/** - * @struct - */ export class Location { private path_: string; @@ -62,7 +60,7 @@ export class Location { if (bucketLocation.path === '') { return bucketLocation; } else { - throw errorsExports.invalidDefaultBucket(bucketString); + throw invalidDefaultBucket(bucketString); } } @@ -128,7 +126,7 @@ export class Location { } } if (location == null) { - throw errorsExports.invalidUrl(url); + throw invalidUrl(url); } return location; } diff --git a/packages/storage/src/implementation/metadata.ts b/packages/storage/src/implementation/metadata.ts index 2ce6dc35783..22be073f116 100644 --- a/packages/storage/src/implementation/metadata.ts +++ b/packages/storage/src/implementation/metadata.ts @@ -32,9 +32,6 @@ export function noXform_(metadata: Metadata, value: T): T { return value; } -/** - * @struct - */ class Mapping { local: string; writable: boolean; @@ -90,9 +87,9 @@ export function getMappings(): Mappings { */ function xformSize( _metadata: Metadata, - size: number | string | undefined + size?: number | string ): number | undefined { - if (type.isDef(size)) { + if (size !== undefined) { return Number(size); } else { return size; diff --git a/packages/storage/src/implementation/observer.ts b/packages/storage/src/implementation/observer.ts index 0e7967c92ae..d4867d42477 100644 --- a/packages/storage/src/implementation/observer.ts +++ b/packages/storage/src/implementation/observer.ts @@ -18,52 +18,47 @@ import * as type from './type'; import { FirebaseStorageError } from './error'; export type NextFn = (value: T) => void; -export type ErrorFn = (error: Error | FirebaseStorageError) => void; +export type ErrorFn = (error: FirebaseStorageError) => void; export type CompleteFn = () => void; export type Unsubscribe = () => void; export interface StorageObserver { - next?: NextFn | null; - error?: ErrorFn | null; - complete?: CompleteFn | null; + next?: NextFn; + error?: ErrorFn; + complete?: CompleteFn; } export type Subscribe = ( - next?: NextFn | StorageObserver | null, - error?: ErrorFn | null, - complete?: CompleteFn | null + next?: NextFn | StorageObserver, + error?: ErrorFn, + complete?: CompleteFn ) => Unsubscribe; -/** - * @struct - */ export class Observer implements StorageObserver { - next?: NextFn | null; - error?: ErrorFn | null; - complete?: CompleteFn | null; + next?: NextFn; + error?: ErrorFn; + complete?: CompleteFn; constructor( - nextOrObserver?: NextFn | StorageObserver | null, - error?: ErrorFn | null, - complete?: CompleteFn | null + nextOrObserver?: NextFn | StorageObserver, + error?: ErrorFn, + complete?: CompleteFn ) { const asFunctions = - type.isFunction(nextOrObserver) || - type.isDef(error) || - type.isDef(complete); + type.isFunction(nextOrObserver) || error != null || complete != null; if (asFunctions) { - this.next = nextOrObserver as NextFn | null; - this.error = error || null; - this.complete = complete || null; + this.next = nextOrObserver as NextFn; + this.error = error; + this.complete = complete; } else { const observer = nextOrObserver as { - next?: NextFn | null; - error?: ErrorFn | null; - complete?: CompleteFn | null; + next?: NextFn; + error?: ErrorFn; + complete?: CompleteFn; }; - this.next = observer.next || null; - this.error = observer.error || null; - this.complete = observer.complete || null; + this.next = observer.next; + this.error = observer.error; + this.complete = observer.complete; } } } diff --git a/packages/storage/src/implementation/request.ts b/packages/storage/src/implementation/request.ts index 5b553ad3af8..f1119e4c64c 100644 --- a/packages/storage/src/implementation/request.ts +++ b/packages/storage/src/implementation/request.ts @@ -35,9 +35,6 @@ import * as UrlUtils from './url'; import { Headers, XhrIo, ErrorCode } from './xhrio'; import { XhrIoPool } from './xhriopool'; -/** - * @template T - */ export interface Request { getPromise(): Promise; @@ -46,15 +43,11 @@ export interface Request { * appropriate value (if the request is finished before you call this method, * but the promise has not yet been resolved), so don't just assume it will be * rejected if you call this function. - * @param appDelete True if the cancelation came from the app being deleted. + * @param appDelete - True if the cancelation came from the app being deleted. */ cancel(appDelete?: boolean): void; } -/** - * @struct - * @template T - */ class NetworkRequest implements Request { private url_: string; private method_: string; @@ -164,7 +157,7 @@ class NetworkRequest implements Request { } /** - * @param requestWentThrough True if the request eventually went + * @param requestWentThrough - True if the request eventually went * through, false if it hit the retry limit or was canceled. */ function backoffDone( @@ -248,8 +241,7 @@ class NetworkRequest implements Request { /** * A collection of information about the result of a network request. - * @param opt_canceled Defaults to false. - * @struct + * @param opt_canceled - Defaults to false. */ export class RequestEndStatus { /** @@ -287,9 +279,6 @@ export function addGmpidHeader_(headers: Headers, appId: string | null): void { } } -/** - * @template T - */ export function makeRequest( requestInfo: RequestInfo, appId: string | null, diff --git a/packages/storage/src/implementation/requests.ts b/packages/storage/src/implementation/requests.ts index 8b1fe288e25..940026ba0b6 100644 --- a/packages/storage/src/implementation/requests.ts +++ b/packages/storage/src/implementation/requests.ts @@ -342,7 +342,6 @@ export function multipartUpload( * @param opt_finalized True if the server has finished the upload. * @param opt_metadata The upload metadata, should * only be passed if opt_finalized is true. - * @struct */ export class ResumableUploadStatus { finalized: boolean; diff --git a/packages/storage/src/implementation/string.ts b/packages/storage/src/implementation/string.ts index 3018d88c1a4..c24f653dab9 100644 --- a/packages/storage/src/implementation/string.ts +++ b/packages/storage/src/implementation/string.ts @@ -14,10 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import * as errorsExports from './error'; + +import { unknown, invalidFormat } from './error'; /** - * @enum {string} + * An enumeration of the possible string formats for upload. */ export type StringFormat = string; export const StringFormat = { @@ -27,9 +28,6 @@ export const StringFormat = { DATA_URL: 'data_url' }; -/** - * @struct - */ export class StringData { contentType: string | null; @@ -58,7 +56,7 @@ export function dataFromString( } // assert(false); - throw errorsExports.unknown(); + throw unknown(); } export function utf8Bytes_(value: string): Uint8Array { @@ -108,10 +106,7 @@ export function percentEncodedBytes_(value: string): Uint8Array { try { decoded = decodeURIComponent(value); } catch (e) { - throw errorsExports.invalidFormat( - StringFormat.DATA_URL, - 'Malformed data URL.' - ); + throw invalidFormat(StringFormat.DATA_URL, 'Malformed data URL.'); } return utf8Bytes_(decoded); } @@ -123,7 +118,7 @@ export function base64Bytes_(format: StringFormat, value: string): Uint8Array { const hasUnder = value.indexOf('_') !== -1; if (hasMinus || hasUnder) { const invalidChar = hasMinus ? '-' : '_'; - throw errorsExports.invalidFormat( + throw invalidFormat( format, "Invalid character '" + invalidChar + @@ -137,7 +132,7 @@ export function base64Bytes_(format: StringFormat, value: string): Uint8Array { const hasSlash = value.indexOf('/') !== -1; if (hasPlus || hasSlash) { const invalidChar = hasPlus ? '+' : '/'; - throw errorsExports.invalidFormat( + throw invalidFormat( format, "Invalid character '" + invalidChar + "' found: is it base64 encoded?" ); @@ -152,7 +147,7 @@ export function base64Bytes_(format: StringFormat, value: string): Uint8Array { try { bytes = atob(value); } catch (e) { - throw errorsExports.invalidFormat(format, 'Invalid character found'); + throw invalidFormat(format, 'Invalid character found'); } const array = new Uint8Array(bytes.length); for (let i = 0; i < bytes.length; i++) { @@ -161,9 +156,6 @@ export function base64Bytes_(format: StringFormat, value: string): Uint8Array { return array; } -/** - * @struct - */ class DataURLParts { base64: boolean = false; contentType: string | null = null; @@ -172,7 +164,7 @@ class DataURLParts { constructor(dataURL: string) { const matches = dataURL.match(/^data:([^,]+)?,/); if (matches === null) { - throw errorsExports.invalidFormat( + throw invalidFormat( StringFormat.DATA_URL, "Must be formatted 'data:[][;base64]," ); diff --git a/packages/storage/src/implementation/taskenums.ts b/packages/storage/src/implementation/taskenums.ts index a9a63055ae1..5001e413529 100644 --- a/packages/storage/src/implementation/taskenums.ts +++ b/packages/storage/src/implementation/taskenums.ts @@ -21,7 +21,6 @@ /** * Enum for task events. - * @enum {string} */ export type TaskEvent = string; export const TaskEvent = { @@ -31,7 +30,6 @@ export const TaskEvent = { /** * Internal enum for task state. - * @enum {string} */ export type InternalTaskState = string; export const InternalTaskState = { @@ -46,7 +44,6 @@ export const InternalTaskState = { /** * External (API-surfaced) enum for task state. - * @enum {string} */ export type TaskState = string; export const TaskState = { diff --git a/packages/storage/src/implementation/type.ts b/packages/storage/src/implementation/type.ts index f9287b96b7c..1335ee4fab1 100644 --- a/packages/storage/src/implementation/type.ts +++ b/packages/storage/src/implementation/type.ts @@ -17,13 +17,6 @@ import { Code, FirebaseStorageError } from './error'; -/** - * @return False if the object is undefined or null, true otherwise. - */ -export function isDef(p: T | null | undefined): p is T { - return p != null; -} - export function isJustDef(p: T | null | undefined): p is T | null { return p !== void 0; } diff --git a/packages/storage/src/implementation/url.ts b/packages/storage/src/implementation/url.ts index 8c6a4472949..caea91f35b3 100644 --- a/packages/storage/src/implementation/url.ts +++ b/packages/storage/src/implementation/url.ts @@ -30,7 +30,6 @@ export function makeQueryString(params: UrlParams): string { let queryPart = '?'; for (const key in params) { if (params.hasOwnProperty(key)) { - // @ts-ignore TODO: remove once typescript is upgraded to 3.5.x const nextPart = encode(key) + '=' + encode(params[key]); queryPart = queryPart + nextPart + '&'; } diff --git a/packages/storage/src/implementation/xhrio.ts b/packages/storage/src/implementation/xhrio.ts index 0b3ae2c506e..8adc8e86a8b 100644 --- a/packages/storage/src/implementation/xhrio.ts +++ b/packages/storage/src/implementation/xhrio.ts @@ -20,6 +20,9 @@ * goog.net.XhrIo-like interface. */ +/** + * XHR headers + */ export interface Headers { [name: string]: string | number; } @@ -50,9 +53,6 @@ export interface XhrIo { removeUploadProgressListener(listener: (p1: ProgressEvent) => void): void; } -/** - * @enum{number} - */ export enum ErrorCode { NO_ERROR = 0, NETWORK_ERROR = 1, diff --git a/packages/storage/src/implementation/xhrio_network.ts b/packages/storage/src/implementation/xhrio_network.ts index e7f04a8be45..016307be71e 100644 --- a/packages/storage/src/implementation/xhrio_network.ts +++ b/packages/storage/src/implementation/xhrio_network.ts @@ -14,9 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import * as errorsExports from './error'; -import * as type from './type'; import { Headers, XhrIo, ErrorCode } from './xhrio'; +import { internalError } from './error'; /** * We use this instead of goog.net.XhrIo because goog.net.XhrIo is hyuuuuge and @@ -52,22 +51,22 @@ export class NetworkXhrIo implements XhrIo { send( url: string, method: string, - body?: ArrayBufferView | Blob | string | null, + body?: ArrayBufferView | Blob | string, headers?: Headers ): Promise { if (this.sent_) { - throw errorsExports.internalError('cannot .send() more than once'); + throw internalError('cannot .send() more than once'); } this.sent_ = true; this.xhr_.open(method, url, true); - if (type.isDef(headers)) { + if (headers !== undefined) { for (const key in headers) { if (headers.hasOwnProperty(key)) { this.xhr_.setRequestHeader(key, headers[key].toString()); } } } - if (type.isDef(body)) { + if (body !== undefined) { this.xhr_.send(body); } else { this.xhr_.send(); @@ -80,9 +79,7 @@ export class NetworkXhrIo implements XhrIo { */ getErrorCode(): ErrorCode { if (!this.sent_) { - throw errorsExports.internalError( - 'cannot .getErrorCode() before sending' - ); + throw internalError('cannot .getErrorCode() before sending'); } return this.errorCode_; } @@ -92,7 +89,7 @@ export class NetworkXhrIo implements XhrIo { */ getStatus(): number { if (!this.sent_) { - throw errorsExports.internalError('cannot .getStatus() before sending'); + throw internalError('cannot .getStatus() before sending'); } try { return this.xhr_.status; @@ -106,9 +103,7 @@ export class NetworkXhrIo implements XhrIo { */ getResponseText(): string { if (!this.sent_) { - throw errorsExports.internalError( - 'cannot .getResponseText() before sending' - ); + throw internalError('cannot .getResponseText() before sending'); } return this.xhr_.responseText; } @@ -132,7 +127,7 @@ export class NetworkXhrIo implements XhrIo { * @override */ addUploadProgressListener(listener: (p1: ProgressEvent) => void): void { - if (type.isDef(this.xhr_.upload)) { + if (this.xhr_.upload != null) { this.xhr_.upload.addEventListener('progress', listener); } } @@ -141,7 +136,7 @@ export class NetworkXhrIo implements XhrIo { * @override */ removeUploadProgressListener(listener: (p1: ProgressEvent) => void): void { - if (type.isDef(this.xhr_.upload)) { + if (this.xhr_.upload != null) { this.xhr_.upload.removeEventListener('progress', listener); } } diff --git a/packages/storage/src/list.ts b/packages/storage/src/list.ts index 732deac2afa..a2124b88c1e 100644 --- a/packages/storage/src/list.ts +++ b/packages/storage/src/list.ts @@ -14,18 +14,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import * as types from '@firebase/storage-types'; import { Reference } from './reference'; /** * @fileoverview Documentation for ListOptions and ListResult format. */ -export interface ListOptions { - maxResults?: number | null; - pageToken?: string | null; -} +/** + * The options `list()` accepts. + */ +export interface ListOptions extends types.ListOptions {} + +/** + * Result returned by list(). + */ export interface ListResult { prefixes: Reference[]; items: Reference[]; - nextPageToken?: string | null; + nextPageToken?: string; } diff --git a/packages/storage/src/metadata.ts b/packages/storage/src/metadata.ts index 9dfba61cdc8..3f0aa225da6 100644 --- a/packages/storage/src/metadata.ts +++ b/packages/storage/src/metadata.ts @@ -14,21 +14,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import * as types from '@firebase/storage-types'; import { Reference } from './reference'; /** * @fileoverview Documentation for the metadata format. */ -interface Metadata { - bucket: string | undefined; - generation: string | undefined; - metageneration: string | undefined; - fullPath: string | undefined; - name: string | undefined; - size: number | undefined; + +/** + * The full set of object metadata, including read-only properties. + */ +interface Metadata extends types.FullMetadata { type: string | undefined; - timeCreated: string | undefined; - updated: string | undefined; md5Hash: string | undefined; cacheControl: string | undefined; contentDisposition: string | undefined; diff --git a/packages/storage/src/reference.ts b/packages/storage/src/reference.ts index 2d66d889508..bffadda8afe 100644 --- a/packages/storage/src/reference.ts +++ b/packages/storage/src/reference.ts @@ -18,23 +18,29 @@ /** * @fileoverview Defines the Firebase Storage Reference class. */ + import { FbsBlob } from './implementation/blob'; -import * as errorsExports from './implementation/error'; import { Location } from './implementation/location'; -import * as metadata from './implementation/metadata'; -import * as path from './implementation/path'; -import * as requests from './implementation/requests'; -import { dataFromString, StringFormat } from './implementation/string'; -import * as type from './implementation/type'; -import { validateNumber } from './implementation/type'; +import { getMappings } from './implementation/metadata'; +import { child, parent, lastComponent } from './implementation/path'; +import { + list as requestsList, + getMetadata as requestsGetMetadata, + updateMetadata as requestsUpdateMetadata, + getDownloadUrl as requestsGetDownloadUrl, + deleteObject as requestsDeleteObject +} from './implementation/requests'; +import { StringFormat, dataFromString } from './implementation/string'; import { Metadata } from './metadata'; import { StorageService } from './service'; -import { UploadTask } from './task'; import { ListOptions, ListResult } from './list'; +import { UploadTask } from './task'; +import { invalidRootOperation, noDownloadURL } from './implementation/error'; +import { validateNumber } from './implementation/type'; /** * Provides methods to interact with a bucket in the Firebase Storage service. - * @param location An fbs.location, or the URL at + * @param location - An fbs.location, or the URL at * which to base this object, in one of the following forms: * gs:/// * http[s]://firebasestorage.googleapis.com/ @@ -44,295 +50,309 @@ import { ListOptions, ListResult } from './list'; * the project ID of the base firebase.App instance. */ export class Reference { - protected location: Location; + /** + * @internal + */ + _location: Location; - constructor(protected service: StorageService, location: string | Location) { + constructor(private _service: StorageService, location: string | Location) { if (location instanceof Location) { - this.location = location; + this._location = location; } else { - this.location = Location.makeFromUrl(location); + this._location = Location.makeFromUrl(location); } } /** - * @return The URL for the bucket and path this object references, + * @returns The URL for the bucket and path this object references, * in the form gs:/// * @override */ toString(): string { - return 'gs://' + this.location.bucket + '/' + this.location.path; + return 'gs://' + this._location.bucket + '/' + this._location.path; } protected newRef(service: StorageService, location: Location): Reference { return new Reference(service, location); } - protected mappings(): metadata.Mappings { - return metadata.getMappings(); - } - - /** - * @return A reference to the object obtained by - * appending childPath, removing any duplicate, beginning, or trailing - * slashes. - */ - child(childPath: string): Reference { - const newPath = path.child(this.location.path, childPath); - const location = new Location(this.location.bucket, newPath); - return this.newRef(this.service, location); - } - /** - * @return A reference to the parent of the - * current object, or null if the current object is the root. - */ - get parent(): Reference | null { - const newPath = path.parent(this.location.path); - if (newPath === null) { - return null; - } - const location = new Location(this.location.bucket, newPath); - return this.newRef(this.service, location); - } - - /** - * @return An reference to the root of this + * @returns An reference to the root of this * object's bucket. */ get root(): Reference { - const location = new Location(this.location.bucket, ''); - return this.newRef(this.service, location); + const location = new Location(this._location.bucket, ''); + return this.newRef(this._service, location); } get bucket(): string { - return this.location.bucket; + return this._location.bucket; } get fullPath(): string { - return this.location.path; + return this._location.path; } get name(): string { - return path.lastComponent(this.location.path); + return lastComponent(this._location.path); } get storage(): StorageService { - return this.service; + return this._service; } - /** - * Uploads a blob to this object's location. - * @param data The blob to upload. - * @return An UploadTask that lets you control and - * observe the upload. - */ - put( - data: Blob | Uint8Array | ArrayBuffer, - metadata: Metadata | null = null - ): UploadTask { - this.throwIfRoot_('put'); - return new UploadTask( - this, - this.service, - this.location, - this.mappings(), - new FbsBlob(data), - metadata - ); + get parent(): Reference | null { + const newPath = parent(this._location.path); + if (newPath === null) { + return null; + } + const location = new Location(this._location.bucket, newPath); + return new Reference(this._service, location); } - /** - * Uploads a string to this object's location. - * @param value The string to upload. - * @param format The format of the string to upload. - * @return An UploadTask that lets you control and - * observe the upload. - */ - putString( - value: string, - format: StringFormat = StringFormat.RAW, - metadata?: Metadata - ): UploadTask { - this.throwIfRoot_('putString'); - const data = dataFromString(format, value); - const metadataClone = Object.assign({}, metadata); - if ( - !type.isDef(metadataClone['contentType']) && - type.isDef(data.contentType) - ) { - metadataClone['contentType'] = data.contentType!; + _throwIfRoot(name: string): void { + if (this._location.path === '') { + throw invalidRootOperation(name); } - return new UploadTask( - this, - this.service, - this.location, - this.mappings(), - new FbsBlob(data.data, true), - metadataClone - ); } +} - /** - * Deletes the object at this location. - * @return A promise that resolves if the deletion succeeds. - */ - delete(): Promise { - this.throwIfRoot_('delete'); - return this.service.getAuthToken().then(authToken => { - const requestInfo = requests.deleteObject(this.service, this.location); - return this.service.makeRequest(requestInfo, authToken).getPromise(); - }); - } +/** + * Uploads a blob to this object's location. + * @public + * @param ref - Storage Reference where data should be uploaded. + * @param data - The data to upload. + * @param metadata - Metadata for the newly uploaded string. + * @returns An UploadTask that lets you control and + * observe the upload. + */ +export function uploadBytesResumable( + ref: Reference, + data: Blob | Uint8Array | ArrayBuffer, + metadata: Metadata | null = null +): UploadTask { + ref._throwIfRoot('uploadBytesResumable'); + return new UploadTask(ref, new FbsBlob(data), metadata); +} - /** - * List all items (files) and prefixes (folders) under this storage reference. - * - * This is a helper method for calling list() repeatedly until there are - * no more results. The default pagination size is 1000. - * - * Note: The results may not be consistent if objects are changed while this - * operation is running. - * - * Warning: listAll may potentially consume too many resources if there are - * too many results. - * - * @return A Promise that resolves with all the items and prefixes under - * the current storage reference. `prefixes` contains references to - * sub-directories and `items` contains references to objects in this - * folder. `nextPageToken` is never returned. - */ - listAll(): Promise { - const accumulator = { - prefixes: [], - items: [] - }; - return this.listAllHelper(accumulator).then(() => accumulator); +/** + * Uploads a string to this object's location. + * @public + * @param ref - Storage Reference where string should be uploaded. + * @param value - The string to upload. + * @param format - The format of the string to upload. + * @param metadata - Metadata for the newly uploaded object. + * @returns An UploadTask that lets you control and + * observe the upload. + */ +export function uploadString( + ref: Reference, + value: string, + format: StringFormat = StringFormat.RAW, + metadata?: Metadata +): UploadTask { + ref._throwIfRoot('putString'); + const data = dataFromString(format, value); + const metadataClone = { ...metadata } as Metadata; + if (metadataClone['contentType'] == null && data.contentType != null) { + metadataClone['contentType'] = data.contentType!; } + return new UploadTask(ref, new FbsBlob(data.data, true), metadataClone); +} - private async listAllHelper( - accumulator: ListResult, - pageToken?: string - ): Promise { - const opt: ListOptions = { - // maxResults is 1000 by default. - pageToken - }; - const nextPage = await this.list(opt); - accumulator.prefixes.push(...nextPage.prefixes); - accumulator.items.push(...nextPage.items); - if (nextPage.nextPageToken != null) { - await this.listAllHelper(accumulator, nextPage.nextPageToken); - } +/** + * List all items (files) and prefixes (folders) under this storage reference. + * + * This is a helper method for calling list() repeatedly until there are + * no more results. The default pagination size is 1000. + * + * Note: The results may not be consistent if objects are changed while this + * operation is running. + * + * Warning: listAll may potentially consume too many resources if there are + * too many results. + * @public + * @param ref - Storage Reference to get list from. + * + * @returns A Promise that resolves with all the items and prefixes under + * the current storage reference. `prefixes` contains references to + * sub-directories and `items` contains references to objects in this + * folder. `nextPageToken` is never returned. + */ +export function listAll(ref: Reference): Promise { + const accumulator: ListResult = { + prefixes: [], + items: [] + }; + return listAllHelper(ref, accumulator).then(() => accumulator); +} + +/** + * Separated from listAll because async functions can't use "arguments". + * @internal + * @param ref + * @param accumulator + * @param pageToken + */ +async function listAllHelper( + ref: Reference, + accumulator: ListResult, + pageToken?: string +): Promise { + const opt: ListOptions = { + // maxResults is 1000 by default. + pageToken + }; + const nextPage = await list(ref, opt); + accumulator.prefixes.push(...nextPage.prefixes); + accumulator.items.push(...nextPage.items); + if (nextPage.nextPageToken != null) { + await listAllHelper(ref, accumulator, nextPage.nextPageToken); } +} - /** - * List items (files) and prefixes (folders) under this storage reference. - * - * List API is only available for Firebase Rules Version 2. - * - * GCS is a key-blob store. Firebase Storage imposes the semantic of '/' - * delimited folder structure. - * Refer to GCS's List API if you want to learn more. - * - * To adhere to Firebase Rules's Semantics, Firebase Storage does not - * support objects whose paths end with "/" or contain two consecutive - * "/"s. Firebase Storage List API will filter these unsupported objects. - * list() may fail if there are too many unsupported objects in the bucket. - * - * @param options See ListOptions for details. - * @return A Promise that resolves with the items and prefixes. - * `prefixes` contains references to sub-folders and `items` - * contains references to objects in this folder. `nextPageToken` - * can be used to get the rest of the results. - */ - list(options?: ListOptions | null): Promise { - const op = options || {}; - if (typeof op.maxResults === 'number') { +/** + * List items (files) and prefixes (folders) under this storage reference. + * + * List API is only available for Firebase Rules Version 2. + * + * GCS is a key-blob store. Firebase Storage imposes the semantic of '/' + * delimited folder structure. + * Refer to GCS's List API if you want to learn more. + * + * To adhere to Firebase Rules's Semantics, Firebase Storage does not + * support objects whose paths end with "/" or contain two consecutive + * "/"s. Firebase Storage List API will filter these unsupported objects. + * list() may fail if there are too many unsupported objects in the bucket. + * @public + * + * @param ref - Storage Reference to get list from. + * @param options - See ListOptions for details. + * @returns A Promise that resolves with the items and prefixes. + * `prefixes` contains references to sub-folders and `items` + * contains references to objects in this folder. `nextPageToken` + * can be used to get the rest of the results. + */ +export async function list( + ref: Reference, + options?: ListOptions | null +): Promise { + if (options != null) { + if (typeof options.maxResults === 'number') { validateNumber( 'options.maxResults', /* minValue= */ 1, /* maxValue= */ 1000, - op.maxResults + options.maxResults ); } - return this.service.getAuthToken().then(authToken => { - const requestInfo = requests.list( - this.service, - this.location, - /*delimiter= */ '/', - op.pageToken, - op.maxResults - ); - return this.service.makeRequest(requestInfo, authToken).getPromise(); - }); } + const authToken = await ref.storage.getAuthToken(); + const op = options || {}; + const requestInfo = requestsList( + ref.storage, + ref._location, + /*delimiter= */ '/', + op.pageToken, + op.maxResults + ); + return ref.storage.makeRequest(requestInfo, authToken).getPromise(); +} - /** - * A promise that resolves with the metadata for this object. If this - * object doesn't exist or metadata cannot be retreived, the promise is - * rejected. - */ - getMetadata(): Promise { - this.throwIfRoot_('getMetadata'); - return this.service.getAuthToken().then(authToken => { - const requestInfo = requests.getMetadata( - this.service, - this.location, - this.mappings() - ); - return this.service.makeRequest(requestInfo, authToken).getPromise(); - }); - } +/** + * A promise that resolves with the metadata for this object. If this + * object doesn't exist or metadata cannot be retreived, the promise is + * rejected. + * @public + * @param ref - Storage Reference to get metadata from. + */ +export async function getMetadata(ref: Reference): Promise { + ref._throwIfRoot('getMetadata'); + const authToken = await ref.storage.getAuthToken(); + const requestInfo = requestsGetMetadata( + ref.storage, + ref._location, + getMappings() + ); + return ref.storage.makeRequest(requestInfo, authToken).getPromise(); +} - /** - * Updates the metadata for this object. - * @param metadata The new metadata for the object. - * Only values that have been explicitly set will be changed. Explicitly - * setting a value to null will remove the metadata. - * @return A promise that resolves - * with the new metadata for this object. - * @see firebaseStorage.Reference.prototype.getMetadata - */ - updateMetadata(metadata: Metadata): Promise { - this.throwIfRoot_('updateMetadata'); - return this.service.getAuthToken().then(authToken => { - const requestInfo = requests.updateMetadata( - this.service, - this.location, - metadata, - this.mappings() - ); - return this.service.makeRequest(requestInfo, authToken).getPromise(); - }); - } +/** + * Updates the metadata for this object. + * @public + * @param ref - Storage Reference to update metadata for. + * @param metadata - The new metadata for the object. + * Only values that have been explicitly set will be changed. Explicitly + * setting a value to null will remove the metadata. + * @returns A promise that resolves + * with the new metadata for this object. + * See `firebaseStorage.Reference.prototype.getMetadata` + */ +export async function updateMetadata( + ref: Reference, + metadata: Metadata +): Promise { + ref._throwIfRoot('updateMetadata'); + const authToken = await ref.storage.getAuthToken(); + const requestInfo = requestsUpdateMetadata( + ref.storage, + ref._location, + metadata, + getMappings() + ); + return ref.storage.makeRequest(requestInfo, authToken).getPromise(); +} - /** - * @return A promise that resolves with the download - * URL for this object. - */ - getDownloadURL(): Promise { - this.throwIfRoot_('getDownloadURL'); - return this.service.getAuthToken().then(authToken => { - const requestInfo = requests.getDownloadUrl( - this.service, - this.location, - this.mappings() - ); - return this.service - .makeRequest(requestInfo, authToken) - .getPromise() - .then(url => { - if (url === null) { - throw errorsExports.noDownloadURL(); - } - return url; - }); +/** + * Returns the download URL for the given Reference. + * @public + * @returns A promise that resolves with the download + * URL for this object. + */ +export async function getDownloadURL(ref: Reference): Promise { + ref._throwIfRoot('getDownloadURL'); + const authToken = await ref.storage.getAuthToken(); + const requestInfo = requestsGetDownloadUrl( + ref.storage, + ref._location, + getMappings() + ); + return ref.storage + .makeRequest(requestInfo, authToken) + .getPromise() + .then(url => { + if (url === null) { + throw noDownloadURL(); + } + return url; }); - } +} - private throwIfRoot_(name: string): void { - if (this.location.path === '') { - throw errorsExports.invalidRootOperation(name); - } - } +/** + * Deletes the object at this location. + * @public + * @param ref - Storage Reference for object to delete. + * @returns A promise that resolves if the deletion succeeds. + */ +export async function deleteObject(ref: Reference): Promise { + ref._throwIfRoot('deleteObject'); + const authToken = await ref.storage.getAuthToken(); + const requestInfo = requestsDeleteObject(ref.storage, ref._location); + return ref.storage.makeRequest(requestInfo, authToken).getPromise(); +} + +/** + * Returns reference for object obtained by appending `childPath` to `ref`. + * @internal + * + * @param ref - Storage Reference to get child of. + * @param childPath - Child path from provided ref. + * @returns A reference to the object obtained by + * appending childPath, removing any duplicate, beginning, or trailing + * slashes. + */ +export function getChild(ref: Reference, childPath: string): Reference { + const newPath = child(ref._location.path, childPath); + const location = new Location(ref._location.bucket, newPath); + return new Reference(ref.storage, location); } diff --git a/packages/storage/src/service.ts b/packages/storage/src/service.ts index 54377f7b2fa..d2aba09ba17 100644 --- a/packages/storage/src/service.ts +++ b/packages/storage/src/service.ts @@ -15,220 +15,237 @@ * limitations under the License. */ -import { FirebaseApp } from '@firebase/app-types'; import { Location } from './implementation/location'; import { FailRequest } from './implementation/failrequest'; import { Request, makeRequest } from './implementation/request'; import { RequestInfo } from './implementation/requestinfo'; import { XhrIoPool } from './implementation/xhriopool'; -import { Reference } from './reference'; +import { Reference, getChild } from './reference'; import { Provider } from '@firebase/component'; import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; -import { FirebaseOptions } from '@firebase/app-types-exp'; +import { + FirebaseApp, + FirebaseOptions, + _FirebaseService +} from '@firebase/app-types-exp'; import * as constants from '../src/implementation/constants'; -import * as errorsExports from './implementation/error'; -import { Code, FirebaseStorageError } from './implementation/error'; +import { + invalidArgument, + appDeleted, + noDefaultBucket +} from './implementation/error'; import { validateNumber } from './implementation/type'; +export function isUrl(path?: string): boolean { + return /^[A-Za-z]+:\/\//.test(path as string); +} + /** - * A service that provides firebaseStorage.Reference instances. - * @param opt_url gs:// url to a custom Storage Bucket - * - * @struct + * Returns a firebaseStorage.Reference for the given url. */ -export class StorageService { - private app_: FirebaseApp | null; - private readonly bucket_: Location | null = null; - private readonly internals_: ServiceInternals; - private readonly authProvider_: Provider; - private readonly appId_: string | null = null; - private readonly pool_: XhrIoPool; - private readonly requests_: Set>; - private deleted_: boolean = false; - private maxOperationRetryTime_: number; - private maxUploadRetryTime_: number; +function refFromURL(service: StorageService, url: string): Reference { + return new Reference(service, url); +} - constructor( - app: FirebaseApp | null, - authProvider: Provider, - pool: XhrIoPool, - url?: string - ) { - this.app_ = app; - this.authProvider_ = authProvider; - this.maxOperationRetryTime_ = constants.DEFAULT_MAX_OPERATION_RETRY_TIME; - this.maxUploadRetryTime_ = constants.DEFAULT_MAX_UPLOAD_RETRY_TIME; - this.requests_ = new Set(); - this.pool_ = pool; - if (url != null) { - this.bucket_ = Location.makeFromBucketSpec(url); - } else { - this.bucket_ = StorageService.extractBucket_(this.app_?.options); +/** + * Returns a firebaseStorage.Reference for the given path in the default + * bucket. + */ +function refFromPath( + ref: StorageService | Reference, + path?: string +): Reference { + if (ref instanceof StorageService) { + const service = ref; + if (service._bucket == null) { + throw noDefaultBucket(); } - this.internals_ = new ServiceInternals(this); - } - - private static extractBucket_(config?: FirebaseOptions): Location | null { - const bucketString = config?.[constants.CONFIG_STORAGE_BUCKET_KEY]; - if (bucketString == null) { - return null; + const reference = new Reference(service, service._bucket!); + if (path != null) { + return refFromPath(reference, path); + } else { + return reference; } - return Location.makeFromBucketSpec(bucketString); - } - - async getAuthToken(): Promise { - const auth = this.authProvider_.getImmediate({ optional: true }); - if (auth) { - const tokenData = await auth.getToken(); - if (tokenData !== null) { - return tokenData.accessToken; + } else { + // ref is a Reference + if (path !== undefined) { + if (path.includes('..')) { + throw invalidArgument('`path` param cannot contain ".."'); } - } - return null; - } - - /** - * Stop running requests and prevent more from being created. - */ - deleteApp(): void { - this.deleted_ = true; - this.app_ = null; - this.requests_.forEach(request => request.cancel()); - this.requests_.clear(); - } - - /** - * Returns a new firebaseStorage.Reference object referencing this StorageService - * at the given Location. - * @param loc The Location. - * @return A firebaseStorage.Reference. - */ - makeStorageReference(loc: Location): Reference { - return new Reference(this, loc); - } - - makeRequest( - requestInfo: RequestInfo, - authToken: string | null - ): Request { - if (!this.deleted_) { - const request = makeRequest( - requestInfo, - this.appId_, - authToken, - this.pool_ - ); - this.requests_.add(request); - // Request removes itself from set when complete. - request.getPromise().then( - () => this.requests_.delete(request), - () => this.requests_.delete(request) - ); - return request; + return getChild(ref, path); } else { - return new FailRequest(errorsExports.appDeleted()); + return ref; } } +} - /** - * Returns a firebaseStorage.Reference for the given path in the default - * bucket. - */ - ref(path?: string): Reference { - if (/^[A-Za-z]+:\/\//.test(path as string)) { - throw new FirebaseStorageError( - Code.INVALID_ARGUMENT, - 'Expected child path but got a URL, use refFromURL instead.' +/** + * Returns a storage Reference for the given url. + * @param storage - `Storage` instance. + * @param url - URL. If empty, returns root reference. + * @public + */ +export function ref(storage: StorageService, url?: string): Reference; +/** + * Returns a storage Reference for the given path in the + * default bucket. + * @param storageOrRef - `Storage` service or storage `Reference`. + * @param pathOrUrlStorage - path. If empty, returns root reference (if Storage + * instance provided) or returns same reference (if Reference provided). + * @public + */ +export function ref( + storageOrRef: StorageService | Reference, + path?: string +): Reference; +export function ref( + serviceOrRef: StorageService | Reference, + pathOrUrl?: string +): Reference | null { + if (pathOrUrl && isUrl(pathOrUrl)) { + if (serviceOrRef instanceof StorageService) { + return refFromURL(serviceOrRef, pathOrUrl); + } else { + throw invalidArgument( + 'To use ref(service, url), the first argument must be a Storage instance.' ); } + } else { + return refFromPath(serviceOrRef, pathOrUrl); + } +} - if (this.bucket_ == null) { - throw new Error('No Storage Bucket defined in Firebase Options.'); - } - - const ref = new Reference(this, this.bucket_); - if (path != null) { - return ref.child(path); - } else { - return ref; - } +function extractBucket(config?: FirebaseOptions): Location | null { + const bucketString = config?.[constants.CONFIG_STORAGE_BUCKET_KEY]; + if (bucketString == null) { + return null; } + return Location.makeFromBucketSpec(bucketString); +} +/** + * A service that provides Firebase Storage Reference instances. + * @param opt_url - gs:// url to a custom Storage Bucket + */ +export class StorageService implements _FirebaseService { /** - * Returns a firebaseStorage.Reference object for the given absolute URL, - * which must be a gs:// or http[s]:// URL. + * @internal */ - refFromURL(url: string): Reference { - if (!/^[A-Za-z]+:\/\//.test(url)) { - throw new FirebaseStorageError( - Code.INVALID_ARGUMENT, - 'Expected full URL but got a child path, use ref instead.' - ); - } - try { - Location.makeFromUrl(url); - } catch (e) { - throw new FirebaseStorageError( - Code.INVALID_ARGUMENT, - 'Expected valid full URL but got an invalid one.' - ); - } + readonly _bucket: Location | null = null; + protected readonly _appId: string | null = null; + private readonly _requests: Set>; + private _deleted: boolean = false; + private _maxOperationRetryTime: number; + private _maxUploadRetryTime: number; - return new Reference(this, url); + constructor( + readonly app: FirebaseApp, + /** + * @internal + */ + readonly _authProvider: Provider, + /** + * @internal + */ + readonly _pool: XhrIoPool, + /** + * @internal + */ + readonly _url?: string + ) { + this._maxOperationRetryTime = constants.DEFAULT_MAX_OPERATION_RETRY_TIME; + this._maxUploadRetryTime = constants.DEFAULT_MAX_UPLOAD_RETRY_TIME; + this._requests = new Set(); + if (_url != null) { + this._bucket = Location.makeFromBucketSpec(_url); + } else { + this._bucket = extractBucket(this.app.options); + } } get maxUploadRetryTime(): number { - return this.maxUploadRetryTime_; + return this._maxUploadRetryTime; } - setMaxUploadRetryTime(time: number): void { + set maxUploadRetryTime(time: number) { validateNumber( 'time', /* minValue=*/ 0, /* maxValue= */ Number.POSITIVE_INFINITY, time ); - this.maxUploadRetryTime_ = time; + this._maxUploadRetryTime = time; } get maxOperationRetryTime(): number { - return this.maxOperationRetryTime_; + return this._maxOperationRetryTime; } - setMaxOperationRetryTime(time: number): void { + set maxOperationRetryTime(time: number) { validateNumber( 'time', /* minValue=*/ 0, /* maxValue= */ Number.POSITIVE_INFINITY, time ); - this.maxOperationRetryTime_ = time; + this._maxOperationRetryTime = time; } - get app(): FirebaseApp | null { - return this.app_; + async getAuthToken(): Promise { + const auth = this._authProvider.getImmediate({ optional: true }); + if (auth) { + const tokenData = await auth.getToken(); + if (tokenData !== null) { + return tokenData.accessToken; + } + } + return null; } - get INTERNAL(): ServiceInternals { - return this.internals_; + /** + * Stop running requests and prevent more from being created. + * @internal + */ + _delete(): Promise { + this._deleted = true; + this._requests.forEach(request => request.cancel()); + this._requests.clear(); + return Promise.resolve(); } -} - -/** - * @struct - */ -export class ServiceInternals { - service_: StorageService; - constructor(service: StorageService) { - this.service_ = service; + /** + * Returns a new firebaseStorage.Reference object referencing this StorageService + * at the given Location. + */ + makeStorageReference(loc: Location): Reference { + return new Reference(this, loc); } /** - * Called when the associated app is deleted. + * @internal + * @param requestInfo - HTTP RequestInfo object + * @param authToken - Firebase auth token */ - delete(): Promise { - this.service_.deleteApp(); - return Promise.resolve(); + makeRequest( + requestInfo: RequestInfo, + authToken: string | null + ): Request { + if (!this._deleted) { + const request = makeRequest( + requestInfo, + this._appId, + authToken, + this._pool + ); + this._requests.add(request); + // Request removes itself from set when complete. + request.getPromise().then( + () => this._requests.delete(request), + () => this._requests.delete(request) + ); + return request; + } else { + return new FailRequest(appDeleted()); + } } } diff --git a/packages/storage/src/task.ts b/packages/storage/src/task.ts index 0d330d9a5db..9d9242444c7 100644 --- a/packages/storage/src/task.ts +++ b/packages/storage/src/task.ts @@ -30,7 +30,6 @@ import { Metadata } from './metadata'; import { CompleteFn, ErrorFn, - NextFn, Observer, StorageObserver, Subscribe, @@ -39,140 +38,140 @@ import { import { Request } from './implementation/request'; import { UploadTaskSnapshot } from './tasksnapshot'; import { async as fbsAsync } from './implementation/async'; -import { Location } from './implementation/location'; import * as fbsMetadata from './implementation/metadata'; import * as fbsRequests from './implementation/requests'; import { Reference } from './reference'; -import { StorageService } from './service'; +import { getMappings } from './implementation/metadata'; /** * Represents a blob being uploaded. Can be used to pause/resume/cancel the * upload and manage callbacks for various events. */ export class UploadTask { - private ref_: Reference; - private service_: StorageService; - private location_: Location; - private blob_: FbsBlob; - private metadata_: Metadata | null; - private mappings_: fbsMetadata.Mappings; - private transferred_: number = 0; - private needToFetchStatus_: boolean = false; - private needToFetchMetadata_: boolean = false; - private observers_: Array> = []; - private resumable_: boolean; - private state_: InternalTaskState; - private error_: Error | null = null; - private uploadUrl_: string | null = null; - private request_: Request | null = null; - private chunkMultiplier_: number = 1; - private errorHandler_: (p1: FirebaseStorageError) => void; - private metadataErrorHandler_: (p1: FirebaseStorageError) => void; - private resolve_: ((p1: UploadTaskSnapshot) => void) | null = null; - private reject_: ((p1: Error) => void) | null = null; - private promise_: Promise; + private _ref: Reference; + /** + * @internal + */ + _blob: FbsBlob; + /** + * @internal + */ + _metadata: Metadata | null; + private _mappings: fbsMetadata.Mappings; + /** + * @internal + */ + _transferred: number = 0; + private _needToFetchStatus: boolean = false; + private _needToFetchMetadata: boolean = false; + private _observers: Array> = []; + private _resumable: boolean; + /** + * @internal + */ + _state: InternalTaskState; + private _error?: FirebaseStorageError = undefined; + private _uploadUrl?: string = undefined; + private _request?: Request = undefined; + private _chunkMultiplier: number = 1; + private _errorHandler: (p1: FirebaseStorageError) => void; + private _metadataErrorHandler: (p1: FirebaseStorageError) => void; + private _resolve?: (p1: UploadTaskSnapshot) => void = undefined; + private _reject?: (p1: FirebaseStorageError) => void = undefined; + private _promise: Promise; /** - * @param ref The firebaseStorage.Reference object this task came + * @param ref - The firebaseStorage.Reference object this task came * from, untyped to avoid cyclic dependencies. - * @param blob The blob to upload. + * @param blob - The blob to upload. */ - constructor( - ref: Reference, - service: StorageService, - location: Location, - mappings: fbsMetadata.Mappings, - blob: FbsBlob, - metadata: Metadata | null = null - ) { - this.ref_ = ref; - this.service_ = service; - this.location_ = location; - this.blob_ = blob; - this.metadata_ = metadata; - this.mappings_ = mappings; - this.resumable_ = this.shouldDoResumable_(this.blob_); - this.state_ = InternalTaskState.RUNNING; - this.errorHandler_ = error => { - this.request_ = null; - this.chunkMultiplier_ = 1; + constructor(ref: Reference, blob: FbsBlob, metadata: Metadata | null = null) { + this._ref = ref; + this._blob = blob; + this._metadata = metadata; + this._mappings = getMappings(); + this._resumable = this._shouldDoResumable(this._blob); + this._state = InternalTaskState.RUNNING; + this._errorHandler = error => { + this._request = undefined; + this._chunkMultiplier = 1; if (error.codeEquals(Code.CANCELED)) { - this.needToFetchStatus_ = true; + this._needToFetchStatus = true; this.completeTransitions_(); } else { - this.error_ = error; - this.transition_(InternalTaskState.ERROR); + this._error = error; + this._transition(InternalTaskState.ERROR); } }; - this.metadataErrorHandler_ = error => { - this.request_ = null; + this._metadataErrorHandler = error => { + this._request = undefined; if (error.codeEquals(Code.CANCELED)) { this.completeTransitions_(); } else { - this.error_ = error; - this.transition_(InternalTaskState.ERROR); + this._error = error; + this._transition(InternalTaskState.ERROR); } }; - this.promise_ = new Promise((resolve, reject) => { - this.resolve_ = resolve; - this.reject_ = reject; - this.start_(); + this._promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + this._start(); }); // Prevent uncaught rejections on the internal promise from bubbling out // to the top level with a dummy handler. - this.promise_.then(null, () => {}); + this._promise.then(null, () => {}); } - private makeProgressCallback_(): (p1: number, p2: number) => void { - const sizeBefore = this.transferred_; - return loaded => this.updateProgress_(sizeBefore + loaded); + private _makeProgressCallback(): (p1: number, p2: number) => void { + const sizeBefore = this._transferred; + return loaded => this._updateProgress(sizeBefore + loaded); } - private shouldDoResumable_(blob: FbsBlob): boolean { + private _shouldDoResumable(blob: FbsBlob): boolean { return blob.size() > 256 * 1024; } - private start_(): void { - if (this.state_ !== InternalTaskState.RUNNING) { + private _start(): void { + if (this._state !== InternalTaskState.RUNNING) { // This can happen if someone pauses us in a resume callback, for example. return; } - if (this.request_ !== null) { + if (this._request !== undefined) { return; } - if (this.resumable_) { - if (this.uploadUrl_ === null) { - this.createResumable_(); + if (this._resumable) { + if (this._uploadUrl === undefined) { + this._createResumable(); } else { - if (this.needToFetchStatus_) { - this.fetchStatus_(); + if (this._needToFetchStatus) { + this._fetchStatus(); } else { - if (this.needToFetchMetadata_) { + if (this._needToFetchMetadata) { // Happens if we miss the metadata on upload completion. - this.fetchMetadata_(); + this._fetchMetadata(); } else { - this.continueUpload_(); + this._continueUpload(); } } } } else { - this.oneShotUpload_(); + this._oneShotUpload(); } } - private resolveToken_(callback: (p1: string | null) => void): void { + private _resolveToken(callback: (p1: string | null) => void): void { // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.service_.getAuthToken().then(authToken => { - switch (this.state_) { + this._ref.storage.getAuthToken().then(authToken => { + switch (this._state) { case InternalTaskState.RUNNING: callback(authToken); break; case InternalTaskState.CANCELING: - this.transition_(InternalTaskState.CANCELED); + this._transition(InternalTaskState.CANCELED); break; case InternalTaskState.PAUSING: - this.transition_(InternalTaskState.PAUSED); + this._transition(InternalTaskState.PAUSED); break; default: } @@ -181,161 +180,173 @@ export class UploadTask { // TODO(andysoto): assert false - private createResumable_(): void { - this.resolveToken_(authToken => { + private _createResumable(): void { + this._resolveToken(authToken => { const requestInfo = fbsRequests.createResumableUpload( - this.service_, - this.location_, - this.mappings_, - this.blob_, - this.metadata_ + this._ref.storage, + this._ref._location, + this._mappings, + this._blob, + this._metadata + ); + const createRequest = this._ref.storage.makeRequest( + requestInfo, + authToken ); - const createRequest = this.service_.makeRequest(requestInfo, authToken); - this.request_ = createRequest; + this._request = createRequest; createRequest.getPromise().then((url: string) => { - this.request_ = null; - this.uploadUrl_ = url; - this.needToFetchStatus_ = false; + this._request = undefined; + this._uploadUrl = url; + this._needToFetchStatus = false; this.completeTransitions_(); - }, this.errorHandler_); + }, this._errorHandler); }); } - private fetchStatus_(): void { + private _fetchStatus(): void { // TODO(andysoto): assert(this.uploadUrl_ !== null); - const url = this.uploadUrl_ as string; - this.resolveToken_(authToken => { + const url = this._uploadUrl as string; + this._resolveToken(authToken => { const requestInfo = fbsRequests.getResumableUploadStatus( - this.service_, - this.location_, + this._ref.storage, + this._ref._location, url, - this.blob_ + this._blob ); - const statusRequest = this.service_.makeRequest(requestInfo, authToken); - this.request_ = statusRequest; + const statusRequest = this._ref.storage.makeRequest( + requestInfo, + authToken + ); + this._request = statusRequest; statusRequest.getPromise().then(status => { status = status as fbsRequests.ResumableUploadStatus; - this.request_ = null; - this.updateProgress_(status.current); - this.needToFetchStatus_ = false; + this._request = undefined; + this._updateProgress(status.current); + this._needToFetchStatus = false; if (status.finalized) { - this.needToFetchMetadata_ = true; + this._needToFetchMetadata = true; } this.completeTransitions_(); - }, this.errorHandler_); + }, this._errorHandler); }); } - private continueUpload_(): void { + private _continueUpload(): void { const chunkSize = - fbsRequests.resumableUploadChunkSize * this.chunkMultiplier_; + fbsRequests.resumableUploadChunkSize * this._chunkMultiplier; const status = new fbsRequests.ResumableUploadStatus( - this.transferred_, - this.blob_.size() + this._transferred, + this._blob.size() ); // TODO(andysoto): assert(this.uploadUrl_ !== null); - const url = this.uploadUrl_ as string; - this.resolveToken_(authToken => { + const url = this._uploadUrl as string; + this._resolveToken(authToken => { let requestInfo; try { requestInfo = fbsRequests.continueResumableUpload( - this.location_, - this.service_, + this._ref._location, + this._ref.storage, url, - this.blob_, + this._blob, chunkSize, - this.mappings_, + this._mappings, status, - this.makeProgressCallback_() + this._makeProgressCallback() ); } catch (e) { - this.error_ = e; - this.transition_(InternalTaskState.ERROR); + this._error = e; + this._transition(InternalTaskState.ERROR); return; } - const uploadRequest = this.service_.makeRequest(requestInfo, authToken); - this.request_ = uploadRequest; + const uploadRequest = this._ref.storage.makeRequest( + requestInfo, + authToken + ); + this._request = uploadRequest; uploadRequest .getPromise() .then((newStatus: fbsRequests.ResumableUploadStatus) => { - this.increaseMultiplier_(); - this.request_ = null; - this.updateProgress_(newStatus.current); + this._increaseMultiplier(); + this._request = undefined; + this._updateProgress(newStatus.current); if (newStatus.finalized) { - this.metadata_ = newStatus.metadata; - this.transition_(InternalTaskState.SUCCESS); + this._metadata = newStatus.metadata; + this._transition(InternalTaskState.SUCCESS); } else { this.completeTransitions_(); } - }, this.errorHandler_); + }, this._errorHandler); }); } - private increaseMultiplier_(): void { + private _increaseMultiplier(): void { const currentSize = - fbsRequests.resumableUploadChunkSize * this.chunkMultiplier_; + fbsRequests.resumableUploadChunkSize * this._chunkMultiplier; // Max chunk size is 32M. if (currentSize < 32 * 1024 * 1024) { - this.chunkMultiplier_ *= 2; + this._chunkMultiplier *= 2; } } - private fetchMetadata_(): void { - this.resolveToken_(authToken => { + private _fetchMetadata(): void { + this._resolveToken(authToken => { const requestInfo = fbsRequests.getMetadata( - this.service_, - this.location_, - this.mappings_ + this._ref.storage, + this._ref._location, + this._mappings + ); + const metadataRequest = this._ref.storage.makeRequest( + requestInfo, + authToken ); - const metadataRequest = this.service_.makeRequest(requestInfo, authToken); - this.request_ = metadataRequest; + this._request = metadataRequest; metadataRequest.getPromise().then(metadata => { - this.request_ = null; - this.metadata_ = metadata; - this.transition_(InternalTaskState.SUCCESS); - }, this.metadataErrorHandler_); + this._request = undefined; + this._metadata = metadata; + this._transition(InternalTaskState.SUCCESS); + }, this._metadataErrorHandler); }); } - private oneShotUpload_(): void { - this.resolveToken_(authToken => { + private _oneShotUpload(): void { + this._resolveToken(authToken => { const requestInfo = fbsRequests.multipartUpload( - this.service_, - this.location_, - this.mappings_, - this.blob_, - this.metadata_ + this._ref.storage, + this._ref._location, + this._mappings, + this._blob, + this._metadata ); - const multipartRequest = this.service_.makeRequest( + const multipartRequest = this._ref.storage.makeRequest( requestInfo, authToken ); - this.request_ = multipartRequest; + this._request = multipartRequest; multipartRequest.getPromise().then(metadata => { - this.request_ = null; - this.metadata_ = metadata; - this.updateProgress_(this.blob_.size()); - this.transition_(InternalTaskState.SUCCESS); - }, this.errorHandler_); + this._request = undefined; + this._metadata = metadata; + this._updateProgress(this._blob.size()); + this._transition(InternalTaskState.SUCCESS); + }, this._errorHandler); }); } - private updateProgress_(transferred: number): void { - const old = this.transferred_; - this.transferred_ = transferred; + private _updateProgress(transferred: number): void { + const old = this._transferred; + this._transferred = transferred; // A progress update can make the "transferred" value smaller (e.g. a // partial upload not completed by server, after which the "transferred" // value may reset to the value at the beginning of the request). - if (this.transferred_ !== old) { - this.notifyObservers_(); + if (this._transferred !== old) { + this._notifyObservers(); } } - private transition_(state: InternalTaskState): void { - if (this.state_ === state) { + private _transition(state: InternalTaskState): void { + if (this._state === state) { return; } switch (state) { @@ -343,74 +354,74 @@ export class UploadTask { // TODO(andysoto): // assert(this.state_ === InternalTaskState.RUNNING || // this.state_ === InternalTaskState.PAUSING); - this.state_ = state; - if (this.request_ !== null) { - this.request_.cancel(); + this._state = state; + if (this._request !== undefined) { + this._request.cancel(); } break; case InternalTaskState.PAUSING: // TODO(andysoto): // assert(this.state_ === InternalTaskState.RUNNING); - this.state_ = state; - if (this.request_ !== null) { - this.request_.cancel(); + this._state = state; + if (this._request !== undefined) { + this._request.cancel(); } break; case InternalTaskState.RUNNING: // TODO(andysoto): // assert(this.state_ === InternalTaskState.PAUSED || // this.state_ === InternalTaskState.PAUSING); - const wasPaused = this.state_ === InternalTaskState.PAUSED; - this.state_ = state; + const wasPaused = this._state === InternalTaskState.PAUSED; + this._state = state; if (wasPaused) { - this.notifyObservers_(); - this.start_(); + this._notifyObservers(); + this._start(); } break; case InternalTaskState.PAUSED: // TODO(andysoto): // assert(this.state_ === InternalTaskState.PAUSING); - this.state_ = state; - this.notifyObservers_(); + this._state = state; + this._notifyObservers(); break; case InternalTaskState.CANCELED: // TODO(andysoto): // assert(this.state_ === InternalTaskState.PAUSED || // this.state_ === InternalTaskState.CANCELING); - this.error_ = canceled(); - this.state_ = state; - this.notifyObservers_(); + this._error = canceled(); + this._state = state; + this._notifyObservers(); break; case InternalTaskState.ERROR: // TODO(andysoto): // assert(this.state_ === InternalTaskState.RUNNING || // this.state_ === InternalTaskState.PAUSING || // this.state_ === InternalTaskState.CANCELING); - this.state_ = state; - this.notifyObservers_(); + this._state = state; + this._notifyObservers(); break; case InternalTaskState.SUCCESS: // TODO(andysoto): // assert(this.state_ === InternalTaskState.RUNNING || // this.state_ === InternalTaskState.PAUSING || // this.state_ === InternalTaskState.CANCELING); - this.state_ = state; - this.notifyObservers_(); + this._state = state; + this._notifyObservers(); break; default: // Ignore } } private completeTransitions_(): void { - switch (this.state_) { + switch (this._state) { case InternalTaskState.PAUSING: - this.transition_(InternalTaskState.PAUSED); + this._transition(InternalTaskState.PAUSED); break; case InternalTaskState.CANCELING: - this.transition_(InternalTaskState.CANCELED); + this._transition(InternalTaskState.CANCELED); break; case InternalTaskState.RUNNING: - this.start_(); + this._start(); break; default: // TODO(andysoto): assert(false); @@ -419,50 +430,49 @@ export class UploadTask { } get snapshot(): UploadTaskSnapshot { - const externalState = taskStateFromInternalTaskState(this.state_); + const externalState = taskStateFromInternalTaskState(this._state); return new UploadTaskSnapshot( - this.transferred_, - this.blob_.size(), + this._transferred, + this._blob.size(), externalState, - this.metadata_, + this._metadata!, this, - this.ref_ + this._ref ); } /** * Adds a callback for an event. - * @param type The type of event to listen for. + * @param type - The type of event to listen for. */ on( type: TaskEvent, nextOrObserver?: - | NextFn | StorageObserver - | null, - error?: ErrorFn | null, - completed?: CompleteFn | null + | ((a: UploadTaskSnapshot) => unknown), + error?: ErrorFn, + completed?: CompleteFn ): Unsubscribe | Subscribe { const observer = new Observer(nextOrObserver, error, completed); - this.addObserver_(observer); + this._addObserver(observer); return () => { - this.removeObserver_(observer); + this._removeObserver(observer); }; } /** * This object behaves like a Promise, and resolves with its snapshot data * when the upload completes. - * @param onFulfilled The fulfillment callback. Promise chaining works as normal. - * @param onRejected The rejection callback. + * @param onFulfilled - The fulfillment callback. Promise chaining works as normal. + * @param onRejected - The rejection callback. */ then( onFulfilled?: ((value: UploadTaskSnapshot) => U | Promise) | null, - onRejected?: ((error: Error) => U | Promise) | null + onRejected?: ((error: FirebaseStorageError) => U | Promise) | null ): Promise { // These casts are needed so that TypeScript can infer the types of the // resulting Promise. - return this.promise_.then( + return this._promise.then( onFulfilled as (value: UploadTaskSnapshot) => U | Promise, onRejected as ((error: unknown) => Promise) | null ); @@ -471,61 +481,63 @@ export class UploadTask { /** * Equivalent to calling `then(null, onRejected)`. */ - catch(onRejected: (p1: Error) => T | Promise): Promise { + catch( + onRejected: (p1: FirebaseStorageError) => T | Promise + ): Promise { return this.then(null, onRejected); } /** * Adds the given observer. */ - private addObserver_(observer: Observer): void { - this.observers_.push(observer); - this.notifyObserver_(observer); + private _addObserver(observer: Observer): void { + this._observers.push(observer); + this._notifyObserver(observer); } /** * Removes the given observer. */ - private removeObserver_(observer: Observer): void { - const i = this.observers_.indexOf(observer); + private _removeObserver(observer: Observer): void { + const i = this._observers.indexOf(observer); if (i !== -1) { - this.observers_.splice(i, 1); + this._observers.splice(i, 1); } } - private notifyObservers_(): void { - this.finishPromise_(); - const observers = this.observers_.slice(); + private _notifyObservers(): void { + this._finishPromise(); + const observers = this._observers.slice(); observers.forEach(observer => { - this.notifyObserver_(observer); + this._notifyObserver(observer); }); } - private finishPromise_(): void { - if (this.resolve_ !== null) { + private _finishPromise(): void { + if (this._resolve !== undefined) { let triggered = true; - switch (taskStateFromInternalTaskState(this.state_)) { + switch (taskStateFromInternalTaskState(this._state)) { case TaskState.SUCCESS: - fbsAsync(this.resolve_.bind(null, this.snapshot))(); + fbsAsync(this._resolve.bind(null, this.snapshot))(); break; case TaskState.CANCELED: case TaskState.ERROR: - const toCall = this.reject_ as (p1: Error) => void; - fbsAsync(toCall.bind(null, this.error_ as Error))(); + const toCall = this._reject as (p1: FirebaseStorageError) => void; + fbsAsync(toCall.bind(null, this._error as FirebaseStorageError))(); break; default: triggered = false; break; } if (triggered) { - this.resolve_ = null; - this.reject_ = null; + this._resolve = undefined; + this._reject = undefined; } } } - private notifyObserver_(observer: Observer): void { - const externalState = taskStateFromInternalTaskState(this.state_); + private _notifyObserver(observer: Observer): void { + const externalState = taskStateFromInternalTaskState(this._state); switch (externalState) { case TaskState.RUNNING: case TaskState.PAUSED: @@ -541,39 +553,43 @@ export class UploadTask { case TaskState.CANCELED: case TaskState.ERROR: if (observer.error) { - fbsAsync(observer.error.bind(observer, this.error_ as Error))(); + fbsAsync( + observer.error.bind(observer, this._error as FirebaseStorageError) + )(); } break; default: // TODO(andysoto): assert(false); if (observer.error) { - fbsAsync(observer.error.bind(observer, this.error_ as Error))(); + fbsAsync( + observer.error.bind(observer, this._error as FirebaseStorageError) + )(); } } } /** * Resumes a paused task. Has no effect on a currently running or failed task. - * @return True if the operation took effect, false if ignored. + * @returns True if the operation took effect, false if ignored. */ resume(): boolean { const valid = - this.state_ === InternalTaskState.PAUSED || - this.state_ === InternalTaskState.PAUSING; + this._state === InternalTaskState.PAUSED || + this._state === InternalTaskState.PAUSING; if (valid) { - this.transition_(InternalTaskState.RUNNING); + this._transition(InternalTaskState.RUNNING); } return valid; } /** * Pauses a currently running task. Has no effect on a paused or failed task. - * @return True if the operation took effect, false if ignored. + * @returns True if the operation took effect, false if ignored. */ pause(): boolean { - const valid = this.state_ === InternalTaskState.RUNNING; + const valid = this._state === InternalTaskState.RUNNING; if (valid) { - this.transition_(InternalTaskState.PAUSING); + this._transition(InternalTaskState.PAUSING); } return valid; } @@ -581,14 +597,14 @@ export class UploadTask { /** * Cancels a currently running or paused task. Has no effect on a complete or * failed task. - * @return True if the operation took effect, false if ignored. + * @returns True if the operation took effect, false if ignored. */ cancel(): boolean { const valid = - this.state_ === InternalTaskState.RUNNING || - this.state_ === InternalTaskState.PAUSING; + this._state === InternalTaskState.RUNNING || + this._state === InternalTaskState.PAUSING; if (valid) { - this.transition_(InternalTaskState.CANCELING); + this._transition(InternalTaskState.CANCELING); } return valid; } diff --git a/packages/storage/src/tasksnapshot.ts b/packages/storage/src/tasksnapshot.ts index 7524ee4bbc4..6f3395fd567 100644 --- a/packages/storage/src/tasksnapshot.ts +++ b/packages/storage/src/tasksnapshot.ts @@ -24,7 +24,7 @@ export class UploadTaskSnapshot { readonly bytesTransferred: number, readonly totalBytes: number, readonly state: TaskState, - readonly metadata: Metadata | null, + readonly metadata: Metadata, readonly task: UploadTask, readonly ref: Reference ) {} diff --git a/packages/storage/test/integration/integration.test.ts b/packages/storage/test/integration/integration.test.ts index 931ee648fa8..ba4a0cf07db 100644 --- a/packages/storage/test/integration/integration.test.ts +++ b/packages/storage/test/integration/integration.test.ts @@ -23,7 +23,7 @@ import '@firebase/auth'; import * as storage from '@firebase/storage-types'; import { expect } from 'chai'; -import '../../index'; +import '../../compat/index'; // eslint-disable-next-line @typescript-eslint/no-require-imports const PROJECT_CONFIG = require('../../../../config/project.json'); diff --git a/packages/storage/test/unit/reference.test.ts b/packages/storage/test/unit/reference.compat.test.ts similarity index 56% rename from packages/storage/test/unit/reference.test.ts rename to packages/storage/test/unit/reference.compat.test.ts index d1955500a2a..46e07c2544f 100644 --- a/packages/storage/test/unit/reference.test.ts +++ b/packages/storage/test/unit/reference.compat.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google LLC + * 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. @@ -14,35 +14,45 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { assert } from 'chai'; +import { expect } from 'chai'; import { FirebaseApp } from '@firebase/app-types'; import { StringFormat } from '../../src/implementation/string'; import { Headers } from '../../src/implementation/xhrio'; import { Metadata } from '../../src/metadata'; -import { Reference } from '../../src/reference'; -import { StorageService } from '../../src/service'; +import { ReferenceCompat } from '../../compat/reference'; +import { StorageServiceCompat } from '../../compat/service'; import * as testShared from './testshared'; import { SendHook, TestingXhrIo } from './xhrio'; import { DEFAULT_HOST } from '../../src/implementation/constants'; import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { Provider } from '@firebase/component'; +import { StorageService } from '../../src/service'; +import { Reference } from '../../src/reference'; /* eslint-disable @typescript-eslint/no-floating-promises */ function makeFakeService( app: FirebaseApp, authProvider: Provider, sendHook: SendHook -): StorageService { - return new StorageService(app, authProvider, testShared.makePool(sendHook)); +): StorageServiceCompat { + const storageServiceCompat: StorageServiceCompat = new StorageServiceCompat( + app, + new StorageService(app, authProvider, testShared.makePool(sendHook)) + ); + return storageServiceCompat; } -function makeStorage(url: string): Reference { +function makeStorage(url: string): ReferenceCompat { const service = new StorageService( - null, + {} as FirebaseApp, testShared.emptyAuthProvider, testShared.makePool(null) ); - return new Reference(service, url); + const storageServiceCompat: StorageServiceCompat = new StorageServiceCompat( + {} as FirebaseApp, + service + ); + return new ReferenceCompat(new Reference(service, url), storageServiceCompat); } describe('Firebase Storage > Reference', () => { @@ -50,28 +60,28 @@ describe('Firebase Storage > Reference', () => { const child = makeStorage('gs://test-bucket/hello'); describe('Path constructor', () => { it('root', () => { - assert.equal(root.toString(), 'gs://test-bucket/'); + expect(root.toString()).to.equal('gs://test-bucket/'); }); it('keeps characters after ? on a gs:// string', () => { const s = makeStorage('gs://test-bucket/this/ismyobject?hello'); - assert.equal(s.toString(), 'gs://test-bucket/this/ismyobject?hello'); + expect(s.toString()).to.equal('gs://test-bucket/this/ismyobject?hello'); }); it("doesn't URL-decode on a gs:// string", () => { const s = makeStorage('gs://test-bucket/%3F'); - assert.equal(s.toString(), 'gs://test-bucket/%3F'); + expect(s.toString()).to.equal('gs://test-bucket/%3F'); }); it('ignores URL params and fragments on an http URL', () => { const s = makeStorage( `http://${DEFAULT_HOST}/v0/b/test-bucket/o/my/object.txt` + '?ignoreme#please' ); - assert.equal(s.toString(), 'gs://test-bucket/my/object.txt'); + expect(s.toString()).to.equal('gs://test-bucket/my/object.txt'); }); it('URL-decodes and ignores fragment on an http URL', () => { const s = makeStorage( `http://${DEFAULT_HOST}/v0/b/test-bucket/o/%3F?ignore` ); - assert.equal(s.toString(), 'gs://test-bucket/?'); + expect(s.toString()).to.equal('gs://test-bucket/?'); }); it('ignores URL params and fragments on an https URL', () => { @@ -79,95 +89,93 @@ describe('Firebase Storage > Reference', () => { `https://${DEFAULT_HOST}/v0/b/test-bucket/o/my/object.txt` + '?ignoreme#please' ); - assert.equal(s.toString(), 'gs://test-bucket/my/object.txt'); + expect(s.toString()).to.equal('gs://test-bucket/my/object.txt'); }); it('URL-decodes and ignores fragment on an https URL', () => { const s = makeStorage( `https://${DEFAULT_HOST}/v0/b/test-bucket/o/%3F?ignore` ); - assert.equal(s.toString(), 'gs://test-bucket/?'); + expect(s.toString()).to.equal('gs://test-bucket/?'); }); }); describe('toString', () => { it("Doesn't add trailing slash", () => { const s = makeStorage('gs://test-bucket/foo'); - assert.equal(s.toString(), 'gs://test-bucket/foo'); + expect(s.toString()).to.equal('gs://test-bucket/foo'); }); it('Strips trailing slash', () => { const s = makeStorage('gs://test-bucket/foo/'); - assert.equal(s.toString(), 'gs://test-bucket/foo'); + expect(s.toString()).to.equal('gs://test-bucket/foo'); }); }); describe('parent', () => { it('Returns null at root', () => { - assert.isNull(root.parent); + expect(root.parent).to.be.null; }); it('Returns root one level down', () => { - assert.equal(child.parent!.toString(), 'gs://test-bucket/'); + expect(child.parent!.toString()).to.equal('gs://test-bucket/'); }); it('Works correctly with empty levels', () => { const s = makeStorage('gs://test-bucket/a///'); - assert.equal(s.parent!.toString(), 'gs://test-bucket/a/'); + expect(s.parent!.toString()).to.equal('gs://test-bucket/a/'); }); }); describe('root', () => { it('Returns self at root', () => { - assert.equal(root.root.toString(), 'gs://test-bucket/'); + expect(root.root.toString()).to.equal('gs://test-bucket/'); }); it('Returns root multiple levels down', () => { const s = makeStorage('gs://test-bucket/a/b/c/d'); - assert.equal(s.root.toString(), 'gs://test-bucket/'); + expect(s.root.toString()).to.equal('gs://test-bucket/'); }); }); describe('bucket', () => { it('Returns bucket name', () => { - assert.equal(root.bucket, 'test-bucket'); + expect(root.bucket).to.equal('test-bucket'); }); }); describe('fullPath', () => { it('Returns full path without leading slash', () => { const s = makeStorage('gs://test-bucket/full/path'); - assert.equal(s.fullPath, 'full/path'); + expect(s.fullPath).to.equal('full/path'); }); }); describe('name', () => { it('Works at top level', () => { const s = makeStorage('gs://test-bucket/toplevel.txt'); - assert.equal(s.name, 'toplevel.txt'); + expect(s.name).to.equal('toplevel.txt'); }); it('Works at not the top level', () => { const s = makeStorage('gs://test-bucket/not/toplevel.txt'); - assert.equal('toplevel.txt', s.name); + expect('toplevel.txt').to.equal(s.name); }); }); describe('child', () => { it('works with a simple string', () => { - assert.equal(root.child('a').toString(), 'gs://test-bucket/a'); + expect(root.child('a').toString()).to.equal('gs://test-bucket/a'); }); it('drops a trailing slash', () => { - assert.equal(root.child('ab/').toString(), 'gs://test-bucket/ab'); + expect(root.child('ab/').toString()).to.equal('gs://test-bucket/ab'); }); it('compresses repeated slashes', () => { - assert.equal( - root.child('//a///b/////').toString(), + expect(root.child('//a///b/////').toString()).to.equal( 'gs://test-bucket/a/b' ); }); it('works chained multiple times with leading slashes', () => { - assert.equal( - root.child('a').child('/b').child('c').child('d/e').toString(), - 'gs://test-bucket/a/b/c/d/e' - ); + expect( + root.child('a').child('/b').child('c').child('d/e').toString() + ).to.equal('gs://test-bucket/a/b/c/d/e'); }); }); @@ -179,8 +187,8 @@ describe('Firebase Storage > Reference', () => { body?: ArrayBufferView | Blob | string | null, headers?: Headers ): void { - assert.isDefined(headers); - assert.isUndefined(headers!['Authorization']); + expect(headers).to.not.be.undefined; + expect(headers!['Authorization']).to.be.undefined; done(); } @@ -202,9 +210,8 @@ describe('Firebase Storage > Reference', () => { body?: ArrayBufferView | Blob | string | null, headers?: Headers ): void { - assert.isDefined(headers); - assert.equal( - headers!['Authorization'], + expect(headers).to.not.be.undefined; + expect(headers!['Authorization']).to.equal( 'Firebase ' + testShared.authToken ); done(); @@ -225,7 +232,7 @@ describe('Firebase Storage > Reference', () => { const task = child.putString('hello', StringFormat.RAW, { contentType: 'lol/wut' } as Metadata); - assert.equal(task.snapshot.metadata!.contentType, 'lol/wut'); + expect(task.snapshot.metadata!.contentType).to.equal('lol/wut'); task.cancel(); }); it('Uses embedded content type in DATA_URL format', () => { @@ -233,7 +240,7 @@ describe('Firebase Storage > Reference', () => { 'data:lol/wat;base64,aaaa', StringFormat.DATA_URL ); - assert.equal(task.snapshot.metadata!.contentType, 'lol/wat'); + expect(task.snapshot.metadata!.contentType).to.equal('lol/wat'); task.cancel(); }); it('Lets metadata.contentType override embedded content type in DATA_URL format', () => { @@ -242,7 +249,7 @@ describe('Firebase Storage > Reference', () => { StringFormat.DATA_URL, { contentType: 'tomato/soup' } as Metadata ); - assert.equal(task.snapshot.metadata!.contentType, 'tomato/soup'); + expect(task.snapshot.metadata!.contentType).to.equal('tomato/soup'); task.cancel(); }); }); @@ -250,133 +257,47 @@ describe('Firebase Storage > Reference', () => { describe('Argument verification', () => { describe('list', () => { it('throws on invalid maxResults', () => { - testShared.assertThrows( - testShared.bind(child.list, child, { maxResults: 0 }), - 'storage/invalid-argument' - ); - testShared.assertThrows( - testShared.bind(child.list, child, { maxResults: -4 }), - 'storage/invalid-argument' - ); - testShared.assertThrows( - testShared.bind(child.list, child, { maxResults: 1001 }), - 'storage/invalid-argument' - ); - }); - }); - }); - - describe('non-root operations', () => { - it("put doesn't throw", () => { - assert.doesNotThrow(() => { - child.put(new Blob(['a'])); - child.put(new Uint8Array(10)); - child.put(new ArrayBuffer(10)); - }); - }); - it("putString doesn't throw", () => { - assert.doesNotThrow(() => { - child.putString('raw', StringFormat.RAW); - child.putString('aaaa', StringFormat.BASE64); - child.putString('aaaa', StringFormat.BASE64URL); - child.putString( - 'data:application/octet-stream;base64,aaaa', - StringFormat.DATA_URL - ); - }); - }); - it("delete doesn't throw", () => { - assert.doesNotThrow(() => { - child.delete(); - }); - }); - it("getMetadata doesn't throw", () => { - assert.doesNotThrow(() => { - child.getMetadata(); - }); - }); - it("listAll doesn't throw", () => { - assert.doesNotThrow(() => { - child.listAll(); - }); - }); - it("list doesn't throw", () => { - assert.doesNotThrow(() => { - child.list(); - }); - assert.doesNotThrow(() => { - child.list({ pageToken: 'xxx', maxResults: 4 }); - }); - assert.doesNotThrow(() => { - child.list({ pageToken: 'xxx' }); - }); - assert.doesNotThrow(() => { - child.list({ maxResults: 4 }); - }); - assert.doesNotThrow(() => { - child.list({ maxResults: 4, pageToken: null }); - }); - }); - it("updateMetadata doesn't throw", () => { - assert.doesNotThrow(() => { - child.updateMetadata({} as Metadata); - }); - }); - it("getDownloadURL doesn't throw", () => { - assert.doesNotThrow(() => { - child.getDownloadURL(); + it('throws on invalid maxResults', async () => { + await expect(child.list({ maxResults: 0 })).to.be.rejectedWith( + 'storage/invalid-argument' + ); + await expect(child.list({ maxResults: -4 })).to.be.rejectedWith( + 'storage/invalid-argument' + ); + await expect(child.list({ maxResults: 1001 })).to.be.rejectedWith( + 'storage/invalid-argument' + ); + }); }); }); }); describe('root operations', () => { it('put throws', () => { - testShared.assertThrows( - root.put.bind(root, new Blob(['a'])), + expect(() => root.put(new Blob(['a']))).to.throw( 'storage/invalid-root-operation' ); }); it('putString throws', () => { - testShared.assertThrows( - root.putString.bind(root, 'raw', StringFormat.RAW), + expect(() => root.putString('raw', StringFormat.RAW)).to.throw( 'storage/invalid-root-operation' ); }); it('delete throws', () => { - testShared.assertThrows( - root.delete.bind(root), - 'storage/invalid-root-operation' - ); + expect(() => root.delete()).to.throw('storage/invalid-root-operation'); }); - it('getMetadata throws', () => { - testShared.assertThrows( - root.getMetadata.bind(root), + it('getMetadata throws', async () => { + await expect(root.getMetadata()).to.be.rejectedWith( 'storage/invalid-root-operation' ); }); - it("listAll doesn't throw", () => { - assert.doesNotThrow(() => { - root.listAll(); - }); - }); - it("list doesn't throw", () => { - assert.doesNotThrow(() => { - root.list(); - }); - assert.doesNotThrow(() => { - root.list({ pageToken: 'xxx', maxResults: 4 }); - }); - }); - it('updateMetadata throws', () => { - testShared.assertThrows( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (root as any).updateMetadata.bind(root, {}), + it('updateMetadata throws', async () => { + await expect(root.updateMetadata({} as Metadata)).to.be.rejectedWith( 'storage/invalid-root-operation' ); }); - it('getDownloadURL throws', () => { - testShared.assertThrows( - root.getDownloadURL.bind(root), + it('getDownloadURL throws', async () => { + await expect(root.getDownloadURL()).to.be.rejectedWith( 'storage/invalid-root-operation' ); }); diff --git a/packages/storage/test/unit/reference.exp.test.ts b/packages/storage/test/unit/reference.exp.test.ts new file mode 100644 index 00000000000..0ee0cfb6b9f --- /dev/null +++ b/packages/storage/test/unit/reference.exp.test.ts @@ -0,0 +1,306 @@ +/** + * @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 { expect } from 'chai'; +import { FirebaseApp } from '@firebase/app-types'; +import { StringFormat } from '../../src/implementation/string'; +import { Headers } from '../../src/implementation/xhrio'; +import { Metadata } from '../../src/metadata'; +import { + Reference, + uploadString, + uploadBytesResumable, + deleteObject, + list, + getMetadata, + updateMetadata, + getDownloadURL +} from '../../src/reference'; +import { StorageService, ref } from '../../src/service'; +import * as testShared from './testshared'; +import { SendHook, TestingXhrIo } from './xhrio'; +import { DEFAULT_HOST } from '../../src/implementation/constants'; +import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; +import { Provider } from '@firebase/component'; + +/* eslint-disable @typescript-eslint/no-floating-promises */ +function makeFakeService( + app: FirebaseApp, + authProvider: Provider, + sendHook: SendHook +): StorageService { + return new StorageService(app, authProvider, testShared.makePool(sendHook)); +} + +function makeStorage(url: string): Reference { + const service = new StorageService( + {} as FirebaseApp, + testShared.emptyAuthProvider, + testShared.makePool(null) + ); + return new Reference(service, url); +} + +describe('Firebase Storage > Reference', () => { + const root = makeStorage('gs://test-bucket/'); + const child = makeStorage('gs://test-bucket/hello'); + describe('Path constructor', () => { + it('root', () => { + expect(root.toString()).to.equal('gs://test-bucket/'); + }); + it('keeps characters after ? on a gs:// string', () => { + const s = makeStorage('gs://test-bucket/this/ismyobject?hello'); + expect(s.toString()).to.equal('gs://test-bucket/this/ismyobject?hello'); + }); + it("doesn't URL-decode on a gs:// string", () => { + const s = makeStorage('gs://test-bucket/%3F'); + expect(s.toString()).to.equal('gs://test-bucket/%3F'); + }); + it('ignores URL params and fragments on an http URL', () => { + const s = makeStorage( + `http://${DEFAULT_HOST}/v0/b/test-bucket/o/my/object.txt` + + '?ignoreme#please' + ); + expect(s.toString()).to.equal('gs://test-bucket/my/object.txt'); + }); + it('URL-decodes and ignores fragment on an http URL', () => { + const s = makeStorage( + `http://${DEFAULT_HOST}/v0/b/test-bucket/o/%3F?ignore` + ); + expect(s.toString()).to.equal('gs://test-bucket/?'); + }); + + it('ignores URL params and fragments on an https URL', () => { + const s = makeStorage( + `https://${DEFAULT_HOST}/v0/b/test-bucket/o/my/object.txt` + + '?ignoreme#please' + ); + expect(s.toString()).to.equal('gs://test-bucket/my/object.txt'); + }); + + it('URL-decodes and ignores fragment on an https URL', () => { + const s = makeStorage( + `https://${DEFAULT_HOST}/v0/b/test-bucket/o/%3F?ignore` + ); + expect(s.toString()).to.equal('gs://test-bucket/?'); + }); + }); + + describe('toString', () => { + it("Doesn't add trailing slash", () => { + const s = makeStorage('gs://test-bucket/foo'); + expect(s.toString()).to.equal('gs://test-bucket/foo'); + }); + it('Strips trailing slash', () => { + const s = makeStorage('gs://test-bucket/foo/'); + expect(s.toString()).to.equal('gs://test-bucket/foo'); + }); + }); + + describe('parentReference', () => { + it('Returns null at root', () => { + expect(root.parent).to.be.null; + }); + it('Returns root one level down', () => { + expect(child.parent!.toString()).to.equal('gs://test-bucket/'); + }); + it('Works correctly with empty levels', () => { + const s = makeStorage('gs://test-bucket/a///'); + expect(s.parent!.toString()).to.equal('gs://test-bucket/a/'); + }); + }); + + describe('root', () => { + it('Returns self at root', () => { + expect(root.root.toString()).to.equal('gs://test-bucket/'); + }); + + it('Returns root multiple levels down', () => { + const s = makeStorage('gs://test-bucket/a/b/c/d'); + expect(s.root.toString()).to.equal('gs://test-bucket/'); + }); + }); + + describe('bucket', () => { + it('Returns bucket name', () => { + expect(root.bucket).to.equal('test-bucket'); + }); + }); + + describe('fullPath', () => { + it('Returns full path without leading slash', () => { + const s = makeStorage('gs://test-bucket/full/path'); + expect(s.fullPath).to.equal('full/path'); + }); + }); + + describe('name', () => { + it('Works at top level', () => { + const s = makeStorage('gs://test-bucket/toplevel.txt'); + expect(s.name).to.equal('toplevel.txt'); + }); + + it('Works at not the top level', () => { + const s = makeStorage('gs://test-bucket/not/toplevel.txt'); + expect(s.name).to.equal('toplevel.txt'); + }); + }); + + describe('get child with ref()', () => { + it('works with a simple string', () => { + expect(ref(root, 'a').toString()).to.equal('gs://test-bucket/a'); + }); + it('drops a trailing slash', () => { + expect(ref(root, 'ab/').toString()).to.equal('gs://test-bucket/ab'); + }); + it('compresses repeated slashes', () => { + expect(ref(root, '//a///b/////').toString()).to.equal( + 'gs://test-bucket/a/b' + ); + }); + it('works chained multiple times with leading slashes', () => { + expect( + ref(ref(ref(ref(root, 'a'), '/b'), 'c'), 'd/e').toString() + ).to.equal('gs://test-bucket/a/b/c/d/e'); + }); + }); + + it("Doesn't send Authorization on null auth token", done => { + function newSend( + xhrio: TestingXhrIo, + url: string, + method: string, + body?: ArrayBufferView | Blob | string | null, + headers?: Headers + ): void { + expect(headers).to.not.be.undefined; + expect(headers!['Authorization']).to.be.undefined; + done(); + } + + const service = makeFakeService( + testShared.fakeApp, + testShared.emptyAuthProvider, + newSend + ); + const reference = ref(service, 'gs://test-bucket'); + getMetadata(ref(reference, 'foo')); + }); + + it('Works if the user logs in before creating the storage reference', done => { + // Regression test for b/27227221 + function newSend( + xhrio: TestingXhrIo, + url: string, + method: string, + body?: ArrayBufferView | Blob | string | null, + headers?: Headers + ): void { + expect(headers).to.not.be.undefined; + expect(headers!['Authorization']).to.equal( + 'Firebase ' + testShared.authToken + ); + done(); + } + + const service = makeFakeService( + testShared.fakeApp, + testShared.fakeAuthProvider, + newSend + ); + const reference = ref(service, 'gs://test-bucket'); + getMetadata(ref(reference, 'foo')); + }); + + describe('uploadString', () => { + it('Uses metadata.contentType for RAW format', () => { + // Regression test for b/30989476 + const task = uploadString(child, 'hello', StringFormat.RAW, { + contentType: 'lol/wut' + } as Metadata); + expect(task.snapshot.metadata!.contentType).to.equal('lol/wut'); + task.cancel(); + }); + it('Uses embedded content type in DATA_URL format', () => { + const task = uploadString( + child, + 'data:lol/wat;base64,aaaa', + StringFormat.DATA_URL + ); + expect(task.snapshot.metadata!.contentType).to.equal('lol/wat'); + task.cancel(); + }); + it('Lets metadata.contentType override embedded content type in DATA_URL format', () => { + const task = uploadString( + child, + 'data:ignore/me;base64,aaaa', + StringFormat.DATA_URL, + { contentType: 'tomato/soup' } as Metadata + ); + expect(task.snapshot.metadata!.contentType).to.equal('tomato/soup'); + task.cancel(); + }); + }); + + describe('Argument verification', () => { + describe('list', () => { + it('throws on invalid maxResults', async () => { + await expect(list(child, { maxResults: 0 })).to.be.rejectedWith( + 'storage/invalid-argument' + ); + await expect(list(child, { maxResults: -4 })).to.be.rejectedWith( + 'storage/invalid-argument' + ); + await expect(list(child, { maxResults: 1001 })).to.be.rejectedWith( + 'storage/invalid-argument' + ); + }); + }); + }); + + describe('root operations', () => { + it('uploadBytesResumable throws', () => { + expect(() => uploadBytesResumable(root, new Blob(['a']))).to.throw( + 'storage/invalid-root-operation' + ); + }); + it('uploadString throws', () => { + expect(() => uploadString(root, 'raw', StringFormat.RAW)).to.throw( + 'storage/invalid-root-operation' + ); + }); + it('deleteObject throws', async () => { + await expect(deleteObject(root)).to.be.rejectedWith( + 'storage/invalid-root-operation' + ); + }); + it('getMetadata throws', async () => { + await expect(getMetadata(root)).to.be.rejectedWith( + 'storage/invalid-root-operation' + ); + }); + it('updateMetadata throws', async () => { + await expect(updateMetadata(root, {} as Metadata)).to.be.rejectedWith( + 'storage/invalid-root-operation' + ); + }); + it('getDownloadURL throws', async () => { + await expect(getDownloadURL(root)).to.be.rejectedWith( + 'storage/invalid-root-operation' + ); + }); + }); +}); diff --git a/packages/storage/test/unit/service.test.ts b/packages/storage/test/unit/service.compat.test.ts similarity index 72% rename from packages/storage/test/unit/service.test.ts rename to packages/storage/test/unit/service.compat.test.ts index 595add06579..a7502f531ca 100644 --- a/packages/storage/test/unit/service.test.ts +++ b/packages/storage/test/unit/service.compat.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google LLC + * 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. @@ -14,13 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { assert } from 'chai'; +import { expect } from 'chai'; import { TaskEvent } from '../../src/implementation/taskenums'; import { XhrIoPool } from '../../src/implementation/xhriopool'; -import { StorageService } from '../../src/service'; +import { StorageServiceCompat } from '../../compat/service'; import * as testShared from './testshared'; import { DEFAULT_HOST } from '../../src/implementation/constants'; import { FirebaseStorageError } from '../../src/implementation/error'; +import { StorageService } from '../../src/service'; +import { FirebaseApp } from '@firebase/app-types'; +import { Provider } from '@firebase/component'; +import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; const fakeAppGs = testShared.makeFakeApp('gs://mybucket'); const fakeAppGsEndingSlash = testShared.makeFakeApp('gs://mybucket/'); @@ -31,192 +35,200 @@ function makeGsUrl(child: string = ''): string { return 'gs://' + testShared.bucket + '/' + child; } +function makeService( + app: FirebaseApp, + authProvider: Provider, + pool: XhrIoPool, + url?: string +): StorageServiceCompat { + const storageServiceCompat: StorageServiceCompat = new StorageServiceCompat( + app, + new StorageService(app, authProvider, pool, url) + ); + return storageServiceCompat; +} + describe('Firebase Storage > Service', () => { describe('simple constructor', () => { - const service = new StorageService( + const service = makeService( testShared.fakeApp, testShared.fakeAuthProvider, xhrIoPool ); it('Root refs point to the right place', () => { const ref = service.ref(); - assert.equal(ref.toString(), makeGsUrl()); + expect(ref.toString()).to.equal(makeGsUrl()); }); it('Child refs point to the right place', () => { const ref = service.ref('path/to/child'); - assert.equal(ref.toString(), makeGsUrl('path/to/child')); + expect(ref.toString()).to.equal(makeGsUrl('path/to/child')); }); it('Throws calling ref with a gs:// URL', () => { const error = testShared.assertThrows(() => { service.ref('gs://bucket/object'); }, 'storage/invalid-argument'); - assert.match(error.message, /refFromURL/); + expect(error.message).to.match(/refFromURL/); }); it('Throws calling ref with an http:// URL', () => { const error = testShared.assertThrows(() => { service.ref(`http://${DEFAULT_HOST}/etc`); }, 'storage/invalid-argument'); - assert.match(error.message, /refFromURL/); + expect(error.message).to.match(/refFromURL/); }); it('Throws calling ref with an https:// URL', () => { const error = testShared.assertThrows(() => { service.ref(`https://${DEFAULT_HOST}/etc`); }, 'storage/invalid-argument'); - assert.match(error.message, /refFromURL/); + expect(error.message).to.match(/refFromURL/); }); }); describe('custom bucket constructor', () => { it('gs:// custom bucket constructor refs point to the right place', () => { - const service = new StorageService( + const service = makeService( testShared.fakeApp, testShared.fakeAuthProvider, xhrIoPool, 'gs://foo-bar.appspot.com' ); const ref = service.ref(); - assert.equal(ref.toString(), 'gs://foo-bar.appspot.com/'); + expect(ref.toString()).to.equal('gs://foo-bar.appspot.com/'); }); it('http:// custom bucket constructor refs point to the right place', () => { - const service = new StorageService( + const service = makeService( testShared.fakeApp, testShared.fakeAuthProvider, xhrIoPool, `http://${DEFAULT_HOST}/v1/b/foo-bar.appspot.com/o` ); const ref = service.ref(); - assert.equal(ref.toString(), 'gs://foo-bar.appspot.com/'); + expect(ref.toString()).to.equal('gs://foo-bar.appspot.com/'); }); it('https:// custom bucket constructor refs point to the right place', () => { - const service = new StorageService( + const service = makeService( testShared.fakeApp, testShared.fakeAuthProvider, xhrIoPool, `https://${DEFAULT_HOST}/v1/b/foo-bar.appspot.com/o` ); const ref = service.ref(); - assert.equal(ref.toString(), 'gs://foo-bar.appspot.com/'); + expect(ref.toString()).to.equal('gs://foo-bar.appspot.com/'); }); it('Bare bucket name constructor refs point to the right place', () => { - const service = new StorageService( + const service = makeService( testShared.fakeApp, testShared.fakeAuthProvider, xhrIoPool, 'foo-bar.appspot.com' ); const ref = service.ref(); - assert.equal(ref.toString(), 'gs://foo-bar.appspot.com/'); + expect(ref.toString()).to.equal('gs://foo-bar.appspot.com/'); }); it('Child refs point to the right place', () => { - const service = new StorageService( + const service = makeService( testShared.fakeApp, testShared.fakeAuthProvider, xhrIoPool, 'foo-bar.appspot.com' ); const ref = service.ref('path/to/child'); - assert.equal(ref.toString(), 'gs://foo-bar.appspot.com/path/to/child'); + expect(ref.toString()).to.equal('gs://foo-bar.appspot.com/path/to/child'); }); it('Throws trying to construct with a gs:// URL containing an object path', () => { const error = testShared.assertThrows(() => { - new StorageService( + makeService( testShared.fakeApp, testShared.fakeAuthProvider, xhrIoPool, 'gs://bucket/object/' ); }, 'storage/invalid-default-bucket'); - assert.match(error.message, /Invalid default bucket/); + expect(error.message).to.match(/Invalid default bucket/); }); }); describe('default bucket config', () => { it('gs:// works without ending slash', () => { - const service = new StorageService( + const service = makeService( fakeAppGs, testShared.fakeAuthProvider, xhrIoPool ); - assert.equal(service.ref().toString(), 'gs://mybucket/'); + expect(service.ref().toString()).to.equal('gs://mybucket/'); }); it('gs:// works with ending slash', () => { - const service = new StorageService( + const service = makeService( fakeAppGsEndingSlash, testShared.fakeAuthProvider, xhrIoPool ); - assert.equal(service.ref().toString(), 'gs://mybucket/'); + expect(service.ref().toString()).to.equal('gs://mybucket/'); }); it('Throws when config bucket is gs:// with an object path', () => { testShared.assertThrows(() => { - new StorageService( - fakeAppInvalidGs, - testShared.fakeAuthProvider, - xhrIoPool - ); + makeService(fakeAppInvalidGs, testShared.fakeAuthProvider, xhrIoPool); }, 'storage/invalid-default-bucket'); }); }); describe('refFromURL', () => { - const service = new StorageService( + const service = makeService( testShared.fakeApp, testShared.fakeAuthProvider, xhrIoPool ); - it('Throws on non-URL arg', () => { - const error = testShared.assertThrows(() => { - service.refFromURL('path/to/child'); - }, 'storage/invalid-argument'); - assert.match( - error.message, - /Expected full URL but got a child path, use ref instead/i - ); - }); it('Works with gs:// URLs', () => { const ref = service.refFromURL('gs://mybucket/child/path/image.png'); - assert.equal(ref.toString(), 'gs://mybucket/child/path/image.png'); + expect(ref.toString()).to.equal('gs://mybucket/child/path/image.png'); }); it('Works with http:// URLs', () => { const ref = service.refFromURL( `http://${DEFAULT_HOST}/v0/b/` + 'mybucket/o/child%2Fpath%2Fimage.png?downloadToken=hello' ); - assert.equal(ref.toString(), 'gs://mybucket/child/path/image.png'); + expect(ref.toString()).to.equal('gs://mybucket/child/path/image.png'); }); it('Works with https:// URLs', () => { const ref = service.refFromURL( `https://${DEFAULT_HOST}/v0/b/` + 'mybucket/o/child%2Fpath%2Fimage.png?downloadToken=hello' ); - assert.equal(ref.toString(), 'gs://mybucket/child/path/image.png'); + expect(ref.toString()).to.equal('gs://mybucket/child/path/image.png'); }); it('Works with storage.googleapis.com URLs', () => { const ref = service.refFromURL( `https://storage.googleapis.com/mybucket/path%20with%20space/image.png` ); - assert.equal(ref.toString(), 'gs://mybucket/path with space/image.png'); + expect(ref.toString()).to.equal( + 'gs://mybucket/path with space/image.png' + ); }); it('Works with storage.googleapis.com URLs with query params', () => { const ref = service.refFromURL( `https://storage.googleapis.com/mybucket/path%20with%20space/image.png?X-Goog-Algorithm= GOOG4-RSA-SHA256` ); - assert.equal(ref.toString(), 'gs://mybucket/path with space/image.png'); + expect(ref.toString()).to.equal( + 'gs://mybucket/path with space/image.png' + ); }); it('Works with storage.cloud.google.com URLs', () => { const ref = service.refFromURL( `https://storage.cloud.google.com/mybucket/path%20with%20space/image.png` ); - assert.equal(ref.toString(), 'gs://mybucket/path with space/image.png'); + expect(ref.toString()).to.equal( + 'gs://mybucket/path with space/image.png' + ); }); it('Works with storage.cloud.google.com URLs and escaped slash', () => { const ref = service.refFromURL( `https://storage.cloud.google.com/mybucket/path%20with%20space%2Fimage.png` ); - assert.equal(ref.toString(), 'gs://mybucket/path with space/image.png'); + expect(ref.toString()).to.equal( + 'gs://mybucket/path with space/image.png' + ); }); }); describe('Argument verification', () => { - const service = new StorageService( + const service = makeService( testShared.fakeApp, testShared.fakeAuthProvider, xhrIoPool @@ -230,6 +242,15 @@ GOOG4-RSA-SHA256` }); }); describe('refFromURL', () => { + it('Throws with a non-URL string arg', () => { + const error = testShared.assertThrows( + testShared.bind(service.refFromURL, service, 'child'), + 'storage/invalid-argument' + ); + expect(error.message).to.match( + /expected a full URL but got a child path/i + ); + }); it('Throws with an invalid URL arg', () => { testShared.assertThrows( testShared.bind(service.refFromURL, service, 'notlegit://url'), @@ -256,38 +277,24 @@ GOOG4-RSA-SHA256` }); describe('Deletion', () => { - const service = new StorageService( + const service = makeService( testShared.fakeApp, testShared.fakeAuthProvider, xhrIoPool ); - it('In-flight requests are canceled when the service is deleted', () => { + it('In-flight requests are canceled when the service is deleted', async () => { const ref = service.refFromURL('gs://mybucket/image.jpg'); - const toReturn = ref.getMetadata().then( - () => { - assert.fail('Promise succeeded, should have been canceled'); - }, - err => { - assert.equal(err.code, 'storage/app-deleted'); - } - ); + const metadataPromise = ref.getMetadata(); // eslint-disable-next-line @typescript-eslint/no-floating-promises service.INTERNAL.delete(); - return toReturn; + await expect(metadataPromise).to.be.rejectedWith('storage/app-deleted'); }); - it('Requests fail when started after the service is deleted', () => { + it('Requests fail when started after the service is deleted', async () => { const ref = service.refFromURL('gs://mybucket/image.jpg'); // eslint-disable-next-line @typescript-eslint/no-floating-promises service.INTERNAL.delete(); - const toReturn = ref.getMetadata().then( - () => { - assert.fail('Promise succeeded, should have been canceled'); - }, - err => { - assert.equal(err.code, 'storage/app-deleted'); - } - ); - return toReturn; + + await expect(ref.getMetadata()).to.be.rejectedWith('storage/app-deleted'); }); it('Running uploads fail when the service is deleted', () => { const ref = service.refFromURL('gs://mybucket/image.jpg'); @@ -296,14 +303,13 @@ GOOG4-RSA-SHA256` TaskEvent.STATE_CHANGED, null, (err: FirebaseStorageError | Error) => { - assert.equal( - (err as FirebaseStorageError).code, + expect((err as FirebaseStorageError).code).to.equal( 'storage/app-deleted' ); resolve(); }, () => { - assert.fail('Upload completed, should have been canceled'); + reject('Upload completed, should have been canceled'); } ); // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/packages/storage/test/unit/service.exp.test.ts b/packages/storage/test/unit/service.exp.test.ts new file mode 100644 index 00000000000..941c0507cb1 --- /dev/null +++ b/packages/storage/test/unit/service.exp.test.ts @@ -0,0 +1,321 @@ +/** + * @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 { expect } from 'chai'; +import { TaskEvent } from '../../src/implementation/taskenums'; +import { XhrIoPool } from '../../src/implementation/xhriopool'; +import { StorageService, ref } from '../../src/service'; +import * as testShared from './testshared'; +import { DEFAULT_HOST } from '../../src/implementation/constants'; +import { FirebaseStorageError } from '../../src/implementation/error'; +import { + Reference, + getMetadata, + uploadBytesResumable +} from '../../src/reference'; +import { Location } from '../../src/implementation/location'; + +const fakeAppGs = testShared.makeFakeApp('gs://mybucket'); +const fakeAppGsEndingSlash = testShared.makeFakeApp('gs://mybucket/'); +const fakeAppInvalidGs = testShared.makeFakeApp('gs://mybucket/hello'); +const xhrIoPool = new XhrIoPool(); +const testLocation = new Location('bucket', 'object'); + +function makeGsUrl(child: string = ''): string { + return 'gs://' + testShared.bucket + '/' + child; +} + +describe('Firebase Storage > Service', () => { + describe('simple constructor', () => { + const service = new StorageService( + testShared.fakeApp, + testShared.fakeAuthProvider, + xhrIoPool + ); + it('Root refs point to the right place', () => { + const reference = ref(service); + expect(reference.toString()).to.equal(makeGsUrl()); + }); + it('Child refs point to the right place', () => { + const reference = ref(service, 'path/to/child'); + expect(reference.toString()).to.equal(makeGsUrl('path/to/child')); + }); + }); + describe('custom bucket constructor', () => { + it('gs:// custom bucket constructor refs point to the right place', () => { + const service = new StorageService( + testShared.fakeApp, + testShared.fakeAuthProvider, + xhrIoPool, + 'gs://foo-bar.appspot.com' + ); + const reference = ref(service); + expect(reference.toString()).to.equal('gs://foo-bar.appspot.com/'); + }); + it('http:// custom bucket constructor refs point to the right place', () => { + const service = new StorageService( + testShared.fakeApp, + testShared.fakeAuthProvider, + xhrIoPool, + `http://${DEFAULT_HOST}/v1/b/foo-bar.appspot.com/o` + ); + const reference = ref(service); + expect(reference.toString()).to.equal('gs://foo-bar.appspot.com/'); + }); + it('https:// custom bucket constructor refs point to the right place', () => { + const service = new StorageService( + testShared.fakeApp, + testShared.fakeAuthProvider, + xhrIoPool, + `https://${DEFAULT_HOST}/v1/b/foo-bar.appspot.com/o` + ); + const reference = ref(service); + expect(reference.toString()).to.equal('gs://foo-bar.appspot.com/'); + }); + + it('Bare bucket name constructor refs point to the right place', () => { + const service = new StorageService( + testShared.fakeApp, + testShared.fakeAuthProvider, + xhrIoPool, + 'foo-bar.appspot.com' + ); + const reference = ref(service); + expect(reference.toString()).to.equal('gs://foo-bar.appspot.com/'); + }); + it('Child refs point to the right place', () => { + const service = new StorageService( + testShared.fakeApp, + testShared.fakeAuthProvider, + xhrIoPool, + 'foo-bar.appspot.com' + ); + const reference = ref(service, 'path/to/child'); + expect(reference.toString()).to.equal( + 'gs://foo-bar.appspot.com/path/to/child' + ); + }); + it('Throws trying to construct with a gs:// URL containing an object path', () => { + const error = testShared.assertThrows(() => { + new StorageService( + testShared.fakeApp, + testShared.fakeAuthProvider, + xhrIoPool, + 'gs://bucket/object/' + ); + }, 'storage/invalid-default-bucket'); + expect(error.message).to.match(/Invalid default bucket/); + }); + }); + describe('default bucket config', () => { + it('gs:// works without ending slash', () => { + const service = new StorageService( + fakeAppGs, + testShared.fakeAuthProvider, + xhrIoPool + ); + expect(ref(service)?.toString()).to.equal('gs://mybucket/'); + }); + it('gs:// works with ending slash', () => { + const service = new StorageService( + fakeAppGsEndingSlash, + testShared.fakeAuthProvider, + xhrIoPool + ); + expect(ref(service)?.toString()).to.equal('gs://mybucket/'); + }); + it('Throws when config bucket is gs:// with an object path', () => { + testShared.assertThrows(() => { + new StorageService( + fakeAppInvalidGs, + testShared.fakeAuthProvider, + xhrIoPool + ); + }, 'storage/invalid-default-bucket'); + }); + }); + describe('ref(service, url)', () => { + const service = new StorageService( + testShared.fakeApp, + testShared.fakeAuthProvider, + xhrIoPool + ); + it('Works with gs:// URLs', () => { + const reference = ref(service, 'gs://mybucket/child/path/image.png'); + expect(reference.toString()).to.equal( + 'gs://mybucket/child/path/image.png' + ); + }); + it('Works with http:// URLs', () => { + const reference = ref( + service, + `http://${DEFAULT_HOST}/v0/b/` + + 'mybucket/o/child%2Fpath%2Fimage.png?downloadToken=hello' + ); + expect(reference.toString()).to.equal( + 'gs://mybucket/child/path/image.png' + ); + }); + it('Works with https:// URLs', () => { + const reference = ref( + service, + `https://${DEFAULT_HOST}/v0/b/` + + 'mybucket/o/child%2Fpath%2Fimage.png?downloadToken=hello' + ); + expect(reference.toString()).to.equal( + 'gs://mybucket/child/path/image.png' + ); + }); + it('Works with storage.googleapis.com URLs', () => { + const reference = ref( + service, + `https://storage.googleapis.com/mybucket/path%20with%20space/image.png` + ); + expect(reference.toString()).to.equal( + 'gs://mybucket/path with space/image.png' + ); + }); + it('Works with storage.googleapis.com URLs with query params', () => { + const reference = ref( + service, + `https://storage.googleapis.com/mybucket/path%20with%20space/image.png?X-Goog-Algorithm= +GOOG4-RSA-SHA256` + ); + expect(reference.toString()).to.equal( + 'gs://mybucket/path with space/image.png' + ); + }); + it('Works with storage.cloud.google.com URLs', () => { + const reference = ref( + service, + `https://storage.cloud.google.com/mybucket/path%20with%20space/image.png` + ); + expect(reference.toString()).to.equal( + 'gs://mybucket/path with space/image.png' + ); + }); + it('Works with storage.cloud.google.com URLs and escaped slash', () => { + const reference = ref( + service, + `https://storage.cloud.google.com/mybucket/path%20with%20space%2Fimage.png` + ); + expect(reference.toString()).to.equal( + 'gs://mybucket/path with space/image.png' + ); + }); + }); + describe('ref(service, path)', () => { + const service = new StorageService( + testShared.fakeApp, + testShared.fakeAuthProvider, + xhrIoPool + ); + it('Works with non URL paths', () => { + const newRef = ref(service, 'child/path/image.png'); + expect(newRef.toString()).to.equal('gs://mybucket/child/path/image.png'); + }); + it('Works with no path', () => { + const newRef = ref(service); + expect(newRef.toString()).to.equal('gs://mybucket/'); + }); + }); + describe('ref(reference, path)', () => { + const service = new StorageService( + testShared.fakeApp, + testShared.fakeAuthProvider, + xhrIoPool + ); + const reference = new Reference(service, testLocation); + it('Throws calling ref(reference, path) with a gs:// URL', () => { + const error = testShared.assertThrows(() => { + ref(reference, 'gs://bucket/object'); + }, 'storage/invalid-argument'); + expect(error.message).to.match(/url/); + }); + it('Throws calling ref(reference, path) with an http:// URL', () => { + const error = testShared.assertThrows(() => { + ref(reference, `http://${DEFAULT_HOST}/etc`); + }, 'storage/invalid-argument'); + expect(error.message).to.match(/url/); + }); + it('Throws calling ref(reference, path) with an https:// URL', () => { + const error = testShared.assertThrows(() => { + ref(reference, `https://${DEFAULT_HOST}/etc`); + }, 'storage/invalid-argument'); + expect(error.message).to.match(/url/); + }); + it('Works with non URL paths', () => { + const newRef = ref(reference, 'child/path/image.png'); + expect(newRef.toString()).to.equal( + 'gs://bucket/object/child/path/image.png' + ); + }); + it('Works with no path', () => { + const newRef = ref(reference); + expect(newRef.toString()).to.equal('gs://bucket/object'); + }); + it('Throws calling ref(reference, path) if path contains ".."', () => { + const error = testShared.assertThrows(() => { + ref(reference, `../child/path`); + }, 'storage/invalid-argument'); + expect(error.message).to.match(/"\.\."/); + }); + }); + + describe('Deletion', () => { + const service = new StorageService( + testShared.fakeApp, + testShared.fakeAuthProvider, + xhrIoPool + ); + it('In-flight requests are canceled when the service is deleted', async () => { + const reference = ref(service, 'gs://mybucket/image.jpg'); + const metadataPromise = getMetadata(reference); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + service._delete(); + await expect(metadataPromise).to.be.rejectedWith('storage/app-deleted'); + }); + it('Requests fail when started after the service is deleted', async () => { + const reference = ref(service, 'gs://mybucket/image.jpg'); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + service._delete(); + await expect(getMetadata(reference)).to.be.rejectedWith( + 'storage/app-deleted' + ); + }); + it('Running uploads fail when the service is deleted', () => { + const reference = ref(service, 'gs://mybucket/image.jpg'); + const toReturn = new Promise((resolve, reject) => { + uploadBytesResumable(reference, new Blob(['a'])).on( + TaskEvent.STATE_CHANGED, + undefined, + (err: FirebaseStorageError | Error) => { + expect((err as FirebaseStorageError).code).to.equal( + 'storage/app-deleted' + ); + resolve(); + }, + () => { + reject('Upload completed, should have been canceled'); + } + ); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + service._delete(); + }); + return toReturn; + }); + }); +}); diff --git a/packages/storage/test/unit/task.test.ts b/packages/storage/test/unit/task.test.ts index d4cd24f8a6d..61a81fff13f 100644 --- a/packages/storage/test/unit/task.test.ts +++ b/packages/storage/test/unit/task.test.ts @@ -17,7 +17,6 @@ import { assert } from 'chai'; import { FbsBlob } from '../../src/implementation/blob'; import { Location } from '../../src/implementation/location'; -import { getMappings } from '../../src/implementation/metadata'; import { Unsubscribe } from '../../src/implementation/observer'; import { TaskEvent, TaskState } from '../../src/implementation/taskenums'; import { Headers } from '../../src/implementation/xhrio'; @@ -26,13 +25,12 @@ import { StorageService } from '../../src/service'; import { UploadTask } from '../../src/task'; import { makePool, emptyAuthProvider } from './testshared'; import { StringHeaders, TestingXhrIo } from './xhrio'; +import { FirebaseApp } from '@firebase/app-types'; const testLocation = new Location('bucket', 'object'); const smallBlob = new FbsBlob(new Blob(['a'])); const bigBlob = new FbsBlob(new Blob([new ArrayBuffer(1024 * 1024)])); -const mappings = getMappings(); - const fakeMetadata = '{ "downloadTokens": "a,b" }'; interface Response { @@ -59,7 +57,11 @@ function storageServiceWithHandler(handler: RequestHandler): StorageService { xhrio.simulateResponse(response.status, response.body, response.headers); } - return new StorageService(null, emptyAuthProvider, makePool(newSend)); + return new StorageService( + {} as FirebaseApp, + emptyAuthProvider, + makePool(newSend) + ); } function fakeServerHandler(): RequestHandler { @@ -191,15 +193,12 @@ describe('Firebase Storage > Upload Task', () => { it('Works for a small upload w/ an observer', done => { const storageService = storageServiceWithHandler(fakeServerHandler()); const task = new UploadTask( - {} as Reference, - storageService, - testLocation, - mappings, + new Reference(storageService, testLocation), smallBlob ); task.on( TaskEvent.STATE_CHANGED, - null, + undefined, () => assert.fail('Unexpected upload failure'), () => done() ); @@ -207,10 +206,7 @@ describe('Firebase Storage > Upload Task', () => { it('Works for a small upload w/ a promise', () => { const storageService = storageServiceWithHandler(fakeServerHandler()); const task = new UploadTask( - {} as Reference, - storageService, - testLocation, - mappings, + new Reference(storageService, testLocation), smallBlob ); return task.then(snapshot => { @@ -220,10 +216,7 @@ describe('Firebase Storage > Upload Task', () => { it('Works for a small upload canceled w/ a promise', () => { const storageService = storageServiceWithHandler(fakeServerHandler()); const task = new UploadTask( - {} as Reference, - storageService, - testLocation, - mappings, + new Reference(storageService, testLocation), smallBlob ); const promise: Promise = task.then( @@ -239,20 +232,27 @@ describe('Firebase Storage > Upload Task', () => { it('Works properly with multiple observers', () => { const storageService = storageServiceWithHandler(fakeServerHandler()); const task = new UploadTask( - {} as Reference, - storageService, - testLocation, - mappings, + new Reference(storageService, testLocation), smallBlob ); let badComplete = false; - const h1: Unsubscribe = task.on(TaskEvent.STATE_CHANGED, null, null, () => { - badComplete = true; - }) as Unsubscribe; - const h2: Unsubscribe = task.on(TaskEvent.STATE_CHANGED, null, null, () => { - badComplete = true; - }) as Unsubscribe; + const h1: Unsubscribe = task.on( + TaskEvent.STATE_CHANGED, + undefined, + undefined, + () => { + badComplete = true; + } + ) as Unsubscribe; + const h2: Unsubscribe = task.on( + TaskEvent.STATE_CHANGED, + undefined, + undefined, + () => { + badComplete = true; + } + ) as Unsubscribe; let resumed = 0; @@ -270,8 +270,8 @@ describe('Firebase Storage > Upload Task', () => { } lastState = snapshot.state; }, - null, - null + undefined, + undefined ); })(); h1(); @@ -280,7 +280,7 @@ describe('Firebase Storage > Upload Task', () => { return new Promise(resolve => { task.on( TaskEvent.STATE_CHANGED, - null, + undefined, () => { assert.fail('Upload failed'); }, @@ -295,10 +295,7 @@ describe('Firebase Storage > Upload Task', () => { it("Works properly with an observer missing the 'next' method", () => { const storageService = storageServiceWithHandler(fakeServerHandler()); const task = new UploadTask( - {} as Reference, - storageService, - testLocation, - mappings, + new Reference(storageService, testLocation), smallBlob ); return new Promise(resolve => { @@ -316,10 +313,7 @@ describe('Firebase Storage > Upload Task', () => { function runNormalUploadTest(blob: FbsBlob): Promise { const storageService = storageServiceWithHandler(fakeServerHandler()); const task = new UploadTask( - {} as Reference, - storageService, - testLocation, - mappings, + new Reference(storageService, testLocation), blob ); @@ -404,7 +398,7 @@ describe('Firebase Storage > Upload Task', () => { let completeTriggered = false; - task.on(TaskEvent.STATE_CHANGED, null, null, () => { + task.on(TaskEvent.STATE_CHANGED, undefined, undefined, () => { fixedAssertFalse(completeTriggered); completeTriggered = true; @@ -438,10 +432,7 @@ describe('Firebase Storage > Upload Task', () => { fixedAssertTrue(lastIsAll); const task2 = new UploadTask( - {} as Reference, - storageService, - testLocation, - mappings, + new Reference(storageService, testLocation), blob ); const events2: string[] = []; diff --git a/packages/storage/test/unit/testshared.ts b/packages/storage/test/unit/testshared.ts index 0737d8916d0..8a3d4c8b7a4 100644 --- a/packages/storage/test/unit/testshared.ts +++ b/packages/storage/test/unit/testshared.ts @@ -14,11 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { expect } from 'chai'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +use(chaiAsPromised); + import { FirebaseApp } from '@firebase/app-types'; import * as constants from '../../src/implementation/constants'; import { Code, FirebaseStorageError } from '../../src/implementation/error'; -import * as type from '../../src/implementation/type'; import { Headers, XhrIo } from '../../src/implementation/xhrio'; import { XhrIoPool } from '../../src/implementation/xhriopool'; import { SendHook, StringHeaders, TestingXhrIo } from './xhrio'; @@ -44,7 +47,7 @@ export const emptyAuthProvider = new Provider( export function makeFakeApp(bucketArg?: string): FirebaseApp { const app: any = {}; app.options = {}; - if (type.isDef(bucketArg)) { + if (bucketArg != null) { app.options[constants.CONFIG_STORAGE_BUCKET_KEY] = bucketArg; } else { app.options[constants.CONFIG_STORAGE_BUCKET_KEY] = bucket; diff --git a/packages/storage/test/unit/xhrio.ts b/packages/storage/test/unit/xhrio.ts index 187d87cf0e4..ff558ab01d0 100644 --- a/packages/storage/test/unit/xhrio.ts +++ b/packages/storage/test/unit/xhrio.ts @@ -14,8 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import * as type from '../../src/implementation/type'; import { ErrorCode, Headers, XhrIo } from '../../src/implementation/xhrio'; +import { FirebaseStorageError, Code } from '../../src/implementation/error'; export type SendHook = ( xhrio: TestingXhrIo, @@ -64,7 +64,7 @@ export class TestingXhrIo implements XhrIo { headers?: Headers ): Promise { if (this.state !== State.START) { - throw new Error("Can't send again"); + throw new FirebaseStorageError(Code.UNKNOWN, "Can't send again"); } this.state = State.SENT; @@ -81,7 +81,10 @@ export class TestingXhrIo implements XhrIo { headers: { [key: string]: string } ): void { if (this.state !== State.SENT) { - throw new Error("Can't simulate response before send/more than once"); + throw new FirebaseStorageError( + Code.UNKNOWN, + "Can't simulate response before send/more than once" + ); } this.status = status; @@ -116,7 +119,7 @@ export class TestingXhrIo implements XhrIo { getResponseHeader(header: string): string | null { const headerValue = this.headers[header.toLowerCase()]; - if (type.isDef(headerValue)) { + if (headerValue != null) { return headerValue; } else { return null; diff --git a/packages/template-types/package.json b/packages/template-types/package.json index 2daf4dc4171..2a15fb29f61 100644 --- a/packages/template-types/package.json +++ b/packages/template-types/package.json @@ -21,6 +21,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" } } diff --git a/packages/template/package.json b/packages/template/package.json index 1d014822114..825994e9b5d 100644 --- a/packages/template/package.json +++ b/packages/template/package.json @@ -32,10 +32,10 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.6.12", - "rollup": "2.29.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "@firebase/app": "0.6.13", + "rollup": "2.33.1", + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages/template", diff --git a/packages/util/CHANGELOG.md b/packages/util/CHANGELOG.md index 165a29da248..7ff60bb6186 100644 --- a/packages/util/CHANGELOG.md +++ b/packages/util/CHANGELOG.md @@ -1,5 +1,11 @@ # @firebase/util +## 0.3.4 + +### Patch Changes + +- [`9cf727fcc`](https://github.com/firebase/firebase-js-sdk/commit/9cf727fcc3d049551b16ae0698ac33dc2fe45ada) [#4001](https://github.com/firebase/firebase-js-sdk/pull/4001) - Do not merge `__proto__` in `deepExtend` to prevent `__proto__` pollution. + ## 0.3.3 ### Patch Changes diff --git a/packages/util/package.json b/packages/util/package.json index 57d208709d3..f068947f347 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/util", - "version": "0.3.3", + "version": "0.3.4", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", @@ -26,9 +26,9 @@ "tslib": "^1.11.1" }, "devDependencies": { - "rollup": "2.29.0", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "rollup": "2.33.1", + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages/util", diff --git a/packages/webchannel-wrapper/package.json b/packages/webchannel-wrapper/package.json index 72676c84956..f79124cf33a 100644 --- a/packages/webchannel-wrapper/package.json +++ b/packages/webchannel-wrapper/package.json @@ -17,14 +17,14 @@ "license": "Apache-2.0", "devDependencies": { "google-closure-compiler": "20200628.0.0", - "google-closure-library": "20200628.0.0", + "google-closure-library": "20200830.0.0", "gulp": "4.0.2", "gulp-sourcemaps": "2.6.5", - "rollup": "2.29.0", + "rollup": "2.33.1", "@rollup/plugin-commonjs": "15.1.0", "rollup-plugin-sourcemaps": "0.6.3", - "rollup-plugin-typescript2": "0.27.3", - "typescript": "4.0.2" + "rollup-plugin-typescript2": "0.29.0", + "typescript": "4.0.5" }, "repository": { "directory": "packages/webchannel-wrapper", diff --git a/repo-scripts/changelog-generator/package.json b/repo-scripts/changelog-generator/package.json index caab43e5e59..3bbb73d11d2 100644 --- a/repo-scripts/changelog-generator/package.json +++ b/repo-scripts/changelog-generator/package.json @@ -24,7 +24,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "typescript": "4.0.2" + "typescript": "4.0.5" }, "repository": { "directory": "repo-scripts/changelog-generator", diff --git a/repo-scripts/size-analysis/package.json b/repo-scripts/size-analysis/package.json index 04151d152b2..b54b9da7f91 100644 --- a/repo-scripts/size-analysis/package.json +++ b/repo-scripts/size-analysis/package.json @@ -16,29 +16,29 @@ "test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha **/*.test.ts --config ../../config/mocharc.node.js --timeout 60000" }, "dependencies": { - "rollup": "2.29.0", + "rollup": "2.33.1", "@rollup/plugin-commonjs": "15.1.0", "@rollup/plugin-json": "4.1.0", "@rollup/plugin-node-resolve": "9.0.0", "rollup-plugin-replace": "2.2.0", - "rollup-plugin-typescript2": "0.27.3", + "rollup-plugin-typescript2": "0.29.0", "@rollup/plugin-virtual": "2.0.3", "webpack": "4.44.2", - "@types/webpack": "4.41.22", - "webpack-virtual-modules": "0.3.1", + "@types/webpack": "4.41.24", + "webpack-virtual-modules": "0.3.2", "child-process-promise": "2.2.1", "memfs": "3.2.0", "tmp": "0.2.1", - "typescript": "4.0.2", - "terser": "5.3.5", - "yargs": "16.0.3", - "@firebase/util": "0.3.3", + "typescript": "4.0.5", + "terser": "5.3.8", + "yargs": "16.1.0", + "@firebase/util": "0.3.4", "gzip-size": "5.1.1" }, "license": "Apache-2.0", "devDependencies": { "@firebase/logger": "0.2.6", - "@firebase/app": "0.6.12" + "@firebase/app": "0.6.13" }, "repository": { "directory": "repo-scripts/size-analysis", diff --git a/scripts/check_changeset.ts b/scripts/check_changeset.ts index 41532423f03..12ed838178b 100644 --- a/scripts/check_changeset.ts +++ b/scripts/check_changeset.ts @@ -87,6 +87,7 @@ async function main() { const errors = []; try { await exec('yarn changeset status'); + console.log(`::set-output name=BLOCKING_FAILURE::false`); } catch (e) { const messageLines = e.message.replace(/🦋 error /g, '').split('\n'); let formattedStatusError = @@ -111,9 +112,7 @@ async function main() { } try { const diffData = await getDiffData(); - if (diffData == null) { - process.exit(); - } else { + if (diffData != null) { const { changedPackages, changesetFile } = diffData; const changesetPackages = await parseChangesetFile(changesetFile); const missingPackages = [...changedPackages].filter( diff --git a/scripts/ci-test/build_changed.ts b/scripts/ci-test/build_changed.ts index a0f8802da53..7de0fd3f690 100644 --- a/scripts/ci-test/build_changed.ts +++ b/scripts/ci-test/build_changed.ts @@ -29,12 +29,18 @@ const argv = yargs.options({ type: 'boolean', desc: 'whether or not build @firebase/app-exp first. It is a hack required to build Firestore' + }, + buildAppCompat: { + type: 'boolean', + desc: + 'whether or not build @firebase/app-compat first. It is a hack required to build Firestore' } }).argv; const allTestConfigNames = Object.keys(testConfig); const inputTestConfigName = argv._[0]; const buildAppExp = argv.buildAppExp; +const buildAppCompat = argv.buildAppCompat; if (!inputTestConfigName) { throw Error(` @@ -82,6 +88,23 @@ async function buildForTests(config: TestConfig, buildAppExp = false) { { stdio: 'inherit', cwd: root } ); } + // hack to build Firestore which depends on @firebase/app-exp (because of firestore exp), + // but doesn't list it as a dependency in its package.json + // TODO: remove once modular SDKs become official + if (buildAppCompat) { + await spawn( + 'npx', + [ + 'lerna', + 'run', + '--scope', + '@firebase/app-compat', + '--include-dependencies', + 'build' + ], + { stdio: 'inherit', cwd: root } + ); + } const lernaCmd = ['lerna', 'run']; console.log(chalk`{blue Running build in:}`); diff --git a/scripts/emulator-testing/emulators/database-emulator.ts b/scripts/emulator-testing/emulators/database-emulator.ts index ddd126e7698..d252f87c727 100644 --- a/scripts/emulator-testing/emulators/database-emulator.ts +++ b/scripts/emulator-testing/emulators/database-emulator.ts @@ -24,7 +24,7 @@ export class DatabaseEmulator extends Emulator { constructor(port = 8088, namespace = 'test-emulator') { super( - 'database-emulator.jar', + 'firebase-database-emulator-v4.4.1.jar', // Use locked version of emulator for test to be deterministic. // The latest version can be found from database emulator doc: // https://firebase.google.com/docs/database/security/test-rules-emulator diff --git a/scripts/emulator-testing/emulators/emulator.ts b/scripts/emulator-testing/emulators/emulator.ts index e5d49433c2b..bea75d39dc4 100644 --- a/scripts/emulator-testing/emulators/emulator.ts +++ b/scripts/emulator-testing/emulators/emulator.ts @@ -19,6 +19,7 @@ import { spawn } from 'child-process-promise'; import { ChildProcess } from 'child_process'; import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; import * as request from 'request'; // @ts-ignore @@ -28,13 +29,25 @@ export abstract class Emulator { binaryPath: string | null = null; emulator: ChildProcess | null = null; + cacheDirectory: string; + cacheBinaryPath: string; + constructor( private binaryName: string, private binaryUrl: string, public port: number - ) {} + ) { + this.cacheDirectory = path.join(os.homedir(), `.cache/firebase-js-sdk`); + this.cacheBinaryPath = path.join(this.cacheDirectory, binaryName); + } download(): Promise { + if (fs.existsSync(this.cacheBinaryPath)) { + console.log(`Emulator found in cache: ${this.cacheBinaryPath}`); + this.binaryPath = this.cacheBinaryPath; + return Promise.resolve(); + } + return new Promise((resolve, reject) => { tmp.dir((err: Error | null, dir: string) => { if (err) reject(err); @@ -55,6 +68,10 @@ export abstract class Emulator { if (err) reject(err); console.log(`Changed emulator file permissions to 'rwxr-xr-x'.`); this.binaryPath = filepath; + + if (this.copyToCache()) { + console.log(`Cached emulator at ${this.cacheBinaryPath}`); + } resolve(); }); }) @@ -129,4 +146,23 @@ export abstract class Emulator { fs.unlinkSync(this.binaryPath); } } + + private copyToCache(): boolean { + if (!this.binaryPath) { + return false; + } + + try { + if (!fs.existsSync(this.cacheDirectory)) { + fs.mkdirSync(this.cacheDirectory, { recursive: true }); + } + fs.copyFileSync(this.binaryPath, this.cacheBinaryPath); + + return true; + } catch (e) { + console.warn(`Unable to cache ${this.binaryName}`, e); + } + + return false; + } } diff --git a/scripts/emulator-testing/emulators/firestore-emulator.ts b/scripts/emulator-testing/emulators/firestore-emulator.ts index 84cd23d2eeb..3752a61c203 100644 --- a/scripts/emulator-testing/emulators/firestore-emulator.ts +++ b/scripts/emulator-testing/emulators/firestore-emulator.ts @@ -22,7 +22,7 @@ export class FirestoreEmulator extends Emulator { constructor(port: number, projectId = 'test-emulator') { super( - 'firestore-emulator.jar', + 'cloud-firestore-emulator-v1.11.7.jar', // Use locked version of emulator for test to be deterministic. // The latest version can be found from firestore emulator doc: // https://firebase.google.com/docs/firestore/security/test-rules-emulator diff --git a/yarn.lock b/yarn.lock index f6cb6b44204..f564a6d9db4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18,16 +18,34 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/compat-data@^7.10.4", "@babel/compat-data@^7.11.0": - version "7.11.0" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.11.0.tgz#e9f73efe09af1355b723a7f39b11bad637d7c99c" - integrity sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ== +"@babel/compat-data@^7.12.1", "@babel/compat-data@^7.12.5": + version "7.12.5" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.5.tgz#f56db0c4bb1bbbf221b4e81345aab4141e7cb0e9" + integrity sha512-DTsS7cxrsH3by8nqQSpFSyjSfSYl57D6Cf4q8dW3LK83tBKBDCkfcay1nYkXq1nIHXnpX8WMMb/O25HOy3h1zg== + +"@babel/core@7.12.3": + version "7.12.3" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.12.3.tgz#1b436884e1e3bff6fb1328dc02b208759de92ad8" + integrity sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g== dependencies: - browserslist "^4.12.0" - invariant "^2.2.4" - semver "^5.5.0" + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.1" + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helpers" "^7.12.1" + "@babel/parser" "^7.12.3" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" -"@babel/core@7.11.6", "@babel/core@^7.7.5": +"@babel/core@^7.7.5": version "7.11.6" resolved "https://registry.npmjs.org/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651" integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg== @@ -67,6 +85,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.12.5": + version "7.12.5" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de" + integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A== + dependencies: + "@babel/types" "^7.12.5" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" @@ -82,27 +109,25 @@ "@babel/helper-explode-assignable-expression" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-compilation-targets@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz#804ae8e3f04376607cc791b9d47d540276332bd2" - integrity sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ== +"@babel/helper-compilation-targets@^7.12.1": + version "7.12.5" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz#cb470c76198db6a24e9dbc8987275631e5d29831" + integrity sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw== dependencies: - "@babel/compat-data" "^7.10.4" - browserslist "^4.12.0" - invariant "^2.2.4" - levenary "^1.1.1" + "@babel/compat-data" "^7.12.5" + "@babel/helper-validator-option" "^7.12.1" + browserslist "^4.14.5" semver "^5.5.0" -"@babel/helper-create-class-features-plugin@^7.10.4": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" - integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== +"@babel/helper-create-class-features-plugin@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz#3c45998f431edd4a9214c5f1d3ad1448a6137f6e" + integrity sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w== dependencies: "@babel/helper-function-name" "^7.10.4" - "@babel/helper-member-expression-to-functions" "^7.10.5" + "@babel/helper-member-expression-to-functions" "^7.12.1" "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.1" "@babel/helper-split-export-declaration" "^7.10.4" "@babel/helper-create-regexp-features-plugin@^7.10.4": @@ -114,6 +139,15 @@ "@babel/helper-regex" "^7.10.4" regexpu-core "^4.7.0" +"@babel/helper-create-regexp-features-plugin@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.1.tgz#18b1302d4677f9dc4740fe8c9ed96680e29d37e8" + integrity sha512-rsZ4LGvFTZnzdNZR5HZdmJVuXK8834R5QkF3WvcnBhrlVtF0HSIUC6zbreL9MgjTywhKokn8RIYRiq99+DLAxA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-regex" "^7.10.4" + regexpu-core "^4.7.1" + "@babel/helper-define-map@^7.10.4": version "7.10.5" resolved "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" @@ -153,7 +187,7 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": +"@babel/helper-member-expression-to-functions@^7.10.4": version "7.11.0" resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== @@ -181,7 +215,7 @@ dependencies: "@babel/types" "^7.12.1" -"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0": +"@babel/helper-module-transforms@^7.11.0": version "7.11.0" resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== @@ -228,15 +262,14 @@ dependencies: lodash "^4.17.19" -"@babel/helper-remap-async-to-generator@^7.10.4": - version "7.11.4" - resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz#4474ea9f7438f18575e30b0cac784045b402a12d" - integrity sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA== +"@babel/helper-remap-async-to-generator@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz#8c4dbbf916314f6047dc05e6a2217074238347fd" + integrity sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-wrap-function" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/types" "^7.12.1" "@babel/helper-replace-supers@^7.10.4": version "7.10.4" @@ -273,12 +306,12 @@ dependencies: "@babel/types" "^7.12.1" -"@babel/helper-skip-transparent-expression-wrappers@^7.11.0": - version "7.11.0" - resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729" - integrity sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q== +"@babel/helper-skip-transparent-expression-wrappers@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf" + integrity sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA== dependencies: - "@babel/types" "^7.11.0" + "@babel/types" "^7.12.1" "@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0": version "7.11.0" @@ -292,6 +325,11 @@ resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== +"@babel/helper-validator-option@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz#175567380c3e77d60ff98a54bb015fe78f2178d9" + integrity sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A== + "@babel/helper-wrap-function@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" @@ -311,6 +349,15 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helpers@^7.12.1": + version "7.12.5" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e" + integrity sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA== + dependencies: + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.5" + "@babel/types" "^7.12.5" + "@babel/highlight@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" @@ -330,106 +377,119 @@ resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.12.3.tgz#a305415ebe7a6c7023b40b5122a0662d928334cd" integrity sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw== -"@babel/plugin-proposal-async-generator-functions@^7.10.4": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558" - integrity sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg== +"@babel/parser@^7.12.3", "@babel/parser@^7.12.5": + version "7.12.5" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz#b4af32ddd473c0bfa643bd7ff0728b8e71b81ea0" + integrity sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ== + +"@babel/plugin-proposal-async-generator-functions@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz#dc6c1170e27d8aca99ff65f4925bd06b1c90550e" + integrity sha512-d+/o30tJxFxrA1lhzJqiUcEJdI6jKlNregCv5bASeGf2Q4MXmnwH7viDo7nhx1/ohf09oaH8j1GVYG/e3Yqk6A== dependencies: "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-remap-async-to-generator" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.12.1" "@babel/plugin-syntax-async-generators" "^7.8.0" -"@babel/plugin-proposal-class-properties@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" - integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== +"@babel/plugin-proposal-class-properties@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz#a082ff541f2a29a4821065b8add9346c0c16e5de" + integrity sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w== dependencies: - "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-create-class-features-plugin" "^7.12.1" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-dynamic-import@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e" - integrity sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ== +"@babel/plugin-proposal-dynamic-import@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz#43eb5c2a3487ecd98c5c8ea8b5fdb69a2749b2dc" + integrity sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" -"@babel/plugin-proposal-export-namespace-from@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz#570d883b91031637b3e2958eea3c438e62c05f54" - integrity sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg== +"@babel/plugin-proposal-export-namespace-from@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz#8b9b8f376b2d88f5dd774e4d24a5cc2e3679b6d4" + integrity sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" -"@babel/plugin-proposal-json-strings@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db" - integrity sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw== +"@babel/plugin-proposal-json-strings@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz#d45423b517714eedd5621a9dfdc03fa9f4eb241c" + integrity sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.0" -"@babel/plugin-proposal-logical-assignment-operators@^7.11.0": - version "7.11.0" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz#9f80e482c03083c87125dee10026b58527ea20c8" - integrity sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q== +"@babel/plugin-proposal-logical-assignment-operators@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz#f2c490d36e1b3c9659241034a5d2cd50263a2751" + integrity sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a" - integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw== +"@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz#3ed4fff31c015e7f3f1467f190dbe545cd7b046c" + integrity sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06" - integrity sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA== +"@babel/plugin-proposal-numeric-separator@^7.12.1": + version "7.12.5" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.5.tgz#b1ce757156d40ed79d59d467cb2b154a5c4149ba" + integrity sha512-UiAnkKuOrCyjZ3sYNHlRlfuZJbBHknMQ9VMwVeX97Ofwx7RpD6gS2HfqTCh8KNUQgcOm8IKt103oR4KIjh7Q8g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@^7.11.0": - version "7.11.0" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af" - integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA== +"@babel/plugin-proposal-object-rest-spread@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz#def9bd03cea0f9b72283dac0ec22d289c7691069" + integrity sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-transform-parameters" "^7.10.4" + "@babel/plugin-transform-parameters" "^7.12.1" -"@babel/plugin-proposal-optional-catch-binding@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd" - integrity sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g== +"@babel/plugin-proposal-optional-catch-binding@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz#ccc2421af64d3aae50b558a71cede929a5ab2942" + integrity sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.11.0": - version "7.11.0" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" - integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA== +"@babel/plugin-proposal-optional-chaining@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.1.tgz#cce122203fc8a32794296fc377c6dedaf4363797" + integrity sha512-c2uRpY6WzaVDzynVY9liyykS+kVU+WRZPMPYpkelXH8KBt1oXoI89kPbZKKG/jDT5UK92FTW2fZkZaJhdiBabw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" "@babel/plugin-syntax-optional-chaining" "^7.8.0" -"@babel/plugin-proposal-private-methods@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz#b160d972b8fdba5c7d111a145fc8c421fc2a6909" - integrity sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw== +"@babel/plugin-proposal-private-methods@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz#86814f6e7a21374c980c10d38b4493e703f4a389" + integrity sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-proposal-unicode-property-regex@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz#2a183958d417765b9eae334f47758e5d6a82e072" + integrity sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w== dependencies: - "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-create-regexp-features-plugin" "^7.12.1" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-unicode-property-regex@^7.10.4", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": +"@babel/plugin-proposal-unicode-property-regex@^7.4.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d" integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA== @@ -444,10 +504,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c" - integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA== +"@babel/plugin-syntax-class-properties@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz#bcb297c5366e79bebadef509549cd93b04f19978" + integrity sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" @@ -514,72 +574,80 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-top-level-await@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz#4bbeb8917b54fcf768364e0a81f560e33a3ef57d" - integrity sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ== +"@babel/plugin-syntax-top-level-await@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz#dd6c0b357ac1bb142d98537450a319625d13d2a0" + integrity sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-arrow-functions@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz#e22960d77e697c74f41c501d44d73dbf8a6a64cd" - integrity sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA== +"@babel/plugin-transform-arrow-functions@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz#8083ffc86ac8e777fbe24b5967c4b2521f3cb2b3" + integrity sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-async-to-generator@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz#41a5017e49eb6f3cda9392a51eef29405b245a37" - integrity sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ== +"@babel/plugin-transform-async-to-generator@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz#3849a49cc2a22e9743cbd6b52926d30337229af1" + integrity sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A== dependencies: - "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-module-imports" "^7.12.1" "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-remap-async-to-generator" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.12.1" -"@babel/plugin-transform-block-scoped-functions@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz#1afa595744f75e43a91af73b0d998ecfe4ebc2e8" - integrity sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA== +"@babel/plugin-transform-block-scoped-functions@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz#f2a1a365bde2b7112e0a6ded9067fdd7c07905d9" + integrity sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-block-scoping@^7.10.4": - version "7.11.1" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz#5b7efe98852bef8d652c0b28144cd93a9e4b5215" - integrity sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew== +"@babel/plugin-transform-block-scoping@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.1.tgz#f0ee727874b42a208a48a586b84c3d222c2bbef1" + integrity sha512-zJyAC9sZdE60r1nVQHblcfCj29Dh2Y0DOvlMkcqSo0ckqjiCwNiUezUKw+RjOCwGfpLRwnAeQ2XlLpsnGkvv9w== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-classes@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7" - integrity sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA== +"@babel/plugin-transform-classes@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz#65e650fcaddd3d88ddce67c0f834a3d436a32db6" + integrity sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-define-map" "^7.10.4" "@babel/helper-function-name" "^7.10.4" "@babel/helper-optimise-call-expression" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.1" "@babel/helper-split-export-declaration" "^7.10.4" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz#9ded83a816e82ded28d52d4b4ecbdd810cdfc0eb" - integrity sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw== +"@babel/plugin-transform-computed-properties@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz#d68cf6c9b7f838a8a4144badbe97541ea0904852" + integrity sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-destructuring@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz#70ddd2b3d1bea83d01509e9bb25ddb3a74fc85e5" - integrity sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA== +"@babel/plugin-transform-destructuring@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz#b9a570fe0d0a8d460116413cb4f97e8e08b2f847" + integrity sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-dotall-regex@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz#a1d16c14862817b6409c0a678d6f9373ca9cd975" + integrity sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA== dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.12.1" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-dotall-regex@^7.10.4", "@babel/plugin-transform-dotall-regex@^7.4.4": +"@babel/plugin-transform-dotall-regex@^7.4.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee" integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA== @@ -587,60 +655,60 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-duplicate-keys@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47" - integrity sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA== +"@babel/plugin-transform-duplicate-keys@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz#745661baba295ac06e686822797a69fbaa2ca228" + integrity sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-exponentiation-operator@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz#5ae338c57f8cf4001bdb35607ae66b92d665af2e" - integrity sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw== +"@babel/plugin-transform-exponentiation-operator@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz#b0f2ed356ba1be1428ecaf128ff8a24f02830ae0" + integrity sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug== dependencies: "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-for-of@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz#c08892e8819d3a5db29031b115af511dbbfebae9" - integrity sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ== +"@babel/plugin-transform-for-of@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz#07640f28867ed16f9511c99c888291f560921cfa" + integrity sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-function-name@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz#6a467880e0fc9638514ba369111811ddbe2644b7" - integrity sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg== +"@babel/plugin-transform-function-name@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz#2ec76258c70fe08c6d7da154003a480620eba667" + integrity sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw== dependencies: "@babel/helper-function-name" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-literals@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz#9f42ba0841100a135f22712d0e391c462f571f3c" - integrity sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ== +"@babel/plugin-transform-literals@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz#d73b803a26b37017ddf9d3bb8f4dc58bfb806f57" + integrity sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-member-expression-literals@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz#b1ec44fcf195afcb8db2c62cd8e551c881baf8b7" - integrity sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw== +"@babel/plugin-transform-member-expression-literals@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz#496038602daf1514a64d43d8e17cbb2755e0c3ad" + integrity sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-modules-amd@^7.10.4": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1" - integrity sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw== +"@babel/plugin-transform-modules-amd@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz#3154300b026185666eebb0c0ed7f8415fefcf6f9" + integrity sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ== dependencies: - "@babel/helper-module-transforms" "^7.10.5" + "@babel/helper-module-transforms" "^7.12.1" "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@7.12.1": +"@babel/plugin-transform-modules-commonjs@7.12.1", "@babel/plugin-transform-modules-commonjs@^7.12.1": version "7.12.1" resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz#fa403124542636c786cf9b460a0ffbb48a86e648" integrity sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag== @@ -650,162 +718,152 @@ "@babel/helper-simple-access" "^7.12.1" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" - integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w== - dependencies: - "@babel/helper-module-transforms" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-simple-access" "^7.10.4" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-systemjs@^7.10.4": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" - integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw== +"@babel/plugin-transform-modules-systemjs@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz#663fea620d593c93f214a464cd399bf6dc683086" + integrity sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q== dependencies: "@babel/helper-hoist-variables" "^7.10.4" - "@babel/helper-module-transforms" "^7.10.5" + "@babel/helper-module-transforms" "^7.12.1" "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-validator-identifier" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-umd@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz#9a8481fe81b824654b3a0b65da3df89f3d21839e" - integrity sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA== +"@babel/plugin-transform-modules-umd@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz#eb5a218d6b1c68f3d6217b8fa2cc82fec6547902" + integrity sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q== dependencies: - "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-module-transforms" "^7.12.1" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-named-capturing-groups-regex@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz#78b4d978810b6f3bcf03f9e318f2fc0ed41aecb6" - integrity sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA== +"@babel/plugin-transform-named-capturing-groups-regex@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz#b407f5c96be0d9f5f88467497fa82b30ac3e8753" + integrity sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-create-regexp-features-plugin" "^7.12.1" -"@babel/plugin-transform-new-target@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz#9097d753cb7b024cb7381a3b2e52e9513a9c6888" - integrity sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw== +"@babel/plugin-transform-new-target@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz#80073f02ee1bb2d365c3416490e085c95759dec0" + integrity sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-object-super@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz#d7146c4d139433e7a6526f888c667e314a093894" - integrity sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ== +"@babel/plugin-transform-object-super@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz#4ea08696b8d2e65841d0c7706482b048bed1066e" + integrity sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.1" -"@babel/plugin-transform-parameters@^7.10.4": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a" - integrity sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw== +"@babel/plugin-transform-parameters@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz#d2e963b038771650c922eff593799c96d853255d" + integrity sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg== dependencies: - "@babel/helper-get-function-arity" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-property-literals@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz#f6fe54b6590352298785b83edd815d214c42e3c0" - integrity sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g== +"@babel/plugin-transform-property-literals@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz#41bc81200d730abb4456ab8b3fbd5537b59adecd" + integrity sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-regenerator@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz#2015e59d839074e76838de2159db421966fd8b63" - integrity sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw== +"@babel/plugin-transform-regenerator@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz#5f0a28d842f6462281f06a964e88ba8d7ab49753" + integrity sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng== dependencies: regenerator-transform "^0.14.2" -"@babel/plugin-transform-reserved-words@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz#8f2682bcdcef9ed327e1b0861585d7013f8a54dd" - integrity sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ== +"@babel/plugin-transform-reserved-words@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz#6fdfc8cc7edcc42b36a7c12188c6787c873adcd8" + integrity sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-shorthand-properties@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz#9fd25ec5cdd555bb7f473e5e6ee1c971eede4dd6" - integrity sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q== +"@babel/plugin-transform-shorthand-properties@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz#0bf9cac5550fce0cfdf043420f661d645fdc75e3" + integrity sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-spread@^7.11.0": - version "7.11.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz#fa84d300f5e4f57752fe41a6d1b3c554f13f17cc" - integrity sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw== +"@babel/plugin-transform-spread@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz#527f9f311be4ec7fdc2b79bb89f7bf884b3e1e1e" + integrity sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng== dependencies: "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" -"@babel/plugin-transform-sticky-regex@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz#8f3889ee8657581130a29d9cc91d7c73b7c4a28d" - integrity sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ== +"@babel/plugin-transform-sticky-regex@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.1.tgz#5c24cf50de396d30e99afc8d1c700e8bce0f5caf" + integrity sha512-CiUgKQ3AGVk7kveIaPEET1jNDhZZEl1RPMWdTBE1799bdz++SwqDHStmxfCtDfBhQgCl38YRiSnrMuUMZIWSUQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-regex" "^7.10.4" -"@babel/plugin-transform-template-literals@^7.10.4": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c" - integrity sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw== +"@babel/plugin-transform-template-literals@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz#b43ece6ed9a79c0c71119f576d299ef09d942843" + integrity sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-typeof-symbol@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz#9509f1a7eec31c4edbffe137c16cc33ff0bc5bfc" - integrity sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA== +"@babel/plugin-transform-typeof-symbol@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.1.tgz#9ca6be343d42512fbc2e68236a82ae64bc7af78a" + integrity sha512-EPGgpGy+O5Kg5pJFNDKuxt9RdmTgj5sgrus2XVeMp/ZIbOESadgILUbm50SNpghOh3/6yrbsH+NB5+WJTmsA7Q== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-unicode-escapes@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz#feae523391c7651ddac115dae0a9d06857892007" - integrity sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg== +"@babel/plugin-transform-unicode-escapes@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz#5232b9f81ccb07070b7c3c36c67a1b78f1845709" + integrity sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-unicode-regex@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz#e56d71f9282fac6db09c82742055576d5e6d80a8" - integrity sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A== +"@babel/plugin-transform-unicode-regex@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz#cc9661f61390db5c65e3febaccefd5c6ac3faecb" + integrity sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-create-regexp-features-plugin" "^7.12.1" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/preset-env@7.11.5": - version "7.11.5" - resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.5.tgz#18cb4b9379e3e92ffea92c07471a99a2914e4272" - integrity sha512-kXqmW1jVcnB2cdueV+fyBM8estd5mlNfaQi6lwLgRwCby4edpavgbFhiBNjmWA3JpB/yZGSISa7Srf+TwxDQoA== +"@babel/preset-env@7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.1.tgz#9c7e5ca82a19efc865384bb4989148d2ee5d7ac2" + integrity sha512-H8kxXmtPaAGT7TyBvSSkoSTUK6RHh61So05SyEbpmr0MCZrsNYn7mGMzzeYoOUCdHzww61k8XBft2TaES+xPLg== dependencies: - "@babel/compat-data" "^7.11.0" - "@babel/helper-compilation-targets" "^7.10.4" - "@babel/helper-module-imports" "^7.10.4" + "@babel/compat-data" "^7.12.1" + "@babel/helper-compilation-targets" "^7.12.1" + "@babel/helper-module-imports" "^7.12.1" "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-proposal-async-generator-functions" "^7.10.4" - "@babel/plugin-proposal-class-properties" "^7.10.4" - "@babel/plugin-proposal-dynamic-import" "^7.10.4" - "@babel/plugin-proposal-export-namespace-from" "^7.10.4" - "@babel/plugin-proposal-json-strings" "^7.10.4" - "@babel/plugin-proposal-logical-assignment-operators" "^7.11.0" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4" - "@babel/plugin-proposal-numeric-separator" "^7.10.4" - "@babel/plugin-proposal-object-rest-spread" "^7.11.0" - "@babel/plugin-proposal-optional-catch-binding" "^7.10.4" - "@babel/plugin-proposal-optional-chaining" "^7.11.0" - "@babel/plugin-proposal-private-methods" "^7.10.4" - "@babel/plugin-proposal-unicode-property-regex" "^7.10.4" + "@babel/helper-validator-option" "^7.12.1" + "@babel/plugin-proposal-async-generator-functions" "^7.12.1" + "@babel/plugin-proposal-class-properties" "^7.12.1" + "@babel/plugin-proposal-dynamic-import" "^7.12.1" + "@babel/plugin-proposal-export-namespace-from" "^7.12.1" + "@babel/plugin-proposal-json-strings" "^7.12.1" + "@babel/plugin-proposal-logical-assignment-operators" "^7.12.1" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.1" + "@babel/plugin-proposal-numeric-separator" "^7.12.1" + "@babel/plugin-proposal-object-rest-spread" "^7.12.1" + "@babel/plugin-proposal-optional-catch-binding" "^7.12.1" + "@babel/plugin-proposal-optional-chaining" "^7.12.1" + "@babel/plugin-proposal-private-methods" "^7.12.1" + "@babel/plugin-proposal-unicode-property-regex" "^7.12.1" "@babel/plugin-syntax-async-generators" "^7.8.0" - "@babel/plugin-syntax-class-properties" "^7.10.4" + "@babel/plugin-syntax-class-properties" "^7.12.1" "@babel/plugin-syntax-dynamic-import" "^7.8.0" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" "@babel/plugin-syntax-json-strings" "^7.8.0" @@ -815,45 +873,42 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-syntax-top-level-await" "^7.10.4" - "@babel/plugin-transform-arrow-functions" "^7.10.4" - "@babel/plugin-transform-async-to-generator" "^7.10.4" - "@babel/plugin-transform-block-scoped-functions" "^7.10.4" - "@babel/plugin-transform-block-scoping" "^7.10.4" - "@babel/plugin-transform-classes" "^7.10.4" - "@babel/plugin-transform-computed-properties" "^7.10.4" - "@babel/plugin-transform-destructuring" "^7.10.4" - "@babel/plugin-transform-dotall-regex" "^7.10.4" - "@babel/plugin-transform-duplicate-keys" "^7.10.4" - "@babel/plugin-transform-exponentiation-operator" "^7.10.4" - "@babel/plugin-transform-for-of" "^7.10.4" - "@babel/plugin-transform-function-name" "^7.10.4" - "@babel/plugin-transform-literals" "^7.10.4" - "@babel/plugin-transform-member-expression-literals" "^7.10.4" - "@babel/plugin-transform-modules-amd" "^7.10.4" - "@babel/plugin-transform-modules-commonjs" "^7.10.4" - "@babel/plugin-transform-modules-systemjs" "^7.10.4" - "@babel/plugin-transform-modules-umd" "^7.10.4" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.4" - "@babel/plugin-transform-new-target" "^7.10.4" - "@babel/plugin-transform-object-super" "^7.10.4" - "@babel/plugin-transform-parameters" "^7.10.4" - "@babel/plugin-transform-property-literals" "^7.10.4" - "@babel/plugin-transform-regenerator" "^7.10.4" - "@babel/plugin-transform-reserved-words" "^7.10.4" - "@babel/plugin-transform-shorthand-properties" "^7.10.4" - "@babel/plugin-transform-spread" "^7.11.0" - "@babel/plugin-transform-sticky-regex" "^7.10.4" - "@babel/plugin-transform-template-literals" "^7.10.4" - "@babel/plugin-transform-typeof-symbol" "^7.10.4" - "@babel/plugin-transform-unicode-escapes" "^7.10.4" - "@babel/plugin-transform-unicode-regex" "^7.10.4" + "@babel/plugin-syntax-top-level-await" "^7.12.1" + "@babel/plugin-transform-arrow-functions" "^7.12.1" + "@babel/plugin-transform-async-to-generator" "^7.12.1" + "@babel/plugin-transform-block-scoped-functions" "^7.12.1" + "@babel/plugin-transform-block-scoping" "^7.12.1" + "@babel/plugin-transform-classes" "^7.12.1" + "@babel/plugin-transform-computed-properties" "^7.12.1" + "@babel/plugin-transform-destructuring" "^7.12.1" + "@babel/plugin-transform-dotall-regex" "^7.12.1" + "@babel/plugin-transform-duplicate-keys" "^7.12.1" + "@babel/plugin-transform-exponentiation-operator" "^7.12.1" + "@babel/plugin-transform-for-of" "^7.12.1" + "@babel/plugin-transform-function-name" "^7.12.1" + "@babel/plugin-transform-literals" "^7.12.1" + "@babel/plugin-transform-member-expression-literals" "^7.12.1" + "@babel/plugin-transform-modules-amd" "^7.12.1" + "@babel/plugin-transform-modules-commonjs" "^7.12.1" + "@babel/plugin-transform-modules-systemjs" "^7.12.1" + "@babel/plugin-transform-modules-umd" "^7.12.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.12.1" + "@babel/plugin-transform-new-target" "^7.12.1" + "@babel/plugin-transform-object-super" "^7.12.1" + "@babel/plugin-transform-parameters" "^7.12.1" + "@babel/plugin-transform-property-literals" "^7.12.1" + "@babel/plugin-transform-regenerator" "^7.12.1" + "@babel/plugin-transform-reserved-words" "^7.12.1" + "@babel/plugin-transform-shorthand-properties" "^7.12.1" + "@babel/plugin-transform-spread" "^7.12.1" + "@babel/plugin-transform-sticky-regex" "^7.12.1" + "@babel/plugin-transform-template-literals" "^7.12.1" + "@babel/plugin-transform-typeof-symbol" "^7.12.1" + "@babel/plugin-transform-unicode-escapes" "^7.12.1" + "@babel/plugin-transform-unicode-regex" "^7.12.1" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.11.5" - browserslist "^4.12.0" + "@babel/types" "^7.12.1" core-js-compat "^3.6.2" - invariant "^2.2.2" - levenary "^1.1.1" semver "^5.5.0" "@babel/preset-modules@^0.1.3": @@ -913,6 +968,21 @@ globals "^11.1.0" lodash "^4.17.19" +"@babel/traverse@^7.12.5": + version "7.12.5" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz#78a0c68c8e8a35e4cacfd31db8bb303d5606f095" + integrity sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.5" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/parser" "^7.12.5" + "@babel/types" "^7.12.5" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.4.0", "@babel/types@^7.4.4": version "7.11.5" resolved "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" @@ -931,6 +1001,15 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.12.5": + version "7.12.6" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz#ae0e55ef1cce1fbc881cd26f8234eb3e657edc96" + integrity sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@changesets/apply-release-plan@^4.0.0": version "4.0.0" resolved "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-4.0.0.tgz#e78efb56a4e459a8dab814ba43045f2ace0f27c9" @@ -970,10 +1049,10 @@ "@changesets/types" "^3.0.0" dotenv "^8.1.0" -"@changesets/cli@2.11.0": - version "2.11.0" - resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.11.0.tgz#048449289da95f24d96b187f3148c2955065d609" - integrity sha512-imsXWxl+QpLt6PYHSiuZS7xPdKrGs2c8saSG4ENzeZBgnTBMiJdxDloOT6Bmv15ICggHZj9mnaUBNOMPbvnlbA== +"@changesets/cli@2.11.2": + version "2.11.2" + resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.11.2.tgz#6c2d9470a9c89e7389db5ac80069a77072079413" + integrity sha512-Lfw4MWj46H7dgPzgYmRJ8QbpDxi02dK+21zuWzBjVtsf3AqJgy7oVdXl4Yga/JhBq8eeoxBS9NoCVw8/JOZBcg== dependencies: "@babel/runtime" "^7.10.4" "@changesets/apply-release-plan" "^4.0.0" @@ -997,14 +1076,14 @@ fs-extra "^7.0.1" human-id "^1.0.2" is-ci "^2.0.0" - meow "^5.0.0" + meow "^6.0.0" outdent "^0.5.0" p-limit "^2.2.0" preferred-pm "^3.0.0" semver "^5.4.1" spawndamnit "^2.0.0" term-size "^2.1.0" - tty-table "^2.7.0" + tty-table "^2.8.10" "@changesets/config@^1.2.0": version "1.3.0" @@ -1157,10 +1236,10 @@ enabled "2.0.x" kuler "^2.0.0" -"@eslint/eslintrc@^0.1.3": - version "0.1.3" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz#7d1a2b2358552cc04834c0979bd4275362e37085" - integrity sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA== +"@eslint/eslintrc@^0.2.1": + version "0.2.1" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.1.tgz#f72069c330461a06684d119384435e12a5d76e3c" + integrity sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA== dependencies: ajv "^6.12.4" debug "^4.1.1" @@ -1297,7 +1376,16 @@ retry-request "^4.1.1" teeny-request "^7.0.0" -"@google-cloud/firestore@4.4.0", "@google-cloud/firestore@^4.0.0": +"@google-cloud/firestore@4.7.0": + version "4.7.0" + resolved "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.7.0.tgz#8a176e41773b2a06320e1281d93994624b321541" + integrity sha512-srkT0LxbKBEo3hWlgjJenT6+bPJK4D+vuKiV/EZFc6sWhDNQwgOgKF6Rf16mggHwKOL9Sx08Veu0BUaX1uyh4g== + dependencies: + fast-deep-equal "^3.1.1" + functional-red-black-tree "^1.0.1" + google-gax "^2.2.0" + +"@google-cloud/firestore@^4.0.0": version "4.4.0" resolved "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.4.0.tgz#6cdbd462f32a8f94e138c57ef81195156c79e680" integrity sha512-nixsumd4C7eL+hHEgyihspzhBBNe3agsvNFRX0xfqO3uR/6ro4CUj9XdcCvdnSSd3yTyqKfdBSRK2fEj1jIbYg== @@ -2589,10 +2677,10 @@ resolved "https://registry.npmjs.org/@types/chai/-/chai-4.2.12.tgz#6160ae454cd89dae05adc3bb97997f488b608201" integrity sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ== -"@types/chai@4.2.13": - version "4.2.13" - resolved "https://registry.npmjs.org/@types/chai/-/chai-4.2.13.tgz#8a3801f6655179d1803d81e94a2e4aaf317abd16" - integrity sha512-o3SGYRlOpvLFpwJA6Sl1UPOwKFEvE4FxTEB/c9XHI2whdnd4kmPVkNLL8gY4vWGBxWWDumzLbKsAhEH5SKn37Q== +"@types/chai@4.2.14": + version "4.2.14" + resolved "https://registry.npmjs.org/@types/chai/-/chai-4.2.14.tgz#44d2dd0b5de6185089375d976b4ec5caf6861193" + integrity sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ== "@types/child-process-promise@2.2.1": version "2.2.1" @@ -2724,10 +2812,10 @@ resolved "https://registry.npmjs.org/@types/mocha/-/mocha-7.0.2.tgz#b17f16cf933597e10d6d78eae3251e692ce8b0ce" integrity sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w== -"@types/mz@2.7.1": - version "2.7.1" - resolved "https://registry.npmjs.org/@types/mz/-/mz-2.7.1.tgz#1ac1d69b039c8b3cbe603972b5c12d3167a84f58" - integrity sha512-H86h7KmRDVs9UeSiQvtUeVhS+WYpJSYSsZrRvNYpGWGiytEqxwEtvgRnINESQtCgnojIH2wS2WgaMTJP0firBw== +"@types/mz@2.7.2": + version "2.7.2" + resolved "https://registry.npmjs.org/@types/mz/-/mz-2.7.2.tgz#48de03aacc98aeb40cfdeb8c58fff9fb8a2d8dfc" + integrity sha512-a6E4fuAakzq/uRHEohiYo9M5G+NV33FodRGCU+N4W65CAk6PPJo9KWPH8hxkqiiUgfs8EgUFuKPxclcUEahsRw== dependencies: "@types/node" "*" @@ -2749,10 +2837,10 @@ resolved "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== -"@types/node@12.12.67": - version "12.12.67" - resolved "https://registry.npmjs.org/@types/node/-/node-12.12.67.tgz#4f86badb292e822e3b13730a1f9713ed2377f789" - integrity sha512-R48tgL2izApf+9rYNH+3RBMbRpPeW3N8f0I9HMhggeq4UXwBDqumJ14SDs4ctTMhG11pIOduZ4z3QWGOiMc9Vg== +"@types/node@12.19.3": + version "12.19.3" + resolved "https://registry.npmjs.org/@types/node/-/node-12.19.3.tgz#a6e252973214079155f749e8bef99cc80af182fa" + integrity sha512-8Jduo8wvvwDzEVJCOvS/G6sgilOLvvhn1eMmK3TW8/T217O7u1jdrK6ImKLv80tVryaPSVeKu6sjDEiFjd4/eg== "@types/node@^10.10.0": version "10.17.35" @@ -2899,10 +2987,10 @@ "@types/source-list-map" "*" source-map "^0.7.3" -"@types/webpack@4.41.22": - version "4.41.22" - resolved "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.22.tgz#ff9758a17c6bd499e459b91e78539848c32d0731" - integrity sha512-JQDJK6pj8OMV9gWOnN1dcLCyU9Hzs6lux0wBO4lr1+gyEhIBR9U3FMrz12t2GPkg110XAxEAw2WHF6g7nZIbRQ== +"@types/webpack@4.41.24": + version "4.41.24" + resolved "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.24.tgz#75b664abe3d5bcfe54e64313ca3b43e498550422" + integrity sha512-1A0MXPwZiMOD3DPMuOKUKcpkdPo8Lq33UGggZ7xio6wJ/jV1dAu5cXDrOfGDnldUroPIRLsr/DT43/GqOA4RFQ== dependencies: "@types/anymatch" "*" "@types/node" "*" @@ -2916,10 +3004,10 @@ resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== -"@types/yargs@15.0.8": - version "15.0.8" - resolved "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.8.tgz#7644904cad7427eb704331ea9bf1ee5499b82e23" - integrity sha512-b0BYzFUzBpOhPjpl1wtAHU994jBeKF4TKVlT7ssFv44T617XNcPdRoG4AzHLVshLzlrF7i3lTelH7UbuNYV58Q== +"@types/yargs@15.0.9": + version "15.0.9" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.9.tgz#524cd7998fe810cdb02f26101b699cccd156ff19" + integrity sha512-HmU8SeIRhZCWcnRskCs36Q1Q00KBV6Cqh/ora8WN1+22dY07AZdn6Gel8QZ3t26XYPImtcL8WV/eqjhVmMEw4g== dependencies: "@types/yargs-parser" "*" @@ -2930,69 +3018,69 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin-tslint@4.4.1": - version "4.4.1" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin-tslint/-/eslint-plugin-tslint-4.4.1.tgz#b2fe38ab8e07f6c9de228dd99a1014672aa20513" - integrity sha512-5IVPQjhS2NzWjRpP18SuPnip5ep7zChPZLkPJr1onHg0TADDBolg35hCk9/55FedjBin8LIjzTbyMn24XXr+QQ== +"@typescript-eslint/eslint-plugin-tslint@4.6.1": + version "4.6.1" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin-tslint/-/eslint-plugin-tslint-4.6.1.tgz#a4efbe58469897717e55d900fe33e64242ef17c1" + integrity sha512-mcFnLShw4pXoEGOsZgLo3b7q2OuEWWGj2sHrcn2mZx0dDlF9e+Q12XmLlqw6Q14mQVIUQrmg1E13xTWTpaagmw== dependencies: - "@typescript-eslint/experimental-utils" "4.4.1" + "@typescript-eslint/experimental-utils" "4.6.1" lodash "^4.17.15" -"@typescript-eslint/eslint-plugin@4.4.1": - version "4.4.1" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.4.1.tgz#b8acea0373bd2a388ac47df44652f00bf8b368f5" - integrity sha512-O+8Utz8pb4OmcA+Nfi5THQnQpHSD2sDUNw9AxNHpuYOo326HZTtG8gsfT+EAYuVrFNaLyNb2QnUNkmTRDskuRA== +"@typescript-eslint/eslint-plugin@4.6.1": + version "4.6.1" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.6.1.tgz#99d77eb7a016fd5a5e749d2c44a7e4c317eb7da3" + integrity sha512-SNZyflefTMK2JyrPfFFzzoy2asLmZvZJ6+/L5cIqg4HfKGiW2Gr1Go1OyEVqne/U4QwmoasuMwppoBHWBWF2nA== dependencies: - "@typescript-eslint/experimental-utils" "4.4.1" - "@typescript-eslint/scope-manager" "4.4.1" + "@typescript-eslint/experimental-utils" "4.6.1" + "@typescript-eslint/scope-manager" "4.6.1" debug "^4.1.1" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@4.4.1": - version "4.4.1" - resolved "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.4.1.tgz#40613b9757fa0170de3e0043254dbb077cafac0c" - integrity sha512-Nt4EVlb1mqExW9cWhpV6pd1a3DkUbX9DeyYsdoeziKOpIJ04S2KMVDO+SEidsXRH/XHDpbzXykKcMTLdTXH6cQ== +"@typescript-eslint/experimental-utils@4.6.1": + version "4.6.1" + resolved "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.6.1.tgz#a9c691dfd530a9570274fe68907c24c07a06c4aa" + integrity sha512-qyPqCFWlHZXkEBoV56UxHSoXW2qnTr4JrWVXOh3soBP3q0o7p4pUEMfInDwIa0dB/ypdtm7gLOS0hg0a73ijfg== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.4.1" - "@typescript-eslint/types" "4.4.1" - "@typescript-eslint/typescript-estree" "4.4.1" + "@typescript-eslint/scope-manager" "4.6.1" + "@typescript-eslint/types" "4.6.1" + "@typescript-eslint/typescript-estree" "4.6.1" eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@4.4.1": - version "4.4.1" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.4.1.tgz#25fde9c080611f303f2f33cedb145d2c59915b80" - integrity sha512-S0fuX5lDku28Au9REYUsV+hdJpW/rNW0gWlc4SXzF/kdrRaAVX9YCxKpziH7djeWT/HFAjLZcnY7NJD8xTeUEg== +"@typescript-eslint/parser@4.6.1": + version "4.6.1" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.6.1.tgz#b801bff67b536ecc4a840ac9289ba2be57e02428" + integrity sha512-lScKRPt1wM9UwyKkGKyQDqf0bh6jm8DQ5iN37urRIXDm16GEv+HGEmum2Fc423xlk5NUOkOpfTnKZc/tqKZkDQ== dependencies: - "@typescript-eslint/scope-manager" "4.4.1" - "@typescript-eslint/types" "4.4.1" - "@typescript-eslint/typescript-estree" "4.4.1" + "@typescript-eslint/scope-manager" "4.6.1" + "@typescript-eslint/types" "4.6.1" + "@typescript-eslint/typescript-estree" "4.6.1" debug "^4.1.1" -"@typescript-eslint/scope-manager@4.4.1": - version "4.4.1" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.4.1.tgz#d19447e60db2ce9c425898d62fa03b2cce8ea3f9" - integrity sha512-2oD/ZqD4Gj41UdFeWZxegH3cVEEH/Z6Bhr/XvwTtGv66737XkR4C9IqEkebCuqArqBJQSj4AgNHHiN1okzD/wQ== +"@typescript-eslint/scope-manager@4.6.1": + version "4.6.1" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.6.1.tgz#21872b91cbf7adfc7083f17b8041149148baf992" + integrity sha512-f95+80r6VdINYscJY1KDUEDcxZ3prAWHulL4qRDfNVD0I5QAVSGqFkwHERDoLYJJWmEAkUMdQVvx7/c2Hp+Bjg== dependencies: - "@typescript-eslint/types" "4.4.1" - "@typescript-eslint/visitor-keys" "4.4.1" + "@typescript-eslint/types" "4.6.1" + "@typescript-eslint/visitor-keys" "4.6.1" -"@typescript-eslint/types@4.4.1": - version "4.4.1" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.4.1.tgz#c507b35cf523bc7ba00aae5f75ee9b810cdabbc1" - integrity sha512-KNDfH2bCyax5db+KKIZT4rfA8rEk5N0EJ8P0T5AJjo5xrV26UAzaiqoJCxeaibqc0c/IvZxp7v2g3difn2Pn3w== +"@typescript-eslint/types@4.6.1": + version "4.6.1" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.6.1.tgz#d3ad7478f53f22e7339dc006ab61aac131231552" + integrity sha512-k2ZCHhJ96YZyPIsykickez+OMHkz06xppVLfJ+DY90i532/Cx2Z+HiRMH8YZQo7a4zVd/TwNBuRCdXlGK4yo8w== -"@typescript-eslint/typescript-estree@4.4.1": - version "4.4.1" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.4.1.tgz#598f6de488106c2587d47ca2462c60f6e2797cb8" - integrity sha512-wP/V7ScKzgSdtcY1a0pZYBoCxrCstLrgRQ2O9MmCUZDtmgxCO/TCqOTGRVwpP4/2hVfqMz/Vw1ZYrG8cVxvN3g== +"@typescript-eslint/typescript-estree@4.6.1": + version "4.6.1" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.6.1.tgz#6025cce724329413f57e4959b2d676fceeca246f" + integrity sha512-/J/kxiyjQQKqEr5kuKLNQ1Finpfb8gf/NpbwqFFYEBjxOsZ621r9AqwS9UDRA1Rrr/eneX/YsbPAIhU2rFLjXQ== dependencies: - "@typescript-eslint/types" "4.4.1" - "@typescript-eslint/visitor-keys" "4.4.1" + "@typescript-eslint/types" "4.6.1" + "@typescript-eslint/visitor-keys" "4.6.1" debug "^4.1.1" globby "^11.0.1" is-glob "^4.0.1" @@ -3000,12 +3088,12 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@4.4.1": - version "4.4.1" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.4.1.tgz#1769dc7a9e2d7d2cfd3318b77ed8249187aed5c3" - integrity sha512-H2JMWhLaJNeaylSnMSQFEhT/S/FsJbebQALmoJxMPMxLtlVAMy2uJP/Z543n9IizhjRayLSqoInehCeNW9rWcw== +"@typescript-eslint/visitor-keys@4.6.1": + version "4.6.1" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.6.1.tgz#6b125883402d8939df7b54528d879e88f7ba3614" + integrity sha512-owABze4toX7QXwOLT3/D5a8NecZEjEWU1srqxENTfqsY3bwVnl3YYbOh6s1rp2wQKO9RTHFGjKes08FgE7SVMw== dependencies: - "@typescript-eslint/types" "4.4.1" + "@typescript-eslint/types" "4.6.1" eslint-visitor-keys "^2.0.0" "@webassemblyjs/ast@1.9.0": @@ -4348,7 +4436,17 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.12.0, browserslist@^4.8.5: +browserslist@^4.14.5: + version "4.14.6" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.14.6.tgz#97702a9c212e0c6b6afefad913d3a1538e348457" + integrity sha512-zeFYcUo85ENhc/zxHbiIp0LGzzTrE2Pv2JhxvS7kpUb9Q9D38kUX6Bie7pGutJ/5iF5rOxE7CepAuWD56xJ33A== + dependencies: + caniuse-lite "^1.0.30001154" + electron-to-chromium "^1.3.585" + escalade "^3.1.1" + node-releases "^1.1.65" + +browserslist@^4.8.5: version "4.14.5" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015" integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA== @@ -4606,6 +4704,11 @@ caniuse-lite@^1.0.30001135: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001141.tgz#214a196d81aa938b268fb0cb6d8fab23fdf14378" integrity sha512-EHfInJHoQTmlMdVZrEc5gmwPc0zyN/hVufmGHPbVNQwlk7tJfCmQ2ysRZMY2MeleBivALUTyyxXnQjK18XrVpA== +caniuse-lite@^1.0.30001154: + version "1.0.30001156" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001156.tgz#75c20937b6012fe2b02ab58b30d475bf0718de97" + integrity sha512-z7qztybA2eFZTB6Z3yvaQBIoJpQtsewRD74adw2UbRWwsRq3jIPvgrQGawBMbfafekQaD21FWuXNcywtTDGGCw== + capture-stack-trace@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d" @@ -4917,10 +5020,10 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -cliui@^7.0.0: - version "7.0.1" - resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.1.tgz#a4cb67aad45cd83d8d05128fc9f4d8fbb887e6b3" - integrity sha512-rcvHOWyGyid6I1WjT/3NatKj2kDt9OdSHSXpyLXaMWFbKpGACNW8pRhhdPUq9MWUOdwn8Rz9AVETjF4105rZZQ== +cliui@^7.0.2: + version "7.0.3" + resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.3.tgz#ef180f26c8d9bff3927ee52428bfec2090427981" + integrity sha512-Gj3QHTkVMPKqwP3f7B4KPkBZRMR9r4rfi5bXFpg1a+Svvj8l7q5CnkBkVQzfxT5DFSsGk2+PascOgL0JYkL2kw== dependencies: string-width "^4.2.0" strip-ansi "^6.0.0" @@ -6213,6 +6316,11 @@ electron-to-chromium@^1.3.571: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.576.tgz#2e70234484e03d7c7e90310d7d79fd3775379c34" integrity sha512-uSEI0XZ//5ic+0NdOqlxp0liCD44ck20OAGyLMSymIWTEAtHKVJi6JM18acOnRgUgX7Q65QqnI+sNncNvIy8ew== +electron-to-chromium@^1.3.585: + version "1.3.588" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.588.tgz#c6515571737bfb42678115a5eaa818384593a9a5" + integrity sha512-0zr+ZfytnLeJZxGgmEpPTcItu5Mm4A5zHPZXLfHcGp0mdsk95rmD7ePNewYtK1yIdLbk8Z1U2oTRRfOtR4gbYg== + elegant-spinner@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" @@ -6462,11 +6570,16 @@ es6-weak-map@^2.0.1, es6-weak-map@^2.0.2: es6-iterator "^2.0.3" es6-symbol "^3.1.1" -escalade@^3.0.2, escalade@^3.1.0: +escalade@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e" integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig== +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + escape-goat@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" @@ -6550,13 +6663,13 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint@7.11.0: - version "7.11.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-7.11.0.tgz#aaf2d23a0b5f1d652a08edacea0c19f7fadc0b3b" - integrity sha512-G9+qtYVCHaDi1ZuWzBsOWo2wSwd70TXnU6UHA3cTYHp7gCTXZcpggWFoUVAMRarg68qtPoNfFbzPh+VdOgmwmw== +eslint@7.12.1: + version "7.12.1" + resolved "https://registry.npmjs.org/eslint/-/eslint-7.12.1.tgz#bd9a81fa67a6cfd51656cdb88812ce49ccec5801" + integrity sha512-HlMTEdr/LicJfN08LB3nM1rRYliDXOmfoO4vj39xN6BLpFzF00hbwBoqHk8UcJ2M/3nlARZWy/mslvGEuZFvsg== dependencies: "@babel/code-frame" "^7.0.0" - "@eslint/eslintrc" "^0.1.3" + "@eslint/eslintrc" "^0.2.1" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -7197,10 +7310,10 @@ fined@^1.0.1: object.pick "^1.2.0" parse-filepath "^1.0.1" -firebase-admin@9.2.0: - version "9.2.0" - resolved "https://registry.npmjs.org/firebase-admin/-/firebase-admin-9.2.0.tgz#df5176e2d0c5711df6dbf7012320492a703538ea" - integrity sha512-LhnMYl71B4gP1FlTLfwaYlOWhBCAcNF+byb2CPTfaW/T4hkp4qlXOgo2bws/zbAv5X9GTFqGir3KexMslVGsIA== +firebase-admin@9.3.0: + version "9.3.0" + resolved "https://registry.npmjs.org/firebase-admin/-/firebase-admin-9.3.0.tgz#05f3efb1bb97f17b2c562f4b59820a381d2f903f" + integrity sha512-qMUITOp2QKLLc2o0/wSiDC2OO2knejjieZN/8Or9AzfFk8ftTcUKq5ALNlQXu+7aUzGe0IwSJq9TVnkIU0h1xw== dependencies: "@firebase/database" "^0.6.10" "@firebase/database-types" "^0.5.2" @@ -7222,10 +7335,10 @@ firebase-functions@3.11.0: express "^4.17.1" lodash "^4.17.14" -firebase-tools@8.12.1: - version "8.12.1" - resolved "https://registry.npmjs.org/firebase-tools/-/firebase-tools-8.12.1.tgz#360b291cbe26545231764ab1f8fbd265ee8c48d3" - integrity sha512-nNXtLdlVTjBz5PEMMmInhEp23mBD5NRJ791j2BONTFwd+KH8dXBopAhLuSkRtGp8XNDSNcRwjaNPCoLYE5RoZg== +firebase-tools@8.15.0: + version "8.15.0" + resolved "https://registry.npmjs.org/firebase-tools/-/firebase-tools-8.15.0.tgz#cfbad03c33f48e3854371c4d034b3ab43fa6794f" + integrity sha512-NE+RtSqYw8Qjs1Ud5D4HsBIepFBUuL65ZlU6KUtm7dgZl6b7N9gC2nsjagNzdDYV27vgvnR+2mf2W1dCRsD0Fw== dependencies: "@google-cloud/pubsub" "^1.7.0" JSONStream "^1.2.1" @@ -7259,6 +7372,7 @@ firebase-tools@8.12.1: marked-terminal "^3.3.0" minimatch "^3.0.4" morgan "^1.10.0" + node-fetch "^2.6.1" open "^6.3.0" ora "^3.4.0" plist "^3.0.1" @@ -8074,15 +8188,10 @@ google-closure-compiler@20200628.0.0: google-closure-compiler-osx "^20200628.0.0" google-closure-compiler-windows "^20200628.0.0" -google-closure-library@20200224.0.0: - version "20200224.0.0" - resolved "https://registry.npmjs.org/google-closure-library/-/google-closure-library-20200224.0.0.tgz#70c36576b21c81c514cfbfcc001780c2fdacd173" - integrity sha512-cF9LP7F5Klj4go5TB4cPpcCvC/qgSVNYgzVS+bzxPgLvIiVL8aWOwApj6rsCkPY9Yr675FouylqNE24F31LWeQ== - -google-closure-library@20200628.0.0: - version "20200628.0.0" - resolved "https://registry.npmjs.org/google-closure-library/-/google-closure-library-20200628.0.0.tgz#0a1b6f40a4b1dc408832d33bfe3494a2ef212f3f" - integrity sha512-G+PoGe8vtcjxszr1ie3GSrlxjO+gZDuOZClanGJMWHsefUynzPqJeIGh3zMpSsCAXZXtO6DpB59l99DVvRVcVQ== +google-closure-library@20200830.0.0: + version "20200830.0.0" + resolved "https://registry.npmjs.org/google-closure-library/-/google-closure-library-20200830.0.0.tgz#9f3807e5a4af55ebf2c8a22853d53b8da39a48e8" + integrity sha512-s4ma73K+FTeVywSMjVOxQ435t6kPfSlxEtIflq7Gabp2fxAnc9i8vUpvT8ZP/GH89LwSJReIaBGtrn72rfNC5Q== google-gax@^1.14.2: version "1.15.3" @@ -8897,7 +9006,7 @@ interpret@^1.0.0, interpret@^1.4.0: resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== -invariant@^2.2.2, invariant@^2.2.4: +invariant@^2.2.2: version "2.2.4" resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -9353,7 +9462,7 @@ is-wsl@^1.1.0: resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= -is-wsl@^2.1.0: +is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -9911,12 +10020,13 @@ karma-coverage-istanbul-reporter@2.1.1: istanbul-api "^2.1.6" minimatch "^3.0.4" -karma-firefox-launcher@1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-1.3.0.tgz#ebcbb1d1ddfada6be900eb8fae25bcf2dcdc8171" - integrity sha512-Fi7xPhwrRgr+94BnHX0F5dCl1miIW4RHnzjIGxF8GaIEp7rNqX7LSi7ok63VXs3PS/5MQaQMhGxw+bvD+pibBQ== +karma-firefox-launcher@2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.0.tgz#d0d328c93dfcf9b46f1ac83b4bb32f43aadb2050" + integrity sha512-dkiyqN2R6fCWt78rciOXJLFDWcQ7QEQi++HgebPJlw1y0ycDjGNDHuSrhdh48QG02fzZKK20WHFWVyBZ6CPngg== dependencies: - is-wsl "^2.1.0" + is-wsl "^2.2.0" + which "^2.0.1" karma-mocha-reporter@2.2.5: version "2.2.5" @@ -10182,13 +10292,6 @@ leven@^3.1.0: resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== -levenary@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77" - integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ== - dependencies: - leven "^3.1.0" - levn@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -10944,20 +11047,22 @@ meow@^4.0.0: redent "^2.0.0" trim-newlines "^2.0.0" -meow@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4" - integrity sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig== +meow@^6.0.0: + version "6.1.1" + resolved "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467" + integrity sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg== dependencies: - camelcase-keys "^4.0.0" - decamelize-keys "^1.0.0" - loud-rejection "^1.0.0" - minimist-options "^3.0.1" - normalize-package-data "^2.3.4" - read-pkg-up "^3.0.0" - redent "^2.0.0" - trim-newlines "^2.0.0" - yargs-parser "^10.0.0" + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "^4.0.2" + normalize-package-data "^2.5.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.13.1" + yargs-parser "^18.1.3" meow@^7.0.0: version "7.1.1" @@ -11100,7 +11205,7 @@ minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.3, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist-options@4.1.0: +minimist-options@4.1.0, minimist-options@^4.0.2: version "4.1.0" resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== @@ -11546,6 +11651,11 @@ node-releases@^1.1.61: resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e" integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g== +node-releases@^1.1.65: + version "1.1.65" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.65.tgz#52d9579176bd60f23eba05c4438583f341944b81" + integrity sha512-YpzJOe2WFIW0V4ZkJQd/DGR/zdVwc/pI4Nl1CZrBO19FdRcSTmsuhdttw9rsTzzJLrNcSloLiBbEYx1C4f6gpA== + node-status-codes@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f" @@ -12628,10 +12738,10 @@ promise-inflight@^1.0.1: resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= -promise-polyfill@8.1.3: - version "8.1.3" - resolved "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.3.tgz#8c99b3cf53f3a91c68226ffde7bde81d7f904116" - integrity sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g== +promise-polyfill@8.2.0: + version "8.2.0" + resolved "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.0.tgz#367394726da7561457aba2133c9ceefbd6267da0" + integrity sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g== promise-polyfill@^6.0.1: version "6.1.0" @@ -13183,7 +13293,7 @@ regexpp@^3.0.0, regexpp@^3.1.0: resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== -regexpu-core@^4.7.0: +regexpu-core@^4.7.0, regexpu-core@^4.7.1: version "4.7.1" resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== @@ -13538,10 +13648,10 @@ rollup-plugin-terser@7.0.2: serialize-javascript "^4.0.0" terser "^5.0.0" -rollup-plugin-typescript2@0.27.3: - version "0.27.3" - resolved "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.27.3.tgz#cd9455ac026d325b20c5728d2cc54a08a771b68b" - integrity sha512-gmYPIFmALj9D3Ga1ZbTZAKTXq1JKlTQBtj299DXhqYz9cL3g/AQfUvbb2UhH+Nf++cCq941W2Mv7UcrcgLzJJg== +rollup-plugin-typescript2@0.29.0: + version "0.29.0" + resolved "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.29.0.tgz#b7ad83f5241dbc5bdf1e98d9c3fca005ffe39e1a" + integrity sha512-YytahBSZCIjn/elFugEGQR5qTsVhxhUwGZIsA9TmrSsC88qroGo65O5HZP/TTArH2dm0vUmYWhKchhwi2wL9bw== dependencies: "@rollup/pluginutils" "^3.1.0" find-cache-dir "^3.3.1" @@ -13566,10 +13676,10 @@ rollup-pluginutils@^2.6.0: dependencies: estree-walker "^0.6.1" -rollup@2.29.0: - version "2.29.0" - resolved "https://registry.npmjs.org/rollup/-/rollup-2.29.0.tgz#0c5c5968530b21ca0e32f8b94b7cd9346cfb0eec" - integrity sha512-gtU0sjxMpsVlpuAf4QXienPmUAhd6Kc7owQ4f5lypoxBW18fw2UNYZ4NssLGsri6WhUZkE/Ts3EMRebN+gNLiQ== +rollup@2.33.1: + version "2.33.1" + resolved "https://registry.npmjs.org/rollup/-/rollup-2.33.1.tgz#802795164164ee63cd47769d8879c33ec8ae0c40" + integrity sha512-uY4O/IoL9oNW8MMcbA5hcOaz6tZTMIh7qJHx/tzIJm+n1wLoY38BLn6fuy7DhR57oNFLMbDQtDeJoFURt5933w== optionalDependencies: fsevents "~2.1.2" @@ -13934,10 +14044,10 @@ sinon-chai@3.5.0: resolved "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.5.0.tgz#c9a78304b0e15befe57ef68e8a85a00553f5c60e" integrity sha512-IifbusYiQBpUxxFJkR3wTU68xzBN0+bxCScEaKMjBvAQERg6FnTTc1F17rseLb1tjmkJ23730AXpFI0c47FgAg== -sinon@9.2.0: - version "9.2.0" - resolved "https://registry.npmjs.org/sinon/-/sinon-9.2.0.tgz#1d333967e30023609f7347351ebc0dc964c0f3c9" - integrity sha512-eSNXz1XMcGEMHw08NJXSyTHIu6qTCOiN8x9ODACmZpNQpr0aXTBXBnI4xTzQzR+TEpOmLiKowGf9flCuKIzsbw== +sinon@9.2.1: + version "9.2.1" + resolved "https://registry.npmjs.org/sinon/-/sinon-9.2.1.tgz#64cc88beac718557055bd8caa526b34a2231be6d" + integrity sha512-naPfsamB5KEE1aiioaoqJ6MEhdUs/2vtI5w1hPAXX/UwvoPjXcwh1m5HiKx0HGgKR8lQSoFIgY5jM6KK8VrS9w== dependencies: "@sinonjs/commons" "^1.8.1" "@sinonjs/fake-timers" "^6.0.1" @@ -14110,10 +14220,10 @@ source-list-map@^2.0.0: resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== -source-map-loader@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.1.1.tgz#1dd964294cfcc3d9bab65f46af97a38d8ae0c65d" - integrity sha512-m2HjSWP2R1yR9P31e4+ciGHFOPvW6GmqHgZkneOkrME2VvWysXTGi4o0yS28iKWWP3vAUmAoa+3x5ZRI2BIX6A== +source-map-loader@1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.1.2.tgz#5b782bf08496d3a7f355e1780df0e25190a80991" + integrity sha512-bjf6eSENOYBX4JZDfl9vVLNsGAQ6Uz90fLmOazcmMcyDYOBFsGxPNn83jXezWLY9bJsVAo1ObztxPcV8HAbjVA== dependencies: abab "^2.0.5" iconv-lite "^0.6.2" @@ -14831,10 +14941,10 @@ terser-webpack-plugin@^1.4.3: webpack-sources "^1.4.0" worker-farm "^1.7.0" -terser@5.3.5: - version "5.3.5" - resolved "https://registry.npmjs.org/terser/-/terser-5.3.5.tgz#9e080baa0568f96654621b20eb9effa440b1484e" - integrity sha512-Qw3CZAMmmfU824AoGKalx+riwocSI5Cs0PoGp9RdSLfmxkmJgyBxqLBP/isDNtFyhHnitikvRMZzyVgeq+U+Tg== +terser@5.3.8: + version "5.3.8" + resolved "https://registry.npmjs.org/terser/-/terser-5.3.8.tgz#991ae8ba21a3d990579b54aa9af11586197a75dd" + integrity sha512-zVotuHoIfnYjtlurOouTazciEfL7V38QMAOhGqpXDEg6yT13cF4+fEP9b0rrCEQTn+tT46uxgFsTZzhygk+CzQ== dependencies: commander "^2.20.0" source-map "~0.7.2" @@ -15148,15 +15258,15 @@ try-require@^1.0.0: resolved "https://registry.npmjs.org/try-require/-/try-require-1.2.1.tgz#34489a2cac0c09c1cc10ed91ba011594d4333be2" integrity sha1-NEiaLKwMCcHMEO2RugEVlNQzO+I= -ts-essentials@7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.0.tgz#eb807945d65e258bae8914f541543758f879f6c5" - integrity sha512-DptrzFAwb5afnsTdKxfScHqLbVZHl7YsxvDT+iT8tbXWFGSzbXALhfWlal25HBesqlX0NZd6wz9KBGnJcWScdQ== +ts-essentials@7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.1.tgz#d205508cae0cdadfb73c89503140cf2228389e2d" + integrity sha512-8lwh3QJtIc1UWhkQtr9XuksXu3O0YQdEE5g79guDfhCaU1FWTDIEDZ1ZSx4HTHUmlJZ8L812j3BZQ4a0aOUkSA== -ts-loader@8.0.5: - version "8.0.5" - resolved "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.5.tgz#fa42b9305247eb964843df1ecb0e589b1bff0f77" - integrity sha512-MvLXmjDxl2Mhv17nvkrB6BrpC8FTwSb7K38oIgdUI6BMx4XgVbljmcoOzlrYn4wyjNTFQ3utd7s2TyigJyR3YA== +ts-loader@8.0.9: + version "8.0.9" + resolved "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.9.tgz#890fc25f49a99124268f4e738ed22d00f666dc37" + integrity sha512-rQd+iIfz5z4HSVzhhRFP4M2OQ0QmihilWWauYvvowBfnRvr4DW+gqA2om70xp/07EQj1qBkLMWobnXsgmWMbmg== dependencies: chalk "^2.3.0" enhanced-resolve "^4.0.0" @@ -15243,7 +15353,7 @@ tty-browserify@^0.0.1: resolved "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw== -tty-table@^2.7.0: +tty-table@^2.8.10: version "2.8.13" resolved "https://registry.npmjs.org/tty-table/-/tty-table-2.8.13.tgz#d484a416381973eaebbdf19c79136b390e5c6d70" integrity sha512-eVV/+kB6fIIdx+iUImhXrO22gl7f6VmmYh0Zbu6C196fe1elcHXd7U6LcLXu0YoVPc2kNesWiukYcdK8ZmJ6aQ== @@ -15379,10 +15489,10 @@ typescript@3.7.x: resolved "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw== -typescript@4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" - integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== +typescript@4.0.5: + version "4.0.5" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389" + integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ== typescript@~3.9.5: version "3.9.7" @@ -15963,10 +16073,10 @@ webpack-stream@6.1.0: vinyl "^2.1.0" webpack "^4.26.1" -webpack-virtual-modules@0.3.1: - version "0.3.1" - resolved "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.3.1.tgz#78cbf1a41a699890a706e789a682dd1120558bf4" - integrity sha512-C9Zbb9rny/SaZJ7gTgJjyB2Qt4G4dbT5rVZywYpyk3L6qyf006RepODREXC4rcQCiTPdZnqnebRq5Chsxg+SgQ== +webpack-virtual-modules@0.3.2: + version "0.3.2" + resolved "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.3.2.tgz#b7baa30971a22d99451f897db053af48ec29ad2c" + integrity sha512-RXQXioY6MhzM4CNQwmBwKXYgBs6ulaiQ8bkNQEl2J6Z+V+s7lgl/wGvaI/I0dLnYKB8cKsxQc17QOAVIphPLDw== dependencies: debug "^3.0.0" @@ -16353,10 +16463,10 @@ y18n@^4.0.0: resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== -y18n@^5.0.1: - version "5.0.2" - resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.2.tgz#48218df5da2731b4403115c39a1af709c873f829" - integrity sha512-CkwaeZw6dQgqgPGeTWKMXCRmMcBgETFlTml1+ZOO+q7kGst8NREJ+eWwFNPVUQ4QGdAaklbqCZHH6Zuep1RjiA== +y18n@^5.0.2: + version "5.0.5" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18" + integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== yallist@^2.1.2: version "2.1.2" @@ -16394,13 +16504,6 @@ yargs-parser@5.0.0-security.0: camelcase "^3.0.0" object.assign "^4.1.0" -yargs-parser@^10.0.0: - version "10.1.0" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== - dependencies: - camelcase "^4.1.0" - yargs-parser@^15.0.1: version "15.0.1" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz#54786af40b820dcb2fb8025b11b4d659d76323b3" @@ -16417,10 +16520,10 @@ yargs-parser@^18.1.2, yargs-parser@^18.1.3: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.0.0: - version "20.2.1" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.1.tgz#28f3773c546cdd8a69ddae68116b48a5da328e77" - integrity sha512-yYsjuSkjbLMBp16eaOt7/siKTjNVjMm3SoJnIg3sEh/JsvqVVDyjRKmaJV4cl+lNIgq6QEco2i3gDebJl7/vLA== +yargs-parser@^20.2.2: + version "20.2.3" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.3.tgz#92419ba867b858c868acf8bae9bf74af0dd0ce26" + integrity sha512-emOFRT9WVHw03QSvN5qor9QQT9+sw5vwxfYweivSMHTcAXPefwVae2FjO7JJjj8hCE4CzPOPeFM83VwT29HCww== yargs-unparser@1.6.0: version "1.6.0" @@ -16447,18 +16550,18 @@ yargs@13.3.2, yargs@^13.3.0: y18n "^4.0.0" yargs-parser "^13.1.2" -yargs@16.0.3: - version "16.0.3" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.0.3.tgz#7a919b9e43c90f80d4a142a89795e85399a7e54c" - integrity sha512-6+nLw8xa9uK1BOEOykaiYAJVh6/CjxWXK/q9b5FpRgNslt8s22F2xMBqVIKgCRjNgGvGPBy8Vog7WN7yh4amtA== +yargs@16.1.0: + version "16.1.0" + resolved "https://registry.npmjs.org/yargs/-/yargs-16.1.0.tgz#fc333fe4791660eace5a894b39d42f851cd48f2a" + integrity sha512-upWFJOmDdHN0syLuESuvXDmrRcWd1QafJolHskzaw79uZa7/x53gxQKiR07W59GWY1tFhhU/Th9DrtSfpS782g== dependencies: - cliui "^7.0.0" - escalade "^3.0.2" + cliui "^7.0.2" + escalade "^3.1.1" get-caller-file "^2.0.5" require-directory "^2.1.1" string-width "^4.2.0" - y18n "^5.0.1" - yargs-parser "^20.0.0" + y18n "^5.0.2" + yargs-parser "^20.2.2" yargs@^14.2.2: version "14.2.3" From 80dc41ddcc71c8d90c2c9ae50b222538734002cb Mon Sep 17 00:00:00 2001 From: Wu-Hui Date: Mon, 9 Nov 2020 11:23:56 -0500 Subject: [PATCH 25/27] Address comments --- .changeset/lemon-steaks-draw.md | 4 +- packages/firestore/rollup.shared.js | 69 ++++++++++++++++++- packages/firestore/src/local/local_store.ts | 2 +- .../src/local/shared_client_state_schema.ts | 2 +- 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/.changeset/lemon-steaks-draw.md b/.changeset/lemon-steaks-draw.md index 853d812bb36..26acaf8b2ba 100644 --- a/.changeset/lemon-steaks-draw.md +++ b/.changeset/lemon-steaks-draw.md @@ -1,3 +1,5 @@ --- - +"@firebase/firestore": feature --- + +Add support for loading Firestore Bundle Files. diff --git a/packages/firestore/rollup.shared.js b/packages/firestore/rollup.shared.js index 071714a9fc1..009facb4041 100644 --- a/packages/firestore/rollup.shared.js +++ b/packages/firestore/rollup.shared.js @@ -130,7 +130,74 @@ const manglePrivatePropertiesOptions = { mangle: { properties: { regex: /^__PRIVATE_/, - reserved: ['do'] + // All JS Keywords are reserved. Although this should be taken cared of by + // Terser, we have seen issues with `do`, hence the extra caution. + reserved: [ + 'abstract', + 'arguments', + 'await', + 'boolean', + 'break', + 'byte', + 'case', + 'catch', + 'char', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'double', + 'else', + 'enum', + 'eval', + 'export', + 'extends', + 'false', + 'final', + 'finally', + 'float', + 'for', + 'function', + 'goto', + 'if', + 'implements', + 'import', + 'in', + 'instanceof', + 'int', + 'interface', + 'let', + 'long', + 'native', + 'new', + 'null', + 'package', + 'private', + 'protected', + 'public', + 'return', + 'short', + 'static', + 'super', + 'switch', + 'synchronized', + 'this', + 'throw', + 'throws', + 'transient', + 'true', + 'try', + 'typeof', + 'var', + 'void', + 'volatile', + 'while', + 'with', + 'yield' + ] } } }; diff --git a/packages/firestore/src/local/local_store.ts b/packages/firestore/src/local/local_store.ts index 5531926fa5c..576cece89fc 100644 --- a/packages/firestore/src/local/local_store.ts +++ b/packages/firestore/src/local/local_store.ts @@ -1278,7 +1278,7 @@ export async function ignoreIfPrimaryLeaseLoss( * This ensures that the loaded documents do not get garbage collected * right away. */ -export function umbrellaTarget(bundleName: string): Target { +function umbrellaTarget(bundleName: string): Target { // It is OK that the path used for the query is not valid, because this will // not be read and queried. return queryToTarget( diff --git a/packages/firestore/src/local/shared_client_state_schema.ts b/packages/firestore/src/local/shared_client_state_schema.ts index fd47332bc4a..2f8035fce4e 100644 --- a/packages/firestore/src/local/shared_client_state_schema.ts +++ b/packages/firestore/src/local/shared_client_state_schema.ts @@ -118,7 +118,7 @@ export function createWebStorageOnlineStateKey(persistenceKey: string): string { // The WebStorage prefix that plays as a event to indicate the remote documents // might have changed due to some secondary tabs loading a bundle. // format of the key is: -// firestore_remote_documents_changed_ +// firestore_bundle_loaded_ export const BUNDLE_LOADED_KEY_PREFIX = 'firestore_bundle_loaded'; export function createBundleLoadedKey(persistenceKey: string): string { return `${BUNDLE_LOADED_KEY_PREFIX}_${persistenceKey}`; From d4ab17f040d227b749894314c27ddf335d16ddae Mon Sep 17 00:00:00 2001 From: Wu-Hui Date: Wed, 11 Nov 2020 15:12:15 -0500 Subject: [PATCH 26/27] Remove bundles from public interfaces. --- integration/firestore/firebase_export.ts | 13 +- .../firestore/firebase_export_memory.ts | 13 +- packages/firestore-types/index.d.ts | 10 - packages/firestore/exp/test/shim.ts | 3 +- packages/firestore/register-module.ts | 2 - packages/firestore/src/api/database.ts | 35 +- packages/firestore/src/config.ts | 4 - .../test/integration/api/bundle.test.ts | 490 +++++++++--------- .../test/integration/util/firebase_export.ts | 17 +- 9 files changed, 251 insertions(+), 336 deletions(-) diff --git a/integration/firestore/firebase_export.ts b/integration/firestore/firebase_export.ts index 75c1844a70d..4eeb1c7bfa9 100644 --- a/integration/firestore/firebase_export.ts +++ b/integration/firestore/firebase_export.ts @@ -55,16 +55,5 @@ const Timestamp = firebase.firestore.Timestamp; const GeoPoint = firebase.firestore.GeoPoint; const FieldValue = firebase.firestore.FieldValue; const Blob = firebase.firestore.Blob; -const loadBundle = firebase.firestore.loadBundle; -const namedQuery = firebase.firestore.namedQuery; -export { - Firestore, - FieldValue, - FieldPath, - Timestamp, - Blob, - GeoPoint, - loadBundle, - namedQuery -}; +export { Firestore, FieldValue, FieldPath, Timestamp, Blob, GeoPoint }; diff --git a/integration/firestore/firebase_export_memory.ts b/integration/firestore/firebase_export_memory.ts index 90750994ee3..441c778ef5a 100644 --- a/integration/firestore/firebase_export_memory.ts +++ b/integration/firestore/firebase_export_memory.ts @@ -55,16 +55,5 @@ const Timestamp = firebase.firestore.Timestamp; const GeoPoint = firebase.firestore.GeoPoint; const FieldValue = firebase.firestore.FieldValue; const Blob = firebase.firestore.Blob; -const loadBundle = firebase.firestore.loadBundle; -const namedQuery = firebase.firestore.namedQuery; -export { - Firestore, - FieldValue, - FieldPath, - Timestamp, - Blob, - GeoPoint, - loadBundle, - namedQuery -}; +export { Firestore, FieldValue, FieldPath, Timestamp, Blob, GeoPoint }; diff --git a/packages/firestore-types/index.d.ts b/packages/firestore-types/index.d.ts index d6bef76eba1..d815ea5cad9 100644 --- a/packages/firestore-types/index.d.ts +++ b/packages/firestore-types/index.d.ts @@ -99,16 +99,6 @@ export class FirebaseFirestore { INTERNAL: { delete: () => Promise }; } -export function loadBundle( - db: FirebaseFirestore, - bundleData: ArrayBuffer | ReadableStream | string -): LoadBundleTask; - -export function namedQuery( - db: FirebaseFirestore, - name: string -): Promise | null>; - export interface LoadBundleTask { onProgress( next?: (progress: LoadBundleTaskProgress) => any, diff --git a/packages/firestore/exp/test/shim.ts b/packages/firestore/exp/test/shim.ts index 665ca65c154..e7d4bb2491c 100644 --- a/packages/firestore/exp/test/shim.ts +++ b/packages/firestore/exp/test/shim.ts @@ -55,10 +55,9 @@ import { validateSetOptions } from '../../src/util/input_validation'; import { Compat } from '../../src/compat/compat'; -import { Firestore, loadBundle, namedQuery } from '../../src/api/database'; +import { Firestore } from '../../src/api/database'; export { GeoPoint, Timestamp } from '../index'; -export { loadBundle, namedQuery }; /* eslint-disable @typescript-eslint/no-explicit-any */ diff --git a/packages/firestore/register-module.ts b/packages/firestore/register-module.ts index 2ae215e68b8..f76fabd5a1d 100644 --- a/packages/firestore/register-module.ts +++ b/packages/firestore/register-module.ts @@ -36,8 +36,6 @@ declare module '@firebase/app-types' { Transaction: typeof types.Transaction; WriteBatch: typeof types.WriteBatch; setLogLevel: typeof types.setLogLevel; - loadBundle: typeof types.loadBundle; - namedQuery: typeof types.namedQuery; }; } interface FirebaseApp { diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index baafb861a54..834388c17d5 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -30,9 +30,7 @@ import { firestoreClientGetDocumentViaSnapshotListener, firestoreClientListen, firestoreClientTransaction, - firestoreClientWrite, - firestoreClientLoadBundle, - firestoreClientGetNamedQuery + firestoreClientWrite } from '../core/firestore_client'; import { Bound, @@ -143,7 +141,6 @@ import { import { newUserDataReader } from '../../lite/src/api/reference'; import { makeDatabaseInfo } from '../../lite/src/api/database'; import { DEFAULT_HOST } from '../../lite/src/api/components'; -import { LoadBundleTask } from './bundle'; /** * Constant used to indicate the LRU garbage collection should be disabled. @@ -427,36 +424,6 @@ export function setLogLevel(level: PublicLogLevel): void { setClientLogLevel(level); } -export function loadBundle( - db: Firestore, - bundleData: ArrayBuffer | ReadableStream | string -): LoadBundleTask { - const resultTask = new LoadBundleTask(); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - firestoreClientLoadBundle( - ensureFirestoreConfigured(db._delegate), - bundleData, - resultTask - ); - return resultTask; -} - -export function namedQuery( - db: Firestore, - name: string -): Promise { - return firestoreClientGetNamedQuery( - ensureFirestoreConfigured(db._delegate), - name - ).then(namedQuery => { - if (!namedQuery) { - return null; - } - - return new Query(namedQuery.query, db, null); - }); -} - /** * A reference to a transaction. */ diff --git a/packages/firestore/src/config.ts b/packages/firestore/src/config.ts index bd4a0d31e3f..8de2d9b9dca 100644 --- a/packages/firestore/src/config.ts +++ b/packages/firestore/src/config.ts @@ -30,8 +30,6 @@ import { QuerySnapshot, Transaction, WriteBatch, - loadBundle, - namedQuery, setLogLevel } from './api/database'; import { Blob } from './api/blob'; @@ -56,8 +54,6 @@ const firestoreNamespace = { FieldPath, FieldValue, setLogLevel, - loadBundle, - namedQuery, CACHE_SIZE_UNLIMITED }; diff --git a/packages/firestore/test/integration/api/bundle.test.ts b/packages/firestore/test/integration/api/bundle.test.ts index b3fb949d91a..cfc507d7e62 100644 --- a/packages/firestore/test/integration/api/bundle.test.ts +++ b/packages/firestore/test/integration/api/bundle.test.ts @@ -15,247 +15,249 @@ * limitations under the License. */ -import * as firestore from '@firebase/firestore-types'; -import { expect } from 'chai'; -import { - apiDescribe, - toDataArray, - withAlternateTestDb, - withTestDb -} from '../util/helpers'; -import { EventsAccumulator } from '../util/events_accumulator'; -import * as firebaseExport from '../util/firebase_export'; - -const loadBundle = firebaseExport.loadBundle; -const namedQuery = firebaseExport.namedQuery; - -export const encoder = new TextEncoder(); - -function verifySuccessProgress(p: firestore.LoadBundleTaskProgress): void { - expect(p.taskState).to.equal('Success'); - expect(p.bytesLoaded).to.be.equal(p.totalBytes); - expect(p.documentsLoaded).to.equal(p.totalDocuments); -} - -function verifyInProgress( - p: firestore.LoadBundleTaskProgress, - expectedDocuments: number -): void { - expect(p.taskState).to.equal('Running'); - expect(p.bytesLoaded <= p.totalBytes).to.be.true; - expect(p.documentsLoaded <= p.totalDocuments).to.be.true; - expect(p.documentsLoaded).to.equal(expectedDocuments); -} - -// This template is generated from bundleWithTestDocsAndQueries in '../util/internal_helpsers.ts', -// and manually copied here. -const BUNDLE_TEMPLATE = [ - '{"metadata":{"id":"test-bundle","createTime":{"seconds":1001,"nanos":9999},"version":1,"totalDocuments":2,"totalBytes":1503}}', - '{"namedQuery":{"name":"limit","readTime":{"seconds":1000,"nanos":9999},"bundledQuery":{"parent":"projects/{0}/databases/(default)/documents","structuredQuery":{"from":[{"collectionId":"coll-1"}],"orderBy":[{"field":{"fieldPath":"bar"},"direction":"DESCENDING"},{"field":{"fieldPath":"__name__"},"direction":"DESCENDING"}],"limit":{"value":1}},"limitType":"FIRST"}}}', - '{"namedQuery":{"name":"limit-to-last","readTime":{"seconds":1000,"nanos":9999},"bundledQuery":{"parent":"projects/{0}/databases/(default)/documents","structuredQuery":{"from":[{"collectionId":"coll-1"}],"orderBy":[{"field":{"fieldPath":"bar"},"direction":"DESCENDING"},{"field":{"fieldPath":"__name__"},"direction":"DESCENDING"}],"limit":{"value":1}},"limitType":"LAST"}}}', - '{"documentMetadata":{"name":"projects/{0}/databases/(default)/documents/coll-1/a","readTime":{"seconds":1000,"nanos":9999},"exists":true}}', - '{"document":{"name":"projects/{0}/databases/(default)/documents/coll-1/a","createTime":{"seconds":1,"nanos":9},"updateTime":{"seconds":1,"nanos":9},"fields":{"k":{"stringValue":"a"},"bar":{"integerValue":1}}}}', - '{"documentMetadata":{"name":"projects/{0}/databases/(default)/documents/coll-1/b","readTime":{"seconds":1000,"nanos":9999},"exists":true}}', - '{"document":{"name":"projects/{0}/databases/(default)/documents/coll-1/b","createTime":{"seconds":1,"nanos":9},"updateTime":{"seconds":1,"nanos":9},"fields":{"k":{"stringValue":"b"},"bar":{"integerValue":2}}}}' -]; - -apiDescribe('Bundles', (persistence: boolean) => { - function verifySnapEqualsTestDocs(snap: firestore.QuerySnapshot): void { - expect(toDataArray(snap)).to.deep.equal([ - { k: 'a', bar: 1 }, - { k: 'b', bar: 2 } - ]); - } - - /** - * Returns a valid bundle string from replacing project id in `BUNDLE_TEMPLATE` with the given - * db project id (also recalculate length prefixes). - */ - function bundleString(db: firestore.FirebaseFirestore): string { - const projectId: string = db.app.options.projectId; - - // Extract elements from BUNDLE_TEMPLATE and replace the project ID. - const elements = BUNDLE_TEMPLATE.map(e => e.replace('{0}', projectId)); - - // Recalculating length prefixes for elements that are not BundleMetadata. - let bundleContent = ''; - for (const element of elements.slice(1)) { - const length = encoder.encode(element).byteLength; - bundleContent += `${length}${element}`; - } - - // Update BundleMetadata with new totalBytes. - const totalBytes = encoder.encode(bundleContent).byteLength; - const metadata = JSON.parse(elements[0]); - metadata.metadata.totalBytes = totalBytes; - const metadataContent = JSON.stringify(metadata); - const metadataLength = encoder.encode(metadataContent).byteLength; - return `${metadataLength}${metadataContent}${bundleContent}`; - } - - it('load with documents only with on progress and promise interface', () => { - return withTestDb(persistence, async db => { - const progressEvents: firestore.LoadBundleTaskProgress[] = []; - let completeCalled = false; - const task: firestore.LoadBundleTask = loadBundle(db, bundleString(db)); - task.onProgress( - progress => { - progressEvents.push(progress); - }, - undefined, - () => { - completeCalled = true; - } - ); - await task; - let fulfillProgress: firestore.LoadBundleTaskProgress; - await task.then(progress => { - fulfillProgress = progress; - }); - - expect(completeCalled).to.be.true; - expect(progressEvents.length).to.equal(4); - verifyInProgress(progressEvents[0], 0); - verifyInProgress(progressEvents[1], 1); - verifyInProgress(progressEvents[2], 2); - verifySuccessProgress(progressEvents[3]); - expect(fulfillProgress!).to.deep.equal(progressEvents[3]); - - // Read from cache. These documents do not exist in backend, so they can - // only be read from cache. - let snap = await db.collection('coll-1').get({ source: 'cache' }); - verifySnapEqualsTestDocs(snap); - - snap = await (await namedQuery(db, 'limit'))!.get({ - source: 'cache' - }); - expect(toDataArray(snap)).to.deep.equal([{ k: 'b', bar: 2 }]); - - snap = await (await namedQuery(db, 'limit-to-last'))!.get({ - source: 'cache' - }); - expect(toDataArray(snap)).to.deep.equal([{ k: 'a', bar: 1 }]); - }); - }); - - it('load with documents and queries with promise interface', () => { - return withTestDb(persistence, async db => { - const fulfillProgress: firestore.LoadBundleTaskProgress = await loadBundle( - db, - bundleString(db) - ); - - verifySuccessProgress(fulfillProgress!); - - // Read from cache. These documents do not exist in backend, so they can - // only be read from cache. - const snap = await db.collection('coll-1').get({ source: 'cache' }); - verifySnapEqualsTestDocs(snap); - }); - }); - - it('load for a second time skips', () => { - return withTestDb(persistence, async db => { - await loadBundle(db, bundleString(db)); - - let completeCalled = false; - const progressEvents: firestore.LoadBundleTaskProgress[] = []; - const task: firestore.LoadBundleTask = loadBundle( - db, - encoder.encode(bundleString(db)) - ); - task.onProgress( - progress => { - progressEvents.push(progress); - }, - error => {}, - () => { - completeCalled = true; - } - ); - await task; - - expect(completeCalled).to.be.true; - // No loading actually happened in the second `loadBundle` call only the - // success progress is recorded. - expect(progressEvents.length).to.equal(1); - verifySuccessProgress(progressEvents[0]); - - // Read from cache. These documents do not exist in backend, so they can - // only be read from cache. - const snap = await db.collection('coll-1').get({ source: 'cache' }); - verifySnapEqualsTestDocs(snap); - }); - }); - - it('load with documents already pulled from backend', () => { - return withTestDb(persistence, async db => { - await db.doc('coll-1/a').set({ k: 'a', bar: 0 }); - await db.doc('coll-1/b').set({ k: 'b', bar: 0 }); - - const accumulator = new EventsAccumulator(); - db.collection('coll-1').onSnapshot(accumulator.storeEvent); - await accumulator.awaitEvent(); - - const progress = await loadBundle( - db, - // Testing passing in non-string bundles. - encoder.encode(bundleString(db)) - ); - - verifySuccessProgress(progress); - // The test bundle is holding ancient documents, so no events are - // generated as a result. The case where a bundle has newer doc than - // cache can only be tested in spec tests. - await accumulator.assertNoAdditionalEvents(); - - let snap = await (await namedQuery(db, 'limit'))!.get(); - expect(toDataArray(snap)).to.deep.equal([{ k: 'b', bar: 0 }]); - - snap = await (await namedQuery(db, 'limit-to-last'))!.get(); - expect(toDataArray(snap)).to.deep.equal([{ k: 'a', bar: 0 }]); - }); - }); - - it('loaded documents should not be GC-ed right away', () => { - return withTestDb(persistence, async db => { - const fulfillProgress: firestore.LoadBundleTaskProgress = await loadBundle( - db, - bundleString(db) - ); - - verifySuccessProgress(fulfillProgress!); - - // Read a different collection, this will trigger GC. - let snap = await db.collection('coll-other').get(); - expect(snap.empty).to.be.true; - - // Read the loaded documents, expecting document in cache. With memory - // GC, the documents would get GC-ed if we did not hold the document keys - // in a "umbrella" target. See local_store.ts for details. - snap = await db.collection('coll-1').get({ source: 'cache' }); - verifySnapEqualsTestDocs(snap); - }); - }); - - it('load with documents from other projects fails', () => { - return withTestDb(persistence, async db => { - return withAlternateTestDb(persistence, async otherDb => { - await expect(loadBundle(otherDb, bundleString(db))).to.be.rejectedWith( - 'Tried to deserialize key from different project' - ); - - // Verify otherDb still functions, despite loaded a problematic bundle. - const finalProgress = await loadBundle(otherDb, bundleString(otherDb)); - verifySuccessProgress(finalProgress); - - // Read from cache. These documents do not exist in backend, so they can - // only be read from cache. - const snap = await otherDb - .collection('coll-1') - .get({ source: 'cache' }); - verifySnapEqualsTestDocs(snap); - }); - }); - }); -}); +// TODO(wuandy): Uncomment this file once prototype patch works for bundles + +// import * as firestore from '@firebase/firestore-types'; +// import { expect } from 'chai'; +// import { +// apiDescribe, +// toDataArray, +// withAlternateTestDb, +// withTestDb +// } from '../util/helpers'; +// import { EventsAccumulator } from '../util/events_accumulator'; +// import * as firebaseExport from '../util/firebase_export'; +// +// const loadBundle = firebaseExport.loadBundle; +// const namedQuery = firebaseExport.namedQuery; +// +// export const encoder = new TextEncoder(); +// +// function verifySuccessProgress(p: firestore.LoadBundleTaskProgress): void { +// expect(p.taskState).to.equal('Success'); +// expect(p.bytesLoaded).to.be.equal(p.totalBytes); +// expect(p.documentsLoaded).to.equal(p.totalDocuments); +// } +// +// function verifyInProgress( +// p: firestore.LoadBundleTaskProgress, +// expectedDocuments: number +// ): void { +// expect(p.taskState).to.equal('Running'); +// expect(p.bytesLoaded <= p.totalBytes).to.be.true; +// expect(p.documentsLoaded <= p.totalDocuments).to.be.true; +// expect(p.documentsLoaded).to.equal(expectedDocuments); +// } +// +// // This template is generated from bundleWithTestDocsAndQueries in '../util/internal_helpsers.ts', +// // and manually copied here. +// const BUNDLE_TEMPLATE = [ +// '{"metadata":{"id":"test-bundle","createTime":{"seconds":1001,"nanos":9999},"version":1,"totalDocuments":2,"totalBytes":1503}}', +// '{"namedQuery":{"name":"limit","readTime":{"seconds":1000,"nanos":9999},"bundledQuery":{"parent":"projects/{0}/databases/(default)/documents","structuredQuery":{"from":[{"collectionId":"coll-1"}],"orderBy":[{"field":{"fieldPath":"bar"},"direction":"DESCENDING"},{"field":{"fieldPath":"__name__"},"direction":"DESCENDING"}],"limit":{"value":1}},"limitType":"FIRST"}}}', +// '{"namedQuery":{"name":"limit-to-last","readTime":{"seconds":1000,"nanos":9999},"bundledQuery":{"parent":"projects/{0}/databases/(default)/documents","structuredQuery":{"from":[{"collectionId":"coll-1"}],"orderBy":[{"field":{"fieldPath":"bar"},"direction":"DESCENDING"},{"field":{"fieldPath":"__name__"},"direction":"DESCENDING"}],"limit":{"value":1}},"limitType":"LAST"}}}', +// '{"documentMetadata":{"name":"projects/{0}/databases/(default)/documents/coll-1/a","readTime":{"seconds":1000,"nanos":9999},"exists":true}}', +// '{"document":{"name":"projects/{0}/databases/(default)/documents/coll-1/a","createTime":{"seconds":1,"nanos":9},"updateTime":{"seconds":1,"nanos":9},"fields":{"k":{"stringValue":"a"},"bar":{"integerValue":1}}}}', +// '{"documentMetadata":{"name":"projects/{0}/databases/(default)/documents/coll-1/b","readTime":{"seconds":1000,"nanos":9999},"exists":true}}', +// '{"document":{"name":"projects/{0}/databases/(default)/documents/coll-1/b","createTime":{"seconds":1,"nanos":9},"updateTime":{"seconds":1,"nanos":9},"fields":{"k":{"stringValue":"b"},"bar":{"integerValue":2}}}}' +// ]; +// +// apiDescribe('Bundles', (persistence: boolean) => { +// function verifySnapEqualsTestDocs(snap: firestore.QuerySnapshot): void { +// expect(toDataArray(snap)).to.deep.equal([ +// { k: 'a', bar: 1 }, +// { k: 'b', bar: 2 } +// ]); +// } +// +// /** +// * Returns a valid bundle string from replacing project id in `BUNDLE_TEMPLATE` with the given +// * db project id (also recalculate length prefixes). +// */ +// function bundleString(db: firestore.FirebaseFirestore): string { +// const projectId: string = db.app.options.projectId; +// +// // Extract elements from BUNDLE_TEMPLATE and replace the project ID. +// const elements = BUNDLE_TEMPLATE.map(e => e.replace('{0}', projectId)); +// +// // Recalculating length prefixes for elements that are not BundleMetadata. +// let bundleContent = ''; +// for (const element of elements.slice(1)) { +// const length = encoder.encode(element).byteLength; +// bundleContent += `${length}${element}`; +// } +// +// // Update BundleMetadata with new totalBytes. +// const totalBytes = encoder.encode(bundleContent).byteLength; +// const metadata = JSON.parse(elements[0]); +// metadata.metadata.totalBytes = totalBytes; +// const metadataContent = JSON.stringify(metadata); +// const metadataLength = encoder.encode(metadataContent).byteLength; +// return `${metadataLength}${metadataContent}${bundleContent}`; +// } +// +// it('load with documents only with on progress and promise interface', () => { +// return withTestDb(persistence, async db => { +// const progressEvents: firestore.LoadBundleTaskProgress[] = []; +// let completeCalled = false; +// const task: firestore.LoadBundleTask = loadBundle(db, bundleString(db)); +// task.onProgress( +// progress => { +// progressEvents.push(progress); +// }, +// undefined, +// () => { +// completeCalled = true; +// } +// ); +// await task; +// let fulfillProgress: firestore.LoadBundleTaskProgress; +// await task.then(progress => { +// fulfillProgress = progress; +// }); +// +// expect(completeCalled).to.be.true; +// expect(progressEvents.length).to.equal(4); +// verifyInProgress(progressEvents[0], 0); +// verifyInProgress(progressEvents[1], 1); +// verifyInProgress(progressEvents[2], 2); +// verifySuccessProgress(progressEvents[3]); +// expect(fulfillProgress!).to.deep.equal(progressEvents[3]); +// +// // Read from cache. These documents do not exist in backend, so they can +// // only be read from cache. +// let snap = await db.collection('coll-1').get({ source: 'cache' }); +// verifySnapEqualsTestDocs(snap); +// +// snap = await (await namedQuery(db, 'limit'))!.get({ +// source: 'cache' +// }); +// expect(toDataArray(snap)).to.deep.equal([{ k: 'b', bar: 2 }]); +// +// snap = await (await namedQuery(db, 'limit-to-last'))!.get({ +// source: 'cache' +// }); +// expect(toDataArray(snap)).to.deep.equal([{ k: 'a', bar: 1 }]); +// }); +// }); +// +// it('load with documents and queries with promise interface', () => { +// return withTestDb(persistence, async db => { +// const fulfillProgress: firestore.LoadBundleTaskProgress = await loadBundle( +// db, +// bundleString(db) +// ); +// +// verifySuccessProgress(fulfillProgress!); +// +// // Read from cache. These documents do not exist in backend, so they can +// // only be read from cache. +// const snap = await db.collection('coll-1').get({ source: 'cache' }); +// verifySnapEqualsTestDocs(snap); +// }); +// }); +// +// it('load for a second time skips', () => { +// return withTestDb(persistence, async db => { +// await loadBundle(db, bundleString(db)); +// +// let completeCalled = false; +// const progressEvents: firestore.LoadBundleTaskProgress[] = []; +// const task: firestore.LoadBundleTask = loadBundle( +// db, +// encoder.encode(bundleString(db)) +// ); +// task.onProgress( +// progress => { +// progressEvents.push(progress); +// }, +// error => {}, +// () => { +// completeCalled = true; +// } +// ); +// await task; +// +// expect(completeCalled).to.be.true; +// // No loading actually happened in the second `loadBundle` call only the +// // success progress is recorded. +// expect(progressEvents.length).to.equal(1); +// verifySuccessProgress(progressEvents[0]); +// +// // Read from cache. These documents do not exist in backend, so they can +// // only be read from cache. +// const snap = await db.collection('coll-1').get({ source: 'cache' }); +// verifySnapEqualsTestDocs(snap); +// }); +// }); +// +// it('load with documents already pulled from backend', () => { +// return withTestDb(persistence, async db => { +// await db.doc('coll-1/a').set({ k: 'a', bar: 0 }); +// await db.doc('coll-1/b').set({ k: 'b', bar: 0 }); +// +// const accumulator = new EventsAccumulator(); +// db.collection('coll-1').onSnapshot(accumulator.storeEvent); +// await accumulator.awaitEvent(); +// +// const progress = await loadBundle( +// db, +// // Testing passing in non-string bundles. +// encoder.encode(bundleString(db)) +// ); +// +// verifySuccessProgress(progress); +// // The test bundle is holding ancient documents, so no events are +// // generated as a result. The case where a bundle has newer doc than +// // cache can only be tested in spec tests. +// await accumulator.assertNoAdditionalEvents(); +// +// let snap = await (await namedQuery(db, 'limit'))!.get(); +// expect(toDataArray(snap)).to.deep.equal([{ k: 'b', bar: 0 }]); +// +// snap = await (await namedQuery(db, 'limit-to-last'))!.get(); +// expect(toDataArray(snap)).to.deep.equal([{ k: 'a', bar: 0 }]); +// }); +// }); +// +// it('loaded documents should not be GC-ed right away', () => { +// return withTestDb(persistence, async db => { +// const fulfillProgress: firestore.LoadBundleTaskProgress = await loadBundle( +// db, +// bundleString(db) +// ); +// +// verifySuccessProgress(fulfillProgress!); +// +// // Read a different collection, this will trigger GC. +// let snap = await db.collection('coll-other').get(); +// expect(snap.empty).to.be.true; +// +// // Read the loaded documents, expecting document in cache. With memory +// // GC, the documents would get GC-ed if we did not hold the document keys +// // in a "umbrella" target. See local_store.ts for details. +// snap = await db.collection('coll-1').get({ source: 'cache' }); +// verifySnapEqualsTestDocs(snap); +// }); +// }); +// +// it('load with documents from other projects fails', () => { +// return withTestDb(persistence, async db => { +// return withAlternateTestDb(persistence, async otherDb => { +// await expect(loadBundle(otherDb, bundleString(db))).to.be.rejectedWith( +// 'Tried to deserialize key from different project' +// ); +// +// // Verify otherDb still functions, despite loaded a problematic bundle. +// const finalProgress = await loadBundle(otherDb, bundleString(otherDb)); +// verifySuccessProgress(finalProgress); +// +// // Read from cache. These documents do not exist in backend, so they can +// // only be read from cache. +// const snap = await otherDb +// .collection('coll-1') +// .get({ source: 'cache' }); +// verifySnapEqualsTestDocs(snap); +// }); +// }); +// }); +// }); diff --git a/packages/firestore/test/integration/util/firebase_export.ts b/packages/firestore/test/integration/util/firebase_export.ts index 722d2d90c5e..3dfd156c8eb 100644 --- a/packages/firestore/test/integration/util/firebase_export.ts +++ b/packages/firestore/test/integration/util/firebase_export.ts @@ -123,20 +123,5 @@ const Timestamp = usesFunctionalApi() : legacyNamespace.Timestamp; const GeoPoint = usesFunctionalApi() ? exp.GeoPoint : legacyNamespace.GeoPoint; const Blob = usesFunctionalApi() ? exp.Blob : legacyNamespace.Blob; -const loadBundle = usesFunctionalApi() - ? exp.loadBundle - : legacyNamespace.loadBundle; -const namedQuery = usesFunctionalApi() - ? exp.namedQuery - : legacyNamespace.namedQuery; -export { - Firestore, - FieldValue, - FieldPath, - Timestamp, - Blob, - GeoPoint, - loadBundle, - namedQuery -}; +export { Firestore, FieldValue, FieldPath, Timestamp, Blob, GeoPoint }; From 1b3d6331752826ca29f897f31ee8645456d4828f Mon Sep 17 00:00:00 2001 From: Wu-Hui Date: Thu, 12 Nov 2020 10:12:13 -0500 Subject: [PATCH 27/27] Remove bundles from public interfaces take 2. --- .changeset/lemon-steaks-draw.md | 4 +- packages/firebase/index.d.ts | 37 ------------ packages/firestore-types/index.d.ts | 27 --------- packages/firestore/exp-types/index.d.ts | 37 ------------ packages/firestore/src/api/bundle.ts | 56 +++++++++++++------ packages/firestore/src/api/database.ts | 33 +++++++++++ packages/firestore/src/core/bundle.ts | 12 ++-- .../firestore/src/core/firestore_client.ts | 3 - 8 files changed, 80 insertions(+), 129 deletions(-) diff --git a/.changeset/lemon-steaks-draw.md b/.changeset/lemon-steaks-draw.md index 26acaf8b2ba..f920c46404e 100644 --- a/.changeset/lemon-steaks-draw.md +++ b/.changeset/lemon-steaks-draw.md @@ -1,5 +1,5 @@ --- -"@firebase/firestore": feature +"@firebase/firestore": internal --- -Add support for loading Firestore Bundle Files. +Merge bundle loading implementation without exposing public API diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index e3fe793bcd4..2cae198c74d 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -8285,43 +8285,6 @@ declare namespace firebase.firestore { INTERNAL: { delete: () => Promise }; } - export function loadBundle( - db: Firestore, - bundleData: ArrayBuffer | ReadableStream | string - ): LoadBundleTask; - - export function namedQuery( - db: Firestore, - name: string - ): Promise | null>; - - export interface LoadBundleTask { - onProgress( - next?: (progress: LoadBundleTaskProgress) => any, - error?: (error: Error) => any, - complete?: () => void - ): void; - - then( - onFulfilled?: (a: LoadBundleTaskProgress) => T | PromiseLike, - onRejected?: (a: Error) => R | PromiseLike - ): Promise; - - catch( - onRejected: (a: Error) => R | PromiseLike - ): Promise; - } - - export interface LoadBundleTaskProgress { - documentsLoaded: number; - totalDocuments: number; - bytesLoaded: number; - totalBytes: number; - taskState: TaskState; - } - - export type TaskState = 'Error' | 'Running' | 'Success'; - /** * An immutable object representing a geo point in Firestore. The geo point * is represented as latitude/longitude pair. diff --git a/packages/firestore-types/index.d.ts b/packages/firestore-types/index.d.ts index d815ea5cad9..2c94ea2842f 100644 --- a/packages/firestore-types/index.d.ts +++ b/packages/firestore-types/index.d.ts @@ -99,33 +99,6 @@ export class FirebaseFirestore { INTERNAL: { delete: () => Promise }; } -export interface LoadBundleTask { - onProgress( - next?: (progress: LoadBundleTaskProgress) => any, - error?: (error: Error) => any, - complete?: () => void - ): void; - - then( - onFulfilled?: (a: LoadBundleTaskProgress) => T | PromiseLike, - onRejected?: (a: Error) => R | PromiseLike - ): Promise; - - catch( - onRejected: (a: Error) => R | PromiseLike - ): Promise; -} - -export interface LoadBundleTaskProgress { - documentsLoaded: number; - totalDocuments: number; - bytesLoaded: number; - totalBytes: number; - taskState: TaskState; -} - -export type TaskState = 'Error' | 'Running' | 'Success'; - export class GeoPoint { constructor(latitude: number, longitude: number); diff --git a/packages/firestore/exp-types/index.d.ts b/packages/firestore/exp-types/index.d.ts index 19994191aab..5c1bb7f2b68 100644 --- a/packages/firestore/exp-types/index.d.ts +++ b/packages/firestore/exp-types/index.d.ts @@ -516,43 +516,6 @@ export function snapshotEqual( right: DocumentSnapshot | QuerySnapshot ): boolean; -export interface LoadBundleTask { - onProgress( - next?: (progress: LoadBundleTaskProgress) => any, - error?: (error: Error) => any, - complete?: () => void - ): void; - - then( - onFulfilled?: (a: LoadBundleTaskProgress) => T | PromiseLike, - onRejected?: (a: Error) => R | PromiseLike - ): Promise; - - catch( - onRejected: (a: Error) => R | PromiseLike - ): Promise; -} - -export interface LoadBundleTaskProgress { - documentsLoaded: number; - totalDocuments: number; - bytesLoaded: number; - totalBytes: number; - taskState: TaskState; -} - -export type TaskState = 'Error' | 'Running' | 'Success'; - -export function loadBundle( - firestore: FirebaseFirestore, - bundleData: ArrayBuffer | ReadableStream | string -): LoadBundleTask; - -export function namedQuery( - firestore: FirebaseFirestore, - name: string -): Promise | null>; - export type FirestoreErrorCode = | 'cancelled' | 'unknown' diff --git a/packages/firestore/src/api/bundle.ts b/packages/firestore/src/api/bundle.ts index 012827ca701..8bd3b4b74fa 100644 --- a/packages/firestore/src/api/bundle.ts +++ b/packages/firestore/src/api/bundle.ts @@ -15,24 +15,46 @@ * limitations under the License. */ -import * as firestore from '@firebase/firestore-types'; import { Deferred } from '../util/promise'; import { PartialObserver } from './observer'; import { debugAssert } from '../util/assert'; import { FirestoreError } from '../util/error'; +export interface ApiLoadBundleTask { + onProgress( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + next?: (progress: ApiLoadBundleTaskProgress) => any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error?: (error: Error) => any, + complete?: () => void + ): void; + + then( + onFulfilled?: (a: ApiLoadBundleTaskProgress) => T | PromiseLike, + onRejected?: (a: Error) => R | PromiseLike + ): Promise; + + catch( + onRejected: (a: Error) => R | PromiseLike + ): Promise; +} + +export interface ApiLoadBundleTaskProgress { + documentsLoaded: number; + totalDocuments: number; + bytesLoaded: number; + totalBytes: number; + taskState: TaskState; +} + +export type TaskState = 'Error' | 'Running' | 'Success'; + export class LoadBundleTask - implements - firestore.LoadBundleTask, - PromiseLike { - private _progressObserver: PartialObserver< - firestore.LoadBundleTaskProgress - > = {}; - private _taskCompletionResolver = new Deferred< - firestore.LoadBundleTaskProgress - >(); - - private _lastProgress: firestore.LoadBundleTaskProgress = { + implements ApiLoadBundleTask, PromiseLike { + private _progressObserver: PartialObserver = {}; + private _taskCompletionResolver = new Deferred(); + + private _lastProgress: ApiLoadBundleTaskProgress = { taskState: 'Running', totalBytes: 0, totalDocuments: 0, @@ -41,7 +63,7 @@ export class LoadBundleTask }; onProgress( - next?: (progress: firestore.LoadBundleTaskProgress) => unknown, + next?: (progress: ApiLoadBundleTaskProgress) => unknown, error?: (err: Error) => unknown, complete?: () => void ): void { @@ -54,12 +76,12 @@ export class LoadBundleTask catch( onRejected: (a: Error) => R | PromiseLike - ): Promise { + ): Promise { return this._taskCompletionResolver.promise.catch(onRejected); } then( - onFulfilled?: (a: firestore.LoadBundleTaskProgress) => T | PromiseLike, + onFulfilled?: (a: ApiLoadBundleTaskProgress) => T | PromiseLike, onRejected?: (a: Error) => R | PromiseLike ): Promise { return this._taskCompletionResolver.promise.then(onFulfilled, onRejected); @@ -69,7 +91,7 @@ export class LoadBundleTask * Notifies all observers that bundle loading has completed, with a provided * `LoadBundleTaskProgress` object. */ - _completeWith(progress: firestore.LoadBundleTaskProgress): void { + _completeWith(progress: ApiLoadBundleTaskProgress): void { debugAssert( progress.taskState === 'Success', 'Task is not completed with Success.' @@ -104,7 +126,7 @@ export class LoadBundleTask * Notifies a progress update of loading a bundle. * @param progress The new progress. */ - _updateProgress(progress: firestore.LoadBundleTaskProgress): void { + _updateProgress(progress: ApiLoadBundleTaskProgress): void { debugAssert( this._lastProgress.taskState === 'Running', 'Cannot update progress on a completed or failed task' diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 834388c17d5..11eede5191c 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -28,7 +28,9 @@ import { firestoreClientGetDocumentsFromLocalCache, firestoreClientGetDocumentsViaSnapshotListener, firestoreClientGetDocumentViaSnapshotListener, + firestoreClientGetNamedQuery, firestoreClientListen, + firestoreClientLoadBundle, firestoreClientTransaction, firestoreClientWrite } from '../core/firestore_client'; @@ -141,6 +143,7 @@ import { import { newUserDataReader } from '../../lite/src/api/reference'; import { makeDatabaseInfo } from '../../lite/src/api/database'; import { DEFAULT_HOST } from '../../lite/src/api/components'; +import { ApiLoadBundleTask, LoadBundleTask } from './bundle'; /** * Constant used to indicate the LRU garbage collection should be disabled. @@ -424,6 +427,36 @@ export function setLogLevel(level: PublicLogLevel): void { setClientLogLevel(level); } +export function loadBundle( + db: Firestore, + bundleData: ArrayBuffer | ReadableStream | string +): ApiLoadBundleTask { + const resultTask = new LoadBundleTask(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + firestoreClientLoadBundle( + ensureFirestoreConfigured(db._delegate), + bundleData, + resultTask + ); + return resultTask; +} + +export function namedQuery( + db: Firestore, + name: string +): Promise { + return firestoreClientGetNamedQuery( + ensureFirestoreConfigured(db._delegate), + name + ).then(namedQuery => { + if (!namedQuery) { + return null; + } + + return new Query(namedQuery.query, db, null); + }); +} + /** * A reference to a transaction. */ diff --git a/packages/firestore/src/core/bundle.ts b/packages/firestore/src/core/bundle.ts index 32dcc965cc1..e68c28ed2eb 100644 --- a/packages/firestore/src/core/bundle.ts +++ b/packages/firestore/src/core/bundle.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import * as firestore from '@firebase/firestore-types'; import { Query } from './query'; import { SnapshotVersion } from './snapshot_version'; import { @@ -41,6 +40,7 @@ import { DocumentKeySet, MaybeDocumentMap } from '../model/collections'; +import { ApiLoadBundleTaskProgress } from '../api/bundle'; /** * Represents a Firestore bundle saved by the SDK in its local storage. @@ -118,7 +118,7 @@ export class BundleConverter { */ export function bundleInitialProgress( metadata: BundleMetadata -): firestore.LoadBundleTaskProgress { +): ApiLoadBundleTaskProgress { return { taskState: 'Running', documentsLoaded: 0, @@ -134,7 +134,7 @@ export function bundleInitialProgress( */ export function bundleSuccessProgress( metadata: BundleMetadata -): firestore.LoadBundleTaskProgress { +): ApiLoadBundleTaskProgress { return { taskState: 'Success', documentsLoaded: metadata.totalDocuments!, @@ -146,7 +146,7 @@ export function bundleSuccessProgress( export class BundleLoadResult { constructor( - readonly progress: firestore.LoadBundleTaskProgress, + readonly progress: ApiLoadBundleTaskProgress, readonly changedDocs: MaybeDocumentMap ) {} } @@ -157,7 +157,7 @@ export class BundleLoadResult { */ export class BundleLoader { /** The current progress of loading */ - private progress: firestore.LoadBundleTaskProgress; + private progress: ApiLoadBundleTaskProgress; /** Batched queries to be saved into storage */ private queries: bundleProto.NamedQuery[] = []; /** Batched documents to be saved into storage */ @@ -179,7 +179,7 @@ export class BundleLoader { */ addSizedElement( element: SizedBundleElement - ): firestore.LoadBundleTaskProgress | null { + ): ApiLoadBundleTaskProgress | null { debugAssert(!element.isBundleMetadata(), 'Unexpected bundle metadata.'); this.progress.bytesLoaded += element.byteLength; diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 2b92e1dc589..c504017c429 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -662,8 +662,6 @@ export async function firestoreClientLoadBundle( data: ReadableStream | ArrayBuffer | string, resultTask: LoadBundleTask ): Promise { - client.verifyNotTerminated(); - const reader = createBundleReader( data, newSerializer((await client.getConfiguration()).databaseInfo.databaseId) @@ -677,7 +675,6 @@ export function firestoreClientGetNamedQuery( client: FirestoreClient, queryName: string ): Promise { - client.verifyNotTerminated(); return client.asyncQueue.enqueue(async () => getNamedQuery(await getLocalStore(client), queryName) );