@firebase/data-connect 0.5.0 → 0.6.0-20260409172004
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs.js +1173 -143
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1172 -144
- package/dist/index.esm.js.map +1 -1
- package/dist/index.node.cjs.js +1239 -190
- package/dist/index.node.cjs.js.map +1 -1
- package/dist/internal.d.ts +133 -12
- package/dist/node-esm/index.node.esm.js +1238 -191
- package/dist/node-esm/index.node.esm.js.map +1 -1
- package/dist/node-esm/src/api/Mutation.d.ts +2 -2
- package/dist/node-esm/src/api/query.d.ts +1 -1
- package/dist/node-esm/src/core/query/QueryManager.d.ts +22 -3
- package/dist/node-esm/src/network/index.d.ts +1 -1
- package/dist/node-esm/src/network/manager.d.ts +61 -0
- package/dist/node-esm/src/network/{fetch.d.ts → rest/fetch.d.ts} +9 -4
- package/dist/node-esm/src/network/rest/index.d.ts +18 -0
- package/dist/node-esm/src/network/rest/restTransport.d.ts +33 -0
- package/dist/node-esm/src/network/stream/streamTransport.d.ts +243 -0
- package/dist/node-esm/src/network/stream/websocket.d.ts +90 -0
- package/dist/node-esm/src/network/stream/wire.d.ts +138 -0
- package/dist/node-esm/src/network/transport.d.ts +179 -0
- package/dist/node-esm/src/util/url.d.ts +3 -1
- package/dist/private.d.ts +29 -7
- package/dist/public.d.ts +3 -1
- package/dist/src/api/Mutation.d.ts +2 -2
- package/dist/src/api/query.d.ts +1 -1
- package/dist/src/core/query/QueryManager.d.ts +22 -3
- package/dist/src/network/index.d.ts +1 -1
- package/dist/src/network/manager.d.ts +61 -0
- package/dist/src/network/{fetch.d.ts → rest/fetch.d.ts} +9 -4
- package/dist/src/network/rest/index.d.ts +18 -0
- package/dist/src/network/rest/restTransport.d.ts +33 -0
- package/dist/src/network/stream/streamTransport.d.ts +243 -0
- package/dist/src/network/stream/websocket.d.ts +90 -0
- package/dist/src/network/stream/wire.d.ts +138 -0
- package/dist/src/network/transport.d.ts +179 -0
- package/dist/src/util/url.d.ts +3 -1
- package/package.json +1 -1
- package/dist/node-esm/src/network/transport/index.d.ts +0 -81
- package/dist/node-esm/src/network/transport/rest.d.ts +0 -49
- package/dist/src/network/transport/index.d.ts +0 -81
- package/dist/src/network/transport/rest.d.ts +0 -49
package/dist/index.node.cjs.js
CHANGED
|
@@ -136,6 +136,138 @@ const CallerSdkTypeEnum = {
|
|
|
136
136
|
TanstackAngularCore: 'TanstackAngularCore', // Tanstack non-generated Angular SDK
|
|
137
137
|
GeneratedAngular: 'GeneratedAngular' // Generated Angular SDK
|
|
138
138
|
};
|
|
139
|
+
/**
|
|
140
|
+
* Constructs the value for the X-Goog-Api-Client header
|
|
141
|
+
* @internal
|
|
142
|
+
*/
|
|
143
|
+
function getGoogApiClientValue$1(isUsingGen, callerSdkType) {
|
|
144
|
+
let str = 'gl-js/ fire/' + SDK_VERSION;
|
|
145
|
+
if (callerSdkType !== CallerSdkTypeEnum.Base &&
|
|
146
|
+
callerSdkType !== CallerSdkTypeEnum.Generated) {
|
|
147
|
+
str += ' js/' + callerSdkType.toLowerCase();
|
|
148
|
+
}
|
|
149
|
+
else if (isUsingGen || callerSdkType === CallerSdkTypeEnum.Generated) {
|
|
150
|
+
str += ' js/gen';
|
|
151
|
+
}
|
|
152
|
+
return str;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The base class for all DataConnectTransportInterface implementations. Handles common logic such as
|
|
156
|
+
* URL construction, auth token management, and emulator usage. Concrete transport implementations
|
|
157
|
+
* should extend this class and implement the abstract {@link DataConnectTransportInterface} methods.
|
|
158
|
+
* @internal
|
|
159
|
+
*/
|
|
160
|
+
class AbstractDataConnectTransport {
|
|
161
|
+
constructor(options, apiKey, appId, authProvider, appCheckProvider, transportOptions, _isUsingGen = false, _callerSdkType = CallerSdkTypeEnum.Base) {
|
|
162
|
+
this.apiKey = apiKey;
|
|
163
|
+
this.appId = appId;
|
|
164
|
+
this.authProvider = authProvider;
|
|
165
|
+
this.appCheckProvider = appCheckProvider;
|
|
166
|
+
this._isUsingGen = _isUsingGen;
|
|
167
|
+
this._callerSdkType = _callerSdkType;
|
|
168
|
+
this._host = '';
|
|
169
|
+
this._location = 'l';
|
|
170
|
+
this._connectorName = '';
|
|
171
|
+
this._secure = true;
|
|
172
|
+
this._project = 'p';
|
|
173
|
+
this._authToken = null;
|
|
174
|
+
this._appCheckToken = null;
|
|
175
|
+
this._lastToken = null;
|
|
176
|
+
this._isUsingEmulator = false;
|
|
177
|
+
if (transportOptions) {
|
|
178
|
+
if (typeof transportOptions.port === 'number') {
|
|
179
|
+
this._port = transportOptions.port;
|
|
180
|
+
}
|
|
181
|
+
if (typeof transportOptions.sslEnabled !== 'undefined') {
|
|
182
|
+
this._secure = transportOptions.sslEnabled;
|
|
183
|
+
}
|
|
184
|
+
this._host = transportOptions.host;
|
|
185
|
+
}
|
|
186
|
+
const { location, projectId: project, connector, service } = options;
|
|
187
|
+
if (location) {
|
|
188
|
+
this._location = location;
|
|
189
|
+
}
|
|
190
|
+
if (project) {
|
|
191
|
+
this._project = project;
|
|
192
|
+
}
|
|
193
|
+
this._serviceName = service;
|
|
194
|
+
if (!connector) {
|
|
195
|
+
throw new DataConnectError(Code.INVALID_ARGUMENT, 'Connector Name required!');
|
|
196
|
+
}
|
|
197
|
+
this._connectorName = connector;
|
|
198
|
+
this._connectorResourcePath = `projects/${this._project}/locations/${this._location}/services/${this._serviceName}/connectors/${this._connectorName}`;
|
|
199
|
+
this.authProvider?.addTokenChangeListener(token => {
|
|
200
|
+
logDebug(`New Token Available: ${token}`);
|
|
201
|
+
this.onAuthTokenChanged(token);
|
|
202
|
+
});
|
|
203
|
+
this.appCheckProvider?.addTokenChangeListener(result => {
|
|
204
|
+
const { token } = result;
|
|
205
|
+
logDebug(`New App Check Token Available: ${token}`);
|
|
206
|
+
this._appCheckToken = token;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
useEmulator(host, port, isSecure) {
|
|
210
|
+
this._host = host;
|
|
211
|
+
this._isUsingEmulator = true;
|
|
212
|
+
if (typeof port === 'number') {
|
|
213
|
+
this._port = port;
|
|
214
|
+
}
|
|
215
|
+
if (typeof isSecure !== 'undefined') {
|
|
216
|
+
this._secure = isSecure;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async getWithAuth(forceToken = false) {
|
|
220
|
+
let starterPromise = new Promise(resolve => resolve(this._authToken));
|
|
221
|
+
if (this.appCheckProvider) {
|
|
222
|
+
const appCheckToken = await this.appCheckProvider.getToken();
|
|
223
|
+
if (appCheckToken) {
|
|
224
|
+
this._appCheckToken = appCheckToken.token;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (this.authProvider) {
|
|
228
|
+
starterPromise = this.authProvider
|
|
229
|
+
.getToken(/*forceToken=*/ forceToken)
|
|
230
|
+
.then(data => {
|
|
231
|
+
if (!data) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
this._authToken = data.accessToken;
|
|
235
|
+
return this._authToken;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
starterPromise = new Promise(resolve => resolve(''));
|
|
240
|
+
}
|
|
241
|
+
return starterPromise;
|
|
242
|
+
}
|
|
243
|
+
async withRetry(promiseFactory, retry = false) {
|
|
244
|
+
let isNewToken = false;
|
|
245
|
+
return this.getWithAuth(retry)
|
|
246
|
+
.then(res => {
|
|
247
|
+
isNewToken = this._lastToken !== res;
|
|
248
|
+
this._lastToken = res;
|
|
249
|
+
return res;
|
|
250
|
+
})
|
|
251
|
+
.then(promiseFactory)
|
|
252
|
+
.catch(err => {
|
|
253
|
+
// Only retry if the result is unauthorized and the last token isn't the same as the new one.
|
|
254
|
+
if ('code' in err &&
|
|
255
|
+
err.code === Code.UNAUTHORIZED &&
|
|
256
|
+
!retry &&
|
|
257
|
+
isNewToken) {
|
|
258
|
+
logDebug('Retrying due to unauthorized');
|
|
259
|
+
return this.withRetry(promiseFactory, true);
|
|
260
|
+
}
|
|
261
|
+
throw err;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
_setLastToken(lastToken) {
|
|
265
|
+
this._lastToken = lastToken;
|
|
266
|
+
}
|
|
267
|
+
_setCallerSdkType(callerSdkType) {
|
|
268
|
+
this._callerSdkType = callerSdkType;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
139
271
|
|
|
140
272
|
/**
|
|
141
273
|
* @license
|
|
@@ -153,7 +285,13 @@ const CallerSdkTypeEnum = {
|
|
|
153
285
|
* See the License for the specific language governing permissions and
|
|
154
286
|
* limitations under the License.
|
|
155
287
|
*/
|
|
288
|
+
/** The fetch implementation to be used by the {@link RESTTransport}. */
|
|
156
289
|
let connectFetch = globalThis.fetch;
|
|
290
|
+
/**
|
|
291
|
+
* This function is ONLY used for testing and for ensuring compatability in environments which may
|
|
292
|
+
* be using a poyfill and/or bundlers. It should not be called by users of the Firebase JS SDK.
|
|
293
|
+
* @internal
|
|
294
|
+
*/
|
|
157
295
|
function initializeFetch(fetchImpl) {
|
|
158
296
|
connectFetch = fetchImpl;
|
|
159
297
|
}
|
|
@@ -200,14 +338,20 @@ async function dcFetch(url, body, { signal }, appId, accessToken, appCheckToken,
|
|
|
200
338
|
response = await connectFetch(url, fetchOptions);
|
|
201
339
|
}
|
|
202
340
|
catch (err) {
|
|
203
|
-
|
|
341
|
+
const message = err && typeof err === 'object' && 'message' in err
|
|
342
|
+
? err['message']
|
|
343
|
+
: String(err);
|
|
344
|
+
throw new DataConnectError(Code.OTHER, 'Failed to fetch: ' + message);
|
|
204
345
|
}
|
|
205
346
|
let jsonResponse;
|
|
206
347
|
try {
|
|
207
348
|
jsonResponse = await response.json();
|
|
208
349
|
}
|
|
209
350
|
catch (e) {
|
|
210
|
-
|
|
351
|
+
const message = e && typeof e === 'object' && 'message' in e
|
|
352
|
+
? e['message']
|
|
353
|
+
: String(e);
|
|
354
|
+
throw new DataConnectError(Code.OTHER, 'Failed to parse JSON response: ' + message);
|
|
211
355
|
}
|
|
212
356
|
const message = getErrorMessage(jsonResponse);
|
|
213
357
|
if (response.status >= 400) {
|
|
@@ -215,32 +359,918 @@ async function dcFetch(url, body, { signal }, appId, accessToken, appCheckToken,
|
|
|
215
359
|
if (response.status === 401) {
|
|
216
360
|
throw new DataConnectError(Code.UNAUTHORIZED, message);
|
|
217
361
|
}
|
|
218
|
-
throw new DataConnectError(Code.OTHER, message);
|
|
362
|
+
throw new DataConnectError(Code.OTHER, message);
|
|
363
|
+
}
|
|
364
|
+
if (jsonResponse.errors && jsonResponse.errors.length) {
|
|
365
|
+
const stringified = JSON.stringify(jsonResponse.errors);
|
|
366
|
+
const failureResponse = {
|
|
367
|
+
errors: jsonResponse.errors,
|
|
368
|
+
data: jsonResponse.data
|
|
369
|
+
};
|
|
370
|
+
throw new DataConnectOperationError('DataConnect error while performing request: ' + stringified, failureResponse);
|
|
371
|
+
}
|
|
372
|
+
if (!jsonResponse.extensions) {
|
|
373
|
+
jsonResponse.extensions = {
|
|
374
|
+
dataConnect: []
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return jsonResponse;
|
|
378
|
+
}
|
|
379
|
+
function getErrorMessage(obj) {
|
|
380
|
+
if ('message' in obj && obj.message) {
|
|
381
|
+
return obj.message;
|
|
382
|
+
}
|
|
383
|
+
return JSON.stringify(obj);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* @license
|
|
388
|
+
* Copyright 2024 Google LLC
|
|
389
|
+
*
|
|
390
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
391
|
+
* you may not use this file except in compliance with the License.
|
|
392
|
+
* You may obtain a copy of the License at
|
|
393
|
+
*
|
|
394
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
395
|
+
*
|
|
396
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
397
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
398
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
399
|
+
* See the License for the specific language governing permissions and
|
|
400
|
+
* limitations under the License.
|
|
401
|
+
*/
|
|
402
|
+
const PROD_HOST = 'firebasedataconnect.googleapis.com';
|
|
403
|
+
const WEBSOCKET_PATH = 'ws/google.firebase.dataconnect.v1.ConnectorStreamService';
|
|
404
|
+
function restUrlBuilder(projectConfig, transportOptions) {
|
|
405
|
+
const { connector, location, projectId: project, service } = projectConfig;
|
|
406
|
+
const { host, sslEnabled, port } = transportOptions;
|
|
407
|
+
const protocol = sslEnabled ? 'https' : 'http';
|
|
408
|
+
const realHost = host || PROD_HOST;
|
|
409
|
+
let baseUrl = `${protocol}://${realHost}`;
|
|
410
|
+
if (typeof port === 'number') {
|
|
411
|
+
baseUrl += `:${port}`;
|
|
412
|
+
}
|
|
413
|
+
else if (typeof port !== 'undefined') {
|
|
414
|
+
logError('Port type is of an invalid type');
|
|
415
|
+
throw new DataConnectError(Code.INVALID_ARGUMENT, 'Incorrect type for port passed in!');
|
|
416
|
+
}
|
|
417
|
+
return `${baseUrl}/v1/projects/${project}/locations/${location}/services/${service}/connectors/${connector}`;
|
|
418
|
+
}
|
|
419
|
+
function websocketUrlBuilder(projectConfig, transportOptions) {
|
|
420
|
+
const { location } = projectConfig;
|
|
421
|
+
const { host, sslEnabled, port } = transportOptions;
|
|
422
|
+
const protocol = sslEnabled ? 'wss' : 'ws';
|
|
423
|
+
const realHost = host || PROD_HOST;
|
|
424
|
+
let baseUrl = `${protocol}://${realHost}`;
|
|
425
|
+
if (typeof port === 'number') {
|
|
426
|
+
baseUrl += `:${port}`;
|
|
427
|
+
}
|
|
428
|
+
else if (typeof port !== 'undefined') {
|
|
429
|
+
logError('Port type is of an invalid type');
|
|
430
|
+
throw new DataConnectError(Code.INVALID_ARGUMENT, 'Incorrect type for port passed in!');
|
|
431
|
+
}
|
|
432
|
+
return `${baseUrl}/${WEBSOCKET_PATH}/Connect/locations/${location}`;
|
|
433
|
+
}
|
|
434
|
+
function addToken(url, apiKey) {
|
|
435
|
+
if (!apiKey) {
|
|
436
|
+
return url;
|
|
437
|
+
}
|
|
438
|
+
const newUrl = new URL(url);
|
|
439
|
+
newUrl.searchParams.append('key', apiKey);
|
|
440
|
+
return newUrl.toString();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* @license
|
|
445
|
+
* Copyright 2024 Google LLC
|
|
446
|
+
*
|
|
447
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
448
|
+
* you may not use this file except in compliance with the License.
|
|
449
|
+
* You may obtain a copy of the License at
|
|
450
|
+
*
|
|
451
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
452
|
+
*
|
|
453
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
454
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
455
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
456
|
+
* See the License for the specific language governing permissions and
|
|
457
|
+
* limitations under the License.
|
|
458
|
+
*/
|
|
459
|
+
/**
|
|
460
|
+
* Fetch-based REST implementation of {@link AbstractDataConnectTransport}.
|
|
461
|
+
* @internal
|
|
462
|
+
*/
|
|
463
|
+
class RESTTransport extends AbstractDataConnectTransport {
|
|
464
|
+
constructor(options, apiKey, appId, authProvider, appCheckProvider, transportOptions, _isUsingGen = false, _callerSdkType = CallerSdkTypeEnum.Base) {
|
|
465
|
+
super(options, apiKey, appId, authProvider, appCheckProvider, transportOptions, _isUsingGen, _callerSdkType);
|
|
466
|
+
this.invokeQuery = (queryName, body) => {
|
|
467
|
+
const abortController = new AbortController();
|
|
468
|
+
// TODO(mtewani): Update to proper value
|
|
469
|
+
const withAuth = this.withRetry(() => dcFetch(addToken(`${this.endpointUrl}:executeQuery`, this.apiKey), {
|
|
470
|
+
name: this._connectorResourcePath,
|
|
471
|
+
operationName: queryName,
|
|
472
|
+
variables: body
|
|
473
|
+
}, abortController, this.appId, this._authToken, this._appCheckToken, this._isUsingGen, this._callerSdkType, this._isUsingEmulator));
|
|
474
|
+
return withAuth;
|
|
475
|
+
};
|
|
476
|
+
this.invokeMutation = (mutationName, body) => {
|
|
477
|
+
const abortController = new AbortController();
|
|
478
|
+
const taskResult = this.withRetry(() => {
|
|
479
|
+
return dcFetch(addToken(`${this.endpointUrl}:executeMutation`, this.apiKey), {
|
|
480
|
+
name: this._connectorResourcePath,
|
|
481
|
+
operationName: mutationName,
|
|
482
|
+
variables: body
|
|
483
|
+
}, abortController, this.appId, this._authToken, this._appCheckToken, this._isUsingGen, this._callerSdkType, this._isUsingEmulator);
|
|
484
|
+
});
|
|
485
|
+
return taskResult;
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
get endpointUrl() {
|
|
489
|
+
return restUrlBuilder({
|
|
490
|
+
connector: this._connectorName,
|
|
491
|
+
location: this._location,
|
|
492
|
+
projectId: this._project,
|
|
493
|
+
service: this._serviceName
|
|
494
|
+
}, {
|
|
495
|
+
host: this._host,
|
|
496
|
+
sslEnabled: this._secure,
|
|
497
|
+
port: this._port
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
invokeSubscribe(observer, queryName, body) {
|
|
501
|
+
throw new DataConnectError(Code.NOT_SUPPORTED, 'Subscriptions are not supported using REST!');
|
|
502
|
+
}
|
|
503
|
+
invokeUnsubscribe(queryName, body) {
|
|
504
|
+
throw new DataConnectError(Code.NOT_SUPPORTED, 'Unsubscriptions are not supported using REST!');
|
|
505
|
+
}
|
|
506
|
+
onAuthTokenChanged(newToken) {
|
|
507
|
+
this._authToken = newToken;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* @license
|
|
513
|
+
* Copyright 2026 Google LLC
|
|
514
|
+
*
|
|
515
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
516
|
+
* you may not use this file except in compliance with the License.
|
|
517
|
+
* You may obtain a copy of the License at
|
|
518
|
+
*
|
|
519
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
520
|
+
*
|
|
521
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
522
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
523
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
524
|
+
* See the License for the specific language governing permissions and
|
|
525
|
+
* limitations under the License.
|
|
526
|
+
*/
|
|
527
|
+
/** The request id of the first request over the stream */
|
|
528
|
+
const FIRST_REQUEST_ID = 1;
|
|
529
|
+
/** Time to wait before closing an idle connection (no active subscriptions) */
|
|
530
|
+
const IDLE_CONNECTION_TIMEOUT_MS = 60 * 1000; // 1 minute
|
|
531
|
+
/**
|
|
532
|
+
* The base class for all {@link DataConnectStreamTransport | Stream Transport} implementations.
|
|
533
|
+
* Handles management of logical streams (requests), authentication, data routing to query layer, etc.
|
|
534
|
+
* @internal
|
|
535
|
+
*/
|
|
536
|
+
class AbstractDataConnectStreamTransport extends AbstractDataConnectTransport {
|
|
537
|
+
constructor() {
|
|
538
|
+
super(...arguments);
|
|
539
|
+
this.pendingClose = false;
|
|
540
|
+
/** True if the transport is unable to connect to the server */
|
|
541
|
+
this.isUnableToConnect = false;
|
|
542
|
+
/** The request ID of the next message to be sent. Monotonically increasing sequence number. */
|
|
543
|
+
this.requestNumber = FIRST_REQUEST_ID;
|
|
544
|
+
/**
|
|
545
|
+
* Map of query/variables to their active execute/resume request bodies.
|
|
546
|
+
*/
|
|
547
|
+
this.activeQueryExecuteRequests = new Map();
|
|
548
|
+
/**
|
|
549
|
+
* Map of mutation/variables to their active execute request bodies.
|
|
550
|
+
*/
|
|
551
|
+
this.activeMutationExecuteRequests = new Map();
|
|
552
|
+
/**
|
|
553
|
+
* Map of query/variables to their active subscribe request bodies.
|
|
554
|
+
*/
|
|
555
|
+
this.activeSubscribeRequests = new Map();
|
|
556
|
+
/**
|
|
557
|
+
* Map of active execution RequestIds and their corresponding Promises and resolvers.
|
|
558
|
+
*/
|
|
559
|
+
this.executeRequestPromises = new Map();
|
|
560
|
+
/**
|
|
561
|
+
* Map of active subscription RequestIds and their corresponding observers.
|
|
562
|
+
*/
|
|
563
|
+
this.subscribeObservers = new Map();
|
|
564
|
+
/** current close timeout from setTimeout(), if any */
|
|
565
|
+
this.closeTimeout = null;
|
|
566
|
+
/** has the close timeout finished? */
|
|
567
|
+
this.closeTimeoutFinished = false;
|
|
568
|
+
/** Flag to ensure we wait for the initial auth state once per connection attempt. */
|
|
569
|
+
this.hasWaitedForInitialAuth = false;
|
|
570
|
+
/**
|
|
571
|
+
* Tracks if the next message to be sent is the first message of the stream.
|
|
572
|
+
*/
|
|
573
|
+
this.isFirstStreamMessage = true;
|
|
574
|
+
/**
|
|
575
|
+
* Tracks the last auth token sent to the server.
|
|
576
|
+
* Used to detect if the token has changed and needs to be resent.
|
|
577
|
+
*/
|
|
578
|
+
this.lastSentAuthToken = null;
|
|
579
|
+
}
|
|
580
|
+
/** Is the stream currently waiting to close connection? */
|
|
581
|
+
get isPendingClose() {
|
|
582
|
+
return this.pendingClose;
|
|
583
|
+
}
|
|
584
|
+
/** True if there are active subscriptions on the stream */
|
|
585
|
+
get hasActiveSubscriptions() {
|
|
586
|
+
return this.activeSubscribeRequests.size > 0;
|
|
587
|
+
}
|
|
588
|
+
/** True if there are active execute or mutation requests on the stream */
|
|
589
|
+
get hasActiveExecuteRequests() {
|
|
590
|
+
return (this.activeQueryExecuteRequests.size > 0 ||
|
|
591
|
+
this.activeMutationExecuteRequests.size > 0);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Generates and returns the next request ID.
|
|
595
|
+
*/
|
|
596
|
+
nextRequestId() {
|
|
597
|
+
return (this.requestNumber++).toString();
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Tracks a query execution request, storing the request body and creating and storing a promise that
|
|
601
|
+
* will be resolved when the response is received.
|
|
602
|
+
* @returns The reject function and the response promise.
|
|
603
|
+
*
|
|
604
|
+
* @remarks
|
|
605
|
+
* This method returns a promise, but is synchronous.
|
|
606
|
+
*/
|
|
607
|
+
trackQueryExecuteRequest(requestId, mapKey, executeBody) {
|
|
608
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
609
|
+
let resolveFn;
|
|
610
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
611
|
+
let rejectFn;
|
|
612
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
613
|
+
resolveFn = resolve;
|
|
614
|
+
rejectFn = reject;
|
|
615
|
+
});
|
|
616
|
+
const executeRequestPromise = {
|
|
617
|
+
responsePromise,
|
|
618
|
+
resolveFn: resolveFn,
|
|
619
|
+
rejectFn: rejectFn
|
|
620
|
+
};
|
|
621
|
+
this.activeQueryExecuteRequests.set(mapKey, executeBody);
|
|
622
|
+
this.executeRequestPromises.set(requestId, executeRequestPromise);
|
|
623
|
+
return executeRequestPromise;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Tracks a mutation execution request, storing the request body and creating and storing a promise
|
|
627
|
+
* that will be resolved when the response is received.
|
|
628
|
+
* @returns The reject function and the response promise.
|
|
629
|
+
*
|
|
630
|
+
* @remarks
|
|
631
|
+
* This method returns a promise, but is synchronous.
|
|
632
|
+
*/
|
|
633
|
+
trackMutationExecuteRequest(requestId, mapKey, executeBody) {
|
|
634
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
635
|
+
let resolveFn;
|
|
636
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
637
|
+
let rejectFn;
|
|
638
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
639
|
+
resolveFn = resolve;
|
|
640
|
+
rejectFn = reject;
|
|
641
|
+
});
|
|
642
|
+
const executeRequestPromise = {
|
|
643
|
+
responsePromise,
|
|
644
|
+
resolveFn: resolveFn,
|
|
645
|
+
rejectFn: rejectFn
|
|
646
|
+
};
|
|
647
|
+
const activeRequests = this.activeMutationExecuteRequests.get(mapKey) || [];
|
|
648
|
+
activeRequests.push(executeBody);
|
|
649
|
+
this.activeMutationExecuteRequests.set(mapKey, activeRequests);
|
|
650
|
+
this.executeRequestPromises.set(requestId, executeRequestPromise);
|
|
651
|
+
return executeRequestPromise;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Tracks a subscribe request, storing the request body and the notification observer.
|
|
655
|
+
* @remarks
|
|
656
|
+
* This method is synchronous.
|
|
657
|
+
*/
|
|
658
|
+
trackSubscribeRequest(requestId, mapKey, subscribeBody, observer) {
|
|
659
|
+
this.activeSubscribeRequests.set(mapKey, subscribeBody);
|
|
660
|
+
this.subscribeObservers.set(requestId, observer);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Cleans up the query execute request tracking data structures, deleting the tracked request and
|
|
664
|
+
* it's associated promise.
|
|
665
|
+
*/
|
|
666
|
+
cleanupQueryExecuteRequest(requestId, mapKey) {
|
|
667
|
+
this.activeQueryExecuteRequests.delete(mapKey);
|
|
668
|
+
this.executeRequestPromises.delete(requestId);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Cleans up the mutation execute request tracking data structures, deleting the tracked request and
|
|
672
|
+
* it's associated promise.
|
|
673
|
+
*/
|
|
674
|
+
cleanupMutationExecuteRequest(requestId, mapKey) {
|
|
675
|
+
const executeRequests = this.activeMutationExecuteRequests.get(mapKey);
|
|
676
|
+
if (executeRequests) {
|
|
677
|
+
const updatedRequests = executeRequests.filter(req => req.requestId !== requestId);
|
|
678
|
+
if (updatedRequests.length > 0) {
|
|
679
|
+
this.activeMutationExecuteRequests.set(mapKey, updatedRequests);
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
this.activeMutationExecuteRequests.delete(mapKey);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
this.executeRequestPromises.delete(requestId);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Cleans up the subscribe request tracking data structures, deleting the tracked request and
|
|
689
|
+
* it's associated promise.
|
|
690
|
+
*/
|
|
691
|
+
cleanupSubscribeRequest(requestId, mapKey) {
|
|
692
|
+
this.activeSubscribeRequests.delete(mapKey);
|
|
693
|
+
this.subscribeObservers.delete(requestId);
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Indicates whether we should include the auth token in the next message.
|
|
697
|
+
* Only true if there is an auth token and it is different from the last sent auth token, or this
|
|
698
|
+
* is the first message.
|
|
699
|
+
*/
|
|
700
|
+
get shouldIncludeAuth() {
|
|
701
|
+
return (this.isFirstStreamMessage ||
|
|
702
|
+
(!!this._authToken && this._authToken !== this.lastSentAuthToken));
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Called by the concrete transport implementation when the physical connection is ready.
|
|
706
|
+
*/
|
|
707
|
+
onConnectionReady() {
|
|
708
|
+
this.isFirstStreamMessage = true;
|
|
709
|
+
this.lastSentAuthToken = null;
|
|
710
|
+
this.hasWaitedForInitialAuth = false;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Attempt to close the connection. Will only close if there are no active requests preventing it
|
|
714
|
+
* from doing so.
|
|
715
|
+
*/
|
|
716
|
+
async attemptClose() {
|
|
717
|
+
if (this.hasActiveSubscriptions || this.hasActiveExecuteRequests) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
this.cancelClose();
|
|
721
|
+
await this.closeConnection();
|
|
722
|
+
this.onGracefulStreamClose?.();
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Begin closing the connection. Waits for and cleans up all active requests, and waits for
|
|
726
|
+
* {@link IDLE_CONNECTION_TIMEOUT_MS}. This is a graceful close - it will be called when there are
|
|
727
|
+
* no more active subscriptions, so there's no need to cleanup.
|
|
728
|
+
*/
|
|
729
|
+
prepareToCloseGracefully() {
|
|
730
|
+
if (this.pendingClose) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
this.pendingClose = true;
|
|
734
|
+
this.closeTimeoutFinished = false;
|
|
735
|
+
this.closeTimeout = setTimeout(() => {
|
|
736
|
+
this.closeTimeoutFinished = true;
|
|
737
|
+
void this.attemptClose();
|
|
738
|
+
}, IDLE_CONNECTION_TIMEOUT_MS);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Cancel closing the connection.
|
|
742
|
+
*/
|
|
743
|
+
cancelClose() {
|
|
744
|
+
if (this.closeTimeout) {
|
|
745
|
+
clearTimeout(this.closeTimeout);
|
|
746
|
+
}
|
|
747
|
+
this.pendingClose = false;
|
|
748
|
+
this.closeTimeoutFinished = false;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Reject all active execute promises and notify all subscribe observers with the given error.
|
|
752
|
+
* Clear active request tracking maps without cancelling or re-invoking any requests.
|
|
753
|
+
*/
|
|
754
|
+
rejectAllActiveRequests(code, reason) {
|
|
755
|
+
this.activeQueryExecuteRequests.clear();
|
|
756
|
+
this.activeMutationExecuteRequests.clear();
|
|
757
|
+
this.activeSubscribeRequests.clear();
|
|
758
|
+
const error = new DataConnectError(code, reason);
|
|
759
|
+
for (const [requestId, { rejectFn }] of this.executeRequestPromises) {
|
|
760
|
+
this.executeRequestPromises.delete(requestId);
|
|
761
|
+
rejectFn(error);
|
|
762
|
+
}
|
|
763
|
+
for (const [requestId, observer] of this.subscribeObservers) {
|
|
764
|
+
this.subscribeObservers.delete(requestId);
|
|
765
|
+
observer.onDisconnect(code, reason);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Called by concrete implementations when the stream is successfully closed, gracefully or otherwise.
|
|
770
|
+
*/
|
|
771
|
+
onStreamClose(code, reason) {
|
|
772
|
+
this.rejectAllActiveRequests(Code.OTHER, `Stream disconnected with code ${code}: ${reason}`);
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Prepares a stream request message by adding necessary headers and metadata.
|
|
776
|
+
* If this is the first message on the stream, it includes the resource name, auth token, and App Check token.
|
|
777
|
+
* If the auth token has refreshed since the last message, it includes the new auth token.
|
|
778
|
+
*
|
|
779
|
+
* This method is called by the concrete transport implementation before sending a message.
|
|
780
|
+
*
|
|
781
|
+
* @returns the requestBody, with attached headers and initial request fields
|
|
782
|
+
*/
|
|
783
|
+
prepareMessage(requestBody) {
|
|
784
|
+
const preparedRequestBody = { ...requestBody };
|
|
785
|
+
const headers = {};
|
|
786
|
+
if (this.appId) {
|
|
787
|
+
headers['x-firebase-gmpid'] = this.appId;
|
|
788
|
+
}
|
|
789
|
+
headers['X-Goog-Api-Client'] = getGoogApiClientValue$1(this._isUsingGen, this._callerSdkType);
|
|
790
|
+
if (this.shouldIncludeAuth && this._authToken) {
|
|
791
|
+
headers['X-Firebase-Auth-Token'] = this._authToken;
|
|
792
|
+
this.lastSentAuthToken = this._authToken;
|
|
793
|
+
}
|
|
794
|
+
if (this.isFirstStreamMessage) {
|
|
795
|
+
if (this._appCheckToken) {
|
|
796
|
+
headers['X-Firebase-App-Check'] = this._appCheckToken;
|
|
797
|
+
}
|
|
798
|
+
preparedRequestBody.name = this._connectorResourcePath;
|
|
799
|
+
}
|
|
800
|
+
preparedRequestBody.headers = headers;
|
|
801
|
+
this.isFirstStreamMessage = false;
|
|
802
|
+
return preparedRequestBody;
|
|
803
|
+
}
|
|
804
|
+
// TODO(stephenarosaj): just make this async
|
|
805
|
+
/**
|
|
806
|
+
* Sends a request message to the server via the concrete implementation.
|
|
807
|
+
* Ensures the connection is ready and prepares the message before sending.
|
|
808
|
+
* @returns A promise that resolves when the request message has been sent.
|
|
809
|
+
*/
|
|
810
|
+
async sendRequestMessage(requestBody) {
|
|
811
|
+
if (!this.hasWaitedForInitialAuth && this.authProvider) {
|
|
812
|
+
await this.getWithAuth();
|
|
813
|
+
this.hasWaitedForInitialAuth = true;
|
|
814
|
+
}
|
|
815
|
+
if (this.streamIsReady) {
|
|
816
|
+
const prepared = this.prepareMessage(requestBody);
|
|
817
|
+
return this.sendMessage(prepared);
|
|
818
|
+
}
|
|
819
|
+
return this.ensureConnection().then(() => {
|
|
820
|
+
const prepared = this.prepareMessage(requestBody);
|
|
821
|
+
return this.sendMessage(prepared);
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Helper to generate a consistent string key for the tracking maps.
|
|
826
|
+
*/
|
|
827
|
+
getMapKey(operationName, variables) {
|
|
828
|
+
const sortedVariables = this.sortObjectKeys(variables);
|
|
829
|
+
return JSON.stringify({ operationName, variables: sortedVariables });
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Recursively sorts the keys of an object.
|
|
833
|
+
*/
|
|
834
|
+
sortObjectKeys(obj) {
|
|
835
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
836
|
+
return obj;
|
|
837
|
+
}
|
|
838
|
+
const sortedObj = {};
|
|
839
|
+
Object.keys(obj)
|
|
840
|
+
.sort()
|
|
841
|
+
.forEach(key => {
|
|
842
|
+
sortedObj[key] = this.sortObjectKeys(obj[key]);
|
|
843
|
+
});
|
|
844
|
+
return sortedObj;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* @inheritdoc
|
|
848
|
+
* @remarks
|
|
849
|
+
* This method synchronously updates the request tracking data structures before sending any message.
|
|
850
|
+
* If any asynchronous functionality is added to this function, it MUST be done in a way that
|
|
851
|
+
* preserves the synchronous update of the tracking data structures before the method returns.
|
|
852
|
+
*/
|
|
853
|
+
invokeQuery(queryName, variables) {
|
|
854
|
+
const requestId = this.nextRequestId();
|
|
855
|
+
const activeRequestKey = { operationName: queryName, variables };
|
|
856
|
+
const mapKey = this.getMapKey(queryName, variables);
|
|
857
|
+
const executeBody = {
|
|
858
|
+
requestId,
|
|
859
|
+
execute: activeRequestKey
|
|
860
|
+
};
|
|
861
|
+
let { responsePromise, rejectFn } = this.trackQueryExecuteRequest(requestId, mapKey, executeBody);
|
|
862
|
+
responsePromise = responsePromise.finally(() => {
|
|
863
|
+
this.cleanupQueryExecuteRequest(requestId, mapKey);
|
|
864
|
+
if (!this.hasActiveSubscriptions &&
|
|
865
|
+
!this.hasActiveExecuteRequests &&
|
|
866
|
+
this.closeTimeoutFinished) {
|
|
867
|
+
void this.attemptClose();
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
// asynchronous, fire and forget
|
|
871
|
+
this.sendRequestMessage(executeBody).catch(err => {
|
|
872
|
+
rejectFn(err);
|
|
873
|
+
});
|
|
874
|
+
return responsePromise;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* @inheritdoc
|
|
878
|
+
* @remarks
|
|
879
|
+
* This method synchronously updates the request tracking data structures before sending any message.
|
|
880
|
+
* If any asynchronous functionality is added to this function, it MUST be done in a way that
|
|
881
|
+
* preserves the synchronous update of the tracking data structures before the method returns.
|
|
882
|
+
*/
|
|
883
|
+
invokeMutation(mutationName, variables) {
|
|
884
|
+
const requestId = this.nextRequestId();
|
|
885
|
+
const activeRequestKey = { operationName: mutationName, variables };
|
|
886
|
+
const mapKey = this.getMapKey(mutationName, variables);
|
|
887
|
+
const executeBody = {
|
|
888
|
+
requestId,
|
|
889
|
+
execute: activeRequestKey
|
|
890
|
+
};
|
|
891
|
+
let { responsePromise, rejectFn } = this.trackMutationExecuteRequest(requestId, mapKey, executeBody);
|
|
892
|
+
responsePromise = responsePromise.finally(() => {
|
|
893
|
+
this.cleanupMutationExecuteRequest(requestId, mapKey);
|
|
894
|
+
if (!this.hasActiveSubscriptions &&
|
|
895
|
+
!this.hasActiveExecuteRequests &&
|
|
896
|
+
this.closeTimeoutFinished) {
|
|
897
|
+
void this.attemptClose();
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
// asynchronous, fire and forget
|
|
901
|
+
this.sendRequestMessage(executeBody).catch(err => {
|
|
902
|
+
rejectFn(err);
|
|
903
|
+
});
|
|
904
|
+
return responsePromise;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* @inheritdoc
|
|
908
|
+
* @remarks
|
|
909
|
+
* This method synchronously updates the request tracking data structures before sending any message
|
|
910
|
+
* or cancelling the closing of the stream. If any asynchronous functionality is added to this function,
|
|
911
|
+
* it MUST be done in a way that preserves the synchronous update of the tracking data structures
|
|
912
|
+
* before the method returns.
|
|
913
|
+
*/
|
|
914
|
+
invokeSubscribe(observer, queryName, variables) {
|
|
915
|
+
// if we are waiting to close the stream, cancel closing!
|
|
916
|
+
this.cancelClose();
|
|
917
|
+
const requestId = this.nextRequestId();
|
|
918
|
+
const activeRequestKey = { operationName: queryName, variables };
|
|
919
|
+
const mapKey = this.getMapKey(queryName, variables);
|
|
920
|
+
const subscribeBody = {
|
|
921
|
+
requestId,
|
|
922
|
+
subscribe: activeRequestKey
|
|
923
|
+
};
|
|
924
|
+
this.trackSubscribeRequest(requestId, mapKey, subscribeBody, observer);
|
|
925
|
+
// asynchronous, fire and forget
|
|
926
|
+
this.sendRequestMessage(subscribeBody).catch(err => {
|
|
927
|
+
observer.onError(err instanceof Error ? err : new Error(String(err)));
|
|
928
|
+
this.cleanupSubscribeRequest(requestId, mapKey);
|
|
929
|
+
if (!this.hasActiveSubscriptions) {
|
|
930
|
+
this.prepareToCloseGracefully();
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* @inheritdoc
|
|
936
|
+
* @remarks
|
|
937
|
+
* This method synchronously updates the request tracking data structures before sending any message.
|
|
938
|
+
* If any asynchronous functionality is added to this function, it MUST be done in a way that
|
|
939
|
+
* preserves the synchronous update of the tracking data structures before the method returns.
|
|
940
|
+
*/
|
|
941
|
+
invokeUnsubscribe(queryName, variables) {
|
|
942
|
+
const mapKey = this.getMapKey(queryName, variables);
|
|
943
|
+
const subscribeRequest = this.activeSubscribeRequests.get(mapKey);
|
|
944
|
+
if (!subscribeRequest) {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const requestId = subscribeRequest.requestId;
|
|
948
|
+
const cancelBody = {
|
|
949
|
+
requestId,
|
|
950
|
+
cancel: {}
|
|
951
|
+
};
|
|
952
|
+
this.cleanupSubscribeRequest(requestId, mapKey);
|
|
953
|
+
// asynchronous, fire and forget
|
|
954
|
+
this.sendRequestMessage(cancelBody).catch(err => {
|
|
955
|
+
logError(`Stream Transport failed to send unsubscribe message: ${err}`);
|
|
956
|
+
});
|
|
957
|
+
if (!this.hasActiveSubscriptions) {
|
|
958
|
+
this.prepareToCloseGracefully();
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
onAuthTokenChanged(newToken) {
|
|
962
|
+
const oldAuthToken = this._authToken;
|
|
963
|
+
this._authToken = newToken;
|
|
964
|
+
const oldAuthUid = this.authUid;
|
|
965
|
+
const newAuthUid = this.authProvider?.getAuth()?.getUid();
|
|
966
|
+
this.authUid = newAuthUid;
|
|
967
|
+
// onAuthTokenChanged gets called by the auth provider once it initializes, so we must make sure
|
|
968
|
+
// we don't prematurely disconnect the stream if this is the initial call.
|
|
969
|
+
const isInitialAuth = oldAuthUid === undefined;
|
|
970
|
+
if (isInitialAuth) {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
if ((oldAuthToken && newToken === null) || // user logged out
|
|
974
|
+
(!oldAuthUid && newAuthUid) || // user logged in
|
|
975
|
+
(oldAuthUid && newAuthUid !== oldAuthUid) // logged in user changed
|
|
976
|
+
) {
|
|
977
|
+
this.rejectAllActiveRequests(Code.UNAUTHORIZED, 'Stream disconnected due to auth change.');
|
|
978
|
+
void this.attemptClose();
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Handle a response message from the server. Called by the connection-specific implementation after
|
|
983
|
+
* it's transformed a message from the server into a {@link DataConnectResponse}.
|
|
984
|
+
* @param requestId the requestId associated with this response.
|
|
985
|
+
* @param response the response from the server.
|
|
986
|
+
*/
|
|
987
|
+
async handleResponse(requestId, response) {
|
|
988
|
+
if (this.executeRequestPromises.has(requestId)) {
|
|
989
|
+
// don't clean up the tracking maps here, they're handled automatically when the execute promise settles
|
|
990
|
+
const { resolveFn, rejectFn } = this.executeRequestPromises.get(requestId);
|
|
991
|
+
if (response.errors && response.errors.length) {
|
|
992
|
+
const failureResponse = {
|
|
993
|
+
errors: response.errors,
|
|
994
|
+
data: response.data
|
|
995
|
+
};
|
|
996
|
+
const stringified = JSON.stringify(response.errors);
|
|
997
|
+
rejectFn(new DataConnectOperationError('DataConnect error while performing request: ' + stringified, failureResponse));
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
resolveFn(response);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
else if (this.subscribeObservers.has(requestId)) {
|
|
1004
|
+
const observer = this.subscribeObservers.get(requestId);
|
|
1005
|
+
await observer.onData(response);
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
throw new DataConnectError(Code.OTHER, `Stream response contained unrecognized requestId '${requestId}'`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* @license
|
|
1015
|
+
* Copyright 2026 Google LLC
|
|
1016
|
+
*
|
|
1017
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1018
|
+
* you may not use this file except in compliance with the License.
|
|
1019
|
+
* You may obtain a copy of the License at
|
|
1020
|
+
*
|
|
1021
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1022
|
+
*
|
|
1023
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1024
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1025
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1026
|
+
* See the License for the specific language governing permissions and
|
|
1027
|
+
* limitations under the License.
|
|
1028
|
+
*/
|
|
1029
|
+
/** The WebSocket implementation to be used by the {@link WebSocketTransport}. */
|
|
1030
|
+
let connectWebSocket = globalThis.WebSocket;
|
|
1031
|
+
/**
|
|
1032
|
+
* This function is ONLY used for testing and for ensuring compatability in environments which may
|
|
1033
|
+
* be using a poyfill and/or bundlers. It should not be called by users of the Firebase JS SDK.
|
|
1034
|
+
* @internal
|
|
1035
|
+
*/
|
|
1036
|
+
function initializeWebSocket(webSocketImpl) {
|
|
1037
|
+
connectWebSocket = webSocketImpl;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* The code used to close the WebSocket connection.
|
|
1041
|
+
* This is a protocol-level code, and is not the same as the {@link Code | DataConnect error code}.
|
|
1042
|
+
* @internal
|
|
1043
|
+
*/
|
|
1044
|
+
const WEBSOCKET_CLOSE_CODE = 1000;
|
|
1045
|
+
/**
|
|
1046
|
+
* An {@link AbstractDataConnectStreamTransport | Stream Transport} implementation that uses {@link WebSocket | WebSockets} to stream requests and responses.
|
|
1047
|
+
* This class handles the lifecycle of the WebSocket connection, including automatic
|
|
1048
|
+
* reconnection and request correlation.
|
|
1049
|
+
* @internal
|
|
1050
|
+
*/
|
|
1051
|
+
class WebSocketTransport extends AbstractDataConnectStreamTransport {
|
|
1052
|
+
get endpointUrl() {
|
|
1053
|
+
return websocketUrlBuilder({
|
|
1054
|
+
connector: this._connectorName,
|
|
1055
|
+
location: this._location,
|
|
1056
|
+
projectId: this._project,
|
|
1057
|
+
service: this._serviceName
|
|
1058
|
+
}, {
|
|
1059
|
+
host: this._host,
|
|
1060
|
+
sslEnabled: this._secure,
|
|
1061
|
+
port: this._port
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Decodes a WebSocket response from a Uint8Array to a JSON object.
|
|
1066
|
+
* Emulator does not send messages as Uint8Arrays, but prod does.
|
|
1067
|
+
*/
|
|
1068
|
+
decodeBinaryResponse(data) {
|
|
1069
|
+
if (!this.decoder) {
|
|
1070
|
+
this.decoder = new TextDecoder('utf-8');
|
|
1071
|
+
}
|
|
1072
|
+
return this.decoder.decode(data);
|
|
1073
|
+
}
|
|
1074
|
+
get streamIsReady() {
|
|
1075
|
+
return this.connection?.readyState === WebSocket.OPEN;
|
|
1076
|
+
}
|
|
1077
|
+
constructor(options, apiKey, appId, authProvider, appCheckProvider, transportOptions, _isUsingGen = false, _callerSdkType = CallerSdkTypeEnum.Base) {
|
|
1078
|
+
super(options, apiKey, appId, authProvider, appCheckProvider, transportOptions, _isUsingGen, _callerSdkType);
|
|
1079
|
+
this.apiKey = apiKey;
|
|
1080
|
+
this.appId = appId;
|
|
1081
|
+
this.authProvider = authProvider;
|
|
1082
|
+
this.appCheckProvider = appCheckProvider;
|
|
1083
|
+
this._isUsingGen = _isUsingGen;
|
|
1084
|
+
this._callerSdkType = _callerSdkType;
|
|
1085
|
+
/** Decodes binary WebSocket responses to strings */
|
|
1086
|
+
this.decoder = undefined;
|
|
1087
|
+
/** The current connection to the server. Undefined if disconnected. */
|
|
1088
|
+
this.connection = undefined;
|
|
1089
|
+
/**
|
|
1090
|
+
* Current connection attempt. If null, we are not currently attemping to connect (not connected,
|
|
1091
|
+
* or already connected). Will be resolved or rejected when the connection is opened or fails to open.
|
|
1092
|
+
*/
|
|
1093
|
+
this.connectionAttempt = null;
|
|
1094
|
+
}
|
|
1095
|
+
ensureConnection() {
|
|
1096
|
+
try {
|
|
1097
|
+
if (this.streamIsReady) {
|
|
1098
|
+
return Promise.resolve();
|
|
1099
|
+
}
|
|
1100
|
+
if (this.connectionAttempt) {
|
|
1101
|
+
return this.connectionAttempt;
|
|
1102
|
+
}
|
|
1103
|
+
this.connectionAttempt = new Promise((resolve, reject) => {
|
|
1104
|
+
if (!connectWebSocket) {
|
|
1105
|
+
throw new DataConnectError(Code.OTHER, 'No WebSocket Implementation detected!');
|
|
1106
|
+
}
|
|
1107
|
+
const ws = new connectWebSocket(this.endpointUrl);
|
|
1108
|
+
this.connection = ws;
|
|
1109
|
+
this.connection.binaryType = 'arraybuffer';
|
|
1110
|
+
ws.onopen = () => {
|
|
1111
|
+
this.isUnableToConnect = false;
|
|
1112
|
+
this.onConnectionReady();
|
|
1113
|
+
resolve();
|
|
1114
|
+
};
|
|
1115
|
+
ws.onerror = event => {
|
|
1116
|
+
this.connectionAttempt = null;
|
|
1117
|
+
this.isUnableToConnect = true;
|
|
1118
|
+
const error = new DataConnectError(Code.OTHER, `Error using WebSocket connection, closing WebSocket`);
|
|
1119
|
+
this.handleError(error);
|
|
1120
|
+
reject(error);
|
|
1121
|
+
};
|
|
1122
|
+
ws.onmessage = ev => this.handleWebSocketMessage(ev).catch(async (reason) => {
|
|
1123
|
+
this.handleError(reason);
|
|
1124
|
+
});
|
|
1125
|
+
ws.onclose = ev => this.handleWebsocketDisconnect(ev);
|
|
1126
|
+
});
|
|
1127
|
+
return this.connectionAttempt;
|
|
1128
|
+
}
|
|
1129
|
+
catch (error) {
|
|
1130
|
+
this.handleError(error);
|
|
1131
|
+
throw error;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
openConnection() {
|
|
1135
|
+
return this.ensureConnection().catch(err => {
|
|
1136
|
+
throw new DataConnectError(Code.OTHER, `Failed to open connection: ${err}`);
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
closeConnection(code, reason) {
|
|
1140
|
+
if (!this.connection) {
|
|
1141
|
+
this.connectionAttempt = null;
|
|
1142
|
+
return Promise.resolve();
|
|
1143
|
+
}
|
|
1144
|
+
let error;
|
|
1145
|
+
try {
|
|
1146
|
+
if (reason) {
|
|
1147
|
+
// reason string can be max 123 bytes (not characters, bytes)
|
|
1148
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocketStream/close#parameters
|
|
1149
|
+
const MAX_BYTES = 123;
|
|
1150
|
+
const encoder = new TextEncoder();
|
|
1151
|
+
const bytes = encoder.encode(reason);
|
|
1152
|
+
if (bytes.length <= MAX_BYTES) {
|
|
1153
|
+
this.connection.close(code, reason);
|
|
1154
|
+
}
|
|
1155
|
+
else {
|
|
1156
|
+
const buf = new Uint8Array(MAX_BYTES);
|
|
1157
|
+
const { read } = encoder.encodeInto(reason, buf);
|
|
1158
|
+
const truncatedReason = reason.substring(0, read);
|
|
1159
|
+
this.connection.close(code, truncatedReason);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
this.connection.close(code);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
catch (e) {
|
|
1167
|
+
error = e;
|
|
1168
|
+
}
|
|
1169
|
+
finally {
|
|
1170
|
+
this.connection = undefined;
|
|
1171
|
+
this.connectionAttempt = null;
|
|
1172
|
+
}
|
|
1173
|
+
if (error) {
|
|
1174
|
+
return Promise.reject(error);
|
|
1175
|
+
}
|
|
1176
|
+
return Promise.resolve();
|
|
219
1177
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
1178
|
+
/**
|
|
1179
|
+
* Handle a disconnection from the server. Initiates graceful clean up and reconnection attempts.
|
|
1180
|
+
* @param ev the {@link CloseEvent} that closed the WebSocket.
|
|
1181
|
+
*/
|
|
1182
|
+
handleWebsocketDisconnect(ev) {
|
|
1183
|
+
this.connection = undefined;
|
|
1184
|
+
this.connectionAttempt = null;
|
|
1185
|
+
this.onStreamClose(ev.code, ev.reason);
|
|
227
1186
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
1187
|
+
/**
|
|
1188
|
+
* Handle an error that occurred on the WebSocket. Close the connection and reject all active requests.
|
|
1189
|
+
*/
|
|
1190
|
+
handleError(error) {
|
|
1191
|
+
logError(`DataConnect WebSocket error, closing stream: ${error}`);
|
|
1192
|
+
let reason = error ? String(error) : 'Unknown Error';
|
|
1193
|
+
if (error instanceof DataConnectError) {
|
|
1194
|
+
reason = error.message;
|
|
1195
|
+
}
|
|
1196
|
+
void this.closeConnection(WEBSOCKET_CLOSE_CODE, reason);
|
|
1197
|
+
}
|
|
1198
|
+
sendMessage(requestBody) {
|
|
1199
|
+
return this.ensureConnection().then(() => {
|
|
1200
|
+
try {
|
|
1201
|
+
this.connection.send(JSON.stringify(requestBody));
|
|
1202
|
+
return Promise.resolve();
|
|
1203
|
+
}
|
|
1204
|
+
catch (err) {
|
|
1205
|
+
this.handleError(err);
|
|
1206
|
+
throw new DataConnectError(Code.OTHER, `Failed to send message: ${String(err)}`);
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Handles incoming WebSocket messages.
|
|
1212
|
+
* @param ev The {@link MessageEvent} from the WebSocket.
|
|
1213
|
+
*/
|
|
1214
|
+
async handleWebSocketMessage(ev) {
|
|
1215
|
+
const result = this.parseWebSocketData(ev.data);
|
|
1216
|
+
const requestId = result.requestId;
|
|
1217
|
+
const response = {
|
|
1218
|
+
data: result.data,
|
|
1219
|
+
errors: result.errors,
|
|
1220
|
+
extensions: result.extensions || { dataConnect: [] }
|
|
231
1221
|
};
|
|
1222
|
+
await this.handleResponse(requestId, response);
|
|
232
1223
|
}
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
1224
|
+
/**
|
|
1225
|
+
* Parse a response from the server. Assert that it has a {@link DataConnectStreamResponse.requestId | requestId}.
|
|
1226
|
+
* @param data the message from the server to be parsed
|
|
1227
|
+
* @returns the parsed message as a {@link DataConnectStreamResponse}
|
|
1228
|
+
* @throws {DataConnectError} if parsing fails or message is malformed.
|
|
1229
|
+
*/
|
|
1230
|
+
parseWebSocketData(
|
|
1231
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1232
|
+
data) {
|
|
1233
|
+
const dataIsString = typeof data === 'string';
|
|
1234
|
+
/** raw websocket message */
|
|
1235
|
+
let webSocketMessage;
|
|
1236
|
+
/** object containing data, errors, and extensions */
|
|
1237
|
+
let result;
|
|
1238
|
+
try {
|
|
1239
|
+
if (dataIsString) {
|
|
1240
|
+
webSocketMessage = JSON.parse(data);
|
|
1241
|
+
}
|
|
1242
|
+
else {
|
|
1243
|
+
webSocketMessage = JSON.parse(this.decodeBinaryResponse(data));
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
catch (err) {
|
|
1247
|
+
throw new DataConnectError(Code.OTHER, `Could not parse WebSocket message: ${err instanceof Error ? err.message : String(err)}`);
|
|
1248
|
+
}
|
|
1249
|
+
if (typeof webSocketMessage !== 'object' || webSocketMessage === null) {
|
|
1250
|
+
throw new DataConnectError(Code.OTHER, 'WebSocket message is not an object');
|
|
1251
|
+
}
|
|
1252
|
+
if (dataIsString) {
|
|
1253
|
+
if (!('result' in webSocketMessage)) {
|
|
1254
|
+
throw new DataConnectError(Code.OTHER, 'WebSocket message from emulator did not include result');
|
|
1255
|
+
}
|
|
1256
|
+
if (typeof webSocketMessage.result !== 'object' ||
|
|
1257
|
+
webSocketMessage.result === null) {
|
|
1258
|
+
throw new DataConnectError(Code.OTHER, 'WebSocket message result is not an object');
|
|
1259
|
+
}
|
|
1260
|
+
result = webSocketMessage.result;
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
result = webSocketMessage;
|
|
1264
|
+
}
|
|
1265
|
+
if (!('requestId' in result)) {
|
|
1266
|
+
throw new DataConnectError(Code.OTHER, 'WebSocket message did not include requestId');
|
|
1267
|
+
}
|
|
1268
|
+
return result;
|
|
238
1269
|
}
|
|
239
|
-
return JSON.stringify(obj);
|
|
240
1270
|
}
|
|
241
1271
|
|
|
242
1272
|
const name = "@firebase/data-connect";
|
|
243
|
-
const version = "0.
|
|
1273
|
+
const version = "0.6.0-20260409172004";
|
|
244
1274
|
|
|
245
1275
|
/**
|
|
246
1276
|
* @license
|
|
@@ -1047,6 +2077,10 @@ class QueryManager {
|
|
|
1047
2077
|
this.dc = dc;
|
|
1048
2078
|
this.cache = cache;
|
|
1049
2079
|
this.callbacks = new Map();
|
|
2080
|
+
/**
|
|
2081
|
+
* Map of serialized query keys to most recent Query Result. Used as a simple fallback cache
|
|
2082
|
+
* for subsciptions if caching is not enabled.
|
|
2083
|
+
*/
|
|
1050
2084
|
this.subscriptionCache = new Map();
|
|
1051
2085
|
this.queue = [];
|
|
1052
2086
|
}
|
|
@@ -1092,7 +2126,12 @@ class QueryManager {
|
|
|
1092
2126
|
const unsubscribe = () => {
|
|
1093
2127
|
if (this.callbacks.has(key)) {
|
|
1094
2128
|
const callbackList = this.callbacks.get(key);
|
|
1095
|
-
|
|
2129
|
+
const newList = callbackList.filter(callback => callback !== subscription);
|
|
2130
|
+
this.callbacks.set(key, newList);
|
|
2131
|
+
if (newList.length === 0) {
|
|
2132
|
+
this.callbacks.delete(key);
|
|
2133
|
+
this.transport.invokeUnsubscribe(queryRef.name, queryRef.variables);
|
|
2134
|
+
}
|
|
1096
2135
|
onCompleteCallback?.();
|
|
1097
2136
|
}
|
|
1098
2137
|
};
|
|
@@ -1107,12 +2146,18 @@ class QueryManager {
|
|
|
1107
2146
|
const promise = this.preferCacheResults(queryRef, /*allowStale=*/ true);
|
|
1108
2147
|
// We want to ignore the error and let subscriptions handle it
|
|
1109
2148
|
promise.then(undefined, err => { });
|
|
1110
|
-
if (
|
|
1111
|
-
this.callbacks
|
|
2149
|
+
if (this.callbacks.has(key)) {
|
|
2150
|
+
this.callbacks
|
|
2151
|
+
.get(key)
|
|
2152
|
+
.push(subscription);
|
|
2153
|
+
}
|
|
2154
|
+
else {
|
|
2155
|
+
this.callbacks.set(key, [
|
|
2156
|
+
subscription
|
|
2157
|
+
]);
|
|
2158
|
+
// only invoke subscription if we don't already have an active subscription
|
|
2159
|
+
this.transport.invokeSubscribe(this.makeSubscribeObserver(queryRef), queryRef.name, queryRef.variables);
|
|
1112
2160
|
}
|
|
1113
|
-
this.callbacks
|
|
1114
|
-
.get(key)
|
|
1115
|
-
.push(subscription);
|
|
1116
2161
|
return unsubscribe;
|
|
1117
2162
|
}
|
|
1118
2163
|
async fetchServerResults(queryRef) {
|
|
@@ -1135,8 +2180,7 @@ class QueryManager {
|
|
|
1135
2180
|
extensions: getDataConnectExtensionsWithoutMaxAge(originalExtensions),
|
|
1136
2181
|
toJSON: getRefSerializer(queryRef, result.data, SOURCE_SERVER, fetchTime)
|
|
1137
2182
|
};
|
|
1138
|
-
|
|
1139
|
-
updatedKeys = await this.updateCache(queryResult, originalExtensions?.dataConnect);
|
|
2183
|
+
const updatedKeys = await this.updateCache(queryResult, originalExtensions?.dataConnect);
|
|
1140
2184
|
this.publishDataToSubscribers(key, queryResult);
|
|
1141
2185
|
if (this.cache) {
|
|
1142
2186
|
await this.publishCacheResultsToSubscribers(updatedKeys, fetchTime);
|
|
@@ -1237,6 +2281,7 @@ class QueryManager {
|
|
|
1237
2281
|
result.toJSON = getRefSerializer(result.ref, result.data, SOURCE_CACHE, result.fetchTime);
|
|
1238
2282
|
return result;
|
|
1239
2283
|
}
|
|
2284
|
+
/** Call the registered onNext callbacks for the given key */
|
|
1240
2285
|
publishDataToSubscribers(key, queryResult) {
|
|
1241
2286
|
if (!this.callbacks.has(key)) {
|
|
1242
2287
|
return;
|
|
@@ -1277,6 +2322,80 @@ class QueryManager {
|
|
|
1277
2322
|
enableEmulator(host, port) {
|
|
1278
2323
|
this.transport.useEmulator(host, port);
|
|
1279
2324
|
}
|
|
2325
|
+
/**
|
|
2326
|
+
* Create a new {@link SubscribeObserver} for the given QueryRef. This will be passed to
|
|
2327
|
+
* {@link DataConnectTransportInterface.invokeSubscribe | invokeSubscribe()} to notify the query
|
|
2328
|
+
* layer of data update notifications or if the stream disconnected.
|
|
2329
|
+
*/
|
|
2330
|
+
makeSubscribeObserver(queryRef) {
|
|
2331
|
+
const key = encoderImpl({
|
|
2332
|
+
name: queryRef.name,
|
|
2333
|
+
variables: queryRef.variables,
|
|
2334
|
+
refType: QUERY_STR
|
|
2335
|
+
});
|
|
2336
|
+
return {
|
|
2337
|
+
onData: async (response) => {
|
|
2338
|
+
await this.handleStreamNotification(key, response, queryRef);
|
|
2339
|
+
},
|
|
2340
|
+
onDisconnect: (code, reason) => {
|
|
2341
|
+
this.handleStreamDisconnect(key, code, reason);
|
|
2342
|
+
},
|
|
2343
|
+
onError: error => {
|
|
2344
|
+
this.publishErrorToSubscribers(key, error);
|
|
2345
|
+
}
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Handle a data update notification from the stream. Notify subscribers of results/errors, and
|
|
2350
|
+
* update the cache.
|
|
2351
|
+
*/
|
|
2352
|
+
async handleStreamNotification(key, response, queryRef) {
|
|
2353
|
+
if (response.errors && response.errors.length > 0) {
|
|
2354
|
+
const stringified = JSON.stringify(response.errors.map(e => {
|
|
2355
|
+
if (e && typeof e === 'object') {
|
|
2356
|
+
return {
|
|
2357
|
+
message: e.message,
|
|
2358
|
+
code: e.code
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
return e;
|
|
2362
|
+
}));
|
|
2363
|
+
const failureResponse = {
|
|
2364
|
+
errors: response.errors,
|
|
2365
|
+
data: response.data
|
|
2366
|
+
};
|
|
2367
|
+
const error = new DataConnectOperationError('DataConnect error received from subscribe notification: ' +
|
|
2368
|
+
stringified, failureResponse);
|
|
2369
|
+
this.publishErrorToSubscribers(key, error);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
const fetchTime = Date.now().toString();
|
|
2373
|
+
const queryResult = {
|
|
2374
|
+
ref: queryRef,
|
|
2375
|
+
source: SOURCE_SERVER,
|
|
2376
|
+
fetchTime,
|
|
2377
|
+
data: response.data,
|
|
2378
|
+
extensions: getDataConnectExtensionsWithoutMaxAge(response.extensions),
|
|
2379
|
+
toJSON: getRefSerializer(queryRef, response.data, SOURCE_SERVER, fetchTime)
|
|
2380
|
+
};
|
|
2381
|
+
const updatedKeys = await this.updateCache(queryResult, response.extensions?.dataConnect);
|
|
2382
|
+
this.publishDataToSubscribers(key, queryResult);
|
|
2383
|
+
if (this.cache) {
|
|
2384
|
+
await this.publishCacheResultsToSubscribers(updatedKeys, fetchTime);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
/**
|
|
2388
|
+
* Handle a disconnect from the stream. Unsubscribe all callbacks for the given key.
|
|
2389
|
+
*/
|
|
2390
|
+
handleStreamDisconnect(key, code, reason) {
|
|
2391
|
+
const error = new DataConnectError(code, reason);
|
|
2392
|
+
this.publishErrorToSubscribers(key, error);
|
|
2393
|
+
const callbacks = this.callbacks.get(key);
|
|
2394
|
+
if (callbacks) {
|
|
2395
|
+
[...callbacks].forEach(cb => cb.unsubscribe());
|
|
2396
|
+
}
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
1280
2399
|
}
|
|
1281
2400
|
function getMaxAgeFromExtensions(extensions) {
|
|
1282
2401
|
if (!extensions) {
|
|
@@ -1300,7 +2419,7 @@ function getDataConnectExtensionsWithoutMaxAge(extensions) {
|
|
|
1300
2419
|
|
|
1301
2420
|
/**
|
|
1302
2421
|
* @license
|
|
1303
|
-
* Copyright
|
|
2422
|
+
* Copyright 2026 Google LLC
|
|
1304
2423
|
*
|
|
1305
2424
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1306
2425
|
* you may not use this file except in compliance with the License.
|
|
@@ -1314,188 +2433,110 @@ function getDataConnectExtensionsWithoutMaxAge(extensions) {
|
|
|
1314
2433
|
* See the License for the specific language governing permissions and
|
|
1315
2434
|
* limitations under the License.
|
|
1316
2435
|
*/
|
|
1317
|
-
const PROD_HOST = 'firebasedataconnect.googleapis.com';
|
|
1318
|
-
function urlBuilder(projectConfig, transportOptions) {
|
|
1319
|
-
const { connector, location, projectId: project, service } = projectConfig;
|
|
1320
|
-
const { host, sslEnabled, port } = transportOptions;
|
|
1321
|
-
const protocol = sslEnabled ? 'https' : 'http';
|
|
1322
|
-
const realHost = host || PROD_HOST;
|
|
1323
|
-
let baseUrl = `${protocol}://${realHost}`;
|
|
1324
|
-
if (typeof port === 'number') {
|
|
1325
|
-
baseUrl += `:${port}`;
|
|
1326
|
-
}
|
|
1327
|
-
else if (typeof port !== 'undefined') {
|
|
1328
|
-
logError('Port type is of an invalid type');
|
|
1329
|
-
throw new DataConnectError(Code.INVALID_ARGUMENT, 'Incorrect type for port passed in!');
|
|
1330
|
-
}
|
|
1331
|
-
return `${baseUrl}/v1/projects/${project}/locations/${location}/services/${service}/connectors/${connector}`;
|
|
1332
|
-
}
|
|
1333
|
-
function addToken(url, apiKey) {
|
|
1334
|
-
if (!apiKey) {
|
|
1335
|
-
return url;
|
|
1336
|
-
}
|
|
1337
|
-
const newUrl = new URL(url);
|
|
1338
|
-
newUrl.searchParams.append('key', apiKey);
|
|
1339
|
-
return newUrl.toString();
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
2436
|
/**
|
|
1343
|
-
*
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
1346
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1347
|
-
* you may not use this file except in compliance with the License.
|
|
1348
|
-
* You may obtain a copy of the License at
|
|
1349
|
-
*
|
|
1350
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1351
|
-
*
|
|
1352
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1353
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1354
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1355
|
-
* See the License for the specific language governing permissions and
|
|
1356
|
-
* limitations under the License.
|
|
2437
|
+
* Entry point for the transport layer. Manages routing between transport implementations.
|
|
2438
|
+
* @internal
|
|
1357
2439
|
*/
|
|
1358
|
-
class
|
|
1359
|
-
constructor(options, apiKey, appId, authProvider, appCheckProvider, transportOptions, _isUsingGen = false, _callerSdkType
|
|
2440
|
+
class DataConnectTransportManager {
|
|
2441
|
+
constructor(options, apiKey, appId, authProvider, appCheckProvider, transportOptions, _isUsingGen = false, _callerSdkType) {
|
|
2442
|
+
this.options = options;
|
|
1360
2443
|
this.apiKey = apiKey;
|
|
1361
2444
|
this.appId = appId;
|
|
1362
2445
|
this.authProvider = authProvider;
|
|
1363
2446
|
this.appCheckProvider = appCheckProvider;
|
|
2447
|
+
this.transportOptions = transportOptions;
|
|
1364
2448
|
this._isUsingGen = _isUsingGen;
|
|
1365
2449
|
this._callerSdkType = _callerSdkType;
|
|
1366
|
-
this.
|
|
1367
|
-
this.
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
this.
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
const abortController = new AbortController();
|
|
1378
|
-
// TODO(mtewani): Update to proper value
|
|
1379
|
-
const withAuth = this.withRetry(() => dcFetch(addToken(`${this.endpointUrl}:executeQuery`, this.apiKey), {
|
|
1380
|
-
name: `projects/${this._project}/locations/${this._location}/services/${this._serviceName}/connectors/${this._connectorName}`,
|
|
1381
|
-
operationName: queryName,
|
|
1382
|
-
variables: body
|
|
1383
|
-
}, abortController, this.appId, this._accessToken, this._appCheckToken, this._isUsingGen, this._callerSdkType, this._isUsingEmulator));
|
|
1384
|
-
return withAuth;
|
|
1385
|
-
};
|
|
1386
|
-
this.invokeMutation = (mutationName, body) => {
|
|
1387
|
-
const abortController = new AbortController();
|
|
1388
|
-
const taskResult = this.withRetry(() => {
|
|
1389
|
-
return dcFetch(addToken(`${this.endpointUrl}:executeMutation`, this.apiKey), {
|
|
1390
|
-
name: `projects/${this._project}/locations/${this._location}/services/${this._serviceName}/connectors/${this._connectorName}`,
|
|
1391
|
-
operationName: mutationName,
|
|
1392
|
-
variables: body
|
|
1393
|
-
}, abortController, this.appId, this._accessToken, this._appCheckToken, this._isUsingGen, this._callerSdkType, this._isUsingEmulator);
|
|
1394
|
-
});
|
|
1395
|
-
return taskResult;
|
|
1396
|
-
};
|
|
1397
|
-
if (transportOptions) {
|
|
1398
|
-
if (typeof transportOptions.port === 'number') {
|
|
1399
|
-
this._port = transportOptions.port;
|
|
1400
|
-
}
|
|
1401
|
-
if (typeof transportOptions.sslEnabled !== 'undefined') {
|
|
1402
|
-
this._secure = transportOptions.sslEnabled;
|
|
2450
|
+
this.isUsingEmulator = false;
|
|
2451
|
+
this.restTransport = new RESTTransport(options, apiKey, appId, authProvider, appCheckProvider, transportOptions, _isUsingGen, _callerSdkType);
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* Initializes the stream transport if it hasn't been already.
|
|
2455
|
+
*/
|
|
2456
|
+
initStreamTransport() {
|
|
2457
|
+
if (!this.streamTransport) {
|
|
2458
|
+
this.streamTransport = new WebSocketTransport(this.options, this.apiKey, this.appId, this.authProvider, this.appCheckProvider, this.transportOptions, this._isUsingGen, this._callerSdkType);
|
|
2459
|
+
if (this.isUsingEmulator && this.transportOptions) {
|
|
2460
|
+
this.streamTransport.useEmulator(this.transportOptions.host, this.transportOptions.port, this.transportOptions.sslEnabled);
|
|
1403
2461
|
}
|
|
1404
|
-
this.
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
if (location) {
|
|
1408
|
-
this._location = location;
|
|
1409
|
-
}
|
|
1410
|
-
if (project) {
|
|
1411
|
-
this._project = project;
|
|
1412
|
-
}
|
|
1413
|
-
this._serviceName = service;
|
|
1414
|
-
if (!connector) {
|
|
1415
|
-
throw new DataConnectError(Code.INVALID_ARGUMENT, 'Connector Name required!');
|
|
2462
|
+
this.streamTransport.onGracefulStreamClose = () => {
|
|
2463
|
+
this.streamTransport = undefined;
|
|
2464
|
+
};
|
|
1416
2465
|
}
|
|
1417
|
-
this.
|
|
1418
|
-
this.authProvider?.addTokenChangeListener(token => {
|
|
1419
|
-
logDebug(`New Token Available: ${token}`);
|
|
1420
|
-
this._accessToken = token;
|
|
1421
|
-
});
|
|
1422
|
-
this.appCheckProvider?.addTokenChangeListener(result => {
|
|
1423
|
-
const { token } = result;
|
|
1424
|
-
logDebug(`New App Check Token Available: ${token}`);
|
|
1425
|
-
this._appCheckToken = token;
|
|
1426
|
-
});
|
|
2466
|
+
return this.streamTransport;
|
|
1427
2467
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
2468
|
+
/**
|
|
2469
|
+
* Returns true if the stream is in a healthy, ready connection state and has active subscriptions.
|
|
2470
|
+
*/
|
|
2471
|
+
executeShouldUseStream() {
|
|
2472
|
+
return (!!this.streamTransport &&
|
|
2473
|
+
!this.streamTransport.isPendingClose &&
|
|
2474
|
+
this.streamTransport.streamIsReady &&
|
|
2475
|
+
this.streamTransport.hasActiveSubscriptions &&
|
|
2476
|
+
!this.streamTransport.isUnableToConnect);
|
|
1435
2477
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
2478
|
+
/**
|
|
2479
|
+
* Prefer to use Streaming Transport connection when one is available.
|
|
2480
|
+
* @inheritdoc
|
|
2481
|
+
*/
|
|
2482
|
+
invokeQuery(queryName, body) {
|
|
2483
|
+
if (this.executeShouldUseStream()) {
|
|
2484
|
+
return this.streamTransport.invokeQuery(queryName, body).catch(err => {
|
|
2485
|
+
if (this.executeShouldUseStream()) {
|
|
2486
|
+
throw err;
|
|
2487
|
+
}
|
|
2488
|
+
return this.restTransport.invokeQuery(queryName, body);
|
|
2489
|
+
});
|
|
1444
2490
|
}
|
|
2491
|
+
return this.restTransport.invokeQuery(queryName, body);
|
|
1445
2492
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
if (this.
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
if (this.authProvider) {
|
|
1458
|
-
starterPromise = this.authProvider
|
|
1459
|
-
.getToken(/*forceToken=*/ forceToken)
|
|
1460
|
-
.then(data => {
|
|
1461
|
-
if (!data) {
|
|
1462
|
-
return null;
|
|
2493
|
+
/**
|
|
2494
|
+
* Prefer to use Streaming Transport connection when one is available.
|
|
2495
|
+
* @inheritdoc
|
|
2496
|
+
*/
|
|
2497
|
+
invokeMutation(queryName, body) {
|
|
2498
|
+
if (this.executeShouldUseStream()) {
|
|
2499
|
+
return this.streamTransport.invokeMutation(queryName, body).catch(err => {
|
|
2500
|
+
if (this.executeShouldUseStream()) {
|
|
2501
|
+
throw err;
|
|
1463
2502
|
}
|
|
1464
|
-
this.
|
|
1465
|
-
return this._accessToken;
|
|
2503
|
+
return this.restTransport.invokeMutation(queryName, body);
|
|
1466
2504
|
});
|
|
1467
2505
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
2506
|
+
return this.restTransport.invokeMutation(queryName, body);
|
|
2507
|
+
}
|
|
2508
|
+
invokeSubscribe(observer, queryName, body) {
|
|
2509
|
+
const streamTransport = this.initStreamTransport();
|
|
2510
|
+
if (streamTransport.isUnableToConnect) {
|
|
2511
|
+
throw new DataConnectError(Code.OTHER, 'Unable to connect streaming connection to server. Subscriptions are unavailable.');
|
|
1470
2512
|
}
|
|
1471
|
-
|
|
2513
|
+
streamTransport.invokeSubscribe(observer, queryName, body);
|
|
1472
2514
|
}
|
|
1473
|
-
|
|
1474
|
-
this.
|
|
2515
|
+
invokeUnsubscribe(queryName, body) {
|
|
2516
|
+
if (this.streamTransport) {
|
|
2517
|
+
this.streamTransport.invokeUnsubscribe(queryName, body);
|
|
2518
|
+
}
|
|
1475
2519
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
this.
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
!retry &&
|
|
1490
|
-
isNewToken) {
|
|
1491
|
-
logDebug('Retrying due to unauthorized');
|
|
1492
|
-
return this.withRetry(promiseFactory, true);
|
|
1493
|
-
}
|
|
1494
|
-
throw err;
|
|
1495
|
-
});
|
|
2520
|
+
useEmulator(host, port, sslEnabled) {
|
|
2521
|
+
this.isUsingEmulator = true;
|
|
2522
|
+
this.transportOptions = { host, port, sslEnabled };
|
|
2523
|
+
this.restTransport.useEmulator(host, port, sslEnabled);
|
|
2524
|
+
if (this.streamTransport) {
|
|
2525
|
+
this.streamTransport.useEmulator(host, port, sslEnabled);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
onAuthTokenChanged(token) {
|
|
2529
|
+
this.restTransport.onAuthTokenChanged(token);
|
|
2530
|
+
if (this.streamTransport) {
|
|
2531
|
+
this.streamTransport.onAuthTokenChanged(token);
|
|
2532
|
+
}
|
|
1496
2533
|
}
|
|
1497
2534
|
_setCallerSdkType(callerSdkType) {
|
|
1498
2535
|
this._callerSdkType = callerSdkType;
|
|
2536
|
+
this.restTransport._setCallerSdkType(callerSdkType);
|
|
2537
|
+
if (this.streamTransport) {
|
|
2538
|
+
this.streamTransport._setCallerSdkType(callerSdkType);
|
|
2539
|
+
}
|
|
1499
2540
|
}
|
|
1500
2541
|
}
|
|
1501
2542
|
|
|
@@ -1661,8 +2702,8 @@ class DataConnect {
|
|
|
1661
2702
|
return;
|
|
1662
2703
|
}
|
|
1663
2704
|
if (this._transportClass === undefined) {
|
|
1664
|
-
logDebug('transportClass not provided. Defaulting to
|
|
1665
|
-
this._transportClass =
|
|
2705
|
+
logDebug('transportClass not provided. Defaulting to DataConnectTransportManager.');
|
|
2706
|
+
this._transportClass = DataConnectTransportManager;
|
|
1666
2707
|
}
|
|
1667
2708
|
this._authTokenProvider = new FirebaseAuthProvider(this.app.name, this.app.options, this._authProvider);
|
|
1668
2709
|
const connectorConfig = {
|
|
@@ -2105,8 +3146,15 @@ function subscribe(queryRefOrSerializedResult, observerOrOnNext, onError, onComp
|
|
|
2105
3146
|
* limitations under the License.
|
|
2106
3147
|
*/
|
|
2107
3148
|
initializeFetch(fetch);
|
|
3149
|
+
if (typeof WebSocket !== 'undefined') {
|
|
3150
|
+
initializeWebSocket(WebSocket);
|
|
3151
|
+
}
|
|
3152
|
+
else {
|
|
3153
|
+
console.warn('WebSocket is not available in this environment. Use a polyfill or upgrade your Node version to one that supports WebSockets.');
|
|
3154
|
+
}
|
|
2108
3155
|
registerDataConnect('node');
|
|
2109
3156
|
|
|
3157
|
+
exports.AbstractDataConnectTransport = AbstractDataConnectTransport;
|
|
2110
3158
|
exports.CallerSdkTypeEnum = CallerSdkTypeEnum;
|
|
2111
3159
|
exports.Code = Code;
|
|
2112
3160
|
exports.DataConnect = DataConnect;
|
|
@@ -2124,6 +3172,7 @@ exports.connectDataConnectEmulator = connectDataConnectEmulator;
|
|
|
2124
3172
|
exports.executeMutation = executeMutation;
|
|
2125
3173
|
exports.executeQuery = executeQuery;
|
|
2126
3174
|
exports.getDataConnect = getDataConnect;
|
|
3175
|
+
exports.getGoogApiClientValue = getGoogApiClientValue$1;
|
|
2127
3176
|
exports.makeMemoryCacheProvider = makeMemoryCacheProvider;
|
|
2128
3177
|
exports.mutationRef = mutationRef;
|
|
2129
3178
|
exports.parseOptions = parseOptions;
|