Showing preview only (245K chars total). Download the full file or copy to clipboard to get everything.
Repository: matthiasn/systems-toolbox
Branch: master
Commit: 6bcc44af449b
Files: 71
Total size: 223.8 KB
Directory structure:
gitextract_ypcdeinq/
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── circle.yml
├── dev-resources/
│ └── logback-test.xml
├── doc/
│ ├── rationale.md
│ └── systems-thinking.md
├── examples/
│ ├── redux-counter01/
│ │ ├── .bowerrc
│ │ ├── .gitignore
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── bower.json
│ │ ├── env/
│ │ │ └── dev/
│ │ │ └── cljs/
│ │ │ └── example/
│ │ │ └── dev.cljs
│ │ ├── project.clj
│ │ ├── resources/
│ │ │ ├── logback.xml
│ │ │ └── public/
│ │ │ └── css/
│ │ │ └── example.css
│ │ └── src/
│ │ ├── clj/
│ │ │ └── example/
│ │ │ ├── core.clj
│ │ │ └── index.clj
│ │ └── cljs/
│ │ └── example/
│ │ ├── core.cljs
│ │ ├── counter_ui.cljs
│ │ └── store.cljs
│ └── trailing-mouse-pointer/
│ ├── .gitignore
│ ├── LICENSE
│ ├── README.md
│ ├── env/
│ │ └── dev/
│ │ └── cljs/
│ │ └── example/
│ │ └── dev.cljs
│ ├── project.clj
│ ├── resources/
│ │ ├── logback.xml
│ │ └── public/
│ │ └── css/
│ │ └── example.css
│ └── src/
│ ├── clj/
│ │ └── example/
│ │ ├── core.clj
│ │ └── index.clj
│ ├── cljc/
│ │ └── example/
│ │ ├── pointer.cljc
│ │ ├── server_switchboard.cljc
│ │ └── spec.cljc
│ └── cljs/
│ └── example/
│ ├── core.cljs
│ ├── hist_calc.cljs
│ ├── histogram.cljs
│ ├── observer.cljs
│ ├── re_frame.cljs
│ ├── store.cljs
│ ├── ui_histograms.cljs
│ ├── ui_info.cljs
│ ├── ui_mouse_moves.cljs
│ └── utils.cljs
├── perf/
│ └── matthiasn/
│ └── systems_toolbox/
│ └── runtime_perf_test.cljc
├── project.clj
├── src/
│ └── cljc/
│ └── matthiasn/
│ └── systems_toolbox/
│ ├── component/
│ │ ├── helpers.cljc
│ │ └── msg_handling.cljc
│ ├── component.cljc
│ ├── handler_utils.cljc
│ ├── log.cljc
│ ├── scheduler.cljc
│ ├── spec.cljc
│ ├── switchboard/
│ │ ├── helpers.cljc
│ │ ├── init.cljc
│ │ ├── observe.cljc
│ │ ├── route.cljc
│ │ └── spec.cljc
│ └── switchboard.cljc
└── test/
└── matthiasn/
└── systems_toolbox/
├── component_test.cljc
├── handler_utils_test.cljc
├── perf_runner.cljs
├── runner.cljs
├── scheduler_test.cljc
├── switchboard_observe_tests.cljc
├── switchboard_route_tests.cljc
├── system.cljc
├── system_test.cljc
├── test_promise.cljc
└── test_spec.cljc
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.hgignore
.hg/
.idea/
*.iml
.DS_Store
*.log
test2junit/
build.xml
resources/public/js/
out/
================================================
FILE: .travis.yml
================================================
language: clojure
script: lein doo node cljs-test once
jdk:
- oraclejdk11
================================================
FILE: CHANGELOG.md
================================================
# Change Log
All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/).
## [0.6.41] - 2019-05-09
### Changed
- dependencies
- core.async as dev dependency, needs to be specifically depended on
## [0.6.40] - 2019-05-09
### Changed
- missing spec
## [0.6.39] - 2019-05-09
### Changed
- `:schedule/new` and `:schedule/delete` message types
- dependencies
## [0.6.38] - 2018-12-19
### Changed
- Clojure 1.10.0
- dependencies
## [0.6.37] - 2018-07-08
### Changed
- dependencies
- tests with Clojure 1.10.0-alpha6
- adapted specs
## [0.6.36] - 2018-06-04
### Changed
- dependencies
## [0.6.35] - 2018-05-23
### Changed
- dependencies
## [0.6.34] - 2018-02-27
### Changed
- assign system-id, node-type, and node-id (useful for application clusters)
## [0.6.33] - 2018-02-05
### Changed
- component shutdown function
## [0.6.32] - 2018-01-24
### Changed
- deps
## [0.6.30] - 2018-01-24
### Changed
- improved metadata handling in scheduler
## [0.6.29] - 2018-01-05
### Changed
- record tag creation timestamp
## [0.6.28] - 2018-01-04
### Changed
- only keep last two component ids in cmp-seq when dispatched from scheduler
## [0.6.27] - 2017-11-28
### Changed
- specs for messages without payload
## [0.6.26] - 2017-11-15
### Changed
- Make UUID's pass uuid? predicate
- PR #48 from kamituel
## [0.6.25] - 2017-11-15
### Changed
- Clojure 1.9 RC1; deps
## [0.6.24] - 2017-10-26
### Changed
- filter identity on component maps set
## [0.6.23] - 2017-10-26
### Changed
- test with Clojure 1.9 beta 3
## [0.6.22] - 2017-10-24
### Changed
- expound messages on firehose
## [0.6.21] - 2017-10-13
### Changed
- expound for human friendly spec validation errors
## [0.6.20] - 2017-10-12
### Changed
- pull request #47 from kamituel merged
- see PR for details
## [0.6.19] - 2017-10-04
### Changed
- latest Clojure and ClojureScript in tests
## [0.6.18] - 2017-10-02
### Changed
- fixed cljs tests
## [0.6.17] - 2017-09-20
### Changed
- record processing time
## [0.6.16] - 2017-09-19
### Changed
- run test with Clojure 1.9.0-beta1
## [0.6.15] - 2017-09-15
### Changed
- wrapped put-fn
## [0.6.13] - 2017-09-03
### Changed
- cmp sequence in msg metadata
## [0.6.12] - 2017-09-01
### Changed
- shutdown when encountering any exception during component init
## [0.6.11] - 2017-08-24
### Changed
- latest dependencies
## [0.6.10] - 2017-08-01
### Changed
- latest deps
## [0.6.9] - 2017-05-30
### Changed
- Make switchboard's spec validation be configurable (from kamituel, PR #43)
- latest deps
- latest Clojure and ClojureScript after spec split
- replace all occurrences of `clojure.spec` with `clojure.spec.alpha`
- replace all occurrences of `cljs.spec` with `cljs.spec.alpha`
## [0.6.8] - 2017-04-25
### Changed
- Include message type in a log statement for invalid handler return. (from kamituel, PR #41)
## [0.6.7] - 2017-04-13
### Changed
- tests with latest ClojureScript
## [0.6.6] - 2017-03-15
### Changed
- tests with Clojure 1.9.0-alpha15
## [0.6.5] - 2017-02-24
### Changed
- latest core.async & other deps
## [0.6.4] - 2017-01-16
### Changed
- put-fn also accepts a vector of messages
## [0.6.3] - 2017-01-09
### Changed
- fixed NPE when using empty vector in :emit-msg
## [0.6.2] - 2016-11-25
### Changed
- Fix for issue #38
## [0.6.1] - 2016-11-23
### Changed
- moving away from alpha status
- Clojure 1.9 just works, and because of clojure.spec, you should adopt it, too
## [0.6.1-alpha11] - 2016-11-12
### Changed
- improved error handling
## [0.6.1-alpha10] - 2016-11-09
### Changed
- shutdown handler in scheduler
## [0.6.1-alpha9] - 2016-11-02
### Changed
- improvements in scheduler
## [0.6.1-alpha8] - 2016-10-13
### Changed
- latest dependencies
## [0.6.1-alpha7] - 2016-09-21
### Changed
- tests with Clojure 1.9.0-alpha12
- latest core.async
## [0.6.1-alpha6] - 2016-08-24
### Changed
- tests with Clojure 1.9.0-alpha11
## [0.6.1-alpha5] - 2016-08-20
### Changed
- improved error handling in msg-handler-loop
## [0.6.1-alpha4] - 2016-08-01
### Changed
- Formatting
- Firehose message handling improved
## [0.6.1-alpha3] - 2016-08-01
### Changed
- Firehose message ID added
## [0.6.1-alpha2] - 2016-07-12
### Changed
- Clojure 1.9.0-alpha10
## [0.6.1-alpha1] - 2016-07-06
### BREAKING CHANGES
- Clojure 1.9 required
- component IDs MUST be namespaced keywords
- message types MUST be namespaced keywords
## [0.5.22] - 2016-06-06
### Changed
- support multiple messages in `send-to-self` and `emit-msg`. `emit-msgs` deprecated
## [0.5.20] - 2016-06-04
### Changed
- dependencies; no dependency on specific Clojure version
## [0.5.19] - 2016-05-28
### Changed
- components support `observed-xform` function: applied to observed snapshot before resetting the local observed state
## [0.5.18] - 2016-05-11
### Changed
- `:emit-msgs` and `:send-to-self` in return map of handler
## [0.5.15] - 2016-03-30
### Changed
- Clojure 1.8, ClojureScript 1.8.40, dependencies
## [0.5.14] - 2016-03-08
### Changed
- handler functions can now be free from side effects
## [0.5.13] - 2016-03-05
### Changed
- additional tests; checking for overhead introduced by library
## [0.5.12] - 2016-03-04
### Changed
- library now testable on JVM and browser
## [0.5.11] - 2016-03-03
### Changed
- performance improvements in browser by using requestAnimationFrame more sparingly
## [0.5.10] - 2016-02-21
### Changed
- some refactoring and additional test
## [0.5.9] - 2016-02-19
### Changed
- broader use of blocking put; additional test
## [0.5.8] - 2016-02-17
### Changed
- send-msg blocks by default; component tests
## [0.5.7] - 2016-01-29
### Changed
- PR from clyfe: stoppable scheduler
## [0.5.1] - 2016-01-03
### Changed
- Sente and Reagent/UI components moved into separate repos
## [0.4.11] - 2015-12-30
### Changed
- Kafka consumer and producer moved from systems-toolbox into separate repo
## [0.4.10] - 2015-12-29
### Changed
- allow specifying a user-id-fn for sente (from PR)
## [0.4.9] - 2015-12-27
### Changed
- Kafka producer and consumer components
## [0.4.8] - 2015-12-22
### Changed
- fwd-as-w-meta function from PR
## [0.4.7] - 2015-12-22
### Changed
- allow user-specified state-pub-handler in views, for example for resetting state on logout
## [0.4.6] - 2015-12-21
### Changed
- fn for sending message to single component
## [0.4.5] - 2015-12-17
### Changed
- alternative sente config; version bumps
## [0.4.2] - 2015-12-07
### Changed
- minor histogram improvements
## [0.4.1] - 2015-12-05
### BREAKING CHANGES
- state-fn now needs to return a map, where the fresh component state is expected under the `:state` key. In addition, an optional `:shutdown-fn` can be specified, which will be called when the component is shutdown or restarted. This is for example useful when resources such as web servers or database connections need to be shut down.
## [0.3.15] - 2015-12-03
### Changed
- documentation; dependency upgrades; Clojure 1.8.0-RC3 in sample
## [0.3.14] - 2015-11-30
### Changed
- histogram more configurable
## [0.3.12] - 2015-11-26
### Changed
- more reasonable x-axis increments
## [0.3.11] - 2015-11-09
### Changed
- latest version of ClojureScript (much faster compiles)
## [0.3.10] - 2015-10-31
### Changed
- simplified component wiring
## [0.3.9] - 2015-10-30
### Changed
- allow passing of middleware to the sente component, besides the index-page-fn
## [0.3.8] - 2015-10-28
### Changed
- version bumps, including core.async v0.2.371
## [0.3.7] - 2015-10-26
### Changed
- fix for exception when putting too many messages at once
## [0.3.6] - 2015-10-15
### Changed
- optional message filtering via predicate function (see example)
## [0.3.1] - 2015-10-13
### Changed
- component initialization can now also be handled by the switchboard. See commit message and sample.
## [0.2.31] - 2015-10-12
### Changed
- unhandled handler: this function is called for each message that is not handled by another handler in the :handler-map of a component.
## [0.2.30] - 2015-09-23
### Changed
- version bumps
## [0.2.29] - 2015-08-29
### Changed
- Buffer WS messages until the connection is opened.
## [0.2.28] - 2015-08-28
### Changed
- host and port configuration via environment variables (e.g. for use with Docker)
## [0.2.27] - 2015-08-21
### Changed
- dependency updates & web server options
## [0.2.26] - 2015-08-10
### Changed
- Immutant-web instead of http-kit: as far as open source projects go, http-kit does not look very healthy.
- Less logging: the log component is probably not very useful at all. There's no good reason not to use 'conventional' logging inside handler code. Being able to inspect input and output messages without recompile should greatly reduce the need for logging anyway.
## [0.2.25] - 2015-08-08
### Changed
- refactoring, more readable component namespace
## [0.2.24] - 2015-08-06
### Changed
- recording the sequence of handling components
- dependency bumps
## [0.2.23] - 2015-08-06
### Changed
- UUIDs sent as strings
## [0.2.22] - 2015-08-05
### Changed
- Only non-firehose messages are wrapped when putting on firehose channel
## [0.2.21] - 2015-08-04
### Changed
- messages that are sent between component started and system completely wired are kept
## [0.2.20] - 2015-07-30
### Changed
- Expose metadata explicitly in firehose messages
## [0.2.19] - 2015-07-30
### Changed
- Reagent components now support :lifecycle-callbacks parameter that can be used to attach React components' lifecycle methods such as :component-will-update.
## [0.2.18] - 2015-07-28
### Changed
- When a message is first emitted, a :tag UUID is attached to the metadata, which allows tracking a message on its way through the system. Also, a correlation UUID is attached, which uniquely marks an emitted message.
- The full sequence of components that a message passes through is recorded on the metadata.
## [0.2.17] - 2015-07-23
### Changed
- View components can call init-fn on initialization. This can for example be useful when attaching a watcher to the local state atom.
## [0.2.16] - 2015-07-21
### Changed
- no requirement for scheduler id, default is the keyword in the first position inside message to be sent
- switchboard prints component state for inspection when receiving [cmd/print-cmp-state cmp-id] message
- simplification of and documentation for `route-handler`
## [0.2.15] - 2015-07-09
### Changed
- minor rewrites after 'lein kibit'
## [0.2.14] - 2015-07-09
### Changed
- Documentation
## [0.2.13] - 2015-07-08
### Changed
- Reloadable components without modification, e.g. for use with Figwheel
- Specify components that can't be reloaded, e.g. WebSockets connection component
## [0.2.10] - 2015-07-06
### Changed
- Better error handling and logging in message handler loops
- Using aviso/pretty for exception logging in example
## [0.2.9] - 2015-07-05
### Changed
- Figwheel in example
- Publish state snapshot on reload
## [0.2.8] - 2015-07-03
### Changed
- Reader conditionals instead of the now-deprecated CLJX
## [0.2.7] - 2015-07-01
### Changed
- Clojure 1.7 final instead of release candidate
- observer component tweaks
## [0.2.6] - 2015-06-24
### Changed
- Custom state snapshot transformer function used in switchboard. With that, routing snapshots to server no longer fails. Minimal functionality, state snapshots from switchboard should include more information, not just component keys.
- Observer component feeds entirely off of messages on firehose; no need for subscribing to switchboard state.s
## [0.2.5] - 2015-06-23
### Changed
- Custom state snapshot function: allows stripping state of functions, channels and the like. Particularly useful in switchboard where state snapshots could otherwise not traverse the WebSockets connection between client and server.
- Should the full state with non-serializable values such as channels ever be needed, there could still be a message for retrieving the full state map.
## [0.2.4] - 2015-06-19
### Changed
- Enable handler maps inside UI components. This allows for UI components that are more independent and don't require an external store/state component. This feature can be useful for components that do not share any state with other components.
================================================
FILE: LICENSE
================================================
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of the initial Contributor, the initial code and
documentation distributed under this Agreement, and
b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate from and are
distributed by that particular Contributor. A Contribution 'originates' from
a Contributor if it was added to the Program by such Contributor itself or
anyone acting on such Contributor's behalf. Contributions do not include
additions to the Program which: (i) are separate modules of software
distributed in conjunction with the Program under their own license
agreement, and (ii) are not derivative works of the Program.
"Contributor" means any person or entity that distributes the Program.
"Licensed Patents" mean patent claims licensable by a Contributor which are
necessarily infringed by the use or sale of its Contribution alone or when
combined with the Program.
"Program" means the Contributions distributed in accordance with this
Agreement.
"Recipient" means anyone who receives the Program under this Agreement,
including all Contributors.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free copyright license to
reproduce, prepare derivative works of, publicly display, publicly perform,
distribute and sublicense the Contribution of such Contributor, if any, and
such derivative works, in source code and object code form.
b) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free patent license under
Licensed Patents to make, use, sell, offer to sell, import and otherwise
transfer the Contribution of such Contributor, if any, in source code and
object code form. This patent license shall apply to the combination of the
Contribution and the Program if, at the time the Contribution is added by the
Contributor, such addition of the Contribution causes such combination to be
covered by the Licensed Patents. The patent license shall not apply to any
other combinations which include the Contribution. No hardware per se is
licensed hereunder.
c) Recipient understands that although each Contributor grants the licenses
to its Contributions set forth herein, no assurances are provided by any
Contributor that the Program does not infringe the patent or other
intellectual property rights of any other entity. Each Contributor disclaims
any liability to Recipient for claims brought by any other entity based on
infringement of intellectual property rights or otherwise. As a condition to
exercising the rights and licenses granted hereunder, each Recipient hereby
assumes sole responsibility to secure any other intellectual property rights
needed, if any. For example, if a third party patent license is required to
allow Recipient to distribute the Program, it is Recipient's responsibility
to acquire that license before distributing the Program.
d) Each Contributor represents that to its knowledge it has sufficient
copyright rights in its Contribution, if any, to grant the copyright license
set forth in this Agreement.
3. REQUIREMENTS
A Contributor may choose to distribute the Program in object code form under
its own license agreement, provided that:
a) it complies with the terms and conditions of this Agreement; and
b) its license agreement:
i) effectively disclaims on behalf of all Contributors all warranties and
conditions, express and implied, including warranties or conditions of title
and non-infringement, and implied warranties or conditions of merchantability
and fitness for a particular purpose;
ii) effectively excludes on behalf of all Contributors all liability for
damages, including direct, indirect, special, incidental and consequential
damages, such as lost profits;
iii) states that any provisions which differ from this Agreement are offered
by that Contributor alone and not by any other party; and
iv) states that source code for the Program is available from such
Contributor, and informs licensees how to obtain it in a reasonable manner on
or through a medium customarily used for software exchange.
When the Program is made available in source code form:
a) it must be made available under this Agreement; and
b) a copy of this Agreement must be included with each copy of the Program.
Contributors may not remove or alter any copyright notices contained within
the Program.
Each Contributor must identify itself as the originator of its Contribution,
if any, in a manner that reasonably allows subsequent Recipients to identify
the originator of the Contribution.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain responsibilities with
respect to end users, business partners and the like. While this license is
intended to facilitate the commercial use of the Program, the Contributor who
includes the Program in a commercial product offering should do so in a
manner which does not create potential liability for other Contributors.
Therefore, if a Contributor includes the Program in a commercial product
offering, such Contributor ("Commercial Contributor") hereby agrees to defend
and indemnify every other Contributor ("Indemnified Contributor") against any
losses, damages and costs (collectively "Losses") arising from claims,
lawsuits and other legal actions brought by a third party against the
Indemnified Contributor to the extent caused by the acts or omissions of such
Commercial Contributor in connection with its distribution of the Program in
a commercial product offering. The obligations in this section do not apply
to any claims or Losses relating to any actual or alleged intellectual
property infringement. In order to qualify, an Indemnified Contributor must:
a) promptly notify the Commercial Contributor in writing of such claim, and
b) allow the Commercial Contributor tocontrol, and cooperate with the
Commercial Contributor in, the defense and any related settlement
negotiations. The Indemnified Contributor may participate in any such claim
at its own expense.
For example, a Contributor might include the Program in a commercial product
offering, Product X. That Contributor is then a Commercial Contributor. If
that Commercial Contributor then makes performance claims, or offers
warranties related to Product X, those performance claims and warranties are
such Commercial Contributor's responsibility alone. Under this section, the
Commercial Contributor would have to defend claims against the other
Contributors related to those performance claims and warranties, and if a
court requires any other Contributor to pay any damages as a result, the
Commercial Contributor must pay those damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
appropriateness of using and distributing the Program and assumes all risks
associated with its exercise of rights under this Agreement , including but
not limited to the risks and costs of program errors, compliance with
applicable laws, damage to or loss of data, programs or equipment, and
unavailability or interruption of operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of the
remainder of the terms of this Agreement, and without further action by the
parties hereto, such provision shall be reformed to the minimum extent
necessary to make such provision valid and enforceable.
If Recipient institutes patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Program itself
(excluding combinations of the Program with other software or hardware)
infringes such Recipient's patent(s), then such Recipient's rights granted
under Section 2(b) shall terminate as of the date such litigation is filed.
All Recipient's rights under this Agreement shall terminate if it fails to
comply with any of the material terms or conditions of this Agreement and
does not cure such failure in a reasonable period of time after becoming
aware of such noncompliance. If all Recipient's rights under this Agreement
terminate, Recipient agrees to cease use and distribution of the Program as
soon as reasonably practicable. However, Recipient's obligations under this
Agreement and any licenses granted by Recipient relating to the Program shall
continue and survive.
Everyone is permitted to copy and distribute copies of this Agreement, but in
order to avoid inconsistency the Agreement is copyrighted and may only be
modified in the following manner. The Agreement Steward reserves the right to
publish new versions (including revisions) of this Agreement from time to
time. No one other than the Agreement Steward has the right to modify this
Agreement. The Eclipse Foundation is the initial Agreement Steward. The
Eclipse Foundation may assign the responsibility to serve as the Agreement
Steward to a suitable separate entity. Each new version of the Agreement will
be given a distinguishing version number. The Program (including
Contributions) may always be distributed subject to the version of the
Agreement under which it was received. In addition, after a new version of
the Agreement is published, Contributor may elect to distribute the Program
(including its Contributions) under the new version. Except as expressly
stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
licenses to the intellectual property of any Contributor under this
Agreement, whether expressly, by implication, estoppel or otherwise. All
rights in the Program not expressly granted under this Agreement are
reserved.
This Agreement is governed by the laws of the State of New York and the
intellectual property laws of the United States of America. No party to this
Agreement will bring a legal action under this Agreement more than one year
after the cause of action arose. Each party waives its rights to a jury trial
in any resulting litigation.
================================================
FILE: README.md
================================================
# systems-toolbox
Applications are systems. Systems are fascinating entities, and one of their characteristics is that we can observe them. Read more about that **[here](doc/systems-thinking.md)**. Also make sure you read about the **[rationale](doc/rationale.md)** behind this library.
[](https://jarkeeper.com/matthiasn/systems-toolbox)
## What's in the box?
This library helps you build distributed systems. Such a larger system could, for example, consist of multiple processes in different **JVM**, plus all connected browser instances, which, if you think about it, are an important part of the overall distributed systems. Going forward, there is also support planned for native apps. Except for a different presentation layer, the required code should be the exact same as for single-page web applications.
This library only contains the bare minimum for building and wiring systems. Additional functionality can be found in these repositories:
* **[systems-toolbox-ui](https://github.com/matthiasn/systems-toolbox-ui)**: This library gives you a simple way to build user interfaces using **[Reagent](https://github.com/reagent-project/reagent)**. This provided functionality is somewhat comparable to **[React](https://facebook.github.io/react/)** & **[Redux](https://github.com/reactjs/redux)**. I'll elaborate on this soon. This library powers the user interfaces of the example applications.
* **[systems-toolbox-sente](https://github.com/matthiasn/systems-toolbox-sente)**: This library connects browser-based subsystems with a backend system using WebSockets. This library contains everything you need for serving your application via **HTTP**, including support for **HTTPS**, **HTTP2**, deployment in application servers, and serving **REST** resources. This library serves the sample applications and provides the communication with their respective backends.
* **[systems-toolbox-kafka](https://github.com/matthiasn/systems-toolbox-kafka)**: This library connects different systems via **[Kafka](http://kafka.apache.org/)** so that they can form a larger, distributed system.
* **[systems-toolbox-metrics](https://github.com/matthiasn/systems-toolbox-metrics)**: This library is a small example for how functionality can be implemented across subsystems. Here, we have a server-side component which gathers some stats about the host and JVM, plus a UI "widget" that can be embedded in a **systems-toolbox-ui** interface. You can see it in use in both example applications.
## Artifacts
Artifacts are [released to Clojars](https://clojars.org/matthiasn/systems-toolbox).
With Leiningen, add the following dependency to your `project.clj`:
[](https://clojars.org/matthiasn/systems-toolbox)
In addition, you also need to add the dependency for [core.async](https://mvnrepository.com/artifact/org.clojure/core.async), e.g. with Leiningen:
[org.clojure/core.async "0.6.532"]
## Testing
This library targets both **Clojure** and **Clojurescript** and is written entirely in `.clc`. Accordingly, testing needs to happen on both the **JVM** and at least one of the **JS** runtimes out there. For testing on the JVM, you simply run:
$ lein test
On the JavaScript side, you have more options, for example:
$ lein doo node cljs-test once
Instead of `once`, you can also use `auto` to run the tests automatically when changes are detected. For more information about the options, check out the documentation for **[doo](https://github.com/bensu/doo)**.
Both ways of testing run automatically on each new commit. On the **JVM**, we use **CircleCI**: [](https://circleci.com/gh/matthiasn/systems-toolbox)
On **TravisCI**, the tests then run in a **JS** environment, on **[Node.js](https://nodejs.org/)**: [](https://travis-ci.org/matthiasn/systems-toolbox)
Check out the `circle.yml` and `.travis.yml` files when you need an example for how to set up your projects with these providers.
## Examples
Right now, there are two example applications:
* There's an **[example project](https://github.com/matthiasn/systems-toolbox/tree/master/examples/trailing-mouse-pointer)** in this repository that visualizes WebSocket round trip delay by recording mouse moves and showing two circles at the latest mouse position. One of them is driven by a message that only makes a local round trip in the web application, and the other one is driven by a message that is sent to the server, counted and sent back to the client. Thus, you will see the delay introduced by the by the client-server-client round trip immediately when you move the mouse. Also, there are some histograms for visualizing where time is spent. There's a live example of this application **[here](http://systems-toolbox.matthiasnehlsen.com/)**.

* Then, there's the toy example I mentioned above, **[BirdWatch](https://github.com/matthiasn/BirdWatch)**. This application provided the inspiration for this library. A running demo instance can be seen **[here](http://birdwatch.matthiasnehlsen.com)**.

## Feedback and question
Please feel free to open issues here or in any of the related projects when you have a question. Also, you can send the author an email, but issues are generally preferred as more users would benefit from the resulting discussion. Also, there's a chat on [](https://gitter.im/matthiasn/systems-toolbox?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge).
## Contributions
Contributions always welcome! Unless it's nothing more than the fix of a typo though, the best approach is to start with an issue and a brief discussion of the problem or improvement, rather than start the process with a pull request. Cheers.
## Project maturity
This project is quite young and APIs may still change. However, you can expect that minor version bumps do not break your existing system.
## License
Copyright © 2015, 2016 Matthias Nehlsen
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.
================================================
FILE: circle.yml
================================================
machine:
java:
version: openjdk8
test:
override:
- lein test2junit
post:
- ant
================================================
FILE: dev-resources/logback-test.xml
================================================
<!-- Logback configuration. See http://logback.qos.ch/manual/index.html -->
<configuration scan="true" scanPeriod="10 seconds">
<!-- Simple file output -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- encoder defaults to ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- rollover daily -->
<fileNamePattern>logs/systems-toolbox-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- or whenever the file size reaches 64 MB -->
<maxFileSize>64 MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- Safely log to the same file from multiple JVMs. Degrades performance! -->
<prudent>true</prudent>
</appender>
<!-- Console output -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoder defaults to ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
</encoder>
<!-- Only log level INFO and above -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
</appender>
<!-- Enable FILE and STDOUT appenders for all log messages.
By default, only log at level INFO and above. -->
<root level="INFO">
<!-- appender-ref ref="FILE" / -->
<appender-ref ref="STDOUT" />
</root>
<logger name="matthiasn.systems-toolbox-kafka" level="ALL"/>
</configuration>
================================================
FILE: doc/rationale.md
================================================
## Rationale
Some time ago, I wrote this toy application called **[BirdWatch](http://github.com/matthiasn/BirdWatch)**. The first and very basic version was using **[Scala](http://www.scala-lang.org/)** on the server side and **[Knockout](https://github.com/knockout/knockout)** on the client. The next version was then using **[AngularJS](https://angularjs.org/)** on the client side, followed by another client in **[React](https://facebook.github.io/react/)**.js. Then, I fell in love with **[Clojure](http://clojure.org/)**. So I wrote the application again, this time with the backend written in Clojure and the frontend written in **[ClojureScript](https://github.com/clojure/clojurescript)**. It was the first application I had ever written in Clojure. While it worked well, it was a bit of an entangled mess (and hard to maintain). When I discovered Stuart Sierra's component library, I thought this could be a way to get more structure into my system. But it wasn't solving many of the problems that I had. Instead, I wanted to build a different kind of system, one that is primarily messaging-driven, spans multiple machines (such as web client and a server, or also multiple machines on the server side) and that uses **[core.async](https://github.com/clojure/core.async)** for message conveyance. I thought there must be another way to do it, but I didn't find an existing library. Also, at the time, the component library only worked on the server side, whereas I thought that communicating subsystems are a universal thing, not only something that one finds on a server. So I thought, why not write a library that solves my problems, one that works on both client and server? Then came along a consulting gig that allowed me to explore the problem while we wrote a commercial application with it.
## Assumptions
Unsurprisingly, the **systems-toolbox** library makes a few assumptions:
* A system is made out of **subsystems**, which communicate by sending each other immutable messages via **[core.async](https://github.com/clojure/core.async)** channels, which conceptually can be seen as conveyor belts. You WILL have to either watch Rich Hickey's **[talk](http://www.infoq.com/presentations/clojure-core-async)** on this subject or read the **[transcript](https://github.com/matthiasn/talk-transcripts/blob/master/Hickey_Rich/CoreAsync.md)** -- or both. Seriously, do that NOW.
* Hey, welcome back, what do you think about the talk? I think the conveyor belt metaphor is fascinating and may well be more appropriate than in **[other places](http://www.jfs.tku.edu.tw/wp-content/uploads/2014/01/121-E01.pdf)** where it is apparently used as well. Also, I feel the implementation is ready for production usage. However, I think one piece is missing, at least when you follow the metaphor. Let me explain. Say we have a factory or the luggage transportation system in an airport and there's indeed a conveyor belt. The decoupling that comes from using this mechanism (or any other queue, for that matter) is essential for larger systems that don't make you want to pull your hair out. There's one difference, though. The conveyor belt is observable, without interfering with it -- **[core.async](https://github.com/clojure/core.async)** is not.
* **Black boxes** aren't as useful as I used to think. The chances are that a portion of the taxes you pay goes into some very expensive contraptions such as CERNs **[Large Hadron Collider](http://home.cern/topics/large-hadron-collider)** that help us get a better understanding how particles interact, and rightfully so. In natural sciences, it is considered a good thing to look inside stuff (like atoms) and not stop just because someone tells you that you have to respect the borders of a black box. And yet in computer (science), we are supposed to accept that? Come on. When I come into a new project, I would like to look into all the parts of the system and see how they massage the data flowing through that very system. I do understand that implementation details of subsystems may not be important, but at least I need to understand what goes in and what goes out. And this is exactly where I think core.async falls short of the promise of building better systems. Why does the channel need to be a black box that I cannot inspect? I want to be able to see what goes onto a channel, and I want to see what is taken off a channel, just like we a can in the factory with the conveyor belt, without interfering with the system any more than necessary. Of course, everything I just mentioned only works in the realm of **functional programming**. Black boxes make a whole lot more sense when you need to protect mutable state inside objects.
* Subsystems or components wired by the **systems-toolbox** are observable, they emit all messages onto a **firehose** channel, including state changes, without requiring a single extra line of code. This firehose channel contains all the messages flowing through a system, just like the **[Twitter Firehose](https://dev.twitter.com/streaming/firehose)** contains all the tweets flowing through Twitter.
* Subsystems can **send** and **receive** messages. These messages are like sending your tax declaration to the IRS. You expect a response at some point, but you don't know when. Or, to stay more on topic, these are like the messages one layer below TCP. When your computer sends a datagram out, you don't know yet if anyone will respond. Maybe, who knows. If not and a timeout is reached, you will have to deal with that. One might think of this as a limitation, but I have not found that to be the case yet when building actual applications with it. Any reliable transport out there is best just effort plus timeouts plus retries and an eventual exception.
* I like building systems with user interfaces. Therefore, this library also provides the building blocks for user interfaces. This part of the library is opinionated towards **[Reagent](https://github.com/reagent-project/reagent)**, as I like writing DOM subtrees in **[Hiccup](https://github.com/weavejester/hiccup)**. However, it should be simple to write building blocks for the other wrappers for React out there. If I had more time, I'd probably write those.
================================================
FILE: doc/systems-thinking.md
================================================
# Systems Thinking
Applications are systems; however, don't take my word for it. Let's see how an expert on **Systems Thinking** defines a system:
> "A system isn't just any old collection of things. A system is an interconnected set of elements that is coherently organized in a way that achieves something. If you look at that definition closely for a minute, you can see that a system must consist of three kinds of things: elements, interconnections, and a function or purpose." - Meadows, Donatella H. (2008) Thinking in Systems: A Primer, Page 11
This applies to every meaningful application I've ever written. How do we get closer to understanding such a system? Again, here's a quote:
> "The behavior of a system is its performance over time--its growth, stagnation, decline, oscillation, randomness, or evolution." - Meadows, Donatella H. (2008) Thinking in Systems: A Primer, Page 88
Her remarks make sense. The code itself is just the blueprint for the system. Code is like the blueprint for a busy train station versus the actual train station. You won't see where, exactly, congestion will occur until you observe bottlenecks in the living system. We need to observe and monitor a running system to understand it better.
At the same time, when writing applications using **[core.async](https://github.com/clojure/core.async)**, I find myself dealing with building blocks time and time again that have little to do with the observable logic. I've seen this with **[BirdWatch](https://github.com/matthiasn/BirdWatch)**, **[inspect](https://github.com/matthiasn/inspect)**, or also an **AngularJS markup to Hiccup conversion tool** (not published). I repeatedly wrote **[channels](http://clojure.github.io/core.async/#clojure.core.async/chan)** and **[go-loops](http://clojure.github.io/core.async/#clojure.core.async/go-loop)**. These are just incidental complexity and orthogonal to what the application is trying to solve.
Instead, there should be more high-level building blocks that only handle incoming messages and potentially emit messages, as well. If you think that I am referring to the **[actor model](http://en.wikipedia.org/wiki/Actor_model)**, not so fast. Yes, **actors** have desirable properties, but I don't like that they need to know where to send messages.
So here's my idea: there are message switchboards that connect to components because we wire them inside the switchboard logic and route messages depending on the kind of message. Other than a namespaced keyword that describes the payload (potentially while checking the compliance with a schema), the switchboards do not care about the payload at all.
Switchboards then dispatch messages to the connected components. These components process messages, for example, by publishing a document to a database or answering a query or whatnot; or, such components could be additional, cascaded switchboards that, again, route messages to other components.
Message routing should be possible by matching namespaced keywords either exactly or with wildcard matches to allow for the utmost flexibility.
I want to start and wire components at compile time. I also want to fire up components, wire them in a switchboard, disconnect them, or shut them down during run time. A system is a living thing, so I should be able to modify its behavior whenever I feel like it.
Also, observability needs to be an integral part of the system from the first moment on, not as an afterthought. As we can learn from the quote above, a system expresses a behavior over time. Since we want to leverage this behavior to get a better insight into the system, what could be of interest? I think waiting times until a message is getting processed and processing time for each message are suitable candidates, as well as the development of these metrics over time. Also, once we know how long it took to process an individual message, we may also want to know what the message itself was. The system should harvest these data points by default. We don't necessarily need to persist every message, but at least recent messages should be available for close inspection. The number of these depends on the available resources at any given point in time.
Combine this with a built-in visualizer of the information flow. Since the structure of the application and its flow is nothing but data, we can take advantage of the **[SVG](http://en.wikipedia.org/wiki/Scalable_Vector_Graphics)** drawing capability of **[ReactJS](http://facebook.github.io/react/)** and **[Reagent](http://reagent-project.github.io)**. Any visualization always reflects the status quo of the structure of the system. When I fire up a new component at run time, this should be reflected immediately. Then, for each visualized component, there are gauges and charts that display how the components behave, both now and in the past. Also, the user interface displays incoming and outflowing data structures as desired.
These observation tools should put us in an excellent place for understanding a running system by observing its behavior. Then, we can learn more about our systems, both under real load and under simulated load, and determine where, exactly, additional effort is well spent.
Components could, for example, as already suggested, take care of database lookups. Also, they could provide a bi-directional connection between client and server over a WebSockets connection. Yet another kind of component could facilitate communication between two JVMs, e.g. using **[Kafka](http://kafka.apache.org)**, **[RabbitMQ](http://www.rabbitmq.com)**, **[Redis](http://redis.io)**, or **[HornetQ](http://hornetq.jboss.org)**.
Other components encapsulate application state and surrounding business logic. The only way to interact with the application state is via messages, where it is entirely up to the state handling logic how to deal with those messages. Inside, the state is kept in an atom but this atom is not freely passed around. Only the dereferenced application state is sent back to the connected switchboard when a change occurs.
Other components can render the received data as HTML using ReactJS and Reagent and emit messages back to the Switchboard when the user clicks a button, or when any other kind of interaction with the UI occurs. The state handling components can then react to the event, or the switchboard forwards a query to the server; this totally depends on how we wire up the switchboard for the particular message type.
================================================
FILE: examples/redux-counter01/.bowerrc
================================================
{
"directory": "resources/public/bower_components"
}
================================================
FILE: examples/redux-counter01/.gitignore
================================================
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.idea
*.iml
.DS_Store
resources/public/js/build/
resources/public/bower_components/
*.pid
*.out
================================================
FILE: examples/redux-counter01/LICENSE
================================================
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of the initial Contributor, the initial code and
documentation distributed under this Agreement, and
b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate from and are
distributed by that particular Contributor. A Contribution 'originates' from
a Contributor if it was added to the Program by such Contributor itself or
anyone acting on such Contributor's behalf. Contributions do not include
additions to the Program which: (i) are separate modules of software
distributed in conjunction with the Program under their own license
agreement, and (ii) are not derivative works of the Program.
"Contributor" means any person or entity that distributes the Program.
"Licensed Patents" mean patent claims licensable by a Contributor which are
necessarily infringed by the use or sale of its Contribution alone or when
combined with the Program.
"Program" means the Contributions distributed in accordance with this
Agreement.
"Recipient" means anyone who receives the Program under this Agreement,
including all Contributors.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free copyright license to
reproduce, prepare derivative works of, publicly display, publicly perform,
distribute and sublicense the Contribution of such Contributor, if any, and
such derivative works, in source code and object code form.
b) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free patent license under
Licensed Patents to make, use, sell, offer to sell, import and otherwise
transfer the Contribution of such Contributor, if any, in source code and
object code form. This patent license shall apply to the combination of the
Contribution and the Program if, at the time the Contribution is added by the
Contributor, such addition of the Contribution causes such combination to be
covered by the Licensed Patents. The patent license shall not apply to any
other combinations which include the Contribution. No hardware per se is
licensed hereunder.
c) Recipient understands that although each Contributor grants the licenses
to its Contributions set forth herein, no assurances are provided by any
Contributor that the Program does not infringe the patent or other
intellectual property rights of any other entity. Each Contributor disclaims
any liability to Recipient for claims brought by any other entity based on
infringement of intellectual property rights or otherwise. As a condition to
exercising the rights and licenses granted hereunder, each Recipient hereby
assumes sole responsibility to secure any other intellectual property rights
needed, if any. For example, if a third party patent license is required to
allow Recipient to distribute the Program, it is Recipient's responsibility
to acquire that license before distributing the Program.
d) Each Contributor represents that to its knowledge it has sufficient
copyright rights in its Contribution, if any, to grant the copyright license
set forth in this Agreement.
3. REQUIREMENTS
A Contributor may choose to distribute the Program in object code form under
its own license agreement, provided that:
a) it complies with the terms and conditions of this Agreement; and
b) its license agreement:
i) effectively disclaims on behalf of all Contributors all warranties and
conditions, express and implied, including warranties or conditions of title
and non-infringement, and implied warranties or conditions of merchantability
and fitness for a particular purpose;
ii) effectively excludes on behalf of all Contributors all liability for
damages, including direct, indirect, special, incidental and consequential
damages, such as lost profits;
iii) states that any provisions which differ from this Agreement are offered
by that Contributor alone and not by any other party; and
iv) states that source code for the Program is available from such
Contributor, and informs licensees how to obtain it in a reasonable manner on
or through a medium customarily used for software exchange.
When the Program is made available in source code form:
a) it must be made available under this Agreement; and
b) a copy of this Agreement must be included with each copy of the Program.
Contributors may not remove or alter any copyright notices contained within
the Program.
Each Contributor must identify itself as the originator of its Contribution,
if any, in a manner that reasonably allows subsequent Recipients to identify
the originator of the Contribution.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain responsibilities with
respect to end users, business partners and the like. While this license is
intended to facilitate the commercial use of the Program, the Contributor who
includes the Program in a commercial product offering should do so in a
manner which does not create potential liability for other Contributors.
Therefore, if a Contributor includes the Program in a commercial product
offering, such Contributor ("Commercial Contributor") hereby agrees to defend
and indemnify every other Contributor ("Indemnified Contributor") against any
losses, damages and costs (collectively "Losses") arising from claims,
lawsuits and other legal actions brought by a third party against the
Indemnified Contributor to the extent caused by the acts or omissions of such
Commercial Contributor in connection with its distribution of the Program in
a commercial product offering. The obligations in this section do not apply
to any claims or Losses relating to any actual or alleged intellectual
property infringement. In order to qualify, an Indemnified Contributor must:
a) promptly notify the Commercial Contributor in writing of such claim, and
b) allow the Commercial Contributor tocontrol, and cooperate with the
Commercial Contributor in, the defense and any related settlement
negotiations. The Indemnified Contributor may participate in any such claim
at its own expense.
For example, a Contributor might include the Program in a commercial product
offering, Product X. That Contributor is then a Commercial Contributor. If
that Commercial Contributor then makes performance claims, or offers
warranties related to Product X, those performance claims and warranties are
such Commercial Contributor's responsibility alone. Under this section, the
Commercial Contributor would have to defend claims against the other
Contributors related to those performance claims and warranties, and if a
court requires any other Contributor to pay any damages as a result, the
Commercial Contributor must pay those damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
appropriateness of using and distributing the Program and assumes all risks
associated with its exercise of rights under this Agreement , including but
not limited to the risks and costs of program errors, compliance with
applicable laws, damage to or loss of data, programs or equipment, and
unavailability or interruption of operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of the
remainder of the terms of this Agreement, and without further action by the
parties hereto, such provision shall be reformed to the minimum extent
necessary to make such provision valid and enforceable.
If Recipient institutes patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Program itself
(excluding combinations of the Program with other software or hardware)
infringes such Recipient's patent(s), then such Recipient's rights granted
under Section 2(b) shall terminate as of the date such litigation is filed.
All Recipient's rights under this Agreement shall terminate if it fails to
comply with any of the material terms or conditions of this Agreement and
does not cure such failure in a reasonable period of time after becoming
aware of such noncompliance. If all Recipient's rights under this Agreement
terminate, Recipient agrees to cease use and distribution of the Program as
soon as reasonably practicable. However, Recipient's obligations under this
Agreement and any licenses granted by Recipient relating to the Program shall
continue and survive.
Everyone is permitted to copy and distribute copies of this Agreement, but in
order to avoid inconsistency the Agreement is copyrighted and may only be
modified in the following manner. The Agreement Steward reserves the right to
publish new versions (including revisions) of this Agreement from time to
time. No one other than the Agreement Steward has the right to modify this
Agreement. The Eclipse Foundation is the initial Agreement Steward. The
Eclipse Foundation may assign the responsibility to serve as the Agreement
Steward to a suitable separate entity. Each new version of the Agreement will
be given a distinguishing version number. The Program (including
Contributions) may always be distributed subject to the version of the
Agreement under which it was received. In addition, after a new version of
the Agreement is published, Contributor may elect to distribute the Program
(including its Contributions) under the new version. Except as expressly
stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
licenses to the intellectual property of any Contributor under this
Agreement, whether expressly, by implication, estoppel or otherwise. All
rights in the Program not expressly granted under this Agreement are
reserved.
This Agreement is governed by the laws of the State of New York and the
intellectual property laws of the United States of America. No party to this
Agreement will bring a legal action under this Agreement more than one year
after the cause of action arose. Each party waives its rights to a jury trial
in any resulting litigation.
================================================
FILE: examples/redux-counter01/README.md
================================================
# systems-toolbox-ui - Redux-style counter example

## Usage
You can start the server side application as usual:
$ lein run
Note that in this step, it does not do anything other than serve a client-side application. This will run the application on **[http://localhost:8888/](http://localhost:8888/)**. You can also change the port using and environment variable:
$ PORT=9999 lein run
Then, you need to compile the **ClojureScript**:
$ lein cljsbuild auto release
This will compile the ClojureScript into JavaScript using `:advanced` optimization.
You can also use **[lein-figwheel](https://github.com/bhauman/lein-figwheel)** to automatically recompile and reload the application when you code changes:
$ lein figwheel
Try for example changing the `inc-handler` after creating a few counter with different number. You'll see that each click increments the counter by 1. Then change the function as follows and DON'T reload the page:
```
(defn inc-handler
"Handler for incrementing specific counter"
[{:keys [current-state msg-payload]}]
{:new-state (update-in current-state [:counters (:counter msg-payload)] #(+ % 11))})
```
Figwheel will reload the application automatically, while the *[systems-toolbox](https://github.com/matthiasn/systems-toolbox)* ensures that each component will retain its previous state. Then click any `inc` button and you'll see that now it increments the counter by 11. This reload also works when changing the CSS.
## License
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.
================================================
FILE: examples/redux-counter01/bower.json
================================================
{
"name": "trailing-mouse-pointer",
"version": "0.1.0",
"homepage": "https://github.com/matthiasn/systems-toolbox/examples",
"authors": [
"Matthias Nehlsen <matthias.nehlsen@gmail.com>"
],
"description": "Tool for visualizing WebSocket Latency.",
"license": "Apache",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"app/bower_components",
"test",
"tests"
],
"dependencies": {
"pure": "~0.5.0"
}
}
================================================
FILE: examples/redux-counter01/env/dev/cljs/example/dev.cljs
================================================
(ns ^:figwheel-no-load example.dev
(:require [example.core :as c]
[figwheel.client :as figwheel :include-macros true]))
(enable-console-print!)
(defn jscb []
(c/init))
(figwheel/watch-and-reload
:websocket-url "ws://localhost:3452/figwheel-ws"
:jsload-callback jscb)
(c/init)
================================================
FILE: examples/redux-counter01/project.clj
================================================
(defproject matthiasn/redux-counter01 "0.6.1-SNAPSHOT"
:description "Sample application built with systems-toolbox library"
:url "https://github.com/matthiasn/systems-toolbox"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.10.1"]
[org.clojure/clojurescript "1.10.597"]
[org.clojure/core.async "0.6.532"]
[hiccup "1.0.5"]
[re-frame "0.10.9"]
[clj-pid "0.1.2"]
[ch.qos.logback/logback-classic "1.2.3"]
[org.clojure/tools.logging "0.5.0"]
[matthiasn/systemd-watchdog "0.1.3"]
[matthiasn/systems-toolbox "0.6.41"]
[matthiasn/systems-toolbox-sente "0.6.32"]]
:source-paths ["src/clj/"]
:clean-targets ^{:protect false} ["resources/public/js/build/" "target/"]
:main example.core
:plugins [[lein-cljsbuild "1.1.7"]
[lein-figwheel "0.5.19"]]
:figwheel {:server-port 3452
:css-dirs ["resources/public/css"]}
:profiles {:uberjar {:aot :all
:auto-clean false}}
:cljsbuild
{:builds
[{:id "dev"
:source-paths ["src/cljs" "env/dev/cljs"]
:figwheel true
:compiler {:main "example.dev"
:asset-path "js/build"
:optimizations :none
:output-dir "resources/public/js/build/"
:output-to "resources/public/js/build/example.js"
:source-map true}}
{:id "release"
:source-paths ["src/cljs"]
:compiler {:main "example.core"
:asset-path "js/build"
:output-to "resources/public/js/build/example.js"
:optimizations :advanced}}]})
================================================
FILE: examples/redux-counter01/resources/logback.xml
================================================
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%-20.20thread] %-5level %25.25logger{25} - %msg%n</pattern>
</encoder>
</appender>
<!-- Errors -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- Only ERROR or above should go into this file -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover -->
<fileNamePattern>/tmp/s-t-redux.error.log.%d{yyyy-MM-dd}</fileNamePattern>
<!-- keep 30 days' worth of history -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{dd-MM-yy HH:mm:ss.SSS} [%-20.20thread] %-5level %25.25logger{25} - %msg%n</pattern>
<immediateFlush>true</immediateFlush>
</encoder>
</appender>
<!-- The rest (messages without markers or with markers other than MESSAGE) -->
<appender name="UNCATEGORIZED_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/tmp/s-t-redux.misc.log.%d{yyyy-MM-dd}</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{dd-MM-yy HH:mm:ss.SSS} [%-20.20thread] %-5level %25.25logger{25} %class - %msg%n</pattern>
<immediateFlush>true</immediateFlush>
</encoder>
</appender>
<logger name="io.undertow" level="warn"/>
<logger name="org.xnio.nio" level="warn"/>
<logger name="matthiasn.systems-toolbox" level="info"/>
<logger name="iwaswhere-web.store" level="debug"/>
<logger name="matthiasn.systems-toolbox-sente" level="info"/>
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ERROR_FILE"/>
<appender-ref ref="UNCATEGORIZED_FILE"/>
</root>
</configuration>
================================================
FILE: examples/redux-counter01/resources/public/css/example.css
================================================
.counters {
margin: 20px;
font-family: sans-serif;
color: #555;
}
.counters button {
font-weight: 300;
color: #777;
}
.counters h1 {
font-weight: 300;
margin-bottom: 0.3em;
}
.counters pre {
margin-bottom: 2em;
}
================================================
FILE: examples/redux-counter01/src/clj/example/core.clj
================================================
(ns example.core
(:require [matthiasn.systems-toolbox.switchboard :as sb]
[matthiasn.systems-toolbox-sente.server :as sente]
[example.index :as idx]
[clojure.tools.logging :as log]
[clj-pid.core :as pid]
[matthiasn.systemd-watchdog.core :as wd])
(:gen-class))
(defonce switchboard (sb/component :server/switchboard))
(defn restart!
"Starts or restarts system by asking switchboard to fire up the provided
ws-cmp, a scheduler component and the ptr component, which handles and counts
messages about mouse moves."
[]
(sb/send-mult-cmd
switchboard
[[:cmd/init-comp (sente/cmp-map :server/ws-cmp idx/sente-map)]]))
(defn -main
"Starts the application from command line, saves and logs process ID. The
system that is fired up when restart! is called proceeds in core.async's
thread pool. Since we don't want the application to exit just because the
current thread is out of work after startup, we just put it to sleep."
[& args]
(pid/save "example.pid")
(pid/delete-on-shutdown! "example.pid")
(log/info "Application started, PID" (pid/current))
(restart!)
(wd/start-watchdog! 5000)
(Thread/sleep Long/MAX_VALUE))
================================================
FILE: examples/redux-counter01/src/clj/example/index.clj
================================================
(ns example.index
(:require [hiccup.core :refer [html]]))
(defn index-page
"Generates index page HTML."
[_]
(html
[:html
{:lang "en"}
[:head
[:meta {:name "viewport" :content "width=device-width, minimum-scale=1.0"}]
[:title "Counter"]
[:link {:href "/css/example.css" :rel "stylesheet"}]]
[:body
[:div#counter]
[:script {:src "/js/build/example.js"}]]]))
(def sente-map
"Configuration map for sente-cmp."
{:index-page-fn index-page
:port 8764
:relay-types #{}})
================================================
FILE: examples/redux-counter01/src/cljs/example/core.cljs
================================================
(ns example.core
(:require [example.store :as store]
[example.counter-ui :as cnt]
[matthiasn.systems-toolbox.switchboard :as sb]))
(enable-console-print!)
(defonce switchboard (sb/component :client/switchboard))
(defn init
[]
(sb/send-mult-cmd
switchboard
[[:cmd/init-comp (cnt/cmp-map :client/cnt-cmp)]
[:cmd/init-comp (store/cmp-map :client/store-cmp)]
[:cmd/route {:from :client/cnt-cmp :to :client/store-cmp}]
[:cmd/observe-state {:from :client/store-cmp :to :client/cnt-cmp}]]))
(init)
================================================
FILE: examples/redux-counter01/src/cljs/example/counter_ui.cljs
================================================
(ns example.counter-ui
(:require [reagent.core :as r]
[re-frame.core :refer [reg-sub subscribe]]
[re-frame.db :as rdb]
[cljs.pprint :as pp]))
(reg-sub :state (fn [db _] db))
(defn pp-div [current-state]
[:pre [:code (with-out-str (pp/pprint current-state))]])
(defn counter-view
"Renders individual counter view, with buttons for increasing or decreasing
the value."
[idx v put-fn]
[:div
[:h1 v]
[:button {:on-click #(put-fn [:cnt/dec {:counter idx}])} "dec"]
[:button {:on-click #(put-fn [:cnt/inc {:counter idx}])} "inc"]])
(defn counters-view
"Renders counters view which observes the state held by the state component.
Contains two buttons for adding or removing counters, plus a counter-view
for every element in the observed state."
[put-fn]
(let [state (subscribe [:state])]
(fn counters-view-render [put-fn]
(let [current-state @state
indexed (map-indexed vector (:counters current-state))]
[:div.counters
[pp-div current-state]
[:button {:on-click #(put-fn [:cnt/remove])} "remove"]
[:button {:on-click #(put-fn [:cnt/add])} "add"]
(for [[idx v] indexed]
^{:key idx} [counter-view idx v put-fn])]))))
(defn state-fn
"Renders main view component and wires the central re-frame app-db as the
observed component state, which will then be updated whenever the store-cmp
changes."
[put-fn]
(r/render [counters-view put-fn] (.getElementById js/document "counter"))
{:observed rdb/app-db})
(defn cmp-map
[cmp-id]
{:cmp-id cmp-id
:state-fn state-fn})
================================================
FILE: examples/redux-counter01/src/cljs/example/store.cljs
================================================
(ns example.store
"In this namespace, the app state is managed. One can only interact with the
state by sending immutable messages. Each such message is then handled by a
handler function. These handler functions here are pure functions, they
receive message and previous state and return the new state.
Both the messages passed around and the new state returned by the handlers
are validated using clojure.spec. This eliminates an entire class of possible
bugs, where failing to comply with data structure expectations might now
immediately become obvious."
(:require [cljs.spec.alpha :as s]))
(defn inc-handler
"Handler for incrementing specific counter"
[{:keys [current-state msg-payload]}]
{:new-state
(update-in current-state [:counters (:counter msg-payload)] #(+ % 1))})
(defn dec-handler
"Handler for decrementing specific counter"
[{:keys [current-state msg-payload]}]
{:new-state (update-in current-state [:counters (:counter msg-payload)] dec)})
(defn remove-handler
"Handler for removing last counter"
[{:keys [current-state]}]
{:new-state (update-in current-state [:counters] #(into [] (butlast %)))})
(defn add-handler
"Handler for adding counter at the end"
[{:keys [current-state]}]
{:new-state (update-in current-state [:counters] conj 0)})
(defn state-fn
"Returns clean initial component state atom"
[_put-fn]
{:state (atom {:counters [2 0 1]})})
;; validate messages using clojure.spec
(s/def :redux-ex1/counter #(and (integer? %) (>= % 0)))
(s/def :cnt/inc (s/keys :req-un [:redux-ex1/counter]))
(s/def :cnt/dec (s/keys :req-un [:redux-ex1/counter]))
;; validate component state using clojure.spec
(s/def :redux-ex1/counters (s/coll-of integer?))
(s/def :redux-ex1/store-spec (s/keys :req-un [:redux-ex1/counters]))
(defn cmp-map
[cmp-id]
{:cmp-id cmp-id
:state-fn state-fn
:state-spec :redux-ex1/store-spec
:handler-map {:cnt/inc inc-handler
:cnt/dec dec-handler
:cnt/remove remove-handler
:cnt/add add-handler}})
================================================
FILE: examples/trailing-mouse-pointer/.gitignore
================================================
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.idea
*.iml
.DS_Store
resources/public/js/build/
resources/public/bower_components/
*.pid
*.out
resources/public/css/tufte-css/
================================================
FILE: examples/trailing-mouse-pointer/LICENSE
================================================
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of the initial Contributor, the initial code and
documentation distributed under this Agreement, and
b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate from and are
distributed by that particular Contributor. A Contribution 'originates' from
a Contributor if it was added to the Program by such Contributor itself or
anyone acting on such Contributor's behalf. Contributions do not include
additions to the Program which: (i) are separate modules of software
distributed in conjunction with the Program under their own license
agreement, and (ii) are not derivative works of the Program.
"Contributor" means any person or entity that distributes the Program.
"Licensed Patents" mean patent claims licensable by a Contributor which are
necessarily infringed by the use or sale of its Contribution alone or when
combined with the Program.
"Program" means the Contributions distributed in accordance with this
Agreement.
"Recipient" means anyone who receives the Program under this Agreement,
including all Contributors.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free copyright license to
reproduce, prepare derivative works of, publicly display, publicly perform,
distribute and sublicense the Contribution of such Contributor, if any, and
such derivative works, in source code and object code form.
b) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free patent license under
Licensed Patents to make, use, sell, offer to sell, import and otherwise
transfer the Contribution of such Contributor, if any, in source code and
object code form. This patent license shall apply to the combination of the
Contribution and the Program if, at the time the Contribution is added by the
Contributor, such addition of the Contribution causes such combination to be
covered by the Licensed Patents. The patent license shall not apply to any
other combinations which include the Contribution. No hardware per se is
licensed hereunder.
c) Recipient understands that although each Contributor grants the licenses
to its Contributions set forth herein, no assurances are provided by any
Contributor that the Program does not infringe the patent or other
intellectual property rights of any other entity. Each Contributor disclaims
any liability to Recipient for claims brought by any other entity based on
infringement of intellectual property rights or otherwise. As a condition to
exercising the rights and licenses granted hereunder, each Recipient hereby
assumes sole responsibility to secure any other intellectual property rights
needed, if any. For example, if a third party patent license is required to
allow Recipient to distribute the Program, it is Recipient's responsibility
to acquire that license before distributing the Program.
d) Each Contributor represents that to its knowledge it has sufficient
copyright rights in its Contribution, if any, to grant the copyright license
set forth in this Agreement.
3. REQUIREMENTS
A Contributor may choose to distribute the Program in object code form under
its own license agreement, provided that:
a) it complies with the terms and conditions of this Agreement; and
b) its license agreement:
i) effectively disclaims on behalf of all Contributors all warranties and
conditions, express and implied, including warranties or conditions of title
and non-infringement, and implied warranties or conditions of merchantability
and fitness for a particular purpose;
ii) effectively excludes on behalf of all Contributors all liability for
damages, including direct, indirect, special, incidental and consequential
damages, such as lost profits;
iii) states that any provisions which differ from this Agreement are offered
by that Contributor alone and not by any other party; and
iv) states that source code for the Program is available from such
Contributor, and informs licensees how to obtain it in a reasonable manner on
or through a medium customarily used for software exchange.
When the Program is made available in source code form:
a) it must be made available under this Agreement; and
b) a copy of this Agreement must be included with each copy of the Program.
Contributors may not remove or alter any copyright notices contained within
the Program.
Each Contributor must identify itself as the originator of its Contribution,
if any, in a manner that reasonably allows subsequent Recipients to identify
the originator of the Contribution.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain responsibilities with
respect to end users, business partners and the like. While this license is
intended to facilitate the commercial use of the Program, the Contributor who
includes the Program in a commercial product offering should do so in a
manner which does not create potential liability for other Contributors.
Therefore, if a Contributor includes the Program in a commercial product
offering, such Contributor ("Commercial Contributor") hereby agrees to defend
and indemnify every other Contributor ("Indemnified Contributor") against any
losses, damages and costs (collectively "Losses") arising from claims,
lawsuits and other legal actions brought by a third party against the
Indemnified Contributor to the extent caused by the acts or omissions of such
Commercial Contributor in connection with its distribution of the Program in
a commercial product offering. The obligations in this section do not apply
to any claims or Losses relating to any actual or alleged intellectual
property infringement. In order to qualify, an Indemnified Contributor must:
a) promptly notify the Commercial Contributor in writing of such claim, and
b) allow the Commercial Contributor tocontrol, and cooperate with the
Commercial Contributor in, the defense and any related settlement
negotiations. The Indemnified Contributor may participate in any such claim
at its own expense.
For example, a Contributor might include the Program in a commercial product
offering, Product X. That Contributor is then a Commercial Contributor. If
that Commercial Contributor then makes performance claims, or offers
warranties related to Product X, those performance claims and warranties are
such Commercial Contributor's responsibility alone. Under this section, the
Commercial Contributor would have to defend claims against the other
Contributors related to those performance claims and warranties, and if a
court requires any other Contributor to pay any damages as a result, the
Commercial Contributor must pay those damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
appropriateness of using and distributing the Program and assumes all risks
associated with its exercise of rights under this Agreement , including but
not limited to the risks and costs of program errors, compliance with
applicable laws, damage to or loss of data, programs or equipment, and
unavailability or interruption of operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of the
remainder of the terms of this Agreement, and without further action by the
parties hereto, such provision shall be reformed to the minimum extent
necessary to make such provision valid and enforceable.
If Recipient institutes patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Program itself
(excluding combinations of the Program with other software or hardware)
infringes such Recipient's patent(s), then such Recipient's rights granted
under Section 2(b) shall terminate as of the date such litigation is filed.
All Recipient's rights under this Agreement shall terminate if it fails to
comply with any of the material terms or conditions of this Agreement and
does not cure such failure in a reasonable period of time after becoming
aware of such noncompliance. If all Recipient's rights under this Agreement
terminate, Recipient agrees to cease use and distribution of the Program as
soon as reasonably practicable. However, Recipient's obligations under this
Agreement and any licenses granted by Recipient relating to the Program shall
continue and survive.
Everyone is permitted to copy and distribute copies of this Agreement, but in
order to avoid inconsistency the Agreement is copyrighted and may only be
modified in the following manner. The Agreement Steward reserves the right to
publish new versions (including revisions) of this Agreement from time to
time. No one other than the Agreement Steward has the right to modify this
Agreement. The Eclipse Foundation is the initial Agreement Steward. The
Eclipse Foundation may assign the responsibility to serve as the Agreement
Steward to a suitable separate entity. Each new version of the Agreement will
be given a distinguishing version number. The Program (including
Contributions) may always be distributed subject to the version of the
Agreement under which it was received. In addition, after a new version of
the Agreement is published, Contributor may elect to distribute the Program
(including its Contributions) under the new version. Except as expressly
stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
licenses to the intellectual property of any Contributor under this
Agreement, whether expressly, by implication, estoppel or otherwise. All
rights in the Program not expressly granted under this Agreement are
reserved.
This Agreement is governed by the laws of the State of New York and the
intellectual property laws of the United States of America. No party to this
Agreement will bring a legal action under this Agreement more than one year
after the cause of action arose. Each party waives its rights to a jury trial
in any resulting litigation.
================================================
FILE: examples/trailing-mouse-pointer/README.md
================================================
# systems-toolbox - Trailing Mouse Example
## Usage
Before the first usage, you want to install the **[Bower](http://bower.io)** dependencies:
$ cd resources/public/css/
$ git clone https://github.com/edwardtufte/tufte-css.git
Once this is done, you can start the application as usual:
$ lein run
This will run the application on **[http://localhost:8888/](http://localhost:8888/)**. However, we will still need to compile the ClojureScript:
$ lein cljsbuild auto release
This will compile the ClojureScript into JavaScript using `:advanced` optimization.
You can also use **[Figwheel](https://github.com/bhauman/lein-figwheel)** to automatically update the application as you make changes. For that, open another terminal:
$ lein figwheel
You can then for example inspect the state of a component by using the following commands in the Figwheel REPL:
(require '[matthiasn.systems-toolbox.switchboard :as sb])
(require '[example.core :as c])
(sb/send-cmd c/switchboard [:cmd/print-cmp-state :client/histogram-cmp])
By default, the webserver exposed by the systems-toolbox library listens on port 8888 and only binds to the localhost interface. You can use environment variables to change this behavior, for example:
$ HOST="0.0.0.0" PORT=8010 lein run
## License
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.
================================================
FILE: examples/trailing-mouse-pointer/env/dev/cljs/example/dev.cljs
================================================
(ns ^:figwheel-no-load example.dev
(:require [example.core :as c]
[figwheel.client :as figwheel :include-macros true]))
(enable-console-print!)
(defn jscb []
(c/init!))
(figwheel/watch-and-reload
:websocket-url "ws://localhost:3451/figwheel-ws"
:jsload-callback jscb)
================================================
FILE: examples/trailing-mouse-pointer/project.clj
================================================
(defproject matthiasn/trailing-mouse-pointer "0.6.1-SNAPSHOT"
:description "Sample application built with systems-toolbox library"
:url "https://github.com/matthiasn/systems-toolbox"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.10.1"]
[org.clojure/clojurescript "1.10.597"]
[org.clojure/core.async "0.6.532"]
[org.clojure/tools.logging "0.5.0"]
[org.clojure/tools.namespace "0.3.1"]
[ch.qos.logback/logback-classic "1.2.3"]
[matthiasn/systemd-watchdog "0.1.3"]
[hiccup "1.0.5"]
[clj-pid "0.1.2"]
[matthiasn/systems-toolbox "0.6.41"]
[matthiasn/systems-toolbox-sente "0.6.32"]
[matthiasn/systems-toolbox-kafka "0.6.18"]
[re-frame "0.10.9"]
[clj-time "0.15.2"]]
:source-paths ["src/cljc/" "src/clj/"]
:clean-targets ^{:protect false} ["resources/public/js/build/" "target/"]
:main example.core
:plugins [[lein-cljsbuild "1.1.7"]
[lein-figwheel "0.5.19"]]
:figwheel {:server-port 3451
:css-dirs ["resources/public/css"]}
:profiles {:uberjar {:aot :all
:auto-clean false}}
:cljsbuild
{:builds
[{:id "dev"
:source-paths ["src/cljc/" "src/cljs" "env/dev/cljs"]
:figwheel true
:compiler {:main "example.dev"
:asset-path "js/build"
:optimizations :none
:output-dir "resources/public/js/build/"
:output-to "resources/public/js/build/example.js"
:source-map true}}
{:id "release"
:source-paths ["src/cljc/" "src/cljs"]
:compiler {:main "example.core"
:asset-path "js/build"
:output-to "resources/public/js/build/example.js"
:optimizations :advanced}}]})
================================================
FILE: examples/trailing-mouse-pointer/resources/logback.xml
================================================
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%-20.20thread] %-5level %25.25logger{25} - %msg%n</pattern>
</encoder>
</appender>
<!-- Errors -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- Only ERROR or above should go into this file -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover -->
<fileNamePattern>/tmp/st-ex2.error.log.%d{yyyy-MM-dd}</fileNamePattern>
<!-- keep 30 days' worth of history -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{dd-MM-yy HH:mm:ss.SSS} [%-20.20thread] %-5level %25.25logger{25} - %msg%n</pattern>
<immediateFlush>true</immediateFlush>
</encoder>
</appender>
<!-- The rest (messages without markers or with markers other than MESSAGE) -->
<appender name="UNCATEGORIZED_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/tmp/st-ex2.misc.log.%d{yyyy-MM-dd}</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{dd-MM-yy HH:mm:ss.SSS} [%-20.20thread] %-5level %25.25logger{25} %class - %msg%n</pattern>
<immediateFlush>true</immediateFlush>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ERROR_FILE"/>
<appender-ref ref="UNCATEGORIZED_FILE"/>
</root>
</configuration>
================================================
FILE: examples/trailing-mouse-pointer/resources/public/css/example.css
================================================
body {
margin: 0;
-webkit-overflow-scrolling: touch;
overflow-x: hidden;
}
#mouse {
position: absolute;
top: 0;
width: 100%;
pointer-events: none;
z-index: 10;
margin-left: -12.5%;
}
article {
padding: 2em 0;
}
#histograms {
margin-bottom: 1em;
}
#histograms div div{
display: flex;
flex-flow: row;
align-items: flex-start;
}
.show {
background-color: wheat;
padding: 2px 5px;
cursor: pointer;
}
================================================
FILE: examples/trailing-mouse-pointer/src/clj/example/core.clj
================================================
(ns example.core
(:require [example.spec]
[matthiasn.systems-toolbox.switchboard :as sb]
[matthiasn.systems-toolbox-sente.server :as sente]
[matthiasn.systems-toolbox-kafka.kafka-producer2 :as kp2]
[example.index :as index]
[clojure.tools.logging :as log]
[clj-pid.core :as pid]
[matthiasn.systemd-watchdog.core :as wd]
[example.pointer :as ptr])
(:gen-class))
(defonce switchboard (sb/component :server/switchboard))
(defn make-observable [components]
(if (System/getenv "OBSERVER")
(let [cfg {:cfg {:bootstrap-servers "localhost:9092"
:auto-offset-reset "latest"
:topic "firehose"}
:relay-types #{:firehose/cmp-put
:firehose/cmp-recv}}
mapper #(assoc-in % [:opts :msgs-on-firehose] true)
components (set (mapv mapper components))
firehose-kafka (kp2/cmp-map :server/kafka-firehose cfg)]
(conj components firehose-kafka))
components))
(defn restart!
"Starts or restarts system by asking switchboard to fire up the provided
ws-cmp and the ptr component, which handles and counts messages about mouse
moves."
[]
(let [components #{(sente/cmp-map :server/ws-cmp index/sente-map)
(ptr/cmp-map :server/ptr-cmp)}
components (make-observable components)]
(sb/send-mult-cmd
switchboard
[[:cmd/init-comp components]
[:cmd/route {:from :server/ptr-cmp
:to :server/ws-cmp}]
[:cmd/route {:from :server/ws-cmp
:to :server/ptr-cmp}]
(when (System/getenv "OBSERVER")
[:cmd/attach-to-firehose :server/kafka-firehose])])))
(defn -main [& args]
(pid/save "example.pid")
(pid/delete-on-shutdown! "ws-example.pid")
(log/info "Application started, PID" (pid/current))
(restart!)
(wd/start-watchdog! 5000)
(Thread/sleep Long/MAX_VALUE))
================================================
FILE: examples/trailing-mouse-pointer/src/clj/example/index.clj
================================================
(ns example.index
(:require
[hiccup.core :refer [html]]))
(defn index-page
"Generates index page HTML with the specified page title."
[dev?]
(html
[:html
{:lang "en"}
[:head
[:meta {:name "viewport" :content "width=device-width, minimum-scale=1.0"}]
[:title "Systems-Toolbox: Trailing Mouse Pointer Example"]
[:link {:href "/css/tufte-css/tufte.css" :media "screen" :rel "stylesheet"}]
[:link {:href "/css/example.css" :media "screen" :rel "stylesheet"}]
[:link {:href "/images/favicon.png"
:rel "shortcut icon"
:type "image/png"}]]
[:body
[:div#ui]
[:div#jvm-stats-frame]
[:script {:src "/js/build/example.js"}]
; Google Analytics for tracking demo page
[:script {:async "" :src "https://www.google-analytics.com/analytics.js"}]
[:script (str "(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-40261983-6', 'auto'); ga('send', 'pageview');")]]]))
(def sente-map
"Configuration map for sente-cmp."
{:index-page-fn index-page
:port 8763
:relay-types #{:mouse/pos :mouse/hist}})
================================================
FILE: examples/trailing-mouse-pointer/src/cljc/example/pointer.cljc
================================================
(ns example.pointer
"This component receives messages, keeps a counter, decorates them with the
state of the counter, and sends them back. Here, this provides a way to
measure roundtrip time from the UI, as timestamps are recorded as the message
flows through the system.
Also records a recent history of mouse positions for all clients, which the
component provides to clients upon request.")
(defn process-mouse-pos
"Handler function for received mouse positions, increments counter and returns
mouse position to sender."
[{:keys [current-state msg-meta msg-payload]}]
(let [new-state (-> current-state
(update-in [:count] inc)
(update-in [:mouse-moves]
#(vec (take-last 1000 (conj % msg-payload)))))]
{:new-state new-state
:emit-msg (with-meta
[:mouse/pos (assoc msg-payload :count (:count new-state))]
msg-meta)}))
(defn get-mouse-hist
"Gets the recent mouse position history from server."
[{:keys [current-state msg-meta]}]
{:emit-msg (with-meta [:mouse/hist (:mouse-moves current-state)] msg-meta)})
(defn cmp-map
[cmp-id]
{:cmp-id cmp-id
:state-fn (fn [_] {:state (atom {:count 0 :mouse-moves []})})
:handler-map {:mouse/pos process-mouse-pos
:mouse/get-hist get-mouse-hist}
:opts {:msgs-on-firehose true
:snapshots-on-firehose true}})
================================================
FILE: examples/trailing-mouse-pointer/src/cljc/example/server_switchboard.cljc
================================================
(ns example.server-switchboard
(:require
[matthiasn.systems-toolbox.switchboard :as sb]
[matthiasn.systems-toolbox.scheduler :as sched]
[example.pointer :as ptr]))
(defonce switchboard (sb/component :server/switchboard))
(defn restart!
"Starts or restarts system by asking switchboard to fire up the provided
ws-cmp, a scheduler component and the ptr component, which handles and counts
messages about mouse moves."
[ws-cmp]
(sb/send-mult-cmd
switchboard
[ws-cmp
[:cmd/init-comp (sched/cmp-map :server/scheduler-cmp)]
[:cmd/init-comp (ptr/cmp-map :server/ptr-cmp)]
[:cmd/route-all {:from :server/ptr-cmp :to :server/ws-cmp}]
[:cmd/route {:from :server/ws-cmp :to :server/ptr-cmp}]]))
================================================
FILE: examples/trailing-mouse-pointer/src/cljc/example/spec.cljc
================================================
(ns example.spec
(:require
#?(:clj [clojure.spec.alpha :as s]
:cljs [cljs.spec.alpha :as s])))
(s/def :ex/x integer?)
(s/def :ex/y integer?)
(s/def :mouse/pos
(s/keys :req-un [:ex/x :ex/y]))
(s/def :cmd/show-all #{:local :server})
(s/def :mouse/hist (s/* :mouse/pos))
================================================
FILE: examples/trailing-mouse-pointer/src/cljs/example/core.cljs
================================================
(ns example.core
(:require [example.spec]
[example.store :as store]
[example.ui-histograms :as hist]
[example.ui-info :as info]
[example.re-frame :as ui]
[example.observer :as observer]
[matthiasn.systems-toolbox.switchboard :as sb]
[matthiasn.systems-toolbox-sente.client :as sente]
[clojure.string :as s]))
(enable-console-print!)
(defonce switchboard (sb/component :client/switchboard))
(def OBSERVER
(or (.-OBSERVER js/window)
(s/includes? (aget js/window "location" "search") "OBSERVER=true")))
(defn make-observable [components]
(if OBSERVER
(let [mapper #(assoc-in % [:opts :msgs-on-firehose] true)]
(prn "Attaching firehose")
(set (mapv mapper components)))
components))
(def sente-cfg {:relay-types #{:mouse/pos
:mouse/get-hist
:firehose/cmp-put
:firehose/cmp-recv
:firehose/cmp-publish-state
:firehose/cmp-recv-state}
:msgs-on-firehose true})
(defn init! []
(let [components #{(sente/cmp-map :client/ws-cmp sente-cfg)
(ui/cmp-map :client/ui-cmp)
(store/cmp-map :client/store-cmp)}
components (make-observable components)]
(sb/send-mult-cmd
switchboard
[[:cmd/init-comp components]
[:cmd/route {:from :client/ui-cmp
:to #{:client/store-cmp :client/ws-cmp}}]
[:cmd/route {:from #{:client/store-cmp
:client/ws-cmp}
:to :client/store-cmp}]
[:cmd/observe-state {:from :client/store-cmp
:to :client/ui-cmp}]
(when OBSERVER
[:cmd/attach-to-firehose :client/ws-cmp])]))
(observer/init! switchboard))
(init!)
================================================
FILE: examples/trailing-mouse-pointer/src/cljs/example/hist_calc.cljs
================================================
(ns example.hist-calc)
(defn mean
"From: https://github.com/clojure-cookbook/"
[coll]
(let [sum (apply + coll)
count (count coll)]
(if (pos? count)
(/ sum count)
0)))
(defn median
"Modified from: https://github.com/clojure-cookbook/
Adapted to return nil when collection empty."
[sorted]
(let [cnt (count sorted)
halfway (quot cnt 2)]
(if (empty? sorted)
nil
(if (odd? cnt)
(nth sorted halfway)
(let [bottom (dec halfway)
bottom-val (nth sorted bottom)
top-val (nth sorted halfway)]
(mean [bottom-val top-val]))))))
(defn interquartile-range
"Determines the interquartile range of values in a sequence of numbers.
Returns nil when sequence empty or only contains a single entry."
[sorted]
(let [cnt (count sorted)
half-cnt (quot cnt 2)
q1 (median (take half-cnt sorted))
q3 (median (take-last half-cnt sorted))]
(when (and q3 q1) (- q3 q1))))
(defn percentile-range
"Returns only the values within the given percentile range."
[sorted percentile]
(let [cnt (count sorted)
keep-n (Math/ceil (* cnt (/ percentile 100)))]
(take keep-n sorted)))
(defn freedman-diaconis-rule
"Implements approximation of the Freedman-Diaconis rule for determing bin size
in histograms: bin size = 2 IQR(x) n^-1/3 where IQR(x) is the interquartile
range of the data and n is the number of observations in sample x. Argument
is expected to be a sequence of numbers."
[sample]
(let [n (count sample)]
(when (pos? n)
(* 2 (interquartile-range sample) (Math/pow n (/ -1 3))))))
(defn round-up [n increment] (* (Math/ceil (/ n increment)) increment))
(defn round-down [n increment] (* (Math/floor (/ n increment)) increment))
(defn best-increment-fn
"Takes a seq of increments, a desired number of intervals in histogram axis,
and the range of the values in the histogram. Sorts the values in increments
by dividing the range by each to determine number of intervals with this
value, subtracting the desired number of intervals, and then returning the
increment with the smallest delta."
[increments desired-n rng]
(first (sort-by #(Math/abs (- (/ rng %) desired-n)) increments)))
(defn default-increment-fn
"Determines the increment between intervals in histogram axis.
Defaults to increments in a range between 1 and 5,000,000."
[rng]
(if rng
(let [multipliers (mapv #(Math/pow 10 %) (range 0 6))
increments (flatten (mapv (fn [i] (mapv #(* i %) multipliers)) [1 2.5 5]))
best-increment (best-increment-fn increments 5 rng)]
(if (zero? (mod best-increment 1))
(int best-increment)
best-increment))
1))
(defn histogram-calc
"Calculations for histogram."
[{:keys [data bin-cf max-bins increment-fn]}]
(let [mx (apply max data)
mn (apply min data)
rng (- mx mn)
increment-fn (or increment-fn default-increment-fn)
increment (increment-fn rng)
bin-size (max (/ rng max-bins) (* (freedman-diaconis-rule data) bin-cf))
binned-freq (frequencies
(mapv (fn [n] (Math/floor (/ (- n mn) bin-size))) data))]
{:mn mn
:mn2 (round-down (or mn 0) increment)
:mx2 (round-up (or mx 10) increment)
:rng rng
:increment increment
:binned-freq binned-freq
:binned-freq-mx (apply max (mapv (fn [[_ f]] f) binned-freq))
:bins (inc (apply max (mapv (fn [[v _]] v) binned-freq)))}))
================================================
FILE: examples/trailing-mouse-pointer/src/cljs/example/histogram.cljs
================================================
(ns example.histogram
"Functions for building a histogram, rendered as SVG using Reagent and React."
(:require [example.hist-calc :as m]))
(def text-default {:stroke "none" :fill "black" :style {:font-size 12}})
(def text-bold (merge text-default {:style {:font-weight :bold :font-size 12}}))
(def x-axis-label (merge text-default {:text-anchor :middle}))
(def y-axis-label (merge text-default {:text-anchor :end}))
(defn path
"Renders path with the given path description attribute."
[d]
[:path {:fill :black
:stroke :black
:stroke-width 1
:d d}])
(defn histogram-y-axis
"Draws y-axis for histogram."
[x y h mx y-label]
(let [incr (m/default-increment-fn mx)
rng (range 0 (inc (m/round-up mx incr)) incr)
scale (/ h (dec (count rng)))]
[:g
[path (str "M" x " " y "l 0 " (* h -1) " z")]
(for [n rng]
^{:key n}
[path (str "M" x " " (- y (* (/ n incr) scale)) "l -" 6 " 0")])
(for [n rng]
^{:key n}
[:text (merge y-axis-label {:x (- x 10)
:y (- y (* (/ n incr) scale) -4)}) n])
[:text (let [x-coord (- x 45)
y-coord (- y (/ h 3))
rotate (str "rotate(270 " x-coord " " y-coord ")")]
(merge x-axis-label text-bold {:x x-coord
:y y-coord
:transform rotate})) y-label]]))
(defn histogram-x-axis
"Draws x-axis for histogram."
[x y mn mx w scale increment x-label]
(let [rng (range mn (inc mx) increment)]
[:g
[path (str "M" x " " y "l" w " 0 z")]
(for [n rng]
^{:key n}
[path (str "M" (+ x (* (- n mn) scale)) " " y "l 0 " 6)])
(for [n rng]
^{:key n}
[:text (merge x-axis-label {:x (+ x (* (- n mn) scale))
:y (+ y 20)}) n])
[:text (merge x-axis-label text-bold {:x (+ x (/ w 2))
:y (+ y 48)}) x-label]]))
(defn insufficient-data
"Renders warning when data insufficient."
[x y w text]
[:text {:x (+ x (/ w 2))
:y (- y 50)
:stroke "none"
:fill "#DDD"
:text-anchor :middle
:style {:font-weight :bold :font-size 24}} text])
(defn histogram-view-fn
"Renders a histogram. Only takes care of the presentational aspects, the
calculations are done in the histogram-calc function in
matthiasn.systems-toolbox-ui.charts.math."
[{:keys [x y w h x-label y-label color min-bins warning] :as args}]
(let [{:keys [mn mn2 mx2 rng increment bins binned-freq binned-freq-mx]}
(m/histogram-calc args)
x-scale (/ w (- mx2 mn2))
y-scale (/ (- h 20) binned-freq-mx)
bar-width (/ (* rng x-scale) bins)]
[:g
(if (>= bins min-bins)
(for [[v f] binned-freq]
^{:key v}
[:rect {:x (+ x (* (- mn mn2) x-scale) (* v bar-width))
:y (- y (* f y-scale))
:fill color :stroke "black"
:width bar-width
:height (* f y-scale)}])
[insufficient-data x y w warning])
[histogram-x-axis x (+ y 7) mn2 mx2 w x-scale increment x-label]
[histogram-y-axis (- x 7) y h (or binned-freq-mx 5) y-label]]))
(defn histogram-view
"Renders an individual histogram for the given data, dimension, label and
color, with a reasonable size inside a viewBox, which will then scale
smoothly into any div you put it in."
[data label color]
[:svg {:width "100%"
:viewBox "0 0 400 250"}
(histogram-view-fn {:data data
:x 80
:y 180
:w 300
:h 160
:x-label label
:y-label "Frequencies"
:warning "insufficient data"
:color color
:bin-cf 0.8
:min-bins 5
:max-bins 25})])
================================================
FILE: examples/trailing-mouse-pointer/src/cljs/example/observer.cljs
================================================
(ns example.observer
(:require [reagent.core :as r :refer [atom]]
[example.spec]
[matthiasn.systems-toolbox.switchboard :as sb]
[clojure.set :as s]))
(defn now [] (.getTime (js/Date.)))
(defn r [] (.random js/Math))
(defn by-id [id] (.getElementById js/document id))
(def request-animation-frame
(or (.-requestAnimationFrame js/window)
(.-webkitRequestAnimationFrame js/window)
(.-mozRequestAnimationFrame js/window)
(.-msRequestAnimationFrame js/window)
(fn [callback] (js/setTimeout callback 17))))
(defn nodes-map-fn
[nodes-list obs-cfg]
(let [nodes (map (fn [k]
(let [fixed-nodes (:fixed-nodes obs-cfg)]
{:name (if (namespace k)
(str (namespace k) "/" (name k))
(name k))
:key k
:x (if (contains? fixed-nodes k)
(-> fixed-nodes k :x)
(+ (* 800 (r)) 100))
:y (if (contains? fixed-nodes k)
(-> fixed-nodes k :y)
(+ (* 800 (r)) 100))
:last-received (now)}))
nodes-list)]
(into {} (map (fn [itm] [(:key itm) itm]) nodes))))
(defn links-fn
[nodes-map links]
(vec (map (fn [m] {:source (:idx ((:from m) nodes-map))
:target (:idx ((:to m) nodes-map))}) links)))
(defn cmp-node
[app node cmp-key]
(let [x (:x node)
y (:y node)
grp (:group node)
ms-since-rx (- (:now @app) (:last-rx node))
ms-since-tx (- (:now @app) (:last-tx node))
rx-cnt (:rx-count node)
tx-cnt (:tx-count node)]
(when x
[:g {:transform (str "translate(" x "," y ")")
:on-click #(prn ((-> @app
(:switchboard-state)
(:components)
(cmp-key)
(:state-snapshot-fn))))}
[:rect {:x -60
:y -25
:width 120
:height 50
:rx 5
:ry 5
:fill :white
:stroke (if (zero? grp) "#C55" "#5C5") :stroke-width "2px"}]
[:text {:dy "-.5em"
:text-anchor :middle
:text-rendering "geometricPrecision"
:stroke :none
:fill :black
:font-size "11px"
:style {:font-weight :bold}}
(str cmp-key)]
[:text {:dy "1em"
:text-anchor :middle
:text-rendering "geometricPrecision"
:stroke :none
:fill :gray
:font-size "11px"
:style {:font-weight :bold}}
(str (when rx-cnt (str "rx: " rx-cnt)) (when tx-cnt (str " tx: " tx-cnt)))]
[:rect {:x 44
:y 5
:width 10
:height 10
:rx 1
:ry 1
:fill :green
:style {:opacity (/ (max 0 (- 250 ms-since-tx)) 250)}}]
[:rect {:x -54
:y 5
:width 10
:height 10
:rx 1
:ry 1
:fill :orangered
:style {:opacity (/ (max 0 (- 250 ms-since-rx)) 250)}}]])))
(defn system-view
"Renders SVG with an area in which components of a system are shown as a visual representation.
These visual representations aim at helping in observing a running system."
[app put-fn cfg]
(let [nodes-map (:nodes-map @app)
links (:links @app)]
[:div
[:svg (merge {:width "100%" :viewBox "0 0 1000 1000"}
(:svg-props cfg))
[:g
(for [l links]
^{:key (str "force-link-" l)}
[:line.link {:stroke (condp = (:type l)
:sub "#00CC33"
:tap "#0033CC"
:fh-tap "#CC0033")
:stroke-width "3px"
:x1 (:x ((:from l) nodes-map))
:x2 (:x ((:to l) nodes-map))
:y1 (:y ((:from l) nodes-map))
:y2 (:y ((:to l) nodes-map))}])
(for [[k v] nodes-map]
^{:key (str "force-node-" k)}
[cmp-node app v k])]]]))
(defn mk-state
"Return clean initial component state atom."
[obs-cfg]
(fn
[put-fn]
(let [app (atom {:time (now)
:obs-cfg obs-cfg})
system-view-elem (by-id (:dom-id obs-cfg))]
(r/render-component [system-view app put-fn obs-cfg] system-view-elem)
(letfn [(step []
(request-animation-frame step)
(swap! app assoc :now (now)))]
(request-animation-frame step))
{:state app})))
(defn count-msg
"Creates a handler function for collecting stats about messages and and their display."
[ts-key count-key]
(fn [{:keys [cmp-state msg-payload cmp-id]}]
(let [other-id (:cmp-id msg-payload)]
(when (:switchboard-state @cmp-state)
(swap! cmp-state assoc-in [:nodes-map other-id ts-key] (now))
(swap! cmp-state update-in [:nodes-map other-id count-key] #(inc (or % 0)))
(swap! cmp-state assoc-in [:nodes-map cmp-id :last-rx] (now))
(swap! cmp-state update-in [:nodes-map cmp-id :rx-count] #(inc (or % 0)))))
{}))
(defn state-snapshot-handler
"Creates a handler function for component snapshot messages. Uses messages from switchboard for
configuring the UI."
[switchbrd-id]
(fn [{:keys [cmp-state msg-payload] :as msg-map}]
(let [other-id (:cmp-id msg-payload)
count-fn (count-msg :last-rx :rx-count)]
(count-fn msg-map)
(when (= other-id switchbrd-id)
(let [switchboard-state (:snapshot msg-payload)
obs-cfg (:obs-cfg @cmp-state)
nodes-map (nodes-map-fn (keys (:components switchboard-state)) obs-cfg)
subscriptions-set (:subs switchboard-state)
taps-set (:taps switchboard-state)
fh-taps-set (:fh-taps switchboard-state)
links (s/union subscriptions-set taps-set fh-taps-set)]
(swap! cmp-state assoc :switchboard-state switchboard-state)
(swap! cmp-state assoc :nodes-map nodes-map)
(swap! cmp-state assoc :links links))))
{}))
(defn cmp-map
{:added "0.3.1"}
[cmp-id obs-cfg]
{:cmp-id cmp-id
:state-fn (mk-state obs-cfg)
:handler-map {:firehose/cmp-put (count-msg :last-tx :tx-count)
:firehose/cmp-publish-state (state-snapshot-handler (:switchbrd-id obs-cfg))
:firehose/cmp-recv (count-msg :last-rx :rx-count)
:firehose/cmp-recv-state (count-msg :last-rx :rx-count)}
:opts {:snapshots-on-firehose false
:reload-cmp false}})
(def observer-cfg
{:dom-id "observer"
:switchbrd-id :client/switchboard
:fixed-nodes {:client/ws-cmp {:x 100 :y 300}
:client/observer-cmp {:x 360 :y 550}
:client/switchboard {:x 360 :y 420}
:client/ui-cmp {:x 650 :y 300}
:client/store-cmp {:x 360 :y 120}}
:svg-props {:viewBox "0 0 1000 600"}})
(defn init!
"Initialize and wire Observer component."
[switchboard]
(sb/send-mult-cmd
switchboard
[[:cmd/init-comp (cmp-map :client/observer-cmp observer-cfg)]
[:cmd/attach-to-firehose :client/observer-cmp]]))
================================================
FILE: examples/trailing-mouse-pointer/src/cljs/example/re_frame.cljs
================================================
(ns example.re-frame
(:require-macros [reagent.ratom :refer [reaction]])
(:require [reagent.core :as reagent]
[example.ui-mouse-moves :as mm]
[example.ui-histograms :as hist]
[example.ui-info :as info]
[example.utils :as u]
[re-frame.core :refer [reg-sub]]
[re-frame.db :as rdb]))
;; Subscription Handlers
(reg-sub :local (fn [db _] (:local db)))
(reg-sub :rtt-times (fn [db _] (:rtt-times db)))
(reg-sub :network-times (fn [db _] (:network-times db)))
(reg-sub :from-server (fn [db _] (:from-server db)))
(defn re-frame-ui
"Main view component"
[put-fn]
[:div
[:div#mouse
[mm/mouse-view]]
[:article
[:h1 "WebSockets Latency Visualization"]
[:p [:a {:href "https://github.com/matthiasn" :target "_blank"}
"Matthias Nehlsen"]]
[:section
[:p
"WebSockets bring bi-directional communication to the browser. This
enables you to deliver interactive, real time web applications where
all the data is as of right now, rather than always being outdated,
and then constantly refreshed."]
[:p
"But how fast is this transport mechanism? Let's have a look. You may
have noticed the circle around the mouse pointer on this page, "
[:label {:for "explain1" :class "margin-toggle"} " ⊕ "]
[:input#explain1 {:type "checkbox" :class "margin-toggle"}]
[:span.marginnote
"What happens here is that movements of the mouse (or your finger on
your mobile device) are captured. The more reddish one is then painted
immediately, whereas the bluish one is painted after the event is sent
to a server somewhere in Germany, and then back to wherever you are."]
"or in fact the two circles, where one of them appears to follow the other.
Both represent your last mouse position, only that one was sent to and
returned from the server in the meantime. This gives you an intuition
for how long it takes. Also, with your movement of the mouse, you
generate data for the histograms below, which show the roundtrip duration:"]
[hist/histograms-view]
[info/info-view put-fn]
[:p
"Now, since we are already capturing the movement of the mouse, you
may think that it could be interesting to see where the users' mouses
go, as a proxy for where they are looking on a page. Surely not as
accurate as actual eye tracking, but probably much better than nothing.
Now let's see where your mouse was since you started interacting with
this page. Click the \"show all\" button in the info section below,"
[:label {:for "explain2" :class "margin-toggle"} " ⊕ "]
[:input#explain2 {:type "checkbox" :class "margin-toggle"}]
[:span.marginnote
"By clicking those buttons again, you can switch the display on or off."]
" and you see where your mouse goes. Then, by clicking
\"show all (server)\", you can also display the most recent mouse
positions of all visitors on this page."]
[:p
"You are looking at a web application written in Clojure andClojureScript.
It is one of the example applications of the systems-toolbox library.
The histograms above are rendered entirely in ClojureScript
- without any additional charting library."
[:span.marginnote "The "
[:a {:href "http://en.wikipedia.org/wiki/Freedman–Diaconis_rule"}
"Freedman-Diaconis rule"]
" determines the number of bins in the histograms. The first
histogram takes the entire sample into account whereas the second only
displays the observations that fall within the 99th percentile to
remove potential outliers."]]
[:figure
[:label {:for "fig2" :class "margin-toggle"} " ⊕ "]
[:input#fig2 {:type "checkbox" :class "margin-toggle"}]
[:span.marginnote
"Structure of the ClojureScript application, with their message flow
visualized as rx and tx LEDs, like on a network card."]
[:div#observer]]
[:p
"If you want to know how this application was built, have a look at the
code on "
[:a {:href "https://github.com/matthiasn/systems-toolbox"
:target "_blank"} "GitHub"]
" or the book "
[:a {:href "https://leanpub.com/building-a-system-in-clojure"
:target "_blank"} "Building Systems in Clojure(Script)"]
". Also, check for a future blog post on "
[:a {:href "https://matthiasnehlsen.com" :target "_blank"}
"matthiasnehlsen.com"]
"."]
[:p
"Finally, if you like the layout of this page, you need to look at "
[:a {:href "https://edwardtufte.github.io/tufte-css/" :target "_blank"}
"Tufte CSS"]
". It allowed me to write this application with only around 30 lines of
CSS, most of which is related to the flexbox layout for histogram SVGs."]]]])
(defn state-fn
"Renders main view component and wires the central re-frame app-db as the
observed component state, which will then be updated whenever the store-cmp
changes."
[put-fn]
(reagent/render [re-frame-ui put-fn] (.getElementById js/document "ui"))
(aset js/window "onmousemove"
(fn [ev]
(put-fn [:mouse/pos {:x (.-pageX ev) :y (.-pageY ev)}])))
(aset js/window "ontouchmove"
(fn [ev]
(let [t (aget (.-targetTouches ev) 0)]
(put-fn [:mouse/pos {:x (.-pageX t) :y (.-pageY t)}]))))
{:observed rdb/app-db})
(defn cmp-map
[cmp-id]
{:cmp-id cmp-id
:state-fn state-fn
:opts {:msgs-on-firehose true}})
================================================
FILE: examples/trailing-mouse-pointer/src/cljs/example/store.cljs
================================================
(ns example.store)
(defn mouse-pos-handler
"Handler function for mouse position messages. When message from server:
- determine the round trip time (RTT) by subtracting the message creation
timestamp from the timestamp when the message is finally received by the
store component.
- determine server side processing time is determined. For this, we can use
the timestamps from when the ws-cmp on the server side emits a message
coming from the client and when the processed message is received back for
delivery to the client.
- update component state with the new mouse location under :from-server.
When message received locally, only update position in :local."
[{:keys [current-state msg-payload msg-meta]}]
(let [new-state
(if (:count msg-payload)
(let [mouse-out-ts (:out-ts (:client/ui-cmp msg-meta))
store-in-ts (:in-ts (:client/store-cmp msg-meta))
rt-time (- store-in-ts mouse-out-ts)
srv-ws-meta (:server/ws-cmp msg-meta)
srv-proc-time (- (:in-ts srv-ws-meta) (:out-ts srv-ws-meta))
network-time (- rt-time srv-proc-time)]
(-> current-state
(assoc-in [:from-server] (assoc msg-payload :rt-time rt-time))
(update-in [:count] inc)
(update-in [:rtt-times] conj rt-time)
;(update-in [:rtt-times] #(sort (conj % rt-time)))
(update-in [:network-times] conj network-time)))
(-> current-state
(assoc-in [:local] msg-payload)
(update-in [:local-hist] conj msg-payload)))]
{:new-state new-state}))
(defn show-all-handler
"Toggles boolean value in component state for provided key."
[{:keys [current-state msg-payload]}]
{:new-state (update-in current-state [:show-all msg-payload] not)})
(defn mouse-hist-handler
"Saves the received vector with mouse positions in component state."
[{:keys [current-state msg-payload]}]
{:new-state (assoc-in current-state [:server-hist] msg-payload)})
(defn state-fn
"Return clean initial component state atom."
[_put-fn]
{:state (atom {:count 0
:rtt-times []
:network-times []
:local {:x 0 :y 0}
:show-all {:local false
:remote false}})})
(defn cmp-map
"Configuration map that specifies how to instantiate component."
[cmp-id]
{:cmp-id cmp-id
:state-fn state-fn
:handler-map {:mouse/pos mouse-pos-handler
:cmd/show-all show-all-handler
:mouse/hist mouse-hist-handler}
:opts {:msgs-on-firehose true}})
================================================
FILE: examples/trailing-mouse-pointer/src/cljs/example/ui_histograms.cljs
================================================
(ns example.ui-histograms
(:require-macros [reagent.ratom :refer [reaction]])
(:require [example.histogram :as h]
[matthiasn.systems-toolbox.component :as st]
[re-frame.core :refer [subscribe]]
[example.hist-calc :as m]
[reagent.core :as r]
[example.utils :as u]))
(defn histograms-view
"Renders histograms with different data sets, labels and colors."
[]
(let [rtt-times (subscribe [:rtt-times])
network-times (subscribe [:network-times])
show-all? (r/atom false)]
(fn histograms-render []
(let [rtt-times (sort @rtt-times)]
[:figure#histograms.fullwidth
[:span.show
{:on-click #(swap! show-all? not)}
(if @show-all?
"show single"
"show all")]
(if @show-all?
[:div
[:div
[h/histogram-view
rtt-times
"Roundtrip t/ms" "#D94B61"]
[h/histogram-view
(m/percentile-range rtt-times 99)
"Roundtrip t/ms (within 99th percentile)" "#D94B61"]
[h/histogram-view
(m/percentile-range rtt-times 95)
"Roundtrip t/ms (within 95th percentile)" "#D94B61"]]
#_
(let [network-times (sort @network-times)]
[:div
[h/histogram-view
network-times
"Network t/ms" "#66A9A5"]
[h/histogram-view
(m/percentile-range network-times 99)
"Network t/ms (within 99th percentile)" "#66A9A5"]
[h/histogram-view
(m/percentile-range network-times 95)
"Network t/ms (within 95th percentile)" "#66A9A5"]])]
[:div
[:div
[h/histogram-view
rtt-times
"Roundtrip t/ms" "#D94B61"]]])]))))
================================================
FILE: examples/trailing-mouse-pointer/src/cljs/example/ui_info.cljs
================================================
(ns example.ui-info
(:require [re-frame.core :refer [subscribe]]))
(defn info-view
"Show some info about app state, plus toggle buttons for showing all mouse
positions, both local and from server."
[put-fn]
(let [from-server (subscribe [:from-server])
rtt-times (subscribe [:rtt-times])
local (subscribe [:local])]
(fn [put-fn]
(let [last-rt (:rt-time @from-server)
rtt-times @rtt-times
mx (apply max rtt-times)
mn (apply min rtt-times)
cnt (count rtt-times)
mean (.round js/Math (if (seq rtt-times)
(/ (apply + rtt-times) cnt)
0))
local-pos @local
latency-string (str mean " mean / " mn " min / " mx " max / " last-rt
" last")]
[:div
[:strong "Mouse Moves Processed: "] cnt [:br]
[:strong "Processed since Startup: "]
(:count @from-server) [:br]
[:strong "Current position: "] "x: " (:x local-pos) " y: " (:y local-pos)
[:br]
[:strong "Latency (ms): "] latency-string [:br]
[:br]
#_#_[:button {:on-click #(put-fn [:cmd/show-all :local])} "show all"]
[:button {:on-click #(do (put-fn [:mouse/get-hist])
(put-fn [:cmd/show-all :server]))}
"show all (server)"]]))))
================================================
FILE: examples/trailing-mouse-pointer/src/cljs/example/ui_mouse_moves.cljs
================================================
(ns example.ui-mouse-moves
(:require [re-frame.core :refer [subscribe]]
[reagent.core :as rc]))
;; some SVG defaults
(def circle-defaults {:fill "rgba(255,0,0,0.1)
" :stroke "rgba(0,0,0,0.5)"
:stroke-width 2 :r 15})
(def text-default {:stroke "none" :fill "black" :style {:font-size 12}})
(def text-bold (merge text-default {:style {:font-weight :bold :font-size 12}}))
(defn mouse-hist-view
"Render SVG group with filled circles from a vector of mouse positions in state."
[state state-key stroke fill]
(let [positions (map-indexed vector (state-key state))]
(when (seq positions)
[:g {:opacity 0.5}
(for [[idx pos] positions]
^{:key (str "circle" state-key idx)}
[:circle {:stroke stroke
:stroke-width 2
:r 15
:cx (:x pos)
:cy (:y pos)
:fill fill}])])))
(defn trailing-circles
"Displays two transparent circles. The position of the circles comes from
the most recent messages, one sent locally and the other with a roundtrip to
the server in between. This makes it easier to visually detect any delays."
[]
(let [local-pos (subscribe [:local])
from-server (subscribe [:from-server])]
(fn []
[:g
[:circle (merge circle-defaults {:cx (:x @local-pos)
:cy (:y @local-pos)})]
[:circle (merge circle-defaults {:cx (:x @from-server)
:cy (:y @from-server)
:fill "rgba(0,0,255,0.1)"})]])))
(defn mouse-view
"Renders SVG with both local mouse position and the last one returned from the
server, in an area that covers the entire visible page."
[]
(let [local (rc/atom {})
update-dim (fn [_ev]
(let [h 3000
w (.-innerWidth js/window)]
(swap! local assoc :width w)
(swap! local assoc :height h)))]
(update-dim nil)
(aset js/window "onresize" update-dim)
(fn mouse-view-render []
[:div
[:svg {:width (:width @local)
:height (:height @local)}
[trailing-circles]
#_#_(when (-> state-snapshot :show-all :local)
[mouse-hist-view state-snapshot :local-hist
"rgba(0,0,0,0.06)" "rgba(0,255,0,0.05)"])
(when (-> state-snapshot :show-all :server)
[mouse-hist-view state-snapshot :server-hist
"rgba(0,0,0,0.06)" "rgba(0,0,128,0.05)"])]])))
================================================
FILE: examples/trailing-mouse-pointer/src/cljs/example/utils.cljs
================================================
(ns example.utils
(:require-macros [cljs.core.async.macros :refer [go-loop]])
(:require [cljs.core.async :refer [chan sliding-buffer timeout put! <!]]))
(defn throttle [f time]
(let [c (chan (sliding-buffer 1))]
(go-loop []
(apply f (<! c))
(<! (timeout time))
(recur))
(fn [& args]
(put! c (or args [])))))
================================================
FILE: perf/matthiasn/systems_toolbox/runtime_perf_test.cljc
================================================
(ns matthiasn.systems-toolbox.runtime-perf-test
"This namespace provides a sanity check before endeavoring in premature optimization. For example,
if we can swap or reset an atom 70 million times per second on the JVM but 'only' process 80K messages
per second, it is quite unlikely that using volatile! instead of an atom would speed things up. Here,
core.async seems to be the more interesting candidate to look at, where the very simple operation
of putting a message on a chan with an attached mult and no other chan to take it off there can be
performed a little over 200K times per second."
#?(:cljs (:require-macros [cljs.core.async.macros :refer [go go-loop]]))
(:require
#?(:clj [clojure.test :refer [deftest testing is]]
:cljs [cljs.test :refer-macros [deftest testing is]])
#?(:clj [clojure.core.async :refer [<! chan mult buffer put! go go-loop timeout promise-chan >! tap
sliding-buffer onto-chan]]
:cljs [cljs.core.async :refer [<! chan mult put! timeout promise-chan >! tap sliding-buffer]])
[matthiasn.systems-toolbox.test-promise :as tp]
[matthiasn.systems-toolbox.component :as component]
#?(:clj [clojure.tools.logging :as log]
:cljs [matthiasn.systems-toolbox.log :as log])))
; here, we can tweak the number of test runs, which is useful if we are interested in JIT optimizations
(def test-runs 0)
(defn swap-atom-repeatedly-fn
[]
"This test aims at getting some perspective how expensive swapping an atom is in Clojure/ClojureScript.
Answer: not terribly expensive. On the JVM, this can be performed around 70 million times per second,
whereas in ClojureScript, this can be done 15 million times per second (2015 Retina MacBook)."
(let [start-ts (component/now)
cnt (* 1000 1000)
state (atom 0)]
(dotimes [_ cnt] (swap! state inc))
(let [ops-per-sec (int (* (/ 1000 (- (component/now) start-ts)) cnt))]
(log/info "Atom swaps/s:" ops-per-sec)
(is (> ops-per-sec 1000)))))
(deftest swap-atom-repeatedly
(dotimes [_ test-runs]
(swap-atom-repeatedly-fn)))
(defn swap-watched-atom-repeatedly-fn
[]
"This test aims at getting some perspective how expensive swapping an atom is in Clojure/ClojureScript.
Answer: not terribly expensive. On the JVM, this can be performed around 70 million times per second,
whereas in ClojureScript, this can be done 15 million times per second (2015 Retina MacBook)."
(let [start-ts (component/now)
cnt (* 1000 1000)
state (atom 0)]
(add-watch state :watcher (fn [_ _ _ _new-state] #()))
(dotimes [_ cnt] (swap! state inc))
(let [ops-per-sec (int (* (/ 1000 (- (component/now) start-ts)) cnt))]
(log/info "Watched atom swaps/s:" ops-per-sec)
(is (> ops-per-sec 1000)))))
(deftest swap-watched-atom-repeatedly
(dotimes [_ test-runs]
(swap-watched-atom-repeatedly-fn)))
(defn reset-atom-repeatedly-fn
[]
"This test aims at getting some perspective how expensive resetting an atom is in Clojure/ClojureScript.
Answer: not terribly expensive. On the JVM, this can be performed around 90 million times per second,
whereas in ClojureScript, this can be done 15 million times per second on PhantomJS and over 60 million
times per second in Firefox (2015 Retina MacBook)."
(let [start-ts (component/now)
cnt (* 1000 1000)
state (atom 0)]
(dotimes [n cnt] (reset! state n))
(let [ops-per-sec (int (* (/ 1000 (- (component/now) start-ts)) cnt))]
(log/info "Atom resets/s:" ops-per-sec)
(is (> ops-per-sec 1000)))))
(deftest reset-atom-repeatedly
(dotimes [_ test-runs]
(reset-atom-repeatedly-fn)))
(defn deref-atom-repeatedly-fn
[]
"This test aims at getting some perspective how expensive dereferencing an atom is in Clojure/ClojureScript.
Answer: not terribly expensive. On the JVM, this can be performed around 30 million times per second,
whereas in ClojureScript, this can be done 8 million times per second (2015 Retina MacBook)."
(let [start-ts (component/now)
cnt (* 1000 1000)
state (atom {:foo 1000})]
(dotimes [n cnt] (/ (:foo @state) 10))
(let [ops-per-sec (int (* (/ 1000 (- (component/now) start-ts)) cnt))]
(log/info "Atom derefs/s:" ops-per-sec)
(is (> ops-per-sec 1000)))))
(deftest deref-atom-repeatedly
(dotimes [_ test-runs]
(deref-atom-repeatedly-fn)))
(defn put-on-chan-repeatedly-fn
"Channel with attached mult and no other channels tapping into mult: messages silently dropped."
[]
(let [start-ts (component/now)
cnt (* 100 1000)
ch (chan)
m (mult ch)
done (promise-chan)]
(go
(dotimes [n cnt] (>! ch n))
(put! done true))
(tp/w-timeout cnt (go
(testing "all messages received"
(is (true? (<! done))))
(let [ops-per-sec (int (* (/ 1000 (- (component/now) start-ts)) cnt))]
(log/info "Channel puts/s:" ops-per-sec)
(is (> ops-per-sec 1000)))))))
(deftest put-on-chan-repeatedly1 (put-on-chan-repeatedly-fn))
(deftest put-on-chan-repeatedly2 (put-on-chan-repeatedly-fn))
(deftest put-on-chan-repeatedly3 (put-on-chan-repeatedly-fn))
(deftest put-on-chan-repeatedly4 (put-on-chan-repeatedly-fn))
(deftest put-on-chan-repeatedly5 (put-on-chan-repeatedly-fn))
(deftest put-on-chan-repeatedly6 (put-on-chan-repeatedly-fn))
(deftest put-consume-repeatedly
"Channel with attached go-loop, simple calculation using messages from channel."
(let [start-ts (component/now)
cnt (* 100 1000)
ch (chan)
state (atom 0)
done (promise-chan)]
(go (dotimes [n cnt] (>! ch n)))
(go-loop []
(let [n (<! ch)
res (+ @state n)]
(reset! state res)
(when (= (dec cnt) n)
(put! done true)))
(recur))
(tp/w-timeout cnt (go
(testing "all messages received"
(is (true? (<! done))))
(let [ops-per-sec (int (* (/ 1000 (- (component/now) start-ts)) cnt))]
(log/info "Channel puts and consume/s:" ops-per-sec)
(is (> ops-per-sec 1000)))
(testing "all messages received (sum of all number sent matches)"
(is (= @state (reduce + (range cnt)))))))))
(deftest put-consume-mult-repeatedly
"Channel with attached go-loop, simple calculation using messages from channel."
(let [start-ts (component/now)
cnt (* 100 1000)
ch (chan)
m (mult ch)
ch2 (chan)
state (atom 0)
done (promise-chan)]
(go (dotimes [n cnt] (>! ch n)))
(tap m ch2)
(go-loop []
(let [n (<! ch2)
res (+ @state n)]
(reset! state res)
(when (= (dec cnt) n)
(put! done true)))
(recur))
(tp/w-timeout cnt (go
(testing "all messages received"
(is (true? (<! done))))
(let [ops-per-sec (int (* (/ 1000 (- (component/now) start-ts)) cnt))]
(log/info "Channel puts and consume from mult/s:" ops-per-sec)
(is (> ops-per-sec 1000)))
(testing "all messages received (sum of all number sent matches)"
(is (= @state (reduce + (range cnt)))))))))
(defn put-consume-mult-w-pub-repeatedly-fn
"Channel with attached go-loop, simple calculation using messages from channel, publication of state change. This
imitates the basic use case of the systems-toolbox: there's a go-loop, some processing and publication of component
state. Running this test gives some perspective of the amount of overhead that the systems-toolbox introduces,
such as adding metadata to messages."
[]
(let [start-ts (component/now)
cnt (* 100 1000)
ch (chan)
m (mult ch)
ch2 (chan)
state-pub-chan (chan (sliding-buffer 1))
state-mult (mult state-pub-chan)
state (atom 0)
done (promise-chan)]
(go-loop []
(let [n (<! ch2)
res (+ @state n)]
(reset! state res)
(>! state-pub-chan res)
(when (= (dec cnt) n)
(put! done true)))
(recur))
(tap m ch2)
(go (dotimes [n cnt] (>! ch n)))
(tp/w-timeout cnt (go
(testing "promise delivered"
(is (true? (<! done))))
(let [ops-per-sec (int (* (/ 1000 (- (component/now) start-ts)) cnt))]
(log/info "Channel puts and consume from mult/s (w/pub):" ops-per-sec)
(is (> ops-per-sec 1000)))
(testing "all messages received (sum of all number sent matches)"
(is (= @state (reduce + (range cnt)))))
:done))))
(deftest put-consume-mult-w-pub-repeatedly (put-consume-mult-w-pub-repeatedly-fn))
(deftest put-consume-mult-w-pub-repeatedly2 (put-consume-mult-w-pub-repeatedly-fn))
(deftest put-consume-mult-w-pub-repeatedly3 (put-consume-mult-w-pub-repeatedly-fn))
(deftest put-consume-mult-w-pub-repeatedly4 (put-consume-mult-w-pub-repeatedly-fn))
(deftest put-consume-mult-w-pub-repeatedly5 (put-consume-mult-w-pub-repeatedly-fn))
(deftest put-consume-mult-w-pub-repeatedly6 (put-consume-mult-w-pub-repeatedly-fn))
================================================
FILE: project.clj
================================================
(defproject matthiasn/systems-toolbox "0.6.41"
:description "Toolbox for building Systems in Clojure"
:url "https://github.com/matthiasn/systems-toolbox"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:source-paths ["src/cljc"]
:dependencies [[org.clojure/core.match "0.3.0"]
[org.clojure/tools.logging "0.5.0"]
[io.aviso/pretty "0.1.37"]
[expound "0.8.2"]
[com.lucasbradstreet/cljs-uuid-utils "1.0.2"]]
:plugins [[lein-codox "0.10.7"]
[test2junit "1.4.2"]
[lein-doo "0.1.11"]
[lein-cloverage "1.1.2"]
[lein-ancient "0.6.15"]
[com.jakemccrary/lein-test-refresh "0.24.1"]
[lein-cljsbuild "1.1.7"]]
:test2junit-output-dir
~(or (System/getenv "CIRCLE_TEST_REPORTS") "target/test2junit")
:test-refresh {:notify-on-success false
:changes-only false}
:clean-targets ^{:protect false} ["target/" "out/"]
:test-paths ["test"]
;:test-paths ["dev-resources" "test" "perf"]
:profiles {:dev {:dependencies [[org.clojure/clojure "1.10.1"]
[org.clojure/clojurescript "1.10.597"]
[org.clojure/core.async "0.6.532"]
[ch.qos.logback/logback-classic "1.2.3"]]
:jvm-opts ["-Dclojure.compiler.direct-linking=true"]}}
:cljsbuild
{:builds [{:id "cljs-test"
:source-paths ["src" "test"]
:compiler {:output-to "out/testable.js"
:main matthiasn.systems-toolbox.runner
:target :nodejs
:optimizations :advanced}}
{:id "cljs-perf-test"
:source-paths ["perf" "src" "test"]
:compiler {:output-to "out/perf.js"
:target :nodejs
:main matthiasn.systems-toolbox.perf-runner
:optimizations :advanced}}]})
================================================
FILE: src/cljc/matthiasn/systems_toolbox/component/helpers.cljc
================================================
(ns matthiasn.systems-toolbox.component.helpers
(:require
#?(:clj [clojure.pprint :as pp]
:cljs [cljs.pprint :as pp])
#?(:cljs [cljs-uuid-utils.core :as uuid])))
(defn now
"Get milliseconds since epoch."
[]
#?(:clj (System/currentTimeMillis)
:cljs (.getTime (js/Date.))))
(defn pp-str [data] (with-out-str (pp/pprint data)))
(defn make-uuid
"Get a random UUID."
[]
#?(:clj (java.util.UUID/randomUUID)
:cljs (uuid/make-random-uuid)))
#?(:cljs (def request-animation-frame
(or (when (exists? js/window)
(or (.-requestAnimationFrame js/window)
(.-webkitRequestAnimationFrame js/window)
(.-mozRequestAnimationFrame js/window)
(.-msRequestAnimationFrame js/window)))
(fn [callback] (js/setTimeout callback 17)))))
================================================
FILE: src/cljc/matthiasn/systems_toolbox/component/msg_handling.cljc
================================================
(ns matthiasn.systems-toolbox.component.msg-handling
#?(:cljs (:require-macros [cljs.core.async.macros :as cam :refer [go-loop]]
[cljs.core :refer [exists?]]))
(:require [matthiasn.systems-toolbox.spec :as spec]
#?(:clj [clojure.tools.logging :as l]
:cljs [matthiasn.systems-toolbox.log :as l])
[matthiasn.systems-toolbox.component.helpers :as h]
#?(:clj [io.aviso.exception :as ex])
#?(:clj [clojure.core.match :refer [match]]
:cljs [cljs.core.match :refer-macros [match]])
#?(:clj [clojure.core.async :as a :refer [chan go-loop]]
:cljs [cljs.core.async :as a :refer [chan]])
[clojure.set :as s]))
(defn put-msg
"On the JVM, always uses the blocking operation for putting messages on a
channel, as otherwise the system easily blows up when there are more than 1024
pending put operations. On the ClojureScript side, there is no equivalent of
the blocking put, so the asynchronous operation will have to do. But then,
more than 1024 pending operations in the browser wouldn't happen often,
if ever."
[channel msg]
#?(:clj (a/>!! channel msg)
:cljs (a/put! channel msg)))
(defn make-chan-w-buf
"Create a channel with a buffer of the specified size and type."
[config]
(match config
[:sliding n] (chan (a/sliding-buffer n))
[:buffer n] (chan (a/buffer n))
:else (prn "invalid: " config)))
(defn add-to-msg-seq
"Function for adding the current component ID to the sequence that the message
has traversed thus far. The specified component IDs is either added when the
cmp-seq is empty in the case of an initial send or when the message is
received by a component. This avoids recording component IDs multiple times."
[msg-meta cmp-id in-out]
(let [cmp-seq (vec (:cmp-seq msg-meta))]
(if (not= (last cmp-seq) cmp-id)
(assoc-in msg-meta [:cmp-seq] (conj cmp-seq cmp-id))
msg-meta)))
(defn default-state-pub-handler
"Default handler function, can be replaced by a more application-specific
handler function, for example for resetting local component state when user
is not logged in."
[{:keys [observed msg-payload observed-xform]}]
(let [new-state (if observed-xform (observed-xform msg-payload) msg-payload)]
(when (not= @observed new-state)
(reset! observed new-state))
{}))
(defn mk-handler-return-fn
"Returns function for handling the return value of a handler function.
This returned map can contain :new-state, :emit-msg and :send-to-self keys."
[{:keys [state-reset-fn cfg put-fn cmp-id] :as cmp-map} in-chan msg-type msg-meta]
(fn [{:keys [new-state emit-msg emit-msgs send-to-self] :as handler-res}]
(l/debug cmp-id "handler returned")
(let [emit-msg-fn (fn [msg]
(when (seq msg)
(put-fn (with-meta msg (or (meta msg) msg-meta)))))]
(when new-state (when-let [state-spec (:state-spec cmp-map)]
(when (:validate-state cfg)
(assert (spec/valid-or-no-spec? state-spec new-state))
(l/debug cmp-id "returned state validated")))
(state-reset-fn new-state))
(when send-to-self (if (vector? (first send-to-self))
(a/onto-chan in-chan send-to-self false)
(a/onto-chan in-chan [send-to-self] false)))
(when emit-msg (if (vector? (first emit-msg))
(doseq [msg-to-emit emit-msg] (emit-msg-fn msg-to-emit))
(emit-msg-fn emit-msg)))
(let [res-keys (set (keys handler-res))
known-keys #{:new-state :emit-msg :emit-msgs :send-to-self}]
(when-not (s/subset? res-keys known-keys)
(l/warn "Unknown keys in handler result. THIS IS PROBABLY NOT WHAT YOU WANT."
(s/difference res-keys known-keys)
cmp-id
msg-type)))
(when emit-msgs
(l/warn "DEPRECATED: emit-msgs, use emit-msg with a msg vector instead")
(doseq [msg-to-emit emit-msgs]
(emit-msg-fn msg-to-emit))))))
(defn msg-handler-loop
"Constructs a map with a channel for the provided channel keyword, with the
buffer configured according to cfg for the channel keyword. Then starts loop
for taking messages off the returned channel and calling the provided
handler-fn with the msg.
Uses return value from handler function to change state and emit messages if
the respective keys :new-state and :emit-msg exist. Thus, the handler
function can be free from side effects.
For backwards compatibility, it is also possible interact with the put-fn and
the cmp-state atom directly, in which case the handler function itself would
produce side effects.
This, however, makes such handler functions somewhat more difficult to test."
[cmp-map chan-key]
(let [{:keys [handler-map all-msgs-handler state-pub-handler cfg cmp-id
firehose-chan snapshot-publish-fn unhandled-handler
state-snapshot-fn system-info]
:or {handler-map {}}} cmp-map
in-chan (make-chan-w-buf (chan-key cfg))
onto-in-chan #(a/onto-chan in-chan % false)]
(go-loop []
(let [msg (a/<! in-chan)]
(l/debug cmp-id "msg received" msg)
(try
(let [recv-ts (h/now)
msg-meta (-> (merge (meta msg) {})
(add-to-msg-seq cmp-id :in)
(assoc-in [cmp-id :in-ts] (h/now))
(assoc-in [:system-info] system-info))
[msg-type msg-payload] msg
handler-fn (msg-type handler-map)
put-fn (fn [msg]
(let [msg-meta (merge msg-meta (meta msg))
wrapped-put-fn (:put-fn cmp-map)]
(wrapped-put-fn (with-meta msg msg-meta))))
msg-map-fn (fn []
(merge
cmp-map
{:msg (with-meta msg msg-meta)
:msg-type msg-type
:msg-meta msg-meta
:put-fn put-fn
:msg-payload msg-payload
:onto-in-chan onto-in-chan
:current-state (state-snapshot-fn)}))
handler-return-fn (mk-handler-return-fn cmp-map in-chan msg-type msg-meta)
observed-state-handler (or state-pub-handler
default-state-pub-handler)]
(when (:validate-in cfg)
(assert (spec/valid-or-no-spec? msg-type msg-payload)))
(l/debug cmp-id "msg validated")
(when (= chan-key :sliding-in-chan)
(handler-return-fn (observed-state-handler (msg-map-fn)))
(l/debug cmp-id "observed-state-handler done")
(when (and (:snapshots-on-firehose cfg)
(not= "firehose" (namespace msg-type)))
(put-msg firehose-chan
[:firehose/cmp-recv-state {:cmp-id cmp-id :msg msg}]))
(l/debug cmp-id "state snapshot published on firehose")
(a/<! (a/timeout (:throttle-ms cfg))))
(when (= chan-key :in-chan)
(when (= msg-type :cmd/publish-state) (snapshot-publish-fn))
(when handler-fn
(handler-return-fn (handler-fn (msg-map-fn)))
(l/debug cmp-id "handler function done"))
(when unhandled-handler
(when-not (contains? handler-map msg-type)
(handler-return-fn (unhandled-handler (msg-map-fn)))
(l/debug cmp-id "unhandled-handler function done")))
(when all-msgs-handler
(handler-return-fn (all-msgs-handler (msg-map-fn)))
(l/debug cmp-id "all-msgs-handler function done"))
(when (and (:msgs-on-firehose cfg)
(not= "firehose" (namespace msg-type)))
(let [now (h/now)]
(put-msg firehose-chan [:firehose/cmp-recv
{:cmp-id cmp-id
:firehose-id (h/make-uuid)
:system-info system-info
:msg msg
:msg-meta msg-meta
:duration (- now recv-ts)
:ts now}])))
(l/debug cmp-id "received message published on firehose")))
#?(:clj (catch Exception e
(l/error "Exception in" cmp-id "when receiving message:"
(ex/format-exception e) (h/pp-str msg)))
:cljs (catch js/Object e
(l/error e
(str "Exception in " cmp-id " when receiving message:"
(h/pp-str msg)))))
#?(:clj (catch AssertionError e
(l/error "AssertionError in" cmp-id "when receiving message:"
(ex/format-exception e) (h/pp-str msg)))))
(recur)))
{chan-key in-chan}))
(defn make-put-fn
"The put-fn is used inside each component for emitting messages to the outside
world, from the component's point of view. All the component needs to know is
the type of the message.
Messages are vectors of two elements, where the first one is the type as a
namespaced keyword and the second one is the message payload, like this:
[:some/msg-type {:some \"data\"}]
Message payloads are typically maps or vectors, but they can also be strings,
primitive types, or nil. As long as they are local, they can even be any
type, e.g. a channel, but once we want messages to traverse some message
transport (WebSockets, some message queue), the types need to be limited to
what EDN or Transit can serialize.
Note that on component startup, this channel is not wired anywhere until the
'system-ready-fn' (below) is called, which pipes this channel into the actual
out-chan. Thus, components should not try call more messages than fit in the
buffer before the entire system is up."
[{:keys [cmp-id put-chan cfg firehose-chan system-info]}]
(let [put-one-msg
(fn [msg]
(try
(l/debug cmp-id "put-fn called")
(let [msg-meta (-> (merge (meta msg) {})
(add-to-msg-seq cmp-id :out)
(assoc-in [cmp-id :out-ts] (h/now)))
corr-id (h/make-uuid)
tag (or (:tag msg-meta) (h/make-uuid))
tag-ts (or (:tag-ts msg-meta) (h/now))
completed-meta (merge msg-meta {:corr-id corr-id
:tag-ts tag-ts
:tag tag})
msg-w-meta (with-meta msg completed-meta)
msg-type (first msg)
msg-payload (second msg)
msg-from-firehose? (= "firehose" (namespace msg-type))]
(when (:validate-out cfg)
(assert (spec/valid-or-no-spec?
msg-type
msg-payload
#(put-msg firehose-chan
[:firehose/cmp-put
(merge %
{:cmp-id cmp-id
:firehose-id (h/make-uuid)
:system-info system-info
:msg msg-w-meta
:msg-meta completed-meta
:ts (h/now)})])))
(l/debug cmp-id "put-fn msg validated"))
(put-msg put-chan msg-w-meta)
(l/debug cmp-id "put-fn: msg sent")
;; Not all components should emit firehose messages. For example, messages
;; that process firehose messages should not do so again in order to avoid
;; infinite messaging loops.
;; This behavior can be configured when the component is fired up.
(when (:msgs-on-firehose cfg)
;; Some components may emit firehose messages directly. One such example
;; is the WebSockets component which can be used for relaying firehose
;; messages, either from client to server or from server to client.
;; In those cases, the emitted message should go on the firehose channel
;; on the receiving end as such, not wrapped as other messages would
;; (see the second case in the if-clause).
(if msg-from-firehose?
(put-msg firehose-chan msg-w-meta)
(put-msg firehose-chan
[:firehose/cmp-put {:cmp-id cmp-id
:firehose-id (h/make-uuid)
:system-info system-info
:msg msg-w-meta
:msg-meta completed-meta
:ts (h/now)}]))
(l/debug cmp-id "put-fn: msg put on firehose")))
#?(:clj (catch Exception e
(l/error "Exception in" cmp-id "when sending message:"
(ex/format-exception e) "\n" (h/pp-str msg)))
:cljs (catch js/Object e
(l/error (str "Exception in " cmp-id
" when sending message:\n" (h/pp-str msg))
e)))
#?(:clj (catch AssertionError e
(l/error "AssertionError in" cmp-id "when sending message:"
(ex/format-exception e) (h/pp-str msg))))))]
(fn [m]
(if (vector? (first m))
(doseq [msg m] (put-one-msg msg))
(put-one-msg m)))))
(defn send-msg
"Sends message to the specified component. By default, calls to this function
will block when no buffer space is available. Asynchronous handling is also
possible (JVM only), however the implications should be understood, such as
that core.async will throw an exception when there are more than 1024 pending
operations. Under most circumstances, blocking seems like the safer bet.
Note that, unless specified otherwise, the buffer for a component's in-chan
is of size one, see 'component-defaults'."
([cmp msg] (send-msg cmp msg true))
([cmp msg blocking?]
(let [in-chan (:in-chan cmp)]
#?(:clj (if blocking?
(a/>!! in-chan msg)
(a/put! in-chan msg))
:cljs (a/put! in-chan msg)))))
(defn send-msgs
"Sends multiple messages to a component. Takes the component itself plus a
sequence with messages to send to the component.
Does not close the :in-chan of the component."
[cmp msgs]
(let [in-chan (:in-chan cmp)]
(a/onto-chan in-chan msgs false)))
================================================
FILE: src/cljc/matthiasn/systems_toolbox/component.cljc
================================================
(ns matthiasn.systems-toolbox.component
(:require [matthiasn.systems-toolbox.spec :as s]
#?(:clj [clojure.tools.logging :as l]
:cljs [matthiasn.systems-toolbox.log :as l])
#?(:clj [io.aviso.exception :as ex])
[matthiasn.systems-toolbox.component.helpers :as h]
[matthiasn.systems-toolbox.component.msg-handling :as msg]
#?(:clj [clojure.core.async :as a :refer [chan]]
:cljs [cljs.core.async :as a :refer [chan]])))
(defn now [] (h/now))
(defn make-uuid [] (h/make-uuid))
(def send-msg msg/send-msg)
(def send-msgs msg/send-msgs)
(def component-defaults
{:in-chan [:buffer 1]
:sliding-in-chan [:sliding 1]
:throttle-ms 1
:out-chan [:buffer 1]
:sliding-out-chan [:sliding 1]
:firehose-chan [:buffer 1]
:publish-snapshots true
:snapshots-on-firehose false
:msgs-on-firehose false
:reload-cmp true
:validate-in false
:validate-out true
:validate-state true})
(defn make-snapshot-publish-fn
"Creates a function for publishing changes to the component state atom as
snapshot messages."
[{:keys [watch-state snapshot-xform-fn cmp-id sliding-out-chan cfg
firehose-chan]}]
(fn []
(when (:publish-snapshots cfg)
(let [snapshot @watch-state
snapshot-xform (if snapshot-xform-fn
(snapshot-xform-fn snapshot)
snapshot)
snapshot-msg (with-meta [:app/state snapshot-xform] {:from cmp-id})
state-firehose-chan (chan (a/sliding-buffer 1))]
(a/pipe state-firehose-chan firehose-chan)
(msg/put-msg sliding-out-chan snapshot-msg)
(when (:snapshots-on-firehose cfg)
(msg/put-msg state-firehose-chan
[:firehose/cmp-publish-state {:cmp-id cmp-id
:firehose-id (h/make-uuid)
:snapshot snapshot-xform
:ts (now)}]))))))
(defn detect-changes
"Detect changes to the component state atom and then publish a snapshot using
the 'snapshot-publish-fn'."
[{:keys [watch-state cmp-id snapshot-publish-fn]}]
(try
(add-watch watch-state cmp-id (fn [_ _ _ _new-state]
(snapshot-publish-fn)))
#?(:clj (catch Exception e
(l/error "Failed watching atom" cmp-id
(ex/format-exception e)
(h/pp-str watch-state)))
:cljs (catch js/Object e (l/error e "Failed watching atom" cmp-id
(h/pp-str watch-state))))))
(defn make-system-ready-fn
"This function is called by the switchboard that wired this component when all
other components are up and the channels between them connected. At this
point, messages that were accumulated on the 'put-chan' buffer since startup
are released. Also, the component state is published."
[{:keys [put-chan out-chan snapshot-publish-fn]}]
(fn []
(a/pipe put-chan out-chan)
(snapshot-publish-fn)))
(defn initial-cmp-map
"Assembles initial component map with core.async channels.
- :put-chan is used in component's put-fn, not connected at first
- :out-chan is the outgoing channel
- :firehose-chan is for where all messages go (for debugging)
- :sliding-out-chan is for state snapshots
"
[cmp-map cfg]
(merge cmp-map
{:put-chan (msg/make-chan-w-buf (:out-chan cfg))
:out-chan (msg/make-chan-w-buf (:out-chan cfg))
:cfg cfg
:firehose-chan (msg/make-chan-w-buf (:firehose-chan cfg))
:sliding-out-chan (msg/make-chan-w-buf (:sliding-out-chan cfg))}))
(defn make-component
"Creates a component with attached in-chan, out-chan, sliding-in-chan and
sliding-out-chan.
It takes the initial state atom, the handler function for messages on
the in-chan, and the sliding-handler function, which handles messages on
:sliding-in-chan.
By default, in-chan and out-chan have standard buffers of size one, whereas
sliding-in-chan and sliding-out-chan have sliding buffers of size one.
The buffer sizes can be configured.
The sliding-channels are meant for events where only ever the latest version
is of interest, such as whenUI components rendering state snapshots from
other components.
Components send messages by using the put-fn, which is provided to the
component when creating it's initial state, and then subsequently in every
call to any of the handler functions. On every message send, a unique
correlation ID is attached to every message.
Also, messages are automatically assigned a tag, which is a unique ID that
doesn't change when a message flows through the system. This tag can also be
assigned manually by initially sending a message with the tag set on the
metadata, as this tag will not be touched by the library whenever it exists
already.
The configuration of a component comes from merging the component defaults
with the opts map that is passed on component creation the :opts key. The
order of the merge operation allows overwriting the default settings.
An observed-xform function can be provided, which transforms the observed
state before resetting the respective observed state. This function takes a
single argument, the observed state snapshot, and is expected to return a
single map with the transformed snapshot."
[{:keys [state-fn opts] :as cmp-map}]
(try
(let [cfg (merge component-defaults opts)
out-pub-chan (msg/make-chan-w-buf (:out-chan cfg))
cmp-map (initial-cmp-map cmp-map cfg)
put-fn (msg/make-put-fn cmp-map)
state-map
(merge
{:state (atom {})
:observed (atom {})}
(when state-fn
(let [new-state (state-fn put-fn)]
(when-let [state-spec (:state-spec cmp-map)]
(when (:validate-state cfg)
(assert (s/valid-or-no-spec? state-spec @(:state new-state)))
(l/debug (:cmp-id cmp-map) "returned state validated")))
new-state)))
state (:state state-map)
watch-state (if-let [watch (:watch opts)] ; watchable atom
(watch state)
state)
cmp-map (merge cmp-map {:watch-state watch-state})
cmp-map (merge
cmp-map
{:snapshot-publish-fn (make-snapshot-publish-fn cmp-map)})
cmp-map
(merge cmp-map
{:out-mult (a/mult (:out-chan cmp-map))
:firehose-mult (a/mult (:firehose-chan cmp-map))
:out-pub (a/pub out-pub-chan first)
:state-pub (a/pub (:sliding-out-chan cmp-map) first)
:cmp-state state
:observed (:observed state-map)
:put-fn put-fn
:system-ready-fn (make-system-ready-fn cmp-map)
:shutdown-fn (:shutdown-fn state-map)
:state-snapshot-fn (fn [] @watch-state)
:state-reset-fn (fn [new-state]
(reset! watch-state new-state))})]
(a/tap (:out-mult cmp-map) out-pub-chan) ; connect out-pub-chan to out-mult
(detect-changes cmp-map) ; publish snapshots when changes are detected
(merge cmp-map
(msg/msg-handler-loop cmp-map :in-chan)
(msg/msg-handler-loop cmp-map :sliding-in-chan)))
#?(:clj (catch Exception e (l/error "Failed to init" (:cmp-id cmp-map)
(ex/format-exception e))
(System/exit 1))
:cljs (catch js/Object e (l/error "Failed to init" (:cmp-id cmp-map) e)))))
================================================
FILE: src/cljc/matthiasn/systems_toolbox/handler_utils.cljc
================================================
(ns matthiasn.systems-toolbox.handler-utils
(:require [clojure.set :refer [subset?]]))
(defn fwd-as
"Creates a handler function that sends the payload of handled message as a new
message type while discarding any metadata on the original message."
[new-type]
(fn
[{:keys [put-fn msg-payload]}]
(put-fn [new-type msg-payload])))
(defn fwd-as-w-meta
"Creates a handler function that sends the payload of the handled message as a
new message type preserving metadata of the original message."
[new-type]
(fn
[{:keys [put-fn msg-payload msg-meta]}]
(put-fn (with-meta [new-type msg-payload] msg-meta))))
(defn run-handler
"Runs another handler function with a new message and otherwise the same
context."
([handler-key msg-payload msg-map]
(let [handler-fn (handler-key (:handler-map msg-map))]
(when handler-fn (handler-fn (assoc msg-map :msg-type handler-key
:msg-payload msg-payload
:msg [handler-key msg-payload])))))
([handler-key msg-map]
;; A common mistake is to call (run-handler) with a handler-key and msg-payload, but not with
;; a msg-map. In such case, this fn would do nothing and fail silently, leaving user in a blank.
;; Assert to verify against that.
(assert (subset? #{:cmp-state :msg-meta :msg-payload} (set (keys msg-map)))
"(run-handler) invoked with invalid arguments. Make sure you did pass msg-map.")
(let [handler-fn (handler-key (:handler-map msg-map))]
(when handler-fn (handler-fn (assoc msg-map :msg-type handler-key
:msg [handler-key]))))))
(defn assoc-in-cmp
"Helper for creating a function that sets value in component atom in given path."
[path]
(fn [{:keys [current-state msg-payload]}]
{:new-state (assoc-in current-state path msg-payload)}))
(defn update-in-cmp
"Helper for creating a function that updates value in component atom in given
path by applying f."
[path f]
(fn [{:keys [current-state]}]
{:new-state (update-in current-state path f)}))
================================================
FILE: src/cljc/matthiasn/systems_toolbox/log.cljc
================================================
(ns matthiasn.systems-toolbox.log
"Some helpers for logging in ClojureScript"
(:require [clojure.string :as s]
[matthiasn.systems-toolbox.component.helpers :as h]))
(def ^{:dynamic true} *debug-enabled* false)
(defn enable-debug-log! [] (set! *debug-enabled* true))
(defn info [& args]
(println (s/join " " args)))
(defn warn [& args]
(println (str "WARN: " (s/join " " args))))
(defn error [& args]
(println (str "ERROR: " (s/join " " args))))
(defn debug [& args]
(when *debug-enabled*
(println (str (h/now) " DEBUG: " (s/join " " args)))))
================================================
FILE: src/cljc/matthiasn/systems_toolbox/scheduler.cljc
================================================
(ns matthiasn.systems-toolbox.scheduler
#?(:cljs (:require-macros [cljs.core.async.macros :refer [go-loop]]))
(:require [matthiasn.systems-toolbox.component :as comp]
#?(:clj [clojure.tools.logging :as l]
:cljs [matthiasn.systems-toolbox.log :as l])
#?(:clj [clojure.core.async :refer [<! go-loop timeout]])
#?(:cljs [cljs.core.async :refer [<! timeout]])))
;;; Systems Toolbox - Scheduler Subsystem
;;; This namespace describes a component / subsystem for scheduling the sending of messages that
;;; can then elsewhere trigger some action.
;;; Example: we want to let web clients know how many documents we have in a database so they can
;;; update the UI accordingly. The subsystem handling the database connectivity has the logic for
;;; figuring out how many documents there are when receiving a request, but no notion of repeatedly
;;; emitting this information itself. Now say we want this every 10 seconds. We tell the scheduler
;;; to emit the message type that will trigger the request every 10 seconds, and that's it.
;;; Internally, each scheduled event starts a go-loop with a timeout of the specified duration
;;; while recording the scheduled event in the state atom. Post-timeout, it is checked if the
;;; message is still scheduled to be sent and if so, the specified message is sent.
;;; Scheduled events can be deleted. TODO: implement
;;; When the same, optional :id is set on multiple message sent to scheduler component, only
;;; first of those messages will result in scheduling a new timer.
;;; TODO: record start time so that the scheduled time can be shown in UI. Platform-specific implementation.
;;; WARNING: timeouts specified here are not precise unless proven otherwise. Even if timeouts
;;; happen to have a sufficiently precise duration, the go-loop in which they run (and the
;;; associated thread pool) may be busy otherwise and delay the next iteration.
(defn start-loop
"Starts a loop for sending messages at set intervals."
[{:keys [current-state cmp-state put-fn msg-meta msg-payload]}]
(let [timeout-ms (:timeout msg-payload)
msg-to-send (:message msg-payload)
msg-meta (merge
(update-in msg-meta [:cmp-seq] #(vec (take-last 2 %)))
(meta msg-to-send)
(:msg-meta msg-payload))
msg-to-send (with-meta msg-to-send msg-meta)
scheduler-id (or (:id msg-payload) (first msg-to-send))]
(if (get-in current-state [:active-timers scheduler-id])
(l/debug (str "Timer " scheduler-id " already scheduled - ignoring."))
(do (when (:initial msg-payload) (put-fn msg-to-send))
(go-loop []
(<! (timeout timeout-ms))
(let [active-timer (get-in @cmp-state [:active-timers scheduler-id])]
(put-fn msg-to-send)
(if active-timer
(if (:repeat active-timer)
(recur)
(swap! cmp-state update :active-timers dissoc scheduler-id))
(put-fn [:info/deleted-timer scheduler-id]))))
{:new-state (assoc-in current-state [:active-timers scheduler-id] msg-payload)}))))
(defn stop-loop
"Stops a an loop that was previously scheduled."
[{:keys [current-state msg-payload]}]
(let [scheduler-id (:id msg-payload)]
(if (get-in current-state [:active-timers scheduler-id])
{:new-state (update-in current-state [:active-timers scheduler-id] msg-payload)}
(l/warn (str "Timer with id: " (:id msg-payload) " not found - did not stop.")))))
(defn state-fn
[_put-fn]
(let [initial-state {:active-timers {}}
state-atom (atom initial-state)]
{:state state-atom
:shutdown-fn #(reset! state-atom initial-state)}))
(defn cmp-map
{:added "0.3.1"}
[cmp-id]
{:cmp-id cmp-id
:state-fn state-fn
:handler-map {:schedule/new start-loop
:schedule/delete stop-loop
:cmd/schedule-new start-loop
:cmd/schedule-delete stop-loop}
:opts {:reload-cmp false}})
(defn component
{:deprecated "0.3.1"}
[cmp-id]
(comp/make-component (cmp-map cmp-id)))
================================================
FILE: src/cljc/matthiasn/systems_toolbox/spec.cljc
================================================
(ns matthiasn.systems-toolbox.spec
(:require [expound.alpha :as exp]
#?(:clj [clojure.spec.alpha :as s]
:cljs [cljs.spec.alpha :as s])
#?(:clj [clojure.tools.logging :as l]
:cljs [matthiasn.systems-toolbox.log :as l])))
(defn valid-or-no-spec?
"If spec exists, validate spec and warn if x is invalid, with detailed
explanation. Also puts that information on firehose for use in inspect."
([spec x] (valid-or-no-spec? spec x nil))
([spec x firehose-put]
(if (contains? (s/registry) spec)
(if (s/valid? spec x)
true
(let [validation-error (exp/expound-str spec x)]
(l/error validation-error)
(when firehose-put (firehose-put {:spec-error validation-error}))
false))
(let [warning (str "UNDEFINED SPEC " spec)]
(l/warn warning)
(when firehose-put (firehose-put {:spec-warning warning}))
true))))
(defn namespaced-keyword? [k] (and (keyword? k) (namespace k)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Message Spec
(s/def :systems-toolbox/msg-spec
(s/or :no-payload (s/cat :msg-type namespaced-keyword?)
:payload (s/cat :msg-type namespaced-keyword?
:msg-payload (s/or :map-payload map?
:vector-payload vector?
:nil-payload nil?
:bool-payload boolean?
:number-payload number?
:string-payload string?
:keyword-payload keyword?))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Firehose Spec
(s/def :st.firehose/cmp-id namespaced-keyword?)
(s/def :st.firehose/msg :systems-toolbox/msg-spec)
(s/def :st.firehose/ts pos-int?)
(s/def :st.firehose/snapshot map?)
(s/def :firehose/cmp-recv
(s/keys :req-un [:st.firehose/cmp-id
:st.firehose/msg
:st.firehose/msg-meta
:st.firehose/ts]))
(s/def :firehose/cmp-put :firehose/cmp-recv)
(s/def :firehose/cmp-publish-state
(s/keys :req-un [:st.firehose/cmp-id
:st.firehose/snapshot
:st.firehose/ts]))
(s/def :firehose/cmp-recv-state
(s/keys :req-un [:st.firehose/cmp-id
:st.firehose/msg]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Scheduler Spec
(s/def :st.schedule/timeout pos-int?)
(s/def :st.schedule/message :systems-toolbox/msg-spec)
(s/def :st.schedule/id keyword?)
(s/def :st.schedule/repeat boolean?)
(s/def :st.schedule/initial boolean?)
(s/def :schedule/new
(s/keys :req-un [:st.schedule/timeout
:st.schedule/message]
:opt-un [:st.schedule/id
:st.schedule/repeat
:st.schedule/initial]))
(s/def :schedule/delete
(s/keys :req-un [:st.schedule/id]))
(s/def :cmd/schedule-new :schedule/new)
(s/def :cmd/schedule-delete :schedule/delete)
(s/def :info/deleted-timer keyword?)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; App State Specs
;; TODO: define specific specs for a component in order to validate the :new-state returned by handler functions
(s/def :app/state map?)
(s/def :cmd/publish-state nil?)
================================================
FILE: src/cljc/matthiasn/systems_toolbox/switchboard/helpers.cljc
================================================
(ns matthiasn.systems-toolbox.switchboard.helpers
"Helper functions used by switchboard."
(:require [matthiasn.systems-toolbox.spec :as spec]
#?(:clj [clojure.tools.logging :as l]
:cljs [matthiasn.systems-toolbox.log :as l])))
(defn cartesian-product
"All the ways to take one item from each sequence.
Borrowed from: https://github.com/clojure/math.combinatorics/blob/master/src/main/clojure/clojure/math/combinatorics.clj
Reason: https://groups.google.com/forum/#!topic/clojure-dev/PDyOklDEv7Y"
[& seqs]
(let [v-original-seqs (vec seqs)
step
(fn step [v-seqs]
(let [increment
(fn [v-seqs]
(loop [i (dec (count v-seqs)), v-seqs v-seqs]
(when-not (= i -1)
(if-let [rst (next (v-seqs i))]
(assoc v-seqs i rst)
(recur (dec i) (assoc v-seqs i (v-original-seqs i)))))))]
(when v-seqs
(cons (map first v-seqs)
(lazy-seq (step (increment v-seqs)))))))]
(when (every? seq seqs)
(lazy-seq (step v-original-seqs)))))
(defn cmp-ids-set
"Returns a set with component IDs."
[val]
(cond (set? val) val
(spec/namespaced-keyword? val) #{val}
(vector? val)
(do (l/warn "Use of vector is deprecated, use a set instead:" val)
(set val))))
================================================
FILE: src/cljc/matthiasn/systems_toolbox/switchboard/init.cljc
================================================
(ns matthiasn.systems-toolbox.switchboard.init
(:require [matthiasn.systems-toolbox.component :as comp]
[matthiasn.systems-toolbox.switchboard.spec]
#?(:clj [clojure.core.async :refer [put! tap untap-all untap unsub-all close!]]
:cljs [cljs.core.async :refer [put! tap untap-all untap unsub-all close!]])
#?(:clj [clojure.tools.logging :as l]
:cljs [matthiasn.systems-toolbox.log :as l])
#?(:clj [clojure.spec.alpha :as s]
:cljs [cljs.spec.alpha :as s])))
(defn cmp-maps-set
"Returns a set with component maps."
[val]
(cond
(set? val) val
(vector? val)
(do (l/warn "Use of vector is deprecated, use a set instead:" val)
(set val))
:else #{val}))
(defn wire-or-init-comp
"Either wire existing and already instantiated component or instantiate a
component from a component map.
Also capable of reloading component, e.g. when using Figwheel on the client
side.
When a previous component with the same name exists, this function first of
all unwires that previous component by unsubscribing and untapping all
connected channels. Then, the state of that previous component is used in the
new component in order to provide a smooth developer experience.
Finally, the new component is tapped into the switchboard's firehose and the
component is also asked to publish its state once (also useful for Figwheel)."
[init?]
(fn [{:keys [current-state msg-payload cmp-id system-info]}]
(let [cmp-maps-set (set (filter identity (cmp-maps-set msg-payload)))
reducer-fn
(fn [acc cmp]
(let [cmp-id-to-wire (:cmp-id cmp)
firehose-chan (:firehose-chan (cmp-id (:components current-state)))
reload? (:reload-cmp (merge comp/component-defaults (:opts cmp)))
prev-cmp (get-in current-state [:components cmp-id-to-wire])]
(when prev-cmp
(untap-all (:firehose-mult prev-cmp))
(untap (:firehose-mult (cmp-id (:components current-state)))
(:in-chan prev-cmp))
(unsub-all (:out-pub prev-cmp))
(unsub-all (:state-pub prev-cmp)))
(when (and prev-cmp reload?)
(when-let [shutdown-fn (:shutdown-fn prev-cmp)]
(shutdown-fn)))
(let [cmp (assoc-in cmp [:system-info] system-info)
cmp (if (or (not prev-cmp) reload?)
(if init? (comp/make-component cmp) cmp)
prev-cmp)]
(if cmp
(let [in-chan (:in-chan cmp)
new-state (-> acc
(assoc-in [:components cmp-id-to-wire] cmp)
(update-in [:fh-taps] conj
{:from cmp-id-to-wire
:to cmp-id
:type :fh-tap}))]
(when-let [prev-state (:watch-state prev-cmp)]
(reset! (:watch-state cmp) @prev-state))
(tap (:firehose-mult cmp) firehose-chan)
(let [known-cmp-ids (set (keys (:components new-state)))]
(s/def :st.switchboard/cmp known-cmp-ids))
(put! in-chan [:cmd/publish-state])
new-state)
acc))))
new-state (reduce reducer-fn current-state cmp-maps-set)]
{:new-state new-state})))
(defn shutdown-all
"Call shutdown function on each component to prepare for reload."
[{:keys [current-state]}]
(let [cmps (:components current-state)]
(doseq [cmp (vals cmps)]
(when-let [shutdown-fn (:shutdown-fn cmp)]
(shutdown-fn)))))
(defn shutdown-cmp
"Call shutdown function on specified component."
[{:keys [current-state msg-payload]}]
(let [cmp (-> current-state :components msg-payload)
new-state (update-in current-state [:components] dissoc msg-payload)]
(when-let [shutdown-fn (:shutdown-fn cmp)]
(shutdown-fn))
{:new-state new-state
:emit-msg [:switchboard/status {:cmd :shutdown
:status :success}]}))
================================================
FILE: src/cljc/matthiasn/systems_toolbox/switchboard/observe.cljc
================================================
(ns matthiasn.systems-toolbox.switchboard.observe
(:require [matthiasn.systems-toolbox.switchboard.helpers :as h]
#?(:clj [clojure.core.async :refer [sub]]
:cljs [cljs.core.async :refer [sub]])))
(defn observe-state
"Handler function for letting one component observe the state of another."
[{:keys [current-state msg-payload]}]
(let [{:keys [from to]} msg-payload
reducer-fn
(fn [acc to]
(let [pub-comp (from (:components acc))
sub-comp (to (:components acc))]
(sub (:state-pub pub-comp) :app/state (:sliding-in-chan sub-comp))
(update-in acc [:subs] conj {:from from
:to to
:msg-type :app/state
:type :sub})))]
{:new-state (reduce reducer-fn current-state (h/cmp-ids-set to))}))
================================================
FILE: src/cljc/matthiasn/systems_toolbox/switchboard/route.cljc
================================================
(ns matthiasn.systems-toolbox.switchboard.route
(:require [matthiasn.systems-toolbox.spec :as spec]
[matthiasn.systems-toolbox.switchboard.helpers :as h]
#?(:clj [clojure.core.async :refer [chan pipe sub tap]]
:cljs [cljs.core.async :refer [chan pipe sub tap]])
#?(:clj [clojure.tools.logging :as l]
:cljs [matthiasn.systems-toolbox.log :as l])
[clojure.set :as set]))
(defn subscribe-fn
"Subscribe component to a specified publisher."
[from to pred]
(fn [current-state msg-type]
(let [in-chan (:in-chan (to (:components current-state)))
target-chan (if pred (let [filtered-chan (chan 1 (filter pred))]
(pipe filtered-chan in-chan)
filtered-chan)
in-chan)]
(sub (:out-pub (from (:components current-state))) msg-type target-chan)
(update-in current-state [:subs] conj {:from from
:to to
:msg-type msg-type
:type :sub}))))
(defn route-handler
"Creates subscriptions between one component's out-pub and another component's
in-chan.
Requires a map with at least the :from and :to keys.
Also, routing can be limited to message types specified under the :only
keyword. Here, either a single message type or a vector with multiple message
types can be used."
[{:keys [current-state msg-payload]}]
{:pre (empty? (set/intersection (h/cmp-ids-set (:from msg-payload))
(h/cmp-ids-set (:to msg-payload))))}
(let [{:keys [from to only pred]} msg-payload
connections (h/cartesian-product (h/cmp-ids-set from) (h/cmp-ids-set to))
subscribe-reducer-fn
(fn [acc [from to]]
(let [handled-messages (keys (:handler-map (to (:components acc))))
msg-types (if only (flatten [only]) (vec handled-messages))
subscribe (subscribe-fn from to pred)]
(reduce subscribe acc msg-types)))]
{:new-state (reduce subscribe-reducer-fn current-state connections)}))
;; TODO: implement filtering with comparable semantics as in route-handler, see issue #34
(defn route-all-handler
"Connects two components where ALL messages are routed to recipient(s), not
only those for which there is a specific handler. This results in both the
all-msgs-handler receiving all messages and the unhandled-handler receiving
those for which there is no handler."
[{:keys [current-state msg-payload]}]
{:pre (empty? (set/intersection (h/cmp-ids-set (:from msg-payload))
(h/cmp-ids-set (:to msg-payload))))}
(let [{:keys [from to pred]} msg-payload
components (:components current-state)
connections (h/cartesian-product (h/cmp-ids-set from) (h/cmp-ids-set to))
reducer-fn (fn [acc [from to]]
(let [in-chan (:in-chan (to components))
target-chan (if pred
(let [filtered-ch (chan 1 (filter pred))]
(pipe filtered-ch in-chan)
filtered-ch)
in-chan)]
(tap (:out-mult (from components)) target-chan)
(update-in acc [:taps] conj {:from from
:to to
:type :tap})))]
{:new-state (reduce reducer-fn current-state connections)}))
================================================
FILE: src/cljc/matthiasn/systems_toolbox/switchboard/spec.cljc
================================================
(ns matthiasn.systems-toolbox.switchboard.spec
(:require [matthiasn.systems-toolbox.spec :as sts]
#?(:clj [clojure.spec.alpha :as s]
:cljs [cljs.spec.alpha :as s])))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Spec for :cmd/init-comp
(s/def :st.switchboard.init/cmp-id sts/namespaced-keyword?)
(s/def :st.switchboard.init/state-fn fn?)
(s/def :st.switchboard.init/handler-map
(s/nilable (s/map-of sts/namespaced-keyword? fn?)))
(s/def :st.switchboard.init/all-msgs-handler fn?)
(s/def :st.switchboard.init/state-pub-handler (s/nilable fn?))
(s/def :st.switchboard.init/observed-xform (s/nilable fn?))
(s/def :st.switchboard.init/opts map?)
(s/def :st.switchboard.init/state-spec sts/namespaced-keyword?)
(s/def :st.switchboard.init/cmp-map
(s/keys :req-un [:st.switchboard.init/cmp-id]
:opt-un [:st.switchboard.init/state-fn
:st.switchboard.init/handler-map
:st.switchboard.init/all-msgs-handler
:st.switchboard.init/state-pub-handler
:st.switchboard.init/observed-xform
:st.switchboard.init/opts
:st.switchboard.init/state-spec]))
(s/def :cmd/init-comp (s/or :single-cmp :st.switchboard.init/cmp-map
:multiple-cmps (s/+ :st.switchboard.init/cmp-map)))
(s/def :st.switchboard/cmp sts/namespaced-keyword?)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Spec for :cmd/route
(s/def :st.switchboard.route/from (s/or :single :st.switchboard/cmp
:multiple (s/+ :st.switchboard/cmp)))
(s/def :st.switchboard.route/to (s/or :single :st.switchboard/cmp
:multiple (s/+ :st.switchboard/cmp)))
(s/def :st.switchboard.route/only :st.switchboard/cmp)
(s/def :st.switchboard.route/pred fn?)
(s/def :cmd/route
(s/keys :req-un [:st.switchboard.route/from
:st.switchboard.route/to]
:opt-un [:st.switchboard.route/only
:st.switchboard.route/pred]))
(s/def :cmd/route-all
(s/keys :req-un [:st.switchboard.route/from
:st.switchboard.route/to]
:opt-un [:st.switchboard.route/pred]))
(s/def :cmd/attach-to-firehose sts/namespaced-keyword?)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Spec for :cmd/observe-state
(s/def :st.switchboard.observe/from :st.switchboard/cmp)
(s/def :st.switchboard.observe/to (s/or :single :st.switchboard/cmp
:multiple (s/+ :st.switchboard/cmp)))
(s/def :cmd/observe-state
(s/keys :req-un [:st.switchboard.observe/from
:st.switchboard.observe/to]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Spec for :cmd/send
(s/def :st.switchboard-send/to :st.switchboard/cmp)
(s/def :st.switchboard-send/msg :systems-toolbox/msg-spec)
(s/def :cmd/send
(s/keys :req-un [:st.switchboard-send/to
:st.switchboard-send/msg]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Misc Switchboard Specs
;; TODO: define structure of component map. Here, the switchboard is passed.
(s/def :cmd/self-register map?)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Switchboard State Spec
(s/def :st.switchboard/components (s/map-of sts/namespaced-keyword? map?))
(s/def :st.switchboard.sub/from :st.switchboard/cmp)
(s/def :st.switchboard.sub/to :st.switchboard/cmp)
(s/def :st.switchboard.sub/msg-type sts/namespaced-keyword?)
(s/def :st.switchboard.sub/type #{:sub})
(s/def :st.switchboard/sub-map
(s/keys :req-un [:st.switchboard.sub/from
:st.switchboard.sub/to
:st.switchboard.sub/msg-type
:st.switchboard.sub/type]))
(s/def :st.switchboard/subs (s/and (s/coll-of :st.switchboard/sub-map) set?))
(s/def :st.switchboard.fh-tap/type #{:fh-tap})
(s/def :st.switchboard/fh-tap-map
(s/keys :req-un [:st.switchboard.sub/from
:st.switchboard.sub/to
:st.switchboard.fh-tap/type]))
(s/def :st.switchboard/fh-taps (s/and (s/coll-of :st.switchboard/fh-tap-map) set?))
(s/def :st.switchboard/state-spec
(s/keys :req-un [:st.switchboard/components
:st.switchboard/subs
:st.switchboard/taps
:st.switchboard/fh-taps]))
================================================
FILE: src/cljc/matthiasn/systems_toolbox/switchboard.cljc
================================================
(ns matthiasn.systems-toolbox.switchboard
(:require
[matthiasn.systems-toolbox.component :as comp]
[matthiasn.systems-toolbox.switchboard.route :as rt]
[matthiasn.systems-toolbox.switchboard.observe :as obs]
[matthiasn.systems-toolbox.switchboard.init :as i]
[matthiasn.systems-toolbox.component.helpers :as h]
#?(:clj [clojure.core.async :refer [put! chan pipe sub tap]]
:cljs [cljs.core.async :refer [put! chan pipe sub tap]])
#?(:clj [clojure.pprint :as pp]
:cljs [cljs.pprint :as pp])
#?(:clj [clojure.tools.logging :as l]
:cljs [matthiasn.systems-toolbox.log :as l])
#?(:clj [io.aviso.exception :as ex])
#?(:clj [clojure.spec.alpha :as s]
:cljs [cljs.spec.alpha :as s])))
(defn self-register
"Registers switchboard itself as another component that can be wired. Useful
for communication with the outside world / within hierarchies where a
subsystem has its own switchboard."
[{:keys [cmp-state msg-payload cmp-id]}]
(swap! cmp-state assoc-in [:components cmp-id] msg-payload)
(swap! cmp-state assoc-in [:switchboard-id] cmp-id)
{})
(defn mk-state [_put-fn]
{:state (atom {:components {}
:subs #{}
:taps #{}
:fh-taps #{}})})
(defn attach-to-firehose
"Attaches a component to firehose channel. For example for observational
components."
[{:keys [current-state msg-payload cmp-id]}]
(let [to msg-payload
sw-firehose-mult (:firehose-mult (cmp-id (:components current-state)))
to-comp (to (:components current-state))]
(try
(do
(tap sw-firehose-mult (:in-chan to-comp))
{:new-state (update-in current-state [:fh-taps] conj {:from cmp-id
:to to
:type :fh-tap})})
#?(:clj (catch Exception e
(l/error "Could not create tap: " cmp-id " -> " to " - "
(ex/format-exception e)))
:cljs (catch js/Object e (l/error "Could not create tap: " cmp-id
" -> " to " - " e))))))
(defn send-to
"Send message to specified component."
[{:keys [cmp-state msg-payload]}]
(let [{:keys [to msg]} msg-payload
dest-comp (to (:components @cmp-state))]
(put! (:in-chan dest-comp) msg))
{})
(defn wire-all-out-channels
"Function for calling the system-ready-fn on each component, which will pipe
the channel used by the put-fn to the out-chan when the system is connected.
Otherwise, messages sent before all channels are wired would get lost."
[{:keys [cmp-state]}]
(doseq [[_ cmp] (:components @cmp-state)]
((:system-ready-fn cmp))))
(def handler-map
{:cmd/route rt/route-handler
:cmd/route-all rt/route-all-handler
:cmd/wire-comp (i/wire-or-init-comp false)
:cmd/init-comp (i/wire-or-init-comp true)
:cmd/shutdown-all i/shutdown-all
:cmd/shutdown i/shutdown-cmp
:cmd/attach-to-firehose attach-to-firehose
:cmd/self-register self-register
:cmd/observe-state obs/observe-state
:cmd/send send-to
:status/system-ready wire-all-out-channels})
(defn xform-fn
"Transformer function for switchboard state snapshot. Allows serialization of
snapshot for sending, e.g. over WebSockets or other transports."
[m]
(update-in m [:components] (fn [cmps]
(into {} (mapv (fn [[k v]] [k k]) cmps)))))
(defn component
"Creates a switchboard component that wires individual components together
into a communicating system."
([switchboard-id]
(component switchboard-id {}))
([switchboard-id cmp-opts]
(let [system-info (merge {:system-id (str (h/make-uuid))
:node-type (str (h/make-uuid))
:node-id (str (h/make-uuid))}
(select-keys cmp-opts [:system-id :node-type :node-id]))
switchboard (comp/make-component
{:cmp-id switchboard-id
:state-fn mk-state
:handler-map handler-map
:system-info system-info
:state-spec :st.switchboard/state-spec
:opts (merge {:msgs-on-firehose false
:snapshots-on-firehose true}
cmp-opts)
gitextract_ypcdeinq/
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── circle.yml
├── dev-resources/
│ └── logback-test.xml
├── doc/
│ ├── rationale.md
│ └── systems-thinking.md
├── examples/
│ ├── redux-counter01/
│ │ ├── .bowerrc
│ │ ├── .gitignore
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── bower.json
│ │ ├── env/
│ │ │ └── dev/
│ │ │ └── cljs/
│ │ │ └── example/
│ │ │ └── dev.cljs
│ │ ├── project.clj
│ │ ├── resources/
│ │ │ ├── logback.xml
│ │ │ └── public/
│ │ │ └── css/
│ │ │ └── example.css
│ │ └── src/
│ │ ├── clj/
│ │ │ └── example/
│ │ │ ├── core.clj
│ │ │ └── index.clj
│ │ └── cljs/
│ │ └── example/
│ │ ├── core.cljs
│ │ ├── counter_ui.cljs
│ │ └── store.cljs
│ └── trailing-mouse-pointer/
│ ├── .gitignore
│ ├── LICENSE
│ ├── README.md
│ ├── env/
│ │ └── dev/
│ │ └── cljs/
│ │ └── example/
│ │ └── dev.cljs
│ ├── project.clj
│ ├── resources/
│ │ ├── logback.xml
│ │ └── public/
│ │ └── css/
│ │ └── example.css
│ └── src/
│ ├── clj/
│ │ └── example/
│ │ ├── core.clj
│ │ └── index.clj
│ ├── cljc/
│ │ └── example/
│ │ ├── pointer.cljc
│ │ ├── server_switchboard.cljc
│ │ └── spec.cljc
│ └── cljs/
│ └── example/
│ ├── core.cljs
│ ├── hist_calc.cljs
│ ├── histogram.cljs
│ ├── observer.cljs
│ ├── re_frame.cljs
│ ├── store.cljs
│ ├── ui_histograms.cljs
│ ├── ui_info.cljs
│ ├── ui_mouse_moves.cljs
│ └── utils.cljs
├── perf/
│ └── matthiasn/
│ └── systems_toolbox/
│ └── runtime_perf_test.cljc
├── project.clj
├── src/
│ └── cljc/
│ └── matthiasn/
│ └── systems_toolbox/
│ ├── component/
│ │ ├── helpers.cljc
│ │ └── msg_handling.cljc
│ ├── component.cljc
│ ├── handler_utils.cljc
│ ├── log.cljc
│ ├── scheduler.cljc
│ ├── spec.cljc
│ ├── switchboard/
│ │ ├── helpers.cljc
│ │ ├── init.cljc
│ │ ├── observe.cljc
│ │ ├── route.cljc
│ │ └── spec.cljc
│ └── switchboard.cljc
└── test/
└── matthiasn/
└── systems_toolbox/
├── component_test.cljc
├── handler_utils_test.cljc
├── perf_runner.cljs
├── runner.cljs
├── scheduler_test.cljc
├── switchboard_observe_tests.cljc
├── switchboard_route_tests.cljc
├── system.cljc
├── system_test.cljc
├── test_promise.cljc
└── test_spec.cljc
Condensed preview — 71 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (241K chars).
[
{
"path": ".gitignore",
"chars": 176,
"preview": "/target\n/classes\n/checkouts\npom.xml\npom.xml.asc\n*.jar\n*.class\n/.lein-*\n/.nrepl-port\n.hgignore\n.hg/\n.idea/\n*.iml\n.DS_Stor"
},
{
"path": ".travis.yml",
"chars": 75,
"preview": "language: clojure\nscript: lein doo node cljs-test once\njdk:\n - oraclejdk11"
},
{
"path": "CHANGELOG.md",
"chars": 12289,
"preview": "# Change Log\nAll notable changes to this project will be documented in this file. This change log follows the convention"
},
{
"path": "LICENSE",
"chars": 11218,
"preview": "THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC\nLICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION"
},
{
"path": "README.md",
"chars": 6591,
"preview": "# systems-toolbox\n\nApplications are systems. Systems are fascinating entities, and one of their characteristics is that "
},
{
"path": "circle.yml",
"chars": 97,
"preview": "machine:\n java:\n version: openjdk8\ntest:\n override:\n - lein test2junit\n post:\n - ant\n"
},
{
"path": "dev-resources/logback-test.xml",
"chars": 1944,
"preview": "<!-- Logback configuration. See http://logback.qos.ch/manual/index.html -->\n<configuration scan=\"true\" scanPeriod=\"10 se"
},
{
"path": "doc/rationale.md",
"chars": 6246,
"preview": "## Rationale\n\nSome time ago, I wrote this toy application called **[BirdWatch](http://github.com/matthiasn/BirdWatch)**."
},
{
"path": "doc/systems-thinking.md",
"chars": 6498,
"preview": "# Systems Thinking\n\nApplications are systems; however, don't take my word for it. Let's see how an expert on **Systems T"
},
{
"path": "examples/redux-counter01/.bowerrc",
"chars": 55,
"preview": "{\n \"directory\": \"resources/public/bower_components\"\n}\n"
},
{
"path": "examples/redux-counter01/.gitignore",
"chars": 181,
"preview": "/target\n/classes\n/checkouts\npom.xml\npom.xml.asc\n*.jar\n*.class\n/.lein-*\n/.nrepl-port\n.idea\n*.iml\n.DS_Store\n\nresources/pub"
},
{
"path": "examples/redux-counter01/LICENSE",
"chars": 11218,
"preview": "THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC\nLICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION"
},
{
"path": "examples/redux-counter01/README.md",
"chars": 1633,
"preview": "# systems-toolbox-ui - Redux-style counter example\n\n\n\n\n## Usage\n\nYou can start the serv"
},
{
"path": "examples/redux-counter01/bower.json",
"chars": 480,
"preview": "{\n \"name\": \"trailing-mouse-pointer\",\n \"version\": \"0.1.0\",\n \"homepage\": \"https://github.com/matthiasn/systems-toolbox/"
},
{
"path": "examples/redux-counter01/env/dev/cljs/example/dev.cljs",
"chars": 301,
"preview": "(ns ^:figwheel-no-load example.dev\n (:require [example.core :as c]\n [figwheel.client :as figwheel :include-m"
},
{
"path": "examples/redux-counter01/project.clj",
"chars": 1919,
"preview": "(defproject matthiasn/redux-counter01 \"0.6.1-SNAPSHOT\"\n :description \"Sample application built with systems-toolbox lib"
},
{
"path": "examples/redux-counter01/resources/logback.xml",
"chars": 2160,
"preview": "<configuration>\n\n <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n <encoder>\n "
},
{
"path": "examples/redux-counter01/resources/public/css/example.css",
"chars": 247,
"preview": ".counters {\n margin: 20px;\n font-family: sans-serif;\n color: #555;\n}\n\n.counters button {\n font-weight: 300;\n"
},
{
"path": "examples/redux-counter01/src/clj/example/core.clj",
"chars": 1221,
"preview": "(ns example.core\n (:require [matthiasn.systems-toolbox.switchboard :as sb]\n [matthiasn.systems-toolbox-sente"
},
{
"path": "examples/redux-counter01/src/clj/example/index.clj",
"chars": 543,
"preview": "(ns example.index\n (:require [hiccup.core :refer [html]]))\n\n(defn index-page\n \"Generates index page HTML.\"\n [_]\n (ht"
},
{
"path": "examples/redux-counter01/src/cljs/example/core.cljs",
"chars": 548,
"preview": "(ns example.core\n (:require [example.store :as store]\n [example.counter-ui :as cnt]\n [matthiasn.s"
},
{
"path": "examples/redux-counter01/src/cljs/example/counter_ui.cljs",
"chars": 1626,
"preview": "(ns example.counter-ui\n (:require [reagent.core :as r]\n [re-frame.core :refer [reg-sub subscribe]]\n "
},
{
"path": "examples/redux-counter01/src/cljs/example/store.cljs",
"chars": 2085,
"preview": "(ns example.store\n \"In this namespace, the app state is managed. One can only interact with the\n state by sending imm"
},
{
"path": "examples/trailing-mouse-pointer/.gitignore",
"chars": 213,
"preview": "/target\n/classes\n/checkouts\npom.xml\npom.xml.asc\n*.jar\n*.class\n/.lein-*\n/.nrepl-port\n.idea\n*.iml\n.DS_Store\n\nresources/pub"
},
{
"path": "examples/trailing-mouse-pointer/LICENSE",
"chars": 11218,
"preview": "THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC\nLICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION"
},
{
"path": "examples/trailing-mouse-pointer/README.md",
"chars": 1418,
"preview": "# systems-toolbox - Trailing Mouse Example\n\n## Usage\n\nBefore the first usage, you want to install the **[Bower](http://b"
},
{
"path": "examples/trailing-mouse-pointer/env/dev/cljs/example/dev.cljs",
"chars": 292,
"preview": "(ns ^:figwheel-no-load example.dev\n (:require [example.core :as c]\n [figwheel.client :as figwheel :include-m"
},
{
"path": "examples/trailing-mouse-pointer/project.clj",
"chars": 2114,
"preview": "(defproject matthiasn/trailing-mouse-pointer \"0.6.1-SNAPSHOT\"\n :description \"Sample application built with systems-tool"
},
{
"path": "examples/trailing-mouse-pointer/resources/logback.xml",
"chars": 1879,
"preview": "<configuration>\n\n <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n <encoder>\n "
},
{
"path": "examples/trailing-mouse-pointer/resources/public/css/example.css",
"chars": 468,
"preview": "body {\n margin: 0;\n -webkit-overflow-scrolling: touch;\n overflow-x: hidden;\n}\n\n#mouse {\n position: absolute;"
},
{
"path": "examples/trailing-mouse-pointer/src/clj/example/core.clj",
"chars": 2035,
"preview": "(ns example.core\n (:require [example.spec]\n [matthiasn.systems-toolbox.switchboard :as sb]\n [matt"
},
{
"path": "examples/trailing-mouse-pointer/src/clj/example/index.clj",
"chars": 1496,
"preview": "(ns example.index\n (:require\n [hiccup.core :refer [html]]))\n\n(defn index-page\n \"Generates index page HTML with the "
},
{
"path": "examples/trailing-mouse-pointer/src/cljc/example/pointer.cljc",
"chars": 1470,
"preview": "(ns example.pointer\n \"This component receives messages, keeps a counter, decorates them with the\n state of the counte"
},
{
"path": "examples/trailing-mouse-pointer/src/cljc/example/server_switchboard.cljc",
"chars": 740,
"preview": "(ns example.server-switchboard\n (:require\n [matthiasn.systems-toolbox.switchboard :as sb]\n [matthiasn.systems-too"
},
{
"path": "examples/trailing-mouse-pointer/src/cljc/example/spec.cljc",
"chars": 288,
"preview": "(ns example.spec\n (:require\n #?(:clj [clojure.spec.alpha :as s]\n :cljs [cljs.spec.alpha :as s])))\n\n(s/def :ex"
},
{
"path": "examples/trailing-mouse-pointer/src/cljs/example/core.cljs",
"chars": 1929,
"preview": "(ns example.core\n (:require [example.spec]\n [example.store :as store]\n [example.ui-histograms :as"
},
{
"path": "examples/trailing-mouse-pointer/src/cljs/example/hist_calc.cljs",
"chars": 3593,
"preview": "(ns example.hist-calc)\n\n(defn mean\n \"From: https://github.com/clojure-cookbook/\"\n [coll]\n (let [sum (apply + coll)\n "
},
{
"path": "examples/trailing-mouse-pointer/src/cljs/example/histogram.cljs",
"chars": 4156,
"preview": "(ns example.histogram\n \"Functions for building a histogram, rendered as SVG using Reagent and React.\"\n (:require [exam"
},
{
"path": "examples/trailing-mouse-pointer/src/cljs/example/observer.cljs",
"chars": 7952,
"preview": "(ns example.observer\n (:require [reagent.core :as r :refer [atom]]\n [example.spec]\n [matthiasn.sy"
},
{
"path": "examples/trailing-mouse-pointer/src/cljs/example/re_frame.cljs",
"chars": 5641,
"preview": "(ns example.re-frame\n (:require-macros [reagent.ratom :refer [reaction]])\n (:require [reagent.core :as reagent]\n "
},
{
"path": "examples/trailing-mouse-pointer/src/cljs/example/store.cljs",
"chars": 2737,
"preview": "(ns example.store)\n\n(defn mouse-pos-handler\n \"Handler function for mouse position messages. When message from server:\n "
},
{
"path": "examples/trailing-mouse-pointer/src/cljs/example/ui_histograms.cljs",
"chars": 1898,
"preview": "(ns example.ui-histograms\n (:require-macros [reagent.ratom :refer [reaction]])\n (:require [example.histogram :as h]\n "
},
{
"path": "examples/trailing-mouse-pointer/src/cljs/example/ui_info.cljs",
"chars": 1433,
"preview": "(ns example.ui-info\n (:require [re-frame.core :refer [subscribe]]))\n\n(defn info-view\n \"Show some info about app state,"
},
{
"path": "examples/trailing-mouse-pointer/src/cljs/example/ui_mouse_moves.cljs",
"chars": 2645,
"preview": "(ns example.ui-mouse-moves\n (:require [re-frame.core :refer [subscribe]]\n [reagent.core :as rc]))\n\n;; some S"
},
{
"path": "examples/trailing-mouse-pointer/src/cljs/example/utils.cljs",
"chars": 367,
"preview": "(ns example.utils\n (:require-macros [cljs.core.async.macros :refer [go-loop]])\n (:require [cljs.core.async :refer [cha"
},
{
"path": "perf/matthiasn/systems_toolbox/runtime_perf_test.cljc",
"chars": 9545,
"preview": "(ns matthiasn.systems-toolbox.runtime-perf-test\n\n \"This namespace provides a sanity check before endeavoring in prematu"
},
{
"path": "project.clj",
"chars": 2153,
"preview": "(defproject matthiasn/systems-toolbox \"0.6.41\"\n :description \"Toolbox for building Systems in Clojure\"\n :url \"https://"
},
{
"path": "src/cljc/matthiasn/systems_toolbox/component/helpers.cljc",
"chars": 861,
"preview": "(ns matthiasn.systems-toolbox.component.helpers\n (:require\n #?(:clj [clojure.pprint :as pp]\n :cljs [cljs.ppri"
},
{
"path": "src/cljc/matthiasn/systems_toolbox/component/msg_handling.cljc",
"chars": 15550,
"preview": "(ns matthiasn.systems-toolbox.component.msg-handling\n #?(:cljs (:require-macros [cljs.core.async.macros :as cam :refer "
},
{
"path": "src/cljc/matthiasn/systems_toolbox/component.cljc",
"chars": 8135,
"preview": "(ns matthiasn.systems-toolbox.component\n (:require [matthiasn.systems-toolbox.spec :as s]\n #?(:clj [clojure.tools.lo"
},
{
"path": "src/cljc/matthiasn/systems_toolbox/handler_utils.cljc",
"chars": 2141,
"preview": "(ns matthiasn.systems-toolbox.handler-utils\n (:require [clojure.set :refer [subset?]]))\n\n(defn fwd-as\n \"Creates a hand"
},
{
"path": "src/cljc/matthiasn/systems_toolbox/log.cljc",
"chars": 576,
"preview": "(ns matthiasn.systems-toolbox.log\n \"Some helpers for logging in ClojureScript\"\n (:require [clojure.string :as s]\n "
},
{
"path": "src/cljc/matthiasn/systems_toolbox/scheduler.cljc",
"chars": 4190,
"preview": "(ns matthiasn.systems-toolbox.scheduler\n #?(:cljs (:require-macros [cljs.core.async.macros :refer [go-loop]]))\n (:requ"
},
{
"path": "src/cljc/matthiasn/systems_toolbox/spec.cljc",
"chars": 3216,
"preview": "(ns matthiasn.systems-toolbox.spec\n (:require [expound.alpha :as exp]\n #?(:clj [clojure.spec.alpha :as s]\n :"
},
{
"path": "src/cljc/matthiasn/systems_toolbox/switchboard/helpers.cljc",
"chars": 1397,
"preview": "(ns matthiasn.systems-toolbox.switchboard.helpers\n \"Helper functions used by switchboard.\"\n (:require [matthiasn.syst"
},
{
"path": "src/cljc/matthiasn/systems_toolbox/switchboard/init.cljc",
"chars": 4299,
"preview": "(ns matthiasn.systems-toolbox.switchboard.init\n (:require [matthiasn.systems-toolbox.component :as comp]\n [m"
},
{
"path": "src/cljc/matthiasn/systems_toolbox/switchboard/observe.cljc",
"chars": 910,
"preview": "(ns matthiasn.systems-toolbox.switchboard.observe\n (:require [matthiasn.systems-toolbox.switchboard.helpers :as h]\n "
},
{
"path": "src/cljc/matthiasn/systems_toolbox/switchboard/route.cljc",
"chars": 3671,
"preview": "(ns matthiasn.systems-toolbox.switchboard.route\n (:require [matthiasn.systems-toolbox.spec :as spec]\n [mat"
},
{
"path": "src/cljc/matthiasn/systems_toolbox/switchboard/spec.cljc",
"chars": 4206,
"preview": "(ns matthiasn.systems-toolbox.switchboard.spec\n (:require [matthiasn.systems-toolbox.spec :as sts]\n #?(:clj [cloju"
},
{
"path": "src/cljc/matthiasn/systems_toolbox/switchboard.cljc",
"chars": 5146,
"preview": "(ns matthiasn.systems-toolbox.switchboard\n (:require\n [matthiasn.systems-toolbox.component :as comp]\n [matthiasn."
},
{
"path": "test/matthiasn/systems_toolbox/component_test.cljc",
"chars": 8056,
"preview": "(ns matthiasn.systems-toolbox.component-test\n \"Interact with components by sending some messages directly, see them han"
},
{
"path": "test/matthiasn/systems_toolbox/handler_utils_test.cljc",
"chars": 809,
"preview": "(ns matthiasn.systems-toolbox.handler-utils-test\n \"Test that handler utils work as expected.\"\n (:require [matthiasn.sy"
},
{
"path": "test/matthiasn/systems_toolbox/perf_runner.cljs",
"chars": 211,
"preview": "(ns matthiasn.systems-toolbox.perf-runner\n (:require [doo.runner :refer-macros [doo-tests]]\n [matthiasn.syst"
},
{
"path": "test/matthiasn/systems_toolbox/runner.cljs",
"chars": 484,
"preview": "(ns matthiasn.systems-toolbox.runner\n (:require [doo.runner :refer-macros [doo-tests]]\n [matthiasn.systems-t"
},
{
"path": "test/matthiasn/systems_toolbox/scheduler_test.cljc",
"chars": 2279,
"preview": "(ns matthiasn.systems-toolbox.scheduler-test\n\n #?(:cljs (:require-macros [cljs.core.async.macros :refer [go]]))\n (:req"
},
{
"path": "test/matthiasn/systems_toolbox/switchboard_observe_tests.cljc",
"chars": 7318,
"preview": "(ns matthiasn.systems-toolbox.switchboard-observe-tests\n \"Here, we test the route and route-all wiring between componen"
},
{
"path": "test/matthiasn/systems_toolbox/switchboard_route_tests.cljc",
"chars": 12822,
"preview": "(ns matthiasn.systems-toolbox.switchboard-route-tests\n \"Here, we test the route and route-all wiring between components"
},
{
"path": "test/matthiasn/systems_toolbox/system.cljc",
"chars": 2339,
"preview": "(ns matthiasn.systems-toolbox.system\n\n \"A dummy system for use in tests. Has:\n * a switchboard\n * a ping componen"
},
{
"path": "test/matthiasn/systems_toolbox/system_test.cljc",
"chars": 1381,
"preview": "(ns matthiasn.systems-toolbox.system-test\n\n \"Create a system, send some messages, see them flowing correctly.\"\n #?(:cl"
},
{
"path": "test/matthiasn/systems_toolbox/test_promise.cljc",
"chars": 1529,
"preview": "(ns matthiasn.systems-toolbox.test-promise\n\n \"Provide a promise-like experience for testing.\"\n\n #?(:cljs (:require-mac"
},
{
"path": "test/matthiasn/systems_toolbox/test_spec.cljc",
"chars": 598,
"preview": "(ns matthiasn.systems-toolbox.test-spec\n (:require\n #?(:clj [clojure.spec.alpha :as s]\n :cljs [cljs.spec.alph"
}
]
About this extraction
This page contains the full source code of the matthiasn/systems-toolbox GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 71 files (223.8 KB), approximately 60.0k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.