Add `debounce` utilities and prepare for publish (dart-lang/stream_transform#3)
- Add `debounce` and `debounceBuffer` to drop or collect values that
occur quickly
- Add tests
- Drop `-dev` from pubspec to prepare for publishing very early version
- Add LICENSE file
- Exclude dartanalyzer and dartfmt test from 1.22.1 SDK
diff --git a/pkgs/stream_transform/.travis.yml b/pkgs/stream_transform/.travis.yml
index ed6c5bb..4c02d8e 100644
--- a/pkgs/stream_transform/.travis.yml
+++ b/pkgs/stream_transform/.travis.yml
@@ -11,3 +11,9 @@
- test
- dartfmt
- dartanalyzer
+matrix:
+ exclude:
+ - dart: 1.22.1
+ dart_task: dartfmt
+ - dart: 1.22.1
+ dart_task: dartanalyzer
diff --git a/pkgs/stream_transform/CHANGELOG.md b/pkgs/stream_transform/CHANGELOG.md
index af394b6..cf303c7 100644
--- a/pkgs/stream_transform/CHANGELOG.md
+++ b/pkgs/stream_transform/CHANGELOG.md
@@ -1,4 +1,6 @@
## 0.0.1
-- Add `buffer` utility. Collects events in a `List` until a `trigger` stream
- fires.
+- Initial release with the following utilities:
+ - `buffer`: Collects events in a `List` until a `trigger` stream fires.
+ - `debounce`, `debounceBuffer`: Collect or drop events which occur closer in
+ time than a given duration.
diff --git a/pkgs/stream_transform/LICENSE b/pkgs/stream_transform/LICENSE
new file mode 100644
index 0000000..389ce98
--- /dev/null
+++ b/pkgs/stream_transform/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2017, the Dart project authors. All rights reserved.
+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 Inc. 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/stream_transform/lib/src/debounce.dart b/pkgs/stream_transform/lib/src/debounce.dart
new file mode 100644
index 0000000..980c371
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/debounce.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.
+import 'dart:async';
+
+/// Creates a StreamTransformer which only emits when the source stream does not
+/// emit for [duration].
+///
+/// Source values will always be delayed by at least [duration], and values
+/// which come within this time will replace the old values, only the most
+/// recent value will be emitted.
+StreamTransformer<T, T> debounce<T>(Duration duration) =>
+ _debounceAggregate(duration, _dropPrevious);
+
+/// Creates a StreamTransformer which collects values until the source stream
+/// does not emit for [duration] then emits the collected values.
+///
+/// This differs from [debounce] in that values are aggregated instead of
+/// skipped.
+StreamTransformer<T, List<T>> debounceBuffer<T>(Duration duration) =>
+ _debounceAggregate(duration, _collectToList);
+
+List<T> _collectToList<T>(T element, List<T> soFar) {
+ soFar ??= <T>[];
+ soFar.add(element);
+ return soFar;
+}
+
+T _dropPrevious<T>(T element, _) => element;
+
+/// Creates a StreamTransformer which aggregates values until the source stream
+/// does not emit for [duration], then emits the aggregated values.
+StreamTransformer<T, R> _debounceAggregate<T, R>(
+ Duration duration, R collect(T element, R soFar)) {
+ Timer timer;
+ R soFar;
+ bool shouldClose = false;
+ return new StreamTransformer.fromHandlers(
+ handleData: (T value, EventSink<R> sink) {
+ timer?.cancel();
+ timer = new Timer(duration, () {
+ sink.add(soFar);
+ if (shouldClose) {
+ sink.close();
+ }
+ soFar = null;
+ timer = null;
+ });
+ soFar = collect(value, soFar);
+ }, handleDone: (EventSink<T> sink) {
+ if (soFar != null) {
+ shouldClose = true;
+ } else {
+ sink.close();
+ }
+ });
+}
diff --git a/pkgs/stream_transform/lib/stream_transform.dart b/pkgs/stream_transform/lib/stream_transform.dart
index 02069f5..49a714e 100644
--- a/pkgs/stream_transform/lib/stream_transform.dart
+++ b/pkgs/stream_transform/lib/stream_transform.dart
@@ -3,3 +3,4 @@
// BSD-style license that can be found in the LICENSE file.
export 'src/buffer.dart';
+export 'src/debounce.dart';
diff --git a/pkgs/stream_transform/pubspec.yaml b/pkgs/stream_transform/pubspec.yaml
index b260f12..1cf4034 100644
--- a/pkgs/stream_transform/pubspec.yaml
+++ b/pkgs/stream_transform/pubspec.yaml
@@ -2,7 +2,7 @@
description: A collection of utilities to transform and manipulate streams.
author: Dart Team <misc@dartlang.org>
homepage: https://www.github.com/dart-lang/stream_transform
-version: 0.0.1-dev
+version: 0.0.1
environment:
sdk: ">=1.22.0 <2.0.0"
diff --git a/pkgs/stream_transform/test/debounce_test.dart b/pkgs/stream_transform/test/debounce_test.dart
new file mode 100644
index 0000000..a556b41
--- /dev/null
+++ b/pkgs/stream_transform/test/debounce_test.dart
@@ -0,0 +1,110 @@
+import 'dart:async';
+import 'package:test/test.dart';
+import 'package:stream_transform/stream_transform.dart';
+
+void main() {
+ var streamTypes = {
+ 'single subscription': () => new StreamController(),
+ 'broadcast': () => new StreamController.broadcast()
+ };
+ for (var streamType in streamTypes.keys) {
+ group('Stream type [$streamType]', () {
+ StreamController values;
+ List emittedValues;
+ bool valuesCanceled;
+ bool isDone;
+ List errors;
+ StreamSubscription subscription;
+
+ void setUpStreams(StreamTransformer transformer) {
+ valuesCanceled = false;
+ values = streamTypes[streamType]()
+ ..onCancel = () {
+ valuesCanceled = true;
+ };
+ emittedValues = [];
+ errors = [];
+ isDone = false;
+ subscription = values.stream
+ .transform(transformer)
+ .listen(emittedValues.add, onError: errors.add, onDone: () {
+ isDone = true;
+ });
+ }
+
+ group('debounce', () {
+ setUp(() async {
+ setUpStreams(debounce(const Duration(milliseconds: 5)));
+ });
+
+ test('cancels values', () async {
+ await subscription.cancel();
+ expect(valuesCanceled, true);
+ });
+
+ test('swallows values that come faster than duration', () async {
+ values.add(1);
+ values.add(2);
+ await values.close();
+ await new Future.delayed(const Duration(milliseconds: 10));
+ expect(emittedValues, [2]);
+ });
+
+ test('outputs multiple values spaced further than duration', () async {
+ values.add(1);
+ await new Future.delayed(const Duration(milliseconds: 10));
+ values.add(2);
+ await new Future.delayed(const Duration(milliseconds: 10));
+ expect(emittedValues, [1, 2]);
+ });
+
+ test('waits for pending value to close', () async {
+ values.add(1);
+ await new Future.delayed(const Duration(milliseconds: 10));
+ await values.close();
+ await new Future(() {});
+ expect(isDone, true);
+ });
+
+ test('closes output if there are no pending values', () async {
+ values.add(1);
+ await new Future.delayed(const Duration(milliseconds: 10));
+ values.add(2);
+ await new Future(() {});
+ await values.close();
+ expect(isDone, false);
+ await new Future.delayed(const Duration(milliseconds: 10));
+ expect(isDone, true);
+ });
+ });
+
+ group('debounceBuffer', () {
+ setUp(() async {
+ setUpStreams(debounceBuffer(const Duration(milliseconds: 5)));
+ });
+
+ test('Emits all values as a list', () async {
+ values.add(1);
+ values.add(2);
+ await values.close();
+ await new Future.delayed(const Duration(milliseconds: 10));
+ expect(emittedValues, [
+ [1, 2]
+ ]);
+ });
+
+ test('separate lists for multiple values spaced further than duration',
+ () async {
+ values.add(1);
+ await new Future.delayed(const Duration(milliseconds: 10));
+ values.add(2);
+ await new Future.delayed(const Duration(milliseconds: 10));
+ expect(emittedValues, [
+ [1],
+ [2]
+ ]);
+ });
+ });
+ });
+ }
+}