@firebase/data-connect 0.3.12 → 0.4.0-canary.78384d32c

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 (64) hide show
  1. package/dist/index.cjs.js +1012 -210
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.esm.js +1011 -212
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/index.node.cjs.js +960 -158
  6. package/dist/index.node.cjs.js.map +1 -1
  7. package/dist/internal.d.ts +311 -25
  8. package/dist/node-esm/index.node.esm.js +959 -160
  9. package/dist/node-esm/index.node.esm.js.map +1 -1
  10. package/dist/node-esm/src/api/DataConnect.d.ts +44 -3
  11. package/dist/node-esm/src/api/Reference.d.ts +2 -0
  12. package/dist/node-esm/src/api/index.d.ts +2 -1
  13. package/dist/node-esm/src/api/query.d.ts +9 -26
  14. package/dist/node-esm/src/api.browser.d.ts +2 -18
  15. package/dist/node-esm/src/api.node.d.ts +2 -1
  16. package/dist/node-esm/src/cache/Cache.d.ts +53 -0
  17. package/dist/node-esm/src/cache/CacheProvider.d.ts +25 -0
  18. package/dist/node-esm/src/cache/EntityDataObject.d.ts +37 -0
  19. package/dist/node-esm/src/cache/EntityNode.d.ts +56 -0
  20. package/dist/node-esm/src/cache/ImpactedQueryRefsAccumulator.d.ts +23 -0
  21. package/dist/node-esm/src/cache/InMemoryCacheProvider.d.ts +30 -0
  22. package/dist/node-esm/src/cache/ResultTree.d.ts +42 -0
  23. package/dist/node-esm/src/cache/ResultTreeProcessor.d.ts +40 -0
  24. package/dist/node-esm/src/cache/cacheUtils.d.ts +20 -0
  25. package/dist/node-esm/src/core/FirebaseAuthProvider.d.ts +3 -1
  26. package/dist/node-esm/src/core/query/QueryManager.d.ts +47 -0
  27. package/dist/node-esm/src/core/query/queryOptions.d.ts +25 -0
  28. package/dist/node-esm/src/core/query/subscribe.d.ts +67 -0
  29. package/dist/node-esm/src/network/fetch.d.ts +2 -5
  30. package/dist/node-esm/src/network/index.d.ts +1 -1
  31. package/dist/node-esm/src/network/transport/index.d.ts +37 -8
  32. package/dist/node-esm/src/network/transport/rest.d.ts +5 -17
  33. package/dist/node-esm/src/util/encoder.d.ts +4 -1
  34. package/dist/node-esm/src/util/url.d.ts +1 -0
  35. package/dist/private.d.ts +278 -17
  36. package/dist/public.d.ts +77 -3
  37. package/dist/src/api/DataConnect.d.ts +44 -3
  38. package/dist/src/api/Reference.d.ts +2 -0
  39. package/dist/src/api/index.d.ts +2 -1
  40. package/dist/src/api/query.d.ts +9 -26
  41. package/dist/src/api.browser.d.ts +2 -18
  42. package/dist/src/api.node.d.ts +2 -1
  43. package/dist/src/cache/Cache.d.ts +53 -0
  44. package/dist/src/cache/CacheProvider.d.ts +25 -0
  45. package/dist/src/cache/EntityDataObject.d.ts +37 -0
  46. package/dist/src/cache/EntityNode.d.ts +56 -0
  47. package/dist/src/cache/ImpactedQueryRefsAccumulator.d.ts +23 -0
  48. package/dist/src/cache/InMemoryCacheProvider.d.ts +30 -0
  49. package/dist/src/cache/ResultTree.d.ts +42 -0
  50. package/dist/src/cache/ResultTreeProcessor.d.ts +40 -0
  51. package/dist/src/cache/cacheUtils.d.ts +20 -0
  52. package/dist/src/core/FirebaseAuthProvider.d.ts +3 -1
  53. package/dist/src/core/query/QueryManager.d.ts +47 -0
  54. package/dist/src/core/query/queryOptions.d.ts +25 -0
  55. package/dist/src/core/query/subscribe.d.ts +67 -0
  56. package/dist/src/network/fetch.d.ts +2 -5
  57. package/dist/src/network/index.d.ts +1 -1
  58. package/dist/src/network/transport/index.d.ts +37 -8
  59. package/dist/src/network/transport/rest.d.ts +5 -17
  60. package/dist/src/util/encoder.d.ts +4 -1
  61. package/dist/src/util/url.d.ts +1 -0
  62. package/package.json +7 -7
  63. package/dist/node-esm/src/core/QueryManager.d.ts +0 -45
  64. package/dist/src/core/QueryManager.d.ts +0 -45
@@ -168,7 +168,7 @@ function getGoogApiClientValue(_isUsingGen, _callerSdkType) {
168
168
  }
169
169
  return str;
170
170
  }
171
- function dcFetch(url, body, { signal }, appId, accessToken, appCheckToken, _isUsingGen, _callerSdkType, _isUsingEmulator) {
171
+ async function dcFetch(url, body, { signal }, appId, accessToken, appCheckToken, _isUsingGen, _callerSdkType, _isUsingEmulator) {
172
172
  if (!connectFetch) {
173
173
  throw new DataConnectError(Code.OTHER, 'No Fetch Implementation detected!');
174
174
  }
@@ -195,41 +195,44 @@ function dcFetch(url, body, { signal }, appId, accessToken, appCheckToken, _isUs
195
195
  if (util.isCloudWorkstation(url) && _isUsingEmulator) {
196
196
  fetchOptions.credentials = 'include';
197
197
  }
198
- return connectFetch(url, fetchOptions)
199
- .catch(err => {
198
+ let response;
199
+ try {
200
+ response = await connectFetch(url, fetchOptions);
201
+ }
202
+ catch (err) {
200
203
  throw new DataConnectError(Code.OTHER, 'Failed to fetch: ' + JSON.stringify(err));
201
- })
202
- .then(async (response) => {
203
- let jsonResponse = null;
204
- try {
205
- jsonResponse = await response.json();
206
- }
207
- catch (e) {
208
- throw new DataConnectError(Code.OTHER, JSON.stringify(e));
209
- }
210
- const message = getMessage(jsonResponse);
211
- if (response.status >= 400) {
212
- logError('Error while performing request: ' + JSON.stringify(jsonResponse));
213
- if (response.status === 401) {
214
- throw new DataConnectError(Code.UNAUTHORIZED, message);
215
- }
216
- throw new DataConnectError(Code.OTHER, message);
217
- }
218
- return jsonResponse;
219
- })
220
- .then(res => {
221
- if (res.errors && res.errors.length) {
222
- const stringified = JSON.stringify(res.errors);
223
- const response = {
224
- errors: res.errors,
225
- data: res.data
226
- };
227
- throw new DataConnectOperationError('DataConnect error while performing request: ' + stringified, response);
204
+ }
205
+ let jsonResponse;
206
+ try {
207
+ jsonResponse = await response.json();
208
+ }
209
+ catch (e) {
210
+ throw new DataConnectError(Code.OTHER, JSON.stringify(e));
211
+ }
212
+ const message = getErrorMessage(jsonResponse);
213
+ if (response.status >= 400) {
214
+ logError('Error while performing request: ' + JSON.stringify(jsonResponse));
215
+ if (response.status === 401) {
216
+ throw new DataConnectError(Code.UNAUTHORIZED, message);
228
217
  }
229
- return res;
230
- });
218
+ throw new DataConnectError(Code.OTHER, message);
219
+ }
220
+ if (jsonResponse.errors && jsonResponse.errors.length) {
221
+ const stringified = JSON.stringify(jsonResponse.errors);
222
+ const failureResponse = {
223
+ errors: jsonResponse.errors,
224
+ data: jsonResponse.data
225
+ };
226
+ throw new DataConnectOperationError('DataConnect error while performing request: ' + stringified, failureResponse);
227
+ }
228
+ if (!jsonResponse.extensions) {
229
+ jsonResponse.extensions = {
230
+ dataConnect: []
231
+ };
232
+ }
233
+ return jsonResponse;
231
234
  }
232
- function getMessage(obj) {
235
+ function getErrorMessage(obj) {
233
236
  if ('message' in obj && obj.message) {
234
237
  return obj.message;
235
238
  }
@@ -237,7 +240,521 @@ function getMessage(obj) {
237
240
  }
238
241
 
239
242
  const name = "@firebase/data-connect";
240
- const version = "0.3.12";
243
+ const version = "0.4.0-canary.78384d32c";
244
+
245
+ /**
246
+ * @license
247
+ * Copyright 2025 Google LLC
248
+ *
249
+ * Licensed under the Apache License, Version 2.0 (the "License");
250
+ * you may not use this file except in compliance with the License.
251
+ * You may obtain a copy of the License at
252
+ *
253
+ * http://www.apache.org/licenses/LICENSE-2.0
254
+ *
255
+ * Unless required by applicable law or agreed to in writing, software
256
+ * distributed under the License is distributed on an "AS IS" BASIS,
257
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
258
+ * See the License for the specific language governing permissions and
259
+ * limitations under the License.
260
+ */
261
+ class EntityDataObject {
262
+ getServerValue(key) {
263
+ return this.serverValues[key];
264
+ }
265
+ constructor(globalID) {
266
+ this.globalID = globalID;
267
+ this.serverValues = {};
268
+ this.referencedFrom = new Set();
269
+ }
270
+ getServerValues() {
271
+ return this.serverValues;
272
+ }
273
+ toJSON() {
274
+ return {
275
+ globalID: this.globalID,
276
+ map: this.serverValues,
277
+ referencedFrom: Array.from(this.referencedFrom)
278
+ };
279
+ }
280
+ static fromJSON(json) {
281
+ const edo = new EntityDataObject(json.globalID);
282
+ edo.serverValues = json.map;
283
+ edo.referencedFrom = new Set(json.referencedFrom);
284
+ return edo;
285
+ }
286
+ updateServerValue(key, value, requestedFrom) {
287
+ this.serverValues[key] = value;
288
+ this.referencedFrom.add(requestedFrom);
289
+ return Array.from(this.referencedFrom);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * @license
295
+ * Copyright 2025 Google LLC
296
+ *
297
+ * Licensed under the Apache License, Version 2.0 (the "License");
298
+ * you may not use this file except in compliance with the License.
299
+ * You may obtain a copy of the License at
300
+ *
301
+ * http://www.apache.org/licenses/LICENSE-2.0
302
+ *
303
+ * Unless required by applicable law or agreed to in writing, software
304
+ * distributed under the License is distributed on an "AS IS" BASIS,
305
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
306
+ * See the License for the specific language governing permissions and
307
+ * limitations under the License.
308
+ */
309
+ class InMemoryCacheProvider {
310
+ constructor(_keyId) {
311
+ this._keyId = _keyId;
312
+ this.edos = new Map();
313
+ this.resultTrees = new Map();
314
+ }
315
+ async setResultTree(queryId, rt) {
316
+ this.resultTrees.set(queryId, rt);
317
+ }
318
+ async getResultTree(queryId) {
319
+ return this.resultTrees.get(queryId);
320
+ }
321
+ async updateEntityData(entityData) {
322
+ this.edos.set(entityData.globalID, entityData);
323
+ }
324
+ async getEntityData(globalId) {
325
+ if (!this.edos.has(globalId)) {
326
+ this.edos.set(globalId, new EntityDataObject(globalId));
327
+ }
328
+ // Because of the above, we can guarantee that there will be an EDO at the globalId.
329
+ return this.edos.get(globalId);
330
+ }
331
+ close() {
332
+ // No-op
333
+ return Promise.resolve();
334
+ }
335
+ }
336
+
337
+ /**
338
+ * @license
339
+ * Copyright 2025 Google LLC
340
+ *
341
+ * Licensed under the Apache License, Version 2.0 (the "License");
342
+ * you may not use this file except in compliance with the License.
343
+ * You may obtain a copy of the License at
344
+ *
345
+ * http://www.apache.org/licenses/LICENSE-2.0
346
+ *
347
+ * Unless required by applicable law or agreed to in writing, software
348
+ * distributed under the License is distributed on an "AS IS" BASIS,
349
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
350
+ * See the License for the specific language governing permissions and
351
+ * limitations under the License.
352
+ */
353
+ const GLOBAL_ID_KEY = '_id';
354
+ const OBJECT_LISTS_KEY = '_objectLists';
355
+ const REFERENCES_KEY = '_references';
356
+ const SCALARS_KEY = '_scalars';
357
+ const ENTITY_DATA_KEYS_KEY = '_entity_data_keys';
358
+ class EntityNode {
359
+ constructor() {
360
+ this.scalars = {};
361
+ this.references = {};
362
+ this.objectLists = {};
363
+ this.entityDataKeys = new Set();
364
+ }
365
+ async loadData(queryId, values, entityIds, acc, cacheProvider) {
366
+ if (values === undefined) {
367
+ return;
368
+ }
369
+ if (typeof values !== 'object' || Array.isArray(values)) {
370
+ throw new DataConnectError(Code.INVALID_ARGUMENT, 'EntityNode initialized with non-object value');
371
+ }
372
+ if (values === null) {
373
+ return;
374
+ }
375
+ if (typeof values === 'object' &&
376
+ entityIds &&
377
+ entityIds[GLOBAL_ID_KEY] &&
378
+ typeof entityIds[GLOBAL_ID_KEY] === 'string') {
379
+ this.globalId = entityIds[GLOBAL_ID_KEY];
380
+ this.entityData = await cacheProvider.getEntityData(this.globalId);
381
+ }
382
+ for (const key in values) {
383
+ if (values.hasOwnProperty(key)) {
384
+ if (typeof values[key] === 'object') {
385
+ if (Array.isArray(values[key])) {
386
+ const ids = entityIds && entityIds[key];
387
+ const objArray = [];
388
+ const scalarArray = [];
389
+ for (const [index, value] of values[key].entries()) {
390
+ if (typeof value === 'object') {
391
+ if (Array.isArray(value)) ;
392
+ else {
393
+ const entityNode = new EntityNode();
394
+ await entityNode.loadData(queryId, value, ids && ids[index], acc, cacheProvider);
395
+ objArray.push(entityNode);
396
+ }
397
+ }
398
+ else {
399
+ scalarArray.push(value);
400
+ }
401
+ }
402
+ if (scalarArray.length > 0 && objArray.length > 0) {
403
+ this.scalars[key] = values[key];
404
+ }
405
+ else if (scalarArray.length > 0) {
406
+ if (this.entityData) {
407
+ const impactedRefs = this.entityData.updateServerValue(key, scalarArray, queryId);
408
+ this.entityDataKeys.add(key);
409
+ acc.add(impactedRefs);
410
+ }
411
+ else {
412
+ this.scalars[key] = scalarArray;
413
+ }
414
+ }
415
+ else if (objArray.length > 0) {
416
+ this.objectLists[key] = objArray;
417
+ }
418
+ else {
419
+ this.scalars[key] = [];
420
+ }
421
+ }
422
+ else {
423
+ if (values[key] === null) {
424
+ this.scalars[key] = null;
425
+ continue;
426
+ }
427
+ const entityNode = new EntityNode();
428
+ // TODO: Load Data might need to be pushed into ResultTreeProcessor instead.
429
+ await entityNode.loadData(queryId, values[key], entityIds && entityIds[key], acc, cacheProvider);
430
+ this.references[key] = entityNode;
431
+ }
432
+ }
433
+ else {
434
+ if (this.entityData) {
435
+ const impactedRefs = this.entityData.updateServerValue(key, values[key], queryId);
436
+ this.entityDataKeys.add(key);
437
+ acc.add(impactedRefs);
438
+ }
439
+ else {
440
+ this.scalars[key] = values[key];
441
+ }
442
+ }
443
+ }
444
+ }
445
+ if (this.entityData) {
446
+ await cacheProvider.updateEntityData(this.entityData);
447
+ }
448
+ }
449
+ toJSON(mode) {
450
+ const resultObject = {};
451
+ if (mode === EncodingMode.hydrated) {
452
+ if (this.entityData) {
453
+ for (const key of this.entityDataKeys) {
454
+ resultObject[key] = this.entityData.getServerValue(key);
455
+ }
456
+ }
457
+ if (this.scalars) {
458
+ Object.assign(resultObject, this.scalars);
459
+ }
460
+ if (this.references) {
461
+ for (const key in this.references) {
462
+ if (this.references.hasOwnProperty(key)) {
463
+ resultObject[key] = this.references[key].toJSON(mode);
464
+ }
465
+ }
466
+ }
467
+ if (this.objectLists) {
468
+ for (const key in this.objectLists) {
469
+ if (this.objectLists.hasOwnProperty(key)) {
470
+ resultObject[key] = this.objectLists[key].map(obj => obj.toJSON(mode));
471
+ }
472
+ }
473
+ }
474
+ return resultObject;
475
+ }
476
+ else {
477
+ // Get JSON representation of dehydrated list
478
+ if (this.entityData) {
479
+ resultObject[GLOBAL_ID_KEY] = this.entityData.globalID;
480
+ }
481
+ resultObject[ENTITY_DATA_KEYS_KEY] = Array.from(this.entityDataKeys);
482
+ if (this.scalars) {
483
+ resultObject[SCALARS_KEY] = this.scalars;
484
+ }
485
+ if (this.references) {
486
+ const references = {};
487
+ for (const key in this.references) {
488
+ if (this.references.hasOwnProperty(key)) {
489
+ references[key] = this.references[key].toJSON(mode);
490
+ }
491
+ }
492
+ resultObject[REFERENCES_KEY] = references;
493
+ }
494
+ if (this.objectLists) {
495
+ const objectLists = {};
496
+ for (const key in this.objectLists) {
497
+ if (this.objectLists.hasOwnProperty(key)) {
498
+ objectLists[key] = this.objectLists[key].map(obj => obj.toJSON(mode));
499
+ }
500
+ }
501
+ resultObject[OBJECT_LISTS_KEY] = objectLists;
502
+ }
503
+ }
504
+ return resultObject;
505
+ }
506
+ static fromJson(obj) {
507
+ const sdo = new EntityNode();
508
+ if (obj.backingData) {
509
+ sdo.entityData = EntityDataObject.fromJSON(obj.backingData);
510
+ }
511
+ sdo.globalId = obj.globalID;
512
+ sdo.scalars = obj.scalars;
513
+ if (obj.references) {
514
+ const references = {};
515
+ for (const key in obj.references) {
516
+ if (obj.references.hasOwnProperty(key)) {
517
+ references[key] = EntityNode.fromJson(obj.references[key]);
518
+ }
519
+ }
520
+ sdo.references = references;
521
+ }
522
+ if (obj.objectLists) {
523
+ const objectLists = {};
524
+ for (const key in obj.objectLists) {
525
+ if (obj.objectLists.hasOwnProperty(key)) {
526
+ objectLists[key] = obj.objectLists[key].map(obj => EntityNode.fromJson(obj));
527
+ }
528
+ }
529
+ sdo.objectLists = objectLists;
530
+ }
531
+ return sdo;
532
+ }
533
+ }
534
+ // Helpful for storing in persistent cache, which is not available yet.
535
+ var EncodingMode;
536
+ (function (EncodingMode) {
537
+ EncodingMode[EncodingMode["hydrated"] = 0] = "hydrated";
538
+ EncodingMode[EncodingMode["dehydrated"] = 1] = "dehydrated";
539
+ })(EncodingMode || (EncodingMode = {}));
540
+
541
+ /**
542
+ * @license
543
+ * Copyright 2025 Google LLC
544
+ *
545
+ * Licensed under the Apache License, Version 2.0 (the "License");
546
+ * you may not use this file except in compliance with the License.
547
+ * You may obtain a copy of the License at
548
+ *
549
+ * http://www.apache.org/licenses/LICENSE-2.0
550
+ *
551
+ * Unless required by applicable law or agreed to in writing, software
552
+ * distributed under the License is distributed on an "AS IS" BASIS,
553
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
554
+ * See the License for the specific language governing permissions and
555
+ * limitations under the License.
556
+ */
557
+ class ResultTree {
558
+ /**
559
+ * Create a {@link ResultTree} from a dehydrated JSON object.
560
+ * @param value The dehydrated JSON object.
561
+ * @returns The {@link ResultTree}.
562
+ */
563
+ static fromJson(value) {
564
+ return new ResultTree(EntityNode.fromJson(value.rootStub), value.maxAge, value.cachedAt, value.lastAccessed);
565
+ }
566
+ constructor(rootStub, maxAge = 0, cachedAt, _lastAccessed) {
567
+ this.rootStub = rootStub;
568
+ this.maxAge = maxAge;
569
+ this.cachedAt = cachedAt;
570
+ this._lastAccessed = _lastAccessed;
571
+ }
572
+ isStale() {
573
+ return (Date.now() - new Date(this.cachedAt.getTime()).getTime() >
574
+ this.maxAge * 1000);
575
+ }
576
+ updateMaxAge(maxAgeInSeconds) {
577
+ this.maxAge = maxAgeInSeconds;
578
+ }
579
+ updateAccessed() {
580
+ this._lastAccessed = new Date();
581
+ }
582
+ get lastAccessed() {
583
+ return this._lastAccessed;
584
+ }
585
+ getRootStub() {
586
+ return this.rootStub;
587
+ }
588
+ }
589
+
590
+ /**
591
+ * @license
592
+ * Copyright 2025 Google LLC
593
+ *
594
+ * Licensed under the Apache License, Version 2.0 (the "License");
595
+ * you may not use this file except in compliance with the License.
596
+ * You may obtain a copy of the License at
597
+ *
598
+ * http://www.apache.org/licenses/LICENSE-2.0
599
+ *
600
+ * Unless required by applicable law or agreed to in writing, software
601
+ * distributed under the License is distributed on an "AS IS" BASIS,
602
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
603
+ * See the License for the specific language governing permissions and
604
+ * limitations under the License.
605
+ */
606
+ class ImpactedQueryRefsAccumulator {
607
+ constructor(queryId) {
608
+ this.queryId = queryId;
609
+ this.impacted = new Set();
610
+ }
611
+ add(impacted) {
612
+ impacted
613
+ .filter(ref => ref !== this.queryId)
614
+ .forEach(ref => this.impacted.add(ref));
615
+ }
616
+ consumeEvents() {
617
+ const events = Array.from(this.impacted);
618
+ this.impacted.clear();
619
+ return events;
620
+ }
621
+ }
622
+
623
+ /**
624
+ * @license
625
+ * Copyright 2025 Google LLC
626
+ *
627
+ * Licensed under the Apache License, Version 2.0 (the "License");
628
+ * you may not use this file except in compliance with the License.
629
+ * You may obtain a copy of the License at
630
+ *
631
+ * http://www.apache.org/licenses/LICENSE-2.0
632
+ *
633
+ * Unless required by applicable law or agreed to in writing, software
634
+ * distributed under the License is distributed on an "AS IS" BASIS,
635
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
636
+ * See the License for the specific language governing permissions and
637
+ * limitations under the License.
638
+ */
639
+ class ResultTreeProcessor {
640
+ /**
641
+ * Hydrate the EntityNode into a JSON object so that it can be returned to the user.
642
+ * @param rootStubObject
643
+ * @returns {string}
644
+ */
645
+ hydrateResults(rootStubObject) {
646
+ return rootStubObject.toJSON(EncodingMode.hydrated);
647
+ }
648
+ /**
649
+ * Dehydrate results so that they can be stored in the cache.
650
+ * @param json
651
+ * @param entityIds
652
+ * @param cacheProvider
653
+ * @param queryId
654
+ * @returns {Promise<DehydratedResults>}
655
+ */
656
+ async dehydrateResults(json, entityIds, cacheProvider, queryId) {
657
+ const acc = new ImpactedQueryRefsAccumulator(queryId);
658
+ const entityNode = new EntityNode();
659
+ await entityNode.loadData(queryId, json, entityIds, acc, cacheProvider);
660
+ return {
661
+ entityNode,
662
+ impacted: acc.consumeEvents()
663
+ };
664
+ }
665
+ }
666
+
667
+ /**
668
+ * @license
669
+ * Copyright 2025 Google LLC
670
+ *
671
+ * Licensed under the Apache License, Version 2.0 (the "License");
672
+ * you may not use this file except in compliance with the License.
673
+ * You may obtain a copy of the License at
674
+ *
675
+ * http://www.apache.org/licenses/LICENSE-2.0
676
+ *
677
+ * Unless required by applicable law or agreed to in writing, software
678
+ * distributed under the License is distributed on an "AS IS" BASIS,
679
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
680
+ * See the License for the specific language governing permissions and
681
+ * limitations under the License.
682
+ */
683
+ class DataConnectCache {
684
+ constructor(authProvider, projectId, connectorConfig, host, cacheSettings) {
685
+ this.authProvider = authProvider;
686
+ this.projectId = projectId;
687
+ this.connectorConfig = connectorConfig;
688
+ this.host = host;
689
+ this.cacheSettings = cacheSettings;
690
+ this.cacheProvider = null;
691
+ this.uid = null;
692
+ this.authProvider.addTokenChangeListener(async (_) => {
693
+ const newUid = this.authProvider.getAuth().getUid();
694
+ // We should only close if the token changes and so does the new UID
695
+ if (this.uid !== newUid) {
696
+ this.cacheProvider?.close();
697
+ this.uid = newUid;
698
+ const identifier = await this.getIdentifier(this.uid);
699
+ this.cacheProvider = this.initializeNewProviders(identifier);
700
+ }
701
+ });
702
+ }
703
+ async initialize() {
704
+ if (!this.cacheProvider) {
705
+ const identifier = await this.getIdentifier(this.uid);
706
+ this.cacheProvider = this.initializeNewProviders(identifier);
707
+ }
708
+ }
709
+ async getIdentifier(uid) {
710
+ const identifier = `${'memory' // TODO: replace this with indexeddb when persistence is available.
711
+ }-${this.projectId}-${this.connectorConfig.service}-${this.connectorConfig.connector}-${this.connectorConfig.location}-${uid}-${this.host}`;
712
+ const sha256 = await util.generateSHA256Hash(identifier);
713
+ return sha256;
714
+ }
715
+ initializeNewProviders(identifier) {
716
+ return this.cacheSettings.cacheProvider.initialize(identifier);
717
+ }
718
+ async containsResultTree(queryId) {
719
+ await this.initialize();
720
+ const resultTree = await this.cacheProvider.getResultTree(queryId);
721
+ return resultTree !== undefined;
722
+ }
723
+ async getResultTree(queryId) {
724
+ await this.initialize();
725
+ return this.cacheProvider.getResultTree(queryId);
726
+ }
727
+ async getResultJSON(queryId) {
728
+ await this.initialize();
729
+ const processor = new ResultTreeProcessor();
730
+ const cacheProvider = this.cacheProvider;
731
+ const resultTree = await cacheProvider.getResultTree(queryId);
732
+ if (!resultTree) {
733
+ throw new DataConnectError(Code.INVALID_ARGUMENT, `${queryId} not found in cache. Call "update()" first.`);
734
+ }
735
+ return processor.hydrateResults(resultTree.getRootStub());
736
+ }
737
+ async update(queryId, serverValues, entityIds) {
738
+ await this.initialize();
739
+ const processor = new ResultTreeProcessor();
740
+ const cacheProvider = this.cacheProvider;
741
+ const { entityNode: stubDataObject, impacted } = await processor.dehydrateResults(serverValues, entityIds, cacheProvider, queryId);
742
+ const now = new Date();
743
+ await cacheProvider.setResultTree(queryId, new ResultTree(stubDataObject, serverValues.maxAge || this.cacheSettings.maxAgeSeconds, now, now));
744
+ return impacted;
745
+ }
746
+ }
747
+ class MemoryStub {
748
+ constructor() {
749
+ this.type = 'MEMORY';
750
+ }
751
+ /**
752
+ * @internal
753
+ */
754
+ initialize(cacheId) {
755
+ return new InMemoryCacheProvider(cacheId);
756
+ }
757
+ }
241
758
 
242
759
  /**
243
760
  * @license
@@ -329,6 +846,9 @@ class FirebaseAuthProvider {
329
846
  _authProvider.onInit(auth => (this._auth = auth));
330
847
  }
331
848
  }
849
+ getAuth() {
850
+ return this._auth;
851
+ }
332
852
  getToken(forceRefresh) {
333
853
  if (!this._auth) {
334
854
  return new Promise((resolve, reject) => {
@@ -388,7 +908,7 @@ const SOURCE_CACHE = 'CACHE';
388
908
 
389
909
  /**
390
910
  * @license
391
- * Copyright 2024 Google LLC
911
+ * Copyright 2026 Google LLC
392
912
  *
393
913
  * Licensed under the Apache License, Version 2.0 (the "License");
394
914
  * you may not use this file except in compliance with the License.
@@ -402,11 +922,43 @@ const SOURCE_CACHE = 'CACHE';
402
922
  * See the License for the specific language governing permissions and
403
923
  * limitations under the License.
404
924
  */
405
- let encoderImpl;
406
- function setEncoder(encoder) {
407
- encoderImpl = encoder;
925
+ function parseEntityIds(result) {
926
+ // Iterate through extensions.dataConnect
927
+ const dataConnectExtensions = result.extensions?.dataConnect;
928
+ const dataCopy = Object.assign(result);
929
+ if (!dataConnectExtensions) {
930
+ return dataCopy;
931
+ }
932
+ const ret = {};
933
+ for (const extension of dataConnectExtensions) {
934
+ const { path } = extension;
935
+ populatePath(path, ret, extension);
936
+ }
937
+ return ret;
938
+ }
939
+ // mutates the object to update the path
940
+ function populatePath(path, toUpdate, extension) {
941
+ let curObj = toUpdate;
942
+ for (const slice of path) {
943
+ if (typeof curObj[slice] !== 'object') {
944
+ curObj[slice] = {};
945
+ }
946
+ curObj = curObj[slice];
947
+ }
948
+ if ('entityId' in extension && extension.entityId) {
949
+ curObj['_id'] = extension.entityId;
950
+ }
951
+ else if ('entityIds' in extension) {
952
+ const entityArr = extension.entityIds;
953
+ for (let i = 0; i < entityArr.length; i++) {
954
+ const entityId = entityArr[i];
955
+ if (typeof curObj[i] === 'undefined') {
956
+ curObj[i] = {};
957
+ }
958
+ curObj[i]._id = entityId;
959
+ }
960
+ }
408
961
  }
409
- setEncoder(o => JSON.stringify(o));
410
962
 
411
963
  /**
412
964
  * @license
@@ -424,11 +976,24 @@ setEncoder(o => JSON.stringify(o));
424
976
  * See the License for the specific language governing permissions and
425
977
  * limitations under the License.
426
978
  */
427
- function setIfNotExists(map, key, val) {
428
- if (!map.has(key)) {
429
- map.set(key, val);
430
- }
979
+ let encoderImpl;
980
+ let decoderImpl;
981
+ function setEncoder(encoder) {
982
+ encoderImpl = encoder;
983
+ }
984
+ function setDecoder(decoder) {
985
+ decoderImpl = decoder;
986
+ }
987
+ function sortKeysForObj(o) {
988
+ return Object.keys(o)
989
+ .sort()
990
+ .reduce((accumulator, currentKey) => {
991
+ accumulator[currentKey] = o[currentKey];
992
+ return accumulator;
993
+ }, {});
431
994
  }
995
+ setEncoder((o) => JSON.stringify(sortKeysForObj(o)));
996
+ setDecoder(s => sortKeysForObj(JSON.parse(s)));
432
997
 
433
998
  /**
434
999
  * @license
@@ -446,7 +1011,7 @@ function setIfNotExists(map, key, val) {
446
1011
  * See the License for the specific language governing permissions and
447
1012
  * limitations under the License.
448
1013
  */
449
- function getRefSerializer(queryRef, data, source) {
1014
+ function getRefSerializer(queryRef, data, source, fetchTime) {
450
1015
  return function toJSON() {
451
1016
  return {
452
1017
  data,
@@ -458,32 +1023,65 @@ function getRefSerializer(queryRef, data, source) {
458
1023
  ...queryRef.dataConnect.getSettings()
459
1024
  }
460
1025
  },
461
- fetchTime: Date.now().toLocaleString(),
1026
+ fetchTime,
462
1027
  source
463
1028
  };
464
1029
  };
465
1030
  }
466
1031
  class QueryManager {
467
- constructor(transport) {
1032
+ async preferCacheResults(queryRef, allowStale = false) {
1033
+ let cacheResult;
1034
+ try {
1035
+ cacheResult = await this.fetchCacheResults(queryRef, allowStale);
1036
+ }
1037
+ catch (e) {
1038
+ // Ignore the error and try to fetch from the server.
1039
+ }
1040
+ if (cacheResult) {
1041
+ return cacheResult;
1042
+ }
1043
+ return this.fetchServerResults(queryRef);
1044
+ }
1045
+ constructor(transport, dc, cache) {
468
1046
  this.transport = transport;
469
- this._queries = new Map();
1047
+ this.dc = dc;
1048
+ this.cache = cache;
1049
+ this.callbacks = new Map();
1050
+ this.subscriptionCache = new Map();
1051
+ this.queue = [];
470
1052
  }
471
- track(queryName, variables, initialCache) {
472
- const ref = {
473
- name: queryName,
474
- variables,
475
- refType: QUERY_STR
476
- };
477
- const key = encoderImpl(ref);
478
- const newTrackedQuery = {
479
- ref,
480
- subscriptions: [],
481
- currentCache: initialCache || null,
482
- lastError: null
483
- };
484
- // @ts-ignore
485
- setIfNotExists(this._queries, key, newTrackedQuery);
486
- return this._queries.get(key);
1053
+ async waitForQueuedWrites() {
1054
+ for (const promise of this.queue) {
1055
+ await promise;
1056
+ }
1057
+ this.queue = [];
1058
+ }
1059
+ updateSSR(updatedData) {
1060
+ this.queue.push(this.updateCache(updatedData).then(async (result) => this.publishCacheResultsToSubscribers(result, updatedData.fetchTime)));
1061
+ }
1062
+ async updateCache(result, extensions) {
1063
+ await this.waitForQueuedWrites();
1064
+ if (this.cache) {
1065
+ const entityIds = parseEntityIds(result);
1066
+ const updatedMaxAge = getMaxAgeFromExtensions(extensions);
1067
+ if (updatedMaxAge !== undefined) {
1068
+ this.cache.cacheSettings.maxAgeSeconds = updatedMaxAge;
1069
+ }
1070
+ return this.cache.update(encoderImpl({
1071
+ name: result.ref.name,
1072
+ variables: result.ref.variables,
1073
+ refType: QUERY_STR
1074
+ }), result.data, entityIds);
1075
+ }
1076
+ else {
1077
+ const key = encoderImpl({
1078
+ name: result.ref.name,
1079
+ variables: result.ref.variables,
1080
+ refType: QUERY_STR
1081
+ });
1082
+ this.subscriptionCache.set(key, result);
1083
+ return [key];
1084
+ }
487
1085
  }
488
1086
  addSubscription(queryRef, onResultCallback, onCompleteCallback, onErrorCallback, initialCache) {
489
1087
  const key = encoderImpl({
@@ -491,99 +1089,213 @@ class QueryManager {
491
1089
  variables: queryRef.variables,
492
1090
  refType: QUERY_STR
493
1091
  });
494
- const trackedQuery = this._queries.get(key);
495
- const subscription = {
496
- userCallback: onResultCallback,
497
- onCompleteCallback,
498
- errCallback: onErrorCallback
499
- };
500
1092
  const unsubscribe = () => {
501
- const trackedQuery = this._queries.get(key);
502
- trackedQuery.subscriptions = trackedQuery.subscriptions.filter(sub => sub !== subscription);
503
- onCompleteCallback?.();
504
- };
505
- if (initialCache && trackedQuery.currentCache !== initialCache) {
506
- logDebug('Initial cache found. Comparing dates.');
507
- if (!trackedQuery.currentCache ||
508
- (trackedQuery.currentCache &&
509
- compareDates(trackedQuery.currentCache.fetchTime, initialCache.fetchTime))) {
510
- trackedQuery.currentCache = initialCache;
511
- }
512
- }
513
- if (trackedQuery.currentCache !== null) {
514
- const cachedData = trackedQuery.currentCache.data;
515
- onResultCallback({
516
- data: cachedData,
517
- source: SOURCE_CACHE,
518
- ref: queryRef,
519
- toJSON: getRefSerializer(queryRef, trackedQuery.currentCache.data, SOURCE_CACHE),
520
- fetchTime: trackedQuery.currentCache.fetchTime
521
- });
522
- if (trackedQuery.lastError !== null && onErrorCallback) {
523
- onErrorCallback(undefined);
1093
+ if (this.callbacks.has(key)) {
1094
+ const callbackList = this.callbacks.get(key);
1095
+ this.callbacks.set(key, callbackList.filter(callback => callback !== subscription));
1096
+ onCompleteCallback?.();
524
1097
  }
525
- }
526
- trackedQuery.subscriptions.push({
1098
+ };
1099
+ const subscription = {
527
1100
  userCallback: onResultCallback,
528
1101
  errCallback: onErrorCallback,
529
1102
  unsubscribe
530
- });
531
- if (!trackedQuery.currentCache) {
532
- logDebug(`No cache available for query ${queryRef.name} with variables ${JSON.stringify(queryRef.variables)}. Calling executeQuery.`);
533
- const promise = this.executeQuery(queryRef);
534
- // We want to ignore the error and let subscriptions handle it
535
- promise.then(undefined, err => { });
1103
+ };
1104
+ if (initialCache) {
1105
+ this.updateSSR(initialCache);
1106
+ }
1107
+ const promise = this.preferCacheResults(queryRef, /*allowStale=*/ true);
1108
+ // We want to ignore the error and let subscriptions handle it
1109
+ promise.then(undefined, err => { });
1110
+ if (!this.callbacks.has(key)) {
1111
+ this.callbacks.set(key, []);
536
1112
  }
1113
+ this.callbacks
1114
+ .get(key)
1115
+ .push(subscription);
537
1116
  return unsubscribe;
538
1117
  }
539
- executeQuery(queryRef) {
540
- if (queryRef.refType !== QUERY_STR) {
541
- throw new DataConnectError(Code.INVALID_ARGUMENT, `ExecuteQuery can only execute query operation`);
542
- }
1118
+ async fetchServerResults(queryRef) {
1119
+ await this.waitForQueuedWrites();
543
1120
  const key = encoderImpl({
544
1121
  name: queryRef.name,
545
1122
  variables: queryRef.variables,
546
1123
  refType: QUERY_STR
547
1124
  });
548
- const trackedQuery = this._queries.get(key);
549
- const result = this.transport.invokeQuery(queryRef.name, queryRef.variables);
550
- const newR = result.then(res => {
551
- const fetchTime = new Date().toString();
552
- const result = {
553
- ...res,
554
- source: SOURCE_SERVER,
1125
+ try {
1126
+ const result = await this.transport.invokeQuery(queryRef.name, queryRef.variables);
1127
+ const fetchTime = Date.now().toString();
1128
+ const originalExtensions = result.extensions;
1129
+ const queryResult = {
1130
+ ...result,
555
1131
  ref: queryRef,
556
- toJSON: getRefSerializer(queryRef, res.data, SOURCE_SERVER),
557
- fetchTime
1132
+ source: SOURCE_SERVER,
1133
+ fetchTime,
1134
+ data: result.data,
1135
+ extensions: getDataConnectExtensionsWithoutMaxAge(originalExtensions),
1136
+ toJSON: getRefSerializer(queryRef, result.data, SOURCE_SERVER, fetchTime)
558
1137
  };
559
- trackedQuery.subscriptions.forEach(subscription => {
560
- subscription.userCallback(result);
1138
+ let updatedKeys = [];
1139
+ updatedKeys = await this.updateCache(queryResult, originalExtensions?.dataConnect);
1140
+ this.publishDataToSubscribers(key, queryResult);
1141
+ if (this.cache) {
1142
+ await this.publishCacheResultsToSubscribers(updatedKeys, fetchTime);
1143
+ }
1144
+ else {
1145
+ this.subscriptionCache.set(key, queryResult);
1146
+ }
1147
+ return queryResult;
1148
+ }
1149
+ catch (e) {
1150
+ this.publishErrorToSubscribers(key, e);
1151
+ throw e;
1152
+ }
1153
+ }
1154
+ async fetchCacheResults(queryRef, allowStale = false) {
1155
+ await this.waitForQueuedWrites();
1156
+ let result;
1157
+ if (!this.cache) {
1158
+ result = await this.getFromSubscriberCache(queryRef);
1159
+ }
1160
+ else {
1161
+ result = await this.getFromResultTreeCache(queryRef, allowStale);
1162
+ }
1163
+ if (!result) {
1164
+ throw new DataConnectError(Code.OTHER, 'No cache entry found for query: ' + queryRef.name);
1165
+ }
1166
+ const fetchTime = Date.now().toString();
1167
+ const queryResult = {
1168
+ ...result,
1169
+ ref: queryRef,
1170
+ source: SOURCE_CACHE,
1171
+ fetchTime,
1172
+ data: result.data,
1173
+ extensions: result.extensions,
1174
+ toJSON: getRefSerializer(queryRef, result.data, SOURCE_CACHE, fetchTime)
1175
+ };
1176
+ if (this.cache) {
1177
+ const key = encoderImpl({
1178
+ name: queryRef.name,
1179
+ variables: queryRef.variables,
1180
+ refType: QUERY_STR
561
1181
  });
562
- trackedQuery.currentCache = {
563
- data: res.data,
564
- source: SOURCE_CACHE,
565
- fetchTime
566
- };
567
- return result;
568
- }, err => {
569
- trackedQuery.lastError = err;
570
- trackedQuery.subscriptions.forEach(subscription => {
571
- if (subscription.errCallback) {
572
- subscription.errCallback(err);
573
- }
1182
+ await this.publishCacheResultsToSubscribers([key], fetchTime);
1183
+ }
1184
+ else {
1185
+ const key = encoderImpl({
1186
+ name: queryRef.name,
1187
+ variables: queryRef.variables,
1188
+ refType: QUERY_STR
574
1189
  });
575
- throw err;
1190
+ this.subscriptionCache.set(key, queryResult);
1191
+ this.publishDataToSubscribers(key, queryResult);
1192
+ }
1193
+ return queryResult;
1194
+ }
1195
+ publishErrorToSubscribers(key, err) {
1196
+ this.callbacks.get(key)?.forEach(subscription => {
1197
+ if (subscription.errCallback) {
1198
+ subscription.errCallback(err);
1199
+ }
1200
+ });
1201
+ }
1202
+ async getFromResultTreeCache(queryRef, allowStale = false) {
1203
+ const key = encoderImpl({
1204
+ name: queryRef.name,
1205
+ variables: queryRef.variables,
1206
+ refType: QUERY_STR
1207
+ });
1208
+ if (!this.cache || !(await this.cache.containsResultTree(key))) {
1209
+ return null;
1210
+ }
1211
+ const cacheResult = (await this.cache.getResultJSON(key));
1212
+ const resultTree = await this.cache.getResultTree(key);
1213
+ if (!allowStale && resultTree.isStale()) {
1214
+ return null;
1215
+ }
1216
+ const result = {
1217
+ source: SOURCE_CACHE,
1218
+ ref: queryRef,
1219
+ data: cacheResult,
1220
+ toJSON: getRefSerializer(queryRef, cacheResult, SOURCE_CACHE, resultTree.cachedAt.toString()),
1221
+ fetchTime: resultTree.cachedAt.toString()
1222
+ };
1223
+ (await this.cache.getResultTree(key)).updateAccessed();
1224
+ return result;
1225
+ }
1226
+ async getFromSubscriberCache(queryRef) {
1227
+ const key = encoderImpl({
1228
+ name: queryRef.name,
1229
+ variables: queryRef.variables,
1230
+ refType: QUERY_STR
1231
+ });
1232
+ if (!this.subscriptionCache.has(key)) {
1233
+ return;
1234
+ }
1235
+ const result = this.subscriptionCache.get(key);
1236
+ result.source = SOURCE_CACHE;
1237
+ result.toJSON = getRefSerializer(result.ref, result.data, SOURCE_CACHE, result.fetchTime);
1238
+ return result;
1239
+ }
1240
+ publishDataToSubscribers(key, queryResult) {
1241
+ if (!this.callbacks.has(key)) {
1242
+ return;
1243
+ }
1244
+ const subscribers = this.callbacks.get(key);
1245
+ subscribers.forEach(callback => {
1246
+ callback.userCallback(queryResult);
576
1247
  });
577
- return newR;
1248
+ }
1249
+ async publishCacheResultsToSubscribers(impactedQueries, fetchTime) {
1250
+ if (!this.cache) {
1251
+ return;
1252
+ }
1253
+ for (const query of impactedQueries) {
1254
+ const callbacks = this.callbacks.get(query);
1255
+ if (!callbacks) {
1256
+ continue;
1257
+ }
1258
+ const newJson = (await this.cache.getResultTree(query))
1259
+ .getRootStub()
1260
+ .toJSON(EncodingMode.hydrated);
1261
+ const { name, variables } = decoderImpl(query);
1262
+ const queryRef = {
1263
+ dataConnect: this.dc,
1264
+ refType: QUERY_STR,
1265
+ name,
1266
+ variables
1267
+ };
1268
+ this.publishDataToSubscribers(query, {
1269
+ data: newJson,
1270
+ fetchTime,
1271
+ ref: queryRef,
1272
+ source: SOURCE_CACHE,
1273
+ toJSON: getRefSerializer(queryRef, newJson, SOURCE_CACHE, fetchTime)
1274
+ });
1275
+ }
578
1276
  }
579
1277
  enableEmulator(host, port) {
580
1278
  this.transport.useEmulator(host, port);
581
1279
  }
582
1280
  }
583
- function compareDates(str1, str2) {
584
- const date1 = new Date(str1);
585
- const date2 = new Date(str2);
586
- return date1.getTime() < date2.getTime();
1281
+ function getMaxAgeFromExtensions(extensions) {
1282
+ if (!extensions) {
1283
+ return;
1284
+ }
1285
+ for (const extension of extensions) {
1286
+ if ('maxAge' in extension &&
1287
+ extension.maxAge !== undefined &&
1288
+ extension.maxAge !== null) {
1289
+ if (extension.maxAge.endsWith('s')) {
1290
+ return Number(extension.maxAge.substring(0, extension.maxAge.length - 1));
1291
+ }
1292
+ }
1293
+ }
1294
+ }
1295
+ function getDataConnectExtensionsWithoutMaxAge(extensions) {
1296
+ return {
1297
+ dataConnect: extensions.dataConnect?.filter(extension => 'entityId' in extension || 'entityIds' in extension)
1298
+ };
587
1299
  }
588
1300
 
589
1301
  /**
@@ -602,11 +1314,12 @@ function compareDates(str1, str2) {
602
1314
  * See the License for the specific language governing permissions and
603
1315
  * limitations under the License.
604
1316
  */
1317
+ const PROD_HOST = 'firebasedataconnect.googleapis.com';
605
1318
  function urlBuilder(projectConfig, transportOptions) {
606
1319
  const { connector, location, projectId: project, service } = projectConfig;
607
1320
  const { host, sslEnabled, port } = transportOptions;
608
1321
  const protocol = sslEnabled ? 'https' : 'http';
609
- const realHost = host || `firebasedataconnect.googleapis.com`;
1322
+ const realHost = host || PROD_HOST;
610
1323
  let baseUrl = `${protocol}://${realHost}`;
611
1324
  if (typeof port === 'number') {
612
1325
  baseUrl += `:${port}`;
@@ -736,7 +1449,10 @@ class RESTTransport {
736
1449
  async getWithAuth(forceToken = false) {
737
1450
  let starterPromise = new Promise(resolve => resolve(this._accessToken));
738
1451
  if (this.appCheckProvider) {
739
- this._appCheckToken = (await this.appCheckProvider.getToken())?.token;
1452
+ const appCheckToken = await this.appCheckProvider.getToken();
1453
+ if (appCheckToken) {
1454
+ this._appCheckToken = appCheckToken.token;
1455
+ }
740
1456
  }
741
1457
  if (this.authProvider) {
742
1458
  starterPromise = this.authProvider
@@ -905,6 +1621,12 @@ class DataConnect {
905
1621
  }
906
1622
  }
907
1623
  }
1624
+ /**
1625
+ * @internal
1626
+ */
1627
+ getCache() {
1628
+ return this.cache;
1629
+ }
908
1630
  // @internal
909
1631
  _useGeneratedSdk() {
910
1632
  if (!this._isUsingGeneratedSdk) {
@@ -927,6 +1649,12 @@ class DataConnect {
927
1649
  delete copy.projectId;
928
1650
  return copy;
929
1651
  }
1652
+ /**
1653
+ * @internal
1654
+ */
1655
+ setCacheSettings(cacheSettings) {
1656
+ this._cacheSettings = cacheSettings;
1657
+ }
930
1658
  // @internal
931
1659
  setInitialized() {
932
1660
  if (this._initialized) {
@@ -936,19 +1664,25 @@ class DataConnect {
936
1664
  logDebug('transportClass not provided. Defaulting to RESTTransport.');
937
1665
  this._transportClass = RESTTransport;
938
1666
  }
939
- if (this._authProvider) {
940
- this._authTokenProvider = new FirebaseAuthProvider(this.app.name, this.app.options, this._authProvider);
1667
+ this._authTokenProvider = new FirebaseAuthProvider(this.app.name, this.app.options, this._authProvider);
1668
+ const connectorConfig = {
1669
+ connector: this.dataConnectOptions.connector,
1670
+ service: this.dataConnectOptions.service,
1671
+ location: this.dataConnectOptions.location
1672
+ };
1673
+ if (this._cacheSettings) {
1674
+ this.cache = new DataConnectCache(this._authTokenProvider, this.app.options.projectId, connectorConfig, this._transportOptions?.host || PROD_HOST, this._cacheSettings);
941
1675
  }
942
1676
  if (this._appCheckProvider) {
943
1677
  this._appCheckTokenProvider = new AppCheckTokenProvider(this.app, this._appCheckProvider);
944
1678
  }
945
- this._initialized = true;
946
1679
  this._transport = new this._transportClass(this.dataConnectOptions, this.app.options.apiKey, this.app.options.appId, this._authTokenProvider, this._appCheckTokenProvider, undefined, this._isUsingGeneratedSdk, this._callerSdkType);
947
1680
  if (this._transportOptions) {
948
1681
  this._transport.useEmulator(this._transportOptions.host, this._transportOptions.port, this._transportOptions.sslEnabled);
949
1682
  }
950
- this._queryManager = new QueryManager(this._transport);
1683
+ this._queryManager = new QueryManager(this._transport, this, this.cache);
951
1684
  this._mutationManager = new MutationManager(this._transport);
1685
+ this._initialized = true;
952
1686
  }
953
1687
  // @internal
954
1688
  enableEmulator(transportOptions) {
@@ -988,22 +1722,32 @@ function connectDataConnectEmulator(dc, host, port, sslEnabled = false) {
988
1722
  }
989
1723
  dc.enableEmulator({ host, port, sslEnabled });
990
1724
  }
991
- function getDataConnect(appOrOptions, optionalOptions) {
1725
+ function getDataConnect(appOrConnectorConfig, settingsOrConnectorConfig, settings) {
992
1726
  let app$1;
993
- let dcOptions;
994
- if ('location' in appOrOptions) {
995
- dcOptions = appOrOptions;
1727
+ let connectorConfig;
1728
+ let realSettings;
1729
+ if ('location' in appOrConnectorConfig) {
1730
+ connectorConfig = appOrConnectorConfig;
996
1731
  app$1 = app.getApp();
1732
+ realSettings = settingsOrConnectorConfig;
997
1733
  }
998
1734
  else {
999
- dcOptions = optionalOptions;
1000
- app$1 = appOrOptions;
1735
+ app$1 = appOrConnectorConfig;
1736
+ connectorConfig = settingsOrConnectorConfig;
1737
+ realSettings = settings;
1001
1738
  }
1002
1739
  if (!app$1 || Object.keys(app$1).length === 0) {
1003
1740
  app$1 = app.getApp();
1004
1741
  }
1742
+ // Options to store in Firebase Component Provider.
1743
+ const serializedOptions = {
1744
+ ...connectorConfig,
1745
+ projectId: app$1.options.projectId
1746
+ };
1747
+ // We should sort the keys before initialization.
1748
+ const sortedSerialized = Object.fromEntries(Object.entries(serializedOptions).sort());
1005
1749
  const provider = app._getProvider(app$1, 'data-connect');
1006
- const identifier = JSON.stringify(dcOptions);
1750
+ const identifier = JSON.stringify(sortedSerialized);
1007
1751
  if (provider.isInitialized(identifier)) {
1008
1752
  const dcInstance = provider.getImmediate({ identifier });
1009
1753
  const options = provider.getOptions(identifier);
@@ -1013,13 +1757,19 @@ function getDataConnect(appOrOptions, optionalOptions) {
1013
1757
  return dcInstance;
1014
1758
  }
1015
1759
  }
1016
- validateDCOptions(dcOptions);
1760
+ validateDCOptions(connectorConfig);
1017
1761
  logDebug('Creating new DataConnect instance');
1018
1762
  // Initialize with options.
1019
- return provider.initialize({
1763
+ const dataConnect = provider.initialize({
1020
1764
  instanceIdentifier: identifier,
1021
- options: dcOptions
1765
+ options: Object.fromEntries(Object.entries({
1766
+ ...sortedSerialized
1767
+ }).sort())
1022
1768
  });
1769
+ if (realSettings?.cacheSettings) {
1770
+ dataConnect.setCacheSettings(realSettings.cacheSettings);
1771
+ }
1772
+ return dataConnect;
1023
1773
  }
1024
1774
  /**
1025
1775
  *
@@ -1049,6 +1799,12 @@ function terminate(dataConnect) {
1049
1799
  return dataConnect._delete();
1050
1800
  // TODO(mtewani): Stop pending tasks
1051
1801
  }
1802
+ const StorageType = {
1803
+ MEMORY: 'MEMORY'
1804
+ };
1805
+ function makeMemoryCacheProvider() {
1806
+ return new MemoryStub();
1807
+ }
1052
1808
 
1053
1809
  /**
1054
1810
  * @license
@@ -1068,13 +1824,16 @@ function terminate(dataConnect) {
1068
1824
  */
1069
1825
  function registerDataConnect(variant) {
1070
1826
  setSDKVersion(app.SDK_VERSION);
1071
- app._registerComponent(new component.Component('data-connect', (container, { instanceIdentifier: settings, options }) => {
1827
+ app._registerComponent(new component.Component('data-connect', (container, { instanceIdentifier: connectorConfigStr, options }) => {
1072
1828
  const app = container.getProvider('app').getImmediate();
1073
1829
  const authProvider = container.getProvider('auth-internal');
1074
1830
  const appCheckProvider = container.getProvider('app-check-internal');
1075
1831
  let newOpts = options;
1076
- if (settings) {
1077
- newOpts = JSON.parse(settings);
1832
+ if (connectorConfigStr) {
1833
+ newOpts = {
1834
+ ...JSON.parse(connectorConfigStr),
1835
+ ...newOpts
1836
+ };
1078
1837
  }
1079
1838
  if (!app.options.projectId) {
1080
1839
  throw new DataConnectError(Code.INVALID_ARGUMENT, 'Project ID must be provided. Did you pass in a proper projectId to initializeApp?');
@@ -1086,6 +1845,28 @@ function registerDataConnect(variant) {
1086
1845
  app.registerVersion(name, version, 'cjs2020');
1087
1846
  }
1088
1847
 
1848
+ /**
1849
+ * @license
1850
+ * Copyright 2025 Google LLC
1851
+ *
1852
+ * Licensed under the Apache License, Version 2.0 (the "License");
1853
+ * you may not use this file except in compliance with the License.
1854
+ * You may obtain a copy of the License at
1855
+ *
1856
+ * http://www.apache.org/licenses/LICENSE-2.0
1857
+ *
1858
+ * Unless required by applicable law or agreed to in writing, software
1859
+ * distributed under the License is distributed on an "AS IS" BASIS,
1860
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1861
+ * See the License for the specific language governing permissions and
1862
+ * limitations under the License.
1863
+ */
1864
+ const QueryFetchPolicy = {
1865
+ PREFER_CACHE: 'PREFER_CACHE',
1866
+ CACHE_ONLY: 'CACHE_ONLY',
1867
+ SERVER_ONLY: 'SERVER_ONLY'
1868
+ };
1869
+
1089
1870
  /**
1090
1871
  * @license
1091
1872
  * Copyright 2024 Google LLC
@@ -1107,8 +1888,22 @@ function registerDataConnect(variant) {
1107
1888
  * @param queryRef query to execute.
1108
1889
  * @returns `QueryPromise`
1109
1890
  */
1110
- function executeQuery(queryRef) {
1111
- return queryRef.dataConnect._queryManager.executeQuery(queryRef);
1891
+ function executeQuery(queryRef, options) {
1892
+ if (queryRef.refType !== QUERY_STR) {
1893
+ return Promise.reject(new DataConnectError(Code.INVALID_ARGUMENT, `ExecuteQuery can only execute query operations`));
1894
+ }
1895
+ const queryManager = queryRef.dataConnect._queryManager;
1896
+ const fetchPolicy = options?.fetchPolicy ?? QueryFetchPolicy.PREFER_CACHE;
1897
+ switch (fetchPolicy) {
1898
+ case QueryFetchPolicy.SERVER_ONLY:
1899
+ return queryManager.fetchServerResults(queryRef);
1900
+ case QueryFetchPolicy.CACHE_ONLY:
1901
+ return queryManager.fetchCacheResults(queryRef, true);
1902
+ case QueryFetchPolicy.PREFER_CACHE:
1903
+ return queryManager.preferCacheResults(queryRef, false);
1904
+ default:
1905
+ throw new DataConnectError(Code.INVALID_ARGUMENT, `Invalid fetch policy: ${fetchPolicy}`);
1906
+ }
1112
1907
  }
1113
1908
  /**
1114
1909
  * Execute Query
@@ -1120,7 +1915,9 @@ function executeQuery(queryRef) {
1120
1915
  */
1121
1916
  function queryRef(dcInstance, queryName, variables, initialCache) {
1122
1917
  dcInstance.setInitialized();
1123
- dcInstance._queryManager.track(queryName, variables, initialCache);
1918
+ if (initialCache !== undefined) {
1919
+ dcInstance._queryManager.updateSSR(initialCache);
1920
+ }
1124
1921
  return {
1125
1922
  dataConnect: dcInstance,
1126
1923
  refType: QUERY_STR,
@@ -1183,7 +1980,7 @@ function validateArgs(connectorConfig, dcOrVars, vars, validateVars) {
1183
1980
 
1184
1981
  /**
1185
1982
  * @license
1186
- * Copyright 2024 Google LLC
1983
+ * Copyright 2025 Google LLC
1187
1984
  *
1188
1985
  * Licensed under the Apache License, Version 2.0 (the "License");
1189
1986
  * you may not use this file except in compliance with the License.
@@ -1211,12 +2008,14 @@ function subscribe(queryRefOrSerializedResult, observerOrOnNext, onError, onComp
1211
2008
  if ('refInfo' in queryRefOrSerializedResult) {
1212
2009
  const serializedRef = queryRefOrSerializedResult;
1213
2010
  const { data, source, fetchTime } = serializedRef;
2011
+ ref = toQueryRef(serializedRef);
1214
2012
  initialCache = {
1215
2013
  data,
1216
2014
  source,
1217
- fetchTime
2015
+ fetchTime,
2016
+ ref,
2017
+ toJSON: getRefSerializer(ref, data, source, fetchTime)
1218
2018
  };
1219
- ref = toQueryRef(serializedRef);
1220
2019
  }
1221
2020
  else {
1222
2021
  ref = queryRefOrSerializedResult;
@@ -1263,13 +2062,16 @@ exports.DataConnectOperationError = DataConnectOperationError;
1263
2062
  exports.MUTATION_STR = MUTATION_STR;
1264
2063
  exports.MutationManager = MutationManager;
1265
2064
  exports.QUERY_STR = QUERY_STR;
2065
+ exports.QueryFetchPolicy = QueryFetchPolicy;
1266
2066
  exports.SOURCE_CACHE = SOURCE_CACHE;
1267
2067
  exports.SOURCE_SERVER = SOURCE_SERVER;
2068
+ exports.StorageType = StorageType;
1268
2069
  exports.areTransportOptionsEqual = areTransportOptionsEqual;
1269
2070
  exports.connectDataConnectEmulator = connectDataConnectEmulator;
1270
2071
  exports.executeMutation = executeMutation;
1271
2072
  exports.executeQuery = executeQuery;
1272
2073
  exports.getDataConnect = getDataConnect;
2074
+ exports.makeMemoryCacheProvider = makeMemoryCacheProvider;
1273
2075
  exports.mutationRef = mutationRef;
1274
2076
  exports.parseOptions = parseOptions;
1275
2077
  exports.queryRef = queryRef;