Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
#!/bin/bash

# Uses dart protoc_plugin version 21.1.2. There are compilation issues with newer plugin versions.
# https://github.com/google/protobuf.dart/releases/tag/protoc_plugin-v21.1.2
# Run `pub global activate protoc_plugin 21.1.2`

rm -rf lib/src/generated
mkdir lib/src/generated
protoc --dart_out=grpc:lib/src/generated -I./protos/firebase -I./protos/google connector_service.proto google/protobuf/struct.proto graphql_error.proto --proto_path=./protos
protoc --dart_out=grpc:lib/src/generated -I./protos/firebase -I./protos/google connector_service.proto google/protobuf/struct.proto google/protobuf/duration.proto graphql_error.proto graphql_response_extensions.proto --proto_path=./protos
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,13 @@ class Cache {
Stream<Set<String>> get impactedQueries => _impactedQueryController.stream;

String _constructCacheIdentifier() {
final rawIdentifier =
'${_settings.storage}-${dataConnect.app.options.projectId}-${dataConnect.app.name}-${dataConnect.connectorConfig.serviceId}-${dataConnect.connectorConfig.connector}-${dataConnect.connectorConfig.location}-${dataConnect.auth?.currentUser?.uid ?? 'anon'}-${dataConnect.transport.transportOptions.host}';
return convertToSha256(rawIdentifier);
final rawPrefix =
'${_settings.storage}-${dataConnect.app.options.projectId}-${dataConnect.app.name}-${dataConnect.connectorConfig.serviceId}-${dataConnect.connectorConfig.connector}-${dataConnect.connectorConfig.location}-${dataConnect.transport.transportOptions.host}';
final prefixSha = convertToSha256(rawPrefix);
final rawSuffix = dataConnect.auth?.currentUser?.uid ?? 'anon';
final suffixSha = convertToSha256(rawSuffix);

return '$prefixSha-$suffixSha';
}

void _initializeProvider() {
Expand Down Expand Up @@ -92,31 +96,41 @@ class Cache {
return;
}

final dehydrationResult = await _resultTreeProcessor.dehydrate(
queryId, serverResponse.data, _cacheProvider!);
final Map<DataConnectPath, PathMetadata> paths =
serverResponse.extensions != null
? ExtensionResponse.fromJson(serverResponse.extensions!)
.flattenPathMetadata()
: {};

final dehydrationResult = await _resultTreeProcessor.dehydrateResults(
queryId, serverResponse.data, _cacheProvider!, paths);

EntityNode rootNode = dehydrationResult.dehydratedTree;
Map<String, dynamic> dehydratedMap =
rootNode.toJson(mode: EncodingMode.dehydrated);

// if we have server ttl, that overrides maxAge from cacheSettings
Duration ttl =
serverResponse.ttl != null ? serverResponse.ttl! : _settings.maxAge;
Duration ttl = serverResponse.extensions != null &&
serverResponse.extensions!['ttl'] != null
? Duration(seconds: serverResponse.extensions!['ttl'] as int)
: (serverResponse.ttl ?? _settings.maxAge);

final resultTree = ResultTree(
data: dehydratedMap,
ttl: ttl,
cachedAt: DateTime.now(),
lastAccessed: DateTime.now());

_cacheProvider!.saveResultTree(queryId, resultTree);
_cacheProvider!.setResultTree(queryId, resultTree);

Set<String> impactedQueryIds = dehydrationResult.impactedQueryIds;
impactedQueryIds.remove(queryId); // remove query being cached
_impactedQueryController.add(impactedQueryIds);
}

/// Fetches a cached result.
Future<Map<String, dynamic>?> get(String queryId, bool allowStale) async {
Future<Map<String, dynamic>?> resultTree(
String queryId, bool allowStale) async {
if (_cacheProvider == null) {
return null;
}
Expand All @@ -137,23 +151,20 @@ class Cache {
}

resultTree.lastAccessed = DateTime.now();
_cacheProvider!.saveResultTree(queryId, resultTree);
_cacheProvider!.setResultTree(queryId, resultTree);

EntityNode rootNode =
EntityNode.fromJson(resultTree.data, _cacheProvider!);

Map<String, dynamic> hydratedJson =
rootNode.toJson(); //default mode for toJson is hydrate
await _resultTreeProcessor.hydrateResults(rootNode, _cacheProvider!);

return hydratedJson;
}

return null;
}

/// Invalidates the cache.
Future<void> invalidate() async {
_cacheProvider?.clear();
}

void dispose() {
_impactedQueryController.close();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,41 +15,144 @@
import 'dart:convert';

import 'package:firebase_data_connect/src/cache/cache_provider.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:firebase_data_connect/src/common/common_library.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show kIsWeb, listEquals;

/// Type of storage to use for the cache
enum CacheStorage { persistent, memory }

const String kGlobalIDKey = 'cacheId';
const String kGlobalIDKey = 'guid';

@immutable
class DataConnectPath {
final List<DataConnectPathSegment> components;

DataConnectPath([List<DataConnectPathSegment>? components])
: components = components ?? [];

DataConnectPath appending(DataConnectPathSegment segment) {
return DataConnectPath([...components, segment]);
}

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is DataConnectPath &&
runtimeType == other.runtimeType &&
listEquals(components, other.components);

@override
int get hashCode => Object.hashAll(components);

@override
String toString() => 'DataConnectPath($components)';
}

/// Additional information about object / field identified by a path
class PathMetadata {
final DataConnectPath path;
final String? entityId;

PathMetadata({required this.path, this.entityId});

@override
String toString() {
return '$path : ${entityId ?? "null"}';
}
}

/// Represents the server response contained within the extension response
class PathMetadataResponse {
final List<DataConnectPathSegment> path;
final String? entityId;
final List<String>? entityIds;

PathMetadataResponse({required this.path, this.entityId, this.entityIds});

factory PathMetadataResponse.fromJson(Map<String, dynamic> json) {
return PathMetadataResponse(
path: (json['path'] as List).map(_parsePathSegment).toList(),
entityId: json['entityId'] as String?,
entityIds: (json['entityIds'] as List?)?.cast<String>(),
);
}
}

DataConnectPathSegment _parsePathSegment(dynamic segment) {
if (segment is String) {
return DataConnectFieldPathSegment(segment);
} else if (segment is double || segment is int) {
int index = (segment is double) ? segment.toInt() : segment;
return DataConnectListIndexPathSegment(index);
}
throw ArgumentError('Invalid path segment type: ${segment.runtimeType}');
}

/// Represents the extension section within the server response
class ExtensionResponse {
final Duration? maxAge;
final List<PathMetadataResponse> dataConnect;

ExtensionResponse({this.maxAge, required this.dataConnect});

factory ExtensionResponse.fromJson(Map<String, dynamic> json) {
return ExtensionResponse(
maxAge:
json['ttl'] != null ? Duration(seconds: json['ttl'] as int) : null,
dataConnect: (json['dataConnect'] as List?)
?.map((e) =>
PathMetadataResponse.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
}

Map<DataConnectPath, PathMetadata> flattenPathMetadata() {
final Map<DataConnectPath, PathMetadata> result = {};
for (final pmr in dataConnect) {
if (pmr.entityId != null) {
final pm = PathMetadata(
path: DataConnectPath(pmr.path), entityId: pmr.entityId);
result[pm.path] = pm;
}

if (pmr.entityIds != null) {
for (var i = 0; i < pmr.entityIds!.length; i++) {
final entityId = pmr.entityIds![i];
final indexPath = DataConnectPath(pmr.path)
.appending(DataConnectListIndexPathSegment(i));
final pm = PathMetadata(path: indexPath, entityId: entityId);
result[pm.path] = pm;
}
}
}
return result;
}
}

/// Configuration for the cache
class CacheSettings {
/// The type of storage to use (e.g., "persistent", "memory")
final CacheStorage storage;

/// The maximum size of the cache in bytes
final int maxSizeBytes;

/// Duration for which cache is used before revalidation with server
final Duration maxAge;

// Internal const constructor
const CacheSettings._internal({
required this.storage,
required this.maxSizeBytes,
required this.maxAge,
});

// Factory constructor to handle the logic
factory CacheSettings({
CacheStorage? storage,
int? maxSizeBytes,
Duration maxAge = Duration.zero,
}) {
return CacheSettings._internal(
storage:
storage ?? (kIsWeb ? CacheStorage.memory : CacheStorage.persistent),
maxSizeBytes: maxSizeBytes ?? (kIsWeb ? 40000000 : 100000000),
maxAge: maxAge,
);
}
Expand Down Expand Up @@ -203,7 +306,7 @@ class EntityNode {
Map<String, dynamic> json, CacheProvider cacheProvider) {
EntityDataObject? entity;
if (json[kGlobalIDKey] != null) {
entity = cacheProvider.getEntityDataObject(json[kGlobalIDKey]);
entity = cacheProvider.getEntityData(json[kGlobalIDKey]);
}

Map<String, dynamic>? scalars;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,16 @@ abstract class CacheProvider {
Future<bool> initialize();

/// Stores a `ResultTree` object.
void saveResultTree(String queryId, ResultTree resultTree);
void setResultTree(String queryId, ResultTree resultTree);

/// Retrieves a `ResultTree` object.
ResultTree? getResultTree(String queryId);

/// Stores an `EntityDataObject` object.
void saveEntityDataObject(EntityDataObject edo);
void updateEntityData(EntityDataObject edo);

/// Retrieves an `EntityDataObject` object.
EntityDataObject getEntityDataObject(String guid);

/// Manages the cache size and eviction policies.
void manageCacheSize();
EntityDataObject getEntityData(String guid);

/// Clears all data from the cache.
void clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'cache_data_types.dart';
import 'cache_provider.dart';

/// An in-memory implementation of the `CacheProvider`.
/// This is used for the web platform
class InMemoryCacheProvider implements CacheProvider {
final Map<String, ResultTree> _resultTrees = {};
final Map<String, EntityDataObject> _edos = {};
Expand All @@ -31,12 +32,12 @@ class InMemoryCacheProvider implements CacheProvider {

@override
Future<bool> initialize() async {
// nothing to be intialized.
// nothing to be intialized
return true;
}

@override
void saveResultTree(String queryId, ResultTree resultTree) {
void setResultTree(String queryId, ResultTree resultTree) {
_resultTrees[queryId] = resultTree;
}

Expand All @@ -46,20 +47,15 @@ class InMemoryCacheProvider implements CacheProvider {
}

@override
void saveEntityDataObject(EntityDataObject edo) {
void updateEntityData(EntityDataObject edo) {
_edos[edo.guid] = edo;
}

@override
EntityDataObject getEntityDataObject(String guid) {
EntityDataObject getEntityData(String guid) {
return _edos.putIfAbsent(guid, () => EntityDataObject(guid: guid));
}

@override
void manageCacheSize() {
// In-memory cache doesn't have a size limit in this implementation.
}

@override
void clear() {
_resultTrees.clear();
Expand Down
Loading
Loading