1515 * limitations under the License.
1616 */
1717
18- import { queryToTarget } from '../../src/core/query' ;
18+ import { queryToTarget } from '../../src/core/query' ;
1919import {
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' ;
2930import {
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
3741import {
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
4256const BUNDLE_VERSION = 1 ;
4357
4458/**
4559 * Builds a Firestore data bundle with results from the given document and query snapshots.
4660 */
4761export 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