Skip to content

Commit 9b3e2f4

Browse files
committed
Ideas to support converting bundle protos to Proto3 JSON compatible format
1 parent 5dd5ae4 commit 9b3e2f4

File tree

2 files changed

+93
-58
lines changed

2 files changed

+93
-58
lines changed

packages/firestore/src/lite-api/user_data_reader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,7 @@ export function parseData(
778778
}
779779
}
780780

781-
function parseObject(
781+
export function parseObject(
782782
obj: Dict<unknown>,
783783
context: ParseContextImpl
784784
): { mapValue: ProtoMapValue } {

packages/firestore/src/util/bundle_builder_impl.ts

Lines changed: 92 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,37 +15,51 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { queryToTarget } from '../../src/core/query';
18+
import {queryToTarget} from '../../src/core/query';
1919
import {
2020
JsonProtoSerializer,
2121
toDocument,
2222
toName,
2323
toQueryTarget,
24+
toTimestamp,
2425
} from '../../src/remote/serializer';
25-
import { Firestore } from '../api/database';
26-
import { DatabaseId } from '../core/database_info';
27-
import { DocumentSnapshot, QuerySnapshot } from '../lite-api/snapshot';
28-
import { Timestamp } from '../lite-api/timestamp';
26+
import {Firestore} from '../api/database';
27+
import {DatabaseId} from '../core/database_info';
28+
import {DocumentSnapshot, QuerySnapshot} from '../lite-api/snapshot';
29+
import {Timestamp} from '../lite-api/timestamp';
2930
import {
31+
BundledDocumentMetadata as ProtoBundledDocumentMetadata,
3032
BundleElement as ProtoBundleElement,
3133
BundleMetadata as ProtoBundleMetadata,
32-
BundledDocumentMetadata as ProtoBundledDocumentMetadata,
3334
NamedQuery as ProtoNamedQuery,
3435
} from '../protos/firestore_bundle_proto';
35-
import { Document } from '../protos/firestore_proto_api';
36+
import {
37+
Document as ProtoDocument,
38+
Document
39+
} from '../protos/firestore_proto_api';
3640

3741
import {
3842
invalidArgumentMessage,
3943
validateString,
4044
} from './bundle_builder_validation_utils';
45+
import {encoder} from "../../test/unit/util/bundle_data";
46+
import {
47+
parseData, parseObject,
48+
UserDataReader,
49+
UserDataSource
50+
} from "../lite-api/user_data_reader";
51+
import {AbstractUserDataWriter} from "../lite-api/user_data_writer";
52+
import {ExpUserDataWriter} from "../api/reference_impl";
53+
import {MutableDocument} from "../model/document";
54+
import {debugAssert} from "./assert";
4155

4256
const BUNDLE_VERSION = 1;
4357

4458
/**
4559
* Builds a Firestore data bundle with results from the given document and query snapshots.
4660
*/
4761
export class BundleBuilder {
48-
62+
4963
// Resulting documents for the bundle, keyed by full document path.
5064
private documents: Map<string, BundledDocument> = new Map();
5165
// Named queries saved in the bundle, keyed by query name.
@@ -56,10 +70,21 @@ export class BundleBuilder {
5670

5771
private databaseId: DatabaseId;
5872

73+
private readonly serializer: JsonProtoSerializer;
74+
private readonly userDataReader: UserDataReader;
75+
private readonly userDataWriter: AbstractUserDataWriter;
76+
5977
constructor(private firestore: Firestore, readonly bundleId: string) {
6078
this.databaseId = firestore._databaseId;
79+
80+
// useProto3Json is true because the objects will be serialized to JSON string
81+
// before being written to the bundle buffer.
82+
this.serializer = new JsonProtoSerializer(this.databaseId, /*useProto3Json=*/ true);
83+
84+
this.userDataWriter = new ExpUserDataWriter(firestore);
85+
this.userDataReader = new UserDataReader(this.databaseId, true, this.serializer);
6186
}
62-
87+
6388
/**
6489
* Adds a Firestore document snapshot or query snapshot to the bundle.
6590
* Both the documents data and the query read time will be included in the bundle.
@@ -97,7 +122,34 @@ export class BundleBuilder {
97122
}
98123
return this;
99124
}
100-
125+
126+
toBundleDocument(
127+
document: MutableDocument
128+
): ProtoDocument {
129+
// TODO handle documents that have mutations
130+
debugAssert(
131+
!document.hasLocalMutations,
132+
"Can't serialize documents with mutations."
133+
);
134+
135+
// Convert document fields proto to DocumentData and then back
136+
// to Proto3 JSON objects. This is the same approach used in
137+
// bundling in the nodejs-firestore SDK. It may not be the most
138+
// performant approach.
139+
const documentData = this.userDataWriter.convertObjectMap(document.data.value.mapValue.fields, 'previous');
140+
// a parse context is typically used for validating and parsing user data, but in this
141+
// case we are using it internally to convert DocumentData to Proto3 JSON
142+
const context = this.userDataReader.createContext(UserDataSource.ArrayArgument, 'internal toBundledDocument');
143+
const proto3Fields = parseObject(documentData, context);
144+
145+
return {
146+
name: toName(this.serializer, document.key),
147+
fields: proto3Fields.mapValue.fields,
148+
updateTime: toTimestamp(this.serializer, document.version.toTimestamp()),
149+
createTime: toTimestamp(this.serializer, document.createTime.toTimestamp())
150+
};
151+
}
152+
101153
private addBundledDocument(snap: DocumentSnapshot, queryName?: string): void {
102154
// TODO: is this a valid shortcircuit?
103155
if(!snap._document || !snap._document.isValidDocument()) {
@@ -114,15 +166,11 @@ export class BundleBuilder {
114166
(snapReadTime && originalDocument.metadata.readTime! < snapReadTime)
115167
) {
116168

117-
// TODO: Should I create on serializer for the bundler instance, or just created one adhoc
118-
// like this?
119-
const serializer = new JsonProtoSerializer(this.databaseId, /*useProto3Json=*/ false);
120-
121169
this.documents.set(snap.ref.path, {
122-
document: snap._document.isFoundDocument() ? toDocument(serializer, mutableCopy) : undefined,
170+
document: snap._document.isFoundDocument() ? this.toBundleDocument(mutableCopy) : undefined,
123171
metadata: {
124-
name: toName(serializer, mutableCopy.key),
125-
readTime: snapReadTime,
172+
name: toName(this.serializer, mutableCopy.key),
173+
readTime: !!snapReadTime ? toTimestamp(this.serializer, snapReadTime) : undefined,
126174
exists: snap.exists(),
127175
},
128176
});
@@ -145,10 +193,8 @@ export class BundleBuilder {
145193
if (this.namedQueries.has(name)) {
146194
throw new Error(`Query name conflict: ${name} has already been added.`);
147195
}
196+
const queryTarget = toQueryTarget(this.serializer, queryToTarget(querySnap.query._query));
148197

149-
const serializer = new JsonProtoSerializer(this.databaseId, /*useProto3Json=*/ false);
150-
const queryTarget = toQueryTarget(serializer, queryToTarget(querySnap.query._query));
151-
152198
// TODO: if we can't resolve the query's readTime then can we set it to the latest
153199
// of the document collection?
154200
let latestReadTime = new Timestamp(0, 0);
@@ -169,7 +215,7 @@ export class BundleBuilder {
169215
this.namedQueries.set(name, {
170216
name,
171217
bundledQuery,
172-
readTime: latestReadTime
218+
readTime: toTimestamp(this.serializer, latestReadTime)
173219
});
174220
}
175221

@@ -178,65 +224,54 @@ export class BundleBuilder {
178224
* of the element.
179225
* @private
180226
* @internal
227+
* @param bundleElement A ProtoBundleElement that is expected to be Proto3 JSON compatible.
181228
*/
182-
private elementToLengthPrefixedBuffer(
229+
private lengthPrefixedString(
183230
bundleElement: ProtoBundleElement
184-
): Buffer {
185-
// Convert to a valid proto message object then take its JSON representation.
186-
// This take cares of stuff like converting internal byte array fields
187-
// to Base64 encodings.
188-
189-
// TODO: This fails. BundleElement doesn't have a toJSON method.
190-
const message = require('../protos/firestore_v1_proto_api')
191-
.firestore.BundleElement.fromObject(bundleElement)
192-
.toJSON();
193-
const buffer = Buffer.from(JSON.stringify(message), 'utf-8');
194-
const lengthBuffer = Buffer.from(buffer.length.toString());
195-
return Buffer.concat([lengthBuffer, buffer]);
231+
): string {
232+
const str = JSON.stringify(bundleElement);
233+
// TODO: it's not ideal to have to re-encode all of these strings multiple times
234+
// It may be more performant to return a UInt8Array that is concatenated to other
235+
// UInt8Arrays instead of returning and concatenating strings and then
236+
// converting the full string to UInt8Array.
237+
const l = encoder.encode(str).byteLength;
238+
return `${l}${str}`;
196239
}
197240

198-
build(): Buffer {
199-
200-
let bundleBuffer = Buffer.alloc(0);
241+
build(): Uint8Array {
242+
let bundleString = '';
201243

202244
for (const namedQuery of this.namedQueries.values()) {
203-
bundleBuffer = Buffer.concat([
204-
bundleBuffer,
205-
this.elementToLengthPrefixedBuffer({namedQuery}),
206-
]);
245+
bundleString += this.lengthPrefixedString({namedQuery});
207246
}
208247

209248
for (const bundledDocument of this.documents.values()) {
210249
const documentMetadata: ProtoBundledDocumentMetadata =
211250
bundledDocument.metadata;
212251

213-
bundleBuffer = Buffer.concat([
214-
bundleBuffer,
215-
this.elementToLengthPrefixedBuffer({documentMetadata}),
216-
]);
252+
bundleString += this.lengthPrefixedString({documentMetadata});
217253
// Write to the bundle if document exists.
218254
const document = bundledDocument.document;
219255
if (document) {
220-
bundleBuffer = Buffer.concat([
221-
bundleBuffer,
222-
this.elementToLengthPrefixedBuffer({document}),
223-
]);
256+
bundleString += this.lengthPrefixedString({document});
224257
}
225258
}
226259

227260
const metadata: ProtoBundleMetadata = {
228261
id: this.bundleId,
229-
createTime: this.latestReadTime,
262+
createTime: toTimestamp(this.serializer, this.latestReadTime),
230263
version: BUNDLE_VERSION,
231264
totalDocuments: this.documents.size,
232-
totalBytes: bundleBuffer.length,
265+
// TODO: it's not ideal to have to re-encode all of these strings multiple times
266+
totalBytes: encoder.encode(bundleString).length,
233267
};
234268
// Prepends the metadata element to the bundleBuffer: `bundleBuffer` is the second argument to `Buffer.concat`.
235-
bundleBuffer = Buffer.concat([
236-
this.elementToLengthPrefixedBuffer({metadata}),
237-
bundleBuffer,
238-
]);
239-
return bundleBuffer;
269+
bundleString = this.lengthPrefixedString({metadata}) + bundleString;
270+
271+
// TODO: it's not ideal to have to re-encode all of these strings multiple times
272+
// the implementation in nodejs-firestore concatenates Buffers instead of
273+
// concatenating strings.
274+
return encoder.encode(bundleString);
240275
}
241276
}
242277

@@ -278,4 +313,4 @@ function validateQuerySnapshot(arg: string | number, value: unknown): void {
278313
if (!(value instanceof QuerySnapshot)) {
279314
throw new Error(invalidArgumentMessage(arg, 'QuerySnapshot'));
280315
}
281-
}
316+
}

0 commit comments

Comments
 (0)