Skip to content

Commit 2f17ba0

Browse files
Multi-Tab Persistence
As per go/multi-tab. This feature is not yet enabled for third-party users.
1 parent dc9a5d1 commit 2f17ba0

File tree

75 files changed

+8447
-1550
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+8447
-1550
lines changed

packages/firebase/index.d.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,27 @@ declare namespace firebase.firestore {
769769
timestampsInSnapshots?: boolean;
770770
}
771771

772+
// TODO(multitab): Uncomment when multi-tab is released publicly.
773+
// /**
774+
// * Settings that can be passed to Firestore.enablePersistence() to configure
775+
// * Firestore persistence.
776+
// */
777+
// export interface PersistenceSettings {
778+
// /**
779+
// * Whether to synchronize the in-memory state of multiple tabs. Setting this
780+
// * to 'true' in all open tabs enables shared access to local persistence,
781+
// * shared execution of queries and latency-compensated local document updates
782+
// * across all connected instances.
783+
// *
784+
// * To enable this mode, `experimentalTabSynchronization:true` needs to be set
785+
// * globally in all active tabs. If omitted or set to 'false',
786+
// * `enablePersistence()` will fail in all but the first tab.
787+
// *
788+
// * NOTE: This mode is not yet recommended for production use.
789+
// */
790+
// experimentalTabSynchronization?: boolean;
791+
// }
792+
772793
export type LogLevel = 'debug' | 'error' | 'silent';
773794

774795
export function setLogLevel(logLevel: LogLevel): void;
@@ -808,6 +829,29 @@ declare namespace firebase.firestore {
808829
*/
809830
enablePersistence(): Promise<void>;
810831

832+
// TODO(multitab): Uncomment when multi-tab is released publicly.
833+
// /**
834+
// * Attempts to enable persistent storage, if possible.
835+
// *
836+
// * Must be called before any other methods (other than settings()).
837+
// *
838+
// * If this fails, enablePersistence() will reject the promise it returns.
839+
// * Note that even after this failure, the firestore instance will remain
840+
// * usable, however offline persistence will be disabled.
841+
// *
842+
// * There are several reasons why this can fail, which can be identified by
843+
// * the `code` on the error.
844+
// *
845+
// * * failed-precondition: The app is already open in another browser tab.
846+
// * * unimplemented: The browser is incompatible with the offline
847+
// * persistence implementation.
848+
// *
849+
// * @param settings Optional settings object to configure persistence.
850+
// * @return A promise that represents successfully enabling persistent
851+
// * storage.
852+
// */
853+
// enablePersistence(settings?: PersistenceSettings): Promise<void>;
854+
811855
/**
812856
* Gets a `CollectionReference` instance that refers to the collection at
813857
* the specified path.

packages/firestore-types/index.d.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,27 @@ export interface Settings {
5757
timestampsInSnapshots?: boolean;
5858
}
5959

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

6283
export function setLogLevel(logLevel: LogLevel): void;
@@ -96,6 +117,29 @@ export class FirebaseFirestore {
96117
*/
97118
enablePersistence(): Promise<void>;
98119

120+
// TODO(multitab): Uncomment when multi-tab is released publicly.
121+
// /**
122+
// * Attempts to enable persistent storage, if possible.
123+
// *
124+
// * Must be called before any other methods (other than settings()).
125+
// *
126+
// * If this fails, enablePersistence() will reject the promise it returns.
127+
// * Note that even after this failure, the firestore instance will remain
128+
// * usable, however offline persistence will be disabled.
129+
// *
130+
// * There are several reasons why this can fail, which can be identified by
131+
// * the `code` on the error.
132+
// *
133+
// * * failed-precondition: The app is already open in another browser tab.
134+
// * * unimplemented: The browser is incompatible with the offline
135+
// * persistence implementation.
136+
// *
137+
// * @param settings Optional settings object to configure persistence.
138+
// * @return A promise that represents successfully enabling persistent
139+
// * storage.
140+
// */
141+
// enablePersistence(settings?: PersistenceSettings): Promise<void>;
142+
99143
/**
100144
* Gets a `CollectionReference` instance that refers to the collection at
101145
* the specified path.

packages/firestore/.idea/runConfigurations/All_Tests.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/firestore/.idea/runConfigurations/Integration_Tests.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/firestore/.idea/runConfigurations/Unit_Tests.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/firestore/src/api/database.ts

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

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

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

204+
// TODO(multitab): Replace with Firestore.PersistenceSettings
205+
// tslint:disable-next-line:no-any The definition for these settings is private
206+
export type _PersistenceSettings = any;
207+
208+
/**
209+
* Encapsulates the settings that can be used to configure Firestore
210+
* persistence.
211+
*/
212+
export class PersistenceSettings {
213+
/** Whether to enable multi-tab synchronization. */
214+
experimentalTabSynchronization: boolean;
215+
216+
constructor(readonly enabled: boolean, settings?: _PersistenceSettings) {
217+
assert(
218+
enabled || !settings,
219+
'Can only provide PersistenceSettings with persistence enabled'
220+
);
221+
settings = settings || {};
222+
this.experimentalTabSynchronization = objUtils.defaulted(
223+
settings.experimentalTabSynchronization,
224+
DEFAULT_SYNCHRONIZE_TABS
225+
);
226+
}
227+
228+
isEqual(other: PersistenceSettings): boolean {
229+
return (
230+
this.enabled === other.enabled &&
231+
this.experimentalTabSynchronization ===
232+
other.experimentalTabSynchronization
233+
);
234+
}
235+
}
236+
200237
/**
201238
* The root reference to the database.
202239
*/
@@ -290,7 +327,7 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService {
290327
return this._firestoreClient.disableNetwork();
291328
}
292329

293-
enablePersistence(): Promise<void> {
330+
enablePersistence(settings?: _PersistenceSettings): Promise<void> {
294331
if (this._firestoreClient) {
295332
throw new FirestoreError(
296333
Code.FAILED_PRECONDITION,
@@ -300,19 +337,23 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService {
300337
);
301338
}
302339

303-
return this.configureClient(/* persistence= */ true);
340+
return this.configureClient(
341+
new PersistenceSettings(/* enabled= */ true, settings)
342+
);
304343
}
305344

306345
ensureClientConfigured(): FirestoreClient {
307346
if (!this._firestoreClient) {
308347
// Kick off starting the client but don't actually wait for it.
309348
// tslint:disable-next-line:no-floating-promises
310-
this.configureClient(/* persistence= */ false);
349+
this.configureClient(new PersistenceSettings(/* enabled= */ false));
311350
}
312351
return this._firestoreClient as FirestoreClient;
313352
}
314353

315-
private configureClient(persistence: boolean): Promise<void> {
354+
private configureClient(
355+
persistenceSettings: PersistenceSettings
356+
): Promise<void> {
316357
assert(
317358
!!this._config.settings.host,
318359
'FirestoreSettings.host cannot be falsey'
@@ -378,7 +419,7 @@ follow these steps, YOUR APP MAY BREAK.`);
378419
this._config.credentials,
379420
this._queue
380421
);
381-
return this._firestoreClient.start(persistence);
422+
return this._firestoreClient.start(persistenceSettings);
382423
}
383424

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

packages/firestore/src/core/event_manager.ts

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@
1515
*/
1616

1717
import { Query } from './query';
18-
import { SyncEngine } from './sync_engine';
18+
import { SyncEngine, SyncEngineListener } 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';
@@ -47,18 +46,15 @@ export interface Observer<T> {
4746
* It handles "fan-out". -- Identical queries will re-use the same watch on the
4847
* backend.
4948
*/
50-
export class EventManager {
49+
export class EventManager implements SyncEngineListener {
5150
private queries = new ObjectMap<Query, QueryListenersInfo>(q =>
5251
q.canonicalId()
5352
);
5453

5554
private onlineState: OnlineState = OnlineState.Unknown;
5655

5756
constructor(private syncEngine: SyncEngine) {
58-
this.syncEngine.subscribe(
59-
this.onChange.bind(this),
60-
this.onError.bind(this)
61-
);
57+
this.syncEngine.subscribe(this);
6258
}
6359

6460
listen(listener: QueryListener): Promise<TargetId> {
@@ -106,7 +102,7 @@ export class EventManager {
106102
}
107103
}
108104

109-
onChange(viewSnaps: ViewSnapshot[]): void {
105+
onWatchChange(viewSnaps: ViewSnapshot[]): void {
110106
for (const viewSnap of viewSnaps) {
111107
const query = viewSnap.query;
112108
const queryInfo = this.queries.get(query);
@@ -119,7 +115,7 @@ export class EventManager {
119115
}
120116
}
121117

122-
onError(query: Query, error: Error): void {
118+
onWatchError(query: Query, error: Error): void {
123119
const queryInfo = this.queries.get(query);
124120
if (queryInfo) {
125121
for (const listener of queryInfo.listeners) {
@@ -132,7 +128,7 @@ export class EventManager {
132128
this.queries.delete(query);
133129
}
134130

135-
applyOnlineStateChange(onlineState: OnlineState): void {
131+
onOnlineStateChange(onlineState: OnlineState): void {
136132
this.onlineState = onlineState;
137133
this.queries.forEach((_, queryInfo) => {
138134
for (const listener of queryInfo.listeners) {
@@ -289,28 +285,13 @@ export class QueryListener {
289285
!this.raisedInitialEvent,
290286
'Trying to raise initial events for second time'
291287
);
292-
snap = new ViewSnapshot(
288+
snap = ViewSnapshot.fromInitialDocuments(
293289
snap.query,
294290
snap.docs,
295-
DocumentSet.emptySet(snap.docs),
296-
QueryListener.getInitialViewChanges(snap),
297291
snap.fromCache,
298-
snap.hasPendingWrites,
299-
/* syncChangesState= */ true,
300-
/* excludesMetadataChanges= */ false
292+
snap.hasPendingWrites
301293
);
302294
this.raisedInitialEvent = true;
303295
this.queryObserver.next(snap);
304296
}
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-
}
316297
}

0 commit comments

Comments
 (0)