Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add StreamChannel.withCloseGuarantee.
This also properly enforces the close guarantee for transformers provided by this package. R=rnystrom@google.com Review URL: https://codereview.chromium.org//2041983003 .
- Loading branch information
Showing
6 changed files
with
189 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// Copyright (c) 2016, 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 'package:async/async.dart'; | ||
|
||
import '../stream_channel.dart'; | ||
|
||
/// A [StreamChannel] that specifically enforces the stream channel guarantee | ||
/// that closing the sink causes the stream to close before it emits any more | ||
/// events | ||
/// | ||
/// This is exposed via [new StreamChannel.withCloseGuarantee]. | ||
class CloseGuaranteeChannel<T> extends StreamChannelMixin<T> { | ||
Stream<T> get stream => _stream; | ||
_CloseGuaranteeStream<T> _stream; | ||
|
||
StreamSink<T> get sink => _sink; | ||
_CloseGuaranteeSink<T> _sink; | ||
|
||
/// The subscription to the inner stream. | ||
StreamSubscription<T> _subscription; | ||
|
||
/// Whether the sink has closed, causing the underlying channel to disconnect. | ||
bool _disconnected = false; | ||
|
||
CloseGuaranteeChannel(Stream<T> innerStream, StreamSink<T> innerSink) { | ||
_sink = new _CloseGuaranteeSink<T>(innerSink, this); | ||
_stream = new _CloseGuaranteeStream<T>(innerStream, this); | ||
} | ||
} | ||
|
||
/// The stream for [CloseGuaranteeChannel]. | ||
/// | ||
/// This wraps the inner stream to save the subscription on the channel when | ||
/// [listen] is called. | ||
class _CloseGuaranteeStream<T> extends Stream<T> { | ||
/// The inner stream this is delegating to. | ||
final Stream<T> _inner; | ||
|
||
/// The [CloseGuaranteeChannel] this belongs to. | ||
final CloseGuaranteeChannel<T> _channel; | ||
|
||
_CloseGuaranteeStream(this._inner, this._channel); | ||
|
||
StreamSubscription<T> listen(void onData(T event), | ||
{Function onError, void onDone(), bool cancelOnError}) { | ||
// If the channel is already disconnected, we shouldn't dispatch anything | ||
// but a done event. | ||
if (_channel._disconnected) { | ||
onData = null; | ||
onError = null; | ||
} | ||
|
||
var subscription = _inner.listen(onData, | ||
onError: onError, onDone: onDone, cancelOnError: cancelOnError); | ||
if (!_channel._disconnected) { | ||
_channel._subscription = subscription; | ||
} | ||
return subscription; | ||
} | ||
} | ||
|
||
/// The sink for [CloseGuaranteeChannel]. | ||
/// | ||
/// This wraps the inner sink to cancel the stream subscription when the sink is | ||
/// canceled. | ||
class _CloseGuaranteeSink<T> extends DelegatingStreamSink<T> { | ||
/// The [CloseGuaranteeChannel] this belongs to. | ||
final CloseGuaranteeChannel<T> _channel; | ||
|
||
_CloseGuaranteeSink(StreamSink<T> inner, this._channel) : super(inner); | ||
|
||
Future close() { | ||
var done = super.close(); | ||
_channel._disconnected = true; | ||
if (_channel._subscription != null) { | ||
// Don't dispatch anything but a done event. | ||
_channel._subscription.onData(null); | ||
_channel._subscription.onError(null); | ||
} | ||
return done; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// Copyright (c) 2016, 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 'package:async/async.dart'; | ||
import 'package:stream_channel/stream_channel.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
import 'utils.dart'; | ||
|
||
final _delayTransformer = new StreamTransformer.fromHandlers( | ||
handleData: (data, sink) => new Future.microtask(() => sink.add(data)), | ||
handleDone: (sink) => new Future.microtask(() => sink.close())); | ||
|
||
final _delaySinkTransformer = | ||
new StreamSinkTransformer.fromStreamTransformer(_delayTransformer); | ||
|
||
void main() { | ||
var controller; | ||
var channel; | ||
setUp(() { | ||
controller = new StreamChannelController(); | ||
|
||
// Add a bunch of layers of asynchronous dispatch between the channel and | ||
// the underlying controllers. | ||
var stream = controller.foreign.stream; | ||
var sink = controller.foreign.sink; | ||
for (var i = 0; i < 10; i++) { | ||
stream = stream.transform(_delayTransformer); | ||
sink = _delaySinkTransformer.bind(sink); | ||
} | ||
|
||
channel = new StreamChannel.withCloseGuarantee(stream, sink); | ||
}); | ||
|
||
test("closing the event sink causes the stream to close before it emits any " | ||
"more events", () async { | ||
controller.local.sink.add(1); | ||
controller.local.sink.add(2); | ||
controller.local.sink.add(3); | ||
|
||
expect(channel.stream.listen(expectAsync((event) { | ||
if (event == 2) channel.sink.close(); | ||
}, count: 2)).asFuture(), completes); | ||
|
||
await pumpEventQueue(); | ||
}); | ||
|
||
test("closing the event sink before events are emitted causes the stream to " | ||
"close immediately", () async { | ||
channel.sink.close(); | ||
channel.stream.listen( | ||
expectAsync((_) {}, count: 0), | ||
onError: expectAsync((_, __) {}, count: 0), | ||
onDone: expectAsync(() {})); | ||
|
||
controller.local.sink.add(1); | ||
controller.local.sink.add(2); | ||
controller.local.sink.add(3); | ||
controller.local.sink.close(); | ||
|
||
await pumpEventQueue(); | ||
}); | ||
} |