-
-
Notifications
You must be signed in to change notification settings - Fork 643
Description
Is your feature request related to a problem? Please describe.
I would like to use graphql cache but I don't want to include hive as dependency. I'm looking for alternatives.
Describe the solution you'd like
I would love to see example Store implementation that uses something different to store cache e.g. filesystem with handling of expiration. Currently only InMemoryStore is available but it lacks features like cache expiration handling.
Describe alternatives you've considered
I tried implementing my own store but it tends to fail with CacheMissException, not sure why.
Custom json store implementation
Note: this is LLM-generated code that I haven't paid too much attention to. Don't copy and paste it into your app without prior testing. It doesn't work for me now.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:graphql/client.dart';
class SimpleStore extends Store {
SimpleStore({
required this.cacheDirectory,
this.prefix = 'cache_',
this.expiryDuration = const Duration(hours: 1),
}) {
// Schedule cleanup of stale files after a few seconds
Future.delayed(const Duration(seconds: 5), _cleanUpStaleFiles);
}
final String cacheDirectory;
final String prefix;
final Duration expiryDuration;
String _sanitizeDataId(String dataId) {
// Replace problematic characters with underscores
return dataId.replaceAll(RegExp(r'[\/\.\:\*\?"<>|]'), '_');
}
void _cleanUpStaleFiles() {
final directory = Directory(cacheDirectory);
if (!directory.existsSync()) return;
final now = DateTime.now();
final staleThreshold = expiryDuration * 2;
for (final file in directory.listSync()) {
if (file is File && file.path.contains(prefix)) {
final fileName = file.uri.pathSegments.last;
// Extract timestamp from the filename
final timestampString = fileName.split('_').last.replaceFirst('.json', '');
final fileTimestamp = int.tryParse(timestampString);
if (fileTimestamp != null) {
final fileDateTime = DateTime.fromMillisecondsSinceEpoch(fileTimestamp);
// Delete files older than 2x expiryDuration
if (now.difference(fileDateTime) > staleThreshold) {
file.deleteSync();
}
}
}
}
}
@override
void delete(String dataId) {
final sanitizedDataId = _sanitizeDataId(dataId);
final directory = Directory(cacheDirectory);
if (!directory.existsSync()) return;
final files = directory.listSync().where((file) {
return file is File && file.path.contains('$prefix$sanitizedDataId');
});
for (final file in files) {
file.deleteSync();
}
}
@override
void put(String dataId, Map<String, dynamic>? value) {
if (value == null) return;
final sanitizedDataId = _sanitizeDataId(dataId);
final directory = Directory(cacheDirectory);
if (!directory.existsSync()) {
directory.createSync(recursive: true);
}
final filePath = dataId == 'Query'
? '${directory.path}/$prefix$sanitizedDataId.json'
: '${directory.path}/$prefix${sanitizedDataId}_${DateTime.now().millisecondsSinceEpoch}.json';
File(filePath).writeAsStringSync(jsonEncode(value));
}
@override
Map<String, dynamic>? get(String dataId) {
print('Fetching data for $dataId from SimpleStore');
final sanitizedDataId = _sanitizeDataId(dataId);
final directory = Directory(cacheDirectory);
if (!directory.existsSync()) return null;
final files = directory.listSync().where((file) {
return file is File && file.path.contains('$prefix$sanitizedDataId');
});
print('Found ${files.length} files for $dataId in ${directory.path}');
if (files.isEmpty) return null;
final file = files.first as File;
final fileName = file.uri.pathSegments.last;
if (dataId != 'Query') {
// Extract timestamp from the filename
final timestampString = fileName
.replaceFirst('$prefix${sanitizedDataId}_', '')
.replaceFirst('.json', '');
final fileTimestamp = int.tryParse(timestampString);
if (fileTimestamp == null) return null;
// Check if the file is expired
final fileDateTime = DateTime.fromMillisecondsSinceEpoch(fileTimestamp);
if (DateTime.now().difference(fileDateTime) > expiryDuration) {
file.deleteSync(); // Optionally delete expired file
return null;
}
}
final content = file.readAsStringSync();
return jsonDecode(content) as Map<String, dynamic>;
}
@override
void putAll(Map<String, Map<String, dynamic>?> data) {
data.forEach(put);
}
@override
void reset() {
final directory = Directory(cacheDirectory);
if (!directory.existsSync()) return;
for (final file in directory.listSync()) {
if (file is File && file.path.contains(prefix)) {
file.deleteSync();
}
}
}
@override
Map<String, Map<String, dynamic>?> toMap() {
final directory = Directory(cacheDirectory);
if (!directory.existsSync()) return {};
final result = <String, Map<String, dynamic>>{};
for (final file in directory.listSync()) {
if (file is File && file.path.contains(prefix)) {
final content = file.readAsStringSync();
final data = jsonDecode(content) as Map<String, dynamic>;
final fileName = file.uri.pathSegments.last;
final sanitizedDataId = fileName.replaceFirst(prefix, '').split('_').first; // Extract sanitized dataId from filename
result[sanitizedDataId] = data;
}
}
return result;
}
}
Additional context