@@ -25,45 +25,21 @@ import { forEach } from '../util/obj';
25
25
import { FieldMask } from './field_mask' ;
26
26
import { FieldPath } from './path' ;
27
27
import { isServerTimestamp } from './server_timestamps' ;
28
- import { TypeOrder } from './type_order' ;
29
- import { isMapValue , typeOrder , valueEquals } from './values' ;
28
+ import { deepClone , isMapValue , valueEquals } from './values' ;
30
29
31
30
export interface JsonObject < T > {
32
31
[ name : string ] : T ;
33
32
}
34
-
35
- /**
36
- * An Overlay, which contains an update to apply. Can either be Value proto, a
37
- * map of Overlay values (to represent additional nesting at the given key) or
38
- * `null` (to represent field deletes).
39
- */
40
- type Overlay = Map < string , Overlay > | ProtoValue | null ;
41
-
42
33
/**
43
34
* An ObjectValue represents a MapValue in the Firestore Proto and offers the
44
35
* ability to add and remove fields (via the ObjectValueBuilder).
45
36
*/
46
37
export class ObjectValue {
47
- /**
48
- * The immutable Value proto for this object. Local mutations are stored in
49
- * `overlayMap` and only applied when `buildProto()` is invoked.
50
- */
51
- private partialValue : { mapValue : ProtoMapValue } ;
52
-
53
- /**
54
- * A nested map that contains the accumulated changes that haven't yet been
55
- * applied to `partialValue`. Values can either be `Value` protos, Map<String,
56
- * Object> values (to represent additional nesting) or `null` (to represent
57
- * field deletes).
58
- */
59
- private overlayMap = new Map < string , Overlay > ( ) ;
60
-
61
- constructor ( proto : { mapValue : ProtoMapValue } ) {
38
+ constructor ( readonly value : { mapValue : ProtoMapValue } ) {
62
39
debugAssert (
63
- ! isServerTimestamp ( proto ) ,
40
+ ! isServerTimestamp ( value ) ,
64
41
'ServerTimestamps should be converted to ServerTimestampValue'
65
42
) ;
66
- this . partialValue = proto ;
67
43
}
68
44
69
45
static empty ( ) : ObjectValue {
@@ -77,12 +53,19 @@ export class ObjectValue {
77
53
* @returns The value at the path or null if the path is not set.
78
54
*/
79
55
field ( path : FieldPath ) : ProtoValue | null {
80
- return ObjectValue . extractNestedValue ( this . buildProto ( ) , path ) ;
81
- }
82
-
83
- /** Returns the full protobuf representation. */
84
- toProto ( ) : { mapValue : ProtoMapValue } {
85
- return this . field ( FieldPath . emptyPath ( ) ) as { mapValue : ProtoMapValue } ;
56
+ if ( path . isEmpty ( ) ) {
57
+ return this . value ;
58
+ } else {
59
+ let currentLevel : ProtoValue = this . value ;
60
+ for ( let i = 0 ; i < path . length - 1 ; ++ i ) {
61
+ currentLevel = ( currentLevel . mapValue ! . fields || { } ) [ path . get ( i ) ] ;
62
+ if ( ! isMapValue ( currentLevel ) ) {
63
+ return null ;
64
+ }
65
+ }
66
+ currentLevel = ( currentLevel . mapValue ! . fields ! || { } ) [ path . lastSegment ( ) ] ;
67
+ return currentLevel || null ;
68
+ }
86
69
}
87
70
88
71
/**
@@ -96,7 +79,8 @@ export class ObjectValue {
96
79
! path . isEmpty ( ) ,
97
80
'Cannot set field for empty path on ObjectValue'
98
81
) ;
99
- this . setOverlay ( path , value ) ;
82
+ const fieldsMap = this . getFieldsMap ( path . popLast ( ) ) ;
83
+ fieldsMap [ path . lastSegment ( ) ] = deepClone ( value ) ;
100
84
}
101
85
102
86
/**
@@ -105,13 +89,30 @@ export class ObjectValue {
105
89
* @param data - A map of fields to values (or null for deletes).
106
90
*/
107
91
setAll ( data : Map < FieldPath , ProtoValue | null > ) : void {
108
- data . forEach ( ( value , fieldPath ) => {
92
+ let parent = FieldPath . emptyPath ( ) ;
93
+
94
+ let upserts : { [ key : string ] : ProtoValue } = { } ;
95
+ let deletes : string [ ] = [ ] ;
96
+
97
+ data . forEach ( ( value , path ) => {
98
+ if ( ! parent . isImmediateParentOf ( path ) ) {
99
+ // Insert the accumulated changes at this parent location
100
+ const fieldsMap = this . getFieldsMap ( parent ) ;
101
+ this . applyChanges ( fieldsMap , upserts , deletes ) ;
102
+ upserts = { } ;
103
+ deletes = [ ] ;
104
+ parent = path . popLast ( ) ;
105
+ }
106
+
109
107
if ( value ) {
110
- this . set ( fieldPath , value ) ;
108
+ upserts [ path . lastSegment ( ) ] = deepClone ( value ) ;
111
109
} else {
112
- this . delete ( fieldPath ) ;
110
+ deletes . push ( path . lastSegment ( ) ) ;
113
111
}
114
112
} ) ;
113
+
114
+ const fieldsMap = this . getFieldsMap ( parent ) ;
115
+ this . applyChanges ( fieldsMap , upserts , deletes ) ;
115
116
}
116
117
117
118
/**
@@ -125,138 +126,58 @@ export class ObjectValue {
125
126
! path . isEmpty ( ) ,
126
127
'Cannot delete field for empty path on ObjectValue'
127
128
) ;
128
- this . setOverlay ( path , null ) ;
129
+ const nestedValue = this . field ( path . popLast ( ) ) ;
130
+ if ( isMapValue ( nestedValue ) && nestedValue . mapValue . fields ) {
131
+ delete nestedValue . mapValue . fields [ path . lastSegment ( ) ] ;
132
+ }
129
133
}
130
134
131
135
isEqual ( other : ObjectValue ) : boolean {
132
- return valueEquals ( this . buildProto ( ) , other . buildProto ( ) ) ;
136
+ return valueEquals ( this . value , other . value ) ;
133
137
}
134
138
135
139
/**
136
- * Adds `value` to the overlay map at `path`. Creates nested map entries if
137
- * needed .
140
+ * Returns the map that contains the leaf element of `path`. If the parent
141
+ * entry does not yet exist, or if it is not a map, a new map will be created .
138
142
*/
139
- private setOverlay ( path : FieldPath , value : ProtoValue | null ) : void {
140
- let currentLevel = this . overlayMap ;
143
+ private getFieldsMap ( path : FieldPath ) : Record < string , ProtoValue > {
144
+ let current = this . value ;
141
145
142
- for ( let i = 0 ; i < path . length - 1 ; ++ i ) {
143
- const currentSegment = path . get ( i ) ;
144
- let currentValue = currentLevel . get ( currentSegment ) ;
145
-
146
- if ( currentValue instanceof Map ) {
147
- // Re-use a previously created map
148
- currentLevel = currentValue ;
149
- } else if (
150
- currentValue &&
151
- typeOrder ( currentValue ) === TypeOrder . ObjectValue
152
- ) {
153
- // Convert the existing Protobuf MapValue into a map
154
- currentValue = new Map < string , Overlay > (
155
- Object . entries ( currentValue . mapValue ! . fields || { } )
156
- ) ;
157
- currentLevel . set ( currentSegment , currentValue ) ;
158
- currentLevel = currentValue ;
159
- } else {
160
- // Create an empty map to represent the current nesting level
161
- currentValue = new Map < string , Overlay > ( ) ;
162
- currentLevel . set ( currentSegment , currentValue ) ;
163
- currentLevel = currentValue ;
164
- }
146
+ if ( ! current . mapValue ! . fields ) {
147
+ current . mapValue = { fields : { } } ;
165
148
}
166
149
167
- currentLevel . set ( path . lastSegment ( ) , value ) ;
168
- }
169
-
170
- /**
171
- * Applies any overlays from `currentOverlays` that exist at `currentPath`
172
- * and returns the merged data at `currentPath` (or null if there were no
173
- * changes).
174
- *
175
- * @param currentPath - The path at the current nesting level. Can be set to
176
- * FieldValue.emptyPath() to represent the root.
177
- * @param currentOverlays - The overlays at the current nesting level in the
178
- * same format as `overlayMap`.
179
- * @returns The merged data at `currentPath` or null if no modifications
180
- * were applied.
181
- */
182
- private applyOverlay (
183
- currentPath : FieldPath ,
184
- currentOverlays : Map < string , Overlay >
185
- ) : { mapValue : ProtoMapValue } | null {
186
- let modified = false ;
187
-
188
- const existingValue = ObjectValue . extractNestedValue (
189
- this . partialValue ,
190
- currentPath
191
- ) ;
192
- const resultAtPath = isMapValue ( existingValue )
193
- ? // If there is already data at the current path, base our
194
- // modifications on top of the existing data.
195
- { ...existingValue . mapValue . fields }
196
- : { } ;
197
-
198
- currentOverlays . forEach ( ( value , pathSegment ) => {
199
- if ( value instanceof Map ) {
200
- const nested = this . applyOverlay ( currentPath . child ( pathSegment ) , value ) ;
201
- if ( nested != null ) {
202
- resultAtPath [ pathSegment ] = nested ;
203
- modified = true ;
204
- }
205
- } else if ( value !== null ) {
206
- resultAtPath [ pathSegment ] = value ;
207
- modified = true ;
208
- } else if ( resultAtPath . hasOwnProperty ( pathSegment ) ) {
209
- delete resultAtPath [ pathSegment ] ;
210
- modified = true ;
150
+ for ( let i = 0 ; i < path . length ; ++ i ) {
151
+ let next = current . mapValue ! . fields ! [ path . get ( i ) ] ;
152
+ if ( ! isMapValue ( next ) || ! next . mapValue . fields ) {
153
+ next = { mapValue : { fields : { } } } ;
154
+ current . mapValue ! . fields ! [ path . get ( i ) ] = next ;
211
155
}
212
- } ) ;
156
+ current = next as { mapValue : ProtoMapValue } ;
157
+ }
213
158
214
- return modified ? { mapValue : { fields : resultAtPath } } : null ;
159
+ return current . mapValue ! . fields ! ;
215
160
}
216
161
217
162
/**
218
- * Builds the Protobuf that backs this ObjectValue.
219
- *
220
- * This method applies any outstanding modifications and memoizes the result.
221
- * Further invocations are based on this memoized result.
163
+ * Modifies `fieldsMap` by adding, replacing or deleting the specified
164
+ * entries.
222
165
*/
223
- private buildProto ( ) : { mapValue : ProtoMapValue } {
224
- const mergedResult = this . applyOverlay (
225
- FieldPath . emptyPath ( ) ,
226
- this . overlayMap
227
- ) ;
228
- if ( mergedResult != null ) {
229
- this . partialValue = mergedResult ;
230
- this . overlayMap . clear ( ) ;
231
- }
232
- return this . partialValue ;
233
- }
234
-
235
- private static extractNestedValue (
236
- proto : ProtoValue ,
237
- path : FieldPath
238
- ) : ProtoValue | null {
239
- if ( path . isEmpty ( ) ) {
240
- return proto ;
241
- } else {
242
- let value : ProtoValue = proto ;
243
- for ( let i = 0 ; i < path . length - 1 ; ++ i ) {
244
- if ( ! value . mapValue ! . fields ) {
245
- return null ;
246
- }
247
- value = value . mapValue ! . fields [ path . get ( i ) ] ;
248
- if ( ! isMapValue ( value ) ) {
249
- return null ;
250
- }
251
- }
252
-
253
- value = ( value . mapValue ! . fields || { } ) [ path . lastSegment ( ) ] ;
254
- return value || null ;
166
+ private applyChanges (
167
+ fieldsMap : Record < string , ProtoValue > ,
168
+ inserts : { [ key : string ] : ProtoValue } ,
169
+ deletes : string [ ]
170
+ ) : void {
171
+ forEach ( inserts , ( key , val ) => ( fieldsMap [ key ] = val ) ) ;
172
+ for ( const field of deletes ) {
173
+ delete fieldsMap [ field ] ;
255
174
}
256
175
}
257
176
258
177
clone ( ) : ObjectValue {
259
- return new ObjectValue ( this . buildProto ( ) ) ;
178
+ return new ObjectValue (
179
+ deepClone ( this . value ) as { mapValue : ProtoMapValue }
180
+ ) ;
260
181
}
261
182
}
262
183
@@ -265,7 +186,7 @@ export class ObjectValue {
265
186
*/
266
187
export function extractFieldMask ( value : ProtoMapValue ) : FieldMask {
267
188
const fields : FieldPath [ ] = [ ] ;
268
- forEach ( value ! . fields || { } , ( key , value ) => {
189
+ forEach ( value ! . fields , ( key , value ) => {
269
190
const currentPath = new FieldPath ( [ key ] ) ;
270
191
if ( isMapValue ( value ) ) {
271
192
const nestedMask = extractFieldMask ( value . mapValue ! ) ;
0 commit comments