Skip to content

Commit 4091720

Browse files
Simplify ObjectValue (#4933)
1 parent cc2132c commit 4091720

16 files changed

+117
-174
lines changed

packages/firestore/src/exp/snapshot.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ export class DocumentSnapshot<
278278
return this._converter.fromFirestore(snapshot, options);
279279
} else {
280280
return this._userDataWriter.convertValue(
281-
this._document.data.toProto(),
281+
this._document.data.value,
282282
options.serverTimestamps
283283
) as T;
284284
}

packages/firestore/src/lite/snapshot.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,7 @@ export class DocumentSnapshot<T = DocumentData> {
174174
);
175175
return this._converter.fromFirestore(snapshot);
176176
} else {
177-
return this._userDataWriter.convertValue(
178-
this._document.data.toProto()
179-
) as T;
177+
return this._userDataWriter.convertValue(this._document.data.value) as T;
180178
}
181179
}
182180

packages/firestore/src/lite/user_data_writer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export abstract class AbstractUserDataWriter {
9292
serverTimestampBehavior: ServerTimestampBehavior
9393
): DocumentData {
9494
const result: DocumentData = {};
95-
forEach(mapValue.fields || {}, (key, value) => {
95+
forEach(mapValue.fields, (key, value) => {
9696
result[key] = this.convertValue(value, serverTimestampBehavior);
9797
});
9898
return result;

packages/firestore/src/local/local_store_impl.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717

1818
import { User } from '../auth/user';
19-
import { BundledDocuments, NamedQuery, BundleConverter } from '../core/bundle';
19+
import { BundleConverter, BundledDocuments, NamedQuery } from '../core/bundle';
2020
import { newQueryForPath, Query, queryToTarget } from '../core/query';
2121
import { SnapshotVersion } from '../core/snapshot_version';
2222
import { canonifyTarget, Target, targetEquals } from '../core/target';
@@ -330,7 +330,7 @@ export function localStoreWriteLocally(
330330
new PatchMutation(
331331
mutation.key,
332332
baseValue,
333-
extractFieldMask(baseValue.toProto().mapValue!),
333+
extractFieldMask(baseValue.value.mapValue),
334334
Precondition.exists(true)
335335
)
336336
);

packages/firestore/src/local/memory_persistence.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ export class MemoryLruDelegate implements ReferenceDelegate, LruDelegate {
474474
documentSize(document: Document): number {
475475
let documentSize = document.key.toString().length;
476476
if (document.isFoundDocument()) {
477-
documentSize += estimateByteSize(document.data.toProto());
477+
documentSize += estimateByteSize(document.data.value);
478478
}
479479
return documentSize;
480480
}

packages/firestore/src/model/document.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ export class MutableDocument implements Document {
331331
toString(): string {
332332
return (
333333
`Document(${this.key}, ${this.version}, ${JSON.stringify(
334-
this.data.toProto()
334+
this.data.value
335335
)}, ` +
336336
`{documentType: ${this.documentType}}), ` +
337337
`{documentState: ${this.documentState}})`

packages/firestore/src/model/object_value.ts

+71-150
Original file line numberDiff line numberDiff line change
@@ -25,45 +25,21 @@ import { forEach } from '../util/obj';
2525
import { FieldMask } from './field_mask';
2626
import { FieldPath } from './path';
2727
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';
3029

3130
export interface JsonObject<T> {
3231
[name: string]: T;
3332
}
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-
4233
/**
4334
* An ObjectValue represents a MapValue in the Firestore Proto and offers the
4435
* ability to add and remove fields (via the ObjectValueBuilder).
4536
*/
4637
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 }) {
6239
debugAssert(
63-
!isServerTimestamp(proto),
40+
!isServerTimestamp(value),
6441
'ServerTimestamps should be converted to ServerTimestampValue'
6542
);
66-
this.partialValue = proto;
6743
}
6844

6945
static empty(): ObjectValue {
@@ -77,12 +53,19 @@ export class ObjectValue {
7753
* @returns The value at the path or null if the path is not set.
7854
*/
7955
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+
}
8669
}
8770

8871
/**
@@ -96,7 +79,8 @@ export class ObjectValue {
9679
!path.isEmpty(),
9780
'Cannot set field for empty path on ObjectValue'
9881
);
99-
this.setOverlay(path, value);
82+
const fieldsMap = this.getFieldsMap(path.popLast());
83+
fieldsMap[path.lastSegment()] = deepClone(value);
10084
}
10185

10286
/**
@@ -105,13 +89,30 @@ export class ObjectValue {
10589
* @param data - A map of fields to values (or null for deletes).
10690
*/
10791
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+
109107
if (value) {
110-
this.set(fieldPath, value);
108+
upserts[path.lastSegment()] = deepClone(value);
111109
} else {
112-
this.delete(fieldPath);
110+
deletes.push(path.lastSegment());
113111
}
114112
});
113+
114+
const fieldsMap = this.getFieldsMap(parent);
115+
this.applyChanges(fieldsMap, upserts, deletes);
115116
}
116117

117118
/**
@@ -125,138 +126,58 @@ export class ObjectValue {
125126
!path.isEmpty(),
126127
'Cannot delete field for empty path on ObjectValue'
127128
);
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+
}
129133
}
130134

131135
isEqual(other: ObjectValue): boolean {
132-
return valueEquals(this.buildProto(), other.buildProto());
136+
return valueEquals(this.value, other.value);
133137
}
134138

135139
/**
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.
138142
*/
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;
141145

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: {} };
165148
}
166149

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;
211155
}
212-
});
156+
current = next as { mapValue: ProtoMapValue };
157+
}
213158

214-
return modified ? { mapValue: { fields: resultAtPath } } : null;
159+
return current.mapValue!.fields!;
215160
}
216161

217162
/**
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.
222165
*/
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];
255174
}
256175
}
257176

258177
clone(): ObjectValue {
259-
return new ObjectValue(this.buildProto());
178+
return new ObjectValue(
179+
deepClone(this.value) as { mapValue: ProtoMapValue }
180+
);
260181
}
261182
}
262183

@@ -265,7 +186,7 @@ export class ObjectValue {
265186
*/
266187
export function extractFieldMask(value: ProtoMapValue): FieldMask {
267188
const fields: FieldPath[] = [];
268-
forEach(value!.fields || {}, (key, value) => {
189+
forEach(value!.fields, (key, value) => {
269190
const currentPath = new FieldPath([key]);
270191
if (isMapValue(value)) {
271192
const nestedMask = extractFieldMask(value.mapValue!);

0 commit comments

Comments
 (0)