Skip to content

Commit a1faa32

Browse files
Add OfflineCause
1 parent 00c9136 commit a1faa32

File tree

4 files changed

+91
-45
lines changed

4 files changed

+91
-45
lines changed

packages/firestore/src/core/firestore_client.ts

Lines changed: 1 addition & 1 deletion
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

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ 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+
/** We are restarting the streams due to an Auth credential change. */
74+
CredentialChange,
75+
/** The connectivity state of the environment has changed. */
76+
ConnectivityChange,
77+
/** The RemoteStore has been shut down. */
78+
Shutdown
79+
}
80+
6581
/**
6682
* RemoteStore - An interface to remotely stored data, basically providing a
6783
* wrapper around the Datastore that is more reliable for the rest of the
@@ -118,19 +134,10 @@ export class RemoteStore implements TargetMetadataProvider {
118134
private watchChangeAggregator: WatchChangeAggregator | null = null;
119135

120136
/**
121-
* Set to true by enableNetwork() and false by disableNetwork() and indicates
122-
* the user-preferred network state.
137+
* A set of reasons for why the RemoteStore may be offline. If empty, the
138+
* RemoteStore may start its network connections.
123139
*/
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.
132-
*/
133-
private indexedDbFailed = false;
140+
private offlineCauses = new Set<OfflineCause>();
134141

135142
private onlineStateTracker: OnlineStateTracker;
136143

@@ -194,7 +201,7 @@ export class RemoteStore implements TargetMetadataProvider {
194201

195202
/** Re-enables the network. Idempotent. */
196203
enableNetwork(): Promise<void> {
197-
this.networkEnabled = true;
204+
this.offlineCauses.delete(OfflineCause.UserDisabled);
198205
return this.enableNetworkInternal();
199206
}
200207

@@ -216,7 +223,7 @@ export class RemoteStore implements TargetMetadataProvider {
216223
* enableNetwork().
217224
*/
218225
async disableNetwork(): Promise<void> {
219-
this.networkEnabled = false;
226+
this.offlineCauses.add(OfflineCause.UserDisabled);
220227
await this.disableNetworkInternal();
221228

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

241248
async shutdown(): Promise<void> {
242249
logDebug(LOG_TAG, 'RemoteStore shutting down.');
243-
this.networkEnabled = false;
250+
this.offlineCauses.add(OfflineCause.Shutdown);
244251
await this.disableNetworkInternal();
245252
this.connectivityMonitor.shutdown();
246253

@@ -349,7 +356,7 @@ export class RemoteStore implements TargetMetadataProvider {
349356
}
350357

351358
canUseNetwork(): boolean {
352-
return !this.indexedDbFailed && this.isPrimary && this.networkEnabled;
359+
return this.offlineCauses.size === 0;
353360
}
354361

355362
private cleanUpWatchStreamState(): void {
@@ -457,10 +464,10 @@ export class RemoteStore implements TargetMetadataProvider {
457464
): Promise<void> {
458465
if (isIndexedDbTransactionError(e)) {
459466
debugAssert(
460-
!this.indexedDbFailed,
467+
!this.offlineCauses.has(OfflineCause.IndexedDbFailed),
461468
'Unexpected network event when IndexedDB was marked failed.'
462469
);
463-
this.indexedDbFailed = true;
470+
this.offlineCauses.add(OfflineCause.IndexedDbFailed);
464471

465472
// Disable network and raise offline snapshots
466473
await this.disableNetworkInternal();
@@ -477,7 +484,7 @@ export class RemoteStore implements TargetMetadataProvider {
477484
this.asyncQueue.enqueueRetryable(async () => {
478485
logDebug(LOG_TAG, 'Retrying IndexedDB access');
479486
await op!();
480-
this.indexedDbFailed = false;
487+
this.offlineCauses.delete(OfflineCause.IndexedDbFailed);
481488
await this.enableNetworkInternal();
482489
});
483490
} else {
@@ -751,45 +758,39 @@ export class RemoteStore implements TargetMetadataProvider {
751758
}
752759

753760
private async restartNetwork(): Promise<void> {
754-
this.networkEnabled = false;
761+
this.offlineCauses.add(OfflineCause.ConnectivityChange);
755762
await this.disableNetworkInternal();
756763
this.onlineStateTracker.set(OnlineState.Unknown);
757-
await this.enableNetwork();
764+
this.offlineCauses.delete(OfflineCause.ConnectivityChange);
765+
await this.enableNetworkInternal();
758766
}
759767

760768
async handleCredentialChange(user: User): Promise<void> {
761769
this.asyncQueue.verifyOperationInProgress();
762770

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');
771+
// Tear down and re-create our network streams. This will ensure we get a
772+
// fresh auth token for the new user and re-fill the write pipeline with
773+
// new mutations from the LocalStore (since mutations are per-user).
774+
logDebug(LOG_TAG, 'RemoteStore received new credentials');
775+
this.offlineCauses.add(OfflineCause.CredentialChange);
768776

769-
this.networkEnabled = false;
770-
await this.disableNetworkInternal();
771-
this.onlineStateTracker.set(OnlineState.Unknown);
777+
await this.disableNetworkInternal();
778+
this.onlineStateTracker.set(OnlineState.Unknown);
779+
await this.syncEngine.handleUserChange(user);
772780

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-
}
781+
this.offlineCauses.delete(OfflineCause.CredentialChange);
782+
await this.enableNetworkInternal();
782783
}
783784

784785
/**
785786
* Toggles the network state when the client gains or loses its primary lease.
786787
*/
787788
async applyPrimaryState(isPrimary: boolean): Promise<void> {
788-
this.isPrimary = isPrimary;
789-
790-
if (isPrimary && this.networkEnabled) {
791-
await this.enableNetwork();
789+
if (isPrimary) {
790+
this.offlineCauses.delete(OfflineCause.IsSecondary);
791+
await this.enableNetworkInternal();
792792
} else if (!isPrimary) {
793+
this.offlineCauses.add(OfflineCause.IsSecondary);
793794
await this.disableNetworkInternal();
794795
this.onlineStateTracker.set(OnlineState.Unknown);
795796
}

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

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -716,8 +716,11 @@ 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+
// We don't block on `handleCredentialChange` as it may not get executed
720+
// during an IndexedDb failure. Non-recovery tests will pick up the user
721+
// change when the AsyncQueue is drained.
722+
this.queue.enqueueRetryable(() =>
723+
this.remoteStore.handleCredentialChange(new User(user))
721724
);
722725
}
723726

0 commit comments

Comments
 (0)