Skip to content

Commit e51265b

Browse files
firestore-multi-tab Merge
2 parents c40ff09 + 62d94ab commit e51265b

18 files changed

+709
-331
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/event_manager.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { SyncEngine } from './sync_engine';
1919
import { OnlineState, TargetId } from './types';
2020
import { DocumentViewChange } from './view_snapshot';
2121
import { ChangeType, ViewSnapshot } from './view_snapshot';
22-
import { DocumentSet } from '../model/document_set';
2322
import { assert } from '../util/assert';
2423
import { EventHandler } from '../util/misc';
2524
import { ObjectMap } from '../util/obj_map';
@@ -289,28 +288,13 @@ export class QueryListener {
289288
!this.raisedInitialEvent,
290289
'Trying to raise initial events for second time'
291290
);
292-
snap = new ViewSnapshot(
291+
snap = ViewSnapshot.fromInitialDocuments(
293292
snap.query,
294293
snap.docs,
295-
DocumentSet.emptySet(snap.docs),
296-
QueryListener.getInitialViewChanges(snap),
297294
snap.fromCache,
298-
snap.hasPendingWrites,
299-
/* syncChangesState= */ true,
300-
/* excludesMetadataChanges= */ false
295+
snap.hasPendingWrites
301296
);
302297
this.raisedInitialEvent = true;
303298
this.queryObserver.next(snap);
304299
}
305-
306-
/** Returns changes as if all documents in the snap were added. */
307-
private static getInitialViewChanges(
308-
snap: ViewSnapshot
309-
): DocumentViewChange[] {
310-
const result: DocumentViewChange[] = [];
311-
snap.docs.forEach(doc => {
312-
result.push({ type: ChangeType.Added, doc });
313-
});
314-
return result;
315-
}
316300
}

packages/firestore/src/core/firestore_client.ts

Lines changed: 42 additions & 28 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

@@ -394,7 +408,7 @@ export class FirestoreClient {
394408

395409
// NOTE: This will immediately call the listener, so we make sure to
396410
// set it after localStore / remoteStore are started.
397-
this.persistence.setPrimaryStateListener(isPrimary =>
411+
await this.persistence.setPrimaryStateListener(isPrimary =>
398412
this.syncEngine.applyPrimaryState(isPrimary)
399413
);
400414
});
@@ -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

packages/firestore/src/core/sync_engine.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -178,28 +178,40 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
178178
* server. All the subsequent view snapshots or errors are sent to the
179179
* subscribed handlers. Returns the targetId of the query.
180180
*/
181-
listen(query: Query): Promise<TargetId> {
181+
async listen(query: Query): Promise<TargetId> {
182182
this.assertSubscribed('listen()');
183-
assert(
184-
!this.queryViewsByQuery.has(query),
185-
'We already listen to the query: ' + query
186-
);
187183

188-
return this.localStore.allocateQuery(query).then(queryData => {
184+
let targetId;
185+
let viewSnapshot;
186+
187+
const queryView = this.queryViewsByQuery.get(query);
188+
if (queryView) {
189+
// PORTING NOTE: With Mult-Tab Web, it is possible that a query view
190+
// already exists when EventManager calls us for the first time. This
191+
// happens when the primary tab is already listening to this query on
192+
// behalf of another tab and the user of the primary also starts listening
193+
// to the query. EventManager will not have an assigned target ID in this
194+
// case and calls `listen` to obtain this ID.
195+
targetId = queryView.targetId;
196+
this.sharedClientState.addLocalQueryTarget(targetId);
197+
viewSnapshot = queryView.view.computeInitialSnapshot();
198+
} else {
199+
const queryData = await this.localStore.allocateQuery(query);
189200
const status = this.sharedClientState.addLocalQueryTarget(
190201
queryData.targetId
191202
);
192-
return this.initializeViewAndComputeInitialSnapshot(
203+
targetId = queryData.targetId;
204+
viewSnapshot = await this.initializeViewAndComputeInitialSnapshot(
193205
queryData,
194206
status === 'current'
195-
).then(viewSnapshot => {
196-
if (this.isPrimary) {
197-
this.remoteStore.listen(queryData);
198-
}
199-
this.viewHandler!([viewSnapshot]);
200-
return queryData.targetId;
201-
});
202-
});
207+
);
208+
if (this.isPrimary) {
209+
this.remoteStore.listen(queryData);
210+
}
211+
}
212+
213+
this.viewHandler!([viewSnapshot]);
214+
return targetId;
203215
}
204216

205217
private initializeViewAndComputeInitialSnapshot(

0 commit comments

Comments
 (0)