@firebase/data-connect 0.3.11-20250716004940 → 0.3.11-caching-fdc.9f17eac6e

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 (58) hide show
  1. package/dist/index.cjs.js +931 -164
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.esm.js +928 -166
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/index.node.cjs.js +866 -101
  6. package/dist/index.node.cjs.js.map +1 -1
  7. package/dist/internal.d.ts +249 -16
  8. package/dist/node-esm/index.node.esm.js +865 -103
  9. package/dist/node-esm/index.node.esm.js.map +1 -1
  10. package/dist/node-esm/src/api/DataConnect.d.ts +27 -4
  11. package/dist/node-esm/src/api/index.d.ts +2 -1
  12. package/dist/node-esm/src/api/query.d.ts +2 -34
  13. package/dist/node-esm/src/api.browser.d.ts +3 -18
  14. package/dist/node-esm/src/api.node.d.ts +1 -0
  15. package/dist/node-esm/src/cache/Cache.d.ts +52 -0
  16. package/dist/node-esm/src/cache/CacheProvider.d.ts +26 -0
  17. package/dist/node-esm/src/cache/EntityDataObject.d.ts +41 -0
  18. package/dist/node-esm/src/cache/EntityNode.d.ts +63 -0
  19. package/dist/node-esm/src/cache/ImpactedQueryRefsAccumulator.d.ts +21 -0
  20. package/dist/node-esm/src/cache/InMemoryCacheProvider.d.ts +31 -0
  21. package/dist/node-esm/src/cache/IndexedDBCacheProvider.d.ts +39 -0
  22. package/dist/node-esm/src/cache/ResultTree.d.ts +39 -0
  23. package/dist/node-esm/src/cache/ResultTreeProcessor.d.ts +28 -0
  24. package/dist/node-esm/src/core/AppCheckTokenProvider.d.ts +1 -1
  25. package/dist/node-esm/src/core/FirebaseAuthProvider.d.ts +3 -1
  26. package/dist/node-esm/src/core/{QueryManager.d.ts → query/QueryManager.d.ts} +17 -15
  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 +1 -1
  30. package/dist/node-esm/src/network/transport/rest.d.ts +2 -2
  31. package/dist/node-esm/src/util/encoder.d.ts +3 -0
  32. package/dist/node-esm/src/util/url.d.ts +1 -0
  33. package/dist/private.d.ts +248 -15
  34. package/dist/public.d.ts +54 -4
  35. package/dist/src/api/DataConnect.d.ts +27 -4
  36. package/dist/src/api/index.d.ts +2 -1
  37. package/dist/src/api/query.d.ts +2 -34
  38. package/dist/src/api.browser.d.ts +3 -18
  39. package/dist/src/api.node.d.ts +1 -0
  40. package/dist/src/cache/Cache.d.ts +52 -0
  41. package/dist/src/cache/CacheProvider.d.ts +26 -0
  42. package/dist/src/cache/EntityDataObject.d.ts +41 -0
  43. package/dist/src/cache/EntityNode.d.ts +63 -0
  44. package/dist/src/cache/ImpactedQueryRefsAccumulator.d.ts +21 -0
  45. package/dist/src/cache/InMemoryCacheProvider.d.ts +31 -0
  46. package/dist/src/cache/IndexedDBCacheProvider.d.ts +39 -0
  47. package/dist/src/cache/ResultTree.d.ts +39 -0
  48. package/dist/src/cache/ResultTreeProcessor.d.ts +28 -0
  49. package/dist/src/core/AppCheckTokenProvider.d.ts +1 -1
  50. package/dist/src/core/FirebaseAuthProvider.d.ts +3 -1
  51. package/dist/src/core/{QueryManager.d.ts → query/QueryManager.d.ts} +17 -15
  52. package/dist/src/core/query/queryOptions.d.ts +25 -0
  53. package/dist/src/core/query/subscribe.d.ts +67 -0
  54. package/dist/src/network/fetch.d.ts +1 -1
  55. package/dist/src/network/transport/rest.d.ts +2 -2
  56. package/dist/src/util/encoder.d.ts +3 -0
  57. package/dist/src/util/url.d.ts +1 -0
  58. package/package.json +7 -7
package/dist/index.cjs.js CHANGED
@@ -8,7 +8,7 @@ var util = require('@firebase/util');
8
8
  var logger$1 = require('@firebase/logger');
9
9
 
10
10
  const name = "@firebase/data-connect";
11
- const version = "0.3.11-20250716004940";
11
+ const version = "0.3.11-caching-fdc.9f17eac6e";
12
12
 
13
13
  /**
14
14
  * @license
@@ -36,69 +36,6 @@ function setSDKVersion(version) {
36
36
  SDK_VERSION = version;
37
37
  }
38
38
 
39
- /**
40
- * @license
41
- * Copyright 2024 Google LLC
42
- *
43
- * Licensed under the Apache License, Version 2.0 (the "License");
44
- * you may not use this file except in compliance with the License.
45
- * You may obtain a copy of the License at
46
- *
47
- * http://www.apache.org/licenses/LICENSE-2.0
48
- *
49
- * Unless required by applicable law or agreed to in writing, software
50
- * distributed under the License is distributed on an "AS IS" BASIS,
51
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
52
- * See the License for the specific language governing permissions and
53
- * limitations under the License.
54
- */
55
- /**
56
- * @internal
57
- * Abstraction around AppCheck's token fetching capabilities.
58
- */
59
- class AppCheckTokenProvider {
60
- constructor(app$1, appCheckProvider) {
61
- this.appCheckProvider = appCheckProvider;
62
- if (app._isFirebaseServerApp(app$1) && app$1.settings.appCheckToken) {
63
- this.serverAppAppCheckToken = app$1.settings.appCheckToken;
64
- }
65
- this.appCheck = appCheckProvider?.getImmediate({ optional: true });
66
- if (!this.appCheck) {
67
- void appCheckProvider
68
- ?.get()
69
- .then(appCheck => (this.appCheck = appCheck))
70
- .catch();
71
- }
72
- }
73
- getToken() {
74
- if (this.serverAppAppCheckToken) {
75
- return Promise.resolve({ token: this.serverAppAppCheckToken });
76
- }
77
- if (!this.appCheck) {
78
- return new Promise((resolve, reject) => {
79
- // Support delayed initialization of FirebaseAppCheck. This allows our
80
- // customers to initialize the RTDB SDK before initializing Firebase
81
- // AppCheck and ensures that all requests are authenticated if a token
82
- // becomes available before the timoeout below expires.
83
- setTimeout(() => {
84
- if (this.appCheck) {
85
- this.getToken().then(resolve, reject);
86
- }
87
- else {
88
- resolve(null);
89
- }
90
- }, 0);
91
- });
92
- }
93
- return this.appCheck.getToken();
94
- }
95
- addTokenChangeListener(listener) {
96
- void this.appCheckProvider
97
- ?.get()
98
- .then(appCheck => appCheck.addTokenListener(listener));
99
- }
100
- }
101
-
102
39
  /**
103
40
  * @license
104
41
  * Copyright 2024 Google LLC
@@ -178,6 +115,769 @@ function logError(msg) {
178
115
  logger.error(`DataConnect (${SDK_VERSION}): ${msg}`);
179
116
  }
180
117
 
118
+ /**
119
+ * @license
120
+ * Copyright 2025 Google LLC
121
+ *
122
+ * Licensed under the Apache License, Version 2.0 (the "License");
123
+ * you may not use this file except in compliance with the License.
124
+ * You may obtain a copy of the License at
125
+ *
126
+ * http://www.apache.org/licenses/LICENSE-2.0
127
+ *
128
+ * Unless required by applicable law or agreed to in writing, software
129
+ * distributed under the License is distributed on an "AS IS" BASIS,
130
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
131
+ * See the License for the specific language governing permissions and
132
+ * limitations under the License.
133
+ */
134
+ class ImpactedQueryRefsAccumulator {
135
+ constructor() {
136
+ this.impacted = new Set();
137
+ }
138
+ add(impacted) {
139
+ impacted.forEach(ref => this.impacted.add(ref));
140
+ }
141
+ consumeEvents() {
142
+ const events = Array.from(this.impacted);
143
+ this.impacted.clear();
144
+ return events;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * @license
150
+ * Copyright 2025 Google LLC
151
+ *
152
+ * Licensed under the Apache License, Version 2.0 (the "License");
153
+ * you may not use this file except in compliance with the License.
154
+ * You may obtain a copy of the License at
155
+ *
156
+ * http://www.apache.org/licenses/LICENSE-2.0
157
+ *
158
+ * Unless required by applicable law or agreed to in writing, software
159
+ * distributed under the License is distributed on an "AS IS" BASIS,
160
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
161
+ * See the License for the specific language governing permissions and
162
+ * limitations under the License.
163
+ */
164
+ class EntityDataObject {
165
+ getMap() {
166
+ return this.map;
167
+ }
168
+ getStorableMap(map) {
169
+ const newMap = {};
170
+ for (const key in map) {
171
+ if (map.hasOwnProperty(key)) {
172
+ newMap[key] = map[key];
173
+ }
174
+ }
175
+ return newMap;
176
+ }
177
+ toStorableJson() {
178
+ return {
179
+ globalID: this.globalID,
180
+ map: this.getStorableMap(this.map),
181
+ queriesReferenced: this.queriesReferenced
182
+ };
183
+ }
184
+ static fromStorableJson(json) {
185
+ const bdo = new EntityDataObject(json.globalID);
186
+ bdo.map = json.map;
187
+ bdo.queriesReferenced = json.queriesReferenced;
188
+ return bdo;
189
+ }
190
+ constructor(globalID) {
191
+ this.globalID = globalID;
192
+ this.map = {};
193
+ this.queriesReferenced = new Set();
194
+ }
195
+ updateServerValue(key, value, requestedFrom) {
196
+ this.map[key] = value;
197
+ this.queriesReferenced.add(requestedFrom);
198
+ return Array.from(this.queriesReferenced);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * @license
204
+ * Copyright 2025 Google LLC
205
+ *
206
+ * Licensed under the Apache License, Version 2.0 (the "License");
207
+ * you may not use this file except in compliance with the License.
208
+ * You may obtain a copy of the License at
209
+ *
210
+ * http://www.apache.org/licenses/LICENSE-2.0
211
+ *
212
+ * Unless required by applicable law or agreed to in writing, software
213
+ * distributed under the License is distributed on an "AS IS" BASIS,
214
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
215
+ * See the License for the specific language governing permissions and
216
+ * limitations under the License.
217
+ */
218
+ const GLOBAL_ID_KEY = '_id';
219
+ class EntityNode {
220
+ constructor(acc = new ImpactedQueryRefsAccumulator()) {
221
+ this.acc = acc;
222
+ this.scalars = {};
223
+ this.references = {};
224
+ this.objectLists = {};
225
+ this.impactedQueryRefs = new Set();
226
+ }
227
+ async loadData(queryId, values, cacheProvider) {
228
+ if (values === undefined && cacheProvider === undefined) {
229
+ return;
230
+ }
231
+ if (typeof values !== 'object' || Array.isArray(values)) {
232
+ throw new DataConnectError('invalid-argument', 'EntityNode initialized with non-object value');
233
+ }
234
+ if (values === null) {
235
+ return;
236
+ }
237
+ if (values.hasOwnProperty(GLOBAL_ID_KEY) &&
238
+ typeof values[GLOBAL_ID_KEY] === 'string') {
239
+ this.globalId = values[GLOBAL_ID_KEY];
240
+ // TODO: Add current query id to BDO
241
+ this.entityData = await cacheProvider.getBdo(this.globalId);
242
+ }
243
+ for (const key in values) {
244
+ if (values.hasOwnProperty(key)) {
245
+ if (key === GLOBAL_ID_KEY) {
246
+ continue;
247
+ }
248
+ if (typeof values[key] === 'object') {
249
+ if (Array.isArray(values[key])) {
250
+ const objArray = [];
251
+ const scalarArray = [];
252
+ for (const value of values[key]) {
253
+ if (typeof value === 'object') {
254
+ if (Array.isArray(value)) ;
255
+ else {
256
+ const entityNode = new EntityNode(this.acc);
257
+ await entityNode.loadData(queryId, value, cacheProvider);
258
+ objArray.push(entityNode);
259
+ }
260
+ }
261
+ else {
262
+ scalarArray.push(value);
263
+ }
264
+ }
265
+ if (scalarArray.length > 0 && objArray.length > 0) {
266
+ throw new DataConnectError('invalid-argument', 'Sparse array detected.');
267
+ }
268
+ if (scalarArray.length > 0) {
269
+ if (this.entityData) {
270
+ const impactedRefs = this.entityData.updateServerValue(key, scalarArray, queryId);
271
+ this.acc.add(impactedRefs);
272
+ }
273
+ else {
274
+ this.scalars[key] = scalarArray;
275
+ }
276
+ }
277
+ else if (objArray.length > 0) {
278
+ this.objectLists[key] = objArray;
279
+ }
280
+ else {
281
+ this.scalars[key] = [];
282
+ }
283
+ }
284
+ else {
285
+ if (values[key] === null) {
286
+ this.scalars[key] = null;
287
+ continue;
288
+ }
289
+ const stubDataObject = new EntityNode(this.acc);
290
+ await stubDataObject.loadData(queryId, values[key], cacheProvider);
291
+ this.references[key] = stubDataObject;
292
+ }
293
+ }
294
+ else {
295
+ if (this.entityData) {
296
+ // TODO: Track only the fields we need for the BDO
297
+ const impactedRefs = this.entityData.updateServerValue(key, values[key], queryId);
298
+ this.acc.add(impactedRefs);
299
+ }
300
+ else {
301
+ this.scalars[key] = values[key];
302
+ }
303
+ }
304
+ }
305
+ }
306
+ if (this.entityData) {
307
+ await cacheProvider.updateBackingData(this.entityData);
308
+ }
309
+ }
310
+ toJson() {
311
+ const resultObject = {};
312
+ const entityDataMap = this.entityData?.getMap();
313
+ for (const key in entityDataMap) {
314
+ if (entityDataMap?.hasOwnProperty(key)) {
315
+ resultObject[key] = entityDataMap[key];
316
+ }
317
+ }
318
+ // Scalars should never have stubdataobjects
319
+ for (const key in this.scalars) {
320
+ if (this.scalars.hasOwnProperty(key)) {
321
+ resultObject[key] = this.scalars[key];
322
+ }
323
+ }
324
+ for (const key in this.references) {
325
+ if (this.references.hasOwnProperty(key)) {
326
+ resultObject[key] = this.references[key].toJson();
327
+ }
328
+ }
329
+ for (const key in this.objectLists) {
330
+ if (this.objectLists.hasOwnProperty(key)) {
331
+ resultObject[key] = this.objectLists[key].map(obj => {
332
+ return obj.toJson();
333
+ });
334
+ }
335
+ }
336
+ return resultObject;
337
+ }
338
+ static parseMap(map, isSdo = false) {
339
+ const newMap = {};
340
+ for (const key in map) {
341
+ if (map.hasOwnProperty(key)) {
342
+ if (Array.isArray(map[key])) {
343
+ newMap[key] = map[key].map(value => isSdo ? EntityNode.fromStorableJson(value) : value);
344
+ }
345
+ else {
346
+ newMap[key] = isSdo
347
+ ? EntityNode.fromStorableJson(map[key])
348
+ : map[key];
349
+ }
350
+ }
351
+ }
352
+ return newMap;
353
+ }
354
+ static fromStorableJson(obj) {
355
+ const sdo = new EntityNode();
356
+ if (obj.backingData) {
357
+ sdo.entityData = EntityDataObject.fromStorableJson(obj.backingData);
358
+ }
359
+ sdo.acc = new ImpactedQueryRefsAccumulator();
360
+ sdo.globalId = obj.globalID;
361
+ sdo.impactedQueryRefs = new Set();
362
+ sdo.scalars = EntityNode.parseMap(obj.scalars);
363
+ sdo.references = EntityNode.parseMap(obj.references);
364
+ sdo.objectLists = EntityNode.parseMap(obj.objectLists, true);
365
+ return sdo;
366
+ }
367
+ getStorableMap(map) {
368
+ const newMap = {};
369
+ for (const key in map) {
370
+ if (map.hasOwnProperty(key)) {
371
+ if (Array.isArray(map[key])) {
372
+ newMap[key] = map[key].map(value => value.toStorableJson());
373
+ }
374
+ else {
375
+ newMap[key] = map[key].toStorableJson();
376
+ }
377
+ }
378
+ }
379
+ return newMap;
380
+ }
381
+ toStorableJson() {
382
+ const obj = {
383
+ globalID: this.globalId,
384
+ scalars: this.scalars,
385
+ references: this.getStorableMap(this.references),
386
+ objectLists: this.getStorableMap(this.objectLists)
387
+ };
388
+ if (this.entityData) {
389
+ obj.backingData = this.entityData.toStorableJson();
390
+ }
391
+ return obj;
392
+ }
393
+ }
394
+
395
+ /**
396
+ * @license
397
+ * Copyright 2025 Google LLC
398
+ *
399
+ * Licensed under the Apache License, Version 2.0 (the "License");
400
+ * you may not use this file except in compliance with the License.
401
+ * You may obtain a copy of the License at
402
+ *
403
+ * http://www.apache.org/licenses/LICENSE-2.0
404
+ *
405
+ * Unless required by applicable law or agreed to in writing, software
406
+ * distributed under the License is distributed on an "AS IS" BASIS,
407
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
408
+ * See the License for the specific language governing permissions and
409
+ * limitations under the License.
410
+ */
411
+ class ResultTree {
412
+ static parse(value) {
413
+ const rt = new ResultTree(value.data, EntityNode.fromStorableJson(value.rootStub), value.ttlInMs, value.cachedAt, value.lastAccessed);
414
+ return rt;
415
+ }
416
+ constructor(data, rootStub, ttlInMs = 30000, cachedAt, _lastAccessed) {
417
+ this.data = data;
418
+ this.rootStub = rootStub;
419
+ this.ttlInMs = ttlInMs;
420
+ this.cachedAt = cachedAt;
421
+ this._lastAccessed = _lastAccessed;
422
+ }
423
+ isStale() {
424
+ return (Date.now() - new Date(this.cachedAt.getTime()).getTime() > this.ttlInMs);
425
+ }
426
+ updateTtl(ttlInMs) {
427
+ this.ttlInMs = ttlInMs;
428
+ }
429
+ updateAccessed() {
430
+ this._lastAccessed = new Date();
431
+ }
432
+ get lastAccessed() {
433
+ return this._lastAccessed;
434
+ }
435
+ getRootStub() {
436
+ return this.rootStub;
437
+ }
438
+ }
439
+
440
+ /**
441
+ * @license
442
+ * Copyright 2025 Google LLC
443
+ *
444
+ * Licensed under the Apache License, Version 2.0 (the "License");
445
+ * you may not use this file except in compliance with the License.
446
+ * You may obtain a copy of the License at
447
+ *
448
+ * http://www.apache.org/licenses/LICENSE-2.0
449
+ *
450
+ * Unless required by applicable law or agreed to in writing, software
451
+ * distributed under the License is distributed on an "AS IS" BASIS,
452
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
453
+ * See the License for the specific language governing permissions and
454
+ * limitations under the License.
455
+ */
456
+ const BDO_OBJECT_STORE_NAME = 'data-connect-bdos';
457
+ const SRT_OBJECT_STORE_NAME = 'data-connect-srts';
458
+ class IndexedDBCacheProvider {
459
+ isIdbAvailable() {
460
+ return typeof window !== 'undefined' && 'indexedDB' in window;
461
+ }
462
+ // TODO: Figure out how to deal with caching across tabs.
463
+ // We could use the web locks api
464
+ constructor(cacheId) {
465
+ this.cacheId = cacheId;
466
+ this.bdos = new Map();
467
+ this.resultTrees = new Map();
468
+ this.initialized = false;
469
+ if (!this.isIdbAvailable()) {
470
+ return;
471
+ }
472
+ this.idbManager = new IndexedDbManager(this.cacheId);
473
+ this.idbManager.open(1);
474
+ }
475
+ async initialize() {
476
+ // load BDOs
477
+ if (this.initialized) {
478
+ return;
479
+ }
480
+ const db = await this.idbManager.dbPromise;
481
+ const resultTrees = await this.idbManager.readFromDb(db);
482
+ resultTrees.forEach((resultTree, key) => {
483
+ this.resultTrees.set(key, resultTree);
484
+ });
485
+ }
486
+ async commitBdoChanges(backingData) {
487
+ // TODO: Move these into a new function.
488
+ if (!this.isIdbAvailable()) {
489
+ return;
490
+ }
491
+ await this.initialize();
492
+ void this.idbManager.updateBdo(backingData);
493
+ }
494
+ async commitResultTreeChanges(queryId, rt) {
495
+ if (!this.isIdbAvailable()) {
496
+ return;
497
+ }
498
+ await this.initialize();
499
+ void this.idbManager.updateResultTree(rt, queryId);
500
+ }
501
+ async setResultTree(queryId, rt) {
502
+ this.resultTrees.set(queryId, rt);
503
+ // maybe this needs to be async?
504
+ // TODO: replace array with valid data
505
+ await this.commitResultTreeChanges(queryId, rt);
506
+ }
507
+ async getResultTree(queryId) {
508
+ await this.initialize();
509
+ const ret = this.resultTrees.get(queryId);
510
+ return ret;
511
+ }
512
+ createGlobalId() {
513
+ return crypto.randomUUID();
514
+ }
515
+ async getBdo(globalId) {
516
+ await this.initialize();
517
+ if (!this.bdos.has(globalId)) {
518
+ this.bdos.set(globalId, new EntityDataObject(globalId));
519
+ }
520
+ // Because of the above, we can guarantee that there will be a BDO at the globalId.
521
+ return this.bdos.get(globalId);
522
+ }
523
+ async updateBackingData(backingData) {
524
+ this.bdos.set(backingData.globalID, backingData);
525
+ await this.commitBdoChanges(backingData);
526
+ }
527
+ async close() {
528
+ await this.idbManager.close();
529
+ }
530
+ }
531
+ const dbName = 'data-connect';
532
+ class IndexedDbManager {
533
+ constructor(cacheId) {
534
+ this.cacheId = cacheId;
535
+ this.alreadyRead = false;
536
+ }
537
+ open(version) {
538
+ this.dbPromise = new Promise((dbResolve, dbReject) => {
539
+ // TODO: See when, or if ever, cacheId is null
540
+ const request = indexedDB.open(`${dbName}-${this.cacheId}`, version);
541
+ request.onupgradeneeded = event => {
542
+ // TODO: Handle what happens if the version changes.
543
+ const db = event.target.result;
544
+ db.createObjectStore(BDO_OBJECT_STORE_NAME);
545
+ db.createObjectStore(SRT_OBJECT_STORE_NAME);
546
+ dbResolve(db);
547
+ };
548
+ request.onsuccess = async (event) => {
549
+ const db = event.target.result;
550
+ dbResolve(db);
551
+ };
552
+ request.onerror = error => {
553
+ // TODO(mtewani): Use proper error.
554
+ dbReject(error);
555
+ };
556
+ });
557
+ }
558
+ async updateBdo(backingData) {
559
+ const db = await this.dbPromise;
560
+ db.transaction([BDO_OBJECT_STORE_NAME], 'readwrite')
561
+ .objectStore(BDO_OBJECT_STORE_NAME)
562
+ .put(backingData, backingData.globalID);
563
+ }
564
+ async updateResultTree(rt, queryId) {
565
+ const db = await this.dbPromise;
566
+ const objectStore = db
567
+ .transaction([SRT_OBJECT_STORE_NAME], 'readwrite')
568
+ .objectStore(SRT_OBJECT_STORE_NAME);
569
+ // TODO: What happens if you override an existing entry?
570
+ // TODO: We should first check whether the tree is hydrated or not.
571
+ // TODO: We should make sure that everything has been written.
572
+ objectStore.put(rt.getRootStub().toStorableJson(), queryId);
573
+ }
574
+ async readFromDb(db) {
575
+ const resultTrees = new Map();
576
+ if (this.alreadyRead) {
577
+ return resultTrees;
578
+ }
579
+ const bdos = new Map();
580
+ const tx = db.transaction([BDO_OBJECT_STORE_NAME, SRT_OBJECT_STORE_NAME], 'readonly');
581
+ const bdoStore = tx.objectStore(BDO_OBJECT_STORE_NAME);
582
+ const srtStore = tx.objectStore(SRT_OBJECT_STORE_NAME);
583
+ const bdoComplete = new Promise((resolve, reject) => {
584
+ const openCursor = bdoStore.openCursor();
585
+ openCursor.onsuccess = event => {
586
+ const cursor = event.target
587
+ .result;
588
+ if (cursor) {
589
+ bdos.set(cursor.key, EntityDataObject.fromStorableJson(cursor.value));
590
+ cursor.continue();
591
+ }
592
+ else {
593
+ // No more entries
594
+ resolve(null);
595
+ }
596
+ };
597
+ openCursor.onerror = error => {
598
+ console.error(error);
599
+ reject(error);
600
+ };
601
+ });
602
+ const srtComplete = new Promise((resolve, reject) => {
603
+ const openCursor = srtStore.openCursor();
604
+ openCursor.onsuccess = event => {
605
+ const cursor = event.target
606
+ .result;
607
+ if (cursor) {
608
+ const srt = ResultTree.parse(cursor.value);
609
+ resultTrees.set(cursor.key, srt);
610
+ cursor.continue();
611
+ }
612
+ else {
613
+ // No more entries
614
+ resolve(null);
615
+ }
616
+ };
617
+ openCursor.onerror = error => {
618
+ console.error(error);
619
+ reject(error);
620
+ };
621
+ });
622
+ await Promise.all([bdoComplete, srtComplete]);
623
+ return resultTrees;
624
+ }
625
+ async close() {
626
+ const db = await this.dbPromise;
627
+ db.close();
628
+ }
629
+ }
630
+
631
+ /**
632
+ * @license
633
+ * Copyright 2025 Google LLC
634
+ *
635
+ * Licensed under the Apache License, Version 2.0 (the "License");
636
+ * you may not use this file except in compliance with the License.
637
+ * You may obtain a copy of the License at
638
+ *
639
+ * http://www.apache.org/licenses/LICENSE-2.0
640
+ *
641
+ * Unless required by applicable law or agreed to in writing, software
642
+ * distributed under the License is distributed on an "AS IS" BASIS,
643
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
644
+ * See the License for the specific language governing permissions and
645
+ * limitations under the License.
646
+ */
647
+ class InMemoryCacheProvider {
648
+ constructor(_keyId) {
649
+ this._keyId = _keyId;
650
+ this.bdos = new Map();
651
+ this.resultTrees = new Map();
652
+ }
653
+ setResultTree(queryId, rt) {
654
+ this.resultTrees.set(queryId, rt);
655
+ return Promise.resolve();
656
+ }
657
+ // TODO: Should this be in the cache provider? This seems common along all CacheProviders.
658
+ async getResultTree(queryId) {
659
+ return this.resultTrees.get(queryId);
660
+ }
661
+ createGlobalId() {
662
+ return crypto.randomUUID();
663
+ }
664
+ updateBackingData(backingData) {
665
+ this.bdos.set(backingData.globalID, backingData);
666
+ return Promise.resolve();
667
+ }
668
+ async getBdo(globalId) {
669
+ if (!this.bdos.has(globalId)) {
670
+ this.bdos.set(globalId, new EntityDataObject(globalId));
671
+ }
672
+ // Because of the above, we can guarantee that there will be a BDO at the globalId.
673
+ return this.bdos.get(globalId);
674
+ }
675
+ close() {
676
+ // TODO: Noop
677
+ return Promise.resolve();
678
+ }
679
+ }
680
+
681
+ /**
682
+ * @license
683
+ * Copyright 2025 Google LLC
684
+ *
685
+ * Licensed under the Apache License, Version 2.0 (the "License");
686
+ * you may not use this file except in compliance with the License.
687
+ * You may obtain a copy of the License at
688
+ *
689
+ * http://www.apache.org/licenses/LICENSE-2.0
690
+ *
691
+ * Unless required by applicable law or agreed to in writing, software
692
+ * distributed under the License is distributed on an "AS IS" BASIS,
693
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
694
+ * See the License for the specific language governing permissions and
695
+ * limitations under the License.
696
+ */
697
+ class ResultTreeProcessor {
698
+ hydrateResults(rootStubObject) {
699
+ return JSON.stringify(rootStubObject.toJson());
700
+ }
701
+ async dehydrateResults(json, cacheProvider, acc, queryId) {
702
+ const stubDataObject = new EntityNode(acc);
703
+ await stubDataObject.loadData(queryId, json, cacheProvider);
704
+ return {
705
+ stubDataObject,
706
+ data: JSON.stringify(stubDataObject.toStorableJson())
707
+ };
708
+ }
709
+ }
710
+
711
+ /**
712
+ * @license
713
+ * Copyright 2025 Google LLC
714
+ *
715
+ * Licensed under the Apache License, Version 2.0 (the "License");
716
+ * you may not use this file except in compliance with the License.
717
+ * You may obtain a copy of the License at
718
+ *
719
+ * http://www.apache.org/licenses/LICENSE-2.0
720
+ *
721
+ * Unless required by applicable law or agreed to in writing, software
722
+ * distributed under the License is distributed on an "AS IS" BASIS,
723
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
724
+ * See the License for the specific language governing permissions and
725
+ * limitations under the License.
726
+ */
727
+ class DataConnectCache {
728
+ constructor(authProvider, projectId, connectorConfig, host, cacheSettings) {
729
+ this.authProvider = authProvider;
730
+ this.projectId = projectId;
731
+ this.connectorConfig = connectorConfig;
732
+ this.host = host;
733
+ this.cacheSettings = cacheSettings;
734
+ this.cacheProvider = null;
735
+ this.uid = null;
736
+ this.authProvider.addTokenChangeListener(async (_) => {
737
+ const newUid = this.authProvider.getAuth().getUid();
738
+ // We should only close if the token changes and so does the new UID
739
+ if (this.uid !== newUid) {
740
+ await this.cacheProvider?.close();
741
+ this.uid = newUid;
742
+ const identifier = await this.getIdentifier(this.uid);
743
+ this.cacheProvider = this.initializeNewProviders(identifier);
744
+ }
745
+ });
746
+ }
747
+ async initialize() {
748
+ if (!this.cacheProvider) {
749
+ const identifier = await this.getIdentifier(this.uid);
750
+ this.cacheProvider = this.initializeNewProviders(identifier);
751
+ }
752
+ }
753
+ async getIdentifier(uid) {
754
+ const identifier = `${this.cacheSettings?.cacheProvider instanceof IndexedDBStub
755
+ ? 'persistent'
756
+ : 'memory'}-${this.projectId}-${this.connectorConfig.service}-${this.connectorConfig.connector}-${this.connectorConfig.location}-${uid}-${this.host}`;
757
+ const sha256 = await util.generateSHA256HashBrowser(identifier);
758
+ return sha256;
759
+ }
760
+ initializeNewProviders(identifier) {
761
+ let cacheProvider;
762
+ if (this.cacheSettings) {
763
+ cacheProvider =
764
+ this.cacheSettings.cacheProvider?.type === 'MEMORY' ||
765
+ !util.isIndexedDBAvailable()
766
+ ? new InMemoryCacheProvider(identifier)
767
+ : new IndexedDBCacheProvider(identifier);
768
+ }
769
+ else {
770
+ logDebug('IndexedDB is not available. Using In-Memory Cache Provider instead.');
771
+ cacheProvider = new InMemoryCacheProvider(identifier);
772
+ }
773
+ return cacheProvider;
774
+ }
775
+ async containsResultTree(queryId) {
776
+ await this.initialize();
777
+ const resultTree = await this.cacheProvider.getResultTree(queryId);
778
+ return resultTree !== undefined;
779
+ }
780
+ async getResultTree(queryId) {
781
+ await this.initialize();
782
+ const cacheProvider = this.cacheProvider;
783
+ return cacheProvider.getResultTree(queryId);
784
+ }
785
+ async getResultJSON(queryId) {
786
+ await this.initialize();
787
+ const processor = new ResultTreeProcessor();
788
+ const cacheProvider = this.cacheProvider;
789
+ const resultTree = await cacheProvider.getResultTree(queryId);
790
+ if (!resultTree) {
791
+ throw new DataConnectError('invalid-argument', `${queryId} not found in cache. Call "update() first."`);
792
+ }
793
+ return processor.hydrateResults(resultTree.getRootStub());
794
+ }
795
+ async update(queryId, serverValues) {
796
+ const processor = new ResultTreeProcessor();
797
+ const acc = new ImpactedQueryRefsAccumulator();
798
+ const cacheProvider = this.cacheProvider;
799
+ const { data, stubDataObject } = await processor.dehydrateResults(serverValues, cacheProvider, acc, queryId);
800
+ const now = new Date();
801
+ // TODO: Check if ttl actually gets passed.
802
+ // TODO: Check API Proposal fields.
803
+ await cacheProvider.setResultTree(queryId, new ResultTree(data, stubDataObject, serverValues.ttl, now, now));
804
+ return acc.consumeEvents();
805
+ }
806
+ }
807
+ class IndexedDBStub {
808
+ constructor() {
809
+ this.type = 'PERSISTENT';
810
+ }
811
+ }
812
+ class MemoryStub {
813
+ constructor() {
814
+ this.type = 'MEMORY';
815
+ }
816
+ }
817
+
818
+ /**
819
+ * @license
820
+ * Copyright 2024 Google LLC
821
+ *
822
+ * Licensed under the Apache License, Version 2.0 (the "License");
823
+ * you may not use this file except in compliance with the License.
824
+ * You may obtain a copy of the License at
825
+ *
826
+ * http://www.apache.org/licenses/LICENSE-2.0
827
+ *
828
+ * Unless required by applicable law or agreed to in writing, software
829
+ * distributed under the License is distributed on an "AS IS" BASIS,
830
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
831
+ * See the License for the specific language governing permissions and
832
+ * limitations under the License.
833
+ */
834
+ /**
835
+ * @internal
836
+ * Abstraction around AppCheck's token fetching capabilities.
837
+ */
838
+ class AppCheckTokenProvider {
839
+ constructor(app$1, appCheckProvider) {
840
+ this.appCheckProvider = appCheckProvider;
841
+ if (app._isFirebaseServerApp(app$1) && app$1.settings.appCheckToken) {
842
+ this.serverAppAppCheckToken = app$1.settings.appCheckToken;
843
+ }
844
+ this.appCheck = appCheckProvider?.getImmediate({ optional: true });
845
+ if (!this.appCheck) {
846
+ void appCheckProvider
847
+ ?.get()
848
+ .then(appCheck => (this.appCheck = appCheck))
849
+ .catch();
850
+ }
851
+ }
852
+ getToken() {
853
+ if (this.serverAppAppCheckToken) {
854
+ return Promise.resolve({ token: this.serverAppAppCheckToken });
855
+ }
856
+ if (!this.appCheck) {
857
+ return new Promise((resolve, reject) => {
858
+ // Support delayed initialization of FirebaseAppCheck. This allows our
859
+ // customers to initialize the RTDB SDK before initializing Firebase
860
+ // AppCheck and ensures that all requests are authenticated if a token
861
+ // becomes available before the timoeout below expires.
862
+ setTimeout(() => {
863
+ if (this.appCheck) {
864
+ this.getToken().then(resolve, reject);
865
+ }
866
+ else {
867
+ resolve(null);
868
+ }
869
+ }, 0);
870
+ });
871
+ }
872
+ return this.appCheck.getToken();
873
+ }
874
+ addTokenChangeListener(listener) {
875
+ void this.appCheckProvider
876
+ ?.get()
877
+ .then(appCheck => appCheck.addTokenListener(listener));
878
+ }
879
+ }
880
+
181
881
  /**
182
882
  * @license
183
883
  * Copyright 2024 Google LLC
@@ -205,6 +905,9 @@ class FirebaseAuthProvider {
205
905
  _authProvider.onInit(auth => (this._auth = auth));
206
906
  }
207
907
  }
908
+ getAuth() {
909
+ return this._auth;
910
+ }
208
911
  getToken(forceRefresh) {
209
912
  if (!this._auth) {
210
913
  return new Promise((resolve, reject) => {
@@ -279,14 +982,20 @@ const SOURCE_CACHE = 'CACHE';
279
982
  * limitations under the License.
280
983
  */
281
984
  let encoderImpl;
985
+ let decoderImpl;
282
986
  function setEncoder(encoder) {
283
987
  encoderImpl = encoder;
284
988
  }
989
+ function setDecoder(decoder) {
990
+ decoderImpl = decoder;
991
+ }
992
+ // TODO(mtewani): Fix issue where if fields are out of order, caching breaks.
285
993
  setEncoder(o => JSON.stringify(o));
994
+ setDecoder(s => JSON.parse(s));
286
995
 
287
996
  /**
288
997
  * @license
289
- * Copyright 2024 Google LLC
998
+ * Copyright 2025 Google LLC
290
999
  *
291
1000
  * Licensed under the Apache License, Version 2.0 (the "License");
292
1001
  * you may not use this file except in compliance with the License.
@@ -300,11 +1009,11 @@ setEncoder(o => JSON.stringify(o));
300
1009
  * See the License for the specific language governing permissions and
301
1010
  * limitations under the License.
302
1011
  */
303
- function setIfNotExists(map, key, val) {
304
- if (!map.has(key)) {
305
- map.set(key, val);
306
- }
307
- }
1012
+ const QueryFetchPolicy = {
1013
+ PREFER_CACHE: 'PREFER_CACHE',
1014
+ CACHE_ONLY: 'CACHE_ONLY',
1015
+ SERVER_ONLY: 'SERVER_ONLY'
1016
+ };
308
1017
 
309
1018
  /**
310
1019
  * @license
@@ -334,32 +1043,35 @@ function getRefSerializer(queryRef, data, source) {
334
1043
  ...queryRef.dataConnect.getSettings()
335
1044
  }
336
1045
  },
337
- fetchTime: Date.now().toLocaleString(),
1046
+ fetchTime: Date.now().toLocaleString(), // TODO: Fix the fetch time here.
338
1047
  source
339
1048
  };
340
1049
  };
341
1050
  }
342
1051
  class QueryManager {
343
- constructor(transport) {
1052
+ constructor(transport, cache, dc) {
344
1053
  this.transport = transport;
345
- this._queries = new Map();
1054
+ this.cache = cache;
1055
+ this.dc = dc;
1056
+ this.callbacks = new Map();
1057
+ this.queue = [];
1058
+ }
1059
+ async waitForQueuedWrites() {
1060
+ for (const promise of this.queue) {
1061
+ await promise;
1062
+ }
1063
+ this.queue = [];
1064
+ }
1065
+ updateSSR(updatedData) {
1066
+ this.queue.push(this.updateCache(updatedData).then(async (result) => this.publishCacheResultsToSubscribers(result)));
346
1067
  }
347
- track(queryName, variables, initialCache) {
348
- const ref = {
349
- name: queryName,
350
- variables,
1068
+ async updateCache(result) {
1069
+ await this.waitForQueuedWrites();
1070
+ return this.cache.update(encoderImpl({
1071
+ name: result.ref.name,
1072
+ variables: result.ref.variables,
351
1073
  refType: QUERY_STR
352
- };
353
- const key = encoderImpl(ref);
354
- const newTrackedQuery = {
355
- ref,
356
- subscriptions: [],
357
- currentCache: initialCache || null,
358
- lastError: null
359
- };
360
- // @ts-ignore
361
- setIfNotExists(this._queries, key, newTrackedQuery);
362
- return this._queries.get(key);
1074
+ }), result.data);
363
1075
  }
364
1076
  addSubscription(queryRef, onResultCallback, onErrorCallback, initialCache) {
365
1077
  const key = encoderImpl({
@@ -367,98 +1079,119 @@ class QueryManager {
367
1079
  variables: queryRef.variables,
368
1080
  refType: QUERY_STR
369
1081
  });
370
- const trackedQuery = this._queries.get(key);
371
- const subscription = {
372
- userCallback: onResultCallback,
373
- errCallback: onErrorCallback
374
- };
375
1082
  const unsubscribe = () => {
376
- const trackedQuery = this._queries.get(key);
377
- trackedQuery.subscriptions = trackedQuery.subscriptions.filter(sub => sub !== subscription);
378
- };
379
- if (initialCache && trackedQuery.currentCache !== initialCache) {
380
- logDebug('Initial cache found. Comparing dates.');
381
- if (!trackedQuery.currentCache ||
382
- (trackedQuery.currentCache &&
383
- compareDates(trackedQuery.currentCache.fetchTime, initialCache.fetchTime))) {
384
- trackedQuery.currentCache = initialCache;
1083
+ if (this.callbacks.has(key)) {
1084
+ const callbackList = this.callbacks.get(key);
1085
+ this.callbacks.set(key, callbackList.filter(callback => callback.userCallback !== onResultCallback));
385
1086
  }
1087
+ };
1088
+ if (initialCache) {
1089
+ this.updateSSR(initialCache);
386
1090
  }
387
- if (trackedQuery.currentCache !== null) {
388
- const cachedData = trackedQuery.currentCache.data;
389
- onResultCallback({
390
- data: cachedData,
391
- source: SOURCE_CACHE,
392
- ref: queryRef,
393
- toJSON: getRefSerializer(queryRef, trackedQuery.currentCache.data, SOURCE_CACHE),
394
- fetchTime: trackedQuery.currentCache.fetchTime
395
- });
396
- if (trackedQuery.lastError !== null && onErrorCallback) {
397
- onErrorCallback(undefined);
398
- }
1091
+ logDebug(`Cache not available for query ${queryRef.name} with variables ${JSON.stringify(queryRef.variables)}. Calling executeQuery.`);
1092
+ const promise = this.executeQuery(queryRef);
1093
+ // We want to ignore the error and let subscriptions handle it
1094
+ promise.then(undefined, err => { });
1095
+ if (!this.callbacks.has(key)) {
1096
+ this.callbacks.set(key, []);
399
1097
  }
400
- trackedQuery.subscriptions.push({
1098
+ this.callbacks.get(key).push({
401
1099
  userCallback: onResultCallback,
402
1100
  errCallback: onErrorCallback,
403
1101
  unsubscribe
404
1102
  });
405
- if (!trackedQuery.currentCache) {
406
- logDebug(`No cache available for query ${queryRef.name} with variables ${JSON.stringify(queryRef.variables)}. Calling executeQuery.`);
407
- const promise = this.executeQuery(queryRef);
408
- // We want to ignore the error and let subscriptions handle it
409
- promise.then(undefined, err => { });
410
- }
411
1103
  return unsubscribe;
412
1104
  }
413
- executeQuery(queryRef) {
1105
+ async executeQuery(queryRef, options) {
1106
+ await this.waitForQueuedWrites();
414
1107
  if (queryRef.refType !== QUERY_STR) {
415
- throw new DataConnectError(Code.INVALID_ARGUMENT, `ExecuteQuery can only execute query operation`);
1108
+ throw new DataConnectError(Code.INVALID_ARGUMENT, `ExecuteQuery can only execute query operations`);
416
1109
  }
417
1110
  const key = encoderImpl({
418
1111
  name: queryRef.name,
419
1112
  variables: queryRef.variables,
420
1113
  refType: QUERY_STR
421
1114
  });
422
- const trackedQuery = this._queries.get(key);
423
- const result = this.transport.invokeQuery(queryRef.name, queryRef.variables);
424
- const newR = result.then(res => {
1115
+ if (options?.fetchPolicy !== QueryFetchPolicy.SERVER_ONLY &&
1116
+ (await this.cache.containsResultTree(key)) &&
1117
+ !(await this.cache.getResultTree(key)).isStale()) {
1118
+ const cacheResult = JSON.parse(await this.cache.getResultJSON(key));
1119
+ const resultTree = await this.cache.getResultTree(key);
1120
+ const result = {
1121
+ source: SOURCE_CACHE,
1122
+ ref: queryRef,
1123
+ data: cacheResult,
1124
+ toJSON: getRefSerializer(queryRef, cacheResult, SOURCE_CACHE),
1125
+ fetchTime: resultTree.cachedAt.toString()
1126
+ };
1127
+ (await this.cache.getResultTree(key)).updateAccessed();
1128
+ logDebug(`Cache found for query ${queryRef.name} with variables ${JSON.stringify(queryRef.variables)}. Calling executeQuery`);
1129
+ return result;
1130
+ }
1131
+ else {
1132
+ if (options?.fetchPolicy === QueryFetchPolicy.SERVER_ONLY) {
1133
+ logDebug(`Skipping cache for fetch policy "serverOnly"`);
1134
+ }
1135
+ else {
1136
+ logDebug(`No Cache found for query ${queryRef.name} with variables ${JSON.stringify(queryRef.variables)}. Calling executeQuery`);
1137
+ }
1138
+ }
1139
+ try {
1140
+ const res = await this.transport.invokeQuery(queryRef.name, queryRef.variables);
425
1141
  const fetchTime = new Date().toString();
426
1142
  const result = {
427
- ...res,
1143
+ data: res.data,
428
1144
  source: SOURCE_SERVER,
429
1145
  ref: queryRef,
430
1146
  toJSON: getRefSerializer(queryRef, res.data, SOURCE_SERVER),
431
1147
  fetchTime
432
1148
  };
433
- trackedQuery.subscriptions.forEach(subscription => {
434
- subscription.userCallback(result);
435
- });
436
- trackedQuery.currentCache = {
437
- data: res.data,
438
- source: SOURCE_CACHE,
439
- fetchTime
440
- };
1149
+ if (await this.cache.containsResultTree(key)) {
1150
+ (await this.cache.getResultTree(key)).updateAccessed();
1151
+ }
1152
+ const impactedQueries = await this.cache.update(key, result.data);
1153
+ await this.publishCacheResultsToSubscribers(impactedQueries);
441
1154
  return result;
442
- }, err => {
443
- trackedQuery.lastError = err;
444
- trackedQuery.subscriptions.forEach(subscription => {
1155
+ }
1156
+ catch (err) {
1157
+ this.callbacks.get(key)?.forEach(subscription => {
445
1158
  if (subscription.errCallback) {
446
1159
  subscription.errCallback(err);
447
1160
  }
448
1161
  });
449
1162
  throw err;
450
- });
451
- return newR;
1163
+ }
1164
+ }
1165
+ async publishCacheResultsToSubscribers(impactedQueries) {
1166
+ for (const query of impactedQueries) {
1167
+ const callbacks = this.callbacks.get(query);
1168
+ if (!callbacks) {
1169
+ continue;
1170
+ }
1171
+ const newJson = (await this.cache.getResultTree(query))
1172
+ .getRootStub()
1173
+ .toJson();
1174
+ const { name, variables } = decoderImpl(query);
1175
+ const queryRef = {
1176
+ dataConnect: this.dc,
1177
+ refType: QUERY_STR,
1178
+ name,
1179
+ variables
1180
+ };
1181
+ callbacks.forEach(callback => {
1182
+ callback.userCallback({
1183
+ data: newJson,
1184
+ fetchTime: new Date().toISOString(),
1185
+ source: SOURCE_CACHE,
1186
+ ref: queryRef
1187
+ });
1188
+ });
1189
+ }
452
1190
  }
453
1191
  enableEmulator(host, port) {
454
1192
  this.transport.useEmulator(host, port);
455
1193
  }
456
1194
  }
457
- function compareDates(str1, str2) {
458
- const date1 = new Date(str1);
459
- const date2 = new Date(str2);
460
- return date1.getTime() < date2.getTime();
461
- }
462
1195
 
463
1196
  /**
464
1197
  * @license
@@ -501,11 +1234,12 @@ const CallerSdkTypeEnum = {
501
1234
  * See the License for the specific language governing permissions and
502
1235
  * limitations under the License.
503
1236
  */
1237
+ const PROD_HOST = 'firebasedataconnect.googleapis.com';
504
1238
  function urlBuilder(projectConfig, transportOptions) {
505
1239
  const { connector, location, projectId: project, service } = projectConfig;
506
1240
  const { host, sslEnabled, port } = transportOptions;
507
1241
  const protocol = sslEnabled ? 'https' : 'http';
508
- const realHost = host || `firebasedataconnect.googleapis.com`;
1242
+ const realHost = host || PROD_HOST;
509
1243
  let baseUrl = `${protocol}://${realHost}`;
510
1244
  if (typeof port === 'number') {
511
1245
  baseUrl += `:${port}`;
@@ -585,7 +1319,7 @@ function dcFetch(url, body, { signal }, appId, accessToken, appCheckToken, _isUs
585
1319
  throw new DataConnectError(Code.OTHER, 'Failed to fetch: ' + JSON.stringify(err));
586
1320
  })
587
1321
  .then(async (response) => {
588
- let jsonResponse = null;
1322
+ let jsonResponse;
589
1323
  try {
590
1324
  jsonResponse = await response.json();
591
1325
  }
@@ -615,7 +1349,7 @@ function dcFetch(url, body, { signal }, appId, accessToken, appCheckToken, _isUs
615
1349
  });
616
1350
  }
617
1351
  function getMessage(obj) {
618
- if ('message' in obj) {
1352
+ if ('message' in obj && obj.message) {
619
1353
  return obj.message;
620
1354
  }
621
1355
  return JSON.stringify(obj);
@@ -731,7 +1465,10 @@ class RESTTransport {
731
1465
  async getWithAuth(forceToken = false) {
732
1466
  let starterPromise = new Promise(resolve => resolve(this._accessToken));
733
1467
  if (this.appCheckProvider) {
734
- this._appCheckToken = (await this.appCheckProvider.getToken())?.token;
1468
+ const appCheckToken = await this.appCheckProvider.getToken();
1469
+ if (appCheckToken) {
1470
+ this._appCheckToken = appCheckToken.token;
1471
+ }
735
1472
  }
736
1473
  if (this.authProvider) {
737
1474
  starterPromise = this.authProvider
@@ -934,20 +1671,27 @@ class DataConnect {
934
1671
  if (this._authProvider) {
935
1672
  this._authTokenProvider = new FirebaseAuthProvider(this.app.name, this.app.options, this._authProvider);
936
1673
  }
1674
+ const connectorConfig = {
1675
+ connector: this.dataConnectOptions.connector,
1676
+ service: this.dataConnectOptions.service,
1677
+ location: this.dataConnectOptions.location
1678
+ };
1679
+ this.cache = new DataConnectCache(this._authTokenProvider, this.app.options.projectId, connectorConfig, this._transportOptions?.host || PROD_HOST, this.dataConnectOptions.cacheSettings);
937
1680
  if (this._appCheckProvider) {
938
1681
  this._appCheckTokenProvider = new AppCheckTokenProvider(this.app, this._appCheckProvider);
939
1682
  }
940
- this._initialized = true;
941
1683
  this._transport = new this._transportClass(this.dataConnectOptions, this.app.options.apiKey, this.app.options.appId, this._authTokenProvider, this._appCheckTokenProvider, undefined, this._isUsingGeneratedSdk, this._callerSdkType);
942
1684
  if (this._transportOptions) {
943
1685
  this._transport.useEmulator(this._transportOptions.host, this._transportOptions.port, this._transportOptions.sslEnabled);
944
1686
  }
945
- this._queryManager = new QueryManager(this._transport);
1687
+ this._queryManager = new QueryManager(this._transport, this.cache, this);
946
1688
  this._mutationManager = new MutationManager(this._transport);
1689
+ this._initialized = true;
947
1690
  }
948
1691
  // @internal
949
1692
  enableEmulator(transportOptions) {
950
- if (this._initialized &&
1693
+ if (this._transportOptions &&
1694
+ this._initialized &&
951
1695
  !areTransportOptionsEqual(this._transportOptions, transportOptions)) {
952
1696
  logError('enableEmulator called after initialization');
953
1697
  throw new DataConnectError(Code.ALREADY_INITIALIZED, 'DataConnect instance already initialized!');
@@ -982,21 +1726,31 @@ function connectDataConnectEmulator(dc, host, port, sslEnabled = false) {
982
1726
  }
983
1727
  dc.enableEmulator({ host, port, sslEnabled });
984
1728
  }
985
- function getDataConnect(appOrOptions, optionalOptions) {
1729
+ function getDataConnect(appOrConnectorConfig, settingsOrConnectorConfig, settings // TODO: This doesn't serialize well because it has a function.
1730
+ ) {
986
1731
  let app$1;
987
- let dcOptions;
988
- if ('location' in appOrOptions) {
989
- dcOptions = appOrOptions;
1732
+ let connectorConfig;
1733
+ let realSettings;
1734
+ if ('location' in appOrConnectorConfig) {
1735
+ connectorConfig = appOrConnectorConfig;
990
1736
  app$1 = app.getApp();
1737
+ realSettings = settingsOrConnectorConfig;
991
1738
  }
992
1739
  else {
993
- dcOptions = optionalOptions;
994
- app$1 = appOrOptions;
1740
+ app$1 = appOrConnectorConfig;
1741
+ connectorConfig = settingsOrConnectorConfig;
1742
+ realSettings = settings;
995
1743
  }
996
1744
  if (!app$1 || Object.keys(app$1).length === 0) {
997
1745
  app$1 = app.getApp();
998
1746
  }
1747
+ const dcOptions = {
1748
+ ...realSettings,
1749
+ ...connectorConfig,
1750
+ projectId: app$1.options.projectId
1751
+ };
999
1752
  const provider = app._getProvider(app$1, 'data-connect');
1753
+ // TODO: Deal with the parsing of these options properly.
1000
1754
  const identifier = JSON.stringify(dcOptions);
1001
1755
  if (provider.isInitialized(identifier)) {
1002
1756
  const dcInstance = provider.getImmediate({ identifier });
@@ -1007,7 +1761,7 @@ function getDataConnect(appOrOptions, optionalOptions) {
1007
1761
  return dcInstance;
1008
1762
  }
1009
1763
  }
1010
- validateDCOptions(dcOptions);
1764
+ validateDCOptions(connectorConfig);
1011
1765
  logDebug('Creating new DataConnect instance');
1012
1766
  // Initialize with options.
1013
1767
  return provider.initialize({
@@ -1042,6 +1796,12 @@ function terminate(dataConnect) {
1042
1796
  return dataConnect._delete();
1043
1797
  // TODO(mtewani): Stop pending tasks
1044
1798
  }
1799
+ const StorageType = {
1800
+ MEMORY: 'MEMORY'
1801
+ };
1802
+ function makeMemoryCacheProvider() {
1803
+ return new MemoryStub();
1804
+ }
1045
1805
 
1046
1806
  /**
1047
1807
  * @license
@@ -1100,8 +1860,8 @@ function registerDataConnect(variant) {
1100
1860
  * @param queryRef query to execute.
1101
1861
  * @returns `QueryPromise`
1102
1862
  */
1103
- function executeQuery(queryRef) {
1104
- return queryRef.dataConnect._queryManager.executeQuery(queryRef);
1863
+ function executeQuery(queryRef, options) {
1864
+ return queryRef.dataConnect._queryManager.executeQuery(queryRef, options);
1105
1865
  }
1106
1866
  /**
1107
1867
  * Execute Query
@@ -1113,12 +1873,14 @@ function executeQuery(queryRef) {
1113
1873
  */
1114
1874
  function queryRef(dcInstance, queryName, variables, initialCache) {
1115
1875
  dcInstance.setInitialized();
1116
- dcInstance._queryManager.track(queryName, variables, initialCache);
1876
+ if (initialCache !== undefined) {
1877
+ dcInstance._queryManager.updateSSR(initialCache);
1878
+ }
1117
1879
  return {
1118
1880
  dataConnect: dcInstance,
1119
1881
  refType: QUERY_STR,
1120
1882
  name: queryName,
1121
- variables
1883
+ variables: variables
1122
1884
  };
1123
1885
  }
1124
1886
  /**
@@ -1176,7 +1938,7 @@ function validateArgs(connectorConfig, dcOrVars, vars, validateVars) {
1176
1938
 
1177
1939
  /**
1178
1940
  * @license
1179
- * Copyright 2024 Google LLC
1941
+ * Copyright 2025 Google LLC
1180
1942
  *
1181
1943
  * Licensed under the Apache License, Version 2.0 (the "License");
1182
1944
  * you may not use this file except in compliance with the License.
@@ -1241,16 +2003,21 @@ exports.Code = Code;
1241
2003
  exports.DataConnect = DataConnect;
1242
2004
  exports.DataConnectError = DataConnectError;
1243
2005
  exports.DataConnectOperationError = DataConnectOperationError;
2006
+ exports.IndexedDBStub = IndexedDBStub;
1244
2007
  exports.MUTATION_STR = MUTATION_STR;
1245
2008
  exports.MutationManager = MutationManager;
2009
+ exports.PersistentStub = MemoryStub;
1246
2010
  exports.QUERY_STR = QUERY_STR;
2011
+ exports.QueryFetchPolicy = QueryFetchPolicy;
1247
2012
  exports.SOURCE_CACHE = SOURCE_CACHE;
1248
2013
  exports.SOURCE_SERVER = SOURCE_SERVER;
2014
+ exports.StorageType = StorageType;
1249
2015
  exports.areTransportOptionsEqual = areTransportOptionsEqual;
1250
2016
  exports.connectDataConnectEmulator = connectDataConnectEmulator;
1251
2017
  exports.executeMutation = executeMutation;
1252
2018
  exports.executeQuery = executeQuery;
1253
2019
  exports.getDataConnect = getDataConnect;
2020
+ exports.makeMemoryCacheProvider = makeMemoryCacheProvider;
1254
2021
  exports.mutationRef = mutationRef;
1255
2022
  exports.parseOptions = parseOptions;
1256
2023
  exports.queryRef = queryRef;