From 9b7bf84acfebdfee190ba1e5437361ce3274c1b9 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Wed, 7 Feb 2018 00:21:14 -0500 Subject: [PATCH] Deprecated `ResponseContext.io`, added HTTP equivalent --- .idea/workspace.xml | 180 +++++++++++++----------- lib/src/http/angel_http.dart | 3 +- lib/src/http/http_response_context.dart | 120 ++++++++++++++++ lib/src/http/response_context.dart | 165 +++++++--------------- lib/src/http/server.dart | 1 - 5 files changed, 272 insertions(+), 197 deletions(-) create mode 100644 lib/src/http/http_response_context.dart diff --git a/.idea/workspace.xml b/.idea/workspace.xml index adcfb23a..04c5ce5d 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,8 +2,11 @@ + + + @@ -25,16 +28,26 @@ - + - - + + - - + + + + + + + + + + + + - + @@ -44,11 +57,11 @@ - - + + - - + + @@ -70,12 +83,6 @@ - handleRequest - flatten - pipeline - pat - createR - print dispos xhr accepts @@ -100,6 +107,12 @@ handleRe instead. io\b + _isOpen + _isClosed + end + isOpen + _useStream + isClosed _isClosed @@ -123,6 +136,10 @@ autoSnakeCaseNames == false ? $0 : '$1ated_at' 'content-type' appa + isClosed + useStream + streaming + !isOpen C:\Users\thosa\Source\Angel\framework\lib @@ -149,7 +166,6 @@ @@ -613,14 +630,7 @@ - - - - 1497200046584 - 1497200256280 @@ -958,7 +968,14 @@ - @@ -994,7 +1011,7 @@ - @@ -1053,7 +1070,6 @@ - - - - - - - - @@ -1277,14 +1287,6 @@ - - - - - - - - @@ -1312,16 +1314,6 @@ - - - - - - - - - - @@ -1443,16 +1435,6 @@ - - - - - - - - - - @@ -1477,16 +1459,6 @@ - - - - - - - - - - @@ -1497,16 +1469,64 @@ - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/src/http/angel_http.dart b/lib/src/http/angel_http.dart index da3ad43f..0c0b12b3 100644 --- a/lib/src/http/angel_http.dart +++ b/lib/src/http/angel_http.dart @@ -8,6 +8,7 @@ import 'package:json_god/json_god.dart' as god; import 'package:pool/pool.dart'; import 'package:tuple/tuple.dart'; import 'http_request_context.dart'; +import 'http_response_context.dart'; import 'request_context.dart'; import 'response_context.dart'; import 'server.dart'; @@ -289,7 +290,7 @@ class AngelHttp { Future createResponseContext(HttpResponse response, [RequestContext correspondingRequest]) => new Future.value( - new ResponseContext(response, app, correspondingRequest) + new HttpResponseContextImpl(response, app, correspondingRequest) ..serializer = (app.serializer ?? god.serialize) ..encoders.addAll(app.encoders ?? {})); diff --git a/lib/src/http/http_response_context.dart b/lib/src/http/http_response_context.dart new file mode 100644 index 00000000..6ed7bd03 --- /dev/null +++ b/lib/src/http/http_response_context.dart @@ -0,0 +1,120 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'http_request_context.dart'; +import 'request_context.dart'; +import 'response_context.dart'; +import 'server.dart'; + +class HttpResponseContextImpl extends ResponseContext { + /// The underlying [HttpResponse] under this instance. + @override + final HttpResponse io; + Angel app; + + final HttpRequestContextImpl _correspondingRequest; + bool _isClosed = false, _useStream = false; + + HttpResponseContextImpl(this.io, this.app, [this._correspondingRequest]); + + @override + RequestContext get correspondingRequest { + return _correspondingRequest; + } + + @override + bool get streaming { + return _useStream; + } + + @override + void addError(Object error, [StackTrace stackTrace]) { + io.addError(error, stackTrace); + super.addError(error, stackTrace); + } + + @override + bool useStream() { + if (!_useStream) { + // If this is the first stream added to this response, + // then add headers, status code, etc. + io + ..statusCode = statusCode + ..cookies.addAll(cookies); + headers.forEach(io.headers.set); + willCloseItself = _useStream = _isClosed = true; + releaseCorrespondingRequest(); + return true; + } + + return false; + } + + @override + void end() { + _isClosed = true; + super.end(); + } + + @override + Future addStream(Stream> stream) { + if (_isClosed && !_useStream) throw ResponseContext.closed(); + var firstStream = useStream(); + + Stream> output = stream; + + if (encoders.isNotEmpty && correspondingRequest != null) { + var allowedEncodings = + (correspondingRequest.headers[HttpHeaders.ACCEPT_ENCODING] ?? []) + .map((str) { + // Ignore quality specifications in accept-encoding + // ex. gzip;q=0.8 + if (!str.contains(';')) return str; + return str.split(';')[0]; + }); + + for (var encodingName in allowedEncodings) { + Converter, List> encoder; + String key = encodingName; + + if (encoders.containsKey(encodingName)) + encoder = encoders[encodingName]; + else if (encodingName == '*') { + encoder = encoders[key = encoders.keys.first]; + } + + if (encoder != null) { + if (firstStream) { + io.headers.set(HttpHeaders.CONTENT_ENCODING, key); + } + + output = encoders[key].bind(output); + break; + } + } + } + + return io.addStream(output); + } + + @override + void add(List data) { + if (_isClosed && !_useStream) + throw ResponseContext.closed(); + else if (_useStream) + io.add(data); + else + buffer.add(data); + } + + @override + Future close() async { + if (_useStream) { + await io.close(); + } + + _isClosed = true; + await super.close(); + _useStream = false; + } +} \ No newline at end of file diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index 8a5d3c19..15aaba43 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -23,14 +23,12 @@ final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); typedef String ResponseSerializer(data); /// A convenience wrapper around an outgoing HTTP request. -class ResponseContext implements StreamSink>, StringSink { +abstract class ResponseContext implements StreamSink>, StringSink { final Map properties = {}; final BytesBuilder _buffer = new _LockableBytesBuilder(); final Map _headers = {HttpHeaders.SERVER: 'angel'}; - final RequestContext _correspondingRequest; Completer _done; - bool _isOpen = true, _isClosed = false, _useStream = false; int _statusCode = 200; /// The [Angel] instance that is sending a response. @@ -48,7 +46,7 @@ class ResponseContext implements StreamSink>, StringSink { final Map, List>> encoders = {}; /// Points to the [RequestContext] corresponding to this response. - RequestContext get correspondingRequest => _correspondingRequest; + RequestContext get correspondingRequest; @override Future get done => (_done ?? new Completer()).future; @@ -56,7 +54,7 @@ class ResponseContext implements StreamSink>, StringSink { /// Headers that will be sent to the user. Map get headers { /// If the response is closed, then this getter will return an immutable `Map`. - if (_isClosed) + if (!isOpen) return new Map.unmodifiable(_headers); else return _headers; @@ -80,20 +78,23 @@ class ResponseContext implements StreamSink>, StringSink { int get statusCode => _statusCode; void set statusCode(int value) { - if (_isClosed) - throw _closed(); + if (!isOpen) + throw closed(); else _statusCode = value ?? 200; } /// Can we still write to this response? - bool get isOpen => _isOpen && !_isClosed; + bool get isOpen => !!isOpen; + + /// Returns `true` if a [Stream] is being written directly. + bool get streaming; /// A set of UTF-8 encoded bytes that will be written to the response. BytesBuilder get buffer => _buffer; /// The underlying [HttpResponse] under this instance. - final HttpResponse io; + HttpResponse get io; /// Gets the Content-Type header. ContentType get contentType { @@ -116,25 +117,23 @@ class ResponseContext implements StreamSink>, StringSink { headers[HttpHeaders.CONTENT_TYPE] = contentType.toString(); } - ResponseContext(this.io, this.app, [this._correspondingRequest]); - /// Set this to true if you will manually close the response. /// /// If `true`, all response finalizers will be skipped. bool willCloseItself = false; - StateError _closed() => new StateError('Cannot modify a closed response.'); + static StateError closed() => new StateError('Cannot modify a closed response.'); /// Sends a download as a response. Future download(File file, {String filename}) async { - if (!_isOpen) throw _closed(); + if (!isOpen) throw closed(); headers["Content-Disposition"] = 'attachment; filename="${filename ?? file.path}"'; headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path); headers[HttpHeaders.CONTENT_LENGTH] = file.lengthSync().toString(); - if (_useStream) { + if (streaming) { await file.openRead().pipe(this); } else { buffer.add(await file.readAsBytes()); @@ -143,22 +142,17 @@ class ResponseContext implements StreamSink>, StringSink { } /// Prevents more data from being written to the response, and locks it entire from further editing. + /// + /// This method should be overwritten, setting [streaming] to `false`, **after** a `super` call. Future close() { - var f = new Future.value(); - - if (_useStream) { - _useStream = false; + if (streaming) { _buffer?.clear(); - f = io.close(); } else if (_buffer is _LockableBytesBuilder) { (_buffer as _LockableBytesBuilder)._lock(); } - _isOpen = _useStream = false; - _isClosed = true; - if (_done?.isCompleted == false) _done.complete(); - return f; + return new Future.value(); } /// Disposes of all resources. @@ -176,8 +170,9 @@ class ResponseContext implements StreamSink>, StringSink { /// Prevents further request handlers from running on the response, except for response finalizers. /// /// To disable response finalizers, see [willCloseItself]. + /// + /// This method should also set [!isOpen] to true. void end() { - _isOpen = false; if (_done?.isCompleted == false) _done.complete(); } @@ -186,7 +181,7 @@ class ResponseContext implements StreamSink>, StringSink { /// Returns a JSONP response. void jsonp(value, {String callbackName: "callback", contentType}) { - if (_isClosed) throw _closed(); + if (!isOpen) throw closed(); write("$callbackName(${serializer(value)})"); if (contentType != null) { @@ -202,7 +197,7 @@ class ResponseContext implements StreamSink>, StringSink { /// Renders a view to the response stream, and closes the response. Future render(String view, [Map data]) async { - if (_isClosed) throw _closed(); + if (!isOpen) throw closed(); write(await app.viewGenerator(view, data)); headers[HttpHeaders.CONTENT_TYPE] = ContentType.HTML.toString(); end(); @@ -216,7 +211,7 @@ class ResponseContext implements StreamSink>, StringSink { /// /// See [Router]#navigate for more. :) void redirect(url, {bool absolute: true, int code: 302}) { - if (_isClosed) throw _closed(); + if (!isOpen) throw closed(); headers ..[HttpHeaders.CONTENT_TYPE] = ContentType.HTML.toString() ..[HttpHeaders.LOCATION] = @@ -244,7 +239,7 @@ class ResponseContext implements StreamSink>, StringSink { /// Redirects to the given named [Route]. void redirectTo(String name, [Map params, int code]) { - if (_isClosed) throw _closed(); + if (!isOpen) throw closed(); Route _findRoute(Router r) { for (Route route in r.routes) { if (route is SymlinkRoute) { @@ -269,7 +264,7 @@ class ResponseContext implements StreamSink>, StringSink { /// Redirects to the given [Controller] action. void redirectToAction(String action, [Map params, int code]) { - if (_isClosed) throw _closed(); + if (!isOpen) throw closed(); // UserController@show List split = action.split("@"); @@ -298,7 +293,7 @@ class ResponseContext implements StreamSink>, StringSink { /// Copies a file's contents into the response buffer. Future sendFile(File file) async { - if (_isClosed) throw _closed(); + if (!isOpen) throw closed(); headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path); buffer.add(await file.readAsBytes()); @@ -309,7 +304,7 @@ class ResponseContext implements StreamSink>, StringSink { /// /// [contentType] can be either a [String], or a [ContentType]. void serialize(value, {contentType}) { - if (_isClosed) throw _closed(); + if (!isOpen) throw closed(); var text = serializer(value); @@ -329,99 +324,39 @@ class ResponseContext implements StreamSink>, StringSink { /// /// You can optionally transform the file stream with a [codec]. Future streamFile(File file) { - if (_isClosed) throw _closed(); + if (!isOpen) throw closed(); headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path); return file.openRead().pipe(this); } - @override - void add(List data) { - if (_isClosed && !_useStream) - throw _closed(); - else if (_useStream) - io.add(data); - else - buffer.add(data); + /// Releases critical resources from the [correspondingRequest]. + void releaseCorrespondingRequest() { + if (correspondingRequest?.injections?.containsKey(Stopwatch) == true) { + (correspondingRequest.injections[Stopwatch] as Stopwatch).stop(); + } + + if (correspondingRequest?.injections?.containsKey(PoolResource) == + true) { + (correspondingRequest.injections[PoolResource] as PoolResource) + .release(); + } } /// Configure the response to write directly to the output stream, instead of buffering. - bool useStream() { - if (!_useStream) { - // If this is the first stream added to this response, - // then add headers, status code, etc. - io - ..statusCode = statusCode - ..cookies.addAll(cookies); - headers.forEach(io.headers.set); - willCloseItself = _useStream = _isClosed = true; + bool useStream(); - if (_correspondingRequest?.injections?.containsKey(Stopwatch) == true) { - (_correspondingRequest.injections[Stopwatch] as Stopwatch).stop(); - } - - if (_correspondingRequest?.injections?.containsKey(PoolResource) == - true) { - (_correspondingRequest.injections[PoolResource] as PoolResource) - .release(); - } - - return true; - } - - return false; - } - - /// Adds a stream directly the underlying dart:[io] response. + /// Adds a stream directly the underlying response. /// /// This will also set [willCloseItself] to `true`, thus canceling out response finalizers. /// /// If this instance has access to a [correspondingRequest], then it will attempt to transform /// the content using at most one of the response [encoders]. @override - Future addStream(Stream> stream) { - if (_isClosed && !_useStream) throw _closed(); - var firstStream = useStream(); - - Stream> output = stream; - - if (encoders.isNotEmpty && correspondingRequest != null) { - var allowedEncodings = - (correspondingRequest.headers[HttpHeaders.ACCEPT_ENCODING] ?? []) - .map((str) { - // Ignore quality specifications in accept-encoding - // ex. gzip;q=0.8 - if (!str.contains(';')) return str; - return str.split(';')[0]; - }); - - for (var encodingName in allowedEncodings) { - Converter, List> encoder; - String key = encodingName; - - if (encoders.containsKey(encodingName)) - encoder = encoders[encodingName]; - else if (encodingName == '*') { - encoder = encoders[key = encoders.keys.first]; - } - - if (encoder != null) { - if (firstStream) { - io.headers.set(HttpHeaders.CONTENT_ENCODING, key); - } - - output = encoders[key].bind(output); - break; - } - } - } - - return io.addStream(output); - } + Future addStream(Stream> stream); @override void addError(Object error, [StackTrace stackTrace]) { - io.addError(error, stackTrace); if (_done?.isCompleted == false) _done.completeError(error, stackTrace); } @@ -429,13 +364,13 @@ class ResponseContext implements StreamSink>, StringSink { void write(value, {Encoding encoding}) { encoding ??= UTF8; - if (_isClosed && !_useStream) - throw _closed(); - else if (_useStream) { + if (!isOpen && !streaming) + throw closed(); + else if (streaming) { if (value is List) - io.add(value); + add(value); else - io.add(encoding.encode(value.toString())); + add(encoding.encode(value.toString())); } else { if (value is List) buffer.add(value); @@ -446,10 +381,10 @@ class ResponseContext implements StreamSink>, StringSink { @override void writeCharCode(int charCode) { - if (_isClosed && !_useStream) - throw _closed(); - else if (_useStream) - io.add([charCode]); + if (!isOpen && !streaming) + throw closed(); + else if (streaming) + add([charCode]); else buffer.addByte(charCode); } diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index c8ea6eb9..18ede8bb 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -40,7 +40,6 @@ class Angel extends AngelBase { AngelHttp _http; bool _isProduction; Angel _parent; - Future Function(dynamic, int) _serverGenerator = HttpServer.bind; /// A global Map of converters that can transform responses bodies. final Map, List>> encoders = {};