Skip to content

Commit fb9f23b

Browse files
Add OfflineCause
1 parent 00c9136 commit fb9f23b

File tree

4 files changed

+91
-45
lines changed

4 files changed

+91
-45
lines changed

packages/firestore/src/core/firestore_client.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export class FirestoreClient {
179179
persistenceResult
180180
).then(initializationDone.resolve, initializationDone.reject);
181181
} else {
182-
this.asyncQueue.enqueueAndForget(() =>
182+
this.asyncQueue.enqueueRetryable(() =>
183183
this.remoteStore.handleCredentialChange(user)
184184
);
185185
}

packages/firestore/src/remote/remote_store.ts

+46-42
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,25 @@ const LOG_TAG = 'RemoteStore';
6262
// TODO(b/35853402): Negotiate this with the stream.
6363
const MAX_PENDING_WRITES = 10;
6464

65+
/** Reasons for why the RemoteStore may be offline. */
66+
const enum OfflineCause {
67+
/** The user has explicitly disabled the network (via `disableNetwork()`). */
68+
UserDisabled,
69+
/** An IndexedDb failure occurred while persisting a stream update. */
70+
IndexedDbFailed,
71+
/** The tab is not the primary tab (only relevant with multi-tab). */
72+
IsSecondary,
73+
/**
74+
* We have received new Auth credentials and the RemoteStore is restarting
75+
* the streams.
76+
*/
77+
CredentialChange,
78+
/** The connectivity state of the environment has changed. */
79+
ConnectivityChange,
80+
/** The RemoteStore has been shut down. */
81+
Shutdown
82+
}
83+
6584
/**
6685
* RemoteStore - An interface to remotely stored data, basically providing a
6786
* wrapper around the Datastore that is more reliable for the rest of the
@@ -118,19 +137,10 @@ export class RemoteStore implements TargetMetadataProvider {
118137
private watchChangeAggregator: WatchChangeAggregator | null = null;
119138

120139
/**
121-
* Set to true by enableNetwork() and false by disableNetwork() and indicates
122-
* the user-preferred network state.
123-
*/
124-
private networkEnabled = false;
125-
126-
private isPrimary = false;
127-
128-
/**
129-
* When set to `true`, the network was taken offline due to an IndexedDB
130-
* failure. The state is flipped to `false` when access becomes available
131-
* again.
140+
* A set of reasons for why the RemoteStore may be offline. If empty, the
141+
* RemoteStore may start its network connections.
132142
*/
133-
private indexedDbFailed = false;
143+
private offlineCauses = new Set<OfflineCause>();
134144

135145
private onlineStateTracker: OnlineStateTracker;
136146

@@ -194,7 +204,7 @@ export class RemoteStore implements TargetMetadataProvider {
194204

195205
/** Re-enables the network. Idempotent. */
196206
enableNetwork(): Promise<void> {
197-
this.networkEnabled = true;
207+
this.offlineCauses.delete(OfflineCause.UserDisabled);
198208
return this.enableNetworkInternal();
199209
}
200210

@@ -216,7 +226,7 @@ export class RemoteStore implements TargetMetadataProvider {
216226
* enableNetwork().
217227
*/
218228
async disableNetwork(): Promise<void> {
219-
this.networkEnabled = false;
229+
this.offlineCauses.add(OfflineCause.UserDisabled);
220230
await this.disableNetworkInternal();
221231

222232
// Set the OnlineState to Offline so get()s return from cache, etc.
@@ -240,7 +250,7 @@ export class RemoteStore implements TargetMetadataProvider {
240250

241251
async shutdown(): Promise<void> {
242252
logDebug(LOG_TAG, 'RemoteStore shutting down.');
243-
this.networkEnabled = false;
253+
this.offlineCauses.add(OfflineCause.Shutdown);
244254
await this.disableNetworkInternal();
245255
this.connectivityMonitor.shutdown();
246256

@@ -349,7 +359,7 @@ export class RemoteStore implements TargetMetadataProvider {
349359
}
350360

351361
canUseNetwork(): boolean {
352-
return !this.indexedDbFailed && this.isPrimary && this.networkEnabled;
362+
return this.offlineCauses.size === 0;
353363
}
354364

355365
private cleanUpWatchStreamState(): void {
@@ -457,10 +467,10 @@ export class RemoteStore implements TargetMetadataProvider {
457467
): Promise<void> {
458468
if (isIndexedDbTransactionError(e)) {
459469
debugAssert(
460-
!this.indexedDbFailed,
470+
!this.offlineCauses.has(OfflineCause.IndexedDbFailed),
461471
'Unexpected network event when IndexedDB was marked failed.'
462472
);
463-
this.indexedDbFailed = true;
473+
this.offlineCauses.add(OfflineCause.IndexedDbFailed);
464474

465475
// Disable network and raise offline snapshots
466476
await this.disableNetworkInternal();
@@ -477,7 +487,7 @@ export class RemoteStore implements TargetMetadataProvider {
477487
this.asyncQueue.enqueueRetryable(async () => {
478488
logDebug(LOG_TAG, 'Retrying IndexedDB access');
479489
await op!();
480-
this.indexedDbFailed = false;
490+
this.offlineCauses.delete(OfflineCause.IndexedDbFailed);
481491
await this.enableNetworkInternal();
482492
});
483493
} else {
@@ -751,45 +761,39 @@ export class RemoteStore implements TargetMetadataProvider {
751761
}
752762

753763
private async restartNetwork(): Promise<void> {
754-
this.networkEnabled = false;
764+
this.offlineCauses.add(OfflineCause.ConnectivityChange);
755765
await this.disableNetworkInternal();
756766
this.onlineStateTracker.set(OnlineState.Unknown);
757-
await this.enableNetwork();
767+
this.offlineCauses.delete(OfflineCause.ConnectivityChange);
768+
await this.enableNetworkInternal();
758769
}
759770

760771
async handleCredentialChange(user: User): Promise<void> {
761772
this.asyncQueue.verifyOperationInProgress();
762773

763-
if (this.canUseNetwork()) {
764-
// Tear down and re-create our network streams. This will ensure we get a
765-
// fresh auth token for the new user and re-fill the write pipeline with
766-
// new mutations from the LocalStore (since mutations are per-user).
767-
logDebug(LOG_TAG, 'RemoteStore restarting streams for new credential');
774+
// Tear down and re-create our network streams. This will ensure we get a
775+
// fresh auth token for the new user and re-fill the write pipeline with
776+
// new mutations from the LocalStore (since mutations are per-user).
777+
logDebug(LOG_TAG, 'RemoteStore received new credentials');
778+
this.offlineCauses.add(OfflineCause.CredentialChange);
768779

769-
this.networkEnabled = false;
770-
await this.disableNetworkInternal();
771-
this.onlineStateTracker.set(OnlineState.Unknown);
780+
await this.disableNetworkInternal();
781+
this.onlineStateTracker.set(OnlineState.Unknown);
782+
await this.syncEngine.handleUserChange(user);
772783

773-
await this.executeWithRecovery(async () => {
774-
await this.syncEngine.handleUserChange(user);
775-
await this.enableNetwork();
776-
});
777-
} else {
778-
await this.executeWithRecovery(() =>
779-
this.syncEngine.handleUserChange(user)
780-
);
781-
}
784+
this.offlineCauses.delete(OfflineCause.CredentialChange);
785+
await this.enableNetworkInternal();
782786
}
783787

784788
/**
785789
* Toggles the network state when the client gains or loses its primary lease.
786790
*/
787791
async applyPrimaryState(isPrimary: boolean): Promise<void> {
788-
this.isPrimary = isPrimary;
789-
790-
if (isPrimary && this.networkEnabled) {
791-
await this.enableNetwork();
792+
if (isPrimary) {
793+
this.offlineCauses.delete(OfflineCause.IsSecondary);
794+
await this.enableNetworkInternal();
792795
} else if (!isPrimary) {
796+
this.offlineCauses.add(OfflineCause.IsSecondary);
793797
await this.disableNetworkInternal();
794798
this.onlineStateTracker.set(OnlineState.Unknown);
795799
}

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

+42
Original file line numberDiff line numberDiff line change
@@ -754,4 +754,46 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
754754
);
755755
}
756756
);
757+
758+
specTest(
759+
'Multiple user changes during transaction failure (with recovery)',
760+
['durable-persistence'],
761+
() => {
762+
const query = Query.atPath(path('collection'));
763+
const doc1 = doc(
764+
'collection/key1',
765+
0,
766+
{ foo: 'a' },
767+
{ hasLocalMutations: true }
768+
);
769+
return (
770+
spec()
771+
.changeUser('user1')
772+
.userSets('collection/key1', { foo: 'a' })
773+
.userListens(query)
774+
.expectEvents(query, {
775+
added: [doc1],
776+
fromCache: true,
777+
hasPendingWrites: true
778+
})
779+
// Change the user to user2 and back to user1 while IndexedDB is failed
780+
.failDatabaseTransactions('Handle user change')
781+
.changeUser('user2')
782+
// The network is offline due to the failed user change
783+
.expectActiveTargets()
784+
.changeUser('user1')
785+
.recoverDatabase()
786+
.runTimer(TimerId.AsyncQueueRetry)
787+
.expectActiveTargets({ query })
788+
// We are now user 2
789+
.expectEvents(query, { removed: [doc1], fromCache: true })
790+
// We are now user 1
791+
.expectEvents(query, {
792+
added: [doc1],
793+
fromCache: true,
794+
hasPendingWrites: true
795+
})
796+
);
797+
}
798+
);
757799
});

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -716,8 +716,8 @@ abstract class TestRunner {
716716

717717
private async doChangeUser(user: string | null): Promise<void> {
718718
this.user = new User(user);
719-
return this.queue.enqueue(() =>
720-
this.remoteStore.handleCredentialChange(this.user)
719+
this.queue.enqueueRetryable(() =>
720+
this.remoteStore.handleCredentialChange(new User(user))
721721
);
722722
}
723723

0 commit comments

Comments
 (0)