{"id":19942065,"url":"https://github.com/day8/re-frame-async-flow-fx","last_synced_at":"2025-10-08T12:15:12.472Z","repository":{"id":54211892,"uuid":"63046329","full_name":"day8/re-frame-async-flow-fx","owner":"day8","description":"A re-frame effects handler for coordinating the kind of async control flow which often happens on app startup.","archived":false,"fork":false,"pushed_at":"2023-05-24T02:54:11.000Z","size":327,"stargazers_count":185,"open_issues_count":4,"forks_count":18,"subscribers_count":18,"default_branch":"master","last_synced_at":"2025-04-04T04:11:54.211Z","etag":null,"topics":["re-frame","state-machine"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/day8.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"mike-thompson-day8"}},"created_at":"2016-07-11T07:24:37.000Z","updated_at":"2025-01-10T14:29:40.000Z","dependencies_parsed_at":"2024-06-19T02:54:22.725Z","dependency_job_id":"c0b064a8-a1b0-4ee0-aaa8-b4d3bdd021e9","html_url":"https://github.com/day8/re-frame-async-flow-fx","commit_stats":{"total_commits":260,"total_committers":17,"mean_commits":"15.294117647058824","dds":0.573076923076923,"last_synced_commit":"a9bc180ad1ab25492726f32409869392edcdb5e7"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/day8%2Fre-frame-async-flow-fx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/day8%2Fre-frame-async-flow-fx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/day8%2Fre-frame-async-flow-fx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/day8%2Fre-frame-async-flow-fx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/day8","download_url":"https://codeload.github.com/day8/re-frame-async-flow-fx/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248654394,"owners_count":21140291,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["re-frame","state-machine"],"created_at":"2024-11-13T00:11:34.411Z","updated_at":"2025-10-08T12:15:07.427Z","avatar_url":"https://github.com/day8.png","language":"Clojure","funding_links":["https://github.com/sponsors/mike-thompson-day8"],"categories":[],"sub_categories":[],"readme":"\u003c!-- [![CI](https://github.com/day8/re-frame-async-flow-fx/workflows/ci/badge.svg)](https://github.com/day8/re-frame-async-flow-fx/actions?workflow=ci)\n[![CD](https://github.com/day8/re-frame-async-flow-fx/workflows/cd/badge.svg)](https://github.com/day8/re-frame-async-flow-fx/actions?workflow=cd)\n[![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/day8/re-frame-async-flow-fx?style=flat)](https://github.com/day8/re-frame-async-flow-fx/tags) \n[![GitHub pull requests](https://img.shields.io/github/issues-pr/day8/re-frame-async-flow-fx?style=for-the-badge\u0026logo=github)](https://github.com/day8/re-frame-async-flow-fx/pulls)\n--\u003e\n[![Clojars Project](https://img.shields.io/clojars/v/day8.re-frame/async-flow-fx?style=for-the-badge\u0026logo=clojure\u0026logoColor=fff)](https://clojars.org/day8.re-frame/async-flow-fx)\n[![GitHub issues](https://img.shields.io/github/issues-raw/day8/re-frame-async-flow-fx?style=for-the-badge\u0026logo=github)](https://github.com/day8/re-frame-async-flow-fx/issues)\n[![License](https://img.shields.io/github/license/day8/re-frame-async-flow-fx?style=for-the-badge)](LICENSE)\n\n## Async Control Flow In re-frame\n\nThis [re-frame library](https://github.com/day8/re-frame) coordinates a set of\nasynchronous, stateful tasks which have dependencies (and consequently need to be ordered).\n\nUsing this library, you can **coordinate the kind of asynchronous control flow** which \nis often necessary to successfully boot \na re-frame application \"up\" into a functioning state, while also gracefully handling failures.\n\nAs an example, imagine an application which, during startup, has to connect with\na backend server via a web socket, load some user settings data, and then a user portfolio, while also\nconnecting to Intercom, but not until the user settings were loaded, while remembering that\nany of these processes might fail because of network problems.\n\nSo, it has a similar intent to [mount](https://github.com/tolitius/mount) \nor [component](https://github.com/stuartsierra/component),\nbut it dovetails with, and leverages the event driven nature of re-frame's architecture. \n\nTechnically, this library implements an [Effect Handler](https://github.com/day8/re-frame/blob/master/docs/Effects.md), \nkeyed `:async-flow`. It has a declarative, data oriented design.\n\n#### TOC\n\n- [Quick Start Guide](#quick-start-guide)\n- [Testing](#testing)\n- [Debugging Flows](#debugging)\n- [Problem Definition](#problem-definition)\n- [The Solution](#the-solution)\n- [Design Philosophy](#design-philosophy)\n\n## Quick Start Guide\n\n### Step 1. Add Dependency\n\nAdd the following project dependency: \u003cbr\u003e\n[![Clojars Project](https://img.shields.io/clojars/v/day8.re-frame/async-flow-fx?style=for-the-badge\u0026logo=clojure\u0026logoColor=fff)](https://clojars.org/day8.re-frame/async-flow-fx)\n\nRequires re-frame \u003e= 0.8.0\n\n### Step 2. Initiate Boot\n\nIn your app's main entry function, we want to initiate the boot process:\n```clj\n(defn ^:export main\n  []\n  (dispatch-sync [:boot])            ;; \u003c--- boot process is started\n  (reagent/render\n    [this-app.views/main]\n    (.getElementById js/document \"app\")))\n```\n\nWhy the use of `dispatch-sync`, rather than `dispatch`?\n\nWell, `dispatch-sync` is convenient here because it ensures that\n`app-db` is synchronously initialised **before** we start mounting components/views (which subscribe to state). Using\n`dispatch` would work too, except it runs the handler **later**. So, we'd have to then code\ndefensively in our subscriptions and views, guarding against having an uninitialised `app-db`.\n\nThis is the only known case where you should use `dispatch-sync` over `dispatch` (other than in tests).\n\n### Step 3. Registration And Use\n\nIn the namespace where you register your event handlers, perhaps called `events.cljs`, you have 3 things to do.\n\n**First**, add this require to the `ns`:\n```clj\n(ns app.events\n  (:require\n    ...\n    [day8.re-frame.async-flow-fx :as async-flow-fx]   ;; \u003c-- add this\n    ...))\n```\n\nBecause we never subsequently use this `require`, it\nappears redundant. But its existence will cause the `:async-flow` effect\nhandler to self-register with re-frame, which is important\nto everything that follows.\n\n**Second**, write a function which returns a declarative description (as a data structure) of the async flow required, like this:\n```clj\n(defn boot-flow\n  []\n  {:first-dispatch [:do-X]              ;; what event kicks things off ?\n   :rules [                             ;; a set of rules describing the required flow\n     {:when :seen? :events :success-X  :dispatch [:do-Y]}\n     {:when :seen? :events :success-Y  :dispatch [:do-Z]}\n     {:when :seen? :events :success-Z  :halt? true}\n     {:when :seen-any-of? :events [:fail-X :fail-Y :fail-Z] :dispatch [:app-failed-state] :halt? true}]})\n```\nTry to read each `rule` line as an English sentence. When this event happens, dispatch another event. \nThe rules above combine to run tasks X, Y and Z serially, like dominoes. Much more complicated\nscenarios are possible. Full particulars of this data structure are provided below in the Tutorial section.\n\n**Third**, write the event handler for `:boot`:\n\nRemember that `(dispatch-sync [:boot])` in step 2. We are now writing and registering the associated event handler.\n\nThis event handler will do two things:\n  1. It goes though an initial synchronous series of tasks which get app-db into the right state\n  2. It kicks off a multistep asynchronous flow\n\n```clj\n(reg-event-fx                    ;; note the -fx\n  :boot                          ;; usage: (dispatch [:boot])  See step 3\n  (fn [_ _]\n    {:db (-\u003e {}                  ;; do whatever synchronous work needs to be done\n            task1-fn             ;; ?? set state to show \"loading\" twirly for user??\n            task2-fn)            ;; ?? do some other simple initialising of state\n     :async-flow (boot-flow)})) ;; kick off the async process\n```\n\nNotice at that last line. This library provides the \"effect handler\" which implements `:async-flow`. It reads\nand actions the data structure returned by `(boot-flow)`.\n\n## Testing\n\nUnit tests use standard cljs.test\n\nTo run tests in a browser\n\n```\nlein watch\n```\n\nTo run the tests with Karma\n\n```\nnpm install karma-cli -g # install the global CLI Karma tool\nlein ci \n```\n\n## Debugging\n\nBeginning with version 0.3.0, there is extra console logging support to help debug complex flows.\nThis can be enabled in two ways:\n1. Globally enable logging for all flows. Typically, somewhere in your top level re-frame events NS\n   ```clojure\n    (async-flow-fx/enable-debug? true)\n   ```\n2. Or on a flow by flow basis, you can specify `:debug?` property, a boolean value, in the flow map. \n   ```clojure\n   {:debug? true}\n   ```\n\nWhen enabled, console logs look like following. Although specifying an `:id` in your flow is optional,\nwhen debugging complex flows, like when one flow triggers another flow before the parent flow completes, it is useful \nto differentiate the log entries based on the flow `:id`. \nNotice you see logs for `:setup`, `:first-dispatch`, `:dispatching`, the event causing\na dispatch by the flow, the `:signal`, and when a flow is `:halting`\n\n**Devtools console example**\n\n```\nasync-flow [:my-app.events/boot-app-flow :setup]\n    {:id my-app.events/boot-app-flow, :ts 822.44, :signal :setup}\nasync-flow [:my-app.events/boot-app-flow :first-dispatch]\n    {:id :my-app.events/boot-app-flow :ts 823.5 :dispatching [:init-db]}\nasync-flow [:my-app.events/boot-app-flow :dispatching]\n    {:id :my-app.events/boot-app-flow\n     :ts 2066.700\n     :signal [:database.connection/success-jwt]\n     :dispatched ([:day8.voz-rf.events/load-assets])}\nasync-flow [:my-app.events/boot-app-flow :dispatching]\n    {:id :my-app.events/boot-flow-voz-rf\n     :ts 2625.5\n     :signal [:my-app.events/success-load-assets {:uri \"/django/app-assets\"}]\n     :dispatched ([:my-app.events/success-bootstrap :my-app])}\nasync-flow [:my-app.events/boot-app-flow :halting]\n    {:id :my-app.events/boot-app-flow\n     :ts 2627.600000023842\n     :signal [:my-app.events/success-load-assets {:uri \"/django/app-assets\"}]\n     :rule ({:id 2\n            :halt? true\n            :when-seen-all-of? :events #{:my-app.events/success-load-assets}\n            :dispatch-n ([:my-app.events/success-bootstrap :my-app])})}\n...\n```\n\n\n## Problem Definition\n\nWhen an App boots, it performs a set of tasks to initialise itself.\n\nInvariably, there are dependencies between these tasks, like task1 has to run before task2.\n\nBecause of these dependencies, \"something\" has to coordinate how tasks are run. Within the clojure community,\na library like [Component](https://github.com/stuartsierra/component) or [mount](https://github.com/tolitius/mount)\nis often turned to in these moments, but we won't be\ndoing that here. We'll be using an approach which is more re-frame friendly.\n\n### Easy\n\nIf the tasks are all synchronous, then the coordination can be done in code.\n\nEach task is a function, and we satisfy the task dependencies by correctly\nordering how they are called. In a re-frame context, we'd have this:\n```clj\n(reg-event-db\n  :boot\n  (fn [db]\n    (-\u003e {}\n        task1-fn\n        task2-fn\n        task3-fn)))\n```\n\nand in our app's `main` function we'd `(dispatch [:boot])`\n\n\n### Time\n\nBut, of course, it is never that easy because some of the tasks will be asynchronous.\n\nA booting app will invariably have to coordinate **asynchronous tasks** like\n\"open a websocket\", \"establish a database connections\", \"load from LocalStore\",\n\"GET configuration from an S3 bucket\" and \"querying the database for the user profile\".\n\n**Coordinating asynchronous tasks means finding ways to represent and manage time**,\nand time is a programming menace.\nIn Greek mythology, Cronus was the much feared Titan of Time, believed to\nbring cruelty and tempestuous disorder, which surely makes him the patron saint of asynchronous programming.\n\nSolutions like promises and futures attempt to make time disappear and allow you to program\nwith the illusion of synchronous computation. But time has a tendency to act like a liquid under\npressure, finding the cracks and leaking through the abstractions.\n\nSomething like [CSP](https://en.wikipedia.org/wiki/Communicating_sequential_processes) (core.async) is\nmore of an event oriented treatment. Less pretending. But... unfortunately more complicated.\n`core.async` builds a little state machine for you, under the covers, so that you can\nbuild your own state machine on\ntop of that again via deft use of go loops, channels, gets and puts. Both layers try to model/control time.\n\nIn our solution, we'll be using a re-frame variation which hides (most of) the\nstate machine complexity.\n\n\n### Failures\n\nThere will also be failures and errors!\n\nNothing messes up tight, elegant code quite like error\nhandling. Did the Ancient Greeks have a terrifying Titan for the unhappy\npath too? Ernos? They should have.\n\nWhen one of the asynchronous startup tasks fails, we\nmust be able to stop the normal boot sequence and put the application\nin a satisfactory failed state, sending necessary logs and explaining to the user\nwhat went wrong - eg: \"No Internet connection\" or \"Couldn't load user portfolio\". \n\n\n### Efficiency\n\nAnd then, of course, there's the familiar pull of efficiency.\n\nWe want our app to boot in the shortest possible amount of time. So any asynchronous\ntasks which can be done in parallel, must be done in parallel.\n\nThe boot process is seldom linear, one task after an another. Instead, it involves\ndependencies like: when task1 has finished, start all of task2, task3 and task4 in\nparallel. And task5 can be started only when both task2 and task3 has completed successfully.\nAnd task6 can start when task3 alone has completed, but we really don't care if it finishes\nproperly - it is non essential to a working app.\n\nSo, we need to coordinate asynchronous flows, with complex dependencies, while handling failures.\nNot easy, but that's why they pay us the big bucks.\n\n### As Data Please\n\nBecause we program in Clojure, we spend time in hammocks dreamily re-watching Rich Hickey videos and\nmeditating on essential truths like \"data is the ultimate in late binding\".\n\nSo, our solution must involve \"programming with data\" and be, at once, all synonyms of easy.\n\n### In One Place\n\nThe control flow should be described in just one place, and easily\ngrokable as a unit.\n\nTo put that another way: we do not want a programmer to have to look\nin multiple places to reconstruct a mental model of the overall control flow.\n\n## The Solution\n\n**re-frame has events. That's how we roll.**\n\nA re-frame application can't step forward in time unless an event happens; unless something\ndoes a `dispatch`. Events will be the organising principle in our solution exactly\nbecause events are an organising principle within re-frame itself.\n\n### Tasks and Events\n\nAs you'll soon see, our solution assumes the following about tasks...\n\nIf we take an X-ray of an async task, we'll see this event skeleton:\n - an event is used to start the task\n - if the task succeeds, an event is dispatched\n - if the task fails, an event is dispatched\n\nSo that's three events: **one to start and two ways to finish**. Please read that again\n- its importance is sometimes missed on first reading. Your tasks must conform to this \n3 event structure (which is not hard).\n\nOf course, re-frame will route all three events to their registered handler. The actual WORK of\nstarting the task, or handling the errors, will be done in the event handler that you write.\n\nBut, here, none of that menial labour concerns us. **Here we care only about the coordination of\ntasks**. We care only that task2 is started when task1 finishes successfully, and we don't need\nto know what task1 or task2 actually do.\n\nTo distill that: we care only that the `dispatch` to start task2 is fired correctly when we\nhave seen an event saying that task1 finished successfully.\n\n### When-E1-Then-E2\n\nRead that last paragraph again. It distills further to: when event E1 happens then \n`dispatch` event E2. Or, more pithily again, When-E1-Then-E2.\n\nWhen-E1-Then-E2 is the simple case, with more complicated variations like:\n  - when **both** events E1 and E2 have happened, then dispatch E3\n  - when **either** events E1 or E2 happens, then dispatch E3\n  - when event E1 happens, then dispatch both E2 and E3\n\n**We call these \"rules\". A collection of such rules defines a \"flow\".**\n\n### Flow As Data\n\nCollectively, a set of When-E1-then-E2 rules can describe the entire async boot flow of an app.\n\nHere's how that might look in data:\n```clj\n[{:when :seen?        :events :success-db-connect   :dispatch-n '([:do-query-user] [:do-query-site-prefs])}\n {:when :seen-both?   :events [:success-user-query :success-site-prefs-query] :dispatch [:success-boot] :halt? true}\n {:when :seen-any-of? :events [:fail-user-query :fail-site-prefs-query :fail-db-connect] :dispatch [:fail-boot] :halt? true}\n {:when :seen?        :events :success-user-query   :dispatch [:do-intercom]}]\n```\n\nThat's a vector of 4 maps (one per line), where each represents a single rule. Try reading each\nline as if it was an English sentence and something like this should emerge: `when we have seen all of events E1 and E2, then dispatch this other event`\n\nThe structure of each rule (map) is:\n\n```clj\n{:when       W     ;; one of:  :seen?, :seen-both?, :seen-all-of?, :seen-any-of?\n :events     X     ;; either a single keyword or a seq of keywords representing event ids\n :dispatch   Y     ;; (optional) single vector (to dispatch)\n :dispatch-n Z     ;; (optional) list of vectors (to dispatch)\n :halt?    true}   ;; optional, will teardown the flow after the last event is dispatched\n```\nAlthough optional, only one of :dispatch or :dispatch-n can be specified\n\nIn our mythical app, we can't issue a database query until we have a database connection, so the 1st rule (above) says:\n  1. When `:success-db-connect` is dispatched, presumably signalling that we have a database connection...\n  2. then `(dispatch [:query-user])` and `(dispatch [:query-site-prefs])`\n\nWe have successfully booted when both database queries succeed, so the 2nd rule says:\n  1. When both success events have been seen (they may arrive in any order),\n  2. then `(dispatch [:success-queries])` and cleanup because the boot process is done.\n\nIf any task fails, then the boot fails, and the app can't start which means go into a\nfailure mode, so the 3rd rules says:\n  1.  If any one of the various tasks fail...\n  2.  then `(dispatch [:fail-boot])` and cleanup because the boot process is done.\n\nOnce we have user data (from the user-query), we can start the intercom process, so the 4th rules days:\n  1. When `:success-user-query` is dispatched\n  2. then `(dispatch [:do-intercom])`\n\nFurther Notes:\n\n1. The 4th rule starts \"Intercom\" once we have user data. But notice that\n   nowhere do we wait for a `:success-intercom`. We want this process started,\n   but it is not essential for the app's function, so we don't wait for it to complete.\n\n2. The coordination processes never actively participate in handling any events. Event handlers\n   themselves do all that work. They know how to handle success or failure - what state to record so\n   that the twirly thing is shown to users, or not. What messages are shown. Etc.\n\n3. A naming convention for events is adopted. Each task can have 3 associated events which\n   are named as follows: `:do-*` is for starting tasks. Task completion is either `:success-*`\n   or `:fail-*`\n\n4. The `:halt?` value of true means the boot flow is completed. Clean up the flow coordinator.\n   It will have some state somewhere. So get rid of that. And it will have been \"sniffing events\",\n   so stop doing that, too. You should provide at least one of these in your rules.\n\n5. There's nothing in here about the teardown process as the application is closing. Here we're only\n   helping the boot process.\n\n6. There will need to be something that kicks off the whole flow. In the case above, presumably\n   a `(dispatch [:do-connect-db])` is how it all starts.\n\n7. A word on Retries. XXX\n\n### The Flow Specification\n\nThe `:async-flow` data structure has the following fields:\n\n  - `:id` - optional - an identifier, typically a unique namespaced keyword.\n    If not supplied, a unique value will be generated.\n    Must not clash with the identifier for any event handler (because internally\n    an event handler is registered using this id).\n    If two flows have the same ID, they cannot be run at the same time.\n\n  - `db-path` - optional - the path within `app-db` where the coordination logic should store state. Two pieces\n    of state are stored: the set of seen events, and the set of started tasks.\n    If absent, then state is not stored in app-db and is instead held in an internal atom.\n    We prefer to store state in app-db because we like the philosophy of having all the data in the one place,\n    but it is not essential.\n  - `first-dispatch` - optional - the event which initiates the async flow. This is often\n    something like the event which will open a websocket or HTTP GET configuration from the server.\n\tIf omitted, it is up to you to organise the dispatch of any initial event(s).\n  - `rules` - mandatory - a vector of maps. Each map is a `rule`.\n\nA `rule` is a map with the following fields:\n\n  - `:when` one of `:seen?`, `:seen-both?`. `:seen-all-of?`, `:seen-any-of?`\n    `:seen?`, `:seen-both?` and `:seen-all-of?` are interchangeable.\n  - `:events` either a single keyword, or a collection of keywords, presumably event ids.\n              a collection can also contain whole event vectors that will be matched,\n              or event predicates that return true or false when passed an event vector.\n  - `:dispatch` can be a single vector representing one event to dispatch.\n  - `:dispatch-n` to dispatch multiple events, must be a coll where each elem represents one event to dispatch.\n  - `:dispatch-fn` can be a function that accepts the seen event, and returns a coll where each elem represents one event to dispatch.\n  - `:halt?` optional boolean. If true, the flow enters teardown and stops.\n\n\n### Under The Covers\n\nHow does async-flow work? It does the following:\n\n  1. It dynamically creates an event handler to perform the flow coordination.\n  2. It registers this event handler using the supplied `:id`\n  3. It requests that all `:events` mentioned in `flow` rules should be \"forwarded\" to\n     this event handler, after they have been handled by their normal handlers.\n     So, if the event `:abc` was part of `flow` spec, then after `[:abc 1]` was handled by its normal handler\n     there would be an additional `(dispatch [:id [:abc 1]])` which would be handled the coordinator\n     created in steps 1 and 2.\n  4. The event handler keeps track of what events have occurred, and what tasks have already\n     been started. It keeps this state at the path nominated in `:db-path` or in local atom, if there is no `:db-path`.\n  5. The event handler uses your `flow` specification and the state it internally\n     maintains to work out how it should respond to each newly forwarded event.\n  6. At some point, the flow finishes (failed or succeeded) and the event handler from step 1\n     sees a rule with `:halt?` set to `true`. Event handler de-registers itself, removes its state, and stops all event sniffing.\n\n\nNotes:\n  1.  This pattern is flexible. You could use it to implement a more complex FSM coordinator.\n  2.  All the work is done in a normal event handler (dynamically created for you). And\n      it is all organised around events which this event handler processes. So this\n      solution is aligned on re-frame fundamentals.\n\n\n### Advanced use\n\nIn some circumstances, it is necessary to hook into not just the event itself, but the data carried in the event.\nIn these cases, functions can be used as an event predicate, or a dispatch rule.\n\nFor example, when uploading a file, a success event may return an id which needs to be passed on to a subsequent event.\n\n```clj\n{:when :seen? :events :upload/success\n :dispatch-fn (fn [[e id]] [[:remote/file-uploaded id]])}\n```\n\nOr, to dispatch a server error event if a status of 500 or above has been seen\n\n```clj\n{:when :seen? :events (fn [[e status]] (and (= e :http/response-received) (\u003e= status 500)))\n :dispatch [:server/error]))\n```\n\n## Event Messaging\n\nIn an async flow it is often useful to dispatch an event to indicate success or failure of\nsome process. However, this event may not actually have to do anything. `re-frame` will issue a \nwarning if an event is dispatched that has not been registered by `reg-event-db` or `reg-event-fx`.\nIf you use this pattern a lot it quickly becomes inconvenient to register multiple no-op handlers to \navoid this warning.\n\nTo address this issue we provide a no-op handler `::async-flow-fx/notify` which you can use in the \nfollowing way.\n\n```clj\n[{:when :seen? :events [[::async-flow-fx/notify :success-db-connect]]   \n  :dispatch-n '([:do-query-user] [:do-query-site-prefs])}\n ...]}]\n```\n\nNote  `::async-flow-fx/notify` doesn't do anything special and if you like you can add your own \nno-op event.\n\n## Design Philosophy\n\nManaging async task flow means managing time, and managing time requires a state machine. You need:\n   - some retained state (describing where we have got to)\n   - events which announce that something has happened or not happened (aka FSM triggers)\n   - a set of rules about transitioning app state and triggering further activity when events arrive.\n\nOne way or another you'll be implementing a state machine. There's no getting away from that.\n\nAlthough there are ways of hiding it!! Redux-saga uses ES6 generator functions to provide the illusion of a\nsynchronous control flow. The \"state machine\" is encoded directly into the generator function's\nstatements (sequentially, or via `if` `then` `else` logic, or via `loops`). And that's a nice\nand simple abstraction for many cases. This equivalence between a state machine and generator functions is further described [here](http://250bpm.com/blog:141).\n\nBut, as always, there are trade-offs.\n\nFirst, the state machine is encoded in javascript \"code\" (the generator function implements\nthe state machine). In clojure, we have a preference for \"programming in data\" where possible.\n\nSecond, coding (in javascript) a more complicated state machine with a bunch of failure states and\ncascades will ultimately get messy. Time is like a liquid under pressure and it will force its way\nout through the cracks in the abstraction. A long history with FSM encourages us to implement \nstate machines in a data driven way (a table driven way).\n\nSo we choose data and reject the redux-saga approach (while being mindful of the takeoffs).\n\nBut it would be quite possible to create a re-frame version of redux-saga. In ClosureScript\nwe have `core.async` instead of generator functions. That is left as an exercise for the motivated reader.\n\nA motivated user might also produce a fully general FSM version of this effects handler.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fday8%2Fre-frame-async-flow-fx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fday8%2Fre-frame-async-flow-fx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fday8%2Fre-frame-async-flow-fx/lists"}