├── analysis_options.yaml ├── .gitignore ├── AUTHORS ├── lib ├── isolate_channel.dart ├── src │ ├── delegating_stream_channel.dart │ ├── json_document_transformer.dart │ ├── stream_channel_transformer.dart │ ├── stream_channel_completer.dart │ ├── stream_channel_controller.dart │ ├── close_guarantee_channel.dart │ ├── isolate_channel.dart │ ├── disconnector.dart │ ├── guarantee_channel.dart │ └── multi_channel.dart └── stream_channel.dart ├── pubspec.yaml ├── .github ├── dependabot.yml └── workflows │ ├── publish.yaml │ ├── no-response.yml │ └── test-package.yml ├── README.md ├── LICENSE ├── CONTRIBUTING.md ├── test ├── json_document_transformer_test.dart ├── with_close_guarantee_test.dart ├── stream_channel_controller_test.dart ├── stream_channel_completer_test.dart ├── stream_channel_test.dart ├── disconnector_test.dart ├── isolate_channel_test.dart ├── with_guarantees_test.dart └── multi_channel_test.dart ├── CHANGELOG.md └── example └── example.dart /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:dart_flutter_team_lints/analysis_options.yaml 2 | 3 | analyzer: 4 | language: 5 | strict-casts: true 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .buildlog 2 | .dart_tool/ 3 | .DS_Store 4 | .idea 5 | .pub/ 6 | .settings/ 7 | build/ 8 | packages 9 | .packages 10 | pubspec.lock 11 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | -------------------------------------------------------------------------------- /lib/isolate_channel.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | export 'src/isolate_channel.dart' show IsolateChannel; 6 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: stream_channel 2 | version: 2.1.3-wip 3 | description: >- 4 | An abstraction for two-way communication channels based on the Dart Stream 5 | class. 6 | repository: https://github.com/dart-lang/stream_channel 7 | 8 | environment: 9 | sdk: ^3.3.0 10 | 11 | dependencies: 12 | async: ^2.5.0 13 | 14 | dev_dependencies: 15 | dart_flutter_team_lints: ^3.0.0 16 | test: ^1.16.6 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | # See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | schedule: 9 | interval: monthly 10 | labels: 11 | - autosubmit 12 | groups: 13 | github-actions: 14 | patterns: 15 | - "*" 16 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # A CI configuration to auto-publish pub packages. 2 | 3 | name: Publish 4 | 5 | on: 6 | pull_request: 7 | branches: [ master ] 8 | push: 9 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 10 | 11 | jobs: 12 | publish: 13 | if: ${{ github.repository_owner == 'dart-lang' }} 14 | uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main 15 | permissions: 16 | id-token: write # Required for authentication using OIDC 17 | pull-requests: write # Required for writing the pull request note 18 | -------------------------------------------------------------------------------- /lib/src/delegating_stream_channel.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import '../stream_channel.dart'; 8 | 9 | /// A simple delegating wrapper around [StreamChannel]. 10 | /// 11 | /// Subclasses can override individual methods, or use this to expose only 12 | /// [StreamChannel] methods. 13 | class DelegatingStreamChannel extends StreamChannelMixin { 14 | /// The inner channel to which methods are forwarded. 15 | final StreamChannel _inner; 16 | 17 | @override 18 | Stream get stream => _inner.stream; 19 | @override 20 | StreamSink get sink => _inner.sink; 21 | 22 | DelegatingStreamChannel(this._inner); 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repo has moved to https://github.com/dart-lang/tools/tree/main/pkgs/stream_channel 3 | 4 | [![Dart CI](https://github.com/dart-lang/stream_channel/actions/workflows/test-package.yml/badge.svg)](https://github.com/dart-lang/stream_channel/actions/workflows/test-package.yml) 5 | [![pub package](https://img.shields.io/pub/v/stream_channel.svg)](https://pub.dev/packages/stream_channel) 6 | [![package publisher](https://img.shields.io/pub/publisher/stream_channel.svg)](https://pub.dev/packages/stream_channel/publisher) 7 | 8 | This package exposes the `StreamChannel` interface, which represents a two-way 9 | communication channel. Each `StreamChannel` exposes a `Stream` for receiving 10 | data and a `StreamSink` for sending it. 11 | 12 | `StreamChannel` helps abstract communication logic away from the underlying 13 | protocol. For example, the [`test`][test] package re-uses its test suite 14 | communication protocol for both WebSocket connections to browser suites and 15 | Isolate connections to VM tests. 16 | 17 | [test]: https://pub.dev/packages/test 18 | 19 | This package also contains utilities for dealing with `StreamChannel`s and with 20 | two-way communications in general. For documentation of these utilities, see 21 | [the API docs][api]. 22 | 23 | [api]: https://pub.dev/documentation/stream_channel/latest/ 24 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | # A workflow to close issues where the author hasn't responded to a request for 2 | # more information; see https://github.com/actions/stale. 3 | 4 | name: No Response 5 | 6 | # Run as a daily cron. 7 | on: 8 | schedule: 9 | # Every day at 8am 10 | - cron: '0 8 * * *' 11 | 12 | # All permissions not specified are set to 'none'. 13 | permissions: 14 | issues: write 15 | pull-requests: write 16 | 17 | jobs: 18 | no-response: 19 | runs-on: ubuntu-latest 20 | if: ${{ github.repository_owner == 'dart-lang' }} 21 | steps: 22 | - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 23 | with: 24 | # Don't automatically mark inactive issues+PRs as stale. 25 | days-before-stale: -1 26 | # Close needs-info issues and PRs after 14 days of inactivity. 27 | days-before-close: 14 28 | stale-issue-label: "needs-info" 29 | close-issue-message: > 30 | Without additional information we're not able to resolve this issue. 31 | Feel free to add more info or respond to any questions above and we 32 | can reopen the case. Thanks for your contribution! 33 | stale-pr-label: "needs-info" 34 | close-pr-message: > 35 | Without additional information we're not able to resolve this PR. 36 | Feel free to add more info or respond to any questions above. 37 | Thanks for your contribution! 38 | -------------------------------------------------------------------------------- /lib/src/json_document_transformer.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | 7 | import 'package:async/async.dart'; 8 | 9 | import '../stream_channel.dart'; 10 | 11 | /// A [StreamChannelTransformer] that transforms JSON documents—strings that 12 | /// contain individual objects encoded as JSON—into decoded Dart objects. 13 | /// 14 | /// This decodes JSON that's emitted by the transformed channel's stream, and 15 | /// encodes objects so that JSON is passed to the transformed channel's sink. 16 | /// 17 | /// If the transformed channel emits invalid JSON, this emits a 18 | /// [FormatException]. If an unencodable object is added to the sink, it 19 | /// synchronously throws a [JsonUnsupportedObjectError]. 20 | final StreamChannelTransformer jsonDocument = 21 | const _JsonDocument(); 22 | 23 | class _JsonDocument implements StreamChannelTransformer { 24 | const _JsonDocument(); 25 | 26 | @override 27 | StreamChannel bind(StreamChannel channel) { 28 | var stream = channel.stream.map(jsonDecode); 29 | var sink = StreamSinkTransformer.fromHandlers( 30 | handleData: (data, sink) { 31 | sink.add(jsonEncode(data)); 32 | }).bind(channel.sink); 33 | return StreamChannel.withCloseGuarantee(stream, sink); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | Before we can use your code, you must sign the 6 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) 7 | (CLA), which you can do online. The CLA is necessary mainly because you own the 8 | copyright to your changes, even after your contribution becomes part of our 9 | codebase, so we need your permission to use and distribute your code. We also 10 | need to be sure of various other things—for instance that you'll tell us if you 11 | know that your code infringes on other people's patents. You don't have to sign 12 | the CLA until after you've submitted your code for review and a member has 13 | approved it, but you must do it before we can put your code into our codebase. 14 | 15 | Before you start working on a larger contribution, you should get in touch with 16 | us first through the issue tracker with your idea so that we can help out and 17 | possibly guide you. Coordinating up front makes it much easier to avoid 18 | frustration later on. 19 | 20 | ### Code reviews 21 | All submissions, including submissions by project members, require review. 22 | 23 | ### File headers 24 | All files in the project must start with the following header. 25 | 26 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 27 | // for details. All rights reserved. Use of this source code is governed by a 28 | // BSD-style license that can be found in the LICENSE file. 29 | 30 | ### The small print 31 | Contributions made by corporations are covered by a different agreement than the 32 | one above, the 33 | [Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). 34 | -------------------------------------------------------------------------------- /test/json_document_transformer_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | 8 | import 'package:stream_channel/stream_channel.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | void main() { 12 | late StreamController streamController; 13 | late StreamController sinkController; 14 | late StreamChannel channel; 15 | setUp(() { 16 | streamController = StreamController(); 17 | sinkController = StreamController(); 18 | channel = 19 | StreamChannel(streamController.stream, sinkController.sink); 20 | }); 21 | 22 | test('decodes JSON emitted by the channel', () { 23 | var transformed = channel.transform(jsonDocument); 24 | streamController.add('{"foo": "bar"}'); 25 | expect(transformed.stream.first, completion(equals({'foo': 'bar'}))); 26 | }); 27 | 28 | test('encodes objects added to the channel', () { 29 | var transformed = channel.transform(jsonDocument); 30 | transformed.sink.add({'foo': 'bar'}); 31 | expect(sinkController.stream.first, 32 | completion(equals(jsonEncode({'foo': 'bar'})))); 33 | }); 34 | 35 | test('emits a stream error when incoming JSON is malformed', () { 36 | var transformed = channel.transform(jsonDocument); 37 | streamController.add('{invalid'); 38 | expect(transformed.stream.first, throwsFormatException); 39 | }); 40 | 41 | test('synchronously throws if an unencodable object is added', () { 42 | var transformed = channel.transform(jsonDocument); 43 | expect(() => transformed.sink.add(Object()), 44 | throwsA(const TypeMatcher())); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | # Run on PRs and pushes to the default branch. 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | env: 13 | PUB_ENVIRONMENT: bot.github 14 | 15 | jobs: 16 | # Check code formatting and static analysis on a single OS (linux) 17 | # against Dart dev. 18 | analyze: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | sdk: [dev] 24 | steps: 25 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 26 | - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 27 | with: 28 | sdk: ${{ matrix.sdk }} 29 | - id: install 30 | name: Install dependencies 31 | run: dart pub get 32 | - name: Check formatting 33 | run: dart format --output=none --set-exit-if-changed . 34 | if: always() && steps.install.outcome == 'success' 35 | - name: Analyze code 36 | run: dart analyze --fatal-infos 37 | if: always() && steps.install.outcome == 'success' 38 | 39 | # Run tests on a matrix consisting of two dimensions: 40 | # 1. OS: ubuntu-latest, (macos-latest, windows-latest) 41 | # 2. release channel: dev 42 | test: 43 | needs: analyze 44 | runs-on: ${{ matrix.os }} 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | # Add macos-latest and/or windows-latest if relevant for this package. 49 | os: [ubuntu-latest] 50 | sdk: [3.3, dev] 51 | steps: 52 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 53 | - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 54 | with: 55 | sdk: ${{ matrix.sdk }} 56 | - id: install 57 | name: Install dependencies 58 | run: dart pub get 59 | - name: Run VM tests 60 | run: dart test --platform vm 61 | if: always() && steps.install.outcome == 'success' 62 | - name: Run Chrome tests 63 | run: dart test --platform chrome 64 | if: always() && steps.install.outcome == 'success' 65 | -------------------------------------------------------------------------------- /test/with_close_guarantee_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:async/async.dart'; 8 | import 'package:stream_channel/stream_channel.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | final _delayTransformer = StreamTransformer.fromHandlers( 12 | handleData: (data, sink) => Future.microtask(() => sink.add(data)), 13 | handleDone: (sink) => Future.microtask(() => sink.close())); 14 | 15 | final _delaySinkTransformer = 16 | StreamSinkTransformer.fromStreamTransformer(_delayTransformer); 17 | 18 | void main() { 19 | late StreamChannelController controller; 20 | late StreamChannel channel; 21 | setUp(() { 22 | controller = StreamChannelController(); 23 | 24 | // Add a bunch of layers of asynchronous dispatch between the channel and 25 | // the underlying controllers. 26 | var stream = controller.foreign.stream; 27 | var sink = controller.foreign.sink; 28 | for (var i = 0; i < 10; i++) { 29 | stream = stream.transform(_delayTransformer); 30 | sink = _delaySinkTransformer.bind(sink); 31 | } 32 | 33 | channel = StreamChannel.withCloseGuarantee(stream, sink); 34 | }); 35 | 36 | test( 37 | 'closing the event sink causes the stream to close before it emits any ' 38 | 'more events', () async { 39 | controller.local.sink.add(1); 40 | controller.local.sink.add(2); 41 | controller.local.sink.add(3); 42 | 43 | expect( 44 | channel.stream 45 | .listen(expectAsync1((event) { 46 | if (event == 2) channel.sink.close(); 47 | }, count: 2)) 48 | .asFuture(), 49 | completes); 50 | 51 | await pumpEventQueue(); 52 | }); 53 | 54 | test( 55 | 'closing the event sink before events are emitted causes the stream to ' 56 | 'close immediately', () async { 57 | unawaited(channel.sink.close()); 58 | channel.stream.listen(expectAsync1((_) {}, count: 0), 59 | onError: expectAsync2((_, __) {}, count: 0), 60 | onDone: expectAsync0(() {})); 61 | 62 | controller.local.sink.add(1); 63 | controller.local.sink.add(2); 64 | controller.local.sink.add(3); 65 | unawaited(controller.local.sink.close()); 66 | 67 | await pumpEventQueue(); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /lib/src/stream_channel_transformer.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | 8 | import 'package:async/async.dart'; 9 | 10 | import '../stream_channel.dart'; 11 | 12 | /// A [StreamChannelTransformer] transforms the events being passed to and 13 | /// emitted by a [StreamChannel]. 14 | /// 15 | /// This works on the same principle as [StreamTransformer] and 16 | /// [StreamSinkTransformer]. Each transformer defines a [bind] method that takes 17 | /// in the original [StreamChannel] and returns the transformed version. 18 | /// 19 | /// Transformers must be able to have [bind] called multiple times. If a 20 | /// subclass implements [bind] explicitly, it should be sure that the returned 21 | /// stream follows the second stream channel guarantee: closing the sink causes 22 | /// the stream to close before it emits any more events. This guarantee is 23 | /// invalidated when an asynchronous gap is added between the original stream's 24 | /// event dispatch and the returned stream's, for example by transforming it 25 | /// with a [StreamTransformer]. The guarantee can be easily preserved using 26 | /// [StreamChannel.withCloseGuarantee]. 27 | class StreamChannelTransformer { 28 | /// The transformer to use on the channel's stream. 29 | final StreamTransformer _streamTransformer; 30 | 31 | /// The transformer to use on the channel's sink. 32 | final StreamSinkTransformer _sinkTransformer; 33 | 34 | /// Creates a [StreamChannelTransformer] from existing stream and sink 35 | /// transformers. 36 | const StreamChannelTransformer( 37 | this._streamTransformer, this._sinkTransformer); 38 | 39 | /// Creates a [StreamChannelTransformer] from a codec's encoder and decoder. 40 | /// 41 | /// All input to the inner channel's sink is encoded using [Codec.encoder], 42 | /// and all output from its stream is decoded using [Codec.decoder]. 43 | StreamChannelTransformer.fromCodec(Codec codec) 44 | : this(codec.decoder, 45 | StreamSinkTransformer.fromStreamTransformer(codec.encoder)); 46 | 47 | /// Transforms the events sent to and emitted by [channel]. 48 | /// 49 | /// Creates a new channel. When events are passed to the returned channel's 50 | /// sink, the transformer will transform them and pass the transformed 51 | /// versions to `channel.sink`. When events are emitted from the 52 | /// `channel.straem`, the transformer will transform them and pass the 53 | /// transformed versions to the returned channel's stream. 54 | StreamChannel bind(StreamChannel channel) => 55 | StreamChannel.withCloseGuarantee( 56 | channel.stream.transform(_streamTransformer), 57 | _sinkTransformer.bind(channel.sink)); 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/stream_channel_completer.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:async/async.dart'; 6 | 7 | import '../stream_channel.dart'; 8 | 9 | /// A [channel] where the source and destination are provided later. 10 | /// 11 | /// The [channel] is a normal channel that can be listened to and that events 12 | /// can be added to immediately, but until [setChannel] is called it won't emit 13 | /// any events and all events added to it will be buffered. 14 | class StreamChannelCompleter { 15 | /// The completer for this channel's stream. 16 | final _streamCompleter = StreamCompleter(); 17 | 18 | /// The completer for this channel's sink. 19 | final _sinkCompleter = StreamSinkCompleter(); 20 | 21 | /// The channel for this completer. 22 | StreamChannel get channel => _channel; 23 | late final StreamChannel _channel; 24 | 25 | /// Whether [setChannel] has been called. 26 | bool _set = false; 27 | 28 | /// Convert a `Future` to a `StreamChannel`. 29 | /// 30 | /// This creates a channel using a channel completer, and sets the source 31 | /// channel to the result of the future when the future completes. 32 | /// 33 | /// If the future completes with an error, the returned channel's stream will 34 | /// instead contain just that error. The sink will silently discard all 35 | /// events. 36 | static StreamChannel fromFuture(Future channelFuture) { 37 | var completer = StreamChannelCompleter(); 38 | channelFuture.then(completer.setChannel, onError: completer.setError); 39 | return completer.channel; 40 | } 41 | 42 | StreamChannelCompleter() { 43 | _channel = StreamChannel(_streamCompleter.stream, _sinkCompleter.sink); 44 | } 45 | 46 | /// Set a channel as the source and destination for [channel]. 47 | /// 48 | /// A channel may be set at most once. 49 | /// 50 | /// Either [setChannel] or [setError] may be called at most once. Trying to 51 | /// call either of them again will fail. 52 | void setChannel(StreamChannel channel) { 53 | if (_set) throw StateError('The channel has already been set.'); 54 | _set = true; 55 | 56 | _streamCompleter.setSourceStream(channel.stream); 57 | _sinkCompleter.setDestinationSink(channel.sink); 58 | } 59 | 60 | /// Indicates that there was an error connecting the channel. 61 | /// 62 | /// This makes the stream emit [error] and close. It makes the sink discard 63 | /// all its events. 64 | /// 65 | /// Either [setChannel] or [setError] may be called at most once. Trying to 66 | /// call either of them again will fail. 67 | void setError(Object error, [StackTrace? stackTrace]) { 68 | if (_set) throw StateError('The channel has already been set.'); 69 | _set = true; 70 | 71 | _streamCompleter.setError(error, stackTrace); 72 | _sinkCompleter.setDestinationSink(NullStreamSink()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/stream_channel_controller.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// @docImport 'isolate_channel.dart'; 6 | library; 7 | 8 | import 'dart:async'; 9 | 10 | import '../stream_channel.dart'; 11 | 12 | /// A controller for exposing a new [StreamChannel]. 13 | /// 14 | /// This exposes two connected [StreamChannel]s, [local] and [foreign]. The 15 | /// user's code should use [local] to emit and receive events. Then [foreign] 16 | /// can be returned for others to use. For example, here's a simplified version 17 | /// of the implementation of [IsolateChannel.new]: 18 | /// 19 | /// ```dart 20 | /// StreamChannel isolateChannel(ReceivePort receivePort, SendPort sendPort) { 21 | /// var controller = new StreamChannelController(allowForeignErrors: false); 22 | /// 23 | /// // Pipe all events from the receive port into the local sink... 24 | /// receivePort.pipe(controller.local.sink); 25 | /// 26 | /// // ...and all events from the local stream into the send port. 27 | /// controller.local.stream.listen(sendPort.send, onDone: receivePort.close); 28 | /// 29 | /// // Then return the foreign controller for your users to use. 30 | /// return controller.foreign; 31 | /// } 32 | /// ``` 33 | class StreamChannelController { 34 | /// The local channel. 35 | /// 36 | /// This channel should be used directly by the creator of this 37 | /// [StreamChannelController] to send and receive events. 38 | StreamChannel get local => _local; 39 | late final StreamChannel _local; 40 | 41 | /// The foreign channel. 42 | /// 43 | /// This channel should be returned to external users so they can communicate 44 | /// with [local]. 45 | StreamChannel get foreign => _foreign; 46 | late final StreamChannel _foreign; 47 | 48 | /// Creates a [StreamChannelController]. 49 | /// 50 | /// If [sync] is true, events added to either channel's sink are synchronously 51 | /// dispatched to the other channel's stream. This should only be done if the 52 | /// source of those events is already asynchronous. 53 | /// 54 | /// If [allowForeignErrors] is `false`, errors are not allowed to be passed to 55 | /// the foreign channel's sink. If any are, the connection will close and the 56 | /// error will be forwarded to the foreign channel's [StreamSink.done] future. 57 | /// This guarantees that the local stream will never emit errors. 58 | StreamChannelController({bool allowForeignErrors = true, bool sync = false}) { 59 | var localToForeignController = StreamController(sync: sync); 60 | var foreignToLocalController = StreamController(sync: sync); 61 | _local = StreamChannel.withGuarantees( 62 | foreignToLocalController.stream, localToForeignController.sink); 63 | _foreign = StreamChannel.withGuarantees( 64 | localToForeignController.stream, foreignToLocalController.sink, 65 | allowSinkErrors: allowForeignErrors); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/close_guarantee_channel.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:async/async.dart'; 8 | 9 | import '../stream_channel.dart'; 10 | 11 | /// A [StreamChannel] that specifically enforces the stream channel guarantee 12 | /// that closing the sink causes the stream to close before it emits any more 13 | /// events 14 | /// 15 | /// This is exposed via [StreamChannel.withCloseGuarantee]. 16 | class CloseGuaranteeChannel extends StreamChannelMixin { 17 | @override 18 | Stream get stream => _stream; 19 | late final _CloseGuaranteeStream _stream; 20 | 21 | @override 22 | StreamSink get sink => _sink; 23 | late final _CloseGuaranteeSink _sink; 24 | 25 | /// The subscription to the inner stream. 26 | StreamSubscription? _subscription; 27 | 28 | /// Whether the sink has closed, causing the underlying channel to disconnect. 29 | bool _disconnected = false; 30 | 31 | CloseGuaranteeChannel(Stream innerStream, StreamSink innerSink) { 32 | _sink = _CloseGuaranteeSink(innerSink, this); 33 | _stream = _CloseGuaranteeStream(innerStream, this); 34 | } 35 | } 36 | 37 | /// The stream for [CloseGuaranteeChannel]. 38 | /// 39 | /// This wraps the inner stream to save the subscription on the channel when 40 | /// [listen] is called. 41 | class _CloseGuaranteeStream extends Stream { 42 | /// The inner stream this is delegating to. 43 | final Stream _inner; 44 | 45 | /// The [CloseGuaranteeChannel] this belongs to. 46 | final CloseGuaranteeChannel _channel; 47 | 48 | _CloseGuaranteeStream(this._inner, this._channel); 49 | 50 | @override 51 | StreamSubscription listen(void Function(T)? onData, 52 | {Function? onError, void Function()? onDone, bool? cancelOnError}) { 53 | // If the channel is already disconnected, we shouldn't dispatch anything 54 | // but a done event. 55 | if (_channel._disconnected) { 56 | onData = null; 57 | onError = null; 58 | } 59 | 60 | var subscription = _inner.listen(onData, 61 | onError: onError, onDone: onDone, cancelOnError: cancelOnError); 62 | if (!_channel._disconnected) { 63 | _channel._subscription = subscription; 64 | } 65 | return subscription; 66 | } 67 | } 68 | 69 | /// The sink for [CloseGuaranteeChannel]. 70 | /// 71 | /// This wraps the inner sink to cancel the stream subscription when the sink is 72 | /// canceled. 73 | class _CloseGuaranteeSink extends DelegatingStreamSink { 74 | /// The [CloseGuaranteeChannel] this belongs to. 75 | final CloseGuaranteeChannel _channel; 76 | 77 | _CloseGuaranteeSink(super.inner, this._channel); 78 | 79 | @override 80 | Future close() { 81 | var done = super.close(); 82 | _channel._disconnected = true; 83 | var subscription = _channel._subscription; 84 | if (subscription != null) { 85 | // Don't dispatch anything but a done event. 86 | subscription.onData(null); 87 | subscription.onError(null); 88 | } 89 | return done; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/stream_channel_controller_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:stream_channel/stream_channel.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('asynchronously', () { 10 | late StreamChannelController controller; 11 | setUp(() { 12 | controller = StreamChannelController(); 13 | }); 14 | 15 | test('forwards events from the local sink to the foreign stream', () { 16 | controller.local.sink 17 | ..add(1) 18 | ..add(2) 19 | ..add(3) 20 | ..close(); 21 | expect(controller.foreign.stream.toList(), completion(equals([1, 2, 3]))); 22 | }); 23 | 24 | test('forwards events from the foreign sink to the local stream', () { 25 | controller.foreign.sink 26 | ..add(1) 27 | ..add(2) 28 | ..add(3) 29 | ..close(); 30 | expect(controller.local.stream.toList(), completion(equals([1, 2, 3]))); 31 | }); 32 | 33 | test( 34 | 'with allowForeignErrors: false, shuts down the connection if an ' 35 | 'error is added to the foreign channel', () { 36 | controller = StreamChannelController(allowForeignErrors: false); 37 | 38 | controller.foreign.sink.addError('oh no'); 39 | expect(controller.foreign.sink.done, throwsA('oh no')); 40 | expect(controller.foreign.stream.toList(), completion(isEmpty)); 41 | expect(controller.local.sink.done, completes); 42 | expect(controller.local.stream.toList(), completion(isEmpty)); 43 | }); 44 | }); 45 | 46 | group('synchronously', () { 47 | late StreamChannelController controller; 48 | setUp(() { 49 | controller = StreamChannelController(sync: true); 50 | }); 51 | 52 | test( 53 | 'synchronously forwards events from the local sink to the foreign ' 54 | 'stream', () { 55 | var receivedEvent = false; 56 | var receivedError = false; 57 | var receivedDone = false; 58 | controller.foreign.stream.listen(expectAsync1((event) { 59 | expect(event, equals(1)); 60 | receivedEvent = true; 61 | }), onError: expectAsync1((error) { 62 | expect(error, equals('oh no')); 63 | receivedError = true; 64 | }), onDone: expectAsync0(() { 65 | receivedDone = true; 66 | })); 67 | 68 | controller.local.sink.add(1); 69 | expect(receivedEvent, isTrue); 70 | 71 | controller.local.sink.addError('oh no'); 72 | expect(receivedError, isTrue); 73 | 74 | controller.local.sink.close(); 75 | expect(receivedDone, isTrue); 76 | }); 77 | 78 | test( 79 | 'synchronously forwards events from the foreign sink to the local ' 80 | 'stream', () { 81 | var receivedEvent = false; 82 | var receivedError = false; 83 | var receivedDone = false; 84 | controller.local.stream.listen(expectAsync1((event) { 85 | expect(event, equals(1)); 86 | receivedEvent = true; 87 | }), onError: expectAsync1((error) { 88 | expect(error, equals('oh no')); 89 | receivedError = true; 90 | }), onDone: expectAsync0(() { 91 | receivedDone = true; 92 | })); 93 | 94 | controller.foreign.sink.add(1); 95 | expect(receivedEvent, isTrue); 96 | 97 | controller.foreign.sink.addError('oh no'); 98 | expect(receivedError, isTrue); 99 | 100 | controller.foreign.sink.close(); 101 | expect(receivedDone, isTrue); 102 | }); 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /test/stream_channel_completer_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:stream_channel/stream_channel.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | void main() { 11 | late StreamChannelCompleter completer; 12 | late StreamController streamController; 13 | late StreamController sinkController; 14 | late StreamChannel innerChannel; 15 | setUp(() { 16 | completer = StreamChannelCompleter(); 17 | streamController = StreamController(); 18 | sinkController = StreamController(); 19 | innerChannel = StreamChannel(streamController.stream, sinkController.sink); 20 | }); 21 | 22 | group('when a channel is set before accessing', () { 23 | test('forwards events through the stream', () { 24 | completer.setChannel(innerChannel); 25 | expect(completer.channel.stream.toList(), completion(equals([1, 2, 3]))); 26 | 27 | streamController.add(1); 28 | streamController.add(2); 29 | streamController.add(3); 30 | streamController.close(); 31 | }); 32 | 33 | test('forwards events through the sink', () { 34 | completer.setChannel(innerChannel); 35 | expect(sinkController.stream.toList(), completion(equals([1, 2, 3]))); 36 | 37 | completer.channel.sink.add(1); 38 | completer.channel.sink.add(2); 39 | completer.channel.sink.add(3); 40 | completer.channel.sink.close(); 41 | }); 42 | 43 | test('forwards an error through the stream', () { 44 | completer.setError('oh no'); 45 | expect(completer.channel.stream.first, throwsA('oh no')); 46 | }); 47 | 48 | test('drops sink events', () { 49 | completer.setError('oh no'); 50 | expect(completer.channel.sink.done, completes); 51 | completer.channel.sink.add(1); 52 | completer.channel.sink.addError('oh no'); 53 | }); 54 | }); 55 | 56 | group('when a channel is set after accessing', () { 57 | test('forwards events through the stream', () async { 58 | expect(completer.channel.stream.toList(), completion(equals([1, 2, 3]))); 59 | await pumpEventQueue(); 60 | 61 | completer.setChannel(innerChannel); 62 | streamController.add(1); 63 | streamController.add(2); 64 | streamController.add(3); 65 | unawaited(streamController.close()); 66 | }); 67 | 68 | test('forwards events through the sink', () async { 69 | completer.channel.sink.add(1); 70 | completer.channel.sink.add(2); 71 | completer.channel.sink.add(3); 72 | unawaited(completer.channel.sink.close()); 73 | await pumpEventQueue(); 74 | 75 | completer.setChannel(innerChannel); 76 | expect(sinkController.stream.toList(), completion(equals([1, 2, 3]))); 77 | }); 78 | 79 | test('forwards an error through the stream', () async { 80 | expect(completer.channel.stream.first, throwsA('oh no')); 81 | await pumpEventQueue(); 82 | 83 | completer.setError('oh no'); 84 | }); 85 | 86 | test('drops sink events', () async { 87 | expect(completer.channel.sink.done, completes); 88 | completer.channel.sink.add(1); 89 | completer.channel.sink.addError('oh no'); 90 | await pumpEventQueue(); 91 | 92 | completer.setError('oh no'); 93 | }); 94 | }); 95 | 96 | group('forFuture', () { 97 | test('forwards a StreamChannel', () { 98 | var channel = 99 | StreamChannelCompleter.fromFuture(Future.value(innerChannel)); 100 | channel.sink.add(1); 101 | channel.sink.close(); 102 | streamController.sink.add(2); 103 | streamController.sink.close(); 104 | 105 | expect(sinkController.stream.toList(), completion(equals([1]))); 106 | expect(channel.stream.toList(), completion(equals([2]))); 107 | }); 108 | 109 | test('forwards an error', () { 110 | var channel = StreamChannelCompleter.fromFuture(Future.error('oh no')); 111 | expect(channel.stream.toList(), throwsA('oh no')); 112 | }); 113 | }); 114 | 115 | test("doesn't allow the channel to be set multiple times", () { 116 | completer.setChannel(innerChannel); 117 | expect(() => completer.setChannel(innerChannel), throwsStateError); 118 | expect(() => completer.setChannel(innerChannel), throwsStateError); 119 | }); 120 | } 121 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.1.3-wip 2 | 3 | * Require Dart 3.3 4 | 5 | ## 2.1.2 6 | 7 | * Require Dart 2.19 8 | * Add an example. 9 | * Fix a race condition in `IsolateChannel.connectReceive()` where the channel 10 | could hang forever if its sink was closed before the connection was established. 11 | 12 | ## 2.1.1 13 | 14 | * Require Dart 2.14 15 | * Populate the pubspec `repository` field. 16 | * Handle multichannel messages where the ID element is a `double` at runtime 17 | instead of an `int`. When reading an array with `dart2wasm` numbers within the 18 | array are parsed as `double`. 19 | 20 | ## 2.1.0 21 | 22 | * Stable release for null safety. 23 | 24 | ## 2.0.0 25 | 26 | **Breaking changes** 27 | 28 | * `IsolateChannel` requires a separate import 29 | `package:stram_channel/isolate_channel.dart`. 30 | `package:stream_channel/stream_channel.dart` will now not trigger any platform 31 | concerns due to importing `dart:isolate`. 32 | * Remove `JsonDocumentTransformer` class. The `jsonDocument` top level is still 33 | available. 34 | * Remove `StreamChannelTransformer.typed`. Use `.cast` on the transformed 35 | channel instead. 36 | * Change `Future` returns to `Future`. 37 | 38 | ## 1.7.0 39 | 40 | * Make `IsolateChannel` available through 41 | `package:stream_channel/isolate_channel.dart`. This will be the required 42 | import in the next release. 43 | * Require `2.0.0` or newer SDK. 44 | * Internal style changes. 45 | 46 | ## 1.6.8 47 | 48 | * Set max SDK version to `<3.0.0`, and adjust other dependencies. 49 | 50 | ## 1.6.7+1 51 | 52 | * Fix Dart 2 runtime types in `IsolateChannel`. 53 | 54 | ## 1.6.7 55 | 56 | * Update SDK version to 2.0.0-dev.17.0. 57 | * Add a type argument to `MultiChannel`. 58 | 59 | ## 1.6.6 60 | 61 | * Fix a Dart 2 issue with inner stream transformation in `GuaranteeChannel`. 62 | 63 | * Fix a Dart 2 issue with `StreamChannelTransformer.fromCodec()`. 64 | 65 | ## 1.6.5 66 | 67 | * Fix an issue with `JsonDocumentTransformer.bind` where it created an internal 68 | stream channel which didn't get a properly inferred type for its `sink`. 69 | 70 | ## 1.6.4 71 | 72 | * Fix a race condition in `MultiChannel` where messages from a remote virtual 73 | channel could get dropped if the corresponding local channel wasn't registered 74 | quickly enough. 75 | 76 | ## 1.6.3 77 | 78 | * Use `pumpEventQueue()` from test. 79 | 80 | ## 1.6.2 81 | 82 | * Declare support for `async` 2.0.0. 83 | 84 | ## 1.6.1 85 | 86 | * Fix the type of `StreamChannel.transform()`. This previously inverted the 87 | generic parameters, so it only really worked with transformers where both 88 | generic types were identical. 89 | 90 | ## 1.6.0 91 | 92 | * `Disconnector.disconnect()` now returns a future that completes when all the 93 | inner `StreamSink.close()` futures have completed. 94 | 95 | ## 1.5.0 96 | 97 | * Add `new StreamChannel.withCloseGuarantee()` to provide the specific guarantee 98 | that closing the sink causes the stream to close before it emits any more 99 | events. This is the only guarantee that isn't automatically preserved when 100 | transforming a channel. 101 | 102 | * `StreamChannelTransformer`s provided by the `stream_channel` package now 103 | properly provide the guarantee that closing the sink causes the stream to 104 | close before it emits any more events 105 | 106 | ## 1.4.0 107 | 108 | * Add `StreamChannel.cast()`, which soundly coerces the generic type of a 109 | channel. 110 | 111 | * Add `StreamChannelTransformer.typed()`, which soundly coerces the generic type 112 | of a transformer. 113 | 114 | ## 1.3.2 115 | 116 | * Fix all strong-mode errors and warnings. 117 | 118 | ## 1.3.1 119 | 120 | * Make `IsolateChannel` slightly more efficient. 121 | 122 | * Make `MultiChannel` follow the stream channel rules. 123 | 124 | ## 1.3.0 125 | 126 | * Add `Disconnector`, a transformer that allows the caller to disconnect the 127 | transformed channel. 128 | 129 | ## 1.2.0 130 | 131 | * Add `new StreamChannel.withGuarantees()`, which creates a channel with extra 132 | wrapping to ensure that it obeys the stream channel guarantees. 133 | 134 | * Add `StreamChannelController`, which can be used to create custom 135 | `StreamChannel` objects. 136 | 137 | ## 1.1.1 138 | 139 | * Fix the type annotation for `StreamChannel.transform()`'s parameter. 140 | 141 | ## 1.1.0 142 | 143 | * Add `StreamChannel.transformStream()`, `StreamChannel.transformSink()`, 144 | `StreamChannel.changeStream()`, and `StreamChannel.changeSink()` to support 145 | changing only the stream or only the sink of a channel. 146 | 147 | * Be more explicit about `JsonDocumentTransformer`'s error-handling behavior. 148 | 149 | ## 1.0.1 150 | 151 | * Fix `MultiChannel`'s constructor to take a `StreamChannel`. This is 152 | technically a breaking change, but since 1.0.0 was only released an hour ago, 153 | we're treating it as a bug fix. 154 | 155 | ## 1.0.0 156 | 157 | * Initial version 158 | -------------------------------------------------------------------------------- /lib/src/isolate_channel.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:isolate'; 7 | 8 | import 'package:async/async.dart'; 9 | 10 | import '../stream_channel.dart'; 11 | 12 | /// A [StreamChannel] that communicates over a [ReceivePort]/[SendPort] pair, 13 | /// presumably with another isolate. 14 | /// 15 | /// The remote endpoint doesn't necessarily need to be running an 16 | /// [IsolateChannel]. This can be used with any two ports, although the 17 | /// [StreamChannel] semantics mean that this class will treat them as being 18 | /// paired (for example, closing the [sink] will cause the [stream] to stop 19 | /// emitting events). 20 | /// 21 | /// The underlying isolate ports have no notion of closing connections. This 22 | /// means that [stream] won't close unless [sink] is closed, and that closing 23 | /// [sink] won't cause the remote endpoint to close. Users should take care to 24 | /// ensure that they always close the [sink] of every [IsolateChannel] they use 25 | /// to avoid leaving dangling [ReceivePort]s. 26 | class IsolateChannel extends StreamChannelMixin { 27 | @override 28 | final Stream stream; 29 | @override 30 | final StreamSink sink; 31 | 32 | /// Connects to a remote channel that was created with 33 | /// [IsolateChannel.connectSend]. 34 | /// 35 | /// These constructors establish a connection using only a single 36 | /// [SendPort]/[ReceivePort] pair, as long as each side uses one of the 37 | /// connect constructors. 38 | /// 39 | /// The connection protocol is guaranteed to remain compatible across versions 40 | /// at least until the next major version release. If the protocol is 41 | /// violated, the resulting channel will emit a single value on its stream and 42 | /// then close. 43 | factory IsolateChannel.connectReceive(ReceivePort receivePort) { 44 | // We can't use a [StreamChannelCompleter] here because we need the return 45 | // value to be an [IsolateChannel]. 46 | var isCompleted = false; 47 | var streamCompleter = StreamCompleter(); 48 | var sinkCompleter = StreamSinkCompleter(); 49 | 50 | var channel = IsolateChannel._(streamCompleter.stream, sinkCompleter.sink 51 | .transform(StreamSinkTransformer.fromHandlers(handleDone: (sink) { 52 | if (!isCompleted) { 53 | receivePort.close(); 54 | streamCompleter.setSourceStream(const Stream.empty()); 55 | sinkCompleter.setDestinationSink(NullStreamSink()); 56 | } 57 | sink.close(); 58 | }))); 59 | 60 | // The first message across the ReceivePort should be a SendPort pointing to 61 | // the remote end. If it's not, we'll make the stream emit an error 62 | // complaining. 63 | late StreamSubscription subscription; 64 | subscription = receivePort.listen((message) { 65 | isCompleted = true; 66 | if (message is SendPort) { 67 | var controller = 68 | StreamChannelController(allowForeignErrors: false, sync: true); 69 | SubscriptionStream(subscription).cast().pipe(controller.local.sink); 70 | controller.local.stream 71 | .listen((data) => message.send(data), onDone: receivePort.close); 72 | 73 | streamCompleter.setSourceStream(controller.foreign.stream); 74 | sinkCompleter.setDestinationSink(controller.foreign.sink); 75 | return; 76 | } 77 | 78 | streamCompleter.setError( 79 | StateError('Unexpected Isolate response "$message".'), 80 | StackTrace.current); 81 | sinkCompleter.setDestinationSink(NullStreamSink()); 82 | subscription.cancel(); 83 | }); 84 | 85 | return channel; 86 | } 87 | 88 | /// Connects to a remote channel that was created with 89 | /// [IsolateChannel.connectReceive]. 90 | /// 91 | /// These constructors establish a connection using only a single 92 | /// [SendPort]/[ReceivePort] pair, as long as each side uses one of the 93 | /// connect constructors. 94 | /// 95 | /// The connection protocol is guaranteed to remain compatible across versions 96 | /// at least until the next major version release. 97 | factory IsolateChannel.connectSend(SendPort sendPort) { 98 | var receivePort = ReceivePort(); 99 | sendPort.send(receivePort.sendPort); 100 | return IsolateChannel(receivePort, sendPort); 101 | } 102 | 103 | /// Creates a stream channel that receives messages from [receivePort] and 104 | /// sends them over [sendPort]. 105 | factory IsolateChannel(ReceivePort receivePort, SendPort sendPort) { 106 | var controller = 107 | StreamChannelController(allowForeignErrors: false, sync: true); 108 | receivePort.cast().pipe(controller.local.sink); 109 | controller.local.stream 110 | .listen((data) => sendPort.send(data), onDone: receivePort.close); 111 | return IsolateChannel._(controller.foreign.stream, controller.foreign.sink); 112 | } 113 | 114 | IsolateChannel._(this.stream, this.sink); 115 | } 116 | -------------------------------------------------------------------------------- /test/stream_channel_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | 8 | import 'package:async/async.dart'; 9 | import 'package:stream_channel/stream_channel.dart'; 10 | import 'package:test/test.dart'; 11 | 12 | void main() { 13 | test("pipe() pipes data from each channel's stream into the other's sink", 14 | () { 15 | var otherStreamController = StreamController(); 16 | var otherSinkController = StreamController(); 17 | var otherChannel = 18 | StreamChannel(otherStreamController.stream, otherSinkController.sink); 19 | 20 | var streamController = StreamController(); 21 | var sinkController = StreamController(); 22 | var channel = StreamChannel(streamController.stream, sinkController.sink); 23 | 24 | channel.pipe(otherChannel); 25 | 26 | streamController.add(1); 27 | streamController.add(2); 28 | streamController.add(3); 29 | streamController.close(); 30 | expect(otherSinkController.stream.toList(), completion(equals([1, 2, 3]))); 31 | 32 | otherStreamController.add(4); 33 | otherStreamController.add(5); 34 | otherStreamController.add(6); 35 | otherStreamController.close(); 36 | expect(sinkController.stream.toList(), completion(equals([4, 5, 6]))); 37 | }); 38 | 39 | test('transform() transforms the channel', () async { 40 | var streamController = StreamController>(); 41 | var sinkController = StreamController>(); 42 | var channel = StreamChannel(streamController.stream, sinkController.sink); 43 | 44 | var transformed = channel 45 | .cast>() 46 | .transform(StreamChannelTransformer.fromCodec(utf8)); 47 | 48 | streamController.add([102, 111, 111, 98, 97, 114]); 49 | unawaited(streamController.close()); 50 | expect(await transformed.stream.toList(), equals(['foobar'])); 51 | 52 | transformed.sink.add('fblthp'); 53 | unawaited(transformed.sink.close()); 54 | expect( 55 | sinkController.stream.toList(), 56 | completion(equals([ 57 | [102, 98, 108, 116, 104, 112] 58 | ]))); 59 | }); 60 | 61 | test('transformStream() transforms only the stream', () async { 62 | var streamController = StreamController(); 63 | var sinkController = StreamController(); 64 | var channel = StreamChannel(streamController.stream, sinkController.sink); 65 | 66 | var transformed = 67 | channel.cast().transformStream(const LineSplitter()); 68 | 69 | streamController.add('hello world'); 70 | streamController.add(' what\nis'); 71 | streamController.add('\nup'); 72 | unawaited(streamController.close()); 73 | expect(await transformed.stream.toList(), 74 | equals(['hello world what', 'is', 'up'])); 75 | 76 | transformed.sink.add('fbl\nthp'); 77 | unawaited(transformed.sink.close()); 78 | expect(sinkController.stream.toList(), completion(equals(['fbl\nthp']))); 79 | }); 80 | 81 | test('transformSink() transforms only the sink', () async { 82 | var streamController = StreamController(); 83 | var sinkController = StreamController(); 84 | var channel = StreamChannel(streamController.stream, sinkController.sink); 85 | 86 | var transformed = channel.cast().transformSink( 87 | const StreamSinkTransformer.fromStreamTransformer(LineSplitter())); 88 | 89 | streamController.add('fbl\nthp'); 90 | unawaited(streamController.close()); 91 | expect(await transformed.stream.toList(), equals(['fbl\nthp'])); 92 | 93 | transformed.sink.add('hello world'); 94 | transformed.sink.add(' what\nis'); 95 | transformed.sink.add('\nup'); 96 | unawaited(transformed.sink.close()); 97 | expect(sinkController.stream.toList(), 98 | completion(equals(['hello world what', 'is', 'up']))); 99 | }); 100 | 101 | test('changeStream() changes the stream', () { 102 | var streamController = StreamController(); 103 | var sinkController = StreamController(); 104 | var channel = StreamChannel(streamController.stream, sinkController.sink); 105 | 106 | var newController = StreamController(); 107 | var changed = channel.changeStream((stream) { 108 | expect(stream, equals(channel.stream)); 109 | return newController.stream; 110 | }); 111 | 112 | newController.add(10); 113 | newController.close(); 114 | 115 | streamController.add(20); 116 | streamController.close(); 117 | 118 | expect(changed.stream.toList(), completion(equals([10]))); 119 | }); 120 | 121 | test('changeSink() changes the sink', () { 122 | var streamController = StreamController(); 123 | var sinkController = StreamController(); 124 | var channel = StreamChannel(streamController.stream, sinkController.sink); 125 | 126 | var newController = StreamController(); 127 | var changed = channel.changeSink((sink) { 128 | expect(sink, equals(channel.sink)); 129 | return newController.sink; 130 | }); 131 | 132 | expect(newController.stream.toList(), completion(equals([10]))); 133 | streamController.stream.listen(expectAsync1((_) {}, count: 0)); 134 | 135 | changed.sink.add(10); 136 | changed.sink.close(); 137 | }); 138 | } 139 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | import 'dart:io'; 8 | import 'dart:isolate'; 9 | 10 | import 'package:stream_channel/isolate_channel.dart'; 11 | import 'package:stream_channel/stream_channel.dart'; 12 | 13 | Future main() async { 14 | // A StreamChannel, is in simplest terms, a wrapper around a Stream and 15 | // a StreamSink. For example, you can create a channel that wraps standard 16 | // IO: 17 | var stdioChannel = StreamChannel(stdin, stdout); 18 | stdioChannel.sink.add('Hello!\n'.codeUnits); 19 | 20 | // Like a Stream can be transformed with a StreamTransformer, a 21 | // StreamChannel can be transformed with a StreamChannelTransformer. 22 | // For example, we can handle standard input as strings: 23 | var stringChannel = stdioChannel 24 | .transform(StreamChannelTransformer.fromCodec(utf8)) 25 | .transformStream(const LineSplitter()); 26 | stringChannel.sink.add('world!\n'); 27 | 28 | // You can implement StreamChannel by extending StreamChannelMixin, but 29 | // it's much easier to use a StreamChannelController. A controller has two 30 | // StreamChannel members: `local` and `foreign`. The creator of a 31 | // controller should work with the `local` channel, while the recipient should 32 | // work with the `foreign` channel, and usually will not have direct access to 33 | // the underlying controller. 34 | var ctrl = StreamChannelController(); 35 | ctrl.local.stream.listen((event) { 36 | // Do something useful here... 37 | }); 38 | 39 | // You can also pipe events from one channel to another. 40 | ctrl 41 | ..foreign.pipe(stringChannel) 42 | ..local.sink.add('Piped!\n'); 43 | await ctrl.local.sink.close(); 44 | 45 | // The StreamChannel interface provides several guarantees, which can be 46 | // found here: 47 | // https://pub.dev/documentation/stream_channel/latest/stream_channel/StreamChannel-class.html 48 | // 49 | // By calling `StreamChannel.withGuarantees()`, you can create a 50 | // StreamChannel that provides all guarantees. 51 | var dummyCtrl0 = StreamChannelController(); 52 | var guaranteedChannel = StreamChannel.withGuarantees( 53 | dummyCtrl0.foreign.stream, dummyCtrl0.foreign.sink); 54 | 55 | // To close a StreamChannel, use `sink.close()`. 56 | await guaranteedChannel.sink.close(); 57 | 58 | // A MultiChannel multiplexes multiple virtual channels across a single 59 | // underlying transport layer. For example, an application listening over 60 | // standard I/O can still support multiple clients if it has a mechanism to 61 | // separate events from different clients. 62 | // 63 | // A MultiChannel splits events into numbered channels, which are 64 | // instances of VirtualChannel. 65 | var dummyCtrl1 = StreamChannelController(); 66 | var multiChannel = MultiChannel(dummyCtrl1.foreign); 67 | var channel1 = multiChannel.virtualChannel(); 68 | await multiChannel.sink.close(); 69 | 70 | // The client/peer should also create its own MultiChannel, connected to 71 | // the underlying transport, use the corresponding ID's to handle events in 72 | // their respective channels. It is up to you how to communicate channel ID's 73 | // across different endpoints. 74 | var dummyCtrl2 = StreamChannelController(); 75 | var multiChannel2 = MultiChannel(dummyCtrl2.foreign); 76 | var channel2 = multiChannel2.virtualChannel(channel1.id); 77 | await channel2.sink.close(); 78 | await multiChannel2.sink.close(); 79 | 80 | // Multiple instances of a Dart application can communicate easily across 81 | // `SendPort`/`ReceivePort` pairs by means of the `IsolateChannel` class. 82 | // Typically, one endpoint will create a `ReceivePort`, and call the 83 | // `IsolateChannel.connectReceive` constructor. The other endpoint will be 84 | // given the corresponding `SendPort`, and then call 85 | // `IsolateChannel.connectSend`. 86 | var recv = ReceivePort(); 87 | var recvChannel = IsolateChannel.connectReceive(recv); 88 | var sendChannel = IsolateChannel.connectSend(recv.sendPort); 89 | 90 | // You must manually close `IsolateChannel` sinks, however. 91 | await recvChannel.sink.close(); 92 | await sendChannel.sink.close(); 93 | 94 | // You can use the `Disconnector` transformer to cause a channel to act as 95 | // though the remote end of its transport had disconnected. 96 | var disconnector = Disconnector(); 97 | var disconnectable = stringChannel.transform(disconnector); 98 | disconnectable.sink.add('Still connected!'); 99 | await disconnector.disconnect(); 100 | 101 | // Additionally: 102 | // * The `DelegatingStreamController` class can be extended to build a 103 | // basis for wrapping other `StreamChannel` objects. 104 | // * The `jsonDocument` transformer converts events to/from JSON, using 105 | // the `json` codec from `dart:convert`. 106 | // * `package:json_rpc_2` directly builds on top of 107 | // `package:stream_channel`, so any compatible transport can be used to 108 | // create interactive client/server or peer-to-peer applications (i.e. 109 | // language servers, microservices, etc. 110 | } 111 | -------------------------------------------------------------------------------- /test/disconnector_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:async/async.dart'; 8 | import 'package:stream_channel/stream_channel.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | void main() { 12 | late StreamController streamController; 13 | late StreamController sinkController; 14 | late Disconnector disconnector; 15 | late StreamChannel channel; 16 | setUp(() { 17 | streamController = StreamController(); 18 | sinkController = StreamController(); 19 | disconnector = Disconnector(); 20 | channel = StreamChannel.withGuarantees( 21 | streamController.stream, sinkController.sink) 22 | .transform(disconnector); 23 | }); 24 | 25 | group('before disconnection', () { 26 | test('forwards events from the sink as normal', () { 27 | channel.sink.add(1); 28 | channel.sink.add(2); 29 | channel.sink.add(3); 30 | channel.sink.close(); 31 | 32 | expect(sinkController.stream.toList(), completion(equals([1, 2, 3]))); 33 | }); 34 | 35 | test('forwards events to the stream as normal', () { 36 | streamController.add(1); 37 | streamController.add(2); 38 | streamController.add(3); 39 | streamController.close(); 40 | 41 | expect(channel.stream.toList(), completion(equals([1, 2, 3]))); 42 | }); 43 | 44 | test("events can't be added when the sink is explicitly closed", () { 45 | sinkController.stream.listen(null); // Work around sdk#19095. 46 | 47 | expect(channel.sink.close(), completes); 48 | expect(() => channel.sink.add(1), throwsStateError); 49 | expect(() => channel.sink.addError('oh no'), throwsStateError); 50 | expect(() => channel.sink.addStream(Stream.fromIterable([])), 51 | throwsStateError); 52 | }); 53 | 54 | test("events can't be added while a stream is being added", () { 55 | var controller = StreamController(); 56 | channel.sink.addStream(controller.stream); 57 | 58 | expect(() => channel.sink.add(1), throwsStateError); 59 | expect(() => channel.sink.addError('oh no'), throwsStateError); 60 | expect(() => channel.sink.addStream(Stream.fromIterable([])), 61 | throwsStateError); 62 | expect(() => channel.sink.close(), throwsStateError); 63 | 64 | controller.close(); 65 | }); 66 | }); 67 | 68 | test('cancels addStream when disconnected', () async { 69 | var canceled = false; 70 | var controller = StreamController(onCancel: () { 71 | canceled = true; 72 | }); 73 | expect(channel.sink.addStream(controller.stream), completes); 74 | unawaited(disconnector.disconnect()); 75 | 76 | await pumpEventQueue(); 77 | expect(canceled, isTrue); 78 | }); 79 | 80 | test('disconnect() returns the close future from the inner sink', () async { 81 | var streamController = StreamController(); 82 | var sinkController = StreamController(); 83 | var disconnector = Disconnector(); 84 | var sink = _CloseCompleterSink(sinkController.sink); 85 | StreamChannel.withGuarantees(streamController.stream, sink) 86 | .transform(disconnector); 87 | 88 | var disconnectFutureFired = false; 89 | expect( 90 | disconnector.disconnect().then((_) { 91 | disconnectFutureFired = true; 92 | }), 93 | completes); 94 | 95 | // Give the future time to fire early if it's going to. 96 | await pumpEventQueue(); 97 | expect(disconnectFutureFired, isFalse); 98 | 99 | // When the inner sink's close future completes, so should the 100 | // disconnector's. 101 | sink.completer.complete(); 102 | await pumpEventQueue(); 103 | expect(disconnectFutureFired, isTrue); 104 | }); 105 | 106 | group('after disconnection', () { 107 | setUp(() { 108 | disconnector.disconnect(); 109 | }); 110 | 111 | test('closes the inner sink and ignores events to the outer sink', () { 112 | channel.sink.add(1); 113 | channel.sink.add(2); 114 | channel.sink.add(3); 115 | channel.sink.close(); 116 | 117 | expect(sinkController.stream.toList(), completion(isEmpty)); 118 | }); 119 | 120 | test('closes the stream', () { 121 | expect(channel.stream.toList(), completion(isEmpty)); 122 | }); 123 | 124 | test('completes done', () { 125 | sinkController.stream.listen(null); // Work around sdk#19095. 126 | expect(channel.sink.done, completes); 127 | }); 128 | 129 | test('still emits state errors after explicit close', () { 130 | sinkController.stream.listen(null); // Work around sdk#19095. 131 | expect(channel.sink.close(), completes); 132 | 133 | expect(() => channel.sink.add(1), throwsStateError); 134 | expect(() => channel.sink.addError('oh no'), throwsStateError); 135 | }); 136 | }); 137 | } 138 | 139 | /// A [StreamSink] wrapper that adds the ability to manually complete the Future 140 | /// returned by [close] using [completer]. 141 | class _CloseCompleterSink extends DelegatingStreamSink { 142 | /// The completer for the future returned by [close]. 143 | final completer = Completer(); 144 | 145 | _CloseCompleterSink(super.inner); 146 | 147 | @override 148 | Future close() { 149 | super.close(); 150 | return completer.future; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /lib/src/disconnector.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:async/async.dart'; 8 | 9 | import '../stream_channel.dart'; 10 | 11 | /// Allows the caller to force a channel to disconnect. 12 | /// 13 | /// When [disconnect] is called, the channel (or channels) transformed by this 14 | /// transformer will act as though the remote end had disconnected—the stream 15 | /// will emit a done event, and the sink will ignore future inputs. The inner 16 | /// sink will also be closed to notify the remote end of the disconnection. 17 | /// 18 | /// If a channel is transformed after the [disconnect] has been called, it will 19 | /// be disconnected immediately. 20 | class Disconnector implements StreamChannelTransformer { 21 | /// Whether [disconnect] has been called. 22 | bool get isDisconnected => _disconnectMemo.hasRun; 23 | 24 | /// The sinks for transformed channels. 25 | /// 26 | /// Note that we assume that transformed channels provide the stream channel 27 | /// guarantees. This allows us to only track sinks, because we know closing 28 | /// the underlying sink will cause the stream to emit a done event. 29 | final _sinks = <_DisconnectorSink>[]; 30 | 31 | /// Disconnects all channels that have been transformed. 32 | /// 33 | /// Returns a future that completes when all inner sinks' [StreamSink.close] 34 | /// futures have completed. Note that a [StreamController]'s sink won't close 35 | /// until the corresponding stream has a listener. 36 | Future disconnect() => _disconnectMemo.runOnce(() { 37 | var futures = _sinks.map((sink) => sink._disconnect()).toList(); 38 | _sinks.clear(); 39 | return Future.wait(futures, eagerError: true); 40 | }); 41 | final _disconnectMemo = AsyncMemoizer>(); 42 | 43 | @override 44 | StreamChannel bind(StreamChannel channel) { 45 | return channel.changeSink((innerSink) { 46 | var sink = _DisconnectorSink(innerSink); 47 | 48 | if (isDisconnected) { 49 | // Ignore errors here, because otherwise there would be no way for the 50 | // user to handle them gracefully. 51 | sink._disconnect().catchError((_) {}); 52 | } else { 53 | _sinks.add(sink); 54 | } 55 | 56 | return sink; 57 | }); 58 | } 59 | } 60 | 61 | /// A sink wrapper that can force a disconnection. 62 | class _DisconnectorSink implements StreamSink { 63 | /// The inner sink. 64 | final StreamSink _inner; 65 | 66 | @override 67 | Future get done => _inner.done; 68 | 69 | /// Whether [Disconnector.disconnect] has been called. 70 | var _isDisconnected = false; 71 | 72 | /// Whether the user has called [close]. 73 | var _closed = false; 74 | 75 | /// The subscription to the stream passed to [addStream], if a stream is 76 | /// currently being added. 77 | StreamSubscription? _addStreamSubscription; 78 | 79 | /// The completer for the future returned by [addStream], if a stream is 80 | /// currently being added. 81 | Completer? _addStreamCompleter; 82 | 83 | /// Whether we're currently adding a stream with [addStream]. 84 | bool get _inAddStream => _addStreamSubscription != null; 85 | 86 | _DisconnectorSink(this._inner); 87 | 88 | @override 89 | void add(T data) { 90 | if (_closed) throw StateError('Cannot add event after closing.'); 91 | if (_inAddStream) { 92 | throw StateError('Cannot add event while adding stream.'); 93 | } 94 | if (_isDisconnected) return; 95 | 96 | _inner.add(data); 97 | } 98 | 99 | @override 100 | void addError(Object error, [StackTrace? stackTrace]) { 101 | if (_closed) throw StateError('Cannot add event after closing.'); 102 | if (_inAddStream) { 103 | throw StateError('Cannot add event while adding stream.'); 104 | } 105 | if (_isDisconnected) return; 106 | 107 | _inner.addError(error, stackTrace); 108 | } 109 | 110 | @override 111 | Future addStream(Stream stream) { 112 | if (_closed) throw StateError('Cannot add stream after closing.'); 113 | if (_inAddStream) { 114 | throw StateError('Cannot add stream while adding stream.'); 115 | } 116 | if (_isDisconnected) return Future.value(); 117 | 118 | _addStreamCompleter = Completer.sync(); 119 | _addStreamSubscription = stream.listen(_inner.add, 120 | onError: _inner.addError, onDone: _addStreamCompleter!.complete); 121 | return _addStreamCompleter!.future.then((_) { 122 | _addStreamCompleter = null; 123 | _addStreamSubscription = null; 124 | }); 125 | } 126 | 127 | @override 128 | Future close() { 129 | if (_inAddStream) { 130 | throw StateError('Cannot close sink while adding stream.'); 131 | } 132 | 133 | _closed = true; 134 | return _inner.close(); 135 | } 136 | 137 | /// Disconnects this sink. 138 | /// 139 | /// This closes the underlying sink and stops forwarding events. It returns 140 | /// the [StreamSink.close] future for the underlying sink. 141 | Future _disconnect() { 142 | _isDisconnected = true; 143 | var future = _inner.close(); 144 | 145 | if (_inAddStream) { 146 | _addStreamCompleter!.complete(_addStreamSubscription!.cancel()); 147 | _addStreamCompleter = null; 148 | _addStreamSubscription = null; 149 | } 150 | 151 | return future; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /test/isolate_channel_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @TestOn('vm') 6 | library; 7 | 8 | import 'dart:async'; 9 | import 'dart:isolate'; 10 | 11 | import 'package:stream_channel/isolate_channel.dart'; 12 | import 'package:stream_channel/stream_channel.dart'; 13 | import 'package:test/test.dart'; 14 | 15 | void main() { 16 | late ReceivePort receivePort; 17 | late SendPort sendPort; 18 | late StreamChannel channel; 19 | setUp(() { 20 | receivePort = ReceivePort(); 21 | var receivePortForSend = ReceivePort(); 22 | sendPort = receivePortForSend.sendPort; 23 | channel = IsolateChannel(receivePortForSend, receivePort.sendPort); 24 | }); 25 | 26 | tearDown(() { 27 | receivePort.close(); 28 | channel.sink.close(); 29 | }); 30 | 31 | test('the channel can send messages', () { 32 | channel.sink.add(1); 33 | channel.sink.add(2); 34 | channel.sink.add(3); 35 | 36 | expect(receivePort.take(3).toList(), completion(equals([1, 2, 3]))); 37 | }); 38 | 39 | test('the channel can receive messages', () { 40 | sendPort.send(1); 41 | sendPort.send(2); 42 | sendPort.send(3); 43 | 44 | expect(channel.stream.take(3).toList(), completion(equals([1, 2, 3]))); 45 | }); 46 | 47 | test("events can't be added to an explicitly-closed sink", () { 48 | expect(channel.sink.close(), completes); 49 | expect(() => channel.sink.add(1), throwsStateError); 50 | expect(() => channel.sink.addError('oh no'), throwsStateError); 51 | expect(() => channel.sink.addStream(Stream.fromIterable([])), 52 | throwsStateError); 53 | }); 54 | 55 | test("events can't be added while a stream is being added", () { 56 | var controller = StreamController(); 57 | channel.sink.addStream(controller.stream); 58 | 59 | expect(() => channel.sink.add(1), throwsStateError); 60 | expect(() => channel.sink.addError('oh no'), throwsStateError); 61 | expect(() => channel.sink.addStream(Stream.fromIterable([])), 62 | throwsStateError); 63 | expect(() => channel.sink.close(), throwsStateError); 64 | 65 | controller.close(); 66 | }); 67 | 68 | group('stream channel rules', () { 69 | test( 70 | 'closing the sink causes the stream to close before it emits any more ' 71 | 'events', () { 72 | sendPort.send(1); 73 | sendPort.send(2); 74 | sendPort.send(3); 75 | sendPort.send(4); 76 | sendPort.send(5); 77 | 78 | channel.stream.listen(expectAsync1((message) { 79 | expect(message, equals(1)); 80 | channel.sink.close(); 81 | }, count: 1)); 82 | }); 83 | 84 | test("cancelling the stream's subscription has no effect on the sink", 85 | () async { 86 | unawaited(channel.stream.listen(null).cancel()); 87 | await pumpEventQueue(); 88 | 89 | channel.sink.add(1); 90 | channel.sink.add(2); 91 | channel.sink.add(3); 92 | expect(receivePort.take(3).toList(), completion(equals([1, 2, 3]))); 93 | }); 94 | 95 | test('the sink closes as soon as an error is added', () async { 96 | channel.sink.addError('oh no'); 97 | channel.sink.add(1); 98 | expect(channel.sink.done, throwsA('oh no')); 99 | 100 | // Since the sink is closed, the stream should also be closed. 101 | expect(channel.stream.isEmpty, completion(isTrue)); 102 | 103 | // The other end shouldn't receive the next event, since the sink was 104 | // closed. Pump the event queue to give it a chance to. 105 | receivePort.listen(expectAsync1((_) {}, count: 0)); 106 | await pumpEventQueue(); 107 | }); 108 | 109 | test('the sink closes as soon as an error is added via addStream', 110 | () async { 111 | var canceled = false; 112 | var controller = StreamController(onCancel: () { 113 | canceled = true; 114 | }); 115 | 116 | // This future shouldn't get the error, because it's sent to [Sink.done]. 117 | expect(channel.sink.addStream(controller.stream), completes); 118 | 119 | controller.addError('oh no'); 120 | expect(channel.sink.done, throwsA('oh no')); 121 | await pumpEventQueue(); 122 | expect(canceled, isTrue); 123 | 124 | // Even though the sink is closed, this shouldn't throw an error because 125 | // the user didn't explicitly close it. 126 | channel.sink.add(1); 127 | }); 128 | }); 129 | 130 | group('connect constructors', () { 131 | late ReceivePort connectPort; 132 | setUp(() { 133 | connectPort = ReceivePort(); 134 | }); 135 | 136 | tearDown(() { 137 | connectPort.close(); 138 | }); 139 | 140 | test('create a connected pair of channels', () async { 141 | var channel1 = IsolateChannel.connectReceive(connectPort); 142 | var channel2 = IsolateChannel.connectSend(connectPort.sendPort); 143 | 144 | channel1.sink.add(1); 145 | channel1.sink.add(2); 146 | channel1.sink.add(3); 147 | expect(await channel2.stream.take(3).toList(), equals([1, 2, 3])); 148 | 149 | channel2.sink.add(4); 150 | channel2.sink.add(5); 151 | channel2.sink.add(6); 152 | expect(await channel1.stream.take(3).toList(), equals([4, 5, 6])); 153 | 154 | await channel2.sink.close(); 155 | }); 156 | 157 | test('the receiving channel produces an error if it gets the wrong message', 158 | () { 159 | var connectedChannel = IsolateChannel.connectReceive(connectPort); 160 | connectPort.sendPort.send('wrong value'); 161 | 162 | expect(connectedChannel.stream.toList(), throwsStateError); 163 | expect(connectedChannel.sink.done, completes); 164 | }); 165 | 166 | test('the receiving channel closes gracefully without a connection', 167 | () async { 168 | var connectedChannel = IsolateChannel.connectReceive(connectPort); 169 | await connectedChannel.sink.close(); 170 | await expectLater(connectedChannel.stream.toList(), completion(isEmpty)); 171 | await expectLater(connectedChannel.sink.done, completes); 172 | }); 173 | }); 174 | } 175 | -------------------------------------------------------------------------------- /test/with_guarantees_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:stream_channel/stream_channel.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | void main() { 11 | late StreamController streamController; 12 | late StreamController sinkController; 13 | late StreamChannel channel; 14 | setUp(() { 15 | streamController = StreamController(); 16 | sinkController = StreamController(); 17 | channel = StreamChannel.withGuarantees( 18 | streamController.stream, sinkController.sink); 19 | }); 20 | 21 | group('with a broadcast stream', () { 22 | setUp(() { 23 | streamController = StreamController.broadcast(); 24 | channel = StreamChannel.withGuarantees( 25 | streamController.stream, sinkController.sink); 26 | }); 27 | 28 | test('buffers events', () async { 29 | streamController.add(1); 30 | streamController.add(2); 31 | streamController.add(3); 32 | await pumpEventQueue(); 33 | 34 | expect(channel.stream.toList(), completion(equals([1, 2, 3]))); 35 | unawaited(streamController.close()); 36 | }); 37 | 38 | test('only allows a single subscription', () { 39 | channel.stream.listen(null); 40 | expect(() => channel.stream.listen(null), throwsStateError); 41 | }); 42 | }); 43 | 44 | test( 45 | 'closing the event sink causes the stream to close before it emits any ' 46 | 'more events', () { 47 | streamController.add(1); 48 | streamController.add(2); 49 | streamController.add(3); 50 | 51 | expect( 52 | channel.stream 53 | .listen(expectAsync1((event) { 54 | if (event == 2) channel.sink.close(); 55 | }, count: 2)) 56 | .asFuture(), 57 | completes); 58 | }); 59 | 60 | test('after the stream closes, the sink ignores events', () async { 61 | unawaited(streamController.close()); 62 | 63 | // Wait for the done event to be delivered. 64 | await channel.stream.toList(); 65 | channel.sink.add(1); 66 | channel.sink.add(2); 67 | channel.sink.add(3); 68 | unawaited(channel.sink.close()); 69 | 70 | // None of our channel.sink additions should make it to the other endpoint. 71 | sinkController.stream.listen(expectAsync1((_) {}, count: 0), 72 | onDone: expectAsync0(() {}, count: 0)); 73 | await pumpEventQueue(); 74 | }); 75 | 76 | test("canceling the stream's subscription has no effect on the sink", 77 | () async { 78 | unawaited(channel.stream.listen(null).cancel()); 79 | await pumpEventQueue(); 80 | 81 | channel.sink.add(1); 82 | channel.sink.add(2); 83 | channel.sink.add(3); 84 | unawaited(channel.sink.close()); 85 | expect(sinkController.stream.toList(), completion(equals([1, 2, 3]))); 86 | }); 87 | 88 | test("canceling the stream's subscription doesn't stop a done event", 89 | () async { 90 | unawaited(channel.stream.listen(null).cancel()); 91 | await pumpEventQueue(); 92 | 93 | unawaited(streamController.close()); 94 | await pumpEventQueue(); 95 | 96 | channel.sink.add(1); 97 | channel.sink.add(2); 98 | channel.sink.add(3); 99 | unawaited(channel.sink.close()); 100 | 101 | // The sink should be ignoring events because the stream closed. 102 | sinkController.stream.listen(expectAsync1((_) {}, count: 0), 103 | onDone: expectAsync0(() {}, count: 0)); 104 | await pumpEventQueue(); 105 | }); 106 | 107 | test('forwards errors to the other endpoint', () { 108 | channel.sink.addError('error'); 109 | expect(sinkController.stream.first, throwsA('error')); 110 | }); 111 | 112 | test('Sink.done completes once the stream is done', () { 113 | channel.stream.listen(null); 114 | expect(channel.sink.done, completes); 115 | streamController.close(); 116 | }); 117 | 118 | test("events can't be added to an explicitly-closed sink", () { 119 | sinkController.stream.listen(null); // Work around sdk#19095. 120 | 121 | expect(channel.sink.close(), completes); 122 | expect(() => channel.sink.add(1), throwsStateError); 123 | expect(() => channel.sink.addError('oh no'), throwsStateError); 124 | expect(() => channel.sink.addStream(Stream.fromIterable([])), 125 | throwsStateError); 126 | }); 127 | 128 | test("events can't be added while a stream is being added", () { 129 | var controller = StreamController(); 130 | channel.sink.addStream(controller.stream); 131 | 132 | expect(() => channel.sink.add(1), throwsStateError); 133 | expect(() => channel.sink.addError('oh no'), throwsStateError); 134 | expect(() => channel.sink.addStream(Stream.fromIterable([])), 135 | throwsStateError); 136 | expect(() => channel.sink.close(), throwsStateError); 137 | 138 | controller.close(); 139 | }); 140 | 141 | group('with allowSinkErrors: false', () { 142 | setUp(() { 143 | streamController = StreamController(); 144 | sinkController = StreamController(); 145 | channel = StreamChannel.withGuarantees( 146 | streamController.stream, sinkController.sink, 147 | allowSinkErrors: false); 148 | }); 149 | 150 | test('forwards errors to Sink.done but not the stream', () { 151 | channel.sink.addError('oh no'); 152 | expect(channel.sink.done, throwsA('oh no')); 153 | sinkController.stream 154 | .listen(null, onError: expectAsync1((dynamic _) {}, count: 0)); 155 | }); 156 | 157 | test('adding an error causes the stream to emit a done event', () { 158 | expect(channel.sink.done, throwsA('oh no')); 159 | 160 | streamController.add(1); 161 | streamController.add(2); 162 | streamController.add(3); 163 | 164 | expect( 165 | channel.stream 166 | .listen(expectAsync1((event) { 167 | if (event == 2) channel.sink.addError('oh no'); 168 | }, count: 2)) 169 | .asFuture(), 170 | completes); 171 | }); 172 | 173 | test('adding an error closes the inner sink', () { 174 | channel.sink.addError('oh no'); 175 | expect(channel.sink.done, throwsA('oh no')); 176 | expect(sinkController.stream.toList(), completion(isEmpty)); 177 | }); 178 | 179 | test( 180 | 'adding an error via via addStream causes the stream to emit a done ' 181 | 'event', () async { 182 | var canceled = false; 183 | var controller = StreamController(onCancel: () { 184 | canceled = true; 185 | }); 186 | 187 | // This future shouldn't get the error, because it's sent to [Sink.done]. 188 | expect(channel.sink.addStream(controller.stream), completes); 189 | 190 | controller.addError('oh no'); 191 | expect(channel.sink.done, throwsA('oh no')); 192 | await pumpEventQueue(); 193 | expect(canceled, isTrue); 194 | 195 | // Even though the sink is closed, this shouldn't throw an error because 196 | // the user didn't explicitly close it. 197 | channel.sink.add(1); 198 | }); 199 | }); 200 | } 201 | -------------------------------------------------------------------------------- /lib/src/guarantee_channel.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:async/async.dart'; 8 | 9 | import '../stream_channel.dart'; 10 | 11 | /// A [StreamChannel] that enforces the stream channel guarantees. 12 | /// 13 | /// This is exposed via [StreamChannel.withGuarantees]. 14 | class GuaranteeChannel extends StreamChannelMixin { 15 | @override 16 | Stream get stream => _streamController.stream; 17 | 18 | @override 19 | StreamSink get sink => _sink; 20 | late final _GuaranteeSink _sink; 21 | 22 | /// The controller for [stream]. 23 | /// 24 | /// This intermediate controller allows us to continue listening for a done 25 | /// event even after the user has canceled their subscription, and to send our 26 | /// own done event when the sink is closed. 27 | late final StreamController _streamController; 28 | 29 | /// The subscription to the inner stream. 30 | StreamSubscription? _subscription; 31 | 32 | /// Whether the sink has closed, causing the underlying channel to disconnect. 33 | bool _disconnected = false; 34 | 35 | GuaranteeChannel(Stream innerStream, StreamSink innerSink, 36 | {bool allowSinkErrors = true}) { 37 | _sink = _GuaranteeSink(innerSink, this, allowErrors: allowSinkErrors); 38 | 39 | // Enforce the single-subscription guarantee by changing a broadcast stream 40 | // to single-subscription. 41 | if (innerStream.isBroadcast) { 42 | innerStream = 43 | innerStream.transform(SingleSubscriptionTransformer()); 44 | } 45 | 46 | _streamController = StreamController( 47 | onListen: () { 48 | // If the sink has disconnected, we've already called 49 | // [_streamController.close]. 50 | if (_disconnected) return; 51 | 52 | _subscription = innerStream.listen(_streamController.add, 53 | onError: _streamController.addError, onDone: () { 54 | _sink._onStreamDisconnected(); 55 | _streamController.close(); 56 | }); 57 | }, 58 | sync: true); 59 | } 60 | 61 | /// Called by [_GuaranteeSink] when the user closes it. 62 | /// 63 | /// The sink closing indicates that the connection is closed, so the stream 64 | /// should stop emitting events. 65 | void _onSinkDisconnected() { 66 | _disconnected = true; 67 | var subscription = _subscription; 68 | if (subscription != null) subscription.cancel(); 69 | _streamController.close(); 70 | } 71 | } 72 | 73 | /// The sink for [GuaranteeChannel]. 74 | /// 75 | /// This wraps the inner sink to ignore events and cancel any in-progress 76 | /// [addStream] calls when the underlying channel closes. 77 | class _GuaranteeSink implements StreamSink { 78 | /// The inner sink being wrapped. 79 | final StreamSink _inner; 80 | 81 | /// The [GuaranteeChannel] this belongs to. 82 | final GuaranteeChannel _channel; 83 | 84 | @override 85 | Future get done => _doneCompleter.future; 86 | final _doneCompleter = Completer(); 87 | 88 | /// Whether connection is disconnected. 89 | /// 90 | /// This can happen because the stream has emitted a done event, or because 91 | /// the user added an error when [_allowErrors] is `false`. 92 | bool _disconnected = false; 93 | 94 | /// Whether the user has called [close]. 95 | bool _closed = false; 96 | 97 | /// The subscription to the stream passed to [addStream], if a stream is 98 | /// currently being added. 99 | StreamSubscription? _addStreamSubscription; 100 | 101 | /// The completer for the future returned by [addStream], if a stream is 102 | /// currently being added. 103 | Completer? _addStreamCompleter; 104 | 105 | /// Whether we're currently adding a stream with [addStream]. 106 | bool get _inAddStream => _addStreamSubscription != null; 107 | 108 | /// Whether errors are passed on to the underlying sink. 109 | /// 110 | /// If this is `false`, any error passed to the sink is piped to [done] and 111 | /// the underlying sink is closed. 112 | final bool _allowErrors; 113 | 114 | _GuaranteeSink(this._inner, this._channel, {bool allowErrors = true}) 115 | : _allowErrors = allowErrors; 116 | 117 | @override 118 | void add(T data) { 119 | if (_closed) throw StateError('Cannot add event after closing.'); 120 | if (_inAddStream) { 121 | throw StateError('Cannot add event while adding stream.'); 122 | } 123 | if (_disconnected) return; 124 | 125 | _inner.add(data); 126 | } 127 | 128 | @override 129 | void addError(Object error, [StackTrace? stackTrace]) { 130 | if (_closed) throw StateError('Cannot add event after closing.'); 131 | if (_inAddStream) { 132 | throw StateError('Cannot add event while adding stream.'); 133 | } 134 | if (_disconnected) return; 135 | 136 | _addError(error, stackTrace); 137 | } 138 | 139 | /// Like [addError], but doesn't check to ensure that an error can be added. 140 | /// 141 | /// This is called from [addStream], so it shouldn't fail if a stream is being 142 | /// added. 143 | void _addError(Object error, [StackTrace? stackTrace]) { 144 | if (_allowErrors) { 145 | _inner.addError(error, stackTrace); 146 | return; 147 | } 148 | 149 | _doneCompleter.completeError(error, stackTrace); 150 | 151 | // Treat an error like both the stream and sink disconnecting. 152 | _onStreamDisconnected(); 153 | _channel._onSinkDisconnected(); 154 | 155 | // Ignore errors from the inner sink. We're already surfacing one error, and 156 | // if the user handles it we don't want them to have another top-level. 157 | _inner.close().catchError((_) {}); 158 | } 159 | 160 | @override 161 | Future addStream(Stream stream) { 162 | if (_closed) throw StateError('Cannot add stream after closing.'); 163 | if (_inAddStream) { 164 | throw StateError('Cannot add stream while adding stream.'); 165 | } 166 | if (_disconnected) return Future.value(); 167 | 168 | _addStreamCompleter = Completer.sync(); 169 | _addStreamSubscription = stream.listen(_inner.add, 170 | onError: _addError, onDone: _addStreamCompleter!.complete); 171 | return _addStreamCompleter!.future.then((_) { 172 | _addStreamCompleter = null; 173 | _addStreamSubscription = null; 174 | }); 175 | } 176 | 177 | @override 178 | Future close() { 179 | if (_inAddStream) { 180 | throw StateError('Cannot close sink while adding stream.'); 181 | } 182 | 183 | if (_closed) return done; 184 | _closed = true; 185 | 186 | if (!_disconnected) { 187 | _channel._onSinkDisconnected(); 188 | _doneCompleter.complete(_inner.close()); 189 | } 190 | 191 | return done; 192 | } 193 | 194 | /// Called by [GuaranteeChannel] when the stream emits a done event. 195 | /// 196 | /// The stream being done indicates that the connection is closed, so the 197 | /// sink should stop forwarding events. 198 | void _onStreamDisconnected() { 199 | _disconnected = true; 200 | if (!_doneCompleter.isCompleted) _doneCompleter.complete(); 201 | 202 | if (!_inAddStream) return; 203 | _addStreamCompleter!.complete(_addStreamSubscription!.cancel()); 204 | _addStreamCompleter = null; 205 | _addStreamSubscription = null; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /lib/stream_channel.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:async/async.dart'; 8 | 9 | import 'src/close_guarantee_channel.dart'; 10 | import 'src/guarantee_channel.dart'; 11 | import 'src/stream_channel_transformer.dart'; 12 | 13 | export 'src/delegating_stream_channel.dart'; 14 | export 'src/disconnector.dart'; 15 | export 'src/json_document_transformer.dart'; 16 | export 'src/multi_channel.dart'; 17 | export 'src/stream_channel_completer.dart'; 18 | export 'src/stream_channel_controller.dart'; 19 | export 'src/stream_channel_transformer.dart'; 20 | 21 | /// An abstract class representing a two-way communication channel. 22 | /// 23 | /// Users should consider the [stream] emitting a "done" event to be the 24 | /// canonical indicator that the channel has closed. If they wish to close the 25 | /// channel, they should close the [sink]—canceling the stream subscription is 26 | /// not sufficient. Protocol errors may be emitted through the stream or through 27 | /// [sink].done, depending on their underlying cause. Note that the sink may 28 | /// silently drop events if the channel closes before [sink].close is called. 29 | /// 30 | /// Implementations are strongly encouraged to mix in or extend 31 | /// [StreamChannelMixin] to get default implementations of the various instance 32 | /// methods. Adding new methods to this interface will not be considered a 33 | /// breaking change if implementations are also added to [StreamChannelMixin]. 34 | /// 35 | /// Implementations must provide the following guarantees: 36 | /// 37 | /// * The stream is single-subscription, and must follow all the guarantees of 38 | /// single-subscription streams. 39 | /// 40 | /// * Closing the sink causes the stream to close before it emits any more 41 | /// events. 42 | /// 43 | /// * After the stream closes, the sink is automatically closed. If this 44 | /// happens, sink methods should silently drop their arguments until 45 | /// [sink].close is called. 46 | /// 47 | /// * If the stream closes before it has a listener, the sink should silently 48 | /// drop events if possible. 49 | /// 50 | /// * Canceling the stream's subscription has no effect on the sink. The channel 51 | /// must still be able to respond to the other endpoint closing the channel 52 | /// even after the subscription has been canceled. 53 | /// 54 | /// * The sink *either* forwards errors to the other endpoint *or* closes as 55 | /// soon as an error is added and forwards that error to the [sink].done 56 | /// future. 57 | /// 58 | /// These guarantees allow users to interact uniformly with all implementations, 59 | /// and ensure that either endpoint closing the stream produces consistent 60 | /// behavior. 61 | abstract class StreamChannel { 62 | /// The single-subscription stream that emits values from the other endpoint. 63 | Stream get stream; 64 | 65 | /// The sink for sending values to the other endpoint. 66 | StreamSink get sink; 67 | 68 | /// Creates a new [StreamChannel] that communicates over [stream] and [sink]. 69 | /// 70 | /// Note that this stream/sink pair must provide the guarantees listed in the 71 | /// [StreamChannel] documentation. If they don't do so natively, 72 | /// [StreamChannel.withGuarantees] should be used instead. 73 | factory StreamChannel(Stream stream, StreamSink sink) => 74 | _StreamChannel(stream, sink); 75 | 76 | /// Creates a new [StreamChannel] that communicates over [stream] and [sink]. 77 | /// 78 | /// Unlike [StreamChannel.new], this enforces the guarantees listed in the 79 | /// [StreamChannel] documentation. This makes it somewhat less efficient than 80 | /// just wrapping a stream and a sink directly, so [StreamChannel.new] should 81 | /// be used when the guarantees are provided natively. 82 | /// 83 | /// If [allowSinkErrors] is `false`, errors are not allowed to be passed to 84 | /// [sink]. If any are, the connection will close and the error will be 85 | /// forwarded to [sink].done. 86 | factory StreamChannel.withGuarantees(Stream stream, StreamSink sink, 87 | {bool allowSinkErrors = true}) => 88 | GuaranteeChannel(stream, sink, allowSinkErrors: allowSinkErrors); 89 | 90 | /// Creates a new [StreamChannel] that communicates over [stream] and [sink]. 91 | /// 92 | /// This specifically enforces the second guarantee: closing the sink causes 93 | /// the stream to close before it emits any more events. This guarantee is 94 | /// invalidated when an asynchronous gap is added between the original 95 | /// stream's event dispatch and the returned stream's, for example by 96 | /// transforming it with a [StreamTransformer]. This is a lighter-weight way 97 | /// of preserving that guarantee in particular than 98 | /// [StreamChannel.withGuarantees]. 99 | factory StreamChannel.withCloseGuarantee( 100 | Stream stream, StreamSink sink) => 101 | CloseGuaranteeChannel(stream, sink); 102 | 103 | /// Connects this to [other], so that any values emitted by either are sent 104 | /// directly to the other. 105 | void pipe(StreamChannel other); 106 | 107 | /// Transforms this using [transformer]. 108 | /// 109 | /// This is identical to calling `transformer.bind(channel)`. 110 | StreamChannel transform(StreamChannelTransformer transformer); 111 | 112 | /// Transforms only the [stream] component of this using [transformer]. 113 | StreamChannel transformStream(StreamTransformer transformer); 114 | 115 | /// Transforms only the [sink] component of this using [transformer]. 116 | StreamChannel transformSink(StreamSinkTransformer transformer); 117 | 118 | /// Returns a copy of this with [stream] replaced by [change]'s return 119 | /// value. 120 | StreamChannel changeStream(Stream Function(Stream) change); 121 | 122 | /// Returns a copy of this with [sink] replaced by [change]'s return 123 | /// value. 124 | StreamChannel changeSink(StreamSink Function(StreamSink) change); 125 | 126 | /// Returns a copy of this with the generic type coerced to [S]. 127 | /// 128 | /// If any events emitted by [stream] aren't of type [S], they're converted 129 | /// into [TypeError] events (`CastError` on some SDK versions). Similarly, if 130 | /// any events are added to [sink] that aren't of type [S], a [TypeError] is 131 | /// thrown. 132 | StreamChannel cast(); 133 | } 134 | 135 | /// An implementation of [StreamChannel] that simply takes a stream and a sink 136 | /// as parameters. 137 | /// 138 | /// This is distinct from [StreamChannel] so that it can use 139 | /// [StreamChannelMixin]. 140 | class _StreamChannel extends StreamChannelMixin { 141 | @override 142 | final Stream stream; 143 | @override 144 | final StreamSink sink; 145 | 146 | _StreamChannel(this.stream, this.sink); 147 | } 148 | 149 | /// A mixin that implements the instance methods of [StreamChannel] in terms of 150 | /// [stream] and [sink]. 151 | abstract class StreamChannelMixin implements StreamChannel { 152 | @override 153 | void pipe(StreamChannel other) { 154 | stream.pipe(other.sink); 155 | other.stream.pipe(sink); 156 | } 157 | 158 | @override 159 | StreamChannel transform(StreamChannelTransformer transformer) => 160 | transformer.bind(this); 161 | 162 | @override 163 | StreamChannel transformStream(StreamTransformer transformer) => 164 | changeStream(transformer.bind); 165 | 166 | @override 167 | StreamChannel transformSink(StreamSinkTransformer transformer) => 168 | changeSink(transformer.bind); 169 | 170 | @override 171 | StreamChannel changeStream(Stream Function(Stream) change) => 172 | StreamChannel.withCloseGuarantee(change(stream), sink); 173 | 174 | @override 175 | StreamChannel changeSink(StreamSink Function(StreamSink) change) => 176 | StreamChannel.withCloseGuarantee(stream, change(sink)); 177 | 178 | @override 179 | StreamChannel cast() => StreamChannel( 180 | stream.cast(), StreamController(sync: true)..stream.cast().pipe(sink)); 181 | } 182 | -------------------------------------------------------------------------------- /lib/src/multi_channel.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:async/async.dart'; 8 | 9 | import '../stream_channel.dart'; 10 | 11 | /// A class that multiplexes multiple virtual channels across a single 12 | /// underlying transport layer. 13 | /// 14 | /// This should be connected to another [MultiChannel] on the other end of the 15 | /// underlying channel. It starts with a single default virtual channel, 16 | /// accessible via [stream] and [sink]. Additional virtual channels can be 17 | /// created with [virtualChannel]. 18 | /// 19 | /// When a virtual channel is created by one endpoint, the other must connect to 20 | /// it before messages may be sent through it. The first endpoint passes its 21 | /// [VirtualChannel.id] to the second, which then creates a channel from that id 22 | /// also using [virtualChannel]. For example: 23 | /// 24 | /// ```dart 25 | /// // First endpoint 26 | /// var virtual = multiChannel.virtualChannel(); 27 | /// multiChannel.sink.add({ 28 | /// "channel": virtual.id 29 | /// }); 30 | /// 31 | /// // Second endpoint 32 | /// multiChannel.stream.listen((message) { 33 | /// var virtual = multiChannel.virtualChannel(message["channel"]); 34 | /// // ... 35 | /// }); 36 | /// ``` 37 | /// 38 | /// Sending errors across a [MultiChannel] is not supported. Any errors from the 39 | /// underlying stream will be reported only via the default 40 | /// [MultiChannel.stream]. 41 | /// 42 | /// Each virtual channel may be closed individually. When all of them are 43 | /// closed, the underlying [StreamSink] is closed automatically. 44 | abstract class MultiChannel implements StreamChannel { 45 | /// The default input stream. 46 | /// 47 | /// This connects to the remote [sink]. 48 | @override 49 | Stream get stream; 50 | 51 | /// The default output stream. 52 | /// 53 | /// This connects to the remote [stream]. If this is closed, the remote 54 | /// [stream] will close, but other virtual channels will remain open and new 55 | /// virtual channels may be opened. 56 | @override 57 | StreamSink get sink; 58 | 59 | /// Creates a new [MultiChannel] that sends and receives messages over 60 | /// [inner]. 61 | /// 62 | /// The inner channel must take JSON-like objects. 63 | factory MultiChannel(StreamChannel inner) => _MultiChannel(inner); 64 | 65 | /// Creates a new virtual channel. 66 | /// 67 | /// If [id] is not passed, this creates a virtual channel from scratch. Before 68 | /// it's used, its [VirtualChannel.id] must be sent to the remote endpoint 69 | /// where [virtualChannel] should be called with that id. 70 | /// 71 | /// If [id] is passed, this creates a virtual channel corresponding to the 72 | /// channel with that id on the remote channel. 73 | /// 74 | /// Throws an [ArgumentError] if a virtual channel already exists for [id]. 75 | /// Throws a [StateError] if the underlying channel is closed. 76 | VirtualChannel virtualChannel([int? id]); 77 | } 78 | 79 | /// The implementation of [MultiChannel]. 80 | /// 81 | /// This is private so that [VirtualChannel] can inherit from [MultiChannel] 82 | /// without having to implement all the private members. 83 | class _MultiChannel extends StreamChannelMixin 84 | implements MultiChannel { 85 | /// The inner channel over which all communication is conducted. 86 | /// 87 | /// This will be `null` if the underlying communication channel is closed. 88 | StreamChannel? _inner; 89 | 90 | /// The subscription to [_inner].stream. 91 | StreamSubscription? _innerStreamSubscription; 92 | 93 | @override 94 | Stream get stream => _mainController.foreign.stream; 95 | @override 96 | StreamSink get sink => _mainController.foreign.sink; 97 | 98 | /// The controller for this channel. 99 | final _mainController = StreamChannelController(sync: true); 100 | 101 | /// A map from input IDs to [StreamChannelController]s that should be used to 102 | /// communicate over those channels. 103 | final _controllers = >{}; 104 | 105 | /// Input IDs of controllers in [_controllers] that we've received messages 106 | /// for but that have not yet had a local [virtualChannel] created. 107 | final _pendingIds = {}; 108 | 109 | /// Input IDs of virtual channels that used to exist but have since been 110 | /// closed. 111 | final _closedIds = {}; 112 | 113 | /// The next id to use for a local virtual channel. 114 | /// 115 | /// Ids are used to identify virtual channels. Each message is tagged with an 116 | /// id; the receiving [MultiChannel] uses this id to look up which 117 | /// [VirtualChannel] the message should be dispatched to. 118 | /// 119 | /// The id scheme for virtual channels is somewhat complicated. This is 120 | /// necessary to ensure that there are no conflicts even when both endpoints 121 | /// have virtual channels with the same id; since both endpoints can send and 122 | /// receive messages across each virtual channel, a naïve scheme would make it 123 | /// impossible to tell whether a message was from a channel that originated in 124 | /// the remote endpoint or a reply on a channel that originated in the local 125 | /// endpoint. 126 | /// 127 | /// The trick is that each endpoint only uses odd ids for its own channels. 128 | /// When sending a message over a channel that was created by the remote 129 | /// endpoint, the channel's id plus one is used. This way each [MultiChannel] 130 | /// knows that if an incoming message has an odd id, it's coming from a 131 | /// channel that was originally created remotely, but if it has an even id, 132 | /// it's coming from a channel that was originally created locally. 133 | var _nextId = 1; 134 | 135 | _MultiChannel(StreamChannel inner) : _inner = inner { 136 | // The default connection is a special case which has id 0 on both ends. 137 | // This allows it to begin connected without having to send over an id. 138 | _controllers[0] = _mainController; 139 | _mainController.local.stream.listen( 140 | (message) => _inner!.sink.add([0, message]), 141 | onDone: () => _closeChannel(0, 0)); 142 | 143 | _innerStreamSubscription = _inner!.stream.cast().listen((message) { 144 | var id = (message[0] as num).toInt(); 145 | 146 | // If the channel was closed before an incoming message was processed, 147 | // ignore that message. 148 | if (_closedIds.contains(id)) return; 149 | 150 | var controller = _controllers.putIfAbsent(id, () { 151 | // If we receive a message for a controller that doesn't have a local 152 | // counterpart yet, create a controller for it to buffer incoming 153 | // messages for when a local connection is created. 154 | _pendingIds.add(id); 155 | return StreamChannelController(sync: true); 156 | }); 157 | 158 | if (message.length > 1) { 159 | controller.local.sink.add(message[1] as T); 160 | } else { 161 | // A message without data indicates that the channel has been closed. We 162 | // can just close the sink here without doing any more cleanup, because 163 | // the sink closing will cause the stream to emit a done event which 164 | // will trigger more cleanup. 165 | controller.local.sink.close(); 166 | } 167 | }, 168 | onDone: _closeInnerChannel, 169 | onError: _mainController.local.sink.addError); 170 | } 171 | 172 | @override 173 | VirtualChannel virtualChannel([int? id]) { 174 | int inputId; 175 | int outputId; 176 | if (id != null) { 177 | // Since the user is passing in an id, we're connected to a remote 178 | // VirtualChannel. This means messages they send over this channel will 179 | // have the original odd id, but our replies will have an even id. 180 | inputId = id; 181 | outputId = id + 1; 182 | } else { 183 | // Since we're generating an id, we originated this VirtualChannel. This 184 | // means messages we send over this channel will have the original odd id, 185 | // but the remote channel's replies will have an even id. 186 | inputId = _nextId + 1; 187 | outputId = _nextId; 188 | _nextId += 2; 189 | } 190 | 191 | // If the inner channel has already closed, create new virtual channels in a 192 | // closed state. 193 | if (_inner == null) { 194 | return VirtualChannel._( 195 | this, inputId, const Stream.empty(), NullStreamSink()); 196 | } 197 | 198 | late StreamChannelController controller; 199 | if (_pendingIds.remove(inputId)) { 200 | // If we've already received messages for this channel, use the controller 201 | // where those messages are buffered. 202 | controller = _controllers[inputId]!; 203 | } else if (_controllers.containsKey(inputId) || 204 | _closedIds.contains(inputId)) { 205 | throw ArgumentError('A virtual channel with id $id already exists.'); 206 | } else { 207 | controller = StreamChannelController(sync: true); 208 | _controllers[inputId] = controller; 209 | } 210 | 211 | controller.local.stream.listen( 212 | (message) => _inner!.sink.add([outputId, message]), 213 | onDone: () => _closeChannel(inputId, outputId)); 214 | return VirtualChannel._( 215 | this, outputId, controller.foreign.stream, controller.foreign.sink); 216 | } 217 | 218 | /// Closes the virtual channel for which incoming messages have [inputId] and 219 | /// outgoing messages have [outputId]. 220 | void _closeChannel(int inputId, int outputId) { 221 | _closedIds.add(inputId); 222 | var controller = _controllers.remove(inputId)!; 223 | controller.local.sink.close(); 224 | 225 | if (_inner == null) return; 226 | 227 | // A message without data indicates that the virtual channel has been 228 | // closed. 229 | _inner!.sink.add([outputId]); 230 | if (_controllers.isEmpty) _closeInnerChannel(); 231 | } 232 | 233 | /// Closes the underlying communication channel. 234 | void _closeInnerChannel() { 235 | _inner!.sink.close(); 236 | _innerStreamSubscription!.cancel(); 237 | _inner = null; 238 | 239 | // Convert this to a list because the close is dispatched synchronously, and 240 | // that could conceivably remove a controller from [_controllers]. 241 | for (var controller in _controllers.values.toList(growable: false)) { 242 | controller.local.sink.close(); 243 | } 244 | _controllers.clear(); 245 | } 246 | } 247 | 248 | /// A virtual channel created by [MultiChannel]. 249 | /// 250 | /// This implements [MultiChannel] for convenience. 251 | /// [VirtualChannel.virtualChannel] is semantically identical to the parent's 252 | /// [MultiChannel.virtualChannel]. 253 | class VirtualChannel extends StreamChannelMixin 254 | implements MultiChannel { 255 | /// The [MultiChannel] that created this. 256 | final MultiChannel _parent; 257 | 258 | /// The identifier for this channel. 259 | /// 260 | /// This can be sent across the [MultiChannel] to provide the remote endpoint 261 | /// a means to connect to this channel. Nothing about this is guaranteed 262 | /// except that it will be JSON-serializable. 263 | final int id; 264 | 265 | @override 266 | final Stream stream; 267 | @override 268 | final StreamSink sink; 269 | 270 | VirtualChannel._(this._parent, this.id, this.stream, this.sink); 271 | 272 | @override 273 | VirtualChannel virtualChannel([int? id]) => _parent.virtualChannel(id); 274 | } 275 | -------------------------------------------------------------------------------- /test/multi_channel_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:stream_channel/stream_channel.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | void main() { 11 | late StreamChannelController controller; 12 | late MultiChannel channel1; 13 | late MultiChannel channel2; 14 | setUp(() { 15 | controller = StreamChannelController(); 16 | channel1 = MultiChannel(controller.local); 17 | channel2 = MultiChannel(controller.foreign); 18 | }); 19 | 20 | group('the default virtual channel', () { 21 | test('begins connected', () { 22 | var first = true; 23 | channel2.stream.listen(expectAsync1((message) { 24 | if (first) { 25 | expect(message, equals(1)); 26 | first = false; 27 | } else { 28 | expect(message, equals(2)); 29 | } 30 | }, count: 2)); 31 | 32 | channel1.sink.add(1); 33 | channel1.sink.add(2); 34 | }); 35 | 36 | test('closes the remote virtual channel when it closes', () { 37 | expect(channel2.stream.toList(), completion(isEmpty)); 38 | expect(channel2.sink.done, completes); 39 | 40 | channel1.sink.close(); 41 | }); 42 | 43 | test('closes the local virtual channel when it closes', () { 44 | expect(channel1.stream.toList(), completion(isEmpty)); 45 | expect(channel1.sink.done, completes); 46 | 47 | channel1.sink.close(); 48 | }); 49 | 50 | test( 51 | "doesn't closes the local virtual channel when the stream " 52 | 'subscription is canceled', () { 53 | channel1.sink.done.then(expectAsync1((_) {}, count: 0)); 54 | 55 | channel1.stream.listen((_) {}).cancel(); 56 | 57 | // Ensure that there's enough time for the channel to close if it's going 58 | // to. 59 | return pumpEventQueue(); 60 | }); 61 | 62 | test( 63 | 'closes the underlying channel when it closes without any other ' 64 | 'virtual channels', () { 65 | expect(controller.local.sink.done, completes); 66 | expect(controller.foreign.sink.done, completes); 67 | 68 | channel1.sink.close(); 69 | }); 70 | 71 | test( 72 | "doesn't close the underlying channel when it closes with other " 73 | 'virtual channels', () { 74 | controller.local.sink.done.then(expectAsync1((_) {}, count: 0)); 75 | controller.foreign.sink.done.then(expectAsync1((_) {}, count: 0)); 76 | 77 | // Establish another virtual connection which should keep the underlying 78 | // connection open. 79 | channel2.virtualChannel(channel1.virtualChannel().id); 80 | channel1.sink.close(); 81 | 82 | // Ensure that there's enough time for the underlying channel to complete 83 | // if it's going to. 84 | return pumpEventQueue(); 85 | }); 86 | }); 87 | 88 | group('a locally-created virtual channel', () { 89 | late VirtualChannel virtual1; 90 | late VirtualChannel virtual2; 91 | setUp(() { 92 | virtual1 = channel1.virtualChannel(); 93 | virtual2 = channel2.virtualChannel(virtual1.id); 94 | }); 95 | 96 | test('sends messages only to the other virtual channel', () { 97 | var first = true; 98 | virtual2.stream.listen(expectAsync1((message) { 99 | if (first) { 100 | expect(message, equals(1)); 101 | first = false; 102 | } else { 103 | expect(message, equals(2)); 104 | } 105 | }, count: 2)); 106 | 107 | // No other virtual channels should receive the message. 108 | for (var i = 0; i < 10; i++) { 109 | var virtual = channel2.virtualChannel(channel1.virtualChannel().id); 110 | virtual.stream.listen(expectAsync1((_) {}, count: 0)); 111 | } 112 | channel2.stream.listen(expectAsync1((_) {}, count: 0)); 113 | 114 | virtual1.sink.add(1); 115 | virtual1.sink.add(2); 116 | }); 117 | 118 | test('closes the remote virtual channel when it closes', () { 119 | expect(virtual2.stream.toList(), completion(isEmpty)); 120 | expect(virtual2.sink.done, completes); 121 | 122 | virtual1.sink.close(); 123 | }); 124 | 125 | test('closes the local virtual channel when it closes', () { 126 | expect(virtual1.stream.toList(), completion(isEmpty)); 127 | expect(virtual1.sink.done, completes); 128 | 129 | virtual1.sink.close(); 130 | }); 131 | 132 | test( 133 | "doesn't closes the local virtual channel when the stream " 134 | 'subscription is canceled', () { 135 | virtual1.sink.done.then(expectAsync1((_) {}, count: 0)); 136 | virtual1.stream.listen((_) {}).cancel(); 137 | 138 | // Ensure that there's enough time for the channel to close if it's going 139 | // to. 140 | return pumpEventQueue(); 141 | }); 142 | 143 | test( 144 | 'closes the underlying channel when it closes without any other ' 145 | 'virtual channels', () async { 146 | // First close the default channel so we can test the new channel as the 147 | // last living virtual channel. 148 | unawaited(channel1.sink.close()); 149 | 150 | await channel2.stream.toList(); 151 | expect(controller.local.sink.done, completes); 152 | expect(controller.foreign.sink.done, completes); 153 | 154 | unawaited(virtual1.sink.close()); 155 | }); 156 | 157 | test( 158 | "doesn't close the underlying channel when it closes with other " 159 | 'virtual channels', () { 160 | controller.local.sink.done.then(expectAsync1((_) {}, count: 0)); 161 | controller.foreign.sink.done.then(expectAsync1((_) {}, count: 0)); 162 | 163 | virtual1.sink.close(); 164 | 165 | // Ensure that there's enough time for the underlying channel to complete 166 | // if it's going to. 167 | return pumpEventQueue(); 168 | }); 169 | 170 | test("doesn't conflict with a remote virtual channel", () { 171 | var virtual3 = channel2.virtualChannel(); 172 | var virtual4 = channel1.virtualChannel(virtual3.id); 173 | 174 | // This is an implementation detail, but we assert it here to make sure 175 | // we're properly testing two channels with the same id. 176 | expect(virtual1.id, equals(virtual3.id)); 177 | 178 | virtual2.stream 179 | .listen(expectAsync1((message) => expect(message, equals(1)))); 180 | virtual4.stream 181 | .listen(expectAsync1((message) => expect(message, equals(2)))); 182 | 183 | virtual1.sink.add(1); 184 | virtual3.sink.add(2); 185 | }); 186 | }); 187 | 188 | group('a remotely-created virtual channel', () { 189 | late VirtualChannel virtual1; 190 | late VirtualChannel virtual2; 191 | setUp(() { 192 | virtual1 = channel1.virtualChannel(); 193 | virtual2 = channel2.virtualChannel(virtual1.id); 194 | }); 195 | 196 | test('sends messages only to the other virtual channel', () { 197 | var first = true; 198 | virtual1.stream.listen(expectAsync1((message) { 199 | if (first) { 200 | expect(message, equals(1)); 201 | first = false; 202 | } else { 203 | expect(message, equals(2)); 204 | } 205 | }, count: 2)); 206 | 207 | // No other virtual channels should receive the message. 208 | for (var i = 0; i < 10; i++) { 209 | var virtual = channel2.virtualChannel(channel1.virtualChannel().id); 210 | virtual.stream.listen(expectAsync1((_) {}, count: 0)); 211 | } 212 | channel1.stream.listen(expectAsync1((_) {}, count: 0)); 213 | 214 | virtual2.sink.add(1); 215 | virtual2.sink.add(2); 216 | }); 217 | 218 | test('closes the remote virtual channel when it closes', () { 219 | expect(virtual1.stream.toList(), completion(isEmpty)); 220 | expect(virtual1.sink.done, completes); 221 | 222 | virtual2.sink.close(); 223 | }); 224 | 225 | test('closes the local virtual channel when it closes', () { 226 | expect(virtual2.stream.toList(), completion(isEmpty)); 227 | expect(virtual2.sink.done, completes); 228 | 229 | virtual2.sink.close(); 230 | }); 231 | 232 | test( 233 | "doesn't closes the local virtual channel when the stream " 234 | 'subscription is canceled', () { 235 | virtual2.sink.done.then(expectAsync1((_) {}, count: 0)); 236 | virtual2.stream.listen((_) {}).cancel(); 237 | 238 | // Ensure that there's enough time for the channel to close if it's going 239 | // to. 240 | return pumpEventQueue(); 241 | }); 242 | 243 | test( 244 | 'closes the underlying channel when it closes without any other ' 245 | 'virtual channels', () async { 246 | // First close the default channel so we can test the new channel as the 247 | // last living virtual channel. 248 | unawaited(channel2.sink.close()); 249 | 250 | await channel1.stream.toList(); 251 | expect(controller.local.sink.done, completes); 252 | expect(controller.foreign.sink.done, completes); 253 | 254 | unawaited(virtual2.sink.close()); 255 | }); 256 | 257 | test( 258 | "doesn't close the underlying channel when it closes with other " 259 | 'virtual channels', () { 260 | controller.local.sink.done.then(expectAsync1((_) {}, count: 0)); 261 | controller.foreign.sink.done.then(expectAsync1((_) {}, count: 0)); 262 | 263 | virtual2.sink.close(); 264 | 265 | // Ensure that there's enough time for the underlying channel to complete 266 | // if it's going to. 267 | return pumpEventQueue(); 268 | }); 269 | 270 | test("doesn't allow another virtual channel with the same id", () { 271 | expect(() => channel2.virtualChannel(virtual1.id), throwsArgumentError); 272 | }); 273 | 274 | test('dispatches events received before the virtual channel is created', 275 | () async { 276 | virtual1 = channel1.virtualChannel(); 277 | 278 | virtual1.sink.add(1); 279 | await pumpEventQueue(); 280 | 281 | virtual1.sink.add(2); 282 | await pumpEventQueue(); 283 | 284 | expect(channel2.virtualChannel(virtual1.id).stream, emitsInOrder([1, 2])); 285 | }); 286 | 287 | test( 288 | 'dispatches close events received before the virtual channel is ' 289 | 'created', () async { 290 | virtual1 = channel1.virtualChannel(); 291 | 292 | unawaited(virtual1.sink.close()); 293 | await pumpEventQueue(); 294 | 295 | expect(channel2.virtualChannel(virtual1.id).stream.toList(), 296 | completion(isEmpty)); 297 | }); 298 | }); 299 | 300 | group('when the underlying stream', () { 301 | late VirtualChannel virtual1; 302 | late VirtualChannel virtual2; 303 | setUp(() { 304 | virtual1 = channel1.virtualChannel(); 305 | virtual2 = channel2.virtualChannel(virtual1.id); 306 | }); 307 | 308 | test('closes, all virtual channels close', () { 309 | expect(channel1.stream.toList(), completion(isEmpty)); 310 | expect(channel1.sink.done, completes); 311 | expect(channel2.stream.toList(), completion(isEmpty)); 312 | expect(channel2.sink.done, completes); 313 | expect(virtual1.stream.toList(), completion(isEmpty)); 314 | expect(virtual1.sink.done, completes); 315 | expect(virtual2.stream.toList(), completion(isEmpty)); 316 | expect(virtual2.sink.done, completes); 317 | 318 | controller.local.sink.close(); 319 | }); 320 | 321 | test('closes, more virtual channels are created closed', () async { 322 | unawaited(channel2.sink.close()); 323 | unawaited(virtual2.sink.close()); 324 | 325 | // Wait for the existing channels to emit done events. 326 | await channel1.stream.toList(); 327 | await virtual1.stream.toList(); 328 | 329 | var virtual = channel1.virtualChannel(); 330 | expect(virtual.stream.toList(), completion(isEmpty)); 331 | expect(virtual.sink.done, completes); 332 | 333 | virtual = channel1.virtualChannel(); 334 | expect(virtual.stream.toList(), completion(isEmpty)); 335 | expect(virtual.sink.done, completes); 336 | }); 337 | 338 | test('emits an error, the error is sent only to the default channel', () { 339 | channel1.stream.listen(expectAsync1((_) {}, count: 0), 340 | onError: expectAsync1((error) => expect(error, equals('oh no')))); 341 | virtual1.stream.listen(expectAsync1((_) {}, count: 0), 342 | onError: expectAsync1((_) {}, count: 0)); 343 | 344 | controller.foreign.sink.addError('oh no'); 345 | }); 346 | }); 347 | 348 | group('stream channel rules', () { 349 | group('for the main stream:', () { 350 | test( 351 | 'closing the sink causes the stream to close before it emits any ' 352 | 'more events', () { 353 | channel1.sink.add(1); 354 | channel1.sink.add(2); 355 | channel1.sink.add(3); 356 | 357 | channel2.stream.listen(expectAsync1((message) { 358 | expect(message, equals(1)); 359 | channel2.sink.close(); 360 | }, count: 1)); 361 | }); 362 | 363 | test('after the stream closes, the sink ignores events', () async { 364 | unawaited(channel1.sink.close()); 365 | 366 | // Wait for the done event to be delivered. 367 | await channel2.stream.toList(); 368 | channel2.sink.add(1); 369 | channel2.sink.add(2); 370 | channel2.sink.add(3); 371 | unawaited(channel2.sink.close()); 372 | 373 | // None of our channel.sink additions should make it to the other 374 | // endpoint. 375 | channel1.stream.listen(expectAsync1((_) {}, count: 0)); 376 | await pumpEventQueue(); 377 | }); 378 | 379 | test("canceling the stream's subscription has no effect on the sink", 380 | () async { 381 | unawaited(channel1.stream.listen(null).cancel()); 382 | await pumpEventQueue(); 383 | 384 | channel1.sink.add(1); 385 | channel1.sink.add(2); 386 | channel1.sink.add(3); 387 | unawaited(channel1.sink.close()); 388 | expect(channel2.stream.toList(), completion(equals([1, 2, 3]))); 389 | }); 390 | 391 | test("canceling the stream's subscription doesn't stop a done event", 392 | () async { 393 | unawaited(channel1.stream.listen(null).cancel()); 394 | await pumpEventQueue(); 395 | 396 | unawaited(channel2.sink.close()); 397 | await pumpEventQueue(); 398 | 399 | channel1.sink.add(1); 400 | channel1.sink.add(2); 401 | channel1.sink.add(3); 402 | unawaited(channel1.sink.close()); 403 | 404 | // The sink should be ignoring events because the channel closed. 405 | channel2.stream.listen(expectAsync1((_) {}, count: 0)); 406 | await pumpEventQueue(); 407 | }); 408 | }); 409 | 410 | group('for a virtual channel:', () { 411 | late VirtualChannel virtual1; 412 | late VirtualChannel virtual2; 413 | setUp(() { 414 | virtual1 = channel1.virtualChannel(); 415 | virtual2 = channel2.virtualChannel(virtual1.id); 416 | }); 417 | 418 | test( 419 | 'closing the sink causes the stream to close before it emits any ' 420 | 'more events', () { 421 | virtual1.sink.add(1); 422 | virtual1.sink.add(2); 423 | virtual1.sink.add(3); 424 | 425 | virtual2.stream.listen(expectAsync1((message) { 426 | expect(message, equals(1)); 427 | virtual2.sink.close(); 428 | }, count: 1)); 429 | }); 430 | 431 | test('after the stream closes, the sink ignores events', () async { 432 | unawaited(virtual1.sink.close()); 433 | 434 | // Wait for the done event to be delivered. 435 | await virtual2.stream.toList(); 436 | virtual2.sink.add(1); 437 | virtual2.sink.add(2); 438 | virtual2.sink.add(3); 439 | unawaited(virtual2.sink.close()); 440 | 441 | // None of our virtual.sink additions should make it to the other 442 | // endpoint. 443 | virtual1.stream.listen(expectAsync1((_) {}, count: 0)); 444 | await pumpEventQueue(); 445 | }); 446 | 447 | test("canceling the stream's subscription has no effect on the sink", 448 | () async { 449 | unawaited(virtual1.stream.listen(null).cancel()); 450 | await pumpEventQueue(); 451 | 452 | virtual1.sink.add(1); 453 | virtual1.sink.add(2); 454 | virtual1.sink.add(3); 455 | unawaited(virtual1.sink.close()); 456 | expect(virtual2.stream.toList(), completion(equals([1, 2, 3]))); 457 | }); 458 | 459 | test("canceling the stream's subscription doesn't stop a done event", 460 | () async { 461 | unawaited(virtual1.stream.listen(null).cancel()); 462 | await pumpEventQueue(); 463 | 464 | unawaited(virtual2.sink.close()); 465 | await pumpEventQueue(); 466 | 467 | virtual1.sink.add(1); 468 | virtual1.sink.add(2); 469 | virtual1.sink.add(3); 470 | unawaited(virtual1.sink.close()); 471 | 472 | // The sink should be ignoring events because the stream closed. 473 | virtual2.stream.listen(expectAsync1((_) {}, count: 0)); 474 | await pumpEventQueue(); 475 | }); 476 | }); 477 | }); 478 | } 479 | --------------------------------------------------------------------------------