@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.
Files changed (42) hide show
  1. package/dist/index.cjs.js +1173 -143
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.esm.js +1172 -144
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/index.node.cjs.js +1239 -190
  6. package/dist/index.node.cjs.js.map +1 -1
  7. package/dist/internal.d.ts +133 -12
  8. package/dist/node-esm/index.node.esm.js +1238 -191
  9. package/dist/node-esm/index.node.esm.js.map +1 -1
  10. package/dist/node-esm/src/api/Mutation.d.ts +2 -2
  11. package/dist/node-esm/src/api/query.d.ts +1 -1
  12. package/dist/node-esm/src/core/query/QueryManager.d.ts +22 -3
  13. package/dist/node-esm/src/network/index.d.ts +1 -1
  14. package/dist/node-esm/src/network/manager.d.ts +61 -0
  15. package/dist/node-esm/src/network/{fetch.d.ts → rest/fetch.d.ts} +9 -4
  16. package/dist/node-esm/src/network/rest/index.d.ts +18 -0
  17. package/dist/node-esm/src/network/rest/restTransport.d.ts +33 -0
  18. package/dist/node-esm/src/network/stream/streamTransport.d.ts +243 -0
  19. package/dist/node-esm/src/network/stream/websocket.d.ts +90 -0
  20. package/dist/node-esm/src/network/stream/wire.d.ts +138 -0
  21. package/dist/node-esm/src/network/transport.d.ts +179 -0
  22. package/dist/node-esm/src/util/url.d.ts +3 -1
  23. package/dist/private.d.ts +29 -7
  24. package/dist/public.d.ts +3 -1
  25. package/dist/src/api/Mutation.d.ts +2 -2
  26. package/dist/src/api/query.d.ts +1 -1
  27. package/dist/src/core/query/QueryManager.d.ts +22 -3
  28. package/dist/src/network/index.d.ts +1 -1
  29. package/dist/src/network/manager.d.ts +61 -0
  30. package/dist/src/network/{fetch.d.ts → rest/fetch.d.ts} +9 -4
  31. package/dist/src/network/rest/index.d.ts +18 -0
  32. package/dist/src/network/rest/restTransport.d.ts +33 -0
  33. package/dist/src/network/stream/streamTransport.d.ts +243 -0
  34. package/dist/src/network/stream/websocket.d.ts +90 -0
  35. package/dist/src/network/stream/wire.d.ts +138 -0
  36. package/dist/src/network/transport.d.ts +179 -0
  37. package/dist/src/util/url.d.ts +3 -1
  38. package/package.json +1 -1
  39. package/dist/node-esm/src/network/transport/index.d.ts +0 -81
  40. package/dist/node-esm/src/network/transport/rest.d.ts +0 -49
  41. package/dist/src/network/transport/index.d.ts +0 -81
  42. package/dist/src/network/transport/rest.d.ts +0 -49
package/dist/index.esm.js CHANGED
@@ -4,7 +4,7 @@ import { FirebaseError, generateSHA256Hash, isCloudWorkstation, pingServer } fro
4
4
  import { Logger } from '@firebase/logger';
5
5
 
6
6
  const name = "@firebase/data-connect";
7
- const version = "0.5.0";
7
+ const version = "0.6.0-20260409172004";
8
8
 
9
9
  /**
10
10
  * @license
@@ -916,6 +916,10 @@ class QueryManager {
916
916
  this.dc = dc;
917
917
  this.cache = cache;
918
918
  this.callbacks = new Map();
919
+ /**
920
+ * Map of serialized query keys to most recent Query Result. Used as a simple fallback cache
921
+ * for subsciptions if caching is not enabled.
922
+ */
919
923
  this.subscriptionCache = new Map();
920
924
  this.queue = [];
921
925
  }
@@ -961,7 +965,12 @@ class QueryManager {
961
965
  const unsubscribe = () => {
962
966
  if (this.callbacks.has(key)) {
963
967
  const callbackList = this.callbacks.get(key);
964
- this.callbacks.set(key, callbackList.filter(callback => callback !== subscription));
968
+ const newList = callbackList.filter(callback => callback !== subscription);
969
+ this.callbacks.set(key, newList);
970
+ if (newList.length === 0) {
971
+ this.callbacks.delete(key);
972
+ this.transport.invokeUnsubscribe(queryRef.name, queryRef.variables);
973
+ }
965
974
  onCompleteCallback?.();
966
975
  }
967
976
  };
@@ -976,12 +985,18 @@ class QueryManager {
976
985
  const promise = this.preferCacheResults(queryRef, /*allowStale=*/ true);
977
986
  // We want to ignore the error and let subscriptions handle it
978
987
  promise.then(undefined, err => { });
979
- if (!this.callbacks.has(key)) {
980
- this.callbacks.set(key, []);
988
+ if (this.callbacks.has(key)) {
989
+ this.callbacks
990
+ .get(key)
991
+ .push(subscription);
992
+ }
993
+ else {
994
+ this.callbacks.set(key, [
995
+ subscription
996
+ ]);
997
+ // only invoke subscription if we don't already have an active subscription
998
+ this.transport.invokeSubscribe(this.makeSubscribeObserver(queryRef), queryRef.name, queryRef.variables);
981
999
  }
982
- this.callbacks
983
- .get(key)
984
- .push(subscription);
985
1000
  return unsubscribe;
986
1001
  }
987
1002
  async fetchServerResults(queryRef) {
@@ -1004,8 +1019,7 @@ class QueryManager {
1004
1019
  extensions: getDataConnectExtensionsWithoutMaxAge(originalExtensions),
1005
1020
  toJSON: getRefSerializer(queryRef, result.data, SOURCE_SERVER, fetchTime)
1006
1021
  };
1007
- let updatedKeys = [];
1008
- updatedKeys = await this.updateCache(queryResult, originalExtensions?.dataConnect);
1022
+ const updatedKeys = await this.updateCache(queryResult, originalExtensions?.dataConnect);
1009
1023
  this.publishDataToSubscribers(key, queryResult);
1010
1024
  if (this.cache) {
1011
1025
  await this.publishCacheResultsToSubscribers(updatedKeys, fetchTime);
@@ -1106,6 +1120,7 @@ class QueryManager {
1106
1120
  result.toJSON = getRefSerializer(result.ref, result.data, SOURCE_CACHE, result.fetchTime);
1107
1121
  return result;
1108
1122
  }
1123
+ /** Call the registered onNext callbacks for the given key */
1109
1124
  publishDataToSubscribers(key, queryResult) {
1110
1125
  if (!this.callbacks.has(key)) {
1111
1126
  return;
@@ -1146,6 +1161,80 @@ class QueryManager {
1146
1161
  enableEmulator(host, port) {
1147
1162
  this.transport.useEmulator(host, port);
1148
1163
  }
1164
+ /**
1165
+ * Create a new {@link SubscribeObserver} for the given QueryRef. This will be passed to
1166
+ * {@link DataConnectTransportInterface.invokeSubscribe | invokeSubscribe()} to notify the query
1167
+ * layer of data update notifications or if the stream disconnected.
1168
+ */
1169
+ makeSubscribeObserver(queryRef) {
1170
+ const key = encoderImpl({
1171
+ name: queryRef.name,
1172
+ variables: queryRef.variables,
1173
+ refType: QUERY_STR
1174
+ });
1175
+ return {
1176
+ onData: async (response) => {
1177
+ await this.handleStreamNotification(key, response, queryRef);
1178
+ },
1179
+ onDisconnect: (code, reason) => {
1180
+ this.handleStreamDisconnect(key, code, reason);
1181
+ },
1182
+ onError: error => {
1183
+ this.publishErrorToSubscribers(key, error);
1184
+ }
1185
+ };
1186
+ }
1187
+ /**
1188
+ * Handle a data update notification from the stream. Notify subscribers of results/errors, and
1189
+ * update the cache.
1190
+ */
1191
+ async handleStreamNotification(key, response, queryRef) {
1192
+ if (response.errors && response.errors.length > 0) {
1193
+ const stringified = JSON.stringify(response.errors.map(e => {
1194
+ if (e && typeof e === 'object') {
1195
+ return {
1196
+ message: e.message,
1197
+ code: e.code
1198
+ };
1199
+ }
1200
+ return e;
1201
+ }));
1202
+ const failureResponse = {
1203
+ errors: response.errors,
1204
+ data: response.data
1205
+ };
1206
+ const error = new DataConnectOperationError('DataConnect error received from subscribe notification: ' +
1207
+ stringified, failureResponse);
1208
+ this.publishErrorToSubscribers(key, error);
1209
+ return;
1210
+ }
1211
+ const fetchTime = Date.now().toString();
1212
+ const queryResult = {
1213
+ ref: queryRef,
1214
+ source: SOURCE_SERVER,
1215
+ fetchTime,
1216
+ data: response.data,
1217
+ extensions: getDataConnectExtensionsWithoutMaxAge(response.extensions),
1218
+ toJSON: getRefSerializer(queryRef, response.data, SOURCE_SERVER, fetchTime)
1219
+ };
1220
+ const updatedKeys = await this.updateCache(queryResult, response.extensions?.dataConnect);
1221
+ this.publishDataToSubscribers(key, queryResult);
1222
+ if (this.cache) {
1223
+ await this.publishCacheResultsToSubscribers(updatedKeys, fetchTime);
1224
+ }
1225
+ }
1226
+ /**
1227
+ * Handle a disconnect from the stream. Unsubscribe all callbacks for the given key.
1228
+ */
1229
+ handleStreamDisconnect(key, code, reason) {
1230
+ const error = new DataConnectError(code, reason);
1231
+ this.publishErrorToSubscribers(key, error);
1232
+ const callbacks = this.callbacks.get(key);
1233
+ if (callbacks) {
1234
+ [...callbacks].forEach(cb => cb.unsubscribe());
1235
+ }
1236
+ return;
1237
+ }
1149
1238
  }
1150
1239
  function getMaxAgeFromExtensions(extensions) {
1151
1240
  if (!extensions) {
@@ -1191,46 +1280,137 @@ const CallerSdkTypeEnum = {
1191
1280
  TanstackAngularCore: 'TanstackAngularCore', // Tanstack non-generated Angular SDK
1192
1281
  GeneratedAngular: 'GeneratedAngular' // Generated Angular SDK
1193
1282
  };
1194
-
1195
1283
  /**
1196
- * @license
1197
- * Copyright 2024 Google LLC
1198
- *
1199
- * Licensed under the Apache License, Version 2.0 (the "License");
1200
- * you may not use this file except in compliance with the License.
1201
- * You may obtain a copy of the License at
1202
- *
1203
- * http://www.apache.org/licenses/LICENSE-2.0
1204
- *
1205
- * Unless required by applicable law or agreed to in writing, software
1206
- * distributed under the License is distributed on an "AS IS" BASIS,
1207
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1208
- * See the License for the specific language governing permissions and
1209
- * limitations under the License.
1284
+ * Constructs the value for the X-Goog-Api-Client header
1285
+ * @internal
1210
1286
  */
1211
- const PROD_HOST = 'firebasedataconnect.googleapis.com';
1212
- function urlBuilder(projectConfig, transportOptions) {
1213
- const { connector, location, projectId: project, service } = projectConfig;
1214
- const { host, sslEnabled, port } = transportOptions;
1215
- const protocol = sslEnabled ? 'https' : 'http';
1216
- const realHost = host || PROD_HOST;
1217
- let baseUrl = `${protocol}://${realHost}`;
1218
- if (typeof port === 'number') {
1219
- baseUrl += `:${port}`;
1287
+ function getGoogApiClientValue$1(isUsingGen, callerSdkType) {
1288
+ let str = 'gl-js/ fire/' + SDK_VERSION;
1289
+ if (callerSdkType !== CallerSdkTypeEnum.Base &&
1290
+ callerSdkType !== CallerSdkTypeEnum.Generated) {
1291
+ str += ' js/' + callerSdkType.toLowerCase();
1220
1292
  }
1221
- else if (typeof port !== 'undefined') {
1222
- logError('Port type is of an invalid type');
1223
- throw new DataConnectError(Code.INVALID_ARGUMENT, 'Incorrect type for port passed in!');
1293
+ else if (isUsingGen || callerSdkType === CallerSdkTypeEnum.Generated) {
1294
+ str += ' js/gen';
1224
1295
  }
1225
- return `${baseUrl}/v1/projects/${project}/locations/${location}/services/${service}/connectors/${connector}`;
1296
+ return str;
1226
1297
  }
1227
- function addToken(url, apiKey) {
1228
- if (!apiKey) {
1229
- return url;
1298
+ /**
1299
+ * The base class for all DataConnectTransportInterface implementations. Handles common logic such as
1300
+ * URL construction, auth token management, and emulator usage. Concrete transport implementations
1301
+ * should extend this class and implement the abstract {@link DataConnectTransportInterface} methods.
1302
+ * @internal
1303
+ */
1304
+ class AbstractDataConnectTransport {
1305
+ constructor(options, apiKey, appId, authProvider, appCheckProvider, transportOptions, _isUsingGen = false, _callerSdkType = CallerSdkTypeEnum.Base) {
1306
+ this.apiKey = apiKey;
1307
+ this.appId = appId;
1308
+ this.authProvider = authProvider;
1309
+ this.appCheckProvider = appCheckProvider;
1310
+ this._isUsingGen = _isUsingGen;
1311
+ this._callerSdkType = _callerSdkType;
1312
+ this._host = '';
1313
+ this._location = 'l';
1314
+ this._connectorName = '';
1315
+ this._secure = true;
1316
+ this._project = 'p';
1317
+ this._authToken = null;
1318
+ this._appCheckToken = null;
1319
+ this._lastToken = null;
1320
+ this._isUsingEmulator = false;
1321
+ if (transportOptions) {
1322
+ if (typeof transportOptions.port === 'number') {
1323
+ this._port = transportOptions.port;
1324
+ }
1325
+ if (typeof transportOptions.sslEnabled !== 'undefined') {
1326
+ this._secure = transportOptions.sslEnabled;
1327
+ }
1328
+ this._host = transportOptions.host;
1329
+ }
1330
+ const { location, projectId: project, connector, service } = options;
1331
+ if (location) {
1332
+ this._location = location;
1333
+ }
1334
+ if (project) {
1335
+ this._project = project;
1336
+ }
1337
+ this._serviceName = service;
1338
+ if (!connector) {
1339
+ throw new DataConnectError(Code.INVALID_ARGUMENT, 'Connector Name required!');
1340
+ }
1341
+ this._connectorName = connector;
1342
+ this._connectorResourcePath = `projects/${this._project}/locations/${this._location}/services/${this._serviceName}/connectors/${this._connectorName}`;
1343
+ this.authProvider?.addTokenChangeListener(token => {
1344
+ logDebug(`New Token Available: ${token}`);
1345
+ this.onAuthTokenChanged(token);
1346
+ });
1347
+ this.appCheckProvider?.addTokenChangeListener(result => {
1348
+ const { token } = result;
1349
+ logDebug(`New App Check Token Available: ${token}`);
1350
+ this._appCheckToken = token;
1351
+ });
1352
+ }
1353
+ useEmulator(host, port, isSecure) {
1354
+ this._host = host;
1355
+ this._isUsingEmulator = true;
1356
+ if (typeof port === 'number') {
1357
+ this._port = port;
1358
+ }
1359
+ if (typeof isSecure !== 'undefined') {
1360
+ this._secure = isSecure;
1361
+ }
1362
+ }
1363
+ async getWithAuth(forceToken = false) {
1364
+ let starterPromise = new Promise(resolve => resolve(this._authToken));
1365
+ if (this.appCheckProvider) {
1366
+ const appCheckToken = await this.appCheckProvider.getToken();
1367
+ if (appCheckToken) {
1368
+ this._appCheckToken = appCheckToken.token;
1369
+ }
1370
+ }
1371
+ if (this.authProvider) {
1372
+ starterPromise = this.authProvider
1373
+ .getToken(/*forceToken=*/ forceToken)
1374
+ .then(data => {
1375
+ if (!data) {
1376
+ return null;
1377
+ }
1378
+ this._authToken = data.accessToken;
1379
+ return this._authToken;
1380
+ });
1381
+ }
1382
+ else {
1383
+ starterPromise = new Promise(resolve => resolve(''));
1384
+ }
1385
+ return starterPromise;
1386
+ }
1387
+ async withRetry(promiseFactory, retry = false) {
1388
+ let isNewToken = false;
1389
+ return this.getWithAuth(retry)
1390
+ .then(res => {
1391
+ isNewToken = this._lastToken !== res;
1392
+ this._lastToken = res;
1393
+ return res;
1394
+ })
1395
+ .then(promiseFactory)
1396
+ .catch(err => {
1397
+ // Only retry if the result is unauthorized and the last token isn't the same as the new one.
1398
+ if ('code' in err &&
1399
+ err.code === Code.UNAUTHORIZED &&
1400
+ !retry &&
1401
+ isNewToken) {
1402
+ logDebug('Retrying due to unauthorized');
1403
+ return this.withRetry(promiseFactory, true);
1404
+ }
1405
+ throw err;
1406
+ });
1407
+ }
1408
+ _setLastToken(lastToken) {
1409
+ this._lastToken = lastToken;
1410
+ }
1411
+ _setCallerSdkType(callerSdkType) {
1412
+ this._callerSdkType = callerSdkType;
1230
1413
  }
1231
- const newUrl = new URL(url);
1232
- newUrl.searchParams.append('key', apiKey);
1233
- return newUrl.toString();
1234
1414
  }
1235
1415
 
1236
1416
  /**
@@ -1249,6 +1429,7 @@ function addToken(url, apiKey) {
1249
1429
  * See the License for the specific language governing permissions and
1250
1430
  * limitations under the License.
1251
1431
  */
1432
+ /** The fetch implementation to be used by the {@link RESTTransport}. */
1252
1433
  let connectFetch = globalThis.fetch;
1253
1434
  function getGoogApiClientValue(_isUsingGen, _callerSdkType) {
1254
1435
  let str = 'gl-js/ fire/' + SDK_VERSION;
@@ -1293,14 +1474,20 @@ async function dcFetch(url, body, { signal }, appId, accessToken, appCheckToken,
1293
1474
  response = await connectFetch(url, fetchOptions);
1294
1475
  }
1295
1476
  catch (err) {
1296
- throw new DataConnectError(Code.OTHER, 'Failed to fetch: ' + JSON.stringify(err));
1477
+ const message = err && typeof err === 'object' && 'message' in err
1478
+ ? err['message']
1479
+ : String(err);
1480
+ throw new DataConnectError(Code.OTHER, 'Failed to fetch: ' + message);
1297
1481
  }
1298
1482
  let jsonResponse;
1299
1483
  try {
1300
1484
  jsonResponse = await response.json();
1301
1485
  }
1302
1486
  catch (e) {
1303
- throw new DataConnectError(Code.OTHER, JSON.stringify(e));
1487
+ const message = e && typeof e === 'object' && 'message' in e
1488
+ ? e['message']
1489
+ : String(e);
1490
+ throw new DataConnectError(Code.OTHER, 'Failed to parse JSON response: ' + message);
1304
1491
  }
1305
1492
  const message = getErrorMessage(jsonResponse);
1306
1493
  if (response.status >= 400) {
@@ -1348,147 +1535,988 @@ function getErrorMessage(obj) {
1348
1535
  * See the License for the specific language governing permissions and
1349
1536
  * limitations under the License.
1350
1537
  */
1351
- class RESTTransport {
1538
+ const PROD_HOST = 'firebasedataconnect.googleapis.com';
1539
+ const WEBSOCKET_PATH = 'ws/google.firebase.dataconnect.v1.ConnectorStreamService';
1540
+ function restUrlBuilder(projectConfig, transportOptions) {
1541
+ const { connector, location, projectId: project, service } = projectConfig;
1542
+ const { host, sslEnabled, port } = transportOptions;
1543
+ const protocol = sslEnabled ? 'https' : 'http';
1544
+ const realHost = host || PROD_HOST;
1545
+ let baseUrl = `${protocol}://${realHost}`;
1546
+ if (typeof port === 'number') {
1547
+ baseUrl += `:${port}`;
1548
+ }
1549
+ else if (typeof port !== 'undefined') {
1550
+ logError('Port type is of an invalid type');
1551
+ throw new DataConnectError(Code.INVALID_ARGUMENT, 'Incorrect type for port passed in!');
1552
+ }
1553
+ return `${baseUrl}/v1/projects/${project}/locations/${location}/services/${service}/connectors/${connector}`;
1554
+ }
1555
+ function websocketUrlBuilder(projectConfig, transportOptions) {
1556
+ const { location } = projectConfig;
1557
+ const { host, sslEnabled, port } = transportOptions;
1558
+ const protocol = sslEnabled ? 'wss' : 'ws';
1559
+ const realHost = host || PROD_HOST;
1560
+ let baseUrl = `${protocol}://${realHost}`;
1561
+ if (typeof port === 'number') {
1562
+ baseUrl += `:${port}`;
1563
+ }
1564
+ else if (typeof port !== 'undefined') {
1565
+ logError('Port type is of an invalid type');
1566
+ throw new DataConnectError(Code.INVALID_ARGUMENT, 'Incorrect type for port passed in!');
1567
+ }
1568
+ return `${baseUrl}/${WEBSOCKET_PATH}/Connect/locations/${location}`;
1569
+ }
1570
+ function addToken(url, apiKey) {
1571
+ if (!apiKey) {
1572
+ return url;
1573
+ }
1574
+ const newUrl = new URL(url);
1575
+ newUrl.searchParams.append('key', apiKey);
1576
+ return newUrl.toString();
1577
+ }
1578
+
1579
+ /**
1580
+ * @license
1581
+ * Copyright 2024 Google LLC
1582
+ *
1583
+ * Licensed under the Apache License, Version 2.0 (the "License");
1584
+ * you may not use this file except in compliance with the License.
1585
+ * You may obtain a copy of the License at
1586
+ *
1587
+ * http://www.apache.org/licenses/LICENSE-2.0
1588
+ *
1589
+ * Unless required by applicable law or agreed to in writing, software
1590
+ * distributed under the License is distributed on an "AS IS" BASIS,
1591
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1592
+ * See the License for the specific language governing permissions and
1593
+ * limitations under the License.
1594
+ */
1595
+ /**
1596
+ * Fetch-based REST implementation of {@link AbstractDataConnectTransport}.
1597
+ * @internal
1598
+ */
1599
+ class RESTTransport extends AbstractDataConnectTransport {
1352
1600
  constructor(options, apiKey, appId, authProvider, appCheckProvider, transportOptions, _isUsingGen = false, _callerSdkType = CallerSdkTypeEnum.Base) {
1353
- this.apiKey = apiKey;
1354
- this.appId = appId;
1355
- this.authProvider = authProvider;
1356
- this.appCheckProvider = appCheckProvider;
1357
- this._isUsingGen = _isUsingGen;
1358
- this._callerSdkType = _callerSdkType;
1359
- this._host = '';
1360
- this._location = 'l';
1361
- this._connectorName = '';
1362
- this._secure = true;
1363
- this._project = 'p';
1364
- this._accessToken = null;
1365
- this._appCheckToken = null;
1366
- this._lastToken = null;
1367
- this._isUsingEmulator = false;
1368
- // TODO(mtewani): Update U to include shape of body defined in line 13.
1601
+ super(options, apiKey, appId, authProvider, appCheckProvider, transportOptions, _isUsingGen, _callerSdkType);
1369
1602
  this.invokeQuery = (queryName, body) => {
1370
1603
  const abortController = new AbortController();
1371
1604
  // TODO(mtewani): Update to proper value
1372
1605
  const withAuth = this.withRetry(() => dcFetch(addToken(`${this.endpointUrl}:executeQuery`, this.apiKey), {
1373
- name: `projects/${this._project}/locations/${this._location}/services/${this._serviceName}/connectors/${this._connectorName}`,
1606
+ name: this._connectorResourcePath,
1374
1607
  operationName: queryName,
1375
1608
  variables: body
1376
- }, abortController, this.appId, this._accessToken, this._appCheckToken, this._isUsingGen, this._callerSdkType, this._isUsingEmulator));
1609
+ }, abortController, this.appId, this._authToken, this._appCheckToken, this._isUsingGen, this._callerSdkType, this._isUsingEmulator));
1377
1610
  return withAuth;
1378
1611
  };
1379
1612
  this.invokeMutation = (mutationName, body) => {
1380
1613
  const abortController = new AbortController();
1381
1614
  const taskResult = this.withRetry(() => {
1382
1615
  return dcFetch(addToken(`${this.endpointUrl}:executeMutation`, this.apiKey), {
1383
- name: `projects/${this._project}/locations/${this._location}/services/${this._serviceName}/connectors/${this._connectorName}`,
1616
+ name: this._connectorResourcePath,
1384
1617
  operationName: mutationName,
1385
1618
  variables: body
1386
- }, abortController, this.appId, this._accessToken, this._appCheckToken, this._isUsingGen, this._callerSdkType, this._isUsingEmulator);
1619
+ }, abortController, this.appId, this._authToken, this._appCheckToken, this._isUsingGen, this._callerSdkType, this._isUsingEmulator);
1387
1620
  });
1388
1621
  return taskResult;
1389
1622
  };
1390
- if (transportOptions) {
1391
- if (typeof transportOptions.port === 'number') {
1392
- this._port = transportOptions.port;
1393
- }
1394
- if (typeof transportOptions.sslEnabled !== 'undefined') {
1395
- this._secure = transportOptions.sslEnabled;
1396
- }
1397
- this._host = transportOptions.host;
1398
- }
1399
- const { location, projectId: project, connector, service } = options;
1400
- if (location) {
1401
- this._location = location;
1623
+ }
1624
+ get endpointUrl() {
1625
+ return restUrlBuilder({
1626
+ connector: this._connectorName,
1627
+ location: this._location,
1628
+ projectId: this._project,
1629
+ service: this._serviceName
1630
+ }, {
1631
+ host: this._host,
1632
+ sslEnabled: this._secure,
1633
+ port: this._port
1634
+ });
1635
+ }
1636
+ invokeSubscribe(observer, queryName, body) {
1637
+ throw new DataConnectError(Code.NOT_SUPPORTED, 'Subscriptions are not supported using REST!');
1638
+ }
1639
+ invokeUnsubscribe(queryName, body) {
1640
+ throw new DataConnectError(Code.NOT_SUPPORTED, 'Unsubscriptions are not supported using REST!');
1641
+ }
1642
+ onAuthTokenChanged(newToken) {
1643
+ this._authToken = newToken;
1644
+ }
1645
+ }
1646
+
1647
+ /**
1648
+ * @license
1649
+ * Copyright 2026 Google LLC
1650
+ *
1651
+ * Licensed under the Apache License, Version 2.0 (the "License");
1652
+ * you may not use this file except in compliance with the License.
1653
+ * You may obtain a copy of the License at
1654
+ *
1655
+ * http://www.apache.org/licenses/LICENSE-2.0
1656
+ *
1657
+ * Unless required by applicable law or agreed to in writing, software
1658
+ * distributed under the License is distributed on an "AS IS" BASIS,
1659
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1660
+ * See the License for the specific language governing permissions and
1661
+ * limitations under the License.
1662
+ */
1663
+ /** The request id of the first request over the stream */
1664
+ const FIRST_REQUEST_ID = 1;
1665
+ /** Time to wait before closing an idle connection (no active subscriptions) */
1666
+ const IDLE_CONNECTION_TIMEOUT_MS = 60 * 1000; // 1 minute
1667
+ /**
1668
+ * The base class for all {@link DataConnectStreamTransport | Stream Transport} implementations.
1669
+ * Handles management of logical streams (requests), authentication, data routing to query layer, etc.
1670
+ * @internal
1671
+ */
1672
+ class AbstractDataConnectStreamTransport extends AbstractDataConnectTransport {
1673
+ constructor() {
1674
+ super(...arguments);
1675
+ this.pendingClose = false;
1676
+ /** True if the transport is unable to connect to the server */
1677
+ this.isUnableToConnect = false;
1678
+ /** The request ID of the next message to be sent. Monotonically increasing sequence number. */
1679
+ this.requestNumber = FIRST_REQUEST_ID;
1680
+ /**
1681
+ * Map of query/variables to their active execute/resume request bodies.
1682
+ */
1683
+ this.activeQueryExecuteRequests = new Map();
1684
+ /**
1685
+ * Map of mutation/variables to their active execute request bodies.
1686
+ */
1687
+ this.activeMutationExecuteRequests = new Map();
1688
+ /**
1689
+ * Map of query/variables to their active subscribe request bodies.
1690
+ */
1691
+ this.activeSubscribeRequests = new Map();
1692
+ /**
1693
+ * Map of active execution RequestIds and their corresponding Promises and resolvers.
1694
+ */
1695
+ this.executeRequestPromises = new Map();
1696
+ /**
1697
+ * Map of active subscription RequestIds and their corresponding observers.
1698
+ */
1699
+ this.subscribeObservers = new Map();
1700
+ /** current close timeout from setTimeout(), if any */
1701
+ this.closeTimeout = null;
1702
+ /** has the close timeout finished? */
1703
+ this.closeTimeoutFinished = false;
1704
+ /** Flag to ensure we wait for the initial auth state once per connection attempt. */
1705
+ this.hasWaitedForInitialAuth = false;
1706
+ /**
1707
+ * Tracks if the next message to be sent is the first message of the stream.
1708
+ */
1709
+ this.isFirstStreamMessage = true;
1710
+ /**
1711
+ * Tracks the last auth token sent to the server.
1712
+ * Used to detect if the token has changed and needs to be resent.
1713
+ */
1714
+ this.lastSentAuthToken = null;
1715
+ }
1716
+ /** Is the stream currently waiting to close connection? */
1717
+ get isPendingClose() {
1718
+ return this.pendingClose;
1719
+ }
1720
+ /** True if there are active subscriptions on the stream */
1721
+ get hasActiveSubscriptions() {
1722
+ return this.activeSubscribeRequests.size > 0;
1723
+ }
1724
+ /** True if there are active execute or mutation requests on the stream */
1725
+ get hasActiveExecuteRequests() {
1726
+ return (this.activeQueryExecuteRequests.size > 0 ||
1727
+ this.activeMutationExecuteRequests.size > 0);
1728
+ }
1729
+ /**
1730
+ * Generates and returns the next request ID.
1731
+ */
1732
+ nextRequestId() {
1733
+ return (this.requestNumber++).toString();
1734
+ }
1735
+ /**
1736
+ * Tracks a query execution request, storing the request body and creating and storing a promise that
1737
+ * will be resolved when the response is received.
1738
+ * @returns The reject function and the response promise.
1739
+ *
1740
+ * @remarks
1741
+ * This method returns a promise, but is synchronous.
1742
+ */
1743
+ trackQueryExecuteRequest(requestId, mapKey, executeBody) {
1744
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1745
+ let resolveFn;
1746
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1747
+ let rejectFn;
1748
+ const responsePromise = new Promise((resolve, reject) => {
1749
+ resolveFn = resolve;
1750
+ rejectFn = reject;
1751
+ });
1752
+ const executeRequestPromise = {
1753
+ responsePromise,
1754
+ resolveFn: resolveFn,
1755
+ rejectFn: rejectFn
1756
+ };
1757
+ this.activeQueryExecuteRequests.set(mapKey, executeBody);
1758
+ this.executeRequestPromises.set(requestId, executeRequestPromise);
1759
+ return executeRequestPromise;
1760
+ }
1761
+ /**
1762
+ * Tracks a mutation execution request, storing the request body and creating and storing a promise
1763
+ * that will be resolved when the response is received.
1764
+ * @returns The reject function and the response promise.
1765
+ *
1766
+ * @remarks
1767
+ * This method returns a promise, but is synchronous.
1768
+ */
1769
+ trackMutationExecuteRequest(requestId, mapKey, executeBody) {
1770
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1771
+ let resolveFn;
1772
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1773
+ let rejectFn;
1774
+ const responsePromise = new Promise((resolve, reject) => {
1775
+ resolveFn = resolve;
1776
+ rejectFn = reject;
1777
+ });
1778
+ const executeRequestPromise = {
1779
+ responsePromise,
1780
+ resolveFn: resolveFn,
1781
+ rejectFn: rejectFn
1782
+ };
1783
+ const activeRequests = this.activeMutationExecuteRequests.get(mapKey) || [];
1784
+ activeRequests.push(executeBody);
1785
+ this.activeMutationExecuteRequests.set(mapKey, activeRequests);
1786
+ this.executeRequestPromises.set(requestId, executeRequestPromise);
1787
+ return executeRequestPromise;
1788
+ }
1789
+ /**
1790
+ * Tracks a subscribe request, storing the request body and the notification observer.
1791
+ * @remarks
1792
+ * This method is synchronous.
1793
+ */
1794
+ trackSubscribeRequest(requestId, mapKey, subscribeBody, observer) {
1795
+ this.activeSubscribeRequests.set(mapKey, subscribeBody);
1796
+ this.subscribeObservers.set(requestId, observer);
1797
+ }
1798
+ /**
1799
+ * Cleans up the query execute request tracking data structures, deleting the tracked request and
1800
+ * it's associated promise.
1801
+ */
1802
+ cleanupQueryExecuteRequest(requestId, mapKey) {
1803
+ this.activeQueryExecuteRequests.delete(mapKey);
1804
+ this.executeRequestPromises.delete(requestId);
1805
+ }
1806
+ /**
1807
+ * Cleans up the mutation execute request tracking data structures, deleting the tracked request and
1808
+ * it's associated promise.
1809
+ */
1810
+ cleanupMutationExecuteRequest(requestId, mapKey) {
1811
+ const executeRequests = this.activeMutationExecuteRequests.get(mapKey);
1812
+ if (executeRequests) {
1813
+ const updatedRequests = executeRequests.filter(req => req.requestId !== requestId);
1814
+ if (updatedRequests.length > 0) {
1815
+ this.activeMutationExecuteRequests.set(mapKey, updatedRequests);
1816
+ }
1817
+ else {
1818
+ this.activeMutationExecuteRequests.delete(mapKey);
1819
+ }
1402
1820
  }
1403
- if (project) {
1404
- this._project = project;
1821
+ this.executeRequestPromises.delete(requestId);
1822
+ }
1823
+ /**
1824
+ * Cleans up the subscribe request tracking data structures, deleting the tracked request and
1825
+ * it's associated promise.
1826
+ */
1827
+ cleanupSubscribeRequest(requestId, mapKey) {
1828
+ this.activeSubscribeRequests.delete(mapKey);
1829
+ this.subscribeObservers.delete(requestId);
1830
+ }
1831
+ /**
1832
+ * Indicates whether we should include the auth token in the next message.
1833
+ * Only true if there is an auth token and it is different from the last sent auth token, or this
1834
+ * is the first message.
1835
+ */
1836
+ get shouldIncludeAuth() {
1837
+ return (this.isFirstStreamMessage ||
1838
+ (!!this._authToken && this._authToken !== this.lastSentAuthToken));
1839
+ }
1840
+ /**
1841
+ * Called by the concrete transport implementation when the physical connection is ready.
1842
+ */
1843
+ onConnectionReady() {
1844
+ this.isFirstStreamMessage = true;
1845
+ this.lastSentAuthToken = null;
1846
+ this.hasWaitedForInitialAuth = false;
1847
+ }
1848
+ /**
1849
+ * Attempt to close the connection. Will only close if there are no active requests preventing it
1850
+ * from doing so.
1851
+ */
1852
+ async attemptClose() {
1853
+ if (this.hasActiveSubscriptions || this.hasActiveExecuteRequests) {
1854
+ return;
1405
1855
  }
1406
- this._serviceName = service;
1407
- if (!connector) {
1408
- throw new DataConnectError(Code.INVALID_ARGUMENT, 'Connector Name required!');
1856
+ this.cancelClose();
1857
+ await this.closeConnection();
1858
+ this.onGracefulStreamClose?.();
1859
+ }
1860
+ /**
1861
+ * Begin closing the connection. Waits for and cleans up all active requests, and waits for
1862
+ * {@link IDLE_CONNECTION_TIMEOUT_MS}. This is a graceful close - it will be called when there are
1863
+ * no more active subscriptions, so there's no need to cleanup.
1864
+ */
1865
+ prepareToCloseGracefully() {
1866
+ if (this.pendingClose) {
1867
+ return;
1409
1868
  }
1410
- this._connectorName = connector;
1411
- this.authProvider?.addTokenChangeListener(token => {
1412
- logDebug(`New Token Available: ${token}`);
1413
- this._accessToken = token;
1869
+ this.pendingClose = true;
1870
+ this.closeTimeoutFinished = false;
1871
+ this.closeTimeout = setTimeout(() => {
1872
+ this.closeTimeoutFinished = true;
1873
+ void this.attemptClose();
1874
+ }, IDLE_CONNECTION_TIMEOUT_MS);
1875
+ }
1876
+ /**
1877
+ * Cancel closing the connection.
1878
+ */
1879
+ cancelClose() {
1880
+ if (this.closeTimeout) {
1881
+ clearTimeout(this.closeTimeout);
1882
+ }
1883
+ this.pendingClose = false;
1884
+ this.closeTimeoutFinished = false;
1885
+ }
1886
+ /**
1887
+ * Reject all active execute promises and notify all subscribe observers with the given error.
1888
+ * Clear active request tracking maps without cancelling or re-invoking any requests.
1889
+ */
1890
+ rejectAllActiveRequests(code, reason) {
1891
+ this.activeQueryExecuteRequests.clear();
1892
+ this.activeMutationExecuteRequests.clear();
1893
+ this.activeSubscribeRequests.clear();
1894
+ const error = new DataConnectError(code, reason);
1895
+ for (const [requestId, { rejectFn }] of this.executeRequestPromises) {
1896
+ this.executeRequestPromises.delete(requestId);
1897
+ rejectFn(error);
1898
+ }
1899
+ for (const [requestId, observer] of this.subscribeObservers) {
1900
+ this.subscribeObservers.delete(requestId);
1901
+ observer.onDisconnect(code, reason);
1902
+ }
1903
+ }
1904
+ /**
1905
+ * Called by concrete implementations when the stream is successfully closed, gracefully or otherwise.
1906
+ */
1907
+ onStreamClose(code, reason) {
1908
+ this.rejectAllActiveRequests(Code.OTHER, `Stream disconnected with code ${code}: ${reason}`);
1909
+ }
1910
+ /**
1911
+ * Prepares a stream request message by adding necessary headers and metadata.
1912
+ * If this is the first message on the stream, it includes the resource name, auth token, and App Check token.
1913
+ * If the auth token has refreshed since the last message, it includes the new auth token.
1914
+ *
1915
+ * This method is called by the concrete transport implementation before sending a message.
1916
+ *
1917
+ * @returns the requestBody, with attached headers and initial request fields
1918
+ */
1919
+ prepareMessage(requestBody) {
1920
+ const preparedRequestBody = { ...requestBody };
1921
+ const headers = {};
1922
+ if (this.appId) {
1923
+ headers['x-firebase-gmpid'] = this.appId;
1924
+ }
1925
+ headers['X-Goog-Api-Client'] = getGoogApiClientValue$1(this._isUsingGen, this._callerSdkType);
1926
+ if (this.shouldIncludeAuth && this._authToken) {
1927
+ headers['X-Firebase-Auth-Token'] = this._authToken;
1928
+ this.lastSentAuthToken = this._authToken;
1929
+ }
1930
+ if (this.isFirstStreamMessage) {
1931
+ if (this._appCheckToken) {
1932
+ headers['X-Firebase-App-Check'] = this._appCheckToken;
1933
+ }
1934
+ preparedRequestBody.name = this._connectorResourcePath;
1935
+ }
1936
+ preparedRequestBody.headers = headers;
1937
+ this.isFirstStreamMessage = false;
1938
+ return preparedRequestBody;
1939
+ }
1940
+ // TODO(stephenarosaj): just make this async
1941
+ /**
1942
+ * Sends a request message to the server via the concrete implementation.
1943
+ * Ensures the connection is ready and prepares the message before sending.
1944
+ * @returns A promise that resolves when the request message has been sent.
1945
+ */
1946
+ async sendRequestMessage(requestBody) {
1947
+ if (!this.hasWaitedForInitialAuth && this.authProvider) {
1948
+ await this.getWithAuth();
1949
+ this.hasWaitedForInitialAuth = true;
1950
+ }
1951
+ if (this.streamIsReady) {
1952
+ const prepared = this.prepareMessage(requestBody);
1953
+ return this.sendMessage(prepared);
1954
+ }
1955
+ return this.ensureConnection().then(() => {
1956
+ const prepared = this.prepareMessage(requestBody);
1957
+ return this.sendMessage(prepared);
1414
1958
  });
1415
- this.appCheckProvider?.addTokenChangeListener(result => {
1416
- const { token } = result;
1417
- logDebug(`New App Check Token Available: ${token}`);
1418
- this._appCheckToken = token;
1959
+ }
1960
+ /**
1961
+ * Helper to generate a consistent string key for the tracking maps.
1962
+ */
1963
+ getMapKey(operationName, variables) {
1964
+ const sortedVariables = this.sortObjectKeys(variables);
1965
+ return JSON.stringify({ operationName, variables: sortedVariables });
1966
+ }
1967
+ /**
1968
+ * Recursively sorts the keys of an object.
1969
+ */
1970
+ sortObjectKeys(obj) {
1971
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
1972
+ return obj;
1973
+ }
1974
+ const sortedObj = {};
1975
+ Object.keys(obj)
1976
+ .sort()
1977
+ .forEach(key => {
1978
+ sortedObj[key] = this.sortObjectKeys(obj[key]);
1979
+ });
1980
+ return sortedObj;
1981
+ }
1982
+ /**
1983
+ * @inheritdoc
1984
+ * @remarks
1985
+ * This method synchronously updates the request tracking data structures before sending any message.
1986
+ * If any asynchronous functionality is added to this function, it MUST be done in a way that
1987
+ * preserves the synchronous update of the tracking data structures before the method returns.
1988
+ */
1989
+ invokeQuery(queryName, variables) {
1990
+ const requestId = this.nextRequestId();
1991
+ const activeRequestKey = { operationName: queryName, variables };
1992
+ const mapKey = this.getMapKey(queryName, variables);
1993
+ const executeBody = {
1994
+ requestId,
1995
+ execute: activeRequestKey
1996
+ };
1997
+ let { responsePromise, rejectFn } = this.trackQueryExecuteRequest(requestId, mapKey, executeBody);
1998
+ responsePromise = responsePromise.finally(() => {
1999
+ this.cleanupQueryExecuteRequest(requestId, mapKey);
2000
+ if (!this.hasActiveSubscriptions &&
2001
+ !this.hasActiveExecuteRequests &&
2002
+ this.closeTimeoutFinished) {
2003
+ void this.attemptClose();
2004
+ }
2005
+ });
2006
+ // asynchronous, fire and forget
2007
+ this.sendRequestMessage(executeBody).catch(err => {
2008
+ rejectFn(err);
2009
+ });
2010
+ return responsePromise;
2011
+ }
2012
+ /**
2013
+ * @inheritdoc
2014
+ * @remarks
2015
+ * This method synchronously updates the request tracking data structures before sending any message.
2016
+ * If any asynchronous functionality is added to this function, it MUST be done in a way that
2017
+ * preserves the synchronous update of the tracking data structures before the method returns.
2018
+ */
2019
+ invokeMutation(mutationName, variables) {
2020
+ const requestId = this.nextRequestId();
2021
+ const activeRequestKey = { operationName: mutationName, variables };
2022
+ const mapKey = this.getMapKey(mutationName, variables);
2023
+ const executeBody = {
2024
+ requestId,
2025
+ execute: activeRequestKey
2026
+ };
2027
+ let { responsePromise, rejectFn } = this.trackMutationExecuteRequest(requestId, mapKey, executeBody);
2028
+ responsePromise = responsePromise.finally(() => {
2029
+ this.cleanupMutationExecuteRequest(requestId, mapKey);
2030
+ if (!this.hasActiveSubscriptions &&
2031
+ !this.hasActiveExecuteRequests &&
2032
+ this.closeTimeoutFinished) {
2033
+ void this.attemptClose();
2034
+ }
2035
+ });
2036
+ // asynchronous, fire and forget
2037
+ this.sendRequestMessage(executeBody).catch(err => {
2038
+ rejectFn(err);
1419
2039
  });
2040
+ return responsePromise;
2041
+ }
2042
+ /**
2043
+ * @inheritdoc
2044
+ * @remarks
2045
+ * This method synchronously updates the request tracking data structures before sending any message
2046
+ * or cancelling the closing of the stream. If any asynchronous functionality is added to this function,
2047
+ * it MUST be done in a way that preserves the synchronous update of the tracking data structures
2048
+ * before the method returns.
2049
+ */
2050
+ invokeSubscribe(observer, queryName, variables) {
2051
+ // if we are waiting to close the stream, cancel closing!
2052
+ this.cancelClose();
2053
+ const requestId = this.nextRequestId();
2054
+ const activeRequestKey = { operationName: queryName, variables };
2055
+ const mapKey = this.getMapKey(queryName, variables);
2056
+ const subscribeBody = {
2057
+ requestId,
2058
+ subscribe: activeRequestKey
2059
+ };
2060
+ this.trackSubscribeRequest(requestId, mapKey, subscribeBody, observer);
2061
+ // asynchronous, fire and forget
2062
+ this.sendRequestMessage(subscribeBody).catch(err => {
2063
+ observer.onError(err instanceof Error ? err : new Error(String(err)));
2064
+ this.cleanupSubscribeRequest(requestId, mapKey);
2065
+ if (!this.hasActiveSubscriptions) {
2066
+ this.prepareToCloseGracefully();
2067
+ }
2068
+ });
2069
+ }
2070
+ /**
2071
+ * @inheritdoc
2072
+ * @remarks
2073
+ * This method synchronously updates the request tracking data structures before sending any message.
2074
+ * If any asynchronous functionality is added to this function, it MUST be done in a way that
2075
+ * preserves the synchronous update of the tracking data structures before the method returns.
2076
+ */
2077
+ invokeUnsubscribe(queryName, variables) {
2078
+ const mapKey = this.getMapKey(queryName, variables);
2079
+ const subscribeRequest = this.activeSubscribeRequests.get(mapKey);
2080
+ if (!subscribeRequest) {
2081
+ return;
2082
+ }
2083
+ const requestId = subscribeRequest.requestId;
2084
+ const cancelBody = {
2085
+ requestId,
2086
+ cancel: {}
2087
+ };
2088
+ this.cleanupSubscribeRequest(requestId, mapKey);
2089
+ // asynchronous, fire and forget
2090
+ this.sendRequestMessage(cancelBody).catch(err => {
2091
+ logError(`Stream Transport failed to send unsubscribe message: ${err}`);
2092
+ });
2093
+ if (!this.hasActiveSubscriptions) {
2094
+ this.prepareToCloseGracefully();
2095
+ }
2096
+ }
2097
+ onAuthTokenChanged(newToken) {
2098
+ const oldAuthToken = this._authToken;
2099
+ this._authToken = newToken;
2100
+ const oldAuthUid = this.authUid;
2101
+ const newAuthUid = this.authProvider?.getAuth()?.getUid();
2102
+ this.authUid = newAuthUid;
2103
+ // onAuthTokenChanged gets called by the auth provider once it initializes, so we must make sure
2104
+ // we don't prematurely disconnect the stream if this is the initial call.
2105
+ const isInitialAuth = oldAuthUid === undefined;
2106
+ if (isInitialAuth) {
2107
+ return;
2108
+ }
2109
+ if ((oldAuthToken && newToken === null) || // user logged out
2110
+ (!oldAuthUid && newAuthUid) || // user logged in
2111
+ (oldAuthUid && newAuthUid !== oldAuthUid) // logged in user changed
2112
+ ) {
2113
+ this.rejectAllActiveRequests(Code.UNAUTHORIZED, 'Stream disconnected due to auth change.');
2114
+ void this.attemptClose();
2115
+ }
2116
+ }
2117
+ /**
2118
+ * Handle a response message from the server. Called by the connection-specific implementation after
2119
+ * it's transformed a message from the server into a {@link DataConnectResponse}.
2120
+ * @param requestId the requestId associated with this response.
2121
+ * @param response the response from the server.
2122
+ */
2123
+ async handleResponse(requestId, response) {
2124
+ if (this.executeRequestPromises.has(requestId)) {
2125
+ // don't clean up the tracking maps here, they're handled automatically when the execute promise settles
2126
+ const { resolveFn, rejectFn } = this.executeRequestPromises.get(requestId);
2127
+ if (response.errors && response.errors.length) {
2128
+ const failureResponse = {
2129
+ errors: response.errors,
2130
+ data: response.data
2131
+ };
2132
+ const stringified = JSON.stringify(response.errors);
2133
+ rejectFn(new DataConnectOperationError('DataConnect error while performing request: ' + stringified, failureResponse));
2134
+ }
2135
+ else {
2136
+ resolveFn(response);
2137
+ }
2138
+ }
2139
+ else if (this.subscribeObservers.has(requestId)) {
2140
+ const observer = this.subscribeObservers.get(requestId);
2141
+ await observer.onData(response);
2142
+ }
2143
+ else {
2144
+ throw new DataConnectError(Code.OTHER, `Stream response contained unrecognized requestId '${requestId}'`);
2145
+ }
1420
2146
  }
2147
+ }
2148
+
2149
+ /**
2150
+ * @license
2151
+ * Copyright 2026 Google LLC
2152
+ *
2153
+ * Licensed under the Apache License, Version 2.0 (the "License");
2154
+ * you may not use this file except in compliance with the License.
2155
+ * You may obtain a copy of the License at
2156
+ *
2157
+ * http://www.apache.org/licenses/LICENSE-2.0
2158
+ *
2159
+ * Unless required by applicable law or agreed to in writing, software
2160
+ * distributed under the License is distributed on an "AS IS" BASIS,
2161
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2162
+ * See the License for the specific language governing permissions and
2163
+ * limitations under the License.
2164
+ */
2165
+ /** The WebSocket implementation to be used by the {@link WebSocketTransport}. */
2166
+ let connectWebSocket = globalThis.WebSocket;
2167
+ /**
2168
+ * The code used to close the WebSocket connection.
2169
+ * This is a protocol-level code, and is not the same as the {@link Code | DataConnect error code}.
2170
+ * @internal
2171
+ */
2172
+ const WEBSOCKET_CLOSE_CODE = 1000;
2173
+ /**
2174
+ * An {@link AbstractDataConnectStreamTransport | Stream Transport} implementation that uses {@link WebSocket | WebSockets} to stream requests and responses.
2175
+ * This class handles the lifecycle of the WebSocket connection, including automatic
2176
+ * reconnection and request correlation.
2177
+ * @internal
2178
+ */
2179
+ class WebSocketTransport extends AbstractDataConnectStreamTransport {
1421
2180
  get endpointUrl() {
1422
- return urlBuilder({
2181
+ return websocketUrlBuilder({
1423
2182
  connector: this._connectorName,
1424
2183
  location: this._location,
1425
2184
  projectId: this._project,
1426
2185
  service: this._serviceName
1427
- }, { host: this._host, sslEnabled: this._secure, port: this._port });
2186
+ }, {
2187
+ host: this._host,
2188
+ sslEnabled: this._secure,
2189
+ port: this._port
2190
+ });
1428
2191
  }
1429
- useEmulator(host, port, isSecure) {
1430
- this._host = host;
1431
- this._isUsingEmulator = true;
1432
- if (typeof port === 'number') {
1433
- this._port = port;
1434
- }
1435
- if (typeof isSecure !== 'undefined') {
1436
- this._secure = isSecure;
2192
+ /**
2193
+ * Decodes a WebSocket response from a Uint8Array to a JSON object.
2194
+ * Emulator does not send messages as Uint8Arrays, but prod does.
2195
+ */
2196
+ decodeBinaryResponse(data) {
2197
+ if (!this.decoder) {
2198
+ this.decoder = new TextDecoder('utf-8');
1437
2199
  }
2200
+ return this.decoder.decode(data);
1438
2201
  }
1439
- onTokenChanged(newToken) {
1440
- this._accessToken = newToken;
2202
+ get streamIsReady() {
2203
+ return this.connection?.readyState === WebSocket.OPEN;
1441
2204
  }
1442
- async getWithAuth(forceToken = false) {
1443
- let starterPromise = new Promise(resolve => resolve(this._accessToken));
1444
- if (this.appCheckProvider) {
1445
- const appCheckToken = await this.appCheckProvider.getToken();
1446
- if (appCheckToken) {
1447
- this._appCheckToken = appCheckToken.token;
2205
+ constructor(options, apiKey, appId, authProvider, appCheckProvider, transportOptions, _isUsingGen = false, _callerSdkType = CallerSdkTypeEnum.Base) {
2206
+ super(options, apiKey, appId, authProvider, appCheckProvider, transportOptions, _isUsingGen, _callerSdkType);
2207
+ this.apiKey = apiKey;
2208
+ this.appId = appId;
2209
+ this.authProvider = authProvider;
2210
+ this.appCheckProvider = appCheckProvider;
2211
+ this._isUsingGen = _isUsingGen;
2212
+ this._callerSdkType = _callerSdkType;
2213
+ /** Decodes binary WebSocket responses to strings */
2214
+ this.decoder = undefined;
2215
+ /** The current connection to the server. Undefined if disconnected. */
2216
+ this.connection = undefined;
2217
+ /**
2218
+ * Current connection attempt. If null, we are not currently attemping to connect (not connected,
2219
+ * or already connected). Will be resolved or rejected when the connection is opened or fails to open.
2220
+ */
2221
+ this.connectionAttempt = null;
2222
+ }
2223
+ ensureConnection() {
2224
+ try {
2225
+ if (this.streamIsReady) {
2226
+ return Promise.resolve();
1448
2227
  }
1449
- }
1450
- if (this.authProvider) {
1451
- starterPromise = this.authProvider
1452
- .getToken(/*forceToken=*/ forceToken)
1453
- .then(data => {
1454
- if (!data) {
1455
- return null;
2228
+ if (this.connectionAttempt) {
2229
+ return this.connectionAttempt;
2230
+ }
2231
+ this.connectionAttempt = new Promise((resolve, reject) => {
2232
+ if (!connectWebSocket) {
2233
+ throw new DataConnectError(Code.OTHER, 'No WebSocket Implementation detected!');
1456
2234
  }
1457
- this._accessToken = data.accessToken;
1458
- return this._accessToken;
2235
+ const ws = new connectWebSocket(this.endpointUrl);
2236
+ this.connection = ws;
2237
+ this.connection.binaryType = 'arraybuffer';
2238
+ ws.onopen = () => {
2239
+ this.isUnableToConnect = false;
2240
+ this.onConnectionReady();
2241
+ resolve();
2242
+ };
2243
+ ws.onerror = event => {
2244
+ this.connectionAttempt = null;
2245
+ this.isUnableToConnect = true;
2246
+ const error = new DataConnectError(Code.OTHER, `Error using WebSocket connection, closing WebSocket`);
2247
+ this.handleError(error);
2248
+ reject(error);
2249
+ };
2250
+ ws.onmessage = ev => this.handleWebSocketMessage(ev).catch(async (reason) => {
2251
+ this.handleError(reason);
2252
+ });
2253
+ ws.onclose = ev => this.handleWebsocketDisconnect(ev);
1459
2254
  });
2255
+ return this.connectionAttempt;
1460
2256
  }
1461
- else {
1462
- starterPromise = new Promise(resolve => resolve(''));
2257
+ catch (error) {
2258
+ this.handleError(error);
2259
+ throw error;
1463
2260
  }
1464
- return starterPromise;
1465
2261
  }
1466
- _setLastToken(lastToken) {
1467
- this._lastToken = lastToken;
2262
+ openConnection() {
2263
+ return this.ensureConnection().catch(err => {
2264
+ throw new DataConnectError(Code.OTHER, `Failed to open connection: ${err}`);
2265
+ });
1468
2266
  }
1469
- withRetry(promiseFactory, retry = false) {
1470
- let isNewToken = false;
1471
- return this.getWithAuth(retry)
1472
- .then(res => {
1473
- isNewToken = this._lastToken !== res;
1474
- this._lastToken = res;
1475
- return res;
1476
- })
1477
- .then(promiseFactory)
1478
- .catch(err => {
1479
- // Only retry if the result is unauthorized and the last token isn't the same as the new one.
1480
- if ('code' in err &&
1481
- err.code === Code.UNAUTHORIZED &&
1482
- !retry &&
1483
- isNewToken) {
1484
- logDebug('Retrying due to unauthorized');
1485
- return this.withRetry(promiseFactory, true);
2267
+ closeConnection(code, reason) {
2268
+ if (!this.connection) {
2269
+ this.connectionAttempt = null;
2270
+ return Promise.resolve();
2271
+ }
2272
+ let error;
2273
+ try {
2274
+ if (reason) {
2275
+ // reason string can be max 123 bytes (not characters, bytes)
2276
+ // https://developer.mozilla.org/en-US/docs/Web/API/WebSocketStream/close#parameters
2277
+ const MAX_BYTES = 123;
2278
+ const encoder = new TextEncoder();
2279
+ const bytes = encoder.encode(reason);
2280
+ if (bytes.length <= MAX_BYTES) {
2281
+ this.connection.close(code, reason);
2282
+ }
2283
+ else {
2284
+ const buf = new Uint8Array(MAX_BYTES);
2285
+ const { read } = encoder.encodeInto(reason, buf);
2286
+ const truncatedReason = reason.substring(0, read);
2287
+ this.connection.close(code, truncatedReason);
2288
+ }
2289
+ }
2290
+ else {
2291
+ this.connection.close(code);
2292
+ }
2293
+ }
2294
+ catch (e) {
2295
+ error = e;
2296
+ }
2297
+ finally {
2298
+ this.connection = undefined;
2299
+ this.connectionAttempt = null;
2300
+ }
2301
+ if (error) {
2302
+ return Promise.reject(error);
2303
+ }
2304
+ return Promise.resolve();
2305
+ }
2306
+ /**
2307
+ * Handle a disconnection from the server. Initiates graceful clean up and reconnection attempts.
2308
+ * @param ev the {@link CloseEvent} that closed the WebSocket.
2309
+ */
2310
+ handleWebsocketDisconnect(ev) {
2311
+ this.connection = undefined;
2312
+ this.connectionAttempt = null;
2313
+ this.onStreamClose(ev.code, ev.reason);
2314
+ }
2315
+ /**
2316
+ * Handle an error that occurred on the WebSocket. Close the connection and reject all active requests.
2317
+ */
2318
+ handleError(error) {
2319
+ logError(`DataConnect WebSocket error, closing stream: ${error}`);
2320
+ let reason = error ? String(error) : 'Unknown Error';
2321
+ if (error instanceof DataConnectError) {
2322
+ reason = error.message;
2323
+ }
2324
+ void this.closeConnection(WEBSOCKET_CLOSE_CODE, reason);
2325
+ }
2326
+ sendMessage(requestBody) {
2327
+ return this.ensureConnection().then(() => {
2328
+ try {
2329
+ this.connection.send(JSON.stringify(requestBody));
2330
+ return Promise.resolve();
2331
+ }
2332
+ catch (err) {
2333
+ this.handleError(err);
2334
+ throw new DataConnectError(Code.OTHER, `Failed to send message: ${String(err)}`);
1486
2335
  }
1487
- throw err;
1488
2336
  });
1489
2337
  }
2338
+ /**
2339
+ * Handles incoming WebSocket messages.
2340
+ * @param ev The {@link MessageEvent} from the WebSocket.
2341
+ */
2342
+ async handleWebSocketMessage(ev) {
2343
+ const result = this.parseWebSocketData(ev.data);
2344
+ const requestId = result.requestId;
2345
+ const response = {
2346
+ data: result.data,
2347
+ errors: result.errors,
2348
+ extensions: result.extensions || { dataConnect: [] }
2349
+ };
2350
+ await this.handleResponse(requestId, response);
2351
+ }
2352
+ /**
2353
+ * Parse a response from the server. Assert that it has a {@link DataConnectStreamResponse.requestId | requestId}.
2354
+ * @param data the message from the server to be parsed
2355
+ * @returns the parsed message as a {@link DataConnectStreamResponse}
2356
+ * @throws {DataConnectError} if parsing fails or message is malformed.
2357
+ */
2358
+ parseWebSocketData(
2359
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2360
+ data) {
2361
+ const dataIsString = typeof data === 'string';
2362
+ /** raw websocket message */
2363
+ let webSocketMessage;
2364
+ /** object containing data, errors, and extensions */
2365
+ let result;
2366
+ try {
2367
+ if (dataIsString) {
2368
+ webSocketMessage = JSON.parse(data);
2369
+ }
2370
+ else {
2371
+ webSocketMessage = JSON.parse(this.decodeBinaryResponse(data));
2372
+ }
2373
+ }
2374
+ catch (err) {
2375
+ throw new DataConnectError(Code.OTHER, `Could not parse WebSocket message: ${err instanceof Error ? err.message : String(err)}`);
2376
+ }
2377
+ if (typeof webSocketMessage !== 'object' || webSocketMessage === null) {
2378
+ throw new DataConnectError(Code.OTHER, 'WebSocket message is not an object');
2379
+ }
2380
+ if (dataIsString) {
2381
+ if (!('result' in webSocketMessage)) {
2382
+ throw new DataConnectError(Code.OTHER, 'WebSocket message from emulator did not include result');
2383
+ }
2384
+ if (typeof webSocketMessage.result !== 'object' ||
2385
+ webSocketMessage.result === null) {
2386
+ throw new DataConnectError(Code.OTHER, 'WebSocket message result is not an object');
2387
+ }
2388
+ result = webSocketMessage.result;
2389
+ }
2390
+ else {
2391
+ result = webSocketMessage;
2392
+ }
2393
+ if (!('requestId' in result)) {
2394
+ throw new DataConnectError(Code.OTHER, 'WebSocket message did not include requestId');
2395
+ }
2396
+ return result;
2397
+ }
2398
+ }
2399
+
2400
+ /**
2401
+ * @license
2402
+ * Copyright 2026 Google LLC
2403
+ *
2404
+ * Licensed under the Apache License, Version 2.0 (the "License");
2405
+ * you may not use this file except in compliance with the License.
2406
+ * You may obtain a copy of the License at
2407
+ *
2408
+ * http://www.apache.org/licenses/LICENSE-2.0
2409
+ *
2410
+ * Unless required by applicable law or agreed to in writing, software
2411
+ * distributed under the License is distributed on an "AS IS" BASIS,
2412
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2413
+ * See the License for the specific language governing permissions and
2414
+ * limitations under the License.
2415
+ */
2416
+ /**
2417
+ * Entry point for the transport layer. Manages routing between transport implementations.
2418
+ * @internal
2419
+ */
2420
+ class DataConnectTransportManager {
2421
+ constructor(options, apiKey, appId, authProvider, appCheckProvider, transportOptions, _isUsingGen = false, _callerSdkType) {
2422
+ this.options = options;
2423
+ this.apiKey = apiKey;
2424
+ this.appId = appId;
2425
+ this.authProvider = authProvider;
2426
+ this.appCheckProvider = appCheckProvider;
2427
+ this.transportOptions = transportOptions;
2428
+ this._isUsingGen = _isUsingGen;
2429
+ this._callerSdkType = _callerSdkType;
2430
+ this.isUsingEmulator = false;
2431
+ this.restTransport = new RESTTransport(options, apiKey, appId, authProvider, appCheckProvider, transportOptions, _isUsingGen, _callerSdkType);
2432
+ }
2433
+ /**
2434
+ * Initializes the stream transport if it hasn't been already.
2435
+ */
2436
+ initStreamTransport() {
2437
+ if (!this.streamTransport) {
2438
+ this.streamTransport = new WebSocketTransport(this.options, this.apiKey, this.appId, this.authProvider, this.appCheckProvider, this.transportOptions, this._isUsingGen, this._callerSdkType);
2439
+ if (this.isUsingEmulator && this.transportOptions) {
2440
+ this.streamTransport.useEmulator(this.transportOptions.host, this.transportOptions.port, this.transportOptions.sslEnabled);
2441
+ }
2442
+ this.streamTransport.onGracefulStreamClose = () => {
2443
+ this.streamTransport = undefined;
2444
+ };
2445
+ }
2446
+ return this.streamTransport;
2447
+ }
2448
+ /**
2449
+ * Returns true if the stream is in a healthy, ready connection state and has active subscriptions.
2450
+ */
2451
+ executeShouldUseStream() {
2452
+ return (!!this.streamTransport &&
2453
+ !this.streamTransport.isPendingClose &&
2454
+ this.streamTransport.streamIsReady &&
2455
+ this.streamTransport.hasActiveSubscriptions &&
2456
+ !this.streamTransport.isUnableToConnect);
2457
+ }
2458
+ /**
2459
+ * Prefer to use Streaming Transport connection when one is available.
2460
+ * @inheritdoc
2461
+ */
2462
+ invokeQuery(queryName, body) {
2463
+ if (this.executeShouldUseStream()) {
2464
+ return this.streamTransport.invokeQuery(queryName, body).catch(err => {
2465
+ if (this.executeShouldUseStream()) {
2466
+ throw err;
2467
+ }
2468
+ return this.restTransport.invokeQuery(queryName, body);
2469
+ });
2470
+ }
2471
+ return this.restTransport.invokeQuery(queryName, body);
2472
+ }
2473
+ /**
2474
+ * Prefer to use Streaming Transport connection when one is available.
2475
+ * @inheritdoc
2476
+ */
2477
+ invokeMutation(queryName, body) {
2478
+ if (this.executeShouldUseStream()) {
2479
+ return this.streamTransport.invokeMutation(queryName, body).catch(err => {
2480
+ if (this.executeShouldUseStream()) {
2481
+ throw err;
2482
+ }
2483
+ return this.restTransport.invokeMutation(queryName, body);
2484
+ });
2485
+ }
2486
+ return this.restTransport.invokeMutation(queryName, body);
2487
+ }
2488
+ invokeSubscribe(observer, queryName, body) {
2489
+ const streamTransport = this.initStreamTransport();
2490
+ if (streamTransport.isUnableToConnect) {
2491
+ throw new DataConnectError(Code.OTHER, 'Unable to connect streaming connection to server. Subscriptions are unavailable.');
2492
+ }
2493
+ streamTransport.invokeSubscribe(observer, queryName, body);
2494
+ }
2495
+ invokeUnsubscribe(queryName, body) {
2496
+ if (this.streamTransport) {
2497
+ this.streamTransport.invokeUnsubscribe(queryName, body);
2498
+ }
2499
+ }
2500
+ useEmulator(host, port, sslEnabled) {
2501
+ this.isUsingEmulator = true;
2502
+ this.transportOptions = { host, port, sslEnabled };
2503
+ this.restTransport.useEmulator(host, port, sslEnabled);
2504
+ if (this.streamTransport) {
2505
+ this.streamTransport.useEmulator(host, port, sslEnabled);
2506
+ }
2507
+ }
2508
+ onAuthTokenChanged(token) {
2509
+ this.restTransport.onAuthTokenChanged(token);
2510
+ if (this.streamTransport) {
2511
+ this.streamTransport.onAuthTokenChanged(token);
2512
+ }
2513
+ }
1490
2514
  _setCallerSdkType(callerSdkType) {
1491
2515
  this._callerSdkType = callerSdkType;
2516
+ this.restTransport._setCallerSdkType(callerSdkType);
2517
+ if (this.streamTransport) {
2518
+ this.streamTransport._setCallerSdkType(callerSdkType);
2519
+ }
1492
2520
  }
1493
2521
  }
1494
2522
 
@@ -1654,8 +2682,8 @@ class DataConnect {
1654
2682
  return;
1655
2683
  }
1656
2684
  if (this._transportClass === undefined) {
1657
- logDebug('transportClass not provided. Defaulting to RESTTransport.');
1658
- this._transportClass = RESTTransport;
2685
+ logDebug('transportClass not provided. Defaulting to DataConnectTransportManager.');
2686
+ this._transportClass = DataConnectTransportManager;
1659
2687
  }
1660
2688
  this._authTokenProvider = new FirebaseAuthProvider(this.app.name, this.app.options, this._authProvider);
1661
2689
  const connectorConfig = {
@@ -2088,5 +3116,5 @@ function subscribe(queryRefOrSerializedResult, observerOrOnNext, onError, onComp
2088
3116
  */
2089
3117
  registerDataConnect();
2090
3118
 
2091
- export { CallerSdkTypeEnum, Code, DataConnect, DataConnectError, DataConnectOperationError, MUTATION_STR, MutationManager, QUERY_STR, QueryFetchPolicy, SOURCE_CACHE, SOURCE_SERVER, StorageType, areTransportOptionsEqual, connectDataConnectEmulator, executeMutation, executeQuery, getDataConnect, makeMemoryCacheProvider, mutationRef, parseOptions, queryRef, setLogLevel, subscribe, terminate, toQueryRef, validateArgs, validateArgsWithOptions, validateDCOptions };
3119
+ export { AbstractDataConnectTransport, CallerSdkTypeEnum, Code, DataConnect, DataConnectError, DataConnectOperationError, MUTATION_STR, MutationManager, QUERY_STR, QueryFetchPolicy, SOURCE_CACHE, SOURCE_SERVER, StorageType, areTransportOptionsEqual, connectDataConnectEmulator, executeMutation, executeQuery, getDataConnect, getGoogApiClientValue$1 as getGoogApiClientValue, makeMemoryCacheProvider, mutationRef, parseOptions, queryRef, setLogLevel, subscribe, terminate, toQueryRef, validateArgs, validateArgsWithOptions, validateDCOptions };
2092
3120
  //# sourceMappingURL=index.esm.js.map