Skip to content

Commit 24f4e60

Browse files
Initial Commit
1 parent 345e14f commit 24f4e60

16 files changed

+1018
-101
lines changed

packages/firebase_data_connect/firebase_data_connect/lib/firebase_data_connect.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,6 @@ export 'src/optional.dart'
3939
listDeserializer,
4040
listSerializer;
4141
export 'src/timestamp.dart' show Timestamp;
42+
export 'src/cache/cache_data_types.dart' show CacheSettings, QueryFetchPolicy;
43+
export 'src/cache/cache_manager.dart' show Cache;
44+
export 'src/cache/cache_provider.dart' show CacheProvider;
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
2+
// Copyright 2025 Google LLC
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
/// Type of storage to use for the cache
17+
enum CacheStorage {
18+
persistent,
19+
memory
20+
}
21+
22+
const String GlobalIDKey = 'cacheId';
23+
24+
/// Configuration for the cache
25+
class CacheSettings {
26+
/// The type of storage to use (e.g., "persistent", "ephemeral")
27+
final CacheStorage storage;
28+
29+
/// The maximum size of the cache in bytes
30+
final int maxSizeBytes;
31+
32+
const CacheSettings({this.storage = CacheStorage.memory, this.maxSizeBytes = 100000000});
33+
}
34+
35+
/// Enum to control the fetch policy for a query
36+
enum QueryFetchPolicy {
37+
/// Prefer the cache, but fetch from the server if the cached data is stale
38+
preferCache,
39+
40+
/// Only fetch from the cache
41+
cacheOnly,
42+
43+
/// Only fetch from the server
44+
serverOnly,
45+
}
46+
47+
/// Represents a cached query result.
48+
class ResultTree {
49+
/// The dehydrated query result, typically in a serialized format like JSON.
50+
final Map<String, dynamic> data;
51+
52+
/// The time-to-live for the cached result, indicating how long it is considered "fresh".
53+
final Duration ttl;
54+
55+
/// The timestamp when the result was cached.
56+
final DateTime cachedAt;
57+
58+
/// The timestamp when the result was last accessed.
59+
DateTime lastAccessed;
60+
61+
/// A reference to the root `EntityNode` of the dehydrated tree.
62+
final EntityNode rootObject;
63+
64+
/// Checks if cached data is stale
65+
bool isStale() {
66+
if (DateTime.now().difference(cachedAt) > ttl) {
67+
return true; // stale
68+
} else {
69+
return false;
70+
}
71+
}
72+
73+
74+
ResultTree(
75+
{required this.data,
76+
required this.ttl,
77+
required this.cachedAt,
78+
required this.lastAccessed,
79+
required this.rootObject});
80+
}
81+
82+
/// Target encoding mode
83+
enum EncodingMode {
84+
hydrated,
85+
dehydrated
86+
}
87+
88+
/// Represents a normalized data entity.
89+
class EntityDataObject {
90+
/// A globally unique identifier for the entity, provided by the server.
91+
final String guid;
92+
93+
/// A dictionary of the scalar values of the entity.
94+
Map<String, dynamic> _serverValues = {};
95+
96+
/// A set of identifiers for the `QueryRef`s that reference this EDO.
97+
final Set<String> referencedFrom = {};
98+
99+
void updateServerValue(String prop, dynamic value) {
100+
_serverValues[prop] = value;
101+
}
102+
103+
void setServerValues(Map<String, dynamic> values) {
104+
_serverValues = values;
105+
}
106+
107+
/// Dictionary of prop-values contained in this EDO
108+
Map<String, dynamic> fields() {
109+
return _serverValues;
110+
}
111+
112+
EntityDataObject(
113+
{required this.guid});
114+
}
115+
116+
/// A tree-like data structure that represents the dehydrated or hydrated query result.
117+
class EntityNode {
118+
/// A reference to an `EntityDataObject`.
119+
final EntityDataObject? entity;
120+
121+
/// A dictionary of scalar values (if the node does not represent a normalized entity).
122+
final Map<String, dynamic>? scalarValues;
123+
124+
/// A dictionary of references to other `EntityNode`s (for nested objects).
125+
final Map<String, EntityNode>? nestedObjects;
126+
127+
/// A dictionary of lists of other `EntityNode`s (for arrays of objects).
128+
final Map<String, List<EntityNode>>? nestedObjectLists;
129+
130+
EntityNode(
131+
{this.entity,
132+
this.scalarValues,
133+
this.nestedObjects,
134+
this.nestedObjectLists});
135+
136+
Map<String, dynamic> toJson({EncodingMode mode = EncodingMode.hydrated}) {
137+
Map<String, dynamic> jsonData = {};
138+
if (mode == EncodingMode.hydrated) {
139+
if (entity != null) {
140+
jsonData.addAll(entity!.fields());
141+
}
142+
143+
if (scalarValues != null) {
144+
jsonData.addAll(scalarValues!);
145+
}
146+
147+
if (nestedObjects != null) {
148+
nestedObjects!.forEach((key, edo) {
149+
jsonData[key] = edo.toJson(mode: mode);
150+
});
151+
}
152+
153+
if (nestedObjectLists != null) {
154+
nestedObjectLists!.forEach((key, edoList) {
155+
List<Map<String, dynamic>> jsonList = [];
156+
edoList.forEach((edo){
157+
jsonList.add(edo.toJson(mode: mode));
158+
});
159+
jsonData[key] = jsonList;
160+
});
161+
}
162+
163+
} // if hydrated
164+
else if (mode == EncodingMode.dehydrated) {
165+
// encode the guid so we can extract the EntityDataObject
166+
if (entity != null) {
167+
jsonData[GlobalIDKey] = entity!.guid;
168+
}
169+
170+
if (scalarValues != null) {
171+
jsonData['scalars'] = scalarValues;
172+
}
173+
174+
if (nestedObjects != null) {
175+
List<Map<String, dynamic>> nestedObjectsJson = [];
176+
nestedObjects!.forEach((key, edo){
177+
Map<String, dynamic> obj = {};
178+
obj[key] = edo.toJson(mode: mode);
179+
nestedObjectsJson.add(obj);
180+
});
181+
jsonData['objects'] = nestedObjectsJson;
182+
}
183+
184+
if (nestedObjectLists != null) {
185+
List<Map<String, dynamic>> nestedObjectListsJson = [];
186+
nestedObjectLists!.forEach((key, edoList){
187+
List<Map<String, dynamic>> jsonList = [];
188+
edoList.forEach((edo){
189+
jsonList.add(edo.toJson(mode: mode));
190+
});
191+
nestedObjectListsJson.add({key: jsonList});
192+
});
193+
jsonData['lists'] = nestedObjectListsJson;
194+
}
195+
196+
}
197+
return jsonData;
198+
}
199+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
2+
// Copyright 2025 Google LLC
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
import 'dart:async';
17+
import 'dart:convert';
18+
19+
import '../common/common_library.dart';
20+
21+
import 'cache_data_types.dart';
22+
import 'cache_provider.dart';
23+
import 'result_tree_processor.dart';
24+
25+
/// The central component of the caching system.
26+
class Cache {
27+
final CacheProvider _cacheProvider;
28+
final ResultTreeProcessor _resultTreeProcessor = ResultTreeProcessor();
29+
final _impactedQueryController = StreamController<Set<String>>.broadcast();
30+
31+
Cache(this._cacheProvider);
32+
33+
/// Stream of impacted query IDs.
34+
Stream<Set<String>> get impactedQueries => _impactedQueryController.stream;
35+
36+
/// Caches a server response.
37+
Future<void> update(String queryId, ServerResponse serverResponse) async {
38+
print("updateCache data for $queryId");
39+
40+
final dehydrationResult = await _resultTreeProcessor.dehydrate(
41+
queryId, serverResponse.data, _cacheProvider);
42+
43+
EntityNode rootNode = dehydrationResult.dehydratedTree;
44+
String dehydratedJson = jsonEncode(rootNode.toJson(mode: EncodingMode.dehydrated));
45+
print("cacheUpdate: dehydrateResult ${dehydratedJson}");
46+
47+
Duration ttl = serverResponse.ttl != null ? serverResponse.ttl! : Duration(seconds: 10);
48+
final resultTree = ResultTree(
49+
data: rootNode.toJson(mode: EncodingMode.dehydrated), // Storing the original response for now
50+
ttl: ttl, // Default TTL
51+
cachedAt: DateTime.now(),
52+
lastAccessed: DateTime.now(),
53+
rootObject: dehydrationResult.dehydratedTree);
54+
55+
print("updateCache - got resultTree $resultTree");
56+
57+
_cacheProvider.saveResultTree(queryId, resultTree);
58+
print("updateCache - savedResultTree $queryId - $resultTree");
59+
60+
_impactedQueryController.add(dehydrationResult.impactedQueryIds);
61+
}
62+
63+
/// Fetches a cached result.
64+
Future<Map<String, dynamic>?> get(String queryId, bool allowStale) async {
65+
print("getCache for $queryId");
66+
67+
final resultTree = await _cacheProvider.getResultTree(queryId);
68+
print("getCache resultTree $resultTree");
69+
70+
if (resultTree != null) {
71+
// Simple TTL check
72+
if (resultTree.isStale() && !allowStale) {
73+
print("getCache result is stale and allowStale is false");
74+
return null;
75+
}
76+
77+
resultTree.lastAccessed = DateTime.now();
78+
_cacheProvider.saveResultTree(queryId, resultTree);
79+
print("getCache updated lastAccessed ${resultTree.data}");
80+
81+
return resultTree.rootObject.toJson(); //default mode is hydrated
82+
//return _resultTreeProcessor.hydrate(resultTree.rootObject, _cacheProvider);
83+
}
84+
85+
return null;
86+
}
87+
88+
/// Invalidates the cache.
89+
Future<void> invalidate() async {
90+
_cacheProvider.clear();
91+
}
92+
93+
void dispose() {
94+
_impactedQueryController.close();
95+
}
96+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
2+
// Copyright 2025 Google LLC
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
import 'cache_data_types.dart';
17+
18+
/// An interface that defines the contract for the underlying storage mechanism.
19+
///
20+
/// This allows for different storage implementations to be used (e.g., in-memory, SQLite, IndexedDB).
21+
abstract class CacheProvider {
22+
/// Stores a `ResultTree` object.
23+
void saveResultTree(String queryId, ResultTree resultTree);
24+
25+
/// Retrieves a `ResultTree` object.
26+
ResultTree? getResultTree(String queryId);
27+
28+
/// Stores an `EntityDataObject` object.
29+
void saveEntityDataObject(EntityDataObject edo);
30+
31+
/// Retrieves an `EntityDataObject` object.
32+
EntityDataObject getEntityDataObject(String guid);
33+
34+
/// Manages the cache size and eviction policies.
35+
void manageCacheSize();
36+
37+
/// Clears all data from the cache.
38+
void clear();
39+
}

0 commit comments

Comments
 (0)