Skip to content

Commit 4993479

Browse files
Merge UnknownDocument
1 parent c1c3063 commit 4993479

File tree

11 files changed

+207
-103
lines changed

11 files changed

+207
-103
lines changed

packages/firestore/src/local/indexeddb_schema.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,14 @@ export class DbNoDocument {
425425
constructor(public path: string[], public readTime: DbTimestamp) {}
426426
}
427427

428+
/**
429+
* Represents a document that is know to exist but whose data is unknwon.
430+
* Stored in IndexedDb as part of a DbRemoteDocument object.
431+
*/
432+
export class DbUnknownDocument {
433+
constructor(public path: string[], public readTime: DbTimestamp) {}
434+
}
435+
428436
/**
429437
* An object to be stored in the 'remoteDocuments' store in IndexedDb. It
430438
* represents either a cached document (if it exists) or a cached "no-document"
@@ -437,6 +445,12 @@ export class DbRemoteDocument {
437445
static store = 'remoteDocuments';
438446

439447
constructor(
448+
/**
449+
* Set to an instance of DbUnknownDocument if the data for a document is
450+
* not known, but yet we no the document exist (e.g. it had a successful
451+
* update applied to it)
452+
*/
453+
public unknownDocument: DbUnknownDocument | null,
440454
/**
441455
* Set to an instance of a DbNoDocument if it is known that no document
442456
* exists.

packages/firestore/src/local/local_serializer.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ import * as api from '../protos/firestore_proto_api';
1818
import { Timestamp } from '../api/timestamp';
1919
import { Query } from '../core/query';
2020
import { SnapshotVersion } from '../core/snapshot_version';
21-
import { Document, MaybeDocument, NoDocument } from '../model/document';
21+
import {
22+
Document,
23+
MaybeDocument,
24+
NoDocument,
25+
UnknownDocument
26+
} from '../model/document';
2227
import { DocumentKey } from '../model/document_key';
2328
import { MutationBatch } from '../model/mutation_batch';
2429
import { JsonProtoSerializer } from '../remote/serializer';
@@ -30,7 +35,8 @@ import {
3035
DbQuery,
3136
DbRemoteDocument,
3237
DbTarget,
33-
DbTimestamp
38+
DbTimestamp,
39+
DbUnknownDocument
3440
} from './indexeddb_schema';
3541
import { QueryData, QueryPurpose } from './query_data';
3642
import { decode, encode, EncodedResourcePath } from './encoded_resource_path';
@@ -51,6 +57,10 @@ export class LocalSerializer {
5157
const key = DocumentKey.fromSegments(remoteDoc.noDocument.path);
5258
const version = this.fromDbTimestamp(remoteDoc.noDocument.readTime);
5359
return new NoDocument(key, version);
60+
} else if (remoteDoc.unknownDocument) {
61+
const key = DocumentKey.fromSegments(remoteDoc.unknownDocument.path);
62+
const version = this.fromDbTimestamp(remoteDoc.unknownDocument.readTime);
63+
return new UnknownDocument(key, version);
5464
} else {
5565
return fail('Unexpected DbRemoteDocument');
5666
}
@@ -61,11 +71,27 @@ export class LocalSerializer {
6171
if (maybeDoc instanceof Document) {
6272
const doc = this.remoteSerializer.toDocument(maybeDoc);
6373
const hasCommittedMutations = maybeDoc.hasCommittedMutations;
64-
return new DbRemoteDocument(null, doc, hasCommittedMutations);
65-
} else {
74+
return new DbRemoteDocument(null, null, doc, hasCommittedMutations);
75+
} else if (maybeDoc instanceof NoDocument) {
6676
const path = maybeDoc.key.path.toArray();
6777
const readTime = this.toDbTimestamp(maybeDoc.version);
68-
return new DbRemoteDocument(new DbNoDocument(path, readTime), null);
78+
return new DbRemoteDocument(
79+
null,
80+
new DbNoDocument(path, readTime),
81+
null,
82+
/* hasCommittedMutations= */ false
83+
);
84+
} else if (maybeDoc instanceof UnknownDocument) {
85+
const path = maybeDoc.key.path.toArray();
86+
const readTime = this.toDbTimestamp(maybeDoc.version);
87+
return new DbRemoteDocument(
88+
new DbUnknownDocument(path, readTime),
89+
null,
90+
null,
91+
/* hasCommittedMutations= */ false
92+
);
93+
} else {
94+
return fail('Unexpected MaybeDocumment');
6995
}
7096
}
7197

packages/firestore/src/local/local_store.ts

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
DocumentMap,
2626
MaybeDocumentMap
2727
} from '../model/collections';
28-
import { MaybeDocument } from '../model/document';
28+
import { MaybeDocument, UnknownDocument } from '../model/document';
2929
import { DocumentKey } from '../model/document_key';
3030
import { Mutation } from '../model/mutation';
3131
import {
@@ -498,30 +498,13 @@ export class LocalStore {
498498
const documentBuffer = new RemoteDocumentChangeBuffer(this.remoteDocuments);
499499
return this.persistence.runTransaction('Apply remote event', true, txn => {
500500
const promises = [] as Array<PersistencePromise<void>>;
501-
let authoritativeUpdates = documentKeySet();
502501
objUtils.forEachNumber(
503502
remoteEvent.targetChanges,
504503
(targetId: TargetId, change: TargetChange) => {
505504
// Do not ref/unref unassigned targetIds - it may lead to leaks.
506505
let queryData = this.queryDataByTarget[targetId];
507506
if (!queryData) return;
508507

509-
// When a global snapshot contains updates (either add or modify) we
510-
// can completely trust these updates as authoritative and blindly
511-
// apply them to our cache (as a defensive measure to promote
512-
// self-healing in the unfortunate case that our cache is ever somehow
513-
// corrupted / out-of-sync).
514-
//
515-
// If the document is only updated while removing it from a target
516-
// then watch isn't obligated to send the absolute latest version: it
517-
// can send the first version that caused the document not to match.
518-
change.addedDocuments.forEach(key => {
519-
authoritativeUpdates = authoritativeUpdates.add(key);
520-
});
521-
change.modifiedDocuments.forEach(key => {
522-
authoritativeUpdates = authoritativeUpdates.add(key);
523-
});
524-
525508
promises.push(
526509
this.queryCache
527510
.removeMatchingKeys(txn, change.removedDocuments, targetId)
@@ -559,15 +542,9 @@ export class LocalStore {
559542
changedDocKeys = changedDocKeys.add(key);
560543
promises.push(
561544
documentBuffer.getEntry(txn, key).next(existingDoc => {
562-
// If a document update isn't authoritative, make sure we don't
563-
// apply an old document version to the remote cache. We make an
564-
// exception for SnapshotVersion.MIN which can happen for
565-
// manufactured events (e.g. in the case of a limbo document
566-
// resolution failing).
567545
if (
568546
existingDoc == null ||
569547
doc.version.isEqual(SnapshotVersion.MIN) ||
570-
authoritativeUpdates.has(doc.key) ||
571548
doc.version.compareTo(existingDoc.version) >= 0
572549
) {
573550
documentBuffer.addEntry(doc);

packages/firestore/src/model/document.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export abstract class MaybeDocument {
3737
static compareByKey(d1: MaybeDocument, d2: MaybeDocument): number {
3838
return DocumentKey.comparator(d1.key, d2.key);
3939
}
40+
41+
abstract isEqual(other: MaybeDocument | null | undefined): boolean;
4042
}
4143

4244
/**
@@ -71,7 +73,7 @@ export class Document extends MaybeDocument {
7173
return this.data.value();
7274
}
7375

74-
isEqual(other: Document | null | undefined): boolean {
76+
isEqual(other: MaybeDocument | null | undefined): boolean {
7577
return (
7678
other instanceof Document &&
7779
this.key.isEqual(other.key) &&
@@ -115,9 +117,31 @@ export class NoDocument extends MaybeDocument {
115117
return `NoDocument(${this.key}, ${this.version})`;
116118
}
117119

118-
isEqual(other: NoDocument): boolean {
120+
isEqual(other: MaybeDocument | null | undefined): boolean {
121+
return (
122+
other instanceof NoDocument &&
123+
other.version.isEqual(this.version) &&
124+
other.key.isEqual(this.key)
125+
);
126+
}
127+
}
128+
129+
/**
130+
* A class representing a document whose data is unknown (e.g. a document that
131+
* was updated without a known base version).
132+
*/
133+
export class UnknownDocument extends MaybeDocument {
134+
constructor(key: DocumentKey, version: SnapshotVersion) {
135+
super(key, version);
136+
}
137+
138+
toString(): string {
139+
return `UnknownDocument(${this.key}, ${this.version})`;
140+
}
141+
142+
isEqual(other: MaybeDocument | null | undefined): boolean {
119143
return (
120-
other &&
144+
other instanceof UnknownDocument &&
121145
other.version.isEqual(this.version) &&
122146
other.key.isEqual(this.key)
123147
);

packages/firestore/src/model/mutation.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ import { SnapshotVersion } from '../core/snapshot_version';
1919
import { assert } from '../util/assert';
2020
import * as misc from '../util/misc';
2121

22-
import { Document, MaybeDocument, NoDocument } from './document';
22+
import {
23+
Document,
24+
MaybeDocument,
25+
NoDocument,
26+
UnknownDocument
27+
} from './document';
2328
import { DocumentKey } from './document_key';
2429
import { FieldValue, ObjectValue } from './field_value';
2530
import { FieldPath } from './path';
@@ -221,7 +226,7 @@ export abstract class Mutation {
221226
abstract applyToRemoteDocument(
222227
maybeDoc: MaybeDocument | null,
223228
mutationResult: MutationResult
224-
): MaybeDocument | null;
229+
): MaybeDocument;
225230

226231
/**
227232
* Applies this mutation to the given MaybeDocument or null for the purposes
@@ -290,7 +295,7 @@ export class SetMutation extends Mutation {
290295
applyToRemoteDocument(
291296
maybeDoc: MaybeDocument | null,
292297
mutationResult: MutationResult
293-
): MaybeDocument | null {
298+
): MaybeDocument {
294299
this.verifyKeyMatches(maybeDoc);
295300

296301
assert(
@@ -363,25 +368,26 @@ export class PatchMutation extends Mutation {
363368
applyToRemoteDocument(
364369
maybeDoc: MaybeDocument | null,
365370
mutationResult: MutationResult
366-
): MaybeDocument | null {
371+
): MaybeDocument {
367372
this.verifyKeyMatches(maybeDoc);
368373

369374
assert(
370375
mutationResult.transformResults == null,
371376
'Transform results received by PatchMutation.'
372377
);
373378

379+
const version = mutationResult.version;
380+
374381
// TODO(mcg): Relax enforcement of this precondition
375382
//
376383
// We shouldn't actually enforce the precondition since it already passed on
377384
// the backend, but we may not have a local version of the document to
378385
// patch, so we use the precondition to prevent incorrectly putting a
379386
// partial document into our cache.
380387
if (!this.precondition.isValidFor(maybeDoc)) {
381-
return maybeDoc;
388+
return new UnknownDocument(this.key, version);
382389
}
383390

384-
const version = mutationResult.version;
385391
const newData = this.patchDocument(maybeDoc);
386392
return new Document(this.key, version, newData, {
387393
hasCommittedMutations: true
@@ -470,7 +476,7 @@ export class TransformMutation extends Mutation {
470476
applyToRemoteDocument(
471477
maybeDoc: MaybeDocument | null,
472478
mutationResult: MutationResult
473-
): MaybeDocument | null {
479+
): MaybeDocument {
474480
this.verifyKeyMatches(maybeDoc);
475481

476482
assert(
@@ -485,7 +491,7 @@ export class TransformMutation extends Mutation {
485491
// patch, so we use the precondition to prevent incorrectly putting a
486492
// partial document into our cache.
487493
if (!this.precondition.isValidFor(maybeDoc)) {
488-
return maybeDoc;
494+
return new UnknownDocument(this.key, mutationResult.version);
489495
}
490496

491497
const doc = this.requireDocument(maybeDoc);
@@ -646,7 +652,7 @@ export class DeleteMutation extends Mutation {
646652
applyToRemoteDocument(
647653
maybeDoc: MaybeDocument | null,
648654
mutationResult: MutationResult
649-
): MaybeDocument | null {
655+
): MaybeDocument {
650656
this.verifyKeyMatches(maybeDoc);
651657

652658
assert(
@@ -658,7 +664,7 @@ export class DeleteMutation extends Mutation {
658664
// document the server has accepted the mutation so the precondition must
659665
// have held.
660666

661-
return new NoDocument(this.key, SnapshotVersion.MIN);
667+
return new NoDocument(this.key, mutationResult.version);
662668
}
663669

664670
applyToLocalView(

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@ import {
2424
apiDescribe,
2525
withTestCollection,
2626
withTestDb,
27+
withTestDbs,
2728
withTestDoc,
2829
withTestDocAndInitialData
2930
} from '../util/helpers';
3031
import { query } from '../../util/api_helpers';
32+
import { fail } from '../../../src/util/assert';
33+
import { Code } from '../../../src/util/error';
3134

3235
const Timestamp = firebase.firestore!.Timestamp;
3336
const FieldValue = firebase.firestore!.FieldValue;
@@ -108,6 +111,30 @@ apiDescribe('Database', persistence => {
108111
});
109112
});
110113

114+
(persistence ? it : it.skip)('can update an unknown document', () => {
115+
return withTestDbs(persistence, 2, async ([reader, writer]) => {
116+
const writerRef = writer.collection('collection').doc();
117+
const readerRef = reader.collection('collection').doc(writerRef.id);
118+
await writerRef.set({ a: 'a' });
119+
await readerRef.update({ b: 'b' });
120+
await writerRef
121+
.get({ source: 'cache' })
122+
.then(doc => expect(doc.exists).to.be.true);
123+
await readerRef
124+
.get({ source: 'cache' })
125+
.then(
126+
() => fail('Expected cache miss'),
127+
err => expect(err.code).to.be.equal(Code.UNAVAILABLE)
128+
);
129+
await writerRef
130+
.get()
131+
.then(doc => expect(doc.data()).to.deep.equal({ a: 'a', b: 'b' }));
132+
await readerRef
133+
.get()
134+
.then(doc => expect(doc.data()).to.deep.equal({ a: 'a', b: 'b' }));
135+
});
136+
});
137+
111138
it('can merge data with an existing document using set', () => {
112139
return withTestDoc(persistence, doc => {
113140
const initialData = {

0 commit comments

Comments
 (0)