Skip to content

Commit 0d7bff1

Browse files
committed
Firestore: add experimentalLongPollingTimeout setting (b/266868871, #6987)
1 parent a8d6499 commit 0d7bff1

File tree

14 files changed

+192
-48
lines changed

14 files changed

+192
-48
lines changed

common/api-review/firestore.api.md

+1
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ export interface FirestoreSettings {
227227
cacheSizeBytes?: number;
228228
experimentalAutoDetectLongPolling?: boolean;
229229
experimentalForceLongPolling?: boolean;
230+
experimentalLongPollingTimeout?: number;
230231
host?: string;
231232
ignoreUndefinedProperties?: boolean;
232233
localCache?: FirestoreLocalCache;

packages/firestore/src/api/settings.ts

+19
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,23 @@ export interface FirestoreSettings extends LiteSettings {
9292
* cannot be combined with `experimentalForceLongPolling`.
9393
*/
9494
experimentalAutoDetectLongPolling?: boolean;
95+
96+
/**
97+
* The desired maximum timeout interval (in milliseconds) to complete a
98+
* long-polling GET response.
99+
*
100+
* By default, when long-polling is used the "hanging GET" request sent by the
101+
* client times out after 30 seconds (30,000 milliseconds). To request a
102+
* different timeout from the server, set this setting with the desired
103+
* timeout. This may be useful, for example, if the buffering proxy that
104+
* necessitated enabling long-polling in the first place has a shorter timeout
105+
* for hanging GET requests, in which case setting the long-polling timeout to
106+
* a shorter value, such as 25 seconds, may fix prematurely-closed hanging GET
107+
* requests.
108+
*
109+
* This setting is only used if `experimentalForceLongPolling` is true or if
110+
* `experimentalAutoDetectLongPolling` is true and the auto-detection
111+
* determined that long-polling was needed. Otherwise, it is ignored.
112+
*/
113+
experimentalLongPollingTimeout?: number;
95114
}

packages/firestore/src/core/database_info.ts

+4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ export class DatabaseInfo {
3434
* when using WebChannel as the network transport.
3535
* @param autoDetectLongPolling - Whether to use the detectBufferingProxy
3636
* option when using WebChannel as the network transport.
37+
* @param longPollingTimeout The desired maximum timeout interval (in
38+
* milliseconds) to complete a long-polling GET response; the server _may_
39+
* ignore this value; may be undefined to use the default (30 seconds).
3740
* @param useFetchStreams Whether to use the Fetch API instead of
3841
* XMLHTTPRequest
3942
*/
@@ -45,6 +48,7 @@ export class DatabaseInfo {
4548
readonly ssl: boolean,
4649
readonly forceLongPolling: boolean,
4750
readonly autoDetectLongPolling: boolean,
51+
readonly longPollingTimeout: number | undefined,
4852
readonly useFetchStreams: boolean
4953
) {}
5054
}

packages/firestore/src/lite-api/components.ts

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export function makeDatabaseInfo(
117117
settings.ssl,
118118
settings.experimentalForceLongPolling,
119119
settings.experimentalAutoDetectLongPolling,
120+
settings.experimentalLongPollingTimeout,
120121
settings.useFetchStreams
121122
);
122123
}

packages/firestore/src/lite-api/settings.ts

+44
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ import { validateIsNotUsedTogether } from '../util/input_validation';
2929
export const DEFAULT_HOST = 'firestore.googleapis.com';
3030
export const DEFAULT_SSL = true;
3131

32+
// The minimum long-polling timeout is hardcoded on the server. The value here
33+
// should be kept in sync with the value used by the server, as the server will
34+
// silently ignore a value below the minimum and fall back to the default.
35+
// Googlers see http://google3/net/webchannel/internal/webchannel_config.cc;l=118;rcl=510899643
36+
const MIN_LONG_POLLING_TIMEOUT = 5000;
37+
38+
// No maximum long-polling timeout is not enforced by the server; however, a
39+
// "reasonable" maximum value is set by the client to provide some guard rails.
40+
// Googlers see b/266868871 for relevant discussion.
41+
const MAX_LONG_POLLING_TIMEOUT = 600000;
42+
3243
/**
3344
* Specifies custom configurations for your Cloud Firestore instance.
3445
* You must set these before invoking any other methods.
@@ -60,6 +71,8 @@ export interface PrivateSettings extends FirestoreSettings {
6071
// Used in firestore@exp
6172
experimentalAutoDetectLongPolling?: boolean;
6273
// Used in firestore@exp
74+
experimentalLongPollingTimeout?: number;
75+
// Used in firestore@exp
6376
useFetchStreams?: boolean;
6477

6578
localCache?: FirestoreLocalCache;
@@ -83,6 +96,8 @@ export class FirestoreSettingsImpl {
8396

8497
readonly experimentalAutoDetectLongPolling: boolean;
8598

99+
readonly experimentalLongPollingTimeout: number | undefined;
100+
86101
readonly ignoreUndefinedProperties: boolean;
87102

88103
readonly useFetchStreams: boolean;
@@ -130,6 +145,7 @@ export class FirestoreSettingsImpl {
130145
this.experimentalForceLongPolling = !!settings.experimentalForceLongPolling;
131146
this.experimentalAutoDetectLongPolling =
132147
!!settings.experimentalAutoDetectLongPolling;
148+
this.experimentalLongPollingTimeout = settings?.experimentalLongPollingTimeout;
133149
this.useFetchStreams = !!settings.useFetchStreams;
134150

135151
validateIsNotUsedTogether(
@@ -138,6 +154,32 @@ export class FirestoreSettingsImpl {
138154
'experimentalAutoDetectLongPolling',
139155
settings.experimentalAutoDetectLongPolling
140156
);
157+
158+
if (typeof this.experimentalLongPollingTimeout === 'number') {
159+
if (! Number.isInteger(this.experimentalLongPollingTimeout)) {
160+
throw new FirestoreError(
161+
Code.INVALID_ARGUMENT,
162+
`invalid long polling timeout: ` +
163+
`${this.experimentalLongPollingTimeout} (must be an integer)`
164+
);
165+
}
166+
if (this.experimentalLongPollingTimeout < MIN_LONG_POLLING_TIMEOUT) {
167+
throw new FirestoreError(
168+
Code.INVALID_ARGUMENT,
169+
`invalid long polling timeout: ` +
170+
`${this.experimentalLongPollingTimeout} ` +
171+
`(minimum allowed value is ${MIN_LONG_POLLING_TIMEOUT})`
172+
);
173+
}
174+
if (this.experimentalLongPollingTimeout > MAX_LONG_POLLING_TIMEOUT) {
175+
throw new FirestoreError(
176+
Code.INVALID_ARGUMENT,
177+
`invalid long polling timeout: ` +
178+
`${this.experimentalLongPollingTimeout} ` +
179+
`(maximum allowed value is ${MAX_LONG_POLLING_TIMEOUT})`
180+
);
181+
}
182+
}
141183
}
142184

143185
isEqual(other: FirestoreSettingsImpl): boolean {
@@ -150,6 +192,8 @@ export class FirestoreSettingsImpl {
150192
other.experimentalForceLongPolling &&
151193
this.experimentalAutoDetectLongPolling ===
152194
other.experimentalAutoDetectLongPolling &&
195+
this.experimentalLongPollingTimeout ===
196+
other.experimentalLongPollingTimeout &&
153197
this.ignoreUndefinedProperties === other.ignoreUndefinedProperties &&
154198
this.useFetchStreams === other.useFetchStreams
155199
);

packages/firestore/src/platform/browser/webchannel_connection.ts

+6
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,14 @@ export class WebChannelConnection extends RestConnection {
5757
private readonly forceLongPolling: boolean;
5858
private readonly autoDetectLongPolling: boolean;
5959
private readonly useFetchStreams: boolean;
60+
private readonly longPollingTimeout: number | undefined;
6061

6162
constructor(info: DatabaseInfo) {
6263
super(info);
6364
this.forceLongPolling = info.forceLongPolling;
6465
this.autoDetectLongPolling = info.autoDetectLongPolling;
6566
this.useFetchStreams = info.useFetchStreams;
67+
this.longPollingTimeout = info.longPollingTimeout;
6668
}
6769

6870
protected performRPCRequest<Req, Resp>(
@@ -200,6 +202,10 @@ export class WebChannelConnection extends RestConnection {
200202
detectBufferingProxy: this.autoDetectLongPolling
201203
};
202204

205+
if (this.longPollingTimeout) {
206+
request.longPollingTimeout = this.longPollingTimeout;
207+
}
208+
203209
if (this.useFetchStreams) {
204210
request.xmlHttpFactory = new FetchXmlHttpFactory({});
205211
}

packages/firestore/test/integration/util/internal_helpers.ts

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export function getDefaultDatabaseInfo(): DatabaseInfo {
5757
!!DEFAULT_SETTINGS.ssl,
5858
!!DEFAULT_SETTINGS.experimentalForceLongPolling,
5959
!!DEFAULT_SETTINGS.experimentalAutoDetectLongPolling,
60+
DEFAULT_SETTINGS.experimentalLongPollingTimeout,
6061
/*use FetchStreams= */ false
6162
);
6263
}

packages/firestore/test/unit/api/database.test.ts

+63-1
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ describe('SnapshotMetadata', () => {
265265
});
266266
});
267267

268-
describe('Settings', () => {
268+
describe.only('Settings', () => {
269269
it('can not use mutually exclusive settings together', () => {
270270
// Use a new instance of Firestore in order to configure settings.
271271
const db = newTestFirestore();
@@ -279,6 +279,68 @@ describe('Settings', () => {
279279
);
280280
});
281281

282+
it('experimentalLongPollingTimeout is undefined by default', () => {
283+
// Use a new instance of Firestore in order to configure settings.
284+
const db = newTestFirestore();
285+
expect(db._getSettings().experimentalLongPollingTimeout).to.be.undefined;
286+
});
287+
288+
it('experimentalLongPollingTimeout minimum value is allowed', () => {
289+
// Use a new instance of Firestore in order to configure settings.
290+
const db = newTestFirestore();
291+
db._setSettings({experimentalLongPollingTimeout: 5000});
292+
expect(db._getSettings().experimentalLongPollingTimeout).to.equal(5000);
293+
});
294+
295+
it('experimentalLongPollingTimeout maximum value is allowed', () => {
296+
// Use a new instance of Firestore in order to configure settings.
297+
const db = newTestFirestore();
298+
db._setSettings({experimentalLongPollingTimeout: 600000});
299+
expect(db._getSettings().experimentalLongPollingTimeout).to.equal(600000);
300+
});
301+
302+
it('experimentalLongPollingTimeout typical value is allowed', () => {
303+
// Use a new instance of Firestore in order to configure settings.
304+
const db = newTestFirestore();
305+
db._setSettings({experimentalLongPollingTimeout: 25000});
306+
expect(db._getSettings().experimentalLongPollingTimeout).to.equal(25000);
307+
});
308+
309+
it('experimentalLongPollingTimeout value of 4999 throws', () => {
310+
// Use a new instance of Firestore in order to configure settings.
311+
const db = newTestFirestore();
312+
expect(() => db._setSettings({experimentalLongPollingTimeout: 4999}))
313+
.to.throw(/invalid.*timeout.*4999.*\(.*5000.*\)/i);
314+
});
315+
316+
it('experimentalLongPollingTimeout value of 0 throws', () => {
317+
// Use a new instance of Firestore in order to configure settings.
318+
const db = newTestFirestore();
319+
expect(() => db._setSettings({experimentalLongPollingTimeout: 0}))
320+
.to.throw(/invalid.*timeout.*0.*\(.*5000.*\)/i);
321+
});
322+
323+
it('experimentalLongPollingTimeout value of -1 throws', () => {
324+
// Use a new instance of Firestore in order to configure settings.
325+
const db = newTestFirestore();
326+
expect(() => db._setSettings({experimentalLongPollingTimeout: -1}))
327+
.to.throw(/invalid.*timeout.*-1.*\(.*5000.*\)/i);
328+
});
329+
330+
it('experimentalLongPollingTimeout value of 600001 throws', () => {
331+
// Use a new instance of Firestore in order to configure settings.
332+
const db = newTestFirestore();
333+
expect(() => db._setSettings({experimentalLongPollingTimeout: 600001}))
334+
.to.throw(/invalid.*timeout.*600001.*\(.*600000.*\)/i);
335+
});
336+
337+
it('experimentalLongPollingTimeout non-integral value throws', () => {
338+
// Use a new instance of Firestore in order to configure settings.
339+
const db = newTestFirestore();
340+
expect(() => db._setSettings({experimentalLongPollingTimeout: 123.456}))
341+
.to.throw(/invalid.*timeout.*123.456.*\(.*integer.*\)/i);
342+
});
343+
282344
it('gets settings from useEmulator', () => {
283345
// Use a new instance of Firestore in order to configure settings.
284346
const db = newTestFirestore();

packages/firestore/test/unit/remote/rest_connection.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ describe('RestConnection', () => {
6565
/*ssl=*/ false,
6666
/*forceLongPolling=*/ false,
6767
/*autoDetectLongPolling=*/ false,
68+
/*longPollingTimeout=*/ undefined,
6869
/*useFetchStreams=*/ false
6970
);
7071
const connection = new TestRestConnection(testDatabaseInfo);

packages/firestore/test/unit/specs/spec_test_runner.ts

+1
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ abstract class TestRunner {
273273
/*ssl=*/ false,
274274
/*forceLongPolling=*/ false,
275275
/*autoDetectLongPolling=*/ false,
276+
/*longPollingTimeout=*/ undefined,
276277
/*useFetchStreams=*/ false
277278
);
278279

packages/webchannel-wrapper/externs/overrides.js

+3
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ goog.net.WebChannel.Options.detectBufferingProxy;
6969
/** @type {unknown} */
7070
goog.net.WebChannel.Options.xmlHttpFactory;
7171

72+
/** @type {number|undefined} */
73+
goog.net.WebChannel.Options.longPollingTimeout;
74+
7275
goog.labs.net.webChannel.requestStats.Event = {};
7376
goog.labs.net.webChannel.requestStats.Event.STAT_EVENT;
7477

packages/webchannel-wrapper/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
"license": "Apache-2.0",
2828
"devDependencies": {
2929
"@rollup/plugin-commonjs": "21.1.0",
30-
"google-closure-compiler": "20220301.0.0",
31-
"google-closure-library": "20220301.0.0",
30+
"google-closure-compiler": "20230228.0.0",
31+
"google-closure-library": "20230228.0.0",
3232
"gulp": "4.0.2",
3333
"gulp-sourcemaps": "3.0.0",
3434
"rollup": "2.79.1",

packages/webchannel-wrapper/src/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export interface WebChannelOptions {
101101
encodeInitMessageHeaders?: boolean;
102102
forceLongPolling?: boolean;
103103
detectBufferingProxy?: boolean;
104+
longPollingTimeout?: number;
104105
fastHandshake?: boolean;
105106
disableRedac?: boolean;
106107
clientProfile?: string;

0 commit comments

Comments
 (0)