diff --git a/.github/labeler.yml b/.github/labeler.yml index 378a6351..8ce6083d 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -4,6 +4,10 @@ - changed-files: - any-glob-to-any-file: 'pkgs/args/**' +"package:async": + - changed-files: + - any-glob-to-any-file: 'pkgs/async/**' + "package:convert": - changed-files: - any-glob-to-any-file: 'pkgs/convert/**' diff --git a/.github/workflows/async.yaml b/.github/workflows/async.yaml new file mode 100644 index 00000000..025bfde0 --- /dev/null +++ b/.github/workflows/async.yaml @@ -0,0 +1,75 @@ +name: package:async + +on: + # Run on PRs and pushes to the default branch. + push: + branches: [ main ] + paths: + - '.github/workflows/async.yaml' + - 'pkgs/async/**' + pull_request: + branches: [ main ] + paths: + - '.github/workflows/async.yaml' + - 'pkgs/async/**' + schedule: + - cron: "0 0 * * 0" + +env: + PUB_ENVIRONMENT: bot.github + +defaults: + run: + working-directory: pkgs/async/ + +jobs: + # Check code formatting and static analysis on a single OS (linux) + # against Dart dev. + analyze: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sdk: [dev] + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Check formatting + run: dart format --output=none --set-exit-if-changed . + if: always() && steps.install.outcome == 'success' + - name: Analyze code + run: dart analyze --fatal-infos + if: always() && steps.install.outcome == 'success' + + # Run tests on a matrix consisting of two dimensions: + # 1. OS: ubuntu-latest, (macos-latest, windows-latest) + # 2. release channel: dev + test: + needs: analyze + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Add macos-latest and/or windows-latest if relevant for this package. + os: [ubuntu-latest] + sdk: [3.4, dev] + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Run VM tests + run: dart test --platform vm + if: always() && steps.install.outcome == 'success' + - run: dart test --platform chrome --compiler dart2js + if: always() && steps.install.outcome == 'success' + - run: dart test --platform chrome --compiler dart2wasm + if: always() && steps.install.outcome == 'success' diff --git a/README.md b/README.md index b067178e..d3063b5a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This repository is home to various Dart packages under the [dart.dev](https://pu | Package | Description | Version | |---|---|---| | [args](pkgs/args/) | Library for defining parsers for parsing raw command-line arguments into a set of options and values. | [![pub package](https://img.shields.io/pub/v/args.svg)](https://pub.dev/packages/args) | +| [async](pkgs/async/) | Utility functions and classes related to the 'dart:async' library.| [![pub package](https://img.shields.io/pub/v/async.svg)](https://pub.dev/packages/async) | | [convert](pkgs/convert/) | Utilities for converting between data representations. | [![pub package](https://img.shields.io/pub/v/convert.svg)](https://pub.dev/packages/convert) | | [crypto](pkgs/crypto/) | Implementations of SHA, MD5, and HMAC cryptographic functions. | [![pub package](https://img.shields.io/pub/v/crypto.svg)](https://pub.dev/packages/crypto) | | [fixnum](pkgs/fixnum/) | Library for 32- and 64-bit signed fixed-width integers. | [![pub package](https://img.shields.io/pub/v/fixnum.svg)](https://pub.dev/packages/fixnum) | diff --git a/pkgs/async/.gitignore b/pkgs/async/.gitignore new file mode 100644 index 00000000..29b55910 --- /dev/null +++ b/pkgs/async/.gitignore @@ -0,0 +1,11 @@ +# See https://dart.dev/guides/libraries/private-files + +.buildlog +.DS_Store +.idea + +.dart_tool/ +.settings/ +build/ +pubspec.lock +.packages diff --git a/pkgs/async/AUTHORS b/pkgs/async/AUTHORS new file mode 100644 index 00000000..e8063a8c --- /dev/null +++ b/pkgs/async/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/pkgs/async/CHANGELOG.md b/pkgs/async/CHANGELOG.md new file mode 100644 index 00000000..06ac7d11 --- /dev/null +++ b/pkgs/async/CHANGELOG.md @@ -0,0 +1,378 @@ +## 2.12.0 + +- Require Dart 3.4. +- Move to `dart-lang/core` monorepo. + +## 2.11.0 + +* Add `CancelableOperation.fromValue`. +* Add `StreamExtensions.listenAndBuffer`, which buffers events from a stream + before it has a listener. + +## 2.10.0 + +* Add `CancelableOperation.thenOperation` which gives more flexibility to + complete the resulting operation. +* Add `CancelableCompleter.completeOperation`. +* Require Dart 2.18. + +## 2.9.0 + +* **Potentially Breaking** The default `propagateCancel` argument to + `CancelableOperation.then` changed from `false` to `true`. In most usages this + won't have a meaningful difference in behavior, but in usages where the + behavior is important propagation is the more common need. If there are any + `CancelableOperation` with multiple listeners where canceling subsequent + computation using `.then` shouldn't also cancel the original operation, pass + `propagateCancel: false`. +* Add `StreamExtensions.firstOrNull`. +* Add a `CancelableOperation.fromSubscription()` static factory. +* Add a `CancelableOperation.race()` static method. +* Update `StreamGroup` methods that return a `Future` today to return + a `Future` instead. +* Deprecated `AsyncCache.fetchStream`. +* Make `AsyncCache.ephemeral` invalidate itself immediately when the returned + future completes, rather than wait for a later timer event. + +## 2.8.2 + +* Deprecate `EventSinkBase`, `StreamSinkBase`, `IOSinkBase`. + +## 2.8.1 + +* Don't ignore broadcast streams added to a `StreamGroup` that doesn't have an + active listener but previously had listeners and contains a single + subscription inner stream. + +## 2.8.0 + +* Add `EventSinkBase`, `StreamSinkBase`, and `IOSinkBase` classes to make it + easier to implement custom sinks. +* Improve performance for `ChunkedStreamReader` by creating fewer internal + sublists and specializing to create views for `Uint8List` chunks. + +## 2.7.0 + +* Add a `Stream.slices()` extension method. +* Fix a bug where `CancelableOperation.then` may invoke the `onValue` callback, + even if it had been canceled before `CancelableOperation.value` completes. +* Fix a bug in `CancelableOperation.isComplete` where it may appear to be + complete and no longer be cancelable when it in fact could still be canceled. + +## 2.6.1 + +* When `StreamGroup.stream.listen()` is called, gracefully handle component + streams throwing errors when their `Stream.listen()` methods are called. + +## 2.6.0 + +* Add a `StreamCloser` class, which is a `StreamTransformer` that allows the + caller to force the stream to emit a done event. +* Added `ChunkedStreamReader` for reading _chunked streams_ without managing + buffers. +* Add extensions on `StreamSink`, including `StreamSink.transform()` for + applying `StreamSinkTransformer`s and `StreamSink.rejectErrors()`. +* Add `StreamGroup.isIdle` and `StreamGroup.onIdle`. +* Add `StreamGroup.isClosed` and `FutureGroup.isClosed` getters. + +## 2.5.0 + +* Stable release for null safety. + +## 2.5.0-nullsafety.3 + +* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release + guidelines. + +## 2.5.0-nullsafety.2 + +* Remove the unusable setter `CancelableOperation.operation=`. This was + mistakenly added to the public API but could never be called. +* Allow 2.12.0 dev SDK versions. + +## 2.5.0-nullsafety.1 + +* Allow 2.10 stable and 2.11.0 dev SDK versions. + +## 2.5.0-nullsafety + +* Migrate this package to null safety. + +## 2.4.2 + +* `StreamQueue` starts listening immediately to broadcast strings. + +## 2.4.1 + +* Deprecate `DelegatingStream.typed`. Use `Stream.cast` instead. +* Deprecate `DelegatingStreamSubcription.typed` and + `DelegatingStreamConsumer.typed`. For each of these the `Stream` should be + cast to the correct type before being used. +* Deprecate `DelegatingStreamSink.typed`. `DelegatingSink.typed`, + `DelegatingEventSink.typed`, `DelegatingStreamConsumer.typed`. For each of + these a new `StreamController` can be constructed to forward to the sink. + `StreamController()..stream.cast().pipe(sink)` +* Deprecate `typedStreamTransformer`. Cast after transforming instead. +* Deprecate `StreamSinkTransformer.typed` since there was no usage. +* Improve docs for `CancelablOperation.fromFuture`, indicate that `isCompleted` + starts `true`. + +## 2.4.0 + +* Add `StreamGroup.mergeBroadcast()` utility. + +## 2.3.0 + +* Implement `RestartableTimer.tick`. + +## 2.2.0 + +* Add `then` to `CancelableOperation`. + +## 2.1.0 + +* Fix `CancelableOperation.valueOrCancellation`'s type signature +* Add `isCanceled` and `isCompleted` to `CancelableOperation`. + +## 2.0.8 + +* Set max SDK version to `<3.0.0`. +* Deprecate `DelegatingFuture.typed`, it is not necessary in Dart 2. + +## 2.0.7 + +* Fix Dart 2 runtime errors. +* Stop using deprecated constants from the SDK. + +## 2.0.6 + +* Add further support for Dart 2.0 library changes to `Stream`. + +## 2.0.5 + +* Fix Dart 2.0 [runtime cast errors][sdk#27223] in `StreamQueue`. + +[sdk#27223]: https://github.com/dart-lang/sdk/issues/27223 + +## 2.0.4 + +* Add support for Dart 2.0 library changes to `Stream` and `StreamTransformer`. + Changed classes that implement `StreamTransformer` to extend + `StreamTransformerBase`, and changed signatures of `firstWhere`, `lastWhere`, + and `singleWhere` on classes extending `Stream`. See + also [issue 31847][sdk#31847]. + + [sdk#31847]: https://github.com/dart-lang/sdk/issues/31847 + +## 2.0.3 + +* Fix a bug in `StreamQueue.startTransaction()` and related methods when + rejecting a transaction that isn't the oldest request in the queue. + +## 2.0.2 + +* Add support for Dart 2.0 library changes to class `Timer`. + +## 2.0.1 + +* Fix a fuzzy arrow type warning. + +## 2.0.0 +* Remove deprecated public `result.dart` and `stream_zip.dart` libraries and + deprecated classes `ReleaseStreamTransformer` and `CaptureStreamTransformer`. + +* Add `captureAll` and `flattenList` static methods to `Result`. + +* Change `ErrorResult` to not be generic and always be a `Result`. + That makes an error independent of the type of result it occurs instead of. + +## 1.13.3 + +* Make `TypeSafeStream` extend `Stream` instead of implementing it. This ensures + that new methods on `Stream` are automatically picked up, they will go through + the `listen` method which type-checks every event. + +## 1.13.2 + +* Fix a type-warning. + +## 1.13.1 + +* Use `FutureOr` for various APIs that had previously used `dynamic`. + +## 1.13.0 + +* Add `collectBytes` and `collectBytesCancelable` functions which collects + list-of-byte events into a single byte list. + +* Fix a bug where rejecting a `StreamQueueTransaction` would throw a + `StateError` if `StreamQueue.rest` had been called on one of its child queues. + +* `StreamQueue.withTransaction()` now properly returns whether or not the + transaction was committed. + +## 1.12.0 + +* Add an `AsyncCache` class that caches asynchronous operations for a period of + time. + +* Add `StreamQueue.peek` and `StreamQueue.lookAheead`. + These allow users to look at events without consuming them. + +* Add `StreamQueue.startTransaction()` and `StreamQueue.withTransaction()`. + These allow users to conditionally consume events based on their values. + +* Add `StreamQueue.cancelable()`, which allows users to easily make a + `CancelableOperation` that can be canceled without affecting the queue. + +* Add `StreamQueue.eventsDispatched` which counts the number of events that have + been dispatched by a given queue. + +* Add a `subscriptionTransformer()` function to create `StreamTransformer`s that + modify the behavior of subscriptions to a stream. + +## 1.11.3 + +* Fix strong-mode warning against the signature of Future.then + +## 1.11.1 + +* Fix new strong-mode warnings introduced in Dart 1.17.0. + +## 1.11.0 + +* Add a `typedStreamTransformer()` function. This wraps an untyped + `StreamTransformer` with the correct type parameters, and asserts the types of + events as they're emitted from the transformed stream. + +* Add a `StreamSinkTransformer.typed()` static method. This wraps an untyped + `StreamSinkTransformer` with the correct type parameters, and asserts the + types of arguments passed in to the resulting sink. + +## 1.10.0 + +* Add `DelegatingFuture.typed()`, `DelegatingStreamSubscription.typed()`, + `DelegatingStreamConsumer.typed()`, `DelegatingSink.typed()`, + `DelegatingEventSink.typed()`, and `DelegatingStreamSink.typed()` static + methods. These wrap untyped instances of these classes with the correct type + parameter, and assert the types of values as they're accessed. + +* Add a `DelegatingStream` class. This is behaviorally identical to `StreamView` + from `dart:async`, but it follows this package's naming conventions and + provides a `DelegatingStream.typed()` static method. + +* Fix all strong mode warnings and add generic method annotations. + +* `new StreamQueue()`, `new SubscriptionStream()`, `new + DelegatingStreamSubscription()`, `new DelegatingStreamConsumer()`, `new + DelegatingSink()`, `new DelegatingEventSink()`, and `new + DelegatingStreamSink()` now take arguments with generic type arguments (for + example `Stream`) rather than without (for example `Stream`). + Passing a type that wasn't `is`-compatible with the fully-specified generic + would already throw an error under some circumstances, so this is not + considered a breaking change. + +* `ErrorResult` now takes a type parameter. + +* `Result.asError` now returns a `Result`. + +## 1.9.0 + +* Deprecate top-level libraries other than `package:async/async.dart`, which + exports these libraries' interfaces. + +* Add `Result.captureStreamTransformer`, `Result.releaseStreamTransformer`, + `Result.captureSinkTransformer`, and `Result.releaseSinkTransformer`. + +* Deprecate `CaptureStreamTransformer`, `ReleaseStreamTransformer`, + `CaptureSink`, and `ReleaseSink`. `Result.captureStreamTransformer`, + `Result.releaseStreamTransformer`, `Result.captureSinkTransformer`, and + `Result.releaseSinkTransformer` should be used instead. + +## 1.8.0 + +- Added `StreamSinkCompleter`, for creating a `StreamSink` now and providing its + destination later as another sink. + +- Added `StreamCompleter.setError`, a shortcut for emitting a single error event + on the resulting stream. + +- Added `NullStreamSink`, an implementation of `StreamSink` that discards all + events. + +## 1.7.0 + +- Added `SingleSubscriptionTransformer`, a `StreamTransformer` that converts a + broadcast stream into a single-subscription stream. + +## 1.6.0 + +- Added `CancelableOperation.valueOrCancellation()`, which allows users to be + notified when an operation is canceled elsewhere. + +- Added `StreamSinkTransformer` which transforms events before they're passed to + a `StreamSink`, similarly to how `StreamTransformer` transforms events after + they're emitted by a stream. + +## 1.5.0 + +- Added `LazyStream`, which forwards to the return value of a callback that's + only called when the stream is listened to. + +## 1.4.0 + +- Added `AsyncMemoizer.future`, which allows the result to be accessed before + `runOnce()` is called. + +- Added `CancelableOperation`, an asynchronous operation that can be canceled. + It can be created using a `CancelableCompleter`. + +- Added `RestartableTimer`, a non-periodic timer that can be reset over and + over. + +## 1.3.0 + +- Added `StreamCompleter` class for creating a stream now and providing its + events later as another stream. + +- Added `StreamQueue` class which allows requesting events from a stream + before they are avilable. It is like a `StreamIterator` that can queue + requests. + +- Added `SubscriptionStream` which creates a single-subscription stream + from an existing stream subscription. + +- Added a `ResultFuture` class for synchronously accessing the result of a + wrapped future. + +- Added `FutureGroup.onIdle` and `FutureGroup.isIdle`, which provide visibility + into whether a group is actively waiting on any futures. + +- Add an `AsyncMemoizer` class for running an asynchronous block of code exactly + once. + +- Added delegating wrapper classes for a number of core async types: + `DelegatingFuture`, `DelegatingStreamConsumer`, `DelegatingStreamController`, + `DelegatingSink`, `DelegatingEventSink`, `DelegatingStreamSink`, and + `DelegatingStreamSubscription`. These are all simple wrappers that forward all + calls to the wrapped objects. They can be used to expose only the desired + interface for subclasses, or extended to add extra functionality. + +## 1.2.0 + +- Added a `FutureGroup` class for waiting for a group of futures, potentially of + unknown size, to complete. + +- Added a `StreamGroup` class for merging the events of a group of streams, + potentially of unknown size. + +- Added a `StreamSplitter` class for splitting a stream into multiple new + streams. + +## 1.1.1 + +- Updated SDK version constraint to at least 1.9.0. + +## 1.1.0 + +- ChangeLog starts here. diff --git a/pkgs/async/LICENSE b/pkgs/async/LICENSE new file mode 100644 index 00000000..dbd2843a --- /dev/null +++ b/pkgs/async/LICENSE @@ -0,0 +1,27 @@ +Copyright 2015, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkgs/async/README.md b/pkgs/async/README.md new file mode 100644 index 00000000..cb074d03 --- /dev/null +++ b/pkgs/async/README.md @@ -0,0 +1,99 @@ +[![Dart CI](https://github.com/dart-lang/core/actions/workflows/async.yaml/badge.svg)](https://github.com/dart-lang/core/actions/workflows/async.yaml) +[![pub package](https://img.shields.io/pub/v/async.svg)](https://pub.dev/packages/async) +[![package publisher](https://img.shields.io/pub/publisher/async.svg)](https://pub.dev/packages/async/publisher) + +Contains utility classes in the style of `dart:async` to work with asynchronous +computations. + +## Package API + +* The [`AsyncCache`][AsyncCache] class allows expensive asynchronous + computations values to be cached for a period of time. + +* The [`AsyncMemoizer`][AsyncMemoizer] class makes it easy to only run an + asynchronous operation once on demand. + +* The [`CancelableOperation`][CancelableOperation] class defines an operation + that can be canceled by its consumer. The producer can then listen for this + cancellation and stop producing the future when it's received. It can be + created using a [`CancelableCompleter`][CancelableCompleter]. + +* The delegating wrapper classes allow users to easily add functionality on top + of existing instances of core types from `dart:async`. These include + [`DelegatingFuture`][DelegatingFuture], + [`DelegatingStream`][DelegatingStream], + [`DelegatingStreamSubscription`][DelegatingStreamSubscription], + [`DelegatingStreamConsumer`][DelegatingStreamConsumer], + [`DelegatingSink`][DelegatingSink], + [`DelegatingEventSink`][DelegatingEventSink], and + [`DelegatingStreamSink`][DelegatingStreamSink]. + +* The [`FutureGroup`][FutureGroup] class makes it easy to wait until a group of + futures that may change over time completes. + +* The [`LazyStream`][LazyStream] class allows a stream to be initialized lazily + when `.listen()` is first called. + +* The [`NullStreamSink`][NullStreamSink] class is an implementation of + `StreamSink` that discards all events. + +* The [`RestartableTimer`][RestartableTimer] class extends `Timer` with a + `reset()` method. + +* The [`Result`][Result] class that can hold either a value or an error. It + provides various utilities for converting to and from `Future`s and `Stream`s. + +* The [`StreamGroup`][StreamGroup] class merges a collection of streams into a + single output stream. + +* The [`StreamQueue`][StreamQueue] class allows a stream to be consumed + event-by-event rather than being pushed whichever events as soon as they + arrive. + +* The [`StreamSplitter`][StreamSplitter] class allows a stream to be duplicated + into multiple identical streams. + +* The [`StreamZip`][StreamZip] class combines multiple streams into a single + stream of lists of events. + +* This package contains a number of [`StreamTransformer`][StreamTransformer]s. + [`SingleSubscriptionTransformer`][SingleSubscriptionTransformer] converts a + broadcast stream to a single-subscription stream, and + [`typedStreamTransformer`][typedStreamTransformer] casts the type of a + `Stream`. It also defines a transformer type for [`StreamSink`][StreamSink]s, + [`StreamSinkTransformer`][StreamSinkTransformer]. + +* The [`SubscriptionStream`][SubscriptionStream] class wraps a + `StreamSubscription` so it can be re-used as a `Stream`. + +[AsyncCache]: https://pub.dev/documentation/async/latest/async/AsyncCache-class.html +[AsyncMemoizer]: https://pub.dev/documentation/async/latest/async/AsyncMemoizer-class.html +[CancelableCompleter]: https://pub.dev/documentation/async/latest/async/CancelableCompleter-class.html +[CancelableOperation]: https://pub.dev/documentation/async/latest/async/CancelableOperation-class.html +[DelegatingEventSink]: https://pub.dev/documentation/async/latest/async/DelegatingEventSink-class.html +[DelegatingFuture]: https://pub.dev/documentation/async/latest/async/DelegatingFuture-class.html +[DelegatingSink]: https://pub.dev/documentation/async/latest/async/DelegatingSink-class.html +[DelegatingStreamConsumer]: https://pub.dev/documentation/async/latest/async/DelegatingStreamConsumer-class.html +[DelegatingStreamSink]: https://pub.dev/documentation/async/latest/async/DelegatingStreamSink-class.html +[DelegatingStreamSubscription]: https://pub.dev/documentation/async/latest/async/DelegatingStreamSubscription-class.html +[DelegatingStream]: https://pub.dev/documentation/async/latest/async/DelegatingStream-class.html +[FutureGroup]: https://pub.dev/documentation/async/latest/async/FutureGroup-class.html +[LazyStream]: https://pub.dev/documentation/async/latest/async/LazyStream-class.html +[NullStreamSink]: https://pub.dev/documentation/async/latest/async/NullStreamSink-class.html +[RestartableTimer]: https://pub.dev/documentation/async/latest/async/RestartableTimer-class.html +[Result]: https://pub.dev/documentation/async/latest/async/Result-class.html +[SingleSubscriptionTransformer]: https://pub.dev/documentation/async/latest/async/SingleSubscriptionTransformer-class.html +[StreamGroup]: https://pub.dev/documentation/async/latest/async/StreamGroup-class.html +[StreamQueue]: https://pub.dev/documentation/async/latest/async/StreamQueue-class.html +[StreamSinkTransformer]: https://pub.dev/documentation/async/latest/async/StreamSinkTransformer-class.html +[StreamSink]: https://api.dart.dev/stable/dart-async/StreamSink-class.html +[StreamSplitter]: https://pub.dev/documentation/async/latest/async/StreamSplitter-class.html +[StreamTransformer]: https://api.dart.dev/stable/dart-async/StreamTransformer-class.html +[StreamZip]: https://pub.dev/documentation/async/latest/async/StreamZip-class.html +[SubscriptionStream]: https://pub.dev/documentation/async/latest/async/SubscriptionStream-class.html +[typedStreamTransformer]: https://pub.dev/documentation/async/latest/async/typedStreamTransformer.html + +## Publishing automation + +For information about our publishing automation and release process, see +https://github.com/dart-lang/ecosystem/wiki/Publishing-automation. diff --git a/pkgs/async/analysis_options.yaml b/pkgs/async/analysis_options.yaml new file mode 100644 index 00000000..265204bd --- /dev/null +++ b/pkgs/async/analysis_options.yaml @@ -0,0 +1,21 @@ +# https://dart.dev/tools/analysis#the-analysis-options-file +include: package:dart_flutter_team_lints/analysis_options.yaml + +analyzer: + language: + strict-casts: true + errors: + only_throw_errors: ignore + unawaited_futures: ignore + inference_failure_on_instance_creation: ignore + inference_failure_on_function_invocation: ignore + inference_failure_on_collection_literal: ignore + +linter: + rules: + - avoid_unused_constructor_parameters + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_runtimeType_toString + - package_api_docs diff --git a/pkgs/async/lib/async.dart b/pkgs/async/lib/async.dart new file mode 100644 index 00000000..67480e62 --- /dev/null +++ b/pkgs/async/lib/async.dart @@ -0,0 +1,45 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Utilities that expand on the asynchronous features of the `dart:async` +/// library. +/// +/// {@youtube 560 315 https://www.youtube.com/watch?v=r0tHiCjW2w0} +library; + +export 'src/async_cache.dart'; +export 'src/async_memoizer.dart'; +export 'src/byte_collector.dart'; +export 'src/cancelable_operation.dart'; +export 'src/chunked_stream_reader.dart'; +export 'src/delegate/event_sink.dart'; +export 'src/delegate/future.dart'; +export 'src/delegate/sink.dart'; +export 'src/delegate/stream.dart'; +export 'src/delegate/stream_consumer.dart'; +export 'src/delegate/stream_sink.dart'; +export 'src/delegate/stream_subscription.dart'; +export 'src/future_group.dart'; +export 'src/lazy_stream.dart'; +export 'src/null_stream_sink.dart'; +export 'src/restartable_timer.dart'; +export 'src/result/error.dart'; +export 'src/result/future.dart'; +export 'src/result/result.dart'; +export 'src/result/value.dart'; +export 'src/single_subscription_transformer.dart'; +export 'src/sink_base.dart'; +export 'src/stream_closer.dart'; +export 'src/stream_completer.dart'; +export 'src/stream_extensions.dart'; +export 'src/stream_group.dart'; +export 'src/stream_queue.dart'; +export 'src/stream_sink_completer.dart'; +export 'src/stream_sink_extensions.dart'; +export 'src/stream_sink_transformer.dart'; +export 'src/stream_splitter.dart'; +export 'src/stream_subscription_transformer.dart'; +export 'src/stream_zip.dart'; +export 'src/subscription_stream.dart'; +export 'src/typed_stream_transformer.dart'; diff --git a/pkgs/async/lib/src/async_cache.dart b/pkgs/async/lib/src/async_cache.dart new file mode 100644 index 00000000..6fc7cb0e --- /dev/null +++ b/pkgs/async/lib/src/async_cache.dart @@ -0,0 +1,114 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import '../async.dart'; + +/// Runs asynchronous functions and caches the result for a period of time. +/// +/// This class exists to cover the pattern of having potentially expensive code +/// such as file I/O, network access, or isolate computation that's unlikely to +/// change quickly run fewer times. For example: +/// +/// ```dart +/// final _usersCache = new AsyncCache>(const Duration(hours: 1)); +/// +/// /// Uses the cache if it exists, otherwise calls the closure: +/// Future> get onlineUsers => _usersCache.fetch(() { +/// // Actually fetch online users here. +/// }); +/// ``` +/// +/// This class's timing can be mocked using +/// [`fake_async`](https://pub.dev/packages/fake_async). +class AsyncCache { + /// How long cached values stay fresh. + /// + /// Set to `null` for ephemeral caches, which only stay alive until the + /// future completes. + final Duration? _duration; + + /// Cached results of a previous `fetchStream` call. + StreamSplitter? _cachedStreamSplitter; + + /// Cached results of a previous [fetch] call. + Future? _cachedValueFuture; + + /// Fires when the cache should be considered stale. + Timer? _stale; + + /// Creates a cache that invalidates its contents after [duration] has passed. + /// + /// The [duration] starts counting after the Future returned by [fetch] + /// completes, or after the Stream returned by `fetchStream` emits a done + /// event. + AsyncCache(Duration duration) : _duration = duration; + + /// Creates a cache that invalidates after an in-flight request is complete. + /// + /// An ephemeral cache guarantees that a callback function will only be + /// executed at most once concurrently. This is useful for requests for which + /// data is updated frequently but stale data is acceptable. + AsyncCache.ephemeral() : _duration = null; + + /// Returns a cached value from a previous call to [fetch], or runs [callback] + /// to compute a new one. + /// + /// If [fetch] has been called recently enough, returns its previous return + /// value. Otherwise, runs [callback] and returns its new return value. + Future fetch(Future Function() callback) async { + if (_cachedStreamSplitter != null) { + throw StateError('Previously used to cache via `fetchStream`'); + } + return _cachedValueFuture ??= callback() + ..whenComplete(_startStaleTimer).ignore(); + } + + /// Returns a cached stream from a previous call to [fetchStream], or runs + /// [callback] to compute a new stream. + /// + /// If [fetchStream] has been called recently enough, returns a copy of its + /// previous return value. Otherwise, runs [callback] and returns its new + /// return value. + /// + /// Each call to this function returns a stream which replays the same events, + /// which means that all stream events are cached until this cache is + /// invalidated. + /// + /// Only starts counting time after the stream has been listened to, + /// and it has completed with a `done` event. + @Deprecated('Feature will be removed') + Stream fetchStream(Stream Function() callback) { + if (_cachedValueFuture != null) { + throw StateError('Previously used to cache via `fetch`'); + } + var splitter = _cachedStreamSplitter ??= StreamSplitter( + callback().transform(StreamTransformer.fromHandlers(handleDone: (sink) { + _startStaleTimer(); + sink.close(); + }))); + return splitter.split(); + } + + /// Removes any cached value. + void invalidate() { + // TODO: This does not return a future, but probably should. + _cachedValueFuture = null; + // TODO: This does not await, but probably should. + _cachedStreamSplitter?.close(); + _cachedStreamSplitter = null; + _stale?.cancel(); + _stale = null; + } + + void _startStaleTimer() { + var duration = _duration; + if (duration != null) { + _stale = Timer(duration, invalidate); + } else { + invalidate(); + } + } +} diff --git a/pkgs/async/lib/src/async_memoizer.dart b/pkgs/async/lib/src/async_memoizer.dart new file mode 100644 index 00000000..c05c9275 --- /dev/null +++ b/pkgs/async/lib/src/async_memoizer.dart @@ -0,0 +1,46 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// A class for running an asynchronous function exactly once and caching its +/// result. +/// +/// An `AsyncMemoizer` is used when some function may be run multiple times in +/// order to get its result, but it only actually needs to be run once for its +/// effect. To memoize the result of an async function, you can create a +/// memoizer outside the function (for example as an instance field if you want +/// to memoize the result of a method), and then wrap the function's body in a +/// call to [runOnce]. +/// +/// This is useful for methods like `close()` and getters that need to do +/// asynchronous work. For example: +/// +/// ```dart +/// class SomeResource { +/// final _closeMemo = AsyncMemoizer(); +/// +/// Future close() => _closeMemo.runOnce(() { +/// // ... +/// }); +/// } +/// ``` +class AsyncMemoizer { + /// The future containing the method's result. + /// + /// This can be accessed at any time, and will fire once [runOnce] is called. + Future get future => _completer.future; + final _completer = Completer(); + + /// Whether [runOnce] has been called yet. + bool get hasRun => _completer.isCompleted; + + /// Runs the function, [computation], if it hasn't been run before. + /// + /// If [runOnce] has already been called, this returns the original result. + Future runOnce(FutureOr Function() computation) { + if (!hasRun) _completer.complete(Future.sync(computation)); + return future; + } +} diff --git a/pkgs/async/lib/src/byte_collector.dart b/pkgs/async/lib/src/byte_collector.dart new file mode 100644 index 00000000..0c937616 --- /dev/null +++ b/pkgs/async/lib/src/byte_collector.dart @@ -0,0 +1,52 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:typed_data'; +import 'cancelable_operation.dart'; + +/// Collects an asynchronous sequence of byte lists into a single list of bytes. +/// +/// If the [source] stream emits an error event, +/// the collection fails and the returned future completes with the same error. +/// +/// If any of the input data are not valid bytes, they will be truncated to +/// an eight-bit unsigned value in the resulting list. +Future collectBytes(Stream> source) { + return _collectBytes(source, (_, result) => result); +} + +/// Collects an asynchronous sequence of byte lists into a single list of bytes. +/// +/// Returns a [CancelableOperation] that provides the result future and a way +/// to cancel the collection early. +/// +/// If the [source] stream emits an error event, +/// the collection fails and the returned future completes with the same error. +/// +/// If any of the input data are not valid bytes, they will be truncated to +/// an eight-bit unsigned value in the resulting list. +CancelableOperation collectBytesCancelable( + Stream> source) { + return _collectBytes( + source, + (subscription, result) => CancelableOperation.fromFuture(result, + onCancel: subscription.cancel)); +} + +/// Generalization over [collectBytes] and [collectBytesCancelable]. +/// +/// Performs all the same operations, but the final result is created +/// by the [result] function, which has access to the stream subscription +/// so it can cancel the operation. +T _collectBytes(Stream> source, + T Function(StreamSubscription>, Future) result) { + var bytes = BytesBuilder(copy: false); + var completer = Completer.sync(); + var subscription = + source.listen(bytes.add, onError: completer.completeError, onDone: () { + completer.complete(bytes.takeBytes()); + }, cancelOnError: true); + return result(subscription, completer.future); +} diff --git a/pkgs/async/lib/src/cancelable_operation.dart b/pkgs/async/lib/src/cancelable_operation.dart new file mode 100644 index 00000000..2610613c --- /dev/null +++ b/pkgs/async/lib/src/cancelable_operation.dart @@ -0,0 +1,563 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// An asynchronous operation that can be cancelled. +/// +/// The value of this operation is exposed as [value]. When this operation is +/// cancelled, [value] won't complete either successfully or with an error. If +/// [value] has already completed, cancelling the operation does nothing. +class CancelableOperation { + /// The completer that produced this operation. + /// + /// That completer is canceled when [cancel] is called. + CancelableCompleter _completer; + + CancelableOperation._(this._completer); + + /// Creates a [CancelableOperation] with the same result as the [result] + /// future. + /// + /// When this operation is canceled, [onCancel] will be called and any value + /// or error later produced by [result] will be discarded. + /// If [onCancel] returns a [Future], it will be returned by [cancel]. + /// + /// The [onCancel] function will be called synchronously + /// when the new operation is canceled, and will be called at most once. + /// + /// Calling this constructor is equivalent to creating a + /// [CancelableCompleter] and completing it with [result]. + factory CancelableOperation.fromFuture(Future result, + {FutureOr Function()? onCancel}) => + (CancelableCompleter(onCancel: onCancel)..complete(result)).operation; + + /// Creates a [CancelableOperation] which completes to [value]. + /// + /// Canceling this operation does nothing. + /// + /// Calling this constructor is equivalent to creating a + /// [CancelableCompleter] and completing it with [value]. + factory CancelableOperation.fromValue(T value) => + (CancelableCompleter()..complete(value)).operation; + + /// Creates a [CancelableOperation] wrapping [subscription]. + /// + /// This overrides [StreamSubscription.onDone] and + /// [StreamSubscription.onError] so that the returned operation will complete + /// when the subscription completes or emits an error. + /// When this operation is canceled or when it emits an error, the + /// subscription will be canceled (unlike + /// `CancelableOperation.fromFuture(subscription.asFuture())`). + static CancelableOperation fromSubscription( + StreamSubscription subscription) { + var completer = CancelableCompleter(onCancel: subscription.cancel); + subscription.onDone(completer.complete); + subscription.onError((Object error, StackTrace stackTrace) { + subscription.cancel().whenComplete(() { + completer.completeError(error, stackTrace); + }); + }); + return completer.operation; + } + + /// Creates a [CancelableOperation] that completes with the value of the first + /// of [operations] to complete. + /// + /// Once any of [operations] completes, its result is forwarded to the + /// new [CancelableOperation] and the rest are cancelled. If the + /// new operation is cancelled, all the [operations] are cancelled as + /// well. + static CancelableOperation race( + Iterable> operations) { + operations = operations.toList(); + if (operations.isEmpty) { + throw ArgumentError('May not be empty', 'operations'); + } + + var done = false; + // Note: if one or more of the completers have already completed, + // they're not actually cancelled by this. + Future cancelAll() { + done = true; + return Future.wait([ + for (var operation in operations) + if (!operation.isCanceled) operation.cancel() + ]); + } + + var completer = CancelableCompleter(onCancel: cancelAll); + for (var operation in operations) { + operation.then((value) { + if (!done) cancelAll().whenComplete(() => completer.complete(value)); + }, onError: (error, stackTrace) { + if (!done) { + cancelAll() + .whenComplete(() => completer.completeError(error, stackTrace)); + } + }, propagateCancel: false); + } + + return completer.operation; + } + + /// The result of this operation, if not cancelled. + /// + /// This future will not complete if the operation is cancelled. + /// Use [valueOrCancellation] for a future which completes + /// both if the operation is cancelled and if it isn't. + Future get value => _completer._inner?.future ?? Completer().future; + + /// Creates a [Stream] containing the result of this operation. + /// + /// This is like `value.asStream()`, but if a subscription to the stream is + /// canceled, this operation is as well. + Stream asStream() { + var controller = + StreamController(sync: true, onCancel: _completer._cancel); + + _completer._inner?.future.then((value) { + controller.add(value); + controller.close(); + }, onError: (Object error, StackTrace stackTrace) { + controller.addError(error, stackTrace); + controller.close(); + }); + return controller.stream; + } + + /// Creates a [Future] that completes when this operation completes *or* when + /// it's cancelled. + /// + /// If this operation completes, this completes to the same result as [value]. + /// If this operation is cancelled, the returned future waits for the future + /// returned by [cancel], then completes to [cancellationValue]. + Future valueOrCancellation([T? cancellationValue]) { + var completer = Completer.sync(); + value.then(completer.complete, onError: completer.completeError); + + _completer._cancelCompleter?.future.then((_) { + completer.complete(cancellationValue); + }, onError: completer.completeError); + + return completer.future; + } + + /// Creates a new cancelable operation to be completed when this operation + /// completes normally or as an error, or is cancelled. + /// + /// If this operation completes normally the value is passed to [onValue] + /// and the returned operation is completed with the result. + /// + /// If this operation completes as an error, and no [onError] callback is + /// provided, the returned operation is completed with the same error and + /// stack trace. + /// If this operation completes as an error, and an [onError] callback is + /// provided, the returned operation is completed with the result. + /// + /// If this operation is canceled, and no [onCancel] callback is provided, + /// the returned operation is canceled. + /// If this operation is canceled, and an [onCancel] callback is provided, + /// the returned operation is completed with the result. + /// + /// At most one of [onValue], [onError], or [onCancel] will be called. + /// If any of [onValue], [onError], or [onCancel] throw a synchronous error, + /// or return a `Future` that completes as an error, the error will be + /// forwarded through the returned operation. + /// + /// If the returned operation is canceled before this operation completes or + /// is canceled, the [onValue], [onError], and [onCancel] callbacks will not + /// be invoked. If [propagateCancel] is `true` (the default) then this + /// operation is canceled as well. Pass `false` if there are multiple + /// listeners on this operation and canceling the [onValue], [onError], and + /// [onCancel] callbacks should not cancel the other listeners. + CancelableOperation then(FutureOr Function(T) onValue, + {FutureOr Function(Object, StackTrace)? onError, + FutureOr Function()? onCancel, + bool propagateCancel = true}) => + thenOperation((value, completer) { + completer.complete(onValue(value)); + }, + onError: onError == null + ? null + : (error, stackTrace, completer) { + completer.complete(onError(error, stackTrace)); + }, + onCancel: onCancel == null + ? null + : (completer) { + completer.complete(onCancel()); + }, + propagateCancel: propagateCancel); + + /// Creates a new cancelable operation to be completed when this operation + /// completes normally or as an error, or is cancelled. + /// + /// If this operation completes normally the value is passed to [onValue] + /// with a [CancelableCompleter] controlling the returned operation. + /// + /// If this operation completes as an error, and no [onError] callback is + /// provided, the returned operation is completed with the same error and + /// stack trace. + /// If this operation completes as an error, and an [onError] callback is + /// provided, the error and stack trace are passed to [onError] with a + /// [CancelableCompleter] controlling the returned operation. + /// + /// If this operation is canceled, and no [onCancel] callback is provided, + /// the returned operation is canceled. + /// If this operation is canceled, and an [onCancel] callback is provided, + /// the [onCancel] callback is called with a [CancelableCompleter] controlling + /// the returned operation. + /// + /// At most one of [onValue], [onError], or [onCancel] will be called. + /// If any of [onValue], [onError], or [onCancel] throw a synchronous error, + /// or return a `Future` that completes as an error, the error will be + /// forwarded through the returned operation. + /// + /// If the returned operation is canceled before this operation completes or + /// is canceled, the [onValue], [onError], and [onCancel] callbacks will not + /// be invoked. If [propagateCancel] is `true` (the default) then this + /// operation is canceled as well. Pass `false` if there are multiple + /// listeners on this operation and canceling the [onValue], [onError], and + /// [onCancel] callbacks should not cancel the other listeners. + CancelableOperation thenOperation( + FutureOr Function(T, CancelableCompleter) onValue, + {FutureOr Function(Object, StackTrace, CancelableCompleter)? + onError, + FutureOr Function(CancelableCompleter)? onCancel, + bool propagateCancel = true}) { + final completer = CancelableCompleter( + onCancel: propagateCancel ? _cancelIfNotCanceled : null); + + // if `_completer._inner` completes before `completer` is cancelled + // call `onValue` or `onError` with the result, and complete `completer` + // with the result of that call (unless cancelled in the meantime). + // + // If `_completer._cancelCompleter` completes (always with a value) + // before `completer` is cancelled, then call `onCancel` (if supplied) + // with that that value and complete `completer` with the result of that + // call (unless cancelled in the meantime). + // + // If any of the callbacks throw synchronously, the `completer` is + // completed with that error. + // + // If no `onCancel` is provided, and `_completer._cancelCompleter` + // completes before `completer` is cancelled, + // then cancel `cancelCompleter`. (Cancelling twice is safe.) + + _completer._inner?.future.then((value) async { + if (completer.isCanceled) return; + try { + await onValue(value, completer); + } catch (error, stack) { + completer.completeError(error, stack); + } + }, + onError: onError == null + ? completer.completeError // Is ignored if already cancelled. + : (Object error, StackTrace stack) async { + if (completer.isCanceled) return; + try { + await onError(error, stack, completer); + } catch (error2, stack2) { + completer.completeErrorIfPending( + error2, identical(error, error2) ? stack : stack2); + } + }); + final cancelForwarder = _CancelForwarder(completer, onCancel); + if (_completer.isCanceled) { + cancelForwarder._forward(); + } else { + (_completer._cancelForwarders ??= []).add(cancelForwarder); + } + return completer.operation; + } + + /// Cancels this operation. + /// + /// If this operation [isCompleted] or [isCanceled] this call is ignored. + /// Returns the result of the `onCancel` callback, if one exists. + Future cancel() => _completer._cancel(); + + Future? _cancelIfNotCanceled() => isCanceled ? null : cancel(); + + /// Whether this operation has been canceled before it completed. + bool get isCanceled => _completer._isCanceled; + + /// Whether the result of this operation is ready. + /// + /// When ready, the [value] future is completed with the result value + /// or error, and this operation can no longer be cancelled. + /// An operation may be complete before the listeners on [value] are invoked. + bool get isCompleted => _completer._isCompleted; +} + +/// A completer for a [CancelableOperation]. +class CancelableCompleter { + // The cancelable completer is in one of the following states: + // * Initial: + // _inner != null + // _cancelCompleter != null + // _mayComplete: true + // + // * Async-completed: `complete` called with a future while Initial. + // _inner != null + // _cancelCompleter != null + // _mayComplete: false + // + // * Completed: `complete` called with a value or `completeError` called + // while Initial, or the future passed in Async-completed completes + // while AsyncCompleted. + // _inner != null + // _cancelCompleter == null + // _mayComplete: false + // + // * Cancelled may-complete: `_cancel` called while Initial. + // Allows calling `complete`/`completeError` even if it does nothing. + // _inner == null + // _cancelCompleter != null + // _mayComplete: true + // + // * Cancelled can't-complete: `_cancel` called while Async-completed. + // _inner == null + // _cancelCompleter != null + // _mayComplete: false + + /// The completer for the wrapped future. + /// + /// At most one of `_inner.future` and `_cancelCompleter.future` will + /// ever complete. + /// Set to `null` when when the operation is canceled, because then + /// it's guaranteed that this completer will never complete. + Completer? _inner = Completer(); + + /// Completed when `cancel` is called. + /// + /// At most one of `_inner.future` and `_cancelCompleter.future` will + /// ever complete. + /// Set to `null` when [_inner] is completed, because then it's + /// guaranteed that this completer will never complete. + Completer? _cancelCompleter = Completer(); + + /// The callback to call if the operation is canceled. + final FutureOr Function()? _onCancel; + + /// Additional cancellations to forward during cancel. + /// + /// When a cancelable operation is chained through `then` or `thenOperation` a + /// cancellation on the original operation will synchronously cancel the + /// chained operations. + List<_CancelForwarder>? _cancelForwarders; + + /// Whether [complete] or [completeError] may still be called. + /// + /// Set to false when calling either. + /// + /// When completing by calling [complete] with a future, + /// it's still possible to cancel until the result is actually + /// available. + /// You are also allowed to call [complete] or [completeError] + /// after the operation has been canceled, as long as you only call it once. + /// It just won't do anything after the operation is cancelled. + /// This value only guards the calls to [complete] and [completeError]. + bool _mayComplete = true; + + /// The operation controlled by this completer. + late final operation = CancelableOperation._(this); + + /// Creates a new completer for a [CancelableOperation]. + /// + /// The cancelable [operation] can be completed using + /// [complete] or [completeError]. + /// + /// The [onCancel] function is called if the [operation] is canceled, + /// by calling [CancelableOperation.cancel] + /// before the operation has completed. + /// If [onCancel] returns a [Future], + /// that future is also returned by [CancelableOperation.cancel]. + /// + /// The [onCancel] function will be called at most once. + CancelableCompleter({FutureOr Function()? onCancel}) : _onCancel = onCancel; + + /// Whether the [_inner] completer has been completed. + /// + /// At this point it's no longer possible to cancel the operation. + bool get _isCompleted => _cancelCompleter == null; + + /// Whether the completer was canceled before the result was ready. + /// + /// At this point, it's no longer possible to complete the operation. + bool get _isCanceled => _inner == null; + + /// Whether the [complete] or [completeError] have been called. + /// + /// Once this completer has been completed with either a result or error, + /// neither method may be called again. + /// + /// If [complete] was called with a [Future] argument, this completer may be + /// completed before it's [operation] is completed. In that case the + /// [operation] may still be canceled before the result is available. + bool get isCompleted => !_mayComplete; + + /// Whether the completer was canceled before the result was ready. + bool get isCanceled => _isCanceled; + + /// Completes [operation] with [value]. + /// + /// If [value] is a [Future] the [operation] will complete + /// with the result of that `Future` once it is available. + /// In that case [isCompleted] will be `true` before the [operation] + /// is complete. + /// + /// If the type [T] is not nullable [value] may be not be omitted or `null`. + /// + /// This method may not be called after either [complete] or [completeError] + /// has been called once. + /// The [isCompleted] is true when either of these methods have been called. + void complete([FutureOr? value]) { + if (!_mayComplete) throw StateError('Operation already completed'); + _mayComplete = false; + + if (value is! Future) { + _completeNow()?.complete(value); + return; + } + + if (_inner == null) { + // Make sure errors from [value] aren't top-leveled. + value.ignore(); + return; + } + + value.then((result) { + _completeNow()?.complete(result); + }, onError: (Object error, StackTrace stackTrace) { + _completeNow()?.completeError(error, stackTrace); + }); + } + + /// Makes this [CancelableCompleter.operation] complete with the same result + /// as [result]. + /// + /// If [propagateCancel] is `true` (the default), and the [operation] of this + /// completer is canceled before [result] completes, then [result] is also + /// canceled. + void completeOperation(CancelableOperation result, + {bool propagateCancel = true}) { + if (!_mayComplete) throw StateError('Already completed'); + _mayComplete = false; + if (isCanceled) { + if (propagateCancel) result.cancel(); + result.value.ignore(); + return; + } + result.then((value) { + _inner?.complete( + value); // _inner is set to null if this.operation is cancelled. + }, onError: (error, stack) { + _inner?.completeError(error, stack); + }, onCancel: () { + operation.cancel(); + }); + if (propagateCancel) { + _cancelCompleter?.future.whenComplete(result.cancel); + } + } + + /// Completer to use for completing with a result. + /// + /// Returns `null` if it's not possible to complete any more. + /// Sets [_cancelCompleter] to `null` if returning non-`null`. + Completer? _completeNow() { + var inner = _inner; + if (inner == null) return null; + _cancelCompleter = null; + return inner; + } + + /// Completes [operation] with [error] and [stackTrace]. + /// + /// This method may not be called after either [complete] or [completeError] + /// has been called once. + /// The [isCompleted] is true when either of these methods have been called. + void completeError(Object error, [StackTrace? stackTrace]) { + if (!_mayComplete) throw StateError('Operation already completed'); + _mayComplete = false; + _completeNow()?.completeError(error, stackTrace); + } + + /// Cancels the operation. + /// + /// If the operation has already completed, prior to being cancelled, + /// this method does nothing. + /// If the operation has already been cancelled, this method returns + /// the same result as the first call to `_cancel`. + /// + /// The result of the operation may only be available some time after + /// the completer has been completed (using [complete] or [completeError], + /// which sets [isCompleted] to true) if completed with a [Future]. + /// The completer can be cancelled until the result becomes available, + /// even if [isCompleted] is true. + Future _cancel() { + var cancelCompleter = _cancelCompleter; + if (cancelCompleter == null) return Future.value(null); + + if (_inner != null) { + _inner = null; + cancelCompleter.complete(_invokeCancelCallbacks()); + } + return cancelCompleter.future; + } + + /// Invoke [_onCancel] and forward to other completers in [_cancelForwarders]. + /// + /// Returns the same value as [_onCancel]. Legacy uses may return a value + /// despite the signature having `void` return. + Future _invokeCancelCallbacks() async { + final FutureOr toReturn = _onCancel?.call(); + final isFuture = toReturn is Future; + final cancelFutures = >[ + if (isFuture) toReturn, + ...?_cancelForwarders?.map(_forward).nonNulls + ]; + final results = (isFuture && cancelFutures.length == 1) + ? [await toReturn] + : cancelFutures.isNotEmpty + ? await Future.wait(cancelFutures) + : const []; + return isFuture ? results.first : toReturn; + } +} + +class _CancelForwarder { + final CancelableCompleter completer; + final FutureOr Function(CancelableCompleter)? onCancel; + _CancelForwarder(this.completer, this.onCancel); + + Future? _forward() { + if (completer.isCanceled) return null; + final onCancel = this.onCancel; + if (onCancel == null) return completer._cancel(); + try { + final result = onCancel(completer); + if (result is Future) { + return result.catchError(completer.completeErrorIfPending); + } + } catch (error, stack) { + completer.completeErrorIfPending(error, stack); + } + return null; + } +} + +// Helper function to avoid a closure for `List<_CancelForwarder>.map`. +Future? _forward(_CancelForwarder forwarder) => + forwarder._forward(); + +extension on CancelableCompleter { + void completeErrorIfPending(Object error, StackTrace stackTrace) { + if (isCompleted) return; + completeError(error, stackTrace); + } +} diff --git a/pkgs/async/lib/src/chunked_stream_reader.dart b/pkgs/async/lib/src/chunked_stream_reader.dart new file mode 100644 index 00000000..1d92216a --- /dev/null +++ b/pkgs/async/lib/src/chunked_stream_reader.dart @@ -0,0 +1,216 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:typed_data'; + +import 'byte_collector.dart' show collectBytes; + +/// Utility class for reading elements from a _chunked stream_. +/// +/// A _chunked stream_ is a stream where each event is a chunk of elements. +/// Byte-streams with the type `Stream>` is common of example of this. +/// As illustrated in the example below, this utility class makes it easy to +/// read a _chunked stream_ using custom chunk sizes and sub-stream sizes, +/// without managing partially read chunks. +/// +/// ```dart +/// final r = ChunkedStreamReader(File('myfile.txt').openRead()); +/// try { +/// // Read the first 4 bytes +/// final firstBytes = await r.readChunk(4); +/// if (firstBytes.length < 4) { +/// throw Exception('myfile.txt has less than 4 bytes'); +/// } +/// +/// // Read next 8 kilobytes as a substream +/// Stream> substream = r.readStream(8 * 1024); +/// +/// ... +/// } finally { +/// // We always cancel the ChunkedStreamReader, this ensures the underlying +/// // stream is cancelled. +/// r.cancel(); +/// } +/// ``` +/// +/// The read-operations [readChunk] and [readStream] must not be invoked until +/// the future from a previous call has completed. +class ChunkedStreamReader { + /// Iterator over underlying stream. + /// + /// The reader requests data from this input whenever requests on the + /// reader cannot be fulfilled with the already fetched data. + final StreamIterator> _input; + + /// Sentinel value used for [_buffer] when we have no value. + final List _emptyList = const []; + + /// Last partially consumed chunk received from [_input]. + /// + /// Elements up to [_offset] have already been consumed and should not be + /// consumed again. + List _buffer = []; + + /// Offset into [_buffer] after data which have already been emitted. + /// + /// The offset is between `0` and `_buffer.length`, both inclusive. + /// The data in [_buffer] from [_offset] and forward have not yet been + /// emitted by the chunked stream reader, the data before [_offset] has. + int _offset = 0; + + /// Whether a read request is currently being processed. + /// + /// Is `true` while a request is in progress. + /// While a read request, like [readChunk] or [readStream], is being + /// processed, no new requests can be made. + /// New read attempts will throw instead. + bool _reading = false; + + factory ChunkedStreamReader(Stream> stream) => + ChunkedStreamReader._(StreamIterator(stream)); + + ChunkedStreamReader._(this._input); + + /// Read next [size] elements from _chunked stream_, buffering to create a + /// chunk with [size] elements. + /// + /// This will read _chunks_ from the underlying _chunked stream_ until [size] + /// elements have been buffered, or end-of-stream, then it returns the first + /// [size] buffered elements. + /// + /// If end-of-stream is encountered before [size] elements is read, this + /// returns a list with fewer than [size] elements (indicating end-of-stream). + /// + /// If the underlying stream throws, the stream is cancelled, the exception is + /// propogated and further read operations will fail. + /// + /// Throws, if another read operation is on-going. + Future> readChunk(int size) async { + final result = []; + await for (final chunk in readStream(size)) { + result.addAll(chunk); + } + return result; + } + + /// Read next [size] elements from _chunked stream_ as a sub-stream. + /// + /// This will pass-through _chunks_ from the underlying _chunked stream_ until + /// [size] elements have been returned, or end-of-stream has been encountered. + /// + /// If end-of-stream is encountered before [size] elements is read, this + /// returns a list with fewer than [size] elements (indicating end-of-stream). + /// + /// If the underlying stream throws, the stream is cancelled, the exception is + /// propogated and further read operations will fail. + /// + /// If the sub-stream returned from [readStream] is cancelled the remaining + /// unread elements up-to [size] are drained, allowing subsequent + /// read-operations to proceed after cancellation. + /// + /// Throws, if another read-operation is on-going. + Stream> readStream(int size) { + RangeError.checkNotNegative(size, 'size'); + if (_reading) { + throw StateError('Concurrent read operations are not allowed!'); + } + _reading = true; + + Stream> substream() async* { + // While we have data to read + while (size > 0) { + // Read something into the buffer, if buffer has been consumed. + assert(_offset <= _buffer.length); + if (_offset == _buffer.length) { + if (!(await _input.moveNext())) { + // Don't attempt to read more data, as there is no more data. + size = 0; + _reading = false; + break; + } + _buffer = _input.current; + _offset = 0; + } + + final remainingBuffer = _buffer.length - _offset; + if (remainingBuffer > 0) { + if (remainingBuffer >= size) { + List output; + if (_buffer is Uint8List) { + output = Uint8List.sublistView( + _buffer as Uint8List, _offset, _offset + size) as List; + } else { + output = _buffer.sublist(_offset, _offset + size); + } + _offset += size; + size = 0; + yield output; + _reading = false; + break; + } + + final output = _offset == 0 ? _buffer : _buffer.sublist(_offset); + size -= remainingBuffer; + _buffer = _emptyList; + _offset = 0; + yield output; + } + } + } + + final c = StreamController>(); + c.onListen = () => c.addStream(substream()).whenComplete(c.close); + c.onCancel = () async { + while (size > 0) { + assert(_offset <= _buffer.length); + if (_buffer.length == _offset) { + if (!await _input.moveNext()) { + size = 0; // no more data + break; + } + _buffer = _input.current; + _offset = 0; + } + + final remainingBuffer = _buffer.length - _offset; + if (remainingBuffer >= size) { + _offset += size; + size = 0; + break; + } + + size -= remainingBuffer; + _buffer = _emptyList; + _offset = 0; + } + _reading = false; + }; + + return c.stream; + } + + /// Cancel the underlying _chunked stream_. + /// + /// If a future from [readChunk] or [readStream] is still pending then + /// [cancel] behaves as if the underlying stream ended early. That is a future + /// from [readChunk] may return a partial chunk smaller than the request size. + /// + /// It is always safe to call [cancel], even if the underlying stream was read + /// to completion. + /// + /// It can be a good idea to call [cancel] in a `finally`-block when done + /// using the [ChunkedStreamReader], this mitigates risk of leaking resources. + Future cancel() async => await _input.cancel(); +} + +/// Extensions for using [ChunkedStreamReader] with byte-streams. +extension ChunkedStreamReaderByteStreamExt on ChunkedStreamReader { + /// Read bytes into a [Uint8List]. + /// + /// This does the same as [readChunk], except it uses [collectBytes] to create + /// a [Uint8List], which offers better performance. + Future readBytes(int size) async => + await collectBytes(readStream(size)); +} diff --git a/pkgs/async/lib/src/delegate/event_sink.dart b/pkgs/async/lib/src/delegate/event_sink.dart new file mode 100644 index 00000000..34c119b5 --- /dev/null +++ b/pkgs/async/lib/src/delegate/event_sink.dart @@ -0,0 +1,44 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// Simple delegating wrapper around an [EventSink]. +/// +/// Subclasses can override individual methods, or use this to expose only the +/// [EventSink] methods of a subclass. +class DelegatingEventSink implements EventSink { + final EventSink _sink; + + /// Create a delegating sink forwarding calls to [sink]. + DelegatingEventSink(EventSink sink) : _sink = sink; + + DelegatingEventSink._(this._sink); + + /// Creates a wrapper that coerces the type of [sink]. + /// + /// Unlike [DelegatingEventSink.new], this only requires its argument to be an + /// instance of `EventSink`, not `EventSink`. This means that calls to + /// [add] may throw a [TypeError] if the argument type doesn't match the + /// reified type of [sink]. + @Deprecated( + 'Use StreamController(sync: true)..stream.cast().pipe(sink)') + static EventSink typed(EventSink sink) => + sink is EventSink ? sink : DelegatingEventSink._(sink); + + @override + void add(T data) { + _sink.add(data); + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + _sink.addError(error, stackTrace); + } + + @override + void close() { + _sink.close(); + } +} diff --git a/pkgs/async/lib/src/delegate/future.dart b/pkgs/async/lib/src/delegate/future.dart new file mode 100644 index 00000000..2179de6f --- /dev/null +++ b/pkgs/async/lib/src/delegate/future.dart @@ -0,0 +1,42 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// A wrapper that forwards calls to a [Future]. +class DelegatingFuture implements Future { + /// The wrapped [Future]. + final Future _future; + + DelegatingFuture(this._future); + + /// Creates a wrapper which throws if [future]'s value isn't an instance of + /// `T`. + /// + /// This soundly converts a [Future] to a `Future`, regardless of its + /// original generic type, by asserting that its value is an instance of `T` + /// whenever it's provided. If it's not, the future throws a [TypeError]. + @Deprecated('Use future.then((v) => v as T) instead.') + static Future typed(Future future) => + future is Future ? future : future.then((v) => v as T); + + @override + Stream asStream() => _future.asStream(); + + @override + Future catchError(Function onError, {bool Function(Object error)? test}) => + _future.catchError(onError, test: test); + + @override + Future then(FutureOr Function(T) onValue, {Function? onError}) => + _future.then(onValue, onError: onError); + + @override + Future whenComplete(FutureOr Function() action) => + _future.whenComplete(action); + + @override + Future timeout(Duration timeLimit, {FutureOr Function()? onTimeout}) => + _future.timeout(timeLimit, onTimeout: onTimeout); +} diff --git a/pkgs/async/lib/src/delegate/sink.dart b/pkgs/async/lib/src/delegate/sink.dart new file mode 100644 index 00000000..a1954f0d --- /dev/null +++ b/pkgs/async/lib/src/delegate/sink.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Simple delegating wrapper around a [Sink]. +/// +/// Subclasses can override individual methods, or use this to expose only the +/// [Sink] methods of a subclass. +class DelegatingSink implements Sink { + final Sink _sink; + + /// Create a delegating sink forwarding calls to [sink]. + DelegatingSink(Sink sink) : _sink = sink; + + DelegatingSink._(this._sink); + + /// Creates a wrapper that coerces the type of [sink]. + /// + /// Unlike [DelegatingSink.new], this only requires its argument to be an + /// instance of `Sink`, not `Sink`. This means that calls to [add] may + /// throw a [TypeError] if the argument type doesn't match the reified type of + /// [sink]. + @Deprecated( + 'Use StreamController(sync: true)..stream.cast().pipe(sink)') + static Sink typed(Sink sink) => + sink is Sink ? sink : DelegatingSink._(sink); + + @override + void add(T data) { + _sink.add(data); + } + + @override + void close() { + _sink.close(); + } +} diff --git a/pkgs/async/lib/src/delegate/stream.dart b/pkgs/async/lib/src/delegate/stream.dart new file mode 100644 index 00000000..68992d5b --- /dev/null +++ b/pkgs/async/lib/src/delegate/stream.dart @@ -0,0 +1,26 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// Simple delegating wrapper around a [Stream]. +/// +/// Subclasses can override individual methods, or use this to expose only the +/// [Stream] methods of a subclass. +/// +/// Note that this is identical to [StreamView] in `dart:async`. It's provided +/// under this name for consistency with other `Delegating*` classes. +class DelegatingStream extends StreamView { + DelegatingStream(super.stream); + + /// Creates a wrapper which throws if [stream]'s events aren't instances of + /// `T`. + /// + /// This soundly converts a [Stream] to a `Stream`, regardless of its + /// original generic type, by asserting that its events are instances of `T` + /// whenever they're provided. If they're not, the stream throws a + /// [TypeError]. + @Deprecated('Use stream.cast instead') + static Stream typed(Stream stream) => stream.cast(); +} diff --git a/pkgs/async/lib/src/delegate/stream_consumer.dart b/pkgs/async/lib/src/delegate/stream_consumer.dart new file mode 100644 index 00000000..c911c414 --- /dev/null +++ b/pkgs/async/lib/src/delegate/stream_consumer.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// Simple delegating wrapper around a [StreamConsumer]. +/// +/// Subclasses can override individual methods, or use this to expose only the +/// [StreamConsumer] methods of a subclass. +class DelegatingStreamConsumer implements StreamConsumer { + final StreamConsumer _consumer; + + /// Create a delegating consumer forwarding calls to [consumer]. + DelegatingStreamConsumer(StreamConsumer consumer) : _consumer = consumer; + + DelegatingStreamConsumer._(this._consumer); + + /// Creates a wrapper that coerces the type of [consumer]. + /// + /// Unlike [StreamConsumer.new], this only requires its argument to be an + /// instance of `StreamConsumer`, not `StreamConsumer`. This means that + /// calls to [addStream] may throw a [TypeError] if the argument type doesn't + /// match the reified type of [consumer]. + @Deprecated( + 'Use StreamController(sync: true)..stream.cast().pipe(sink)') + static StreamConsumer typed(StreamConsumer consumer) => + consumer is StreamConsumer + ? consumer + : DelegatingStreamConsumer._(consumer); + + @override + Future addStream(Stream stream) => _consumer.addStream(stream); + + @override + Future close() => _consumer.close(); +} diff --git a/pkgs/async/lib/src/delegate/stream_sink.dart b/pkgs/async/lib/src/delegate/stream_sink.dart new file mode 100644 index 00000000..e6edd2ff --- /dev/null +++ b/pkgs/async/lib/src/delegate/stream_sink.dart @@ -0,0 +1,48 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// Simple delegating wrapper around a [StreamSink]. +/// +/// Subclasses can override individual methods, or use this to expose only the +/// [StreamSink] methods of a subclass. +class DelegatingStreamSink implements StreamSink { + final StreamSink _sink; + + @override + Future get done => _sink.done; + + /// Create delegating sink forwarding calls to [sink]. + DelegatingStreamSink(StreamSink sink) : _sink = sink; + + DelegatingStreamSink._(this._sink); + + /// Creates a wrapper that coerces the type of [sink]. + /// + /// Unlike [StreamSink.new], this only requires its argument to be an instance + /// of `StreamSink`, not `StreamSink`. This means that calls to [add] may + /// throw a [TypeError] if the argument type doesn't match the reified type of + /// [sink]. + @Deprecated( + 'Use StreamController(sync: true)..stream.cast().pipe(sink)') + static StreamSink typed(StreamSink sink) => + sink is StreamSink ? sink : DelegatingStreamSink._(sink); + + @override + void add(T data) { + _sink.add(data); + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + _sink.addError(error, stackTrace); + } + + @override + Future addStream(Stream stream) => _sink.addStream(stream); + + @override + Future close() => _sink.close(); +} diff --git a/pkgs/async/lib/src/delegate/stream_subscription.dart b/pkgs/async/lib/src/delegate/stream_subscription.dart new file mode 100644 index 00000000..581404a6 --- /dev/null +++ b/pkgs/async/lib/src/delegate/stream_subscription.dart @@ -0,0 +1,66 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import '../typed/stream_subscription.dart'; + +/// Simple delegating wrapper around a [StreamSubscription]. +/// +/// Subclasses can override individual methods. +class DelegatingStreamSubscription implements StreamSubscription { + final StreamSubscription _source; + + /// Create delegating subscription forwarding calls to [sourceSubscription]. + DelegatingStreamSubscription(StreamSubscription sourceSubscription) + : _source = sourceSubscription; + + /// Creates a wrapper which throws if [subscription]'s events aren't instances + /// of `T`. + /// + /// This soundly converts a [StreamSubscription] to a `StreamSubscription`, + /// regardless of its original generic type, by asserting that its events are + /// instances of `T` whenever they're provided. If they're not, the + /// subscription throws a [TypeError]. + @Deprecated('Use Stream.cast instead') + // TODO - Remove `TypeSafeStreamSubscription` and tests when removing this. + static StreamSubscription typed(StreamSubscription subscription) => + subscription is StreamSubscription + ? subscription + : TypeSafeStreamSubscription(subscription); + + @override + void onData(void Function(T)? handleData) { + _source.onData(handleData); + } + + @override + void onError(Function? handleError) { + _source.onError(handleError); + } + + @override + void onDone(void Function()? handleDone) { + _source.onDone(handleDone); + } + + @override + void pause([Future? resumeFuture]) { + _source.pause(resumeFuture); + } + + @override + void resume() { + _source.resume(); + } + + @override + Future cancel() => _source.cancel(); + + @override + Future asFuture([E? futureValue]) => _source.asFuture(futureValue); + + @override + bool get isPaused => _source.isPaused; +} diff --git a/pkgs/async/lib/src/future_group.dart b/pkgs/async/lib/src/future_group.dart new file mode 100644 index 00000000..daf985d3 --- /dev/null +++ b/pkgs/async/lib/src/future_group.dart @@ -0,0 +1,107 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// A collection of futures waits until all added [Future]s complete. +/// +/// Futures are added to the group with [add]. Once you're finished adding +/// futures, signal that by calling [close]. Then, once all added futures have +/// completed, [future] will complete with a list of values from the futures in +/// the group, in the order they were added. +/// +/// If any added future completes with an error, [future] will emit that error +/// and the group will be closed, regardless of the state of other futures in +/// the group. +/// +/// This is similar to [Future.wait] with `eagerError` set to `true`, except +/// that a [FutureGroup] can have futures added gradually over time rather than +/// needing them all at once. +class FutureGroup implements Sink> { + /// The number of futures that have yet to complete. + var _pending = 0; + + /// Whether the group is closed, meaning that no more futures may be added. + bool get isClosed => _closed; + + var _closed = false; + + /// The future that fires once [close] has been called and all futures in the + /// group have completed. + /// + /// This will also complete with an error if any of the futures in the group + /// fails, regardless of whether [close] was called. + Future> get future => _completer.future; + final _completer = Completer>(); + + /// Whether this group contains no futures. + /// + /// A [FutureGroup] is idle when it contains no futures, which is the case for + /// a newly created group or one where all added futures have been removed or + /// completed. + bool get isIdle => _pending == 0; + + /// A broadcast stream that emits an event whenever this group becomes idle. + /// + /// A [FutureGroup] is idle when it contains no futures, which is the case for + /// a newly created group or one where all added futures have been removed or + /// completed. + /// + /// This stream will close when this group is idle *and* [close] has been + /// called. + /// + /// Events are delivered asynchronously, so it's possible for the group to + /// become active again before the event is delivered. + Stream get onIdle => + (_onIdleController ??= StreamController.broadcast(sync: true)).stream; + + StreamController? _onIdleController; + + /// The values emitted by the futures that have been added to the group, in + /// the order they were added. + /// + /// The slots for futures that haven't completed yet are `null`. + final _values = []; + + /// Wait for [task] to complete. + @override + void add(Future task) { + if (_closed) throw StateError('The FutureGroup is closed.'); + + // Ensure that future values are put into [values] in the same order they're + // added to the group by pre-allocating a slot for them and recording its + // index. + var index = _values.length; + _values.add(null); + + _pending++; + task.then((value) { + if (_completer.isCompleted) return null; + + _pending--; + _values[index] = value; + + if (_pending != 0) return null; + var onIdleController = _onIdleController; + if (onIdleController != null) onIdleController.add(null); + + if (!_closed) return null; + if (onIdleController != null) onIdleController.close(); + _completer.complete(_values.whereType().toList()); + }).catchError((Object error, StackTrace stackTrace) { + if (_completer.isCompleted) return null; + _completer.completeError(error, stackTrace); + }); + } + + /// Signals to the group that the caller is done adding futures, and so + /// [future] should fire once all added futures have completed. + @override + void close() { + _closed = true; + if (_pending != 0) return; + if (_completer.isCompleted) return; + _completer.complete(_values.whereType().toList()); + } +} diff --git a/pkgs/async/lib/src/lazy_stream.dart b/pkgs/async/lib/src/lazy_stream.dart new file mode 100644 index 00000000..e0facaa5 --- /dev/null +++ b/pkgs/async/lib/src/lazy_stream.dart @@ -0,0 +1,49 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'stream_completer.dart'; + +/// A [Stream] wrapper that forwards to another [Stream] that's initialized +/// lazily. +/// +/// This class allows a concrete `Stream` to be created only once it has a +/// listener. It's useful to wrapping APIs that do expensive computation to +/// produce a `Stream`. +class LazyStream extends Stream { + /// The callback that's called to create the inner stream. + FutureOr> Function()? _callback; + + /// Creates a single-subscription `Stream` that calls [callback] when it gets + /// a listener and forwards to the returned stream. + LazyStream(FutureOr> Function() callback) : _callback = callback { + // Explicitly check for null because we null out [_callback] internally. + if (_callback == null) throw ArgumentError.notNull('callback'); + } + + @override + StreamSubscription listen(void Function(T)? onData, + {Function? onError, void Function()? onDone, bool? cancelOnError}) { + var callback = _callback; + if (callback == null) { + throw StateError('Stream has already been listened to.'); + } + + // Null out the callback before we invoke it to ensure that even while + // running it, this can't be called twice. + _callback = null; + var result = callback(); + + Stream stream; + if (result is Future>) { + stream = StreamCompleter.fromFuture(result); + } else { + stream = result; + } + + return stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } +} diff --git a/pkgs/async/lib/src/null_stream_sink.dart b/pkgs/async/lib/src/null_stream_sink.dart new file mode 100644 index 00000000..4d9bad0d --- /dev/null +++ b/pkgs/async/lib/src/null_stream_sink.dart @@ -0,0 +1,93 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// A [StreamSink] that discards all events. +/// +/// The sink silently drops events until [close] is called, at which point it +/// throws [StateError]s when events are added. This is the same behavior as a +/// sink whose remote end has closed, such as when a `WebSocket` connection has +/// been closed. +/// +/// This can be used when a sink is needed but no events are actually intended +/// to be added. The [NullStreamSink.error] constructor can be used to +/// represent errors when creating a sink, since [StreamSink.done] exposes sink +/// errors. For example: +/// +/// ```dart +/// StreamSink> openForWrite(String filename) { +/// try { +/// return RandomAccessSink(File(filename).openSync()); +/// } on IOException catch (error, stackTrace) { +/// return NullStreamSink.error(error, stackTrace); +/// } +/// } +/// ``` +class NullStreamSink implements StreamSink { + @override + final Future done; + + /// Whether the sink has been closed. + var _closed = false; + + /// Whether an [addStream] call is pending. + /// + /// We don't actually add any events from streams, but it does return the + /// [StreamSubscription.cancel] future so to be [StreamSink]-complaint we + /// reject events until that completes. + var _addingStream = false; + + /// Creates a null sink. + /// + /// If [done] is passed, it's used as the [StreamSink.done] future. Otherwise, + /// a completed future is used. + NullStreamSink({Future? done}) : done = done ?? Future.value(); + + /// Creates a null sink whose [done] future emits [error]. + /// + /// Note that this error will not be considered uncaught. + NullStreamSink.error(Object error, [StackTrace? stackTrace]) + : done = Future.error(error, stackTrace) + // Don't top-level the error. This gives the user a change to call + // [close] or [done], and matches the behavior of a remote endpoint + // experiencing an error. + ..catchError((_) {}); + + @override + void add(T data) { + _checkEventAllowed(); + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + _checkEventAllowed(); + } + + @override + Future addStream(Stream stream) { + _checkEventAllowed(); + + _addingStream = true; + var future = stream.listen(null).cancel(); + return future.whenComplete(() { + _addingStream = false; + }); + } + + /// Throws a [StateError] if [close] has been called or an [addStream] call is + /// pending. + void _checkEventAllowed() { + if (_closed) throw StateError('Cannot add to a closed sink.'); + if (_addingStream) { + throw StateError('Cannot add to a sink while adding a stream.'); + } + } + + @override + Future close() { + _closed = true; + return done; + } +} diff --git a/pkgs/async/lib/src/restartable_timer.dart b/pkgs/async/lib/src/restartable_timer.dart new file mode 100644 index 00000000..1cff458f --- /dev/null +++ b/pkgs/async/lib/src/restartable_timer.dart @@ -0,0 +1,55 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// A non-periodic timer that can be restarted any number of times. +/// +/// Once restarted (via [reset]), the timer counts down from its original +/// duration again. +class RestartableTimer implements Timer { + /// The duration of the timer. + final Duration _duration; + + /// The callback to call when the timer fires. + final ZoneCallback _callback; + + /// The timer for the current or most recent countdown. + /// + /// This timer is canceled and overwritten every time this [RestartableTimer] + /// is reset. + Timer _timer; + + /// Creates a new timer. + /// + /// The [_callback] function is invoked after the given [_duration]. Unlike a + /// normal non-periodic [Timer], [_callback] may be called more than once. + RestartableTimer(this._duration, this._callback) + : _timer = Timer(_duration, _callback); + + @override + bool get isActive => _timer.isActive; + + /// Restarts the timer so that it counts down from its original duration + /// again. + /// + /// This restarts the timer even if it has already fired or has been canceled. + void reset() { + _timer.cancel(); + _timer = Timer(_duration, _callback); + } + + @override + void cancel() { + _timer.cancel(); + } + + /// The number of durations preceding the most recent timer event on the most + /// recent countdown. + /// + /// Calls to [reset] will also reset the tick so subsequent tick values may + /// not be strictly larger than previous values. + @override + int get tick => _timer.tick; +} diff --git a/pkgs/async/lib/src/result/capture_sink.dart b/pkgs/async/lib/src/result/capture_sink.dart new file mode 100644 index 00000000..562f5f95 --- /dev/null +++ b/pkgs/async/lib/src/result/capture_sink.dart @@ -0,0 +1,29 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'result.dart'; + +/// Used by [Result.captureSink]. +class CaptureSink implements EventSink { + final EventSink> _sink; + + CaptureSink(EventSink> sink) : _sink = sink; + + @override + void add(T value) { + _sink.add(Result.value(value)); + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + _sink.add(Result.error(error, stackTrace)); + } + + @override + void close() { + _sink.close(); + } +} diff --git a/pkgs/async/lib/src/result/capture_transformer.dart b/pkgs/async/lib/src/result/capture_transformer.dart new file mode 100644 index 00000000..39aaef9f --- /dev/null +++ b/pkgs/async/lib/src/result/capture_transformer.dart @@ -0,0 +1,20 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'capture_sink.dart'; +import 'result.dart'; + +/// A stream transformer that captures a stream of events into [Result]s. +/// +/// The result of the transformation is a stream of [Result] values and no +/// error events. Exposed by [Result.captureStream]. +class CaptureStreamTransformer extends StreamTransformerBase> { + const CaptureStreamTransformer(); + + @override + Stream> bind(Stream source) => + Stream>.eventTransformed(source, CaptureSink.new); +} diff --git a/pkgs/async/lib/src/result/error.dart b/pkgs/async/lib/src/result/error.dart new file mode 100644 index 00000000..48f71b1d --- /dev/null +++ b/pkgs/async/lib/src/result/error.dart @@ -0,0 +1,66 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'result.dart'; +import 'value.dart'; + +/// A result representing a thrown error. +class ErrorResult implements Result { + /// The error object that was thrown. + final Object error; + + /// The stack trace corresponding to where [error] was thrown. + final StackTrace stackTrace; + + @override + bool get isValue => false; + @override + bool get isError => true; + @override + ValueResult? get asValue => null; + @override + ErrorResult get asError => this; + + ErrorResult(this.error, [StackTrace? stackTrace]) + : stackTrace = stackTrace ?? AsyncError.defaultStackTrace(error); + + @override + void complete(Completer completer) { + completer.completeError(error, stackTrace); + } + + @override + void addTo(EventSink sink) { + sink.addError(error, stackTrace); + } + + @override + Future get asFuture => Future.error(error, stackTrace); + + /// Calls an error handler with the error and stacktrace. + /// + /// An async error handler function is either a function expecting two + /// arguments, which will be called with the error and the stack trace, or it + /// has to be a function expecting only one argument, which will be called + /// with only the error. + void handle(Function errorHandler) { + if (errorHandler is ZoneBinaryCallback) { + errorHandler(error, stackTrace); + } else { + (errorHandler as ZoneUnaryCallback)(error); + } + } + + @override + int get hashCode => error.hashCode ^ stackTrace.hashCode ^ 0x1d61823f; + + /// This is equal only to an error result with equal [error] and [stackTrace]. + @override + bool operator ==(Object other) => + other is ErrorResult && + error == other.error && + stackTrace == other.stackTrace; +} diff --git a/pkgs/async/lib/src/result/future.dart b/pkgs/async/lib/src/result/future.dart new file mode 100644 index 00000000..a8dd3160 --- /dev/null +++ b/pkgs/async/lib/src/result/future.dart @@ -0,0 +1,25 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../delegate/future.dart'; +import 'result.dart'; + +/// A [Future] wrapper that provides synchronous access to the result of the +/// wrapped [Future] once it's completed. +class ResultFuture extends DelegatingFuture { + /// Whether the future has fired and [result] is available. + bool get isComplete => result != null; + + /// The result of the wrapped [Future], if it's completed. + /// + /// If it hasn't completed yet, this will be `null`. + Result? get result => _result; + Result? _result; + + ResultFuture(Future future) : super(future) { + Result.capture(future).then((result) { + _result = result; + }); + } +} diff --git a/pkgs/async/lib/src/result/release_sink.dart b/pkgs/async/lib/src/result/release_sink.dart new file mode 100644 index 00000000..bf6dd505 --- /dev/null +++ b/pkgs/async/lib/src/result/release_sink.dart @@ -0,0 +1,31 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'result.dart'; + +/// Used by [Result.releaseSink]. +class ReleaseSink implements EventSink> { + final EventSink _sink; + + ReleaseSink(this._sink); + + @override + void add(Result result) { + result.addTo(_sink); + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + // Errors may be added by intermediate processing, even if it is never + // added by CaptureSink. + _sink.addError(error, stackTrace); + } + + @override + void close() { + _sink.close(); + } +} diff --git a/pkgs/async/lib/src/result/release_transformer.dart b/pkgs/async/lib/src/result/release_transformer.dart new file mode 100644 index 00000000..2f80d719 --- /dev/null +++ b/pkgs/async/lib/src/result/release_transformer.dart @@ -0,0 +1,21 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'release_sink.dart'; +import 'result.dart'; + +/// A transformer that releases result events as data and error events. +class ReleaseStreamTransformer extends StreamTransformerBase, T> { + const ReleaseStreamTransformer(); + + @override + Stream bind(Stream> source) { + return Stream.eventTransformed(source, _createSink); + } + + // Since Stream.eventTransformed is not generic, this method can be static. + static EventSink _createSink(EventSink sink) => ReleaseSink(sink); +} diff --git a/pkgs/async/lib/src/result/result.dart b/pkgs/async/lib/src/result/result.dart new file mode 100644 index 00000000..124ccefa --- /dev/null +++ b/pkgs/async/lib/src/result/result.dart @@ -0,0 +1,223 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import '../stream_sink_transformer.dart'; +import 'capture_sink.dart'; +import 'capture_transformer.dart'; +import 'error.dart'; +import 'release_sink.dart'; +import 'release_transformer.dart'; +import 'value.dart'; + +/// The result of a computation. +/// +/// Capturing a result (either a returned value or a thrown error) means +/// converting it into a [Result] - either a [ValueResult] or an [ErrorResult]. +/// +/// This value can release itself by writing itself either to an [EventSink] or +/// a [Completer], or by becoming a [Future]. +/// +/// A [Future] represents a potential result, one that might not have been +/// computed yet, and a [Result] is always a completed and available result. +abstract class Result { + /// A stream transformer that captures a stream of events into [Result]s. + /// + /// The result of the transformation is a stream of [Result] values and no + /// error events. This is the transformer used by [captureStream]. + static const StreamTransformer> + captureStreamTransformer = CaptureStreamTransformer(); + + /// A stream transformer that releases a stream of result events. + /// + /// The result of the transformation is a stream of values and error events. + /// This is the transformer used by [releaseStream]. + static const StreamTransformer, Object> + releaseStreamTransformer = ReleaseStreamTransformer(); + + /// A sink transformer that captures events into [Result]s. + /// + /// The result of the transformation is a sink that only forwards [Result] + /// values and no error events. + static const StreamSinkTransformer> + captureSinkTransformer = + StreamSinkTransformer>.fromStreamTransformer( + CaptureStreamTransformer()); + + /// A sink transformer that releases result events. + /// + /// The result of the transformation is a sink that forwards of values and + /// error events. + static const StreamSinkTransformer, Object> + releaseSinkTransformer = + StreamSinkTransformer, Object>.fromStreamTransformer( + ReleaseStreamTransformer()); + + /// Creates a `Result` with the result of calling [computation]. + /// + /// This generates either a [ValueResult] with the value returned by + /// calling `computation`, or an [ErrorResult] with an error thrown by + /// the call. + factory Result(T Function() computation) { + try { + return ValueResult(computation()); + } on Object catch (e, s) { + return ErrorResult(e, s); + } + } + + /// Creates a `Result` holding a value. + /// + /// Alias for [ValueResult.new]. + factory Result.value(T value) = ValueResult; + + /// Creates a `Result` holding an error. + /// + /// Alias for [ErrorResult.new]. + factory Result.error(Object error, [StackTrace? stackTrace]) => + ErrorResult(error, stackTrace); + + /// Captures the result of a future into a `Result` future. + /// + /// The resulting future will never have an error. + /// Errors have been converted to an [ErrorResult] value. + static Future> capture(Future future) { + return future.then(ValueResult.new, onError: ErrorResult.new); + } + + /// Captures each future in [elements], + /// + /// Returns a (future of) a list of results for each element in [elements], + /// in iteration order. + /// Each future in [elements] is [capture]d and each non-future is + /// wrapped as a [Result.value]. + /// The returned future will never have an error. + static Future>> captureAll(Iterable> elements) { + var results = ?>[]; + var pending = 0; + late Completer>> completer; + for (var element in elements) { + if (element is Future) { + var i = results.length; + results.add(null); + pending++; + Result.capture(element).then((result) { + results[i] = result; + if (--pending == 0) { + completer.complete(List.from(results)); + } + }); + } else { + results.add(Result.value(element)); + } + } + if (pending == 0) { + return Future.value(List.from(results)); + } + completer = Completer>>(); + return completer.future; + } + + /// Releases the result of a captured future. + /// + /// Converts the [Result] value of the given [future] to a value or error + /// completion of the returned future. + /// + /// If [future] completes with an error, the returned future completes with + /// the same error. + static Future release(Future> future) => + future.then((result) => result.asFuture); + + /// Captures the results of a stream into a stream of [Result] values. + /// + /// The returned stream will not have any error events. + /// Errors from the source stream have been converted to [ErrorResult]s. + static Stream> captureStream(Stream source) => + source.transform(CaptureStreamTransformer()); + + /// Releases a stream of [source] values into a stream of the results. + /// + /// `Result` values of the source stream become value or error events in + /// the returned stream as appropriate. + /// Errors from the source stream become errors in the returned stream. + static Stream releaseStream(Stream> source) => + source.transform(ReleaseStreamTransformer()); + + /// Releases results added to the returned sink as data and errors on [sink]. + /// + /// A [Result] added to the returned sink is added as a data or error event + /// on [sink]. Errors added to the returned sink are forwarded directly to + /// [sink] and so is the [EventSink.close] calls. + static EventSink> releaseSink(EventSink sink) => + ReleaseSink(sink); + + /// Captures the events of the returned sink into results on [sink]. + /// + /// Data and error events added to the returned sink are captured into + /// [Result] values and added as data events on the provided [sink]. + /// No error events are ever added to [sink]. + /// + /// When the returned sink is closed, so is [sink]. + static EventSink captureSink(EventSink> sink) => + CaptureSink(sink); + + /// Converts a result of a result to a single result. + /// + /// If the result is an error, or it is a `Result` value + /// which is then an error, then a result with that error is returned. + /// Otherwise both levels of results are value results, and a single + /// result with the value is returned. + static Result flatten(Result> result) { + if (result.isValue) return result.asValue!.value; + return result.asError!; + } + + /// Converts a sequence of results to a result of a list. + /// + /// Returns either a list of values if [results] doesn't contain any errors, + /// or the first error result in [results]. + static Result> flattenAll(Iterable> results) { + var values = []; + for (var result in results) { + if (result.isValue) { + values.add(result.asValue!.value); + } else { + return result.asError!; + } + } + return Result>.value(values); + } + + /// Whether this result is a value result. + /// + /// Always the opposite of [isError]. + bool get isValue; + + /// Whether this result is an error result. + /// + /// Always the opposite of [isValue]. + bool get isError; + + /// If this is a value result, returns itself. + /// + /// Otherwise returns `null`. + ValueResult? get asValue; + + /// If this is an error result, returns itself. + /// + /// Otherwise returns `null`. + ErrorResult? get asError; + + /// Completes a completer with this result. + void complete(Completer completer); + + /// Adds this result to an [EventSink]. + /// + /// Calls the sink's `add` or `addError` method as appropriate. + void addTo(EventSink sink); + + /// A future that has been completed with this result as a value or an error. + Future get asFuture; +} diff --git a/pkgs/async/lib/src/result/value.dart b/pkgs/async/lib/src/result/value.dart new file mode 100644 index 00000000..3872dd0b --- /dev/null +++ b/pkgs/async/lib/src/result/value.dart @@ -0,0 +1,45 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'error.dart'; +import 'result.dart'; + +/// A result representing a returned value. +class ValueResult implements Result { + /// The result of a successful computation. + final T value; + + @override + bool get isValue => true; + @override + bool get isError => false; + @override + ValueResult get asValue => this; + @override + ErrorResult? get asError => null; + + ValueResult(this.value); + + @override + void complete(Completer completer) { + completer.complete(value); + } + + @override + void addTo(EventSink sink) { + sink.add(value); + } + + @override + Future get asFuture => Future.value(value); + + @override + int get hashCode => value.hashCode ^ 0x323f1d61; + + @override + bool operator ==(Object other) => + other is ValueResult && value == other.value; +} diff --git a/pkgs/async/lib/src/single_subscription_transformer.dart b/pkgs/async/lib/src/single_subscription_transformer.dart new file mode 100644 index 00000000..ba6f0d2e --- /dev/null +++ b/pkgs/async/lib/src/single_subscription_transformer.dart @@ -0,0 +1,36 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// A transformer that converts a broadcast stream into a single-subscription +/// stream. +/// +/// This buffers the broadcast stream's events, which means that it starts +/// listening to a stream as soon as it's bound. +/// +/// This also casts the source stream's events to type `T`. If the cast fails, +/// the result stream will emit a [TypeError]. This behavior is deprecated, and +/// should not be relied upon. +class SingleSubscriptionTransformer extends StreamTransformerBase { + const SingleSubscriptionTransformer(); + + @override + Stream bind(Stream stream) { + late StreamSubscription subscription; + var controller = + StreamController(sync: true, onCancel: () => subscription.cancel()); + subscription = stream.listen((value) { + // TODO(nweiz): When we release a new major version, get rid of the second + // type parameter and avoid this conversion. + try { + controller.add(value as T); + // ignore: avoid_catching_errors + } on TypeError catch (error, stackTrace) { + controller.addError(error, stackTrace); + } + }, onError: controller.addError, onDone: controller.close); + return controller.stream; + } +} diff --git a/pkgs/async/lib/src/sink_base.dart b/pkgs/async/lib/src/sink_base.dart new file mode 100644 index 00000000..f1d7d14b --- /dev/null +++ b/pkgs/async/lib/src/sink_base.dart @@ -0,0 +1,171 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:meta/meta.dart'; + +import 'async_memoizer.dart'; + +/// An abstract class that implements [EventSink] in terms of [onAdd], +/// [onError], and [onClose] methods. +/// +/// This takes care of ensuring that events can't be added after [close] is +/// called. +@Deprecated('Will be removed in the next major release') +abstract class EventSinkBase implements EventSink { + /// Whether [close] has been called and no more events may be written. + bool get _closed => _closeMemo.hasRun; + + @override + void add(T data) { + _checkCanAddEvent(); + onAdd(data); + } + + /// A method that handles data events that are passed to the sink. + @visibleForOverriding + void onAdd(T data); + + @override + void addError(Object error, [StackTrace? stackTrace]) { + _checkCanAddEvent(); + onError(error, stackTrace); + } + + /// A method that handles error events that are passed to the sink. + @visibleForOverriding + void onError(Object error, [StackTrace? stackTrace]); + + @override + Future close() => _closeMemo.runOnce(onClose); + final _closeMemo = AsyncMemoizer(); + + /// A method that handles the sink being closed. + /// + /// This may return a future that completes once the stream sink has shut + /// down. If cleaning up can fail, the error may be reported in the returned + /// future. + @visibleForOverriding + FutureOr onClose(); + + /// Asserts that the sink is in a state where adding an event is valid. + void _checkCanAddEvent() { + if (_closed) throw StateError('Cannot add event after closing'); + } +} + +/// An abstract class that implements [StreamSink] in terms of [onAdd], +/// [onError], and [onClose] methods. +/// +/// This takes care of ensuring that events can't be added after [close] is +/// called or during a call to [addStream]. +@Deprecated('Will be removed in the next major release') +abstract class StreamSinkBase extends EventSinkBase + implements StreamSink { + /// Whether a call to [addStream] is ongoing. + bool _addingStream = false; + + @override + Future get done => _closeMemo.future; + + @override + Future addStream(Stream stream) { + _checkCanAddEvent(); + + _addingStream = true; + var completer = Completer.sync(); + stream.listen(onAdd, onError: onError, onDone: () { + _addingStream = false; + completer.complete(); + }); + return completer.future; + } + + @override + Future close() { + if (_addingStream) throw StateError('StreamSink is bound to a stream'); + return super.close(); + } + + @override + void _checkCanAddEvent() { + super._checkCanAddEvent(); + if (_addingStream) throw StateError('StreamSink is bound to a stream'); + } +} + +/// An abstract class that implements `dart:io`'s `IOSink`'s API in terms of +/// [onAdd], [onError], [onClose], and [onFlush] methods. +/// +/// Because `IOSink` is defined in `dart:io`, this can't officially implement +/// it. However, it's designed to match its API exactly so that subclasses can +/// implement `IOSink` without any additional modifications. +/// +/// This takes care of ensuring that events can't be added after [close] is +/// called or during a call to [addStream]. +@Deprecated('Will be removed in the next major release') +abstract class IOSinkBase extends StreamSinkBase> { + /// See `IOSink.encoding` from `dart:io`. + Encoding encoding; + + IOSinkBase([this.encoding = utf8]); + + /// See `IOSink.flush` from `dart:io`. + /// + /// Because this base class doesn't do any buffering of its own, [flush] + /// always completes immediately. + /// + /// Subclasses that do buffer events should override [flush] to complete once + /// all events are delivered. They should also call `super.flush()` at the + /// beginning of the method to throw a [StateError] if the sink is currently + /// adding a stream. + Future flush() { + if (_addingStream) throw StateError('StreamSink is bound to a stream'); + if (_closed) return Future.value(); + + _addingStream = true; + return onFlush().whenComplete(() { + _addingStream = false; + }); + } + + /// Flushes any buffered data to the underlying consumer, and returns a future + /// that completes once the consumer has accepted all data. + @visibleForOverriding + Future onFlush(); + + /// See [StringSink.write]. + void write(Object? object) { + var string = object.toString(); + if (string.isEmpty) return; + add(encoding.encode(string)); + } + + /// See [StringSink.writeAll]. + void writeAll(Iterable objects, [String separator = '']) { + var first = true; + for (var object in objects) { + if (first) { + first = false; + } else { + write(separator); + } + + write(object); + } + } + + /// See [StringSink.writeln]. + void writeln([Object? object = '']) { + write(object); + write('\n'); + } + + /// See [StringSink.writeCharCode]. + void writeCharCode(int charCode) { + write(String.fromCharCode(charCode)); + } +} diff --git a/pkgs/async/lib/src/stream_closer.dart b/pkgs/async/lib/src/stream_closer.dart new file mode 100644 index 00000000..91546242 --- /dev/null +++ b/pkgs/async/lib/src/stream_closer.dart @@ -0,0 +1,108 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:meta/meta.dart'; + +/// A [StreamTransformer] that allows the caller to forcibly close the +/// transformed [Stream](s). +/// +/// When [close] is called, any stream (or streams) transformed by this +/// transformer that haven't already completed or been cancelled will emit a +/// done event and cancel their underlying subscriptions. +/// +/// Note that unlike most [StreamTransformer]s, each instance of [StreamCloser] +/// has its own state (whether or not it's been closed), so it's a good idea to +/// construct a new one for each use unless you need to close multiple streams +/// at the same time. +@sealed +class StreamCloser extends StreamTransformerBase { + /// The subscriptions to streams passed to [bind]. + final _subscriptions = >{}; + + /// The controllers for streams returned by [bind]. + final _controllers = >{}; + + /// Closes all transformed streams. + /// + /// Returns a future that completes when all inner subscriptions' + /// [StreamSubscription.cancel] futures have completed. Note that a stream's + /// subscription won't be canceled until the transformed stream has a + /// listener. + /// + /// If a transformed stream is listened to after [close] is called, the + /// original stream will be listened to and then the subscription immediately + /// canceled. If that cancellation throws an error, it will be silently + /// ignored. + Future close() => _closeFuture ??= () { + var futures = [ + for (var subscription in _subscriptions) subscription.cancel() + ]; + _subscriptions.clear(); + + var controllers = _controllers.toList(); + _controllers.clear(); + scheduleMicrotask(() { + for (var controller in controllers) { + scheduleMicrotask(controller.close); + } + }); + + return Future.wait(futures, eagerError: true); + }(); + Future? _closeFuture; + + /// Whether [close] has been called. + bool get isClosed => _closeFuture != null; + + @override + Stream bind(Stream stream) { + var controller = stream.isBroadcast + ? StreamController.broadcast(sync: true) + : StreamController(sync: true); + + controller.onListen = () { + if (isClosed) { + // Ignore errors here, because otherwise there would be no way for the + // user to handle them gracefully. + stream.listen(null).cancel().catchError((_) {}); + return; + } + + var subscription = + stream.listen(controller.add, onError: controller.addError); + subscription.onDone(() { + _subscriptions.remove(subscription); + _controllers.remove(controller); + controller.close(); + }); + _subscriptions.add(subscription); + + if (!stream.isBroadcast) { + controller.onPause = subscription.pause; + controller.onResume = subscription.resume; + } + + controller.onCancel = () { + _controllers.remove(controller); + + // If the subscription has already been removed, that indicates that the + // underlying stream has been cancelled by [close] and its cancellation + // future has been handled there. In that case, we shouldn't forward it + // here as well. + if (_subscriptions.remove(subscription)) return subscription.cancel(); + return null; + }; + }; + + if (isClosed) { + controller.close(); + } else { + _controllers.add(controller); + } + + return controller.stream; + } +} diff --git a/pkgs/async/lib/src/stream_completer.dart b/pkgs/async/lib/src/stream_completer.dart new file mode 100644 index 00000000..27034c2f --- /dev/null +++ b/pkgs/async/lib/src/stream_completer.dart @@ -0,0 +1,182 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// A single-subscription [stream] where the contents are provided later. +/// +/// It is generally recommended that you never create a `Future` +/// because you can just directly create a stream that doesn't do anything +/// until it's ready to do so. +/// This class can be used to create such a stream. +/// +/// The [stream] is a normal stream that you can listen to immediately, +/// but until either [setSourceStream] or [setEmpty] is called, +/// the stream won't produce any events. +/// +/// The same effect can be achieved by using a [StreamController] +/// and adding the stream using `addStream` when both +/// the controller's stream is listened to and the source stream is ready. +/// This class attempts to shortcut some of the overhead when possible. +/// For example, if the [stream] is only listened to +/// after the source stream has been set, +/// the listen is performed directly on the source stream. +class StreamCompleter { + /// The stream doing the actual work, is returned by [stream]. + final _stream = _CompleterStream(); + + /// Convert a `Future` to a `Stream`. + /// + /// This creates a stream using a stream completer, + /// and sets the source stream to the result of the future when the + /// future completes. + /// + /// If the future completes with an error, the returned stream will + /// instead contain just that error. + static Stream fromFuture(Future> streamFuture) { + var completer = StreamCompleter(); + streamFuture.then(completer.setSourceStream, onError: completer.setError); + return completer.stream; + } + + /// The stream of this completer. + /// + /// This stream is always a single-subscription stream. + /// + /// When a source stream is provided, its events will be forwarded to + /// listeners on this stream. + /// + /// The stream can be listened either before or after a source stream + /// is set. + Stream get stream => _stream; + + /// Set a stream as the source of events for the [StreamCompleter]'s + /// [stream]. + /// + /// The completer's `stream` will act exactly as [sourceStream]. + /// + /// If the source stream is set before [stream] is listened to, + /// the listen call on [stream] is forwarded directly to [sourceStream]. + /// + /// If [stream] is listened to before setting the source stream, + /// an intermediate subscription is created. It looks like a completely + /// normal subscription, and can be paused or canceled, but it won't + /// produce any events until a source stream is provided. + /// + /// If the `stream` subscription is canceled before a source stream is set, + /// the source stream will be listened to and immediately canceled again. + /// + /// Otherwise, when the source stream is then set, + /// it is immediately listened to, and its events are forwarded to the + /// existing subscription. + /// + /// Any one of [setSourceStream], [setEmpty], and [setError] may be called at + /// most once. Trying to call any of them again will fail. + void setSourceStream(Stream sourceStream) { + if (_stream._isSourceStreamSet) { + throw StateError('Source stream already set'); + } + _stream._setSourceStream(sourceStream); + } + + /// Equivalent to setting an empty stream using [setSourceStream]. + /// + /// Any one of [setSourceStream], [setEmpty], and [setError] may be called at + /// most once. Trying to call any of them again will fail. + void setEmpty() { + if (_stream._isSourceStreamSet) { + throw StateError('Source stream already set'); + } + _stream._setEmpty(); + } + + /// Completes this to a stream that emits [error] and then closes. + /// + /// This is useful when the process of creating the data for the stream fails. + /// + /// Any one of [setSourceStream], [setEmpty], and [setError] may be called at + /// most once. Trying to call any of them again will fail. + void setError(Object error, [StackTrace? stackTrace]) { + setSourceStream(Stream.fromFuture(Future.error(error, stackTrace))); + } +} + +/// Stream completed by [StreamCompleter]. +class _CompleterStream extends Stream { + /// Controller for an intermediate stream. + /// + /// Created if the user listens on this stream before the source stream + /// is set, or if using [_setEmpty] so there is no source stream. + StreamController? _controller; + + /// Source stream for the events provided by this stream. + /// + /// Set when the completer sets the source stream using [_setSourceStream] + /// or [_setEmpty]. + Stream? _sourceStream; + + @override + StreamSubscription listen(void Function(T)? onData, + {Function? onError, void Function()? onDone, bool? cancelOnError}) { + if (_controller == null) { + var sourceStream = _sourceStream; + if (sourceStream != null && !sourceStream.isBroadcast) { + // If the source stream is itself single subscription, + // just listen to it directly instead of creating a controller. + return sourceStream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + _ensureController(); + if (_sourceStream != null) { + _linkStreamToController(); + } + } + return _controller!.stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + /// Whether a source stream has been set. + /// + /// Used to throw an error if trying to set a source stream twice. + bool get _isSourceStreamSet => _sourceStream != null; + + /// Sets the source stream providing the events for this stream. + /// + /// If set before the user listens, listen calls will be directed directly + /// to the source stream. If the user listenes earlier, and intermediate + /// stream is created using a stream controller, and the source stream is + /// linked into that stream later. + void _setSourceStream(Stream sourceStream) { + assert(_sourceStream == null); + _sourceStream = sourceStream; + if (_controller != null) { + // User has already listened, so provide the data through controller. + _linkStreamToController(); + } + } + + /// Links source stream to controller when both are available. + void _linkStreamToController() { + var controller = _controller!; + controller + .addStream(_sourceStream!, cancelOnError: false) + .whenComplete(controller.close); + } + + /// Sets an empty source stream. + /// + /// Uses [_controller] for the stream, then closes the controller + /// immediately. + void _setEmpty() { + assert(_sourceStream == null); + var controller = _ensureController(); + _sourceStream = controller.stream; // Mark stream as set. + controller.close(); + } + + // Creates the [_controller]. + StreamController _ensureController() { + return _controller ??= StreamController(sync: true); + } +} diff --git a/pkgs/async/lib/src/stream_extensions.dart b/pkgs/async/lib/src/stream_extensions.dart new file mode 100644 index 00000000..4ba9254f --- /dev/null +++ b/pkgs/async/lib/src/stream_extensions.dart @@ -0,0 +1,81 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// Utility extensions on [Stream]. +extension StreamExtensions on Stream { + /// Creates a stream whose elements are contiguous slices of `this`. + /// + /// Each slice is [length] elements long, except for the last one which may be + /// shorter if `this` emits too few elements. Each slice begins after the + /// last one ends. + /// + /// For example, `Stream.fromIterable([1, 2, 3, 4, 5]).slices(2)` emits + /// `([1, 2], [3, 4], [5])`. + /// + /// Errors are forwarded to the result stream immediately when they occur, + /// even if previous data events have not been emitted because the next slice + /// is not complete yet. + Stream> slices(int length) { + if (length < 1) throw RangeError.range(length, 1, null, 'length'); + + var slice = []; + return transform(StreamTransformer.fromHandlers(handleData: (data, sink) { + slice.add(data); + if (slice.length == length) { + sink.add(slice); + slice = []; + } + }, handleDone: (sink) { + if (slice.isNotEmpty) sink.add(slice); + sink.close(); + })); + } + + /// A future which completes with the first event of this stream, or with + /// `null`. + /// + /// This stream is listened to, and if it emits any event, whether a data + /// event or an error event, the future completes with the same data value or + /// error. If the stream ends without emitting any events, the future is + /// completed with `null`. + Future get firstOrNull { + var completer = Completer.sync(); + final subscription = listen(null, + onError: completer.completeError, + onDone: completer.complete, + cancelOnError: true); + subscription.onData((event) { + subscription.cancel().whenComplete(() { + completer.complete(event); + }); + }); + return completer.future; + } + + /// Eagerly listens to this stream and buffers events until needed. + /// + /// The returned stream will emit the same events as this stream, starting + /// from when this method is called. The events are delayed until the returned + /// stream is listened to, at which point all buffered events will be emitted + /// in order, and then further events from this stream will be emitted as they + /// arrive. + /// + /// The buffer will retain all events until the returned stream is listened + /// to, so if the stream can emit arbitrary amounts of data, callers should be + /// careful to listen to the stream eventually or call + /// `stream.listen(null).cancel()` to discard the buffered data if it becomes + /// clear that the data isn't not needed. + Stream listenAndBuffer() { + var controller = StreamController(sync: true); + var subscription = listen(controller.add, + onError: controller.addError, onDone: controller.close); + controller + ..onPause = subscription.pause + ..onResume = subscription.resume + ..onCancel = subscription.cancel; + return controller.stream; + } +} diff --git a/pkgs/async/lib/src/stream_group.dart b/pkgs/async/lib/src/stream_group.dart new file mode 100644 index 00000000..502a111c --- /dev/null +++ b/pkgs/async/lib/src/stream_group.dart @@ -0,0 +1,336 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// A collection of streams whose events are unified and sent through a central +/// stream. +/// +/// Both errors and data events are forwarded through [stream]. The streams in +/// the group won't be listened to until [stream] has a listener. **Note that +/// this means that events emitted by broadcast streams will be dropped until +/// [stream] has a listener.** +/// +/// If the `StreamGroup` is constructed using [StreamGroup.new], [stream] will +/// be single-subscription. In this case, if [stream] is paused or canceled, all +/// streams in the group will likewise be paused or canceled, respectively. +/// +/// If the `StreamGroup` is constructed using [StreamGroup.broadcast], +/// [stream] will be a broadcast stream. In this case, the streams in the group +/// will never be paused and single-subscription streams in the group will never +/// be canceled. **Note that single-subscription streams in a broadcast group +/// may drop events if a listener is added and later removed.** Broadcast +/// streams in the group will be canceled once [stream] has no listeners, and +/// will be listened to again once [stream] has listeners. +/// +/// [stream] won't close until [close] is called on the group *and* every stream +/// in the group closes. +class StreamGroup implements Sink> { + /// The stream through which all events from streams in the group are emitted. + Stream get stream => _controller.stream; + late StreamController _controller; + + /// Whether the group is closed, meaning that no more streams may be added. + bool get isClosed => _closed; + + var _closed = false; + + /// The current state of the group. + /// + /// See [_StreamGroupState] for detailed descriptions of each state. + var _state = _StreamGroupState.dormant; + + /// Whether this group contains no streams. + /// + /// A [StreamGroup] is idle when it contains no streams, which is the case for + /// a newly created group or one where all added streams have been emitted + /// done events (or been [remove]d). + /// + /// If this is a single-subscription group, then cancelling the subscription + /// to [stream] will also remove all streams. + bool get isIdle => _subscriptions.isEmpty; + + /// A broadcast stream that emits an event whenever this group becomes idle. + /// + /// A [StreamGroup] is idle when it contains no streams, which is the case for + /// a newly created group or one where all added streams have been emitted + /// done events (or been [remove]d). + /// + /// This stream will close when either: + /// + /// * This group is idle *and* [close] has been called, or + /// * [stream]'s subscription has been cancelled (if this is a + /// single-subscription group). + /// + /// Note that: + /// + /// * Events won't be emitted on this stream until [stream] has been listened + /// to. + /// * Events are delivered asynchronously, so it's possible for the group to + /// become active again before the event is delivered. + Stream get onIdle => + (_onIdleController ??= StreamController.broadcast()).stream; + + StreamController? _onIdleController; + + /// Streams that have been added to the group, and their subscriptions if they + /// have been subscribed to. + /// + /// The subscriptions will be null until the group has a listener registered. + /// If it's a broadcast group and it goes dormant again, broadcast stream + /// subscriptions will be canceled and set to null again. Single-subscriber + /// stream subscriptions will be left intact, since they can't be + /// re-subscribed. + final _subscriptions = , StreamSubscription?>{}; + + /// Merges the events from [streams] into a single single-subscription stream. + /// + /// This is equivalent to adding [streams] to a group, closing that group, and + /// returning its stream. + static Stream merge(Iterable> streams) { + var group = StreamGroup(); + streams.forEach(group.add); + group.close(); + return group.stream; + } + + /// Merges the events from [streams] into a single broadcast stream. + /// + /// This is equivalent to adding [streams] to a broadcast group, closing that + /// group, and returning its stream. + static Stream mergeBroadcast(Iterable> streams) { + var group = StreamGroup.broadcast(); + streams.forEach(group.add); + group.close(); + return group.stream; + } + + /// Creates a new stream group where [stream] is single-subscriber. + StreamGroup() { + _controller = StreamController( + onListen: _onListen, + onPause: _onPause, + onResume: _onResume, + onCancel: _onCancel, + sync: true); + } + + /// Creates a new stream group where [stream] is a broadcast stream. + StreamGroup.broadcast() { + _controller = StreamController.broadcast( + onListen: _onListen, onCancel: _onCancelBroadcast, sync: true); + } + + /// Adds [stream] as a member of this group. + /// + /// Any events from [stream] will be emitted through [this.stream]. If this + /// group has a listener, [stream] will be listened to immediately; otherwise + /// it will only be listened to once this group gets a listener. + /// + /// If this is a single-subscription group and its subscription has been + /// canceled, [stream] will be canceled as soon as its added. If this returns + /// a [Future], it will be returned from [add]. Otherwise, [add] returns + /// `null`. + /// + /// Throws a [StateError] if this group is closed. + @override + Future? add(Stream stream) { + if (_closed) { + throw StateError("Can't add a Stream to a closed StreamGroup."); + } + + if (_state == _StreamGroupState.dormant) { + _subscriptions.putIfAbsent(stream, () => null); + } else if (_state == _StreamGroupState.canceled) { + // Listen to the stream and cancel it immediately so that no one else can + // listen, for consistency. If the stream has an onCancel listener this + // will also fire that, which may help it clean up resources. + return stream.listen(null).cancel(); + } else { + _subscriptions.putIfAbsent(stream, () => _listenToStream(stream)); + } + + return null; + } + + /// Removes [stream] as a member of this group. + /// + /// No further events from [stream] will be emitted through this group. If + /// [stream] has been listened to, its subscription will be canceled. + /// + /// If [stream] has been listened to, this *synchronously* cancels its + /// subscription. This means that any events from [stream] that haven't yet + /// been emitted through this group will not be. + /// + /// If [stream]'s subscription is canceled, this returns + /// [StreamSubscription.cancel]'s return value. Otherwise, it returns `null`. + Future? remove(Stream stream) { + var subscription = _subscriptions.remove(stream); + var future = subscription?.cancel(); + + if (_subscriptions.isEmpty) { + _onIdleController?.add(null); + if (_closed) { + _onIdleController?.close(); + scheduleMicrotask(_controller.close); + } + } + + return future; + } + + /// A callback called when [stream] is listened to. + /// + /// This is called for both single-subscription and broadcast groups. + void _onListen() { + _state = _StreamGroupState.listening; + + for (var entry in [..._subscriptions.entries]) { + // If this is a broadcast group and this isn't the first time it's been + // listened to, there may still be some subscriptions to + // single-subscription streams. + if (entry.value != null) continue; + + var stream = entry.key; + try { + _subscriptions[stream] = _listenToStream(stream); + } catch (error) { + // If [Stream.listen] throws a synchronous error (for example because + // the stream has already been listened to), cancel all subscriptions + // and rethrow the error. + _onCancel()?.catchError((_) {}); + rethrow; + } + } + } + + /// A callback called when [stream] is paused. + void _onPause() { + _state = _StreamGroupState.paused; + for (var subscription in _subscriptions.values) { + subscription!.pause(); + } + } + + /// A callback called when [stream] is resumed. + void _onResume() { + _state = _StreamGroupState.listening; + for (var subscription in _subscriptions.values) { + subscription!.resume(); + } + } + + /// A callback called when [stream] is canceled. + /// + /// This is only called for single-subscription groups. + Future? _onCancel() { + _state = _StreamGroupState.canceled; + + var futures = _subscriptions.entries + .map((entry) { + var subscription = entry.value; + try { + if (subscription != null) return subscription.cancel(); + return entry.key.listen(null).cancel(); + } catch (_) { + return null; + } + }) + .nonNulls + .toList(); + + _subscriptions.clear(); + + var onIdleController = _onIdleController; + if (onIdleController != null && !onIdleController.isClosed) { + onIdleController.add(null); + onIdleController.close(); + } + + return futures.isEmpty ? null : Future.wait(futures); + } + + /// A callback called when [stream]'s last listener is canceled. + /// + /// This is only called for broadcast groups. + void _onCancelBroadcast() { + _state = _StreamGroupState.dormant; + + _subscriptions.forEach((stream, subscription) { + // Cancel the broadcast streams, since we can re-listen to those later, + // but allow the single-subscription streams to keep firing. Their events + // will still be added to [_controller], but then they'll be dropped since + // it has no listeners. + if (!stream.isBroadcast) return; + subscription!.cancel(); + _subscriptions[stream] = null; + }); + } + + /// Starts actively forwarding events from [stream] to [_controller]. + /// + /// This will pause the resulting subscription if `this` is paused. + StreamSubscription _listenToStream(Stream stream) { + var subscription = stream.listen(_controller.add, + onError: _controller.addError, onDone: () => remove(stream)); + if (_state == _StreamGroupState.paused) subscription.pause(); + return subscription; + } + + /// Closes the group, indicating that no more streams will be added. + /// + /// If there are no streams in the group, [stream] is closed immediately. + /// Otherwise, [stream] will close once all streams in the group close. + /// + /// Returns a [Future] that completes once [stream] has actually been closed. + @override + Future close() { + if (_closed) return _controller.done; + + _closed = true; + if (_subscriptions.isEmpty) _controller.close(); + + return _controller.done; + } +} + +/// An enum of possible states of a [StreamGroup]. +class _StreamGroupState { + /// The group has no listeners. + /// + /// New streams added to the group will be listened once the group has a + /// listener. + static const dormant = _StreamGroupState('dormant'); + + /// The group has one or more listeners and is actively firing events. + /// + /// New streams added to the group will be immediately listeners. + static const listening = _StreamGroupState('listening'); + + /// The group is paused and no more events will be fired until it resumes. + /// + /// New streams added to the group will be listened to, but then paused. They + /// will be resumed once the group itself is resumed. + /// + /// This state is only used by single-subscriber groups. + static const paused = _StreamGroupState('paused'); + + /// The group is canceled and no more events will be fired ever. + /// + /// New streams added to the group will be listened to, canceled, and + /// discarded. + /// + /// This state is only used by single-subscriber groups. + static const canceled = _StreamGroupState('canceled'); + + /// The name of the state. + /// + /// Used for debugging. + final String name; + + const _StreamGroupState(this.name); + + @override + String toString() => name; +} diff --git a/pkgs/async/lib/src/stream_queue.dart b/pkgs/async/lib/src/stream_queue.dart new file mode 100644 index 00000000..c5c0c196 --- /dev/null +++ b/pkgs/async/lib/src/stream_queue.dart @@ -0,0 +1,961 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:collection'; + +import 'package:collection/collection.dart' show QueueList; + +import 'cancelable_operation.dart'; +import 'result/result.dart'; +import 'stream_completer.dart'; +import 'stream_splitter.dart'; +import 'subscription_stream.dart'; + +/// An asynchronous pull-based interface for accessing stream events. +/// +/// Wraps a stream and makes individual events available on request. +/// +/// You can request (and reserve) one or more events from the stream, +/// and after all previous requests have been fulfilled, stream events +/// go towards fulfilling your request. +/// +/// For example, if you ask for [next] two times, the returned futures +/// will be completed by the next two unrequested events from the stream. +/// +/// The stream subscription is paused when there are no active +/// requests. +/// +/// Some streams, including broadcast streams, will buffer +/// events while paused, so waiting too long between requests may +/// cause memory bloat somewhere else. +/// +/// This is similar to, but more convenient than, a [StreamIterator]. +/// A `StreamIterator` requires you to manually check when a new event is +/// available and you can only access the value of that event until you +/// check for the next one. A `StreamQueue` allows you to request, for example, +/// three events at a time, either individually, as a group using [take] +/// or [skip], or in any combination. +/// +/// You can also ask to have the [rest] of the stream provided as +/// a new stream. This allows, for example, taking the first event +/// out of a stream and continuing to use the rest of the stream as a stream. +/// +/// Example: +/// +/// var events = StreamQueue(someStreamOfLines); +/// var first = await events.next; +/// while (first.startsWith('#')) { +/// // Skip comments. +/// first = await events.next; +/// } +/// +/// if (first.startsWith(MAGIC_MARKER)) { +/// var headerCount = +/// first.parseInt(first.substring(MAGIC_MARKER.length + 1)); +/// handleMessage(headers: await events.take(headerCount), +/// body: events.rest); +/// return; +/// } +/// // Error handling. +/// +/// When you need no further events the `StreamQueue` should be closed +/// using [cancel]. This releases the underlying stream subscription. +class StreamQueue { + // This class maintains two queues: one of events and one of requests. + // The active request (the one in front of the queue) is called with + // the current event queue when it becomes active, every time a + // new event arrives, and when the event source closes. + // + // If the request returns `true`, it's complete and will be removed from the + // request queue. + // If the request returns `false`, it needs more events, and will be called + // again when new events are available. It may trigger a call itself by + // calling [_updateRequests]. + // The request can remove events that it uses, or keep them in the event + // queue until it has all that it needs. + // + // This model is very flexible and easily extensible. + // It allows requests that don't consume events (like [hasNext]) or + // potentially a request that takes either five or zero events, determined + // by the content of the fifth event. + + final Stream _source; + + /// Subscription on [_source] while listening for events. + /// + /// Set to subscription when listening, and set to `null` when the + /// subscription is done (and [_isDone] is set to true). + StreamSubscription? _subscription; + + /// Whether the event source is done. + bool _isDone = false; + + /// Whether a closing operation has been performed on the stream queue. + /// + /// Closing operations are [cancel] and [rest]. + bool _isClosed = false; + + /// The number of events dispatched by this queue. + /// + /// This counts error events. It doesn't count done events, or events + /// dispatched to a stream returned by [rest]. + int get eventsDispatched => _eventsReceived - _eventQueue.length; + + /// The number of events received by this queue. + var _eventsReceived = 0; + + /// Queue of events not used by a request yet. + final QueueList> _eventQueue = QueueList(); + + /// Queue of pending requests. + /// + /// Access through methods below to ensure consistency. + final Queue<_EventRequest> _requestQueue = Queue(); + + /// Create a `StreamQueue` of the events of [source]. + factory StreamQueue(Stream source) => StreamQueue._(source); + + // Private generative constructor to avoid subclasses. + StreamQueue._(this._source) { + // Start listening immediately if we could otherwise lose events. + if (_source.isBroadcast) { + _ensureListening(); + _pause(); + } + } + + /// Whether the stream has any more events. + /// + /// Returns a future that completes with `true` if the stream has any + /// more events, whether data or error. + /// If the stream closes without producing any more events, the returned + /// future completes with `false`. + /// + /// Can be used before using [next] to avoid getting an error in the + /// future returned by `next` in the case where there are no more events. + /// Another alternative is to use `take(1)` which returns either zero or + /// one events. + Future get hasNext { + _checkNotClosed(); + var hasNextRequest = _HasNextRequest(); + _addRequest(hasNextRequest); + return hasNextRequest.future; + } + + /// Look at the next [count] data events without consuming them. + /// + /// Works like [take] except that the events are left in the queue. + /// If one of the next [count] events is an error, the returned future + /// completes with this error, and the error is still left in the queue. + Future> lookAhead(int count) { + RangeError.checkNotNegative(count, 'count'); + _checkNotClosed(); + var request = _LookAheadRequest(count); + _addRequest(request); + return request.future; + } + + /// Requests the next (yet unrequested) event from the stream. + /// + /// When the requested event arrives, the returned future is completed with + /// the event. + /// If the event is a data event, the returned future completes + /// with its value. + /// If the event is an error event, the returned future completes with + /// its error and stack trace. + /// If the stream closes before an event arrives, the returned future + /// completes with a [StateError]. + /// + /// It's possible to have several pending [next] calls (or other requests), + /// and they will be completed in the order they were requested, by the + /// first events that were not consumed by previous requeusts. + Future get next { + _checkNotClosed(); + var nextRequest = _NextRequest(); + _addRequest(nextRequest); + return nextRequest.future; + } + + /// Looks at the next (yet unrequested) event from the stream. + /// + /// Like [next] except that the event is not consumed. + /// If the next event is an error event, it stays in the queue. + Future get peek { + _checkNotClosed(); + var nextRequest = _PeekRequest(); + _addRequest(nextRequest); + return nextRequest.future; + } + + /// A stream of all the remaning events of the source stream. + /// + /// All requested [next], [skip] or [take] operations are completed + /// first, and then any remaining events are provided as events of + /// the returned stream. + /// + /// Using `rest` closes this stream queue. After getting the + /// `rest` the caller may no longer request other events, like + /// after calling [cancel]. + Stream get rest { + _checkNotClosed(); + var request = _RestRequest(this); + _isClosed = true; + _addRequest(request); + return request.stream; + } + + /// Skips the next [count] *data* events. + /// + /// The [count] must be non-negative. + /// + /// When successful, this is equivalent to using [take] + /// and ignoring the result. + /// + /// If an error occurs before `count` data events have been skipped, + /// the returned future completes with that error instead. + /// + /// If the stream closes before `count` data events, + /// the remaining unskipped event count is returned. + /// If the returned future completes with the integer `0`, + /// then all events were succssfully skipped. If the value + /// is greater than zero then the stream ended early. + Future skip(int count) { + RangeError.checkNotNegative(count, 'count'); + _checkNotClosed(); + var request = _SkipRequest(count); + _addRequest(request); + return request.future; + } + + /// Requests the next [count] data events as a list. + /// + /// The [count] must be non-negative. + /// + /// Equivalent to calling [next] `count` times and + /// storing the data values in a list. + /// + /// If an error occurs before `count` data events has + /// been collected, the returned future completes with + /// that error instead. + /// + /// If the stream closes before `count` data events, + /// the returned future completes with the list + /// of data collected so far. That is, the returned + /// list may have fewer than [count] elements. + Future> take(int count) { + RangeError.checkNotNegative(count, 'count'); + _checkNotClosed(); + var request = _TakeRequest(count); + _addRequest(request); + return request.future; + } + + /// Requests a transaction that can conditionally consume events. + /// + /// The transaction can create copies of this queue at the current position + /// using [StreamQueueTransaction.newQueue]. Each of these queues is + /// independent of one another and of the parent queue. The transaction + /// finishes when one of two methods is called: + /// + /// * [StreamQueueTransaction.commit] updates the parent queue's position to + /// match that of one of the copies. + /// + /// * [StreamQueueTransaction.reject] causes the parent queue to continue as + /// though [startTransaction] hadn't been called. + /// + /// Until the transaction finishes, this queue won't emit any events. + /// + /// See also [withTransaction] and [cancelable]. + /// + /// ```dart + /// /// Consumes all empty lines from the beginning of [lines]. + /// Future consumeEmptyLines(StreamQueue lines) async { + /// while (await lines.hasNext) { + /// var transaction = lines.startTransaction(); + /// var queue = transaction.newQueue(); + /// if ((await queue.next).isNotEmpty) { + /// transaction.reject(); + /// return; + /// } else { + /// transaction.commit(queue); + /// } + /// } + /// } + /// ``` + StreamQueueTransaction startTransaction() { + _checkNotClosed(); + + var request = _TransactionRequest(this); + _addRequest(request); + return request.transaction; + } + + /// Passes a copy of this queue to [callback], and updates this queue to match + /// the copy's position if [callback] returns `true`. + /// + /// This queue won't emit any events until [callback] returns. If it returns + /// `false`, this queue continues as though [withTransaction] hadn't been + /// called. If it throws an error, this updates this queue to match the copy's + /// position and throws the error from the returned `Future`. + /// + /// Returns the same value as [callback]. + /// + /// See also [startTransaction] and [cancelable]. + /// + /// ```dart + /// /// Consumes all empty lines from the beginning of [lines]. + /// Future consumeEmptyLines(StreamQueue lines) async { + /// while (await lines.hasNext) { + /// // Consume a line if it's empty, otherwise return. + /// if (!await lines.withTransaction( + /// (queue) async => (await queue.next).isEmpty)) { + /// return; + /// } + /// } + /// } + /// ``` + Future withTransaction( + Future Function(StreamQueue) callback) async { + var transaction = startTransaction(); + + var queue = transaction.newQueue(); + bool result; + try { + result = await callback(queue); + } catch (_) { + transaction.commit(queue); + rethrow; + } + if (result) { + transaction.commit(queue); + } else { + transaction.reject(); + } + return result; + } + + /// Passes a copy of this queue to [callback], and updates this queue to match + /// the copy's position once [callback] completes. + /// + /// If the returned [CancelableOperation] is canceled, this queue instead + /// continues as though [cancelable] hadn't been called. Otherwise, it emits + /// the same value or error as [callback]. + /// + /// See also [startTransaction] and [withTransaction]. + /// + /// ```dart + /// final _stdinQueue = StreamQueue(stdin); + /// + /// /// Returns an operation that completes when the user sends a line to + /// /// standard input. + /// /// + /// /// If the operation is canceled, stops waiting for user input. + /// CancelableOperation nextStdinLine() => + /// _stdinQueue.cancelable((queue) => queue.next); + /// ``` + CancelableOperation cancelable( + Future Function(StreamQueue) callback) { + var transaction = startTransaction(); + var completer = CancelableCompleter(onCancel: () { + transaction.reject(); + }); + + var queue = transaction.newQueue(); + completer.complete(callback(queue).whenComplete(() { + if (!completer.isCanceled) transaction.commit(queue); + })); + + return completer.operation; + } + + /// Cancels the underlying event source. + /// + /// If [immediate] is `false` (the default), the cancel operation waits until + /// all previously requested events have been processed, then it cancels the + /// subscription providing the events. + /// + /// If [immediate] is `true`, the source is instead canceled + /// immediately. Any pending events are completed as though the underlying + /// stream had closed. + /// + /// The returned future completes with the result of calling + /// `cancel` on the subscription to the source stream. + /// + /// After calling `cancel`, no further events can be requested. + /// None of [lookAhead], [next], [peek], [rest], [skip], [take] or [cancel] + /// may be called again. + Future? cancel({bool immediate = false}) { + _checkNotClosed(); + _isClosed = true; + + if (!immediate) { + var request = _CancelRequest(this); + _addRequest(request); + return request.future; + } + + if (_isDone && _eventQueue.isEmpty) return Future.value(); + return _cancel(); + } + + // ------------------------------------------------------------------ + // Methods that may be called from the request implementations to + // control the event stream. + + /// Matches events with requests. + /// + /// Called after receiving an event or when the event source closes. + /// + /// May be called by requests which have returned `false` (saying they + /// are not yet done) so they can be checked again before any new + /// events arrive. + /// Any request returing `false` from `update` when `isDone` is `true` + /// *must* call `_updateRequests` when they are ready to continue + /// (since no further events will trigger the call). + void _updateRequests() { + while (_requestQueue.isNotEmpty) { + if (_requestQueue.first.update(_eventQueue, _isDone)) { + _requestQueue.removeFirst(); + } else { + return; + } + } + + if (!_isDone) { + _pause(); + } + } + + /// Extracts a stream from the event source and makes this stream queue + /// unusable. + /// + /// Can only be used by the very last request (the stream queue must + /// be closed by that request). + /// Only used by [rest]. + Stream _extractStream() { + assert(_isClosed); + if (_isDone) { + return Stream.empty(); + } + _isDone = true; + + var subscription = _subscription; + if (subscription == null) { + return _source; + } + _subscription = null; + + var wasPaused = subscription.isPaused; + var result = SubscriptionStream(subscription); + // Resume after creating stream because that pauses the subscription too. + // This way there won't be a short resumption in the middle. + if (wasPaused) subscription.resume(); + return result; + } + + /// Requests that the event source pauses events. + /// + /// This is called automatically when the request queue is empty. + /// + /// The event source is restarted by the next call to [_ensureListening]. + void _pause() { + _subscription!.pause(); + } + + /// Ensures that we are listening on events from the event source. + /// + /// Starts listening for the first time or resumes after a [_pause]. + /// + /// Is called automatically if a request requires more events. + void _ensureListening() { + if (_isDone) return; + if (_subscription == null) { + _subscription = _source.listen((data) { + _addResult(Result.value(data)); + }, onError: (Object error, StackTrace stackTrace) { + _addResult(Result.error(error, stackTrace)); + }, onDone: () { + _subscription = null; + _close(); + }); + } else { + _subscription!.resume(); + } + } + + /// Cancels the underlying event source. + Future? _cancel() { + if (_isDone) return null; + _subscription ??= _source.listen(null); + var future = _subscription!.cancel(); + _close(); + return future; + } + + // ------------------------------------------------------------------ + // Methods called by the event source to add events or say that it's + // done. + + /// Called when the event source adds a new data or error event. + /// Always calls [_updateRequests] after adding. + void _addResult(Result result) { + _eventsReceived++; + _eventQueue.add(result); + _updateRequests(); + } + + /// Called when the event source is done. + /// Always calls [_updateRequests] after adding. + void _close() { + _isDone = true; + _updateRequests(); + } + + // ------------------------------------------------------------------ + // Internal helper methods. + + /// Throws an error if [cancel] or [rest] have already been called. + void _checkNotClosed() { + if (_isClosed) throw StateError('Already cancelled'); + } + + /// Adds a new request to the queue. + /// + /// If the request queue is empty and the request can be completed + /// immediately, it skips the queue. + void _addRequest(_EventRequest request) { + if (_requestQueue.isEmpty) { + if (request.update(_eventQueue, _isDone)) return; + _ensureListening(); + } + _requestQueue.add(request); + } +} + +/// A transaction on a [StreamQueue], created by [StreamQueue.startTransaction]. +/// +/// Copies of the parent queue may be created using [newQueue]. Calling [commit] +/// moves the parent queue to a copy's position, and calling [reject] causes it +/// to continue as though [StreamQueue.startTransaction] was never called. +class StreamQueueTransaction { + /// The parent queue on which this transaction is active. + final StreamQueue _parent; + + /// The splitter that produces copies of the parent queue's stream. + final StreamSplitter _splitter; + + /// Queues created using [newQueue]. + final _queues = {}; + + /// Whether [commit] has been called. + var _committed = false; + + /// Whether [reject] has been called. + var _rejected = false; + + StreamQueueTransaction._(this._parent, Stream source) + : _splitter = StreamSplitter(source); + + /// Creates a new copy of the parent queue. + /// + /// This copy starts at the parent queue's position when + /// [StreamQueue.startTransaction] was called. Its position can be committed + /// to the parent queue using [commit]. + StreamQueue newQueue() { + var queue = StreamQueue(_splitter.split()); + _queues.add(queue); + return queue; + } + + /// Commits a queue created using [newQueue]. + /// + /// The parent queue's position is updated to be the same as [queue]'s. + /// Further requests on all queues created by this transaction, including + /// [queue], will complete as though `cancel` were called with `immediate: + /// true`. + /// + /// Throws a [StateError] if [commit] or [reject] have already been called, or + /// if there are pending requests on [queue]. + void commit(StreamQueue queue) { + _assertActive(); + if (!_queues.contains(queue)) { + throw ArgumentError("Queue doesn't belong to this transaction."); + } else if (queue._requestQueue.isNotEmpty) { + throw StateError("A queue with pending requests can't be committed."); + } + _committed = true; + + // Remove all events from the parent queue that were consumed by the + // child queue. + for (var j = 0; j < queue.eventsDispatched; j++) { + _parent._eventQueue.removeFirst(); + } + + _done(); + } + + /// Rejects this transaction without updating the parent queue. + /// + /// The parent will continue as though [StreamQueue.startTransaction] hadn't + /// been called. Further requests on all queues created by this transaction + /// will complete as though `cancel` were called with `immediate: true`. + /// + /// Throws a [StateError] if [commit] or [reject] have already been called. + void reject() { + _assertActive(); + _rejected = true; + _done(); + } + + // Cancels all [_queues], removes the [_TransactionRequest] from [_parent]'s + // request queue, and runs the next request. + void _done() { + _splitter.close(); + for (var queue in _queues) { + queue._cancel(); + } + // If this is the active request in the queue, mark it as finished. + var currentRequest = _parent._requestQueue.first; + if (currentRequest is _TransactionRequest && + currentRequest.transaction == this) { + _parent._requestQueue.removeFirst(); + _parent._updateRequests(); + } + } + + /// Throws a [StateError] if [commit] or [reject] has already been called. + void _assertActive() { + if (_committed) { + throw StateError('This transaction has already been accepted.'); + } else if (_rejected) { + throw StateError('This transaction has already been rejected.'); + } + } +} + +/// Request object that receives events when they arrive, until fulfilled. +/// +/// Each request that cannot be fulfilled immediately is represented by +/// an `_EventRequest` object in the request queue. +/// +/// Events from the source stream are sent to the first request in the +/// queue until it reports itself as `isComplete`. +/// +/// When the first request in the queue `isComplete`, either when becoming +/// the first request or after receiving an event, its `close` methods is +/// called. +/// +/// The `close` method is also called immediately when the source stream +/// is done. +abstract class _EventRequest { + /// Handle available events. + /// + /// The available events are provided as a queue. The `update` function + /// should only remove events from the front of the event queue, e.g., + /// using `removeFirst`. + /// + /// Returns `true` if the request is completed, or `false` if it needs + /// more events. + /// The call may keep events in the queue until the requeust is complete, + /// or it may remove them immediately. + /// + /// If the method returns true, the request is considered fulfilled, and + /// will never be called again. + /// + /// This method is called when a request reaches the front of the request + /// queue, and if it returns `false`, it's called again every time a new event + /// becomes available, or when the stream closes. + /// If the function returns `false` when the stream has already closed + /// ([isDone] is true), then the request must call + /// [StreamQueue._updateRequests] itself when it's ready to continue. + bool update(QueueList> events, bool isDone); +} + +/// Request for a [StreamQueue.next] call. +/// +/// Completes the returned future when receiving the first event, +/// and is then complete. +class _NextRequest implements _EventRequest { + /// Completer for the future returned by [StreamQueue.next]. + final _completer = Completer(); + + _NextRequest(); + + Future get future => _completer.future; + + @override + bool update(QueueList> events, bool isDone) { + if (events.isNotEmpty) { + events.removeFirst().complete(_completer); + return true; + } + if (isDone) { + _completer.completeError(StateError('No elements'), StackTrace.current); + return true; + } + return false; + } +} + +/// Request for a [StreamQueue.peek] call. +/// +/// Completes the returned future when receiving the first event, +/// and is then complete, but doesn't consume the event. +class _PeekRequest implements _EventRequest { + /// Completer for the future returned by [StreamQueue.next]. + final _completer = Completer(); + + _PeekRequest(); + + Future get future => _completer.future; + + @override + bool update(QueueList> events, bool isDone) { + if (events.isNotEmpty) { + events.first.complete(_completer); + return true; + } + if (isDone) { + _completer.completeError(StateError('No elements'), StackTrace.current); + return true; + } + return false; + } +} + +/// Request for a [StreamQueue.skip] call. +class _SkipRequest implements _EventRequest { + /// Completer for the future returned by the skip call. + final _completer = Completer(); + + /// Number of remaining events to skip. + /// + /// The request `isComplete` when the values reaches zero. + /// + /// Decremented when an event is seen. + /// Set to zero when an error is seen since errors abort the skip request. + int _eventsToSkip; + + _SkipRequest(this._eventsToSkip); + + /// The future completed when the correct number of events have been skipped. + Future get future => _completer.future; + + @override + bool update(QueueList> events, bool isDone) { + while (_eventsToSkip > 0) { + if (events.isEmpty) { + if (isDone) break; + return false; + } + _eventsToSkip--; + + var event = events.removeFirst(); + if (event.isError) { + _completer.completeError( + event.asError!.error, event.asError!.stackTrace); + return true; + } + } + _completer.complete(_eventsToSkip); + return true; + } +} + +/// Common superclass for [_TakeRequest] and [_LookAheadRequest]. +abstract class _ListRequest implements _EventRequest { + /// Completer for the future returned by the take call. + final _completer = Completer>(); + + /// List collecting events until enough have been seen. + final _list = []; + + /// Number of events to capture. + /// + /// The request `isComplete` when the length of [_list] reaches + /// this value. + final int _eventsToTake; + + _ListRequest(this._eventsToTake); + + /// The future completed when the correct number of events have been captured. + Future> get future => _completer.future; +} + +/// Request for a [StreamQueue.take] call. +class _TakeRequest extends _ListRequest { + _TakeRequest(super.eventsToTake); + + @override + bool update(QueueList> events, bool isDone) { + while (_list.length < _eventsToTake) { + if (events.isEmpty) { + if (isDone) break; + return false; + } + + var event = events.removeFirst(); + if (event.isError) { + event.asError!.complete(_completer); + return true; + } + _list.add(event.asValue!.value); + } + _completer.complete(_list); + return true; + } +} + +/// Request for a [StreamQueue.lookAhead] call. +class _LookAheadRequest extends _ListRequest { + _LookAheadRequest(super.eventsToTake); + + @override + bool update(QueueList> events, bool isDone) { + while (_list.length < _eventsToTake) { + if (events.length == _list.length) { + if (isDone) break; + return false; + } + var event = events.elementAt(_list.length); + if (event.isError) { + event.asError!.complete(_completer); + return true; + } + _list.add(event.asValue!.value); + } + _completer.complete(_list); + return true; + } +} + +/// Request for a [StreamQueue.cancel] call. +/// +/// The request needs no events, it just waits in the request queue +/// until all previous events are fulfilled, then it cancels the stream queue +/// source subscription. +class _CancelRequest implements _EventRequest { + /// Completer for the future returned by the `cancel` call. + final _completer = Completer(); + + /// When the event is completed, it needs to cancel the active subscription + /// of the `StreamQueue` object, if any. + final StreamQueue _streamQueue; + + _CancelRequest(this._streamQueue); + + /// The future completed when the cancel request is completed. + Future get future => _completer.future; + + @override + bool update(QueueList> events, bool isDone) { + if (_streamQueue._isDone) { + _completer.complete(); + } else { + _streamQueue._ensureListening(); + _completer.complete(_streamQueue._extractStream().listen(null).cancel()); + } + return true; + } +} + +/// Request for a [StreamQueue.rest] call. +/// +/// The request is always complete, it just waits in the request queue +/// until all previous events are fulfilled, then it takes over the +/// stream events subscription and creates a stream from it. +class _RestRequest implements _EventRequest { + /// Completer for the stream returned by the `rest` call. + final _completer = StreamCompleter(); + + /// The [StreamQueue] object that has this request queued. + /// + /// When the event is completed, it needs to cancel the active subscription + /// of the `StreamQueue` object, if any. + final StreamQueue _streamQueue; + + _RestRequest(this._streamQueue); + + /// The stream which will contain the remaining events of [_streamQueue]. + Stream get stream => _completer.stream; + + @override + bool update(QueueList> events, bool isDone) { + if (events.isEmpty) { + if (_streamQueue._isDone) { + _completer.setEmpty(); + } else { + _completer.setSourceStream(_streamQueue._extractStream()); + } + } else { + // There are prefetched events which needs to be added before the + // remaining stream. + var controller = StreamController(); + for (var event in events) { + event.addTo(controller); + } + controller + .addStream(_streamQueue._extractStream(), cancelOnError: false) + .whenComplete(controller.close); + _completer.setSourceStream(controller.stream); + } + return true; + } +} + +/// Request for a [StreamQueue.hasNext] call. +/// +/// Completes the [future] with `true` if it sees any event, +/// but doesn't consume the event. +/// If the request is closed without seeing an event, then +/// the [future] is completed with `false`. +class _HasNextRequest implements _EventRequest { + final _completer = Completer(); + + Future get future => _completer.future; + + @override + bool update(QueueList> events, bool isDone) { + if (events.isNotEmpty) { + _completer.complete(true); + return true; + } + if (isDone) { + _completer.complete(false); + return true; + } + return false; + } +} + +/// Request for a [StreamQueue.startTransaction] call. +/// +/// This request isn't complete until the user calls +/// [StreamQueueTransaction.commit] or [StreamQueueTransaction.reject], at which +/// point it manually removes itself from the request queue and calls +/// [StreamQueue._updateRequests]. +class _TransactionRequest implements _EventRequest { + /// The transaction created by this request. + late final StreamQueueTransaction transaction; + + /// The controller that passes events to [transaction]. + final _controller = StreamController(sync: true); + + /// The number of events passed to [_controller] so far. + var _eventsSent = 0; + + _TransactionRequest(StreamQueue parent) { + transaction = StreamQueueTransaction._(parent, _controller.stream); + } + + @override + bool update(QueueList> events, bool isDone) { + while (_eventsSent < events.length) { + events[_eventsSent++].addTo(_controller); + } + if (isDone && !_controller.isClosed) _controller.close(); + return transaction._committed || transaction._rejected; + } +} diff --git a/pkgs/async/lib/src/stream_sink_completer.dart b/pkgs/async/lib/src/stream_sink_completer.dart new file mode 100644 index 00000000..dc2f6df3 --- /dev/null +++ b/pkgs/async/lib/src/stream_sink_completer.dart @@ -0,0 +1,180 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'null_stream_sink.dart'; + +/// A [sink] where the destination is provided later. +/// +/// The [sink] is a normal sink that you can add events to to immediately, but +/// until [setDestinationSink] is called, the events will be buffered. +/// +/// The same effect can be achieved by using a [StreamController] and adding it +/// to the sink using [StreamConsumer.addStream] when the destination sink is +/// ready. This +/// class attempts to shortcut some of the overhead when possible. For example, +/// if the [sink] only has events added after the destination sink has been set, +/// those events are added directly to the sink. +class StreamSinkCompleter { + /// The sink for this completer. + /// + /// When a destination sink is provided, events that have been passed to the + /// sink will be forwarded to the destination. + /// + /// Events can be added to the sink either before or after a destination sink + /// is set. + final StreamSink sink = _CompleterSink(); + + /// Returns [sink] typed as a [_CompleterSink]. + _CompleterSink get _sink => sink as _CompleterSink; + + /// Convert a `Future` to a `StreamSink`. + /// + /// This creates a sink using a sink completer, and sets the destination sink + /// to the result of the future when the future completes. + /// + /// If the future completes with an error, the returned sink will instead + /// be closed. Its [StreamSink.done] future will contain the error. + static StreamSink fromFuture(Future> sinkFuture) { + var completer = StreamSinkCompleter(); + sinkFuture.then(completer.setDestinationSink, onError: completer.setError); + return completer.sink; + } + + /// Sets a sink as the destination for events from the [StreamSinkCompleter]'s + /// [sink]. + /// + /// The completer's [sink] will act exactly as [destinationSink]. + /// + /// If the destination sink is set before events are added to [sink], further + /// events are forwarded directly to [destinationSink]. + /// + /// If events are added to [sink] before setting the destination sink, they're + /// buffered until the destination is available. + /// + /// A destination sink may be set at most once. + /// + /// Either of [setDestinationSink] or [setError] may be called at most once. + /// Trying to call either of them again will fail. + void setDestinationSink(StreamSink destinationSink) { + if (_sink._destinationSink != null) { + throw StateError('Destination sink already set'); + } + _sink._setDestinationSink(destinationSink); + } + + /// Completes this to a closed sink whose [StreamSink.done] future emits + /// [error]. + /// + /// This is useful when the process of loading the sink fails. + /// + /// Either of [setDestinationSink] or [setError] may be called at most once. + /// Trying to call either of them again will fail. + void setError(Object error, [StackTrace? stackTrace]) { + setDestinationSink(NullStreamSink.error(error, stackTrace)); + } +} + +/// [StreamSink] completed by [StreamSinkCompleter]. +class _CompleterSink implements StreamSink { + /// Controller for an intermediate sink. + /// + /// Created if the user adds events to this sink before the destination sink + /// is set. + StreamController? _controller; + + /// Completer for [done]. + /// + /// Created if the user requests the [done] future before the destination sink + /// is set. + Completer? _doneCompleter; + + /// Destination sink for the events added to this sink. + /// + /// Set when [StreamSinkCompleter.setDestinationSink] is called. + StreamSink? _destinationSink; + + /// Whether events should be sent directly to [_destinationSink], as opposed + /// to going through [_controller]. + bool get _canSendDirectly => _controller == null && _destinationSink != null; + + @override + Future get done { + if (_doneCompleter != null) return _doneCompleter!.future; + if (_destinationSink == null) { + _doneCompleter = Completer.sync(); + return _doneCompleter!.future; + } + return _destinationSink!.done; + } + + @override + void add(T event) { + if (_canSendDirectly) { + _destinationSink!.add(event); + } else { + _ensureController().add(event); + } + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + if (_canSendDirectly) { + _destinationSink!.addError(error, stackTrace); + } else { + _ensureController().addError(error, stackTrace); + } + } + + @override + Future addStream(Stream stream) { + if (_canSendDirectly) return _destinationSink!.addStream(stream); + + return _ensureController().addStream(stream, cancelOnError: false); + } + + @override + Future close() { + if (_canSendDirectly) { + _destinationSink!.close(); + } else { + _ensureController().close(); + } + return done; + } + + /// Create [_controller] if it doesn't yet exist. + StreamController _ensureController() { + return _controller ??= StreamController(sync: true); + } + + /// Sets the destination sink to which events from this sink will be provided. + /// + /// If set before the user adds events, events will be added directly to the + /// destination sink. If the user adds events earlier, an intermediate sink is + /// created using a stream controller, and the destination sink is linked to + /// it later. + void _setDestinationSink(StreamSink sink) { + assert(_destinationSink == null); + _destinationSink = sink; + + // If the user has already added data, it's buffered in the controller, so + // we add it to the sink. + if (_controller != null) { + // Catch any error that may come from [addStream] or [sink.close]. They'll + // be reported through [done] anyway. + sink + .addStream(_controller!.stream) + .whenComplete(sink.close) + .catchError((_) {}); + } + + // If the user has already asked when the sink is done, connect the sink's + // done callback to that completer. + if (_doneCompleter != null) { + _doneCompleter!.complete(sink.done); + } + } +} diff --git a/pkgs/async/lib/src/stream_sink_extensions.dart b/pkgs/async/lib/src/stream_sink_extensions.dart new file mode 100644 index 00000000..a82cfe1c --- /dev/null +++ b/pkgs/async/lib/src/stream_sink_extensions.dart @@ -0,0 +1,22 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'stream_sink_transformer.dart'; +import 'stream_sink_transformer/reject_errors.dart'; + +/// Extensions on [StreamSink] to make stream transformations more fluent. +extension StreamSinkExtensions on StreamSink { + /// Transforms a [StreamSink] using [transformer]. + StreamSink transform(StreamSinkTransformer transformer) => + transformer.bind(this); + + /// Returns a [StreamSink] that forwards to `this` but rejects errors. + /// + /// If an error is passed (either by [addError] or [addStream]), the + /// underlying sink will be closed and the error will be forwarded to the + /// returned sink's [StreamSink.done] future. Further events will be ignored. + StreamSink rejectErrors() => RejectErrorsSink(this); +} diff --git a/pkgs/async/lib/src/stream_sink_transformer.dart b/pkgs/async/lib/src/stream_sink_transformer.dart new file mode 100644 index 00000000..c1ed7478 --- /dev/null +++ b/pkgs/async/lib/src/stream_sink_transformer.dart @@ -0,0 +1,63 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'stream_sink_transformer/handler_transformer.dart'; +import 'stream_sink_transformer/stream_transformer_wrapper.dart'; +import 'stream_sink_transformer/typed.dart'; + +/// A [StreamSinkTransformer] transforms the events being passed to a sink. +/// +/// This works on the same principle as a [StreamTransformer]. Each transformer +/// defines a [bind] method that takes in the original [StreamSink] and returns +/// the transformed version. However, where a [StreamTransformer] transforms +/// events after they leave the stream, this transforms them before they enter +/// the sink. +/// +/// Transformers must be able to have `bind` called used multiple times. +abstract class StreamSinkTransformer { + /// Creates a [StreamSinkTransformer] that transforms events and errors + /// using [transformer]. + /// + /// This is equivalent to piping all events from the outer sink through a + /// stream transformed by [transformer] and from there into the inner sink. + const factory StreamSinkTransformer.fromStreamTransformer( + StreamTransformer transformer) = StreamTransformerWrapper; + + /// Creates a [StreamSinkTransformer] that delegates events to the given + /// handlers. + /// + /// The handlers work exactly as they do for [StreamTransformer.fromHandlers]. + /// They're called for each incoming event, and any actions on the sink + /// they're passed are forwarded to the inner sink. If a handler is omitted, + /// the event is passed through unaltered. + factory StreamSinkTransformer.fromHandlers( + {void Function(S, EventSink)? handleData, + void Function(Object, StackTrace, EventSink)? handleError, + void Function(EventSink)? handleDone}) { + return HandlerTransformer(handleData, handleError, handleDone); + } + + /// Transforms the events passed to [sink]. + /// + /// Creates a new sink. When events are passed to the returned sink, it will + /// transform them and pass the transformed versions to [sink]. + StreamSink bind(StreamSink sink); + + /// Creates a wrapper that coerces the type of [transformer]. + /// + /// This soundly converts a [StreamSinkTransformer] to a + /// `StreamSinkTransformer`, regardless of its original generic type. + /// This means that calls to [StreamSink.add] on the returned sink may throw a + /// [TypeError] if the argument type doesn't match the reified type of the + /// sink. + @Deprecated('Will be removed in future version') + // TODO remove TypeSafeStreamSinkTransformer + static StreamSinkTransformer typed( + StreamSinkTransformer transformer) => + transformer is StreamSinkTransformer + ? transformer + : TypeSafeStreamSinkTransformer(transformer); +} diff --git a/pkgs/async/lib/src/stream_sink_transformer/handler_transformer.dart b/pkgs/async/lib/src/stream_sink_transformer/handler_transformer.dart new file mode 100644 index 00000000..496c7ca4 --- /dev/null +++ b/pkgs/async/lib/src/stream_sink_transformer/handler_transformer.dart @@ -0,0 +1,110 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import '../delegate/stream_sink.dart'; +import '../stream_sink_transformer.dart'; + +/// The type of the callback for handling data events. +typedef HandleData = void Function(S data, EventSink sink); + +/// The type of the callback for handling error events. +typedef HandleError = void Function(Object error, StackTrace, EventSink); + +/// The type of the callback for handling done events. +typedef HandleDone = void Function(EventSink sink); + +/// A [StreamSinkTransformer] that delegates events to the given handlers. +class HandlerTransformer implements StreamSinkTransformer { + /// The handler for data events. + final HandleData? _handleData; + + /// The handler for error events. + final HandleError? _handleError; + + /// The handler for done events. + final HandleDone? _handleDone; + + HandlerTransformer(this._handleData, this._handleError, this._handleDone); + + @override + StreamSink bind(StreamSink sink) => _HandlerSink(this, sink); +} + +/// A sink created by [HandlerTransformer]. +class _HandlerSink implements StreamSink { + /// The transformer that created this sink. + final HandlerTransformer _transformer; + + /// The original sink that's being transformed. + final StreamSink _inner; + + /// The wrapper for [_inner] whose [StreamSink.close] method can't emit + /// errors. + final StreamSink _safeCloseInner; + + @override + Future get done => _inner.done; + + _HandlerSink(this._transformer, StreamSink inner) + : _inner = inner, + _safeCloseInner = _SafeCloseSink(inner); + + @override + void add(S event) { + var handleData = _transformer._handleData; + if (handleData == null) { + _inner.add(event as T); + } else { + handleData(event, _safeCloseInner); + } + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + var handleError = _transformer._handleError; + if (handleError == null) { + _inner.addError(error, stackTrace); + } else { + handleError(error, stackTrace ?? AsyncError.defaultStackTrace(error), + _safeCloseInner); + } + } + + @override + Future addStream(Stream stream) { + return _inner.addStream(stream.transform( + StreamTransformer.fromHandlers( + handleData: _transformer._handleData, + handleError: _transformer._handleError, + handleDone: _closeSink))); + } + + @override + Future close() { + var handleDone = _transformer._handleDone; + if (handleDone == null) return _inner.close(); + + handleDone(_safeCloseInner); + return _inner.done; + } +} + +/// A wrapper for [StreamSink]s that swallows any errors returned by [close]. +/// +/// [HandlerTransformer] passes this to its handlers to ensure that when they +/// call [close], they don't leave any dangling [Future]s behind that might emit +/// unhandleable errors. +class _SafeCloseSink extends DelegatingStreamSink { + _SafeCloseSink(super.inner); + + @override + Future close() => super.close().catchError((_) {}); +} + +/// A function to pass as a [StreamTransformer]'s `handleDone` callback. +void _closeSink(EventSink sink) { + sink.close(); +} diff --git a/pkgs/async/lib/src/stream_sink_transformer/reject_errors.dart b/pkgs/async/lib/src/stream_sink_transformer/reject_errors.dart new file mode 100644 index 00000000..6d077f46 --- /dev/null +++ b/pkgs/async/lib/src/stream_sink_transformer/reject_errors.dart @@ -0,0 +1,130 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// A [StreamSink] wrapper that rejects all errors passed into the sink. +class RejectErrorsSink implements StreamSink { + /// The target sink. + final StreamSink _inner; + + @override + Future get done => _doneCompleter.future; + final _doneCompleter = Completer(); + + /// Whether the user has called [close]. + /// + /// If [_closed] is true, [_canceled] must be true and [_inAddStream] must be + /// false. + bool _closed = false; + + /// The subscription to the stream passed to [addStream], if a stream is + /// currently being added. + StreamSubscription? _addStreamSubscription; + + /// The completer for the future returned by [addStream], if a stream is + /// currently being added. + Completer? _addStreamCompleter; + + /// Whether we're currently adding a stream with [addStream]. + bool get _inAddStream => _addStreamSubscription != null; + + RejectErrorsSink(this._inner) { + _inner.done.then((value) { + _cancelAddStream(); + if (!_canceled) _doneCompleter.complete(value); + }).onError((error, stackTrace) { + _cancelAddStream(); + if (!_canceled) _doneCompleter.completeError(error, stackTrace); + }); + } + + /// Whether the underlying sink is no longer receiving events. + /// + /// This can happen if: + /// + /// * [close] has been called, + /// * an error has been passed, + /// * or the underlying [StreamSink.done] has completed. + /// + /// If [_canceled] is true, [_inAddStream] must be false. + bool get _canceled => _doneCompleter.isCompleted; + + @override + void add(T data) { + if (_closed) throw StateError('Cannot add event after closing.'); + if (_inAddStream) { + throw StateError('Cannot add event while adding stream.'); + } + if (_canceled) return; + + _inner.add(data); + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + if (_closed) throw StateError('Cannot add event after closing.'); + if (_inAddStream) { + throw StateError('Cannot add event while adding stream.'); + } + if (_canceled) return; + + _addError(error, stackTrace); + } + + /// Like [addError], but doesn't check to ensure that an error can be added. + /// + /// This is called from [addStream], so it shouldn't fail if a stream is being + /// added. + void _addError(Object error, [StackTrace? stackTrace]) { + _cancelAddStream(); + _doneCompleter.completeError(error, stackTrace); + + // Ignore errors from the inner sink. We're already surfacing one error, and + // if the user handles it we don't want them to have another top-level. + _inner.close().catchError((_) {}); + } + + @override + Future addStream(Stream stream) { + if (_closed) throw StateError('Cannot add stream after closing.'); + if (_inAddStream) { + throw StateError('Cannot add stream while adding stream.'); + } + if (_canceled) return Future.value(); + + var addStreamCompleter = _addStreamCompleter = Completer.sync(); + _addStreamSubscription = stream.listen(_inner.add, + onError: _addError, onDone: addStreamCompleter.complete); + return addStreamCompleter.future.then((_) { + _addStreamCompleter = null; + _addStreamSubscription = null; + }); + } + + @override + Future close() { + if (_inAddStream) { + throw StateError('Cannot close sink while adding stream.'); + } + + if (_closed) return done; + _closed = true; + + if (!_canceled) { + // ignore: void_checks + _doneCompleter.complete(_inner.close()); + } + return done; + } + + /// If an [addStream] call is active, cancel its subscription and complete its + /// completer. + void _cancelAddStream() { + if (!_inAddStream) return; + _addStreamCompleter!.complete(_addStreamSubscription!.cancel()); + _addStreamCompleter = null; + _addStreamSubscription = null; + } +} diff --git a/pkgs/async/lib/src/stream_sink_transformer/stream_transformer_wrapper.dart b/pkgs/async/lib/src/stream_sink_transformer/stream_transformer_wrapper.dart new file mode 100644 index 00000000..b30e8ad7 --- /dev/null +++ b/pkgs/async/lib/src/stream_sink_transformer/stream_transformer_wrapper.dart @@ -0,0 +1,65 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import '../stream_sink_transformer.dart'; + +/// A [StreamSinkTransformer] that wraps a pre-existing [StreamTransformer]. +class StreamTransformerWrapper implements StreamSinkTransformer { + /// The wrapped transformer. + final StreamTransformer _transformer; + + const StreamTransformerWrapper(this._transformer); + + @override + StreamSink bind(StreamSink sink) => + _StreamTransformerWrapperSink(_transformer, sink); +} + +/// A sink created by [StreamTransformerWrapper]. +class _StreamTransformerWrapperSink implements StreamSink { + /// The controller through which events are passed. + /// + /// This is used to create a stream that can be transformed by the wrapped + /// transformer. + final _controller = StreamController(sync: true); + + /// The original sink that's being transformed. + final StreamSink _inner; + + @override + Future get done => _inner.done; + + _StreamTransformerWrapperSink( + StreamTransformer transformer, this._inner) { + _controller.stream + .transform(transformer) + .listen(_inner.add, onError: _inner.addError, onDone: () { + // Ignore any errors that come from this call to [_inner.close]. The + // user can access them through [done] or the value returned from + // [this.close], and we don't want them to get top-leveled. + _inner.close().catchError((_) {}); + }); + } + + @override + void add(S event) { + _controller.add(event); + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + _controller.addError(error, stackTrace); + } + + @override + Future addStream(Stream stream) => _controller.addStream(stream); + + @override + Future close() { + _controller.close(); + return _inner.done; + } +} diff --git a/pkgs/async/lib/src/stream_sink_transformer/typed.dart b/pkgs/async/lib/src/stream_sink_transformer/typed.dart new file mode 100644 index 00000000..4743c949 --- /dev/null +++ b/pkgs/async/lib/src/stream_sink_transformer/typed.dart @@ -0,0 +1,20 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import '../stream_sink_transformer.dart'; + +/// A wrapper that coerces the generic type of the sink returned by an inner +/// transformer to `S`. +class TypeSafeStreamSinkTransformer + implements StreamSinkTransformer { + final StreamSinkTransformer _inner; + + TypeSafeStreamSinkTransformer(this._inner); + + @override + StreamSink bind(StreamSink sink) => StreamController(sync: true) + ..stream.cast().pipe(_inner.bind(sink)); +} diff --git a/pkgs/async/lib/src/stream_splitter.dart b/pkgs/async/lib/src/stream_splitter.dart new file mode 100644 index 00000000..f7377d67 --- /dev/null +++ b/pkgs/async/lib/src/stream_splitter.dart @@ -0,0 +1,207 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'future_group.dart'; +import 'result/result.dart'; + +/// A class that splits a single source stream into an arbitrary number of +/// (single-subscription) streams (called "branch") that emit the same events. +/// +/// Each branch will emit all the same values and errors as the source stream, +/// regardless of which values have been emitted on other branches. This means +/// that the splitter stores every event that has been emitted so far, which may +/// consume a lot of memory. The user can call [close] to indicate that no more +/// branches will be created, and this memory will be released. +/// +/// The source stream is only listened to once a branch is created *and listened +/// to*. It's paused when all branches are paused *or when all branches are +/// canceled*, and resumed once there's at least one branch that's listening and +/// unpaused. It's not canceled unless no branches are listening and [close] has +/// been called. +class StreamSplitter { + /// The wrapped stream. + final Stream _stream; + + /// The subscription to [_stream]. + /// + /// This will be `null` until a branch has a listener. + StreamSubscription? _subscription; + + /// The buffer of events or errors that have already been emitted by + /// [_stream]. + final _buffer = >[]; + + /// The controllers for branches that are listening for future events from + /// [_stream]. + /// + /// Once a branch is canceled, it's removed from this list. When [_stream] is + /// done, all branches are removed. + final _controllers = >{}; + + /// A group of futures returned by [close]. + /// + /// This is used to ensure that [close] doesn't complete until all + /// [StreamController.close] and [StreamSubscription.cancel] calls complete. + final _closeGroup = FutureGroup(); + + /// Whether [_stream] is done emitting events. + var _isDone = false; + + /// Whether [close] has been called. + var _isClosed = false; + + /// Splits [stream] into [count] identical streams. + /// + /// [count] defaults to 2. This is the same as creating [count] branches and + /// then closing the [StreamSplitter]. + static List> splitFrom(Stream stream, [int? count]) { + count ??= 2; + var splitter = StreamSplitter(stream); + var streams = List>.generate(count, (_) => splitter.split()); + splitter.close(); + return streams; + } + + StreamSplitter(this._stream); + + /// Returns a single-subscription stream that's a copy of the input stream. + /// + /// This will throw a [StateError] if [close] has been called. + Stream split() { + if (_isClosed) { + throw StateError("Can't call split() on a closed StreamSplitter."); + } + + var controller = StreamController( + onListen: _onListen, onPause: _onPause, onResume: _onResume); + controller.onCancel = () => _onCancel(controller); + + for (var result in _buffer) { + result.addTo(controller); + } + + if (_isDone) { + _closeGroup.add(controller.close()); + } else { + _controllers.add(controller); + } + + return controller.stream; + } + + /// Indicates that no more branches will be requested via [split]. + /// + /// This clears the internal buffer of events. If there are no branches or all + /// branches have been canceled, this cancels the subscription to the input + /// stream. + /// + /// Returns a [Future] that completes once all events have been processed by + /// all branches and (if applicable) the subscription to the input stream has + /// been canceled. + Future close() { + if (_isClosed) return _closeGroup.future; + _isClosed = true; + + _buffer.clear(); + if (_controllers.isEmpty) _cancelSubscription(); + + return _closeGroup.future; + } + + /// Cancel [_subscription] and close [_closeGroup]. + /// + /// This should be called after all the branches' subscriptions have been + /// canceled and the splitter has been closed. In that case, we won't use the + /// events from [_subscription] any more, since there's nothing to pipe them + /// to and no more branches will be created. If [_subscription] is done, + /// canceling it will be a no-op. + /// + /// This may also be called before any branches have been created, in which + /// case [_subscription] will be `null`. + void _cancelSubscription() { + assert(_controllers.isEmpty); + assert(_isClosed); + + Future? future; + if (_subscription != null) future = _subscription!.cancel(); + if (future != null) _closeGroup.add(future); + _closeGroup.close(); + } + + // StreamController events + + /// Subscribe to [_stream] if we haven't yet done so, and resume the + /// subscription if we have. + void _onListen() { + if (_isDone) return; + + if (_subscription != null) { + // Resume the subscription in case it was paused, either because all the + // controllers were paused or because the last one was canceled. If it + // wasn't paused, this will be a no-op. + _subscription!.resume(); + } else { + _subscription = + _stream.listen(_onData, onError: _onError, onDone: _onDone); + } + } + + /// Pauses [_subscription] if every controller is paused. + void _onPause() { + if (!_controllers.every((controller) => controller.isPaused)) return; + _subscription!.pause(); + } + + /// Resumes [_subscription]. + /// + /// If [_subscription] wasn't paused, this is a no-op. + void _onResume() { + _subscription!.resume(); + } + + /// Removes [controller] from [_controllers] and cancels or pauses + /// [_subscription] as appropriate. + /// + /// Since the controller emitting a done event will cause it to register as + /// canceled, this is the only way that a controller is ever removed from + /// [_controllers]. + void _onCancel(StreamController controller) { + _controllers.remove(controller); + if (_controllers.isNotEmpty) return; + + if (_isClosed) { + _cancelSubscription(); + } else { + _subscription!.pause(); + } + } + + // Stream events + + /// Buffers [data] and passes it to [_controllers]. + void _onData(T data) { + if (!_isClosed) _buffer.add(Result.value(data)); + for (var controller in _controllers) { + controller.add(data); + } + } + + /// Buffers [error] and passes it to [_controllers]. + void _onError(Object error, StackTrace stackTrace) { + if (!_isClosed) _buffer.add(Result.error(error, stackTrace)); + for (var controller in _controllers) { + controller.addError(error, stackTrace); + } + } + + /// Marks [_controllers] as done. + void _onDone() { + _isDone = true; + for (var controller in _controllers) { + _closeGroup.add(controller.close()); + } + } +} diff --git a/pkgs/async/lib/src/stream_subscription_transformer.dart b/pkgs/async/lib/src/stream_subscription_transformer.dart new file mode 100644 index 00000000..d03ea700 --- /dev/null +++ b/pkgs/async/lib/src/stream_subscription_transformer.dart @@ -0,0 +1,114 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'async_memoizer.dart'; + +typedef _AsyncHandler = Future Function(StreamSubscription inner); + +typedef _VoidHandler = void Function(StreamSubscription inner); + +/// Creates a [StreamTransformer] that modifies the behavior of subscriptions to +/// a stream. +/// +/// When [StreamSubscription.cancel], [StreamSubscription.pause], or +/// [StreamSubscription.resume] is called, the corresponding handler is invoked. +/// By default, handlers just forward to the underlying subscription. +/// +/// Guarantees that none of the [StreamSubscription] callbacks and none of the +/// callbacks passed to `subscriptionTransformer()` will be invoked once the +/// transformed [StreamSubscription] has been canceled and `handleCancel()` has +/// run. The [handlePause] and [handleResume] are invoked regardless of whether +/// the subscription is paused already or not. +/// +/// In order to preserve [StreamSubscription] guarantees, **all callbacks must +/// synchronously call the corresponding method** on the inner +/// [StreamSubscription]: [handleCancel] must call `cancel()`, [handlePause] +/// must call `pause()`, and [handleResume] must call `resume()`. +StreamTransformer subscriptionTransformer( + {Future Function(StreamSubscription)? handleCancel, + void Function(StreamSubscription)? handlePause, + void Function(StreamSubscription)? handleResume}) { + return StreamTransformer((stream, cancelOnError) { + return _TransformedSubscription( + stream.listen(null, cancelOnError: cancelOnError), + handleCancel ?? (inner) => inner.cancel(), + handlePause ?? + (inner) { + inner.pause(); + }, + handleResume ?? + (inner) { + inner.resume(); + }); + }); +} + +/// A [StreamSubscription] wrapper that calls callbacks for subscription +/// methods. +class _TransformedSubscription implements StreamSubscription { + /// The wrapped subscription. + StreamSubscription? _inner; + + /// The callback to run when [cancel] is called. + final _AsyncHandler _handleCancel; + + /// The callback to run when [pause] is called. + final _VoidHandler _handlePause; + + /// The callback to run when [resume] is called. + final _VoidHandler _handleResume; + + @override + bool get isPaused => _inner?.isPaused ?? false; + + _TransformedSubscription( + this._inner, this._handleCancel, this._handlePause, this._handleResume); + + @override + void onData(void Function(T)? handleData) { + _inner?.onData(handleData); + } + + @override + void onError(Function? handleError) { + _inner?.onError(handleError); + } + + @override + void onDone(void Function()? handleDone) { + _inner?.onDone(handleDone); + } + + @override + Future cancel() => _cancelMemoizer.runOnce(() { + var inner = _inner!; + inner.onData(null); + inner.onDone(null); + + // Setting onError to null will cause errors to be top-leveled. + inner.onError((_, __) {}); + _inner = null; + return _handleCancel(inner); + }); + final _cancelMemoizer = AsyncMemoizer(); + + @override + void pause([Future? resumeFuture]) { + if (_cancelMemoizer.hasRun) return; + if (resumeFuture != null) resumeFuture.whenComplete(resume); + _handlePause(_inner!); + } + + @override + void resume() { + if (_cancelMemoizer.hasRun) return; + _handleResume(_inner!); + } + + @override + Future asFuture([E? futureValue]) => + _inner?.asFuture(futureValue) ?? Completer().future; +} diff --git a/pkgs/async/lib/src/stream_zip.dart b/pkgs/async/lib/src/stream_zip.dart new file mode 100644 index 00000000..f5b8296d --- /dev/null +++ b/pkgs/async/lib/src/stream_zip.dart @@ -0,0 +1,114 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// A stream that combines the values of other streams. +/// +/// This emits lists of collected values from each input stream. The first list +/// contains the first value emitted by each stream, the second contains the +/// second value, and so on. The lists have the same ordering as the iterable +/// passed to [StreamZip.new]. +/// +/// Any errors from any of the streams are forwarded directly to this stream. +class StreamZip extends Stream> { + final Iterable> _streams; + + StreamZip(Iterable> streams) : _streams = streams; + + @override + StreamSubscription> listen(void Function(List)? onData, + {Function? onError, void Function()? onDone, bool? cancelOnError}) { + cancelOnError = identical(true, cancelOnError); + var subscriptions = >[]; + late StreamController> controller; + late List current; + var dataCount = 0; + + /// Called for each data from a subscription in [subscriptions]. + void handleData(int index, T data) { + current[index] = data; + dataCount++; + if (dataCount == subscriptions.length) { + var data = List.from(current); + current = List.filled(subscriptions.length, null); + dataCount = 0; + for (var i = 0; i < subscriptions.length; i++) { + if (i != index) subscriptions[i].resume(); + } + controller.add(data); + } else { + subscriptions[index].pause(); + } + } + + /// Called for each error from a subscription in [subscriptions]. + /// Except if [cancelOnError] is true, in which case the function below + /// is used instead. + void handleError(Object error, StackTrace stackTrace) { + controller.addError(error, stackTrace); + } + + /// Called when a subscription has an error and [cancelOnError] is true. + /// + /// Prematurely cancels all subscriptions since we know that we won't + /// be needing any more values. + void handleErrorCancel(Object error, StackTrace stackTrace) { + for (var i = 0; i < subscriptions.length; i++) { + subscriptions[i].cancel(); + } + controller.addError(error, stackTrace); + } + + void handleDone() { + for (var i = 0; i < subscriptions.length; i++) { + subscriptions[i].cancel(); + } + controller.close(); + } + + try { + for (var stream in _streams) { + var index = subscriptions.length; + subscriptions.add(stream.listen((data) { + handleData(index, data); + }, + onError: cancelOnError ? handleError : handleErrorCancel, + onDone: handleDone, + cancelOnError: cancelOnError)); + } + } catch (e) { + for (var i = subscriptions.length - 1; i >= 0; i--) { + subscriptions[i].cancel(); + } + rethrow; + } + + current = List.filled(subscriptions.length, null); + + controller = StreamController>(onPause: () { + for (var i = 0; i < subscriptions.length; i++) { + // This may pause some subscriptions more than once. + // These will not be resumed by onResume below, but must wait for the + // next round. + subscriptions[i].pause(); + } + }, onResume: () { + for (var i = 0; i < subscriptions.length; i++) { + subscriptions[i].resume(); + } + }, onCancel: () { + for (var i = 0; i < subscriptions.length; i++) { + // Canceling more than once is safe. + subscriptions[i].cancel(); + } + }); + + if (subscriptions.isEmpty) { + controller.close(); + } + return controller.stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } +} diff --git a/pkgs/async/lib/src/subscription_stream.dart b/pkgs/async/lib/src/subscription_stream.dart new file mode 100644 index 00000000..9ce4942c --- /dev/null +++ b/pkgs/async/lib/src/subscription_stream.dart @@ -0,0 +1,87 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'delegate/stream_subscription.dart'; + +/// A [Stream] adapter for a [StreamSubscription]. +/// +/// This class allows a `StreamSubscription` to be treated as a `Stream`. +/// +/// The subscription is paused until the stream is listened to, +/// then it is resumed and the events are passed on to the +/// stream's new subscription. +/// +/// This class assumes that it has control over the original subscription. +/// If other code is accessing the subscription, results may be unpredictable. +class SubscriptionStream extends Stream { + /// The subscription providing the events for this stream. + StreamSubscription? _source; + + /// Create a single-subscription `Stream` from [subscription]. + /// + /// The `subscription` should not be paused. This class will not resume prior + /// pauses, so being paused is indistinguishable from not providing any + /// events. + /// + /// If the `subscription` doesn't send any `done` events, neither will this + /// stream. That may be an issue if `subscription` was made to cancel on + /// an error. + SubscriptionStream(StreamSubscription subscription) + : _source = subscription { + var source = _source!; + source.pause(); + // Clear callbacks to avoid keeping them alive unnecessarily. + source.onData(null); + source.onError(null); + source.onDone(null); + } + + @override + StreamSubscription listen(void Function(T)? onData, + {Function? onError, void Function()? onDone, bool? cancelOnError}) { + var subscription = _source; + if (subscription == null) { + throw StateError('Stream has already been listened to.'); + } + cancelOnError = (true == cancelOnError); + _source = null; + + var result = cancelOnError + ? _CancelOnErrorSubscriptionWrapper(subscription) + : subscription; + result.onData(onData); + result.onError(onError); + result.onDone(onDone); + subscription.resume(); + return result; + } +} + +/// Subscription wrapper that cancels on error. +/// +/// Used by [SubscriptionStream] when forwarding a subscription +/// created with `cancelOnError` as `true` to one with (assumed) +/// `cancelOnError` as `false`. It automatically cancels the +/// source subscription on the first error. +class _CancelOnErrorSubscriptionWrapper + extends DelegatingStreamSubscription { + _CancelOnErrorSubscriptionWrapper(super.subscription); + + @override + void onError(Function? handleError) { + // Cancel when receiving an error. + super.onError((Object error, StackTrace stackTrace) { + // Wait for the cancel to complete before sending the error event. + super.cancel().whenComplete(() { + if (handleError is ZoneBinaryCallback) { + handleError(error, stackTrace); + } else if (handleError != null) { + (handleError as ZoneUnaryCallback)(error); + } + }); + }); + } +} diff --git a/pkgs/async/lib/src/typed/stream_subscription.dart b/pkgs/async/lib/src/typed/stream_subscription.dart new file mode 100644 index 00000000..fe91656c --- /dev/null +++ b/pkgs/async/lib/src/typed/stream_subscription.dart @@ -0,0 +1,47 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +class TypeSafeStreamSubscription implements StreamSubscription { + final StreamSubscription _subscription; + + @override + bool get isPaused => _subscription.isPaused; + + TypeSafeStreamSubscription(this._subscription); + + @override + void onData(void Function(T)? handleData) { + if (handleData == null) return _subscription.onData(null); + _subscription.onData((data) => handleData(data as T)); + } + + @override + void onError(Function? handleError) { + _subscription.onError(handleError); + } + + @override + void onDone(void Function()? handleDone) { + _subscription.onDone(handleDone); + } + + @override + void pause([Future? resumeFuture]) { + _subscription.pause(resumeFuture); + } + + @override + void resume() { + _subscription.resume(); + } + + @override + Future cancel() => _subscription.cancel(); + + @override + Future asFuture([E? futureValue]) => + _subscription.asFuture(futureValue); +} diff --git a/pkgs/async/lib/src/typed_stream_transformer.dart b/pkgs/async/lib/src/typed_stream_transformer.dart new file mode 100644 index 00000000..8a392287 --- /dev/null +++ b/pkgs/async/lib/src/typed_stream_transformer.dart @@ -0,0 +1,29 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +/// Creates a wrapper that coerces the type of [transformer]. +/// +/// This soundly converts a [StreamTransformer] to a `StreamTransformer`, +/// regardless of its original generic type, by asserting that the events +/// emitted by the transformed stream are instances of `T` whenever they're +/// provided. If they're not, the stream throws a [TypeError]. +@Deprecated('Use Stream.cast after binding a transformer instead') +StreamTransformer typedStreamTransformer( + StreamTransformer transformer) => + transformer is StreamTransformer + ? transformer + : _TypeSafeStreamTransformer(transformer); + +/// A wrapper that coerces the type of the stream returned by an inner +/// transformer. +class _TypeSafeStreamTransformer extends StreamTransformerBase { + final StreamTransformer _inner; + + _TypeSafeStreamTransformer(this._inner); + + @override + Stream bind(Stream stream) => _inner.bind(stream).cast(); +} diff --git a/pkgs/async/pubspec.yaml b/pkgs/async/pubspec.yaml new file mode 100644 index 00000000..73c26c53 --- /dev/null +++ b/pkgs/async/pubspec.yaml @@ -0,0 +1,20 @@ +name: async +version: 2.12.0 +description: Utility functions and classes related to the 'dart:async' library. +repository: https://github.com/dart-lang/core/tree/main/pkgs/async + +topics: + - async + +environment: + sdk: ^3.4.0 + +dependencies: + collection: ^1.15.0 + meta: ^1.3.0 + +dev_dependencies: + dart_flutter_team_lints: ^3.0.0 + fake_async: ^1.2.0 + stack_trace: ^1.10.0 + test: ^1.16.6 diff --git a/pkgs/async/test/async_cache_test.dart b/pkgs/async/test/async_cache_test.dart new file mode 100644 index 00000000..f7c8caa6 --- /dev/null +++ b/pkgs/async/test/async_cache_test.dart @@ -0,0 +1,189 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: deprecated_member_use_from_same_package + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:fake_async/fake_async.dart'; +import 'package:test/test.dart'; + +void main() { + late AsyncCache cache; + + setUp(() { + // Create a cache that is fresh for an hour. + cache = AsyncCache(const Duration(hours: 1)); + }); + + test('should fetch via a callback when no cache exists', () async { + expect(await cache.fetch(() async => 'Expensive'), 'Expensive'); + }); + + test('should not fetch via callback when a cache exists', () async { + await cache.fetch(() async => 'Expensive'); + expect(await cache.fetch(expectAsync0(() async => 'fake', count: 0)), + 'Expensive'); + }); + + group('ephemeral cache', () { + test('should not fetch via callback when a future is in-flight', () async { + // No actual caching is done, just avoid duplicate requests. + cache = AsyncCache.ephemeral(); + + var completer = Completer(); + expect(cache.fetch(() => completer.future), completion('Expensive')); + expect(cache.fetch(expectAsync0(() async => 'fake', count: 0)), + completion('Expensive')); + completer.complete('Expensive'); + }); + + test('should fetch via callback when the in-flight future completes', + () async { + // No actual caching is done, just avoid duplicate requests. + cache = AsyncCache.ephemeral(); + + var fetched = cache.fetch(() async => 'first'); + expect(fetched, completion('first')); + expect( + cache.fetch(expectAsync0(() async => fail('not called'), count: 0)), + completion('first')); + await fetched; + expect(cache.fetch(() async => 'second'), completion('second')); + }); + + test('should invalidate even if the future throws an exception', () async { + cache = AsyncCache.ephemeral(); + + Future throwingCall() async => throw Exception(); + await expectLater(cache.fetch(throwingCall), throwsA(isException)); + // To let the timer invalidate the cache + await Future.delayed(const Duration(milliseconds: 5)); + + Future call() async => 'Completed'; + expect(await cache.fetch(call), 'Completed', reason: 'Cache invalidates'); + }); + }); + + test('should fetch via a callback again when cache expires', () { + FakeAsync().run((fakeAsync) async { + var timesCalled = 0; + Future call() async => 'Called ${++timesCalled}'; + expect(await cache.fetch(call), 'Called 1'); + expect(await cache.fetch(call), 'Called 1', reason: 'Cache still fresh'); + + fakeAsync.elapse(const Duration(hours: 1) - const Duration(seconds: 1)); + expect(await cache.fetch(call), 'Called 1', reason: 'Cache still fresh'); + + fakeAsync.elapse(const Duration(seconds: 1)); + expect(await cache.fetch(call), 'Called 2'); + expect(await cache.fetch(call), 'Called 2', reason: 'Cache fresh again'); + + fakeAsync.elapse(const Duration(hours: 1)); + expect(await cache.fetch(call), 'Called 3'); + }); + }); + + test('should fetch via a callback when manually invalidated', () async { + var timesCalled = 0; + Future call() async => 'Called ${++timesCalled}'; + expect(await cache.fetch(call), 'Called 1'); + cache.invalidate(); + expect(await cache.fetch(call), 'Called 2'); + cache.invalidate(); + expect(await cache.fetch(call), 'Called 3'); + }); + + test('should fetch a stream via a callback', () async { + expect( + await cache.fetchStream(expectAsync0(() { + return Stream.fromIterable(['1', '2', '3']); + })).toList(), + ['1', '2', '3']); + }); + + test('should not fetch stream via callback when a cache exists', () async { + await cache.fetchStream(() async* { + yield '1'; + yield '2'; + yield '3'; + }).toList(); + expect( + await cache.fetchStream(expectAsync0(Stream.empty, count: 0)).toList(), + ['1', '2', '3']); + }); + + test('should not fetch stream via callback when request in flight', () async { + // Unlike the above test, we want to verify that we don't make multiple + // calls if a cache is being filled currently, and instead wait for that + // cache to be completed. + var controller = StreamController(); + Stream call() => controller.stream; + expect(cache.fetchStream(call).toList(), completion(['1', '2', '3'])); + controller.add('1'); + controller.add('2'); + await Future.value(); + expect(cache.fetchStream(call).toList(), completion(['1', '2', '3'])); + controller.add('3'); + await controller.close(); + }); + + test('should fetch stream via a callback again when cache expires', () { + FakeAsync().run((fakeAsync) async { + var timesCalled = 0; + Stream call() { + return Stream.fromIterable(['Called ${++timesCalled}']); + } + + expect(await cache.fetchStream(call).toList(), ['Called 1']); + expect(await cache.fetchStream(call).toList(), ['Called 1'], + reason: 'Cache still fresh'); + + fakeAsync.elapse(const Duration(hours: 1) - const Duration(seconds: 1)); + expect(await cache.fetchStream(call).toList(), ['Called 1'], + reason: 'Cache still fresh'); + + fakeAsync.elapse(const Duration(seconds: 1)); + expect(await cache.fetchStream(call).toList(), ['Called 2']); + expect(await cache.fetchStream(call).toList(), ['Called 2'], + reason: 'Cache fresh again'); + + fakeAsync.elapse(const Duration(hours: 1)); + expect(await cache.fetchStream(call).toList(), ['Called 3']); + }); + }); + + test('should fetch via a callback when manually invalidated', () async { + var timesCalled = 0; + Stream call() { + return Stream.fromIterable(['Called ${++timesCalled}']); + } + + expect(await cache.fetchStream(call).toList(), ['Called 1']); + cache.invalidate(); + expect(await cache.fetchStream(call).toList(), ['Called 2']); + cache.invalidate(); + expect(await cache.fetchStream(call).toList(), ['Called 3']); + }); + + test('should cancel a cached stream without affecting others', () async { + Stream call() => Stream.fromIterable(['1', '2', '3']); + + expect(cache.fetchStream(call).toList(), completion(['1', '2', '3'])); + + // Listens to the stream for the initial value, then cancels subscription. + expect(await cache.fetchStream(call).first, '1'); + }); + + test('should pause a cached stream without affecting others', () async { + Stream call() => Stream.fromIterable(['1', '2', '3']); + + late StreamSubscription sub; + sub = cache.fetchStream(call).listen(expectAsync1((event) { + if (event == '1') sub.pause(); + })); + expect(cache.fetchStream(call).toList(), completion(['1', '2', '3'])); + }); +} diff --git a/pkgs/async/test/async_memoizer_test.dart b/pkgs/async/test/async_memoizer_test.dart new file mode 100644 index 00000000..490b389d --- /dev/null +++ b/pkgs/async/test/async_memoizer_test.dart @@ -0,0 +1,38 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +void main() { + late AsyncMemoizer cache; + setUp(() => cache = AsyncMemoizer()); + + test('runs the function only the first time runOnce() is called', () async { + var count = 0; + expect(await cache.runOnce(() => count++), equals(0)); + expect(count, equals(1)); + + expect(await cache.runOnce(() => count++), equals(0)); + expect(count, equals(1)); + }); + + test('forwards the return value from the function', () async { + expect(cache.future, completion(equals('value'))); + expect(cache.runOnce(() => 'value'), completion(equals('value'))); + expect(cache.runOnce(() {}), completion(equals('value'))); + }); + + test('forwards the return value from an async function', () async { + expect(cache.future, completion(equals('value'))); + expect(cache.runOnce(() async => 'value'), completion(equals('value'))); + expect(cache.runOnce(() {}), completion(equals('value'))); + }); + + test('forwards the error from an async function', () async { + expect(cache.future, throwsA('error')); + expect(cache.runOnce(() async => throw 'error'), throwsA('error')); + expect(cache.runOnce(() {}), throwsA('error')); + }); +} diff --git a/pkgs/async/test/byte_collection_test.dart b/pkgs/async/test/byte_collection_test.dart new file mode 100644 index 00000000..67f319b0 --- /dev/null +++ b/pkgs/async/test/byte_collection_test.dart @@ -0,0 +1,90 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +void main() { + group('collectBytes', () { + test('simple list and overflow', () { + var result = collectBytes(Stream.fromIterable([ + [0], + [1], + [2], + [256] + ])); + expect(result, completion([0, 1, 2, 0])); + }); + + test('no events', () { + var result = collectBytes(Stream.fromIterable([])); + expect(result, completion([])); + }); + + test('empty events', () { + var result = collectBytes(Stream.fromIterable([[], []])); + expect(result, completion([])); + }); + + test('error event', () { + var result = collectBytes(Stream.fromIterable( + Iterable.generate(3, (n) => n == 2 ? throw 'badness' : [n]))); + expect(result, throwsA('badness')); + }); + }); + + group('collectBytes', () { + test('simple list and overflow', () { + var result = collectBytesCancelable(Stream.fromIterable([ + [0], + [1], + [2], + [256] + ])); + expect(result.value, completion([0, 1, 2, 0])); + }); + + test('no events', () { + var result = collectBytesCancelable(Stream.fromIterable([])); + expect(result.value, completion([])); + }); + + test('empty events', () { + var result = collectBytesCancelable(Stream.fromIterable([[], []])); + expect(result.value, completion([])); + }); + + test('error event', () { + var result = collectBytesCancelable(Stream.fromIterable( + Iterable.generate(3, (n) => n == 2 ? throw 'badness' : [n]))); + expect(result.value, throwsA('badness')); + }); + + test('cancelled', () async { + var sc = StreamController>(); + var result = collectBytesCancelable(sc.stream); + // Value never completes. + result.value.whenComplete(expectAsync0(() {}, count: 0)); + + expect(sc.hasListener, isTrue); + sc.add([1, 2]); + await nextTimerTick(); + expect(sc.hasListener, isTrue); + sc.add([3, 4]); + await nextTimerTick(); + expect(sc.hasListener, isTrue); + result.cancel(); + expect(sc.hasListener, isFalse); // Cancelled immediately. + var replacement = await result.valueOrCancellation(); + expect(replacement, isNull); + await nextTimerTick(); + sc.close(); + await nextTimerTick(); + }); + }); +} + +Future nextTimerTick() => Future(() {}); diff --git a/pkgs/async/test/cancelable_operation_test.dart b/pkgs/async/test/cancelable_operation_test.dart new file mode 100644 index 00000000..3b096e42 --- /dev/null +++ b/pkgs/async/test/cancelable_operation_test.dart @@ -0,0 +1,934 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: unawaited_futures + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + group('without being canceled', () { + late CancelableCompleter completer; + setUp(() { + completer = CancelableCompleter(onCancel: expectAsync0(() {}, count: 0)); + }); + + test('sends values to the future', () { + expect(completer.operation.value, completion(equals(1))); + expect(completer.isCompleted, isFalse); + completer.complete(1); + expect(completer.isCompleted, isTrue); + }); + + test('sends null values to the future', () { + expect(completer.operation.value, completion(equals(null))); + expect(completer.isCompleted, isFalse); + completer.complete(null); + expect(completer.isCompleted, isTrue); + }); + + test('sends errors to the future', () { + expect(completer.operation.value, throwsA('error')); + expect(completer.isCompleted, isFalse); + completer.completeError('error'); + expect(completer.isCompleted, isTrue); + }); + + test('sends values in a future to the future', () { + expect(completer.operation.value, completion(equals(1))); + expect(completer.isCompleted, isFalse); + completer.complete(Future.value(1)); + expect(completer.isCompleted, isTrue); + }); + + test('sends errors in a future to the future', () async { + expect(completer.operation.value, throwsA('error')); + expect(completer.isCompleted, isFalse); + expect(completer.operation.isCompleted, isFalse); + completer.complete(Future.error('error')); + expect(completer.isCompleted, isTrue); + await flushMicrotasks(); + expect(completer.operation.isCompleted, isTrue); + }); + + test('sends values from a cancelable operation to the future', () { + expect(completer.operation.value, completion(equals(1))); + completer + .completeOperation(CancelableOperation.fromFuture(Future.value(1))); + }); + + test('sends values from a completed cancelable operation to the future', + () async { + final operation = CancelableOperation.fromFuture(Future.value(1)); + await operation.value; + expect(completer.operation.value, completion(equals(1))); + completer.completeOperation(operation); + }); + + test('sends errors from a cancelable operation to the future', () { + expect(completer.operation.value, throwsA('error')); + completer.completeOperation( + CancelableOperation.fromFuture(Future.error('error')..ignore())); + }); + + test('sends errors from a completed cancelable operation to the future', + () async { + final operation = + CancelableOperation.fromFuture(Future.error('error')..ignore()); + try { + await operation.value; + } on Object { + // ignore + } + expect(completer.operation.value, throwsA('error')); + completer.completeOperation(operation); + }); + + test('sends values to valueOrCancellation', () { + expect(completer.operation.valueOrCancellation(), completion(equals(1))); + completer.complete(1); + }); + + test('sends errors to valueOrCancellation', () { + expect(completer.operation.valueOrCancellation(), throwsA('error')); + completer.completeError('error'); + }); + + test('chains null values through .then calls', () async { + var operation = CancelableOperation.fromFuture(Future.value(null)); + expect(await operation.then((_) {}).value, null); + }); + + test('is not complete until the result is available', () async { + var backingWork = Completer(); + var operation = CancelableOperation.fromFuture(backingWork.future); + expect(operation.isCompleted, isFalse); + backingWork.complete(); + await backingWork.future; + expect(operation.isCompleted, isTrue); + }); + + group('throws a StateError if completed', () { + test('successfully twice', () { + completer.complete(1); + expect(() => completer.complete(1), throwsStateError); + }); + + test('successfully then unsuccessfully', () { + completer.complete(1); + expect(() => completer.completeError('error'), throwsStateError); + }); + + test('unsuccessfully twice', () { + expect(completer.operation.value, throwsA('error')); + completer.completeError('error'); + expect(() => completer.completeError('error'), throwsStateError); + }); + + test('successfully then with a future', () { + completer.complete(1); + expect(() => completer.complete(Completer().future), + throwsStateError); + }); + + test('with a future then successfully', () { + completer.complete(Completer().future); + expect(() => completer.complete(1), throwsStateError); + }); + + test('with a future twice', () { + completer.complete(Completer().future); + expect(() => completer.complete(Completer().future), + throwsStateError); + }); + }); + + group('CancelableOperation.fromFuture', () { + test('forwards values', () { + var operation = CancelableOperation.fromFuture(Future.value(1)); + expect(operation.value, completion(equals(1))); + }); + + test('forwards errors', () { + var operation = CancelableOperation.fromFuture(Future.error('error')); + expect(operation.value, throwsA('error')); + }); + }); + + group('CancelableOperation.fromSubscription', () { + test('forwards a done event once it completes', () async { + var controller = StreamController(); + var operationCompleted = false; + CancelableOperation.fromSubscription(controller.stream.listen(null)) + .then((_) { + operationCompleted = true; + }); + + await flushMicrotasks(); + expect(operationCompleted, isFalse); + + controller.close(); + await flushMicrotasks(); + expect(operationCompleted, isTrue); + }); + + test('forwards errors', () { + var operation = CancelableOperation.fromSubscription( + Stream.error('error').listen(null)); + expect(operation.value, throwsA('error')); + }); + }); + }); + + group('when canceled', () { + test('causes the future never to fire', () async { + var completer = CancelableCompleter(); + completer.operation.value.whenComplete(expectAsync0(() {}, count: 0)); + completer.operation.cancel(); + + // Give the future plenty of time to fire if it's going to. + await flushMicrotasks(); + completer.complete(); + await flushMicrotasks(); + }); + + test('fires onCancel', () { + var canceled = false; + late CancelableCompleter completer; + completer = CancelableCompleter(onCancel: expectAsync0(() { + expect(completer.isCanceled, isTrue); + canceled = true; + })); + + expect(canceled, isFalse); + expect(completer.isCanceled, isFalse); + expect(completer.operation.isCanceled, isFalse); + expect(completer.isCompleted, isFalse); + expect(completer.operation.isCompleted, isFalse); + completer.operation.cancel(); + expect(canceled, isTrue); + expect(completer.isCanceled, isTrue); + expect(completer.operation.isCanceled, isTrue); + expect(completer.isCompleted, isFalse); + expect(completer.operation.isCompleted, isFalse); + }); + + test('returns the onCancel future each time cancel is called', () { + var completer = CancelableCompleter(onCancel: expectAsync0(() { + return Future.value(1); + })); + expect(completer.operation.cancel(), completion(equals(1))); + expect(completer.operation.cancel(), completion(equals(1))); + expect(completer.operation.cancel(), completion(equals(1))); + }); + + test("returns a future even if onCancel doesn't", () { + var completer = CancelableCompleter(onCancel: expectAsync0(() {})); + expect(completer.operation.cancel(), completes); + }); + + test("doesn't call onCancel if the completer has completed", () { + var completer = + CancelableCompleter(onCancel: expectAsync0(() {}, count: 0)); + completer.complete(1); + expect(completer.operation.value, completion(equals(1))); + expect(completer.operation.cancel(), completes); + }); + + test( + 'does call onCancel if the completer has completed to an unfired ' + 'Future', () { + var completer = CancelableCompleter(onCancel: expectAsync0(() {})); + completer.complete(Completer().future); + expect(completer.operation.cancel(), completes); + }); + + test( + "doesn't call onCancel if the completer has completed to a fired " + 'Future', () async { + var completer = + CancelableCompleter(onCancel: expectAsync0(() {}, count: 0)); + completer.complete(Future.value(1)); + await completer.operation.value; + expect(completer.operation.cancel(), completes); + }); + + test('can be completed once after being canceled', () async { + var completer = CancelableCompleter(); + completer.operation.value.whenComplete(expectAsync0(() {}, count: 0)); + await completer.operation.cancel(); + completer.complete(1); + expect(() => completer.complete(1), throwsStateError); + }); + + test('fires valueOrCancellation with the given value', () { + var completer = CancelableCompleter(); + expect(completer.operation.valueOrCancellation(1), completion(equals(1))); + completer.operation.cancel(); + }); + + test('pipes an error through valueOrCancellation', () { + var completer = CancelableCompleter(onCancel: () { + throw 'error'; + }); + expect(completer.operation.valueOrCancellation(1), throwsA('error')); + completer.operation.cancel(); + }); + + test('valueOrCancellation waits on the onCancel future', () async { + var innerCompleter = Completer(); + var completer = + CancelableCompleter(onCancel: () => innerCompleter.future); + + var fired = false; + completer.operation.valueOrCancellation().then((_) { + fired = true; + }); + + completer.operation.cancel(); + await flushMicrotasks(); + expect(fired, isFalse); + + innerCompleter.complete(); + await flushMicrotasks(); + expect(fired, isTrue); + }); + + test('CancelableOperation.fromSubscription() cancels the subscription', + () async { + var cancelCompleter = Completer(); + var canceled = false; + var controller = StreamController(onCancel: () { + canceled = true; + return cancelCompleter.future; + }); + var operation = + CancelableOperation.fromSubscription(controller.stream.listen(null)); + + await flushMicrotasks(); + expect(canceled, isFalse); + + // The `cancel()` call shouldn't complete until + // `StreamSubscription.cancel` completes. + var cancelCompleted = false; + expect( + operation.cancel().then((_) { + cancelCompleted = true; + }), + completes); + await flushMicrotasks(); + expect(canceled, isTrue); + expect(cancelCompleted, isFalse); + + cancelCompleter.complete(); + await flushMicrotasks(); + expect(cancelCompleted, isTrue); + }); + + group('completeOperation', () { + test('sends cancellation from a cancelable operation', () async { + final completer = CancelableCompleter(); + completer.operation.value.whenComplete(expectAsync0(() {}, count: 0)); + completer + .completeOperation(CancelableCompleter().operation..cancel()); + await completer.operation.valueOrCancellation(); + expect(completer.operation.isCanceled, true); + }); + + test('sends errors from a completed cancelable operation to the future', + () async { + final operation = CancelableCompleter().operation..cancel(); + await operation.valueOrCancellation(); + final completer = CancelableCompleter(); + completer.operation.value.whenComplete(expectAsync0(() {}, count: 0)); + completer.completeOperation(operation); + await completer.operation.valueOrCancellation(); + expect(completer.operation.isCanceled, true); + }); + + test('propagates cancellation', () { + final completer = CancelableCompleter(); + final operation = + CancelableCompleter(onCancel: expectAsync0(() {}, count: 1)) + .operation; + completer.completeOperation(operation); + completer.operation.cancel(); + }); + + test('propagates cancellation from already canceld completer', () async { + final completer = CancelableCompleter()..operation.cancel(); + await completer.operation.valueOrCancellation(); + final operation = + CancelableCompleter(onCancel: expectAsync0(() {}, count: 1)) + .operation; + completer.completeOperation(operation); + }); + test('cancel propagation can be disabled', () { + final completer = CancelableCompleter(); + final operation = + CancelableCompleter(onCancel: expectAsync0(() {}, count: 0)) + .operation; + completer.completeOperation(operation, propagateCancel: false); + completer.operation.cancel(); + }); + + test('cancel propagation can be disabled from already canceled completed', + () async { + final completer = CancelableCompleter()..operation.cancel(); + await completer.operation.valueOrCancellation(); + final operation = + CancelableCompleter(onCancel: expectAsync0(() {}, count: 0)) + .operation; + completer.completeOperation(operation, propagateCancel: false); + }); + }); + }); + + group('asStream()', () { + test('emits a value and then closes', () { + var completer = CancelableCompleter(); + expect(completer.operation.asStream().toList(), completion(equals([1]))); + completer.complete(1); + }); + + test('emits an error and then closes', () { + var completer = CancelableCompleter(); + var queue = StreamQueue(completer.operation.asStream()); + expect(queue.next, throwsA('error')); + expect(queue.hasNext, completion(isFalse)); + completer.completeError('error'); + }); + + test('cancels the completer when the subscription is canceled', () { + var completer = CancelableCompleter(onCancel: expectAsync0(() {})); + var sub = + completer.operation.asStream().listen(expectAsync1((_) {}, count: 0)); + completer.operation.value.whenComplete(expectAsync0(() {}, count: 0)); + sub.cancel(); + expect(completer.isCanceled, isTrue); + }); + }); + + group('then', () { + FutureOr Function(int)? onValue; + FutureOr Function(Object, StackTrace)? onError; + FutureOr Function()? onCancel; + late bool propagateCancel; + late CancelableCompleter originalCompleter; + + setUp(() { + // Initialize all functions to ones that expect to not be called. + onValue = expectAsync1((_) => 'Fake', count: 0, id: 'onValue'); + onError = expectAsync2((e, s) => 'Fake', count: 0, id: 'onError'); + onCancel = expectAsync0(() => 'Fake', count: 0, id: 'onCancel'); + propagateCancel = false; + originalCompleter = CancelableCompleter(); + }); + + CancelableOperation runThen() { + return originalCompleter.operation.then(onValue!, + onError: onError, + onCancel: onCancel, + propagateCancel: propagateCancel); + } + + group('original operation completes successfully', () { + test('onValue completes successfully', () { + onValue = expectAsync1((v) => v.toString(), count: 1, id: 'onValue'); + + expect(runThen().value, completion('1')); + originalCompleter.complete(1); + }); + + test('onValue throws error', () { + // expectAsync1 only works with functions that do not throw. + onValue = (_) => throw 'error'; + + expect(runThen().value, throwsA('error')); + originalCompleter.complete(1); + }); + + test('onValue returns Future that throws error', () { + onValue = + expectAsync1((v) => Future.error('error'), count: 1, id: 'onValue'); + + expect(runThen().value, throwsA('error')); + originalCompleter.complete(1); + }); + + test('and returned operation is canceled with propagateCancel = false', + () async { + propagateCancel = false; + + runThen().cancel(); + + // onValue should not be called. + originalCompleter.complete(1); + }); + }); + + group('original operation completes with error', () { + test('onError not set', () { + onError = null; + + expect(runThen().value, throwsA('error')); + originalCompleter.completeError('error'); + }); + + test('onError completes successfully', () { + onError = expectAsync2((e, s) => 'onError caught $e', + count: 1, id: 'onError'); + + expect(runThen().value, completion('onError caught error')); + originalCompleter.completeError('error'); + }); + + test('onError throws', () { + // expectAsync2 does not work with functions that throw. + onError = (e, s) => throw 'onError caught $e'; + + expect(runThen().value, throwsA('onError caught error')); + originalCompleter.completeError('error'); + }); + + test('onError returns Future that throws', () { + onError = expectAsync2((e, s) => Future.error('onError caught $e'), + count: 1, id: 'onError'); + + expect(runThen().value, throwsA('onError caught error')); + originalCompleter.completeError('error'); + }); + + test('and returned operation is canceled with propagateCancel = false', + () async { + propagateCancel = false; + + runThen().cancel(); + + // onError should not be called. + originalCompleter.completeError('error'); + }); + }); + + group('original operation canceled', () { + test('onCancel not set', () async { + onCancel = null; + + final operation = runThen(); + + await expectLater(originalCompleter.operation.cancel(), completes); + expect(operation.isCanceled, true); + }); + + test('onCancel completes successfully', () { + onCancel = expectAsync0(() => 'canceled', count: 1, id: 'onCancel'); + + expect(runThen().value, completion('canceled')); + originalCompleter.operation.cancel(); + }); + + test('onCancel throws error', () { + // expectAsync0 only works with functions that do not throw. + onCancel = () => throw 'error'; + + expect(runThen().value, throwsA('error')); + originalCompleter.operation.cancel(); + }); + + test('onCancel returns Future that throws error', () { + onCancel = + expectAsync0(() => Future.error('error'), count: 1, id: 'onCancel'); + + expect(runThen().value, throwsA('error')); + originalCompleter.operation.cancel(); + }); + + test('after completing with a future does not invoke `onValue`', + () async { + onValue = expectAsync1((_) => '', count: 0); + onCancel = null; + var operation = runThen(); + var workCompleter = Completer(); + originalCompleter.complete(workCompleter.future); + var cancelation = originalCompleter.operation.cancel(); + expect(originalCompleter.isCanceled, true); + workCompleter.complete(0); + await cancelation; + expect(operation.isCanceled, true); + await workCompleter.future; + }); + + test('after the value is completed invokes `onValue`', () { + onValue = expectAsync1((_) => 'foo', count: 1); + onCancel = expectAsync1((_) => '', count: 0); + originalCompleter.complete(0); + originalCompleter.operation.cancel(); + var operation = runThen(); + expect(operation.value, completion('foo')); + expect(operation.isCanceled, false); + }); + + test('waits for chained cancellation', () async { + var completer = CancelableCompleter(); + var chainedOperation = completer.operation + .then((_) => Future.delayed(const Duration(milliseconds: 1))) + .then((_) => Future.delayed(const Duration(milliseconds: 1))); + + await completer.operation.cancel(); + expect(completer.operation.isCanceled, true); + expect(chainedOperation.isCanceled, true); + }); + }); + + group('returned operation canceled', () { + test('propagateCancel is true', () async { + propagateCancel = true; + + await runThen().cancel(); + + expect(originalCompleter.isCanceled, true); + }); + + test('propagateCancel is false', () async { + propagateCancel = false; + + await runThen().cancel(); + + expect(originalCompleter.isCanceled, false); + }); + + test('onValue callback not called after cancel', () async { + var called = false; + onValue = expectAsync1((_) { + called = true; + fail('onValue unreachable'); + }, count: 0); + + await runThen().cancel(); + originalCompleter.complete(0); + await flushMicrotasks(); + expect(called, false); + }); + + test('onError callback not called after cancel', () async { + var called = false; + onError = expectAsync2((_, __) { + called = true; + fail('onError unreachable'); + }, count: 0); + + await runThen().cancel(); + originalCompleter.completeError('Error', StackTrace.empty); + await flushMicrotasks(); + expect(called, false); + }); + + test('onCancel callback not called after cancel', () async { + var called = false; + onCancel = expectAsync0(() { + called = true; + fail('onCancel unreachable'); + }, count: 0); + + await runThen().cancel(); + await originalCompleter.operation.cancel(); + await flushMicrotasks(); + expect(called, false); + }); + }); + }); + + group('thenOperation', () { + late void Function(int, CancelableCompleter) onValue; + void Function(Object, StackTrace, CancelableCompleter)? onError; + void Function(CancelableCompleter)? onCancel; + late bool propagateCancel; + late CancelableCompleter originalCompleter; + + setUp(() { + // Initialize all functions to ones that expect to not be called. + onValue = expectAsync2((value, completer) => completer.complete('$value'), + count: 0, id: 'onValue'); + onError = null; + onCancel = null; + propagateCancel = false; + originalCompleter = CancelableCompleter(); + }); + + CancelableOperation runThenOperation() { + return originalCompleter.operation.thenOperation(onValue, + onError: onError, + onCancel: onCancel, + propagateCancel: propagateCancel); + } + + group('original operation completes successfully', () { + test('onValue completes successfully', () { + onValue = + expectAsync2((v, c) => c.complete('$v'), count: 1, id: 'onValue'); + + expect(runThenOperation().value, completion('1')); + originalCompleter.complete(1); + }); + + test('onValue throws error', () { + // expectAsync1 only works with functions that do not throw. + onValue = (_, __) => throw 'error'; + + expect(runThenOperation().value, throwsA('error')); + originalCompleter.complete(1); + }); + + test('onValue completes operation as error', () { + onValue = expectAsync2( + (_, completer) => completer.completeError('error'), + count: 1, + id: 'onValue'); + + expect(runThenOperation().value, throwsA('error')); + originalCompleter.complete(1); + }); + + test('onValue returns a Future that throws error', () { + onValue = expectAsync2((_, completer) => Future.error('error'), + count: 1, id: 'onValue'); + + expect(runThenOperation().value, throwsA('error')); + originalCompleter.complete(1); + }); + + test('and returned operation is canceled', () async { + onValue = expectAsync2((_, __) => throw 'never called', count: 0); + runThenOperation().cancel(); + // onValue should not be called. + originalCompleter.complete(1); + }); + }); + + group('original operation completes with error', () { + test('onError not set', () { + onError = null; + + expect(runThenOperation().value, throwsA('error')); + originalCompleter.completeError('error'); + }); + + test('onError completes operation', () { + onError = expectAsync3((e, s, c) => c.complete('onError caught $e'), + count: 1, id: 'onError'); + + expect(runThenOperation().value, completion('onError caught error')); + originalCompleter.completeError('error'); + }); + + test('onError throws', () { + // expectAsync3 does not work with functions that throw. + onError = (e, s, c) => throw 'onError caught $e'; + + expect(runThenOperation().value, throwsA('onError caught error')); + originalCompleter.completeError('error'); + }); + + test('onError returns Future that throws error', () { + onError = expectAsync3((e, s, c) => Future.error('onError caught $e'), + count: 1, id: 'onError'); + + expect(runThenOperation().value, throwsA('onError caught error')); + originalCompleter.completeError('error'); + }); + + test('onError completes operation as an error', () { + onError = expectAsync3( + (e, s, c) => c.completeError('onError caught $e'), + count: 1, + id: 'onError'); + + expect(runThenOperation().value, throwsA('onError caught error')); + originalCompleter.completeError('error'); + }); + + test('and returned operation is canceled with propagateCancel = false', + () async { + onError = expectAsync3((e, s, c) {}, count: 0); + + runThenOperation().cancel(); + + // onError should not be called. + originalCompleter.completeError('error'); + }); + }); + + group('original operation canceled', () { + test('onCancel not set', () async { + onCancel = null; + + final operation = runThenOperation(); + + await expectLater(originalCompleter.operation.cancel(), completes); + expect(operation.isCanceled, true); + }); + + test('onCancel completes successfully', () { + onCancel = expectAsync1((c) => c.complete('canceled'), + count: 1, id: 'onCancel'); + + expect(runThenOperation().value, completion('canceled')); + originalCompleter.operation.cancel(); + }); + + test('onCancel throws error', () { + // expectAsync0 only works with functions that do not throw. + onCancel = (_) => throw 'error'; + + expect(runThenOperation().value, throwsA('error')); + originalCompleter.operation.cancel(); + }); + + test('onCancel completes operation as error', () { + onCancel = expectAsync1((c) => c.completeError('error'), + count: 1, id: 'onCancel'); + + expect(runThenOperation().value, throwsA('error')); + originalCompleter.operation.cancel(); + }); + + test('onCancel returns Future that throws error', () { + onCancel = expectAsync1((c) => Future.error('error'), + count: 1, id: 'onCancel'); + + expect(runThenOperation().value, throwsA('error')); + originalCompleter.operation.cancel(); + }); + + test('after completing with a future does not invoke `onValue`', + () async { + onValue = expectAsync2((_, __) {}, count: 0); + onCancel = null; + var operation = runThenOperation(); + var workCompleter = Completer(); + originalCompleter.complete(workCompleter.future); + var cancelation = originalCompleter.operation.cancel(); + expect(originalCompleter.isCanceled, true); + workCompleter.complete(0); + await cancelation; + expect(operation.isCanceled, true); + await workCompleter.future; + }); + + test('after the value is completed invokes `onValue`', () { + onValue = expectAsync2((v, c) => c.complete('foo'), count: 1); + onCancel = expectAsync1((_) {}, count: 0); + originalCompleter.complete(0); + originalCompleter.operation.cancel(); + var operation = runThenOperation(); + expect(operation.value, completion('foo')); + expect(operation.isCanceled, false); + }); + }); + + group('returned operation canceled', () { + test('propagateCancel is true', () async { + propagateCancel = true; + + await runThenOperation().cancel(); + + expect(originalCompleter.isCanceled, true); + }); + + test('propagateCancel is false', () async { + propagateCancel = false; + + await runThenOperation().cancel(); + + expect(originalCompleter.isCanceled, false); + }); + + test('onValue callback not called after cancel', () async { + onValue = expectAsync2((_, c) {}, count: 0); + + await runThenOperation().cancel(); + originalCompleter.complete(0); + }); + + test('onError callback not called after cancel', () async { + onError = expectAsync3((_, __, ___) {}, count: 0); + + await runThenOperation().cancel(); + originalCompleter.completeError('Error', StackTrace.empty); + }); + + test('onCancel callback not called after cancel', () async { + onCancel = expectAsync1((_) {}, count: 0); + + await runThenOperation().cancel(); + await originalCompleter.operation.cancel(); + }); + }); + }); + + group('race()', () { + late bool canceled1; + late CancelableCompleter completer1; + late bool canceled2; + late CancelableCompleter completer2; + late bool canceled3; + late CancelableCompleter completer3; + late CancelableOperation operation; + setUp(() { + canceled1 = false; + completer1 = CancelableCompleter(onCancel: () { + canceled1 = true; + }); + + canceled2 = false; + completer2 = CancelableCompleter(onCancel: () { + canceled2 = true; + }); + + canceled3 = false; + completer3 = CancelableCompleter(onCancel: () { + canceled3 = true; + }); + + operation = CancelableOperation.race( + [completer1.operation, completer2.operation, completer3.operation]); + }); + + test('returns the first value to complete', () { + completer1.complete(1); + completer2.complete(2); + completer3.complete(3); + + expect(operation.value, completion(equals(1))); + }); + + test('throws the first error to complete', () { + completer1.completeError('error 1'); + completer2.completeError('error 2'); + completer3.completeError('error 3'); + + expect(operation.value, throwsA('error 1')); + }); + + test('cancels any completers that haven\'t completed', () async { + completer1.complete(1); + await expectLater(operation.value, completion(equals(1))); + expect(canceled1, isFalse); + expect(canceled2, isTrue); + expect(canceled3, isTrue); + }); + + test('cancels all completers when the operation is completed', () async { + await operation.cancel(); + + expect(canceled1, isTrue); + expect(canceled2, isTrue); + expect(canceled3, isTrue); + }); + }); +} diff --git a/pkgs/async/test/chunked_stream_reader.dart b/pkgs/async/test/chunked_stream_reader.dart new file mode 100644 index 00000000..2fc1e8b7 --- /dev/null +++ b/pkgs/async/test/chunked_stream_reader.dart @@ -0,0 +1,482 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +void main() { + test('readChunk() chunk by chunk', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield [3, 4, 5]; + yield [6, 7, 8, 9]; + yield [10]; + }()); + + expect(await r.readChunk(2), equals([1, 2])); + expect(await r.readChunk(3), equals([3, 4, 5])); + expect(await r.readChunk(4), equals([6, 7, 8, 9])); + expect(await r.readChunk(1), equals([10])); + expect(await r.readChunk(1), equals([])); + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readChunk() element by element', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield [3, 4, 5]; + yield [6, 7, 8, 9]; + yield [10]; + }()); + + for (var i = 0; i < 10; i++) { + expect(await r.readChunk(1), equals([i + 1])); + } + expect(await r.readChunk(1), equals([])); + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readChunk() exact elements', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield [3, 4, 5]; + yield [6, 7, 8, 9]; + yield [10]; + }()); + + expect(await r.readChunk(10), equals([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); + expect(await r.readChunk(1), equals([])); + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readChunk() past end', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield [3, 4, 5]; + yield [6, 7, 8, 9]; + yield [10]; + }()); + + expect(await r.readChunk(20), equals([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); + expect(await r.readChunk(1), equals([])); + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readChunk() chunks of 2 elements', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield [3, 4, 5]; + yield [6, 7, 8, 9]; + yield [10]; + }()); + + expect(await r.readChunk(2), equals([1, 2])); + expect(await r.readChunk(2), equals([3, 4])); + expect(await r.readChunk(2), equals([5, 6])); + expect(await r.readChunk(2), equals([7, 8])); + expect(await r.readChunk(2), equals([9, 10])); + expect(await r.readChunk(1), equals([])); + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readChunk() chunks of 3 elements', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield [3, 4, 5]; + yield [6, 7, 8, 9]; + yield [10]; + }()); + + expect(await r.readChunk(3), equals([1, 2, 3])); + expect(await r.readChunk(3), equals([4, 5, 6])); + expect(await r.readChunk(3), equals([7, 8, 9])); + expect(await r.readChunk(3), equals([10])); + expect(await r.readChunk(1), equals([])); + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readChunk() cancel half way', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield [3, 4, 5]; + yield [6, 7, 8, 9]; + yield [10]; + }()); + + expect(await r.readChunk(5), equals([1, 2, 3, 4, 5])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readChunk() propagates exception', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield [3, 4, 5]; + throw Exception('stopping here'); + }()); + + expect(await r.readChunk(3), equals([1, 2, 3])); + await expectLater(r.readChunk(3), throwsException); + + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readStream() forwards chunks', () async { + final chunk2 = [3, 4, 5]; + final chunk3 = [6, 7, 8, 9]; + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield chunk2; + yield chunk3; + yield [10]; + }()); + + expect(await r.readChunk(1), equals([1])); + final i = StreamIterator(r.readStream(9)); + expect(await i.moveNext(), isTrue); + expect(i.current, equals([2])); + + // We must forward the exact chunks otherwise it's not efficient! + // Hence, we have a reference equality check here. + expect(await i.moveNext(), isTrue); + expect(i.current, equals([3, 4, 5])); + expect(i.current == chunk2, isTrue); + + expect(await i.moveNext(), isTrue); + expect(i.current, equals([6, 7, 8, 9])); + expect(i.current == chunk3, isTrue); + + expect(await i.moveNext(), isTrue); + expect(i.current, equals([10])); + expect(await i.moveNext(), isFalse); + + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readStream() cancel at the exact end', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield [3, 4, 5]; + yield [6, 7, 8, 9]; + yield [10]; + }()); + + expect(await r.readChunk(1), equals([1])); + final i = StreamIterator(r.readStream(7)); + expect(await i.moveNext(), isTrue); + expect(i.current, equals([2])); + + expect(await i.moveNext(), isTrue); + expect(i.current, equals([3, 4, 5])); + + expect(await i.moveNext(), isTrue); + expect(i.current, equals([6, 7, 8])); + + await i.cancel(); // cancel substream just as it's ending + + expect(await r.readChunk(2), equals([9, 10])); + + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readStream() cancel at the exact end on chunk boundary', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield [3, 4, 5]; + yield [6, 7, 8, 9]; + yield [10]; + }()); + + expect(await r.readChunk(1), equals([1])); + final i = StreamIterator(r.readStream(8)); + expect(await i.moveNext(), isTrue); + expect(i.current, equals([2])); + + expect(await i.moveNext(), isTrue); + expect(i.current, equals([3, 4, 5])); + + expect(await i.moveNext(), isTrue); + expect(i.current, equals([6, 7, 8, 9])); + + await i.cancel(); // cancel substream just as it's ending + + expect(await r.readChunk(2), equals([10])); + + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readStream() is drained when canceled', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield [3, 4, 5]; + yield [6, 7, 8, 9]; + yield [10]; + }()); + + expect(await r.readChunk(1), equals([1])); + final i = StreamIterator(r.readStream(7)); + expect(await i.moveNext(), isTrue); + expect(i.current, equals([2])); + // Cancelling here should skip the remainder of the substream + // and we continue to read 9 and 10 from r + await i.cancel(); + + expect(await r.readChunk(2), equals([9, 10])); + + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readStream() concurrent reads is forbidden', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield [3, 4, 5]; + yield [6, 7, 8, 9]; + yield [10]; + }()); + + expect(await r.readChunk(1), equals([1])); + // Notice we are not reading this substream: + r.readStream(7); + + expectLater(r.readChunk(2), throwsStateError); + }); + + test('readStream() supports draining', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield [3, 4, 5]; + yield [6, 7, 8, 9]; + yield [10]; + }()); + + expect(await r.readChunk(1), equals([1])); + await r.readStream(7).drain(); + expect(await r.readChunk(2), equals([9, 10])); + + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('nested ChunkedStreamReader', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield [3, 4, 5]; + yield [6, 7, 8, 9]; + yield [10]; + }()); + + expect(await r.readChunk(1), equals([1])); + final r2 = ChunkedStreamReader(r.readStream(7)); + expect(await r2.readChunk(2), equals([2, 3])); + expect(await r2.readChunk(1), equals([4])); + await r2.cancel(); + + expect(await r.readChunk(2), equals([9, 10])); + + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readBytes() chunks of 3 elements', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2]; + yield [3, 4, 5]; + yield [6, 7, 8, 9]; + yield [10]; + }()); + + expect(await r.readBytes(3), allOf(equals([1, 2, 3]), isA())); + expect(await r.readBytes(3), allOf(equals([4, 5, 6]), isA())); + expect(await r.readBytes(3), allOf(equals([7, 8, 9]), isA())); + expect(await r.readBytes(3), allOf(equals([10]), isA())); + expect(await r.readBytes(1), equals([])); + expect(await r.readBytes(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readBytes(1), equals([])); + }); + + test('readChunk() until exact end of stream', () async { + final stream = Stream.fromIterable(Iterable.generate( + 10, + (_) => Uint8List(512), + )); + + final r = ChunkedStreamReader(stream); + while (true) { + final c = await r.readBytes(1024); + if (c.isEmpty) { + break; + } + } + }); + + test('cancel while readChunk() is pending', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2, 3]; + // This will hang forever, so we will call cancel() + await Completer().future; + yield [4]; // this should never be reachable + fail('unreachable!'); + }()); + + expect(await r.readBytes(2), equals([1, 2])); + + final future = r.readChunk(2); + + // Wait a tiny bit and cancel + await Future.microtask(() => null); + r.cancel(); + + expect(await future, hasLength(lessThan(2))); + }); + + test('cancel while readStream() is pending', () async { + final r = ChunkedStreamReader(() async* { + yield [1, 2, 3]; + // This will hang forever, so we will call cancel() + await Completer().future; + yield [4]; // this should never be reachable + fail('unreachable!'); + }()); + + expect(await collectBytes(r.readStream(2)), equals([1, 2])); + + final stream = r.readStream(2); + + // Wait a tiny bit and cancel + await Future.microtask(() => null); + r.cancel(); + + expect(await collectBytes(stream), hasLength(lessThan(2))); + }); + + test('readChunk() chunk by chunk (Uint8List)', () async { + final r = ChunkedStreamReader(() async* { + yield Uint8List.fromList([1, 2]); + yield Uint8List.fromList([3, 4, 5]); + yield Uint8List.fromList([6, 7, 8, 9]); + yield Uint8List.fromList([10]); + }()); + + expect(await r.readChunk(2), equals([1, 2])); + expect(await r.readChunk(3), equals([3, 4, 5])); + expect(await r.readChunk(4), equals([6, 7, 8, 9])); + expect(await r.readChunk(1), equals([10])); + expect(await r.readChunk(1), equals([])); + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readChunk() element by element (Uint8List)', () async { + final r = ChunkedStreamReader(() async* { + yield Uint8List.fromList([1, 2]); + yield Uint8List.fromList([3, 4, 5]); + yield Uint8List.fromList([6, 7, 8, 9]); + yield Uint8List.fromList([10]); + }()); + + for (var i = 0; i < 10; i++) { + expect(await r.readChunk(1), equals([i + 1])); + } + expect(await r.readChunk(1), equals([])); + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readChunk() exact elements (Uint8List)', () async { + final r = ChunkedStreamReader(() async* { + yield Uint8List.fromList([1, 2]); + yield Uint8List.fromList([3, 4, 5]); + yield Uint8List.fromList([6, 7, 8, 9]); + yield Uint8List.fromList([10]); + }()); + + expect(await r.readChunk(10), equals([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); + expect(await r.readChunk(1), equals([])); + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readChunk() past end (Uint8List)', () async { + final r = ChunkedStreamReader(() async* { + yield Uint8List.fromList([1, 2]); + yield Uint8List.fromList([3, 4, 5]); + yield Uint8List.fromList([6, 7, 8, 9]); + yield Uint8List.fromList([10]); + }()); + + expect(await r.readChunk(20), equals([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); + expect(await r.readChunk(1), equals([])); + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readChunk() chunks of 2 elements (Uint8List)', () async { + final r = ChunkedStreamReader(() async* { + yield Uint8List.fromList([1, 2]); + yield Uint8List.fromList([3, 4, 5]); + yield Uint8List.fromList([6, 7, 8, 9]); + yield Uint8List.fromList([10]); + }()); + + expect(await r.readChunk(2), equals([1, 2])); + expect(await r.readChunk(2), equals([3, 4])); + expect(await r.readChunk(2), equals([5, 6])); + expect(await r.readChunk(2), equals([7, 8])); + expect(await r.readChunk(2), equals([9, 10])); + expect(await r.readChunk(1), equals([])); + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); + + test('readChunk() chunks of 3 elements (Uint8List)', () async { + final r = ChunkedStreamReader(() async* { + yield Uint8List.fromList([1, 2]); + yield Uint8List.fromList([3, 4, 5]); + yield Uint8List.fromList([6, 7, 8, 9]); + yield Uint8List.fromList([10]); + }()); + + expect(await r.readChunk(3), equals([1, 2, 3])); + expect(await r.readChunk(3), equals([4, 5, 6])); + expect(await r.readChunk(3), equals([7, 8, 9])); + expect(await r.readChunk(3), equals([10])); + expect(await r.readChunk(1), equals([])); + expect(await r.readChunk(1), equals([])); + await r.cancel(); // check this is okay! + expect(await r.readChunk(1), equals([])); + }); +} diff --git a/pkgs/async/test/future_group_test.dart b/pkgs/async/test/future_group_test.dart new file mode 100644 index 00000000..9729c066 --- /dev/null +++ b/pkgs/async/test/future_group_test.dart @@ -0,0 +1,224 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/src/future_group.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + late FutureGroup futureGroup; + setUp(() { + futureGroup = FutureGroup(); + }); + + group('with no futures', () { + test('never completes if nothing happens', () async { + var completed = false; + futureGroup.future.then((_) => completed = true); + + await flushMicrotasks(); + expect(completed, isFalse); + }); + + test("completes once it's closed", () { + expect(futureGroup.future, completion(isEmpty)); + expect(futureGroup.isClosed, isFalse); + futureGroup.close(); + expect(futureGroup.isClosed, isTrue); + }); + }); + + group('with a future that already completed', () { + test('never completes if nothing happens', () async { + futureGroup.add(Future.value()); + await flushMicrotasks(); + + var completed = false; + futureGroup.future.then((_) => completed = true); + + await flushMicrotasks(); + expect(completed, isFalse); + }); + + test("completes once it's closed", () async { + futureGroup.add(Future.value()); + await flushMicrotasks(); + + expect(futureGroup.future, completes); + expect(futureGroup.isClosed, isFalse); + futureGroup.close(); + expect(futureGroup.isClosed, isTrue); + }); + + test("completes to that future's value", () { + futureGroup.add(Future.value(1)); + futureGroup.close(); + expect(futureGroup.future, completion(equals([1]))); + }); + + test("completes to that future's error, even if it's not closed", () { + futureGroup.add(Future.error('error')); + expect(futureGroup.future, throwsA('error')); + }); + }); + + test('completes once all contained futures complete', () async { + var completer1 = Completer(); + var completer2 = Completer(); + var completer3 = Completer(); + + futureGroup.add(completer1.future); + futureGroup.add(completer2.future); + futureGroup.add(completer3.future); + futureGroup.close(); + + var completed = false; + futureGroup.future.then((_) => completed = true); + + completer1.complete(); + await flushMicrotasks(); + expect(completed, isFalse); + + completer2.complete(); + await flushMicrotasks(); + expect(completed, isFalse); + + completer3.complete(); + await flushMicrotasks(); + expect(completed, isTrue); + }); + + test('completes to the values of the futures in order of addition', () { + var completer1 = Completer(); + var completer2 = Completer(); + var completer3 = Completer(); + + futureGroup.add(completer1.future); + futureGroup.add(completer2.future); + futureGroup.add(completer3.future); + futureGroup.close(); + + // Complete the completers in reverse order to prove that that doesn't + // affect the result order. + completer3.complete(3); + completer2.complete(2); + completer1.complete(1); + expect(futureGroup.future, completion(equals([1, 2, 3]))); + }); + + test("completes to the first error to be emitted, even if it's not closed", + () { + var completer1 = Completer(); + var completer2 = Completer(); + var completer3 = Completer(); + + futureGroup.add(completer1.future); + futureGroup.add(completer2.future); + futureGroup.add(completer3.future); + + completer2.completeError('error 2'); + completer1.completeError('error 1'); + expect(futureGroup.future, throwsA('error 2')); + }); + + group('onIdle:', () { + test('emits an event when the last pending future completes', () async { + var idle = false; + futureGroup.onIdle.listen((_) => idle = true); + + var completer1 = Completer(); + var completer2 = Completer(); + var completer3 = Completer(); + + futureGroup.add(completer1.future); + futureGroup.add(completer2.future); + futureGroup.add(completer3.future); + + await flushMicrotasks(); + expect(idle, isFalse); + expect(futureGroup.isIdle, isFalse); + + completer1.complete(); + await flushMicrotasks(); + expect(idle, isFalse); + expect(futureGroup.isIdle, isFalse); + + completer2.complete(); + await flushMicrotasks(); + expect(idle, isFalse); + expect(futureGroup.isIdle, isFalse); + + completer3.complete(); + await flushMicrotasks(); + expect(idle, isTrue); + expect(futureGroup.isIdle, isTrue); + }); + + test('emits an event each time it becomes idle', () async { + var idle = false; + futureGroup.onIdle.listen((_) => idle = true); + + var completer = Completer(); + futureGroup.add(completer.future); + + completer.complete(); + await flushMicrotasks(); + expect(idle, isTrue); + expect(futureGroup.isIdle, isTrue); + + idle = false; + completer = Completer(); + futureGroup.add(completer.future); + + await flushMicrotasks(); + expect(idle, isFalse); + expect(futureGroup.isIdle, isFalse); + + completer.complete(); + await flushMicrotasks(); + expect(idle, isTrue); + expect(futureGroup.isIdle, isTrue); + }); + + test('emits an event when the group closes', () async { + // It's important that the order of events here stays consistent over + // time, since code may rely on it in subtle ways. + var idle = false; + var onIdleDone = false; + var futureFired = false; + + futureGroup.onIdle.listen(expectAsync1((_) { + expect(futureFired, isFalse); + idle = true; + }), onDone: expectAsync0(() { + expect(idle, isTrue); + expect(futureFired, isFalse); + onIdleDone = true; + })); + + futureGroup.future.then(expectAsync1((_) { + expect(idle, isTrue); + expect(onIdleDone, isTrue); + futureFired = true; + })); + + var completer = Completer(); + futureGroup.add(completer.future); + futureGroup.close(); + + await flushMicrotasks(); + expect(idle, isFalse); + expect(futureGroup.isIdle, isFalse); + + completer.complete(); + await flushMicrotasks(); + expect(idle, isTrue); + expect(futureGroup.isIdle, isTrue); + expect(futureFired, isTrue); + }); + }); +} diff --git a/pkgs/async/test/io_sink_impl.dart b/pkgs/async/test/io_sink_impl.dart new file mode 100644 index 00000000..ccc23c22 --- /dev/null +++ b/pkgs/async/test/io_sink_impl.dart @@ -0,0 +1,26 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@Deprecated('Tests deprecated functionality') +library; + +import 'dart:io'; + +import 'package:async/async.dart'; + +/// This class isn't used, it's just used to verify that [IOSinkBase] produces a +/// valid implementation of [IOSink]. +class IOSinkImpl extends IOSinkBase implements IOSink { + @override + void onAdd(List data) {} + + @override + void onError(Object error, [StackTrace? stackTrace]) {} + + @override + void onClose() {} + + @override + Future onFlush() => Future.value(); +} diff --git a/pkgs/async/test/lazy_stream_test.dart b/pkgs/async/test/lazy_stream_test.dart new file mode 100644 index 00000000..9785b2e1 --- /dev/null +++ b/pkgs/async/test/lazy_stream_test.dart @@ -0,0 +1,102 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + test('calls the callback when the stream is listened', () async { + var callbackCalled = false; + var stream = LazyStream(expectAsync0(() { + callbackCalled = true; + return const Stream.empty(); + })); + + await flushMicrotasks(); + expect(callbackCalled, isFalse); + + stream.listen(null); + expect(callbackCalled, isTrue); + }); + + test('calls the callback when the stream is listened', () async { + var callbackCalled = false; + var stream = LazyStream(expectAsync0(() { + callbackCalled = true; + return const Stream.empty(); + })); + + await flushMicrotasks(); + expect(callbackCalled, isFalse); + + stream.listen(null); + expect(callbackCalled, isTrue); + }); + + test('forwards to a synchronously-provided stream', () async { + var controller = StreamController(); + var stream = LazyStream(expectAsync0(() => controller.stream)); + + var events = []; + stream.listen(events.add); + + controller.add(1); + await flushMicrotasks(); + expect(events, equals([1])); + + controller.add(2); + await flushMicrotasks(); + expect(events, equals([1, 2])); + + controller.add(3); + await flushMicrotasks(); + expect(events, equals([1, 2, 3])); + + controller.close(); + }); + + test('forwards to an asynchronously-provided stream', () async { + var controller = StreamController(); + var stream = LazyStream(expectAsync0(() async => controller.stream)); + + var events = []; + stream.listen(events.add); + + controller.add(1); + await flushMicrotasks(); + expect(events, equals([1])); + + controller.add(2); + await flushMicrotasks(); + expect(events, equals([1, 2])); + + controller.add(3); + await flushMicrotasks(); + expect(events, equals([1, 2, 3])); + + controller.close(); + }); + + test("a lazy stream can't be listened to multiple times", () { + var stream = LazyStream(expectAsync0(Stream.empty)); + expect(stream.isBroadcast, isFalse); + + stream.listen(null); + expect(() => stream.listen(null), throwsStateError); + expect(() => stream.listen(null), throwsStateError); + }); + + test("a lazy stream can't be listened to from within its callback", () { + late LazyStream stream; + stream = LazyStream(expectAsync0(() { + expect(() => stream.listen(null), throwsStateError); + return const Stream.empty(); + })); + stream.listen(null); + }); +} diff --git a/pkgs/async/test/null_stream_sink_test.dart b/pkgs/async/test/null_stream_sink_test.dart new file mode 100644 index 00000000..16d69866 --- /dev/null +++ b/pkgs/async/test/null_stream_sink_test.dart @@ -0,0 +1,113 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + group('constructors', () { + test('done defaults to a completed future', () { + var sink = NullStreamSink(); + expect(sink.done, completes); + }); + + test('a custom future may be passed to done', () async { + var completer = Completer(); + var sink = NullStreamSink(done: completer.future); + + var doneFired = false; + sink.done.then((_) { + doneFired = true; + }); + await flushMicrotasks(); + expect(doneFired, isFalse); + + completer.complete(); + await flushMicrotasks(); + expect(doneFired, isTrue); + }); + + test('NullStreamSink.error passes an error to done', () { + var sink = NullStreamSink.error('oh no'); + expect(sink.done, throwsA('oh no')); + }); + }); + + group('events', () { + test('are silently dropped before close', () { + var sink = NullStreamSink(); + sink.add(1); + sink.addError('oh no'); + }); + + test('throw StateErrors after close', () { + var sink = NullStreamSink(); + expect(sink.close(), completes); + + expect(() => sink.add(1), throwsStateError); + expect(() => sink.addError('oh no'), throwsStateError); + expect(() => sink.addStream(const Stream.empty()), throwsStateError); + }); + + group('addStream', () { + test('listens to the stream then cancels immediately', () async { + var sink = NullStreamSink(); + var canceled = false; + var controller = StreamController(onCancel: () { + canceled = true; + }); + + expect(sink.addStream(controller.stream), completes); + await flushMicrotasks(); + expect(canceled, isTrue); + }); + + test('returns the cancel future', () async { + var completer = Completer(); + var sink = NullStreamSink(); + var controller = StreamController(onCancel: () => completer.future); + + var addStreamFired = false; + sink.addStream(controller.stream).then((_) { + addStreamFired = true; + }); + await flushMicrotasks(); + expect(addStreamFired, isFalse); + + completer.complete(); + await flushMicrotasks(); + expect(addStreamFired, isTrue); + }); + + test('pipes errors from the cancel future through addStream', () async { + var sink = NullStreamSink(); + var controller = StreamController(onCancel: () => throw 'oh no'); + expect(sink.addStream(controller.stream), throwsA('oh no')); + }); + + test('causes events to throw StateErrors until the future completes', + () async { + var sink = NullStreamSink(); + var future = sink.addStream(const Stream.empty()); + expect(() => sink.add(1), throwsStateError); + expect(() => sink.addError('oh no'), throwsStateError); + expect(() => sink.addStream(const Stream.empty()), throwsStateError); + + await future; + sink.add(1); + sink.addError('oh no'); + expect(sink.addStream(const Stream.empty()), completes); + }); + }); + }); + + test('close returns the done future', () { + var sink = NullStreamSink.error('oh no'); + expect(sink.close(), throwsA('oh no')); + }); +} diff --git a/pkgs/async/test/reject_errors_test.dart b/pkgs/async/test/reject_errors_test.dart new file mode 100644 index 00000000..27e3c255 --- /dev/null +++ b/pkgs/async/test/reject_errors_test.dart @@ -0,0 +1,208 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE filevents. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +void main() { + late StreamController controller; + setUp(() { + controller = StreamController(); + }); + + test('passes through data events', () { + controller.sink.rejectErrors() + ..add(1) + ..add(2) + ..add(3); + expect(controller.stream, emitsInOrder([1, 2, 3])); + }); + + test('passes through close events', () { + controller.sink.rejectErrors() + ..add(1) + ..close(); + expect(controller.stream, emitsInOrder([1, emitsDone])); + }); + + test('passes through data events from addStream()', () { + controller.sink.rejectErrors().addStream(Stream.fromIterable([1, 2, 3])); + expect(controller.stream, emitsInOrder([1, 2, 3])); + }); + + test('allows multiple addStream() calls', () async { + var transformed = controller.sink.rejectErrors(); + await transformed.addStream(Stream.fromIterable([1, 2, 3])); + await transformed.addStream(Stream.fromIterable([4, 5, 6])); + expect(controller.stream, emitsInOrder([1, 2, 3, 4, 5, 6])); + }); + + group('on addError()', () { + test('forwards the error to done', () { + var transformed = controller.sink.rejectErrors(); + transformed.addError('oh no'); + expect(transformed.done, throwsA('oh no')); + }); + + test('closes the underlying sink', () { + var transformed = controller.sink.rejectErrors(); + transformed.addError('oh no'); + transformed.done.catchError((_) {}); + + expect(controller.stream, emitsDone); + }); + + test('ignores further events', () async { + var transformed = controller.sink.rejectErrors(); + transformed.addError('oh no'); + transformed.done.catchError((_) {}); + expect(controller.stream, emitsDone); + + // Try adding events synchronously and asynchronously and verify that they + // don't throw and also aren't passed to the underlying sink. + transformed + ..add(1) + ..addError('another'); + await pumpEventQueue(); + transformed + ..add(2) + ..addError('yet another'); + }); + + test('cancels the current subscription', () async { + var inputCanceled = false; + var inputController = + StreamController(onCancel: () => inputCanceled = true); + + var transformed = controller.sink.rejectErrors() + ..addStream(inputController.stream); + inputController.addError('oh no'); + transformed.done.catchError((_) {}); + + await pumpEventQueue(); + expect(inputCanceled, isTrue); + }); + }); + + group('when the inner sink\'s done future completes', () { + test('done completes', () async { + var completer = Completer(); + var transformed = NullStreamSink(done: completer.future).rejectErrors(); + + var doneCompleted = false; + transformed.done.then((_) => doneCompleted = true); + await pumpEventQueue(); + expect(doneCompleted, isFalse); + + completer.complete(); + await pumpEventQueue(); + expect(doneCompleted, isTrue); + }); + + test('an outstanding addStream() completes', () async { + var completer = Completer(); + var transformed = NullStreamSink(done: completer.future).rejectErrors(); + + var addStreamCompleted = false; + transformed + .addStream(StreamController().stream) + .then((_) => addStreamCompleted = true); + await pumpEventQueue(); + expect(addStreamCompleted, isFalse); + + completer.complete(); + await pumpEventQueue(); + expect(addStreamCompleted, isTrue); + }); + + test('an outstanding addStream()\'s subscription is cancelled', () async { + var completer = Completer(); + var transformed = NullStreamSink(done: completer.future).rejectErrors(); + + var addStreamCancelled = false; + transformed.addStream( + StreamController(onCancel: () => addStreamCancelled = true).stream); + await pumpEventQueue(); + expect(addStreamCancelled, isFalse); + + completer.complete(); + await pumpEventQueue(); + expect(addStreamCancelled, isTrue); + }); + + test('forwards an outstanding addStream()\'s cancellation error', () async { + var completer = Completer(); + var transformed = NullStreamSink(done: completer.future).rejectErrors(); + + expect( + transformed.addStream( + StreamController(onCancel: () => throw 'oh no').stream), + throwsA('oh no')); + completer.complete(); + }); + + group('forwards its error', () { + test('through done', () async { + expect(NullStreamSink(done: Future.error('oh no')).rejectErrors().done, + throwsA('oh no')); + }); + + test('through close', () async { + expect( + NullStreamSink(done: Future.error('oh no')).rejectErrors().close(), + throwsA('oh no')); + }); + }); + }); + + group('after closing', () { + test('throws on add()', () { + var sink = controller.sink.rejectErrors()..close(); + expect(() => sink.add(1), throwsStateError); + }); + + test('throws on addError()', () { + var sink = controller.sink.rejectErrors()..close(); + expect(() => sink.addError('oh no'), throwsStateError); + }); + + test('throws on addStream()', () { + var sink = controller.sink.rejectErrors()..close(); + expect(() => sink.addStream(const Stream.empty()), throwsStateError); + }); + + test('allows close()', () { + var sink = controller.sink.rejectErrors()..close(); + sink.close(); // Shouldn't throw + }); + }); + + group('during an active addStream()', () { + test('throws on add()', () { + var sink = controller.sink.rejectErrors() + ..addStream(StreamController().stream); + expect(() => sink.add(1), throwsStateError); + }); + + test('throws on addError()', () { + var sink = controller.sink.rejectErrors() + ..addStream(StreamController().stream); + expect(() => sink.addError('oh no'), throwsStateError); + }); + + test('throws on addStream()', () { + var sink = controller.sink.rejectErrors() + ..addStream(StreamController().stream); + expect(() => sink.addStream(const Stream.empty()), throwsStateError); + }); + + test('throws on close()', () { + var sink = controller.sink.rejectErrors() + ..addStream(StreamController().stream); + expect(() => sink.close(), throwsStateError); + }); + }); +} diff --git a/pkgs/async/test/restartable_timer_test.dart b/pkgs/async/test/restartable_timer_test.dart new file mode 100644 index 00000000..4aab2871 --- /dev/null +++ b/pkgs/async/test/restartable_timer_test.dart @@ -0,0 +1,107 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:async/async.dart'; +import 'package:fake_async/fake_async.dart'; +import 'package:test/test.dart'; + +void main() { + test('runs the callback once the duration has elapsed', () { + FakeAsync().run((async) { + var fired = false; + RestartableTimer(const Duration(seconds: 5), () { + fired = true; + }); + + async.elapse(const Duration(seconds: 4)); + expect(fired, isFalse); + + async.elapse(const Duration(seconds: 1)); + expect(fired, isTrue); + }); + }); + + test("doesn't run the callback if the timer is canceled", () { + FakeAsync().run((async) { + var fired = false; + var timer = RestartableTimer(const Duration(seconds: 5), () { + fired = true; + }); + + async.elapse(const Duration(seconds: 4)); + expect(fired, isFalse); + timer.cancel(); + + async.elapse(const Duration(seconds: 4)); + expect(fired, isFalse); + }); + }); + + test('resets the duration if the timer is reset before it fires', () { + FakeAsync().run((async) { + var fired = false; + var timer = RestartableTimer(const Duration(seconds: 5), () { + fired = true; + }); + + async.elapse(const Duration(seconds: 4)); + expect(fired, isFalse); + timer.reset(); + + async.elapse(const Duration(seconds: 4)); + expect(fired, isFalse); + + async.elapse(const Duration(seconds: 1)); + expect(fired, isTrue); + }); + }); + + test('re-runs the callback if the timer is reset after firing', () { + FakeAsync().run((async) { + var fired = 0; + var timer = RestartableTimer(const Duration(seconds: 5), () { + fired++; + }); + + async.elapse(const Duration(seconds: 5)); + expect(fired, equals(1)); + timer.reset(); + + async.elapse(const Duration(seconds: 5)); + expect(fired, equals(2)); + timer.reset(); + + async.elapse(const Duration(seconds: 5)); + expect(fired, equals(3)); + }); + }); + + test('runs the callback if the timer is reset after being canceled', () { + FakeAsync().run((async) { + var fired = false; + var timer = RestartableTimer(const Duration(seconds: 5), () { + fired = true; + }); + + async.elapse(const Duration(seconds: 4)); + expect(fired, isFalse); + timer.cancel(); + + async.elapse(const Duration(seconds: 4)); + expect(fired, isFalse); + timer.reset(); + + async.elapse(const Duration(seconds: 5)); + expect(fired, isTrue); + }); + }); + + test("only runs the callback once if the timer isn't reset", () { + FakeAsync().run((async) { + RestartableTimer( + const Duration(seconds: 5), expectAsync0(() {}, count: 1)); + async.elapse(const Duration(seconds: 10)); + }); + }); +} diff --git a/pkgs/async/test/result/result_captureAll_test.dart b/pkgs/async/test/result/result_captureAll_test.dart new file mode 100644 index 00000000..e85999e9 --- /dev/null +++ b/pkgs/async/test/result/result_captureAll_test.dart @@ -0,0 +1,195 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: file_names + +import 'dart:async'; +import 'dart:math' show Random; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +final someStack = StackTrace.current; + +Result res(int n) => Result.value(n); + +Result err(int n) => ErrorResult('$n', someStack); + +/// Helper function creating an iterable of futures. +Iterable> futures(int count, + {bool Function(int index)? throwWhen}) sync* { + for (var i = 0; i < count; i++) { + if (throwWhen != null && throwWhen(i)) { + yield Future.error('$i', someStack); + } else { + yield Future.value(i); + } + } +} + +void main() { + test('empty', () async { + var all = await Result.captureAll(futures(0)); + expect(all, []); + }); + + group('futures only,', () { + test('single', () async { + var all = await Result.captureAll(futures(1)); + expect(all, [res(0)]); + }); + + test('multiple', () async { + var all = await Result.captureAll(futures(3)); + expect(all, [res(0), res(1), res(2)]); + }); + + test('error only', () async { + var all = + await Result.captureAll(futures(1, throwWhen: (_) => true)); + expect(all, [err(0)]); + }); + + test('multiple error only', () async { + var all = + await Result.captureAll(futures(3, throwWhen: (_) => true)); + expect(all, [err(0), err(1), err(2)]); + }); + + test('mixed error and value', () async { + var all = + await Result.captureAll(futures(4, throwWhen: (x) => x.isOdd)); + expect(all, [res(0), err(1), res(2), err(3)]); + }); + + test('completion permutation 1-2-3', () async { + var cs = List.generate(3, (_) => Completer()); + var all = Result.captureAll(cs.map((c) => c.future)); + expect(all, completion([res(1), res(2), err(3)])); + await _microTask(); + cs[0].complete(1); + await _microTask(); + cs[1].complete(2); + await _microTask(); + cs[2].completeError('3', someStack); + }); + + test('completion permutation 1-3-2', () async { + var cs = List.generate(3, (_) => Completer()); + var all = Result.captureAll(cs.map((c) => c.future)); + expect(all, completion([res(1), res(2), err(3)])); + await _microTask(); + cs[0].complete(1); + await _microTask(); + cs[2].completeError('3', someStack); + await _microTask(); + cs[1].complete(2); + }); + + test('completion permutation 2-1-3', () async { + var cs = List.generate(3, (_) => Completer()); + var all = Result.captureAll(cs.map((c) => c.future)); + expect(all, completion([res(1), res(2), err(3)])); + await _microTask(); + cs[1].complete(2); + await _microTask(); + cs[0].complete(1); + await _microTask(); + cs[2].completeError('3', someStack); + }); + + test('completion permutation 2-3-1', () async { + var cs = List.generate(3, (_) => Completer()); + var all = Result.captureAll(cs.map((c) => c.future)); + expect(all, completion([res(1), res(2), err(3)])); + await _microTask(); + cs[1].complete(2); + await _microTask(); + cs[2].completeError('3', someStack); + await _microTask(); + cs[0].complete(1); + }); + + test('completion permutation 3-1-2', () async { + var cs = List.generate(3, (_) => Completer()); + var all = Result.captureAll(cs.map((c) => c.future)); + expect(all, completion([res(1), res(2), err(3)])); + await _microTask(); + cs[2].completeError('3', someStack); + await _microTask(); + cs[0].complete(1); + await _microTask(); + cs[1].complete(2); + }); + + test('completion permutation 3-2-1', () async { + var cs = List.generate(3, (_) => Completer()); + var all = Result.captureAll(cs.map((c) => c.future)); + expect(all, completion([res(1), res(2), err(3)])); + await _microTask(); + cs[2].completeError('3', someStack); + await _microTask(); + cs[1].complete(2); + await _microTask(); + cs[0].complete(1); + }); + + var seed = Random().nextInt(0x100000000); + var n = 25; // max 32, otherwise rnd.nextInt(1< Completer()); + var all = Result.captureAll(cs.map((c) => c.future)); + var rnd = Random(seed); + var throwFlags = rnd.nextInt(1 << n); // Bit-flag for throwing. + bool throws(int index) => (throwFlags & (1 << index)) != 0; + var expected = List.generate(n, (x) => throws(x) ? err(x) : res(x)); + + expect(all, completion(expected)); + + var completeFunctions = List.generate(n, (i) { + var c = cs[i]; + return () => + throws(i) ? c.completeError('$i', someStack) : c.complete(i); + }); + completeFunctions.shuffle(rnd); + for (var i = 0; i < n; i++) { + await _microTask(); + completeFunctions[i](); + } + }); + }); + group('values only,', () { + test('single', () async { + var all = await Result.captureAll([1]); + expect(all, [res(1)]); + }); + test('multiple', () async { + var all = await Result.captureAll([1, 2, 3]); + expect(all, [res(1), res(2), res(3)]); + }); + }); + group('mixed futures and values,', () { + test('no error', () async { + var all = await Result.captureAll(>[ + 1, + Future(() => 2), + 3, + Future.value(4), + ]); + expect(all, [res(1), res(2), res(3), res(4)]); + }); + test('error', () async { + var all = await Result.captureAll(>[ + 1, + Future(() => 2), + 3, + Future(() async => await Future.error('4', someStack)), + Future.value(5) + ]); + expect(all, [res(1), res(2), res(3), err(4), res(5)]); + }); + }); +} + +Future _microTask() => Future.microtask(() {}); diff --git a/pkgs/async/test/result/result_flattenAll_test.dart b/pkgs/async/test/result/result_flattenAll_test.dart new file mode 100644 index 00000000..0d2b9634 --- /dev/null +++ b/pkgs/async/test/result/result_flattenAll_test.dart @@ -0,0 +1,57 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: file_names + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +final someStack = StackTrace.current; +Result res(T n) => Result.value(n); +Result err(int n) => ErrorResult('$n', someStack); + +/// Helper function creating an iterable of results. +Iterable> results(int count, + {bool Function(int index)? throwWhen}) sync* { + for (var i = 0; i < count; i++) { + if (throwWhen != null && throwWhen(i)) { + yield err(i); + } else { + yield res(i); + } + } +} + +void main() { + void expectAll(Result result, Result expectation) { + if (expectation.isError) { + expect(result, expectation); + } else { + expect(result.isValue, true); + expect(result.asValue!.value, expectation.asValue!.value); + } + } + + test('empty', () { + expectAll(Result.flattenAll(results(0)), res([])); + }); + test('single value', () { + expectAll(Result.flattenAll(results(1)), res([0])); + }); + test('single error', () { + expectAll( + Result.flattenAll(results(1, throwWhen: (_) => true)), err(0)); + }); + test('multiple values', () { + expectAll(Result.flattenAll(results(5)), res([0, 1, 2, 3, 4])); + }); + test('multiple errors', () { + expectAll(Result.flattenAll(results(5, throwWhen: (x) => x.isOdd)), + err(1)); // First error is result. + }); + test('error last', () { + expectAll( + Result.flattenAll(results(5, throwWhen: (x) => x == 4)), err(4)); + }); +} diff --git a/pkgs/async/test/result/result_future_test.dart b/pkgs/async/test/result/result_future_test.dart new file mode 100644 index 00000000..de218840 --- /dev/null +++ b/pkgs/async/test/result/result_future_test.dart @@ -0,0 +1,44 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:stack_trace/stack_trace.dart'; +import 'package:test/test.dart'; + +void main() { + late Completer completer; + late ResultFuture future; + setUp(() { + completer = Completer(); + future = ResultFuture(completer.future); + }); + + test('before completion, result is null', () { + expect(future.result, isNull); + }); + + test('after successful completion, result is the value of the future', () { + completer.complete(12); + + // The completer calls its listeners asynchronously. We have to wait + // before we can access the result. + expect(future.then((_) => future.result!.asValue!.value), + completion(equals(12))); + }); + + test("after an error completion, result is the future's error", () { + var trace = Trace.current(); + completer.completeError('error', trace); + + // The completer calls its listeners asynchronously. We have to wait + // before we can access the result. + return future.catchError((_) {}).then((_) { + var error = future.result!.asError!; + expect(error.error, equals('error')); + expect(error.stackTrace, equals(trace)); + }); + }); +} diff --git a/pkgs/async/test/result/result_test.dart b/pkgs/async/test/result/result_test.dart new file mode 100644 index 00000000..13a5d536 --- /dev/null +++ b/pkgs/async/test/result/result_test.dart @@ -0,0 +1,358 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:collection'; + +import 'package:async/async.dart'; +import 'package:stack_trace/stack_trace.dart'; +import 'package:test/test.dart'; + +void main() { + var stack = Trace.current(); + + test('create result value', () { + var result = Result.value(42); + expect(result.isValue, isTrue); + expect(result.isError, isFalse); + ValueResult value = result.asValue!; + expect(value.value, equals(42)); + }); + + test('create result value 2', () { + Result result = ValueResult(42); + expect(result.isValue, isTrue); + expect(result.isError, isFalse); + var value = result.asValue!; + expect(value.value, equals(42)); + }); + + test('create result error', () { + var result = Result.error('BAD', stack); + expect(result.isValue, isFalse); + expect(result.isError, isTrue); + var error = result.asError!; + expect(error.error, equals('BAD')); + expect(error.stackTrace, same(stack)); + }); + + test('create result error 2', () { + var result = ErrorResult('BAD', stack); + expect(result.isValue, isFalse); + expect(result.isError, isTrue); + var error = result.asError; + expect(error.error, equals('BAD')); + expect(error.stackTrace, same(stack)); + }); + + test('create result error no stack', () { + var result = Result.error('BAD'); + expect(result.isValue, isFalse); + expect(result.isError, isTrue); + var error = result.asError!; + expect(error.error, equals('BAD')); + // A default stack trace is created + expect(error.stackTrace, isNotNull); + }); + + test('complete with value', () { + Result result = ValueResult(42); + var c = Completer(); + c.future.then(expectAsync1((int v) { + expect(v, equals(42)); + }), onError: (Object? e, s) { + fail('Unexpected error'); + }); + result.complete(c); + }); + + test('complete with error', () { + Result result = ErrorResult('BAD', stack); + var c = Completer(); + c.future.then((bool v) { + fail('Unexpected value $v'); + }).then((_) {}, onError: expectAsync2((e, s) { + expect(e, equals('BAD')); + expect(s, same(stack)); + })); + result.complete(c); + }); + + test('add sink value', () { + var result = ValueResult(42); + EventSink sink = TestSink(onData: expectAsync1((v) { + expect(v, equals(42)); + })); + result.addTo(sink); + }); + + test('add sink error', () { + Result result = ErrorResult('BAD', stack); + EventSink sink = TestSink(onError: expectAsync2((e, s) { + expect(e, equals('BAD')); + expect(s, same(stack)); + })); + result.addTo(sink); + }); + + test('value as future', () { + Result result = ValueResult(42); + result.asFuture.then(expectAsync1((int v) { + expect(v, equals(42)); + }), onError: (Object? e, s) { + fail('Unexpected error'); + }); + }); + + test('error as future', () { + Result result = ErrorResult('BAD', stack); + result.asFuture.then((bool v) { + fail('Unexpected value $v'); + }).then((_) {}, onError: expectAsync2((e, s) { + expect(e, equals('BAD')); + expect(s, same(stack)); + })); + }); + + test('capture future value', () { + var value = Future.value(42); + Result.capture(value).then(expectAsync1((Result result) { + expect(result.isValue, isTrue); + expect(result.isError, isFalse); + var value = result.asValue!; + expect(value.value, equals(42)); + }), onError: (Object? e, s) { + fail('Unexpected error: $e'); + }); + }); + + test('capture future error', () { + var value = Future.error('BAD', stack); + Result.capture(value).then(expectAsync1((Result result) { + expect(result.isValue, isFalse); + expect(result.isError, isTrue); + var error = result.asError!; + expect(error.error, equals('BAD')); + expect(error.stackTrace, same(stack)); + }), onError: (Object? e, s) { + fail('Unexpected error: $e'); + }); + }); + + test('release future value', () { + var future = Future>.value(Result.value(42)); + Result.release(future).then(expectAsync1((v) { + expect(v, equals(42)); + }), onError: (Object? e, s) { + fail('Unexpected error: $e'); + }); + }); + + test('release future error', () { + // An error in the result is unwrapped and reified by release. + var future = Future>.value(Result.error('BAD', stack)); + Result.release(future).then((v) { + fail('Unexpected value: $v'); + }).then((_) {}, onError: expectAsync2((e, s) { + expect(e, equals('BAD')); + expect(s, same(stack)); + })); + }); + + test('release future real error', () { + // An error in the error lane is passed through by release. + var future = Future>.error('BAD', stack); + Result.release(future).then((v) { + fail('Unexpected value: $v'); + }).then((_) {}, onError: expectAsync2((e, s) { + expect(e, equals('BAD')); + expect(s, same(stack)); + })); + }); + + test('capture stream', () { + var c = StreamController(); + var stream = Result.captureStream(c.stream); + var expectedList = Queue.of( + [Result.value(42), Result.error('BAD', stack), Result.value(37)]); + void listener(Result actual) { + expect(expectedList.isEmpty, isFalse); + expectResult(actual, expectedList.removeFirst()); + } + + stream.listen(expectAsync1(listener, count: 3), + onDone: expectAsync0(() {}), cancelOnError: true); + c.add(42); + c.addError('BAD', stack); + c.add(37); + c.close(); + }); + + test('release stream', () { + var c = StreamController>(); + var stream = Result.releaseStream(c.stream); + var events = [ + Result.value(42), + Result.error('BAD', stack), + Result.value(37) + ]; + // Expect the data events, and an extra error event. + var expectedList = Queue.of(events)..add(Result.error('BAD2', stack)); + + void dataListener(int v) { + expect(expectedList.isEmpty, isFalse); + Result expected = expectedList.removeFirst(); + expect(expected.isValue, isTrue); + expect(v, equals(expected.asValue!.value)); + } + + void errorListener(Object error, StackTrace stackTrace) { + expect(expectedList.isEmpty, isFalse); + Result expected = expectedList.removeFirst(); + expect(expected.isError, isTrue); + expect(error, equals(expected.asError!.error)); + expect(stackTrace, same(expected.asError!.stackTrace)); + } + + stream.listen(expectAsync1(dataListener, count: 2), + onError: expectAsync2(errorListener, count: 2), + onDone: expectAsync0(() {})); + for (var result in events) { + c.add(result); // Result value or error in data line. + } + c.addError('BAD2', stack); // Error in error line. + c.close(); + }); + + test('release stream cancel on error', () { + var c = StreamController>(); + var stream = Result.releaseStream(c.stream); + stream.listen(expectAsync1((v) { + expect(v, equals(42)); + }), onError: expectAsync2((e, s) { + expect(e, equals('BAD')); + expect(s, same(stack)); + }), onDone: () { + fail('Unexpected done event'); + }, cancelOnError: true); + c.add(Result.value(42)); + c.add(Result.error('BAD', stack)); + c.add(Result.value(37)); + c.close(); + }); + + test('flatten error 1', () { + var error = Result.error('BAD', stack); + var flattened = Result.flatten(Result>.error('BAD', stack)); + expectResult(flattened, error); + }); + + test('flatten error 2', () { + var error = Result.error('BAD', stack); + var result = Result>.value(error); + var flattened = Result.flatten(result); + expectResult(flattened, error); + }); + + test('flatten value', () { + var result = Result>.value(Result.value(42)); + expectResult(Result.flatten(result), Result.value(42)); + }); + + test('handle unary', () { + var result = ErrorResult('error', stack); + var called = false; + result.handle((Object? error) { + called = true; + expect(error, 'error'); + }); + expect(called, isTrue); + }); + + test('handle binary', () { + var result = ErrorResult('error', stack); + var called = false; + result.handle((Object? error, Object? stackTrace) { + called = true; + expect(error, 'error'); + expect(stackTrace, same(stack)); + }); + expect(called, isTrue); + }); + + test('handle unary and binary', () { + var result = ErrorResult('error', stack); + var called = false; + result.handle((Object? error, [Object? stackTrace]) { + called = true; + expect(error, 'error'); + expect(stackTrace, same(stack)); + }); + expect(called, isTrue); + }); + + test('handle neither unary nor binary', () { + var result = ErrorResult('error', stack); + expect(() => result.handle(() => fail('unreachable')), throwsA(anything)); + expect(() => result.handle((a, b, c) => fail('unreachable')), + throwsA(anything)); + expect(() => result.handle((a, b, {c}) => fail('unreachable')), + throwsA(anything)); + expect(() => result.handle((a, {b}) => fail('unreachable')), + throwsA(anything)); + expect(() => result.handle(({a, b}) => fail('unreachable')), + throwsA(anything)); + expect( + () => result.handle(({a}) => fail('unreachable')), throwsA(anything)); + }); +} + +void expectResult(Result actual, Result expected) { + expect(actual.isValue, equals(expected.isValue)); + expect(actual.isError, equals(expected.isError)); + if (actual.isValue) { + expect(actual.asValue!.value, equals(expected.asValue!.value)); + } else { + expect(actual.asError!.error, equals(expected.asError!.error)); + expect(actual.asError!.stackTrace, same(expected.asError!.stackTrace)); + } +} + +class TestSink implements EventSink { + final void Function(T) onData; + final void Function(dynamic, StackTrace) onError; + final void Function() onDone; + + TestSink( + {this.onData = _nullData, + this.onError = _nullError, + this.onDone = _nullDone}); + + @override + void add(T value) { + onData(value); + } + + @override + void addError(Object error, [StackTrace? stack]) { + onError(error, stack ?? StackTrace.fromString('')); + } + + @override + void close() { + onDone(); + } + + static void _nullData(dynamic value) { + fail('Unexpected sink add: $value'); + } + + static void _nullError(dynamic e, StackTrace s) { + fail('Unexpected sink addError: $e'); + } + + static void _nullDone() { + fail('Unepxected sink close'); + } +} diff --git a/pkgs/async/test/single_subscription_transformer_test.dart b/pkgs/async/test/single_subscription_transformer_test.dart new file mode 100644 index 00000000..95b321b9 --- /dev/null +++ b/pkgs/async/test/single_subscription_transformer_test.dart @@ -0,0 +1,50 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + test("buffers events as soon as it's bound", () async { + var controller = StreamController.broadcast(); + var stream = + controller.stream.transform(const SingleSubscriptionTransformer()); + + // Add events before [stream] has a listener to be sure it buffers them. + controller.add(1); + controller.add(2); + await flushMicrotasks(); + + expect(stream.toList(), completion(equals([1, 2, 3, 4]))); + await flushMicrotasks(); + + controller.add(3); + controller.add(4); + controller.close(); + }); + + test("cancels the subscription to the broadcast stream when it's canceled", + () async { + var canceled = false; + var controller = StreamController.broadcast(onCancel: () { + canceled = true; + }); + var stream = + controller.stream.transform(const SingleSubscriptionTransformer()); + await flushMicrotasks(); + expect(canceled, isFalse); + + var subscription = stream.listen(null); + await flushMicrotasks(); + expect(canceled, isFalse); + + subscription.cancel(); + await flushMicrotasks(); + expect(canceled, isTrue); + }); +} diff --git a/pkgs/async/test/sink_base_test.dart b/pkgs/async/test/sink_base_test.dart new file mode 100644 index 00000000..ee324f51 --- /dev/null +++ b/pkgs/async/test/sink_base_test.dart @@ -0,0 +1,404 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@Deprecated('Tests deprecated functionality') +library; + +import 'dart:async'; +import 'dart:convert'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +const int letterA = 0x41; + +void main() { + // We don't explicitly test [EventSinkBase] because it shares all the relevant + // implementation with [StreamSinkBase]. + group('StreamSinkBase', () { + test('forwards add() to onAdd()', () { + var sink = _StreamSink(onAdd: expectAsync1((value) { + expect(value, equals(123)); + })); + sink.add(123); + }); + + test('forwards addError() to onError()', () { + var sink = _StreamSink(onError: expectAsync2((error, [stackTrace]) { + expect(error, equals('oh no')); + expect(stackTrace, isA()); + })); + sink.addError('oh no', StackTrace.current); + }); + + test('forwards addStream() to onAdd() and onError()', () { + var sink = _StreamSink( + onAdd: expectAsync1((value) { + expect(value, equals(123)); + }, count: 1), + onError: expectAsync2((error, [stackTrace]) { + expect(error, equals('oh no')); + expect(stackTrace, isA()); + })); + + var controller = StreamController(); + sink.addStream(controller.stream); + + controller.add(123); + controller.addError('oh no', StackTrace.current); + }); + + test('addStream() returns once the stream closes', () async { + var sink = _StreamSink(); + var controller = StreamController(); + var addStreamCompleted = false; + sink.addStream(controller.stream).then((_) => addStreamCompleted = true); + + await pumpEventQueue(); + expect(addStreamCompleted, isFalse); + + controller.addError('oh no', StackTrace.current); + await pumpEventQueue(); + expect(addStreamCompleted, isFalse); + + controller.close(); + await pumpEventQueue(); + expect(addStreamCompleted, isTrue); + }); + + test('forwards close() to onClose()', () { + var sink = _StreamSink(onClose: expectAsync0(() {})); + expect(sink.close(), completes); + }); + + test('onClose() is only invoked once', () { + var sink = _StreamSink(onClose: expectAsync0(() {}, count: 1)); + expect(sink.close(), completes); + expect(sink.close(), completes); + expect(sink.close(), completes); + }); + + test('all invocations of close() return the same future', () async { + var completer = Completer(); + var sink = _StreamSink(onClose: expectAsync0(() => completer.future)); + + var close1Completed = false; + sink.close().then((_) => close1Completed = true); + + var close2Completed = false; + sink.close().then((_) => close2Completed = true); + + var doneCompleted = false; + sink.done.then((_) => doneCompleted = true); + + await pumpEventQueue(); + expect(close1Completed, isFalse); + expect(close2Completed, isFalse); + expect(doneCompleted, isFalse); + + completer.complete(); + await pumpEventQueue(); + expect(close1Completed, isTrue); + expect(close2Completed, isTrue); + expect(doneCompleted, isTrue); + }); + + test('done returns a future that completes once close() completes', + () async { + var completer = Completer(); + var sink = _StreamSink(onClose: expectAsync0(() => completer.future)); + + var doneCompleted = false; + sink.done.then((_) => doneCompleted = true); + + await pumpEventQueue(); + expect(doneCompleted, isFalse); + + expect(sink.close(), completes); + await pumpEventQueue(); + expect(doneCompleted, isFalse); + + completer.complete(); + await pumpEventQueue(); + expect(doneCompleted, isTrue); + }); + + group('during addStream()', () { + test('add() throws an error', () { + var sink = _StreamSink(onAdd: expectAsync1((_) {}, count: 0)); + sink.addStream(StreamController().stream); + expect(() => sink.add(1), throwsStateError); + }); + + test('addError() throws an error', () { + var sink = _StreamSink(onError: expectAsync2((_, [__]) {}, count: 0)); + sink.addStream(StreamController().stream); + expect(() => sink.addError('oh no'), throwsStateError); + }); + + test('addStream() throws an error', () { + var sink = _StreamSink(onAdd: expectAsync1((_) {}, count: 0)); + sink.addStream(StreamController().stream); + expect(() => sink.addStream(Stream.value(123)), throwsStateError); + }); + + test('close() throws an error', () { + var sink = _StreamSink(onClose: expectAsync0(() {}, count: 0)); + sink.addStream(StreamController().stream); + expect(() => sink.close(), throwsStateError); + }); + }); + + group("once it's closed", () { + test('add() throws an error', () { + var sink = _StreamSink(onAdd: expectAsync1((_) {}, count: 0)); + expect(sink.close(), completes); + expect(() => sink.add(1), throwsStateError); + }); + + test('addError() throws an error', () { + var sink = _StreamSink(onError: expectAsync2((_, [__]) {}, count: 0)); + expect(sink.close(), completes); + expect(() => sink.addError('oh no'), throwsStateError); + }); + + test('addStream() throws an error', () { + var sink = _StreamSink(onAdd: expectAsync1((_) {}, count: 0)); + expect(sink.close(), completes); + expect(() => sink.addStream(Stream.value(123)), throwsStateError); + }); + }); + }); + + group('IOSinkBase', () { + group('write()', () { + test("doesn't call add() for the empty string", () async { + var sink = _IOSink(onAdd: expectAsync1((_) {}, count: 0)); + sink.write(''); + }); + + test('converts the text to data and passes it to add', () async { + var sink = _IOSink(onAdd: expectAsync1((data) { + expect(data, equals(utf8.encode('hello'))); + })); + sink.write('hello'); + }); + + test('calls Object.toString()', () async { + var sink = _IOSink(onAdd: expectAsync1((data) { + expect(data, equals(utf8.encode('123'))); + })); + sink.write(123); + }); + + test('respects the encoding', () async { + var sink = _IOSink( + onAdd: expectAsync1((data) { + expect(data, equals(latin1.encode('Æ'))); + }), + encoding: latin1); + sink.write('Æ'); + }); + + test('throws if the sink is closed', () async { + var sink = _IOSink(onAdd: expectAsync1((_) {}, count: 0)); + expect(sink.close(), completes); + expect(() => sink.write('hello'), throwsStateError); + }); + }); + + group('writeAll()', () { + test('writes nothing for an empty iterable', () async { + var sink = _IOSink(onAdd: expectAsync1((_) {}, count: 0)); + sink.writeAll([]); + }); + + test('writes each object in the iterable', () async { + var chunks = >[]; + var sink = _IOSink( + onAdd: expectAsync1((data) { + chunks.add(data); + }, count: 3)); + + sink.writeAll(['hello', null, 123]); + expect(chunks, equals(['hello', 'null', '123'].map(utf8.encode))); + }); + + test('writes separators between each object', () async { + var chunks = >[]; + var sink = _IOSink( + onAdd: expectAsync1((data) { + chunks.add(data); + }, count: 5)); + + sink.writeAll(['hello', null, 123], '/'); + expect(chunks, + equals(['hello', '/', 'null', '/', '123'].map(utf8.encode))); + }); + + test('throws if the sink is closed', () async { + var sink = _IOSink(onAdd: expectAsync1((_) {}, count: 0)); + expect(sink.close(), completes); + expect(() => sink.writeAll(['hello']), throwsStateError); + }); + }); + + group('writeln()', () { + test('only writes a newline by default', () async { + var sink = _IOSink( + onAdd: expectAsync1((data) { + expect(data, equals(utf8.encode('\n'))); + }, count: 1)); + sink.writeln(); + }); + + test('writes the object followed by a newline', () async { + var chunks = >[]; + var sink = _IOSink( + onAdd: expectAsync1((data) { + chunks.add(data); + }, count: 2)); + sink.writeln(123); + + expect(chunks, equals(['123', '\n'].map(utf8.encode))); + }); + + test('throws if the sink is closed', () async { + var sink = _IOSink(onAdd: expectAsync1((_) {}, count: 0)); + expect(sink.close(), completes); + expect(() => sink.writeln(), throwsStateError); + }); + }); + + group('writeCharCode()', () { + test('writes the character code', () async { + var sink = _IOSink(onAdd: expectAsync1((data) { + expect(data, equals(utf8.encode('A'))); + })); + sink.writeCharCode(letterA); + }); + + test('respects the encoding', () async { + var sink = _IOSink( + onAdd: expectAsync1((data) { + expect(data, equals(latin1.encode('Æ'))); + }), + encoding: latin1); + sink.writeCharCode('Æ'.runes.first); + }); + + test('throws if the sink is closed', () async { + var sink = _IOSink(onAdd: expectAsync1((_) {}, count: 0)); + expect(sink.close(), completes); + expect(() => sink.writeCharCode(letterA), throwsStateError); + }); + }); + + group('flush()', () { + test('returns a future that completes when onFlush() is done', () async { + var completer = Completer(); + var sink = _IOSink(onFlush: expectAsync0(() => completer.future)); + + var flushDone = false; + sink.flush().then((_) => flushDone = true); + + await pumpEventQueue(); + expect(flushDone, isFalse); + + completer.complete(); + await pumpEventQueue(); + expect(flushDone, isTrue); + }); + + test('does nothing after close() is called', () { + var sink = _IOSink(onFlush: expectAsync0(Future.value, count: 0)); + expect(sink.close(), completes); + expect(sink.flush(), completes); + }); + + test("can't be called during addStream()", () { + var sink = _IOSink(onFlush: expectAsync0(Future.value, count: 0)); + sink.addStream(StreamController>().stream); + expect(() => sink.flush(), throwsStateError); + }); + + test('locks the sink as though a stream was being added', () { + var sink = + _IOSink(onFlush: expectAsync0(() => Completer().future)); + sink.flush(); + expect(() => sink.add([0]), throwsStateError); + expect(() => sink.addError('oh no'), throwsStateError); + expect(() => sink.addStream(const Stream.empty()), throwsStateError); + expect(() => sink.flush(), throwsStateError); + expect(() => sink.close(), throwsStateError); + }); + }); + }); +} + +/// A subclass of [StreamSinkBase] that takes all the overridable methods as +/// callbacks, for ease of testing. +class _StreamSink extends StreamSinkBase { + final void Function(int value) _onAdd; + final void Function(Object error, [StackTrace? stackTrace]) _onError; + final FutureOr Function() _onClose; + + _StreamSink( + {void Function(int value)? onAdd, + void Function(Object error, [StackTrace? stackTrace])? onError, + FutureOr Function()? onClose}) + : _onAdd = onAdd ?? ((_) {}), + _onError = onError ?? ((_, [__]) {}), + _onClose = onClose ?? (() {}); + + @override + void onAdd(int value) { + _onAdd(value); + } + + @override + void onError(Object error, [StackTrace? stackTrace]) { + _onError(error, stackTrace); + } + + @override + FutureOr onClose() => _onClose(); +} + +/// A subclass of [IOSinkBase] that takes all the overridable methods as +/// callbacks, for ease of testing. +class _IOSink extends IOSinkBase { + final void Function(List value) _onAdd; + final void Function(Object error, [StackTrace? stackTrace]) _onError; + final FutureOr Function() _onClose; + final Future Function() _onFlush; + + _IOSink( + {void Function(List value)? onAdd, + void Function(Object error, [StackTrace? stackTrace])? onError, + FutureOr Function()? onClose, + Future Function()? onFlush, + Encoding encoding = utf8}) + : _onAdd = onAdd ?? ((_) {}), + _onError = onError ?? ((_, [__]) {}), + _onClose = onClose ?? (() {}), + _onFlush = onFlush ?? Future.value, + super(encoding); + + @override + void onAdd(List value) { + _onAdd(value); + } + + @override + void onError(Object error, [StackTrace? stackTrace]) { + _onError(error, stackTrace); + } + + @override + FutureOr onClose() => _onClose(); + + @override + Future onFlush() => _onFlush(); +} diff --git a/pkgs/async/test/stream_closer_test.dart b/pkgs/async/test/stream_closer_test.dart new file mode 100644 index 00000000..a2bad1a9 --- /dev/null +++ b/pkgs/async/test/stream_closer_test.dart @@ -0,0 +1,208 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + late StreamCloser closer; + setUp(() { + closer = StreamCloser(); + }); + + group('when the closer is never closed', () { + test('forwards data and done events', () { + expect( + createStream().transform(closer).toList(), completion([1, 2, 3, 4])); + }); + + test('forwards error events', () { + expect(Stream.error('oh no').transform(closer).toList(), + throwsA('oh no')); + }); + + test('transforms a broadcast stream into a broadcast stream', () { + expect(const Stream.empty().transform(closer).isBroadcast, isTrue); + }); + + test("doesn't eagerly listen", () { + var controller = StreamController(); + var transformed = controller.stream.transform(closer); + expect(controller.hasListener, isFalse); + + transformed.listen(null); + expect(controller.hasListener, isTrue); + }); + + test('forwards pause and resume', () { + var controller = StreamController(); + var transformed = controller.stream.transform(closer); + + var subscription = transformed.listen(null); + expect(controller.isPaused, isFalse); + subscription.pause(); + expect(controller.isPaused, isTrue); + subscription.resume(); + expect(controller.isPaused, isFalse); + }); + + test('forwards cancel', () { + var isCancelled = false; + var controller = + StreamController(onCancel: () => isCancelled = true); + var transformed = controller.stream.transform(closer); + + expect(isCancelled, isFalse); + var subscription = transformed.listen(null); + expect(isCancelled, isFalse); + subscription.cancel(); + expect(isCancelled, isTrue); + }); + + test('forwards errors from cancel', () { + var controller = StreamController(onCancel: () => throw 'oh no'); + + expect(controller.stream.transform(closer).listen(null).cancel(), + throwsA('oh no')); + }); + }); + + group('when a stream is added before the closer is closed', () { + test('the stream emits a close event once the closer is closed', () async { + var queue = StreamQueue(createStream().transform(closer)); + await expectLater(queue, emits(1)); + await expectLater(queue, emits(2)); + expect(closer.close(), completes); + expect(queue, emitsDone); + }); + + test('the inner subscription is canceled once the closer is closed', () { + var isCancelled = false; + var controller = + StreamController(onCancel: () => isCancelled = true); + + expect(controller.stream.transform(closer), emitsDone); + expect(closer.close(), completes); + expect(isCancelled, isTrue); + }); + + test('closer.close() forwards errors from StreamSubscription.cancel()', () { + var controller = StreamController(onCancel: () => throw 'oh no'); + + expect(controller.stream.transform(closer), emitsDone); + expect(closer.close(), throwsA('oh no')); + }); + + test('closer.close() works even if a stream has already completed', + () async { + expect(await createStream().transform(closer).toList(), + equals([1, 2, 3, 4])); + expect(closer.close(), completes); + }); + + test('closer.close() works even if a stream has already been canceled', + () async { + createStream().transform(closer).listen(null).cancel(); + expect(closer.close(), completes); + }); + + group('but listened afterwards', () { + test('the output stream immediately emits done', () { + var stream = createStream().transform(closer); + expect(closer.close(), completes); + expect(stream, emitsDone); + }); + + test( + 'the underlying subscription is never listened if the stream is ' + 'never listened', () async { + var controller = + StreamController(onListen: expectAsync0(() {}, count: 0)); + controller.stream.transform(closer); + + expect(closer.close(), completes); + + await pumpEventQueue(); + }); + + test( + 'the underlying subscription is listened and then canceled once the ' + 'stream is listened', () { + var controller = StreamController( + onListen: expectAsync0(() {}), onCancel: expectAsync0(() {})); + var stream = controller.stream.transform(closer); + + expect(closer.close(), completes); + + stream.listen(null); + }); + + test('Subscription.cancel() errors are silently ignored', () async { + var controller = + StreamController(onCancel: expectAsync0(() => throw 'oh no')); + var stream = controller.stream.transform(closer); + + expect(closer.close(), completes); + + stream.listen(null); + await pumpEventQueue(); + }); + }); + }); + + group('when a stream is added after the closer is closed', () { + test('the output stream immediately emits done', () { + expect(closer.close(), completes); + expect(createStream().transform(closer), emitsDone); + }); + + test( + 'the underlying subscription is never listened if the stream is never ' + 'listened', () async { + expect(closer.close(), completes); + + var controller = + StreamController(onListen: expectAsync0(() {}, count: 0)); + controller.stream.transform(closer); + + await pumpEventQueue(); + }); + + test( + 'the underlying subscription is listened and then canceled once the ' + 'stream is listened', () { + expect(closer.close(), completes); + + var controller = StreamController( + onListen: expectAsync0(() {}), onCancel: expectAsync0(() {})); + + controller.stream.transform(closer).listen(null); + }); + + test('Subscription.cancel() errors are silently ignored', () async { + expect(closer.close(), completes); + + var controller = + StreamController(onCancel: expectAsync0(() => throw 'oh no')); + + controller.stream.transform(closer).listen(null); + + await pumpEventQueue(); + }); + }); +} + +Stream createStream() async* { + yield 1; + await flushMicrotasks(); + yield 2; + await flushMicrotasks(); + yield 3; + await flushMicrotasks(); + yield 4; +} diff --git a/pkgs/async/test/stream_completer_test.dart b/pkgs/async/test/stream_completer_test.dart new file mode 100644 index 00000000..f58162e4 --- /dev/null +++ b/pkgs/async/test/stream_completer_test.dart @@ -0,0 +1,365 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart' show StreamCompleter; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + test('a stream is linked before listening', () async { + var completer = StreamCompleter(); + completer.setSourceStream(createStream()); + expect(completer.stream.toList(), completion([1, 2, 3, 4])); + }); + + test('listened to before a stream is linked', () async { + var completer = StreamCompleter(); + var done = completer.stream.toList(); + await flushMicrotasks(); + completer.setSourceStream(createStream()); + expect(done, completion([1, 2, 3, 4])); + }); + + test("cancel before linking a stream doesn't listen on stream", () async { + var completer = StreamCompleter(); + var subscription = completer.stream.listen(null); + subscription.pause(); // Should be ignored. + subscription.cancel(); + completer.setSourceStream(UnusableStream()); // Doesn't throw. + }); + + test('listen and pause before linking stream', () async { + var controller = StreamCompleter(); + var events = []; + var subscription = controller.stream.listen(events.add); + var done = subscription.asFuture(); + subscription.pause(); + var sourceController = StreamController(); + sourceController + ..add(1) + ..add(2) + ..add(3) + ..add(4); + controller.setSourceStream(sourceController.stream); + await flushMicrotasks(); + expect(sourceController.hasListener, isTrue); + expect(sourceController.isPaused, isTrue); + expect(events, []); + subscription.resume(); + await flushMicrotasks(); + expect(sourceController.hasListener, isTrue); + expect(sourceController.isPaused, isFalse); + expect(events, [1, 2, 3, 4]); + sourceController.close(); + await done; + expect(events, [1, 2, 3, 4]); + }); + + test('pause more than once', () async { + var completer = StreamCompleter(); + var events = []; + var subscription = completer.stream.listen(events.add); + var done = subscription.asFuture(); + subscription.pause(); + subscription.pause(); + subscription.pause(); + completer.setSourceStream(createStream()); + for (var i = 0; i < 3; i++) { + await flushMicrotasks(); + expect(events, []); + subscription.resume(); + } + await done; + expect(events, [1, 2, 3, 4]); + }); + + test('cancel new stream before source is done', () async { + var completer = StreamCompleter(); + var lastEvent = -1; + var controller = StreamController(); + late StreamSubscription subscription; + subscription = completer.stream.listen((value) { + expect(value, lessThan(3)); + lastEvent = value; + if (value == 2) { + subscription.cancel(); + } + }, + onError: unreachable('error'), + onDone: unreachable('done'), + cancelOnError: true); + completer.setSourceStream(controller.stream); + expect(controller.hasListener, isTrue); + + await flushMicrotasks(); + expect(controller.hasListener, isTrue); + controller.add(1); + + await flushMicrotasks(); + expect(lastEvent, 1); + expect(controller.hasListener, isTrue); + controller.add(2); + + await flushMicrotasks(); + expect(lastEvent, 2); + expect(controller.hasListener, isFalse); + }); + + test('complete with setEmpty before listening', () async { + var completer = StreamCompleter(); + completer.setEmpty(); + var done = Completer(); + completer.stream.listen(unreachable('data'), + onError: unreachable('error'), onDone: done.complete); + await done.future; + }); + + test('complete with setEmpty after listening', () async { + var completer = StreamCompleter(); + var done = Completer(); + completer.stream.listen(unreachable('data'), + onError: unreachable('error'), onDone: done.complete); + completer.setEmpty(); + await done.future; + }); + + test("source stream isn't listened to until completer stream is", () async { + var completer = StreamCompleter(); + late StreamController controller; + controller = StreamController(onListen: () { + scheduleMicrotask(controller.close); + }); + + completer.setSourceStream(controller.stream); + await flushMicrotasks(); + expect(controller.hasListener, isFalse); + var subscription = completer.stream.listen(null); + expect(controller.hasListener, isTrue); + await subscription.asFuture(); + }); + + test('cancelOnError true when listening before linking stream', () async { + var completer = StreamCompleter(); + Object lastEvent = -1; + var controller = StreamController(); + completer.stream.listen((value) { + expect(value, lessThan(3)); + lastEvent = value; + }, onError: (Object value) { + expect(value, '3'); + lastEvent = value; + }, onDone: unreachable('done'), cancelOnError: true); + completer.setSourceStream(controller.stream); + expect(controller.hasListener, isTrue); + + await flushMicrotasks(); + expect(controller.hasListener, isTrue); + controller.add(1); + + await flushMicrotasks(); + expect(lastEvent, 1); + expect(controller.hasListener, isTrue); + controller.add(2); + + await flushMicrotasks(); + expect(lastEvent, 2); + expect(controller.hasListener, isTrue); + controller.addError('3'); + + await flushMicrotasks(); + expect(lastEvent, '3'); + expect(controller.hasListener, isFalse); + }); + + test('cancelOnError true when listening after linking stream', () async { + var completer = StreamCompleter(); + Object lastEvent = -1; + var controller = StreamController(); + completer.setSourceStream(controller.stream); + controller.add(1); + expect(controller.hasListener, isFalse); + + completer.stream.listen((value) { + expect(value, lessThan(3)); + lastEvent = value; + }, onError: (Object value) { + expect(value, '3'); + lastEvent = value; + }, onDone: unreachable('done'), cancelOnError: true); + + expect(controller.hasListener, isTrue); + + await flushMicrotasks(); + expect(lastEvent, 1); + expect(controller.hasListener, isTrue); + controller.add(2); + + await flushMicrotasks(); + expect(lastEvent, 2); + expect(controller.hasListener, isTrue); + controller.addError('3'); + + await flushMicrotasks(); + expect(controller.hasListener, isFalse); + }); + + test('linking a stream after setSourceStream before listen', () async { + var completer = StreamCompleter(); + completer.setSourceStream(createStream()); + expect(() => completer.setSourceStream(createStream()), throwsStateError); + expect(() => completer.setEmpty(), throwsStateError); + await completer.stream.toList(); + // Still fails after source is done + expect(() => completer.setSourceStream(createStream()), throwsStateError); + expect(() => completer.setEmpty(), throwsStateError); + }); + + test('linking a stream after setSourceStream after listen', () async { + var completer = StreamCompleter(); + var list = completer.stream.toList(); + completer.setSourceStream(createStream()); + expect(() => completer.setSourceStream(createStream()), throwsStateError); + expect(() => completer.setEmpty(), throwsStateError); + await list; + // Still fails after source is done. + expect(() => completer.setSourceStream(createStream()), throwsStateError); + expect(() => completer.setEmpty(), throwsStateError); + }); + + test('linking a stream after setEmpty before listen', () async { + var completer = StreamCompleter(); + completer.setEmpty(); + expect(() => completer.setSourceStream(createStream()), throwsStateError); + expect(() => completer.setEmpty(), throwsStateError); + await completer.stream.toList(); + // Still fails after source is done + expect(() => completer.setSourceStream(createStream()), throwsStateError); + expect(() => completer.setEmpty(), throwsStateError); + }); + + test('linking a stream after setEmpty() after listen', () async { + var completer = StreamCompleter(); + var list = completer.stream.toList(); + completer.setEmpty(); + expect(() => completer.setSourceStream(createStream()), throwsStateError); + expect(() => completer.setEmpty(), throwsStateError); + await list; + // Still fails after source is done. + expect(() => completer.setSourceStream(createStream()), throwsStateError); + expect(() => completer.setEmpty(), throwsStateError); + }); + + test('listening more than once after setting stream', () async { + var completer = StreamCompleter(); + completer.setSourceStream(createStream()); + var list = completer.stream.toList(); + expect(() => completer.stream.toList(), throwsStateError); + await list; + expect(() => completer.stream.toList(), throwsStateError); + }); + + test('listening more than once before setting stream', () async { + var completer = StreamCompleter(); + completer.stream.toList(); + expect(() => completer.stream.toList(), throwsStateError); + }); + + test('setting onData etc. before and after setting stream', () async { + var completer = StreamCompleter(); + var controller = StreamController(); + var subscription = completer.stream.listen(null); + Object lastEvent = 0; + subscription.onData((value) => lastEvent = value); + subscription.onError((Object value) => lastEvent = '$value'); + subscription.onDone(() => lastEvent = -1); + completer.setSourceStream(controller.stream); + await flushMicrotasks(); + controller.add(1); + await flushMicrotasks(); + expect(lastEvent, 1); + controller.addError(2); + await flushMicrotasks(); + expect(lastEvent, '2'); + subscription.onData((value) => lastEvent = -value); + subscription.onError((Object value) => lastEvent = '${-(value as int)}'); + controller.add(1); + await flushMicrotasks(); + expect(lastEvent, -1); + controller.addError(2); + await flushMicrotasks(); + expect(lastEvent, '-2'); + controller.close(); + await flushMicrotasks(); + expect(lastEvent, -1); + }); + + test('pause w/ resume future accross setting stream', () async { + var completer = StreamCompleter(); + var resume = Completer(); + var subscription = completer.stream.listen(unreachable('data')); + subscription.pause(resume.future); + await flushMicrotasks(); + completer.setSourceStream(createStream()); + await flushMicrotasks(); + resume.complete(); + var events = []; + subscription.onData(events.add); + await subscription.asFuture(); + expect(events, [1, 2, 3, 4]); + }); + + test('asFuture with error accross setting stream', () async { + var completer = StreamCompleter(); + var controller = StreamController(); + var subscription = + completer.stream.listen(unreachable('data'), cancelOnError: false); + var done = subscription.asFuture(); + expect(controller.hasListener, isFalse); + completer.setSourceStream(controller.stream); + await flushMicrotasks(); + expect(controller.hasListener, isTrue); + controller.addError(42); + await done.then(unreachable('data'), onError: (Object error) { + expect(error, 42); + }); + expect(controller.hasListener, isFalse); + }); + + group('setError()', () { + test('produces a stream that emits a single error', () { + var completer = StreamCompleter(); + completer.stream.listen(unreachable('data'), + onError: expectAsync2((error, stackTrace) { + expect(error, equals('oh no')); + }), onDone: expectAsync0(() {})); + + completer.setError('oh no'); + }); + + test('produces a stream that emits a single error on a later listen', + () async { + var completer = StreamCompleter(); + completer.setError('oh no'); + await flushMicrotasks(); + + completer.stream.listen(unreachable('data'), + onError: expectAsync2((error, stackTrace) { + expect(error, equals('oh no')); + }), onDone: expectAsync0(() {})); + }); + }); +} + +Stream createStream() async* { + yield 1; + await flushMicrotasks(); + yield 2; + await flushMicrotasks(); + yield 3; + await flushMicrotasks(); + yield 4; +} diff --git a/pkgs/async/test/stream_extensions_test.dart b/pkgs/async/test/stream_extensions_test.dart new file mode 100644 index 00000000..b43dedc1 --- /dev/null +++ b/pkgs/async/test/stream_extensions_test.dart @@ -0,0 +1,185 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE filevents. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +void main() { + group('.slices', () { + test('empty', () { + expect(const Stream.empty().slices(1).toList(), completion(equals([]))); + }); + + test('with the same length as the iterable', () { + expect( + Stream.fromIterable([1, 2, 3]).slices(3).toList(), + completion(equals([ + [1, 2, 3] + ]))); + }); + + test('with a longer length than the iterable', () { + expect( + Stream.fromIterable([1, 2, 3]).slices(5).toList(), + completion(equals([ + [1, 2, 3] + ]))); + }); + + test('with a shorter length than the iterable', () { + expect( + Stream.fromIterable([1, 2, 3]).slices(2).toList(), + completion(equals([ + [1, 2], + [3] + ]))); + }); + + test('with length divisible by the iterable\'s', () { + expect( + Stream.fromIterable([1, 2, 3, 4]).slices(2).toList(), + completion(equals([ + [1, 2], + [3, 4] + ]))); + }); + + test('refuses negative length', () { + expect(() => Stream.fromIterable([1]).slices(-1), throwsRangeError); + }); + + test('refuses length 0', () { + expect(() => Stream.fromIterable([1]).slices(0), throwsRangeError); + }); + }); + + group('.firstOrNull', () { + test('returns the first data event', () { + expect( + Stream.fromIterable([1, 2, 3, 4]).firstOrNull, completion(equals(1))); + }); + + test('returns the first error event', () { + expect(Stream.error('oh no').firstOrNull, throwsA('oh no')); + }); + + test('returns null for an empty stream', () { + expect(const Stream.empty().firstOrNull, completion(isNull)); + }); + + test('cancels the subscription after an event', () async { + var isCancelled = false; + var controller = StreamController(onCancel: () { + isCancelled = true; + }); + controller.add(1); + + await expectLater(controller.stream.firstOrNull, completion(equals(1))); + expect(isCancelled, isTrue); + }); + + test('cancels the subscription after an error', () async { + var isCancelled = false; + var controller = StreamController(onCancel: () { + isCancelled = true; + }); + controller.addError('oh no'); + + await expectLater(controller.stream.firstOrNull, throwsA('oh no')); + expect(isCancelled, isTrue); + }); + }); + + group('.listenAndBuffer', () { + test('emits events added before the listenAndBuffer is listened', () async { + var controller = StreamController() + ..add(1) + ..add(2) + ..add(3) + ..close(); + var stream = controller.stream.listenAndBuffer(); + await pumpEventQueue(); + + expectLater(stream, emitsInOrder([1, 2, 3, emitsDone])); + }); + + test('emits events added after the listenAndBuffer is listened', () async { + var controller = StreamController(); + var stream = controller.stream.listenAndBuffer(); + expectLater(stream, emitsInOrder([1, 2, 3, emitsDone])); + await pumpEventQueue(); + + controller + ..add(1) + ..add(2) + ..add(3) + ..close(); + }); + + test('emits events added before and after the listenAndBuffer is listened', + () async { + var controller = StreamController() + ..add(1) + ..add(2) + ..add(3); + var stream = controller.stream.listenAndBuffer(); + expectLater(stream, emitsInOrder([1, 2, 3, 4, 5, 6, emitsDone])); + await pumpEventQueue(); + + controller + ..add(4) + ..add(5) + ..add(6) + ..close(); + }); + + test('listens as soon as listenAndBuffer() is called', () async { + var listened = false; + var controller = StreamController(onListen: () { + listened = true; + }); + controller.stream.listenAndBuffer(); + expect(listened, isTrue); + }); + + test('forwards pause and resume', () async { + var controller = StreamController(); + var stream = controller.stream.listenAndBuffer(); + expect(controller.isPaused, isFalse); + var subscription = stream.listen(null); + expect(controller.isPaused, isFalse); + subscription.pause(); + expect(controller.isPaused, isTrue); + subscription.resume(); + expect(controller.isPaused, isFalse); + }); + + test('forwards cancel', () async { + var completer = Completer(); + var canceled = false; + var controller = StreamController(onCancel: () { + canceled = true; + return completer.future; + }); + var stream = controller.stream.listenAndBuffer(); + expect(canceled, isFalse); + var subscription = stream.listen(null); + expect(canceled, isFalse); + + var cancelCompleted = false; + subscription.cancel().then((_) { + cancelCompleted = true; + }); + expect(canceled, isTrue); + await pumpEventQueue(); + expect(cancelCompleted, isFalse); + + completer.complete(); + await pumpEventQueue(); + expect(cancelCompleted, isTrue); + }); + }); +} diff --git a/pkgs/async/test/stream_group_test.dart b/pkgs/async/test/stream_group_test.dart new file mode 100644 index 00000000..3700120e --- /dev/null +++ b/pkgs/async/test/stream_group_test.dart @@ -0,0 +1,926 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +void main() { + group('single-subscription', () { + late StreamGroup streamGroup; + setUp(() { + streamGroup = StreamGroup(); + }); + + test('buffers events from multiple sources', () async { + var controller1 = StreamController(); + streamGroup.add(controller1.stream); + controller1.add('first'); + controller1.close(); + + var controller2 = StreamController(); + streamGroup.add(controller2.stream); + controller2.add('second'); + controller2.close(); + + await flushMicrotasks(); + + expect(streamGroup.close(), completes); + + expect(streamGroup.stream.toList(), + completion(unorderedEquals(['first', 'second']))); + }); + + test('buffers errors from multiple sources', () async { + var controller1 = StreamController(); + streamGroup.add(controller1.stream); + controller1.addError('first'); + controller1.close(); + + var controller2 = StreamController(); + streamGroup.add(controller2.stream); + controller2.addError('second'); + controller2.close(); + + await flushMicrotasks(); + + expect(streamGroup.close(), completes); + + var transformed = streamGroup.stream.transform( + StreamTransformer.fromHandlers( + handleError: (error, _, sink) => sink.add('error: $error'))); + expect(transformed.toList(), + completion(equals(['error: first', 'error: second']))); + }); + + test('buffers events and errors together', () async { + var controller = StreamController(); + streamGroup.add(controller.stream); + + controller.add('first'); + controller.addError('second'); + controller.add('third'); + controller.addError('fourth'); + controller.addError('fifth'); + controller.add('sixth'); + controller.close(); + + await flushMicrotasks(); + + expect(streamGroup.close(), completes); + + var transformed = streamGroup.stream.transform( + StreamTransformer.fromHandlers( + handleData: (data, sink) => sink.add('data: $data'), + handleError: (error, _, sink) => sink.add('error: $error'))); + expect( + transformed.toList(), + completion(equals([ + 'data: first', + 'error: second', + 'data: third', + 'error: fourth', + 'error: fifth', + 'data: sixth' + ]))); + }); + + test("emits events once there's a listener", () { + var controller = StreamController(); + streamGroup.add(controller.stream); + + expect( + streamGroup.stream.toList(), completion(equals(['first', 'second']))); + + controller.add('first'); + controller.add('second'); + controller.close(); + + expect(streamGroup.close(), completes); + }); + + test("doesn't buffer events from a broadcast stream", () async { + var controller = StreamController.broadcast(); + streamGroup.add(controller.stream); + + controller.add('first'); + controller.add('second'); + controller.close(); + + await flushMicrotasks(); + + expect(streamGroup.close(), completes); + expect(streamGroup.stream.toList(), completion(isEmpty)); + }); + + test('when paused, buffers events from a broadcast stream', () async { + var controller = StreamController.broadcast(); + streamGroup.add(controller.stream); + + var events = []; + var subscription = streamGroup.stream.listen(events.add); + subscription.pause(); + + controller.add('first'); + controller.add('second'); + controller.close(); + await flushMicrotasks(); + + subscription.resume(); + expect(streamGroup.close(), completes); + await flushMicrotasks(); + + expect(events, equals(['first', 'second'])); + }); + + test("emits events from a broadcast stream once there's a listener", () { + var controller = StreamController.broadcast(); + streamGroup.add(controller.stream); + + expect( + streamGroup.stream.toList(), completion(equals(['first', 'second']))); + + controller.add('first'); + controller.add('second'); + controller.close(); + + expect(streamGroup.close(), completes); + }); + + test('forwards cancel errors', () async { + var subscription = streamGroup.stream.listen(null); + + var controller = StreamController(onCancel: () => throw 'error'); + streamGroup.add(controller.stream); + await flushMicrotasks(); + + expect(subscription.cancel(), throwsA('error')); + }); + + test('forwards a cancel future', () async { + var subscription = streamGroup.stream.listen(null); + + var completer = Completer(); + var controller = + StreamController(onCancel: () => completer.future); + streamGroup.add(controller.stream); + await flushMicrotasks(); + + var fired = false; + subscription.cancel().then((_) => fired = true); + + await flushMicrotasks(); + expect(fired, isFalse); + + completer.complete(); + await flushMicrotasks(); + expect(fired, isTrue); + }); + + test( + 'add() while active pauses the stream if the group is paused, then ' + 'resumes once the group resumes', () async { + var subscription = streamGroup.stream.listen(null); + await flushMicrotasks(); + + var paused = false; + var controller = StreamController( + onPause: () => paused = true, onResume: () => paused = false); + + subscription.pause(); + await flushMicrotasks(); + + streamGroup.add(controller.stream); + await flushMicrotasks(); + expect(paused, isTrue); + + subscription.resume(); + await flushMicrotasks(); + expect(paused, isFalse); + }); + + group('add() while canceled', () { + setUp(() async { + streamGroup.stream.listen(null).cancel(); + await flushMicrotasks(); + }); + + test('immediately listens to and cancels the stream', () async { + var listened = false; + var canceled = false; + var controller = StreamController(onListen: () { + listened = true; + }, onCancel: expectAsync0(() { + expect(listened, isTrue); + canceled = true; + })); + + streamGroup.add(controller.stream); + await flushMicrotasks(); + expect(listened, isTrue); + expect(canceled, isTrue); + }); + + test('forwards cancel errors', () { + var controller = + StreamController(onCancel: () => throw 'error'); + + expect(streamGroup.add(controller.stream), throwsA('error')); + }); + + test('forwards a cancel future', () async { + var completer = Completer(); + var controller = + StreamController(onCancel: () => completer.future); + + var fired = false; + streamGroup.add(controller.stream)!.then((_) => fired = true); + + await flushMicrotasks(); + expect(fired, isFalse); + + completer.complete(); + await flushMicrotasks(); + expect(fired, isTrue); + }); + }); + + group('when listen() throws an error', () { + late Stream alreadyListened; + setUp(() { + alreadyListened = Stream.value('foo')..listen(null); + }); + + group('listen()', () { + test('rethrows that error', () { + streamGroup.add(alreadyListened); + + // We can't use expect(..., throwsStateError) here bceause of + // dart-lang/sdk#45815. + runZonedGuarded( + () => streamGroup.stream.listen(expectAsync1((_) {}, count: 0)), + expectAsync2((error, _) => expect(error, isStateError))); + }); + + test('cancels other subscriptions', () async { + var firstCancelled = false; + var first = + StreamController(onCancel: () => firstCancelled = true); + streamGroup.add(first.stream); + + streamGroup.add(alreadyListened); + + var lastCancelled = false; + var last = + StreamController(onCancel: () => lastCancelled = true); + streamGroup.add(last.stream); + + runZonedGuarded(() => streamGroup.stream.listen(null), (_, __) {}); + + expect(firstCancelled, isTrue); + expect(lastCancelled, isTrue); + }); + + // There really shouldn't even be a subscription here, but due to + // dart-lang/sdk#45815 there is. + group('canceling the subscription is a no-op', () { + test('synchronously', () { + streamGroup.add(alreadyListened); + + var subscription = runZonedGuarded( + () => streamGroup.stream.listen(null), + expectAsync2((_, __) {}, count: 1)); + + expect(subscription!.cancel(), completes); + }); + + test('asynchronously', () async { + streamGroup.add(alreadyListened); + + var subscription = runZonedGuarded( + () => streamGroup.stream.listen(null), + expectAsync2((_, __) {}, count: 1)); + + await pumpEventQueue(); + expect(subscription!.cancel(), completes); + }); + }); + }); + }); + }); + + group('broadcast', () { + late StreamGroup streamGroup; + setUp(() { + streamGroup = StreamGroup.broadcast(); + }); + + test('buffers events from multiple sources', () async { + var controller1 = StreamController(); + streamGroup.add(controller1.stream); + controller1.add('first'); + controller1.close(); + + var controller2 = StreamController(); + streamGroup.add(controller2.stream); + controller2.add('second'); + controller2.close(); + + await flushMicrotasks(); + + expect(streamGroup.close(), completes); + + expect( + streamGroup.stream.toList(), completion(equals(['first', 'second']))); + }); + + test("emits events from multiple sources once there's a listener", () { + var controller1 = StreamController(); + streamGroup.add(controller1.stream); + + var controller2 = StreamController(); + streamGroup.add(controller2.stream); + + expect( + streamGroup.stream.toList(), completion(equals(['first', 'second']))); + + controller1.add('first'); + controller2.add('second'); + controller1.close(); + controller2.close(); + + expect(streamGroup.close(), completes); + }); + + test("doesn't buffer events once a listener has been added and removed", + () async { + var controller = StreamController(); + streamGroup.add(controller.stream); + + streamGroup.stream.listen(null).cancel(); + await flushMicrotasks(); + + controller.add('first'); + controller.addError('second'); + controller.close(); + + await flushMicrotasks(); + + expect(streamGroup.close(), completes); + expect(streamGroup.stream.toList(), completion(isEmpty)); + }); + + test("doesn't buffer events from a broadcast stream", () async { + var controller = StreamController.broadcast(); + streamGroup.add(controller.stream); + controller.add('first'); + controller.addError('second'); + controller.close(); + + await flushMicrotasks(); + + expect(streamGroup.close(), completes); + expect(streamGroup.stream.toList(), completion(isEmpty)); + }); + + test("emits events from a broadcast stream once there's a listener", () { + var controller = StreamController.broadcast(); + streamGroup.add(controller.stream); + + expect( + streamGroup.stream.toList(), completion(equals(['first', 'second']))); + + controller.add('first'); + controller.add('second'); + controller.close(); + + expect(streamGroup.close(), completes); + }); + + test('cancels and re-listens broadcast streams', () async { + var subscription = streamGroup.stream.listen(null); + + var controller = StreamController.broadcast(); + + streamGroup.add(controller.stream); + await flushMicrotasks(); + expect(controller.hasListener, isTrue); + + subscription.cancel(); + await flushMicrotasks(); + expect(controller.hasListener, isFalse); + + streamGroup.stream.listen(null); + await flushMicrotasks(); + expect(controller.hasListener, isTrue); + }); + + test( + 'listens on streams that follow single-subscription streams when ' + 'relistening after a cancel', () async { + var controller1 = StreamController(); + streamGroup.add(controller1.stream); + streamGroup.stream.listen(null).cancel(); + + var controller2 = StreamController(); + streamGroup.add(controller2.stream); + + var emitted = []; + streamGroup.stream.listen(emitted.add); + controller1.add('one'); + controller2.add('two'); + await flushMicrotasks(); + expect(emitted, ['one', 'two']); + }); + + test('never cancels single-subscription streams', () async { + var subscription = streamGroup.stream.listen(null); + + var controller = + StreamController(onCancel: expectAsync0(() {}, count: 0)); + + streamGroup.add(controller.stream); + await flushMicrotasks(); + + subscription.cancel(); + await flushMicrotasks(); + + streamGroup.stream.listen(null); + await flushMicrotasks(); + }); + + test('drops events from a single-subscription stream while dormant', + () async { + var events = []; + var subscription = streamGroup.stream.listen(events.add); + + var controller = StreamController(); + streamGroup.add(controller.stream); + await flushMicrotasks(); + + controller.add('first'); + await flushMicrotasks(); + expect(events, equals(['first'])); + + subscription.cancel(); + controller.add('second'); + await flushMicrotasks(); + expect(events, equals(['first'])); + + streamGroup.stream.listen(events.add); + controller.add('third'); + await flushMicrotasks(); + expect(events, equals(['first', 'third'])); + }); + + test('a single-subscription stream can be removed while dormant', () async { + var controller = StreamController(); + streamGroup.add(controller.stream); + await flushMicrotasks(); + + streamGroup.stream.listen(null).cancel(); + await flushMicrotasks(); + + streamGroup.remove(controller.stream); + expect(controller.hasListener, isFalse); + await flushMicrotasks(); + + expect(streamGroup.stream.toList(), completion(isEmpty)); + controller.add('first'); + expect(streamGroup.close(), completes); + }); + }); + + group('regardless of type', () { + group('single-subscription', () { + regardlessOfType(StreamGroup.new); + }); + + group('broadcast', () { + regardlessOfType(StreamGroup.broadcast); + }); + }); + + test('merge() emits events from all components streams', () async { + var controller1 = StreamController(); + var controller2 = StreamController(); + + var merged = StreamGroup.merge([controller1.stream, controller2.stream]); + + controller1.add('first'); + controller1.close(); + controller2.add('second'); + controller2.close(); + + expect(await merged.toList(), ['first', 'second']); + }); + + test('mergeBroadcast() emits events from all components streams', () async { + var controller1 = StreamController(); + var controller2 = StreamController(); + + var merged = + StreamGroup.mergeBroadcast([controller1.stream, controller2.stream]); + + controller1.add('first'); + controller1.close(); + controller2.add('second'); + controller2.close(); + + expect(merged.isBroadcast, isTrue); + + expect(await merged.toList(), ['first', 'second']); + }); +} + +void regardlessOfType(StreamGroup Function() newStreamGroup) { + late StreamGroup streamGroup; + setUp(() { + streamGroup = newStreamGroup(); + }); + + group('add()', () { + group('while dormant', () { + test("doesn't listen to the stream until the group is listened to", + () async { + var controller = StreamController(); + + expect(streamGroup.add(controller.stream), isNull); + await flushMicrotasks(); + expect(controller.hasListener, isFalse); + + streamGroup.stream.listen(null); + await flushMicrotasks(); + expect(controller.hasListener, isTrue); + }); + + test('is a no-op if the stream is already in the group', () { + var controller = StreamController(); + streamGroup.add(controller.stream); + streamGroup.add(controller.stream); + streamGroup.add(controller.stream); + + // If the stream was actually listened to multiple times, this would + // throw a StateError. + streamGroup.stream.listen(null); + }); + }); + + group('while active', () { + setUp(() async { + streamGroup.stream.listen(null); + await flushMicrotasks(); + }); + + test('listens to the stream immediately', () async { + var controller = StreamController(); + + expect(streamGroup.add(controller.stream), isNull); + await flushMicrotasks(); + expect(controller.hasListener, isTrue); + }); + + test('is a no-op if the stream is already in the group', () async { + var controller = StreamController(); + + // If the stream were actually listened to more than once, future + // calls to [add] would throw [StateError]s. + streamGroup.add(controller.stream); + streamGroup.add(controller.stream); + streamGroup.add(controller.stream); + }); + }); + }); + + group('remove()', () { + group('while dormant', () { + test("stops emitting events for a stream that's removed", () async { + var controller = StreamController(); + streamGroup.add(controller.stream); + + expect(streamGroup.stream.toList(), completion(equals(['first']))); + + controller.add('first'); + await flushMicrotasks(); + controller.add('second'); + + expect(streamGroup.remove(controller.stream), completion(null)); + expect(streamGroup.close(), completes); + }); + + test('is a no-op for an unknown stream', () { + var controller = StreamController(); + expect(streamGroup.remove(controller.stream), isNull); + }); + + test('and closed closes the group when the last stream is removed', + () async { + var controller1 = StreamController(); + var controller2 = StreamController(); + + streamGroup.add(controller1.stream); + streamGroup.add(controller2.stream); + await flushMicrotasks(); + + expect(streamGroup.isClosed, isFalse); + streamGroup.close(); + expect(streamGroup.isClosed, isTrue); + + streamGroup.remove(controller1.stream); + await flushMicrotasks(); + + streamGroup.remove(controller2.stream); + await flushMicrotasks(); + + expect(streamGroup.stream.toList(), completion(isEmpty)); + }); + }); + + group('while listening', () { + test("doesn't emit events from a removed stream", () { + var controller = StreamController(); + streamGroup.add(controller.stream); + + // The subscription to [controller.stream] is canceled synchronously, so + // the first event is dropped even though it was added before the + // removal. This is documented in [StreamGroup.remove]. + expect(streamGroup.stream.toList(), completion(isEmpty)); + + controller.add('first'); + expect(streamGroup.remove(controller.stream), completion(null)); + controller.add('second'); + + expect(streamGroup.close(), completes); + }); + + test("cancels the stream's subscription", () async { + var controller = StreamController(); + streamGroup.add(controller.stream); + + streamGroup.stream.listen(null); + await flushMicrotasks(); + expect(controller.hasListener, isTrue); + + streamGroup.remove(controller.stream); + await flushMicrotasks(); + expect(controller.hasListener, isFalse); + }); + + test('forwards cancel errors', () async { + var controller = + StreamController(onCancel: () => throw 'error'); + streamGroup.add(controller.stream); + + streamGroup.stream.listen(null); + await flushMicrotasks(); + + expect(streamGroup.remove(controller.stream), throwsA('error')); + }); + + test('forwards cancel futures', () async { + var completer = Completer(); + var controller = + StreamController(onCancel: () => completer.future); + + streamGroup.stream.listen(null); + await flushMicrotasks(); + + streamGroup.add(controller.stream); + await flushMicrotasks(); + + var fired = false; + streamGroup.remove(controller.stream)!.then((_) => fired = true); + + await flushMicrotasks(); + expect(fired, isFalse); + + completer.complete(); + await flushMicrotasks(); + expect(fired, isTrue); + }); + + test('is a no-op for an unknown stream', () async { + var controller = StreamController(); + streamGroup.stream.listen(null); + await flushMicrotasks(); + + expect(streamGroup.remove(controller.stream), isNull); + }); + + test('and closed closes the group when the last stream is removed', + () async { + var done = false; + streamGroup.stream.listen(null, onDone: () => done = true); + await flushMicrotasks(); + + var controller1 = StreamController(); + var controller2 = StreamController(); + + streamGroup.add(controller1.stream); + streamGroup.add(controller2.stream); + await flushMicrotasks(); + + streamGroup.close(); + + streamGroup.remove(controller1.stream); + await flushMicrotasks(); + expect(done, isFalse); + + streamGroup.remove(controller2.stream); + await flushMicrotasks(); + expect(done, isTrue); + }); + }); + }); + + group('close()', () { + group('while dormant', () { + test('if there are no streams, closes the group', () { + expect(streamGroup.close(), completes); + expect(streamGroup.stream.toList(), completion(isEmpty)); + }); + + test( + 'if there are streams, closes the group once those streams close ' + "and there's a listener", () async { + var controller1 = StreamController(); + var controller2 = StreamController(); + + streamGroup.add(controller1.stream); + streamGroup.add(controller2.stream); + await flushMicrotasks(); + + streamGroup.close(); + + controller1.close(); + controller2.close(); + expect(streamGroup.stream.toList(), completion(isEmpty)); + }); + }); + + group('while active', () { + test('if there are no streams, closes the group', () { + expect(streamGroup.stream.toList(), completion(isEmpty)); + expect(streamGroup.close(), completes); + }); + + test('if there are streams, closes the group once those streams close', + () async { + var done = false; + streamGroup.stream.listen(null, onDone: () => done = true); + await flushMicrotasks(); + + var controller1 = StreamController(); + var controller2 = StreamController(); + + streamGroup.add(controller1.stream); + streamGroup.add(controller2.stream); + await flushMicrotasks(); + + streamGroup.close(); + await flushMicrotasks(); + expect(done, isFalse); + + controller1.close(); + await flushMicrotasks(); + expect(done, isFalse); + + controller2.close(); + await flushMicrotasks(); + expect(done, isTrue); + }); + }); + + test('returns a Future that completes once all events are dispatched', + () async { + var events = []; + streamGroup.stream.listen(events.add); + + var controller = StreamController(); + streamGroup.add(controller.stream); + await flushMicrotasks(); + + // Add a bunch of events. Each of these will get dispatched in a + // separate microtask, so we can test that [close] only completes once + // all of them have dispatched. + controller.add('one'); + controller.add('two'); + controller.add('three'); + controller.add('four'); + controller.add('five'); + controller.add('six'); + controller.close(); + + await streamGroup.close(); + expect(events, equals(['one', 'two', 'three', 'four', 'five', 'six'])); + }); + }); + + group('onIdle', () { + test('emits an event when the last pending stream emits done', () async { + streamGroup.stream.listen(null); + + var idle = false; + streamGroup.onIdle.listen((_) => idle = true); + + var controller1 = StreamController(); + var controller2 = StreamController(); + var controller3 = StreamController(); + + streamGroup.add(controller1.stream); + streamGroup.add(controller2.stream); + streamGroup.add(controller3.stream); + + await flushMicrotasks(); + expect(idle, isFalse); + expect(streamGroup.isIdle, isFalse); + + controller1.close(); + await flushMicrotasks(); + expect(idle, isFalse); + expect(streamGroup.isIdle, isFalse); + + controller2.close(); + await flushMicrotasks(); + expect(idle, isFalse); + expect(streamGroup.isIdle, isFalse); + + controller3.close(); + await flushMicrotasks(); + expect(idle, isTrue); + expect(streamGroup.isIdle, isTrue); + }); + + test('emits an event each time it becomes idle', () async { + streamGroup.stream.listen(null); + + var idle = false; + streamGroup.onIdle.listen((_) => idle = true); + + var controller = StreamController(); + streamGroup.add(controller.stream); + + controller.close(); + await flushMicrotasks(); + expect(idle, isTrue); + expect(streamGroup.isIdle, isTrue); + + idle = false; + controller = StreamController(); + streamGroup.add(controller.stream); + + await flushMicrotasks(); + expect(idle, isFalse); + expect(streamGroup.isIdle, isFalse); + + controller.close(); + await flushMicrotasks(); + expect(idle, isTrue); + expect(streamGroup.isIdle, isTrue); + }); + + test('emits an event when the group closes', () async { + // It's important that the order of events here stays consistent over + // time, since code may rely on it in subtle ways. Note that this is *not* + // an official guarantee, so the authors of `async` are free to change + // this behavior if they need to. + var idle = false; + var onIdleDone = false; + var streamClosed = false; + + streamGroup.onIdle.listen(expectAsync1((_) { + expect(streamClosed, isFalse); + idle = true; + }), onDone: expectAsync0(() { + expect(idle, isTrue); + expect(streamClosed, isTrue); + onIdleDone = true; + })); + + streamGroup.stream.drain().then(expectAsync1((_) { + expect(idle, isTrue); + expect(onIdleDone, isFalse); + streamClosed = true; + })); + + var controller = StreamController(); + streamGroup.add(controller.stream); + streamGroup.close(); + + await flushMicrotasks(); + expect(idle, isFalse); + expect(streamGroup.isIdle, isFalse); + + controller.close(); + await flushMicrotasks(); + expect(idle, isTrue); + expect(streamGroup.isIdle, isTrue); + expect(streamClosed, isTrue); + }); + }); +} + +/// Wait for all microtasks to complete. +Future flushMicrotasks() => Future.delayed(Duration.zero); diff --git a/pkgs/async/test/stream_queue_test.dart b/pkgs/async/test/stream_queue_test.dart new file mode 100644 index 00000000..cd4433ae --- /dev/null +++ b/pkgs/async/test/stream_queue_test.dart @@ -0,0 +1,1214 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE filevents. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + group('source stream', () { + test('is listened to on first request, paused between requests', () async { + var controller = StreamController(); + var events = StreamQueue(controller.stream); + await flushMicrotasks(); + expect(controller.hasListener, isFalse); + + var next = events.next; + expect(controller.hasListener, isTrue); + expect(controller.isPaused, isFalse); + + controller.add(1); + + expect(await next, 1); + expect(controller.hasListener, isTrue); + expect(controller.isPaused, isTrue); + + next = events.next; + expect(controller.hasListener, isTrue); + expect(controller.isPaused, isFalse); + + controller.add(2); + + expect(await next, 2); + expect(controller.hasListener, isTrue); + expect(controller.isPaused, isTrue); + + events.cancel(); + expect(controller.hasListener, isFalse); + }); + }); + + group('eventsDispatched', () { + test('increments after a next future completes', () async { + var events = StreamQueue(createStream()); + + expect(events.eventsDispatched, equals(0)); + await flushMicrotasks(); + expect(events.eventsDispatched, equals(0)); + + var next = events.next; + expect(events.eventsDispatched, equals(0)); + + await next; + expect(events.eventsDispatched, equals(1)); + + await events.next; + expect(events.eventsDispatched, equals(2)); + }); + + test('increments multiple times for multi-value requests', () async { + var events = StreamQueue(createStream()); + await events.take(3); + expect(events.eventsDispatched, equals(3)); + }); + + test('increments multiple times for an accepted transaction', () async { + var events = StreamQueue(createStream()); + await events.withTransaction((queue) async { + await queue.next; + await queue.next; + return true; + }); + expect(events.eventsDispatched, equals(2)); + }); + + test("doesn't increment for rest requests", () async { + var events = StreamQueue(createStream()); + await events.rest.toList(); + expect(events.eventsDispatched, equals(0)); + }); + }); + + group('lookAhead operation', () { + test('as simple list of events', () async { + var events = StreamQueue(createStream()); + expect(await events.lookAhead(4), [1, 2, 3, 4]); + expect(await events.next, 1); + expect(await events.lookAhead(2), [2, 3]); + expect(await events.take(2), [2, 3]); + expect(await events.next, 4); + await events.cancel(); + }); + + test('of 0 events', () async { + var events = StreamQueue(createStream()); + expect(events.lookAhead(0), completion([])); + expect(events.next, completion(1)); + expect(events.lookAhead(0), completion([])); + expect(events.next, completion(2)); + expect(events.lookAhead(0), completion([])); + expect(events.next, completion(3)); + expect(events.lookAhead(0), completion([])); + expect(events.next, completion(4)); + expect(events.lookAhead(0), completion([])); + expect(events.lookAhead(5), completion([])); + expect(events.next, throwsStateError); + await events.cancel(); + }); + + test('with bad arguments throws', () async { + var events = StreamQueue(createStream()); + expect(() => events.lookAhead(-1), throwsArgumentError); + expect(await events.next, 1); // Did not consume event. + expect(() => events.lookAhead(-1), throwsArgumentError); + expect(await events.next, 2); // Did not consume event. + await events.cancel(); + }); + + test('of too many arguments', () async { + var events = StreamQueue(createStream()); + expect(await events.lookAhead(6), [1, 2, 3, 4]); + await events.cancel(); + }); + + test('too large later', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.next, 2); + expect(await events.lookAhead(6), [3, 4]); + await events.cancel(); + }); + + test('error', () async { + var events = StreamQueue(createErrorStream()); + expect(events.lookAhead(4), throwsA('To err is divine!')); + expect(events.take(4), throwsA('To err is divine!')); + expect(await events.next, 4); + await events.cancel(); + }); + }); + + group('next operation', () { + test('simple sequence of requests', () async { + var events = StreamQueue(createStream()); + for (var i = 1; i <= 4; i++) { + expect(await events.next, i); + } + expect(events.next, throwsStateError); + }); + + test('multiple requests at the same time', () async { + var events = StreamQueue(createStream()); + var result = await Future.wait( + [events.next, events.next, events.next, events.next]); + expect(result, [1, 2, 3, 4]); + await events.cancel(); + }); + + test('sequence of requests with error', () async { + var events = StreamQueue(createErrorStream()); + expect(await events.next, 1); + expect(await events.next, 2); + expect(events.next, throwsA('To err is divine!')); + expect(await events.next, 4); + await events.cancel(); + }); + }); + + group('skip operation', () { + test('of two elements in the middle of sequence', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.skip(2), 0); + expect(await events.next, 4); + await events.cancel(); + }); + + test('with negative/bad arguments throws', () async { + var events = StreamQueue(createStream()); + expect(() => events.skip(-1), throwsArgumentError); + // A non-int throws either a type error or an argument error, + // depending on whether it's checked mode or not. + expect(await events.next, 1); // Did not consume event. + expect(() => events.skip(-1), throwsArgumentError); + expect(await events.next, 2); // Did not consume event. + await events.cancel(); + }); + + test('of 0 elements works', () async { + var events = StreamQueue(createStream()); + expect(events.skip(0), completion(0)); + expect(events.next, completion(1)); + expect(events.skip(0), completion(0)); + expect(events.next, completion(2)); + expect(events.skip(0), completion(0)); + expect(events.next, completion(3)); + expect(events.skip(0), completion(0)); + expect(events.next, completion(4)); + expect(events.skip(0), completion(0)); + expect(events.skip(5), completion(5)); + expect(events.next, throwsStateError); + await events.cancel(); + }); + + test('of too many events ends at stream start', () async { + var events = StreamQueue(createStream()); + expect(await events.skip(6), 2); + await events.cancel(); + }); + + test('of too many events after some events', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.next, 2); + expect(await events.skip(6), 4); + await events.cancel(); + }); + + test('of too many events ends at stream end', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.next, 2); + expect(await events.next, 3); + expect(await events.next, 4); + expect(await events.skip(2), 2); + await events.cancel(); + }); + + test('of events with error', () async { + var events = StreamQueue(createErrorStream()); + expect(events.skip(4), throwsA('To err is divine!')); + expect(await events.next, 4); + await events.cancel(); + }); + + test('of events with error, and skip again after', () async { + var events = StreamQueue(createErrorStream()); + expect(events.skip(4), throwsA('To err is divine!')); + expect(events.skip(2), completion(1)); + await events.cancel(); + }); + test('multiple skips at same time complete in order.', () async { + var events = StreamQueue(createStream()); + var skip1 = events.skip(1); + var skip2 = events.skip(0); + var skip3 = events.skip(4); + var skip4 = events.skip(1); + var index = 0; + // Check that futures complete in order. + Func1Required sequence(int expectedValue, int sequenceIndex) => + (value) { + expect(value, expectedValue); + expect(index, sequenceIndex); + index++; + return null; + }; + await Future.wait([ + skip1.then(sequence(0, 0)), + skip2.then(sequence(0, 1)), + skip3.then(sequence(1, 2)), + skip4.then(sequence(1, 3)) + ]); + await events.cancel(); + }); + }); + + group('take operation', () { + test('as simple take of events', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.take(2), [2, 3]); + expect(await events.next, 4); + await events.cancel(); + }); + + test('of 0 events', () async { + var events = StreamQueue(createStream()); + expect(events.take(0), completion([])); + expect(events.next, completion(1)); + expect(events.take(0), completion([])); + expect(events.next, completion(2)); + expect(events.take(0), completion([])); + expect(events.next, completion(3)); + expect(events.take(0), completion([])); + expect(events.next, completion(4)); + expect(events.take(0), completion([])); + expect(events.take(5), completion([])); + expect(events.next, throwsStateError); + await events.cancel(); + }); + + test('with bad arguments throws', () async { + var events = StreamQueue(createStream()); + expect(() => events.take(-1), throwsArgumentError); + expect(await events.next, 1); // Did not consume event. + expect(() => events.take(-1), throwsArgumentError); + expect(await events.next, 2); // Did not consume event. + await events.cancel(); + }); + + test('of too many arguments', () async { + var events = StreamQueue(createStream()); + expect(await events.take(6), [1, 2, 3, 4]); + await events.cancel(); + }); + + test('too large later', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.next, 2); + expect(await events.take(6), [3, 4]); + await events.cancel(); + }); + + test('error', () async { + var events = StreamQueue(createErrorStream()); + expect(events.take(4), throwsA('To err is divine!')); + expect(await events.next, 4); + await events.cancel(); + }); + }); + + group('rest operation', () { + test('after single next', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.rest.toList(), [2, 3, 4]); + }); + + test('at start', () async { + var events = StreamQueue(createStream()); + expect(await events.rest.toList(), [1, 2, 3, 4]); + }); + + test('at end', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.next, 2); + expect(await events.next, 3); + expect(await events.next, 4); + expect(await events.rest.toList(), isEmpty); + }); + + test('after end', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.next, 2); + expect(await events.next, 3); + expect(await events.next, 4); + expect(events.next, throwsStateError); + expect(await events.rest.toList(), isEmpty); + }); + + test('after receiving done requested before', () async { + var events = StreamQueue(createStream()); + var next1 = events.next; + var next2 = events.next; + var next3 = events.next; + var rest = events.rest; + for (var i = 0; i < 10; i++) { + await flushMicrotasks(); + } + expect(await next1, 1); + expect(await next2, 2); + expect(await next3, 3); + expect(await rest.toList(), [4]); + }); + + test('with an error event error', () async { + var events = StreamQueue(createErrorStream()); + expect(await events.next, 1); + var rest = events.rest; + var events2 = StreamQueue(rest); + expect(await events2.next, 2); + expect(events2.next, throwsA('To err is divine!')); + expect(await events2.next, 4); + }); + + test('closes the events, prevents other operations', () async { + var events = StreamQueue(createStream()); + var stream = events.rest; + expect(() => events.next, throwsStateError); + expect(() => events.skip(1), throwsStateError); + expect(() => events.take(1), throwsStateError); + expect(() => events.rest, throwsStateError); + expect(() => events.cancel(), throwsStateError); + expect(stream.toList(), completion([1, 2, 3, 4])); + }); + + test('forwards to underlying stream', () async { + var cancel = Completer(); + var controller = StreamController(onCancel: () => cancel.future); + var events = StreamQueue(controller.stream); + expect(controller.hasListener, isFalse); + var next = events.next; + expect(controller.hasListener, isTrue); + expect(controller.isPaused, isFalse); + + controller.add(1); + expect(await next, 1); + expect(controller.isPaused, isTrue); + + var rest = events.rest; + var subscription = rest.listen(null); + expect(controller.hasListener, isTrue); + expect(controller.isPaused, isFalse); + + dynamic lastEvent; + subscription.onData((value) => lastEvent = value); + + controller.add(2); + + await flushMicrotasks(); + expect(lastEvent, 2); + expect(controller.hasListener, isTrue); + expect(controller.isPaused, isFalse); + + subscription.pause(); + expect(controller.isPaused, isTrue); + + controller.add(3); + + await flushMicrotasks(); + expect(lastEvent, 2); + subscription.resume(); + + await flushMicrotasks(); + expect(lastEvent, 3); + + var cancelFuture = subscription.cancel(); + expect(controller.hasListener, isFalse); + cancel.complete(42); + expect(cancelFuture, completion(42)); + }); + }); + + group('peek operation', () { + test('peeks one event', () async { + var events = StreamQueue(createStream()); + expect(await events.peek, 1); + expect(await events.next, 1); + expect(await events.peek, 2); + expect(await events.take(2), [2, 3]); + expect(await events.peek, 4); + expect(await events.next, 4); + // Throws at end. + expect(events.peek, throwsA(anything)); + await events.cancel(); + }); + test('multiple requests at the same time', () async { + var events = StreamQueue(createStream()); + var result = await Future.wait( + [events.peek, events.peek, events.next, events.peek, events.peek]); + expect(result, [1, 1, 1, 2, 2]); + await events.cancel(); + }); + test('sequence of requests with error', () async { + var events = StreamQueue(createErrorStream()); + expect(await events.next, 1); + expect(await events.next, 2); + expect(events.peek, throwsA('To err is divine!')); + // Error stays in queue. + expect(events.peek, throwsA('To err is divine!')); + expect(events.next, throwsA('To err is divine!')); + expect(await events.next, 4); + await events.cancel(); + }); + }); + + group('cancel operation', () { + test('closes the events, prevents any other operation', () async { + var events = StreamQueue(createStream()); + await events.cancel(); + expect(() => events.lookAhead(1), throwsStateError); + expect(() => events.next, throwsStateError); + expect(() => events.peek, throwsStateError); + expect(() => events.skip(1), throwsStateError); + expect(() => events.take(1), throwsStateError); + expect(() => events.rest, throwsStateError); + expect(() => events.cancel(), throwsStateError); + }); + + test('cancels underlying subscription when called before any event', + () async { + var cancelFuture = Future.value(42); + var controller = StreamController(onCancel: () => cancelFuture); + var events = StreamQueue(controller.stream); + expect(await events.cancel(), 42); + }); + + test('cancels underlying subscription, returns result', () async { + var cancelFuture = Future.value(42); + var controller = StreamController(onCancel: () => cancelFuture); + var events = StreamQueue(controller.stream); + controller.add(1); + expect(await events.next, 1); + expect(await events.cancel(), 42); + }); + + group('with immediate: true', () { + test('closes the events, prevents any other operation', () async { + var events = StreamQueue(createStream()); + await events.cancel(immediate: true); + expect(() => events.next, throwsStateError); + expect(() => events.skip(1), throwsStateError); + expect(() => events.take(1), throwsStateError); + expect(() => events.rest, throwsStateError); + expect(() => events.cancel(), throwsStateError); + }); + + test('cancels the underlying subscription immediately', () async { + var controller = StreamController(); + controller.add(1); + + var events = StreamQueue(controller.stream); + expect(await events.next, 1); + expect(controller.hasListener, isTrue); + + await events.cancel(immediate: true); + expect(controller.hasListener, isFalse); + }); + + test('cancels the underlying subscription when called before any event', + () async { + var cancelFuture = Future.value(42); + var controller = StreamController(onCancel: () => cancelFuture); + + var events = StreamQueue(controller.stream); + expect(await events.cancel(immediate: true), 42); + }); + + test('closes pending requests', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(events.next, throwsStateError); + expect(events.hasNext, completion(isFalse)); + + await events.cancel(immediate: true); + }); + + test('returns the result of closing the underlying subscription', + () async { + var controller = + StreamController(onCancel: () => Future.value(42)); + var events = StreamQueue(controller.stream); + expect(await events.cancel(immediate: true), 42); + }); + + test("listens and then cancels a stream that hasn't been listened to yet", + () async { + var wasListened = false; + var controller = + StreamController(onListen: () => wasListened = true); + var events = StreamQueue(controller.stream); + expect(wasListened, isFalse); + expect(controller.hasListener, isFalse); + + await events.cancel(immediate: true); + expect(wasListened, isTrue); + expect(controller.hasListener, isFalse); + }); + }); + }); + + group('hasNext operation', () { + test('true at start', () async { + var events = StreamQueue(createStream()); + expect(await events.hasNext, isTrue); + }); + + test('true after start', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.hasNext, isTrue); + }); + + test('true at end', () async { + var events = StreamQueue(createStream()); + for (var i = 1; i <= 4; i++) { + expect(await events.next, i); + } + expect(await events.hasNext, isFalse); + }); + + test('true when enqueued', () async { + var events = StreamQueue(createStream()); + var values = []; + for (var i = 1; i <= 3; i++) { + events.next.then(values.add); + } + expect(values, isEmpty); + expect(await events.hasNext, isTrue); + expect(values, [1, 2, 3]); + }); + + test('false when enqueued', () async { + var events = StreamQueue(createStream()); + var values = []; + for (var i = 1; i <= 4; i++) { + events.next.then(values.add); + } + expect(values, isEmpty); + expect(await events.hasNext, isFalse); + expect(values, [1, 2, 3, 4]); + }); + + test('true when data event', () async { + var controller = StreamController(); + var events = StreamQueue(controller.stream); + + bool? hasNext; + events.hasNext.then((result) { + hasNext = result; + }); + await flushMicrotasks(); + expect(hasNext, isNull); + controller.add(42); + expect(hasNext, isNull); + await flushMicrotasks(); + expect(hasNext, isTrue); + }); + + test('true when error event', () async { + var controller = StreamController(); + var events = StreamQueue(controller.stream); + + bool? hasNext; + events.hasNext.then((result) { + hasNext = result; + }); + await flushMicrotasks(); + expect(hasNext, isNull); + controller.addError('BAD'); + expect(hasNext, isNull); + await flushMicrotasks(); + expect(hasNext, isTrue); + expect(events.next, throwsA('BAD')); + }); + + test('- hasNext after hasNext', () async { + var events = StreamQueue(createStream()); + expect(await events.hasNext, true); + expect(await events.hasNext, true); + expect(await events.next, 1); + expect(await events.hasNext, true); + expect(await events.hasNext, true); + expect(await events.next, 2); + expect(await events.hasNext, true); + expect(await events.hasNext, true); + expect(await events.next, 3); + expect(await events.hasNext, true); + expect(await events.hasNext, true); + expect(await events.next, 4); + expect(await events.hasNext, false); + expect(await events.hasNext, false); + }); + + test('- next after true', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.hasNext, true); + expect(await events.next, 2); + expect(await events.next, 3); + }); + + test('- next after true, enqueued', () async { + var events = StreamQueue(createStream()); + var responses = []; + events.next.then(responses.add); + events.hasNext.then(responses.add); + events.next.then(responses.add); + do { + await flushMicrotasks(); + } while (responses.length < 3); + expect(responses, [1, true, 2]); + }); + + test('- skip 0 after true', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.hasNext, true); + expect(await events.skip(0), 0); + expect(await events.next, 2); + }); + + test('- skip 1 after true', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.hasNext, true); + expect(await events.skip(1), 0); + expect(await events.next, 3); + }); + + test('- skip 2 after true', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.hasNext, true); + expect(await events.skip(2), 0); + expect(await events.next, 4); + }); + + test('- take 0 after true', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.hasNext, true); + expect(await events.take(0), isEmpty); + expect(await events.next, 2); + }); + + test('- take 1 after true', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.hasNext, true); + expect(await events.take(1), [2]); + expect(await events.next, 3); + }); + + test('- take 2 after true', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.hasNext, true); + expect(await events.take(2), [2, 3]); + expect(await events.next, 4); + }); + + test('- rest after true', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.hasNext, true); + var stream = events.rest; + expect(await stream.toList(), [2, 3, 4]); + }); + + test('- rest after true, at last', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.next, 2); + expect(await events.next, 3); + expect(await events.hasNext, true); + var stream = events.rest; + expect(await stream.toList(), [4]); + }); + + test('- rest after false', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.next, 2); + expect(await events.next, 3); + expect(await events.next, 4); + expect(await events.hasNext, false); + var stream = events.rest; + expect(await stream.toList(), isEmpty); + }); + + test('- cancel after true on data', () async { + var events = StreamQueue(createStream()); + expect(await events.next, 1); + expect(await events.next, 2); + expect(await events.hasNext, true); + expect(await events.cancel(), null); + }); + + test('- cancel after true on error', () async { + var events = StreamQueue(createErrorStream()); + expect(await events.next, 1); + expect(await events.next, 2); + expect(await events.hasNext, true); + expect(await events.cancel(), null); + }); + }); + + group('startTransaction operation produces a transaction that', () { + late StreamQueue events; + late StreamQueueTransaction transaction; + late StreamQueue queue1; + late StreamQueue queue2; + setUp(() async { + events = StreamQueue(createStream()); + expect(await events.next, 1); + transaction = events.startTransaction(); + queue1 = transaction.newQueue(); + queue2 = transaction.newQueue(); + }); + + group('emits queues that', () { + test('independently emit events', () async { + expect(await queue1.next, 2); + expect(await queue2.next, 2); + expect(await queue2.next, 3); + expect(await queue1.next, 3); + expect(await queue1.next, 4); + expect(await queue2.next, 4); + expect(await queue1.hasNext, isFalse); + expect(await queue2.hasNext, isFalse); + }); + + test('queue requests for events', () async { + expect(queue1.next, completion(2)); + expect(queue2.next, completion(2)); + expect(queue2.next, completion(3)); + expect(queue1.next, completion(3)); + expect(queue1.next, completion(4)); + expect(queue2.next, completion(4)); + expect(queue1.hasNext, completion(isFalse)); + expect(queue2.hasNext, completion(isFalse)); + }); + + test('independently emit errors', () async { + events = StreamQueue(createErrorStream()); + expect(await events.next, 1); + transaction = events.startTransaction(); + queue1 = transaction.newQueue(); + queue2 = transaction.newQueue(); + + expect(queue1.next, completion(2)); + expect(queue2.next, completion(2)); + expect(queue2.next, throwsA('To err is divine!')); + expect(queue1.next, throwsA('To err is divine!')); + expect(queue1.next, completion(4)); + expect(queue2.next, completion(4)); + expect(queue1.hasNext, completion(isFalse)); + expect(queue2.hasNext, completion(isFalse)); + }); + }); + + group('when rejected', () { + test('further original requests use the previous state', () async { + expect(await queue1.next, 2); + expect(await queue2.next, 2); + expect(await queue2.next, 3); + + await flushMicrotasks(); + transaction.reject(); + + expect(await events.next, 2); + expect(await events.next, 3); + expect(await events.next, 4); + expect(await events.hasNext, isFalse); + }); + + test('pending original requests use the previous state', () async { + expect(await queue1.next, 2); + expect(await queue2.next, 2); + expect(await queue2.next, 3); + expect(events.next, completion(2)); + expect(events.next, completion(3)); + expect(events.next, completion(4)); + expect(events.hasNext, completion(isFalse)); + + await flushMicrotasks(); + transaction.reject(); + }); + + test('further child requests act as though the stream was closed', + () async { + expect(await queue1.next, 2); + transaction.reject(); + + expect(await queue1.hasNext, isFalse); + expect(queue1.next, throwsStateError); + }); + + test('pending child requests act as though the stream was closed', + () async { + expect(await queue1.next, 2); + expect(queue1.hasNext, completion(isFalse)); + expect(queue1.next, throwsStateError); + transaction.reject(); + }); + + // Regression test. + test('pending child rest requests emit no more events', () async { + var controller = StreamController(); + var events = StreamQueue(controller.stream); + var transaction = events.startTransaction(); + var queue = transaction.newQueue(); + + // This should emit no more events after the transaction is rejected. + queue.rest.listen(expectAsync1((_) {}, count: 3), + onDone: expectAsync0(() {}, count: 0)); + + controller.add(1); + controller.add(2); + controller.add(3); + await flushMicrotasks(); + + transaction.reject(); + await flushMicrotasks(); + + // These shouldn't affect the result of `queue.rest.toList()`. + controller.add(4); + controller.add(5); + }); + + test("child requests' cancel() may still be called explicitly", () async { + transaction.reject(); + await queue1.cancel(); + }); + + test('calls to commit() or reject() fail', () async { + transaction.reject(); + expect(transaction.reject, throwsStateError); + expect(() => transaction.commit(queue1), throwsStateError); + }); + + test('before the transaction emits any events, does nothing', () async { + var controller = StreamController(); + var events = StreamQueue(controller.stream); + + // Queue a request before the transaction, but don't let it complete + // until we're done with the transaction. + expect(events.next, completion(equals(1))); + events.startTransaction().reject(); + expect(events.next, completion(equals(2))); + + await flushMicrotasks(); + controller.add(1); + await flushMicrotasks(); + controller.add(2); + await flushMicrotasks(); + controller.close(); + }); + + test( + 'can reject a transaction where one copy is fully consumed ' + 'in a transaction and a second copy is made', () async { + // Regression test for https://github.com/dart-lang/async/issues/229 + final queue = StreamQueue(Stream.fromIterable([0])); + final transaction = queue.startTransaction(); + + final copy1 = transaction.newQueue(); + final inner1 = copy1.startTransaction(); + final innerCopy1 = inner1.newQueue(); + await innerCopy1.next; + + transaction.newQueue(); + + transaction.reject(); + expect(await queue.next, 0); + expect(await queue.hasNext, isFalse); + }, skip: 'https://github.com/dart-lang/async/issues/229'); + }); + + group('when committed', () { + test('further original requests use the committed state', () async { + expect(await queue1.next, 2); + await flushMicrotasks(); + transaction.commit(queue1); + expect(await events.next, 3); + }); + + test('pending original requests use the committed state', () async { + expect(await queue1.next, 2); + expect(events.next, completion(3)); + await flushMicrotasks(); + transaction.commit(queue1); + }); + + test('further child requests act as though the stream was closed', + () async { + expect(await queue2.next, 2); + transaction.commit(queue2); + + expect(await queue1.hasNext, isFalse); + expect(queue1.next, throwsStateError); + }); + + test('pending child requests act as though the stream was closed', + () async { + expect(await queue2.next, 2); + expect(queue1.hasNext, completion(isFalse)); + expect(queue1.next, throwsStateError); + transaction.commit(queue2); + }); + + test('further requests act as though the stream was closed', () async { + expect(await queue1.next, 2); + transaction.commit(queue1); + + expect(await queue1.hasNext, isFalse); + expect(queue1.next, throwsStateError); + }); + + test('cancel() may still be called explicitly', () async { + expect(await queue1.next, 2); + transaction.commit(queue1); + await queue1.cancel(); + }); + + test('throws if there are pending requests', () async { + expect(await queue1.next, 2); + expect(queue1.hasNext, completion(isTrue)); + expect(() => transaction.commit(queue1), throwsStateError); + }); + + test('calls to commit() or reject() fail', () async { + transaction.commit(queue1); + expect(transaction.reject, throwsStateError); + expect(() => transaction.commit(queue1), throwsStateError); + }); + + test('before the transaction emits any events, does nothing', () async { + var controller = StreamController(); + var events = StreamQueue(controller.stream); + + // Queue a request before the transaction, but don't let it complete + // until we're done with the transaction. + expect(events.next, completion(equals(1))); + var transaction = events.startTransaction(); + transaction.commit(transaction.newQueue()); + expect(events.next, completion(equals(2))); + + await flushMicrotasks(); + controller.add(1); + await flushMicrotasks(); + controller.add(2); + await flushMicrotasks(); + controller.close(); + }); + }); + }); + + group('withTransaction operation', () { + late StreamQueue events; + setUp(() async { + events = StreamQueue(createStream()); + expect(await events.next, 1); + }); + + test('passes a copy of the parent queue', () async { + await events.withTransaction(expectAsync1((queue) async { + expect(await queue.next, 2); + expect(await queue.next, 3); + expect(await queue.next, 4); + expect(await queue.hasNext, isFalse); + return true; + })); + }); + + test( + 'the parent queue continues from the child position if it returns ' + 'true', () async { + await events.withTransaction(expectAsync1((queue) async { + expect(await queue.next, 2); + return true; + })); + + expect(await events.next, 3); + }); + + test( + 'the parent queue continues from its original position if it returns ' + 'false', () async { + await events.withTransaction(expectAsync1((queue) async { + expect(await queue.next, 2); + return false; + })); + + expect(await events.next, 2); + }); + + test('the parent queue continues from the child position if it throws', () { + expect(events.withTransaction(expectAsync1((queue) async { + expect(await queue.next, 2); + throw 'oh no'; + })), throwsA('oh no')); + + expect(events.next, completion(3)); + }); + + test('returns whether the transaction succeeded', () { + expect(events.withTransaction((_) async => true), completion(isTrue)); + expect(events.withTransaction((_) async => false), completion(isFalse)); + }); + }); + + group('cancelable operation', () { + late StreamQueue events; + setUp(() async { + events = StreamQueue(createStream()); + expect(await events.next, 1); + }); + + test('passes a copy of the parent queue', () async { + await events.cancelable(expectAsync1((queue) async { + expect(await queue.next, 2); + expect(await queue.next, 3); + expect(await queue.next, 4); + expect(await queue.hasNext, isFalse); + })).value; + }); + + test('the parent queue continues from the child position by default', + () async { + await events.cancelable(expectAsync1((queue) async { + expect(await queue.next, 2); + })).value; + + expect(await events.next, 3); + }); + + test( + 'the parent queue continues from the child position if an error is ' + 'thrown', () async { + expect( + events.cancelable(expectAsync1((queue) async { + expect(await queue.next, 2); + throw 'oh no'; + })).value, + throwsA('oh no')); + + expect(events.next, completion(3)); + }); + + test('the parent queue continues from the original position if canceled', + () async { + var operation = events.cancelable(expectAsync1((queue) async { + expect(await queue.next, 2); + })); + operation.cancel(); + + expect(await events.next, 2); + }); + + test('forwards the value from the callback', () async { + expect( + await events.cancelable(expectAsync1((queue) async { + expect(await queue.next, 2); + return 'value'; + })).value, + 'value'); + }); + }); + + test('all combinations sequential skip/next/take operations', () async { + // Takes all combinations of two of next, skip and take, then ends with + // doing rest. Each of the first rounds do 10 events of each type, + // the rest does 20 elements. + var eventCount = 20 * (3 * 3 + 1); + var events = StreamQueue(createLongStream(eventCount)); + + // Test expecting [startIndex .. startIndex + 9] as events using + // `next`. + void nextTest(int startIndex) { + for (var i = 0; i < 10; i++) { + expect(events.next, completion(startIndex + i)); + } + } + + // Test expecting 10 events to be skipped. + void skipTest(startIndex) { + expect(events.skip(10), completion(0)); + } + + // Test expecting [startIndex .. startIndex + 9] as events using + // `take(10)`. + void takeTest(int startIndex) { + expect(events.take(10), + completion(List.generate(10, (i) => startIndex + i))); + } + + var tests = [nextTest, skipTest, takeTest]; + + var counter = 0; + // Run through all pairs of two tests and run them. + for (var i = 0; i < tests.length; i++) { + for (var j = 0; j < tests.length; j++) { + tests[i](counter); + tests[j](counter + 10); + counter += 20; + } + } + // Then expect 20 more events as a `rest` call. + expect(events.rest.toList(), + completion(List.generate(20, (i) => counter + i))); + }); +} + +typedef Func1Required = T Function(T value); + +Stream createStream() async* { + yield 1; + await flushMicrotasks(); + yield 2; + await flushMicrotasks(); + yield 3; + await flushMicrotasks(); + yield 4; +} + +Stream createErrorStream() { + var controller = StreamController(); + () async { + controller.add(1); + await flushMicrotasks(); + controller.add(2); + await flushMicrotasks(); + controller.addError('To err is divine!'); + await flushMicrotasks(); + controller.add(4); + await flushMicrotasks(); + controller.close(); + }(); + return controller.stream; +} + +Stream createLongStream(int eventCount) async* { + for (var i = 0; i < eventCount; i++) { + yield i; + } +} diff --git a/pkgs/async/test/stream_sink_completer_test.dart b/pkgs/async/test/stream_sink_completer_test.dart new file mode 100644 index 00000000..3a6f25bc --- /dev/null +++ b/pkgs/async/test/stream_sink_completer_test.dart @@ -0,0 +1,307 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + late StreamSinkCompleter completer; + setUp(() { + completer = StreamSinkCompleter(); + }); + + group('when a stream is linked before events are added', () { + test('data events are forwarded', () { + var sink = TestSink(); + completer.setDestinationSink(sink); + completer.sink + ..add(1) + ..add(2) + ..add(3) + ..add(4); + + expect(sink.results[0].asValue!.value, equals(1)); + expect(sink.results[1].asValue!.value, equals(2)); + expect(sink.results[2].asValue!.value, equals(3)); + expect(sink.results[3].asValue!.value, equals(4)); + }); + + test('error events are forwarded', () { + var sink = TestSink(); + completer.setDestinationSink(sink); + completer.sink + ..addError('oh no') + ..addError("that's bad"); + + expect(sink.results[0].asError!.error, equals('oh no')); + expect(sink.results[1].asError!.error, equals("that's bad")); + }); + + test('addStream is forwarded', () async { + var sink = TestSink(); + completer.setDestinationSink(sink); + + var controller = StreamController(); + completer.sink.addStream(controller.stream); + + controller.add(1); + controller.addError('oh no'); + controller.add(2); + controller.addError("that's bad"); + await flushMicrotasks(); + + expect(sink.results[0].asValue!.value, equals(1)); + expect(sink.results[1].asError!.error, equals('oh no')); + expect(sink.results[2].asValue!.value, equals(2)); + expect(sink.results[3].asError!.error, equals("that's bad")); + expect(sink.isClosed, isFalse); + + controller.close(); + await flushMicrotasks(); + expect(sink.isClosed, isFalse); + }); + + test('close() is forwarded', () { + var sink = TestSink(); + completer.setDestinationSink(sink); + completer.sink.close(); + expect(sink.isClosed, isTrue); + }); + + test('the future from the inner close() is returned', () async { + var closeCompleter = Completer(); + var sink = TestSink(onDone: () => closeCompleter.future); + completer.setDestinationSink(sink); + + var closeCompleted = false; + completer.sink.close().then(expectAsync1((_) { + closeCompleted = true; + })); + + await flushMicrotasks(); + expect(closeCompleted, isFalse); + + closeCompleter.complete(); + await flushMicrotasks(); + expect(closeCompleted, isTrue); + }); + + test('errors are forwarded from the inner close()', () { + var sink = TestSink(onDone: () => throw 'oh no'); + completer.setDestinationSink(sink); + expect(completer.sink.done, throwsA('oh no')); + expect(completer.sink.close(), throwsA('oh no')); + }); + + test("errors aren't top-leveled if only close() is listened to", () async { + var sink = TestSink(onDone: () => throw 'oh no'); + completer.setDestinationSink(sink); + expect(completer.sink.close(), throwsA('oh no')); + + // Give the event loop a chance to top-level errors if it's going to. + await flushMicrotasks(); + }); + + test("errors aren't top-leveled if only done is listened to", () async { + var sink = TestSink(onDone: () => throw 'oh no'); + completer.setDestinationSink(sink); + completer.sink.close(); + expect(completer.sink.done, throwsA('oh no')); + + // Give the event loop a chance to top-level errors if it's going to. + await flushMicrotasks(); + }); + }); + + group('when a stream is linked after events are added', () { + test('data events are forwarded', () async { + completer.sink + ..add(1) + ..add(2) + ..add(3) + ..add(4); + await flushMicrotasks(); + + var sink = TestSink(); + completer.setDestinationSink(sink); + await flushMicrotasks(); + + expect(sink.results[0].asValue!.value, equals(1)); + expect(sink.results[1].asValue!.value, equals(2)); + expect(sink.results[2].asValue!.value, equals(3)); + expect(sink.results[3].asValue!.value, equals(4)); + }); + + test('error events are forwarded', () async { + completer.sink + ..addError('oh no') + ..addError("that's bad"); + await flushMicrotasks(); + + var sink = TestSink(); + completer.setDestinationSink(sink); + await flushMicrotasks(); + + expect(sink.results[0].asError!.error, equals('oh no')); + expect(sink.results[1].asError!.error, equals("that's bad")); + }); + + test('addStream is forwarded', () async { + var controller = StreamController(); + completer.sink.addStream(controller.stream); + + controller.add(1); + controller.addError('oh no'); + controller.add(2); + controller.addError("that's bad"); + controller.close(); + await flushMicrotasks(); + + var sink = TestSink(); + completer.setDestinationSink(sink); + await flushMicrotasks(); + + expect(sink.results[0].asValue!.value, equals(1)); + expect(sink.results[1].asError!.error, equals('oh no')); + expect(sink.results[2].asValue!.value, equals(2)); + expect(sink.results[3].asError!.error, equals("that's bad")); + expect(sink.isClosed, isFalse); + }); + + test('close() is forwarded', () async { + completer.sink.close(); + await flushMicrotasks(); + + var sink = TestSink(); + completer.setDestinationSink(sink); + await flushMicrotasks(); + + expect(sink.isClosed, isTrue); + }); + + test('the future from the inner close() is returned', () async { + var closeCompleted = false; + completer.sink.close().then(expectAsync1((_) { + closeCompleted = true; + })); + await flushMicrotasks(); + + var closeCompleter = Completer(); + var sink = TestSink(onDone: () => closeCompleter.future); + completer.setDestinationSink(sink); + await flushMicrotasks(); + expect(closeCompleted, isFalse); + + closeCompleter.complete(); + await flushMicrotasks(); + expect(closeCompleted, isTrue); + }); + + test('errors are forwarded from the inner close()', () async { + expect(completer.sink.done, throwsA('oh no')); + expect(completer.sink.close(), throwsA('oh no')); + await flushMicrotasks(); + + var sink = TestSink(onDone: () => throw 'oh no'); + completer.setDestinationSink(sink); + }); + + test("errors aren't top-leveled if only close() is listened to", () async { + expect(completer.sink.close(), throwsA('oh no')); + await flushMicrotasks(); + + var sink = TestSink(onDone: () => throw 'oh no'); + completer.setDestinationSink(sink); + + // Give the event loop a chance to top-level errors if it's going to. + await flushMicrotasks(); + }); + + test("errors aren't top-leveled if only done is listened to", () async { + completer.sink.close(); + expect(completer.sink.done, throwsA('oh no')); + await flushMicrotasks(); + + var sink = TestSink(onDone: () => throw 'oh no'); + completer.setDestinationSink(sink); + + // Give the event loop a chance to top-level errors if it's going to. + await flushMicrotasks(); + }); + }); + + test('the sink is closed, the destination is set, then done is read', + () async { + expect(completer.sink.close(), completes); + await flushMicrotasks(); + + completer.setDestinationSink(TestSink()); + await flushMicrotasks(); + + expect(completer.sink.done, completes); + }); + + test('done is read, the destination is set, then the sink is closed', + () async { + expect(completer.sink.done, completes); + await flushMicrotasks(); + + completer.setDestinationSink(TestSink()); + await flushMicrotasks(); + + expect(completer.sink.close(), completes); + }); + + group('fromFuture()', () { + test('with a successful completion', () async { + var futureCompleter = Completer(); + var sink = StreamSinkCompleter.fromFuture(futureCompleter.future); + sink.add(1); + sink.add(2); + sink.add(3); + sink.close(); + + var testSink = TestSink(); + futureCompleter.complete(testSink); + await testSink.done; + + expect(testSink.results[0].asValue!.value, equals(1)); + expect(testSink.results[1].asValue!.value, equals(2)); + expect(testSink.results[2].asValue!.value, equals(3)); + }); + + test('with an error', () async { + var futureCompleter = Completer(); + var sink = StreamSinkCompleter.fromFuture(futureCompleter.future); + expect(sink.done, throwsA('oh no')); + futureCompleter.completeError('oh no'); + }); + }); + + group('setError()', () { + test('produces a closed sink with the error', () { + completer.setError('oh no'); + expect(completer.sink.done, throwsA('oh no')); + expect(completer.sink.close(), throwsA('oh no')); + }); + + test('produces an error even if done was accessed earlier', () async { + expect(completer.sink.done, throwsA('oh no')); + expect(completer.sink.close(), throwsA('oh no')); + await flushMicrotasks(); + + completer.setError('oh no'); + }); + }); + + test("doesn't allow the destination sink to be set multiple times", () { + completer.setDestinationSink(TestSink()); + expect(() => completer.setDestinationSink(TestSink()), throwsStateError); + expect(() => completer.setDestinationSink(TestSink()), throwsStateError); + }); +} diff --git a/pkgs/async/test/stream_sink_transformer_test.dart b/pkgs/async/test/stream_sink_transformer_test.dart new file mode 100644 index 00000000..caea3495 --- /dev/null +++ b/pkgs/async/test/stream_sink_transformer_test.dart @@ -0,0 +1,213 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE filevents. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + late StreamController controller; + setUp(() { + controller = StreamController(); + }); + + group('fromStreamTransformer', () { + test('transforms data events', () { + var transformer = StreamSinkTransformer.fromStreamTransformer( + StreamTransformer.fromHandlers(handleData: (int i, sink) { + sink.add(i * 2); + })); + var sink = transformer.bind(controller.sink); + + var results = []; + controller.stream.listen(results.add, onDone: expectAsync0(() { + expect(results, equals([2, 4, 6])); + })); + + sink.add(1); + sink.add(2); + sink.add(3); + sink.close(); + }); + + test('transforms error events', () { + var transformer = StreamSinkTransformer.fromStreamTransformer( + StreamTransformer.fromHandlers(handleError: (i, stackTrace, sink) { + sink.addError((i as num) * 2, stackTrace); + })); + var sink = transformer.bind(controller.sink); + + var results = []; + controller.stream.listen(expectAsync1((_) {}, count: 0), + onError: (Object error, stackTrace) { + results.add(error); + }, onDone: expectAsync0(() { + expect(results, equals([2, 4, 6])); + })); + + sink.addError(1); + sink.addError(2); + sink.addError(3); + sink.close(); + }); + + test('transforms done events', () { + var transformer = StreamSinkTransformer.fromStreamTransformer( + StreamTransformer.fromHandlers(handleDone: (sink) { + sink.add(1); + sink.close(); + })); + var sink = transformer.bind(controller.sink); + + var results = []; + controller.stream.listen(results.add, onDone: expectAsync0(() { + expect(results, equals([1])); + })); + + sink.close(); + }); + + test('forwards the future from inner.close', () async { + var transformer = StreamSinkTransformer.fromStreamTransformer( + StreamTransformer.fromHandlers()); + var innerSink = CompleterStreamSink(); + var sink = transformer.bind(innerSink); + + // The futures shouldn't complete until the inner sink's close future + // completes. + var doneResult = ResultFuture(sink.done); + doneResult.catchError((_) {}); + var closeResult = ResultFuture(sink.close()); + closeResult.catchError((_) {}); + await flushMicrotasks(); + expect(doneResult.isComplete, isFalse); + expect(closeResult.isComplete, isFalse); + + // Once the inner sink is completed, the futures should fire. + innerSink.completer.complete(); + await flushMicrotasks(); + expect(doneResult.isComplete, isTrue); + expect(closeResult.isComplete, isTrue); + }); + + test("doesn't top-level the future from inner.close", () async { + var transformer = StreamSinkTransformer.fromStreamTransformer( + StreamTransformer.fromHandlers(handleData: (_, sink) { + sink.close(); + })); + var innerSink = CompleterStreamSink(); + var sink = transformer.bind(innerSink); + + // This will close the inner sink, but it shouldn't top-level the error. + sink.add(1); + innerSink.completer.completeError('oh no'); + await flushMicrotasks(); + + // The error should be piped through done and close even if they're called + // after the underlying sink is closed. + expect(sink.done, throwsA('oh no')); + expect(sink.close(), throwsA('oh no')); + }); + }); + + group('fromHandlers', () { + test('transforms data events', () { + var transformer = + StreamSinkTransformer.fromHandlers(handleData: (int i, sink) { + sink.add(i * 2); + }); + var sink = transformer.bind(controller.sink); + + var results = []; + controller.stream.listen(results.add, onDone: expectAsync0(() { + expect(results, equals([2, 4, 6])); + })); + + sink.add(1); + sink.add(2); + sink.add(3); + sink.close(); + }); + + test('transforms error events', () { + var transformer = StreamSinkTransformer.fromHandlers( + handleError: (i, stackTrace, sink) { + sink.addError((i as num) * 2, stackTrace); + }); + var sink = transformer.bind(controller.sink); + + var results = []; + controller.stream.listen(expectAsync1((_) {}, count: 0), + onError: (Object error, stackTrace) { + results.add(error); + }, onDone: expectAsync0(() { + expect(results, equals([2, 4, 6])); + })); + + sink.addError(1); + sink.addError(2); + sink.addError(3); + sink.close(); + }); + + test('transforms done events', () { + var transformer = StreamSinkTransformer.fromHandlers(handleDone: (sink) { + sink.add(1); + sink.close(); + }); + var sink = transformer.bind(controller.sink); + + var results = []; + controller.stream.listen(results.add, onDone: expectAsync0(() { + expect(results, equals([1])); + })); + + sink.close(); + }); + + test('forwards the future from inner.close', () async { + var transformer = StreamSinkTransformer.fromHandlers(); + var innerSink = CompleterStreamSink(); + var sink = transformer.bind(innerSink); + + // The futures shouldn't complete until the inner sink's close future + // completes. + var doneResult = ResultFuture(sink.done); + doneResult.catchError((_) {}); + var closeResult = ResultFuture(sink.close()); + closeResult.catchError((_) {}); + await flushMicrotasks(); + expect(doneResult.isComplete, isFalse); + expect(closeResult.isComplete, isFalse); + + // Once the inner sink is completed, the futures should fire. + innerSink.completer.complete(); + await flushMicrotasks(); + expect(doneResult.isComplete, isTrue); + expect(closeResult.isComplete, isTrue); + }); + + test("doesn't top-level the future from inner.close", () async { + var transformer = + StreamSinkTransformer.fromHandlers(handleData: (_, sink) { + sink.close(); + }); + var innerSink = CompleterStreamSink(); + var sink = transformer.bind(innerSink); + + // This will close the inner sink, but it shouldn't top-level the error. + sink.add(1); + innerSink.completer.completeError('oh no'); + await flushMicrotasks(); + + // The error should be piped through done and close even if they're called + // after the underlying sink is closed. + expect(sink.done, throwsA('oh no')); + expect(sink.close(), throwsA('oh no')); + }); + }); +} diff --git a/pkgs/async/test/stream_splitter_test.dart b/pkgs/async/test/stream_splitter_test.dart new file mode 100644 index 00000000..27921db1 --- /dev/null +++ b/pkgs/async/test/stream_splitter_test.dart @@ -0,0 +1,290 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: unawaited_futures + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +void main() { + late StreamController controller; + late StreamSplitter splitter; + setUp(() { + controller = StreamController(); + splitter = StreamSplitter(controller.stream); + }); + + test("a branch that's created before the stream starts to replay it", + () async { + var events = []; + var branch = splitter.split(); + splitter.close(); + branch.listen(events.add); + + controller.add(1); + await flushMicrotasks(); + expect(events, equals([1])); + + controller.add(2); + await flushMicrotasks(); + expect(events, equals([1, 2])); + + controller.add(3); + await flushMicrotasks(); + expect(events, equals([1, 2, 3])); + + controller.close(); + }); + + test('a branch replays error events as well as data events', () { + var branch = splitter.split(); + splitter.close(); + + controller.add(1); + controller.addError('error'); + controller.add(3); + controller.close(); + + var count = 0; + branch.listen( + expectAsync1((value) { + expect(count, anyOf(0, 2)); + expect(value, equals(count + 1)); + count++; + }, count: 2), onError: expectAsync1((error) { + expect(count, equals(1)); + expect(error, equals('error')); + count++; + }), onDone: expectAsync0(() { + expect(count, equals(3)); + })); + }); + + test("a branch that's created in the middle of a stream replays it", + () async { + controller.add(1); + controller.add(2); + await flushMicrotasks(); + + var branch = splitter.split(); + splitter.close(); + + controller.add(3); + controller.add(4); + controller.close(); + + expect(branch.toList(), completion(equals([1, 2, 3, 4]))); + }); + + test("a branch that's created after the stream is finished replays it", + () async { + controller.add(1); + controller.add(2); + controller.add(3); + controller.close(); + await flushMicrotasks(); + + expect(splitter.split().toList(), completion(equals([1, 2, 3]))); + splitter.close(); + }); + + test('creates single-subscription branches', () async { + var branch = splitter.split(); + expect(branch.isBroadcast, isFalse); + branch.listen(null); + expect(() => branch.listen(null), throwsStateError); + expect(() => branch.listen(null), throwsStateError); + }); + + test('multiple branches each replay the stream', () async { + var branch1 = splitter.split(); + controller.add(1); + controller.add(2); + await flushMicrotasks(); + + var branch2 = splitter.split(); + controller.add(3); + controller.close(); + await flushMicrotasks(); + + var branch3 = splitter.split(); + splitter.close(); + + expect(branch1.toList(), completion(equals([1, 2, 3]))); + expect(branch2.toList(), completion(equals([1, 2, 3]))); + expect(branch3.toList(), completion(equals([1, 2, 3]))); + }); + + test("a branch doesn't close until the source stream closes", () async { + var branch = splitter.split(); + splitter.close(); + + var closed = false; + branch.last.then((_) => closed = true); + + controller.add(1); + controller.add(2); + controller.add(3); + await flushMicrotasks(); + expect(closed, isFalse); + + controller.close(); + await flushMicrotasks(); + expect(closed, isTrue); + }); + + test("the source stream isn't listened to until a branch is", () async { + expect(controller.hasListener, isFalse); + + var branch = splitter.split(); + splitter.close(); + await flushMicrotasks(); + expect(controller.hasListener, isFalse); + + branch.listen(null); + await flushMicrotasks(); + expect(controller.hasListener, isTrue); + }); + + test('the source stream is paused when all branches are paused', () async { + var branch1 = splitter.split(); + var branch2 = splitter.split(); + var branch3 = splitter.split(); + splitter.close(); + + var subscription1 = branch1.listen(null); + var subscription2 = branch2.listen(null); + var subscription3 = branch3.listen(null); + + subscription1.pause(); + await flushMicrotasks(); + expect(controller.isPaused, isFalse); + + subscription2.pause(); + await flushMicrotasks(); + expect(controller.isPaused, isFalse); + + subscription3.pause(); + await flushMicrotasks(); + expect(controller.isPaused, isTrue); + + subscription2.resume(); + await flushMicrotasks(); + expect(controller.isPaused, isFalse); + }); + + test('the source stream is paused when all branches are canceled', () async { + var branch1 = splitter.split(); + var branch2 = splitter.split(); + var branch3 = splitter.split(); + + var subscription1 = branch1.listen(null); + var subscription2 = branch2.listen(null); + var subscription3 = branch3.listen(null); + + subscription1.cancel(); + await flushMicrotasks(); + expect(controller.isPaused, isFalse); + + subscription2.cancel(); + await flushMicrotasks(); + expect(controller.isPaused, isFalse); + + subscription3.cancel(); + await flushMicrotasks(); + expect(controller.isPaused, isTrue); + + var branch4 = splitter.split(); + splitter.close(); + await flushMicrotasks(); + expect(controller.isPaused, isTrue); + + branch4.listen(null); + await flushMicrotasks(); + expect(controller.isPaused, isFalse); + }); + + test( + "the source stream is canceled when it's closed after all branches have " + 'been canceled', () async { + var branch1 = splitter.split(); + var branch2 = splitter.split(); + var branch3 = splitter.split(); + + var subscription1 = branch1.listen(null); + var subscription2 = branch2.listen(null); + var subscription3 = branch3.listen(null); + + subscription1.cancel(); + await flushMicrotasks(); + expect(controller.hasListener, isTrue); + + subscription2.cancel(); + await flushMicrotasks(); + expect(controller.hasListener, isTrue); + + subscription3.cancel(); + await flushMicrotasks(); + expect(controller.hasListener, isTrue); + + splitter.close(); + expect(controller.hasListener, isFalse); + }); + + test( + 'the source stream is canceled when all branches are canceled after it ' + 'has been closed', () async { + var branch1 = splitter.split(); + var branch2 = splitter.split(); + var branch3 = splitter.split(); + splitter.close(); + + var subscription1 = branch1.listen(null); + var subscription2 = branch2.listen(null); + var subscription3 = branch3.listen(null); + + subscription1.cancel(); + await flushMicrotasks(); + expect(controller.hasListener, isTrue); + + subscription2.cancel(); + await flushMicrotasks(); + expect(controller.hasListener, isTrue); + + subscription3.cancel(); + await flushMicrotasks(); + expect(controller.hasListener, isFalse); + }); + + test( + "a splitter that's closed before any branches are added never listens " + 'to the source stream', () { + splitter.close(); + + // This would throw an error if the stream had already been listened to. + controller.stream.listen(null); + }); + + test( + 'splitFrom splits a source stream into the designated number of ' + 'branches', () { + var branches = StreamSplitter.splitFrom(controller.stream, 5); + + controller.add(1); + controller.add(2); + controller.add(3); + controller.close(); + + expect(branches[0].toList(), completion(equals([1, 2, 3]))); + expect(branches[1].toList(), completion(equals([1, 2, 3]))); + expect(branches[2].toList(), completion(equals([1, 2, 3]))); + expect(branches[3].toList(), completion(equals([1, 2, 3]))); + expect(branches[4].toList(), completion(equals([1, 2, 3]))); + }); +} + +/// Wait for all microtasks to complete. +Future flushMicrotasks() => Future.delayed(Duration.zero); diff --git a/pkgs/async/test/stream_zip_test.dart b/pkgs/async/test/stream_zip_test.dart new file mode 100644 index 00000000..147d419c --- /dev/null +++ b/pkgs/async/test/stream_zip_test.dart @@ -0,0 +1,336 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +/// Create an error with the same values as [base], except that it throwsA +/// when seeing the value [errorValue]. +Stream streamError(Stream base, int errorValue, Object error) { + return base.map((x) => (x == errorValue) ? throw error : x); +} + +/// Make a [Stream] from an [Iterable] by adding events to a stream controller +/// at periodic intervals. +Stream mks(Iterable iterable) { + var iterator = iterable.iterator; + var controller = StreamController(); + // Some varying time between 3 and 10 ms. + var ms = ((++ctr) * 5) % 7 + 3; + Timer.periodic(Duration(milliseconds: ms), (Timer timer) { + if (iterator.moveNext()) { + controller.add(iterator.current); + } else { + controller.close(); + timer.cancel(); + } + }); + return controller.stream; +} + +/// Counter used to give varying delays for streams. +int ctr = 0; + +void main() { + // Test that zipping [streams] gives the results iterated by [expectedData]. + void testZip(Iterable streams, Iterable expectedData) { + var data = []; + Stream zip = StreamZip(streams); + zip.listen(data.add, onDone: expectAsync0(() { + expect(data, equals(expectedData)); + })); + } + + test('Basic', () { + testZip([ + mks([1, 2, 3]), + mks([4, 5, 6]), + mks([7, 8, 9]) + ], [ + [1, 4, 7], + [2, 5, 8], + [3, 6, 9] + ]); + }); + + test('Uneven length 1', () { + testZip([ + mks([1, 2, 3, 99, 100]), + mks([4, 5, 6]), + mks([7, 8, 9]) + ], [ + [1, 4, 7], + [2, 5, 8], + [3, 6, 9] + ]); + }); + + test('Uneven length 2', () { + testZip([ + mks([1, 2, 3]), + mks([4, 5, 6, 99, 100]), + mks([7, 8, 9]) + ], [ + [1, 4, 7], + [2, 5, 8], + [3, 6, 9] + ]); + }); + + test('Uneven length 3', () { + testZip([ + mks([1, 2, 3]), + mks([4, 5, 6]), + mks([7, 8, 9, 99, 100]) + ], [ + [1, 4, 7], + [2, 5, 8], + [3, 6, 9] + ]); + }); + + test('Uneven length 4', () { + testZip([ + mks([1, 2, 3, 98]), + mks([4, 5, 6]), + mks([7, 8, 9, 99, 100]) + ], [ + [1, 4, 7], + [2, 5, 8], + [3, 6, 9] + ]); + }); + + test('Empty 1', () { + testZip([ + mks([]), + mks([4, 5, 6]), + mks([7, 8, 9]) + ], []); + }); + + test('Empty 2', () { + testZip([ + mks([1, 2, 3]), + mks([]), + mks([7, 8, 9]) + ], []); + }); + + test('Empty 3', () { + testZip([ + mks([1, 2, 3]), + mks([4, 5, 6]), + mks([]) + ], []); + }); + + test('Empty source', () { + testZip([], []); + }); + + test('Single Source', () { + testZip([ + mks([1, 2, 3]) + ], [ + [1], + [2], + [3] + ]); + }); + + test('Other-streams', () { + var st1 = mks([1, 2, 3, 4, 5, 6]).where((x) => x < 4); + Stream st2 = + Stream.periodic(const Duration(milliseconds: 5), (x) => x + 4).take(3); + var c = StreamController.broadcast(); + var st3 = c.stream; + testZip([ + st1, + st2, + st3 + ], [ + [1, 4, 7], + [2, 5, 8], + [3, 6, 9] + ]); + c + ..add(7) + ..add(8) + ..add(9) + ..close(); + }); + + test('Error 1', () { + expect( + StreamZip([ + streamError(mks([1, 2, 3]), 2, 'BAD-1'), + mks([4, 5, 6]), + mks([7, 8, 9]) + ]).toList(), + throwsA(equals('BAD-1'))); + }); + + test('Error 2', () { + expect( + StreamZip([ + mks([1, 2, 3]), + streamError(mks([4, 5, 6]), 5, 'BAD-2'), + mks([7, 8, 9]) + ]).toList(), + throwsA(equals('BAD-2'))); + }); + + test('Error 3', () { + expect( + StreamZip([ + mks([1, 2, 3]), + mks([4, 5, 6]), + streamError(mks([7, 8, 9]), 8, 'BAD-3') + ]).toList(), + throwsA(equals('BAD-3'))); + }); + + test('Error at end', () { + expect( + StreamZip([ + mks([1, 2, 3]), + streamError(mks([4, 5, 6]), 6, 'BAD-4'), + mks([7, 8, 9]) + ]).toList(), + throwsA(equals('BAD-4'))); + }); + + test('Error before first end', () { + // StreamControllers' streams with no "close" called will never be done, + // so the fourth event of the first stream is guaranteed to come first. + expect( + StreamZip([ + streamError(mks([1, 2, 3, 4]), 4, 'BAD-5'), + (StreamController() + ..add(4) + ..add(5) + ..add(6)) + .stream, + (StreamController() + ..add(7) + ..add(8) + ..add(9)) + .stream + ]).toList(), + throwsA(equals('BAD-5'))); + }); + + test('Error after first end', () { + var controller = StreamController(); + controller + ..add(7) + ..add(8) + ..add(9); + // Transformer that puts error into controller when one of the first two + // streams have sent a done event. + var trans = + StreamTransformer.fromHandlers(handleDone: (EventSink s) { + Timer.run(() { + controller.addError('BAD-6'); + }); + s.close(); + }); + testZip([ + mks([1, 2, 3]).transform(trans), + mks([4, 5, 6]).transform(trans), + controller.stream + ], [ + [1, 4, 7], + [2, 5, 8], + [3, 6, 9] + ]); + }); + + test('Pause/Resume', () { + var sc1p = 0; + var c1 = StreamController(onPause: () { + sc1p++; + }, onResume: () { + sc1p--; + }); + + var sc2p = 0; + var c2 = StreamController(onPause: () { + sc2p++; + }, onResume: () { + sc2p--; + }); + + var done = expectAsync0(() { + expect(sc1p, equals(1)); + expect(sc2p, equals(0)); + }); // Call to complete test. + + Stream zip = StreamZip([c1.stream, c2.stream]); + + const ms25 = Duration(milliseconds: 25); + + // StreamIterator uses pause and resume to control flow. + var it = StreamIterator(zip); + + it.moveNext().then((hasMore) { + expect(hasMore, isTrue); + expect(it.current, equals([1, 2])); + return it.moveNext(); + }).then((hasMore) { + expect(hasMore, isTrue); + expect(it.current, equals([3, 4])); + c2.add(6); + return it.moveNext(); + }).then((hasMore) { + expect(hasMore, isTrue); + expect(it.current, equals([5, 6])); + Future.delayed(ms25).then((_) { + c2.add(8); + }); + return it.moveNext(); + }).then((hasMore) { + expect(hasMore, isTrue); + expect(it.current, equals([7, 8])); + c2.add(9); + return it.moveNext(); + }).then((hasMore) { + expect(hasMore, isFalse); + done(); + }); + + c1 + ..add(1) + ..add(3) + ..add(5) + ..add(7) + ..close(); + c2 + ..add(2) + ..add(4); + }); + + test('pause-resume2', () { + var s1 = Stream.fromIterable([0, 2, 4, 6, 8]); + var s2 = Stream.fromIterable([1, 3, 5, 7]); + var sz = StreamZip([s1, s2]); + var ctr = 0; + late StreamSubscription sub; + sub = sz.listen(expectAsync1((v) { + expect(v, equals([ctr * 2, ctr * 2 + 1])); + if (ctr == 1) { + sub.pause(Future.delayed(const Duration(milliseconds: 25))); + } else if (ctr == 2) { + sub.pause(); + Future.delayed(const Duration(milliseconds: 25)).then((_) { + sub.resume(); + }); + } + ctr++; + }, count: 4)); + }); +} diff --git a/pkgs/async/test/stream_zip_zone_test.dart b/pkgs/async/test/stream_zip_zone_test.dart new file mode 100644 index 00000000..a18c7761 --- /dev/null +++ b/pkgs/async/test/stream_zip_zone_test.dart @@ -0,0 +1,66 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:test/test.dart'; + +// Test that stream listener callbacks all happen in the zone where the +// listen occurred. + +void main() { + StreamController controller; + controller = StreamController(); + testStream('singlesub-async', controller, controller.stream); + controller = StreamController.broadcast(); + testStream('broadcast-async', controller, controller.stream); + controller = StreamController(); + testStream( + 'asbroadcast-async', controller, controller.stream.asBroadcastStream()); + + controller = StreamController(sync: true); + testStream('singlesub-sync', controller, controller.stream); + controller = StreamController.broadcast(sync: true); + testStream('broadcast-sync', controller, controller.stream); + controller = StreamController(sync: true); + testStream( + 'asbroadcast-sync', controller, controller.stream.asBroadcastStream()); +} + +void testStream(String name, StreamController controller, Stream stream) { + test(name, () { + var outer = Zone.current; + runZoned(() { + var newZone1 = Zone.current; + late StreamSubscription sub; + sub = stream.listen(expectAsync1((v) { + expect(v, 42); + expect(Zone.current, newZone1); + outer.run(() { + sub.onData(expectAsync1((v) { + expect(v, 37); + expect(Zone.current, newZone1); + runZoned(() { + sub.onData(expectAsync1((v) { + expect(v, 87); + expect(Zone.current, newZone1); + })); + }); + if (controller is SynchronousStreamController) { + scheduleMicrotask(() => controller.add(87)); + } else { + controller.add(87); + } + })); + }); + if (controller is SynchronousStreamController) { + scheduleMicrotask(() => controller.add(37)); + } else { + controller.add(37); + } + })); + }); + controller.add(42); + }); +} diff --git a/pkgs/async/test/subscription_stream_test.dart b/pkgs/async/test/subscription_stream_test.dart new file mode 100644 index 00000000..0d245f38 --- /dev/null +++ b/pkgs/async/test/subscription_stream_test.dart @@ -0,0 +1,182 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart' show SubscriptionStream; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + test('subscription stream of an entire subscription', () async { + var stream = createStream(); + var subscription = stream.listen(null); + var subscriptionStream = SubscriptionStream(subscription); + await flushMicrotasks(); + expect(subscriptionStream.toList(), completion([1, 2, 3, 4])); + }); + + test('subscription stream after two events', () async { + var stream = createStream(); + var skips = 0; + var completer = Completer>(); + late StreamSubscription subscription; + subscription = stream.listen((value) { + ++skips; + expect(value, skips); + if (skips == 2) { + completer.complete(SubscriptionStream(subscription)); + } + }); + var subscriptionStream = await completer.future; + await flushMicrotasks(); + expect(subscriptionStream.toList(), completion([3, 4])); + }); + + test('listening twice fails', () async { + var stream = createStream(); + var sourceSubscription = stream.listen(null); + var subscriptionStream = SubscriptionStream(sourceSubscription); + var subscription = subscriptionStream.listen(null); + expect(() => subscriptionStream.listen(null), throwsA(anything)); + await subscription.cancel(); + }); + + test('pause and cancel passed through to original stream', () async { + var controller = StreamController(onCancel: () async => 42); + var sourceSubscription = controller.stream.listen(null); + var subscriptionStream = SubscriptionStream(sourceSubscription); + expect(controller.isPaused, isTrue); + dynamic lastEvent; + var subscription = subscriptionStream.listen((value) { + lastEvent = value; + }); + controller.add(1); + + await flushMicrotasks(); + expect(lastEvent, 1); + expect(controller.isPaused, isFalse); + + subscription.pause(); + expect(controller.isPaused, isTrue); + + subscription.resume(); + expect(controller.isPaused, isFalse); + + expect(await subscription.cancel() as dynamic, 42); + expect(controller.hasListener, isFalse); + }); + + group('cancelOnError source:', () { + for (var sourceCancels in [false, true]) { + group('${sourceCancels ? "yes" : "no"}:', () { + late SubscriptionStream subscriptionStream; + late Future + onCancel; // Completes if source stream is canceled before done. + setUp(() { + var cancelCompleter = Completer(); + var source = createErrorStream(cancelCompleter); + onCancel = cancelCompleter.future; + var sourceSubscription = + source.listen(null, cancelOnError: sourceCancels); + subscriptionStream = SubscriptionStream(sourceSubscription); + }); + + test('- subscriptionStream: no', () async { + var done = Completer(); + var events = []; + subscriptionStream.listen(events.add, + onError: events.add, onDone: done.complete, cancelOnError: false); + var expected = [1, 2, 'To err is divine!']; + if (sourceCancels) { + await onCancel; + // And [done] won't complete at all. + var isDone = false; + done.future.then((_) { + isDone = true; + }); + await Future.delayed(const Duration(milliseconds: 5)); + expect(isDone, false); + } else { + expected.add(4); + await done.future; + } + expect(events, expected); + }); + + test('- subscriptionStream: yes', () async { + var completer = Completer(); + var events = []; + subscriptionStream.listen(events.add, + onError: (Object? value) { + events.add(value); + completer.complete(); + }, + onDone: () => throw 'should not happen', + cancelOnError: true); + await completer.future; + await flushMicrotasks(); + expect(events, [1, 2, 'To err is divine!']); + }); + }); + } + + for (var cancelOnError in [false, true]) { + group(cancelOnError ? 'yes' : 'no', () { + test('- no error, value goes to asFuture', () async { + var stream = createStream(); + var sourceSubscription = + stream.listen(null, cancelOnError: cancelOnError); + var subscriptionStream = SubscriptionStream(sourceSubscription); + var subscription = + subscriptionStream.listen(null, cancelOnError: cancelOnError); + expect(subscription.asFuture(42), completion(42)); + }); + + test('- error goes to asFuture', () async { + var stream = createErrorStream(); + var sourceSubscription = + stream.listen(null, cancelOnError: cancelOnError); + var subscriptionStream = SubscriptionStream(sourceSubscription); + + var subscription = + subscriptionStream.listen(null, cancelOnError: cancelOnError); + expect(subscription.asFuture(), throwsA(anything)); + }); + }); + } + }); +} + +Stream createStream() async* { + yield 1; + await flushMicrotasks(); + yield 2; + await flushMicrotasks(); + yield 3; + await flushMicrotasks(); + yield 4; +} + +Stream createErrorStream([Completer? onCancel]) async* { + var canceled = true; + try { + yield 1; + await flushMicrotasks(); + yield 2; + await flushMicrotasks(); + yield* Future.error('To err is divine!').asStream(); + await flushMicrotasks(); + yield 4; + await flushMicrotasks(); + canceled = false; + } finally { + // Completes before the "done", but should be after all events. + if (canceled && onCancel != null) { + await flushMicrotasks(); + onCancel.complete(); + } + } +} diff --git a/pkgs/async/test/subscription_transformer_test.dart b/pkgs/async/test/subscription_transformer_test.dart new file mode 100644 index 00000000..53610e19 --- /dev/null +++ b/pkgs/async/test/subscription_transformer_test.dart @@ -0,0 +1,291 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + group('with no callbacks', () { + test('forwards cancellation', () async { + var isCanceled = false; + var cancelCompleter = Completer(); + var controller = + StreamController(onCancel: expectAsync0>(() { + isCanceled = true; + return cancelCompleter.future; + })); + var subscription = controller.stream + .transform(subscriptionTransformer()) + .listen(expectAsync1((_) {}, count: 0)); + + var cancelFired = false; + subscription.cancel().then(expectAsync1((_) { + cancelFired = true; + })); + + await flushMicrotasks(); + expect(isCanceled, isTrue); + expect(cancelFired, isFalse); + + cancelCompleter.complete(); + await flushMicrotasks(); + expect(cancelFired, isTrue); + + // This shouldn't call the onCancel callback again. + expect(subscription.cancel(), completes); + }); + + test('forwards pausing and resuming', () async { + var controller = StreamController(); + var subscription = controller.stream + .transform(subscriptionTransformer()) + .listen(expectAsync1((_) {}, count: 0)); + + subscription.pause(); + await flushMicrotasks(); + expect(controller.isPaused, isTrue); + + subscription.pause(); + await flushMicrotasks(); + expect(controller.isPaused, isTrue); + + subscription.resume(); + await flushMicrotasks(); + expect(controller.isPaused, isTrue); + + subscription.resume(); + await flushMicrotasks(); + expect(controller.isPaused, isFalse); + }); + + test('forwards pausing with a resume future', () async { + var controller = StreamController(); + var subscription = controller.stream + .transform(subscriptionTransformer()) + .listen(expectAsync1((_) {}, count: 0)); + + var completer = Completer(); + subscription.pause(completer.future); + await flushMicrotasks(); + expect(controller.isPaused, isTrue); + + completer.complete(); + await flushMicrotasks(); + expect(controller.isPaused, isFalse); + }); + }); + + group('with a cancel callback', () { + test('invokes the callback when the subscription is canceled', () async { + var isCanceled = false; + var callbackInvoked = false; + var controller = StreamController(onCancel: expectAsync0(() { + isCanceled = true; + })); + var subscription = controller.stream.transform( + subscriptionTransformer(handleCancel: expectAsync1((inner) { + callbackInvoked = true; + inner.cancel(); + return Future.value(); + }))).listen(expectAsync1((_) {}, count: 0)); + + await flushMicrotasks(); + expect(callbackInvoked, isFalse); + expect(isCanceled, isFalse); + + subscription.cancel(); + await flushMicrotasks(); + expect(callbackInvoked, isTrue); + expect(isCanceled, isTrue); + }); + + test('invokes the callback once and caches its result', () async { + var completer = Completer(); + var controller = StreamController(); + var subscription = controller.stream + .transform(subscriptionTransformer( + handleCancel: expectAsync1((inner) => completer.future))) + .listen(expectAsync1((_) {}, count: 0)); + + var cancelFired1 = false; + subscription.cancel().then(expectAsync1((_) { + cancelFired1 = true; + })); + + var cancelFired2 = false; + subscription.cancel().then(expectAsync1((_) { + cancelFired2 = true; + })); + + await flushMicrotasks(); + expect(cancelFired1, isFalse); + expect(cancelFired2, isFalse); + + completer.complete(); + await flushMicrotasks(); + expect(cancelFired1, isTrue); + expect(cancelFired2, isTrue); + }); + }); + + group('with a pause callback', () { + test('invokes the callback when pause is called', () async { + var pauseCount = 0; + var controller = StreamController(); + var subscription = controller.stream + .transform(subscriptionTransformer( + handlePause: expectAsync1((inner) { + pauseCount++; + inner.pause(); + }, count: 3))) + .listen(expectAsync1((_) {}, count: 0)); + + await flushMicrotasks(); + expect(pauseCount, equals(0)); + + subscription.pause(); + await flushMicrotasks(); + expect(pauseCount, equals(1)); + + subscription.pause(); + await flushMicrotasks(); + expect(pauseCount, equals(2)); + + subscription.resume(); + subscription.resume(); + await flushMicrotasks(); + expect(pauseCount, equals(2)); + + subscription.pause(); + await flushMicrotasks(); + expect(pauseCount, equals(3)); + }); + + test("doesn't invoke the callback when the subscription has been canceled", + () async { + var controller = StreamController(); + var subscription = controller.stream + .transform(subscriptionTransformer( + handlePause: expectAsync1((_) {}, count: 0))) + .listen(expectAsync1((_) {}, count: 0)); + + subscription.cancel(); + subscription.pause(); + subscription.pause(); + subscription.pause(); + }); + }); + + group('with a resume callback', () { + test('invokes the callback when resume is called', () async { + var resumeCount = 0; + var controller = StreamController(); + var subscription = controller.stream + .transform(subscriptionTransformer( + handleResume: expectAsync1((inner) { + resumeCount++; + inner.resume(); + }, count: 3))) + .listen(expectAsync1((_) {}, count: 0)); + + await flushMicrotasks(); + expect(resumeCount, equals(0)); + + subscription.resume(); + await flushMicrotasks(); + expect(resumeCount, equals(1)); + + subscription.pause(); + subscription.pause(); + await flushMicrotasks(); + expect(resumeCount, equals(1)); + + subscription.resume(); + await flushMicrotasks(); + expect(resumeCount, equals(2)); + + subscription.resume(); + await flushMicrotasks(); + expect(resumeCount, equals(3)); + }); + + test('invokes the callback when a resume future completes', () async { + var resumed = false; + var controller = StreamController(); + var subscription = controller.stream.transform( + subscriptionTransformer(handleResume: expectAsync1((inner) { + resumed = true; + inner.resume(); + }))).listen(expectAsync1((_) {}, count: 0)); + + var completer = Completer(); + subscription.pause(completer.future); + await flushMicrotasks(); + expect(resumed, isFalse); + + completer.complete(); + await flushMicrotasks(); + expect(resumed, isTrue); + }); + + test("doesn't invoke the callback when the subscription has been canceled", + () async { + var controller = StreamController(); + var subscription = controller.stream + .transform(subscriptionTransformer( + handlePause: expectAsync1((_) {}, count: 0))) + .listen(expectAsync1((_) {}, count: 0)); + + subscription.cancel(); + subscription.resume(); + subscription.resume(); + subscription.resume(); + }); + }); + + group('when the outer subscription is canceled but the inner is not', () { + late StreamSubscription subscription; + setUp(() { + var controller = StreamController(); + subscription = controller.stream + .transform( + subscriptionTransformer(handleCancel: (_) => Future.value())) + .listen(expectAsync1((_) {}, count: 0), + onError: expectAsync2((_, __) {}, count: 0), + onDone: expectAsync0(() {}, count: 0)); + subscription.cancel(); + controller.add(1); + controller.addError('oh no!'); + controller.close(); + }); + + test("doesn't call a new onData", () async { + subscription.onData(expectAsync1((_) {}, count: 0)); + await flushMicrotasks(); + }); + + test("doesn't call a new onError", () async { + subscription.onError(expectAsync2((_, __) {}, count: 0)); + await flushMicrotasks(); + }); + + test("doesn't call a new onDone", () async { + subscription.onDone(expectAsync0(() {}, count: 0)); + await flushMicrotasks(); + }); + + test('isPaused returns false', () { + expect(subscription.isPaused, isFalse); + }); + + test('asFuture never completes', () async { + subscription.asFuture().then(expectAsync1((_) {}, count: 0)); + await flushMicrotasks(); + }); + }); +} diff --git a/pkgs/async/test/typed_wrapper/stream_subscription_test.dart b/pkgs/async/test/typed_wrapper/stream_subscription_test.dart new file mode 100644 index 00000000..74195bab --- /dev/null +++ b/pkgs/async/test/typed_wrapper/stream_subscription_test.dart @@ -0,0 +1,143 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/src/typed/stream_subscription.dart'; +import 'package:test/test.dart'; + +import '../utils.dart'; + +void main() { + group('with valid types, forwards', () { + late StreamController controller; + late StreamSubscription wrapper; + late bool isCanceled; + setUp(() { + isCanceled = false; + controller = StreamController(onCancel: () { + isCanceled = true; + }); + wrapper = TypeSafeStreamSubscription(controller.stream.listen(null)); + }); + + test('onData()', () { + wrapper.onData(expectAsync1((data) { + expect(data, equals(1)); + })); + controller.add(1); + }); + + test('onError()', () { + wrapper.onError(expectAsync1((error) { + expect(error, equals('oh no')); + })); + controller.addError('oh no'); + }); + + test('onDone()', () { + wrapper.onDone(expectAsync0(() {})); + controller.close(); + }); + + test('pause(), resume(), and isPaused', () async { + expect(wrapper.isPaused, isFalse); + + wrapper.pause(); + await flushMicrotasks(); + expect(controller.isPaused, isTrue); + expect(wrapper.isPaused, isTrue); + + wrapper.resume(); + await flushMicrotasks(); + expect(controller.isPaused, isFalse); + expect(wrapper.isPaused, isFalse); + }); + + test('cancel()', () async { + wrapper.cancel(); + await flushMicrotasks(); + expect(isCanceled, isTrue); + }); + + test('asFuture()', () { + expect(wrapper.asFuture(12), completion(equals(12))); + controller.close(); + }); + }); + + group('with invalid types,', () { + late StreamController controller; + late StreamSubscription wrapper; + late bool isCanceled; + setUp(() { + isCanceled = false; + controller = StreamController(onCancel: () { + isCanceled = true; + }); + wrapper = TypeSafeStreamSubscription(controller.stream.listen(null)); + }); + + group('throws a TypeError for', () { + test('onData()', () { + expect(() { + // TODO(nweiz): Use the wrapper declared in setUp when sdk#26226 is + // fixed. + controller = StreamController(); + wrapper = + TypeSafeStreamSubscription(controller.stream.listen(null)); + + wrapper.onData(expectAsync1((_) {}, count: 0)); + controller.add('foo'); + }, throwsZonedTypeError); + }); + }); + + group("doesn't throw a TypeError for", () { + test('onError()', () { + wrapper.onError(expectAsync1((error) { + expect(error, equals('oh no')); + })); + controller.add('foo'); + controller.addError('oh no'); + }); + + test('onDone()', () { + wrapper.onDone(expectAsync0(() {})); + controller.add('foo'); + controller.close(); + }); + + test('pause(), resume(), and isPaused', () async { + controller.add('foo'); + + expect(wrapper.isPaused, isFalse); + + wrapper.pause(); + await flushMicrotasks(); + expect(controller.isPaused, isTrue); + expect(wrapper.isPaused, isTrue); + + wrapper.resume(); + await flushMicrotasks(); + expect(controller.isPaused, isFalse); + expect(wrapper.isPaused, isFalse); + }); + + test('cancel()', () async { + controller.add('foo'); + + wrapper.cancel(); + await flushMicrotasks(); + expect(isCanceled, isTrue); + }); + + test('asFuture()', () { + expect(wrapper.asFuture(12), completion(equals(12))); + controller.add('foo'); + controller.close(); + }); + }); + }); +} diff --git a/pkgs/async/test/utils.dart b/pkgs/async/test/utils.dart new file mode 100644 index 00000000..0a6b339a --- /dev/null +++ b/pkgs/async/test/utils.dart @@ -0,0 +1,126 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Helper utilities for testing. +library; + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +/// A zero-millisecond timer should wait until after all microtasks. +Future flushMicrotasks() => Future.delayed(Duration.zero); + +typedef OptionalArgAction = void Function([dynamic a, dynamic b]); + +/// A generic unreachable callback function. +/// +/// Returns a function that fails the test if it is ever called. +OptionalArgAction unreachable(String name) => + ([a, b]) => fail('Unreachable: $name'); + +/// A matcher that runs a callback in its own zone and asserts that that zone +/// emits an error that matches [matcher]. +Matcher throwsZoned(Matcher matcher) => predicate((void Function() callback) { + var firstError = true; + runZonedGuarded( + callback, + expectAsync2((error, stackTrace) { + if (firstError) { + expect(error, matcher); + firstError = false; + } else { + registerException(error, stackTrace); + } + }, max: -1)); + return true; + }); + +/// A matcher that runs a callback in its own zone and asserts that that zone +/// emits a [TypeError]. +final throwsZonedTypeError = throwsZoned(isA()); + +/// A matcher that matches a callback or future that throws a [TypeError]. +final throwsTypeError = throwsA(isA()); + +/// A badly behaved stream which throws if it's ever listened to. +/// +/// Can be used to test cases where a stream should not be used. +class UnusableStream extends Stream { + @override + StreamSubscription listen(void Function(T event)? onData, + {Function? onError, void Function()? onDone, bool? cancelOnError}) { + throw UnimplementedError('Gotcha!'); + } +} + +/// A dummy [StreamSink] for testing the routing of the [done] and [close] +/// futures. +/// +/// The [completer] field allows the user to control the future returned by +/// [done] and [close]. +class CompleterStreamSink implements StreamSink { + final completer = Completer(); + + @override + Future get done => completer.future; + + @override + void add(T event) {} + @override + void addError(Object error, [StackTrace? stackTrace]) {} + @override + Future addStream(Stream stream) async {} + @override + Future close() => completer.future; +} + +/// A [StreamSink] that collects all events added to it as results. +/// +/// This is used for testing code that interacts with sinks. +class TestSink implements StreamSink { + /// The results corresponding to events that have been added to the sink. + final results = >[]; + + /// Whether [close] has been called. + bool get isClosed => _isClosed; + var _isClosed = false; + + @override + Future get done => _doneCompleter.future; + final _doneCompleter = Completer(); + + final void Function() _onDone; + + /// Creates a new sink. + /// + /// If [onDone] is passed, it's called when the user calls [close]. Its result + /// is piped to the [done] future. + TestSink({void Function()? onDone}) : _onDone = onDone ?? (() {}); + + @override + void add(T event) { + results.add(Result.value(event)); + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + results.add(Result.error(error, stackTrace)); + } + + @override + Future addStream(Stream stream) { + var completer = Completer.sync(); + stream.listen(add, onError: addError, onDone: completer.complete); + return completer.future; + } + + @override + Future close() { + _isClosed = true; + _doneCompleter.complete(Future.microtask(_onDone)); + return done; + } +}