Skip to content

Commit 62d94ab

Browse files
[Multi-Tab] Making Multi-Tab optional (#980)
1 parent 2530138 commit 62d94ab

File tree

9 files changed

+276
-44
lines changed

9 files changed

+276
-44
lines changed

packages/firestore-types/index.d.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,26 @@ export interface Settings {
5757
timestampsInSnapshots?: boolean;
5858
}
5959

60+
/**
61+
* Settings that can be passed to Firestore.enablePersistence() to configure
62+
* Firestore persistence.
63+
*/
64+
export interface PersistenceSettings {
65+
/**
66+
* Whether to synchronize the in-memory state of multiple tabs. Setting this
67+
* to 'true' in all open tabs enables shared access to local persistence,
68+
* shared execution of queries and latency-compensated local document updates
69+
* across all connected instances.
70+
*
71+
* To enable this mode, `synchronizeTabs:true` needs to be set globally in
72+
* all active tabs. If omitted or set to 'false', `enablePersistence()` will
73+
* fail in all but the first tab.
74+
*
75+
* NOTE: This mode is experimental and not yet recommended for production use.
76+
*/
77+
synchronizeTabs?: boolean;
78+
}
79+
6080
export type LogLevel = 'debug' | 'error' | 'silent';
6181

6282
export function setLogLevel(logLevel: LogLevel): void;
@@ -91,10 +111,11 @@ export class FirebaseFirestore {
91111
* * unimplemented: The browser is incompatible with the offline
92112
* persistence implementation.
93113
*
114+
* @param settings Optional settings object to configure persistence.
94115
* @return A promise that represents successfully enabling persistent
95116
* storage.
96117
*/
97-
enablePersistence(): Promise<void>;
118+
enablePersistence(settings?: PersistenceSettings): Promise<void>;
98119

99120
/**
100121
* Gets a `CollectionReference` instance that refers to the collection at

packages/firestore/src/api/database.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,14 @@ import {
9898
// underscore to discourage their use.
9999
// tslint:disable:strip-private-property-underscore
100100

101+
// settings() defaults:
101102
const DEFAULT_HOST = 'firestore.googleapis.com';
102103
const DEFAULT_SSL = true;
103104
const DEFAULT_TIMESTAMPS_IN_SNAPSHOTS = false;
104105

106+
// enablePersistence() defaults:
107+
const DEFAULT_SYNCHRONIZE_TABS = false;
108+
105109
/** Undocumented, private additional settings not exposed in our public API. */
106110
interface PrivateSettings extends firestore.Settings {
107111
// Can be a google-auth-library or gapi client.
@@ -198,6 +202,37 @@ class FirestoreConfig {
198202
persistence: boolean;
199203
}
200204

205+
/**
206+
* Encapsulates the settings that can be used to configure Firestore
207+
* persistence.
208+
*/
209+
export class PersistenceSettings {
210+
/** Whether to enable multi-tab synchronization. */
211+
synchronizeTabs: boolean;
212+
213+
constructor(
214+
readonly enabled: boolean,
215+
settings?: firestore.PersistenceSettings
216+
) {
217+
assert(
218+
enabled || !settings,
219+
'Can only provide PersistenceSettings with persistence enabled'
220+
);
221+
settings = settings || {};
222+
this.synchronizeTabs = objUtils.defaulted(
223+
settings.synchronizeTabs,
224+
DEFAULT_SYNCHRONIZE_TABS
225+
);
226+
}
227+
228+
isEqual(other: PersistenceSettings): boolean {
229+
return (
230+
this.enabled === other.enabled &&
231+
this.synchronizeTabs === other.synchronizeTabs
232+
);
233+
}
234+
}
235+
201236
/**
202237
* The root reference to the database.
203238
*/
@@ -291,7 +326,7 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService {
291326
return this._firestoreClient.disableNetwork();
292327
}
293328

294-
enablePersistence(): Promise<void> {
329+
enablePersistence(settings?: firestore.PersistenceSettings): Promise<void> {
295330
if (this._firestoreClient) {
296331
throw new FirestoreError(
297332
Code.FAILED_PRECONDITION,
@@ -301,17 +336,21 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService {
301336
);
302337
}
303338

304-
return this.configureClient(/* persistence= */ true);
339+
return this.configureClient(
340+
new PersistenceSettings(/* enabled= */ true, settings)
341+
);
305342
}
306343

307344
ensureClientConfigured(): FirestoreClient {
308345
if (!this._firestoreClient) {
309-
this.configureClient(/* persistence= */ false);
346+
this.configureClient(new PersistenceSettings(/* enabled= */ false));
310347
}
311348
return this._firestoreClient as FirestoreClient;
312349
}
313350

314-
private configureClient(persistence: boolean): Promise<void> {
351+
private configureClient(
352+
persistenceSettings: PersistenceSettings
353+
): Promise<void> {
315354
assert(
316355
!!this._config.settings.host,
317356
'FirestoreSettings.host cannot be falsey'
@@ -377,7 +416,7 @@ follow these steps, YOUR APP MAY BREAK.`);
377416
this._config.credentials,
378417
this._queue
379418
);
380-
return this._firestoreClient.start(persistence);
419+
return this._firestoreClient.start(persistenceSettings);
381420
}
382421

383422
private static databaseIdFromApp(app: FirebaseApp): DatabaseId {

packages/firestore/src/core/firestore_client.ts

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ import {
5959
WebStorageSharedClientState
6060
} from '../local/shared_client_state';
6161
import { AutoId } from '../util/misc';
62+
import { PersistenceSettings } from '../api/database';
63+
import { assert } from '../util/assert';
6264

6365
const LOG_TAG = 'FirestoreClient';
6466

@@ -135,13 +137,14 @@ export class FirestoreClient {
135137
* fallback succeeds we signal success to the async queue even though the
136138
* start() itself signals failure.
137139
*
138-
* @param usePersistence Whether or not to attempt to enable persistence.
140+
* @param persistenceSettings Settings object to configure offline
141+
* persistence.
139142
* @returns A deferred result indicating the user-visible result of enabling
140143
* offline persistence. This method will reject this if IndexedDB fails to
141144
* start for any reason. If usePersistence is false this is
142145
* unconditionally resolved.
143146
*/
144-
start(usePersistence: boolean): Promise<void> {
147+
start(persistenceSettings: PersistenceSettings): Promise<void> {
145148
// We defer our initialization until we get the current user from
146149
// setUserChangeListener(). We block the async queue until we got the
147150
// initial user and the initialization is completed. This will prevent
@@ -164,7 +167,7 @@ export class FirestoreClient {
164167
if (!initialized) {
165168
initialized = true;
166169

167-
this.initializePersistence(usePersistence, persistenceResult, user)
170+
this.initializePersistence(persistenceSettings, persistenceResult, user)
168171
.then(() => this.initializeRest(user))
169172
.then(initializationDone.resolve, initializationDone.reject);
170173
} else {
@@ -200,7 +203,7 @@ export class FirestoreClient {
200203
* platform can't possibly support our implementation then this method rejects
201204
* the persistenceResult and falls back on memory-only persistence.
202205
*
203-
* @param usePersistence indicates whether or not to use offline persistence
206+
* @param persistenceSettings Settings object to configure offline persistence
204207
* @param persistenceResult A deferred result indicating the user-visible
205208
* result of enabling offline persistence. This method will reject this if
206209
* IndexedDB fails to start for any reason. If usePersistence is false
@@ -210,12 +213,12 @@ export class FirestoreClient {
210213
* succeeded.
211214
*/
212215
private initializePersistence(
213-
usePersistence: boolean,
216+
persistenceSettings: PersistenceSettings,
214217
persistenceResult: Deferred<void>,
215218
user: User
216219
): Promise<void> {
217-
if (usePersistence) {
218-
return this.startIndexedDbPersistence(user)
220+
if (persistenceSettings.enabled) {
221+
return this.startIndexedDbPersistence(user, persistenceSettings)
219222
.then(persistenceResult.resolve)
220223
.catch(error => {
221224
// Regardless of whether or not the retry succeeds, from an user
@@ -278,7 +281,15 @@ export class FirestoreClient {
278281
*
279282
* @returns A promise indicating success or failure.
280283
*/
281-
private startIndexedDbPersistence(user: User): Promise<void> {
284+
private startIndexedDbPersistence(
285+
user: User,
286+
settings: PersistenceSettings
287+
): Promise<void> {
288+
assert(
289+
settings.enabled,
290+
'Should only start IndexedDb persitence with offline persistence enabled.'
291+
);
292+
282293
// TODO(http://b/33384523): For now we just disable garbage collection
283294
// when persistence is enabled.
284295
this.garbageCollector = new NoOpGarbageCollector();
@@ -291,32 +302,35 @@ export class FirestoreClient {
291302
});
292303

293304
return Promise.resolve().then(() => {
294-
this.persistence = new IndexedDbPersistence(
305+
const persistence: IndexedDbPersistence = new IndexedDbPersistence(
295306
storagePrefix,
296307
this.clientId,
297308
this.platform,
298309
this.asyncQueue,
299310
serializer
300311
);
301-
if (WebStorageSharedClientState.isAvailable(this.platform)) {
302-
this.sharedClientState = new WebStorageSharedClientState(
303-
this.asyncQueue,
304-
this.platform,
305-
storagePrefix,
306-
this.clientId,
307-
user
312+
this.persistence = persistence;
313+
314+
if (
315+
settings.synchronizeTabs &&
316+
!WebStorageSharedClientState.isAvailable(this.platform)
317+
) {
318+
throw new FirestoreError(
319+
Code.UNIMPLEMENTED,
320+
'IndexedDB persistence is only available on platforms that support LocalStorage.'
308321
);
309-
} else {
310-
if (process.env.USE_MOCK_PERSISTENCE !== 'YES') {
311-
throw new FirestoreError(
312-
Code.UNIMPLEMENTED,
313-
'IndexedDB persistence is only available on platforms that support LocalStorage.'
314-
);
315-
}
316-
debug(LOG_TAG, 'Starting Persistence in test-only non multi-tab mode');
317-
this.sharedClientState = new MemorySharedClientState();
318322
}
319-
return this.persistence.start();
323+
324+
this.sharedClientState = settings.synchronizeTabs
325+
? new WebStorageSharedClientState(
326+
this.asyncQueue,
327+
this.platform,
328+
storagePrefix,
329+
this.clientId,
330+
user
331+
)
332+
: new MemorySharedClientState();
333+
return persistence.start(settings.synchronizeTabs);
320334
});
321335
}
322336

@@ -420,11 +434,11 @@ export class FirestoreClient {
420434
return this.asyncQueue.enqueue(async () => {
421435
// PORTING NOTE: LocalStore does not need an explicit shutdown on web.
422436
await this.syncEngine.shutdown();
423-
await this.remoteStore.shutdown();
424437
await this.sharedClientState.shutdown();
425438
await this.persistence.shutdown(
426439
options && options.purgePersistenceWithDataLoss
427440
);
441+
await this.remoteStore.shutdown();
428442

429443
// `removeUserChangeListener` must be called after shutting down the
430444
// RemoteStore as it will prevent the RemoteStore from retrieving

0 commit comments

Comments
 (0)