Add: adding forked event_bus_plus as project event bus

This commit is contained in:
Patrick Stewart 2024-10-02 18:53:12 -07:00
parent c194fadbc2
commit 11c12fd590
28 changed files with 1445 additions and 0 deletions

View file

@ -0,0 +1,54 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Dart
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# Note: This workflow uses the latest stable version of the Dart SDK.
# You can specify other versions if desired, see documentation here:
# https://github.com/dart-lang/setup-dart/blob/main/README.md
# - uses: dart-lang/setup-dart@v1
- uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603
- name: Flutter action
# You may pin to the exact commit or the version.
# uses: subosito/flutter-action@4389e6cbc6cb8a4b18c628ff96ff90be0e926aa8
uses: subosito/flutter-action@v1.5.3
- name: Install dependencies
run: dart pub get
# Uncomment this step to verify the use of 'dart format' on each commit.
# - name: Verify formatting
# run: dart format --output=none --set-exit-if-changed .
# Consider passing '--fatal-infos' for slightly stricter analysis.
- name: Analyze project source
run: dart analyze
# Your project will need to have tests in test/ and a dependency on
# package:test for this step to succeed. Note that Flutter projects will
# want to change this to 'flutter test'.
- name: Run tests
run: flutter test --coverage
- name: Install lcov
run: sudo apt-get install -y lcov
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
with:
file: coverage/lcov.info

30
core/eventbus/.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/
coverage

10
core/eventbus/.metadata Normal file
View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 7e9793dee1b85a243edd0e06cb1658e98b077561
channel: stable
project_type: package

View file

@ -0,0 +1,67 @@
## 0.6.2
* update `logger` dependency
## 0.6.1
* [Add] `allowLogging` to control logging (thanks [@hatemragab](https://github.com/hatemragab))
## 0.6.0
* **[Change]** fire `EmptyEvent` after each event to keep stream empty.
* [Add] [Logger](https://pub.dev/packages/logger)
## 0.5.0
* [Add] timestamp for each `event`.
## 0.4.0
* [Add] mapper
## 0.3.0+3
* Downgrade clock version
## 0.3.0+2
* Improve documentation
## 0.3.0+1
* Improve documentation
## 0.3.0
* [Add] `EventCompletionEvent`
* Improve documentation
## 0.2.2
* [Fix] `clock` dependency
## 0.2.1
* [Add] debug logging
## 0.2.0
* [Add] history
## 0.1.0
* [Add] distinct
## 0.0.2+1
* [Fix] GitHub repository link
## 0.0.2
* [Remove] `id` of `AppEvent`
## 0.0.1
* Initial release.
* [Add] `EventBus`
* [Add] `AppEvent` superclass

21
core/eventbus/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Andrew
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

98
core/eventbus/README.md Normal file
View file

@ -0,0 +1,98 @@
# EventBus: Events for Dart/Flutter
[![pub package](https://img.shields.io/pub/v/event_bus_plus.svg?label=event_bus_plus&color=blue)](https://pub.dev/packages/event_bus_plus)
[![codecov](https://codecov.io/gh/AndrewPiterov/event_bus_plus/branch/main/graph/badge.svg?token=VM9LTJXGQS)](https://codecov.io/gh/AndrewPiterov/event_bus_plus)
[![Dart](https://github.com/AndrewPiterov/event_bus_plus/actions/workflows/dart.yml/badge.svg)](https://github.com/AndrewPiterov/event_bus_plus/actions/workflows/dart.yml)
**EventBus** is an open-source library for Dart and Flutter using the **publisher/subscriber** pattern for loose coupling. **EventBus** enables central communication to decoupled classes with just a few lines of code simplifying the code, removing dependencies, and speeding up app development.
<img src="https://raw.githubusercontent.com/andrewpiterov/event_bus_plus/main/doc/pub_sub.webp" alt="event bus publish subscribe" style="width: 100%; height: auto; "/>
## Your benefits using EventBus: It…
- simplifies the communication between components;
- decouples event senders and receivers;
- performs well with UI artifacts (e.g. Widgets, Controllers);
- avoids complex and error-prone dependencies and life cycle issues.
<img src="https://raw.githubusercontent.com/andrewpiterov/event_bus_plus/main/doc/video_presentation.gif" alt="event bus plus" style="width: 100%; height: auto; "/>
### Define the app's events
```dart
// Initialize the Service Bus
IAppEventBus eventBus = AppEventBus();
// Define your app events
final event = FollowEvent('@devcraft.ninja');
final event = CommentEvent('Awesome package 😎');
```
### Subscribe
```dart
// listen the latest event
final sub = eventBus.last$
.listen((AppEvent event) { /*do something*/ });
// Listen particular event
final sub2 = eventBus.on<FollowAppEvent>()
.listen((e) { /*do something*/ });
```
### Publish
```dart
// fire the event
eventBus.fire(event);
```
### Watch events in progress
```dart
// start watch the event till its completion
eventBus.watch(event);
// and check the progress
eventBus.isInProgress<FollowAppEvent>();
// or listen stream to check the processing
eventBus.inProgress$.map((List<AppEvent> events) =>
events.whereType<FollowAppEvent>().isNotEmpty);
// complete
_eventBus.complete(event);
// or complete with completion event
_eventBus.complete(event, nextEvent: SomeAnotherEvent);
```
## History
```dart
final events = eventBus.history;
```
## Mapping
```dart
final eventBus = bus = EventBus(
map: {
SomeEvent: [
(e) => SomeAnotherEvent(),
],
},
);
```
## Contributing
We accept the following contributions:
* Improving the documentation
* [Reporting issues](https://github.com/AndrewPiterov/event_bus_plus/issues/new)
* Fixing bugs
## Maintainers
* [Andrew Piterov](mailto:contact@andrewpiterov.com?subject=[GitHub]%20Source%20Dart%event_bus_plus)

View file

@ -0,0 +1,224 @@
include: package:flutter_lints/flutter.yaml
analyzer:
strong-mode:
implicit-casts: false
implicit-dynamic: true
errors:
# Close instances of dart.core.Sink.
close_sinks: error
# Cancel instances of dart.async.StreamSubscription.
cancel_subscriptions: error
# treat missing required parameters as a error (not a hint)
missing_required_param: error
# treat missing returns as a error (not a hint)
missing_return: error
# allow having TODOs in the code and treat as warning
todo: warning
# allow self-reference to deprecated members (we do this because otherwise we have
# to annotate every member in every test, assert, etc, when we deprecate something)
deprecated_member_use_from_same_package: ignore
# Ignore analyzer hints for updating pubspecs when using Future or
# Stream and not importing dart:async
# Please see https://github.com/flutter/flutter/pull/24528 for details.
sdk_version_async_exported_from_core: ignore
# await
unawaited_futures: error
avoid_print: warning
always_declare_return_types: error
unrelated_type_equality_checks: error
implementation_imports: error
require_trailing_commas: warning
avoid_slow_async_io: error
use_build_context_synchronously: error
sized_box_for_whitespace: error
dead_code: warning
invalid_assignment: error
exclude:
- "bin/cache/**"
# the following two are relative to the stocks example and the flutter package respectively
# see https://github.com/dart-lang/sdk/issues/28463
- "lib/i18n/messages_*.dart"
- "lib/src/http/**"
- "build/**"
- "lib/**.freezed.dart"
- "lib/**.g.dart"
- "test/**"
- "example/**"
linter:
rules:
# these rules are documented on and in the same order as
# the Dart Lint rules page to make maintenance easier
# https://github.com/dart-lang/linter/blob/master/example/all.yaml
- always_declare_return_types
- always_put_control_body_on_new_line
# - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219
- always_require_non_null_named_parameters
# - always_specify_types
- annotate_overrides
# - avoid_annotating_with_dynamic # conflicts with always_specify_types
# - avoid_as # required for implicit-casts: true
- avoid_bool_literals_in_conditional_expressions
# - avoid_catches_without_on_clauses # we do this commonly
# - avoid_catching_errors # we do this commonly
- avoid_classes_with_only_static_members
# - avoid_double_and_int_checks # only useful when targeting JS runtime
- avoid_empty_else
- avoid_equals_and_hash_code_on_mutable_classes
- avoid_field_initializers_in_const_classes
- avoid_function_literals_in_foreach_calls
# - avoid_implementing_value_types # not yet tested
- avoid_init_to_null
# - avoid_js_rounded_ints # only useful when targeting JS runtime
- avoid_null_checks_in_equality_operators
# - avoid_positional_boolean_parameters # not yet tested
- avoid_print # not yet tested
# - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356)
# - avoid_redundant_argument_values # not yet tested
- avoid_relative_lib_imports
- avoid_renaming_method_parameters
- avoid_return_types_on_setters
# - avoid_returning_null # there are plenty of valid reasons to return null
# - avoid_returning_null_for_future # not yet tested
- avoid_returning_null_for_void
# - avoid_returning_this # there are plenty of valid reasons to return this
# - avoid_setters_without_getters # not yet tested
- avoid_shadowing_type_parameters # not yet tested
- avoid_single_cascade_in_expression_statements
- avoid_slow_async_io
- avoid_types_as_parameter_names
# - avoid_types_on_closure_parameters # conflicts with always_specify_types
# - avoid_unnecessary_containers # not yet tested
- avoid_unused_constructor_parameters
- avoid_void_async
# - avoid_web_libraries_in_flutter # not yet tested
- await_only_futures
- camel_case_extensions
- camel_case_types
- cancel_subscriptions
# - cascade_invocations # not yet tested
# - close_sinks # not reliable enough
# - comment_references # blocked on https://github.com/flutter/flutter/issues/20765
# - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204
- control_flow_in_finally
# - curly_braces_in_flow_control_structures # not yet tested
# - diagnostic_describe_all_properties # not yet tested
- directives_ordering
- empty_catches
- empty_constructor_bodies
- empty_statements
# - file_names # not yet tested
- flutter_style_todos
- hash_and_equals
- implementation_imports
# - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811
- iterable_contains_unrelated_type
# - join_return_with_assignment # not yet tested
- library_names
- library_prefixes
# - lines_longer_than_80_chars # not yet tested
- list_remove_unrelated_type
# - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181
# - missing_whitespace_between_adjacent_strings # not yet tested
- no_adjacent_strings_in_list
- no_duplicate_case_values
# - no_logic_in_create_state # not yet tested
# - no_runtimeType_toString # not yet tested
- non_constant_identifier_names
# - null_closures # not yet tested
- omit_local_variable_types # opposite of always_specify_types
# - one_member_abstracts # too many false positives
# - only_throw_errors # https://github.com/flutter/flutter/issues/5792
- overridden_fields
- package_api_docs
- package_names
- package_prefixed_library_names
# - parameter_assignments # we do this commonly
- prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists
# - prefer_asserts_with_message # not yet tested
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
# - prefer_constructors_over_static_methods # not yet tested
- prefer_contains
# - prefer_double_quotes # opposite of prefer_single_quotes
- prefer_equal_for_default_values
# - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_for_elements_to_map_fromIterable
- prefer_foreach
# - prefer_function_declarations_over_variables # not yet tested
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_if_null_operators
- prefer_initializing_formals
- prefer_inlined_adds
# - prefer_int_literals # not yet tested
# - prefer_interpolation_to_compose_strings # not yet tested
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_iterable_whereType
# - prefer_mixin # https://github.com/dart-lang/language/issues/32
# - prefer_null_aware_operators # disable until NNBD, see https://github.com/flutter/flutter/pull/32711#issuecomment-492930932
# - prefer_relative_imports # not yet tested
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- prefer_void_to_null
# - provide_deprecation_message # not yet tested
- public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml
- recursive_getters
- slash_for_doc_comments
# - sort_child_properties_last # not yet tested
- sort_constructors_first
- sort_pub_dependencies
- sort_unnamed_constructors_first
- test_types_in_equals
- throw_in_finally
# - type_annotate_public_apis # subset of always_specify_types
- type_init_formals
- unawaited_futures # too many false positives
- unnecessary_await_in_return # not yet tested
- unnecessary_brace_in_string_interps
- unnecessary_const
# - unnecessary_final # conflicts with prefer_final_locals
- unnecessary_getters_setters
# - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_in_if_null_operators
- unnecessary_overrides
- unnecessary_parenthesis
- unnecessary_statements
- unnecessary_string_interpolations
- unnecessary_this
- unrelated_type_equality_checks
# - unsafe_html # not yet tested
- use_full_hex_values_for_flutter_colors
# - use_function_type_syntax_for_parameters # not yet tested
# - use_key_in_widget_constructors # not yet tested
- use_late_for_private_fields_and_variables
- use_rethrow_when_possible
# - use_setters_to_change_properties # not yet tested
# - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182
# - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review
- valid_regexps
- void_checks
dart_code_metrics:
rules:
- newline-before-return:
severity: style
- no-boolean-literal-compare:
severity: error
anti-patterns:
- long-method:
severity: warning

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

@ -0,0 +1,3 @@
library event_bus_plus;
export 'res/res.dart';

View file

@ -0,0 +1,29 @@
import 'package:clock/clock.dart';
import 'package:equatable/equatable.dart';
/// The base class for all events
abstract class AppEvent extends Equatable {
/// Create the event
const AppEvent();
/// The event time
DateTime get timestamp => clock.now();
}
/// The event completion event
class EventCompletionEvent extends AppEvent {
/// Create the event
const EventCompletionEvent(this.event);
/// The event that is completed
final AppEvent event;
@override
List<Object> get props => [event];
}
/// The empty event
class EmptyEvent extends AppEvent {
@override
List<Object?> get props => [];
}

View file

@ -0,0 +1,212 @@
import 'package:logger/logger.dart';
import 'package:angel3_reactivex/subjects.dart';
import 'app_event.dart';
import 'history_entry.dart';
import 'subscription.dart';
/// The event bus interface
abstract class IEventBus {
/// Whether the event bus is busy
bool get isBusy;
/// Whether the event bus is busy
Stream<bool> get isBusy$;
/// The last event
AppEvent? get last;
/// The last event
Stream<AppEvent?> get last$;
/// The list of events that are in progress
Stream<List<AppEvent>> get inProgress$;
/// Subscribe `EventBus` on a specific type of event, and register responder to it.
Stream<T> on<T extends AppEvent>();
/// Subscribe `EventBus` on a specific type of event, and register responder to it.
Stream<bool> whileInProgress<T extends AppEvent>();
/// Subscribe `EventBus` on a specific type of event, and register responder to it.
Subscription respond<T>(Responder<T> responder);
/// The history of events
List<EventBusHistoryEntry> get history;
/// Fire a event
void fire(AppEvent event);
/// Fire a event and wait for it to be completed
void watch(AppEvent event);
/// Complete a event
void complete(AppEvent event, {AppEvent? nextEvent});
///
bool isInProgress<T>();
/// Reset the event bus
void reset();
/// Dispose the event bus
void dispose();
/// Clear the history
void clearHistory();
}
/// The event bus implementation
class EventBus implements IEventBus {
/// Create the event bus
EventBus({
this.maxHistoryLength = 100,
this.map = const {},
this.allowLogging = false,
});
final _logger = Logger();
/// The maximum length of history
final int maxHistoryLength;
/// allow to log all events this when you call [fire]
/// the event will be in console log
final bool allowLogging;
/// The map of events
final Map<Type, List<AppEvent Function(AppEvent event)>> map;
@override
bool get isBusy => _inProgress.value.isNotEmpty;
@override
Stream<bool> get isBusy$ => _inProgress.map((event) => event.isNotEmpty);
final _lastEventSubject = BehaviorSubject<AppEvent>();
@override
AppEvent? get last => _lastEventSubject.valueOrNull;
@override
Stream<AppEvent?> get last$ => _lastEventSubject.distinct();
final _inProgress = BehaviorSubject<List<AppEvent>>.seeded([]);
List<AppEvent> get _isInProgressEvents => _inProgress.value;
@override
Stream<List<AppEvent>> get inProgress$ => _inProgress;
@override
List<EventBusHistoryEntry> get history => List.unmodifiable(_history);
final List<EventBusHistoryEntry> _history = [];
@override
void fire(AppEvent event) {
if (_history.length >= maxHistoryLength) {
_history.removeAt(0);
}
_history.add(EventBusHistoryEntry(event, event.timestamp));
// 1. Fire the event
_lastEventSubject.add(event);
// 2. Map if needed
_map(event);
// 3. Reset stream
_lastEventSubject.add(EmptyEvent());
if (allowLogging) {
_logger.d(' ⚡️ [${event.timestamp}] $event');
}
}
@override
void watch(AppEvent event) {
fire(event);
_inProgress.add([
..._isInProgressEvents,
event,
]);
}
@override
void complete(AppEvent event, {AppEvent? nextEvent}) {
// complete the event
if (_isInProgressEvents.any((e) => e == event)) {
final newArr = _isInProgressEvents.toList()
..removeWhere((e) => e == event);
_inProgress.add(newArr);
fire(EventCompletionEvent(event));
}
// fire next event if any
if (nextEvent != null) {
fire(nextEvent);
}
}
@override
bool isInProgress<T>() {
return _isInProgressEvents.whereType<T>().isNotEmpty;
}
@override
Stream<T> on<T extends AppEvent>() {
if (T == dynamic) {
return _lastEventSubject.stream as Stream<T>;
} else {
return _lastEventSubject.stream.where((event) => event is T).cast<T>();
}
}
/// Subscribe `EventBus` on a specific type of event, and register responder to it.
///
/// When [T] is not given or given as `dynamic`, it listens to all events regardless of the type.
/// Returns [Subscription], which can be disposed to cancel all the subscription registered to itself.
@override
Subscription respond<T>(Responder<T> responder) =>
Subscription(_lastEventSubject).respond<T>(responder);
@override
Stream<bool> whileInProgress<T extends AppEvent>() {
return _inProgress.map((events) {
return events.whereType<T>().isNotEmpty;
});
}
void _map(AppEvent? event) {
if (event == null) {
return;
}
final functions = map[event.runtimeType] ?? [];
if (functions.isEmpty) {
return;
}
for (final func in functions) {
final newEvent = func(event);
if (newEvent.runtimeType == event.runtimeType) {
if (allowLogging) {
_logger.d(
' 🟠 SKIP EVENT: ${newEvent.runtimeType} => ${event.runtimeType}',
);
}
continue;
}
fire(newEvent);
}
}
@override
void clearHistory() {
_history.clear();
}
@override
void reset() {
clearHistory();
_inProgress.add([]);
_lastEventSubject.add(EmptyEvent());
}
@override
void dispose() {
_inProgress.close();
_lastEventSubject.close();
}
}

View file

@ -0,0 +1,18 @@
import 'package:equatable/equatable.dart';
import 'app_event.dart';
/// The history entry
class EventBusHistoryEntry extends Equatable {
/// The history entry
const EventBusHistoryEntry(this.event, this.timestamp);
/// The event
final AppEvent event;
/// The timestamp
final DateTime timestamp;
@override
List<Object?> get props => [event, timestamp];
}

View file

@ -0,0 +1,4 @@
export 'app_event.dart';
export 'event_bus.dart';
export 'history_entry.dart';
export 'subscription.dart';

View file

@ -0,0 +1,78 @@
import 'dart:async';
/// The function/method signature for the event handler
typedef Responder<T> = void Function(T event);
/// The class manages the subscription to event bus
class Subscription {
/// Create the subscription
///
/// Should barely used directly, to subscribe to event bus, use `EventBus.respond`.
Subscription(this._stream);
/// Returns an instance that indicates there is no subscription
factory Subscription.empty() => const _EmptySubscription();
final Stream _stream;
/// Subscriptions that registered to event bus
final List<StreamSubscription> subscriptions = [];
Stream<T> _cast<T>() {
if (T == dynamic) {
return _stream as Stream<T>;
} else {
return _stream.where((event) => event is T).cast<T>();
}
}
/// Register a [responder] to event bus for the event type [T].
/// If [T] is omitted or given as `dynamic`, it listens to all events that published on [EventBus].
///
/// Method call can be safely chained, and the order doesn't matter.
///
/// ```
/// eventBus
/// .respond<EventA>(responderA)
/// .respond<EventB>(responderB);
/// ```
Subscription respond<T>(Responder<T> responder) {
subscriptions.add(_cast<T>().listen(responder));
return this;
}
/// Cancel all the registered subscriptions.
/// After calling this method, all the events published won't be delivered to the cleared responders any more.
///
/// No harm to call more than once.
void dispose() {
if (subscriptions.isEmpty) {
return;
}
for (final s in subscriptions) {
s.cancel();
}
subscriptions.clear();
}
}
class _EmptySubscription implements Subscription {
const _EmptySubscription();
static final List<StreamSubscription> emptyList =
List.unmodifiable(<StreamSubscription>[]);
@override
void dispose() {}
@override
Subscription respond<T>(responder) => throw Exception('Not supported');
@override
List<StreamSubscription> get subscriptions => emptyList;
@override
Stream<T> _cast<T>() => throw Exception('Not supported');
@override
Stream get _stream => throw Exception('Not supported');
}

View file

@ -0,0 +1,20 @@
name: angel3_event_bus
description: Event Bus for Dart.
version: 0.6.2
homepage: https://github.com/AndrewPiterov/event_bus_plus
environment:
sdk: '>=2.17.1 <4.0.0'
dependencies:
clock: ^1.1.0
equatable: ^2.0.5
logger: ^2.0.2+1
angel3_reactivex: ^0.27.5
dev_dependencies:
flutter_lints: ^3.0.0
given_when_then_unit_test: ^0.2.1
shouldly: ^0.5.0+1
test: ^1.22.0

View file

@ -0,0 +1,70 @@
import 'package:event_bus_plus/event_bus_plus.dart';
import 'package:given_when_then_unit_test/given_when_then_unit_test.dart';
import 'models.dart';
import 'package:test/test.dart';
void main() {
late IEventBus bus;
before(() {
bus = EventBus();
});
test('emit Follower Events', () {
expect(
bus.on(),
emitsInOrder([
const FollowAppEvent('username'),
EmptyEvent(),
const FollowAppEvent('username3'),
EmptyEvent(),
const FollowAppEvent('username2'),
EmptyEvent(),
]));
bus.fire(const FollowAppEvent('username'));
bus.fire(const FollowAppEvent('username3'));
bus.fire(const FollowAppEvent('username2'));
});
test('start watch but not complete', () {
expect(
bus.on(),
emitsInOrder([
const FollowAppEvent('username'),
]));
bus.watch(const FollowAppEvent('username'));
});
test('start watch and complete', () {
const watchable = FollowAppEvent('username3');
expect(
bus.on(),
emitsInOrder([
watchable,
EmptyEvent(),
const EventCompletionEvent(watchable),
EmptyEvent(),
const FollowSuccessfullyEvent(watchable),
EmptyEvent(),
]));
bus.watch(watchable);
bus.complete(watchable,
nextEvent: const FollowSuccessfullyEvent(watchable));
});
// test('emit Follower Events', () {
// final watchable = FollowAppEvent('username3', id: '3');
// expect(
// _bus.on(),
// emitsInAnyOrder([
// FollowAppEvent('username3', id: '3'),
// FollowSuccessfullyAppEvent(watchable),
// ]));
// _bus.watch(watchable);
// _bus.complete(watchable, withh: FollowSuccessfullyAppEvent(watchable));
// });
}

View file

@ -0,0 +1,58 @@
import 'package:event_bus_plus/event_bus_plus.dart';
import 'package:given_when_then_unit_test/given_when_then_unit_test.dart';
import 'package:test/expect.dart';
import 'package:test/scaffolding.dart';
import 'models.dart';
void main() {
late IEventBus bus;
before(() {
bus = EventBus();
});
test('Call once', () {
expectLater(
bus.last$,
emitsInOrder([
const SomeEvent(),
EmptyEvent(),
const SomeAnotherEvent(),
EmptyEvent(),
]));
bus.fire(const SomeEvent());
bus.fire(const SomeAnotherEvent());
});
// test('Call twice', () {
// const event = SomeEvent();
// expectLater(
// bus.last$,
// emitsInOrder([
// const SomeEvent(),
// EmptyEvent(),
// const SomeAnotherEvent(),
// EmptyEvent(),
// ]));
// bus.fire(event);
// bus.fire(event);
// bus.fire(const SomeAnotherEvent());
// });
// test('Call three times', () {
// const event = SomeEvent();
// expectLater(
// bus.last$,
// emitsInOrder([
// const SomeEvent(),
// EmptyEvent(),
// const SomeAnotherEvent(),
// EmptyEvent(),
// ]));
// bus.fire(event);
// bus.fire(event);
// bus.fire(event);
// bus.fire(const SomeAnotherEvent());
// });
}

View file

@ -0,0 +1,20 @@
import 'package:event_bus_plus/event_bus_plus.dart';
import 'package:test/test.dart';
import 'models.dart';
void main() {
final IEventBus _bus = EventBus();
test('Should fire Empty event', () {
expect(
_bus.last$,
emitsInOrder(
[
const SomeEvent(),
EmptyEvent(),
],
),
);
_bus.fire(const SomeEvent());
}, timeout: const Timeout(Duration(seconds: 1)));
}

View file

@ -0,0 +1,63 @@
import 'package:event_bus_plus/event_bus_plus.dart';
import 'package:given_when_then_unit_test/given_when_then_unit_test.dart';
import 'package:shouldly/shouldly.dart';
import 'package:test/scaffolding.dart';
import 'models.dart';
void main() {
late IEventBus bus;
before(() {
bus = EventBus();
});
test('Empty event bus', () {
bus.isBusy.should.beFalse();
});
when('start some event', () {
const event = FollowAppEvent('username');
// const eventId = '1';
before(() {
bus.watch(event);
});
then('should be busy', () {
bus.isBusy.should.beTrue();
});
then('should be in progress', () {
bus.isInProgress<FollowAppEvent>().should.beTrue();
});
and('complete the event', () {
before(() {
bus.complete(event);
});
then('should not be busy', () {
bus.isBusy.should.beFalse();
});
then('should not be in progress', () {
bus.isInProgress<FollowAppEvent>().should.not.beTrue();
});
});
});
group('compare equality', () {
// test('compare two equal events - should not be equal', () {
// final event = FollowAppEvent('username');
// final event2 = FollowAppEvent('username');
// event.should.not.be(event2);
// });
test('compare two equal events - should be equal', () {
const event = FollowAppEvent('username');
const event2 = FollowAppEvent('username');
event.should.be(event2);
});
});
}

View file

@ -0,0 +1,75 @@
import 'package:clock/clock.dart';
import 'package:event_bus_plus/event_bus_plus.dart';
import 'package:given_when_then_unit_test/given_when_then_unit_test.dart';
import 'package:shouldly/shouldly.dart';
import 'package:test/test.dart';
import 'models.dart';
void main() {
late IEventBus bus;
before(() {
bus = EventBus(maxHistoryLength: 3);
});
test('Keep history', () {
final date = DateTime(2022, 9, 1);
withClock(Clock.fixed(date), () {
bus.fire(const SomeEvent());
bus.fire(const SomeAnotherEvent());
bus.fire(const FollowAppEvent('@username'));
bus.history.should.be([
EventBusHistoryEntry(const SomeEvent(), date),
EventBusHistoryEntry(const SomeAnotherEvent(), date),
EventBusHistoryEntry(const FollowAppEvent('@username'), date),
]);
});
});
test('Keep full history without debounce', () {
final date = DateTime(2022, 9, 1, 4, 59, 55);
withClock(Clock.fixed(date), () {
bus.fire(const SomeEvent());
bus.fire(const SomeEvent());
bus.fire(const SomeAnotherEvent());
bus.history.should.be([
EventBusHistoryEntry(const SomeEvent(), date),
EventBusHistoryEntry(const SomeEvent(), date),
EventBusHistoryEntry(const SomeAnotherEvent(), date),
]);
});
});
test('Clear history', () {
bus.fire(const SomeEvent());
bus.fire(const SomeAnotherEvent());
bus.clearHistory();
bus.history.should.beEmpty();
});
group('History length', () {
test('Add more than max', () {
final date = DateTime(2022, 9, 1, 4, 59, 55);
withClock(Clock.fixed(date), () {
bus.fire(const SomeEvent());
bus.fire(const SomeAnotherEvent());
bus.fire(const FollowAppEvent('@username'));
bus.fire(const NewCommentEvent('text'));
bus.history.length.should.be(3);
bus.history.should.be([
EventBusHistoryEntry(const SomeAnotherEvent(), date),
EventBusHistoryEntry(const FollowAppEvent('@username'), date),
EventBusHistoryEntry(const NewCommentEvent('text'), date),
]);
});
});
});
}

View file

@ -0,0 +1,36 @@
import 'package:event_bus_plus/event_bus_plus.dart';
import 'package:given_when_then_unit_test/given_when_then_unit_test.dart';
import 'package:test/test.dart';
import '../models.dart';
void main() {
late IEventBus bus;
before(
() {
bus = EventBus(
map: {
SomeEvent: [
(e) => e,
(e) => const SomeAnotherEvent(),
],
},
);
},
);
test('does not emit the same event', () {
expect(
bus.last$,
emitsInOrder(
[
const SomeEvent(),
const SomeAnotherEvent(),
EmptyEvent(),
],
),
);
bus.fire(const SomeEvent());
}, timeout: const Timeout(Duration(seconds: 1)));
}

View file

@ -0,0 +1,34 @@
import 'package:event_bus_plus/event_bus_plus.dart';
import 'package:given_when_then_unit_test/given_when_then_unit_test.dart';
import 'package:test/test.dart';
import '../models.dart';
void main() {
late IEventBus bus;
before(
() {
bus = EventBus(
map: {
SomeEvent: [
(e) => const SomeAnotherEvent(),
],
},
);
},
);
test('emits another', () {
expect(
bus.last$,
emitsInOrder(
[
const SomeEvent(),
const SomeAnotherEvent(),
],
),
);
bus.fire(const SomeEvent());
}, timeout: const Timeout(Duration(seconds: 1)));
}

View file

@ -0,0 +1,42 @@
import 'package:event_bus_plus/event_bus_plus.dart';
class FollowAppEvent extends AppEvent {
const FollowAppEvent(this.username);
final String username;
@override
List<Object?> get props => [username];
}
class FollowSuccessfullyEvent extends AppEvent {
const FollowSuccessfullyEvent(this.starting);
final FollowAppEvent starting;
@override
List<Object?> get props => [starting];
}
class NewCommentEvent extends AppEvent {
const NewCommentEvent(this.text);
final String text;
@override
List<Object?> get props => [text];
}
class SomeEvent extends AppEvent {
const SomeEvent();
@override
List<Object?> get props => [];
}
class SomeAnotherEvent extends AppEvent {
const SomeAnotherEvent();
@override
List<Object?> get props => [];
}

View file

@ -0,0 +1,74 @@
// ignore_for_file: prefer_const_constructors
import 'dart:async';
import 'dart:developer';
import 'package:event_bus_plus/event_bus_plus.dart';
import 'package:given_when_then_unit_test/given_when_then_unit_test.dart';
import 'package:test/expect.dart';
import 'package:test/scaffolding.dart';
import 'models.dart';
void main() {
late IEventBus bus;
before(() {
bus = EventBus();
});
test('emit Some Event', () {
final ctrl = StreamController();
final sub = bus.respond<NewCommentEvent>((event) {
log('new comment');
ctrl.add(2);
}).respond<FollowAppEvent>((event) {
log('new follower');
ctrl.add(1);
});
expect(ctrl.stream, emitsInOrder([1, 2]));
bus.fire(FollowAppEvent('username'));
bus.fire(NewCommentEvent('comment #1'));
});
test('emit Some Events', () {
final ctrl = StreamController();
final sub = bus.respond<NewCommentEvent>((event) {
log('new comment');
ctrl.add(2);
}).respond<FollowAppEvent>((event) {
log('new follower');
ctrl.add(1);
});
expect(ctrl.stream, emitsInOrder([1, 2, 1]));
bus.fire(FollowAppEvent('username'));
bus.fire(NewCommentEvent('comment #1'));
bus.fire(FollowAppEvent('username2'));
});
test('emit all Event', () {
final ctrl = StreamController();
final sub = bus.respond<NewCommentEvent>((event) {
log('new comment');
ctrl.add(2);
}).respond<FollowAppEvent>((event) {
log('new follower');
ctrl.add(1);
}).respond((event) {
log('event $event');
ctrl.add(3);
});
expect(ctrl.stream, emitsInOrder([1, 3, 3, 2, 3, 3]));
bus.fire(FollowAppEvent('username'));
bus.fire(NewCommentEvent('comment #1'));
});
}

View file

@ -0,0 +1,56 @@
import 'package:event_bus_plus/event_bus_plus.dart';
import 'package:given_when_then_unit_test/given_when_then_unit_test.dart';
import 'package:test/test.dart';
import 'models.dart';
void main() {
late IEventBus bus;
before(() {
bus = EventBus();
});
test('description', () {
bus.inProgress$.map((List<AppEvent> events) =>
events.whereType<FollowAppEvent>().isNotEmpty);
}, skip: 'should skip');
test('emit Follower Event', () {
const id = 'id';
expect(bus.on(), emitsInAnyOrder([const FollowAppEvent('username')]));
bus.fire(const FollowAppEvent('username'));
});
test('emit Follower Events', () {
expect(
bus.on(),
emitsInOrder([
const FollowAppEvent('username'),
EmptyEvent(),
const FollowAppEvent('username3'),
EmptyEvent(),
const FollowAppEvent('username2'),
EmptyEvent(),
]));
bus.fire(const FollowAppEvent('username'));
bus.fire(const FollowAppEvent('username3'));
bus.fire(const FollowAppEvent('username2'));
});
test('emit New Comment Event', () {
expect(
bus.on<NewCommentEvent>(),
emitsInOrder([
const NewCommentEvent('comment #1'),
const NewCommentEvent('comment #2'),
]));
bus.fire(const FollowAppEvent('username3'));
bus.fire(const FollowAppEvent('username3'));
bus.fire(const NewCommentEvent('comment #1'));
bus.fire(const FollowAppEvent('username3'));
bus.fire(const NewCommentEvent('comment #2'));
bus.fire(const FollowAppEvent('username3'));
});
}

View file

@ -0,0 +1,36 @@
import 'package:clock/clock.dart';
import 'package:event_bus_plus/event_bus_plus.dart';
import 'package:given_when_then_unit_test/given_when_then_unit_test.dart';
import 'package:shouldly/shouldly.dart';
import 'package:test/test.dart';
import 'dart:developer' as dev;
import 'models.dart';
void main() {
late IEventBus bus;
before(() {
bus = EventBus(maxHistoryLength: 3);
});
test('right timestamp', () {
final date = DateTime(2022, 9, 1);
withClock(Clock.fixed(date), () {
bus.fire(const SomeEvent());
bus.fire(const SomeAnotherEvent());
bus.fire(const FollowAppEvent('@username'));
for (final e in bus.history) {
e.timestamp.should.be(date);
dev.log(e.toString());
}
bus.history.should.be([
EventBusHistoryEntry(const SomeEvent(), date),
EventBusHistoryEntry(const SomeAnotherEvent(), date),
EventBusHistoryEntry(const FollowAppEvent('@username'), date),
]);
});
});
}

13
core/eventbus/tool/coverage.sh Executable file
View file

@ -0,0 +1,13 @@
#!/bin/sh
cd ..
# Generate `coverage/lcov.info` file
flutter test --coverage
# Generate HTML report
# Note: on macOS you need to have lcov installed on your system (`brew install lcov`) to use this:
genhtml coverage/lcov.info -o coverage/html
# Open the report
open coverage/html/index.html