1414 * limitations under the License.
1515 */
1616
17- import { replaceProjectIdToken } from '@google-cloud/projectify' ;
18- import * as assert from 'assert' ;
1917import * as bun from 'bun' ;
20- import * as extend from 'extend ' ;
18+ import { CallOptions } from 'google-gax ' ;
2119import * as through2 from 'through2' ;
2220
2321import { google } from '../protos/firestore_proto_api' ;
2422import { fieldsFromJson , timestampFromJson } from './convert' ;
2523import { DocumentSnapshot , DocumentSnapshotBuilder , QueryDocumentSnapshot } from './document' ;
2624import { logger , setLibVersion } from './logger' ;
27- import { FieldPath , validateResourcePath } from './path' ;
25+ import { DATABASE_ID , FieldPath , RelativePath , validateResourcePath } from './path' ;
2826import { ResourcePath } from './path' ;
2927import { ClientPool } from './pool' ;
3028import { CollectionReference } from './reference' ;
@@ -207,7 +205,7 @@ export class Firestore {
207205 * to work around a connection limit of 100 concurrent requests per client.
208206 * @private
209207 */
210- private _clientPool : ClientPool < GapicClient > | null = null ;
208+ private _clientPool : ClientPool < GapicClient > ;
211209
212210 /**
213211 * The configuration options for the GAPIC client.
@@ -223,19 +221,19 @@ export class Firestore {
223221 private _settingsFrozen = false ;
224222
225223 /**
226- * A Promise that resolves when client initialization completes. Can be
227- * 'null' if initialization hasn't started yet.
224+ * The serializer to use for the Protobuf transformation.
228225 * @private
229226 */
230- private _clientInitialized : Promise < void > | null = null ;
227+ _serializer : Serializer | null = null ;
231228
232229 /**
233- * The serializer to use for the Protobuf transformation.
230+ * The project ID for this client.
231+ *
232+ * The project ID is auto-detected during the first request unless a project
233+ * ID is passed to the constructor (or provided via `.settings()`).
234234 * @private
235235 */
236- _serializer : Serializer | null = null ;
237-
238- private _referencePath : ResourcePath | null = null ;
236+ private _projectId : string | undefined = undefined ;
239237
240238 // GCF currently tears down idle connections after two minutes. Requests
241239 // that are issued after this period may fail. On GCF, we therefore issue
@@ -311,6 +309,13 @@ export class Firestore {
311309 logger ( 'Firestore' , null , 'Detected GCF environment' ) ;
312310 }
313311
312+ this . _clientPool =
313+ new ClientPool ( MAX_CONCURRENT_REQUESTS_PER_CLIENT , ( ) => {
314+ const client = new module . exports . v1 ( this . _settings ) ;
315+ logger ( 'Firestore' , null , 'Initialized Firestore GAPIC Client' ) ;
316+ return client ;
317+ } ) ;
318+
314319 logger ( 'Firestore' , null , 'Initialized Firestore' ) ;
315320 }
316321
@@ -331,16 +336,9 @@ export class Firestore {
331336 'settings.timestampsInSnapshots' , settings . timestampsInSnapshots ,
332337 { optional : true } ) ;
333338
334- if ( this . _clientInitialized ) {
335- throw new Error (
336- 'Firestore has already been started and its settings can no longer ' +
337- 'be changed. You can only call settings() before calling any other ' +
338- 'methods on a Firestore object.' ) ;
339- }
340-
341339 if ( this . _settingsFrozen ) {
342340 throw new Error (
343- 'Firestore.settings() has already be called . You can only call ' +
341+ 'Firestore has already been initialized . You can only call ' +
344342 'settings() once, and only before calling any other methods on a ' +
345343 'Firestore object.' ) ;
346344 }
@@ -357,28 +355,33 @@ export class Firestore {
357355
358356 if ( settings && settings . projectId ) {
359357 validateString ( 'settings.projectId' , settings . projectId ) ;
360- this . _referencePath = new ResourcePath ( settings . projectId , '(default)' ) ;
361- } else {
362- // Initialize a temporary reference path that will be overwritten during
363- // project ID detection.
364- this . _referencePath = new ResourcePath ( '{{projectId}}' , '(default)' ) ;
358+ this . _projectId = settings . projectId ;
365359 }
366360
367361 this . _settings = settings ;
368362 this . _serializer = new Serializer ( this ) ;
369363 }
370364
371365 /**
372- * The root path to the database.
366+ * Returns the Project ID for this Firestore instance. Validates that
367+ * `initializeIfNeeded()` was called before.
368+ *
369+ * @private
370+ */
371+ get projectId ( ) : string {
372+ this . ensureClientInitialized ( ) ;
373+ return this . _projectId ! ;
374+ }
375+
376+ /**
377+ * Returns the root path of the database. Validates that
378+ * `initializeIfNeeded()` was called before.
373379 *
374380 * @private
375381 */
376382 get formattedName ( ) : string {
377- const components = [
378- 'projects' , this . _referencePath ! . projectId , 'databases' ,
379- this . _referencePath ! . databaseId
380- ] ;
381- return components . join ( '/' ) ;
383+ this . ensureClientInitialized ( ) ;
384+ return `projects/${ this . projectId } /databases/${ DATABASE_ID } ` ;
382385 }
383386
384387 /**
@@ -396,7 +399,7 @@ export class Firestore {
396399 doc ( documentPath : string ) : DocumentReference {
397400 validateResourcePath ( 'documentPath' , documentPath ) ;
398401
399- const path = this . _referencePath ! . append ( documentPath ) ;
402+ const path = RelativePath . EMPTY . append ( documentPath ) ;
400403 if ( ! path . isDocument ) {
401404 throw new Error ( `Argument "documentPath" must point to a document, but was "${
402405 documentPath } ". Your path does not contain an even number of components.`) ;
@@ -424,7 +427,7 @@ export class Firestore {
424427 collection ( collectionPath : string ) : CollectionReference {
425428 validateResourcePath ( 'collectionPath' , collectionPath ) ;
426429
427- const path = this . _referencePath ! . append ( collectionPath ) ;
430+ const path = RelativePath . EMPTY . append ( collectionPath ) ;
428431 if ( ! path . isCollection ) {
429432 throw new Error ( `Argument "collectionPath" must point to a collection, but was "${
430433 collectionPath } ". Your path does not contain an odd number of components.`) ;
@@ -592,7 +595,8 @@ export class Firestore {
592595 { optional : true , minValue : 1 } ) ;
593596 }
594597
595- return this . _runTransaction ( updateFunction , transactionOptions ) ;
598+ return this . initializeIfNeeded ( ) . then (
599+ ( ) => this . _runTransaction ( updateFunction , transactionOptions ) ) ;
596600 }
597601
598602 _runTransaction < T > (
@@ -667,7 +671,7 @@ export class Firestore {
667671 * });
668672 */
669673 listCollections ( ) {
670- const rootDocument = new DocumentReference ( this , this . _referencePath ! ) ;
674+ const rootDocument = new DocumentReference ( this , RelativePath . EMPTY ) ;
671675 return rootDocument . listCollections ( ) ;
672676 }
673677
@@ -712,7 +716,8 @@ export class Firestore {
712716
713717 const { documents, fieldMask} =
714718 parseGetAllArguments ( documentRefsOrReadOptions ) ;
715- return this . getAll_ ( documents , fieldMask , requestTag ( ) ) ;
719+ return this . initializeIfNeeded ( ) . then (
720+ ( ) => this . getAll_ ( documents , fieldMask , requestTag ( ) ) ) ;
716721 }
717722
718723 /**
@@ -813,13 +818,14 @@ export class Firestore {
813818 }
814819
815820 /**
816- * Executes a new request using the first available GAPIC client.
821+ * Initializes the client if it is not already initialized. All methods in the
822+ * SDK can be used after this method completes.
817823 *
818824 * @private
825+ * @return A Promise that resolves when the client is initialized.
819826 */
820- private _runRequest < T > ( op : ( client : GapicClient ) => Promise < T > ) : Promise < T > {
821- // Initialize the client pool if this is the first request.
822- if ( ! this . _clientInitialized ) {
827+ async initializeIfNeeded ( ) : Promise < void > {
828+ if ( ! this . _settingsFrozen ) {
823829 // Nobody should set timestampsInSnapshots anymore, but the error depends
824830 // on whether they set it to true or false...
825831 if ( this . _settings . timestampsInSnapshots === true ) {
@@ -850,87 +856,51 @@ export class Firestore {
850856 Please audit all existing usages of Date when you enable the new
851857 behavior.` ) ;
852858 }
853- this . _clientInitialized = this . _initClientPool ( ) . then ( clientPool => {
854- this . _clientPool = clientPool ;
855- } ) ;
856859 }
857860
858- return this . _clientInitialized ! . then ( ( ) => this . _clientPool ! . run ( op ) ) ;
859- }
860-
861- /**
862- * Initializes the client pool and invokes Project ID detection. Returns a
863- * Promise on completion.
864- *
865- * @private
866- */
867- private _initClientPool ( ) : Promise < ClientPool < GapicClient > > {
868- assert ( ! this . _clientInitialized , 'Client pool already initialized' ) ;
869-
870- const clientPool =
871- new ClientPool ( MAX_CONCURRENT_REQUESTS_PER_CLIENT , ( ) => {
872- const client = new module . exports . v1 ( this . _settings ) ;
873- logger ( 'Firestore' , null , 'Initialized Firestore GAPIC Client' ) ;
874- return client ;
875- } ) ;
876-
877- const projectIdProvided =
878- this . _referencePath ! . projectId !== '{{projectId}}' ;
861+ this . _settingsFrozen = true ;
879862
880- if ( projectIdProvided ) {
881- return Promise . resolve ( clientPool ) ;
882- } else {
883- return clientPool . run ( client => this . _detectProjectId ( client ) )
884- . then ( projectId => {
885- this . _referencePath =
886- new ResourcePath ( projectId , this . _referencePath ! . databaseId ) ;
887- return clientPool ;
863+ if ( this . _projectId === undefined ) {
864+ this . _projectId = await this . _clientPool . run ( gapicClient => {
865+ return new Promise ( ( resolve , reject ) => {
866+ gapicClient . getProjectId ( ( err : Error , projectId : string ) => {
867+ if ( err ) {
868+ logger (
869+ 'Firestore._detectProjectId' , null ,
870+ 'Failed to detect project ID: %s' , err ) ;
871+ reject ( err ) ;
872+ } else {
873+ logger (
874+ 'Firestore._detectProjectId' , null , 'Detected project ID: %s' ,
875+ projectId ) ;
876+ resolve ( projectId ) ;
877+ }
888878 } ) ;
879+ } ) ;
880+ } ) ;
889881 }
890882 }
891883
892884 /**
893- * Auto-detects the Firestore Project ID.
894- *
885+ * Ensures that the client has been fully initialized;
895886 * @private
896- * @param gapicClient The Firestore GAPIC client.
897- * @return A Promise that resolves with the Project ID.
898887 */
899- private _detectProjectId ( gapicClient : GapicClient ) : Promise < string > {
900- return new Promise ( ( resolve , reject ) => {
901- gapicClient . getProjectId ( ( err : Error , projectId : string ) => {
902- if ( err ) {
903- logger (
904- 'Firestore._detectProjectId' , null ,
905- 'Failed to detect project ID: %s' , err ) ;
906- reject ( err ) ;
907- } else {
908- logger (
909- 'Firestore._detectProjectId' , null , 'Detected project ID: %s' ,
910- projectId ) ;
911- resolve ( projectId ) ;
912- }
913- } ) ;
914- } ) ;
888+ private ensureClientInitialized ( ) : void {
889+ if ( this . _projectId === undefined ) {
890+ // Note that we do not mention `initializeIfNeeded()` here to not leak the
891+ // private API.
892+ throw new Error ( 'INTERNAL ERROR: Client is yet ready to issue requests.' ) ;
893+ }
915894 }
916895
917896 /**
918- * Decorate the request options of an API request. This is used to replace
919- * any `{{projectId}}` placeholders with the value detected from the user's
920- * environment, if one wasn't provided manually.
921- *
897+ * Returns GAX call options that set the cloud resource header.
922898 * @private
923899 */
924- _decorateRequest < T > ( request : T ) : { request : T , gax : { } } {
925- let decoratedRequest = extend ( true , { } , request ) ;
926- decoratedRequest =
927- replaceProjectIdToken ( decoratedRequest , this . _referencePath ! . projectId ) ;
928- const decoratedGax : {
929- otherArgs : { headers : { [ k : string ] : string } }
930- } = { otherArgs : { headers : { } } } ;
931- decoratedGax . otherArgs . headers [ CLOUD_RESOURCE_HEADER ] = this . formattedName ;
932-
933- return { request : decoratedRequest , gax : decoratedGax } ;
900+ private createCallOptions ( ) : CallOptions {
901+ const gaxHeaders : CallOptions = { otherArgs : { headers : { } } } ;
902+ gaxHeaders . otherArgs ! . headers [ CLOUD_RESOURCE_HEADER ] = this . formattedName ;
903+ return gaxHeaders ;
934904 }
935905
936906 /**
@@ -1132,16 +1102,15 @@ export class Firestore {
11321102 methodName : string , request : { } , requestTag : string ,
11331103 allowRetries : boolean ) : Promise < T > {
11341104 const attempts = allowRetries ? MAX_REQUEST_RETRIES : 1 ;
1105+ const callOptions = this . createCallOptions ( ) ;
11351106
1136- return this . _runRequest ( gapicClient => {
1137- const decorated = this . _decorateRequest ( request ) ;
1107+ return this . _clientPool . run ( gapicClient => {
11381108 return this . _retry ( attempts , requestTag , ( ) => {
11391109 return new Promise ( ( resolve , reject ) => {
11401110 logger (
1141- 'Firestore.request' , requestTag , 'Sending request: %j' ,
1142- decorated . request ) ;
1111+ 'Firestore.request' , requestTag , 'Sending request: %j' , request ) ;
11431112 gapicClient [ methodName ] (
1144- decorated . request , decorated . gax , ( err : GrpcError , result : T ) => {
1113+ request , callOptions , ( err : GrpcError , result : T ) => {
11451114 if ( err ) {
11461115 logger (
11471116 'Firestore.request' , requestTag , 'Received error:' , err ) ;
@@ -1178,17 +1147,16 @@ export class Firestore {
11781147 methodName : string , request : { } , requestTag : string ,
11791148 allowRetries : boolean ) : Promise < NodeJS . ReadableStream > {
11801149 const attempts = allowRetries ? MAX_REQUEST_RETRIES : 1 ;
1150+ const callOptions = this . createCallOptions ( ) ;
11811151
1182- return this . _runRequest ( gapicClient => {
1183- const decorated = this . _decorateRequest ( request ) ;
1152+ return this . _clientPool . run ( gapicClient => {
11841153 return this . _retry ( attempts , requestTag , ( ) => {
11851154 return new Promise < NodeJS . ReadableStream > ( ( resolve , reject ) => {
11861155 try {
11871156 logger (
11881157 'Firestore.readStream' , requestTag ,
1189- 'Sending request: %j' , decorated . request ) ;
1190- const stream = gapicClient [ methodName ] (
1191- decorated . request , decorated . gax ) ;
1158+ 'Sending request: %j' , request ) ;
1159+ const stream = gapicClient [ methodName ] ( request , callOptions ) ;
11921160 const logStream =
11931161 through2 . obj ( function ( this , chunk , enc , callback ) {
11941162 logger (
@@ -1229,26 +1197,14 @@ export class Firestore {
12291197 readWriteStream (
12301198 methodName : string , request : { } , requestTag : string ,
12311199 allowRetries : boolean ) : Promise < NodeJS . ReadWriteStream > {
1232- const self = this ;
12331200 const attempts = allowRetries ? MAX_REQUEST_RETRIES : 1 ;
1201+ const callOptions = this . createCallOptions ( ) ;
12341202
1235- return this . _runRequest ( gapicClient => {
1236- const decorated = this . _decorateRequest ( request ) ;
1203+ return this . _clientPool . run ( gapicClient => {
12371204 return this . _retry ( attempts , requestTag , ( ) => {
12381205 return Promise . resolve ( ) . then ( ( ) => {
12391206 logger ( 'Firestore.readWriteStream' , requestTag , 'Opening stream' ) ;
1240- const requestStream = gapicClient [ methodName ] ( decorated . gax ) ;
1241-
1242- // The transform stream to assign the project ID.
1243- const transform = through2 . obj ( ( chunk , encoding , callback ) => {
1244- const decoratedChunk = extend ( true , { } , chunk ) ;
1245- replaceProjectIdToken (
1246- decoratedChunk , self . _referencePath ! . projectId ) ;
1247- logger (
1248- 'Firestore.readWriteStream' , requestTag ,
1249- 'Streaming request: %j' , decoratedChunk ) ;
1250- requestStream . write ( decoratedChunk , encoding , callback ) ;
1251- } ) ;
1207+ const requestStream = gapicClient [ methodName ] ( callOptions ) ;
12521208
12531209 const logStream = through2 . obj ( function ( this , chunk , enc , callback ) {
12541210 logger (
@@ -1258,7 +1214,7 @@ export class Firestore {
12581214 callback ( ) ;
12591215 } ) ;
12601216
1261- const resultStream = bun ( [ transform , requestStream , logStream ] ) ;
1217+ const resultStream = bun ( [ requestStream , logStream ] ) ;
12621218 return this . _initializeStream ( resultStream , requestTag , request ) ;
12631219 } ) ;
12641220 } ) ;
0 commit comments