Skip to content

Commit 100d85a

Browse files
Index-Free: Persist the lastLimboFreeSnapshot version
1 parent 330abde commit 100d85a

File tree

12 files changed

+151
-24
lines changed

12 files changed

+151
-24
lines changed

packages/firestore/src/core/view_snapshot.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,8 @@ export class ViewSnapshot {
168168
changes,
169169
mutatedKeys,
170170
fromCache,
171-
true,
172-
false
171+
/* syncStateChanged= */ true,
172+
/* excludesMetadataChanges= */ false
173173
);
174174
}
175175

packages/firestore/src/local/indexeddb_schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,12 @@ export class DbTarget {
786786
* listened to.
787787
*/
788788
public lastListenSequenceNumber: number,
789+
/**
790+
* Denotes the maximum snapshot version at which the associated query view
791+
* contained no limbo documents. Undefined for data written prior to
792+
* schema version 9.
793+
*/
794+
public lastLimboFreeSnapshotVersion: DbTimestamp | undefined,
789795
/**
790796
* The query for this target.
791797
*

packages/firestore/src/local/local_serializer.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,13 @@ export class LocalSerializer {
202202
/** Decodes a DbTarget into QueryData */
203203
fromDbTarget(dbTarget: DbTarget): QueryData {
204204
const version = this.fromDbTimestamp(dbTarget.readTime);
205+
const lastLimboFreeSnapshotVersion =
206+
dbTarget.lastLimboFreeSnapshotVersion !== undefined
207+
? this.fromDbTimestamp(dbTarget.lastLimboFreeSnapshotVersion)
208+
: SnapshotVersion.MIN;
209+
// TODO(b/140573486): Convert to platform representation
210+
const resumeToken = dbTarget.resumeToken;
211+
205212
let query: Query;
206213
if (isDocumentQuery(dbTarget.query)) {
207214
query = this.remoteSerializer.fromDocumentsTarget(dbTarget.query);
@@ -214,7 +221,8 @@ export class LocalSerializer {
214221
QueryPurpose.Listen,
215222
dbTarget.lastListenSequenceNumber,
216223
version,
217-
dbTarget.resumeToken
224+
lastLimboFreeSnapshotVersion,
225+
resumeToken
218226
);
219227
}
220228

@@ -228,6 +236,9 @@ export class LocalSerializer {
228236
queryData.purpose
229237
);
230238
const dbTimestamp = this.toDbTimestamp(queryData.snapshotVersion);
239+
const dbLastLimboFreeTimestamp = this.toDbTimestamp(
240+
queryData.lastLimboFreeSnapshotVersion
241+
);
231242
let queryProto: DbQuery;
232243
if (queryData.query.isDocumentQuery()) {
233244
queryProto = this.remoteSerializer.toDocumentsTarget(queryData.query);
@@ -255,6 +266,7 @@ export class LocalSerializer {
255266
dbTimestamp,
256267
resumeToken,
257268
queryData.sequenceNumber,
269+
dbLastLimboFreeTimestamp,
258270
queryProto
259271
);
260272
}

packages/firestore/src/local/local_store.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,8 @@ export class LocalStore {
658658
return PersistencePromise.forEach(
659659
viewChanges,
660660
(viewChange: LocalViewChanges) => {
661+
const targetId = viewChange.targetId;
662+
661663
this.localViewReferences.addReferences(
662664
viewChange.addedKeys,
663665
viewChange.targetId
@@ -666,6 +668,21 @@ export class LocalStore {
666668
viewChange.removedKeys,
667669
viewChange.targetId
668670
);
671+
672+
if (viewChange.fromCache) {
673+
const queryData = this.queryDataByTarget[targetId];
674+
assert(
675+
queryData !== undefined,
676+
`Can't set limbo-free snapshot version for unknown target: ${targetId}`
677+
);
678+
679+
// Advance the last limbo free snapshot version
680+
const lastLimboFreeSnapshotVersion = queryData.snapshotVersion;
681+
const updatedQueryData = queryData.withLastLimboFreeSnapshotVersion(
682+
lastLimboFreeSnapshotVersion
683+
);
684+
this.queryDataByTarget[targetId] = updatedQueryData;
685+
}
669686
return PersistencePromise.forEach(
670687
viewChange.removedKeys,
671688
(key: DocumentKey) =>

packages/firestore/src/local/local_view_changes.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { documentKeySet, DocumentKeySet } from '../model/collections';
2727
export class LocalViewChanges {
2828
constructor(
2929
readonly targetId: TargetId,
30+
readonly fromCache: boolean,
3031
readonly addedKeys: DocumentKeySet,
3132
readonly removedKeys: DocumentKeySet
3233
) {}
@@ -51,6 +52,11 @@ export class LocalViewChanges {
5152
}
5253
}
5354

54-
return new LocalViewChanges(targetId, addedKeys, removedKeys);
55+
return new LocalViewChanges(
56+
targetId,
57+
viewSnapshot.fromCache,
58+
addedKeys,
59+
removedKeys
60+
);
5561
}
5662
}

packages/firestore/src/local/query_data.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,18 @@ export class QueryData {
4848
readonly targetId: TargetId,
4949
/** The purpose of the query. */
5050
readonly purpose: QueryPurpose,
51-
/** The sequence number of the last transaction during which this query data was modified */
51+
/**
52+
* The sequence number of the last transaction during which this query data
53+
* was modified.
54+
*/
5255
readonly sequenceNumber: ListenSequenceNumber,
5356
/** The latest snapshot version seen for this target. */
5457
readonly snapshotVersion: SnapshotVersion = SnapshotVersion.MIN,
58+
/**
59+
* The maximum snapshot version at which the associated query view
60+
* contained no limbo documents.
61+
*/
62+
readonly lastLimboFreeSnapshotVersion: SnapshotVersion = SnapshotVersion.MIN,
5563
/**
5664
* An opaque, server-assigned token that allows watching a query to be
5765
* resumed after disconnecting without retransmitting all the data that
@@ -69,6 +77,7 @@ export class QueryData {
6977
this.purpose,
7078
sequenceNumber,
7179
this.snapshotVersion,
80+
this.lastLimboFreeSnapshotVersion,
7281
this.resumeToken
7382
);
7483
}
@@ -87,16 +96,38 @@ export class QueryData {
8796
this.purpose,
8897
this.sequenceNumber,
8998
snapshotVersion,
99+
this.lastLimboFreeSnapshotVersion,
90100
resumeToken
91101
);
92102
}
93103

104+
/**
105+
* Creates a new query data instance with an updated last limbo free
106+
* snapshot version number.
107+
*/
108+
withLastLimboFreeSnapshotVersion(
109+
lastLimboFreeSnapshotVersion: SnapshotVersion
110+
): QueryData {
111+
return new QueryData(
112+
this.query,
113+
this.targetId,
114+
this.purpose,
115+
this.sequenceNumber,
116+
this.snapshotVersion,
117+
lastLimboFreeSnapshotVersion,
118+
this.resumeToken
119+
);
120+
}
121+
94122
isEqual(other: QueryData): boolean {
95123
return (
96124
this.targetId === other.targetId &&
97125
this.purpose === other.purpose &&
98126
this.sequenceNumber === other.sequenceNumber &&
99127
this.snapshotVersion.isEqual(other.snapshotVersion) &&
128+
this.lastLimboFreeSnapshotVersion.isEqual(
129+
other.lastLimboFreeSnapshotVersion
130+
) &&
100131
this.resumeToken === other.resumeToken &&
101132
this.query.isEqual(other.query)
102133
);

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,44 @@ describe('View', () => {
290290
);
291291
});
292292

293+
it('is marked from cache with limbo documents', () => {
294+
const query = Query.atPath(path('rooms/eros/msgs'));
295+
const view = new View(query, documentKeySet());
296+
const doc1 = doc('rooms/eros/msgs/0', 0, {});
297+
const doc2 = doc('rooms/eros/msgs/1', 0, {});
298+
299+
// Doc1 is contained in the local view, but we are not yet CURRENT so it is
300+
// expected that the backend hasn't told us about all documents yet.
301+
let changes = view.computeDocChanges(documentUpdates(doc1));
302+
let viewChange = view.applyChanges(
303+
changes,
304+
/* updateLimboDocuments= */ true
305+
);
306+
expect(viewChange.snapshot!.fromCache).to.be.true;
307+
308+
// Add doc2 to generate a snapshot. Doc1 is still missing.
309+
changes = view.computeDocChanges(documentUpdates(doc2));
310+
viewChange = view.applyChanges(changes, /* updateLimboDocuments= */ true);
311+
expect(viewChange.snapshot!.fromCache).to.be.true;
312+
313+
// Add doc2 to the backend's result set.
314+
viewChange = view.applyChanges(
315+
changes,
316+
/* updateLimboDocuments= */ true,
317+
updateMapping(version(0), [doc2], [], [], /* current= */ true)
318+
);
319+
// We are CURRENT but doc1 is in limbo.
320+
expect(viewChange.snapshot!.fromCache).to.be.true;
321+
322+
// Add doc1 to the backend's result set.
323+
viewChange = view.applyChanges(
324+
changes,
325+
/* updateLimboDocuments= */ true,
326+
updateMapping(version(0), [doc1], [], [], /* current= */ true)
327+
);
328+
expect(viewChange.snapshot!.fromCache).to.be.false;
329+
});
330+
293331
it('resumes queries without creating limbo documents', () => {
294332
const query = Query.atPath(path('rooms/eros/msgs'));
295333
const doc1 = doc('rooms/eros/msgs/0', 0, {});

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -844,13 +844,21 @@ function genericLocalStoreTests(
844844
.after(setMutation('foo/baz', { foo: 'baz' }))
845845
.toContain(doc('foo/bar', 1, { foo: 'bar' }))
846846
.toContain(doc('foo/baz', 0, { foo: 'baz' }, { hasLocalMutations: true }))
847-
.after(localViewChanges(2, { added: ['foo/bar', 'foo/baz'] }))
847+
.after(
848+
localViewChanges(2, /* fromCache= */ false, {
849+
added: ['foo/bar', 'foo/baz']
850+
})
851+
)
848852
.after(docUpdateRemoteEvent(doc('foo/bar', 1, { foo: 'bar' }), [], [2]))
849853
.after(docUpdateRemoteEvent(doc('foo/baz', 2, { foo: 'baz' }), [2]))
850854
.afterAcknowledgingMutation({ documentVersion: 2 })
851855
.toContain(doc('foo/bar', 1, { foo: 'bar' }))
852856
.toContain(doc('foo/baz', 2, { foo: 'baz' }))
853-
.after(localViewChanges(2, { removed: ['foo/bar', 'foo/baz'] }))
857+
.after(
858+
localViewChanges(2, /* fromCache= */ false, {
859+
removed: ['foo/bar', 'foo/baz']
860+
})
861+
)
854862
.afterReleasingQuery(query)
855863
.toNotContain('foo/bar')
856864
.toNotContain('foo/baz')

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

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,20 @@ describe('IndexedDbQueryCache', () => {
6262
const originalSequenceNumber = 1234;
6363
const targetId = 5;
6464
const snapshotVersion = SnapshotVersion.fromTimestamp(new Timestamp(1, 2));
65+
const lastLimboFreeSnapshotVersion = SnapshotVersion.fromTimestamp(
66+
new Timestamp(3, 4)
67+
);
6568
const query = Query.atPath(path('rooms'));
66-
await queryCache1.addQueryData(
67-
new QueryData(
68-
query,
69-
targetId,
70-
QueryPurpose.Listen,
71-
originalSequenceNumber,
72-
snapshotVersion
73-
)
69+
const queryData = new QueryData(
70+
query,
71+
targetId,
72+
QueryPurpose.Listen,
73+
originalSequenceNumber,
74+
snapshotVersion,
75+
lastLimboFreeSnapshotVersion
7476
);
77+
78+
await queryCache1.addQueryData(queryData);
7579
// Snapshot version needs to be set separately
7680
await queryCache1.setTargetsMetadata(
7781
originalSequenceNumber,
@@ -86,6 +90,14 @@ describe('IndexedDbQueryCache', () => {
8690
expect(await queryCache2.getHighestSequenceNumber()).to.equal(
8791
originalSequenceNumber
8892
);
93+
const actualQueryData = await queryCache2.getQueryData(query);
94+
95+
if (process.env.USE_MOCK_PERSISTENCE !== 'YES') {
96+
// TODO(b/140573486): This fails on Node with persistence since the
97+
// resume token is read back as a string.
98+
expect(queryData.isEqual(actualQueryData!)).to.be.true;
99+
}
100+
89101
const actualSnapshotVersion = await queryCache2.getLastRemoteSnapshotVersion();
90102
expect(snapshotVersion.isEqual(actualSnapshotVersion)).to.be.true;
91103
await db2.shutdown();
@@ -127,6 +139,7 @@ function genericQueryCacheTests(
127139
QueryPurpose.Listen,
128140
++previousSequenceNumber,
129141
snapshotVersion,
142+
/* lastLimboFreeSnapshotVersion= */ SnapshotVersion.MIN,
130143
resumeToken
131144
);
132145
}

packages/firestore/test/unit/remote/node/serializer.test.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,7 @@ describe('Serializer', () => {
105105
* variations on Query.
106106
*/
107107
function wrapQueryData(query: Query): QueryData {
108-
return new QueryData(
109-
query,
110-
1,
111-
QueryPurpose.Listen,
112-
2,
113-
SnapshotVersion.MIN,
114-
emptyResumeToken
115-
);
108+
return new QueryData(query, 1, QueryPurpose.Listen, 2);
116109
}
117110

118111
describe('converts value', () => {
@@ -1196,6 +1189,7 @@ describe('Serializer', () => {
11961189
QueryPurpose.Listen,
11971190
4,
11981191
SnapshotVersion.MIN,
1192+
SnapshotVersion.MIN,
11991193
new Uint8Array([1, 2, 3])
12001194
)
12011195
);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,7 @@ abstract class TestRunner {
10721072
QueryPurpose.Listen,
10731073
ARBITRARY_SEQUENCE_NUMBER,
10741074
SnapshotVersion.MIN,
1075+
SnapshotVersion.MIN,
10751076
expected.resumeToken
10761077
)
10771078
);

packages/firestore/test/util/helpers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ export function limboChanges(changes: {
435435

436436
export function localViewChanges(
437437
targetId: TargetId,
438+
fromCache: boolean,
438439
changes: { added?: string[]; removed?: string[] }
439440
): LocalViewChanges {
440441
if (!changes.added) {
@@ -453,7 +454,7 @@ export function localViewChanges(
453454
keyStr => (removedKeys = removedKeys.add(key(keyStr)))
454455
);
455456

456-
return new LocalViewChanges(targetId, addedKeys, removedKeys);
457+
return new LocalViewChanges(targetId, fromCache, addedKeys, removedKeys);
457458
}
458459

459460
/** Creates a resume token to match the given snapshot version. */

0 commit comments

Comments
 (0)