Skip to content

Commit aa4c03d

Browse files
Merge master into release
2 parents 84bf8d9 + e9ff107 commit aa4c03d

File tree

12 files changed

+156
-32
lines changed

12 files changed

+156
-32
lines changed

.changeset/fast-moose-approve.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/app': patch
3+
---
4+
5+
More safeguards to ensure that heartbeat objects queried from IndexedDB include a heartbeats field.

.changeset/olive-dogs-play.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@firebase/firestore': patch
3+
'firebase': patch
4+
---
5+
6+
Fixed an issue in the local cache synchronization logic where all locally-cached documents that matched a resumed query would be unnecessarily re-downloaded; with the fix it now only downloads the documents that are known to be out-of-sync.
7+

packages/app/src/heartbeatService.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ export class HeartbeatServiceImpl implements HeartbeatService {
9090
const date = getUTCDateString();
9191
if (this._heartbeatsCache?.heartbeats == null) {
9292
this._heartbeatsCache = await this._heartbeatsCachePromise;
93+
// If we failed to construct a heartbeats cache, then return immediately.
94+
if (this._heartbeatsCache?.heartbeats == null) {
95+
return;
96+
}
9397
}
9498
// Do not store a heartbeat if one is already stored for this day
9599
// or if a header has already been sent today.
@@ -236,7 +240,11 @@ export class HeartbeatStorageImpl implements HeartbeatStorage {
236240
return { heartbeats: [] };
237241
} else {
238242
const idbHeartbeatObject = await readHeartbeatsFromIndexedDB(this.app);
239-
return idbHeartbeatObject || { heartbeats: [] };
243+
if (idbHeartbeatObject?.heartbeats) {
244+
return idbHeartbeatObject;
245+
} else {
246+
return { heartbeats: [] };
247+
}
240248
}
241249
}
242250
// overwrite the storage with the provided heartbeats

packages/auth/test/integration/webdriver/compat/firebaseui.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ browserDescribe('WebDriver integration with FirebaseUI', driver => {
5151
expect(snap.uid).to.be.a('string');
5252
});
5353

54-
it('allows google redirect sign in', async () => {
54+
it('allows google redirect sign in', async function () {
55+
// Test is ignored for now as it fails.
56+
// TODO: Investigate and unskip the test.
57+
this.skip();
58+
5559
const page = await startUi();
5660
await page.clickGoogleSignIn();
5761
const widget = new IdPPage(driver.webDriver);

packages/auth/test/integration/webdriver/redirect.test.ts

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ browserDescribe('WebDriver redirect IdP test', driver => {
4848
await driver.start('chrome');
4949
});
5050

51-
it('allows users to sign in', async () => {
51+
it('allows users to sign in', async function () {
52+
// Test is ignored for now as it fails.
53+
// TODO: Investigate and unskip the test.
54+
this.skip();
55+
5256
await driver.callNoWait(RedirectFunction.IDP_REDIRECT);
5357
const widget = new IdPPage(driver.webDriver);
5458

@@ -80,6 +84,10 @@ browserDescribe('WebDriver redirect IdP test', driver => {
8084

8185
// Redirect works with middleware for now
8286
it('is blocked by middleware', async function () {
87+
// Test is ignored for now as it fails.
88+
// TODO: Investigate and unskip the test.
89+
this.skip();
90+
8391
if (driver.isCompatLayer()) {
8492
console.warn('Skipping middleware tests in compat');
8593
this.skip();
@@ -106,7 +114,11 @@ browserDescribe('WebDriver redirect IdP test', driver => {
106114
expect(await driver.getUserSnapshot()).to.be.null;
107115
});
108116

109-
it('can link with another account account', async () => {
117+
it('can link with another account account', async function () {
118+
// Test is ignored for now as it fails.
119+
// TODO: Investigate and unskip the test.
120+
this.skip();
121+
110122
// First, sign in anonymously
111123
const { user: anonUser }: UserCredential = await driver.call(
112124
AnonFunction.SIGN_IN_ANONYMOUSLY
@@ -128,7 +140,11 @@ browserDescribe('WebDriver redirect IdP test', driver => {
128140
expect(user.email).to.eq('bob@bob.test');
129141
});
130142

131-
it('can be converted to a credential', async () => {
143+
it('can be converted to a credential', async function () {
144+
// Test is ignored for now as it fails.
145+
// TODO: Investigate and unskip the test.
146+
this.skip();
147+
132148
// Start with redirect
133149
await driver.callNoWait(RedirectFunction.IDP_REDIRECT);
134150
const widget = new IdPPage(driver.webDriver);
@@ -156,7 +172,11 @@ browserDescribe('WebDriver redirect IdP test', driver => {
156172
expect(second.providerData).to.eql(first.providerData);
157173
});
158174

159-
it('handles account exists different credential errors', async () => {
175+
it('handles account exists different credential errors', async function () {
176+
// Test is ignored for now as it fails.
177+
// TODO: Investigate and unskip the test.
178+
this.skip();
179+
160180
// Start with redirect and a verified account
161181
await driver.callNoWait(RedirectFunction.IDP_REDIRECT);
162182
const widget = new IdPPage(driver.webDriver);
@@ -191,7 +211,11 @@ browserDescribe('WebDriver redirect IdP test', driver => {
191211
]);
192212
});
193213

194-
it('does not auto-upgrade anon accounts', async () => {
214+
it('does not auto-upgrade anon accounts', async function () {
215+
// Test is ignored for now as it fails.
216+
// TODO: Investigate and unskip the test.
217+
this.skip();
218+
195219
const { user: anonUser }: UserCredential = await driver.call(
196220
AnonFunction.SIGN_IN_ANONYMOUSLY
197221
);
@@ -208,7 +232,11 @@ browserDescribe('WebDriver redirect IdP test', driver => {
208232
expect(curUser.uid).not.to.eq(anonUser.uid);
209233
});
210234

211-
it('linking with anonymous user upgrades account', async () => {
235+
it('linking with anonymous user upgrades account', async function () {
236+
// Test is ignored for now as it fails.
237+
// TODO: Investigate and unskip the test.
238+
this.skip();
239+
212240
const { user: anonUser }: UserCredential = await driver.call(
213241
AnonFunction.SIGN_IN_ANONYMOUSLY
214242
);
@@ -226,7 +254,11 @@ browserDescribe('WebDriver redirect IdP test', driver => {
226254
expect(curUser.isAnonymous).to.be.false;
227255
});
228256

229-
it('is possible to link with different email', async () => {
257+
it('is possible to link with different email', async function () {
258+
// Test is ignored for now as it fails.
259+
// TODO: Investigate and unskip the test.
260+
this.skip();
261+
230262
const { user: emailUser }: UserCredential = await driver.call(
231263
EmailFunction.CREATE_USER,
232264
'user@test.test'
@@ -249,7 +281,11 @@ browserDescribe('WebDriver redirect IdP test', driver => {
249281
expect(curUser.providerData.length).to.eq(2);
250282
});
251283

252-
it('is possible to link with the same email', async () => {
284+
it('is possible to link with the same email', async function () {
285+
// Test is ignored for now as it fails.
286+
// TODO: Investigate and unskip the test.
287+
this.skip();
288+
253289
const { user: emailUser }: UserCredential = await driver.call(
254290
EmailFunction.CREATE_USER,
255291
'same@test.test'
@@ -291,7 +327,11 @@ browserDescribe('WebDriver redirect IdP test', driver => {
291327
await driver.call(CoreFunction.SIGN_OUT);
292328
});
293329

294-
it('a user can sign in again', async () => {
330+
it('a user can sign in again', async function () {
331+
// Test is ignored for now as it fails.
332+
// TODO: Investigate and unskip the test.
333+
this.skip();
334+
295335
// Sign in using pre-poulated user
296336
await driver.callNoWait(RedirectFunction.IDP_REDIRECT);
297337

@@ -307,7 +347,11 @@ browserDescribe('WebDriver redirect IdP test', driver => {
307347
expect(user.email).to.eq(user1.email);
308348
});
309349

310-
it('reauthenticate works for the correct user', async () => {
350+
it('reauthenticate works for the correct user', async function () {
351+
// Test is ignored for now as it fails.
352+
// TODO: Investigate and unskip the test.
353+
this.skip();
354+
311355
// Sign in using pre-poulated user
312356
await driver.callNoWait(RedirectFunction.IDP_REDIRECT);
313357

packages/firestore/src/core/firestore_client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ async function executeQueryFromCache(
729729
const viewDocChanges = view.computeDocChanges(queryResult.documents);
730730
const viewChange = view.applyChanges(
731731
viewDocChanges,
732-
/* updateLimboDocuments= */ false
732+
/* limboResolutionEnabled= */ false
733733
);
734734
result.resolve(viewChange.snapshot!);
735735
} catch (e) {

packages/firestore/src/core/sync_engine_impl.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ async function initializeViewAndComputeSnapshot(
368368
);
369369
const viewChange = view.applyChanges(
370370
viewDocChanges,
371-
/* updateLimboDocuments= */ syncEngineImpl.isPrimaryClient,
371+
/* limboResolutionEnabled= */ syncEngineImpl.isPrimaryClient,
372372
synthesizedTargetChange
373373
);
374374
updateTrackedLimbos(syncEngineImpl, targetId, viewChange.limboChanges);
@@ -1081,10 +1081,13 @@ async function applyDocChanges(
10811081

10821082
const targetChange =
10831083
remoteEvent && remoteEvent.targetChanges.get(queryView.targetId);
1084+
const targetIsPendingReset =
1085+
remoteEvent && remoteEvent.targetMismatches.get(queryView.targetId) != null;
10841086
const viewChange = queryView.view.applyChanges(
10851087
viewDocChanges,
1086-
/* updateLimboDocuments= */ syncEngineImpl.isPrimaryClient,
1087-
targetChange
1088+
/* limboResolutionEnabled= */ syncEngineImpl.isPrimaryClient,
1089+
targetChange,
1090+
targetIsPendingReset
10881091
);
10891092
updateTrackedLimbos(
10901093
syncEngineImpl,

packages/firestore/src/core/view.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -271,17 +271,21 @@ export class View {
271271
* Updates the view with the given ViewDocumentChanges and optionally updates
272272
* limbo docs and sync state from the provided target change.
273273
* @param docChanges - The set of changes to make to the view's docs.
274-
* @param updateLimboDocuments - Whether to update limbo documents based on
274+
* @param limboResolutionEnabled - Whether to update limbo documents based on
275275
* this change.
276276
* @param targetChange - A target change to apply for computing limbo docs and
277277
* sync state.
278+
* @param targetIsPendingReset - Whether the target is pending to reset due to
279+
* existence filter mismatch. If not explicitly specified, it is treated
280+
* equivalently to `false`.
278281
* @returns A new ViewChange with the given docs, changes, and sync state.
279282
*/
280283
// PORTING NOTE: The iOS/Android clients always compute limbo document changes.
281284
applyChanges(
282285
docChanges: ViewDocumentChanges,
283-
updateLimboDocuments: boolean,
284-
targetChange?: TargetChange
286+
limboResolutionEnabled: boolean,
287+
targetChange?: TargetChange,
288+
targetIsPendingReset?: boolean
285289
): ViewChange {
286290
debugAssert(
287291
!docChanges.needsRefill,
@@ -300,10 +304,18 @@ export class View {
300304
});
301305

302306
this.applyTargetChange(targetChange);
303-
const limboChanges = updateLimboDocuments
304-
? this.updateLimboDocuments()
305-
: [];
306-
const synced = this.limboDocuments.size === 0 && this.current;
307+
308+
targetIsPendingReset = targetIsPendingReset ?? false;
309+
const limboChanges =
310+
limboResolutionEnabled && !targetIsPendingReset
311+
? this.updateLimboDocuments()
312+
: [];
313+
314+
// We are at synced state if there is no limbo docs are waiting to be resolved, view is current
315+
// with the backend, and the query is not pending to reset due to existence filter mismatch.
316+
const synced =
317+
this.limboDocuments.size === 0 && this.current && !targetIsPendingReset;
318+
307319
const newSyncState = synced ? SyncState.Synced : SyncState.Local;
308320
const syncStateChanged = newSyncState !== this.syncState;
309321
this.syncState = newSyncState;
@@ -350,7 +362,7 @@ export class View {
350362
mutatedKeys: this.mutatedKeys,
351363
needsRefill: false
352364
},
353-
/* updateLimboDocuments= */ false
365+
/* limboResolutionEnabled= */ false
354366
);
355367
} else {
356368
// No effect, just return a no-op ViewChange.
@@ -458,7 +470,7 @@ export class View {
458470
this._syncedDocuments = queryResult.remoteKeys;
459471
this.limboDocuments = documentKeySet();
460472
const docChanges = this.computeDocChanges(queryResult.documents);
461-
return this.applyChanges(docChanges, /*updateLimboDocuments=*/ true);
473+
return this.applyChanges(docChanges, /* limboResolutionEnabled= */ true);
462474
}
463475

464476
/**

packages/firestore/test/unit/core/view.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,19 +304,19 @@ describe('View', () => {
304304
let changes = view.computeDocChanges(documentUpdates(doc1));
305305
let viewChange = view.applyChanges(
306306
changes,
307-
/* updateLimboDocuments= */ true
307+
/* limboResolutionEnabled= */ true
308308
);
309309
expect(viewChange.snapshot!.fromCache).to.be.true;
310310

311311
// Add doc2 to generate a snapshot. Doc1 is still missing.
312312
changes = view.computeDocChanges(documentUpdates(doc2));
313-
viewChange = view.applyChanges(changes, /* updateLimboDocuments= */ true);
313+
viewChange = view.applyChanges(changes, /* limboResolutionEnabled= */ true);
314314
expect(viewChange.snapshot!.fromCache).to.be.true;
315315

316316
// Add doc2 to the backend's result set.
317317
viewChange = view.applyChanges(
318318
changes,
319-
/* updateLimboDocuments= */ true,
319+
/* limboResolutionEnabled= */ true,
320320
updateMapping(version(0), [doc2], [], [], /* current= */ true)
321321
);
322322
// We are CURRENT but doc1 is in limbo.
@@ -325,7 +325,7 @@ describe('View', () => {
325325
// Add doc1 to the backend's result set.
326326
viewChange = view.applyChanges(
327327
changes,
328-
/* updateLimboDocuments= */ true,
328+
/* limboResolutionEnabled= */ true,
329329
updateMapping(version(0), [doc1], [], [], /* current= */ true)
330330
);
331331
expect(viewChange.snapshot!.fromCache).to.be.false;

packages/firestore/test/unit/local/query_engine.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ function genericQueryEngineTest(
253253
const viewDocChanges = view.computeDocChanges(docs);
254254
return view.applyChanges(
255255
viewDocChanges,
256-
/*updateLimboDocuments=*/ true
256+
/* limboResolutionEnabled= */ true
257257
).snapshot!.docs;
258258
});
259259
});

0 commit comments

Comments
 (0)