Skip to content

Request for non-hive cache implementation #1498

@orestesgaolin

Description

@orestesgaolin

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions