add http_get_cache

This commit is contained in:
2026-01-20 23:18:39 -08:00
parent 572c0789b7
commit 811e884481
168 changed files with 37335 additions and 0 deletions
@@ -0,0 +1,56 @@
import 'package:collection/collection.dart';
// Clear-Site-Data: cache can be used to clear every stored response for a site in the browser cache, so use this with care. Note that this will not affected shared or intermediate caches.
class CacheControl {
final String value;
CacheControl(this.value);
Iterable<(String, String?)> get parts => value.split(',').map((e) {
final p = e.split('=').map((e) => e.trim()).toList();
return (p.first, p.length > 1 ? p.last : null);
});
int _getSeconds(String key, [bool allowBool = false]) {
final part = parts.firstWhereOrNull((e) {
return e.$1.toLowerCase() == key.toLowerCase();
});
final val = int.tryParse(part?.$2 ?? '');
if (val != null) return val;
if (allowBool && _getBool(key)) return double.minPositive.toInt();
return 0;
}
bool _getBool(String key) {
return parts.any((e) => e.$1.toLowerCase() == key.toLowerCase());
}
(int, bool) _getMixed(String key) => (_getSeconds(key), _getBool(key));
}
class RequestCacheControl extends CacheControl {
RequestCacheControl(super.value);
int get maxAge => _getSeconds('max-age');
int get maxStale => _getSeconds('max-stale');
int get minFresh => _getSeconds('min-fresh');
bool get noCache => _getBool('no-cache');
bool get noStore => _getBool('no-store');
bool get noTransform => _getBool('no-transform');
bool get onlyIfCached => _getBool('only-of-cached');
(int, bool) get staleIfError => _getMixed('stale-if-error');
}
class ResponseCacheControl extends CacheControl {
ResponseCacheControl(super.value);
int get maxAge => _getSeconds('max-age');
int get sMaxAge => _getSeconds('s-maxage');
bool get noCache => _getBool('no-cache');
bool get noStore => _getBool('no-store');
bool get noTransform => _getBool('no-transform');
bool get mustRevalidate => _getBool('must-revalidate');
bool get mustUnderstand => _getBool('must-understand');
bool get private => _getBool('private');
bool get public => _getBool('public');
bool get immutable => _getBool('immutable');
(int, bool) get staleWhileRevalidate => _getMixed('stale-while-revalidate');
(int, bool) get staleIfError => _getMixed('stale-if-error');
}
@@ -0,0 +1,2 @@
export 'http_client_factory.dart'
if (dart.library.js_interop) 'http_client_factory_web.dart';
@@ -0,0 +1,30 @@
import 'dart:io';
import 'package:http/http.dart';
import 'package:http/io_client.dart';
import '../database/database.dart';
import '../http_get_cache.dart';
import '../store/base.dart';
import '../store/sqlite.dart';
Client httpClient({
String userAgent = 'Example',
Client? innerClient,
HttpCacheStore? store,
}) {
final db = HttpGetCacheDatabase.instance;
assert(
db != null,
'Database not initialized "HttpGetCacheDatabase.instance = ..."',
);
return HttpGetCache(
() {
if (innerClient != null) return innerClient;
final client = HttpClient();
client.userAgent = userAgent;
return IOClient(client);
}(),
store ?? SqliteHttpCacheStore(db!),
);
}
@@ -0,0 +1,23 @@
import 'package:fetch_client/fetch_client.dart';
import 'package:http/http.dart';
import '../http_get_cache.dart';
import '../store/base.dart';
Client httpClient({
String userAgent = 'Example',
Client? innerClient,
HttpCacheStore? store,
}) {
return HttpGetCache(
() {
if (innerClient != null) return innerClient;
return FetchClient(
mode: RequestMode.cors,
cache: RequestCache.byDefault,
// streamRequests: true,
);
}(),
store ?? HttpCacheStore(),
);
}
@@ -0,0 +1,4 @@
const bool kReleaseMode = bool.fromEnvironment('dart.vm.product');
const bool kProfileMode = bool.fromEnvironment('dart.vm.profile');
const bool kDebugMode = !kReleaseMode && !kProfileMode;
bool kIsWeb = const bool.fromEnvironment('dart.library.js_util');
@@ -0,0 +1 @@
export 'connect.web.dart' if (dart.library.ffi) 'connect.io.dart';
@@ -0,0 +1,27 @@
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import '../settings.dart';
LazyDatabase createExecutor(DatabaseSettings settings) {
return LazyDatabase(() async {
final dbFile = File(p.join(settings.dbPath, settings.name));
if (!dbFile.existsSync()) {
await dbFile.create(recursive: true);
if (settings.initializeDatabase != null) {
final bytes = await settings.initializeDatabase!();
if (bytes != null) {
await dbFile.writeAsBytes(bytes);
}
}
}
return NativeDatabase.createInBackground(
dbFile,
setup: settings.setup,
logStatements: settings.logStatements,
);
});
}
@@ -0,0 +1,12 @@
import 'package:drift/native.dart';
import '../database.dart';
import '../settings.dart';
HttpGetCacheDatabase createMemoryDatabase(DatabaseSettings settings) {
final e = NativeDatabase.memory(
logStatements: settings.logStatements,
setup: settings.setup,
);
return HttpGetCacheDatabase(settings, e);
}
@@ -0,0 +1,27 @@
import 'dart:developer';
import 'package:drift/drift.dart';
import 'package:drift/wasm.dart';
import '../settings.dart';
QueryExecutor createExecutor(DatabaseSettings settings) {
return DatabaseConnection.delayed(Future(() async {
final db = await WasmDatabase.open(
databaseName: settings.name.split('.').first,
sqlite3Uri: Uri.parse(settings.sqlite3Uri),
driftWorkerUri: Uri.parse(settings.driftWorkerUri),
localSetup: settings.setup,
initializeDatabase: settings.initializeDatabase,
);
if (db.missingFeatures.isNotEmpty) {
log(
'Using ${db.chosenImplementation} due to unsupported '
'browser features: ${db.missingFeatures}',
);
}
return db.resolvedExecutor;
}));
}
@@ -0,0 +1,35 @@
import 'package:drift/drift.dart';
import 'settings.dart';
part 'database.g.dart';
@DriftDatabase(include: {
"sql/http_cache.drift",
})
class HttpGetCacheDatabase extends _$HttpGetCacheDatabase {
final DatabaseSettings settings;
HttpGetCacheDatabase(this.settings, super.e);
@override
int get schemaVersion => 2;
static HttpGetCacheDatabase? instance;
@override
MigrationStrategy get migration {
return MigrationStrategy(
onCreate: (Migrator m) async {
await m.createAll();
},
onUpgrade: (Migrator m, int from, int to) async {
if (from < 1) {
await m.createAll();
}
if (from < 2) {
await m.addColumn(httpCache, httpCache.requestHeaders);
}
},
);
}
}
@@ -0,0 +1,865 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'database.dart';
// ignore_for_file: type=lint
class HttpCache extends Table with TableInfo<HttpCache, HttpCacheData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
HttpCache(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
hasAutoIncrement: true,
type: DriftSqlType.int,
requiredDuringInsert: false,
$customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT');
static const VerificationMeta _urlMeta = const VerificationMeta('url');
late final GeneratedColumn<String> url = GeneratedColumn<String>(
'url', aliasedName, false,
type: DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL');
static const VerificationMeta _headersMeta =
const VerificationMeta('headers');
late final GeneratedColumn<String> headers = GeneratedColumn<String>(
'headers', aliasedName, false,
type: DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL');
static const VerificationMeta _requestHeadersMeta =
const VerificationMeta('requestHeaders');
late final GeneratedColumn<String> requestHeaders = GeneratedColumn<String>(
'request_headers', aliasedName, false,
type: DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL');
static const VerificationMeta _bodyMeta = const VerificationMeta('body');
late final GeneratedColumn<String> body = GeneratedColumn<String>(
'body', aliasedName, false,
type: DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL');
static const VerificationMeta _dateMeta = const VerificationMeta('date');
late final GeneratedColumn<int> date = GeneratedColumn<int>(
'date', aliasedName, false,
type: DriftSqlType.int,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL');
static const VerificationMeta _maxAgeMeta = const VerificationMeta('maxAge');
late final GeneratedColumn<int> maxAge = GeneratedColumn<int>(
'max_age', aliasedName, false,
type: DriftSqlType.int,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL');
static const VerificationMeta _staleWhileRevalidateMeta =
const VerificationMeta('staleWhileRevalidate');
late final GeneratedColumn<int> staleWhileRevalidate = GeneratedColumn<int>(
'stale_while_revalidate', aliasedName, false,
type: DriftSqlType.int,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL');
static const VerificationMeta _staleIfErrorMeta =
const VerificationMeta('staleIfError');
late final GeneratedColumn<int> staleIfError = GeneratedColumn<int>(
'stale_if_error', aliasedName, false,
type: DriftSqlType.int,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL');
static const VerificationMeta _immutableMeta =
const VerificationMeta('immutable');
late final GeneratedColumn<bool> immutable = GeneratedColumn<bool>(
'immutable', aliasedName, false,
type: DriftSqlType.bool,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL');
@override
List<GeneratedColumn> get $columns => [
id,
url,
headers,
requestHeaders,
body,
date,
maxAge,
staleWhileRevalidate,
staleIfError,
immutable
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'http_cache';
@override
VerificationContext validateIntegrity(Insertable<HttpCacheData> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
}
if (data.containsKey('url')) {
context.handle(
_urlMeta, url.isAcceptableOrUnknown(data['url']!, _urlMeta));
} else if (isInserting) {
context.missing(_urlMeta);
}
if (data.containsKey('headers')) {
context.handle(_headersMeta,
headers.isAcceptableOrUnknown(data['headers']!, _headersMeta));
} else if (isInserting) {
context.missing(_headersMeta);
}
if (data.containsKey('request_headers')) {
context.handle(
_requestHeadersMeta,
requestHeaders.isAcceptableOrUnknown(
data['request_headers']!, _requestHeadersMeta));
} else if (isInserting) {
context.missing(_requestHeadersMeta);
}
if (data.containsKey('body')) {
context.handle(
_bodyMeta, body.isAcceptableOrUnknown(data['body']!, _bodyMeta));
} else if (isInserting) {
context.missing(_bodyMeta);
}
if (data.containsKey('date')) {
context.handle(
_dateMeta, date.isAcceptableOrUnknown(data['date']!, _dateMeta));
} else if (isInserting) {
context.missing(_dateMeta);
}
if (data.containsKey('max_age')) {
context.handle(_maxAgeMeta,
maxAge.isAcceptableOrUnknown(data['max_age']!, _maxAgeMeta));
} else if (isInserting) {
context.missing(_maxAgeMeta);
}
if (data.containsKey('stale_while_revalidate')) {
context.handle(
_staleWhileRevalidateMeta,
staleWhileRevalidate.isAcceptableOrUnknown(
data['stale_while_revalidate']!, _staleWhileRevalidateMeta));
} else if (isInserting) {
context.missing(_staleWhileRevalidateMeta);
}
if (data.containsKey('stale_if_error')) {
context.handle(
_staleIfErrorMeta,
staleIfError.isAcceptableOrUnknown(
data['stale_if_error']!, _staleIfErrorMeta));
} else if (isInserting) {
context.missing(_staleIfErrorMeta);
}
if (data.containsKey('immutable')) {
context.handle(_immutableMeta,
immutable.isAcceptableOrUnknown(data['immutable']!, _immutableMeta));
} else if (isInserting) {
context.missing(_immutableMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
List<Set<GeneratedColumn>> get uniqueKeys => [
{url, requestHeaders},
];
@override
HttpCacheData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return HttpCacheData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
url: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}url'])!,
headers: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}headers'])!,
requestHeaders: attachedDatabase.typeMapping.read(
DriftSqlType.string, data['${effectivePrefix}request_headers'])!,
body: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}body'])!,
date: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}date'])!,
maxAge: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}max_age'])!,
staleWhileRevalidate: attachedDatabase.typeMapping.read(
DriftSqlType.int, data['${effectivePrefix}stale_while_revalidate'])!,
staleIfError: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}stale_if_error'])!,
immutable: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}immutable'])!,
);
}
@override
HttpCache createAlias(String alias) {
return HttpCache(attachedDatabase, alias);
}
@override
List<String> get customConstraints => const ['UNIQUE(url, request_headers)'];
@override
bool get dontWriteConstraints => true;
}
class HttpCacheData extends DataClass implements Insertable<HttpCacheData> {
int id;
String url;
String headers;
String requestHeaders;
String body;
int date;
int maxAge;
int staleWhileRevalidate;
int staleIfError;
bool immutable;
HttpCacheData(
{required this.id,
required this.url,
required this.headers,
required this.requestHeaders,
required this.body,
required this.date,
required this.maxAge,
required this.staleWhileRevalidate,
required this.staleIfError,
required this.immutable});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['url'] = Variable<String>(url);
map['headers'] = Variable<String>(headers);
map['request_headers'] = Variable<String>(requestHeaders);
map['body'] = Variable<String>(body);
map['date'] = Variable<int>(date);
map['max_age'] = Variable<int>(maxAge);
map['stale_while_revalidate'] = Variable<int>(staleWhileRevalidate);
map['stale_if_error'] = Variable<int>(staleIfError);
map['immutable'] = Variable<bool>(immutable);
return map;
}
HttpCacheCompanion toCompanion(bool nullToAbsent) {
return HttpCacheCompanion(
id: Value(id),
url: Value(url),
headers: Value(headers),
requestHeaders: Value(requestHeaders),
body: Value(body),
date: Value(date),
maxAge: Value(maxAge),
staleWhileRevalidate: Value(staleWhileRevalidate),
staleIfError: Value(staleIfError),
immutable: Value(immutable),
);
}
factory HttpCacheData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return HttpCacheData(
id: serializer.fromJson<int>(json['id']),
url: serializer.fromJson<String>(json['url']),
headers: serializer.fromJson<String>(json['headers']),
requestHeaders: serializer.fromJson<String>(json['request_headers']),
body: serializer.fromJson<String>(json['body']),
date: serializer.fromJson<int>(json['date']),
maxAge: serializer.fromJson<int>(json['max_age']),
staleWhileRevalidate:
serializer.fromJson<int>(json['stale_while_revalidate']),
staleIfError: serializer.fromJson<int>(json['stale_if_error']),
immutable: serializer.fromJson<bool>(json['immutable']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'url': serializer.toJson<String>(url),
'headers': serializer.toJson<String>(headers),
'request_headers': serializer.toJson<String>(requestHeaders),
'body': serializer.toJson<String>(body),
'date': serializer.toJson<int>(date),
'max_age': serializer.toJson<int>(maxAge),
'stale_while_revalidate': serializer.toJson<int>(staleWhileRevalidate),
'stale_if_error': serializer.toJson<int>(staleIfError),
'immutable': serializer.toJson<bool>(immutable),
};
}
HttpCacheData copyWith(
{int? id,
String? url,
String? headers,
String? requestHeaders,
String? body,
int? date,
int? maxAge,
int? staleWhileRevalidate,
int? staleIfError,
bool? immutable}) =>
HttpCacheData(
id: id ?? this.id,
url: url ?? this.url,
headers: headers ?? this.headers,
requestHeaders: requestHeaders ?? this.requestHeaders,
body: body ?? this.body,
date: date ?? this.date,
maxAge: maxAge ?? this.maxAge,
staleWhileRevalidate: staleWhileRevalidate ?? this.staleWhileRevalidate,
staleIfError: staleIfError ?? this.staleIfError,
immutable: immutable ?? this.immutable,
);
HttpCacheData copyWithCompanion(HttpCacheCompanion data) {
return HttpCacheData(
id: data.id.present ? data.id.value : this.id,
url: data.url.present ? data.url.value : this.url,
headers: data.headers.present ? data.headers.value : this.headers,
requestHeaders: data.requestHeaders.present
? data.requestHeaders.value
: this.requestHeaders,
body: data.body.present ? data.body.value : this.body,
date: data.date.present ? data.date.value : this.date,
maxAge: data.maxAge.present ? data.maxAge.value : this.maxAge,
staleWhileRevalidate: data.staleWhileRevalidate.present
? data.staleWhileRevalidate.value
: this.staleWhileRevalidate,
staleIfError: data.staleIfError.present
? data.staleIfError.value
: this.staleIfError,
immutable: data.immutable.present ? data.immutable.value : this.immutable,
);
}
@override
String toString() {
return (StringBuffer('HttpCacheData(')
..write('id: $id, ')
..write('url: $url, ')
..write('headers: $headers, ')
..write('requestHeaders: $requestHeaders, ')
..write('body: $body, ')
..write('date: $date, ')
..write('maxAge: $maxAge, ')
..write('staleWhileRevalidate: $staleWhileRevalidate, ')
..write('staleIfError: $staleIfError, ')
..write('immutable: $immutable')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, url, headers, requestHeaders, body, date,
maxAge, staleWhileRevalidate, staleIfError, immutable);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is HttpCacheData &&
other.id == this.id &&
other.url == this.url &&
other.headers == this.headers &&
other.requestHeaders == this.requestHeaders &&
other.body == this.body &&
other.date == this.date &&
other.maxAge == this.maxAge &&
other.staleWhileRevalidate == this.staleWhileRevalidate &&
other.staleIfError == this.staleIfError &&
other.immutable == this.immutable);
}
class HttpCacheCompanion extends UpdateCompanion<HttpCacheData> {
Value<int> id;
Value<String> url;
Value<String> headers;
Value<String> requestHeaders;
Value<String> body;
Value<int> date;
Value<int> maxAge;
Value<int> staleWhileRevalidate;
Value<int> staleIfError;
Value<bool> immutable;
HttpCacheCompanion({
this.id = const Value.absent(),
this.url = const Value.absent(),
this.headers = const Value.absent(),
this.requestHeaders = const Value.absent(),
this.body = const Value.absent(),
this.date = const Value.absent(),
this.maxAge = const Value.absent(),
this.staleWhileRevalidate = const Value.absent(),
this.staleIfError = const Value.absent(),
this.immutable = const Value.absent(),
});
HttpCacheCompanion.insert({
this.id = const Value.absent(),
required String url,
required String headers,
required String requestHeaders,
required String body,
required int date,
required int maxAge,
required int staleWhileRevalidate,
required int staleIfError,
required bool immutable,
}) : url = Value(url),
headers = Value(headers),
requestHeaders = Value(requestHeaders),
body = Value(body),
date = Value(date),
maxAge = Value(maxAge),
staleWhileRevalidate = Value(staleWhileRevalidate),
staleIfError = Value(staleIfError),
immutable = Value(immutable);
static Insertable<HttpCacheData> custom({
Expression<int>? id,
Expression<String>? url,
Expression<String>? headers,
Expression<String>? requestHeaders,
Expression<String>? body,
Expression<int>? date,
Expression<int>? maxAge,
Expression<int>? staleWhileRevalidate,
Expression<int>? staleIfError,
Expression<bool>? immutable,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (url != null) 'url': url,
if (headers != null) 'headers': headers,
if (requestHeaders != null) 'request_headers': requestHeaders,
if (body != null) 'body': body,
if (date != null) 'date': date,
if (maxAge != null) 'max_age': maxAge,
if (staleWhileRevalidate != null)
'stale_while_revalidate': staleWhileRevalidate,
if (staleIfError != null) 'stale_if_error': staleIfError,
if (immutable != null) 'immutable': immutable,
});
}
HttpCacheCompanion copyWith(
{Value<int>? id,
Value<String>? url,
Value<String>? headers,
Value<String>? requestHeaders,
Value<String>? body,
Value<int>? date,
Value<int>? maxAge,
Value<int>? staleWhileRevalidate,
Value<int>? staleIfError,
Value<bool>? immutable}) {
return HttpCacheCompanion(
id: id ?? this.id,
url: url ?? this.url,
headers: headers ?? this.headers,
requestHeaders: requestHeaders ?? this.requestHeaders,
body: body ?? this.body,
date: date ?? this.date,
maxAge: maxAge ?? this.maxAge,
staleWhileRevalidate: staleWhileRevalidate ?? this.staleWhileRevalidate,
staleIfError: staleIfError ?? this.staleIfError,
immutable: immutable ?? this.immutable,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (url.present) {
map['url'] = Variable<String>(url.value);
}
if (headers.present) {
map['headers'] = Variable<String>(headers.value);
}
if (requestHeaders.present) {
map['request_headers'] = Variable<String>(requestHeaders.value);
}
if (body.present) {
map['body'] = Variable<String>(body.value);
}
if (date.present) {
map['date'] = Variable<int>(date.value);
}
if (maxAge.present) {
map['max_age'] = Variable<int>(maxAge.value);
}
if (staleWhileRevalidate.present) {
map['stale_while_revalidate'] = Variable<int>(staleWhileRevalidate.value);
}
if (staleIfError.present) {
map['stale_if_error'] = Variable<int>(staleIfError.value);
}
if (immutable.present) {
map['immutable'] = Variable<bool>(immutable.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('HttpCacheCompanion(')
..write('id: $id, ')
..write('url: $url, ')
..write('headers: $headers, ')
..write('requestHeaders: $requestHeaders, ')
..write('body: $body, ')
..write('date: $date, ')
..write('maxAge: $maxAge, ')
..write('staleWhileRevalidate: $staleWhileRevalidate, ')
..write('staleIfError: $staleIfError, ')
..write('immutable: $immutable')
..write(')'))
.toString();
}
}
abstract class _$HttpGetCacheDatabase extends GeneratedDatabase {
_$HttpGetCacheDatabase(QueryExecutor e) : super(e);
$HttpGetCacheDatabaseManager get managers =>
$HttpGetCacheDatabaseManager(this);
late final HttpCache httpCache = HttpCache(this);
late final Index httpCacheIdx = Index('http_cache_idx',
'CREATE UNIQUE INDEX http_cache_idx ON http_cache (url, request_headers, date, max_age, stale_if_error, stale_while_revalidate)');
Future<List<HttpCacheData>> setHttpCache(
{required String url,
required String headers,
required String requestHeaders,
required String body,
required int date,
required int maxAge,
required int staleWhileRevalidate,
required int staleIfError,
required bool immutable}) {
return customWriteReturning(
'INSERT OR REPLACE INTO http_cache (url, headers, request_headers, body, date, max_age, stale_while_revalidate, stale_if_error, immutable) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) RETURNING *',
variables: [
Variable<String>(url),
Variable<String>(headers),
Variable<String>(requestHeaders),
Variable<String>(body),
Variable<int>(date),
Variable<int>(maxAge),
Variable<int>(staleWhileRevalidate),
Variable<int>(staleIfError),
Variable<bool>(immutable)
],
updates: {
httpCache
}).then((rows) => Future.wait(rows.map(httpCache.mapFromRow)));
}
Selectable<HttpCacheData> getHttpCacheByUrl({required String url}) {
return customSelect('SELECT * FROM http_cache WHERE url = ?1', variables: [
Variable<String>(url)
], readsFrom: {
httpCache,
}).asyncMap(httpCache.mapFromRow);
}
Selectable<HttpCacheData> getHttpCache({required String url}) {
return customSelect('SELECT * FROM http_cache WHERE url = ?1', variables: [
Variable<String>(url)
], readsFrom: {
httpCache,
}).asyncMap(httpCache.mapFromRow);
}
Future<int> deleteHttpCache({required String url}) {
return customUpdate(
'DELETE FROM http_cache WHERE url = ?1',
variables: [Variable<String>(url)],
updates: {httpCache},
updateKind: UpdateKind.delete,
);
}
Future<int> deleteHttpCacheStale() {
return customUpdate(
'DELETE FROM http_cache WHERE unixepoch() >(date +(max_age * 1000)+(MAX(stale_while_revalidate, stale_if_error) * 1000))',
variables: [],
updates: {httpCache},
updateKind: UpdateKind.delete,
);
}
Future<int> deleteHttpCacheStaleWithoutRevalidate() {
return customUpdate(
'DELETE FROM http_cache WHERE unixepoch() >(date +(max_age * 1000))',
variables: [],
updates: {httpCache},
updateKind: UpdateKind.delete,
);
}
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [httpCache, httpCacheIdx];
@override
DriftDatabaseOptions get options =>
const DriftDatabaseOptions(storeDateTimeAsText: true);
}
typedef $HttpCacheCreateCompanionBuilder = HttpCacheCompanion Function({
Value<int> id,
required String url,
required String headers,
required String requestHeaders,
required String body,
required int date,
required int maxAge,
required int staleWhileRevalidate,
required int staleIfError,
required bool immutable,
});
typedef $HttpCacheUpdateCompanionBuilder = HttpCacheCompanion Function({
Value<int> id,
Value<String> url,
Value<String> headers,
Value<String> requestHeaders,
Value<String> body,
Value<int> date,
Value<int> maxAge,
Value<int> staleWhileRevalidate,
Value<int> staleIfError,
Value<bool> immutable,
});
class $HttpCacheFilterComposer
extends Composer<_$HttpGetCacheDatabase, HttpCache> {
$HttpCacheFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<int> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get url => $composableBuilder(
column: $table.url, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get headers => $composableBuilder(
column: $table.headers, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get requestHeaders => $composableBuilder(
column: $table.requestHeaders,
builder: (column) => ColumnFilters(column));
ColumnFilters<String> get body => $composableBuilder(
column: $table.body, builder: (column) => ColumnFilters(column));
ColumnFilters<int> get date => $composableBuilder(
column: $table.date, builder: (column) => ColumnFilters(column));
ColumnFilters<int> get maxAge => $composableBuilder(
column: $table.maxAge, builder: (column) => ColumnFilters(column));
ColumnFilters<int> get staleWhileRevalidate => $composableBuilder(
column: $table.staleWhileRevalidate,
builder: (column) => ColumnFilters(column));
ColumnFilters<int> get staleIfError => $composableBuilder(
column: $table.staleIfError, builder: (column) => ColumnFilters(column));
ColumnFilters<bool> get immutable => $composableBuilder(
column: $table.immutable, builder: (column) => ColumnFilters(column));
}
class $HttpCacheOrderingComposer
extends Composer<_$HttpGetCacheDatabase, HttpCache> {
$HttpCacheOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<int> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get url => $composableBuilder(
column: $table.url, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get headers => $composableBuilder(
column: $table.headers, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get requestHeaders => $composableBuilder(
column: $table.requestHeaders,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get body => $composableBuilder(
column: $table.body, builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get date => $composableBuilder(
column: $table.date, builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get maxAge => $composableBuilder(
column: $table.maxAge, builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get staleWhileRevalidate => $composableBuilder(
column: $table.staleWhileRevalidate,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get staleIfError => $composableBuilder(
column: $table.staleIfError,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<bool> get immutable => $composableBuilder(
column: $table.immutable, builder: (column) => ColumnOrderings(column));
}
class $HttpCacheAnnotationComposer
extends Composer<_$HttpGetCacheDatabase, HttpCache> {
$HttpCacheAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<int> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<String> get url =>
$composableBuilder(column: $table.url, builder: (column) => column);
GeneratedColumn<String> get headers =>
$composableBuilder(column: $table.headers, builder: (column) => column);
GeneratedColumn<String> get requestHeaders => $composableBuilder(
column: $table.requestHeaders, builder: (column) => column);
GeneratedColumn<String> get body =>
$composableBuilder(column: $table.body, builder: (column) => column);
GeneratedColumn<int> get date =>
$composableBuilder(column: $table.date, builder: (column) => column);
GeneratedColumn<int> get maxAge =>
$composableBuilder(column: $table.maxAge, builder: (column) => column);
GeneratedColumn<int> get staleWhileRevalidate => $composableBuilder(
column: $table.staleWhileRevalidate, builder: (column) => column);
GeneratedColumn<int> get staleIfError => $composableBuilder(
column: $table.staleIfError, builder: (column) => column);
GeneratedColumn<bool> get immutable =>
$composableBuilder(column: $table.immutable, builder: (column) => column);
}
class $HttpCacheTableManager extends RootTableManager<
_$HttpGetCacheDatabase,
HttpCache,
HttpCacheData,
$HttpCacheFilterComposer,
$HttpCacheOrderingComposer,
$HttpCacheAnnotationComposer,
$HttpCacheCreateCompanionBuilder,
$HttpCacheUpdateCompanionBuilder,
(
HttpCacheData,
BaseReferences<_$HttpGetCacheDatabase, HttpCache, HttpCacheData>
),
HttpCacheData,
PrefetchHooks Function()> {
$HttpCacheTableManager(_$HttpGetCacheDatabase db, HttpCache table)
: super(TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$HttpCacheFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$HttpCacheOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$HttpCacheAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
Value<int> id = const Value.absent(),
Value<String> url = const Value.absent(),
Value<String> headers = const Value.absent(),
Value<String> requestHeaders = const Value.absent(),
Value<String> body = const Value.absent(),
Value<int> date = const Value.absent(),
Value<int> maxAge = const Value.absent(),
Value<int> staleWhileRevalidate = const Value.absent(),
Value<int> staleIfError = const Value.absent(),
Value<bool> immutable = const Value.absent(),
}) =>
HttpCacheCompanion(
id: id,
url: url,
headers: headers,
requestHeaders: requestHeaders,
body: body,
date: date,
maxAge: maxAge,
staleWhileRevalidate: staleWhileRevalidate,
staleIfError: staleIfError,
immutable: immutable,
),
createCompanionCallback: ({
Value<int> id = const Value.absent(),
required String url,
required String headers,
required String requestHeaders,
required String body,
required int date,
required int maxAge,
required int staleWhileRevalidate,
required int staleIfError,
required bool immutable,
}) =>
HttpCacheCompanion.insert(
id: id,
url: url,
headers: headers,
requestHeaders: requestHeaders,
body: body,
date: date,
maxAge: maxAge,
staleWhileRevalidate: staleWhileRevalidate,
staleIfError: staleIfError,
immutable: immutable,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
));
}
typedef $HttpCacheProcessedTableManager = ProcessedTableManager<
_$HttpGetCacheDatabase,
HttpCache,
HttpCacheData,
$HttpCacheFilterComposer,
$HttpCacheOrderingComposer,
$HttpCacheAnnotationComposer,
$HttpCacheCreateCompanionBuilder,
$HttpCacheUpdateCompanionBuilder,
(
HttpCacheData,
BaseReferences<_$HttpGetCacheDatabase, HttpCache, HttpCacheData>
),
HttpCacheData,
PrefetchHooks Function()>;
class $HttpGetCacheDatabaseManager {
final _$HttpGetCacheDatabase _db;
$HttpGetCacheDatabaseManager(this._db);
$HttpCacheTableManager get httpCache =>
$HttpCacheTableManager(_db, _db.httpCache);
}
@@ -0,0 +1,31 @@
import 'dart:typed_data';
import 'package:path/path.dart' as p;
import 'package:sqlite3/common.dart';
import '../constants.dart';
class DatabaseSettings {
final String name;
final bool logStatements;
final String driftWorkerUri;
final String sqlite3Uri;
final String databaseDir;
final String cacheDir;
final void Function(CommonDatabase)? setup;
final Future<Uint8List?> Function()? initializeDatabase;
const DatabaseSettings(
this.name, {
this.logStatements = false,
this.driftWorkerUri = 'drift_worker.js',
this.sqlite3Uri = kDebugMode ? 'sqlite3.debug.wasm' : 'sqlite3.wasm',
this.setup,
this.initializeDatabase,
required this.databaseDir,
required this.cacheDir,
});
String get dbPath => p.join(databaseDir, name);
String get cachePath => p.join(cacheDir, 'cache');
}
@@ -0,0 +1,68 @@
CREATE TABLE http_cache (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL,
headers TEXT NOT NULL,
request_headers TEXT NOT NULL,
body TEXT NOT NULL,
date INT NOT NULL,
max_age INT NOT NULL,
stale_while_revalidate INT NOT NULL,
stale_if_error INT NOT NULL,
immutable BOOLEAN NOT NULL,
UNIQUE(url, request_headers)
);
CREATE UNIQUE INDEX http_cache_idx ON http_cache (
url,
request_headers,
date,
max_age,
stale_if_error,
stale_while_revalidate
);
setHttpCache:
INSERT OR REPLACE
INTO http_cache (
url,
headers,
request_headers,
body,
date,
max_age,
stale_while_revalidate,
stale_if_error,
immutable
)
VALUES (
:url,
:headers,
:requestHeaders,
:body,
:date,
:maxAge,
:staleWhileRevalidate,
:staleIfError,
:immutable
)
RETURNING *;
getHttpCacheByUrl:
SELECT * FROM http_cache
WHERE url = :url;
getHttpCache:
SELECT * FROM http_cache
WHERE url = :url;
deleteHttpCache:
DELETE FROM http_cache
WHERE url = :url;
deleteHttpCacheStale:
DELETE FROM http_cache
WHERE unixepoch() > (date + (max_age * 1000) + (MAX(stale_while_revalidate, stale_if_error) * 1000));
deleteHttpCacheStaleWithoutRevalidate:
DELETE FROM http_cache
WHERE unixepoch() > (date + (max_age * 1000));
@@ -0,0 +1,48 @@
/// Parse a date from a request/response header value into
/// a valid dart DateTime.
///
/// Example: Wed, 17 Jul 2024 23:19:37 GMT
class DateParser {
const DateParser();
DateTime? call(String value) {
if (value.isEmpty) return null;
final d = DateTime.tryParse(value);
if (d != null) return d;
final [
_,
dayOfMonth,
monthName,
yearName,
time,
_,
] = value.trim().split(' ');
var year = int.parse(yearName).toString();
var month = (switch (monthName.toLowerCase()) {
'jan' => DateTime.january,
'feb' => DateTime.february,
'mar' => DateTime.march,
'apr' => DateTime.april,
'may' => DateTime.may,
'jun' => DateTime.june,
'jul' => DateTime.july,
'aug' => DateTime.august,
'sep' => DateTime.september,
'oct' => DateTime.october,
'nov' => DateTime.november,
'dec' => DateTime.december,
(_) => -1,
})
.toString()
.padLeft(2, '0');
var day = int.parse(dayOfMonth).toString().padLeft(2, '0');
final str = '$year-$month-$day $time';
return DateTime.tryParse(str);
}
}
DateTime getDate() => DateTime.now();
int getDateInt() => getDate().toSeconds();
extension DateTimeUtils on DateTime {
int toSeconds() => Duration(milliseconds: millisecondsSinceEpoch).inSeconds;
}
@@ -0,0 +1,26 @@
import 'package:cachecontrol/cachecontrol.dart';
import 'package:http/http.dart';
const _cacheControlHeader = 'cache-control';
extension CacheControlRequest on BaseRequest {
RequestCacheControl get cacheControl {
final raw = headers[_cacheControlHeader] ?? '';
return RequestCacheControl.parse(raw);
}
set cacheControl(RequestCacheControl value) {
headers[_cacheControlHeader] = value.toString();
}
}
extension CacheControlBaseResponse on BaseResponse {
ResponseCacheControl get cacheControl {
final raw = headers[_cacheControlHeader] ?? '';
return ResponseCacheControl.parse(raw);
}
set cacheControl(ResponseCacheControl value) {
headers[_cacheControlHeader] = value.toString();
}
}
@@ -0,0 +1,145 @@
import 'dart:convert';
import 'dart:math';
import 'package:http/http.dart';
import 'constants.dart';
import 'request_headers.dart';
import 'date_parser.dart';
import 'store/base.dart';
class HttpGetCache extends BaseClient {
final HttpCacheStore store;
final Client inner;
HttpGetCache(this.inner, this.store);
static Future<StreamedResponse> networkError(Object? error) async {
return StreamedResponse(
Stream.value(utf8.encode('$error')),
500,
);
}
static Future<StreamedResponse> offlineError(Object? error) async {
return StreamedResponse(
Stream.value(utf8.encode('$error')),
503,
);
}
static Future<StreamedResponse> gatewayTimeout() async {
return StreamedResponse(
Stream.value(utf8.encode('Gateway Timeout')),
504,
);
}
Future<StreamedResponse> save(BaseRequest request) async {
final reqCC = request.cacheControl;
final res = await inner.send(request);
final resCC = res.cacheControl;
if (res.statusCode == 200) {
if (reqCC.noStore || resCC.noStore || resCC.noCache) {
return res;
} else {
return await store.set(request, res);
}
} else {
final cached = await store.get(request);
if (cached != null) {
final cachedCC = cached.cacheControl;
final now = getDateInt();
final date = cached.requestHeaders.created;
if (reqCC.staleIfError.$2 || resCC.staleIfError.$2) {
return cached;
} else if (date != null &&
now <
(date +
cachedCC.maxAge +
max(
cachedCC.staleIfError.$1,
reqCC.staleIfError.$1,
))) {
return cached;
}
}
}
return res;
}
Stream<StreamedResponse> stream(BaseRequest request) async* {
final method = request.method.toLowerCase();
final reqCC = request.cacheControl;
if (kIsWeb || method != 'get') {
yield await inner.send(request);
return;
}
// Check for early fetch
var cached = await store.get(request);
bool needsUpdate = true;
if (cached == null && reqCC.onlyIfCached) {
yield await gatewayTimeout();
return;
}
// Check for cache
if (cached != null) {
final cachedCC = cached.cacheControl;
if (cachedCC.mustRevalidate || cachedCC.noCache || reqCC.noCache) {
needsUpdate = true;
} else {
final now = getDateInt();
final date = cached.requestHeaders.created;
if (date != null && now < (date + cachedCC.maxAge)) {
needsUpdate = false;
yield cached;
} else if (date != null &&
now < (date + cachedCC.maxAge + cachedCC.staleWhileRevalidate.$1)) {
yield cached;
} else if (cachedCC.staleWhileRevalidate.$2) {
yield cached;
}
if (!cachedCC.immutable && needsUpdate) {
// Check for new eTag, Last-Modified change
final headRes = await inner.head(request.url);
final headHeaders = headRes.requestHeaders;
if (headRes.statusCode == 200) {
// Check eTag
if ((cached.requestHeaders.eTag + headHeaders.eTag).isNotEmpty &&
cached.requestHeaders.eTag == headHeaders.eTag) {
needsUpdate = false;
}
if ((max(
cached.requestHeaders.lastModified ?? 0,
headHeaders.lastModified ?? 0,
) >
0) &&
cached.requestHeaders.lastModified ==
headHeaders.lastModified) {
needsUpdate = false;
}
} else if (headRes.statusCode == 304) {
// Or 304 cache is still good to reuse
needsUpdate = false;
if (cachedCC.mustRevalidate || cachedCC.noCache || reqCC.noCache) {
yield cached;
}
}
}
}
}
if (!needsUpdate) return;
yield await save(request);
}
@override
Future<StreamedResponse> send(BaseRequest request) {
return stream(request).last;
}
}
@@ -0,0 +1,75 @@
import 'package:collection/collection.dart';
import 'package:http/http.dart';
import 'cache_control.dart';
import 'date_parser.dart';
class RequestHeaders {
final Map<String, String> value;
RequestHeaders(this.value);
static DateParser dateParser = const DateParser();
String getValue(String key) {
final result = value.entries.firstWhere(
(e) => e.key.toLowerCase() == key.toLowerCase(),
orElse: () => MapEntry(key, ''),
);
return result.value;
}
String get eTag => getValue('eTag');
int get age => int.tryParse(getValue('Age')) ?? 0;
int? get lastModified => dateParser(getValue('Last-Modified'))?.toSeconds();
int? get date => dateParser(getValue('Date'))?.toSeconds();
int? get expires => dateParser(getValue('Expires'))?.toSeconds();
int? get created {
final lastModifiedKey = value.keys.firstWhereOrNull(
(e) => e.toLowerCase() == 'last-modified',
);
final dateKey = value.keys.firstWhereOrNull(
(e) => e.toLowerCase() == 'date',
);
final key = lastModifiedKey ?? dateKey;
if (key != null) {
final d = dateParser(value[key]!);
if (d != null) return d.millisecondsSinceEpoch;
}
return null;
}
}
extension BaseRequestCCUtils on BaseRequest {
RequestHeaders get requestHeaders {
return RequestHeaders(headers);
}
RequestCacheControl get cacheControl {
return RequestCacheControl(requestHeaders.getValue('cache-control'));
}
}
extension StreamedResponseCCUtils on StreamedResponse {
RequestHeaders get requestHeaders {
return RequestHeaders(headers);
}
ResponseCacheControl get cacheControl {
return ResponseCacheControl(requestHeaders.getValue('cache-control'));
}
}
extension ResponseCCUtils on Response {
RequestHeaders get requestHeaders {
return RequestHeaders(headers);
}
ResponseCacheControl get cacheControl {
return ResponseCacheControl(requestHeaders.getValue('cache-control'));
}
}
@@ -0,0 +1,14 @@
import 'package:http/http.dart';
class HttpCacheStore {
Future<StreamedResponse?> get(BaseRequest request) async {
return null;
}
Future<StreamedResponse> set(
BaseRequest request,
StreamedResponse response,
) async {
return response;
}
}
@@ -0,0 +1,221 @@
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart';
import 'package:path/path.dart' as p;
import '../constants.dart';
import '../database/database.dart';
import '../date_parser.dart';
import '../request_headers.dart';
import 'base.dart';
class SqliteHttpCacheStore extends HttpCacheStore {
final HttpGetCacheDatabase database;
SqliteHttpCacheStore(this.database);
static Future<String> getCacheDir(HttpGetCacheDatabase database) async {
if (kIsWeb) return '';
final cachePath = database.settings.cachePath;
final cacheDir = Directory(cachePath);
if (!cacheDir.existsSync()) {
await cacheDir.create(recursive: true);
}
return cacheDir.path;
}
static Future<void> deleteCacheDir(HttpGetCacheDatabase database) async {
if (kIsWeb) return;
final cachePath = await getCacheDir(database);
if (cachePath.isEmpty) return;
final cacheDir = Directory(cachePath);
if (cacheDir.existsSync()) {
cacheDir.deleteSync(recursive: true);
}
}
static Future<int> getCacheDirSize(HttpGetCacheDatabase database) async {
if (kIsWeb) return 0;
final cachePath = await getCacheDir(database);
if (cachePath.isEmpty) return 0;
final cacheDir = Directory(cachePath);
if (cacheDir.existsSync()) {
final files = await cacheDir.list(recursive: true).toList();
final dirSize =
files.fold(0, (int sum, file) => sum + file.statSync().size);
return dirSize;
}
return 0;
}
static Future<void> deleteExpired(HttpGetCacheDatabase database) async {
await database.deleteHttpCacheStale();
}
Stream<List<int>> _streamAndSaveBytes(
BaseRequest req,
StreamedResponse res,
File file,
) async* {
final date = res.requestHeaders.created ?? getDateInt();
if (!file.existsSync()) {
await file.create(recursive: true);
}
final writer = file.openWrite();
await for (final event in res.stream) {
yield event;
writer.add(event);
}
await writer.flush();
await writer.close();
final vary = res.headers['vary'] ?? '';
final requestHeaders = <String, String>{};
if (vary.isNotEmpty && vary != '*') {
final keys =
vary.split(',').map((e) => e.trim()).where((e) => e.isNotEmpty);
for (final key in keys) {
requestHeaders[key] = req.headers[key] ?? '';
}
}
await database.setHttpCache(
url: req.url.key,
headers: jsonEncode(res.headers),
requestHeaders: jsonEncode(requestHeaders),
body: file.path,
maxAge: res.cacheControl.maxAge,
staleWhileRevalidate: res.cacheControl.staleWhileRevalidate.$1,
staleIfError: res.cacheControl.staleIfError.$1,
immutable: res.cacheControl.immutable,
date: date,
);
}
@override
Future<StreamedResponse> set(
BaseRequest request,
StreamedResponse response,
) async {
if (kIsWeb) return response;
// Don't cache if Vary is *
if (response.headers['vary'] == '*') {
return response;
}
if (response.cacheControl.noStore || request.cacheControl.noStore) {
return response;
}
final dirPath = await getCacheDir(database);
if (dirPath.isEmpty) return response;
final dir = Directory(dirPath);
// Use a unique file name to allow multiple responses per URL.
// We can't rely just on URL key anymore.
// We'll append a random string or use UUID if we had it, but here just use random hash or
// simply use the existing method but append request headers hash.
// To safe space and keep it simple, let's use a combination.
// However, if we change the file naming, we orphan old files or we need to manage them.
// Since we delete by DB entry (which stores path), it's fine.
// Just need a unique name.
// Simple unique name strategy: hash(url + vary request headers)
// But we don't know vary headers yet... wait, we do get them from response.
// We computed `requestHeaders` in _streamAndSaveBytes.
// Ideally we compute path there too? But we pass file into it.
// Let's modify behavior: Generate a random ID for the file?
// Or just make `key` unique-ish.
// sha256(url + timestamp + random) is safe enough.
final uniqueId = sha256
.convert(utf8.encode(
'${request.url.key}${DateTime.now().microsecondsSinceEpoch}'))
.toString();
final file = File(p.join(dir.path, uniqueId));
return StreamedResponse(
_streamAndSaveBytes(request, response, file),
response.statusCode,
contentLength: response.contentLength,
request: response.request,
headers: response.headers,
isRedirect: response.isRedirect,
persistentConnection: response.persistentConnection,
reasonPhrase: response.reasonPhrase,
);
}
@override
Future<StreamedResponse?> get(BaseRequest request) async {
final candidates = await database //
.getHttpCache(url: request.url.key)
.get();
for (final candidate in candidates) {
final responseHeaders = _parseHeaders(candidate.headers);
final vary = responseHeaders['vary'];
if (vary == '*') {
continue; // Should not happen if we don't store it, but check anyway.
}
bool match = true;
if (vary != null && vary.isNotEmpty) {
final storedRequestHeaders = _parseHeaders(candidate.requestHeaders);
final varyKeys =
vary.split(',').map((e) => e.trim()).where((e) => e.isNotEmpty);
for (final key in varyKeys) {
if (request.headers[key] != storedRequestHeaders[key]) {
match = false;
break;
}
}
}
if (match) {
final path = candidate.body;
final file = File(path);
if (await file.exists()) {
final stats = await file.stat();
final h = {...responseHeaders};
final rq = RequestHeaders(h);
if (rq.created == null) {
final d = DateTime.fromMillisecondsSinceEpoch(candidate.date);
h['Date'] = d.toIso8601String();
}
return StreamedResponse(
file.openRead(),
200,
contentLength: stats.size,
headers: h,
);
}
}
}
return null;
}
}
// extension on BaseRequest {
// String get key => sha256.convert(utf8.encode(url.key)).toString();
// }
extension on Uri {
String get key => toString();
}
Map<String, String> _parseHeaders(String key) {
final headers = <String, String>{};
if (key.isNotEmpty) {
final data = jsonDecode(key) as Map<String, Object?>;
for (final entry in data.entries) {
headers[entry.key] = entry.value?.toString() ?? '';
}
}
return headers;
}