-
Notifications
You must be signed in to change notification settings - Fork 49
Add *SinkBase classes for implementing custom sinks #188
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file | ||
// for details. 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:async'; | ||
import 'dart:convert'; | ||
|
||
import 'package:meta/meta.dart'; | ||
|
||
import 'async_memoizer.dart'; | ||
|
||
/// An abstract class that implements [EventSink] in terms of [onData], | ||
/// [onError], and [onClose] methods. | ||
/// | ||
/// This takes care of ensuring that events can't be added after [close] is | ||
/// called. | ||
abstract class EventSinkBase<T> implements EventSink<T> { | ||
/// Whether [close] has been called and no more events may be written. | ||
bool get _closed => _closeMemo.hasRun; | ||
|
||
@override | ||
void add(T data) { | ||
_checkCanAddEvent(); | ||
onAdd(data); | ||
} | ||
|
||
/// A method that handles data events that are passed to the sink. | ||
@visibleForOverriding | ||
void onAdd(T data); | ||
|
||
@override | ||
void addError(Object error, [StackTrace? stackTrace]) { | ||
_checkCanAddEvent(); | ||
onError(error, stackTrace); | ||
} | ||
|
||
/// A method that handles error events that are passed to the sink. | ||
@visibleForOverriding | ||
void onError(Object error, [StackTrace? stackTrace]); | ||
|
||
@override | ||
Future<void> close() => _closeMemo.runOnce(onClose); | ||
final _closeMemo = AsyncMemoizer<void>(); | ||
|
||
/// A method that handles the sink being closed. | ||
/// | ||
/// This may return a future that completes once the stream sink has shut | ||
/// down. If cleaning up can fail, the error may be reported in the returned | ||
/// future. | ||
@visibleForOverriding | ||
FutureOr<void> onClose(); | ||
|
||
/// Asserts that the sink is in a state where adding an event is valid. | ||
void _checkCanAddEvent() { | ||
if (_closed) throw StateError('Cannot add event after closing'); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be better to have a Don't mingle the sink and the response APIs into one interface. Don't use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not use "Platform-adjacent packages" are only what you declare them to be, and There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think annotation-based "language extensions" like these are fine for applications, but also that they do not belong in reusable package APIs. Such an API should not expose something that you are not intending to actually expose, because users can, and will, use them as if they were just public. |
||
|
||
/// An abstract class that implements [StreamSink] in terms of [onData], | ||
/// [onError], and [onClose] methods. | ||
/// | ||
/// This takes care of ensuring that events can't be added after [close] is | ||
/// called or during a call to [onStream]. | ||
abstract class StreamSinkBase<T> extends EventSinkBase<T> | ||
implements StreamSink<T> { | ||
/// Whether a call to [addStream] is ongoing. | ||
bool _addingStream = false; | ||
|
||
@override | ||
Future<void> get done => _closeMemo.future; | ||
|
||
@override | ||
Future<void> addStream(Stream<T> stream) { | ||
_checkCanAddEvent(); | ||
|
||
_addingStream = true; | ||
var completer = Completer<void>.sync(); | ||
stream.listen(onAdd, onError: onError, onDone: () { | ||
_addingStream = false; | ||
completer.complete(); | ||
}); | ||
return completer.future; | ||
} | ||
|
||
@override | ||
Future<void> close() { | ||
if (_addingStream) throw StateError('StreamSink is bound to a stream'); | ||
return super.close(); | ||
} | ||
|
||
@override | ||
void _checkCanAddEvent() { | ||
super._checkCanAddEvent(); | ||
if (_addingStream) throw StateError('StreamSink is bound to a stream'); | ||
} | ||
} | ||
|
||
/// An abstract class that implements `dart:io`'s [IOSink]'s API in terms of | ||
/// [onData], [onError], [onClose], and [onFlush] methods. | ||
/// | ||
/// Because [IOSink] is defined in `dart:io`, this can't officially implement | ||
/// it. However, it's designed to match its API exactly so that subclasses can | ||
/// implement [IOSink] without any additional modifications. | ||
/// | ||
/// This takes care of ensuring that events can't be added after [close] is | ||
/// called or during a call to [onStream]. | ||
abstract class IOSinkBase extends StreamSinkBase<List<int>> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure this class belongs in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I originally tried adding this to Also, I don't think |
||
/// See [IOSink.encoding] from `dart:io`. | ||
Encoding encoding; | ||
|
||
IOSinkBase([this.encoding = utf8]); | ||
|
||
/// See [IOSink.flush] from `dart:io`. | ||
/// | ||
/// Because this base class doesn't do any buffering of its own, [flush] | ||
/// always completes immediately. | ||
/// | ||
/// Subclasses that do buffer events should override [flush] to complete once | ||
/// all events are delivered. They should also call `super.flush()` at the | ||
/// beginning of the method to throw a [StateError] if the sink is currently | ||
/// adding a stream. | ||
Future<void> flush() { | ||
if (_addingStream) throw StateError('StreamSink is bound to a stream'); | ||
if (_closed) return Future.value(); | ||
|
||
_addingStream = true; | ||
return onFlush().whenComplete(() { | ||
_addingStream = false; | ||
}); | ||
} | ||
|
||
/// Flushes any buffered data to the underlying consumer, and returns a future | ||
/// that completes once the consumer has accepted all data. | ||
@visibleForOverriding | ||
Future<void> onFlush(); | ||
|
||
/// See [IOSink.write] from `dart:io`. | ||
void write(Object? object) { | ||
var string = object.toString(); | ||
if (string.isEmpty) return; | ||
add(encoding.encode(string)); | ||
} | ||
|
||
/// See [IOSink.writeAll] from `dart:io`. | ||
void writeAll(Iterable<Object?> objects, [String separator = '']) { | ||
var first = true; | ||
for (var object in objects) { | ||
if (first) { | ||
first = false; | ||
} else { | ||
write(separator); | ||
} | ||
|
||
write(object); | ||
} | ||
} | ||
|
||
/// See [IOSink.writeln] from `dart:io`. | ||
void writeln([Object? object = '']) { | ||
write(object); | ||
write('\n'); | ||
} | ||
|
||
/// See [IOSink.writeCharCode] from `dart:io`. | ||
void writeCharCode(int charCode) { | ||
write(String.fromCharCode(charCode)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
name: async | ||
version: 2.7.1-dev | ||
version: 2.8.0 | ||
|
||
description: Utility functions and classes related to the 'dart:async' library. | ||
repository: https://github.com/dart-lang/async | ||
|
@@ -12,6 +12,7 @@ dependencies: | |
meta: ^1.1.7 | ||
|
||
dev_dependencies: | ||
charcode: ^1.3.0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't depend on charcode! it's not a core Dart package! You can use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not? This isn't even a normal dependency, it's only used for tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dart-core packages should not depend on non-dart-core packages (unless absolutely necessary and with good arguments). |
||
fake_async: ^1.2.0 | ||
pedantic: ^1.10.0 | ||
stack_trace: ^1.10.0 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file | ||
// for details. 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:io'; | ||
|
||
import 'package:async/async.dart'; | ||
|
||
/// This class isn't used, it's just used to verify that [IOSinkBase] produces a | ||
/// valid implementation of [IOSink]. | ||
class IOSinkImpl extends IOSinkBase implements IOSink { | ||
@override | ||
void onAdd(List<int> data) {} | ||
|
||
@override | ||
void onError(Object error, [StackTrace? stackTrace]) {} | ||
|
||
@override | ||
void onClose() {} | ||
|
||
@override | ||
Future<void> onFlush() => Future.value(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having an
EventSink
return aFuture
is dangerous since the return type ofEventSink.close
isvoid
. That means that no code expecting anEventSink
will handle the future.Returning a future here is misleading and likely to cause problems for users.