blob: 4059e5a5d1e77cc4eb8515175ebda86dc1eb391a [file] [log] [blame]
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'package:neat_cache/cache_provider.dart';
import 'package:neat_cache/neat_cache.dart';
/// Service for reading and writing values to a cache for quick access of data.
///
/// If [inMemory] is true, a cache with [inMemoryMaxNumberEntries] number
/// of entries will be created. Otherwise, it will use the default redis cache.
class CacheService {
CacheService({
bool inMemory = false,
int inMemoryMaxNumberEntries = 256,
}) : _provider =
inMemory ? Cache.inMemoryCacheProvider(inMemoryMaxNumberEntries) : Cache.redisCacheProvider(memorystoreUri);
final CacheProvider<List<int>> _provider;
Cache<Uint8List> get cache => cacheValue ?? Cache<List<int>>(_provider).withCodec<Uint8List>(const _CacheCodec());
@visibleForTesting
Cache<Uint8List>? cacheValue;
/// Google Cloud Memorystore default url.
static Uri memorystoreUri = Uri.parse('redis://10.0.0.4:6379');
/// An arbritary number for how many times we should try to get from cache
/// before giving up.
///
/// Writing to the cache creates a racy condition for when another operation
/// is trying to get the same key. This race condition throws an exception.
@visibleForTesting
static const int maxCacheGetAttempts = 3;
/// Get value of [key] from the subcache [subcacheName]. If the key has no
/// value, call [createFn] to create a value for it, set it, and return it.
///
/// The underlying cache get function is inherently racy as if there is a
/// write operation while a read operation, getting the value can fail. To
/// handle this racy condition, this attempts to get the value [maxCacheGetAttempts]
/// times before giving up. This is because the cache is magnitudes faster
/// than the fallback operation (usually a Datastore query).
Future<Uint8List?> getOrCreate(
String subcacheName,
String key, {
int attempt = 1,
Future<Uint8List> Function()? createFn,
Duration ttl = const Duration(minutes: 1),
}) async {
final Cache<Uint8List> subcache = cache.withPrefix(subcacheName);
Uint8List? value;
try {
value = await subcache[key].get();
} catch (e) {
if (attempt < maxCacheGetAttempts) {
return getOrCreate(
subcacheName,
key,
attempt: ++attempt,
createFn: createFn,
ttl: ttl,
);
} else {
// Give up on trying to get the value from the cache.
value = null;
}
}
// If given createFn, update the cache value if the value returned was null.
if (createFn != null && value == null) {
// Try creating the value
value = await createFn();
await set(subcacheName, key, value, ttl: ttl);
}
return value;
}
/// Set [value] for [key] in the subcache [subcacheName] with [ttl].
Future<Uint8List?> set(
String subcacheName,
String key,
Uint8List? value, {
Duration ttl = const Duration(minutes: 1),
}) async {
final Cache<Uint8List> subcache = cache.withPrefix(subcacheName);
final Entry<Uint8List> entry = subcache[key];
return entry.set(value, ttl);
}
/// Clear the value stored in subcache [subcacheName] for key [key].
Future<void> purge(String subcacheName, String key) {
final Cache<Uint8List> subcache = cache.withPrefix(subcacheName);
return subcache[key].purge(retries: maxCacheGetAttempts);
}
void dispose() {
_provider.close();
}
}
class _CacheCodec extends Codec<Uint8List, List<int>> {
const _CacheCodec();
@override
Converter<Uint8List, List<int>> get encoder => const _ListIntConverter();
@override
Converter<List<int>, Uint8List> get decoder => const _Uint8ListConverter();
}
class _ListIntConverter extends Converter<Uint8List, List<int>> {
const _ListIntConverter();
@override
List<int> convert(Uint8List input) => input;
}
class _Uint8ListConverter extends Converter<List<int>, Uint8List> {
const _Uint8ListConverter();
@override
Uint8List convert(List<int> input) => Uint8List.fromList(input);
}