Skip to content

Instantly share code, notes, and snippets.

@retro
Created January 30, 2018 11:53
Show Gist options
  • Save retro/62020fd95b9b4f9d1859a6f081ed2ac0 to your computer and use it in GitHub Desktop.
Save retro/62020fd95b9b4f9d1859a6f081ed2ac0 to your computer and use it in GitHub Desktop.

Kechma Toolbox - Tasks and animations

Keechma Toolbox will soon get a big addition, a system for tasks and animations. Tasks are a basis for the animation system, so I want to talk about them a bit.

What are tasks?

Tasks are an abstraction over a processes that run over time, and can have their callback function called multiple times. They are different from promises because promises can be only rejected or resolved. Tasks can (potentially) run forever. Tasks are also designed to only operate on the app-db, they shouldn't communicate with the outer world.

  • Tasks are designed to run inside the pipeline! and can be blocking or non - blocking.
  • There is a guarantee that there will always run only one version of a task at a time
  • Tasks can be stopped from inside the callback function or from the outside
  • Tasks need a producer - something that will trigger the callback function. There are two built in producers - Request animation frame and app-db change watcher

Why tasks?

Task system was created to (primarily) allow rich, state based animations in Keechma. Afterwards we've discovered that they can be useful in other situations too.

Let's take a look at the animation in this tweet:

https://twitter.com/mihaelkonjevic/status/922964823750606852

This button is animated through various states, and some of them are blocking and some of them aren't:

  1. Pressing button - blocking animation
  2. Releasing button - blocking animation
  3. Tranisition to a loader - blocking animation
  4. Loader spinning - non blocking animation (it should run as long as the request is running)
  5. Tranisition to success or error state - blocking animation
  6. Transition to the initial state (or failed initial state) - blocking animation

I'll go into a detailed overview of animations in the future, but this kind of behavior wouldn't be able without tasks.

Here's the code that implements this animation (there is another part of it where you define the values that are animated, but this is out of the scope of this post)

(defmethod forms-core/on-mount RegisterForm [_ app-db form-props]
  (pipeline! [value app-db]
    (cancel-animation! app-db :submit-button form-props)
    (pp/commit! (render-animation-end app-db :submit-button/init form-props))))

(defn delay-pipeline [msec]
  (p/promise (fn [resolve reject]
               (js/setTimeout resolve msec))))

(defmethod forms-core/call RegisterForm [this app-db form-props args]
  
  (cond 
    (= :button-pressed args)
    (pipeline! [value app-db]
      (cancel-animation! app-db :submit-button form-props)
      (if (= :init (get-animation-state app-db :submit-button form-props))
        (blocking-animate-state! app-db :submit-button/pressed form-props)
        (blocking-animate-state! app-db :submit-button/fail-pressed form-props)))

    (= :button-released args)
    (pipeline! [value app-db]
      (pp/run-pipeline! :on-validate [form-props false])
      (cancel-animation! app-db :submit-button form-props)
      (if (empty? (get-in app-db [:kv forms-core/id-key :states form-props :errors]))
        (pipeline! [value app-db]
          (if (= :pressed (get-animation-state app-db :submit-button form-props))
            (blocking-animate-state! app-db :submit-button/init form-props)
            (blocking-animate-state! app-db :submit-button/fail-init form-props))
          (blocking-animate-state! app-db :submit-button/button-loader form-props)
          (pp/commit! (render-animation-end app-db :submit-button/loader form-props))
          (non-blocking-animate-state! app-db :submit-button/loader form-props)
          (pp/execute! :on-submit form-props))
        (blocking-animate-state! app-db :submit-button/fail-init form-props)))

    :else nil))

(defmethod forms-core/on-submit-success RegisterForm [this app-db form-props data]
  (let [res (:register data)
        jwt (:token res)
        account (:account res)]
    (pipeline! [value app-db]
      (pp/commit! (render-animation-end app-db :submit-button/button-loader form-props))
      (blocking-animate-state! app-db :submit-button/success-notice form-props)
      (util/store-token-to-storage :jwt jwt)
      (pp/commit! (-> app-db
                      (assoc-in [:kv :jwt] jwt)
                      (insert-named-item :account :current account)))
      (delay-pipeline 500)
      (pp/send-command! [forms-core/id-key :mount-form] form-props))))

(defmethod forms-core/on-submit-error RegisterForm [this app-db form-props data error]
  (pipeline! [value app-db]
    (pp/commit! (render-animation-end app-db :submit-button/button-loader form-props))
    (blocking-animate-state! app-db :submit-button/fail-notice form-props)
    (blocking-animate-state! app-db :submit-button/fail-init form-props)))

(defn constructor []
(->RegisterForm validator))

blocking-animate-state! and non-blocking-animate-state! functions use tasks under the hood.

Some other examples:

(pipeline! [value app-db]
	(keechma.toolbox.tasks/block-until! :some-id (fn [app-db] (= :foo (get-in app-db [:some :path]))
	(do-something))

do-something function call will be blocked until the (= :foo (get-in app-db [:some :path])) returns true


(pipeline! [value app-db]
	(keechma.toolbox.tasks/non-blocking-raf! :some-id
		(fn [task-meta app-db]
			(assoc-in app-db [:kv :current-time] (.now js/Date)))
	(do-something-that-returns-promise)
	(keechma.toolbox.tasks/stop-task! app-db :some-id)

In this case the task will be started, but it will not block the pipeline, so (do-something-that-returns-promise) will be called immediately after. The task will update the app-db (with the current time) on each animation frame. After the promise returned from do-something-that-returns-promise is resolved, we call keechma.toolbox.tasks/stop-task! which will stop the task. Otherwise the task would run indefinitely.


(pipeline! [value app-db]
	(keechma.toolbox.tasks/blocking-raf! :some-id
		(fn [task-meta app-db]
			(let [times-invoked (:times-invoked task-meta)]
				(if (= times-invoked 10)
				  (keechma.toolbox.tasks/stop-task app-db :some-id)
				  (assoc-in app-db [:kv :times-invoked] times-invoked)))))
	(do-something)
	

In this case we're stopping the task from inside the task, so this task will block until it's invoked 10 times, and then it will be stopped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment