diff --git a/kraken/lib/src/foundation/http_cache.dart b/kraken/lib/src/foundation/http_cache.dart index 1c49d1e1ed..3e1e37df7d 100644 --- a/kraken/lib/src/foundation/http_cache.dart +++ b/kraken/lib/src/foundation/http_cache.dart @@ -93,10 +93,9 @@ class HttpCacheController { HttpClientRequest request, HttpClientResponse response, HttpCacheObject cacheObject) async { - await cacheObject.updateIndex(response); - // Handle with HTTP 304 + // Negotiate cache with HTTP 304 if (response.statusCode == HttpStatus.notModified) { HttpClientResponse? cachedResponse = await cacheObject.toHttpClientResponse(); if (cachedResponse != null) { @@ -104,7 +103,7 @@ class HttpCacheController { } } - if (response.statusCode == HttpStatus.ok && response is! HttpClientCachedResponse) { + if (response.statusCode == HttpStatus.ok) { // Create cache object. HttpCacheObject cacheObject = HttpCacheObject .fromResponse( @@ -162,7 +161,7 @@ class HttpClientCachedResponse extends Stream> implements HttpClientRe void Function(List event)? onData, { Function? onError, void Function()? onDone, bool? cancelOnError }) { - _blobSink = cacheObject.openBlobWrite(); + _blobSink ??= cacheObject.openBlobWrite(); void _handleData(List data) { if (onData != null) onData(data); diff --git a/kraken/lib/src/foundation/http_cache_object.dart b/kraken/lib/src/foundation/http_cache_object.dart index 119f8c76d5..b9202e10af 100644 --- a/kraken/lib/src/foundation/http_cache_object.dart +++ b/kraken/lib/src/foundation/http_cache_object.dart @@ -8,12 +8,14 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as path; import 'http_client_response.dart'; class HttpCacheObject { + static const _httpHeaderCacheHits = 'cache-hits'; + static const _httpCacheHit = 'HIT'; + // The cached url of resource. String url; @@ -112,6 +114,8 @@ class HttpCacheObject { static int NetworkType = 0x01; static int Reserved = 0x00; + // This method write bytes in [Endian.little] order. + // Reference: https://en.wikipedia.org/wiki/Endianness static void writeString(BytesBuilder bytesBuilder, String str, int size) { final int strLength = str.length; for (int i = 0; i < size; i++) { @@ -127,30 +131,13 @@ class HttpCacheObject { } } - static List fromBytes(List list, int size) { - if (list.length % size != 0) { - throw ArgumentError('Wrong size'); - } - - final result = []; - for (var i = 0; i < list.length; i += size) { - var value = 0; - for (var j = 0; j < size; j++) { - var byte = list[i + j]; - final val = (byte & 0xff) << (j * 8); - value |= val; - } - - result.add(value); - } - - return result; - } - bool isDateTimeValid() => expiredTime != null && expiredTime!.isAfter(DateTime.now()); // Validate the cache-control and expires. - bool hitLocalCache(HttpClientRequest request) { + Future hitLocalCache(HttpClientRequest request) async { + if (!valid) { + await read(); + } return isDateTimeValid(); } @@ -164,33 +151,30 @@ class HttpCacheObject { try { Uint8List bytes = await _file.readAsBytes(); + ByteData byteData = bytes.buffer.asByteData(); int index = 0; // Reserved units. index += 4; // Read expiredTime. - Uint8List expiredTimestamp = bytes.sublist(index, index + 8); - expiredTime = DateTime.fromMillisecondsSinceEpoch(fromBytes(expiredTimestamp, 8).single); + expiredTime = DateTime.fromMillisecondsSinceEpoch(byteData.getUint64(index, Endian.little)); index += 8; // Read lastUsed. - Uint8List lastUsedTimestamp = bytes.sublist(index, index + 8); - lastUsed = DateTime.fromMillisecondsSinceEpoch(fromBytes(lastUsedTimestamp, 8).single); + lastUsed = DateTime.fromMillisecondsSinceEpoch(byteData.getUint64(index, Endian.little)); index += 8; // Read lastModified. - Uint8List lastModifiedTimestamp = bytes.sublist(index, index + 8); - lastModified = DateTime.fromMillisecondsSinceEpoch(fromBytes(lastModifiedTimestamp, 8).single); + lastModified = DateTime.fromMillisecondsSinceEpoch(byteData.getUint64(index, Endian.little)); index += 8; // Read contentLength. - contentLength = fromBytes(bytes.sublist(index, index + 4), 4).single; + contentLength = byteData.getUint32(index, Endian.little); index += 4; // Read url. - Uint8List urlLengthValue = bytes.sublist(index, index + 4); - int urlLength = fromBytes(urlLengthValue, 4).single; + int urlLength = byteData.getUint32(index, Endian.little); index += 4; Uint8List urlValue = bytes.sublist(index, index + urlLength); @@ -198,7 +182,7 @@ class HttpCacheObject { index += urlLength; // Read eTag. - int eTagLength = fromBytes(bytes.sublist(index, index + 2), 2).single; + int eTagLength = byteData.getUint16(index, Endian.little); index += 2; Uint8List eTagValue = bytes.sublist(index, index + eTagLength); @@ -221,7 +205,6 @@ class HttpCacheObject { NetworkType, Reserved, Reserved, Reserved, ]); - // | ExpiredTimeStamp x 8 | final int expiredTimeStamp = (expiredTime ?? alwaysExpired).millisecondsSinceEpoch; writeInteger(bytesBuilder, expiredTimeStamp, 8); @@ -271,7 +254,7 @@ class HttpCacheObject { if (expiredTime != null) HttpHeaders.expiresHeader: HttpDate.format(expiredTime!), if (contentLength != null) HttpHeaders.contentLengthHeader: contentLength.toString(), if (lastModified != null) HttpHeaders.lastModifiedHeader: HttpDate.format(lastModified!), - if (kDebugMode) 'x-kraken-cache': 'From http cache', + _httpHeaderCacheHits: _httpCacheHit, }; } @@ -284,8 +267,7 @@ class HttpCacheObject { return await _blob.exists(); } - Future toHttpClientResponse({ - HttpClientResponse? originalResponse }) async { + Future toHttpClientResponse() async { if (!await _exists) { return null; } diff --git a/kraken/lib/src/foundation/http_client_request.dart b/kraken/lib/src/foundation/http_client_request.dart index eb8bf8cf12..9e071e9291 100644 --- a/kraken/lib/src/foundation/http_client_request.dart +++ b/kraken/lib/src/foundation/http_client_request.dart @@ -117,7 +117,7 @@ class ProxyHttpClientRequest extends HttpClientRequest { // if hit, no need to open request. HttpCacheController cacheController = HttpCacheController.instance(origin); HttpCacheObject cacheObject = await cacheController.getCacheObject(request.uri); - if (cacheObject.hitLocalCache(request)) { + if (await cacheObject.hitLocalCache(request)) { HttpClientResponse? cacheResponse = await cacheObject.toHttpClientResponse(); if (cacheResponse != null) { return cacheResponse; @@ -146,16 +146,35 @@ class ProxyHttpClientRequest extends HttpClientRequest { response = await _shouldInterceptRequest(clientInterceptor, request); } + bool hitInterceptorResponse = response != null; + bool hitNegotiateCache = false; + // After this, response should not be null. - response ??= await _requestQueue.add(() async => cacheController - .interceptResponse(request, await request.close(), cacheObject)); + if (!hitInterceptorResponse) { + // Handle 304 here. + final HttpClientResponse rawResponse = await request.close(); + response = await _requestQueue.add(() async => cacheController + .interceptResponse(request, rawResponse, cacheObject)); + + hitNegotiateCache = rawResponse != response; + } // Step 5: Lifecycle of afterResponse. if (clientInterceptor != null) { - response = await _afterResponse(clientInterceptor, request, response!) ?? response; + final HttpClientResponse? interceptorResponse = await _afterResponse(clientInterceptor, request, response!); + if (interceptorResponse != null) { + hitInterceptorResponse = true; + response = interceptorResponse; + } + } + + // Check match cache, and then return cache. + if (hitInterceptorResponse || hitNegotiateCache) { + return Future.value(response); } - // Step 6: Intercept response by cache controller (handle 304). + // Step 6: Intercept response by cache controller. + // Note: No need to negotiate cache here, this is final response, hit or not hit. return cacheController.interceptResponse(request, response!, cacheObject); } else { _clientRequest.add(_data); diff --git a/kraken/lib/src/foundation/http_client_response.dart b/kraken/lib/src/foundation/http_client_response.dart index b0a624b892..48e8fad975 100644 --- a/kraken/lib/src/foundation/http_client_response.dart +++ b/kraken/lib/src/foundation/http_client_response.dart @@ -226,7 +226,7 @@ class HttpClientStreamResponse extends Stream> implements HttpClientRe HttpConnectionInfo get connectionInfo => _HttpConnectionInfo(80, InternetAddress.loopbackIPv4, 80); @override - int get contentLength => -1; + int get contentLength => headers.contentLength; @override List get cookies => []; @@ -255,6 +255,6 @@ class HttpClientStreamResponse extends Stream> implements HttpClientRe @override StreamSubscription> listen(void Function(List event)? onData, { Function? onError, void Function()? onDone, bool? cancelOnError }) { - return data.listen(onData, onDone: onDone, cancelOnError: cancelOnError); + return data.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } } diff --git a/kraken/test/fixtures/GET_js_over_128k b/kraken/test/fixtures/GET_js_over_128k new file mode 100644 index 0000000000..b6bae9077f --- /dev/null +++ b/kraken/test/fixtures/GET_js_over_128k @@ -0,0 +1,188 @@ +HTTP/1.1 200 OK +Date: Tue, 24 Aug 2021 03:45:23 GMT +Cache-Control: max-age=900 +Content-Type: application/javascript +Content-Length: 488796 +ETag: "02E1E2621F0192E3559631E94E2D2D6A" +Last-Modified: Fri, 04 Jun 2021 07:38:53 GMT + + +// {"framework" : "Rax"} + + +window.device = { + DeviceInfo: { + name: 'androidDeviceInfo.device', + }, + os: { + name: 'Android', + version: 'androidDeviceInfo.version.release' + }, + app: { + name: 'packageInfo.appName', + packageName: 'packageInfo.packageName', + buildNumber: 'packageInfo.buildNumber', + scene: '', + sceneInstanceId: '', + version: 'packageInfo.version' + }, + devicePixelRatio: 2.0, + display: { + safeLeft: 0.0, + safeTop: 0.0, + safeRight: 0.0, + safeBottom: 0.0 + } +}; + +//! frameworkData + +!function(t){var n={};function e(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}e.m=t,e.c=n,e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{enumerable:!0,get:r})},e.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},e.t=function(t,n){if(1&n&&(t=e(t)),8&n)return t;if(4&n&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(e.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&n&&"string"!=typeof t)for(var o in t)e.d(r,o,function(n){return t[n]}.bind(null,o));return r},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},e.p="",e(e.s=47)}([function(t,n){var e=t.exports={version:"2.6.11"};"number"==typeof __e&&(__e=e)},function(t,n,e){var r=e(25)("wks"),o=e(16),i=e(2).Symbol,u="function"==typeof i;(t.exports=function(t){return r[t]||(r[t]=u&&i[t]||(u?i:o)("Symbol."+t))}).store=r},function(t,n){var e=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=e)},function(t,n,e){t.exports=!e(11)((function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a}))},function(t,n,e){var r=e(8),o=e(38),i=e(21),u=Object.defineProperty;n.f=e(3)?Object.defineProperty:function(t,n,e){if(r(t),n=i(n,!0),r(e),o)try{return u(t,n,e)}catch(t){}if("get"in e||"set"in e)throw TypeError("Accessors not supported!");return"value"in e&&(t[n]=e.value),t}},function(t,n){var e={}.hasOwnProperty;t.exports=function(t,n){return e.call(t,n)}},function(t,n,e){var r=e(2),o=e(0),i=e(37),u=e(7),f=e(5),c=function(t,n,e){var a,s,l,p=t&c.F,d=t&c.G,y=t&c.S,v=t&c.P,h=t&c.B,b=t&c.W,g=d?o:o[n]||(o[n]={}),m=g.prototype,_=d?r:y?r[n]:(r[n]||{}).prototype;for(a in d&&(e=n),e)(s=!p&&_&&void 0!==_[a])&&f(g,a)||(l=s?_[a]:e[a],g[a]=d&&"function"!=typeof _[a]?e[a]:h&&s?i(l,r):b&&_[a]==l?function(t){var n=function(n,e,r){if(this instanceof t){switch(arguments.length){case 0:return new t;case 1:return new t(n);case 2:return new t(n,e)}return new t(n,e,r)}return t.apply(this,arguments)};return n.prototype=t.prototype,n}(l):v&&"function"==typeof l?i(Function.call,l):l,v&&((g.virtual||(g.virtual={}))[a]=l,t&c.R&&m&&!m[a]&&u(m,a,l)))};c.F=1,c.G=2,c.S=4,c.P=8,c.B=16,c.W=32,c.U=64,c.R=128,t.exports=c},function(t,n,e){var r=e(4),o=e(14);t.exports=e(3)?function(t,n,e){return r.f(t,n,o(1,e))}:function(t,n,e){return t[n]=e,t}},function(t,n,e){var r=e(9);t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},function(t,n){t.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},function(t,n,e){var r=e(42),o=e(20);t.exports=function(t){return r(o(t))}},function(t,n){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,n){t.exports={}},function(t,n){t.exports=!0},function(t,n){t.exports=function(t,n){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:n}}},function(t,n,e){var r=e(41),o=e(26);t.exports=Object.keys||function(t){return r(t,o)}},function(t,n){var e=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++e+r).toString(36))}},function(t,n){n.f={}.propertyIsEnumerable},function(t,n,e){"use strict";var r=e(51)(!0);e(36)(String,"String",(function(t){this._t=String(t),this._i=0}),(function(){var t,n=this._t,e=this._i;return e>=n.length?{value:void 0,done:!0}:(t=r(n,e),this._i+=t.length,{value:t,done:!1})}))},function(t,n){var e=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:e)(t)}},function(t,n){t.exports=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t}},function(t,n,e){var r=e(9);t.exports=function(t,n){if(!r(t))return t;var e,o;if(n&&"function"==typeof(e=t.toString)&&!r(o=e.call(t)))return o;if("function"==typeof(e=t.valueOf)&&!r(o=e.call(t)))return o;if(!n&&"function"==typeof(e=t.toString)&&!r(o=e.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},function(t,n,e){var r=e(8),o=e(54),i=e(26),u=e(24)("IE_PROTO"),f=function(){},c=function(){var t,n=e(39)("iframe"),r=i.length;for(n.style.display="none",e(58).appendChild(n),n.src="javascript:",(t=n.contentWindow.document).open(),t.write("