diff --git a/.changeset/witty-wasps-play.md b/.changeset/witty-wasps-play.md new file mode 100644 index 00000000000..2b28862ce2e --- /dev/null +++ b/.changeset/witty-wasps-play.md @@ -0,0 +1,6 @@ +--- +'@firebase/firestore': minor +'firebase': minor +--- + +Added the ability to configure the long-polling hanging get request timeout using the new `idleHttpRequestTimeoutSeconds` setting diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 53e8e60cab8..580320b0aa3 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -184,6 +184,11 @@ export function endBefore(snapshot: DocumentSnapshot): QueryEndAtConstr // @public export function endBefore(...fieldValues: unknown[]): QueryEndAtConstraint; +// @public +export interface ExperimentalLongPollingOptions { + timeoutSeconds?: number; +} + // @public export class FieldPath { constructor(...fieldNames: string[]); @@ -227,6 +232,7 @@ export interface FirestoreSettings { cacheSizeBytes?: number; experimentalAutoDetectLongPolling?: boolean; experimentalForceLongPolling?: boolean; + experimentalLongPollingOptions?: ExperimentalLongPollingOptions; host?: string; ignoreUndefinedProperties?: boolean; localCache?: FirestoreLocalCache; diff --git a/docs-devsite/firestore_.experimentallongpollingoptions.md b/docs-devsite/firestore_.experimentallongpollingoptions.md new file mode 100644 index 00000000000..2a9ef50af46 --- /dev/null +++ b/docs-devsite/firestore_.experimentallongpollingoptions.md @@ -0,0 +1,43 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# ExperimentalLongPollingOptions interface +Options that configure the SDK’s underlying network transport (WebChannel) when long-polling is used. + +Note: This interface is "experimental" and is subject to change. + +See `FirestoreSettings.experimentalAutoDetectLongPolling`, `FirestoreSettings.experimentalForceLongPolling`, and `FirestoreSettings.experimentalLongPollingOptions`. + +Signature: + +```typescript +export declare interface ExperimentalLongPollingOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [timeoutSeconds](./firestore_.experimentallongpollingoptions.md#experimentallongpollingoptionstimeoutseconds) | number | The desired maximum timeout interval, in seconds, to complete a long-polling GET response. Valid values are between 5 and 30, inclusive. Floating point values are allowed and will be rounded to the nearest millisecond.By default, when long-polling is used the "hanging GET" request sent by the client times out after 30 seconds. To request a different timeout from the server, set this setting with the desired timeout.Changing the default timeout may be useful, for example, if the buffering proxy that necessitated enabling long-polling in the first place has a shorter timeout for hanging GET requests, in which case setting the long-polling timeout to a shorter value, such as 25 seconds, may fix prematurely-closed hanging GET requests. For example, see https://github.com/firebase/firebase-js-sdk/issues/6987. | + +## ExperimentalLongPollingOptions.timeoutSeconds + +The desired maximum timeout interval, in seconds, to complete a long-polling GET response. Valid values are between 5 and 30, inclusive. Floating point values are allowed and will be rounded to the nearest millisecond. + +By default, when long-polling is used the "hanging GET" request sent by the client times out after 30 seconds. To request a different timeout from the server, set this setting with the desired timeout. + +Changing the default timeout may be useful, for example, if the buffering proxy that necessitated enabling long-polling in the first place has a shorter timeout for hanging GET requests, in which case setting the long-polling timeout to a shorter value, such as 25 seconds, may fix prematurely-closed hanging GET requests. For example, see https://github.com/firebase/firebase-js-sdk/issues/6987. + +Signature: + +```typescript +timeoutSeconds?: number; +``` diff --git a/docs-devsite/firestore_.firestoresettings.md b/docs-devsite/firestore_.firestoresettings.md index e4d4846ee64..d5beb7e03c7 100644 --- a/docs-devsite/firestore_.firestoresettings.md +++ b/docs-devsite/firestore_.firestoresettings.md @@ -25,6 +25,7 @@ export declare interface FirestoreSettings | [cacheSizeBytes](./firestore_.firestoresettings.md#firestoresettingscachesizebytes) | number | NOTE: This field will be deprecated in a future major release. Use cache field instead to specify cache size, and other cache configurations.An approximate cache size threshold for the on-disk data. If the cache grows beyond this size, Firestore will start removing data that hasn't been recently used. The size is not a guarantee that the cache will stay below that size, only that if the cache exceeds the given size, cleanup will be attempted.The default value is 40 MB. The threshold must be set to at least 1 MB, and can be set to CACHE_SIZE_UNLIMITED to disable garbage collection. | | [experimentalAutoDetectLongPolling](./firestore_.firestoresettings.md#firestoresettingsexperimentalautodetectlongpolling) | boolean | Configures the SDK's underlying transport (WebChannel) to automatically detect if long-polling should be used. This is very similar to experimentalForceLongPolling, but only uses long-polling if required.This setting will likely be enabled by default in future releases and cannot be combined with experimentalForceLongPolling. | | [experimentalForceLongPolling](./firestore_.firestoresettings.md#firestoresettingsexperimentalforcelongpolling) | boolean | Forces the SDK’s underlying network transport (WebChannel) to use long-polling. Each response from the backend will be closed immediately after the backend sends data (by default responses are kept open in case the backend has more data to send). This avoids incompatibility issues with certain proxies, antivirus software, etc. that incorrectly buffer traffic indefinitely. Use of this option will cause some performance degradation though.This setting cannot be used with experimentalAutoDetectLongPolling and may be removed in a future release. If you find yourself using it to work around a specific network reliability issue, please tell us about it in https://github.com/firebase/firebase-js-sdk/issues/1674. | +| [experimentalLongPollingOptions](./firestore_.firestoresettings.md#firestoresettingsexperimentallongpollingoptions) | [ExperimentalLongPollingOptions](./firestore_.experimentallongpollingoptions.md#experimentallongpollingoptions_interface) | Options that configure the SDK’s underlying network transport (WebChannel) when long-polling is used.These options are only used if experimentalForceLongPolling is true or if experimentalAutoDetectLongPolling is true and the auto-detection determined that long-polling was needed. Otherwise, these options have no effect. | | [host](./firestore_.firestoresettings.md#firestoresettingshost) | string | The hostname to connect to. | | [ignoreUndefinedProperties](./firestore_.firestoresettings.md#firestoresettingsignoreundefinedproperties) | boolean | Whether to skip nested properties that are set to undefined during object serialization. If set to true, these properties are skipped and not written to Firestore. If set to false or omitted, the SDK throws an exception when it encounters properties of type undefined. | | [localCache](./firestore_.firestoresettings.md#firestoresettingslocalcache) | [FirestoreLocalCache](./firestore_.md#firestorelocalcache) | Specifies the cache used by the SDK. Available options are MemoryLocalCache and IndexedDbLocalCache, each with different configuration options.When unspecified, MemoryLocalCache will be used by default.NOTE: setting this field and cacheSizeBytes at the same time will throw exception during SDK initialization. Instead, using the configuration in the FirestoreLocalCache object to specify the cache size. | @@ -68,6 +69,18 @@ This setting cannot be used with `experimentalAutoDetectLongPolling` and may be experimentalForceLongPolling?: boolean; ``` +## FirestoreSettings.experimentalLongPollingOptions + +Options that configure the SDK’s underlying network transport (WebChannel) when long-polling is used. + +These options are only used if `experimentalForceLongPolling` is true or if `experimentalAutoDetectLongPolling` is true and the auto-detection determined that long-polling was needed. Otherwise, these options have no effect. + +Signature: + +```typescript +experimentalLongPollingOptions?: ExperimentalLongPollingOptions; +``` + ## FirestoreSettings.host The hostname to connect to. diff --git a/docs-devsite/firestore_.md b/docs-devsite/firestore_.md index 3fc6c253769..c38b6bb8407 100644 --- a/docs-devsite/firestore_.md +++ b/docs-devsite/firestore_.md @@ -149,6 +149,7 @@ https://github.com/firebase/firebase-js-sdk | [AggregateSpec](./firestore_.aggregatespec.md#aggregatespec_interface) | Specifies a set of aggregations and their aliases. | | [DocumentChange](./firestore_.documentchange.md#documentchange_interface) | A DocumentChange represents a change to the documents matching a query. It contains the document affected and the type of change that occurred. | | [DocumentData](./firestore_.documentdata.md#documentdata_interface) | Document data (for use with [setDoc()](./firestore_lite.md#setdoc)) consists of fields mapped to values. | +| [ExperimentalLongPollingOptions](./firestore_.experimentallongpollingoptions.md#experimentallongpollingoptions_interface) | Options that configure the SDK’s underlying network transport (WebChannel) when long-polling is used.Note: This interface is "experimental" and is subject to change.See FirestoreSettings.experimentalAutoDetectLongPolling, FirestoreSettings.experimentalForceLongPolling, and FirestoreSettings.experimentalLongPollingOptions. | | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface) | Converter used by withConverter() to transform user objects of type T into Firestore data.Using the converter allows you to specify generic type arguments when storing and retrieving objects from Firestore. | | [FirestoreSettings](./firestore_.firestoresettings.md#firestoresettings_interface) | Specifies custom configurations for your Cloud Firestore instance. You must set these before invoking any other methods. | | [Index](./firestore_.index.md#index_interface) | (BETA) The SDK definition of a Firestore index. | diff --git a/packages/firestore/src/api.ts b/packages/firestore/src/api.ts index b8c49cb363a..4254b0c9a06 100644 --- a/packages/firestore/src/api.ts +++ b/packages/firestore/src/api.ts @@ -82,6 +82,7 @@ export { } from './api/bundle'; export { FirestoreSettings, PersistenceSettings } from './api/settings'; +export { ExperimentalLongPollingOptions } from './api/long_polling_options'; export { DocumentChange, diff --git a/packages/firestore/src/api/long_polling_options.ts b/packages/firestore/src/api/long_polling_options.ts new file mode 100644 index 00000000000..8da3fd30c7b --- /dev/null +++ b/packages/firestore/src/api/long_polling_options.ts @@ -0,0 +1,73 @@ +/** + * @license + * Copyright 2023 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. + */ + +/** + * Options that configure the SDK’s underlying network transport (WebChannel) + * when long-polling is used. + * + * Note: This interface is "experimental" and is subject to change. + * + * See `FirestoreSettings.experimentalAutoDetectLongPolling`, + * `FirestoreSettings.experimentalForceLongPolling`, and + * `FirestoreSettings.experimentalLongPollingOptions`. + */ +export interface ExperimentalLongPollingOptions { + /** + * The desired maximum timeout interval, in seconds, to complete a + * long-polling GET response. Valid values are between 5 and 30, inclusive. + * Floating point values are allowed and will be rounded to the nearest + * millisecond. + * + * By default, when long-polling is used the "hanging GET" request sent by + * the client times out after 30 seconds. To request a different timeout + * from the server, set this setting with the desired timeout. + * + * Changing the default timeout may be useful, for example, if the buffering + * proxy that necessitated enabling long-polling in the first place has a + * shorter timeout for hanging GET requests, in which case setting the + * long-polling timeout to a shorter value, such as 25 seconds, may fix + * prematurely-closed hanging GET requests. + * For example, see https://github.com/firebase/firebase-js-sdk/issues/6987. + */ + timeoutSeconds?: number; +} + +/** + * Compares two `ExperimentalLongPollingOptions` objects for equality. + */ +export function longPollingOptionsEqual( + options1: ExperimentalLongPollingOptions, + options2: ExperimentalLongPollingOptions +): boolean { + return options1.timeoutSeconds === options2.timeoutSeconds; +} + +/** + * Creates and returns a new `ExperimentalLongPollingOptions` with the same + * option values as the given instance. + */ +export function cloneLongPollingOptions( + options: ExperimentalLongPollingOptions +): ExperimentalLongPollingOptions { + const clone: ExperimentalLongPollingOptions = {}; + + if (options.timeoutSeconds !== undefined) { + clone.timeoutSeconds = options.timeoutSeconds; + } + + return clone; +} diff --git a/packages/firestore/src/api/settings.ts b/packages/firestore/src/api/settings.ts index 8675b28ca96..babf9499387 100644 --- a/packages/firestore/src/api/settings.ts +++ b/packages/firestore/src/api/settings.ts @@ -18,6 +18,7 @@ import { FirestoreSettings as LiteSettings } from '../lite-api/settings'; import { FirestoreLocalCache } from './cache_config'; +import { ExperimentalLongPollingOptions } from './long_polling_options'; export { DEFAULT_HOST } from '../lite-api/settings'; @@ -92,4 +93,15 @@ export interface FirestoreSettings extends LiteSettings { * cannot be combined with `experimentalForceLongPolling`. */ experimentalAutoDetectLongPolling?: boolean; + + /** + * Options that configure the SDK’s underlying network transport (WebChannel) + * when long-polling is used. + * + * These options are only used if `experimentalForceLongPolling` is true or if + * `experimentalAutoDetectLongPolling` is true and the auto-detection + * determined that long-polling was needed. Otherwise, these options have no + * effect. + */ + experimentalLongPollingOptions?: ExperimentalLongPollingOptions; } diff --git a/packages/firestore/src/core/database_info.ts b/packages/firestore/src/core/database_info.ts index 306a9920ea9..0325f8166b6 100644 --- a/packages/firestore/src/core/database_info.ts +++ b/packages/firestore/src/core/database_info.ts @@ -1,5 +1,6 @@ import { FirebaseApp } from '@firebase/app'; +import { ExperimentalLongPollingOptions } from '../api/long_polling_options'; import { Code, FirestoreError } from '../util/error'; /** @@ -34,6 +35,7 @@ export class DatabaseInfo { * when using WebChannel as the network transport. * @param autoDetectLongPolling - Whether to use the detectBufferingProxy * option when using WebChannel as the network transport. + * @param longPollingOptions Options that configure long-polling. * @param useFetchStreams Whether to use the Fetch API instead of * XMLHTTPRequest */ @@ -45,6 +47,7 @@ export class DatabaseInfo { readonly ssl: boolean, readonly forceLongPolling: boolean, readonly autoDetectLongPolling: boolean, + readonly longPollingOptions: ExperimentalLongPollingOptions, readonly useFetchStreams: boolean ) {} } diff --git a/packages/firestore/src/lite-api/components.ts b/packages/firestore/src/lite-api/components.ts index 8280d396b0b..436d2b5d4d8 100644 --- a/packages/firestore/src/lite-api/components.ts +++ b/packages/firestore/src/lite-api/components.ts @@ -19,6 +19,7 @@ import { _FirebaseService } from '@firebase/app'; import { CredentialsProvider } from '../api/credentials'; +import { cloneLongPollingOptions } from '../api/long_polling_options'; import { User } from '../auth/user'; import { DatabaseId, DatabaseInfo } from '../core/database_info'; import { newConnection } from '../platform/connection'; @@ -117,6 +118,7 @@ export function makeDatabaseInfo( settings.ssl, settings.experimentalForceLongPolling, settings.experimentalAutoDetectLongPolling, + cloneLongPollingOptions(settings.experimentalLongPollingOptions), settings.useFetchStreams ); } diff --git a/packages/firestore/src/lite-api/settings.ts b/packages/firestore/src/lite-api/settings.ts index e9b4e7658c5..3a731689e83 100644 --- a/packages/firestore/src/lite-api/settings.ts +++ b/packages/firestore/src/lite-api/settings.ts @@ -17,6 +17,11 @@ import { FirestoreLocalCache } from '../api/cache_config'; import { CredentialsSettings } from '../api/credentials'; +import { + ExperimentalLongPollingOptions, + cloneLongPollingOptions, + longPollingOptionsEqual +} from '../api/long_polling_options'; import { LRU_COLLECTION_DISABLED, LRU_DEFAULT_CACHE_SIZE_BYTES @@ -29,6 +34,18 @@ import { validateIsNotUsedTogether } from '../util/input_validation'; export const DEFAULT_HOST = 'firestore.googleapis.com'; export const DEFAULT_SSL = true; +// The minimum long-polling timeout is hardcoded on the server. The value here +// should be kept in sync with the value used by the server, as the server will +// silently ignore a value below the minimum and fall back to the default. +// Googlers see b/266868871 for relevant discussion. +const MIN_LONG_POLLING_TIMEOUT_SECONDS = 5; + +// No maximum long-polling timeout is configured in the server, and defaults to +// 30 seconds, which is what Watch appears to use. +// Googlers see b/266868871 for relevant discussion. +const MAX_LONG_POLLING_TIMEOUT_SECONDS = 30; + +// Whether long-polling auto-detected is enabled by default. const DEFAULT_AUTO_DETECT_LONG_POLLING = false; /** @@ -58,6 +75,7 @@ export interface PrivateSettings extends FirestoreSettings { cacheSizeBytes?: number; experimentalForceLongPolling?: boolean; experimentalAutoDetectLongPolling?: boolean; + experimentalLongPollingOptions?: ExperimentalLongPollingOptions; useFetchStreams?: boolean; localCache?: FirestoreLocalCache; @@ -81,6 +99,8 @@ export class FirestoreSettingsImpl { readonly experimentalAutoDetectLongPolling: boolean; + readonly experimentalLongPollingOptions: ExperimentalLongPollingOptions; + readonly ignoreUndefinedProperties: boolean; readonly useFetchStreams: boolean; @@ -146,6 +166,11 @@ export class FirestoreSettingsImpl { !!settings.experimentalAutoDetectLongPolling; } + this.experimentalLongPollingOptions = cloneLongPollingOptions( + settings.experimentalLongPollingOptions ?? {} + ); + validateLongPollingOptions(this.experimentalLongPollingOptions); + this.useFetchStreams = !!settings.useFetchStreams; } @@ -159,8 +184,40 @@ export class FirestoreSettingsImpl { other.experimentalForceLongPolling && this.experimentalAutoDetectLongPolling === other.experimentalAutoDetectLongPolling && + longPollingOptionsEqual( + this.experimentalLongPollingOptions, + other.experimentalLongPollingOptions + ) && this.ignoreUndefinedProperties === other.ignoreUndefinedProperties && this.useFetchStreams === other.useFetchStreams ); } } + +function validateLongPollingOptions( + options: ExperimentalLongPollingOptions +): void { + if (options.timeoutSeconds !== undefined) { + if (isNaN(options.timeoutSeconds)) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + `invalid long polling timeout: ` + + `${options.timeoutSeconds} (must not be NaN)` + ); + } + if (options.timeoutSeconds < MIN_LONG_POLLING_TIMEOUT_SECONDS) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + `invalid long polling timeout: ${options.timeoutSeconds} ` + + `(minimum allowed value is ${MIN_LONG_POLLING_TIMEOUT_SECONDS})` + ); + } + if (options.timeoutSeconds > MAX_LONG_POLLING_TIMEOUT_SECONDS) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + `invalid long polling timeout: ${options.timeoutSeconds} ` + + `(maximum allowed value is ${MAX_LONG_POLLING_TIMEOUT_SECONDS})` + ); + } + } +} diff --git a/packages/firestore/src/platform/browser/webchannel_connection.ts b/packages/firestore/src/platform/browser/webchannel_connection.ts index c7eeec0f7f1..5082418cf69 100644 --- a/packages/firestore/src/platform/browser/webchannel_connection.ts +++ b/packages/firestore/src/platform/browser/webchannel_connection.ts @@ -32,6 +32,7 @@ import { } from '@firebase/webchannel-wrapper'; import { Token } from '../../api/credentials'; +import { ExperimentalLongPollingOptions } from '../../api/long_polling_options'; import { DatabaseInfo } from '../../core/database_info'; import { Stream } from '../../remote/connection'; import { RestConnection } from '../../remote/rest_connection'; @@ -57,12 +58,14 @@ export class WebChannelConnection extends RestConnection { private readonly forceLongPolling: boolean; private readonly autoDetectLongPolling: boolean; private readonly useFetchStreams: boolean; + private readonly longPollingOptions: ExperimentalLongPollingOptions; constructor(info: DatabaseInfo) { super(info); this.forceLongPolling = info.forceLongPolling; this.autoDetectLongPolling = info.autoDetectLongPolling; this.useFetchStreams = info.useFetchStreams; + this.longPollingOptions = info.longPollingOptions; } protected performRPCRequest( @@ -200,6 +203,11 @@ export class WebChannelConnection extends RestConnection { detectBufferingProxy: this.autoDetectLongPolling }; + const longPollingTimeoutSeconds = this.longPollingOptions.timeoutSeconds; + if (longPollingTimeoutSeconds !== undefined) { + request.longPollingTimeout = Math.round(longPollingTimeoutSeconds * 1000); + } + if (this.useFetchStreams) { request.xmlHttpFactory = new FetchXmlHttpFactory({}); } diff --git a/packages/firestore/test/integration/util/internal_helpers.ts b/packages/firestore/test/integration/util/internal_helpers.ts index 58db06b5b85..4abfcf78096 100644 --- a/packages/firestore/test/integration/util/internal_helpers.ts +++ b/packages/firestore/test/integration/util/internal_helpers.ts @@ -21,6 +21,7 @@ import { EmptyAppCheckTokenProvider, EmptyAuthCredentialsProvider } from '../../../src/api/credentials'; +import { cloneLongPollingOptions } from '../../../src/api/long_polling_options'; import { User } from '../../../src/auth/user'; import { DatabaseId, DatabaseInfo } from '../../../src/core/database_info'; import { newConnection } from '../../../src/platform/connection'; @@ -57,6 +58,9 @@ export function getDefaultDatabaseInfo(): DatabaseInfo { !!DEFAULT_SETTINGS.ssl, !!DEFAULT_SETTINGS.experimentalForceLongPolling, !!DEFAULT_SETTINGS.experimentalAutoDetectLongPolling, + cloneLongPollingOptions( + DEFAULT_SETTINGS.experimentalLongPollingOptions ?? {} + ), /*use FetchStreams= */ false ); } diff --git a/packages/firestore/test/unit/api/database.test.ts b/packages/firestore/test/unit/api/database.test.ts index ea79c398bfc..94b67257d2a 100644 --- a/packages/firestore/test/unit/api/database.test.ts +++ b/packages/firestore/test/unit/api/database.test.ts @@ -359,6 +359,131 @@ describe('Settings', () => { expect(db._getSettings().experimentalForceLongPolling).to.be.false; }); + it('timeoutSeconds is undefined by default', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + expect(db._getSettings().experimentalLongPollingOptions.timeoutSeconds).to + .be.undefined; + }); + + it('timeoutSeconds minimum value is allowed', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + db._setSettings({ experimentalLongPollingOptions: { timeoutSeconds: 5 } }); + expect( + db._getSettings().experimentalLongPollingOptions.timeoutSeconds + ).to.equal(5); + }); + + it('timeoutSeconds maximum value is allowed', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + db._setSettings({ experimentalLongPollingOptions: { timeoutSeconds: 30 } }); + expect( + db._getSettings().experimentalLongPollingOptions.timeoutSeconds + ).to.equal(30); + }); + + it('timeoutSeconds typical value is allowed', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + db._setSettings({ experimentalLongPollingOptions: { timeoutSeconds: 25 } }); + expect( + db._getSettings().experimentalLongPollingOptions.timeoutSeconds + ).to.equal(25); + }); + + it('timeoutSeconds floating point value is allowed', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + db._setSettings({ + experimentalLongPollingOptions: { timeoutSeconds: 12.3456 } + }); + expect( + db._getSettings().experimentalLongPollingOptions.timeoutSeconds + ).to.equal(12.3456); + }); + + it('timeoutSeconds value one less than minimum throws', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + expect(() => + db._setSettings({ experimentalLongPollingOptions: { timeoutSeconds: 4 } }) + ).to.throw(/invalid.*timeout.*4.*\(.*5.*\)/i); + }); + + it('timeoutSeconds value one more than maximum throws', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + expect(() => + db._setSettings({ + experimentalLongPollingOptions: { timeoutSeconds: 31 } + }) + ).to.throw(/invalid.*timeout.*31.*\(.*30.*\)/i); + }); + + it('timeoutSeconds value of 0 throws', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + expect(() => + db._setSettings({ experimentalLongPollingOptions: { timeoutSeconds: 0 } }) + ).to.throw(/invalid.*timeout.*0.*\(.*5.*\)/i); + }); + + it('timeoutSeconds value of -0 throws', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + expect(() => + db._setSettings({ + experimentalLongPollingOptions: { timeoutSeconds: -0 } + }) + ).to.throw(/invalid.*timeout.*0.*\(.*5.*\)/i); + }); + + it('timeoutSeconds value of -1 throws', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + expect(() => + db._setSettings({ + experimentalLongPollingOptions: { timeoutSeconds: -1 } + }) + ).to.throw(/invalid.*timeout.*-1.*\(.*5.*\)/i); + }); + + it('timeoutSeconds value of -infinity throws', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + expect(() => + db._setSettings({ + experimentalLongPollingOptions: { + timeoutSeconds: Number.NEGATIVE_INFINITY + } + }) + ).to.throw(/invalid.*timeout.*-Infinity.*\(.*5.*\)/i); + }); + + it('timeoutSeconds value of +infinity throws', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + expect(() => + db._setSettings({ + experimentalLongPollingOptions: { + timeoutSeconds: Number.POSITIVE_INFINITY + } + }) + ).to.throw(/invalid.*timeout.*Infinity.*\(.*30.*\)/i); + }); + + it('timeoutSeconds value of NaN throws', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + expect(() => + db._setSettings({ + experimentalLongPollingOptions: { timeoutSeconds: Number.NaN } + }) + ).to.throw(/invalid.*timeout.*NaN/i); + }); + it('long polling autoDetect=[something truthy] should be coerced to true', () => { // Use a new instance of Firestore in order to configure settings. const db = newTestFirestore(); diff --git a/packages/firestore/test/unit/api/long_polling_options.test.ts b/packages/firestore/test/unit/api/long_polling_options.test.ts new file mode 100644 index 00000000000..228fecb9bf9 --- /dev/null +++ b/packages/firestore/test/unit/api/long_polling_options.test.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright 2023 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 { + ExperimentalLongPollingOptions, + longPollingOptionsEqual, + cloneLongPollingOptions +} from '../../../src/api/long_polling_options'; + +describe('long_polling_options', () => { + it('longPollingOptionsEqual() should return true for empty objects', () => { + expect(longPollingOptionsEqual({}, {})).to.be.true; + }); + + it('longPollingOptionsEqual() should return true if both objects have the same timeoutSeconds', () => { + const options1: ExperimentalLongPollingOptions = { timeoutSeconds: 123 }; + const options2: ExperimentalLongPollingOptions = { timeoutSeconds: 123 }; + expect(longPollingOptionsEqual(options1, options2)).to.be.true; + }); + + it('longPollingOptionsEqual() should return false if the objects have different timeoutSeconds', () => { + const options1: ExperimentalLongPollingOptions = { timeoutSeconds: 123 }; + const options2: ExperimentalLongPollingOptions = { timeoutSeconds: 321 }; + expect(longPollingOptionsEqual(options1, options2)).to.be.false; + }); + + it('longPollingOptionsEqual() should ignore properties not defined in ExperimentalLongPollingOptions', () => { + const options1 = { + timeoutSeconds: 123, + someOtherProperty: 42 + } as ExperimentalLongPollingOptions; + const options2 = { + timeoutSeconds: 123, + someOtherProperty: 24 + } as ExperimentalLongPollingOptions; + expect(longPollingOptionsEqual(options1, options2)).to.be.true; + }); + + it('cloneLongPollingOptions() with an empty object should return an empty object', () => { + expect(cloneLongPollingOptions({})).to.deep.equal({}); + }); + + it('cloneLongPollingOptions() should copy timeoutSeconds', () => { + expect(cloneLongPollingOptions({ timeoutSeconds: 1234 })).to.deep.equal({ + timeoutSeconds: 1234 + }); + }); + + it('cloneLongPollingOptions() should not copy properties not defined in ExperimentalLongPollingOptions', () => { + const options = { + timeoutSeconds: 1234, + someOtherProperty: 42 + } as ExperimentalLongPollingOptions; + expect(cloneLongPollingOptions(options)).to.deep.equal({ + timeoutSeconds: 1234 + }); + }); +}); diff --git a/packages/firestore/test/unit/remote/rest_connection.test.ts b/packages/firestore/test/unit/remote/rest_connection.test.ts index 8266ce74cf2..838e9447660 100644 --- a/packages/firestore/test/unit/remote/rest_connection.test.ts +++ b/packages/firestore/test/unit/remote/rest_connection.test.ts @@ -65,6 +65,7 @@ describe('RestConnection', () => { /*ssl=*/ false, /*forceLongPolling=*/ false, /*autoDetectLongPolling=*/ false, + /*longPollingOptions=*/ {}, /*useFetchStreams=*/ false ); const connection = new TestRestConnection(testDatabaseInfo); diff --git a/packages/firestore/test/unit/specs/spec_test_runner.ts b/packages/firestore/test/unit/specs/spec_test_runner.ts index e61022e7271..1245a1c0231 100644 --- a/packages/firestore/test/unit/specs/spec_test_runner.ts +++ b/packages/firestore/test/unit/specs/spec_test_runner.ts @@ -277,6 +277,7 @@ abstract class TestRunner { /*ssl=*/ false, /*forceLongPolling=*/ false, /*autoDetectLongPolling=*/ false, + /*longPollingOptions=*/ {}, /*useFetchStreams=*/ false ); diff --git a/packages/webchannel-wrapper/externs/overrides.js b/packages/webchannel-wrapper/externs/overrides.js index f40853ec6a1..bccac74db64 100644 --- a/packages/webchannel-wrapper/externs/overrides.js +++ b/packages/webchannel-wrapper/externs/overrides.js @@ -69,6 +69,9 @@ goog.net.WebChannel.Options.detectBufferingProxy; /** @type {unknown} */ goog.net.WebChannel.Options.xmlHttpFactory; +/** @type {number|undefined} */ +goog.net.WebChannel.Options.longPollingTimeout; + goog.labs.net.webChannel.requestStats.Event = {}; goog.labs.net.webChannel.requestStats.Event.STAT_EVENT; diff --git a/packages/webchannel-wrapper/package.json b/packages/webchannel-wrapper/package.json index 27f31e15035..26858ee466e 100644 --- a/packages/webchannel-wrapper/package.json +++ b/packages/webchannel-wrapper/package.json @@ -27,8 +27,8 @@ "license": "Apache-2.0", "devDependencies": { "@rollup/plugin-commonjs": "21.1.0", - "google-closure-compiler": "20220301.0.0", - "google-closure-library": "20220301.0.0", + "google-closure-compiler": "20230228.0.0", + "google-closure-library": "20230228.0.0", "gulp": "4.0.2", "gulp-sourcemaps": "3.0.0", "rollup": "2.79.1", diff --git a/packages/webchannel-wrapper/src/index.d.ts b/packages/webchannel-wrapper/src/index.d.ts index 59e1e45f56f..007287a5d9e 100644 --- a/packages/webchannel-wrapper/src/index.d.ts +++ b/packages/webchannel-wrapper/src/index.d.ts @@ -101,6 +101,7 @@ export interface WebChannelOptions { encodeInitMessageHeaders?: boolean; forceLongPolling?: boolean; detectBufferingProxy?: boolean; + longPollingTimeout?: number; fastHandshake?: boolean; disableRedac?: boolean; clientProfile?: string; diff --git a/yarn.lock b/yarn.lock index 77506626d97..11e31359184 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5758,16 +5758,7 @@ chainsaw@~0.1.0: dependencies: traverse ">=0.3.0 <0.4" -chalk@2.x, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: +chalk@4.1.2, chalk@4.x, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -5786,6 +5777,15 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^5.0.0: version "5.0.1" resolved "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz#ca57d71e82bb534a296df63bbacc4a1c22b2a4b6" @@ -9190,45 +9190,45 @@ google-auth-library@^8.0.2: jws "^4.0.0" lru-cache "^6.0.0" -google-closure-compiler-java@^20220301.0.0: - version "20220301.0.0" - resolved "https://registry.npmjs.org/google-closure-compiler-java/-/google-closure-compiler-java-20220301.0.0.tgz#6283bad6991ae9cfb3a9fdf72bbd7bf0c8f21fb6" - integrity sha512-kv5oaUI4xn3qWYWtRHRqbm314kesfeFlCxiFRcvBIx13mKfR0qvbOkgajLpSM6nb3voNM/E9MB9mfvHJ9XIXSg== - -google-closure-compiler-linux@^20220301.0.0: - version "20220301.0.0" - resolved "https://registry.npmjs.org/google-closure-compiler-linux/-/google-closure-compiler-linux-20220301.0.0.tgz#3ac8cd1cb51d703a89bc49c239df4c10b57f37bb" - integrity sha512-N2D0SRnxZ7kqdoZ2WsmLIjmizR4Xr0HaUYDK2RCOtsV21RYV8OR2u0ATp7aXhYy8WfxvYH478Ehvmc9Uzy986A== - -google-closure-compiler-osx@^20220301.0.0: - version "20220301.0.0" - resolved "https://registry.npmjs.org/google-closure-compiler-osx/-/google-closure-compiler-osx-20220301.0.0.tgz#1a49eb1d78b6bfb90ebe51c24a7151cee4f319a3" - integrity sha512-Xqf0m5takwfv43ML4aODJxmAsAZQMTMo683gyRs0APAecncs+YKxaDPMH+pQAdI3HPY2QsvkarlunAp0HSwU5A== - -google-closure-compiler-windows@^20220301.0.0: - version "20220301.0.0" - resolved "https://registry.npmjs.org/google-closure-compiler-windows/-/google-closure-compiler-windows-20220301.0.0.tgz#b09df91a789e458eb9ebf054a9bb2d2b29622b6f" - integrity sha512-s+FU/vcpLTEgx8MCMgj0STCYkVk7syzF9KqiYPOTtbTD9ra99HPe/CEuQG7iJ3Fty9dhm9zEaetv4Dp4Wr6x+Q== - -google-closure-compiler@20220301.0.0: - version "20220301.0.0" - resolved "https://registry.npmjs.org/google-closure-compiler/-/google-closure-compiler-20220301.0.0.tgz#1c4f56076ae5b2c900a91d0a72515f7ee7f5d3cd" - integrity sha512-+yAqhufKIWddg587tnvRll92eLJQIlzINmgr1h5gLXZVioY3svrSYKH4TZiUuNj0UnVFoK0o1YuW122x+iFl2g== - dependencies: - chalk "2.x" - google-closure-compiler-java "^20220301.0.0" +google-closure-compiler-java@^20230228.0.0: + version "20230228.0.0" + resolved "https://registry.npmjs.org/google-closure-compiler-java/-/google-closure-compiler-java-20230228.0.0.tgz#d74036a40917166e7b4fb6cf5e8878eada2ab93f" + integrity sha512-t0sXYJbhfkuNTF6zniwrTv4gLap620D32v6GwBJQzlYUg0lb7yQHN9KswwqBsuuO917cPNwW4okI0O40G7GrMQ== + +google-closure-compiler-linux@^20230228.0.0: + version "20230228.0.0" + resolved "https://registry.npmjs.org/google-closure-compiler-linux/-/google-closure-compiler-linux-20230228.0.0.tgz#17cb6187015e0e2ae8c2dd5b560228fdf5625818" + integrity sha512-5YLxfWS8lvHkD/a0+pitTuDV1X9QPBToGQ5mnLFg7HcbBR1w6I5ZKHjl7FAsAOHEXYwIrStwwaLzrNzbolrZLg== + +google-closure-compiler-osx@^20230228.0.0: + version "20230228.0.0" + resolved "https://registry.npmjs.org/google-closure-compiler-osx/-/google-closure-compiler-osx-20230228.0.0.tgz#245caa71b2eff3c5f4a4ec2046b2dd766c5fbe2f" + integrity sha512-ORveHpHuNhJEJIGir35+xP4UuBOldSO8XeOwJV5yunUhZAPzR4aixdTdtm6i0GsqW4/Eu2cjcHrkIR3eFCcwSg== + +google-closure-compiler-windows@^20230228.0.0: + version "20230228.0.0" + resolved "https://registry.npmjs.org/google-closure-compiler-windows/-/google-closure-compiler-windows-20230228.0.0.tgz#dff4afcd6d21a831b38a9bdb873eed0daca79807" + integrity sha512-xKMjUq6JwEOFqS97S86TWkn+BMiDHjP85mMgAmR8vRmKxgfHIyxMcr+RlMz0msgY9jedgj119KXyOe32lIQTjA== + +google-closure-compiler@20230228.0.0: + version "20230228.0.0" + resolved "https://registry.npmjs.org/google-closure-compiler/-/google-closure-compiler-20230228.0.0.tgz#a891408dca350bb7fe56ee50e2b4ee97f3a740d6" + integrity sha512-jFI4QNZgM4WhNIoaRNwA5kHq6n6NKSWZj3N9HgRsJE9bN4LUrkIURI+svChbEp/WmGh3Bt3o3/5kUlOOWyCo3Q== + dependencies: + chalk "4.x" + google-closure-compiler-java "^20230228.0.0" minimist "1.x" vinyl "2.x" vinyl-sourcemaps-apply "^0.2.0" optionalDependencies: - google-closure-compiler-linux "^20220301.0.0" - google-closure-compiler-osx "^20220301.0.0" - google-closure-compiler-windows "^20220301.0.0" - -google-closure-library@20220301.0.0: - version "20220301.0.0" - resolved "https://registry.npmjs.org/google-closure-library/-/google-closure-library-20220301.0.0.tgz#c9aaa99218f949b1f914a86f2a4529dea20e2e47" - integrity sha512-GRRBfG80JPqkKkTxiRoVr/x4UmnPW2aeA72NH0zapPtrvSkAOCzfJFrdudLrAJJtXPdSE65+CkYrpZX8tP0mCQ== + google-closure-compiler-linux "^20230228.0.0" + google-closure-compiler-osx "^20230228.0.0" + google-closure-compiler-windows "^20230228.0.0" + +google-closure-library@20230228.0.0: + version "20230228.0.0" + resolved "https://registry.npmjs.org/google-closure-library/-/google-closure-library-20230228.0.0.tgz#8ef2f9de391c69e8b8e26e7c0bcd413b0b6bb605" + integrity sha512-yIe7gpacdmfM8n6Cvswajw7MgiWYwr46/1HHVCYKthyrfEBabDa1zCH6BPJJi07cnN7ImCyKtOGZXz6EAwtXBg== google-gax@^3.0.1: version "3.1.3"