Skip to content
This repository was archived by the owner on Oct 17, 2024. It is now read-only.

Add *SinkBase classes for implementing custom sinks #188

Merged
merged 2 commits into from
Jul 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## 2.7.1-dev
## 2.8.0

* Add `EventSinkBase`, `StreamSinkBase`, and `IOSinkBase` classes to make it
easier to implement custom sinks.
* Improve performance for `ChunkedStreamReader` by creating fewer internal
sublists and specializing to create views for `Uint8List` chunks.

Expand Down
1 change: 1 addition & 0 deletions lib/async.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export 'src/result/error.dart';
export 'src/result/future.dart';
export 'src/result/value.dart';
export 'src/single_subscription_transformer.dart';
export 'src/sink_base.dart';
export 'src/stream_closer.dart';
export 'src/stream_completer.dart';
export 'src/stream_extensions.dart';
Expand Down
168 changes: 168 additions & 0 deletions lib/src/sink_base.dart
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having an EventSink return a Future is dangerous since the return type of EventSink.close is void. That means that no code expecting an EventSink will handle the future.
Returning a future here is misleading and likely to cause problems for users.

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');
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to have a SinkBase which takes onData/onError/onDone as function arguments, or as a single object (could perhaps even be a StreamSubscription-like object).

Don't mingle the sink and the response APIs into one interface.

Don't use @visibleForOverriding. We generally don't use annotations from package:meta in the platform-adjacent packages. (Also not necessary if you don't merge the interfaces)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use package:meta and @visibleForOverriding if they result in better APIs? Using inheritance requires the creation of fewer intermediate objects, which results in less complexity and more efficiency.

"Platform-adjacent packages" are only what you declare them to be, and meta is certainly a reasonable inclusion since it tightly integrates into the platform.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.
So, it's not a better API to me, it's a messier and less structured API, one which mixes different responsibilities in the same interface.


/// 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>> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this class belongs in package:async because we can't depend on dart:io. I'd create a package:io instead if anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally tried adding this to package:io, but it wants to share so much implementation with StreamSinkBase it would have produced a ton of duplication.

Also, I don't think IOSink belongs in dart:io in the first place, since nothing about its API requires or implies access to system IO. See dart-lang/sdk#17293.

/// 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));
}
}
3 changes: 2 additions & 1 deletion pubspec.yaml
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
Expand All @@ -12,6 +12,7 @@ dependencies:
meta: ^1.1.7

dev_dependencies:
charcode: ^1.3.0
Copy link
Contributor

Choose a reason for hiding this comment

The 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 package:charcode to generate the constants you need instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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).
If we do another breaking change like null safety, we don't want our tests to be stuck on waiting for migration of a package that doesn't have similar support promises to the the core package itself. Even if it's only a dev-dependency, we'd still potentially be held back in migrating the tests, which are just as important as the rest of the package.

fake_async: ^1.2.0
pedantic: ^1.10.0
stack_trace: ^1.10.0
Expand Down
23 changes: 23 additions & 0 deletions test/io_sink_impl.dart
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();
}
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy