Skip to content

Commit 1d9f803

Browse files
Adding goOnline/goOffline to the Web SDK (#201)
1 parent d15d48c commit 1d9f803

File tree

6 files changed

+189
-54
lines changed

6 files changed

+189
-54
lines changed

packages/firestore/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"dev": "gulp dev",
77
"test": "run-p test:browser test:node",
88
"test:browser": "karma start --single-run",
9+
"test:browser:debug" : "karma start --browsers=Chrome --auto-watch",
910
"test:node": "mocha 'test/{,!(integration|browser)/**/}*.test.ts' --compilers ts:ts-node/register -r src/platform_node/node_init.ts --retries 5 --timeout 5000",
1011
"prepare": "gulp build"
1112
},

packages/firestore/src/api/database.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,9 @@ export class Firestore implements firestore.Firestore, FirebaseService {
187187
//
188188
// Operations on the _firestoreClient don't block on _firestoreReady. Those
189189
// are already set to synchronize on the async queue.
190-
private _firestoreClient: FirestoreClient | undefined;
190+
//
191+
// This is public for testing.
192+
public _firestoreClient: FirestoreClient | undefined;
191193
public _dataConverter: UserDataConverter;
192194

193195
constructor(databaseIdOrApp: FirestoreDatabase | FirebaseApp) {

packages/firestore/src/core/firestore_client.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,13 @@ export class FirestoreClient {
160160
return persistenceResult.promise;
161161
}
162162

163+
/** Enables the network connection and requeues all pending operations. */
164+
public enableNetwork(): Promise<void> {
165+
return this.asyncQueue.schedule(() => {
166+
return this.remoteStore.enableNetwork();
167+
});
168+
}
169+
163170
/**
164171
* Initializes persistent storage, attempting to use IndexedDB if
165172
* usePersistence is true or memory-only if false.
@@ -314,6 +321,13 @@ export class FirestoreClient {
314321
return this.syncEngine.handleUserChange(user);
315322
}
316323

324+
/** Disables the network connection. Pending operations will not complete. */
325+
public disableNetwork(): Promise<void> {
326+
return this.asyncQueue.schedule(() => {
327+
return this.remoteStore.disableNetwork();
328+
});
329+
}
330+
317331
shutdown(): Promise<void> {
318332
return this.asyncQueue
319333
.schedule(() => {

packages/firestore/src/remote/remote_store.ts

Lines changed: 101 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ export class RemoteStore {
116116

117117
private accumulatedWatchChanges: WatchChange[] = [];
118118

119-
private watchStream: PersistentListenStream;
120-
private writeStream: PersistentWriteStream;
119+
private watchStream: PersistentListenStream = null;
120+
private writeStream: PersistentWriteStream = null;
121121

122122
/**
123123
* The online state of the watch stream. The state is set to healthy if and
@@ -149,10 +149,7 @@ export class RemoteStore {
149149
* LocalStore, etc.
150150
*/
151151
start(): Promise<void> {
152-
return this.setupStreams().then(() => {
153-
// Resume any writes
154-
return this.fillWritePipeline();
155-
});
152+
return this.enableNetwork();
156153
}
157154

158155
private setOnlineStateToHealthy(): void {
@@ -192,7 +189,26 @@ export class RemoteStore {
192189
}
193190
}
194191

195-
private setupStreams(): Promise<void> {
192+
private isNetworkEnabled(): boolean {
193+
assert(
194+
(this.watchStream == null) == (this.writeStream == null),
195+
'WatchStream and WriteStream should both be null or non-null'
196+
);
197+
return this.watchStream != null;
198+
}
199+
200+
/** Re-enables the network. Only to be called as the counterpart to disableNetwork(). */
201+
enableNetwork(): Promise<void> {
202+
assert(
203+
this.watchStream == null,
204+
'enableNetwork() called with non-null watchStream.'
205+
);
206+
assert(
207+
this.writeStream == null,
208+
'enableNetwork() called with non-null writeStream.'
209+
);
210+
211+
// Create new streams (but note they're not started yet).
196212
this.watchStream = this.datastore.newPersistentWatchStream({
197213
onOpen: this.onWatchStreamOpen.bind(this),
198214
onClose: this.onWatchStreamClose.bind(this),
@@ -208,15 +224,38 @@ export class RemoteStore {
208224
// Load any saved stream token from persistent storage
209225
return this.localStore.getLastStreamToken().then(token => {
210226
this.writeStream.lastStreamToken = token;
227+
228+
if (this.shouldStartWatchStream()) {
229+
this.startWatchStream();
230+
}
231+
232+
this.updateAndBroadcastOnlineState(OnlineState.Unknown);
233+
234+
return this.fillWritePipeline(); // This may start the writeStream.
211235
});
212236
}
213237

214-
shutdown(): Promise<void> {
215-
log.debug(LOG_TAG, 'RemoteStore shutting down.');
216-
this.cleanupWatchStreamState();
217-
this.writeStream.stop();
238+
/** Temporarily disables the network. The network can be re-enabled using enableNetwork(). */
239+
disableNetwork(): Promise<void> {
240+
this.updateAndBroadcastOnlineState(OnlineState.Failed);
241+
242+
// NOTE: We're guaranteed not to get any further events from these streams (not even a close
243+
// event).
218244
this.watchStream.stop();
245+
this.writeStream.stop();
246+
247+
this.cleanUpWatchStreamState();
248+
this.cleanUpWriteStreamState();
219249

250+
this.writeStream = null;
251+
this.watchStream = null;
252+
253+
return Promise.resolve();
254+
}
255+
256+
shutdown(): Promise<void> {
257+
log.debug(LOG_TAG, 'RemoteStore shutting down.');
258+
this.disableNetwork();
220259
return Promise.resolve(undefined);
221260
}
222261

@@ -228,11 +267,12 @@ export class RemoteStore {
228267
);
229268
// Mark this as something the client is currently listening for.
230269
this.listenTargets[queryData.targetId] = queryData;
231-
if (this.watchStream.isOpen()) {
232-
this.sendWatchRequest(queryData);
233-
} else if (!this.watchStream.isStarted()) {
270+
271+
if (this.shouldStartWatchStream()) {
234272
// The listen will be sent in onWatchStreamOpen
235273
this.startWatchStream();
274+
} else if (this.isNetworkEnabled() && this.watchStream.isOpen()) {
275+
this.sendWatchRequest(queryData);
236276
}
237277
}
238278

@@ -244,7 +284,7 @@ export class RemoteStore {
244284
);
245285
const queryData = this.listenTargets[targetId];
246286
delete this.listenTargets[targetId];
247-
if (this.watchStream.isOpen()) {
287+
if (this.isNetworkEnabled() && this.watchStream.isOpen()) {
248288
this.sendUnwatchRequest(targetId);
249289
}
250290
}
@@ -279,10 +319,9 @@ export class RemoteStore {
279319
}
280320

281321
private startWatchStream(): void {
282-
assert(!this.watchStream.isStarted(), "Can't restart started watch stream");
283322
assert(
284323
this.shouldStartWatchStream(),
285-
'Tried to start watch stream even though it should not be started'
324+
'startWriteStream() called when shouldStartWatchStream() is false.'
286325
);
287326
this.watchStream.start();
288327
}
@@ -292,10 +331,14 @@ export class RemoteStore {
292331
* active targets trying to be listened too
293332
*/
294333
private shouldStartWatchStream(): boolean {
295-
return !objUtils.isEmpty(this.listenTargets);
334+
return (
335+
this.isNetworkEnabled() &&
336+
!this.watchStream.isStarted() &&
337+
!objUtils.isEmpty(this.listenTargets)
338+
);
296339
}
297340

298-
private cleanupWatchStreamState(): void {
341+
private cleanUpWatchStreamState(): void {
299342
// If the connection is closed then we'll never get a snapshot version for
300343
// the accumulated changes and so we'll never be able to complete the batch.
301344
// When we start up again the server is going to resend these changes
@@ -314,7 +357,12 @@ export class RemoteStore {
314357
}
315358

316359
private onWatchStreamClose(error: FirestoreError | null): Promise<void> {
317-
this.cleanupWatchStreamState();
360+
assert(
361+
this.isNetworkEnabled(),
362+
'onWatchStreamClose() should only be called when the network is enabled'
363+
);
364+
365+
this.cleanUpWatchStreamState();
318366

319367
// If there was an error, retry the connection.
320368
if (this.shouldStartWatchStream()) {
@@ -510,6 +558,11 @@ export class RemoteStore {
510558
return promiseChain;
511559
}
512560

561+
cleanUpWriteStreamState() {
562+
this.lastBatchSeen = BATCHID_UNKNOWN;
563+
this.pendingWrites = [];
564+
}
565+
513566
/**
514567
* Notifies that there are new mutations to process in the queue. This is
515568
* typically called by SyncEngine after it has sent mutations to LocalStore.
@@ -543,7 +596,9 @@ export class RemoteStore {
543596
* writes complete the backend will be able to accept more.
544597
*/
545598
canWriteMutations(): boolean {
546-
return this.pendingWrites.length < MAX_PENDING_WRITES;
599+
return (
600+
this.isNetworkEnabled() && this.pendingWrites.length < MAX_PENDING_WRITES
601+
);
547602
}
548603

549604
// For testing
@@ -565,15 +620,26 @@ export class RemoteStore {
565620

566621
this.pendingWrites.push(batch);
567622

568-
if (!this.writeStream.isStarted()) {
623+
if (this.shouldStartWriteStream()) {
569624
this.startWriteStream();
570-
} else if (this.writeStream.handshakeComplete) {
625+
} else if (this.isNetworkEnabled() && this.writeStream.handshakeComplete) {
571626
this.writeStream.writeMutations(batch.mutations);
572627
}
573628
}
574629

630+
private shouldStartWriteStream(): boolean {
631+
return (
632+
this.isNetworkEnabled() &&
633+
!this.writeStream.isStarted() &&
634+
this.pendingWrites.length > 0
635+
);
636+
}
637+
575638
private startWriteStream(): void {
576-
assert(!this.writeStream.isStarted(), "Can't restart started write stream");
639+
assert(
640+
this.shouldStartWriteStream(),
641+
'startWriteStream() called when shouldStartWriteStream() is false.'
642+
);
577643
this.writeStream.start();
578644
}
579645

@@ -632,6 +698,11 @@ export class RemoteStore {
632698
}
633699

634700
private onWriteStreamClose(error?: FirestoreError): Promise<void> {
701+
assert(
702+
this.isNetworkEnabled(),
703+
'onWriteStreamClose() should only be called when the network is enabled'
704+
);
705+
635706
// Ignore close if there are no pending writes.
636707
if (this.pendingWrites.length > 0) {
637708
assert(
@@ -653,7 +724,7 @@ export class RemoteStore {
653724
return errorHandling.then(() => {
654725
// The write stream might have been started by refilling the write
655726
// pipeline for failed writes
656-
if (this.pendingWrites.length > 0 && !this.writeStream.isStarted()) {
727+
if (this.shouldStartWriteStream()) {
657728
this.startWriteStream();
658729
}
659730
});
@@ -713,33 +784,10 @@ export class RemoteStore {
713784
handleUserChange(user: User): Promise<void> {
714785
log.debug(LOG_TAG, 'RemoteStore changing users: uid=', user.uid);
715786

716-
// Clear pending writes because those are per-user. Watched targets
717-
// persist across users so don't clear those.
718-
this.lastBatchSeen = BATCHID_UNKNOWN;
719-
this.pendingWrites = [];
720-
721-
// Stop the streams. They promise not to call us back.
722-
this.watchStream.stop();
723-
this.writeStream.stop();
724-
725-
this.cleanupWatchStreamState();
726-
727-
// Create new streams (but note they're not started yet).
728-
return this.setupStreams()
729-
.then(() => {
730-
// If there are any watchedTargets, properly handle the stream
731-
// restart now that RemoteStore is ready to handle them.
732-
if (this.shouldStartWatchStream()) {
733-
this.startWatchStream();
734-
}
735-
736-
// Resume any writes
737-
return this.fillWritePipeline();
738-
})
739-
.then(() => {
740-
// User change moves us back to the unknown state because we might
741-
// not want to re-open the stream
742-
this.setOnlineStateToUnknown();
743-
});
787+
// Tear down and re-create our network streams. This will ensure we get a fresh auth token
788+
// for the new user and re-fill the write pipeline with new mutations from the LocalStore
789+
// (since mutations are per-user).
790+
this.disableNetwork();
791+
return this.enableNetwork();
744792
}
745793
}

packages/firestore/test/integration/api/database.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { Deferred } from '../../../src/util/promise';
2121
import { asyncIt } from '../../util/helpers';
2222
import firebase from '../util/firebase_export';
2323
import { apiDescribe, withTestCollection, withTestDb } from '../util/helpers';
24+
import { Firestore } from '../../../src/api/database';
2425

2526
apiDescribe('Database', persistence => {
2627
asyncIt('can set a document', () => {
@@ -514,4 +515,47 @@ apiDescribe('Database', persistence => {
514515
return Promise.resolve();
515516
});
516517
});
518+
519+
asyncIt('can queue writes while offline', () => {
520+
return withTestDb(persistence, db => {
521+
const docRef = db.collection('rooms').doc();
522+
const firestoreClient = (docRef.firestore as Firestore)._firestoreClient;
523+
524+
return firestoreClient
525+
.disableNetwork()
526+
.then(() => {
527+
return Promise.all([
528+
docRef.set({ foo: 'bar' }),
529+
firestoreClient.enableNetwork()
530+
]);
531+
})
532+
.then(() => docRef.get())
533+
.then(doc => {
534+
expect(doc.data()).to.deep.equal({ foo: 'bar' });
535+
});
536+
});
537+
});
538+
539+
asyncIt('can get documents while offline', () => {
540+
return withTestDb(persistence, db => {
541+
const docRef = db.collection('rooms').doc();
542+
const firestoreClient = (docRef.firestore as Firestore)._firestoreClient;
543+
544+
return firestoreClient.disableNetwork().then(() => {
545+
const writePromise = docRef.set({ foo: 'bar' });
546+
547+
return docRef.get().then(snapshot => {
548+
expect(snapshot.metadata.fromCache).to.be.true;
549+
return firestoreClient.enableNetwork().then(() => {
550+
return writePromise.then(() => {
551+
docRef.get().then(doc => {
552+
expect(snapshot.metadata.fromCache).to.be.false;
553+
expect(doc.data()).to.deep.equal({ foo: 'bar' });
554+
});
555+
});
556+
});
557+
});
558+
});
559+
});
560+
});
517561
});

0 commit comments

Comments
 (0)