├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yaml │ └── publish.yaml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── analysis_options.yaml ├── build.yaml ├── dart_dependency_validator.yaml ├── documentation └── tracing.md ├── example ├── README.md ├── pubspec.yaml └── web │ ├── index.html │ ├── panel │ ├── index.html │ ├── modules │ │ ├── basic_module.dart │ │ ├── data_load_async_module.dart │ │ ├── data_load_blocking_module.dart │ │ ├── deferred_heavy_lifter_implementation.dart │ │ ├── deferred_heavy_lifter_interface.dart │ │ ├── deferred_module.dart │ │ ├── flux_module.dart │ │ ├── hierarchy_module.dart │ │ ├── lifecycle_echo_module.dart │ │ ├── panel_module.dart │ │ ├── reject_module.dart │ │ └── sample_tracer.dart │ ├── panel_app.css │ └── panel_app.dart │ └── random_color │ ├── index.html │ └── random_color.dart ├── lib ├── src │ ├── event.dart │ ├── events_collection.dart │ ├── lifecycle_module.dart │ ├── module.dart │ ├── simple_module.dart │ └── timing_specifiers.dart └── w_module.dart ├── pubspec.yaml ├── test ├── event_test.dart ├── events_collection_test.dart ├── lifecycle_module_test.dart ├── module_test.dart ├── simple_module_test.dart ├── test_tracer.dart └── utils.dart └── tool └── dart_dev └── config.dart /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Motivation 2 | 4 | 5 | ## Changes 6 | 7 | 8 | #### Release Notes 9 | 11 | 12 | ## Review 13 | _[See CONTRIBUTING.md][contributing-review-types] for more details on review types (+1 / QA +1 / +10) and code review process._ 14 | 15 | 27 | 28 | Please review: 29 | 30 | ### QA Checklist 31 | - [ ] Tests were updated and provide good coverage of the changeset and other affected code 32 | - [ ] Manual testing was performed if needed 33 | - [ ] Steps from PR author: 34 | 35 | - [ ] Anything falling under manual testing criteria [outlined in CONTRIBUTING.md][contributing-manual-testing] 36 | 37 | ## Merge Checklist 38 | While we perform many automated checks before auto-merging, some manual checks are needed: 39 | - [ ] A Frontend Architecture member has reviewed these changes 40 | - [ ] There are no unaddressed comments _- this check can be automated if reviewers use the "Request Changes" feature_ 41 | - [ ] _For release PRs -_ Version metadata in Rosie comment is correct 42 | 43 | 44 | [contributing-review-types]: https://github.com/Workiva/w_module/blob/master/CONTRIBUTING.md#review-types 45 | [contributing-manual-testing]: https://github.com/Workiva/w_module/blob/master/CONTRIBUTING.md#manual-testing-criteria 46 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | groups: 8 | gha: 9 | patterns: ["*"] 10 | - package-ecosystem: pub 11 | versioning-strategy: increase 12 | directory: / 13 | schedule: 14 | interval: weekly -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - 'master' 8 | 9 | jobs: 10 | checks: 11 | uses: Workiva/gha-dart-oss/.github/workflows/checks.yaml@v0.1.7 12 | build: 13 | uses: Workiva/gha-dart-oss/.github/workflows/build.yaml@v0.1.7 14 | test: 15 | uses: Workiva/gha-dart-oss/.github/workflows/test-unit.yaml@v0.1.7 -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+.[0-9]+.[0-9]+' 7 | 8 | permissions: 9 | contents: write 10 | id-token: write 11 | pull-requests: write 12 | 13 | jobs: 14 | publish: 15 | uses: Workiva/gha-dart-oss/.github/workflows/publish.yaml@v0.1.7 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | # dart 4 | .packages 5 | build 6 | packages 7 | pubspec.lock 8 | .dart_tool/ 9 | 10 | # docs 11 | /doc/api/ 12 | 13 | # coverage 14 | /coverage/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [3.0.6](https://github.com/Workiva/w_module/compare/3.0.5...3.0.6) 2 | 3 | _March 4, 2024_ 4 | 5 | - **Bug Fix:** A child module which loads during the unload of the parent 6 | may cause BadState exceptions. 7 | 8 | ## [3.0.0](https://github.com/Workiva/w_module/compare/2.0.5...3.0.0) 9 | 10 | _August 18, 2023_ 11 | 12 | - **Improvement:** Updated to null safety 13 | 14 | ## [2.0.5](https://github.com/Workiva/w_module/compare/2.0.4...2.0.5) 15 | 16 | _December 13, 2018_ 17 | 18 | - **Bug Fix:** Address some memory leak edge cases around child modules: 19 | - Clear the list of child modules when the parent module is disposed. 20 | - Use `manageDisposable()` to manage a child module as soon as it is added 21 | instead of manually disposing each child module during parent module 22 | disposal. 23 | 24 | ## [2.0.4](https://github.com/Workiva/w_module/compare/2.0.3...2.0.4) 25 | 26 | _November 27, 2018_ 27 | 28 | - **Improvement:** Dart 2 compatible! 29 | 30 | ## [2.0.3](https://github.com/Workiva/w_module/compare/2.0.0...2.0.3) 31 | 32 | _October 16, 2018_ 33 | 34 | - **Feature:** Added OpenTracing support to `Module`. 35 | 36 | See [the tracing documentation][tracing] for more info. 37 | 38 | ## [2.0.0](https://github.com/Workiva/w_module/compare/1.6.2...2.0.0) 39 | 40 | _Sep 13, 2018_ 41 | 42 | [tracing]: https://github.com/Workiva/w_module/blob/master/documentation/tracing.md 43 | 44 | - **BREAKING CHANGE:** Remove the `package:w_module/serializable_module.dart` 45 | entry point, as it depended on `dart:mirrors` which is no longer supported in 46 | the browser in Dart 2. 47 | 48 | Consequently, the following API members have been removed: 49 | 50 | - `Bridge` 51 | - `Reflectable` 52 | - `SerializableBus` 53 | - `SerializableEvent` 54 | - `SerializableEvents` 55 | - `SerializableModule` 56 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to w_module 2 | 3 | - [__Support, Opening Issues__](#support-opening-issues) 4 | - [__Contributing Changes__](#contributing-changes) 5 | - [Coding Standards](#coding-standards) 6 | - [Git Commit Message Standards](#git-commit-message-standards) 7 | - [__Code Review Process and Merging Requirements__](#code-review-process-and-merging-requirements) 8 | - [Minimum Required Review](#minimum-required-review) 9 | - [Review Types](#review-types) 10 | - [Manual Testing Criteria](#manual-testing-criteria) 11 | 12 | --- 13 | 14 | ## Support, Opening Issues 15 | Have a bug to report or an improvement/feature to request? Please 16 | [open an issue](https://github.com/Workiva/w_module/issues/new) and fill out 17 | the issue template with as much detail as necessary. 18 | 19 | ###### Workiva Employees 20 | > __Contact us on Slack:__ [\#support-frontend-architecture](https://workiva.slack.com/app_redirect?channel=support-frontend-architecture) 21 | 22 | Have a bug to report or an improvement/feature to request? 23 | Please contact us on Slack or [create a JIRA ticket](https://jira.atl.workiva.net/secure/CreateIssue!default.jspa?pid=CPLAT&component=w_module) 24 | and fill out the description with as much detail as necessary. 25 | 26 | ## Contributing Changes 27 | If you're contributing a change to w_module, please follow this process: (and 28 | thank you!) 29 | 30 | 1. Before you start working on a larger contribution (e.g. implementing features, 31 | refactoring code, etc.), you should get in touch with us first so that 32 | we can help out and possibly guide you. 33 | 34 | Coordinating up front makes it much easier to avoid frustration later on. 35 | 36 | 1. Commit your changes in logical chunks. Please adhere to these 37 | [coding standards](#coding-standards) and 38 | [Git commit message guidelines](#git-commit-message-standards). 39 | 40 | 1. Write tests for your changes. 41 | - There are very few exceptions. 42 | - If you're having trouble, please reach out for testing advice. We'll be 43 | happy to help! 44 | 1. Open a PR against the `master` branch and fill out the template with as much 45 | detail as necessary. 46 | 1. See instructions in PR template for getting review on your changes. 47 | 48 | 49 | ### Coding Standards 50 | A lot can be gained by writing code in a consistent way. Moreover, always 51 | remember that code is written and maintained by _people_. Ensure your code is 52 | descriptive, well commented, and approachable by others. 53 | 54 | - Dart 55 | - Adhere to the official [Dart Style Guide][dart-style-guide]. _Please take the time to read it if you have never done so._ 56 | - Format your code using `pub run dart_dev format` (this is enforced by a CI check) 57 | 58 | 59 | ### Git Commit Message Standards 60 | 61 | Read [this short post][git-commit-messages] for commit message guidelines (and more information on why standardized commit message format is important). 62 | 63 | [dart-style-guide]: https://www.dartlang.org/articles/style-guide/ 64 | [git-commit-messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 65 | 66 | ## Code Review Process and Merging Requirements 67 | For a PR to merge, it must: 68 | 1. Receive the minimum required review 69 | - High-risk PRs (e.g., large number of lines changed, touching areas at high risk of regression, complex changes) 70 | - __Two__ +1s 71 | - QA +1 by someone other than the commit author 72 | - All other PRs (including trivial changes) 73 | - +1 74 | - QA +1 by someone other than the commit author 75 | 76 | ___NOTE: It does not matter whether the "+1" and "QA +1" come from the same reviewer (e.g., via a "+10").___ 77 | 2. Pass the merge checklist outlined in the PR template 78 | 79 | 80 | ### Review Types 81 | 82 | - __+1__ 83 | - Reviewing code for correctness, robustness, maintainability, performance, etc. 84 | - Reviewing tests for coverage, thoroughness, and clarity 85 | - _This responsibility is shared with QA reviewers_ 86 | - __QA +1__ 87 | - Verifying that adequate tests have been has been added, and reviewing them 88 | for coverage, thoroughness, and clarity 89 | - _This responsibility is shared with code reviewers_ 90 | - Verifying automated tests pass in CI (this is often done automatically by Rosie). 91 | - Performing manual testing instructions, if they exist. (see criteria in the next section) 92 | - __Must be done by someone other than the commit author.__ 93 | - __+10__ 94 | - Exactly equivalent to performing both a "+1" and a "QA +1", just combined into one step. 95 | 96 | ## Manual Testing Criteria 97 | Manual testing instructions (done as part of a "QA +1") should be included for the following scenarios. 98 | 99 | __If none of these apply, then manual testing instructions may be omitted.__ 100 | 101 | - When manual testing can help uncover areas of missed test coverage (in terms of certain scenarios and states, not just lines covered). 102 | 103 | _Examples:_ 104 | - Running CLIs on several different projects to help catch edge-cases. 105 | - Hammering on UI elements to ensure they react gracefully to different sequences of user input. 106 | 107 | - When tests run in CI do not fully exercise the desired behavior. 108 | 109 | _Examples:_ 110 | - Changes were made in areas of the code that shouldn't be tested, like examples. 111 | - Changes were made in areas of the code that are very difficult to fully test, and are more efficient to test manually. 112 | - Changes need to be consumed in a certain library, harness, or deploy to be fully tested. 113 | 114 | - When CI tests do not fully protect against regressions to existing behavior. 115 | 116 | _Examples:_ 117 | - Test coverage around code impacted by changes is sparse. 118 | - Changes need to be integrated with another library to ensure nothing broke. 119 | 120 | - When changes are made to CI itself, and need to be manually verified. 121 | 122 | _Examples:_ 123 | - Test runner was updated. 124 | - New test suites were added. 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Workiva Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | w_module 2 | Copyright 2017 Workiva Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # w_module 2 | [![Pub](https://img.shields.io/pub/v/w_module.svg)](https://pub.dartlang.org/packages/w_module) 3 | [![Build Status](https://travis-ci.org/Workiva/w_module.svg?branch=master)](https://travis-ci.org/Workiva/w_module) 4 | [![codecov.io](http://codecov.io/github/Workiva/w_module/coverage.svg?branch=master)](http://codecov.io/github/Workiva/w_module?branch=master) 5 | [![documentation](https://img.shields.io/badge/Documentation-w_module-blue.svg)](https://www.dartdocs.org/documentation/w_module/latest/) 6 | 7 | > Base module classes with a well defined lifecycle for modular Dart applications. 8 | 9 | - [**Overview**](#overview) 10 | - [**Module Structure**](#module-structure) 11 | - [**API**](#api) 12 | - [**Events**](#events) 13 | - [**Components**](#components) 14 | - [**Module Lifecycle**](#module-lifecycle) 15 | - [**Lifecycle Methods**](#lifecycle-methods) 16 | - [**Lifecycle Events**](#lifecycle-events) 17 | - [**Lifecycle Customization**](#lifecycle-customization) 18 | - [**Module Hierarchies**](#module-hierarchies) 19 | - [**Disposal**](#disposal) 20 | - [**Examples**](#examples) 21 | - [**Development**](#development) 22 | 23 | 24 | --- 25 | 26 | ## Overview 27 | 28 | ![w_module Boundaries Diagram](https://raw.githubusercontent.com/Workiva/w_module/images/images/w_module_boundaries_diagram.png) 29 | 30 | `w_module` implements a module encapsulation and lifecycle pattern for Dart that interfaces well with the application 31 | architecture defined in the [w_flux](https://github.com/Workiva/w_flux) library. `w_module` defines the public interface 32 | for a module and is in no way prescriptive as to how module internals are defined, though the `w_flux` pattern is 33 | recommended. `w_module` defines how data should flow in and out of a module, how renderable UI is exposed to consumers, 34 | and establishes a common module lifecycle that facilitates dynamic loading / unloading of complex module hierarchies. 35 | 36 | 37 | --- 38 | 39 | ## Module Structure 40 | 41 | A `w_module` `Module` encapsulates a well-scoped logical unit of functionality and exposes a discrete public interface for 42 | consumers. It extends `LifecycleModule` to ensure that its load / unload processes adhere to a well-defined lifecycle. 43 | The public interface of a `Module` is comprised of `api`, `events`, and `components`: 44 | - The `api` class exposes public methods that can be used to mutate or query module data. 45 | - The `events` class exposes streams that can be listened to for notification of internal module state change. 46 | - The `components` class exposes react-dart compatible UI components that can be used to render module data. 47 | 48 | Though the class based `Module` convention is somewhat arbitrary, exposing `api`, `events`, and `components` via 49 | aggregate classes simplifies consumption and improves the discoverability of the `Module`'s public interface. 50 | 51 | ```dart 52 | // bare bones module definition 53 | 54 | DispatchKey sampleDispatchKey = DispatchKey('sampleModule'); 55 | 56 | class SampleModule extends Module { 57 | 58 | final String name = 'SampleModule'; 59 | 60 | SampleApi _api; 61 | SampleApi get api => _api; 62 | 63 | SampleEvents _events; 64 | SampleEvents get events => _events; 65 | 66 | SampleComponents _components; 67 | SampleComponents get components => _components; 68 | 69 | SampleModule() { 70 | _api = SampleApi(); 71 | _events = SampleEvents(sampleDispatchKey); 72 | _components = SampleComponents(); 73 | } 74 | } 75 | ``` 76 | 77 | If using `w_module` with `w_flux` internals, `api`, `events`, and `components` should be internally initialized with 78 | access to the module's `actions` and `stores`. 79 | 80 | ```dart 81 | // module definition with w_flux internals 82 | 83 | DispatchKey sampleDispatchKey = DispatchKey('sampleModule'); 84 | 85 | class SampleModule extends Module { 86 | 87 | final String name = 'SampleModule'; 88 | 89 | SampleActions _actions; 90 | SampleStore _store; 91 | 92 | SampleApi _api; 93 | SampleApi get api => _api; 94 | 95 | SampleEvents _events; 96 | SampleEvents get events => _events; 97 | 98 | SampleComponents _components; 99 | SampleComponents get components => _components; 100 | 101 | SampleModule() { 102 | _actions = SampleActions(); 103 | _events = SampleEvents(); 104 | _store = SampleStore(_actions, _events, sampleDispatchKey); 105 | _components = SampleComponents(_actions, _store); 106 | _api = SampleApi(_actions, _store); 107 | } 108 | } 109 | ``` 110 | 111 | 112 | ### API 113 | 114 | A `Module`'s `api` member should expose all public methods that a consumer can use to mutate module state (methods) or 115 | query existing module state (getters). `api` is initially null. If a module exposes a public `api`, this should be 116 | overridden to provide a class defined specifically for the module. 117 | 118 | ```dart 119 | // module api definition 120 | 121 | class SampleApi { 122 | 123 | SampleApi(); 124 | 125 | setSampleValue(String newValue) { 126 | ... 127 | } 128 | 129 | String get sampleValue => ...; 130 | } 131 | ``` 132 | 133 | ```dart 134 | // module api consumption 135 | 136 | sampleModule.api.setSampleValue(...); 137 | 138 | String sampleValue = sampleModule.api.sampleValue; 139 | ``` 140 | 141 | If using `w_module` with `w_flux` internals, module mutation methods should usually dispatch existing `actions` 142 | available within the module. This ensures that the internal unidirectional data flow is maintained, regardless of 143 | the source of the mutation (e.g. external api or internal UI). Likewise, module methods that expose internal state 144 | should usually use existing getter methods available on stores within the module. 145 | 146 | ```dart 147 | // module api definition with w_flux internals 148 | 149 | class SampleApi { 150 | 151 | SampleActions _actions; 152 | SampleStore _store; 153 | 154 | SampleApi(this._actions, this._store); 155 | 156 | setSampleValue(String newValue) { 157 | _actions.setSampleValue(newValue); 158 | } 159 | 160 | String get sampleValue => _store.sampleValue; 161 | } 162 | ``` 163 | 164 | 165 | ### Events 166 | 167 | A `Module`'s `events` member should expose all public streams that a consumer can listen to for notification of 168 | internal state changes. `events` is initially null. If a module exposes public `events`, this should be overridden 169 | to provide a class defined specifically for the module. 170 | 171 | A `Module`'s `events` are intended to be 'read-only'. Though `events` are exposed for listening by external consumers, 172 | they should only be dispatched from within the `Module`. To enforce this limitation, a dispatch key is required to 173 | instantiate the event stream. The same dispatch key must subsequently be used to dispatch all events on the stream. 174 | Keeping the dispatch key private in the `Module` internals effectively prevents uncontrolled external dispatch. 175 | 176 | ```dart 177 | // module events definition 178 | 179 | DispatchKey sampleDispatchKey = DispatchKey('sampleModule'); 180 | 181 | class SampleEvents { 182 | final Event valueChanged = Event(sampleDispatchKey); 183 | } 184 | ``` 185 | 186 | ```dart 187 | // module events dispatch 188 | 189 | _events.valueChanged(_sampleValue, sampleDispatchKey); 190 | ``` 191 | 192 | ```dart 193 | // module events consumption 194 | 195 | sampleModule.events.valueChanged.listen((newValue) { 196 | ... 197 | }); 198 | ``` 199 | 200 | In order to automatically clean up subscriptions if the module is disposed, you can make use of `listenToStream` instead of the built-in `listen`. See [the section on disposal](#disposal) for more details. 201 | 202 | 203 | ```dart 204 | 205 | // module events consumption 206 | listenToStream(sampleModule.events.valueChanged, (newValue) { 207 | ... 208 | }); 209 | ``` 210 | 211 | If using `w_module` with `w_flux` internals, `events` should usually be dispatched by internal stores immediately 212 | prior to a corresponding trigger dispatch. `events` should NOT be dispatched directly by UI components or in 213 | immediate response to actions. This ensures that the internal unidirectional data flow is maintained and external 214 | `events` represent confirmed internal state changes. 215 | 216 | ```dart 217 | // module events dispatch with w_flux internals 218 | 219 | class SampleStore extends Store { 220 | 221 | String _sampleValue = 'something'; 222 | 223 | RandomColorEvents _events; 224 | DispatchKey _dispatchKey; 225 | 226 | SampleActions _actions; 227 | 228 | SampleStore(SampleActions this._actions, SampleEvents this._events, DispatchKey this._dispatchKey) { 229 | ... 230 | _actions.setSampleValue.listen(_setSampleValue); 231 | } 232 | 233 | _setSampleValue(String newValue) { 234 | _sampleValue = newValue; 235 | _events.valueChanged(_sampleValue, _dispatchKey); 236 | trigger(); 237 | } 238 | } 239 | ``` 240 | 241 | To assist in properly disposing of `Event` instances, `w_module` provides an 242 | `EventsCollection` base class that extends 243 | [`Disposable` from the `w_common` package](https://github.com/Workiva/w_common) 244 | with an additional `manageEvent()` method. Colocating related events in an 245 | `EventsCollection` makes it trivial to close all `Event` instances by disposing 246 | the `EventsCollection` instance. 247 | 248 | ```dart 249 | final key = DispatchKey('example'); 250 | 251 | class ExampleEvents extends EventsCollection { 252 | final Event eventA = Event(key); 253 | final Event eventB = Event(key); 254 | 255 | ExampleEvents() : super(key) { 256 | [ 257 | eventA, 258 | eventB, 259 | ].forEach(manageEvent); 260 | } 261 | } 262 | 263 | main() async { 264 | final eventsCollection = EventsCollection(); 265 | await eventsCollection.dispose(); 266 | // All Events on the collection should now be closed. 267 | } 268 | ``` 269 | 270 | ### Components 271 | 272 | A `Module`'s `components` member should expose all react-dart compatible UI component factories that a consumer can 273 | use to render module data. `components` is initially null. If a module exposes public `components`, this should be 274 | overridden to provide a class defined specifically for the module. 275 | 276 | By convention, the custom `components` class should extend the included `ModuleComponents` class to ensure that the 277 | default UI component is available via the `module.components.content()` method. 278 | 279 | ```dart 280 | // module components definition 281 | 282 | class SampleComponents implements ModuleComponents { 283 | 284 | content() => SampleComponent(...); 285 | } 286 | ``` 287 | 288 | ```dart 289 | // module components consumption 290 | 291 | react.render(sampleModule.components.content(), 292 | html.querySelector('#content-container')); 293 | ``` 294 | 295 | If using `w_module` with `w_flux` internals, `components` methods should usually return UI component factories that 296 | have been internally initialized with the proper actions and stores props. This ensures full functionality of the 297 | `components` without any external exposure of the requisite internal actions and stores. 298 | 299 | ```dart 300 | // module components definition with w_flux internals 301 | 302 | class SampleComponents implements ModuleComponents { 303 | 304 | SampleActions _actions; 305 | SampleStore _store; 306 | 307 | SampleComponents(this._actions, this._store); 308 | 309 | content() => SampleComponent({'actions': _actions, 'store': _store}); 310 | } 311 | ``` 312 | 313 | 314 | --- 315 | 316 | ## Module Lifecycle 317 | 318 | `w_module` implements a simple `Module` lifecycle that ensures that modules adhere to a predictable loading and 319 | unloading pattern. Using `Module` as the basis for all modules in an application ensures that this simple pattern 320 | will extrapolate predictably across complex module hierarchies. 321 | 322 | Many examples of `Module` lifecycle behavior and manipulation can be found in the 323 | [Multiple Module Panel](https://github.com/Workiva/w_module/tree/master/example/panel) example. 324 | 325 | ### Lifecycle Methods 326 | 327 | `Module` exposes five lifecycle methods that external consumers should use to trigger loading and unloading 328 | behavior. These methods can return an error or exception thrown by their respective lifecycle handler methods. 329 | This allows dependencies on a `Module` to respond to failures that occur within the overridden lifecycle 330 | behavior. 331 | 332 | Method | Description 333 | -------------- | --------------------------------- 334 | `load` | Triggers loading of a `Module`. Internally, this executes the module's `onLoad` method and dispatches the `willLoad` and `didLoad` events. Returns a future that completes once the module has finished loading. 335 | `suspend` | Suspends the module and all child modules. While suspended modules should make themselves lightweight and avoid making network requests. 336 | `resume` | Brings the module and all child modules out of suspension. Upon resuming, modules should go back to business as usual. 337 | `shouldUnload` | Returns the unloadable state of the `Module` and its child modules as a `ShouldUnloadResult`. Internally, this executes the module's `onShouldUnload` method. 338 | `unload` | Triggers unloading of a `Module` and all of its child modules. Internally, this executes the module's `shouldUnload` method, and, if that completes successfully, executes the module's `onUnload` method. If unloading is rejected, this method will complete with an error. The rejected error will not be added to the `didUnload` lifecycle event stream. 339 | 340 | ![Lifecycle of a LifecycleModule](https://raw.githubusercontent.com/Workiva/w_module/images/images/LifecycleModule_lifecycle_diagram.png) 341 | 342 | The graphic above illustrates legal lifecycle state transitions. Any state 343 | transition that is not defined will throw a `StateError`. No-op transitions are 344 | illustrated with blue arrows. Calling the lifecycle method when the module is 345 | already in the given state will result in a logger warning. If a no-op is 346 | performed on a transitioning state the pending transition future is returned. 347 | 348 | ### Lifecycle Events 349 | 350 | `Module` also exposes lifecycle event streams that an external consumer can listen to. If any corresponding lifecycle handler method throws an exception or error it will be added to the corresponding stream. This allows dependencies on a `Module` to provide an `onError` function to handle recovering from a failure within the module implementation. 351 | 352 | Event | Description 353 | -------------- | --------------------------------- 354 | `willLoad` | Dispatched at the beginning of the module's `load` logic. 355 | `didLoad` | Dispatched at the end of the module's `load` logic. 356 | `willSuspend` | Dispatched at the beginning of the module's `suspend` logic. 357 | `didSuspend` | Dispatched at the end of the module's `suspend` logic. 358 | `willResume` | Dispatched at the beginning of the module's `resume` logic. 359 | `didResume` | Dispatched at the end of the module's `resume` logic. 360 | `willUnload` | Dispatched at the beginning of the module's `unload` logic. 361 | `didUnload` | Dispatched at the end of the module's `unload` logic. 362 | 363 | ### Lifecycle Customization 364 | 365 | Internally, `Module` contains methods that can be overridden to customize module lifecycle behavior. Any error or exception thrown by these overloaded lifecycle methods will be added to their corresponding lifecycle event stream. This error will be returned to the caller of the corresponding lifecycle method. 366 | 367 | Method | Description 368 | ---------------- | --------------------------------- 369 | `onLoad` | Executing during the module's `load` logic. Custom logic for initializing child modules, service access, event listeners, etc. should be implemented here. Deferred module loading behavior can also be hidden from consumers via this method. 370 | `onSuspend` | Executed during the module's `suspend` logic. This method should be used to modify module behavior while suspended. 371 | `onResume` | Executed during the module's `resume` logic. This method should be used to put the module back into its normal mode of operation. 372 | `onShouldUnload` | Executed during the module's `shouldUnload` logic. Custom logic that blocks module unloading under certain conditions should be implemented here. 373 | `onUnload` | Executed during the module's `unload` logic. Custom module clean up logic should be implemented here. Unfortunately, the nature of web browsers is such that module `unload` logic is not guaranteed to be executed under all conditions (browser or tab close), so mission critical logic should not reside here. 374 | 375 | ### Module Hierarchies 376 | 377 | `Module` also supports hierarchical application of the standard lifecycle through child modules: 378 | 379 | Method | Description 380 | -------------------- | --------------------------------- 381 | `loadChildModule` | Loads a child module and registers it with the current module for lifecycle management. 382 | `willLoadChildModule` | Dispatched at the beginning of the child module's `load` logic. 383 | `didLoadChildModule` | Dispatched at the end of the child module's `load` logic. 384 | 385 | ### Getters 386 | 387 | Getter | Description 388 | -------------------- | --------------------------------- 389 | `childModules` | An iterable of child modules. 390 | `isLoaded` | A boolean that indicates whether the module is current loaded. 391 | `isSuspended` | A boolean that indicates whether the module is currently suspended. This will always be false when the module is not loaded. 392 | 393 | ### Disposal 394 | 395 | `Module` extends [`Disposable` from the `w_common` package](https://github.com/Workiva/w_common) 396 | which provides some additional facilities for memory management. This means that 397 | you can leverage any of the disposable management APIs like `listenToStream()` 398 | or `manageDisposable()` within your `Module`. Additionally, it means that 399 | `Module`s can be disposed via `dispose()`. While this is similar to `unload()`, 400 | there is a key difference: 401 | 402 | **`unload()`** will attempt to unload a `Module`, but may fail to complete in 403 | two scenarios: 404 | - if the unload is canceled via `shouldUnload()` 405 | - if the unload fails due to an uncaught exception in `onUnload()` 406 | 407 | If the unload succeeds, then disposal will happen implicitly. 408 | 409 | **`dispose()`** will attempt to unload the `Module` first (unless it has never 410 | been loaded, in which case it will go straight to disposal), but will force 411 | disposal regardless of the outcome of the unload step. 412 | 413 | In most scenarios, consumers should use `unload()` either directly or indirectly 414 | via the other lifecycle management APIs like `loadChildModule()`. 415 | 416 | Calling `dispose()` should be reserved for scenarios where you know the `Module` 417 | has not been loaded, but still needs to be disposed, or when you need to force 418 | the disposal of the `Module` regardless of its state and do not care about the 419 | `Module`s ability to prevent its unloading. Unit tests are a likely candidate 420 | for this usage. 421 | 422 | ![Disposal of a LifecycleModule](https://raw.githubusercontent.com/Workiva/w_module/images/images/LifecycleModule_disposal_diagram.png) 423 | 424 | --- 425 | 426 | ## Examples 427 | 428 | Simple examples of `w_module` usage can be found in the `example` directory. The example [README](example/README.md) 429 | includes instructions for building / running them. 430 | 431 | 432 | --- 433 | 434 | ## Development 435 | 436 | This project leverages [the dart_dev package](https://pub.dartlang.org/packages/dart_dev) 437 | for most of its tooling needs, including static analysis, code formatting, 438 | running tests, collecting coverage, and serving examples. Check out 439 | [the dart_dev readme](https://github.com/Workiva/dart_dev) for more information. 440 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:workiva_analysis_options/v2.yaml 2 | analyzer: 3 | exclude: 4 | - example/** 5 | -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | -------------------------------------------------------------------------------- /dart_dependency_validator.yaml: -------------------------------------------------------------------------------- 1 | exclude: 2 | - "example/**" 3 | -------------------------------------------------------------------------------- /documentation/tracing.md: -------------------------------------------------------------------------------- 1 | # Tracing with w_module 2 | 3 | This library supports [OpenTracing][opentracingio] using [opentracing_dart][opentracingdart]. Your application will need to provide a `Tracer` and initialize it with `initGlobalTracer` to opt in to this feature. 4 | 5 | 6 | To get the traces provided by this library, your module must provide a definition for the `name` getter. This can simply be the name of the class. For example: 7 | 8 | ```dart 9 | class SomeModule extends Module { 10 | @override 11 | final String name = 'SomeModule'; 12 | 13 | // ... the of the module's implementation 14 | } 15 | ``` 16 | 17 | Spans will be in the form of: 18 | 19 | ``` 20 | $name.$operationName 21 | ``` 22 | 23 | ## Types Of Provided Traces 24 | 25 | ### Tracing Lifecycle Methods 26 | 27 | We automically trace each of its lifecycle methods: 28 | 29 | - Load 30 | - Unload 31 | - Suspend 32 | - Resume 33 | 34 | In addition, any spans created by child modules (loaded with `loadChildModule`) will have a `followsFrom` reference to the parent's span of the respective method to complete the story of the trace. 35 | 36 | If you wish to create other `childOf` or `followsFrom` spans on your module's lifecycle spans, you can simply request the `activeSpan`: 37 | 38 | ```dart 39 | @override 40 | Future onLoad() { 41 | // ... some loading logic 42 | 43 | final span = globalTracer().startSpan( 44 | operationName, 45 | childOf: activeSpan.context, // see this line 46 | ); 47 | 48 | // ... more loading logic 49 | span.finish() 50 | } 51 | ``` 52 | 53 | Note that `activeSpan` will be null at times when the module is not in the middle of a lifecycle transition. 54 | 55 | ### Additional Load Time Granularity 56 | 57 | Sometimes, lifecycle methods such as `load` will complete before the module is semantically "loaded". For example, you may begin asynchronously fetching data for your module and then return from `onLoad` to keep from blocking the main thread. 58 | 59 | In cases like these, use `specifyStartupTiming`: 60 | 61 | ```dart 62 | Future onLoad() { 63 | // ... kick off async loadData logic 64 | 65 | listenToStream(_events.didLoadData.take(1), 66 | (_) => specifyStartupTiming(StartupTimingType.firstUseful)); 67 | } 68 | ``` 69 | 70 | This will create a span starting from the same time as `load()` and ending at the moment the method was called. This library will handle the `operationName` and the `followsFrom` reference to the module's `load` span, but tags and references can be passed into this method just like with any other span in optional parameters. 71 | 72 | [opentracingio]: https://opentracing.io/ 73 | [opentracingdart]: https://github.com/Workiva/opentracing_dart/ -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | w_module Examples 2 | ================== 3 | 4 | To run the examples, open the root of this repository in a command-line terminal. 5 | 6 | 1. `pub run dart_dev examples` 7 | 2. Open Chromium/Dartium to `http://localhost:8080` -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: w_module_example 2 | version: 2.1.2 3 | description: Base module classes with a well defined lifecycle for modular Dart applications. 4 | homepage: https://github.com/Workiva/w_module 5 | publish_to: none 6 | 7 | environment: 8 | sdk: ">=2.11.0 <3.0.0" 9 | 10 | dev_dependencies: 11 | build_runner: ^2.1.2 12 | build_web_compilers: '>=3.0.0 <5.0.0' 13 | dependency_validator: ^3.2.2 14 | meta: ^1.16.0 15 | opentracing: ^1.0.1 16 | over_react: ^5.0.0 17 | platform_detect: '>=1.4.2 <3.0.0' 18 | react: ^7.0.0 19 | w_common: ^3.0.0 20 | w_flux: ^3.0.0 21 | w_module: 22 | 23 | dependency_overrides: 24 | w_module: 25 | path: ../ -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | Examples | w_module 22 | 23 | 24 |
25 |

w_module Examples

26 | 30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /example/web/panel/index.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | Panel Example | w_module 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /example/web/panel/modules/basic_module.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.example.panel.modules.basic_module; 16 | 17 | import 'package:react/react.dart' as react; 18 | import 'package:w_module/w_module.dart'; 19 | 20 | class BasicModule extends Module { 21 | @override 22 | final String name = 'BasicModule'; 23 | 24 | BasicModuleComponents _components; 25 | 26 | BasicModule() { 27 | _components = BasicModuleComponents(); 28 | } 29 | 30 | @override 31 | BasicModuleComponents get components => _components; 32 | } 33 | 34 | class BasicModuleComponents implements ModuleComponents { 35 | @override 36 | Object content() => react.div({ 37 | 'style': { 38 | 'padding': '50px', 39 | 'backgroundColor': 'lightgray', 40 | 'color': 'black' 41 | } 42 | }, 'This module does almost nothing.'); 43 | } 44 | -------------------------------------------------------------------------------- /example/web/panel/modules/data_load_async_module.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.example.panel.modules.data_load_async_module; 16 | 17 | import 'dart:async'; 18 | 19 | import 'package:meta/meta.dart' show protected; 20 | import 'package:react/react.dart' as react; 21 | import 'package:w_common/disposable.dart'; 22 | import 'package:w_flux/w_flux.dart'; 23 | import 'package:w_module/w_module.dart'; 24 | 25 | class DataLoadAsyncModule extends Module { 26 | @override 27 | final String name = 'DataLoadAsyncModule'; 28 | 29 | DataLoadAsyncActions _actions; 30 | DataLoadAsyncComponents _components; 31 | DataLoadAsyncEvents _events; 32 | DataLoadAsyncStore _store; 33 | 34 | DataLoadAsyncModule() { 35 | _actions = DataLoadAsyncActions(); 36 | _events = DataLoadAsyncEvents(); 37 | _store = DataLoadAsyncStore(_actions, _events); 38 | _components = DataLoadAsyncComponents(_actions, _store); 39 | 40 | [_events, _store].forEach(manageDisposable); 41 | } 42 | 43 | @override 44 | DataLoadAsyncComponents get components => _components; 45 | 46 | @override 47 | @protected 48 | Future onLoad() { 49 | // trigger non-blocking async load of data 50 | listenToStream(_events.didLoadData.take(1), 51 | (_) => specifyStartupTiming(StartupTimingType.firstUseful)); 52 | _actions.loadData(null); 53 | return Future.value(); 54 | } 55 | } 56 | 57 | class DataLoadAsyncComponents implements ModuleComponents { 58 | DataLoadAsyncActions _actions; 59 | DataLoadAsyncStore _stores; 60 | 61 | DataLoadAsyncComponents(this._actions, this._stores); 62 | 63 | @override 64 | Object content() => 65 | DataLoadAsyncComponent({'actions': _actions, 'store': _stores}); 66 | } 67 | 68 | class DataLoadAsyncActions { 69 | final ActionV2 loadData = ActionV2(); 70 | } 71 | 72 | DispatchKey _dispatchKey = DispatchKey('DataLoadAsync'); 73 | 74 | class DataLoadAsyncEvents extends EventsCollection { 75 | final Event didLoadData = Event(_dispatchKey); 76 | DataLoadAsyncEvents() : super(_dispatchKey) { 77 | manageEvent(Event(_dispatchKey)); 78 | } 79 | } 80 | 81 | class DataLoadAsyncStore extends Store { 82 | DataLoadAsyncActions _actions; 83 | DataLoadAsyncEvents _events; 84 | List _data = []; 85 | bool _isLoading = false; 86 | 87 | DataLoadAsyncStore(this._actions, this._events) { 88 | manageActionSubscription(_actions.loadData.listen(_loadData)); 89 | } 90 | 91 | /// Public data 92 | List get data => _data; 93 | 94 | bool get isLoading => _isLoading; 95 | 96 | Future _loadData(_) async { 97 | // set loading state and trigger to display loading spinner 98 | _isLoading = true; 99 | trigger(); 100 | 101 | // start async load of data (fake it with a Future) 102 | await Future.delayed(Duration(seconds: 1)); 103 | 104 | // trigger on return of final data 105 | _data = ['Aaron', 'Dustin', 'Evan', 'Jay', 'Max', 'Trent']; 106 | _isLoading = false; 107 | _events.didLoadData(null, _dispatchKey); 108 | trigger(); 109 | } 110 | } 111 | 112 | // ignore: non_constant_identifier_names 113 | var DataLoadAsyncComponent = 114 | react.registerComponent(() => _DataLoadAsyncComponent()); 115 | 116 | class _DataLoadAsyncComponent 117 | extends FluxComponent { 118 | @override 119 | Object render() { 120 | var content; 121 | if (store.isLoading) { 122 | content = react.div({'className': 'loader'}, 'Loading data...'); 123 | } else { 124 | int keyCounter = 0; 125 | content = react.ul({'className': 'list-group'}, 126 | store.data.map((item) => react.li({'key': keyCounter++}, item))); 127 | } 128 | 129 | return react.div({ 130 | 'style': { 131 | 'padding': '50px', 132 | 'backgroundColor': 'orange', 133 | 'color': 'white' 134 | } 135 | }, [ 136 | 'This module renders a loading spinner until data is ready for display.', 137 | content 138 | ]); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /example/web/panel/modules/data_load_blocking_module.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.example.panel.modules.data_load_blocking_module; 16 | 17 | import 'dart:async'; 18 | 19 | import 'package:meta/meta.dart' show protected; 20 | import 'package:react/react.dart' as react; 21 | import 'package:w_module/w_module.dart'; 22 | 23 | class DataLoadBlockingModule extends Module { 24 | List data; 25 | 26 | @override 27 | final String name = 'DataLoadBlockingModule'; 28 | 29 | DataLoadBlockingComponents _components; 30 | 31 | DataLoadBlockingModule() { 32 | data = []; 33 | _components = DataLoadBlockingComponents(this); 34 | } 35 | 36 | @override 37 | DataLoadBlockingComponents get components => _components; 38 | 39 | @override 40 | @protected 41 | Future onLoad() async { 42 | // perform async load of data (fake it with a Future) 43 | await Future.delayed(Duration(seconds: 1)); 44 | data = ['Grover', 'Hoffman', 'Lessard', 'Peterson', 'Udey', 'Weible']; 45 | } 46 | } 47 | 48 | class DataLoadBlockingComponents implements ModuleComponents { 49 | DataLoadBlockingModule _module; 50 | DataLoadBlockingComponents(this._module); 51 | 52 | @override 53 | Object content() { 54 | int keyCounter = 0; 55 | return react.div({ 56 | 'style': {'padding': '50px', 'backgroundColor': 'red', 'color': 'white'} 57 | }, [ 58 | 'This module blocks the module loading lifecycle until the data is ready to render.', 59 | react.ul({'className': 'list-group'}, 60 | _module.data.map((item) => react.li({'key': keyCounter++}, item))) 61 | ]); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /example/web/panel/modules/deferred_heavy_lifter_implementation.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.example.panel.modules.deferred_heavy_lifter_implementation; 16 | 17 | import './deferred_heavy_lifter_interface.dart'; 18 | 19 | class RealLifter implements HeavyLifter { 20 | HeavyLifterDivision _division; 21 | 22 | RealLifter(this._division); 23 | 24 | @override 25 | List get competitors { 26 | if (_division == HeavyLifterDivision.featherweight) { 27 | return [ 28 | 'SpongeBob SquarePants', 29 | 'Patrick Star', 30 | 'Gary Wilson Jr the Snail', 31 | 'Sandy Cheeks', 32 | 'Squidward Tentacles', 33 | 'Eugene Krabs', 34 | 'Sheldon Plankton', 35 | 'Karen', 36 | 'Mrs. Puff', 37 | 'Pearl Krabs', 38 | 'Mermaid Man', 39 | 'Barnacle Boy', 40 | 'Larry the Lobster', 41 | 'The Flying Dutchman', 42 | 'Patchy the Pirate', 43 | 'Potty the Parrot', 44 | 'Officer Nancy', 45 | 'Purple Doctorfish', 46 | 'Elaine', 47 | 'Perch Perkins', 48 | 'Harold SquarePants', 49 | 'Squilliam Fancyson', 50 | 'Mrs. Betsy Krabs', 51 | 'Man Ray', 52 | 'Old Man Jenkins', 53 | 'King Neptune', 54 | 'Bubble Buddy', 55 | 'DoodleBob' 56 | ]; 57 | } else if (_division == HeavyLifterDivision.welterweight) { 58 | return [ 59 | 'Philip J. Fry', 60 | 'Turanga Leela', 61 | 'Bender Bending Rodriguez', 62 | 'Amy Wong', 63 | 'Hermes Conrad', 64 | 'Professor Hubert J. Farnsworth', 65 | 'Doctor John Zoidberg', 66 | 'Lord Nibbler', 67 | 'Zapp Brannigan', 68 | 'Kif Kroker', 69 | 'Mom', 70 | 'Headless Body of Agnew', 71 | 'Boxy', 72 | 'Brain Slugs', 73 | 'Brain Spawn', 74 | 'Calculon', 75 | 'The Crushinator', 76 | 'Father Changstein-El-Gamal', 77 | 'Chanukah Zombie', 78 | 'Clamps', 79 | 'Dwight Conrad', 80 | 'LaBarbara Conrad', 81 | 'Donbot', 82 | 'Elzar', 83 | 'Cubert Farnsworth', 84 | 'Flexo' 85 | ]; 86 | } else if (_division == HeavyLifterDivision.heavyweight) { 87 | return [ 88 | 'Apollo Creed', 89 | 'Rocky Balboa', 90 | 'Big Chuck Smith', 91 | 'Big Dipper', 92 | 'Big Yank Ball', 93 | 'Billy Snow', 94 | 'Bob Cray', 95 | 'Buddy Shaw', 96 | 'Burt Judge', 97 | 'Dipper Brown', 98 | 'Ernie Roman', 99 | 'Ivan Drago', 100 | 'Jack Reid', 101 | 'James "Clubber" Lang', 102 | 'Joe Chan', 103 | 'Joe Czak', 104 | 'Jose Mendoza', 105 | 'Kofi Langton', 106 | 'Mac Lee Green', 107 | 'Mason Dixon', 108 | 'Mickey Goldmill', 109 | 'Randy Tate', 110 | 'Spider Rico', 111 | 'Tim Simms', 112 | 'Tommy Gunn', 113 | 'Union Cane', 114 | 'Vito Soto', 115 | 'Wolfgang Peltzer' 116 | ]; 117 | } 118 | return []; 119 | } 120 | 121 | @override 122 | HeavyLifterDivision get division => _division; 123 | } 124 | -------------------------------------------------------------------------------- /example/web/panel/modules/deferred_heavy_lifter_interface.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.example.panel.modules.deferred_heavy_lifter_interface; 16 | 17 | enum HeavyLifterDivision { featherweight, welterweight, heavyweight } 18 | 19 | class HeavyLifter { 20 | HeavyLifterDivision _division; 21 | 22 | HeavyLifter(this._division); 23 | 24 | List get competitors => []; 25 | 26 | HeavyLifterDivision get division => _division; 27 | } 28 | -------------------------------------------------------------------------------- /example/web/panel/modules/deferred_module.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.example.panel.modules.deferred_module; 16 | 17 | import 'dart:async'; 18 | 19 | import 'package:meta/meta.dart' show protected; 20 | import 'package:react/react.dart' as react; 21 | import 'package:w_module/w_module.dart'; 22 | 23 | import './deferred_heavy_lifter_interface.dart'; 24 | import './deferred_heavy_lifter_implementation.dart' 25 | deferred as heavy_lifter_with_data; 26 | 27 | class DeferredModule extends Module { 28 | HeavyLifter data; 29 | 30 | @override 31 | final String name = 'DeferredModule'; 32 | 33 | DeferredComponents _components; 34 | 35 | DeferredModule() { 36 | _components = DeferredComponents(this); 37 | } 38 | 39 | @override 40 | DeferredComponents get components => _components; 41 | 42 | @override 43 | @protected 44 | Future onLoad() async { 45 | await heavy_lifter_with_data.loadLibrary(); 46 | data = heavy_lifter_with_data.RealLifter(HeavyLifterDivision.heavyweight); 47 | } 48 | } 49 | 50 | class DeferredComponents implements ModuleComponents { 51 | DeferredModule _module; 52 | DeferredComponents(this._module); 53 | 54 | @override 55 | Object content() { 56 | int keyCounter = 0; 57 | return react.div({ 58 | 'style': {'padding': '50px', 'backgroundColor': 'blue', 'color': 'white'} 59 | }, [ 60 | 'This module gets its data from a deferred implementation.', 61 | react.ul( 62 | {'className': 'list-group'}, 63 | _module.data.competitors 64 | .map((item) => react.li({'key': keyCounter++}, item))) 65 | ]); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /example/web/panel/modules/flux_module.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.example.panel.modules.flux_module; 16 | 17 | import 'dart:math'; 18 | 19 | import 'package:w_flux/w_flux.dart'; 20 | import 'package:react/react.dart' as react; 21 | import 'package:w_module/w_module.dart'; 22 | 23 | class FluxModule extends Module { 24 | @override 25 | final String name = 'FluxModule'; 26 | 27 | FluxActions _actions; 28 | FluxComponents _components; 29 | FluxStore _stores; 30 | 31 | FluxModule() { 32 | _actions = FluxActions(); 33 | _stores = FluxStore(_actions); 34 | _components = FluxComponents(_actions, _stores); 35 | } 36 | 37 | @override 38 | FluxComponents get components => _components; 39 | } 40 | 41 | class FluxComponents implements ModuleComponents { 42 | FluxActions _actions; 43 | FluxStore _stores; 44 | 45 | FluxComponents(this._actions, this._stores); 46 | 47 | @override 48 | Object content() => MyFluxComponent({'actions': _actions, 'store': _stores}); 49 | } 50 | 51 | class FluxActions { 52 | final ActionV2 changeBackgroundColor = ActionV2(); 53 | } 54 | 55 | class FluxStore extends Store { 56 | FluxActions _actions; 57 | String _backgroundColor = 'gray'; 58 | 59 | FluxStore(this._actions) { 60 | triggerOnActionV2(_actions.changeBackgroundColor, _changeBackgroundColor); 61 | } 62 | 63 | String get backgroundColor => _backgroundColor; 64 | 65 | void _changeBackgroundColor(_) { 66 | // generate a random hex color string 67 | _backgroundColor = 68 | '#' + (Random().nextDouble() * 16777215).floor().toRadixString(16); 69 | } 70 | } 71 | 72 | // ignore: non_constant_identifier_names 73 | var MyFluxComponent = react.registerComponent(() => _MyFluxComponent()); 74 | 75 | class _MyFluxComponent extends FluxComponent { 76 | @override 77 | Object render() { 78 | return react.div({ 79 | 'style': { 80 | 'padding': '50px', 81 | 'backgroundColor': store.backgroundColor, 82 | 'color': 'white' 83 | } 84 | }, [ 85 | 'This module uses a flux pattern to change its background color.', 86 | react.button({ 87 | 'style': {'padding': '10px', 'margin': '10px'}, 88 | 'onClick': (_) => actions.changeBackgroundColor(null) 89 | }, 'Random Background Color') 90 | ]); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /example/web/panel/modules/hierarchy_module.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.example.panel.modules.hierarchy_module; 16 | 17 | import 'dart:async'; 18 | import 'dart:html'; 19 | 20 | import 'package:meta/meta.dart' show protected; 21 | import 'package:react/react.dart' as react; 22 | import 'package:w_module/w_module.dart'; 23 | import 'package:w_flux/w_flux.dart'; 24 | 25 | import './basic_module.dart'; 26 | import './flux_module.dart'; 27 | import './reject_module.dart'; 28 | import './data_load_async_module.dart'; 29 | import './data_load_blocking_module.dart'; 30 | import './deferred_module.dart'; 31 | import './lifecycle_echo_module.dart'; 32 | 33 | class HierarchyModule extends Module { 34 | @override 35 | final String name = 'HierarchyModule'; 36 | 37 | HierarchyActions _actions; 38 | HierarchyComponents _components; 39 | HierarchyStore _stores; 40 | 41 | HierarchyModule() { 42 | _actions = HierarchyActions(); 43 | _stores = HierarchyStore(_actions); 44 | _components = HierarchyComponents(_actions, _stores); 45 | } 46 | 47 | @override 48 | HierarchyComponents get components => _components; 49 | 50 | @override 51 | @protected 52 | Future onLoad() async { 53 | // can optionally await all of the loadModule calls 54 | // to force all children to load before this module 55 | // completes loading (not recommended) 56 | 57 | List allOfThem = [ 58 | BasicModule(), 59 | FluxModule(), 60 | RejectModule(), 61 | DataLoadAsyncModule(), 62 | DataLoadBlockingModule(), 63 | DeferredModule(), 64 | LifecycleEchoModule() 65 | ]; 66 | 67 | allOfThem.forEach((module) { 68 | module.didLoad.listen((_) { 69 | _actions.addChildModule(module); 70 | }); 71 | loadChildModule(module); 72 | }); 73 | } 74 | } 75 | 76 | class HierarchyComponents implements ModuleComponents { 77 | HierarchyActions _actions; 78 | HierarchyStore _stores; 79 | 80 | HierarchyComponents(this._actions, this._stores); 81 | 82 | @override 83 | Object content() => 84 | HierarchyComponent({'actions': _actions, 'store': _stores}); 85 | } 86 | 87 | class HierarchyActions { 88 | final ActionV2 addChildModule = ActionV2(); 89 | final ActionV2 removeChildModule = ActionV2(); 90 | } 91 | 92 | class HierarchyStore extends Store { 93 | HierarchyActions _actions; 94 | List _childModules = []; 95 | 96 | HierarchyStore(this._actions) { 97 | triggerOnActionV2(_actions.addChildModule, _addChildModule); 98 | triggerOnActionV2(_actions.removeChildModule, _removeChildModule); 99 | } 100 | 101 | List get childModules => _childModules; 102 | 103 | void _addChildModule(Module newModule) { 104 | _childModules.add(newModule); 105 | } 106 | 107 | void _removeChildModule(Module oldModule) { 108 | // do we need to reject the unload? 109 | ShouldUnloadResult canUnload = oldModule.shouldUnload(); 110 | if (!canUnload.shouldUnload) { 111 | // reject the change with an alert and short circuit 112 | window.alert(canUnload.messagesAsString()); 113 | return; 114 | } 115 | 116 | // continue with unload 117 | _childModules.remove(oldModule); 118 | oldModule.unload(); 119 | } 120 | } 121 | 122 | // ignore: non_constant_identifier_names 123 | var HierarchyComponent = react.registerComponent(() => _HierarchyComponent()); 124 | 125 | class _HierarchyComponent 126 | extends FluxComponent { 127 | @override 128 | Object render() { 129 | return react.div( 130 | { 131 | 'style': { 132 | 'padding': '10px', 133 | 'backgroundColor': 'lightgray', 134 | 'color': 'black' 135 | } 136 | }, 137 | store.childModules.map((child) => react.div({ 138 | 'style': {'border': '3px dashed gray', 'margin': '5px'} 139 | }, [ 140 | react.button({ 141 | 'style': {'float': 'right', 'margin': '5px'}, 142 | 'onClick': (_) { 143 | actions.removeChildModule(child); 144 | } 145 | }, 'Unload Module'), 146 | child.components.content() 147 | ]))); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /example/web/panel/modules/lifecycle_echo_module.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.example.panel.modules.lifecycle_echo_module; 16 | 17 | import 'dart:async'; 18 | 19 | import 'package:meta/meta.dart' show protected; 20 | import 'package:react/react.dart' as react; 21 | import 'package:w_module/w_module.dart'; 22 | 23 | class LifecycleEchoModule extends Module { 24 | @override 25 | final String name = 'LifecycleEchoModule'; 26 | 27 | LifecycleEchoComponents _components; 28 | 29 | LifecycleEchoModule() { 30 | // load / unload state streams 31 | willLoad.listen((_) { 32 | print('$name: willLoad'); 33 | }); 34 | didLoad.listen((_) { 35 | print('$name: didLoad'); 36 | }); 37 | willUnload.listen((_) { 38 | print('$name: willUnload'); 39 | }); 40 | didUnload.listen((_) { 41 | print('$name: didUnload'); 42 | }); 43 | didLoadChildModule.listen((_) { 44 | print('$name: didLoadChildModule'); 45 | }); 46 | _components = LifecycleEchoComponents(); 47 | } 48 | 49 | @override 50 | LifecycleEchoComponents get components => _components; 51 | 52 | //-------------------------------------------------------- 53 | // Methods that can be optionally implemented by subclasses 54 | // to execute code during certain phases of the module 55 | // lifecycle 56 | //-------------------------------------------------------- 57 | 58 | @override 59 | @protected 60 | Future onLoad() async { 61 | print('$name: onLoad'); 62 | await loadChildModule(LifecycleEchoChildModule()); 63 | await Future.delayed(Duration(seconds: 1)); 64 | } 65 | 66 | @override 67 | @protected 68 | ShouldUnloadResult onShouldUnload() { 69 | print('$name: onShouldUnload'); 70 | return ShouldUnloadResult(); 71 | } 72 | 73 | @override 74 | @protected 75 | Future onUnload() async { 76 | print('$name: onUnload'); 77 | await Future.delayed(Duration(seconds: 1)); 78 | } 79 | } 80 | 81 | class LifecycleEchoComponents implements ModuleComponents { 82 | @override 83 | Object content() => react.div({ 84 | 'style': { 85 | 'padding': '50px', 86 | 'backgroundColor': 'lightGray', 87 | 'color': 'black' 88 | } 89 | }, [ 90 | 'This module echoes all of its lifecycle events to the dev console.' 91 | ]); 92 | } 93 | 94 | class LifecycleEchoChildModule extends Module { 95 | @override 96 | final String name = 'LifecycleEchoChildModule'; 97 | } 98 | -------------------------------------------------------------------------------- /example/web/panel/modules/panel_module.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.example.panel.modules.panel_module; 16 | 17 | import 'dart:async'; 18 | import 'dart:html'; 19 | 20 | import 'package:meta/meta.dart' show protected; 21 | import 'package:react/react.dart' as react; 22 | import 'package:w_flux/w_flux.dart'; 23 | import 'package:w_module/w_module.dart'; 24 | 25 | import './basic_module.dart'; 26 | import './flux_module.dart'; 27 | import './reject_module.dart'; 28 | import './data_load_async_module.dart'; 29 | import './data_load_blocking_module.dart'; 30 | import './deferred_module.dart'; 31 | import './lifecycle_echo_module.dart'; 32 | import './hierarchy_module.dart'; 33 | 34 | class PanelModule extends Module { 35 | @override 36 | final String name = 'PanelModule'; 37 | 38 | PanelActions _actions; 39 | PanelComponents _components; 40 | PanelStore _stores; 41 | 42 | PanelModule() { 43 | _actions = PanelActions(); 44 | _stores = PanelStore(_actions, this); 45 | _components = PanelComponents(_actions, _stores); 46 | } 47 | 48 | @override 49 | PanelComponents get components => _components; 50 | 51 | @override 52 | @protected 53 | Future onLoad() { 54 | _actions.changeToPanel(0); 55 | return Future.value(); 56 | } 57 | 58 | Future addModule(LifecycleModule newModule) { 59 | return loadChildModule(newModule); 60 | } 61 | } 62 | 63 | class PanelComponents implements ModuleComponents { 64 | PanelActions _actions; 65 | PanelStore _stores; 66 | 67 | PanelComponents(this._actions, this._stores); 68 | 69 | @override 70 | Object content() => PanelComponent({'actions': _actions, 'store': _stores}); 71 | } 72 | 73 | class PanelActions { 74 | final ActionV2 changeToPanel = ActionV2(); 75 | } 76 | 77 | class PanelStore extends Store { 78 | /// Public data 79 | num _panelIndex = 0; 80 | bool _isRenderable = false; 81 | Module _panelModule; 82 | 83 | /// Internals 84 | PanelActions _actions; 85 | PanelModule _parentModule; 86 | 87 | PanelStore(this._actions, this._parentModule) { 88 | triggerOnActionV2(_actions.changeToPanel, _changeToPanel); 89 | } 90 | 91 | bool get isRenderable => _isRenderable; 92 | num get panelIndex => _panelIndex; 93 | Module get panelModule => _panelModule; 94 | 95 | Future _changeToPanel(num newPanelIndex) async { 96 | // is there a panel currently loaded? 97 | if (_panelModule != null) { 98 | // do we need to reject the unload of the existing panel? 99 | ShouldUnloadResult canUnload = _panelModule.shouldUnload(); 100 | if (!canUnload.shouldUnload) { 101 | // reject the change with an alert and short circuit 102 | window.alert(canUnload.messagesAsString()); 103 | return; 104 | } 105 | 106 | // unload the existing panel 107 | _isRenderable = false; 108 | await _panelModule.unload(); 109 | } 110 | 111 | // extra trigger to show loading indicator 112 | _panelIndex = newPanelIndex; 113 | trigger(); 114 | 115 | // load the new panel 116 | if (_panelIndex == 0) { 117 | _panelModule = BasicModule(); 118 | } else if (_panelIndex == 1) { 119 | _panelModule = FluxModule(); 120 | } else if (_panelIndex == 2) { 121 | _panelModule = RejectModule(); 122 | } else if (_panelIndex == 3) { 123 | _panelModule = DataLoadAsyncModule(); 124 | } else if (_panelIndex == 4) { 125 | _panelModule = DataLoadBlockingModule(); 126 | } else if (_panelIndex == 5) { 127 | _panelModule = DeferredModule(); 128 | } else if (_panelIndex == 6) { 129 | _panelModule = LifecycleEchoModule(); 130 | } else if (_panelIndex == 7) { 131 | _panelModule = HierarchyModule(); 132 | } else if (_panelIndex == 8) { 133 | _panelModule = PanelModule(); 134 | } 135 | await _parentModule.addModule(_panelModule); 136 | _isRenderable = true; 137 | } 138 | } 139 | 140 | // ignore: non_constant_identifier_names 141 | var PanelComponent = react.registerComponent(() => _PanelComponent()); 142 | 143 | class _PanelComponent extends FluxComponent { 144 | @override 145 | Object render() { 146 | // display a loading placeholder if the module isn't ready for rendering 147 | var content = store.isRenderable 148 | ? store.panelModule.components.content() 149 | : react.div({'className': 'loader'}, 'Loading new panel module...'); 150 | 151 | var tabBar = react.div({ 152 | 'className': 'buttonBar' 153 | }, [ 154 | _renderPanelButton(0, 'Basic'), 155 | _renderPanelButton(1, 'Flux'), 156 | _renderPanelButton(2, 'Reject'), 157 | _renderPanelButton(3, 'Data Load (async)'), 158 | _renderPanelButton(4, 'Data Load (blocking)'), 159 | _renderPanelButton(5, 'Deferred'), 160 | _renderPanelButton(6, 'Lifecycle Echo'), 161 | _renderPanelButton(7, 'All of them'), 162 | _renderPanelButton(8, 'Recursive') 163 | ]); 164 | 165 | return react.div({ 166 | 'style': { 167 | 'padding': '5px', 168 | 'backgroundColor': 'white', 169 | 'color': 'black', 170 | 'border': '1px solid lightgreen' 171 | } 172 | }, [ 173 | tabBar, 174 | content 175 | ]); 176 | } 177 | 178 | Object _renderPanelButton(int index, String label) { 179 | return react.button({ 180 | 'key': index, 181 | 'onClick': (_) => actions.changeToPanel(index), 182 | 'className': store.panelIndex == index ? 'active' : null 183 | }, label); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /example/web/panel/modules/reject_module.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.example.panel.modules.reject_module; 16 | 17 | import 'package:meta/meta.dart' show protected; 18 | import 'package:react/react.dart' as react; 19 | import 'package:w_flux/w_flux.dart'; 20 | import 'package:w_module/w_module.dart'; 21 | 22 | class RejectModule extends Module { 23 | @override 24 | final String name = 'RejectModule'; 25 | 26 | RejectActions _actions; 27 | RejectStore _stores; 28 | 29 | RejectComponents _components; 30 | 31 | RejectModule() { 32 | _actions = RejectActions(); 33 | _stores = RejectStore(_actions); 34 | _components = RejectComponents(_actions, _stores); 35 | } 36 | 37 | @override 38 | RejectComponents get components => _components; 39 | 40 | @override 41 | @protected 42 | ShouldUnloadResult onShouldUnload() { 43 | if (_stores.shouldUnload) { 44 | return ShouldUnloadResult(); 45 | } 46 | return ShouldUnloadResult(false, '$name won\'t let you leave!'); 47 | } 48 | } 49 | 50 | class RejectComponents implements ModuleComponents { 51 | RejectActions _actions; 52 | RejectStore _stores; 53 | 54 | RejectComponents(this._actions, this._stores); 55 | 56 | @override 57 | Object content() => RejectComponent({'actions': _actions, 'store': _stores}); 58 | } 59 | 60 | class RejectActions { 61 | final ActionV2 toggleShouldUnload = ActionV2(); 62 | } 63 | 64 | class RejectStore extends Store { 65 | /// Public data 66 | bool _shouldUnload = true; 67 | 68 | /// Internals 69 | RejectActions _actions; 70 | 71 | RejectStore(this._actions) { 72 | triggerOnActionV2(_actions.toggleShouldUnload, _toggleShouldUnload); 73 | } 74 | 75 | bool get shouldUnload => _shouldUnload; 76 | 77 | void _toggleShouldUnload(_) { 78 | _shouldUnload = !_shouldUnload; 79 | } 80 | } 81 | 82 | // ignore: non_constant_identifier_names 83 | var RejectComponent = react.registerComponent(() => _RejectComponent()); 84 | 85 | class _RejectComponent extends FluxComponent { 86 | @override 87 | Object render() { 88 | return react.div({ 89 | 'style': {'padding': '50px', 'backgroundColor': 'green', 'color': 'white'} 90 | }, [ 91 | 'This module will reject unloading if the checkbox is cleared.', 92 | react.br({}), 93 | react.br({}), 94 | react.label({}, [ 95 | react.input({ 96 | 'type': 'checkbox', 97 | 'label': 'shouldUnload', 98 | 'checked': store.shouldUnload, 99 | 'onChange': (_) => actions.toggleShouldUnload(null) 100 | }), 101 | 'shouldUnload' 102 | ]) 103 | ]); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /example/web/panel/modules/sample_tracer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:opentracing/opentracing.dart'; 3 | 4 | class SampleSpan implements Span { 5 | static int _nextId = 0; 6 | final int _id = _nextId++; 7 | 8 | @override 9 | final List references; 10 | 11 | @override 12 | final Map tags; 13 | 14 | @override 15 | final List logData = []; 16 | 17 | @override 18 | final String operationName; 19 | 20 | @override 21 | SpanContext context; 22 | 23 | @override 24 | DateTime startTime; 25 | 26 | DateTime _endTime; 27 | 28 | Completer _whenFinished = Completer(); 29 | 30 | SampleSpan( 31 | this.operationName, { 32 | SpanContext childOf, 33 | this.references, 34 | DateTime startTime, 35 | Map tags, 36 | }) : this.startTime = startTime ?? DateTime.now(), 37 | this.tags = tags ?? {} { 38 | if (childOf != null) { 39 | references.add(Reference.childOf(childOf)); 40 | } 41 | setTag('span.kind', 'client'); 42 | 43 | final parent = parentContext; 44 | if (parent != null) { 45 | this.context = SpanContext(spanId: _id, traceId: parent.traceId); 46 | this.context.baggage.addAll(parent.baggage); 47 | } else { 48 | this.context = SpanContext(spanId: _id, traceId: _id); 49 | } 50 | } 51 | 52 | @override 53 | void addTags(Map newTags) => tags.addAll(newTags); 54 | 55 | @override 56 | Duration get duration => _endTime?.difference(startTime); 57 | 58 | @override 59 | DateTime get endTime => _endTime; 60 | 61 | @override 62 | void finish({DateTime finishTime}) { 63 | if (_whenFinished == null) { 64 | return; 65 | } 66 | 67 | _endTime = finishTime ?? DateTime.now(); 68 | _whenFinished.complete(this); 69 | _whenFinished = null; 70 | } 71 | 72 | @override 73 | void log(String event, {dynamic payload, DateTime timestamp}) => 74 | logData.add(LogData(timestamp ?? DateTime.now(), event, payload)); 75 | 76 | @override 77 | SpanContext get parentContext => 78 | references.isEmpty ? null : references.first.referencedContext; 79 | 80 | @override 81 | void setTag(String tagName, dynamic value) => tags[tagName] = value; 82 | 83 | @override 84 | Future get whenFinished => _whenFinished.future; 85 | 86 | @override 87 | String toString() { 88 | final sb = StringBuffer('SampleSpan('); 89 | sb 90 | ..writeln('traceId: ${context.traceId}') 91 | ..writeln('spanId: ${context.spanId}') 92 | ..writeln('operationName: $operationName') 93 | ..writeln('tags: ${tags.toString()}') 94 | ..writeln('startTime: ${startTime.toString()}'); 95 | 96 | if (_endTime != null) { 97 | sb 98 | ..writeln('endTime: ${endTime.toString()}') 99 | ..writeln('duration: ${duration.toString()}'); 100 | } 101 | 102 | if (logData.isNotEmpty) { 103 | sb.writeln('logData: ${logData.toString()}'); 104 | } 105 | 106 | if (references.isNotEmpty) { 107 | final reference = references.first; 108 | sb.writeln( 109 | 'reference: ${reference.referenceType} ${reference.referencedContext.spanId}'); 110 | } 111 | 112 | sb.writeln(')'); 113 | 114 | return sb.toString(); 115 | } 116 | } 117 | 118 | class SampleTracer implements AbstractTracer { 119 | @override 120 | SampleSpan startSpan( 121 | String operationName, { 122 | SpanContext childOf, 123 | List references, 124 | DateTime startTime, 125 | Map tags, 126 | }) { 127 | return SampleSpan( 128 | operationName, 129 | childOf: childOf, 130 | references: references, 131 | startTime: startTime, 132 | tags: tags, 133 | )..whenFinished.then((span) { 134 | print(span.toString()); 135 | }); 136 | } 137 | 138 | @override 139 | Reference childOf(SpanContext context) => Reference.childOf(context); 140 | 141 | @override 142 | Reference followsFrom(SpanContext context) => Reference.followsFrom(context); 143 | 144 | @override 145 | SpanContext extract(String format, dynamic carrier) { 146 | throw UnimplementedError( 147 | 'Sample tracer for example purposes does not support advanced tracing behavior.'); 148 | } 149 | 150 | @override 151 | void inject(SpanContext spanContext, String format, dynamic carrier) { 152 | throw UnimplementedError( 153 | 'Sample tracer for example purposes does not support advanced tracing behavior.'); 154 | } 155 | 156 | @override 157 | Future flush() { 158 | return null; 159 | } 160 | 161 | @override 162 | ScopeManager scopeManager; 163 | 164 | @override 165 | Span get activeSpan => scopeManager?.active?.span; 166 | } 167 | -------------------------------------------------------------------------------- /example/web/panel/panel_app.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Workiva Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* panel list styles*/ 18 | 19 | .list-group { 20 | margin-bottom: 20px; 21 | margin-left: 0; 22 | padding-left: 0; 23 | background-color: transparent; 24 | } 25 | 26 | .list-group li { 27 | -webkit-user-select: text; 28 | -moz-user-select: text; 29 | -ms-user-select: text; 30 | user-select: text; 31 | position: relative; 32 | display: block; 33 | margin-bottom: -1px; 34 | border-top: 1px solid #cbcbcb; 35 | padding: 6px 12px 7px; 36 | font-size: 14px; 37 | line-height: 1.4285714286; 38 | } 39 | 40 | .list-group > li:first-child { 41 | border-top-color: transparent; 42 | } 43 | 44 | /* panel button bar styles*/ 45 | 46 | .buttonBar { 47 | position: relative; 48 | display: inline-block; 49 | vertical-align: middle; 50 | padding-bottom: 5px; 51 | } 52 | 53 | .buttonBar > button:not(:first-child):not(:last-child) { 54 | border-radius: 0; 55 | } 56 | 57 | .buttonBar > button:first-child:not(:last-child) { 58 | -moz-border-radius-topright: 0; 59 | -webkit-border-top-right-radius: 0; 60 | border-top-right-radius: 0; 61 | -moz-border-radius-bottomright: 0; 62 | -webkit-border-bottom-right-radius: 0; 63 | border-bottom-right-radius: 0; 64 | } 65 | 66 | .buttonBar > button:last-child:not(:first-child) { 67 | -moz-border-radius-topleft: 0; 68 | -webkit-border-top-left-radius: 0; 69 | border-top-left-radius: 0; 70 | -moz-border-radius-bottomleft: 0; 71 | -webkit-border-bottom-left-radius: 0; 72 | border-bottom-left-radius: 0; 73 | } 74 | 75 | .buttonBar > button + button { 76 | margin-left: -1px; 77 | } 78 | 79 | .buttonBar > button { 80 | position: relative; 81 | float: left; 82 | max-width: none; 83 | overflow: visible; 84 | text-transform: none; 85 | margin: 0; 86 | -webkit-appearance: none; 87 | -moz-appearance: none; 88 | -moz-user-select: -moz-none; 89 | -webkit-user-select: none; 90 | -ms-user-select: none; 91 | user-select: none; 92 | cursor: pointer; 93 | display: inline-block; 94 | text-align: center; 95 | vertical-align: middle; 96 | border: 1px solid #d7d7d7; 97 | white-space: nowrap; 98 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 99 | font-weight: normal; 100 | padding: 6px 12px; 101 | font-size: 14px; 102 | border-radius: 4px; 103 | top: 0; 104 | -webkit-tap-highlight-color: transparent; 105 | -moz-tap-highlight-color: transparent; 106 | -ms-tap-highlight-color: transparent; 107 | tap-highlight-color: transparent; 108 | box-shadow: 0 1px 0 0 #d7d7d7; 109 | background-color: #f7f7f7; 110 | outline: 0; 111 | } 112 | 113 | .buttonBar > button.active { 114 | z-index: 2; 115 | top: 1px; 116 | box-shadow: none !important; 117 | color: #333333; 118 | background-color: #d7d7d7; 119 | border-color: #d7d7d7; 120 | border-color: rgba(0, 0, 0, 0.16); 121 | } 122 | 123 | /* loading spinner styles*/ 124 | 125 | .loader { 126 | margin: 20px auto; 127 | font-size: 10px; 128 | position: relative; 129 | text-indent: -9999em; 130 | border-top: 1.1em solid rgba(0, 0, 0, 0.2); 131 | border-right: 1.1em solid rgba(0, 0, 0, 0.2); 132 | border-bottom: 1.1em solid rgba(0, 0, 0, 0.2); 133 | border-left: 1.1em solid #ffffff; 134 | -webkit-transform: translateZ(0); 135 | -ms-transform: translateZ(0); 136 | transform: translateZ(0); 137 | -webkit-animation: load8 1.1s infinite linear; 138 | animation: load8 1.1s infinite linear; 139 | } 140 | .loader, 141 | .loader:after { 142 | border-radius: 50%; 143 | width: 5em; 144 | height: 5em; 145 | } 146 | @-webkit-keyframes load8 { 147 | 0% { 148 | -webkit-transform: rotate(0deg); 149 | transform: rotate(0deg); 150 | } 151 | 100% { 152 | -webkit-transform: rotate(360deg); 153 | transform: rotate(360deg); 154 | } 155 | } 156 | @keyframes load8 { 157 | 0% { 158 | -webkit-transform: rotate(0deg); 159 | transform: rotate(0deg); 160 | } 161 | 100% { 162 | -webkit-transform: rotate(360deg); 163 | transform: rotate(360deg); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /example/web/panel/panel_app.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.example.panel; 16 | 17 | import 'dart:async'; 18 | import 'dart:html'; 19 | import 'dart:js' as js; 20 | 21 | import 'package:platform_detect/platform_detect.dart'; 22 | import 'package:over_react/over_react.dart'; 23 | import 'package:react/react_dom.dart' as react_dom; 24 | import 'package:react/react_client.dart' as react_client; 25 | import 'package:w_module/w_module.dart' hide Event; 26 | import 'package:opentracing/opentracing.dart'; 27 | 28 | import 'modules/panel_module.dart'; 29 | import 'modules/sample_tracer.dart'; 30 | 31 | Future main() async { 32 | Element container = querySelector('#panel-container'); 33 | react_client.setClientConfiguration(); 34 | 35 | final tracer = SampleTracer(); 36 | initGlobalTracer(tracer); 37 | assert(globalTracer() == tracer); 38 | 39 | // instantiate the core app module and wait for it to complete loading 40 | PanelModule panelModule = PanelModule(); 41 | await panelModule.load(); 42 | 43 | // block browser tab / window close if necessary 44 | window.onBeforeUnload.listen((Event event) { 45 | if (event is! BeforeUnloadEvent) return; 46 | BeforeUnloadEvent beforeUnloadEvent = event; 47 | 48 | // can the app be unloaded? 49 | ShouldUnloadResult res = panelModule.shouldUnload(); 50 | if (!res.shouldUnload) { 51 | // return the supplied error message to block close 52 | beforeUnloadEvent.returnValue = res.messagesAsString(); 53 | } else if (browser.isInternetExplorer) { 54 | // IE interprets a null string as a response and displays an alert to 55 | // the user. Use the `undefined` value of the JS context instead. 56 | // https://github.com/dart-lang/sdk/issues/22589 57 | beforeUnloadEvent.returnValue = js.context['undefined']; 58 | } 59 | }); 60 | 61 | // render the app into the browser 62 | react_dom.render( 63 | ErrorBoundary()(panelModule.components.content()), container); 64 | } 65 | -------------------------------------------------------------------------------- /example/web/random_color/index.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | Random Color Generator Example | w_module 22 | 23 | 24 |
25 |

Exercise the module API here:

26 | 27 | 28 |

29 | Current background color: 30 | 31 |
32 |

33 | 34 |
35 |

Render the module UI here:

36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /example/web/random_color/random_color.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.example.random_color; 16 | 17 | import 'dart:async'; 18 | import 'dart:html' as html; 19 | import 'dart:math'; 20 | 21 | import 'package:react/react.dart' as react; 22 | import 'package:over_react/over_react.dart'; 23 | import 'package:react/react_dom.dart' as react_dom; 24 | import 'package:react/react_client.dart' as react_client; 25 | 26 | import 'package:w_flux/w_flux.dart'; 27 | import 'package:w_module/w_module.dart'; 28 | 29 | Future main() async { 30 | // instantiate the module and wait for it to load 31 | RandomColorModule randomColorModule = RandomColorModule(); 32 | await randomColorModule.load(); 33 | 34 | // render the module's UI component 35 | react_client.setClientConfiguration(); 36 | react_dom.render(ErrorBoundary()(randomColorModule.components.content()), 37 | html.querySelector('#content-container')); 38 | 39 | // exercise the module's API via some simple button clicks 40 | html 41 | .querySelector('#random-color') 42 | .onClick 43 | .listen((_) => randomColorModule.api.changeBackgroundColor()); 44 | html 45 | .querySelector('#purple-color') 46 | .onClick 47 | .listen((_) => randomColorModule.api.setBackgroundColor('purple')); 48 | 49 | // use public API to display the initial background color 50 | html.querySelector('#current-color').innerHtml = 51 | randomColorModule.api.currentBackgroundColor; 52 | 53 | // process public events dispatched from the module 54 | randomColorModule.events.colorChanged.listen((newColor) { 55 | html.Element colorSpan = html.querySelector('#current-color'); 56 | colorSpan.innerHtml = newColor; 57 | colorSpan.style.color = newColor; 58 | }); 59 | } 60 | 61 | DispatchKey randomColorModuleDispatchKey = DispatchKey('randomColor'); 62 | 63 | class RandomColorModule extends Module { 64 | @override 65 | final String name = 'RandomColorModule'; 66 | 67 | RandomColorActions _actions; 68 | RandomColorApi _api; 69 | RandomColorComponents _components; 70 | RandomColorEvents _events; 71 | RandomColorStore _stores; 72 | 73 | RandomColorModule() { 74 | _actions = RandomColorActions(); 75 | _events = RandomColorEvents(); 76 | _stores = RandomColorStore(_actions, _events, randomColorModuleDispatchKey); 77 | _components = RandomColorComponents(_actions, _stores); 78 | _api = RandomColorApi(_actions, _stores); 79 | } 80 | 81 | @override 82 | RandomColorApi get api => _api; 83 | 84 | @override 85 | RandomColorComponents get components => _components; 86 | 87 | @override 88 | RandomColorEvents get events => _events; 89 | } 90 | 91 | class RandomColorApi { 92 | RandomColorActions _actions; 93 | RandomColorStore _stores; 94 | 95 | RandomColorApi(this._actions, this._stores); 96 | 97 | void setBackgroundColor(String newColor) { 98 | _actions.setBackgroundColor(newColor); 99 | } 100 | 101 | void changeBackgroundColor() { 102 | _actions.changeBackgroundColor(null); 103 | } 104 | 105 | String get currentBackgroundColor => _stores.backgroundColor; 106 | } 107 | 108 | class RandomColorEvents { 109 | final Event colorChanged = Event(randomColorModuleDispatchKey); 110 | } 111 | 112 | class RandomColorComponents implements ModuleComponents { 113 | RandomColorActions _actions; 114 | RandomColorStore _stores; 115 | 116 | RandomColorComponents(this._actions, this._stores); 117 | 118 | @override 119 | Object content() => 120 | RandomColorComponent({'actions': _actions, 'store': _stores}); 121 | } 122 | 123 | class RandomColorActions { 124 | final ActionV2 changeBackgroundColor = ActionV2(); 125 | final ActionV2 setBackgroundColor = ActionV2(); 126 | } 127 | 128 | class RandomColorStore extends Store { 129 | /// Public data 130 | String _backgroundColor = 'gray'; 131 | 132 | RandomColorEvents _events; 133 | DispatchKey _dispatchKey; 134 | 135 | /// Internals 136 | RandomColorActions _actions; 137 | 138 | RandomColorStore(this._actions, this._events, this._dispatchKey) { 139 | _actions.changeBackgroundColor.listen(_changeBackgroundColor); 140 | _actions.setBackgroundColor.listen(_setBackgroundColor); 141 | } 142 | 143 | String get backgroundColor => _backgroundColor; 144 | 145 | void _changeBackgroundColor(_) { 146 | // generate a random hex color string 147 | _backgroundColor = 148 | '#' + (Random().nextDouble() * 16777215).floor().toRadixString(16); 149 | _events.colorChanged(_backgroundColor, _dispatchKey); 150 | trigger(); 151 | } 152 | 153 | void _setBackgroundColor(String newColor) { 154 | // generate a random hex color string 155 | _backgroundColor = newColor; 156 | _events.colorChanged(_backgroundColor, _dispatchKey); 157 | trigger(); 158 | } 159 | } 160 | 161 | // ignore: non_constant_identifier_names 162 | var RandomColorComponent = 163 | react.registerComponent(() => _RandomColorComponent()); 164 | 165 | class _RandomColorComponent 166 | extends FluxComponent { 167 | @override 168 | Object render() { 169 | return react.div({ 170 | 'style': { 171 | 'padding': '50px', 172 | 'backgroundColor': store.backgroundColor, 173 | 'color': 'white' 174 | } 175 | }, [ 176 | 'This module uses a flux pattern to change its background color.', 177 | react.button({ 178 | 'style': {'padding': '10px', 'margin': '10px'}, 179 | 'onClick': (_) => actions.changeBackgroundColor(null) 180 | }, 'Change Background Color'), 181 | react.button({ 182 | 'style': {'padding': '10px', 'margin': '10px'}, 183 | 'onClick': (_) => actions.setBackgroundColor('red') 184 | }, 'Make Background Red') 185 | ]); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /lib/src/event.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.src.event; 16 | 17 | import 'dart:async'; 18 | 19 | /// An event stream that can be listened to. A dispatch key is required to 20 | /// instantiate the event stream. The same dispatch key must subsequently be 21 | /// used to dispatch all events on the stream, effectively preventing 22 | /// uncontrolled external dispatch. 23 | class Event extends Stream { 24 | /// This event is associated with a specific dispatch key. 25 | /// In order to control this event stream, this dispatch 26 | /// key must be used. Without it, this event stream is 27 | /// effectively read-only. 28 | DispatchKey _key; 29 | 30 | /// The underlying StreamController that drives the dispatching of and 31 | /// listening for events. 32 | final StreamController _streamController = StreamController.broadcast(); 33 | 34 | /// Create an Event and associate it with [key]. 35 | Event(DispatchKey key) : _key = key; 36 | 37 | /// Whether the Event stream is closed. 38 | /// 39 | /// The Event becomes closed by calling the [close] method. New events cannot 40 | /// be dispatched when this is `true`. 41 | bool get isClosed => _streamController.isClosed; 42 | 43 | /// Closes the Event stream, telling it that no further events will be 44 | /// dispatched. 45 | /// 46 | /// Returns a `Future` which resolves when the underlying `StreamController` 47 | /// has finished closing. 48 | Future close(DispatchKey key) async { 49 | if (key != _key) { 50 | throw ArgumentError( 51 | 'Event.close() expected the "${_key.name}" key but received the ' 52 | '"${key.name}" key.'); 53 | } 54 | await _streamController.close(); 55 | } 56 | 57 | @override 58 | StreamSubscription listen(void onData(T event)?, 59 | {Function? onError, void onDone()?, bool? cancelOnError}) { 60 | return _streamController.stream.listen(onData, 61 | onError: onError, onDone: onDone, cancelOnError: cancelOnError); 62 | } 63 | 64 | /// Dispatch a payload to this event stream. This only works if 65 | /// [key] is the correct key with which this Event was constructed. 66 | void call(T payload, DispatchKey key) { 67 | if (key != _key) { 68 | throw ArgumentError( 69 | 'Event dispatch expected the "${_key.name}" key but received the ' 70 | '"${key.name}" key.'); 71 | } 72 | _streamController.add(payload); 73 | } 74 | } 75 | 76 | /// Key that enables dispatching of events. Every [Event] is 77 | /// associated with a specific key, and that key must be used 78 | /// in order to dispatch an item to that event stream. 79 | /// 80 | /// One key can be used for multiple events. 81 | class DispatchKey { 82 | String? name; 83 | DispatchKey([this.name]); 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/events_collection.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:meta/meta.dart'; 16 | import 'package:w_common/disposable.dart'; 17 | 18 | import 'package:w_module/src/event.dart'; 19 | 20 | /// A base class for a collection of [Event] instances that are all tied to the 21 | /// same [DispatchKey]. 22 | /// 23 | /// Use this class to colocate related [Event] instances and to make disposal of 24 | /// these [Event]s easier. 25 | /// 26 | /// final key = new DispatchKey('example'); 27 | /// 28 | /// class ExampleEvents extends EventsCollection { 29 | /// final Event eventA = new Event(key); 30 | /// final Event eventB = new Event(key); 31 | /// 32 | /// ExampleEvents() : super(key) { 33 | /// [ 34 | /// eventA, 35 | /// eventB, 36 | /// ].forEach(manageEvent); 37 | /// } 38 | /// } 39 | class EventsCollection extends Disposable { 40 | @override 41 | String get disposableTypeName => 'EventsCollection'; 42 | 43 | /// The key that every [Event] instance included as a part of this 44 | /// [EventsCollection] should be tied to. 45 | /// 46 | /// This allows [manageEvent] to close the aforementioned [Event]s. 47 | final DispatchKey _key; 48 | 49 | EventsCollection(DispatchKey key) : _key = key; 50 | 51 | /// Registers an [Event] to be closed when this [EventsCollection] is 52 | /// disposed. 53 | @mustCallSuper 54 | @protected 55 | void manageEvent(Event event) { 56 | getManagedDisposer(() => event.close(_key)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/lifecycle_module.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.src.lifecycle_module; 16 | 17 | import 'dart:async'; 18 | 19 | import 'package:logging/logging.dart'; 20 | import 'package:meta/meta.dart' 21 | show mustCallSuper, protected, visibleForTesting; 22 | import 'package:opentracing/opentracing.dart'; 23 | import 'package:w_common/disposable.dart'; 24 | 25 | import 'package:w_module/src/simple_module.dart'; 26 | import 'package:w_module/src/timing_specifiers.dart'; 27 | 28 | @visibleForTesting 29 | Duration maxChildUnloadDuration = Duration(seconds: 30); 30 | 31 | /// Possible states a [LifecycleModule] may occupy. 32 | enum LifecycleState { 33 | /// The module has been instantiated. 34 | instantiated, 35 | 36 | /// The module is in the process of being loaded. 37 | loading, 38 | 39 | /// The module has been loaded. 40 | loaded, 41 | 42 | /// The module is in the process of being suspended. 43 | suspending, 44 | 45 | /// The module has been suspended. 46 | suspended, 47 | 48 | /// The module is in the process of resuming from the suspended state. 49 | resuming, 50 | 51 | /// The module is in the process of unloading. 52 | unloading, 53 | 54 | /// The module has been unloaded. 55 | unloaded 56 | } 57 | 58 | /// Intended to be extended by most base module classes in order to provide a 59 | /// unified lifecycle API. 60 | abstract class LifecycleModule extends SimpleModule with Disposable { 61 | static int _nextId = 0; 62 | // Used by tracing to tell apart multiple instances of the same module 63 | int _instanceId = _nextId++; 64 | 65 | List _childModules = []; 66 | Logger? _memoLogger; 67 | Logger get _logger => 68 | _memoLogger ??= Logger('w_module.LifecycleModule:$name'); 69 | late String _defaultName; 70 | LifecycleState? _previousState; 71 | LifecycleState? _state = LifecycleState.instantiated; 72 | Completer? _transition; 73 | Span? _activeSpan; 74 | 75 | // Used by tracing to create a span if the consumer specifies when the module 76 | // reaches its first useful state 77 | DateTime? _startLoadTime; 78 | 79 | // Lifecycle event StreamControllers 80 | StreamController _willLoadChildModuleController = 81 | StreamController.broadcast(); 82 | StreamController _didLoadChildModuleController = 83 | StreamController.broadcast(); 84 | 85 | StreamController _willLoadController = 86 | StreamController.broadcast(); 87 | StreamController _didLoadController = 88 | StreamController.broadcast(); 89 | 90 | StreamController _willSuspendController = 91 | StreamController.broadcast(); 92 | StreamController _didSuspendController = 93 | StreamController.broadcast(); 94 | 95 | StreamController _willResumeController = 96 | StreamController.broadcast(); 97 | StreamController _didResumeController = 98 | StreamController.broadcast(); 99 | 100 | StreamController _willUnloadChildModuleController = 101 | StreamController.broadcast(); 102 | StreamController _didUnloadChildModuleController = 103 | StreamController.broadcast(); 104 | 105 | StreamController _willUnloadController = 106 | StreamController.broadcast(); 107 | StreamController _didUnloadController = 108 | StreamController.broadcast(); 109 | 110 | // constructor necessary to init load / unload state stream 111 | LifecycleModule() { 112 | [ 113 | _willLoadController, 114 | _didLoadController, 115 | _willLoadChildModuleController, 116 | _didLoadChildModuleController, 117 | _willSuspendController, 118 | _didSuspendController, 119 | _willResumeController, 120 | _didResumeController, 121 | _willUnloadChildModuleController, 122 | _didUnloadChildModuleController, 123 | _willUnloadController, 124 | _didUnloadController, 125 | ].forEach(manageStreamController); 126 | 127 | { 128 | 'willLoad': willLoad, 129 | 'didLoad': didLoad, 130 | 'willLoadChildModule': willLoadChildModule, 131 | 'didLoadChildModule': didLoadChildModule, 132 | 'willSuspend': willSuspend, 133 | 'didSuspend': didSuspend, 134 | 'willResume': willResume, 135 | 'didResume': didResume, 136 | 'willUnloadChildModule': willUnloadChildModule, 137 | 'didUnloadChildModule': didUnloadChildModule, 138 | 'willUnload': willUnload, 139 | 'didUnload': didUnload, 140 | }.forEach(_logLifecycleEvents); 141 | 142 | _defaultName = 'LifecycleModule($runtimeType)'; 143 | 144 | getManagedDisposer(() async { 145 | _childModules.clear(); 146 | }); 147 | } 148 | 149 | /// If this module is in a transition state, this is the Span capturing the 150 | /// transition state. 151 | /// 152 | /// Example: 153 | /// 154 | /// ```dart 155 | /// @overide 156 | /// Future onLoad() { 157 | /// var value = 'some_value; 158 | /// ... 159 | /// activeSpan.setTag('some.tag.name', value); 160 | /// ... 161 | /// } 162 | /// ``` 163 | @protected 164 | Span? get activeSpan => _activeSpan; 165 | 166 | /// Set internally by this module for the load span so it can be used as a 167 | /// `Reference` to other spans after the span is finished. 168 | SpanContext? _loadContext; 169 | 170 | /// Set internally by the parent module if this module is called by [loadChildModule] 171 | SpanContext? _parentContext; 172 | 173 | /// Builds a span that conditionally applies a followsFrom reference if this module 174 | /// was loaded by a parent module. 175 | /// 176 | /// Returns `null` if no globalTracer is configured, or if this module does 177 | /// not override the [name] getter (as the default name becomes nonsensical 178 | /// when compiled to js). 179 | Span? _startTransitionSpan(String operationName) { 180 | if (name == _defaultName) { 181 | return null; 182 | } 183 | 184 | final tracer = globalTracer(); 185 | 186 | List references = []; 187 | if (_parentContext != null) { 188 | references.add(Reference.followsFrom(_parentContext!)); 189 | } 190 | 191 | return tracer.startSpan( 192 | '$name.$operationName', 193 | references: references, 194 | tags: _defaultTags, 195 | ); 196 | } 197 | 198 | /// Creates a span with `globalTracer` from the start of [load] until now. 199 | /// 200 | /// This span is intended to represent the time it takes for the module to 201 | /// finish asynchronously loading any necessary data and entering a state which 202 | /// is ready for user interaction. 203 | /// 204 | /// Any [tags] or [references] specified will be added to this span. 205 | @protected 206 | void specifyFirstUsefulState({ 207 | Map tags = const {}, 208 | List references = const [], 209 | }) => 210 | specifyStartupTiming( 211 | StartupTimingType.firstUseful, 212 | tags: tags, 213 | references: references, 214 | ); 215 | 216 | /// Creates a span with `globalTracer` from the start of [load] until now. 217 | /// 218 | /// The [specifier] indicates the purpose of this span. 219 | /// 220 | /// Any [tags] or [references] specified will be added to this span. 221 | @protected 222 | void specifyStartupTiming( 223 | StartupTimingType specifier, { 224 | Map tags = const {}, 225 | List references = const [], 226 | }) { 227 | // Load didn't start 228 | if (_loadContext == null || _startLoadTime == null) { 229 | throw StateError( 230 | 'Calling `specifyStartupTiming` before calling `load()`'); 231 | } 232 | 233 | final tracer = globalTracer(); 234 | 235 | tracer 236 | .startSpan( 237 | '$name.${specifier.operationName}', 238 | references: [tracer.followsFrom(_loadContext!)]..addAll(references), 239 | startTime: _startLoadTime, 240 | tags: _defaultTags..addAll(tags), 241 | ) 242 | ?.finish(); 243 | 244 | _startLoadTime = null; 245 | } 246 | 247 | /// Name of the module for identification in exceptions and debug messages. 248 | // ignore: unnecessary_getters_setters 249 | String get name => _defaultName; 250 | 251 | Map get _defaultTags => { 252 | 'span.kind': 'client', 253 | 'module.instance_id': _instanceId, 254 | }; 255 | 256 | /// Deprecated: the module name should be defined by overriding the getter in 257 | /// a subclass and it should not be mutable. 258 | @deprecated 259 | // ignore: unnecessary_getters_setters 260 | set name(String? newName) { 261 | if (newName != null) { 262 | _defaultName = newName; 263 | } 264 | } 265 | 266 | /// List of child components so that lifecycle can iterate over them as needed 267 | Iterable get childModules => _childModules.toList(); 268 | 269 | /// The [LifecycleModule] was loaded. 270 | /// 271 | /// Any error or exception thrown during the [LifecycleModule]'s 272 | /// [onLoad] call will be emitted. 273 | Stream get didLoad => _didLoadController.stream; 274 | 275 | /// A child [LifecycleModule] was loaded. 276 | /// 277 | /// Any error or exception thrown during the child [LifecycleModule]'s 278 | /// [onLoad] call will be emitted. 279 | /// 280 | /// Any error or exception thrown during the parent [LifecycleModule]'s 281 | /// [onDidLoadChildModule] call will be emitted. 282 | Stream get didLoadChildModule => 283 | _didLoadChildModuleController.stream; 284 | 285 | /// The [LifecycleModule] was resumed. 286 | /// 287 | /// Any error or exception thrown during the child [LifecycleModule]'s 288 | /// [resume] call will be emitted. 289 | /// 290 | /// Any error or exception thrown during the [LifecycleModule]'s 291 | /// [onResume] call will be emitted. 292 | Stream get didResume => _didResumeController.stream; 293 | 294 | /// The [LifecycleModule] was suspended. 295 | /// 296 | /// Any error or exception thrown during the child [LifecycleModule]'s 297 | /// [suspend] call will be emitted. 298 | /// 299 | /// Any error or exception thrown during the [LifecycleModule]'s 300 | /// [onSuspend] call will be emitted. 301 | Stream get didSuspend => _didSuspendController.stream; 302 | 303 | /// The [LifecycleModule] was unloaded. 304 | /// 305 | /// Any error or exception thrown during the child [LifecycleModule]'s 306 | /// [unload] call will be emitted. 307 | /// 308 | /// Any error or exception thrown during the [LifecycleModule]'s 309 | /// [onUnload] call will be emitted. 310 | Stream get didUnload => _didUnloadController.stream; 311 | 312 | /// A child [LifecycleModule] was unloaded. 313 | /// 314 | /// Any error or exception thrown during the child [LifecycleModule]'s 315 | /// [onUnload] call will be emitted. 316 | /// 317 | /// Any error or exception thrown during the parent [LifecycleModule]'s 318 | /// [onDidUnloadChildModule] call will be emitted. 319 | Stream get didUnloadChildModule => 320 | _didUnloadChildModuleController.stream; 321 | 322 | /// A child [LifecycleModule] is about to be loaded. 323 | /// 324 | /// Any error or exception thrown during the parent [LifecycleModule]'s 325 | /// [onDidLoadChildModule] call will be emitted. 326 | Stream get willLoadChildModule => 327 | _willLoadChildModuleController.stream; 328 | 329 | /// A child [LifecycleModule] is about to be unloaded. 330 | /// 331 | /// Any error or exception thrown during the parent [LifecycleModule]'s 332 | /// [onDidUnloadChildModule] call will be emitted. 333 | Stream get willUnloadChildModule => 334 | _willUnloadChildModuleController.stream; 335 | 336 | /// The [LifecycleModule] is about to be resumed. 337 | Stream get willResume => _willResumeController.stream; 338 | 339 | /// The [LifecycleModule] is about to be unloaded. 340 | Stream get willUnload => _willUnloadController.stream; 341 | 342 | /// The [LifecycleModule] is about to be loaded. 343 | Stream get willLoad => _willLoadController.stream; 344 | 345 | /// The [LifecycleModule] is about to be suspended. 346 | Stream get willSuspend => _willSuspendController.stream; 347 | 348 | /// Whether the module is currently instantiated. 349 | bool get isInstantiated => _state == LifecycleState.instantiated; 350 | 351 | /// Whether the module is currently loaded. 352 | bool get isLoaded => _state == LifecycleState.loaded; 353 | 354 | /// Whether the module is currently loading. 355 | bool get isLoading => _state == LifecycleState.loading; 356 | 357 | /// Whether the module is currently resuming. 358 | bool get isResuming => _state == LifecycleState.resuming; 359 | 360 | /// Whether the module is currently suspended. 361 | bool get isSuspended => _state == LifecycleState.suspended; 362 | 363 | /// Whether the module is currently suspending. 364 | bool get isSuspending => _state == LifecycleState.suspending; 365 | 366 | /// Whether the module is currently unloaded. 367 | bool get isUnloaded => _state == LifecycleState.unloaded; 368 | 369 | /// Whether the module is currently unloading. 370 | bool get isUnloading => _state == LifecycleState.unloading; 371 | 372 | //-------------------------------------------------------- 373 | // Public methods that can be used directly to trigger 374 | // module lifecycle / check current lifecycle state 375 | //-------------------------------------------------------- 376 | 377 | /// Disposes this module and all its disposable dependencies. 378 | /// 379 | /// If the module has only been instantiated and has not yet started loading 380 | /// or been loaded, then this will immediately dispose of the module. 381 | /// 382 | /// If the module has already started loading, has loaded, or is in any other 383 | /// "loaded" state (suspending, suspended, resuming), then this will attempt 384 | /// to unload the module before disposing. 385 | /// 386 | /// If the module has already started unloading, this will wait for that 387 | /// transition before disposing. 388 | /// 389 | /// If the module has already started disposing or has disposed, then this 390 | /// will return the [Future] from [didDispose]. (An unloaded module will have 391 | /// already started or finished disposal). 392 | /// 393 | /// In any of these cases where an unload is attemped prior to disposal, a 394 | /// failure during unload will be caught and logged, but will not stop 395 | /// disposal. A module who cancels unload via [onShouldUnload] or who throws 396 | /// during [onUnload] will still be disposed. 397 | /// 398 | /// In short, calling [dispose] forces the disposal of this module regardless 399 | /// of its current state and regardless of its ability to unload successfully. 400 | /// 401 | /// If the modules unload is canceled or if an error is thrown during a 402 | /// lifecycle handler like onUnload as a part of this disposal process, they 403 | /// will still be available via their corresponding lifecycle event streams 404 | /// (e.g. [didUnload]). 405 | /// 406 | /// The [Future] returned from this method will resolve when disposal has 407 | /// completed and will only resolve with an error if one is thrown during 408 | /// [onDispose]. 409 | @mustCallSuper 410 | @override 411 | Future dispose() => super.dispose(); 412 | 413 | /// Public method to trigger the loading of a Module. 414 | /// 415 | /// Calls the onLoad() method, which can be implemented on a Module. 416 | /// Executes the willLoad and didLoad event streams. 417 | /// 418 | /// Initiates the loading process when the module is in the instantiated 419 | /// state. If the module is in the loaded or loading state a warning is logged 420 | /// and the method is a noop. If the module is in any other state, a 421 | /// StateError is thrown. 422 | /// 423 | /// If an [Exception] is thrown during the call to [onLoad] it will be emitted 424 | /// on the [didLoad] lifecycle stream. The returned [Future] will also resolve 425 | /// with this exception. 426 | /// 427 | /// Note that [LifecycleModule] only supports one load/unload cycle. If [load] 428 | /// is called after a module has been unloaded, a [StateError] is thrown. 429 | Future load() { 430 | if (isOrWillBeDisposed) { 431 | return _buildDisposedOrDisposingResponse(methodName: 'load'); 432 | } 433 | 434 | if (isLoading || isLoaded) { 435 | return _buildNoopResponse( 436 | isTransitioning: isLoading, 437 | methodName: 'load', 438 | currentState: 439 | isLoading ? LifecycleState.loading : LifecycleState.loaded); 440 | } 441 | 442 | if (!isInstantiated) { 443 | return _buildIllegalTransitionResponse( 444 | reason: 'A module can only be loaded once.'); 445 | } 446 | 447 | _activeSpan = _startTransitionSpan('load'); 448 | _loadContext = _activeSpan?.context; 449 | _startLoadTime = _activeSpan?.startTime; 450 | 451 | _state = LifecycleState.loading; 452 | 453 | // Keep track of this load's completer 454 | final transition = Completer(); 455 | 456 | // because this one can get overwritten 457 | _transition = transition; 458 | 459 | _load().then(transition.complete).catchError((error, trace) { 460 | transition.completeError(error, trace); 461 | _activeSpan?.setTag('error', true); 462 | }).whenComplete(() { 463 | _activeSpan?.finish(); 464 | _activeSpan = null; 465 | }); 466 | 467 | return transition.future; 468 | } 469 | 470 | /// Public method to async load a child module and register it 471 | /// for lifecycle management. 472 | /// 473 | /// If an [Exception] is thrown during the call to the parent 474 | /// [onWillLoadChildModule] it will be emitted on the [willLoadChildModule] 475 | /// lifecycle stream. The returned [Future] will also resolve with this 476 | /// exception. 477 | /// 478 | /// If an [Exception] is thrown during the call to the child [onLoad] it will 479 | /// be emitted on the [didLoadChildModule] lifecycle stream. The returned 480 | /// [Future] will also resolve with this exception. 481 | /// 482 | /// If an [Exception] is thrown during the call to the parent 483 | /// [onDidLoadChildModule] it will be emitted on the [didLoadChildModule] 484 | /// lifecycle stream. The returned [Future] will also resolve with this 485 | /// exception. 486 | /// 487 | /// Attempting to load a child module after a module has been unloaded will 488 | /// throw a [StateError]. 489 | @protected 490 | Future loadChildModule(LifecycleModule? childModule) { 491 | if (isOrWillBeDisposed) { 492 | return _buildDisposedOrDisposingResponse(methodName: 'loadChildModule'); 493 | } 494 | 495 | if (childModule == null || _childModules.contains(childModule)) { 496 | return Future.value(null); 497 | } 498 | 499 | if (isUnloaded || isUnloading) { 500 | var stateLabel = isUnloaded ? 'unloaded' : 'unloading'; 501 | return Future.error( 502 | StateError('Cannot load child module when module is $stateLabel'), 503 | StackTrace.current); 504 | } 505 | 506 | final completer = Completer(); 507 | onWillLoadChildModule(childModule).then((_) async { 508 | // It is possible to reach this point due to the asynchrony of onWillLoadChildModule. 509 | // In that case, simply do not load the child module and instead dispose it. 510 | if (isUnloaded || isUnloading) { 511 | await childModule.dispose(); 512 | completer.complete(); 513 | return; 514 | } 515 | 516 | _willLoadChildModuleController.add(childModule); 517 | 518 | final childModuleWillUnloadSub = listenToStream( 519 | childModule.willUnload, _onChildModuleWillUnload, 520 | onError: _willUnloadChildModuleController.addError); 521 | final childModuleDidUnloadSub = listenToStream( 522 | childModule.didUnload, _onChildModuleDidUnload, 523 | onError: (error, stackTrace) => 524 | _didUnloadChildModuleController.addError); 525 | 526 | // The child module may not reach an unloaded state successfully, but 527 | // should always eventually be disposed. For this reason, we listen for 528 | // its disposal before removing it from the list of child modules. 529 | // ignore: unawaited_futures 530 | childModule.didDispose.then((_) { 531 | _childModules.remove(childModule); 532 | }); 533 | 534 | try { 535 | manageDisposable(childModule); 536 | _childModules.add(childModule); 537 | childModule.parentContext = _loadContext; 538 | 539 | await childModule.load(); 540 | try { 541 | await onDidLoadChildModule(childModule); 542 | } catch (error, stackTrace) { 543 | _logger.severe( 544 | 'Exception in onDidLoadChildModule ($name)', 545 | error, 546 | stackTrace, 547 | ); 548 | rethrow; 549 | } 550 | _didLoadChildModuleController.add(childModule); 551 | completer.complete(); 552 | } catch (error, stackTrace) { 553 | // If the child module failed to load, we can dispose of it and cleanup 554 | // any state/subscriptions related to it. 555 | _childModules.remove(childModule); 556 | await childModule.dispose(); 557 | await childModuleWillUnloadSub.cancel(); 558 | await childModuleDidUnloadSub.cancel(); 559 | 560 | _didLoadChildModuleController.addError(error, stackTrace); 561 | completer.completeError(error, stackTrace); 562 | } finally { 563 | childModule.parentContext = null; 564 | } 565 | }).catchError((Object error, StackTrace stackTrace) { 566 | _logger.severe( 567 | 'Exception in onWillLoadChildModule ($name)', 568 | error, 569 | stackTrace, 570 | ); 571 | _willLoadChildModuleController.addError(error, stackTrace); 572 | completer.completeError(error, stackTrace); 573 | }); 574 | 575 | return completer.future; 576 | } 577 | 578 | /// Provide a way for a module to update its children's parentContext that is compatible with mocking in 2.19. 579 | /// 580 | /// This is only intended for use within this file and is marked protected. 581 | @protected 582 | set parentContext(SpanContext? context) => _parentContext = context; 583 | 584 | /// Public method to suspend the module. 585 | /// 586 | /// Suspend indicates to the module that it should go into a low-activity 587 | /// state. For example, by disconnecting from backend services and unloading 588 | /// heavy data structures. 589 | /// 590 | /// Initiates the suspend process when the module is in the loaded state. If 591 | /// the module is in the suspended or suspending state a warning is logged and 592 | /// the method is a noop. If the module is in any other state, a StateError is 593 | /// thrown. 594 | /// 595 | /// The [Future] values of all children [suspend] calls will be awaited. The 596 | /// first child to return an error value will emit the error on the 597 | /// [didSuspend] lifecycle stream. The returned [Future] will also resolve 598 | /// with this exception. 599 | /// 600 | /// If an [Exception] is thrown during the call to [onSuspend] it will be 601 | /// emitted on the [didSuspend] lifecycle stream. The returned [Future] will 602 | /// also resolve with this exception. 603 | /// 604 | /// If an error or exception is thrown during the call to the parent 605 | /// [onSuspend] lifecycle method it will be emitted on the [didSuspend] 606 | /// lifecycle stream. The error will also be returned by [suspend]. 607 | Future suspend() { 608 | if (isOrWillBeDisposed) { 609 | return _buildDisposedOrDisposingResponse(methodName: 'suspend'); 610 | } 611 | 612 | if (isSuspended || isSuspending) { 613 | return _buildNoopResponse( 614 | isTransitioning: isSuspending, 615 | methodName: 'suspend', 616 | currentState: isSuspending 617 | ? LifecycleState.suspending 618 | : LifecycleState.suspended); 619 | } 620 | 621 | if (!(isLoaded || isLoading || isResuming)) { 622 | return _buildIllegalTransitionResponse( 623 | targetState: LifecycleState.suspended, 624 | allowedStates: [ 625 | LifecycleState.loaded, 626 | LifecycleState.loading, 627 | LifecycleState.resuming 628 | ]); 629 | } 630 | 631 | Future? pendingTransition; 632 | if (_transition != null && !_transition!.isCompleted) { 633 | pendingTransition = _transition!.future.then((_) { 634 | _activeSpan = _startTransitionSpan('suspend'); 635 | }); 636 | } else { 637 | _activeSpan = _startTransitionSpan('suspend'); 638 | } 639 | 640 | final transition = Completer(); 641 | _transition = transition; 642 | _state = LifecycleState.suspending; 643 | 644 | _suspend(pendingTransition) 645 | .then(transition.complete) 646 | .catchError((error, trace) { 647 | transition.completeError(error, trace); 648 | _activeSpan?.setTag('error', true); 649 | }).whenComplete(() { 650 | _activeSpan?.finish(); 651 | _activeSpan = null; 652 | }); 653 | 654 | return transition.future; 655 | } 656 | 657 | /// Public method to resume the module. 658 | /// 659 | /// This should put the module back into its normal state after the module 660 | /// was suspended. 661 | /// 662 | /// Only initiates the resume process when the module is in the suspended 663 | /// state. If the module is in the resuming state a warning is logged and the 664 | /// method is a noop. If the module is in any other state, a StateError is 665 | /// thrown. 666 | /// 667 | /// The [Future] values of all children [resume] calls will be awaited. The 668 | /// first child to return an error value will emit the error on the 669 | /// [didResume] lifecycle stream. The returned [Future] will also resolve with 670 | /// this exception. 671 | /// 672 | /// If an [Exception] is thrown during the call to [onResume] it will be 673 | /// emitted on the [didResume] lifecycle stream. The returned [Future] will 674 | /// also resolve with this exception. 675 | /// 676 | /// If an error or exception is thrown during the call to the parent 677 | /// [onResume] lifecycle method it will be emitted on the [didResume] 678 | /// lifecycle stream. The error will also be returned by [resume]. 679 | Future resume() { 680 | if (isOrWillBeDisposed) { 681 | return _buildDisposedOrDisposingResponse(methodName: 'resume'); 682 | } 683 | 684 | if (isLoaded || isResuming) { 685 | return _buildNoopResponse( 686 | isTransitioning: isResuming, 687 | methodName: 'resume', 688 | currentState: 689 | isResuming ? LifecycleState.resuming : LifecycleState.loaded); 690 | } 691 | 692 | if (!(isSuspended || isSuspending)) { 693 | return _buildIllegalTransitionResponse( 694 | targetState: LifecycleState.loaded, 695 | allowedStates: [LifecycleState.suspended, LifecycleState.suspending]); 696 | } 697 | 698 | Future? pendingTransition; 699 | if (_transition != null && !_transition!.isCompleted) { 700 | pendingTransition = _transition!.future.then((_) { 701 | _activeSpan = _startTransitionSpan('resume'); 702 | }); 703 | } else { 704 | _activeSpan = _startTransitionSpan('resume'); 705 | } 706 | 707 | _state = LifecycleState.resuming; 708 | final transition = Completer(); 709 | _transition = transition; 710 | 711 | _resume(pendingTransition) 712 | .then(transition.complete) 713 | .catchError((error, trace) { 714 | transition.completeError(error, trace); 715 | _activeSpan?.setTag('error', true); 716 | }).whenComplete(() { 717 | _activeSpan?.finish(); 718 | _activeSpan = null; 719 | }); 720 | 721 | return _transition!.future; 722 | } 723 | 724 | /// Public method to query the unloadable state of the Module. 725 | /// 726 | /// Calls the onShouldUnload() method, which can be implemented on a Module. 727 | /// onShouldUnload is also called on all registered child modules. 728 | ShouldUnloadResult shouldUnload() { 729 | // collect results from all child modules and self 730 | List shouldUnloads = []; 731 | for (var child in _childModules) { 732 | if (child.isUnloading || child.isUnloaded || child.isOrWillBeDisposed) { 733 | continue; 734 | } 735 | shouldUnloads.add(child.shouldUnload()); 736 | } 737 | shouldUnloads.add(onShouldUnload()); 738 | 739 | // aggregate into 1 combined result 740 | ShouldUnloadResult finalResult = ShouldUnloadResult(); 741 | for (var result in shouldUnloads) { 742 | if (!result.shouldUnload) { 743 | finalResult.shouldUnload = false; 744 | finalResult.messages.addAll(result.messages); 745 | } 746 | } 747 | return finalResult; 748 | } 749 | 750 | /// Public method to trigger the Module unload cycle. 751 | /// 752 | /// Calls shouldUnload(), and, if that completes successfully, continues to 753 | /// call onUnload() on the module and all registered child modules. If 754 | /// unloading is rejected, this method will complete with an error. The rejection 755 | /// error will not be added to the [didUnload] lifecycle event stream. 756 | /// 757 | /// Initiates the unload process when the module is in the loaded or suspended 758 | /// state. If the module is in the unloading or unloaded state a warning is 759 | /// logged and the method is a noop. If the module is in any other state, a 760 | /// StateError is thrown. 761 | /// 762 | /// The [Future] values of all children [unload] calls will be awaited. The 763 | /// first child to return an error value will emit the error on the 764 | /// [didUnload] lifecycle stream. The returned [Future] will also resolve with 765 | /// this exception. 766 | /// 767 | /// If an [Exception] is thrown during the call to [onUnload] it will be 768 | /// emitted on the [didUnload] lifecycle stream. The returned [Future] will 769 | /// also resolve with this exception. 770 | /// 771 | /// If an error or exception is thrown during the call to the parent 772 | /// [onUnload] lifecycle method it will be emitted on the [didUnload] 773 | /// lifecycle stream. The error will also be returned by [unload]. 774 | /// 775 | /// If the unload succeeds (i.e. is not canceled via [onShouldUnload] and is 776 | /// not prevented by an uncaught exception in [onUnload]), then this module 777 | /// will also be disposed. The [Future] returned by this method will resolve 778 | /// once unload _and_ disposal have completed. 779 | Future unload() { 780 | if (isUnloaded || isUnloading) { 781 | return _buildNoopResponse( 782 | isTransitioning: isUnloading, 783 | methodName: 'unload', 784 | currentState: 785 | isUnloading ? LifecycleState.unloading : LifecycleState.unloaded); 786 | } 787 | 788 | if (isOrWillBeDisposed) { 789 | return _buildDisposedOrDisposingResponse(methodName: 'unload'); 790 | } 791 | 792 | if (!(isLoaded || isLoading || isResuming || isSuspended || isSuspending)) { 793 | return _buildIllegalTransitionResponse( 794 | targetState: LifecycleState.unloaded, 795 | allowedStates: [ 796 | LifecycleState.loaded, 797 | LifecycleState.loading, 798 | LifecycleState.resuming, 799 | LifecycleState.suspended, 800 | LifecycleState.suspending 801 | ]); 802 | } 803 | 804 | Future? pendingTransition; 805 | if (_transition != null && !_transition!.isCompleted) { 806 | pendingTransition = _transition!.future; 807 | } 808 | 809 | _previousState = _state; 810 | _state = LifecycleState.unloading; 811 | final transition = Completer(); 812 | _transition = transition; 813 | 814 | var unloadAndDispose = Completer(); 815 | unloadAndDispose.complete(transition.future.then(((_) => dispose()))); 816 | transition 817 | .complete(_unload(pendingTransition?.then((value) => value as Null))); 818 | return unloadAndDispose.future; 819 | } 820 | 821 | //-------------------------------------------------------- 822 | // Methods that can be optionally implemented by subclasses 823 | // to execute code during certain phases of the module 824 | // lifecycle 825 | //-------------------------------------------------------- 826 | 827 | /// Custom logic to be executed during load. 828 | /// 829 | /// Initial data queries and interactions with the server can be triggered 830 | /// here. Returns a future with no payload that completes when the module has 831 | /// finished loading. 832 | @protected 833 | Future onLoad() async {} 834 | 835 | /// Custom logic to be executed when a child module is to be loaded. 836 | @protected 837 | Future onWillLoadChildModule(LifecycleModule module) async {} 838 | 839 | /// Custom logic to be executed when a child module has been loaded. 840 | @protected 841 | Future onDidLoadChildModule(LifecycleModule module) async {} 842 | 843 | /// Custom logic to be executed when a child module is to be unloaded. 844 | @protected 845 | Future onWillUnloadChildModule(LifecycleModule module) async {} 846 | 847 | /// Custom logic to be executed when a child module has been unloaded. 848 | @protected 849 | Future onDidUnloadChildModule(LifecycleModule module) async {} 850 | 851 | /// Custom logic to be executed during suspend. 852 | /// 853 | /// Server connections can be dropped and large data structures unloaded here. 854 | /// Nothing should be done here that cannot be undone in [onResume]. 855 | @protected 856 | Future onSuspend() async {} 857 | 858 | /// Custom logic to be executed during resume. 859 | /// 860 | /// Any changes made in [onSuspend] can be reverted here. 861 | @protected 862 | Future onResume() async {} 863 | 864 | /// Custom logic to be executed during shouldUnload (consequently also in unload). 865 | /// 866 | /// Returns a ShouldUnloadResult. 867 | /// [ShouldUnloadResult.shouldUnload == true] indicates that the module is safe to unload. 868 | /// [ShouldUnloadResult.shouldUnload == false] indicates that the module should not be unloaded. 869 | /// In this case, ShouldUnloadResult.messages contains a list of string messages indicating 870 | /// why unload was rejected. 871 | @protected 872 | ShouldUnloadResult onShouldUnload() { 873 | return ShouldUnloadResult(); 874 | } 875 | 876 | /// Custom logic to be executed during unload. 877 | /// 878 | /// Called on unload if shouldUnload completes with true. This can be used for 879 | /// cleanup. Returns a future with no payload that completes when the module 880 | /// has finished unloading. 881 | @protected 882 | Future onUnload() async {} 883 | 884 | @mustCallSuper 885 | @override 886 | @protected 887 | Future onWillDispose() async { 888 | if (isInstantiated || isUnloaded) { 889 | return; 890 | } 891 | 892 | try { 893 | Future unloadingTransitionFuture; 894 | if (isUnloading) { 895 | unloadingTransitionFuture = _transition!.future; 896 | } else { 897 | Future? pendingTransition; 898 | if (_transition != null && !_transition!.isCompleted) { 899 | pendingTransition = _transition?.future; 900 | } 901 | _previousState = _state; 902 | _state = LifecycleState.unloading; 903 | unloadingTransitionFuture = 904 | _unload(pendingTransition?.then((value) => value as Null)); 905 | } 906 | await unloadingTransitionFuture; 907 | } on ModuleUnloadCanceledException { 908 | // The unload was canceled, but disposal cannot be canceled. Log a warning 909 | // indicating this and continue with disposal. 910 | _logger.warning( 911 | '.dispose() was called but Module "$name" canceled its ' 912 | 'unload. The module will still be disposed.', 913 | null, 914 | StackTrace.current); 915 | } catch (error, stackTrace) { 916 | // An unexpected exception was thrown during unload. It will be emitted 917 | // as an error on the didUnload stream, but we will also log a warning 918 | // here explaining that disposal will still continue. 919 | _logger.warning( 920 | '.dispose() was called but Module "$name" threw an exception on ' 921 | 'unload. The module will still be disposed.', 922 | error, 923 | stackTrace); 924 | } 925 | } 926 | 927 | Future _buildDisposedOrDisposingResponse({required String methodName}) { 928 | _logger.warning('.$methodName() was called after Module "$name" had ' 929 | // ignore: deprecated_member_use 930 | 'already ${isOrWillBeDisposed ? 'started disposing' : 'disposed'}.'); 931 | return Future.error( 932 | StateError( 933 | 'Calling .$methodName() after disposal has started is not allowed.'), 934 | StackTrace.current); 935 | } 936 | 937 | /// Returns a new [Future] error with a constructed reason. 938 | Future _buildIllegalTransitionResponse( 939 | {LifecycleState? targetState, 940 | Iterable? allowedStates, 941 | String? reason}) { 942 | reason = reason ?? 943 | 'Only a module in the ' 944 | '${allowedStates!.map(_readableStateName).join(", ")} states can ' 945 | 'transition to ${_readableStateName(targetState)}'; 946 | return Future.error( 947 | StateError( 948 | 'Transitioning from $_state to $targetState is not allowed. $reason'), 949 | StackTrace.current); 950 | } 951 | 952 | Future _buildNoopResponse( 953 | {required String methodName, 954 | required LifecycleState currentState, 955 | required isTransitioning}) { 956 | _logger.config( 957 | '.$methodName() was called while Module "$name" is already ' 958 | '${_readableStateName(currentState)}; this is a no-op. Check for any ' 959 | 'unnecessary calls to .$methodName().', 960 | null, 961 | StackTrace.current); 962 | 963 | return _transition?.future ?? Future.value(null); 964 | } 965 | 966 | Future _load() async { 967 | try { 968 | _willLoadController.add(this); 969 | try { 970 | await onLoad(); 971 | } catch (error, stackTrace) { 972 | _logger.severe( 973 | 'Exception in onLoad ($name)', 974 | error, 975 | stackTrace, 976 | ); 977 | rethrow; 978 | } 979 | if (_state == LifecycleState.loading) { 980 | _state = LifecycleState.loaded; 981 | _transition = null; 982 | } 983 | _didLoadController.add(this); 984 | } catch (error, stackTrace) { 985 | _didLoadController.addError(error, stackTrace); 986 | rethrow; 987 | } 988 | } 989 | 990 | /// A utility to logging LifecycleModule lifecycle events 991 | void _logLifecycleEvents( 992 | String logLabel, Stream lifecycleEventStream) { 993 | listenToStream( 994 | lifecycleEventStream, (dynamic _) => _logger.finest(logLabel), 995 | onError: (error, stackTrace) => 996 | _logger.warning('$logLabel error: $error', error, stackTrace)); 997 | } 998 | 999 | /// Handles a child [LifecycleModule]'s [didUnload] event. 1000 | Future _onChildModuleDidUnload(LifecycleModule module) async { 1001 | try { 1002 | try { 1003 | await onDidUnloadChildModule(module); 1004 | } catch (error, stackTrace) { 1005 | _logger.severe( 1006 | 'Exception in onDidUnloadChildModule ($name)', 1007 | error, 1008 | stackTrace, 1009 | ); 1010 | rethrow; 1011 | } 1012 | _didUnloadChildModuleController.add(module); 1013 | } catch (error, stackTrace) { 1014 | _didUnloadChildModuleController.addError(error, stackTrace); 1015 | } 1016 | } 1017 | 1018 | /// Handles a child [LifecycleModule]'s [willUnload] event. 1019 | Future _onChildModuleWillUnload(LifecycleModule module) async { 1020 | try { 1021 | try { 1022 | await onWillUnloadChildModule(module); 1023 | } catch (error, stackTrace) { 1024 | _logger.severe( 1025 | 'Exception in onWillUnloadChildModule ($name)', 1026 | error, 1027 | stackTrace, 1028 | ); 1029 | rethrow; 1030 | } 1031 | _willUnloadChildModuleController.add(module); 1032 | } catch (error, stackTrace) { 1033 | _willUnloadChildModuleController.addError(error, stackTrace); 1034 | } 1035 | } 1036 | 1037 | /// Obtains the value of a [LifecycleState] enumeration. 1038 | String _readableStateName(LifecycleState? state) => '$state'.split('.')[1]; 1039 | 1040 | Future _resume(Future? pendingTransition) async { 1041 | try { 1042 | if (pendingTransition != null) { 1043 | await pendingTransition; 1044 | } 1045 | _willResumeController.add(this); 1046 | List> childResumeFutures = >[]; 1047 | for (var child in _childModules.toList()) { 1048 | childResumeFutures.add(Future.sync(() { 1049 | child.parentContext = _activeSpan?.context; 1050 | return child.resume().whenComplete(() { 1051 | child.parentContext = null; 1052 | }); 1053 | })); 1054 | } 1055 | await Future.wait(childResumeFutures); 1056 | try { 1057 | await onResume(); 1058 | } catch (error, stackTrace) { 1059 | _logger.severe( 1060 | 'Exception in onResume ($name)', 1061 | error, 1062 | stackTrace, 1063 | ); 1064 | rethrow; 1065 | } 1066 | if (_state == LifecycleState.resuming) { 1067 | _state = LifecycleState.loaded; 1068 | _transition = null; 1069 | } 1070 | _didResumeController.add(this); 1071 | } catch (error, stackTrace) { 1072 | _didResumeController.addError(error, stackTrace); 1073 | rethrow; 1074 | } 1075 | } 1076 | 1077 | Future _suspend(Future? pendingTransition) async { 1078 | try { 1079 | if (pendingTransition != null) { 1080 | await pendingTransition; 1081 | } 1082 | _willSuspendController.add(this); 1083 | List> childSuspendFutures = >[]; 1084 | for (var child in _childModules.toList()) { 1085 | childSuspendFutures.add(Future.sync(() async { 1086 | child.parentContext = _activeSpan?.context; 1087 | return child.suspend().whenComplete(() { 1088 | child.parentContext = null; 1089 | }); 1090 | })); 1091 | } 1092 | await Future.wait(childSuspendFutures); 1093 | try { 1094 | await onSuspend(); 1095 | } catch (error, stackTrace) { 1096 | _logger.severe( 1097 | 'Exception in onSuspend ($name)', 1098 | error, 1099 | stackTrace, 1100 | ); 1101 | rethrow; 1102 | } 1103 | if (_state == LifecycleState.suspending) { 1104 | _state = LifecycleState.suspended; 1105 | _transition = null; 1106 | } 1107 | _didSuspendController.add(this); 1108 | } catch (error, stackTrace) { 1109 | _didSuspendController.addError(error, stackTrace); 1110 | rethrow; 1111 | } 1112 | } 1113 | 1114 | Future _unload(Future? pendingTransition) async { 1115 | try { 1116 | if (pendingTransition != null) { 1117 | await pendingTransition; 1118 | } 1119 | 1120 | final shouldUnloadResult = shouldUnload(); 1121 | if (!shouldUnloadResult.shouldUnload) { 1122 | _state = _previousState; 1123 | _previousState = null; 1124 | _transition = null; 1125 | // reject with shouldUnload messages 1126 | throw ModuleUnloadCanceledException( 1127 | shouldUnloadResult.messagesAsString()); 1128 | } 1129 | 1130 | _activeSpan = _startTransitionSpan('unload'); 1131 | 1132 | _willUnloadController.add(this); 1133 | await Future.wait(_childModules.toList().map((child) { 1134 | child.parentContext = _activeSpan?.context; 1135 | return child.unload().timeout(maxChildUnloadDuration, onTimeout: () { 1136 | _logger.warning( 1137 | 'Child module may be stuck unloading: ${child.disposableTypeName}'); 1138 | }).whenComplete(() { 1139 | child.parentContext = null; 1140 | }); 1141 | })); 1142 | try { 1143 | await onUnload(); 1144 | } catch (error, stackTrace) { 1145 | _logger.severe('Exception in onUnload ($name)', error, stackTrace); 1146 | rethrow; 1147 | } 1148 | if (_state == LifecycleState.unloading) { 1149 | _state = LifecycleState.unloaded; 1150 | _previousState = null; 1151 | _transition = null; 1152 | } 1153 | _didUnloadController.add(this); 1154 | } on ModuleUnloadCanceledException catch (error, _) { 1155 | // In the event of a cancellation, rethrow the exception and allow the 1156 | // caller (either unload() or onWillDispose()) to handle it. 1157 | rethrow; 1158 | } catch (error, stackTrace) { 1159 | // In the event of a failed unload (the module threw an exception but did 1160 | // not explicitly cancel the unload), emit the unload failure event and 1161 | // then rethrow the exception so that the caller (either unload() or 1162 | // onWillDispose()) can handle it. 1163 | _didUnloadController.addError(error, stackTrace); 1164 | _activeSpan?.setTag('error', true); 1165 | rethrow; 1166 | } finally { 1167 | _activeSpan?.finish(); 1168 | _activeSpan = null; 1169 | } 1170 | } 1171 | } 1172 | 1173 | /// Exception thrown when unload fails. 1174 | class ModuleUnloadCanceledException implements Exception { 1175 | String message; 1176 | 1177 | ModuleUnloadCanceledException(this.message); 1178 | } 1179 | 1180 | /// A set of messages returned from the hierarchical application of shouldUnload 1181 | class ShouldUnloadResult { 1182 | bool shouldUnload; 1183 | List messages = []; 1184 | 1185 | ShouldUnloadResult([this.shouldUnload = true, String? message]) { 1186 | if (message != null) { 1187 | messages.add(message); 1188 | } 1189 | } 1190 | 1191 | bool call() => shouldUnload; 1192 | 1193 | String messagesAsString() { 1194 | return messages.join('\n'); 1195 | } 1196 | } 1197 | -------------------------------------------------------------------------------- /lib/src/module.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.src.module; 16 | 17 | import 'package:w_module/src/lifecycle_module.dart'; 18 | 19 | /// A [Module] encapsulates a well-scoped logical unit of functionality and 20 | /// exposes a discrete public interface for consumers. It extends 21 | /// [LifecycleModule] to ensure that it adheres to a well-defined lifecycle. 22 | /// 23 | /// The public interface of a [Module] is comprised of [api], [events], 24 | /// and [components]: 25 | /// - The [api] class exposes public methods that can be used to mutate or query 26 | /// module data. 27 | /// - The [events] class exposes streams that can be listened to for 28 | /// notification of internal module state change. 29 | /// - The [components] class exposes react-dart compatible UI components that 30 | /// can be used to render module data. 31 | abstract class Module extends LifecycleModule {} 32 | -------------------------------------------------------------------------------- /lib/src/simple_module.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library w_module.src.simple_module; 16 | 17 | /// A [SimpleModule] encapsulates a well-scoped logical unit of functionality and 18 | /// exposes a discrete public interface for consumers. 19 | /// 20 | /// The public interface of a [SimpleModule] is comprised of [api], [events], 21 | /// and [components]: 22 | /// - The [api] class exposes public methods that can be used to mutate or query 23 | /// module data. 24 | /// - The [events] class exposes streams that can be listened to for 25 | /// notification of internal module state change. 26 | /// - The [components] class exposes react-dart compatible UI components that 27 | /// can be used to render module data. 28 | abstract class SimpleModule { 29 | /// The [api] object should contain all public methods that a consumer can use 30 | /// to mutate module state (methods) or query existing module state (getters). 31 | /// 32 | /// [api] is initially null. If a module exposes a public [api], this should 33 | /// be overridden to provide a class defined specifically for the module. 34 | /// 35 | /// If using with w_flux internals, module mutation methods should usually 36 | /// dispatch existing actions available within the module. This ensures 37 | /// that the internal unidirectional data flow is maintained, regardless of 38 | /// the source of the mutation (e.g. external api or internal UI). Likewise, 39 | /// module methods that expose internal state should usually use existing 40 | /// getter methods available on stores within the module. 41 | Object? get api => null; 42 | 43 | /// The [components] object should contain all react-dart compatible UI 44 | /// component factory methods that a consumer can use to render module data. 45 | /// 46 | /// [components] is initially null. If a module exposes public [components], 47 | /// this should be overridden to provide a class defined specifically for the 48 | /// module. By convention, the custom [components] class should extend 49 | /// [ModuleComponents] to ensure that the default UI component is available 50 | /// via the module.components.content() method. 51 | /// 52 | /// If using with w_flux internals, [components] methods should usually return 53 | /// UI component factories that have been internally initialized with the 54 | /// proper actions and stores props. This ensures full functionality of the 55 | /// [components] without any external exposure of the requisite internal 56 | /// actions and stores. 57 | ModuleComponents? get components => null; 58 | 59 | /// The [events] object should contain all public streams that a consumer can 60 | /// listen to for notification of internal module state change. 61 | /// 62 | /// [events] is initially null. If a module exposes public [events], this 63 | /// should be overridden to provide a class defined specifically for the 64 | /// module. 65 | /// 66 | /// If using with w_flux internals, [events] should usually be dispatched by 67 | /// internal stores immediately prior to a corresponding trigger dispatch. 68 | /// [events] should NOT be dispatched directly by UI components or in 69 | /// immediate response to actions. This ensures that the internal 70 | /// unidirectional data flow is maintained and external [events] represent 71 | /// confirmed internal state changes. 72 | Object? get events => null; 73 | } 74 | 75 | /// Standard [ModuleComponents] class. If a module implements a custom class 76 | /// for its components, it should extend [ModuleComponents]. 77 | abstract class ModuleComponents { 78 | /// The default UI component 79 | Object? content() => null; 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/timing_specifiers.dart: -------------------------------------------------------------------------------- 1 | /// The type of 'startup timing metric' to be used by `specifyStartupTiming` 2 | class StartupTimingType { 3 | /// The `operationName` to be used for spans created using this [StartupTimingType]. 4 | final String operationName; 5 | 6 | const StartupTimingType._(this.operationName); 7 | 8 | /// Specifies that the module finished loading necessary data and is ready for user interaction. 9 | static const StartupTimingType firstUseful = 10 | StartupTimingType._('entered_first_useful_state'); 11 | } 12 | -------------------------------------------------------------------------------- /lib/w_module.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// The w_module library implements a module encapsulation and lifecycle 16 | /// pattern for Dart that interfaces well with the application architecture 17 | /// defined in the w_flux library. 18 | /// 19 | /// w_module defines how data should flow in and out of a module, how renderable 20 | /// UI is exposed to consumers, and establishes a common module lifecycle that 21 | /// facilitates dynamic loading / unloading of modules. 22 | library w_module; 23 | 24 | export 'package:w_module/src/event.dart'; 25 | export 'package:w_module/src/events_collection.dart'; 26 | export 'package:w_module/src/lifecycle_module.dart' 27 | hide LifecycleState, maxChildUnloadDuration; 28 | export 'package:w_module/src/module.dart'; 29 | export 'package:w_module/src/simple_module.dart'; 30 | export 'package:w_module/src/timing_specifiers.dart'; 31 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: w_module 2 | version: 3.0.9 3 | description: Base module classes with a well defined lifecycle for modular Dart applications. 4 | homepage: https://github.com/Workiva/w_module 5 | 6 | environment: 7 | sdk: '>=2.19.0 <3.0.0' 8 | 9 | dependencies: 10 | logging: ^1.0.0 11 | meta: ^1.16.0 12 | opentracing: ^1.0.1 13 | w_common: ^3.0.0 14 | 15 | dev_dependencies: 16 | build_runner: ^2.1.2 17 | build_test: ^2.1.3 18 | build_web_compilers: '>=3.0.0 <5.0.0' 19 | dart_dev: ^4.0.0 20 | dart_style: ^2.1.1 21 | matcher: ^0.12.10 22 | mocktail: ^1.0.3 23 | test: ^1.16.8 24 | workiva_analysis_options: ^1.4.1 -------------------------------------------------------------------------------- /test/event_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | @TestOn('browser') 16 | import 'dart:async'; 17 | 18 | import 'package:test/test.dart'; 19 | import 'package:w_module/w_module.dart'; 20 | 21 | void main() { 22 | group('Event', () { 23 | late Event event; 24 | late DispatchKey key; 25 | 26 | setUp(() { 27 | key = DispatchKey('test'); 28 | event = Event(key); 29 | }); 30 | 31 | test('should provide means to listen to the stream it was created from', 32 | () async { 33 | Completer completer = Completer(); 34 | 35 | event.listen((payload) { 36 | expect(payload, equals('trigger')); 37 | completer.complete(); 38 | }); 39 | 40 | event('trigger', key); 41 | return completer.future; 42 | }); 43 | 44 | test('should support other stream methods', () async { 45 | Completer completer = Completer(); 46 | 47 | // The point of this test is to exercise the `where` method which is made available 48 | // on an action by extending stream and overriding `listen` 49 | Stream filteredStream = event.where((value) => value == 'water'); 50 | filteredStream.listen(expectAsync1((payload) { 51 | expect(payload, equals('water')); 52 | completer.complete(); 53 | })); 54 | 55 | event('water', key); 56 | return completer.future; 57 | }); 58 | 59 | test('should only allow dispatch with correct key', () async { 60 | Completer completer = Completer(); 61 | 62 | event.listen((payload) { 63 | if (payload == 'bad') 64 | throw Exception( 65 | 'Should not be able to dispatch events without the correct key.'); 66 | if (payload == 'good') { 67 | completer.complete(); 68 | } 69 | }); 70 | 71 | // Create a new dispatch key that should not work for this event. 72 | DispatchKey incorrectKey = DispatchKey('incorrect'); 73 | 74 | expect(() { 75 | event('bad', incorrectKey); 76 | }, throwsArgumentError); 77 | event('good', key); 78 | 79 | await completer.future; 80 | }); 81 | 82 | test('should be closable', () async { 83 | expect(event.isClosed, isFalse); 84 | await event.close(key); 85 | expect(event.isClosed, isTrue); 86 | }); 87 | 88 | test('should only allow closing with correct key', () async { 89 | // Create a new dispatch key that should not work for this event. 90 | DispatchKey incorrectKey = DispatchKey('incorrect'); 91 | 92 | expect(event.close(incorrectKey), throwsArgumentError); 93 | }); 94 | 95 | test('should not allow events to be dispatched after being closed', 96 | () async { 97 | await event.close(key); 98 | expect(() { 99 | event('too late', key); 100 | }, throwsStateError); 101 | }); 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /test/events_collection_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | @TestOn('browser') 16 | import 'package:test/test.dart'; 17 | import 'package:w_module/w_module.dart'; 18 | 19 | final _key = DispatchKey('test'); 20 | 21 | class TestEvents extends EventsCollection { 22 | @override 23 | String get disposableTypeName => 'TestEvents'; 24 | 25 | final Event eventA = Event(_key); 26 | final Event eventB = Event(_key); 27 | 28 | TestEvents() : super(_key) { 29 | [ 30 | eventA, 31 | eventB, 32 | ].forEach(manageEvent); 33 | } 34 | } 35 | 36 | void main() { 37 | group('EventsCollection', () { 38 | test('manageEvent() should close Events when the collection is disposed', 39 | () async { 40 | final eventsCollection = TestEvents(); 41 | expect(eventsCollection.eventA.isClosed, isFalse); 42 | expect(eventsCollection.eventB.isClosed, isFalse); 43 | await eventsCollection.dispose(); 44 | expect(eventsCollection.eventA.isClosed, isTrue); 45 | expect(eventsCollection.eventB.isClosed, isTrue); 46 | }); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /test/module_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | @TestOn('browser') 16 | import 'package:test/test.dart'; 17 | import 'package:w_module/w_module.dart'; 18 | 19 | class TestModule extends Module { 20 | @override 21 | final String name = 'TestModule'; 22 | } 23 | 24 | void main() { 25 | group('Module', () { 26 | late TestModule module; 27 | 28 | setUp(() { 29 | module = TestModule(); 30 | }); 31 | 32 | test('should return null from api getter by default', () { 33 | expect(module.api, isNull); 34 | }); 35 | 36 | test('should return null from components getter by default', () { 37 | expect(module.components, isNull); 38 | }); 39 | 40 | test('should return null from events getter by default', () { 41 | expect(module.events, isNull); 42 | }); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /test/simple_module_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | @TestOn('browser') 16 | import 'package:test/test.dart'; 17 | 18 | import 'package:w_module/src/simple_module.dart'; 19 | 20 | class TestModule extends SimpleModule {} 21 | 22 | void main() { 23 | group('SimpleModuleModule', () { 24 | late TestModule simpleModule; 25 | 26 | setUp(() { 27 | simpleModule = TestModule(); 28 | }); 29 | 30 | test('should return null from api getter by default', () { 31 | expect(simpleModule.api, isNull); 32 | }); 33 | 34 | test('should return null from components getter by default', () { 35 | expect(simpleModule.components, isNull); 36 | }); 37 | 38 | test('should return null from events getter by default', () { 39 | expect(simpleModule.events, isNull); 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/test_tracer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:opentracing/opentracing.dart'; 3 | 4 | class TestSpan implements Span { 5 | static int _nextId = 0; 6 | final int _id = _nextId++; 7 | 8 | @override 9 | final List references; 10 | 11 | @override 12 | final Map tags; 13 | 14 | @override 15 | final List logData = []; 16 | 17 | @override 18 | final String operationName; 19 | 20 | @override 21 | late SpanContext context; 22 | 23 | @override 24 | DateTime? startTime; 25 | DateTime? _endTime; 26 | 27 | Completer? _whenFinished = Completer(); 28 | 29 | TestSpan( 30 | this.operationName, { 31 | SpanContext? childOf, 32 | List? references, 33 | DateTime? startTime, 34 | Map? tags, 35 | }) : this.startTime = startTime ?? DateTime.now(), 36 | this.tags = tags ?? {}, 37 | this.references = references ?? [] { 38 | if (childOf != null) { 39 | references!.add(Reference.childOf(childOf)); 40 | } 41 | setTag('span.kind', 'client'); 42 | 43 | final parent = parentContext; 44 | if (parent != null) { 45 | this.context = SpanContext(spanId: _id, traceId: parent.traceId); 46 | this.context.baggage.addAll(parent.baggage); 47 | } else { 48 | this.context = SpanContext(spanId: _id, traceId: _id); 49 | } 50 | } 51 | 52 | @override 53 | void addTags(Map newTags) => tags.addAll(newTags); 54 | 55 | @override 56 | Duration? get duration => _endTime?.difference(startTime!); 57 | 58 | @override 59 | DateTime? get endTime => _endTime; 60 | 61 | @override 62 | void finish({DateTime? finishTime}) { 63 | if (_whenFinished == null) { 64 | return; 65 | } 66 | 67 | _endTime = finishTime ?? DateTime.now(); 68 | _whenFinished!.complete(this); 69 | _whenFinished = null; 70 | } 71 | 72 | @override 73 | void log(String event, {dynamic payload, DateTime? timestamp}) => 74 | logData.add(LogData(timestamp ?? DateTime.now(), event, payload)); 75 | 76 | @override 77 | SpanContext? get parentContext => 78 | references.isEmpty ? null : references.first.referencedContext; 79 | 80 | @override 81 | void setTag(String tagName, dynamic value) => tags[tagName] = value; 82 | 83 | @override 84 | Future get whenFinished => _whenFinished!.future; 85 | 86 | @override 87 | String toString() { 88 | final sb = StringBuffer('SampleSpan('); 89 | sb 90 | ..writeln('traceId: ${context.traceId}') 91 | ..writeln('spanId: ${context.spanId}') 92 | ..writeln('operationName: $operationName') 93 | ..writeln('tags: ${tags.toString()}') 94 | ..writeln('startTime: ${startTime.toString()}'); 95 | 96 | if (_endTime != null) { 97 | sb 98 | ..writeln('endTime: ${endTime.toString()}') 99 | ..writeln('duration: ${duration.toString()}'); 100 | } 101 | 102 | if (logData.isNotEmpty) { 103 | sb.writeln('logData: ${logData.toString()}'); 104 | } 105 | 106 | if (references.isNotEmpty) { 107 | final reference = references.first; 108 | sb.writeln( 109 | 'reference: ${reference.referenceType} ${reference.referencedContext.spanId}'); 110 | } 111 | 112 | sb.writeln(')'); 113 | 114 | return sb.toString(); 115 | } 116 | } 117 | 118 | class TestTracer implements AbstractTracer { 119 | // There should only ever be one of these 120 | // ignore: close_sinks 121 | StreamController _finishController = StreamController.broadcast(); 122 | 123 | Stream get onSpanFinish => _finishController.stream; 124 | 125 | @override 126 | TestSpan startSpan( 127 | String operationName, { 128 | SpanContext? childOf, 129 | List? references, 130 | DateTime? startTime, 131 | Map? tags, 132 | }) { 133 | return TestSpan( 134 | operationName, 135 | childOf: childOf, 136 | references: references, 137 | startTime: startTime, 138 | tags: tags, 139 | )..whenFinished.then(_finishController.add); 140 | } 141 | 142 | @override 143 | Reference childOf(SpanContext context) => Reference.childOf(context); 144 | 145 | @override 146 | Reference followsFrom(SpanContext context) => Reference.followsFrom(context); 147 | 148 | @override 149 | SpanContext extract(String format, dynamic carrier) { 150 | throw UnimplementedError( 151 | 'Sample tracer for example purposes does not support advanced tracing behavior.'); 152 | } 153 | 154 | @override 155 | void inject(SpanContext spanContext, String format, dynamic carrier) { 156 | throw UnimplementedError( 157 | 'Sample tracer for example purposes does not support advanced tracing behavior.'); 158 | } 159 | 160 | @override 161 | Future flush() { 162 | return Future.value(null); 163 | } 164 | 165 | @override 166 | ScopeManager? scopeManager; 167 | 168 | @override 169 | Span? get activeSpan => scopeManager?.active?.span; 170 | } 171 | -------------------------------------------------------------------------------- /test/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:logging/logging.dart'; 2 | import 'package:matcher/matcher.dart'; 3 | 4 | /// Provides a Matcher for [LogRecord]. 5 | /// 6 | /// The [Matcher] only considers provided optional parameters. 7 | Matcher logRecord({Level? level, Matcher? message, Matcher? loggerName}) { 8 | var matchers = []; 9 | if (level != null) { 10 | matchers.add(_LogLevelMatcher(level)); 11 | } 12 | if (message != null) { 13 | matchers.add(_LogMessageMatcher(message)); 14 | } 15 | if (loggerName != null) { 16 | matchers.add(_LoggerNameMatcher(loggerName)); 17 | } 18 | 19 | return allOf(matchers); 20 | } 21 | 22 | class _LogLevelMatcher extends Matcher { 23 | final Level _level; 24 | 25 | _LogLevelMatcher(this._level); 26 | 27 | @override 28 | Description describe(Description description) => 29 | description.add('with $_level log level'); 30 | 31 | @override 32 | bool matches(dynamic record, Map matchState) { 33 | if (record is LogRecord) { 34 | return record.level == _level; 35 | } 36 | return false; 37 | } 38 | } 39 | 40 | class _LogMessageMatcher extends Matcher { 41 | final Matcher _messageMatcher; 42 | 43 | _LogMessageMatcher(this._messageMatcher); 44 | 45 | @override 46 | Description describe(Description description) => 47 | description.add('log message ').addDescriptionOf(_messageMatcher); 48 | 49 | @override 50 | bool matches(dynamic record, Map matchState) { 51 | if (record is LogRecord) { 52 | return _messageMatcher.matches(record.message, matchState); 53 | } 54 | return false; 55 | } 56 | } 57 | 58 | class _LoggerNameMatcher extends Matcher { 59 | final Matcher _nameMatcher; 60 | 61 | _LoggerNameMatcher(this._nameMatcher); 62 | 63 | @override 64 | Description describe(Description description) => 65 | description.add('logger name ').addDescriptionOf(_nameMatcher); 66 | 67 | @override 68 | bool matches(dynamic item, Map matchState) { 69 | if (item is LogRecord) { 70 | return _nameMatcher.matches(item.loggerName, matchState); 71 | } 72 | return false; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tool/dart_dev/config.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:dart_dev/dart_dev.dart'; 16 | 17 | final config = { 18 | ...coreConfig, 19 | 'analyze': AnalyzeTool()..useDartAnalyze = true, 20 | 'format': FormatTool()..formatter = Formatter.dartFormat, 21 | 'test': TestTool()..testArgs = ['--platform=chrome'], 22 | }; 23 | --------------------------------------------------------------------------------