watch generalised observer on state

Author: Chris Zheng  (z@caudate.me)
Date: 27 November 2018
Repository: https://github.com/zcaudate/hara
Version: 3.0.2

1    Introduction

1.1    Installation

Add to project.clj dependencies:

[hara/base "3.0.2"]

All functions are in the hara.watch namespace.

 (use (quote hara.watch))

1.2    Filesystem

If watching on the filesystem is required, add to project.clj dependencies:

[hara/io.file "3.0.2"]

The extension can then be loaded:

(require 'hara.io.file.watch)

2    API



add ^

adds a watch function through the IWatch protocol

v 3.0
(defn add
  ([obj f] (add obj nil f nil))
  ([obj k f] (add obj k f nil))
  ([obj k f opts]
   (protocol.watch/-add-watch obj k f opts)))
link
(def subject (atom nil)) (def observer (atom nil)) (watch/add subject :follow (fn [_ _ _ n] (reset! observer n))) (reset! subject 1) @observer => 1 ;; options can be given to either transform ;; the current input as well as to only execute ;; the callback if there is a difference. (def subject (atom {:a 1 :b 2})) (def observer (atom nil)) (watch/add subject :clone (fn [_ _ p n] (reset! observer n)) {:select :b :diff true}) (swap! subject assoc :a 0) ;; change in :a does not @observer => nil ;; affect watch (swap! subject assoc :b 1) ;; change in :b does @observer => 1

clear ^

clears all watches form the object

v 3.0
(defn clear
  ([obj] (clear obj nil))
  ([obj opts]
   (let [watches (list obj opts)]
     (doseq [k (keys watches)]
       (remove obj k opts)))))
link
(def subject (atom nil)) (do (watch/add subject :a (fn [_ _ _ n])) (watch/add subject :b (fn [_ _ _ n])) (watch/clear subject) (watch/list subject)) => {}

copy ^

copies watches from one object to another

v 3.0
(defn copy
  ([to from] (copy to from nil))
  ([to from opts]
   (let [watches (list from opts)]
     (set to watches opts))))
link
(def obj-a (atom nil)) (def obj-b (atom nil)) (do (watch/set obj-a {:a (fn [_ _ _ n]) :b (fn [_ _ _ n])}) (watch/copy obj-b obj-a) (watch/list obj-b)) => (contains {:a fn? :b fn?})

list ^

lists watch functions through the IWatch protocol

v 3.0
(defn list
  ([obj] (list obj nil))
  ([obj opts] (protocol.watch/-list-watch obj opts)))
link
(def subject (atom nil)) (do (watch/add subject :a (fn [_ _ _ n])) (watch/add subject :b (fn [_ _ _ n])) (watch/list subject)) => (contains {:a fn? :b fn?})

remove ^

removes watch function through the IWatch protocol

v 3.0
(defn remove
  ([obj]   (remove obj nil nil))
  ([obj k] (remove obj k nil))
  ([obj k opts] (protocol.watch/-remove-watch obj k opts)))
link
(def subject (atom nil)) (do (watch/add subject :a (fn [_ _ _ n])) (watch/add subject :b (fn [_ _ _ n])) (watch/remove subject :b) (watch/list subject)) => (contains {:a fn?})

set ^

sets a watch in the form of a map

v 3.0
(defn set
  ([obj watches] (set obj watches nil))
  ([obj watches opts]
   (doseq [[k f] watches]
     (add obj k f opts))
   (list obj opts)))
link
(def obj (atom nil)) (do (watch/set obj {:a (fn [_ _ _ n]) :b (fn [_ _ _ n])}) (watch/list obj)) => (contains {:a fn? :b fn?})

3    Walkthrough

3.1    Watching Atoms

There's a pattern for watching things that already exists in clojure:

(add-watch object :key (fn [object key previous next]))

However, add-watch is a generic concept that exists beyond atoms. It can be applied to all sorts of objects. Furthermore, watching something usually comes with a condition. We usually don't react on every change that comes to us in our lives. We only react when a certain condition comes about. For example, we can see the condition that is placed on this statement:

Watch the noodles on the stove and IF it starts boiling over, add some cold water to the pot

The hara.watch package provides for additional options to be specified when watching the object in question. Is the following example, :select :b is used to focus on :b and :diff true is a setting that configures the watcher so that it will only take action when :b has been changed:

(def subject  (atom {:a 1 :b 2}))
(def observer (atom nil))

(watch/add subject :clone
           (fn [_ _ p n] (reset! observer n))

           ;; Options
           {:select :b   ;; we will only look at :b
            :diff true   ;; we will only trigger if :b changes
})

(swap! subject assoc :a 0) ;; change in :a does not

@observer => nil           ;; affect watch


(swap! subject assoc :b 1) ;; change in :b does

@observer => 1

3.2    Watching Files

The same concept of watch is used for filesystems. So instead of an atom, a directory is specified using very similar semantics:

(def ^:dynamic *happy* (promise))

;; We add a watch
(watch/add (io/file ".") :save
           (fn [f k _ [cmd ^java.io.File file]]

             ;; One-shot strategy where we remove the
             ;; watch after a single event
             (watch/remove f k)
             (.delete file)
             (deliver *happy* [cmd (.getName file)]))

           ;; Options
           {:types #{:create :modify}
            :recursive false
            :filter  [".hara"]
            :exclude [".git" "target"]
            :mode :async})

;; We can look at the watches on the current directory
(watch/list (io/file "."))
=> (contains {:save fn?})

;; Create a file to see if the watch triggers
(spit "happy.hara" "hello")

;; It does!
@*happy*
=> (contains [anything "happy.hara"])

;; We see that the one-shot watch has worked
(watch/list (io/file "."))
=> {}

3.3    Watch Options

There are a couple of cenfigurable options for the filewatch:

  • :types determine which actions are responded to. The possible values are
    • :create, when a file is created
    • :modify, when a file is mobifies
    • :delete, when a file is deleted
    • or a combination of them
  • :recursive determines if subfolders are also going to be responded to
  • :filter will pick out only files that match this pattern.
  • :exclude wil leave out files that match this patter
  • :mode, can be either :sync or :async

3.4    Components

It was actually very easy to build hara.io.file.watch using the idea of something that is startable and stoppable. watcher, start-watcher and stop-watcher all follow the conventions and so it becomes easy to wrap the component model around the three methods:

(require '[hara.component :as component]
         '[hara.io.file.watch :refer :all])

(extend-protocol component/IComponent
  Watcher
  (component/-start [watcher]
    (println "Starting Watcher")
    (start-watcher watcher))

  (component/-stop [watcher]
    (println "Stopping Watcher")
    (stop-watcher watcher)))

(def w (component/start
        (watcher ["."] println
                 {:types #{:create :modify}
                  :recursive false
                  :filter  [".clj"]
                  :exclude [".git"]
                  :async false})))

(component/stop w)