Skip to content

Instantly share code, notes, and snippets.

@jcromartie
Created March 22, 2013 13:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jcromartie/5221441 to your computer and use it in GitHub Desktop.
Save jcromartie/5221441 to your computer and use it in GitHub Desktop.
A tiny database that records and replays events in a repository on a piece of state.
(ns deck.core
(:require [clojure.java.io :as io])
(:import java.io.PushbackReader
java.io.File
java.util.Date))
(defn- dispatch-play
[state [event-name & rest]]
(keyword event-name))
(defmulti play
"Multimethod.
Define your own event names and implementations to change the state
of your data.
e.g.
(defmethod play ::add-user
[state [_ date username]]
(update-in state [:users] conj {:username username :created-at date})
"
(var dispatch-play))
(defprotocol AEventStore
(store [_ e] "stores event for later retrieval")
(events [_] "returns all events in store"))
(defn- read-one
[r]
(try
(read r)
(catch Exception e
nil)))
(defn- form-seq
[f]
(with-open [r (PushbackReader. (io/reader f))]
(binding [*read-eval* false]
(let [forms (repeatedly #(read-one r))]
(doall (take-while identity forms))))))
(defrecord FileEventStore [path agent]
AEventStore
(store [_ e] (send-off agent
(fn [state]
(let [output (str ";; at " (Date.) "\n" (prn-str e))]
(spit path output :append true))
(conj state e))))
(events [_] @agent))
(defn file-store [path]
(FileEventStore. path (agent (if (-> path File. .exists)
(vec (form-seq path))
[]))))
(def ^:dynamic *strict* true)
(defn check-event
[e]
(when *strict*
(when-not (= e (read-string (pr-str e)))
(throw (Exception. "Event is not printable/readable, cannot be recorded")))))
(defprotocol ARecordAndReplay
(record [db event] "Apply and store an event to the state of db")
(replay [db] "Apply all of the events in the event store to the initial state of the db"))
(defrecord RefDeck [init-state
^clojure.lang.IReference state-ref
^deck.core.AEventStore event-store]
clojure.lang.IDeref
(deref [_] (deref state-ref))
ARecordAndReplay
(record [_ e] (dosync
(check-event e)
(store event-store e)
(alter state-ref play e)))
(replay [_] (dosync
(let [new-val (reduce play init-state (events event-store))]
(ref-set state-ref new-val)))))
(defmethod print-method deck.core.RefDeck
[d ^java.io.Writer w]
(binding [*print-length* 10]
(.write w (format "RefDeck (%d events): %s"
(count (events (:event-store d)))
(pr-str @d)))))
(defn deck
"Initialize a new deck that can record and play back events"
[init-state path]
(let [d (RefDeck. init-state (ref init-state) (file-store path))]
(replay d)
d))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment