Date: 27 November 2018
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
(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
(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
(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
(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
(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
(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)