Skip to content
15 changes: 6 additions & 9 deletions lib/src/services/api_call.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,16 @@ class ApiCall extends BaseApiCall<Map<String, dynamic>> {
bool shouldCacheResult = false,
}) =>
shouldCacheResult && config.cachedSearchResultsTTL != Duration.zero
? _requestCache.cache(
? _requestCache.getResponse(
// SplayTreeMap ensures order of the parameters is maintained so
// cache key won't differ because of different ordering of
// parameters.
'$endpoint${SplayTreeMap.from(queryParams)}'.hashCode,
send,
'$endpoint${SplayTreeMap.from(queryParams)}',
(node) => node.client.get(
requestUri(node, endpoint, queryParams),
headers: defaultHeaders,
),
config.cachedSearchResultsTTL,
send,
)
: send((node) => node.client.get(
requestUri(node, endpoint, queryParams),
Expand Down Expand Up @@ -80,19 +79,17 @@ class ApiCall extends BaseApiCall<Map<String, dynamic>> {
bool shouldCacheResult = false,
}) =>
shouldCacheResult && config.cachedSearchResultsTTL != Duration.zero
? _requestCache.cache(
? _requestCache.getResponse(
// SplayTreeMap ensures order of the parameters is maintained so
// cache key won't differ because of different ordering of
// parameters.
'$endpoint${SplayTreeMap.from(queryParams)}${SplayTreeMap.from(additionalHeaders)}${json.encode(bodyParameters)}'
.hashCode,
send,
'$endpoint${SplayTreeMap.from(queryParams)}${SplayTreeMap.from(additionalHeaders)}${json.encode(bodyParameters)}',
(node) => node.client.post(
requestUri(node, endpoint, queryParams),
headers: {...defaultHeaders, ...additionalHeaders},
body: json.encode(bodyParameters),
),
config.cachedSearchResultsTTL,
send,
)
: send((node) => node.client.post(
requestUri(node, endpoint, queryParams),
Expand Down
3 changes: 2 additions & 1 deletion lib/src/services/base_api_call.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';

import 'package:http/http.dart' as http;

import 'typedefs.dart';
import 'node_pool.dart';
import '../configuration.dart';
import '../models/node.dart';
Expand Down Expand Up @@ -51,7 +52,7 @@ abstract class BaseApiCall<R extends Object> {
///
/// Also sets the health status of nodes after each request so it can be put
/// in/out of [NodePool]'s circulation.
Future<R> send(Future<http.Response> Function(Node) request) async {
Future<R> send(Request request) async {
http.Response response;
Node node;
for (var triesLeft = config.numRetries;;) {
Expand Down
50 changes: 21 additions & 29 deletions lib/src/services/request_cache.dart
Original file line number Diff line number Diff line change
@@ -1,44 +1,36 @@
import 'dart:collection';
import 'package:dcache/dcache.dart';

import 'package:http/http.dart' as http;

import '../models/node.dart';
import 'typedefs.dart';

/// Cache store which uses a [HashMap] internally to serve requests.
class RequestCache {
final _cachedResponses = HashMap<int, _Cache>();
Cache<String, Map<String, dynamic>> _cachedResponses;
final _cachedTimestamp = HashMap<String, DateTime>();
final Duration timeToUse;
final int size;

RequestCache(this.size, this.timeToUse) {
_cachedResponses = LruCache<String, Map<String, dynamic>>(storage: InMemoryStorage(size));
}

/// Caches the response of the [request], identified by [key]. The cached
/// response is valid till [cacheTTL].
Future<Map<String, dynamic>> cache(
int key,
Future<Map<String, dynamic>> Function(Future<http.Response> Function(Node))
send,
Future<http.Response> Function(Node) request,
Duration cacheTTL,
Future<Map<String, dynamic>> getResponse(
String key,
Request request,
Send<Map<String, dynamic>> send
) async {
if (_cachedResponses.containsKey(key)) {
if (_isCacheValid(_cachedResponses[key], cacheTTL)) {
// Cache entry is still valid, return it
return Future.value(_cachedResponses[key].data);
} else {
// Cache entry has expired, so delete it explicitly
_cachedResponses.remove(key);
}
if (_cachedResponses.containsKey(key) && _isCacheValid(key)) {
return Future<Map<String, dynamic>>.value(_cachedResponses.get(key));
}

final response = await send(request);
_cachedResponses[key] = _Cache(response, DateTime.now());
var response = await send(request);
_cachedResponses.set(key, response);
_cachedTimestamp[key] = DateTime.now();
return response;
}

bool _isCacheValid(_Cache cache, Duration cacheTTL) =>
DateTime.now().difference(cache.creationTime) < cacheTTL;
}

class _Cache {
final DateTime creationTime;
final Map<String, dynamic> data;

const _Cache(this.data, this.creationTime);
bool _isCacheValid(String key) =>
DateTime.now().difference(_cachedTimestamp[key]) < timeToUse;
}
6 changes: 6 additions & 0 deletions lib/src/services/typedefs.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'package:http/http.dart' as http;

import '../models/node.dart';

typedef Request = Future<http.Response> Function(Node);
typedef Send<R> = Future<R> Function(Request);
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies:
http: ^0.13.3
crypto: ^3.0.1
equatable: ^2.0.2
dcache: ^0.4.0

dev_dependencies:
test: ^1.17.7
Expand Down
31 changes: 30 additions & 1 deletion test/configuration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ void main() {
retryInterval: Duration(seconds: 3),
sendApiKeyAsQueryParam: true,
cachedSearchResultsTTL: Duration(seconds: 30),
cacheCapacity: 101,
);

group('Configuration', () {
Expand Down Expand Up @@ -61,9 +62,12 @@ void main() {
test('has a sendApiKeyAsQueryParam field', () {
expect(config.sendApiKeyAsQueryParam, isTrue);
});
test('has a cacheSearchResults field', () {
test('has a cacheSearchResultsTTL field', () {
expect(config.cachedSearchResultsTTL, equals(Duration(seconds: 30)));
});
test('has a cacheCapacity field', () {
expect(config.cacheCapacity, equals(101));
});
});

group('Configuration initialization', () {
Expand Down Expand Up @@ -180,6 +184,31 @@ void main() {
);
expect(config.retryInterval, equals(Duration(milliseconds: 100)));
});
test('with missing cacheCapacity, sets cacheCapacity to 100', () {
final config = Configuration(
apiKey: 'abc123',
connectionTimeout: Duration(seconds: 10),
healthcheckInterval: Duration(seconds: 5),
nearestNode: Node(
protocol: 'http',
host: 'localhost',
path: '/path/to/service',
),
nodes: {
Node(
protocol: 'https',
host: 'localhost',
path: '/path/to/service',
),
},
numRetries: 5,
retryInterval: Duration(seconds: 3),
sendApiKeyAsQueryParam: true,
cachedSearchResultsTTL: Duration(seconds: 30),
);

expect(config.cacheCapacity, equals(100));
});
test(
'with missing sendApiKeyAsQueryParam, sets sendApiKeyAsQueryParam to false',
() {
Expand Down
Loading