Skip to content

Commit b21b946

Browse files
schmidt-sebastianhsubox76
authored andcommitted
Support iOS 13 with offline persistence (#2271)
* Add transaction retries (#2250) * Marking SimpleDb calls as idempotent (#2251) * Mark mostly readonly calls as idempotent (#2252) * Fix test failure (#2256) * Make handleUserChange idempotent (#2257) * Temporarily disable CountingQueryEngine tests (#2258) * Improve test hack (#2259) * Improve test hack * Comment in test hack * Make getNewDocumentChanges() idempotent (#2255) * Add onCommitted listeners for transactions (#2265) * Fix build * Fix Lint * Make applyRemoteEvent idempotent (#2263) * Make notifyLocalViewChanges idempotent (#2268) * Make releaseQuery idempotent (#2266) * Mark acknowledgeBatch and rejectBatch idempotent (#2269) * idempotent `allocateQuery` and `notifyLocalViewChanges` (#2264) * Mark collectGarbage idempotent (#2267) * Idempotency: Address TODOs, add Changelog (#2270)
1 parent 9da3892 commit b21b946

30 files changed

+1172
-654
lines changed

packages/firestore/CHANGELOG.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
# Unreleased
2+
- [changed] Fixed a crash on iOS 13 that occurred when persistence was enabled
3+
in a background tab (#2232).
4+
- [fixed] Fixed an issue in the interaction with the Firestore Emulator that
5+
caused requests with timestamps to fail.
6+
- [feature] Added a `Firestore.onSnapshotsInSync()` method that notifies you
7+
when all your snapshot listeners are in sync with each other.
8+
9+
# 1.6.0
210
- [fixed] Fixed a regression that caused queries with nested field filters to
311
crash the client if the field was not present in the local copy of the
412
document.
5-
- [fixed] Fixed an issue that caused requests with timestamps to Firestore
6-
Emulator to fail.
713

814
# 1.5.0
915
- [feature] Added a `Firestore.waitForPendingWrites()` method that
@@ -17,8 +23,6 @@
1723
small subset of the documents in a collection.
1824
- [fixed] Fixed a race condition between authenticating and initializing
1925
Firestore that could result in initial writes to the database being dropped.
20-
- [feature] Added a `Firestore.onSnapshotsInSync()` method that notifies you
21-
when all your snapshot listeners are in sync with each other.
2226

2327
# 1.4.10
2428
- [changed] Transactions now perform exponential backoff before retrying.
@@ -37,7 +41,6 @@
3741
match the query (https://github.com/firebase/firebase-android-sdk/issues/155).
3842

3943
# 1.4.4
40-
>>>>>>> master
4144
- [fixed] Fixed an internal assertion that was triggered when an update
4245
with a `FieldValue.serverTimestamp()` and an update with a
4346
`FieldValue.increment()` were pending for the same document.

packages/firestore/src/core/firestore_client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,8 @@ export class FirestoreClient {
400400
// TODO(index-free): Use IndexFreeQueryEngine/IndexedQueryEngine as appropriate.
401401
const queryEngine = new SimpleQueryEngine();
402402
this.localStore = new LocalStore(this.persistence, queryEngine, user);
403+
await this.localStore.start();
404+
403405
if (maybeLruGc) {
404406
// We're running LRU Garbage collection. Set up the scheduler.
405407
this.lruScheduler = new LruScheduler(

packages/firestore/src/local/indexeddb_index_manager.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,28 @@ export class IndexedDbIndexManager implements IndexManager {
4040
*/
4141
private collectionParentsCache = new MemoryCollectionParentIndex();
4242

43+
/**
44+
* Adds a new entry to the collection parent index.
45+
*
46+
* Repeated calls for the same collectionPath should be avoided within a
47+
* transaction as IndexedDbIndexManager only caches writes once a transaction
48+
* has been committed.
49+
*/
4350
addToCollectionParentIndex(
4451
transaction: PersistenceTransaction,
4552
collectionPath: ResourcePath
4653
): PersistencePromise<void> {
4754
assert(collectionPath.length % 2 === 1, 'Expected a collection path.');
48-
if (this.collectionParentsCache.add(collectionPath)) {
49-
assert(collectionPath.length >= 1, 'Invalid collection path.');
55+
if (!this.collectionParentsCache.has(collectionPath)) {
5056
const collectionId = collectionPath.lastSegment();
5157
const parentPath = collectionPath.popLast();
58+
59+
transaction.addOnCommittedListener(() => {
60+
// Add the collection to the in memory cache only if the transaction was
61+
// successfully committed.
62+
this.collectionParentsCache.add(collectionPath);
63+
});
64+
5265
return collectionParentsStore(transaction).put({
5366
collectionId,
5467
parent: encode(parentPath)

packages/firestore/src/local/indexeddb_mutation_queue.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -178,26 +178,33 @@ export class IndexedDbMutationQueue implements MutationQueue {
178178
);
179179
const dbBatch = this.serializer.toDbMutationBatch(this.userId, batch);
180180

181-
this.documentKeysByBatchId[batchId] = batch.keys();
182-
183181
const promises: Array<PersistencePromise<void>> = [];
182+
let collectionParents = new SortedSet<ResourcePath>((l, r) =>
183+
primitiveComparator(l.canonicalString(), r.canonicalString())
184+
);
184185
for (const mutation of mutations) {
185186
const indexKey = DbDocumentMutation.key(
186187
this.userId,
187188
mutation.key.path,
188189
batchId
189190
);
191+
collectionParents = collectionParents.add(mutation.key.path.popLast());
190192
promises.push(mutationStore.put(dbBatch));
191193
promises.push(
192194
documentStore.put(indexKey, DbDocumentMutation.PLACEHOLDER)
193195
);
196+
}
197+
198+
collectionParents.forEach(parent => {
194199
promises.push(
195-
this.indexManager.addToCollectionParentIndex(
196-
transaction,
197-
mutation.key.path.popLast()
198-
)
200+
this.indexManager.addToCollectionParentIndex(transaction, parent)
199201
);
200-
}
202+
});
203+
204+
transaction.addOnCommittedListener(() => {
205+
this.documentKeysByBatchId[batchId] = batch.keys();
206+
});
207+
201208
return PersistencePromise.waitFor(promises).next(() => batch);
202209
});
203210
}
@@ -492,7 +499,9 @@ export class IndexedDbMutationQueue implements MutationQueue {
492499
this.userId,
493500
batch
494501
).next(removedDocuments => {
495-
this.removeCachedMutationKeys(batch.batchId);
502+
transaction.addOnCommittedListener(() => {
503+
this.removeCachedMutationKeys(batch.batchId);
504+
});
496505
return PersistencePromise.forEach(
497506
removedDocuments,
498507
(key: DocumentKey) => {

packages/firestore/src/local/indexeddb_persistence.ts

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { MutationQueue } from './mutation_queue';
6262
import {
6363
Persistence,
6464
PersistenceTransaction,
65+
PersistenceTransactionMode,
6566
PrimaryStateListener,
6667
ReferenceDelegate
6768
} from './persistence';
@@ -316,22 +317,16 @@ export class IndexedDbPersistence implements Persistence {
316317

317318
this.scheduleClientMetadataAndPrimaryLeaseRefreshes();
318319

319-
return this.startRemoteDocumentCache();
320-
})
321-
.then(() => {
322320
return this.simpleDb.runTransaction(
323-
'readonly',
321+
'readonly-idempotent',
324322
[DbTargetGlobal.store],
325-
txn => {
326-
return getHighestListenSequenceNumber(txn).next(
327-
highestListenSequenceNumber => {
328-
this.listenSequence = new ListenSequence(
329-
highestListenSequenceNumber,
330-
this.sequenceNumberSyncer
331-
);
332-
}
333-
);
334-
}
323+
txn => getHighestListenSequenceNumber(txn)
324+
);
325+
})
326+
.then(highestListenSequenceNumber => {
327+
this.listenSequence = new ListenSequence(
328+
highestListenSequenceNumber,
329+
this.sequenceNumberSyncer
335330
);
336331
})
337332
.then(() => {
@@ -343,12 +338,6 @@ export class IndexedDbPersistence implements Persistence {
343338
});
344339
}
345340

346-
private startRemoteDocumentCache(): Promise<void> {
347-
return this.simpleDb.runTransaction('readonly', ALL_STORES, txn =>
348-
this.remoteDocumentCache.start(txn)
349-
);
350-
}
351-
352341
setPrimaryStateListener(
353342
primaryStateListener: PrimaryStateListener
354343
): Promise<void> {
@@ -645,7 +634,7 @@ export class IndexedDbPersistence implements Persistence {
645634
this.detachVisibilityHandler();
646635
this.detachWindowUnloadHook();
647636
await this.simpleDb.runTransaction(
648-
'readwrite',
637+
'readwrite-idempotent',
649638
[DbPrimaryClient.store, DbClientMetadata.store],
650639
txn => {
651640
return this.releasePrimaryLeaseIfHeld(txn).next(() =>
@@ -677,7 +666,7 @@ export class IndexedDbPersistence implements Persistence {
677666

678667
getActiveClients(): Promise<ClientId[]> {
679668
return this.simpleDb.runTransaction(
680-
'readonly',
669+
'readonly-idempotent',
681670
[DbClientMetadata.store],
682671
txn => {
683672
return clientMetadataStore(txn)
@@ -742,20 +731,39 @@ export class IndexedDbPersistence implements Persistence {
742731

743732
runTransaction<T>(
744733
action: string,
745-
mode: 'readonly' | 'readwrite' | 'readwrite-primary',
734+
mode: PersistenceTransactionMode,
746735
transactionOperation: (
747736
transaction: PersistenceTransaction
748737
) => PersistencePromise<T>
749738
): Promise<T> {
750739
log.debug(LOG_TAG, 'Starting transaction:', action);
751740

741+
// TODO(schmidt-sebastian): Simplify once all transactions are idempotent.
742+
const idempotent = mode.endsWith('idempotent');
743+
const readonly = mode.startsWith('readonly');
744+
const simpleDbMode = readonly
745+
? idempotent
746+
? 'readonly-idempotent'
747+
: 'readonly'
748+
: idempotent
749+
? 'readwrite-idempotent'
750+
: 'readwrite';
751+
752+
let persistenceTransaction: PersistenceTransaction;
753+
752754
// Do all transactions as readwrite against all object stores, since we
753755
// are the only reader/writer.
754-
return this.simpleDb.runTransaction(
755-
mode === 'readonly' ? 'readonly' : 'readwrite',
756-
ALL_STORES,
757-
simpleDbTxn => {
758-
if (mode === 'readwrite-primary') {
756+
return this.simpleDb
757+
.runTransaction(simpleDbMode, ALL_STORES, simpleDbTxn => {
758+
persistenceTransaction = new IndexedDbTransaction(
759+
simpleDbTxn,
760+
this.listenSequence.next()
761+
);
762+
763+
if (
764+
mode === 'readwrite-primary' ||
765+
mode === 'readwrite-primary-idempotent'
766+
) {
759767
// While we merely verify that we have (or can acquire) the lease
760768
// immediately, we wait to extend the primary lease until after
761769
// executing transactionOperation(). This ensures that even if the
@@ -776,12 +784,7 @@ export class IndexedDbPersistence implements Persistence {
776784
PRIMARY_LEASE_LOST_ERROR_MSG
777785
);
778786
}
779-
return transactionOperation(
780-
new IndexedDbTransaction(
781-
simpleDbTxn,
782-
this.listenSequence.next()
783-
)
784-
);
787+
return transactionOperation(persistenceTransaction);
785788
})
786789
.next(result => {
787790
return this.acquireOrExtendPrimaryLease(simpleDbTxn).next(
@@ -790,13 +793,14 @@ export class IndexedDbPersistence implements Persistence {
790793
});
791794
} else {
792795
return this.verifyAllowTabSynchronization(simpleDbTxn).next(() =>
793-
transactionOperation(
794-
new IndexedDbTransaction(simpleDbTxn, this.listenSequence.next())
795-
)
796+
transactionOperation(persistenceTransaction)
796797
);
797798
}
798-
}
799-
);
799+
})
800+
.then(result => {
801+
persistenceTransaction.raiseOnCommittedEvent();
802+
return result;
803+
});
800804
}
801805

802806
/**

packages/firestore/src/local/indexeddb_query_cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export class IndexedDbQueryCache implements QueryCache {
164164
const queryData = this.serializer.fromDbTarget(value);
165165
if (
166166
queryData.sequenceNumber <= upperBound &&
167-
activeTargetIds[queryData.targetId] === undefined
167+
activeTargetIds.get(queryData.targetId) === null
168168
) {
169169
count++;
170170
promises.push(this.removeQueryData(txn, queryData));

0 commit comments

Comments
 (0)