├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── example ├── README.md ├── auto_complete │ ├── index.dart │ └── index.html └── registration_form │ ├── README.md │ ├── index.dart │ ├── index.html │ └── styles.css ├── lib ├── frappe.dart └── src │ ├── event_stream.dart │ ├── property.dart │ └── reactable.dart ├── pubspec.yaml ├── test ├── all_tests.dart ├── event_stream_test.dart ├── property_test.dart └── shared │ ├── as_event_stream.dart │ ├── async_expand.dart │ ├── async_map.dart │ ├── buffer_when.dart │ ├── combine.dart │ ├── debounce.dart │ ├── delay.dart │ ├── distinct.dart │ ├── expand.dart │ ├── flat_map.dart │ ├── flat_map_latest.dart │ ├── handle_error.dart │ ├── map.dart │ ├── merge.dart │ ├── return_types.dart │ ├── scan.dart │ ├── skip.dart │ ├── skip_until.dart │ ├── skip_while.dart │ ├── take.dart │ ├── take_until.dart │ ├── take_while.dart │ ├── util.dart │ ├── when.dart │ ├── where.dart │ └── zip.dart └── tool └── grind.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Don’t commit the following directories created by pub. 2 | build/ 3 | packages 4 | .packages 5 | .pub/ 6 | 7 | # Or the files created by dart2js. 8 | *.dart.js 9 | *.dart.precompiled.js 10 | *.js_ 11 | *.js.deps 12 | *.js.map 13 | 14 | doc/api/ 15 | 16 | # Include when developing application packages. 17 | pubspec.lock 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | env: 3 | global: 4 | secure: bYjoEA0gSLf/OzVpwKY/QiWniDNYUQNyOO5A5UpvJytttheuFotDcN87kL82ZoV9BoKLNpkWtEg+Rv8kUl2WohSgRgrNYt3uJi0ZNo1VLB3UXGD7Nx00/Hu+lh/NOxckiGsxeY0CbYfRW18HQDaesO0H8PqyXf9HcSRPwXMXnEw= 5 | dart: 6 | - stable 7 | - dev 8 | script: 9 | - dart tool/grind.dart build 10 | after_success: 11 | - pub global activate dart_coveralls 12 | - dart_coveralls report --exclude-test-files --token $COVERALLS_TOKEN ./test/all_tests.dart 13 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the Frappe project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Dan Schultz 7 | Anders Holmgren 8 | Aaron Washington 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | *Note:* Patch versions that only include documentation changes are omitted. 4 | 5 | ## 0.4.0+5 (08/06/2015) 6 | 7 | - Add better type annotations to suppress warnings in DDC. [[#47](https://github.com/danschultz/frappe/issues/47)] 8 | 9 | ## 0.4.0+3 (03/09/2015) 10 | 11 | - Fix an issue where the stream returned by `Property.asEventStream()` would still behave like a property [[#38](https://github.com/danschultz/frappe/issues/38)] 12 | 13 | ## 0.4.0 (03/02/2015) 14 | 15 | - Fix an issue where `EventStream`s wouldn't be the same type of stream as its source [[#17](https://github.com/danschultz/frappe/issues/17)] 16 | - Transformation methods on `Property` or `EventStream` now return the same type of `Reactable` 17 | - Add `Reactable.concat()` 18 | - Add `Reactable.concatAll()` 19 | - Add `Reactable.doAction()` 20 | - Add `Reactable.mergeAll()` 21 | - Add `Reactable.sampleOn()` 22 | - Add `Reactable.sampleEachPeriod()` 23 | - Add `Reactable.selectFirst()` 24 | - Add `Reactable.startWith()` 25 | - Add `Reactable.startWithValues()` 26 | - Add `Reactable.zip()` 27 | - Add `EventStream.empty()` constructor 28 | - Add `EventStream.fromValue()` constructor 29 | - Add `EventStream.periodic()` constructor 30 | - Bug fixes in `Reactable.isWaitingOn()` 31 | - Deprecate `Reactable.asStream()`, it's now `Reactable.asEventStream()` 32 | - Deprecate `Property` operator overrides, `equals()`, `>`, `>=`, `<`, `<=`, `+`, `-`, `*`, `/` 33 | - `Property.and()` and `Property.or()` can now accept any stream 34 | - `Property.not()` has been moved to `Reactable` 35 | - Remove type declerations for `Reactable.scan()` 36 | 37 | ## 0.3.2+1 (01/10/2015) 38 | 39 | - Move stream transformation classes to the *[stream_transformers]* package. 40 | 41 | [stream_transformers]: https://github.com/frappe-dart/stream_transformers 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Learning Material 4 | 5 | Documentation and examples go a long way in helping others learn about Frappe. Building an example yourself can also be a great way to learn about FRP. If you're interested in contributing to our learning material, take a look at some of these [ideas](https://github.com/danschultz/frappe/issues/37). 6 | 7 | ## Code Changes 8 | 9 | If you'd like to submit a code change, here are some guidelines to follow: 10 | 11 | * If you plan on submitting a transformation method, implement it first as a `StreamTransformer` and submit a PR to the [stream_transformers] package. 12 | * All transformation methods should have a unit test that test the following: 13 | * A transformed `EventStream` that originates from a non-broadcast stream, should return a non-broadcast stream. 14 | * A transformed `EventStream` that originates from a broadcast stream, should return a broadcast stream. 15 | * A transformed `Reactable` should return the appropriate `Reactable` sub-class. For example, `EventStream.map()` should return a `EventStream` and `Property.map()` should return a `Property`. 16 | * Cancellation of the last `StreamSubscription` should call the `onCancel` callback of the source `StreamController`. 17 | * The expected forwarding behavior for errors. 18 | * The expected behavior when closing a source stream. For example, closing the source `StreamController` should call the `StreamSubscription`s `onDone` callback in some cases. 19 | * The expected transfomed values. 20 | * It's a good idea to run `grind build` before submitting a PR. This runs the Dart Analyzer to check for warnings, the linter to check that code adheres to the Dart style guide, and that unit tests pass. 21 | 22 | [stream_transformers]: https://github.com/danschultz/stream_transformers 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dan Schultz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Frappé 2 | 3 | [![Build Status](https://travis-ci.org/danschultz/frappe.svg)](https://travis-ci.org/danschultz/frappe) 4 | [![Coverage Status](https://coveralls.io/repos/danschultz/frappe/badge.svg)](https://coveralls.io/r/danschultz/frappe) 5 | 6 | A functional reactive programming library for Dart. Frappé extends the functionality of Dart's streams, and introduces new concepts like properties/signals. 7 | 8 | ## Why FRP? 9 | 10 | UI applications today are highly interactive and data driven. User input can trigger updates to the DOM, playing animations, invoking network requests, and modifying application state. Using the traditional form of event callbacks and modifying state variables can quickly become difficult to write and maintain. 11 | 12 | Functional reactive programming (FRP) makes it clearer to define user and system events that cause state changes. For instance, it's easy to define "when a user performs A, do X and Y, then output Z." 13 | 14 | When writing reactive code, you'll find yourself focusing more on the dependencies between events for business logic, and less time on their implementation details. 15 | 16 | ## Example 17 | 18 | Lets write an auto-complete movie widget with Frappé. The widget has an input field for the movie name, and a list element that displays movies that most closely match the user's input. A working version can be found [here](http://danschultz.github.io/frappe/examples/auto_complete/). 19 | 20 | ```dart 21 | var searchInput = document.querySelector("#searchInput"); 22 | var suggestionsElement = document.querySelector("#suggestions"); 23 | 24 | var onInput = new EventStream(searchInput.onInput) 25 | .debounce(new Duration(milliseconds: 250)) // Limit the number of network requests 26 | .map((event) => event.target.value) // Get the text from the input field 27 | .distinct(); // Ignore duplicate events with the same text 28 | 29 | // Make a network request to get the list of movie suggestions. Because requests 30 | // are asynchronous, they can complete out of order. Use `flatMapLatest` to only 31 | // respond to request for the last text change. 32 | var suggestions = onInput.flatMapLatest((input) => querySuggestions(input)); 33 | 34 | suggestions.listen((movies) => 35 | suggestionsElement.children 36 | ..clear() 37 | ..addAll(movies.map((movie) => new LIElement()..text = movie)); 38 | 39 | // Show "Searching ..." feedback while the request is pending 40 | var isPending = searchInput.onInput.isWaitingOn(suggestions); 41 | isPending.where((value) => value).listen((_) { 42 | suggestionsElement.children 43 | ..clear() 44 | ..add(new DivElement()..text = "Searching ..."); 45 | }); 46 | 47 | Future> querySuggestions(String input) { 48 | // Query some API that returns suggestions for 'input' 49 | } 50 | ``` 51 | 52 | ## API 53 | 54 | You can explore the full API [here][documentation]. 55 | 56 | ### `Reactable` 57 | 58 | The `Reactable` class extends from Stream and is inherited by `EventStream` and `Property`. Because these classes extend from Dart's `Stream`, you can pass them directly to other APIs that expect a `Stream`. 59 | 60 | ### `EventStream` 61 | 62 | An `EventStream` represents a series of discrete events. They're like a `Stream` in Dart, but extends its functionality with the methods found on `Reactable`. 63 | 64 | Event streams can be created from a property via `Property.asEventStream()`, or through one of its constructor methods. If an event stream is created from a property, its first event will be the property's current value. 65 | 66 | An `EventStream` will inherit the behavior of the stream from which it originated. So if an event stream was created from a broadcast stream, it can support multiple subscriptions. Likewise, if an event stream was created from a single-subscription stream, only one subscription can be added to it. Take a look at the [article](https://www.dartlang.org/articles/broadcast-streams/) on single-subscription streams vs broadcast streams to learn more about their different behaviors. 67 | 68 | ### `Property` 69 | 70 | A `Property` represents a value that changes over time. They're similar to event streams, but they remember their current value. Whenever a subscription is added to a property, it will receive the property's current value as its first event. 71 | 72 | Properties can be created through one of its constructors, or from an event stream via `EventStream.asProperty()`. Depending on how the property was created, it may or may not have a starting value. Separate methods are available for creating properties with an initial value, i.e. `Property.fromStreamWithInitialValue()` and `EventStream.asPropertyWithInitialValue()`. Properties can support having a null initial value, and is partly the motivation for having separate construction methods. 73 | 74 | Internally, properties are implemented as broadcast streams and can receive multiple subscriptions. 75 | 76 | If you were to model text input using properties and streams, the individual key strokes would be events, and the resulting text is a property. 77 | 78 | ## Learning More 79 | 80 | Definitely take a look at the API [documentation], and play around with some of the [examples]. It's also worth checking out [BaconJS] and [RxJS]. They're both mature FRP libraries, and offer some great resourses on the subject. 81 | 82 | ## Running Tests 83 | 84 | Tests are run through the Grinder `build` task. This will run the Dart Analyzer, linter and unit tests. 85 | 86 | * Install *grinder*: `pub global activate grinder` 87 | * Run *grinder*: `grind build` 88 | 89 | ## Features and bugs 90 | 91 | Please file feature requests and bugs at the [issue tracker][tracker]. 92 | 93 | ## Contributing 94 | 95 | Take a look [here][contributing] on ways to contribute to Frappé. 96 | 97 | [documentation]: http://www.dartdocs.org/documentation/frappe/latest 98 | [contributing]: https://github.com/danschultz/frappe/blob/master/CONTRIBUTING.md 99 | [examples]: https://github.com/danschultz/frappe/tree/master/example 100 | [tracker]: https://github.com/danschultz/frappe/issues 101 | [test_runner]: https://pub.dartlang.org/packages/test_runner 102 | [baconjs]: https://github.com/baconjs/bacon.js 103 | [rxjs]: http://reactive-extensions.github.io/RxJS/ 104 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Frappé Examples 2 | 3 | This is a list of examples that highlight the functionality of Frappé. 4 | 5 | ## Auto-complete 6 | 7 | Coordinate input into a text field with querying a web service for movie suggestions. 8 | 9 | * [Working example](http://danschultz.github.io/frappe/examples/auto_complete/) 10 | * [Source code](https://github.com/danschultz/frappe/tree/master/example/auto_complete) 11 | 12 | ## Registration Form 13 | 14 | Handling local and remote form validation. 15 | 16 | * [Working example](http://danschultz.github.io/frappe/examples/registration_form/) 17 | * [Source code](https://github.com/danschultz/frappe/tree/master/example/registration_form) -------------------------------------------------------------------------------- /example/auto_complete/index.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:html'; 3 | import 'package:frappe/frappe.dart'; 4 | 5 | void main() { 6 | var searchInput = querySelector("#searchInput"); 7 | var suggestionsList = querySelector("#suggestions"); 8 | 9 | var searchText = new EventStream(searchInput.onInput.map((event) => event.target.value)); 10 | 11 | var suggestions = 12 | searchText 13 | .debounce(new Duration(milliseconds: 250)) 14 | .doAction((term) => print("Querying `$term`")) 15 | .flatMapLatest((term) => queryTerm(term)) 16 | .doAction((results) => print("Found ${results.length} results")); 17 | 18 | var isPending = searchText.isWaitingOn(suggestions); 19 | 20 | isPending 21 | .where((value) => value) 22 | .forEach((_) => suggestionsList.children..clear()..add(new DivElement()..text = "Loading ...")); 23 | 24 | suggestions.forEach((results) => 25 | suggestionsList.children 26 | ..clear() 27 | ..addAll(results.map((result) => new LIElement()..text = result))); 28 | } 29 | 30 | EventStream> queryTerm(String term) { 31 | if (term.length > 2) { 32 | var params = { 33 | "api_key": "9eae05e667b4d5d9fbb75d27622347fe", 34 | "query": term 35 | }; 36 | 37 | var uri = Uri.parse("http://api.themoviedb.org/3/search/movie").replace(queryParameters: params); 38 | var results = HttpRequest.getString(uri.toString()) 39 | .then((response) => JSON.decode(response)) 40 | .then((json) => json["results"].map((result) => result["original_title"])); 41 | 42 | return new EventStream.fromFuture(results); 43 | } else { 44 | return new EventStream.fromValue([]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /example/auto_complete/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
Search for a movie:
10 | 11 |
12 |
13 |
    14 |
    15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/registration_form/README.md: -------------------------------------------------------------------------------- 1 | # Registration form demo 2 | 3 | A registration form demo that behaves like the one described in BaconJS's [tutorial](http://baconjs.github.io/tutorials.html). 4 | 5 | This simple registration form includes a laundry list of features: 6 | 7 | - [x] Username availability checking while the user is still typing the username 8 | - [x] Showing feedback on unavailable username 9 | - [x] Showing an AJAX indicator while this check is being performed 10 | - [x] Disabling the Register button until both username and fullname have been entered 11 | - [x] Disabling the Register button in case the username is unavailable 12 | - [x] Disabling the Register button while the check is being performed 13 | - [x] Disabling the Register button immediately when pressed to prevent double-submit 14 | - [x] Showing an AJAX indicator while registration is being processed 15 | - [x] Showing feedback after registration 16 | 17 | You can try out the demo for yourself [here](http://danschultz.github.io/frappe/examples/registration_form/). 18 | 19 | ## Overview 20 | 21 | With Frappe, you setup signal graphs that transform input events, and side effects for when these events propagate through your app and cause the signals to change. These side effects can be in the form of modifying the DOM or saving application state to some external store. 22 | 23 | For instance in this demo, we've setup signal graphs for validating a form. If the form isn't valid, the registration button should be disabled. Some validations might happen locally, such as a username or fullname being entered, and some validations happen on the server, such as if the username is available. A server validation is just another type of input into the system. Instead of it being input by the user, it's input from the network. For simplicity, this demo mocks server validations. 24 | 25 | As a user inputs text into these fields, the signals capture these events, run their validations and either enable or disable the registration button. 26 | 27 | ```dart 28 | var username = new Property.fromStreamWithInitialValue(usernameInput.value, usernameInput.onInput.map(inputValue)); 29 | var fullname = new Property.fromStreamWithInitialValue(fullnameInput.value, fullnameInput.onInput.map(inputValue)); 30 | 31 | var isUsernameValid = username.map((value) => value.isNotEmpty); 32 | var isFullnameValid = fullname.map((value) => value.isNotEmpty); 33 | var isUsernameAvailable = 34 | username 35 | .changes 36 | .debounce(new Duration(milliseconds: 250)) 37 | .flatMapLatest((value) => new Stream.fromFuture(fetchIsUsernameAvailable(value))); 38 | 39 | var isValid = 40 | isUsernameValid 41 | .combine(isFullnameValid, (a, b) => a && b) 42 | .combine(isUsernameAvailable, (a, b) => a && b) 43 | .distinct() 44 | .asPropertyWithInitialValue(false); 45 | 46 | isValid.listen((value) => registerButton.disabled = !value); 47 | ``` 48 | 49 | ## Running 50 | 51 | You can try out the demo [here](http://danschultz.github.io/frappe/examples/registration_form/). Otherwise, you can clone the project and run the example locally: 52 | 53 | * `pub serve example` 54 | * Open `http://localhost:8080/registration_form/` -------------------------------------------------------------------------------- /example/registration_form/index.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:html'; 3 | import 'dart:math'; 4 | import 'package:frappe/frappe.dart'; 5 | 6 | InputElement usernameInput = querySelector("#username"); 7 | Element usernameAvailable = querySelector("#usernameAvailable"); 8 | InputElement fullnameInput = querySelector("#fullname"); 9 | ButtonElement registerButton = querySelector("#register"); 10 | Element result = querySelector("#result"); 11 | 12 | void main() { 13 | var username = new Property.fromStreamWithInitialValue(usernameInput.value, usernameInput.onInput.map(inputValue)); 14 | var fullname = new Property.fromStreamWithInitialValue(fullnameInput.value, fullnameInput.onInput.map(inputValue)); 15 | 16 | var isUsernameValid = username.map((value) => value.isNotEmpty); 17 | var isFullnameValid = fullname.map((value) => value.isNotEmpty); 18 | 19 | var availabilityResponse = 20 | username 21 | .changes 22 | .debounce(new Duration(milliseconds: 250)) 23 | .flatMapLatest((value) => new Stream.fromFuture(fetchIsUsernameAvailable(value))); 24 | var isUsernameAvailable = availabilityResponse.asProperty(); 25 | var isCheckingUsername = username.changes.isWaitingOn(availabilityResponse); 26 | 27 | var isValid = 28 | isUsernameValid 29 | .and(isFullnameValid) 30 | .and(isUsernameAvailable) 31 | .distinct() 32 | .doAction((value) => print("Form valid? $value")) 33 | .asPropertyWithInitialValue(false); 34 | 35 | var onSubmit = 36 | new EventStream(registerButton.onClick) 37 | .doAction((_) => print("Submitting ...")); 38 | 39 | var onRequestRegistration = 40 | username 41 | .combine(fullname, (username, fullname) => () => registerUser(username, fullname)) 42 | .sampleOn(onSubmit) 43 | .asyncMap((registerUser) => registerUser()) 44 | .asEventStream() 45 | .doAction((_) => print("Registered!")); 46 | var isSubmittingRegistration = onSubmit.isWaitingOn(onRequestRegistration); 47 | 48 | var canSubmit = 49 | isValid 50 | .and(isCheckingUsername.not()) 51 | .and(isSubmittingRegistration.not()); 52 | 53 | isCheckingUsername 54 | .where((value) => value) 55 | .forEach((_) => usernameAvailable.text = "Checking ..."); 56 | 57 | isUsernameAvailable 58 | .doAction((value) => print("Username available? $value")) 59 | .forEach((value) => usernameAvailable.text = value ? "Available" : "Sorry, username is taken"); 60 | 61 | canSubmit.forEach((value) => registerButton.disabled = !value); 62 | 63 | isSubmittingRegistration.where((value) => value).forEach((_) => result.text = "Registering ..."); 64 | onRequestRegistration.forEach((id) => result.text = "Thanks, your user ID is $id!"); 65 | } 66 | 67 | Future fetchIsUsernameAvailable(String username) { 68 | return new Future.delayed(randomDelay(), () => username.length % 2 == 0); 69 | } 70 | Future registerUser(String username, String fullname) { 71 | return new Future.delayed(randomDelay() * 4, () => username.hashCode + fullname.hashCode); 72 | } 73 | 74 | Duration randomDelay() => new Duration(milliseconds: new Random().nextInt(500) + 500); 75 | 76 | String inputValue(Event event) => (event.target as InputElement).value; 77 | -------------------------------------------------------------------------------- /example/registration_form/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Frappe Registration Form Example 6 | 7 | 8 | 9 |
    10 |

    Demo

    11 |
    12 |
    Username:
    13 | 14 | 15 |
    16 |
    17 |
    Full name:
    18 | 19 |
    20 |
    21 | 22 | 23 |
    24 |
    25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/registration_form/styles.css: -------------------------------------------------------------------------------- 1 | .panel { 2 | display: inline-block; 3 | vertical-align: top; 4 | } 5 | -------------------------------------------------------------------------------- /lib/frappe.dart: -------------------------------------------------------------------------------- 1 | /// A functional reactive programming library for Dart. Frappé extends the 2 | /// functionality of Dart's streams, and introduces new concepts like properties 3 | /// / signals. 4 | library frappe; 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:stream_transformers/stream_transformers.dart'; 9 | 10 | part 'src/reactable.dart'; 11 | part 'src/event_stream.dart'; 12 | part 'src/property.dart'; 13 | -------------------------------------------------------------------------------- /lib/src/event_stream.dart: -------------------------------------------------------------------------------- 1 | part of frappe; 2 | 3 | /// An `EventStream` represents a series of discrete events. They're like a `Stream` in Dart, but 4 | /// extends its functionality with the methods found on [Reactable]. 5 | /// 6 | /// Event streams can be created from a property via [Property.asEventStream], or through one of 7 | /// its constructor methods. If an event stream is created from a property, its first event will be 8 | /// the property's current value. 9 | /// 10 | /// An `EventStream` will inherit the behavior of the stream from which it originated. So if an event 11 | /// stream was created from a broadcast stream, it can support multiple subscriptions. Likewise, if 12 | /// an event stream was created from a single-subscription stream, only one subscription can be added 13 | /// to it. Take a look at the [article](https://www.dartlang.org/articles/broadcast-streams/) on 14 | /// single-subscription streams vs broadcast streams to learn more about their different behaviors. 15 | /// 16 | /// Internally, properties are implemented as broadcast streams and can receive multiple subscriptions. 17 | /// 18 | /// If you were to model text input using properties and streams, the individual key strokes would be 19 | /// events, and the resulting text is a property. 20 | class EventStream extends Reactable { 21 | StreamController _controller; 22 | 23 | @override 24 | bool get isBroadcast => _controller.stream.isBroadcast; 25 | 26 | /// Returns a new stream that wraps a standard Dart `Stream`. 27 | EventStream(Stream stream) { 28 | _controller = _createControllerForStream(stream); 29 | } 30 | 31 | /// Returns a new single subscription stream that doesn't contain any events then completes. 32 | factory EventStream.empty() => new EventStream.fromIterable([]); 33 | 34 | /// Returns a new single subscription stream that contains a single event then completes. 35 | factory EventStream.fromValue(T value) => new EventStream.fromIterable([value]); 36 | 37 | /// Returns a new [EventStream] that contains events from an `Iterable`. 38 | factory EventStream.fromIterable(Iterable iterable) { 39 | return new EventStream(new Stream.fromIterable(iterable)); 40 | } 41 | 42 | /// Returns a new [EventStream] that contains a single event of the completed [future]. 43 | factory EventStream.fromFuture(Future future) { 44 | return new EventStream(new Stream.fromFuture(future)); 45 | } 46 | 47 | /// Creates a stream that repeatedly emits events at period intervals. 48 | /// 49 | /// The event values are computed by invoking `computation`. The argument to this 50 | /// callback is an integer that starts with 0 and is incremented for every event. 51 | /// 52 | /// If computation is omitted the event values will all be `null`. 53 | factory EventStream.periodic(Duration period, T computation(int count)) { 54 | return new EventStream(new Stream.periodic(period, computation)); 55 | } 56 | 57 | StreamController _createControllerForStream(Stream stream) { 58 | StreamSubscription subscription; 59 | 60 | void onListen() { 61 | subscription = stream.listen(_controller.add, onDone: _controller.close, onError: _controller.addError); 62 | } 63 | 64 | void onCancel() { 65 | subscription.cancel(); 66 | } 67 | 68 | return stream.isBroadcast 69 | ? new StreamController.broadcast(onListen: onListen, onCancel: onCancel, sync: true) 70 | : new StreamController(onListen: onListen, onCancel: onCancel, sync: true); 71 | } 72 | 73 | // Overrides 74 | 75 | EventStream asBroadcastStream({void onListen(StreamSubscription subscription), 76 | void onCancel(StreamSubscription subscription)}) { 77 | return new EventStream(super.asBroadcastStream(onListen: onListen, onCancel: onCancel)); 78 | } 79 | 80 | EventStream asEventStream() => this; 81 | 82 | /// Returns a [Property] where the first value will be the next value from this stream. 83 | Property asProperty() => new Property.fromStream(this); 84 | 85 | /// Returns a [Property] where the first value will be the [initialValue], and values 86 | /// after that will be the values from this stream. 87 | Property asPropertyWithInitialValue(T initialValue) => 88 | new Property.fromStreamWithInitialValue(initialValue, this); 89 | 90 | EventStream asyncExpand(Stream convert(T event)) => new EventStream(super.asyncExpand(convert)); 91 | 92 | EventStream asyncMap(dynamic convert(T event)) => new EventStream((super.asyncMap(convert))); 93 | 94 | EventStream bufferWhen(Stream toggle) => transform(new BufferWhen(toggle)); 95 | 96 | EventStream combine(Stream other, Object combiner(T a, b)) => transform(new Combine(other, combiner)); 97 | 98 | EventStream concat(Stream other) => transform(new Concat(other)); 99 | 100 | EventStream concatAll() => transform(new ConcatAll()); 101 | 102 | EventStream debounce(Duration duration) => transform(new Debounce(duration)); 103 | 104 | EventStream delay(Duration duration) => transform(new Delay(duration)); 105 | 106 | EventStream distinct([bool equals(T previous, T next)]) => new EventStream(super.distinct(equals)); 107 | 108 | EventStream doAction(void onData(T value), {Function onError, void onDone()}) => 109 | transform(new DoAction(onData, onError: onError, onDone: onDone)); 110 | 111 | EventStream expand(Iterable convert(T value)) => new EventStream(super.expand(convert)); 112 | 113 | EventStream flatMap(Stream convert(T event)) => transform(new FlatMap(convert)); 114 | 115 | EventStream flatMapLatest(Stream convert(T event)) => transform(new FlatMapLatest(convert)); 116 | 117 | EventStream handleError(Function onError, {bool test(error)}) => 118 | new EventStream(super.handleError(onError, test: test)); 119 | 120 | StreamSubscription listen(void onData(T event), {Function onError, void onDone(), bool cancelOnError}) { 121 | return _controller.stream.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); 122 | } 123 | 124 | EventStream map(convert(T event)) => new EventStream(super.map(convert)); 125 | 126 | EventStream merge(Stream other) => transform(new Merge(other)); 127 | 128 | EventStream mergeAll() => transform(new MergeAll()); 129 | 130 | EventStream not() => super.not(); 131 | 132 | EventStream sampleOn(Stream trigger) => transform(new SampleOn(trigger)); 133 | 134 | EventStream sampleEachPeriod(Duration duration) => transform(new SamplePeriodically(duration)); 135 | 136 | EventStream scan(initialValue, combine(value, T element)) => transform(new Scan(initialValue, combine)); 137 | 138 | EventStream selectFirst(Stream other) => transform(new SelectFirst(other)); 139 | 140 | EventStream skip(int count) => new EventStream(super.skip(count)); 141 | 142 | EventStream skipWhile(bool test(T element)) => new EventStream(super.skipWhile(test)); 143 | 144 | EventStream skipUntil(Stream signal) => transform(new SkipUntil(signal)); 145 | 146 | EventStream startWith(value) => transform(new StartWith(value)); 147 | 148 | EventStream startWithValues(Iterable values) => transform(new StartWith.many(values)); 149 | 150 | EventStream take(int count) => new EventStream(super.take(count)); 151 | 152 | EventStream takeUntil(Stream signal) => transform(new TakeUntil(signal)); 153 | 154 | EventStream takeWhile(bool test(T element)) => new EventStream(super.takeWhile(test)); 155 | 156 | EventStream timeout(Duration timeLimit, {void onTimeout(EventSink sink)}) => 157 | new EventStream(super.timeout(timeLimit, onTimeout: onTimeout)); 158 | 159 | EventStream transform(StreamTransformer streamTransformer) => 160 | new EventStream(super.transform(streamTransformer)); 161 | 162 | EventStream when(Stream toggle) => transform(new When(toggle)); 163 | 164 | EventStream where(bool test(T event)) => new EventStream(super.where(test)); 165 | 166 | EventStream zip(Stream other, Combiner combiner) => transform(new Zip(other, combiner)); 167 | } 168 | -------------------------------------------------------------------------------- /lib/src/property.dart: -------------------------------------------------------------------------------- 1 | part of frappe; 2 | 3 | /// A `Property` represents a value that changes over time. They're similar to event streams, 4 | /// but they remember their current value. Whenever a subscription is added to a property, it 5 | /// will receive the property's current value as its first event. 6 | /// 7 | /// Properties can be created through one of its constructors, or from an event stream via 8 | /// [EventStream.asProperty]. Depending on how the property was created, it may or may not 9 | /// have a starting value. Separate methods are available for creating properties with an 10 | /// initial value, i.e. [Property.fromStreamWithInitialValue] and [EventStream.asPropertyWithInitialValue]. 11 | /// Properties can support having a null initial value, and is partly the motivation for having 12 | /// separate construction methods. 13 | class Property extends Reactable { 14 | StreamController _controller; 15 | bool _hasCurrentValue = false; 16 | T _currentValue; 17 | 18 | /// An [EventStream] that contains the changes of the property. 19 | /// 20 | /// The stream will *not* contain an event for the current value of the `Property`. 21 | EventStream get changes => new EventStream(_controller.stream); 22 | 23 | @override 24 | bool get isBroadcast => _controller.stream.isBroadcast; 25 | 26 | Property._(Stream stream, bool hasInitialValue, [T initialValue]) { 27 | _hasCurrentValue = hasInitialValue; 28 | _currentValue = initialValue; 29 | _controller = _createControllerForStream(stream); 30 | } 31 | 32 | /// Returns a new property where its current value is always [value]. 33 | factory Property.constant(T value) => new Property.fromStream(new Stream.fromIterable([value])); 34 | 35 | /// Returns a new property where its current value is the latest value emitted 36 | /// from [stream]. 37 | factory Property.fromStream(Stream stream) => new Property._(stream, false); 38 | 39 | /// Returns a new property where its starting value is [initialValue], and its 40 | /// value after that is the latest value emitted from [stream]. 41 | factory Property.fromStreamWithInitialValue(T initialValue, Stream stream) => 42 | new Property._(stream, true, initialValue); 43 | 44 | /// Returns a new property where its current value is the completed value of 45 | /// the [future]. 46 | factory Property.fromFuture(Future future) => new Property.fromStream(new Stream.fromFuture(future)); 47 | 48 | /// Returns a new property where the starting value is [initialValue], and its 49 | /// value after that is the value from [future]. 50 | factory Property.fromFutureWithInitialValue(T initialValue, Future future) => 51 | new Property.fromStreamWithInitialValue(initialValue, new Stream.fromFuture(future)); 52 | 53 | StreamController _createControllerForStream(Stream stream) { 54 | var input = stream.asBroadcastStream(onCancel: (subscription) => subscription.cancel()); 55 | 56 | StreamSubscription subscription; 57 | 58 | void onListen() { 59 | if (subscription == null) { 60 | subscription = input.listen( 61 | (value) { 62 | _currentValue = value; 63 | _hasCurrentValue = true; 64 | _controller.add(value); 65 | }, 66 | onError: _controller.addError, 67 | onDone: () { 68 | _controller.close(); 69 | }); 70 | } 71 | } 72 | 73 | void onCancel() { 74 | subscription.cancel(); 75 | subscription = null; 76 | } 77 | 78 | return new StreamController.broadcast(onListen: onListen, onCancel: onCancel, sync: true); 79 | } 80 | 81 | /// Combines this property and [other] with the `&&` operator. 82 | Property and(Stream other) => combine(other, (a, b) => a && b); 83 | 84 | /// Combines this property and [other] with the `||` operator. 85 | Property or(Stream other) => combine(other, (a, b) => a || b); 86 | 87 | // Overrides 88 | 89 | Property asBroadcastStream({void onListen(StreamSubscription subscription), 90 | void onCancel(StreamSubscription subscription)}) { 91 | return new Property.fromStream(super.asBroadcastStream(onListen: onListen, onCancel: onCancel)); 92 | } 93 | 94 | EventStream asEventStream() => new EventStream(this); 95 | 96 | Property asProperty() => this; 97 | 98 | Property asPropertyWithInitialValue(T initialValue) => 99 | new Property.fromStreamWithInitialValue(initialValue, changes); 100 | 101 | Property asyncExpand(Stream convert(T event)) => new Property.fromStream(super.asyncExpand(convert)); 102 | 103 | Property asyncMap(dynamic convert(T event)) => new Property.fromStream((super.asyncMap(convert))); 104 | 105 | Property bufferWhen(Stream toggle) => transform(new BufferWhen(toggle)); 106 | 107 | Property combine(Stream other, Object combiner(T a, b)) => transform(new Combine(other, combiner)); 108 | 109 | Property concat(Stream other) => transform(new Concat(other)); 110 | 111 | Property concatAll() => transform(new ConcatAll()); 112 | 113 | Property debounce(Duration duration) => transform(new Debounce(duration)); 114 | 115 | Property delay(Duration duration) => transform(new Delay(duration)); 116 | 117 | Property distinct([bool equals(T previous, T next)]) => new Property.fromStream(super.distinct(equals)); 118 | 119 | Property doAction(void onData(T value), {Function onError, void onDone()}) => 120 | transform(new DoAction(onData, onError: onError, onDone: onDone)); 121 | 122 | Property expand(Iterable convert(T value)) => new Property.fromStream(super.expand(convert)); 123 | 124 | Property flatMap(Stream convert(T event)) => transform(new FlatMap(convert)); 125 | 126 | Property flatMapLatest(Stream convert(T event)) => transform(new FlatMapLatest(convert)); 127 | 128 | Property handleError(Function onError, {bool test(error)}) => 129 | new Property.fromStream(super.handleError(onError, test: test)); 130 | 131 | StreamSubscription listen(void onData(T value), {Function onError, void onDone(), bool cancelOnError}) { 132 | var controller = new StreamController(sync: true); 133 | 134 | if (_hasCurrentValue) { 135 | controller.add(_currentValue); 136 | } 137 | 138 | controller.addStream(_controller.stream, cancelOnError: false).then((_) => controller.close()); 139 | 140 | return controller.stream.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); 141 | } 142 | 143 | Property map(convert(T event)) => new Property.fromStream(super.map(convert)); 144 | 145 | Property merge(Stream other) => transform(new Merge(other)); 146 | 147 | Property mergeAll() => transform(new MergeAll()); 148 | 149 | Property not() => super.not(); 150 | 151 | Property sampleOn(Stream trigger) => transform(new SampleOn(trigger)); 152 | 153 | Property sampleEachPeriod(Duration duration) => transform(new SamplePeriodically(duration)); 154 | 155 | Property scan(initialValue, combine(value, T element)) => transform(new Scan(initialValue, combine)); 156 | 157 | Property selectFirst(Stream other) => transform(new SelectFirst(other)); 158 | 159 | Property skip(int count) => new Property.fromStream(super.skip(count)); 160 | 161 | Property skipWhile(bool test(T element)) => new Property.fromStream(super.skipWhile(test)); 162 | 163 | Property skipUntil(Stream signal) => transform(new SkipUntil(signal)); 164 | 165 | Property startWith(value) => transform(new StartWith(value)); 166 | 167 | Property startWithValues(Iterable values) => transform(new StartWith.many(values)); 168 | 169 | Property take(int count) => new Property.fromStream(super.take(count)); 170 | 171 | Property takeUntil(Stream signal) => transform(new TakeUntil(signal)); 172 | 173 | Property takeWhile(bool test(T element)) => new Property.fromStream(super.takeWhile(test)); 174 | 175 | Property timeout(Duration timeLimit, {void onTimeout(EventSink sink)}) => 176 | new Property.fromStream(super.timeout(timeLimit, onTimeout: onTimeout)); 177 | 178 | Property transform(StreamTransformer streamTransformer) => 179 | new Property.fromStream(super.transform(streamTransformer)); 180 | 181 | Property when(Stream toggle) => transform(new When(toggle)); 182 | 183 | Property where(bool test(T event)) => new Property.fromStream(super.where(test)); 184 | 185 | Property zip(Stream other, Combiner combiner) => transform(new Zip(other, combiner)); 186 | 187 | // Deprecated 188 | 189 | /// Combines this property and [other] with the `==` operator. 190 | @Deprecated("Expected to be removed in v0.5. Use combine() instead.") 191 | Property equals(Property other) => combine(other, (a, b) => a == b); 192 | 193 | /// Combines this property and [other] with the `>` operator. 194 | @Deprecated("Expected to be removed in v0.5. Use combine() instead.") 195 | Property operator >(Property other) => combine(other, (a, b) => a > b); 196 | 197 | /// Combines this property and [other] with the `>=` operator. 198 | @Deprecated("Expected to be removed in v0.5. Use combine() instead.") 199 | Property operator >=(Property other) => combine(other, (a, b) => a >= b); 200 | 201 | /// Combines this property and [other] with the `<` operator. 202 | @Deprecated("Expected to be removed in v0.5. Use combine() instead.") 203 | Property operator <(Property other) => combine(other, (a, b) => a < b); 204 | 205 | /// Combines this property and [other] with the `<=` operator. 206 | @Deprecated("Expected to be removed in v0.5. Use combine() instead.") 207 | Property operator <=(Property other) => combine(other, (a, b) => a <= b); 208 | 209 | /// Combines this property and [other] with the `+` operator. 210 | @Deprecated("Expected to be removed in v0.5. Use combine() instead.") 211 | Property operator +(Property other) => combine(other, (a, b) => a + b); 212 | 213 | /// Combines this property and [other] with the `-` operator. 214 | @Deprecated("Expected to be removed in v0.5. Use combine() instead.") 215 | Property operator -(Property other) => combine(other, (a, b) => a - b); 216 | 217 | /// Combines this property and [other] with the `*` operator. 218 | @Deprecated("Expected to be removed in v0.5. Use combine() instead.") 219 | Property operator *(Property other) => combine(other, (a, b) => a * b); 220 | 221 | /// Combines this property and [other] with the `/` operator. 222 | @Deprecated("Expected to be removed in v0.5. Use combine() instead.") 223 | Property operator /(Property other) => combine(other, (a, b) => a / b); 224 | } 225 | -------------------------------------------------------------------------------- /lib/src/reactable.dart: -------------------------------------------------------------------------------- 1 | part of frappe; 2 | 3 | /// A [Reactable] is unifies the API between [EventStream]s and [Property]s. 4 | abstract class Reactable extends Stream { 5 | /// Returns a [Property] where the current value is an iterable that contains the 6 | /// latest values from a collection of [reactables]. 7 | /// 8 | /// The supplied [reactables] can be a mixture of [Property]s and [EventStream]s, 9 | /// where any [Property]s will first be converted to a stream. 10 | /// 11 | /// The returned [Property] will only have a value once all the [reactables] contain 12 | /// a value. 13 | static Property collect(Iterable reactables) { 14 | return new Property.fromStream(Combine.all(reactables.toList())); 15 | } 16 | 17 | @override 18 | Reactable asBroadcastStream({void onListen(StreamSubscription subscription), 19 | void onCancel(StreamSubscription subscription)}); 20 | 21 | /// Returns this reactable as a [Property]. 22 | /// 23 | /// If this reactable is already a property, this this returns itself. 24 | Property asProperty(); 25 | 26 | /// Returns this reactable as a [Property] with an initial value. 27 | /// 28 | /// If this reactable is already a [Property], then this method returns a new [Property] 29 | /// where its current value is set to [initialValue]. 30 | Property asPropertyWithInitialValue(T initialValue); 31 | 32 | /// Returns this reactable as an [EventStream]. 33 | @Deprecated("Expected to be removed in v0.5. Use asEventStream() instead.") 34 | EventStream asStream() => asEventStream(); 35 | 36 | /// Returns this reactable as an [EventStream]. 37 | EventStream asEventStream(); 38 | 39 | @override 40 | Reactable asyncExpand(Stream convert(T event)); 41 | 42 | @override 43 | Reactable asyncMap(dynamic convert(T event)); 44 | 45 | /// Pauses the delivery of events from the source stream when the signal stream 46 | /// delivers a value of `true`. The buffered events are delivered when the signal 47 | /// delivers a value of `false`. Errors originating from the source and signal 48 | /// streams will be forwarded to the transformed stream and will not be buffered. 49 | /// If the source stream is a broadcast stream, then the transformed stream will 50 | /// also be a broadcast stream. 51 | /// 52 | /// **Example:** 53 | /// 54 | /// var controller = new StreamController(); 55 | /// var signal = new StreamController(); 56 | /// 57 | /// var stream = new EventStream(controller.stream); 58 | /// var buffered = stream.bufferWhen(signal.stream)); 59 | /// 60 | /// controller.add(1); 61 | /// signal.add(true); 62 | /// controller.add(2); 63 | /// 64 | /// buffered.listen(print); 65 | /// 66 | /// // 1 67 | Reactable bufferWhen(Stream toggle); 68 | 69 | /// Combines the latest values of two streams using a two argument function. 70 | /// The combining function will not be called until each stream delivers its 71 | /// first value. After the first value of each stream is delivered, the 72 | /// combining function will be invoked for each event from the source streams. 73 | /// Errors occurring on the streams will be forwarded to the transformed 74 | /// stream. If the source stream is a broadcast stream, then the transformed 75 | /// stream will also be a broadcast stream. 76 | /// 77 | /// **Example:** 78 | /// 79 | /// var controller1 = new StreamController(); 80 | /// var controller2 = new StreamController(); 81 | /// 82 | /// var combined = new EventStream(controller1.stream).combine(controller2.stream, (a, b) => a + b)); 83 | /// 84 | /// combined.listen(print); 85 | /// 86 | /// controller1.add(1); 87 | /// controller2.add(1); 88 | /// controller1.add(2); 89 | /// controller2.add(2); 90 | /// 91 | /// // 2 92 | /// // 3 93 | /// // 4 94 | Reactable combine(Stream other, Object combiner(T a, b)); 95 | 96 | /// Concatenates two streams into one stream by delivering the values of the source stream, 97 | /// and then delivering the values of the other stream once the source stream completes. 98 | /// This means that it's possible that events from the second stream might not be included 99 | /// if the source stream hasn't completed. Use `Concat.all()` to concatenate many streams. 100 | /// 101 | /// Errors will be forwarded from either stream, whether or not the source stream has 102 | /// completed. If the source stream is a broadcast stream, then the transformed stream will 103 | /// also be a broadcast stream. 104 | /// 105 | /// **Example:** 106 | /// 107 | /// var source = new StreamController(); 108 | /// var other = new StreamController(); 109 | /// 110 | /// var stream = new EventStream(source.stream).concat(other.stream)); 111 | /// stream.listen(print); 112 | /// 113 | /// other..add(1)..add(2); 114 | /// source..add(3)..add(4)..close(); 115 | /// 116 | /// // 3 117 | /// // 4 118 | /// // 1 119 | /// // 2 120 | Reactable concat(Stream other); 121 | 122 | /// Concatenates a stream of streams into a single stream, by delivering the first stream's 123 | /// values, and then delivering the next stream's values after the previous stream has 124 | /// completed. 125 | /// 126 | /// This means that it's possible that events from the second stream might not be included 127 | /// if the source stream hasn't completed. Use `Concat.all()` to concatenate many streams. 128 | /// 129 | /// Errors will be forwarded from either stream, whether or not the source stream has 130 | /// completed. If the source stream is a broadcast stream, then the transformed stream will 131 | /// also be a broadcast stream. 132 | /// 133 | /// **Example:** 134 | /// 135 | /// var source = new StreamController(); 136 | /// var other1 = new StreamController(); 137 | /// var other2 = new StreamController(); 138 | /// 139 | /// source..add(other1.stream)..add(other2.stream); 140 | /// 141 | /// other2..add(1)..add(2); 142 | /// other1..add(3)..add(4)..close(); 143 | /// 144 | /// var stream = new EventStream(source.stream).concatAll()); 145 | /// stream.listen(print); 146 | /// 147 | /// // 3 148 | /// // 4 149 | /// // 1 150 | /// // 2 151 | Reactable concatAll(); 152 | 153 | /// Delivers the last event from the source after the duration has passed 154 | /// without receiving an event. 155 | /// 156 | /// Errors occurring on the source stream will not be ignored. If the source 157 | /// stream is a broadcast stream, then the transformed stream will also be 158 | /// a broadcast stream. 159 | /// 160 | /// source: asdf----asdf---- 161 | /// source.debounce(2): -----f-------f-- 162 | /// 163 | /// **Example:** 164 | /// 165 | /// var controller = new StreamController(); 166 | /// 167 | /// var debounced = new EventStream(controller.stream).debounce(new Duration(seconds:1))); 168 | /// debounced.listen(print); 169 | /// 170 | /// controller.add(1); 171 | /// controller.add(2); 172 | /// controller.add(3); 173 | /// 174 | /// // 3 175 | Reactable debounce(Duration duration); 176 | 177 | /// Throttles the delivery of each event by a given duration. Errors occurring 178 | /// on the source stream will not be delayed. If the source stream is a broadcast 179 | /// stream, then the transformed stream will also be a broadcast stream. 180 | /// 181 | /// **Example:** 182 | /// 183 | /// var controller = new StreamController(); 184 | /// var delayed = new EventStream(controller.stream).delay(new Duration(seconds: 2))); 185 | /// 186 | /// // source: asdf---- 187 | /// // source.delayed(2): --a--s--d--f--- 188 | Reactable delay(Duration duration); 189 | 190 | @override 191 | Reactable distinct([bool equals(T previous, T next)]); 192 | 193 | /// Invokes a side-effect function for each value, error and done event in the stream. 194 | /// 195 | /// This is useful for debugging, but also invoking `preventDefault` for browser events. 196 | /// Side effects will only be invoked once if the transformed stream has multiple 197 | /// subscribers. 198 | /// 199 | /// Errors occurring on the source stream will be forwarded to the returned stream, even 200 | /// when passing an error handler to `DoAction`. If the source stream is a broadcast 201 | /// stream, then the transformed stream will also be a broadcast stream. 202 | /// 203 | /// **Example:** 204 | /// 205 | /// var controller = new StreamController(); 206 | /// var stream = new EventStream(controller.stream).doAction( 207 | /// (value) => print("Do Next: $value"), 208 | /// onError: (error) => print("Do Error: $error"), 209 | /// onDone: () => print("Do Done"));); 210 | /// 211 | /// stream.listen((value) => print("Next: $value"), 212 | /// onError: (e) => print("Error: $e"), 213 | /// onDone: () => print("Done")); 214 | /// 215 | /// controller..add(1)..add(2)..close(); 216 | /// 217 | /// // Do Next: 1 218 | /// // Next: 1 219 | /// // Do Next: 2 220 | /// // Next: 2 221 | /// // Do Done 222 | /// // Done 223 | Reactable doAction(void onData(T value), {Function onError, void onDone()}); 224 | 225 | @override 226 | Reactable expand(Iterable convert(T value)); 227 | 228 | /// Spawns a new stream from a function for each event in the source stream. 229 | /// The returned stream will contain the events and errors from each of the 230 | /// spawned streams until they're closed. If the source stream is a broadcast 231 | /// stream, then the transformed stream will also be a broadcast stream. 232 | /// 233 | /// **Example:** 234 | /// 235 | /// var controller = new StreamController(); 236 | /// var flapMapped = new EventStream(controller.stream).flatMap((value) { 237 | /// return new Stream.fromIterable([value + 1]); 238 | /// }); 239 | /// 240 | /// flatMapped.listen(print); 241 | /// 242 | /// controller.add(1); 243 | /// controller.add(2); 244 | /// 245 | /// // 2 246 | /// // 3 247 | Reactable flatMap(Stream convert(T event)); 248 | 249 | /// Similar to `FlatMap`, but instead of including events from all spawned 250 | /// streams, only includes the ones from the latest stream. Think of this 251 | /// as stream switching. 252 | /// 253 | /// **Example:** 254 | /// 255 | /// var controller = new StreamController(); 256 | /// var latest = new EventStream(controller.stream).flatMapLatest((value) { 257 | /// return new Stream.fromIterable([value + 1]); 258 | /// }); 259 | /// 260 | /// latest.listen(print); 261 | /// 262 | /// controller.add(1); 263 | /// controller.add(2); 264 | /// 265 | /// // 3 266 | Reactable flatMapLatest(Stream convert(T event)); 267 | 268 | /// Returns a property that indicates if this reactable is waiting for an event from 269 | /// another stream. 270 | /// 271 | /// This method is useful for displaying spinners while waiting for AJAX responses. 272 | /// 273 | /// **Example:** 274 | /// 275 | /// var source = new EventStream.single(1); 276 | /// var other = new EventStream.fromFuture(new Future.delayed(new Duration(seconds: 1))); 277 | /// 278 | /// var isWaiting = source.isWaitingOn(other); 279 | /// isWaiting.listen(print); 280 | /// 281 | /// // true 282 | /// // false 283 | Property isWaitingOn(Stream other) { 284 | return new Property.fromStreamWithInitialValue( 285 | false, 286 | flatMapLatest((_) => new EventStream.fromValue(true).merge(other.take(1).map((_) => false)))) 287 | .distinct(); 288 | } 289 | 290 | @override 291 | Reactable handleError(Function onError, {bool test(error)}); 292 | 293 | @override 294 | Reactable map(convert(T event)); 295 | 296 | /// Combines the events from two streams into a single stream. Errors occurring 297 | /// on any merged stream will be forwarded to the transformed stream. If the 298 | /// source stream is a broadcast stream, then the transformed stream will also 299 | /// be a broadcast stream. 300 | /// 301 | /// **Example:** 302 | /// 303 | /// var controller1 = new StreamController(); 304 | /// var controller2 = new StreamController(); 305 | /// 306 | /// var merged = new EventStream(controller1.stream).merge(controller2.stream)); 307 | /// 308 | /// merged.listen(print); 309 | /// 310 | /// controller1.add(1); 311 | /// controller2.add(2); 312 | /// controller1.add(3); 313 | /// controller2.add(4); 314 | /// 315 | /// // 1 316 | /// // 2 317 | /// // 3 318 | /// // 4 319 | Reactable merge(Stream other); 320 | 321 | /// Combines the events from a stream of streams into a single stream. 322 | /// 323 | /// The returned stream will contain the errors occurring on any stream. If the source 324 | /// stream is a broadcast stream, then the transformed stream will also be a broadcast 325 | /// stream. 326 | /// 327 | /// **Example:** 328 | /// 329 | /// var source = new StreamController(); 330 | /// var stream1 = new Stream.fromIterable([1, 2]); 331 | /// var stream2 = new Stream.fromIterable([3, 4]); 332 | /// 333 | /// var merged = new EventStream(source.stream).mergeAll()); 334 | /// source..add(stream1)..add(stream2); 335 | /// 336 | /// merged.listen(print); 337 | /// 338 | /// // 1 339 | /// // 2 340 | /// // 3 341 | /// // 4 342 | Reactable mergeAll(); 343 | 344 | /// Applies the logical `!` operation to each value. 345 | Reactable not() => map((value) => !value); 346 | 347 | /// Takes the latest value of the source stream whenever the trigger stream 348 | /// produces an event. 349 | /// 350 | /// Errors that happen on the source stream will be forwarded to the transformed 351 | /// stream. If the source stream is a broadcast stream, then the transformed 352 | /// stream will also be a broadcast stream. 353 | /// 354 | /// **Example:** 355 | /// 356 | /// // values start at 0 357 | /// var source = new Stream.periodic(new Duration(seconds: 1), (i) => i); 358 | /// var trigger = new Stream.periodic(new Duration(seconds: 2), (i) => i); 359 | /// 360 | /// var stream = new EventStream(source.stream).sampleOn(trigger.stream)).take(3); 361 | /// 362 | /// stream.listen(print); 363 | /// 364 | /// // 0 365 | /// // 2 366 | /// // 4 367 | Reactable sampleOn(Stream trigger); 368 | 369 | /// Takes the latest value of the source stream at a specified interval. 370 | /// 371 | /// Errors that happen on the source stream will be forwarded to the transformed 372 | /// stream. If the source stream is a broadcast stream, then the transformed 373 | /// stream will also be a broadcast stream. 374 | /// 375 | /// **Example:** 376 | /// 377 | /// // values start at 0 378 | /// var source = new Stream.periodic(new Duration(seconds: 1), (i) => i); 379 | /// var stream = new EventStream(source.stream).sampleEachPeriod(new Duration(seconds: 2))).take(3); 380 | /// 381 | /// stream.listen(print); 382 | /// 383 | /// // 0 384 | /// // 2 385 | /// // 4 386 | Reactable sampleEachPeriod(Duration duration); 387 | 388 | /// Reduces the values of a stream into a single value by using an initial 389 | /// value and an accumulator function. The function is passed the previous 390 | /// accumulated value and the current value of the stream. This is useful 391 | /// for maintaining state using a stream. Errors occurring on the source 392 | /// stream will be forwarded to the transformed stream. If the source stream 393 | /// is a broadcast stream, then the transformed stream will also be a 394 | /// broadcast stream. 395 | /// 396 | /// **Example:** 397 | /// 398 | /// var button = new ButtonElement(); 399 | /// 400 | /// var clickCount = new EventStream(button.onClick).scan(0, (previous, current) => previous + 1)); 401 | /// 402 | /// clickCount.listen(print); 403 | /// 404 | /// // [button click] .. prints: 1 405 | /// // [button click] .. prints: 2 406 | Reactable scan(initialValue, combine(value, T element)); 407 | 408 | /// Forwards events from the first stream to deliver an event. 409 | /// 410 | /// Errors are forwarded from both streams until a stream is selected. Once a stream is selected, 411 | /// only errors from the selected stream are forwarded. If the source stream is a broadcast stream, 412 | /// then the transformed stream will also be a broadcast stream. 413 | /// 414 | /// **Example:** 415 | /// 416 | /// var stream1 = new Stream.periodic(new Duration(seconds: 1)).map((_) => "Stream 1"); 417 | /// var stream2 = new Stream.periodic(new Duration(seconds: 2)).map((_) => "Stream 2"); 418 | /// 419 | /// var selected = new EventStream(stream1).selectFirst(stream2)).take(1); 420 | /// selected.listen(print); 421 | /// 422 | /// // Stream 1 423 | Reactable selectFirst(Stream other); 424 | 425 | @override 426 | Reactable skip(int count); 427 | 428 | @override 429 | Reactable skipWhile(bool test(T element)); 430 | 431 | /// Waits to deliver events from a stream until the signal `Stream` delivers a 432 | /// value. Errors that happen on the source stream will be forwarded once the 433 | /// `Stream` delivers its value. Errors happening on the signal stream will be 434 | /// forwarded immediately. If the source stream is a broadcast stream, then the 435 | /// transformed stream will also be a broadcast stream. 436 | /// 437 | /// **Example:** 438 | /// 439 | /// var signal = new StreamController(); 440 | /// var controller = new StreamController(); 441 | /// 442 | /// var skipStream = new EventStream(controller.stream).skipUntil(signal.stream)); 443 | /// 444 | /// skipStream.listen(print); 445 | /// 446 | /// controller.add(1); 447 | /// controller.add(2); 448 | /// signal.add(true); 449 | /// controller.add(3); 450 | /// controller.add(4); 451 | /// 452 | /// // 3 453 | /// // 4 454 | Reactable skipUntil(Stream signal); 455 | 456 | /// Prepends a value to the beginning of a stream. Use [startWithValues] to prepend 457 | /// multiple values. 458 | /// 459 | /// Errors on the source stream will be forwarded to the transformed stream. If the 460 | /// source stream is a broadcast stream, then the transformed stream will also be a 461 | /// broadcast stream. 462 | /// 463 | /// **Example:** 464 | /// 465 | /// var source = new Stream.fromIterable([2, 3]); 466 | /// var stream = new EventStream(source).startWith(1); 467 | /// stream.listen(print); 468 | /// 469 | /// // 1 470 | /// // 2 471 | /// // 3 472 | Reactable startWith(value); 473 | 474 | /// Prepends values to the beginning of a stream. 475 | /// 476 | /// Errors on the source stream will be forwarded to the transformed stream. If the 477 | /// source stream is a broadcast stream, then the transformed stream will also be a 478 | /// broadcast stream. 479 | /// 480 | /// **Example:** 481 | /// 482 | /// var source = new Stream.fromIterable([3]); 483 | /// var stream = new EventStream(source).startWithValues([1, 2]); 484 | /// stream.listen(print); 485 | /// 486 | /// // 1 487 | /// // 2 488 | /// // 3 489 | Reactable startWithValues(Iterable values); 490 | 491 | @override 492 | Reactable take(int count); 493 | 494 | /// Delivers events from the source stream until the signal `Stream` produces a value. 495 | /// At which point, the transformed stream closes. The returned stream will continue 496 | /// to deliver values if the signal stream closes without a value. 497 | /// 498 | /// This is useful for automatically cancelling a stream subscription to prevent memory 499 | /// leaks. Errors that happen on the source and signal stream will be forwarded to the 500 | /// transformed stream. If the source stream is a broadcast stream, then the transformed 501 | /// stream will also be a broadcast stream. 502 | /// 503 | /// **Example:** 504 | /// 505 | /// var signal = new StreamController(); 506 | /// var controller = new StreamController(); 507 | /// 508 | /// var takeUntil = new EventStream(controller.stream).takeUntil(signal.stream)); 509 | /// 510 | /// takeUntil.listen(print, onDone: () => print("done")); 511 | /// 512 | /// controller.add(1); 513 | /// controller.add(2); 514 | /// signal.add(true); 515 | /// controller.add(3); 516 | /// controller.add(4); 517 | /// 518 | /// // 1 519 | /// // 2 520 | /// // done 521 | Reactable takeUntil(Stream signal); 522 | 523 | @override 524 | Reactable takeWhile(bool test(T element)); 525 | 526 | @override 527 | Reactable timeout(Duration timeLimit, {void onTimeout(EventSink sink)}); 528 | 529 | @override 530 | Reactable transform(StreamTransformer streamTransformer); 531 | 532 | /// Starts delivering events from the source stream when the signal stream 533 | /// delivers a value of `true`. Events are skipped when the signal stream 534 | /// delivers a value of `false`. Errors from the source or toggle stream will be 535 | /// forwarded to the transformed stream. If the source stream is a broadcast 536 | /// stream, then the transformed stream will also be a broadcast stream. 537 | /// 538 | /// **Example:** 539 | /// 540 | /// var controller = new StreamController(); 541 | /// var signal = new StreamController(); 542 | /// 543 | /// var whenStream = new EventStream(controller.stream).when(signal.stream)); 544 | /// 545 | /// whenStream.listen(print); 546 | /// 547 | /// controller.add(1); 548 | /// signal.add(true); 549 | /// controller.add(2); 550 | /// signal.add(false); 551 | /// controller.add(3); 552 | /// 553 | /// // 2 554 | Reactable when(Stream toggle); 555 | 556 | @override 557 | Reactable where(bool test(T event)); 558 | 559 | /// Combines the events of two streams into one by invoking a combiner function 560 | /// that is invoked when each stream delivers an event at each index. The 561 | /// transformed stream finishes when either source stream finishes. Errors from 562 | /// either stream will be forwarded to the transformed stream. If the source 563 | /// stream is a broadcast stream, then the transformed stream will also be a 564 | /// broadcast stream. 565 | /// 566 | /// **Example:** 567 | /// 568 | /// var controller1 = new StreamController(); 569 | /// var controller2 = new StreamController(); 570 | /// 571 | /// var zipped = new EventStream(controller1.stream).zip(controller2.stream, (a, b) => a + b)); 572 | /// 573 | /// zipped.listen(print); 574 | /// 575 | /// controller1.add(1); 576 | /// controller1.add(2); 577 | /// controller2.add(1); 578 | /// controller1.add(3); 579 | /// controller2.add(2); 580 | /// controller2.add(3); 581 | /// 582 | /// // 2 583 | /// // 4 584 | /// // 6 585 | Reactable zip(Stream other, Combiner combiner); 586 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: frappe 2 | version: 0.4.0+6 3 | description: > 4 | A functional reactive programming library for Dart. Frappé extends the functionality 5 | of Dart's streams, and introduces new concepts like properties/signals. 6 | authors: 7 | - Dan Schultz 8 | - Anders Holmgren 9 | - Aaron Washington 10 | homepage: http://www.github.com/danschultz/frappe 11 | environment: 12 | sdk: '>=1.8.0' 13 | dependencies: 14 | stream_transformers: ^0.3.0 15 | dev_dependencies: 16 | browser: ^0.10.0 17 | grinder: '>=0.7.0 <0.7.1' 18 | guinness: ^0.1.6 19 | unittest: ^0.11.0 20 | -------------------------------------------------------------------------------- /test/all_tests.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.all_tests; 2 | 3 | import 'event_stream_test.dart' as event_stream; 4 | import 'property_test.dart' as property; 5 | 6 | void main() { 7 | event_stream.main(); 8 | property.main(); 9 | } -------------------------------------------------------------------------------- /test/event_stream_test.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.event_stream_test; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'shared/as_event_stream.dart'; 7 | import 'shared/async_expand.dart'; 8 | import 'shared/async_map.dart'; 9 | import 'shared/buffer_when.dart'; 10 | import 'shared/combine.dart'; 11 | import 'shared/debounce.dart'; 12 | import 'shared/delay.dart'; 13 | import 'shared/distinct.dart'; 14 | import 'shared/expand.dart'; 15 | import 'shared/flat_map.dart'; 16 | import 'shared/flat_map_latest.dart'; 17 | import 'shared/handle_error.dart'; 18 | import 'shared/map.dart'; 19 | import 'shared/merge.dart'; 20 | import 'shared/scan.dart'; 21 | import 'shared/skip.dart'; 22 | import 'shared/skip_while.dart'; 23 | import 'shared/skip_until.dart'; 24 | import 'shared/take.dart'; 25 | import 'shared/take_while.dart'; 26 | import 'shared/take_until.dart'; 27 | import 'shared/when.dart'; 28 | import 'shared/where.dart'; 29 | import 'shared/zip.dart'; 30 | import 'shared/return_types.dart'; 31 | import 'shared/util.dart'; 32 | 33 | void main() => describe("EventStream", () { 34 | EventStream stream; 35 | StreamController controller; 36 | 37 | beforeEach(() { 38 | controller = new StreamController(); 39 | stream = new EventStream(controller.stream); 40 | }); 41 | 42 | it("delivers values from the source stream", () { 43 | return testStream(stream, 44 | behavior: () => controller.add(1), 45 | expectation: (values) => expect(values).toEqual([1])); 46 | }); 47 | 48 | it("is done when the source stream is done", () { 49 | var completer = new Completer(); 50 | stream.listen(null, onDone: completer.complete); 51 | controller.close(); 52 | return completer.future; 53 | }); 54 | 55 | it("cancels subscriptions to the source stream", () { 56 | var completer = new Completer(); 57 | var controller = new StreamController(onCancel: completer.complete); 58 | var property = new EventStream(controller.stream); 59 | property.listen(null).cancel(); 60 | return completer.future; 61 | }); 62 | 63 | testReturnTypes(EventStream, () => new EventStream(new Stream.fromIterable([1]))); 64 | testAsEventStream((stream) => new Property.fromStream(stream)); 65 | testAsyncExpand((stream) => new EventStream(stream)); 66 | testAsyncMap((stream) => new EventStream(stream)); 67 | testBufferWhen((stream) => new EventStream(stream)); 68 | testCombine((stream) => new EventStream(stream)); 69 | testDebounce((stream) => new EventStream(stream)); 70 | testDelay((stream) => new EventStream(stream)); 71 | testDistinct((stream) => new EventStream(stream)); 72 | testExpand((stream) => new EventStream(stream)); 73 | testFlatMap((stream) => new EventStream(stream)); 74 | testFlatMapLatest((stream) => new EventStream(stream)); 75 | testHandleError((stream) => new EventStream(stream)); 76 | testMap((stream) => new EventStream(stream)); 77 | testMerge((stream) => new EventStream(stream)); 78 | testScan((stream) => new EventStream(stream)); 79 | testSkip((stream) => new EventStream(stream)); 80 | testSkipUntil((stream) => new EventStream(stream)); 81 | testSkipWhile((stream) => new EventStream(stream)); 82 | testTake((stream) => new EventStream(stream)); 83 | testTakeWhile((stream) => new EventStream(stream)); 84 | testTakeUntil((stream) => new EventStream(stream)); 85 | testWhen((stream) => new EventStream(stream)); 86 | testWhere((stream) => new EventStream(stream)); 87 | testZip((stream) => new EventStream(stream)); 88 | }); 89 | -------------------------------------------------------------------------------- /test/property_test.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.property_test; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'shared/as_event_stream.dart'; 7 | import 'shared/async_expand.dart'; 8 | import 'shared/async_map.dart'; 9 | import 'shared/buffer_when.dart'; 10 | import 'shared/combine.dart'; 11 | import 'shared/debounce.dart'; 12 | import 'shared/delay.dart'; 13 | import 'shared/distinct.dart'; 14 | import 'shared/expand.dart'; 15 | import 'shared/flat_map.dart'; 16 | import 'shared/flat_map_latest.dart'; 17 | import 'shared/handle_error.dart'; 18 | import 'shared/map.dart'; 19 | import 'shared/merge.dart'; 20 | import 'shared/scan.dart'; 21 | import 'shared/skip.dart'; 22 | import 'shared/skip_while.dart'; 23 | import 'shared/take.dart'; 24 | import 'shared/take_while.dart'; 25 | import 'shared/take_until.dart'; 26 | import 'shared/where.dart'; 27 | import 'shared/zip.dart'; 28 | import 'shared/return_types.dart'; 29 | import 'shared/util.dart'; 30 | 31 | void main() => describe("Property", () { 32 | Property property; 33 | 34 | beforeEach(() => property = new Property.constant(1)); 35 | 36 | describe("constant", () { 37 | beforeEach(() => property = new Property.constant(1)); 38 | 39 | it("delivers the value to the first subscriber", () { 40 | return testStream(property, expectation: (values) => expect(values).toEqual([1])); 41 | }); 42 | 43 | it("delivers the value to multiple subscribers", () { 44 | var value1 = property.first; 45 | var value2 = property.first; 46 | return Future.wait([value1, value2]).then((values) => expect(values).toEqual([1, 1])); 47 | }); 48 | 49 | it("is done after delivering its value", () { 50 | var completer = new Completer(); 51 | property.listen(null, onDone: completer.complete); 52 | return completer.future; 53 | }); 54 | }); 55 | 56 | describe("from stream", () { 57 | StreamController controller; 58 | Property property; 59 | 60 | beforeEach(() { 61 | controller = new StreamController(); 62 | property = new Property.fromStream(controller.stream); 63 | }); 64 | 65 | it("delivers values from the source stream", () { 66 | return testStream(property, 67 | behavior: () => controller.add(1), 68 | expectation: (values) => expect(values).toEqual([1])); 69 | }); 70 | 71 | it("delivers values to multiple subscribers", () { 72 | var values1 = property.toList(); 73 | var values2 = property.toList(); 74 | controller..add(1)..close(); 75 | return Future.wait([values1, values2]).then((values) => expect(values).toEqual([[1], [1]])); 76 | }); 77 | 78 | it("is done when the source stream is done", () { 79 | var completer = new Completer(); 80 | property.listen(null, onDone: completer.complete); 81 | controller.close(); 82 | return completer.future; 83 | }); 84 | 85 | it("cancels subscriptions to the source stream", () { 86 | var completer = new Completer(); 87 | var controller = new StreamController(onCancel: completer.complete); 88 | var property = new Property.fromStream(controller.stream); 89 | property.listen(null).cancel(); 90 | return completer.future; 91 | }); 92 | 93 | describe("changes", () { 94 | it("included new values", () { 95 | return testStream(property.changes, 96 | behavior: () => controller..add(2)..add(3), 97 | expectation: (values) => expect(values).toEqual([2, 3])); 98 | }); 99 | 100 | it("doesn't exclude duplicates", () { 101 | return testStream(property.changes, 102 | behavior: () => controller..add(2)..add(2), 103 | expectation: (values) => expect(values).toEqual([2, 2])); 104 | }); 105 | }); 106 | 107 | describe("with initial value", () { 108 | StreamController controller; 109 | 110 | beforeEach(() { 111 | controller = new StreamController(); 112 | property = new Property.fromStreamWithInitialValue(1, controller.stream); 113 | }); 114 | 115 | it("delivers its initial value", () { 116 | return testStream(property, expectation: (values) => expect(values).toEqual([1])); 117 | }); 118 | 119 | it("delivers values after its initial value", () { 120 | return testStream(property, 121 | behavior: () => controller.add(2), 122 | expectation: (values) => expect(values).toEqual([1, 2])); 123 | }); 124 | 125 | it("delivers values to multiple subscribers", () { 126 | var values1 = property.toList(); 127 | var values2 = property.toList(); 128 | controller..add(2)..close(); 129 | return Future.wait([values1, values2]).then((values) => expect(values).toEqual([[1, 2], [1, 2]])); 130 | }); 131 | 132 | it("is done when the source stream is done", () { 133 | var completer = new Completer(); 134 | property.listen(null, onDone: completer.complete); 135 | controller.close(); 136 | return completer.future; 137 | }); 138 | 139 | it("cancels subscriptions to the source stream", () { 140 | var completer = new Completer(); 141 | var controller = new StreamController(onCancel: completer.complete); 142 | var property = new Property.fromStreamWithInitialValue(1, controller.stream); 143 | property.listen(null).cancel(); 144 | return completer.future; 145 | }); 146 | 147 | describe("changes", () { 148 | it("doesn't include the initial value", () { 149 | return testStream(property.changes, 150 | behavior: () => controller.add(2), 151 | expectation: (values) => expect(values).toEqual([2])); 152 | }); 153 | }); 154 | 155 | describe("asEventStream()", () { 156 | it("includes the initial value and changes", () { 157 | return testStream(property.asEventStream(), 158 | behavior: () => controller.add(2), 159 | expectation: (values) => expect(values).toEqual([1, 2])); 160 | }); 161 | }); 162 | }); 163 | }); 164 | 165 | testReturnTypes(Property, () => new Property.constant(1)); 166 | testAsEventStream((stream) => new Property.fromStream(stream)); 167 | testAsyncExpand((stream) => new Property.fromStream(stream)); 168 | testAsyncMap((stream) => new Property.fromStream(stream)); 169 | testBufferWhen((stream) => new Property.fromStream(stream)); 170 | testCombine((stream) => new Property.fromStream(stream)); 171 | testDebounce((stream) => new Property.fromStream(stream)); 172 | testDelay((stream) => new Property.fromStream(stream)); 173 | testDistinct((stream) => new Property.fromStream(stream)); 174 | testExpand((stream) => new Property.fromStream(stream)); 175 | testFlatMap((stream) => new Property.fromStream(stream)); 176 | testFlatMapLatest((stream) => new Property.fromStream(stream)); 177 | testHandleError((stream) => new Property.fromStream(stream)); 178 | testMap((stream) => new Property.fromStream(stream)); 179 | testMerge((stream) => new Property.fromStream(stream)); 180 | testScan((stream) => new Property.fromStream(stream)); 181 | testSkip((stream) => new Property.fromStream(stream)); 182 | testSkipWhile((stream) => new Property.fromStream(stream)); 183 | testTake((stream) => new Property.fromStream(stream)); 184 | testTakeWhile((stream) => new Property.fromStream(stream)); 185 | testTakeUntil((stream) => new Property.fromStream(stream)); 186 | testWhere((stream) => new Property.fromStream(stream)); 187 | testZip((stream) => new Property.fromStream(stream)); 188 | 189 | // Because Property's always deliver their last value, these test needs to be tweaked in order to pass. 190 | //testSkipUntil((stream) => new Property.fromStream(stream)); 191 | //testWhen((stream) => new Property.fromStream(stream)); 192 | }); 193 | -------------------------------------------------------------------------------- /test/shared/as_event_stream.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.as_event_stream_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testAsEventStream(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("asEventStream()", () { 18 | it("includes new values", () { 19 | return testStream(reactable.asEventStream(), 20 | behavior: () => source..add(2)..add(3), 21 | expectation: (values) { 22 | expect(values).toEqual([2, 3]); 23 | }); 24 | }); 25 | 26 | it("behaves like an event stream", () { 27 | var stream = reactable.asEventStream(); 28 | source..add(1)..close(); 29 | 30 | return stream.toList().then((values) { 31 | expect(values).toEqual([1]); 32 | 33 | // Since this is an event stream, a second call to listen should not deliver any events. 34 | return stream.toList().then((values) { 35 | expect(values).toEqual([]); 36 | }); 37 | }); 38 | }); 39 | }); 40 | } -------------------------------------------------------------------------------- /test/shared/async_expand.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.async_expand_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testAsyncExpand(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("asyncExpand()", () { 18 | it("delivers returned values", () { 19 | return testStream(reactable.asyncExpand((value) => new Stream.fromIterable(["a"])), 20 | behavior: () => source.add(1), 21 | expectation: (values) => expect(values).toEqual(["a"])); 22 | }); 23 | }); 24 | } -------------------------------------------------------------------------------- /test/shared/async_map.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.async_map_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testAsyncMap(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("asyncMap()", () { 18 | it("delivers returned value", () { 19 | return testStream(reactable.asyncMap((value) => new Future(() => "a")), 20 | behavior: () { 21 | source.add(1); 22 | return new Future.delayed(new Duration(milliseconds: 20), () => true); 23 | }, 24 | expectation: (values) => expect(values).toEqual(["a"])); 25 | }); 26 | }); 27 | } -------------------------------------------------------------------------------- /test/shared/buffer_when.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.buffer_when_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testBufferWhen(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | StreamController toggle; 11 | Reactable reactable; 12 | 13 | beforeEach(() { 14 | source = new StreamController(); 15 | toggle = new StreamController(); 16 | reactable = provider(source.stream); 17 | }); 18 | 19 | describe("bufferWhen()", () { 20 | it("buffers when toggle is true", () { 21 | return testStream(reactable.bufferWhen(toggle.stream), 22 | behavior: () { 23 | source.add(1); 24 | 25 | return new Future(() { 26 | toggle.add(true); 27 | source.add(2); 28 | }); 29 | }, 30 | expectation: (values) => expect(values).toEqual([1])); 31 | }); 32 | 33 | it("delivers when toggle is false", () { 34 | return testStream(reactable.bufferWhen(toggle.stream), 35 | behavior: () { 36 | source.add(1); 37 | 38 | return new Future(() { 39 | toggle.add(true); 40 | source.add(2); 41 | 42 | return new Future(() { 43 | toggle.add(false); 44 | source.add(3); 45 | }); 46 | }); 47 | }, 48 | expectation: (values) => expect(values).toEqual([1, 2, 3])); 49 | }); 50 | }); 51 | } -------------------------------------------------------------------------------- /test/shared/combine.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.combine_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testCombine(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | StreamController other; 11 | Reactable reactable; 12 | 13 | beforeEach(() { 14 | source = new StreamController(); 15 | other = new StreamController(); 16 | reactable = provider(source.stream); 17 | }); 18 | 19 | describe("combine()", () { 20 | it("combines values once each reactable has an event", () { 21 | return testStream(reactable.combine(other.stream, (a, b) => [a, b]), 22 | behavior: () { 23 | source.add(1); 24 | other.add(2); 25 | }, 26 | expectation: (values) => expect(values).toEqual([[1, 2]])); 27 | }); 28 | }); 29 | } -------------------------------------------------------------------------------- /test/shared/debounce.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.debounce_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testDebounce(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("debounce()", () { 18 | it("delivers the last event after duration", () { 19 | return testStream(reactable.debounce(new Duration(milliseconds: 50)), 20 | behavior: () { 21 | source..add(1)..add(2)..add(3); 22 | return new Future.delayed(new Duration(seconds: 1)); 23 | }, 24 | expectation: (values) => expect(values).toEqual([3])); 25 | }); 26 | }); 27 | } -------------------------------------------------------------------------------- /test/shared/delay.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.delay_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testDelay(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("delay()", () { 18 | it("throttles the delivery of each event by a duration", () { 19 | return testStream(reactable.delay(new Duration(milliseconds: 50)), 20 | behavior: () { 21 | source..add(1)..add(2)..add(3); 22 | return new Future.delayed(new Duration(milliseconds: 110)); 23 | }, 24 | expectation: (values) => expect(values).toEqual([1, 2])); 25 | }); 26 | }); 27 | } -------------------------------------------------------------------------------- /test/shared/distinct.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.distinct_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testDistinct(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("distinct()", () { 18 | it("omits events that have the same value as the previous", () { 19 | return testStream(reactable.distinct(), 20 | behavior: () => source..add(1)..add(2)..add(2)..add(1), 21 | expectation: (values) => expect(values).toEqual([1, 2, 1])); 22 | }); 23 | }); 24 | } -------------------------------------------------------------------------------- /test/shared/expand.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.expand_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testExpand(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("expand()", () { 18 | it("expands the elements of the event", () { 19 | return testStream(reactable.expand((iterable) => iterable), 20 | behavior: () => source.add([1, 2, 3]), 21 | expectation: (values) => expect(values).toEqual([1, 2, 3])); 22 | }); 23 | }); 24 | } -------------------------------------------------------------------------------- /test/shared/flat_map.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.flat_map_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testFlatMap(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("flatMap()", () { 18 | it("returns the events from each spawned stream", () { 19 | return testStream(reactable.flatMap((value) => new Stream.fromIterable([value, value + 1])), 20 | behavior: () => source..add(1)..add(3)..add(5), 21 | expectation: (values) => expect(values).toEqual([1, 2, 3, 4, 5, 6])); 22 | }); 23 | }); 24 | } -------------------------------------------------------------------------------- /test/shared/flat_map_latest.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.flat_map_latest_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testFlatMapLatest(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("flatMapLatest()", () { 18 | it("returns the events from the latest spawned stream", () { 19 | Stream spawn(value) { 20 | var controller = new StreamController(); 21 | new Future.delayed(new Duration(milliseconds: 25), () => controller..add(value)..add(value + 1)); 22 | return controller.stream; 23 | } 24 | 25 | return testStream(reactable.flatMapLatest((value) => spawn(value)), 26 | behavior: () { 27 | source..add(1)..add(3)..add(5); 28 | return new Future.delayed(new Duration(milliseconds: 50)); 29 | }, 30 | expectation: (values) => expect(values).toEqual([5, 6])); 31 | }); 32 | }); 33 | } -------------------------------------------------------------------------------- /test/shared/handle_error.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.handle_error_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testHandleError(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("handleError()", () { 18 | it("does not throw an error", () { 19 | return testStream(reactable.handleError((e) => e), 20 | behavior: () => source..addError("Oh noez!")..add(1), 21 | expectation: (values) => expect(values).toEqual([1])); 22 | }); 23 | }); 24 | } -------------------------------------------------------------------------------- /test/shared/map.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.map_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testMap(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("map()", () { 18 | it("converts the value of each event", () { 19 | return testStream(reactable.map((value) => value + 1), 20 | behavior: () => source..add(1)..add(2), 21 | expectation: (values) => expect(values).toEqual([2, 3])); 22 | }); 23 | }); 24 | } -------------------------------------------------------------------------------- /test/shared/merge.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.merge_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testMerge(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | StreamController other; 11 | Reactable reactable; 12 | 13 | beforeEach(() { 14 | source = new StreamController(); 15 | other = new StreamController(); 16 | reactable = provider(source.stream); 17 | }); 18 | 19 | describe("merge()", () { 20 | it("joins the values of each stream", () { 21 | return testStream(reactable.merge(other.stream), 22 | behavior: () { 23 | source.add(1); 24 | other.add(2); 25 | }, 26 | expectation: (values) => expect(values).toEqual([1, 2])); 27 | }); 28 | }); 29 | } -------------------------------------------------------------------------------- /test/shared/return_types.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.return_type_shared_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | 7 | void testReturnTypes(Type expectedType, Reactable provider()) => describe("return types", () { 8 | Reactable reactable; 9 | 10 | beforeEach(() { 11 | reactable = provider(); 12 | }); 13 | 14 | describe("asBroadcastStream()", () { 15 | it("returns a $expectedType", () { 16 | var transformed = reactable.asBroadcastStream(); 17 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 18 | }); 19 | }); 20 | 21 | describe("asyncExpand()", () { 22 | it("returns a $expectedType", () { 23 | var transformed = reactable.asyncExpand((value) => new Stream.fromIterable([value])); 24 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 25 | }); 26 | }); 27 | 28 | describe("asyncMap()", () { 29 | it("returns a $expectedType", () { 30 | var transformed = reactable.asyncMap((value) => new Future(() => value)); 31 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 32 | }); 33 | }); 34 | 35 | describe("bufferWhen()", () { 36 | it("returns a $expectedType", () { 37 | var transformed = reactable.bufferWhen(new Stream.fromIterable([])); 38 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 39 | }); 40 | }); 41 | 42 | describe("combine()", () { 43 | it("returns a $expectedType", () { 44 | var transformed = reactable.combine(new Stream.fromIterable([]), (a, b) => a + b); 45 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 46 | }); 47 | }); 48 | 49 | describe("debounce()", () { 50 | it("returns a $expectedType", () { 51 | var transformed = reactable.debounce(new Duration(milliseconds: 1)); 52 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 53 | }); 54 | }); 55 | 56 | describe("delay()", () { 57 | it("returns a $expectedType", () { 58 | var transformed = reactable.delay(new Duration(milliseconds: 1)); 59 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 60 | }); 61 | }); 62 | 63 | describe("distinct()", () { 64 | it("returns a $expectedType", () { 65 | var transformed = reactable.distinct(); 66 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 67 | }); 68 | }); 69 | 70 | describe("expand()", () { 71 | it("returns a $expectedType", () { 72 | var transformed = reactable.expand((value) => [value]); 73 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 74 | }); 75 | }); 76 | 77 | describe("flatMap()", () { 78 | it("returns a $expectedType", () { 79 | var transformed = reactable.flatMap((e) => new Stream.fromIterable([e])); 80 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 81 | }); 82 | }); 83 | 84 | describe("flatMapLatest()", () { 85 | it("returns a $expectedType", () { 86 | var transformed = reactable.flatMapLatest((e) => new Stream.fromIterable([e])); 87 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 88 | }); 89 | }); 90 | 91 | describe("handleError()", () { 92 | it("returns a $expectedType", () { 93 | var transformed = reactable.handleError((e) => e); 94 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 95 | }); 96 | }); 97 | 98 | describe("map()", () { 99 | it("returns a $expectedType", () { 100 | var transformed = reactable.map((e) => e); 101 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 102 | }); 103 | }); 104 | 105 | describe("merge()", () { 106 | it("returns a $expectedType", () { 107 | var transformed = reactable.merge(new Stream.fromIterable([])); 108 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 109 | }); 110 | }); 111 | 112 | describe("scan()", () { 113 | it("returns a $expectedType", () { 114 | var transformed = reactable.scan(1, (a, b) => a + b); 115 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 116 | }); 117 | }); 118 | 119 | describe("skip()", () { 120 | it("returns a $expectedType", () { 121 | var transformed = reactable.skip(1); 122 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 123 | }); 124 | }); 125 | 126 | describe("skipWhile()", () { 127 | it("returns a $expectedType", () { 128 | var transformed = reactable.skipWhile((_) => false); 129 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 130 | }); 131 | }); 132 | 133 | describe("skipUntil()", () { 134 | it("returns a $expectedType", () { 135 | var transformed = reactable.skipUntil(new Stream.fromIterable([])); 136 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 137 | }); 138 | }); 139 | 140 | describe("take()", () { 141 | it("returns a $expectedType", () { 142 | var transformed = reactable.take(1); 143 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 144 | }); 145 | }); 146 | 147 | describe("takeWhile()", () { 148 | it("returns a $expectedType", () { 149 | var transformed = reactable.takeWhile((_) => false); 150 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 151 | }); 152 | }); 153 | 154 | describe("takeUntil()", () { 155 | it("returns a $expectedType", () { 156 | var transformed = reactable.takeUntil(new Stream.fromIterable([])); 157 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 158 | }); 159 | }); 160 | 161 | describe("timeout()", () { 162 | it("returns a $expectedType", () { 163 | var transformed = reactable.timeout(new Duration(milliseconds: 10)); 164 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 165 | }); 166 | }); 167 | 168 | describe("transform()", () { 169 | it("returns a $expectedType", () { 170 | var transformed = reactable.transform(new StreamTransformer.fromHandlers()); 171 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 172 | }); 173 | }); 174 | 175 | describe("when()", () { 176 | it("returns a $expectedType", () { 177 | var transformed = reactable.when(new Stream.fromIterable([])); 178 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 179 | }); 180 | }); 181 | 182 | describe("where()", () { 183 | it("returns a $expectedType", () { 184 | var transformed = reactable.where((e) => true); 185 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 186 | }); 187 | }); 188 | 189 | describe("zip()", () { 190 | it("returns a $expectedType", () { 191 | var transformed = reactable.zip(new Stream.fromIterable([]), (a, b) => [a, b]); 192 | expect(transformed.runtimeType == expectedType).toBeTruthy(); 193 | }); 194 | }); 195 | }); 196 | -------------------------------------------------------------------------------- /test/shared/scan.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.scan_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testScan(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("scan()", () { 18 | it("delivers the initial value", () { 19 | return testStream(reactable.scan(1, (a, b) => a + b), 20 | expectation: (values) => expect(values).toEqual([1])); 21 | }); 22 | 23 | it("delivers the accumlated value for each event", () { 24 | return testStream(reactable.scan(1, (a, b) => a + b), 25 | behavior: () => source..add(2)..add(3), 26 | expectation: (values) => expect(values).toEqual([1, 3, 6])); 27 | }); 28 | }); 29 | } -------------------------------------------------------------------------------- /test/shared/skip.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.skip_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testSkip(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("skip()", () { 18 | it("skips the first N events", () { 19 | return testStream(reactable.skip(1), 20 | behavior: () => source..add(1)..add(2), 21 | expectation: (values) => expect(values).toEqual([2])); 22 | }); 23 | }); 24 | } -------------------------------------------------------------------------------- /test/shared/skip_until.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.skip_until_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testSkipUntil(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | StreamController signal; 11 | Reactable reactable; 12 | 13 | beforeEach(() { 14 | source = new StreamController(); 15 | signal = new StreamController(); 16 | reactable = provider(source.stream); 17 | }); 18 | 19 | describe("skipUntil()", () { 20 | it("skips events until toggle has an event", () { 21 | return testStream(reactable.skipUntil(signal.stream), 22 | behavior: () { 23 | source.add(1); 24 | source.add(2); 25 | 26 | return new Future(() { 27 | signal.add(true); 28 | source.add(3); 29 | }); 30 | }, 31 | expectation: (values) => expect(values).toEqual([3])); 32 | }); 33 | }); 34 | } -------------------------------------------------------------------------------- /test/shared/skip_while.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.skip_while_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testSkipWhile(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("skipWhile()", () { 18 | it("skips events until block return true", () { 19 | return testStream(reactable.skipWhile((value) => value < 3), 20 | behavior: () => source..add(1)..add(2)..add(3)..add(4), 21 | expectation: (values) => expect(values).toEqual([3, 4])); 22 | }); 23 | }); 24 | } -------------------------------------------------------------------------------- /test/shared/take.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.take_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testTake(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("take()", () { 18 | it("takes the first N events", () { 19 | return testStream(reactable.take(2), 20 | behavior: () => source..add(1)..add(2)..add(3), 21 | expectation: (values) => expect(values).toEqual([1, 2])); 22 | }); 23 | }); 24 | } -------------------------------------------------------------------------------- /test/shared/take_until.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.take_until_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testTakeUntil(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | StreamController signal; 11 | Reactable reactable; 12 | 13 | beforeEach(() { 14 | source = new StreamController(sync: true); 15 | signal = new StreamController(sync: true); 16 | reactable = provider(source.stream); 17 | }); 18 | 19 | describe("takeUntil()", () { 20 | it("takes values until the signal stream has a value", () { 21 | return testStream(reactable.takeUntil(signal.stream), 22 | behavior: () { 23 | source.add(1); 24 | source.add(2); 25 | signal.add(true); 26 | source.add(3); 27 | }, 28 | expectation: (values) => expect(values).toEqual([1, 2])); 29 | }); 30 | }); 31 | } -------------------------------------------------------------------------------- /test/shared/take_while.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.take_while_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testTakeWhile(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("takeWhile()", () { 18 | it("takes values while the block returns true", () { 19 | return testStream(reactable.takeWhile((value) => value < 3), 20 | behavior: () => source..add(1)..add(2)..add(3), 21 | expectation: (values) => expect(values).toEqual([1, 2])); 22 | }); 23 | }); 24 | } -------------------------------------------------------------------------------- /test/shared/util.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.util; 2 | 3 | import 'dart:async'; 4 | 5 | Function doNothing = (_) {}; 6 | 7 | Future testStream(Stream stream, {behavior(), expectation(List values)}) { 8 | var results = []; 9 | 10 | var subscription = stream.listen((value) { 11 | results.add(value); 12 | }); 13 | 14 | return new Future(() { 15 | if (behavior != null) { 16 | return behavior(); 17 | } 18 | }) 19 | .then((_) => new Future(() { 20 | subscription.cancel(); 21 | })) 22 | .then((_) => expectation(results)); 23 | } 24 | -------------------------------------------------------------------------------- /test/shared/when.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.when_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testWhen(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | StreamController signal; 11 | Reactable reactable; 12 | 13 | beforeEach(() { 14 | source = new StreamController(sync: true); 15 | signal = new StreamController(sync: true); 16 | reactable = provider(source.stream); 17 | }); 18 | 19 | describe("when()", () { 20 | it("delivers events while signal stream is true", () { 21 | return testStream(reactable.when(signal.stream), 22 | behavior: () { 23 | source.add(1); 24 | signal.add(true); 25 | source.add(2); 26 | }, 27 | expectation: (values) => expect(values).toEqual([2])); 28 | }); 29 | }); 30 | } -------------------------------------------------------------------------------- /test/shared/where.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.where_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testWhere(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | Reactable reactable; 11 | 12 | beforeEach(() { 13 | source = new StreamController(); 14 | reactable = provider(source.stream); 15 | }); 16 | 17 | describe("where()", () { 18 | it("delivers event if block return true", () { 19 | return testStream(reactable.where((value) => value < 3), 20 | behavior: () => source..add(1)..add(3)..add(2), 21 | expectation: (values) => expect(values).toEqual([1, 2])); 22 | }); 23 | }); 24 | } -------------------------------------------------------------------------------- /test/shared/zip.dart: -------------------------------------------------------------------------------- 1 | library frappe.test.shared.zip_tests; 2 | 3 | import 'dart:async'; 4 | import 'package:frappe/frappe.dart'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'util.dart'; 7 | 8 | void testZip(Reactable provider(Stream stream)) { 9 | StreamController source; 10 | StreamController other; 11 | Reactable reactable; 12 | 13 | beforeEach(() { 14 | source = new StreamController(); 15 | other = new StreamController(); 16 | reactable = provider(source.stream); 17 | }); 18 | 19 | describe("zip()", () { 20 | it("combines events at each index", () { 21 | return testStream(reactable.zip(other.stream, (a, b) => [a, b]), 22 | behavior: () { 23 | source.add(1); 24 | other.add(2); 25 | }, 26 | expectation: (values) => expect(values).toEqual([[1, 2]])); 27 | }); 28 | }); 29 | } -------------------------------------------------------------------------------- /tool/grind.dart: -------------------------------------------------------------------------------- 1 | import 'package:grinder/grinder.dart'; 2 | 3 | main(args) => grind(args); 4 | 5 | @Task("Analyzes the package for errors or warnings") 6 | analyze() { 7 | new PubApp.global('tuneup').run(['check']); 8 | } 9 | 10 | @Task("Checks that Dart code adheres to the style guide") 11 | lint() { 12 | new PubApp.global('linter').run(["example/", "lib/", "test/"]); 13 | } 14 | 15 | @Task() 16 | test() => Tests.runCliTests(testFile: "all_tests.dart"); 17 | 18 | @DefaultTask() 19 | @Depends(analyze, test, lint) 20 | build() { 21 | 22 | } --------------------------------------------------------------------------------