@@ -73,27 +73,6 @@ export class FieldMask {
73
73
return found ;
74
74
}
75
75
76
- /**
77
- * Applies this field mask to the provided object value and returns an object
78
- * that only contains fields that are specified in both the input object and
79
- * this field mask.
80
- */
81
- applyTo ( data : ObjectValue ) : ObjectValue {
82
- let filteredObject = ObjectValue . EMPTY ;
83
- this . fields . forEach ( fieldMaskPath => {
84
- if ( fieldMaskPath . isEmpty ( ) ) {
85
- return data ;
86
- } else {
87
- const newValue = data . field ( fieldMaskPath ) ;
88
- if ( newValue !== null ) {
89
- filteredObject = filteredObject . set ( fieldMaskPath , newValue ) ;
90
- }
91
- }
92
- } ) ;
93
-
94
- return filteredObject ;
95
- }
96
-
97
76
isEqual ( other : FieldMask ) : boolean {
98
77
return this . fields . isEqual ( other . fields ) ;
99
78
}
@@ -106,11 +85,6 @@ export class FieldTransform {
106
85
readonly transform : TransformOperation
107
86
) { }
108
87
109
- /** Whether this field transform is idempotent. */
110
- get isIdempotent ( ) : boolean {
111
- return this . transform . isIdempotent ;
112
- }
113
-
114
88
isEqual ( other : FieldTransform ) : boolean {
115
89
return (
116
90
this . field . isEqual ( other . field ) && this . transform . isEqual ( other . transform )
@@ -302,17 +276,25 @@ export abstract class Mutation {
302
276
localWriteTime : Timestamp
303
277
) : MaybeDocument | null ;
304
278
305
- abstract isEqual ( other : Mutation ) : boolean ;
306
-
307
279
/**
308
- * If applicable, returns the field mask for this mutation. Fields that are
309
- * not included in this field mask are not modified when this mutation is
310
- * applied. Mutations that replace the entire document return 'null'.
280
+ * If this mutation is not idempotent, returns the base value to persist with
281
+ * this mutation. If a base value is returned, the mutation is always applied
282
+ * to this base value, even if document has already been updated.
283
+ *
284
+ * The base value is a sparse object that consists of only the document
285
+ * fields for which this mutation contains a non-idempotent transformation
286
+ * (e.g. a numeric increment). The provided alue guarantees consistent
287
+ * behavior for non-idempotent transforms and allow us to return the same
288
+ * latency-compensated value even if the backend has already applied the
289
+ * mutation. The base value is null for idempotent mutations, as they can be
290
+ * re-played even if the backend has already applied them.
291
+ *
292
+ * @return a base value to store along with the mutation, or null for
293
+ * idempotent mutations.
311
294
*/
312
- abstract get fieldMask ( ) : FieldMask | null ;
295
+ abstract extractBaseValue ( maybeDoc : MaybeDocument | null ) : ObjectValue | null ;
313
296
314
- /** Returns whether all operations in the mutation are idempotent. */
315
- abstract get isIdempotent ( ) : boolean ;
297
+ abstract isEqual ( other : Mutation ) : boolean ;
316
298
317
299
protected verifyKeyMatches ( maybeDoc : MaybeDocument | null ) : void {
318
300
if ( maybeDoc != null ) {
@@ -393,11 +375,7 @@ export class SetMutation extends Mutation {
393
375
} ) ;
394
376
}
395
377
396
- get isIdempotent ( ) : true {
397
- return true ;
398
- }
399
-
400
- get fieldMask ( ) : null {
378
+ extractBaseValue ( maybeDoc : MaybeDocument | null ) : null {
401
379
return null ;
402
380
}
403
381
@@ -479,8 +457,8 @@ export class PatchMutation extends Mutation {
479
457
} ) ;
480
458
}
481
459
482
- get isIdempotent ( ) : true {
483
- return true ;
460
+ extractBaseValue ( maybeDoc : MaybeDocument | null ) : null {
461
+ return null ;
484
462
}
485
463
486
464
isEqual ( other : Mutation ) : boolean {
@@ -592,6 +570,7 @@ export class TransformMutation extends Mutation {
592
570
const doc = this . requireDocument ( maybeDoc ) ;
593
571
const transformResults = this . localTransformResults (
594
572
localWriteTime ,
573
+ maybeDoc ,
595
574
baseDoc
596
575
) ;
597
576
const newData = this . transformObject ( doc . data , transformResults ) ;
@@ -600,22 +579,29 @@ export class TransformMutation extends Mutation {
600
579
} ) ;
601
580
}
602
581
603
- get isIdempotent ( ) : boolean {
582
+ extractBaseValue ( maybeDoc : MaybeDocument | null ) : ObjectValue | null {
583
+ let baseObject : ObjectValue | null = null ;
604
584
for ( const fieldTransform of this . fieldTransforms ) {
605
- if ( ! fieldTransform . isIdempotent ) {
606
- return false ;
585
+ const existingValue =
586
+ maybeDoc instanceof Document
587
+ ? maybeDoc . field ( fieldTransform . field )
588
+ : undefined ;
589
+ const coercedValue = fieldTransform . transform . computeBaseValue (
590
+ existingValue || null
591
+ ) ;
592
+
593
+ if ( coercedValue != null ) {
594
+ if ( baseObject == null ) {
595
+ baseObject = ObjectValue . EMPTY . set (
596
+ fieldTransform . field ,
597
+ coercedValue
598
+ ) ;
599
+ } else {
600
+ baseObject = baseObject . set ( fieldTransform . field , coercedValue ) ;
601
+ }
607
602
}
608
603
}
609
-
610
- return true ;
611
- }
612
-
613
- get fieldMask ( ) : FieldMask {
614
- let fieldMask = new SortedSet < FieldPath > ( FieldPath . comparator ) ;
615
- this . fieldTransforms . forEach (
616
- transform => ( fieldMask = fieldMask . add ( transform . field ) )
617
- ) ;
618
- return new FieldMask ( fieldMask ) ;
604
+ return baseObject ;
619
605
}
620
606
621
607
isEqual ( other : Mutation ) : boolean {
@@ -690,19 +676,30 @@ export class TransformMutation extends Mutation {
690
676
*
691
677
* @param localWriteTime The local time of the transform mutation (used to
692
678
* generate ServerTimestampValues).
679
+ * @param maybeDoc The current state of the document after applying all
680
+ * previous mutations.
693
681
* @param baseDoc The document prior to applying this mutation batch.
694
682
* @return The transform results list.
695
683
*/
696
684
private localTransformResults (
697
685
localWriteTime : Timestamp ,
686
+ maybeDoc : MaybeDocument | null ,
698
687
baseDoc : MaybeDocument | null
699
688
) : FieldValue [ ] {
700
689
const transformResults = [ ] as FieldValue [ ] ;
701
690
for ( const fieldTransform of this . fieldTransforms ) {
702
691
const transform = fieldTransform . transform ;
703
692
704
693
let previousValue : FieldValue | null = null ;
705
- if ( baseDoc instanceof Document ) {
694
+ if ( maybeDoc instanceof Document ) {
695
+ previousValue = maybeDoc . field ( fieldTransform . field ) ;
696
+ }
697
+
698
+ if ( previousValue === null && baseDoc instanceof Document ) {
699
+ // If the current document does not contain a value for the mutated
700
+ // field, use the value that existed before applying this mutation
701
+ // batch. This solves an edge case where a PatchMutation clears the
702
+ // values in a nested map before the TransformMutation is applied.
706
703
previousValue = baseDoc . field ( fieldTransform . field ) ;
707
704
}
708
705
@@ -779,19 +776,15 @@ export class DeleteMutation extends Mutation {
779
776
return new NoDocument ( this . key , SnapshotVersion . forDeletedDoc ( ) ) ;
780
777
}
781
778
779
+ extractBaseValue ( maybeDoc : MaybeDocument | null ) : null {
780
+ return null ;
781
+ }
782
+
782
783
isEqual ( other : Mutation ) : boolean {
783
784
return (
784
785
other instanceof DeleteMutation &&
785
786
this . key . isEqual ( other . key ) &&
786
787
this . precondition . isEqual ( other . precondition )
787
788
) ;
788
789
}
789
-
790
- get isIdempotent ( ) : true {
791
- return true ;
792
- }
793
-
794
- get fieldMask ( ) : null {
795
- return null ;
796
- }
797
790
}
0 commit comments