@firebase/data-connect 0.3.12 → 0.4.0-20260224183151

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 +5 -5
  63. package/dist/node-esm/src/core/QueryManager.d.ts +0 -45
  64. package/dist/src/core/QueryManager.d.ts +0 -45
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.12";
11
+ const version = "0.4.0-20260224183151";
12
12
 
13
13
  /**
14
14
  * @license
@@ -36,6 +36,572 @@ 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
+ const Code = {
56
+ OTHER: 'other',
57
+ ALREADY_INITIALIZED: 'already-initialized',
58
+ NOT_INITIALIZED: 'not-initialized',
59
+ NOT_SUPPORTED: 'not-supported',
60
+ INVALID_ARGUMENT: 'invalid-argument',
61
+ PARTIAL_ERROR: 'partial-error',
62
+ UNAUTHORIZED: 'unauthorized'
63
+ };
64
+ /** An error returned by a DataConnect operation. */
65
+ class DataConnectError extends util.FirebaseError {
66
+ constructor(code, message) {
67
+ super(code, message);
68
+ /** @internal */
69
+ this.name = 'DataConnectError';
70
+ // Ensure the instanceof operator works as expected on subclasses of Error.
71
+ // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types
72
+ // and https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
73
+ Object.setPrototypeOf(this, DataConnectError.prototype);
74
+ }
75
+ /** @internal */
76
+ toString() {
77
+ return `${this.name}[code=${this.code}]: ${this.message}`;
78
+ }
79
+ }
80
+ /** An error returned by a DataConnect operation. */
81
+ class DataConnectOperationError extends DataConnectError {
82
+ /** @hideconstructor */
83
+ constructor(message, response) {
84
+ super(Code.PARTIAL_ERROR, message);
85
+ /** @internal */
86
+ this.name = 'DataConnectOperationError';
87
+ this.response = response;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * @license
93
+ * Copyright 2025 Google LLC
94
+ *
95
+ * Licensed under the Apache License, Version 2.0 (the "License");
96
+ * you may not use this file except in compliance with the License.
97
+ * You may obtain a copy of the License at
98
+ *
99
+ * http://www.apache.org/licenses/LICENSE-2.0
100
+ *
101
+ * Unless required by applicable law or agreed to in writing, software
102
+ * distributed under the License is distributed on an "AS IS" BASIS,
103
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
104
+ * See the License for the specific language governing permissions and
105
+ * limitations under the License.
106
+ */
107
+ class EntityDataObject {
108
+ getServerValue(key) {
109
+ return this.serverValues[key];
110
+ }
111
+ constructor(globalID) {
112
+ this.globalID = globalID;
113
+ this.serverValues = {};
114
+ this.referencedFrom = new Set();
115
+ }
116
+ getServerValues() {
117
+ return this.serverValues;
118
+ }
119
+ toJSON() {
120
+ return {
121
+ globalID: this.globalID,
122
+ map: this.serverValues,
123
+ referencedFrom: Array.from(this.referencedFrom)
124
+ };
125
+ }
126
+ static fromJSON(json) {
127
+ const edo = new EntityDataObject(json.globalID);
128
+ edo.serverValues = json.map;
129
+ edo.referencedFrom = new Set(json.referencedFrom);
130
+ return edo;
131
+ }
132
+ updateServerValue(key, value, requestedFrom) {
133
+ this.serverValues[key] = value;
134
+ this.referencedFrom.add(requestedFrom);
135
+ return Array.from(this.referencedFrom);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * @license
141
+ * Copyright 2025 Google LLC
142
+ *
143
+ * Licensed under the Apache License, Version 2.0 (the "License");
144
+ * you may not use this file except in compliance with the License.
145
+ * You may obtain a copy of the License at
146
+ *
147
+ * http://www.apache.org/licenses/LICENSE-2.0
148
+ *
149
+ * Unless required by applicable law or agreed to in writing, software
150
+ * distributed under the License is distributed on an "AS IS" BASIS,
151
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
152
+ * See the License for the specific language governing permissions and
153
+ * limitations under the License.
154
+ */
155
+ class InMemoryCacheProvider {
156
+ constructor(_keyId) {
157
+ this._keyId = _keyId;
158
+ this.edos = new Map();
159
+ this.resultTrees = new Map();
160
+ }
161
+ async setResultTree(queryId, rt) {
162
+ this.resultTrees.set(queryId, rt);
163
+ }
164
+ async getResultTree(queryId) {
165
+ return this.resultTrees.get(queryId);
166
+ }
167
+ async updateEntityData(entityData) {
168
+ this.edos.set(entityData.globalID, entityData);
169
+ }
170
+ async getEntityData(globalId) {
171
+ if (!this.edos.has(globalId)) {
172
+ this.edos.set(globalId, new EntityDataObject(globalId));
173
+ }
174
+ // Because of the above, we can guarantee that there will be an EDO at the globalId.
175
+ return this.edos.get(globalId);
176
+ }
177
+ close() {
178
+ // No-op
179
+ return Promise.resolve();
180
+ }
181
+ }
182
+
183
+ /**
184
+ * @license
185
+ * Copyright 2025 Google LLC
186
+ *
187
+ * Licensed under the Apache License, Version 2.0 (the "License");
188
+ * you may not use this file except in compliance with the License.
189
+ * You may obtain a copy of the License at
190
+ *
191
+ * http://www.apache.org/licenses/LICENSE-2.0
192
+ *
193
+ * Unless required by applicable law or agreed to in writing, software
194
+ * distributed under the License is distributed on an "AS IS" BASIS,
195
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
196
+ * See the License for the specific language governing permissions and
197
+ * limitations under the License.
198
+ */
199
+ const GLOBAL_ID_KEY = '_id';
200
+ const OBJECT_LISTS_KEY = '_objectLists';
201
+ const REFERENCES_KEY = '_references';
202
+ const SCALARS_KEY = '_scalars';
203
+ const ENTITY_DATA_KEYS_KEY = '_entity_data_keys';
204
+ class EntityNode {
205
+ constructor() {
206
+ this.scalars = {};
207
+ this.references = {};
208
+ this.objectLists = {};
209
+ this.entityDataKeys = new Set();
210
+ }
211
+ async loadData(queryId, values, entityIds, acc, cacheProvider) {
212
+ if (values === undefined) {
213
+ return;
214
+ }
215
+ if (typeof values !== 'object' || Array.isArray(values)) {
216
+ throw new DataConnectError(Code.INVALID_ARGUMENT, 'EntityNode initialized with non-object value');
217
+ }
218
+ if (values === null) {
219
+ return;
220
+ }
221
+ if (typeof values === 'object' &&
222
+ entityIds &&
223
+ entityIds[GLOBAL_ID_KEY] &&
224
+ typeof entityIds[GLOBAL_ID_KEY] === 'string') {
225
+ this.globalId = entityIds[GLOBAL_ID_KEY];
226
+ this.entityData = await cacheProvider.getEntityData(this.globalId);
227
+ }
228
+ for (const key in values) {
229
+ if (values.hasOwnProperty(key)) {
230
+ if (typeof values[key] === 'object') {
231
+ if (Array.isArray(values[key])) {
232
+ const ids = entityIds && entityIds[key];
233
+ const objArray = [];
234
+ const scalarArray = [];
235
+ for (const [index, value] of values[key].entries()) {
236
+ if (typeof value === 'object') {
237
+ if (Array.isArray(value)) ;
238
+ else {
239
+ const entityNode = new EntityNode();
240
+ await entityNode.loadData(queryId, value, ids && ids[index], acc, cacheProvider);
241
+ objArray.push(entityNode);
242
+ }
243
+ }
244
+ else {
245
+ scalarArray.push(value);
246
+ }
247
+ }
248
+ if (scalarArray.length > 0 && objArray.length > 0) {
249
+ this.scalars[key] = values[key];
250
+ }
251
+ else if (scalarArray.length > 0) {
252
+ if (this.entityData) {
253
+ const impactedRefs = this.entityData.updateServerValue(key, scalarArray, queryId);
254
+ this.entityDataKeys.add(key);
255
+ acc.add(impactedRefs);
256
+ }
257
+ else {
258
+ this.scalars[key] = scalarArray;
259
+ }
260
+ }
261
+ else if (objArray.length > 0) {
262
+ this.objectLists[key] = objArray;
263
+ }
264
+ else {
265
+ this.scalars[key] = [];
266
+ }
267
+ }
268
+ else {
269
+ if (values[key] === null) {
270
+ this.scalars[key] = null;
271
+ continue;
272
+ }
273
+ const entityNode = new EntityNode();
274
+ // TODO: Load Data might need to be pushed into ResultTreeProcessor instead.
275
+ await entityNode.loadData(queryId, values[key], entityIds && entityIds[key], acc, cacheProvider);
276
+ this.references[key] = entityNode;
277
+ }
278
+ }
279
+ else {
280
+ if (this.entityData) {
281
+ const impactedRefs = this.entityData.updateServerValue(key, values[key], queryId);
282
+ this.entityDataKeys.add(key);
283
+ acc.add(impactedRefs);
284
+ }
285
+ else {
286
+ this.scalars[key] = values[key];
287
+ }
288
+ }
289
+ }
290
+ }
291
+ if (this.entityData) {
292
+ await cacheProvider.updateEntityData(this.entityData);
293
+ }
294
+ }
295
+ toJSON(mode) {
296
+ const resultObject = {};
297
+ if (mode === EncodingMode.hydrated) {
298
+ if (this.entityData) {
299
+ for (const key of this.entityDataKeys) {
300
+ resultObject[key] = this.entityData.getServerValue(key);
301
+ }
302
+ }
303
+ if (this.scalars) {
304
+ Object.assign(resultObject, this.scalars);
305
+ }
306
+ if (this.references) {
307
+ for (const key in this.references) {
308
+ if (this.references.hasOwnProperty(key)) {
309
+ resultObject[key] = this.references[key].toJSON(mode);
310
+ }
311
+ }
312
+ }
313
+ if (this.objectLists) {
314
+ for (const key in this.objectLists) {
315
+ if (this.objectLists.hasOwnProperty(key)) {
316
+ resultObject[key] = this.objectLists[key].map(obj => obj.toJSON(mode));
317
+ }
318
+ }
319
+ }
320
+ return resultObject;
321
+ }
322
+ else {
323
+ // Get JSON representation of dehydrated list
324
+ if (this.entityData) {
325
+ resultObject[GLOBAL_ID_KEY] = this.entityData.globalID;
326
+ }
327
+ resultObject[ENTITY_DATA_KEYS_KEY] = Array.from(this.entityDataKeys);
328
+ if (this.scalars) {
329
+ resultObject[SCALARS_KEY] = this.scalars;
330
+ }
331
+ if (this.references) {
332
+ const references = {};
333
+ for (const key in this.references) {
334
+ if (this.references.hasOwnProperty(key)) {
335
+ references[key] = this.references[key].toJSON(mode);
336
+ }
337
+ }
338
+ resultObject[REFERENCES_KEY] = references;
339
+ }
340
+ if (this.objectLists) {
341
+ const objectLists = {};
342
+ for (const key in this.objectLists) {
343
+ if (this.objectLists.hasOwnProperty(key)) {
344
+ objectLists[key] = this.objectLists[key].map(obj => obj.toJSON(mode));
345
+ }
346
+ }
347
+ resultObject[OBJECT_LISTS_KEY] = objectLists;
348
+ }
349
+ }
350
+ return resultObject;
351
+ }
352
+ static fromJson(obj) {
353
+ const sdo = new EntityNode();
354
+ if (obj.backingData) {
355
+ sdo.entityData = EntityDataObject.fromJSON(obj.backingData);
356
+ }
357
+ sdo.globalId = obj.globalID;
358
+ sdo.scalars = obj.scalars;
359
+ if (obj.references) {
360
+ const references = {};
361
+ for (const key in obj.references) {
362
+ if (obj.references.hasOwnProperty(key)) {
363
+ references[key] = EntityNode.fromJson(obj.references[key]);
364
+ }
365
+ }
366
+ sdo.references = references;
367
+ }
368
+ if (obj.objectLists) {
369
+ const objectLists = {};
370
+ for (const key in obj.objectLists) {
371
+ if (obj.objectLists.hasOwnProperty(key)) {
372
+ objectLists[key] = obj.objectLists[key].map(obj => EntityNode.fromJson(obj));
373
+ }
374
+ }
375
+ sdo.objectLists = objectLists;
376
+ }
377
+ return sdo;
378
+ }
379
+ }
380
+ // Helpful for storing in persistent cache, which is not available yet.
381
+ var EncodingMode;
382
+ (function (EncodingMode) {
383
+ EncodingMode[EncodingMode["hydrated"] = 0] = "hydrated";
384
+ EncodingMode[EncodingMode["dehydrated"] = 1] = "dehydrated";
385
+ })(EncodingMode || (EncodingMode = {}));
386
+
387
+ /**
388
+ * @license
389
+ * Copyright 2025 Google LLC
390
+ *
391
+ * Licensed under the Apache License, Version 2.0 (the "License");
392
+ * you may not use this file except in compliance with the License.
393
+ * You may obtain a copy of the License at
394
+ *
395
+ * http://www.apache.org/licenses/LICENSE-2.0
396
+ *
397
+ * Unless required by applicable law or agreed to in writing, software
398
+ * distributed under the License is distributed on an "AS IS" BASIS,
399
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
400
+ * See the License for the specific language governing permissions and
401
+ * limitations under the License.
402
+ */
403
+ class ResultTree {
404
+ /**
405
+ * Create a {@link ResultTree} from a dehydrated JSON object.
406
+ * @param value The dehydrated JSON object.
407
+ * @returns The {@link ResultTree}.
408
+ */
409
+ static fromJson(value) {
410
+ return new ResultTree(EntityNode.fromJson(value.rootStub), value.maxAge, value.cachedAt, value.lastAccessed);
411
+ }
412
+ constructor(rootStub, maxAge = 0, cachedAt, _lastAccessed) {
413
+ this.rootStub = rootStub;
414
+ this.maxAge = maxAge;
415
+ this.cachedAt = cachedAt;
416
+ this._lastAccessed = _lastAccessed;
417
+ }
418
+ isStale() {
419
+ return (Date.now() - new Date(this.cachedAt.getTime()).getTime() >
420
+ this.maxAge * 1000);
421
+ }
422
+ updateMaxAge(maxAgeInSeconds) {
423
+ this.maxAge = maxAgeInSeconds;
424
+ }
425
+ updateAccessed() {
426
+ this._lastAccessed = new Date();
427
+ }
428
+ get lastAccessed() {
429
+ return this._lastAccessed;
430
+ }
431
+ getRootStub() {
432
+ return this.rootStub;
433
+ }
434
+ }
435
+
436
+ /**
437
+ * @license
438
+ * Copyright 2025 Google LLC
439
+ *
440
+ * Licensed under the Apache License, Version 2.0 (the "License");
441
+ * you may not use this file except in compliance with the License.
442
+ * You may obtain a copy of the License at
443
+ *
444
+ * http://www.apache.org/licenses/LICENSE-2.0
445
+ *
446
+ * Unless required by applicable law or agreed to in writing, software
447
+ * distributed under the License is distributed on an "AS IS" BASIS,
448
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
449
+ * See the License for the specific language governing permissions and
450
+ * limitations under the License.
451
+ */
452
+ class ImpactedQueryRefsAccumulator {
453
+ constructor(queryId) {
454
+ this.queryId = queryId;
455
+ this.impacted = new Set();
456
+ }
457
+ add(impacted) {
458
+ impacted
459
+ .filter(ref => ref !== this.queryId)
460
+ .forEach(ref => this.impacted.add(ref));
461
+ }
462
+ consumeEvents() {
463
+ const events = Array.from(this.impacted);
464
+ this.impacted.clear();
465
+ return events;
466
+ }
467
+ }
468
+
469
+ /**
470
+ * @license
471
+ * Copyright 2025 Google LLC
472
+ *
473
+ * Licensed under the Apache License, Version 2.0 (the "License");
474
+ * you may not use this file except in compliance with the License.
475
+ * You may obtain a copy of the License at
476
+ *
477
+ * http://www.apache.org/licenses/LICENSE-2.0
478
+ *
479
+ * Unless required by applicable law or agreed to in writing, software
480
+ * distributed under the License is distributed on an "AS IS" BASIS,
481
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
482
+ * See the License for the specific language governing permissions and
483
+ * limitations under the License.
484
+ */
485
+ class ResultTreeProcessor {
486
+ /**
487
+ * Hydrate the EntityNode into a JSON object so that it can be returned to the user.
488
+ * @param rootStubObject
489
+ * @returns {string}
490
+ */
491
+ hydrateResults(rootStubObject) {
492
+ return rootStubObject.toJSON(EncodingMode.hydrated);
493
+ }
494
+ /**
495
+ * Dehydrate results so that they can be stored in the cache.
496
+ * @param json
497
+ * @param entityIds
498
+ * @param cacheProvider
499
+ * @param queryId
500
+ * @returns {Promise<DehydratedResults>}
501
+ */
502
+ async dehydrateResults(json, entityIds, cacheProvider, queryId) {
503
+ const acc = new ImpactedQueryRefsAccumulator(queryId);
504
+ const entityNode = new EntityNode();
505
+ await entityNode.loadData(queryId, json, entityIds, acc, cacheProvider);
506
+ return {
507
+ entityNode,
508
+ impacted: acc.consumeEvents()
509
+ };
510
+ }
511
+ }
512
+
513
+ /**
514
+ * @license
515
+ * Copyright 2025 Google LLC
516
+ *
517
+ * Licensed under the Apache License, Version 2.0 (the "License");
518
+ * you may not use this file except in compliance with the License.
519
+ * You may obtain a copy of the License at
520
+ *
521
+ * http://www.apache.org/licenses/LICENSE-2.0
522
+ *
523
+ * Unless required by applicable law or agreed to in writing, software
524
+ * distributed under the License is distributed on an "AS IS" BASIS,
525
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
526
+ * See the License for the specific language governing permissions and
527
+ * limitations under the License.
528
+ */
529
+ class DataConnectCache {
530
+ constructor(authProvider, projectId, connectorConfig, host, cacheSettings) {
531
+ this.authProvider = authProvider;
532
+ this.projectId = projectId;
533
+ this.connectorConfig = connectorConfig;
534
+ this.host = host;
535
+ this.cacheSettings = cacheSettings;
536
+ this.cacheProvider = null;
537
+ this.uid = null;
538
+ this.authProvider.addTokenChangeListener(async (_) => {
539
+ const newUid = this.authProvider.getAuth().getUid();
540
+ // We should only close if the token changes and so does the new UID
541
+ if (this.uid !== newUid) {
542
+ this.cacheProvider?.close();
543
+ this.uid = newUid;
544
+ const identifier = await this.getIdentifier(this.uid);
545
+ this.cacheProvider = this.initializeNewProviders(identifier);
546
+ }
547
+ });
548
+ }
549
+ async initialize() {
550
+ if (!this.cacheProvider) {
551
+ const identifier = await this.getIdentifier(this.uid);
552
+ this.cacheProvider = this.initializeNewProviders(identifier);
553
+ }
554
+ }
555
+ async getIdentifier(uid) {
556
+ const identifier = `${'memory' // TODO: replace this with indexeddb when persistence is available.
557
+ }-${this.projectId}-${this.connectorConfig.service}-${this.connectorConfig.connector}-${this.connectorConfig.location}-${uid}-${this.host}`;
558
+ const sha256 = await util.generateSHA256Hash(identifier);
559
+ return sha256;
560
+ }
561
+ initializeNewProviders(identifier) {
562
+ return this.cacheSettings.cacheProvider.initialize(identifier);
563
+ }
564
+ async containsResultTree(queryId) {
565
+ await this.initialize();
566
+ const resultTree = await this.cacheProvider.getResultTree(queryId);
567
+ return resultTree !== undefined;
568
+ }
569
+ async getResultTree(queryId) {
570
+ await this.initialize();
571
+ return this.cacheProvider.getResultTree(queryId);
572
+ }
573
+ async getResultJSON(queryId) {
574
+ await this.initialize();
575
+ const processor = new ResultTreeProcessor();
576
+ const cacheProvider = this.cacheProvider;
577
+ const resultTree = await cacheProvider.getResultTree(queryId);
578
+ if (!resultTree) {
579
+ throw new DataConnectError(Code.INVALID_ARGUMENT, `${queryId} not found in cache. Call "update()" first.`);
580
+ }
581
+ return processor.hydrateResults(resultTree.getRootStub());
582
+ }
583
+ async update(queryId, serverValues, entityIds) {
584
+ await this.initialize();
585
+ const processor = new ResultTreeProcessor();
586
+ const cacheProvider = this.cacheProvider;
587
+ const { entityNode: stubDataObject, impacted } = await processor.dehydrateResults(serverValues, entityIds, cacheProvider, queryId);
588
+ const now = new Date();
589
+ await cacheProvider.setResultTree(queryId, new ResultTree(stubDataObject, serverValues.maxAge || this.cacheSettings.maxAgeSeconds, now, now));
590
+ return impacted;
591
+ }
592
+ }
593
+ class MemoryStub {
594
+ constructor() {
595
+ this.type = 'MEMORY';
596
+ }
597
+ /**
598
+ * @internal
599
+ */
600
+ initialize(cacheId) {
601
+ return new InMemoryCacheProvider(cacheId);
602
+ }
603
+ }
604
+
39
605
  /**
40
606
  * @license
41
607
  * Copyright 2024 Google LLC
@@ -99,58 +665,6 @@ class AppCheckTokenProvider {
99
665
  }
100
666
  }
101
667
 
102
- /**
103
- * @license
104
- * Copyright 2024 Google LLC
105
- *
106
- * Licensed under the Apache License, Version 2.0 (the "License");
107
- * you may not use this file except in compliance with the License.
108
- * You may obtain a copy of the License at
109
- *
110
- * http://www.apache.org/licenses/LICENSE-2.0
111
- *
112
- * Unless required by applicable law or agreed to in writing, software
113
- * distributed under the License is distributed on an "AS IS" BASIS,
114
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
115
- * See the License for the specific language governing permissions and
116
- * limitations under the License.
117
- */
118
- const Code = {
119
- OTHER: 'other',
120
- ALREADY_INITIALIZED: 'already-initialized',
121
- NOT_INITIALIZED: 'not-initialized',
122
- NOT_SUPPORTED: 'not-supported',
123
- INVALID_ARGUMENT: 'invalid-argument',
124
- PARTIAL_ERROR: 'partial-error',
125
- UNAUTHORIZED: 'unauthorized'
126
- };
127
- /** An error returned by a DataConnect operation. */
128
- class DataConnectError extends util.FirebaseError {
129
- constructor(code, message) {
130
- super(code, message);
131
- /** @internal */
132
- this.name = 'DataConnectError';
133
- // Ensure the instanceof operator works as expected on subclasses of Error.
134
- // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types
135
- // and https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
136
- Object.setPrototypeOf(this, DataConnectError.prototype);
137
- }
138
- /** @internal */
139
- toString() {
140
- return `${this.name}[code=${this.code}]: ${this.message}`;
141
- }
142
- }
143
- /** An error returned by a DataConnect operation. */
144
- class DataConnectOperationError extends DataConnectError {
145
- /** @hideconstructor */
146
- constructor(message, response) {
147
- super(Code.PARTIAL_ERROR, message);
148
- /** @internal */
149
- this.name = 'DataConnectOperationError';
150
- this.response = response;
151
- }
152
- }
153
-
154
668
  /**
155
669
  * @license
156
670
  * Copyright 2024 Google LLC
@@ -205,6 +719,9 @@ class FirebaseAuthProvider {
205
719
  _authProvider.onInit(auth => (this._auth = auth));
206
720
  }
207
721
  }
722
+ getAuth() {
723
+ return this._auth;
724
+ }
208
725
  getToken(forceRefresh) {
209
726
  if (!this._auth) {
210
727
  return new Promise((resolve, reject) => {
@@ -264,7 +781,7 @@ const SOURCE_CACHE = 'CACHE';
264
781
 
265
782
  /**
266
783
  * @license
267
- * Copyright 2024 Google LLC
784
+ * Copyright 2026 Google LLC
268
785
  *
269
786
  * Licensed under the Apache License, Version 2.0 (the "License");
270
787
  * you may not use this file except in compliance with the License.
@@ -278,11 +795,43 @@ const SOURCE_CACHE = 'CACHE';
278
795
  * See the License for the specific language governing permissions and
279
796
  * limitations under the License.
280
797
  */
281
- let encoderImpl;
282
- function setEncoder(encoder) {
283
- encoderImpl = encoder;
798
+ function parseEntityIds(result) {
799
+ // Iterate through extensions.dataConnect
800
+ const dataConnectExtensions = result.extensions?.dataConnect;
801
+ const dataCopy = Object.assign(result);
802
+ if (!dataConnectExtensions) {
803
+ return dataCopy;
804
+ }
805
+ const ret = {};
806
+ for (const extension of dataConnectExtensions) {
807
+ const { path } = extension;
808
+ populatePath(path, ret, extension);
809
+ }
810
+ return ret;
811
+ }
812
+ // mutates the object to update the path
813
+ function populatePath(path, toUpdate, extension) {
814
+ let curObj = toUpdate;
815
+ for (const slice of path) {
816
+ if (typeof curObj[slice] !== 'object') {
817
+ curObj[slice] = {};
818
+ }
819
+ curObj = curObj[slice];
820
+ }
821
+ if ('entityId' in extension && extension.entityId) {
822
+ curObj['_id'] = extension.entityId;
823
+ }
824
+ else if ('entityIds' in extension) {
825
+ const entityArr = extension.entityIds;
826
+ for (let i = 0; i < entityArr.length; i++) {
827
+ const entityId = entityArr[i];
828
+ if (typeof curObj[i] === 'undefined') {
829
+ curObj[i] = {};
830
+ }
831
+ curObj[i]._id = entityId;
832
+ }
833
+ }
284
834
  }
285
- setEncoder(o => JSON.stringify(o));
286
835
 
287
836
  /**
288
837
  * @license
@@ -300,11 +849,24 @@ setEncoder(o => JSON.stringify(o));
300
849
  * See the License for the specific language governing permissions and
301
850
  * limitations under the License.
302
851
  */
303
- function setIfNotExists(map, key, val) {
304
- if (!map.has(key)) {
305
- map.set(key, val);
306
- }
852
+ let encoderImpl;
853
+ let decoderImpl;
854
+ function setEncoder(encoder) {
855
+ encoderImpl = encoder;
856
+ }
857
+ function setDecoder(decoder) {
858
+ decoderImpl = decoder;
307
859
  }
860
+ function sortKeysForObj(o) {
861
+ return Object.keys(o)
862
+ .sort()
863
+ .reduce((accumulator, currentKey) => {
864
+ accumulator[currentKey] = o[currentKey];
865
+ return accumulator;
866
+ }, {});
867
+ }
868
+ setEncoder((o) => JSON.stringify(sortKeysForObj(o)));
869
+ setDecoder(s => sortKeysForObj(JSON.parse(s)));
308
870
 
309
871
  /**
310
872
  * @license
@@ -322,7 +884,7 @@ function setIfNotExists(map, key, val) {
322
884
  * See the License for the specific language governing permissions and
323
885
  * limitations under the License.
324
886
  */
325
- function getRefSerializer(queryRef, data, source) {
887
+ function getRefSerializer(queryRef, data, source, fetchTime) {
326
888
  return function toJSON() {
327
889
  return {
328
890
  data,
@@ -334,32 +896,65 @@ function getRefSerializer(queryRef, data, source) {
334
896
  ...queryRef.dataConnect.getSettings()
335
897
  }
336
898
  },
337
- fetchTime: Date.now().toLocaleString(),
899
+ fetchTime,
338
900
  source
339
901
  };
340
902
  };
341
903
  }
342
904
  class QueryManager {
343
- constructor(transport) {
905
+ async preferCacheResults(queryRef, allowStale = false) {
906
+ let cacheResult;
907
+ try {
908
+ cacheResult = await this.fetchCacheResults(queryRef, allowStale);
909
+ }
910
+ catch (e) {
911
+ // Ignore the error and try to fetch from the server.
912
+ }
913
+ if (cacheResult) {
914
+ return cacheResult;
915
+ }
916
+ return this.fetchServerResults(queryRef);
917
+ }
918
+ constructor(transport, dc, cache) {
344
919
  this.transport = transport;
345
- this._queries = new Map();
920
+ this.dc = dc;
921
+ this.cache = cache;
922
+ this.callbacks = new Map();
923
+ this.subscriptionCache = new Map();
924
+ this.queue = [];
346
925
  }
347
- track(queryName, variables, initialCache) {
348
- const ref = {
349
- name: queryName,
350
- variables,
351
- 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);
926
+ async waitForQueuedWrites() {
927
+ for (const promise of this.queue) {
928
+ await promise;
929
+ }
930
+ this.queue = [];
931
+ }
932
+ updateSSR(updatedData) {
933
+ this.queue.push(this.updateCache(updatedData).then(async (result) => this.publishCacheResultsToSubscribers(result, updatedData.fetchTime)));
934
+ }
935
+ async updateCache(result, extensions) {
936
+ await this.waitForQueuedWrites();
937
+ if (this.cache) {
938
+ const entityIds = parseEntityIds(result);
939
+ const updatedMaxAge = getMaxAgeFromExtensions(extensions);
940
+ if (updatedMaxAge !== undefined) {
941
+ this.cache.cacheSettings.maxAgeSeconds = updatedMaxAge;
942
+ }
943
+ return this.cache.update(encoderImpl({
944
+ name: result.ref.name,
945
+ variables: result.ref.variables,
946
+ refType: QUERY_STR
947
+ }), result.data, entityIds);
948
+ }
949
+ else {
950
+ const key = encoderImpl({
951
+ name: result.ref.name,
952
+ variables: result.ref.variables,
953
+ refType: QUERY_STR
954
+ });
955
+ this.subscriptionCache.set(key, result);
956
+ return [key];
957
+ }
363
958
  }
364
959
  addSubscription(queryRef, onResultCallback, onCompleteCallback, onErrorCallback, initialCache) {
365
960
  const key = encoderImpl({
@@ -367,99 +962,213 @@ class QueryManager {
367
962
  variables: queryRef.variables,
368
963
  refType: QUERY_STR
369
964
  });
370
- const trackedQuery = this._queries.get(key);
371
- const subscription = {
372
- userCallback: onResultCallback,
373
- onCompleteCallback,
374
- errCallback: onErrorCallback
375
- };
376
965
  const unsubscribe = () => {
377
- const trackedQuery = this._queries.get(key);
378
- trackedQuery.subscriptions = trackedQuery.subscriptions.filter(sub => sub !== subscription);
379
- onCompleteCallback?.();
380
- };
381
- if (initialCache && trackedQuery.currentCache !== initialCache) {
382
- logDebug('Initial cache found. Comparing dates.');
383
- if (!trackedQuery.currentCache ||
384
- (trackedQuery.currentCache &&
385
- compareDates(trackedQuery.currentCache.fetchTime, initialCache.fetchTime))) {
386
- trackedQuery.currentCache = initialCache;
966
+ if (this.callbacks.has(key)) {
967
+ const callbackList = this.callbacks.get(key);
968
+ this.callbacks.set(key, callbackList.filter(callback => callback !== subscription));
969
+ onCompleteCallback?.();
387
970
  }
388
- }
389
- if (trackedQuery.currentCache !== null) {
390
- const cachedData = trackedQuery.currentCache.data;
391
- onResultCallback({
392
- data: cachedData,
393
- source: SOURCE_CACHE,
394
- ref: queryRef,
395
- toJSON: getRefSerializer(queryRef, trackedQuery.currentCache.data, SOURCE_CACHE),
396
- fetchTime: trackedQuery.currentCache.fetchTime
397
- });
398
- if (trackedQuery.lastError !== null && onErrorCallback) {
399
- onErrorCallback(undefined);
400
- }
401
- }
402
- trackedQuery.subscriptions.push({
971
+ };
972
+ const subscription = {
403
973
  userCallback: onResultCallback,
404
974
  errCallback: onErrorCallback,
405
975
  unsubscribe
406
- });
407
- if (!trackedQuery.currentCache) {
408
- logDebug(`No cache available for query ${queryRef.name} with variables ${JSON.stringify(queryRef.variables)}. Calling executeQuery.`);
409
- const promise = this.executeQuery(queryRef);
410
- // We want to ignore the error and let subscriptions handle it
411
- promise.then(undefined, err => { });
976
+ };
977
+ if (initialCache) {
978
+ this.updateSSR(initialCache);
979
+ }
980
+ const promise = this.preferCacheResults(queryRef, /*allowStale=*/ true);
981
+ // We want to ignore the error and let subscriptions handle it
982
+ promise.then(undefined, err => { });
983
+ if (!this.callbacks.has(key)) {
984
+ this.callbacks.set(key, []);
412
985
  }
986
+ this.callbacks
987
+ .get(key)
988
+ .push(subscription);
413
989
  return unsubscribe;
414
990
  }
415
- executeQuery(queryRef) {
416
- if (queryRef.refType !== QUERY_STR) {
417
- throw new DataConnectError(Code.INVALID_ARGUMENT, `ExecuteQuery can only execute query operation`);
418
- }
991
+ async fetchServerResults(queryRef) {
992
+ await this.waitForQueuedWrites();
419
993
  const key = encoderImpl({
420
994
  name: queryRef.name,
421
995
  variables: queryRef.variables,
422
996
  refType: QUERY_STR
423
997
  });
424
- const trackedQuery = this._queries.get(key);
425
- const result = this.transport.invokeQuery(queryRef.name, queryRef.variables);
426
- const newR = result.then(res => {
427
- const fetchTime = new Date().toString();
428
- const result = {
429
- ...res,
430
- source: SOURCE_SERVER,
998
+ try {
999
+ const result = await this.transport.invokeQuery(queryRef.name, queryRef.variables);
1000
+ const fetchTime = Date.now().toString();
1001
+ const originalExtensions = result.extensions;
1002
+ const queryResult = {
1003
+ ...result,
431
1004
  ref: queryRef,
432
- toJSON: getRefSerializer(queryRef, res.data, SOURCE_SERVER),
433
- fetchTime
1005
+ source: SOURCE_SERVER,
1006
+ fetchTime,
1007
+ data: result.data,
1008
+ extensions: getDataConnectExtensionsWithoutMaxAge(originalExtensions),
1009
+ toJSON: getRefSerializer(queryRef, result.data, SOURCE_SERVER, fetchTime)
434
1010
  };
435
- trackedQuery.subscriptions.forEach(subscription => {
436
- subscription.userCallback(result);
1011
+ let updatedKeys = [];
1012
+ updatedKeys = await this.updateCache(queryResult, originalExtensions?.dataConnect);
1013
+ this.publishDataToSubscribers(key, queryResult);
1014
+ if (this.cache) {
1015
+ await this.publishCacheResultsToSubscribers(updatedKeys, fetchTime);
1016
+ }
1017
+ else {
1018
+ this.subscriptionCache.set(key, queryResult);
1019
+ }
1020
+ return queryResult;
1021
+ }
1022
+ catch (e) {
1023
+ this.publishErrorToSubscribers(key, e);
1024
+ throw e;
1025
+ }
1026
+ }
1027
+ async fetchCacheResults(queryRef, allowStale = false) {
1028
+ await this.waitForQueuedWrites();
1029
+ let result;
1030
+ if (!this.cache) {
1031
+ result = await this.getFromSubscriberCache(queryRef);
1032
+ }
1033
+ else {
1034
+ result = await this.getFromResultTreeCache(queryRef, allowStale);
1035
+ }
1036
+ if (!result) {
1037
+ throw new DataConnectError(Code.OTHER, 'No cache entry found for query: ' + queryRef.name);
1038
+ }
1039
+ const fetchTime = Date.now().toString();
1040
+ const queryResult = {
1041
+ ...result,
1042
+ ref: queryRef,
1043
+ source: SOURCE_CACHE,
1044
+ fetchTime,
1045
+ data: result.data,
1046
+ extensions: result.extensions,
1047
+ toJSON: getRefSerializer(queryRef, result.data, SOURCE_CACHE, fetchTime)
1048
+ };
1049
+ if (this.cache) {
1050
+ const key = encoderImpl({
1051
+ name: queryRef.name,
1052
+ variables: queryRef.variables,
1053
+ refType: QUERY_STR
437
1054
  });
438
- trackedQuery.currentCache = {
439
- data: res.data,
440
- source: SOURCE_CACHE,
441
- fetchTime
442
- };
443
- return result;
444
- }, err => {
445
- trackedQuery.lastError = err;
446
- trackedQuery.subscriptions.forEach(subscription => {
447
- if (subscription.errCallback) {
448
- subscription.errCallback(err);
449
- }
1055
+ await this.publishCacheResultsToSubscribers([key], fetchTime);
1056
+ }
1057
+ else {
1058
+ const key = encoderImpl({
1059
+ name: queryRef.name,
1060
+ variables: queryRef.variables,
1061
+ refType: QUERY_STR
450
1062
  });
451
- throw err;
1063
+ this.subscriptionCache.set(key, queryResult);
1064
+ this.publishDataToSubscribers(key, queryResult);
1065
+ }
1066
+ return queryResult;
1067
+ }
1068
+ publishErrorToSubscribers(key, err) {
1069
+ this.callbacks.get(key)?.forEach(subscription => {
1070
+ if (subscription.errCallback) {
1071
+ subscription.errCallback(err);
1072
+ }
1073
+ });
1074
+ }
1075
+ async getFromResultTreeCache(queryRef, allowStale = false) {
1076
+ const key = encoderImpl({
1077
+ name: queryRef.name,
1078
+ variables: queryRef.variables,
1079
+ refType: QUERY_STR
1080
+ });
1081
+ if (!this.cache || !(await this.cache.containsResultTree(key))) {
1082
+ return null;
1083
+ }
1084
+ const cacheResult = (await this.cache.getResultJSON(key));
1085
+ const resultTree = await this.cache.getResultTree(key);
1086
+ if (!allowStale && resultTree.isStale()) {
1087
+ return null;
1088
+ }
1089
+ const result = {
1090
+ source: SOURCE_CACHE,
1091
+ ref: queryRef,
1092
+ data: cacheResult,
1093
+ toJSON: getRefSerializer(queryRef, cacheResult, SOURCE_CACHE, resultTree.cachedAt.toString()),
1094
+ fetchTime: resultTree.cachedAt.toString()
1095
+ };
1096
+ (await this.cache.getResultTree(key)).updateAccessed();
1097
+ return result;
1098
+ }
1099
+ async getFromSubscriberCache(queryRef) {
1100
+ const key = encoderImpl({
1101
+ name: queryRef.name,
1102
+ variables: queryRef.variables,
1103
+ refType: QUERY_STR
1104
+ });
1105
+ if (!this.subscriptionCache.has(key)) {
1106
+ return;
1107
+ }
1108
+ const result = this.subscriptionCache.get(key);
1109
+ result.source = SOURCE_CACHE;
1110
+ result.toJSON = getRefSerializer(result.ref, result.data, SOURCE_CACHE, result.fetchTime);
1111
+ return result;
1112
+ }
1113
+ publishDataToSubscribers(key, queryResult) {
1114
+ if (!this.callbacks.has(key)) {
1115
+ return;
1116
+ }
1117
+ const subscribers = this.callbacks.get(key);
1118
+ subscribers.forEach(callback => {
1119
+ callback.userCallback(queryResult);
452
1120
  });
453
- return newR;
1121
+ }
1122
+ async publishCacheResultsToSubscribers(impactedQueries, fetchTime) {
1123
+ if (!this.cache) {
1124
+ return;
1125
+ }
1126
+ for (const query of impactedQueries) {
1127
+ const callbacks = this.callbacks.get(query);
1128
+ if (!callbacks) {
1129
+ continue;
1130
+ }
1131
+ const newJson = (await this.cache.getResultTree(query))
1132
+ .getRootStub()
1133
+ .toJSON(EncodingMode.hydrated);
1134
+ const { name, variables } = decoderImpl(query);
1135
+ const queryRef = {
1136
+ dataConnect: this.dc,
1137
+ refType: QUERY_STR,
1138
+ name,
1139
+ variables
1140
+ };
1141
+ this.publishDataToSubscribers(query, {
1142
+ data: newJson,
1143
+ fetchTime,
1144
+ ref: queryRef,
1145
+ source: SOURCE_CACHE,
1146
+ toJSON: getRefSerializer(queryRef, newJson, SOURCE_CACHE, fetchTime)
1147
+ });
1148
+ }
454
1149
  }
455
1150
  enableEmulator(host, port) {
456
1151
  this.transport.useEmulator(host, port);
457
1152
  }
458
1153
  }
459
- function compareDates(str1, str2) {
460
- const date1 = new Date(str1);
461
- const date2 = new Date(str2);
462
- return date1.getTime() < date2.getTime();
1154
+ function getMaxAgeFromExtensions(extensions) {
1155
+ if (!extensions) {
1156
+ return;
1157
+ }
1158
+ for (const extension of extensions) {
1159
+ if ('maxAge' in extension &&
1160
+ extension.maxAge !== undefined &&
1161
+ extension.maxAge !== null) {
1162
+ if (extension.maxAge.endsWith('s')) {
1163
+ return Number(extension.maxAge.substring(0, extension.maxAge.length - 1));
1164
+ }
1165
+ }
1166
+ }
1167
+ }
1168
+ function getDataConnectExtensionsWithoutMaxAge(extensions) {
1169
+ return {
1170
+ dataConnect: extensions.dataConnect?.filter(extension => 'entityId' in extension || 'entityIds' in extension)
1171
+ };
463
1172
  }
464
1173
 
465
1174
  /**
@@ -503,11 +1212,12 @@ const CallerSdkTypeEnum = {
503
1212
  * See the License for the specific language governing permissions and
504
1213
  * limitations under the License.
505
1214
  */
1215
+ const PROD_HOST = 'firebasedataconnect.googleapis.com';
506
1216
  function urlBuilder(projectConfig, transportOptions) {
507
1217
  const { connector, location, projectId: project, service } = projectConfig;
508
1218
  const { host, sslEnabled, port } = transportOptions;
509
1219
  const protocol = sslEnabled ? 'https' : 'http';
510
- const realHost = host || `firebasedataconnect.googleapis.com`;
1220
+ const realHost = host || PROD_HOST;
511
1221
  let baseUrl = `${protocol}://${realHost}`;
512
1222
  if (typeof port === 'number') {
513
1223
  baseUrl += `:${port}`;
@@ -555,7 +1265,7 @@ function getGoogApiClientValue(_isUsingGen, _callerSdkType) {
555
1265
  }
556
1266
  return str;
557
1267
  }
558
- function dcFetch(url, body, { signal }, appId, accessToken, appCheckToken, _isUsingGen, _callerSdkType, _isUsingEmulator) {
1268
+ async function dcFetch(url, body, { signal }, appId, accessToken, appCheckToken, _isUsingGen, _callerSdkType, _isUsingEmulator) {
559
1269
  if (!connectFetch) {
560
1270
  throw new DataConnectError(Code.OTHER, 'No Fetch Implementation detected!');
561
1271
  }
@@ -582,41 +1292,44 @@ function dcFetch(url, body, { signal }, appId, accessToken, appCheckToken, _isUs
582
1292
  if (util.isCloudWorkstation(url) && _isUsingEmulator) {
583
1293
  fetchOptions.credentials = 'include';
584
1294
  }
585
- return connectFetch(url, fetchOptions)
586
- .catch(err => {
1295
+ let response;
1296
+ try {
1297
+ response = await connectFetch(url, fetchOptions);
1298
+ }
1299
+ catch (err) {
587
1300
  throw new DataConnectError(Code.OTHER, 'Failed to fetch: ' + JSON.stringify(err));
588
- })
589
- .then(async (response) => {
590
- let jsonResponse = null;
591
- try {
592
- jsonResponse = await response.json();
593
- }
594
- catch (e) {
595
- throw new DataConnectError(Code.OTHER, JSON.stringify(e));
596
- }
597
- const message = getMessage(jsonResponse);
598
- if (response.status >= 400) {
599
- logError('Error while performing request: ' + JSON.stringify(jsonResponse));
600
- if (response.status === 401) {
601
- throw new DataConnectError(Code.UNAUTHORIZED, message);
602
- }
603
- throw new DataConnectError(Code.OTHER, message);
604
- }
605
- return jsonResponse;
606
- })
607
- .then(res => {
608
- if (res.errors && res.errors.length) {
609
- const stringified = JSON.stringify(res.errors);
610
- const response = {
611
- errors: res.errors,
612
- data: res.data
613
- };
614
- throw new DataConnectOperationError('DataConnect error while performing request: ' + stringified, response);
1301
+ }
1302
+ let jsonResponse;
1303
+ try {
1304
+ jsonResponse = await response.json();
1305
+ }
1306
+ catch (e) {
1307
+ throw new DataConnectError(Code.OTHER, JSON.stringify(e));
1308
+ }
1309
+ const message = getErrorMessage(jsonResponse);
1310
+ if (response.status >= 400) {
1311
+ logError('Error while performing request: ' + JSON.stringify(jsonResponse));
1312
+ if (response.status === 401) {
1313
+ throw new DataConnectError(Code.UNAUTHORIZED, message);
615
1314
  }
616
- return res;
617
- });
1315
+ throw new DataConnectError(Code.OTHER, message);
1316
+ }
1317
+ if (jsonResponse.errors && jsonResponse.errors.length) {
1318
+ const stringified = JSON.stringify(jsonResponse.errors);
1319
+ const failureResponse = {
1320
+ errors: jsonResponse.errors,
1321
+ data: jsonResponse.data
1322
+ };
1323
+ throw new DataConnectOperationError('DataConnect error while performing request: ' + stringified, failureResponse);
1324
+ }
1325
+ if (!jsonResponse.extensions) {
1326
+ jsonResponse.extensions = {
1327
+ dataConnect: []
1328
+ };
1329
+ }
1330
+ return jsonResponse;
618
1331
  }
619
- function getMessage(obj) {
1332
+ function getErrorMessage(obj) {
620
1333
  if ('message' in obj && obj.message) {
621
1334
  return obj.message;
622
1335
  }
@@ -733,7 +1446,10 @@ class RESTTransport {
733
1446
  async getWithAuth(forceToken = false) {
734
1447
  let starterPromise = new Promise(resolve => resolve(this._accessToken));
735
1448
  if (this.appCheckProvider) {
736
- this._appCheckToken = (await this.appCheckProvider.getToken())?.token;
1449
+ const appCheckToken = await this.appCheckProvider.getToken();
1450
+ if (appCheckToken) {
1451
+ this._appCheckToken = appCheckToken.token;
1452
+ }
737
1453
  }
738
1454
  if (this.authProvider) {
739
1455
  starterPromise = this.authProvider
@@ -902,6 +1618,12 @@ class DataConnect {
902
1618
  }
903
1619
  }
904
1620
  }
1621
+ /**
1622
+ * @internal
1623
+ */
1624
+ getCache() {
1625
+ return this.cache;
1626
+ }
905
1627
  // @internal
906
1628
  _useGeneratedSdk() {
907
1629
  if (!this._isUsingGeneratedSdk) {
@@ -924,6 +1646,12 @@ class DataConnect {
924
1646
  delete copy.projectId;
925
1647
  return copy;
926
1648
  }
1649
+ /**
1650
+ * @internal
1651
+ */
1652
+ setCacheSettings(cacheSettings) {
1653
+ this._cacheSettings = cacheSettings;
1654
+ }
927
1655
  // @internal
928
1656
  setInitialized() {
929
1657
  if (this._initialized) {
@@ -933,19 +1661,25 @@ class DataConnect {
933
1661
  logDebug('transportClass not provided. Defaulting to RESTTransport.');
934
1662
  this._transportClass = RESTTransport;
935
1663
  }
936
- if (this._authProvider) {
937
- this._authTokenProvider = new FirebaseAuthProvider(this.app.name, this.app.options, this._authProvider);
1664
+ this._authTokenProvider = new FirebaseAuthProvider(this.app.name, this.app.options, this._authProvider);
1665
+ const connectorConfig = {
1666
+ connector: this.dataConnectOptions.connector,
1667
+ service: this.dataConnectOptions.service,
1668
+ location: this.dataConnectOptions.location
1669
+ };
1670
+ if (this._cacheSettings) {
1671
+ this.cache = new DataConnectCache(this._authTokenProvider, this.app.options.projectId, connectorConfig, this._transportOptions?.host || PROD_HOST, this._cacheSettings);
938
1672
  }
939
1673
  if (this._appCheckProvider) {
940
1674
  this._appCheckTokenProvider = new AppCheckTokenProvider(this.app, this._appCheckProvider);
941
1675
  }
942
- this._initialized = true;
943
1676
  this._transport = new this._transportClass(this.dataConnectOptions, this.app.options.apiKey, this.app.options.appId, this._authTokenProvider, this._appCheckTokenProvider, undefined, this._isUsingGeneratedSdk, this._callerSdkType);
944
1677
  if (this._transportOptions) {
945
1678
  this._transport.useEmulator(this._transportOptions.host, this._transportOptions.port, this._transportOptions.sslEnabled);
946
1679
  }
947
- this._queryManager = new QueryManager(this._transport);
1680
+ this._queryManager = new QueryManager(this._transport, this, this.cache);
948
1681
  this._mutationManager = new MutationManager(this._transport);
1682
+ this._initialized = true;
949
1683
  }
950
1684
  // @internal
951
1685
  enableEmulator(transportOptions) {
@@ -985,22 +1719,32 @@ function connectDataConnectEmulator(dc, host, port, sslEnabled = false) {
985
1719
  }
986
1720
  dc.enableEmulator({ host, port, sslEnabled });
987
1721
  }
988
- function getDataConnect(appOrOptions, optionalOptions) {
1722
+ function getDataConnect(appOrConnectorConfig, settingsOrConnectorConfig, settings) {
989
1723
  let app$1;
990
- let dcOptions;
991
- if ('location' in appOrOptions) {
992
- dcOptions = appOrOptions;
1724
+ let connectorConfig;
1725
+ let realSettings;
1726
+ if ('location' in appOrConnectorConfig) {
1727
+ connectorConfig = appOrConnectorConfig;
993
1728
  app$1 = app.getApp();
1729
+ realSettings = settingsOrConnectorConfig;
994
1730
  }
995
1731
  else {
996
- dcOptions = optionalOptions;
997
- app$1 = appOrOptions;
1732
+ app$1 = appOrConnectorConfig;
1733
+ connectorConfig = settingsOrConnectorConfig;
1734
+ realSettings = settings;
998
1735
  }
999
1736
  if (!app$1 || Object.keys(app$1).length === 0) {
1000
1737
  app$1 = app.getApp();
1001
1738
  }
1739
+ // Options to store in Firebase Component Provider.
1740
+ const serializedOptions = {
1741
+ ...connectorConfig,
1742
+ projectId: app$1.options.projectId
1743
+ };
1744
+ // We should sort the keys before initialization.
1745
+ const sortedSerialized = Object.fromEntries(Object.entries(serializedOptions).sort());
1002
1746
  const provider = app._getProvider(app$1, 'data-connect');
1003
- const identifier = JSON.stringify(dcOptions);
1747
+ const identifier = JSON.stringify(sortedSerialized);
1004
1748
  if (provider.isInitialized(identifier)) {
1005
1749
  const dcInstance = provider.getImmediate({ identifier });
1006
1750
  const options = provider.getOptions(identifier);
@@ -1010,13 +1754,19 @@ function getDataConnect(appOrOptions, optionalOptions) {
1010
1754
  return dcInstance;
1011
1755
  }
1012
1756
  }
1013
- validateDCOptions(dcOptions);
1757
+ validateDCOptions(connectorConfig);
1014
1758
  logDebug('Creating new DataConnect instance');
1015
1759
  // Initialize with options.
1016
- return provider.initialize({
1760
+ const dataConnect = provider.initialize({
1017
1761
  instanceIdentifier: identifier,
1018
- options: dcOptions
1762
+ options: Object.fromEntries(Object.entries({
1763
+ ...sortedSerialized
1764
+ }).sort())
1019
1765
  });
1766
+ if (realSettings?.cacheSettings) {
1767
+ dataConnect.setCacheSettings(realSettings.cacheSettings);
1768
+ }
1769
+ return dataConnect;
1020
1770
  }
1021
1771
  /**
1022
1772
  *
@@ -1046,6 +1796,12 @@ function terminate(dataConnect) {
1046
1796
  return dataConnect._delete();
1047
1797
  // TODO(mtewani): Stop pending tasks
1048
1798
  }
1799
+ const StorageType = {
1800
+ MEMORY: 'MEMORY'
1801
+ };
1802
+ function makeMemoryCacheProvider() {
1803
+ return new MemoryStub();
1804
+ }
1049
1805
 
1050
1806
  /**
1051
1807
  * @license
@@ -1065,13 +1821,16 @@ function terminate(dataConnect) {
1065
1821
  */
1066
1822
  function registerDataConnect(variant) {
1067
1823
  setSDKVersion(app.SDK_VERSION);
1068
- app._registerComponent(new component.Component('data-connect', (container, { instanceIdentifier: settings, options }) => {
1824
+ app._registerComponent(new component.Component('data-connect', (container, { instanceIdentifier: connectorConfigStr, options }) => {
1069
1825
  const app = container.getProvider('app').getImmediate();
1070
1826
  const authProvider = container.getProvider('auth-internal');
1071
1827
  const appCheckProvider = container.getProvider('app-check-internal');
1072
1828
  let newOpts = options;
1073
- if (settings) {
1074
- newOpts = JSON.parse(settings);
1829
+ if (connectorConfigStr) {
1830
+ newOpts = {
1831
+ ...JSON.parse(connectorConfigStr),
1832
+ ...newOpts
1833
+ };
1075
1834
  }
1076
1835
  if (!app.options.projectId) {
1077
1836
  throw new DataConnectError(Code.INVALID_ARGUMENT, 'Project ID must be provided. Did you pass in a proper projectId to initializeApp?');
@@ -1083,6 +1842,28 @@ function registerDataConnect(variant) {
1083
1842
  app.registerVersion(name, version, 'cjs2020');
1084
1843
  }
1085
1844
 
1845
+ /**
1846
+ * @license
1847
+ * Copyright 2025 Google LLC
1848
+ *
1849
+ * Licensed under the Apache License, Version 2.0 (the "License");
1850
+ * you may not use this file except in compliance with the License.
1851
+ * You may obtain a copy of the License at
1852
+ *
1853
+ * http://www.apache.org/licenses/LICENSE-2.0
1854
+ *
1855
+ * Unless required by applicable law or agreed to in writing, software
1856
+ * distributed under the License is distributed on an "AS IS" BASIS,
1857
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1858
+ * See the License for the specific language governing permissions and
1859
+ * limitations under the License.
1860
+ */
1861
+ const QueryFetchPolicy = {
1862
+ PREFER_CACHE: 'PREFER_CACHE',
1863
+ CACHE_ONLY: 'CACHE_ONLY',
1864
+ SERVER_ONLY: 'SERVER_ONLY'
1865
+ };
1866
+
1086
1867
  /**
1087
1868
  * @license
1088
1869
  * Copyright 2024 Google LLC
@@ -1104,8 +1885,22 @@ function registerDataConnect(variant) {
1104
1885
  * @param queryRef query to execute.
1105
1886
  * @returns `QueryPromise`
1106
1887
  */
1107
- function executeQuery(queryRef) {
1108
- return queryRef.dataConnect._queryManager.executeQuery(queryRef);
1888
+ function executeQuery(queryRef, options) {
1889
+ if (queryRef.refType !== QUERY_STR) {
1890
+ return Promise.reject(new DataConnectError(Code.INVALID_ARGUMENT, `ExecuteQuery can only execute query operations`));
1891
+ }
1892
+ const queryManager = queryRef.dataConnect._queryManager;
1893
+ const fetchPolicy = options?.fetchPolicy ?? QueryFetchPolicy.PREFER_CACHE;
1894
+ switch (fetchPolicy) {
1895
+ case QueryFetchPolicy.SERVER_ONLY:
1896
+ return queryManager.fetchServerResults(queryRef);
1897
+ case QueryFetchPolicy.CACHE_ONLY:
1898
+ return queryManager.fetchCacheResults(queryRef, true);
1899
+ case QueryFetchPolicy.PREFER_CACHE:
1900
+ return queryManager.preferCacheResults(queryRef, false);
1901
+ default:
1902
+ throw new DataConnectError(Code.INVALID_ARGUMENT, `Invalid fetch policy: ${fetchPolicy}`);
1903
+ }
1109
1904
  }
1110
1905
  /**
1111
1906
  * Execute Query
@@ -1117,7 +1912,9 @@ function executeQuery(queryRef) {
1117
1912
  */
1118
1913
  function queryRef(dcInstance, queryName, variables, initialCache) {
1119
1914
  dcInstance.setInitialized();
1120
- dcInstance._queryManager.track(queryName, variables, initialCache);
1915
+ if (initialCache !== undefined) {
1916
+ dcInstance._queryManager.updateSSR(initialCache);
1917
+ }
1121
1918
  return {
1122
1919
  dataConnect: dcInstance,
1123
1920
  refType: QUERY_STR,
@@ -1180,7 +1977,7 @@ function validateArgs(connectorConfig, dcOrVars, vars, validateVars) {
1180
1977
 
1181
1978
  /**
1182
1979
  * @license
1183
- * Copyright 2024 Google LLC
1980
+ * Copyright 2025 Google LLC
1184
1981
  *
1185
1982
  * Licensed under the Apache License, Version 2.0 (the "License");
1186
1983
  * you may not use this file except in compliance with the License.
@@ -1208,12 +2005,14 @@ function subscribe(queryRefOrSerializedResult, observerOrOnNext, onError, onComp
1208
2005
  if ('refInfo' in queryRefOrSerializedResult) {
1209
2006
  const serializedRef = queryRefOrSerializedResult;
1210
2007
  const { data, source, fetchTime } = serializedRef;
2008
+ ref = toQueryRef(serializedRef);
1211
2009
  initialCache = {
1212
2010
  data,
1213
2011
  source,
1214
- fetchTime
2012
+ fetchTime,
2013
+ ref,
2014
+ toJSON: getRefSerializer(ref, data, source, fetchTime)
1215
2015
  };
1216
- ref = toQueryRef(serializedRef);
1217
2016
  }
1218
2017
  else {
1219
2018
  ref = queryRefOrSerializedResult;
@@ -1248,13 +2047,16 @@ exports.DataConnectOperationError = DataConnectOperationError;
1248
2047
  exports.MUTATION_STR = MUTATION_STR;
1249
2048
  exports.MutationManager = MutationManager;
1250
2049
  exports.QUERY_STR = QUERY_STR;
2050
+ exports.QueryFetchPolicy = QueryFetchPolicy;
1251
2051
  exports.SOURCE_CACHE = SOURCE_CACHE;
1252
2052
  exports.SOURCE_SERVER = SOURCE_SERVER;
2053
+ exports.StorageType = StorageType;
1253
2054
  exports.areTransportOptionsEqual = areTransportOptionsEqual;
1254
2055
  exports.connectDataConnectEmulator = connectDataConnectEmulator;
1255
2056
  exports.executeMutation = executeMutation;
1256
2057
  exports.executeQuery = executeQuery;
1257
2058
  exports.getDataConnect = getDataConnect;
2059
+ exports.makeMemoryCacheProvider = makeMemoryCacheProvider;
1258
2060
  exports.mutationRef = mutationRef;
1259
2061
  exports.parseOptions = parseOptions;
1260
2062
  exports.queryRef = queryRef;