Call `tap` data callback once per event (dart-lang/stream_transform#26)
This is slightly different from the other bugs. In this case the output
stream correctly forwarded values to all listeners, but the callback was
also called once per listener.
- Add `handleError` to `fromHandlers` and tests.
- Add tests for expected single-call behavior. These fail on the old
implementation.
- Add test for the values getting to multiple listeners. This passes on
both the old and new implementation.
- Switch to `fromHandlers`.
diff --git a/pkgs/stream_transform/CHANGELOG.md b/pkgs/stream_transform/CHANGELOG.md
index 00e2600..7f1aedf 100644
--- a/pkgs/stream_transform/CHANGELOG.md
+++ b/pkgs/stream_transform/CHANGELOG.md
@@ -2,6 +2,8 @@
- Bug Fix: Some transformers did not correctly add data to all listeners on
broadcast streams. Fixed for `throttle`, `debounce`, and `audit`.
+- Bug Fix: Only call the `tap` data callback once per event rather than once per
+ listener.
## 0.0.5
diff --git a/pkgs/stream_transform/lib/src/from_handlers.dart b/pkgs/stream_transform/lib/src/from_handlers.dart
index 8157ea1..315de96 100644
--- a/pkgs/stream_transform/lib/src/from_handlers.dart
+++ b/pkgs/stream_transform/lib/src/from_handlers.dart
@@ -2,26 +2,43 @@
typedef void HandleData<S, T>(S value, EventSink<T> sink);
typedef void HandleDone<T>(EventSink<T> sink);
+typedef void HandleError<T>(
+ Object error, StackTrace stackTrace, EventSink<T> sink);
/// Like [new StreamTransformer.fromHandlers] but the handlers are called once
/// per event rather than once per listener for broadcast streams.
StreamTransformer<S, T> fromHandlers<S, T>(
- {HandleData<S, T> handleData, HandleDone<T> handleDone}) =>
- new _StreamTransformer(handleData: handleData, handleDone: handleDone);
+ {HandleData<S, T> handleData,
+ HandleError<T> handleError,
+ HandleDone<T> handleDone}) =>
+ new _StreamTransformer(
+ handleData: handleData,
+ handleError: handleError,
+ handleDone: handleDone);
class _StreamTransformer<S, T> implements StreamTransformer<S, T> {
final HandleData<S, T> _handleData;
final HandleDone<T> _handleDone;
+ final HandleError<T> _handleError;
- _StreamTransformer({HandleData<S, T> handleData, HandleDone<T> handleDone})
+ _StreamTransformer(
+ {HandleData<S, T> handleData,
+ HandleError<T> handleError,
+ HandleDone<T> handleDone})
: _handleData = handleData ?? _defaultHandleData,
+ _handleError = handleError ?? _defaultHandleError,
_handleDone = handleDone ?? _defaultHandleDone;
- static _defaultHandleData<S, T>(S value, EventSink<T> sink) {
+ static void _defaultHandleData<S, T>(S value, EventSink<T> sink) {
sink.add(value as T);
}
- static _defaultHandleDone<T>(EventSink<T> sink) {
+ static void _defaultHandleError<T>(
+ Object error, StackTrace stackTrace, EventSink<T> sink) {
+ sink.addError(error, stackTrace);
+ }
+
+ static void _defaultHandleDone<T>(EventSink<T> sink) {
sink.close();
}
@@ -39,7 +56,9 @@
return;
}
subscription = values.listen((value) => _handleData(value, controller),
- onError: controller.addError, onDone: () {
+ onError: (error, stackTrace) {
+ _handleError(error, stackTrace, controller);
+ }, onDone: () {
_handleDone(controller);
});
};
diff --git a/pkgs/stream_transform/lib/src/tap.dart b/pkgs/stream_transform/lib/src/tap.dart
index 2ec8cf8..8046640 100644
--- a/pkgs/stream_transform/lib/src/tap.dart
+++ b/pkgs/stream_transform/lib/src/tap.dart
@@ -3,6 +3,8 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
+import 'from_handlers.dart';
+
/// Taps into a Stream to allow additional handling on a single-subscriber
/// stream without first wrapping as a broadcast stream.
///
@@ -13,7 +15,7 @@
/// listener, and may be canceled only by the listener.
StreamTransformer<T, T> tap<T>(void fn(T value),
{void onError(error, stackTrace), void onDone()}) =>
- new StreamTransformer.fromHandlers(handleData: (value, sink) {
+ fromHandlers(handleData: (value, sink) {
try {
fn(value);
} catch (_) {/*Ignore*/}
diff --git a/pkgs/stream_transform/test/from_handlers_test.dart b/pkgs/stream_transform/test/from_handlers_test.dart
index 28436bd..268b55e 100644
--- a/pkgs/stream_transform/test/from_handlers_test.dart
+++ b/pkgs/stream_transform/test/from_handlers_test.dart
@@ -136,18 +136,23 @@
group('broadcast stream with multiple listeners', () {
int dataCallCount;
int doneCallCount;
+ int errorCallCount;
setUp(() async {
dataCallCount = 0;
doneCallCount = 0;
+ errorCallCount = 0;
setUpForController(
new StreamController.broadcast(),
fromHandlers(handleData: (value, sink) {
dataCallCount++;
+ }, handleError: (error, stackTrace, sink) {
+ errorCallCount++;
+ sink.addError(error, stackTrace);
}, handleDone: (sink) {
doneCallCount++;
}));
- transformed.listen((_) {});
+ transformed.listen((_) {}, onError: (_, __) {});
});
test('handles data once', () async {
@@ -160,6 +165,12 @@
await values.close();
expect(doneCallCount, 1);
});
+
+ test('handles errors once', () async {
+ values.addError('error');
+ await new Future(() {});
+ expect(errorCallCount, 1);
+ });
});
});
}
diff --git a/pkgs/stream_transform/test/tap_test.dart b/pkgs/stream_transform/test/tap_test.dart
index 8aecee6..eb64eee 100644
--- a/pkgs/stream_transform/test/tap_test.dart
+++ b/pkgs/stream_transform/test/tap_test.dart
@@ -53,4 +53,59 @@
await source.close();
expect(doneCalled, true);
});
+
+ test('forwards only once with multiple listeners on a broadcast stream',
+ () async {
+ var dataCallCount = 0;
+ var source = new StreamController.broadcast();
+ source.stream.transform(tap((_) {
+ dataCallCount++;
+ }))
+ ..listen((_) {})
+ ..listen((_) {});
+ source.add(1);
+ await new Future(() {});
+ expect(dataCallCount, 1);
+ });
+
+ test(
+ 'forwards errors only once with multiple listeners on a broadcast stream',
+ () async {
+ var errorCallCount = 0;
+ var source = new StreamController.broadcast();
+ source.stream.transform(tap((_) {}, onError: (_, __) {
+ errorCallCount++;
+ }))
+ ..listen((_) {}, onError: (_, __) {})
+ ..listen((_) {}, onError: (_, __) {});
+ source.addError('error');
+ await new Future(() {});
+ expect(errorCallCount, 1);
+ });
+
+ test('calls onDone only once with multiple listeners on a broadcast stream',
+ () async {
+ var doneCallCount = 0;
+ var source = new StreamController.broadcast();
+ source.stream.transform(tap((_) {}, onDone: () {
+ doneCallCount++;
+ }))
+ ..listen((_) {})
+ ..listen((_) {});
+ await source.close();
+ expect(doneCallCount, 1);
+ });
+
+ test('forwards values to multiple listeners', () async {
+ var source = new StreamController.broadcast();
+ var emittedValues1 = [];
+ var emittedValues2 = [];
+ source.stream.transform(tap((_) {}))
+ ..listen(emittedValues1.add)
+ ..listen(emittedValues2.add);
+ source.add(1);
+ await new Future(() {});
+ expect(emittedValues1, [1]);
+ expect(emittedValues2, [1]);
+ });
}