Skip to content

Commit 15592ad

Browse files
committed
Initial commit
0 parents  commit 15592ad

21 files changed

+924
-0
lines changed

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
public/js/
2+
public/css/
3+
*.iml
4+
.cpcache
5+
.shadow-cljs
6+
node_modules
7+
.clj-kondo
8+
.idea/

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Changelog

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Readme

deps.edn

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{:paths ["src" "test"]
2+
:deps {org.clojure/clojure {:mvn/version "1.10.1"}
3+
org.clojure/clojurescript {:mvn/version "1.10.764"}
4+
org.clojure/core.async {:mvn/version "0.7.559"}
5+
lilactown/helix {:mvn/version "0.0.13"}
6+
funcool/promesa {:mvn/version "5.1.0"}
7+
camel-snake-kebab {:mvn/version "0.4.1"}
8+
medley {:mvn/version "1.2.0"}
9+
cljs-ajax {:mvn/version "0.8.0"}
10+
keechma/next {:mvn/version "0.0.1"}
11+
keechma/router {:mvn/version "1.0.0"}
12+
keechma/entitydb {:mvn/version "2.0.2"}
13+
keechma/pipelines {:mvn/version "0.0.4"}
14+
keechma/forms {:mvn/version "0.1.7"}
15+
cljs-bean {:mvn/version "1.5.0"}}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
(ns keechma.next.controllers.dataloader
2+
(:require [keechma.next.controllers.dataloader.controller]
3+
[keechma.next.protocols :as keechma-pt]
4+
[keechma.next.controllers.dataloader.protocols :as pt]))
5+
6+
(derive :keechma/dataloader :keechma/controller)
7+
8+
(def req (keechma-pt/make-api-proxy pt/req))
9+
(def cached (keechma-pt/make-api-proxy pt/cached))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
(ns keechma.next.controllers.dataloader.controller
2+
(:require [keechma.next.controller :as ctrl]
3+
[keechma.next.controllers.dataloader.protocols :as pt :refer [IDataloaderApi]]
4+
[keechma.next.toolbox.pipeline :as pp :refer [in-pipeline?]]
5+
[keechma.next.toolbox.pipeline.runtime :as ppr]
6+
[cljs.core.async :refer [alts! timeout <! close! chan]]
7+
[goog.object :as gobj]
8+
[promesa.core :as p]
9+
[medley.core :refer [dissoc-in]])
10+
(:require-macros [cljs.core.async.macros :refer [go-loop]]))
11+
12+
(def default-config
13+
{:keechma.dataloader/evict-interval (* 1000 60) ;; Evict every minute
14+
:keechma.dataloader/cache-size 1000})
15+
16+
(def default-request-options
17+
{:keechma.dataloader/max-age 0
18+
:keechma.dataloader/max-stale 0
19+
:keechma.dataloader/no-store true
20+
:keechma.dataloader/stale-while-revalidate false
21+
:keechma.dataloader/stale-if-error false})
22+
23+
(defn get-time-now []
24+
(js/Math.floor (/ (js/Date.now) 1000)))
25+
26+
(defn assoc-inflight [cache loader req-opts req]
27+
(assoc-in cache [:inflight [loader req-opts]] req))
28+
29+
(defn dissoc-inflight [cache loader req-opts]
30+
(dissoc-in cache [:inflight [loader req-opts]]))
31+
32+
(defn assoc-cache [cache time-now loader req-opts res]
33+
(assoc-in cache [:cache [loader req-opts]] {:data res :resolved-at time-now :touched-at time-now}))
34+
35+
(defn dissoc-cache [cache loader req-opts]
36+
(dissoc-in cache [:cache [loader req-opts]]))
37+
38+
(defn make-req [cache* loader req-opts dataloader-opts]
39+
(let [current-req (get-in @cache* [:inflight [loader req-opts]])
40+
req (or current-req (loader req-opts))]
41+
42+
(swap! cache* assoc-inflight loader req-opts req)
43+
(->> req
44+
(p/map (fn [res]
45+
(let [time-now (get-time-now)]
46+
(if (:keechma.dataloader/no-store dataloader-opts)
47+
(swap! cache* (fn [cache]
48+
(-> cache
49+
(dissoc-inflight loader req-opts)
50+
(dissoc-cache loader req-opts))))
51+
(swap! cache* (fn [cache]
52+
(-> cache
53+
(dissoc-inflight loader req-opts)
54+
(assoc-cache time-now loader req-opts res)))))
55+
res)))
56+
(p/error (fn [err]
57+
(let [cached (get-in @cache* [:cache [loader req-opts]])]
58+
(if (and cached (:keechma.dataloader/stale-if-error dataloader-opts))
59+
cached
60+
(throw err))))))))
61+
62+
(defn pp-set-revalidate [interpreter-state runtime pipeline-opts req]
63+
(let [interpreter-state-without-last (vec (drop-last interpreter-state))
64+
last-resumable (last interpreter-state)
65+
state (:state last-resumable)
66+
id (keyword (gensym 'stale-while-revalidate))
67+
resumable (-> interpreter-state
68+
(assoc-in [0 :state :value] req)
69+
(ppr/interpreter-state->resumable true)
70+
(assoc :id id)
71+
(assoc-in [:ident 0] id)
72+
pp/detached)
73+
as-pipeline (fn [_ _]
74+
(ppr/invoke-resumable runtime resumable pipeline-opts))]
75+
76+
(conj interpreter-state-without-last
77+
(-> last-resumable
78+
(update-in [:state :pipeline (:block state)] #(conj (vec %) as-pipeline))))))
79+
80+
(defn make-req-stale-while-revalidate [cache* cached loader req-opts dataloader-opts]
81+
(ppr/fn->pipeline-step
82+
(fn [runtime _ _ _ {:keys [interpreter-state] :as pipeline-opts}]
83+
(ppr/interpreter-state->resumable
84+
(-> interpreter-state
85+
(assoc-in [0 :state :value] cached)
86+
(pp-set-revalidate runtime pipeline-opts (make-req cache* loader req-opts dataloader-opts)))))))
87+
88+
(defn loading-strategy [cached dataloader-opts]
89+
(let [{:keechma.dataloader/keys [max-age max-stale stale-while-revalidate]} dataloader-opts
90+
resolved-at (:resolved-at cached)
91+
age (- (get-time-now) resolved-at)
92+
is-fresh (< age max-age)
93+
is-stale-usable (if (true? max-stale) true (< age (+ max-stale max-age)))]
94+
(cond
95+
is-fresh :cache
96+
(and is-stale-usable stale-while-revalidate (in-pipeline?)) :stale-while-revalidate
97+
is-stale-usable :cache
98+
:else :req)))
99+
100+
(deftype DataloaderApi [ctrl]
101+
IDataloaderApi
102+
(req [this loader]
103+
(pt/req this loader {} {}))
104+
(req [this loader req-opts]
105+
(pt/req this loader req-opts {}))
106+
(req [this loader req-opts dataloader-opts]
107+
(let [cache* (::cache* ctrl)
108+
cached (pt/cached this loader req-opts)]
109+
(if-not cached
110+
(make-req cache* loader req-opts dataloader-opts)
111+
(case (loading-strategy cached dataloader-opts)
112+
:cache (:data cached)
113+
:req (make-req cache* loader req-opts dataloader-opts)
114+
:stale-while-revalidate (make-req-stale-while-revalidate cache* (:data cached) loader req-opts dataloader-opts)
115+
nil))))
116+
(cached [_ loader req-opts]
117+
(let [cache* (::cache* ctrl)]
118+
(get-in @cache* [:cache [loader req-opts]]))))
119+
120+
(defn request-idle-callback-chan! []
121+
(let [cb-chan (chan)]
122+
(if-let [req-idle-cb (gobj/get js/window "requestIdleCallback")]
123+
(req-idle-cb #(close! cb-chan))
124+
(close! cb-chan))
125+
cb-chan))
126+
127+
(defn evict-lru [cache cache-size]
128+
(->> (map identity cache)
129+
(sort-by #(get-in % [1 :touched-at]))
130+
reverse
131+
(take cache-size)
132+
(into {})))
133+
134+
(defn start-evict-lru! [ctrl]
135+
(let [cache* (::cache* ctrl)
136+
interval (:keechma.dataloader/evict-interval ctrl) ;; Vacuum EntityDB every 10 minutes
137+
cache-size (:keechma.dataloader/cache-size ctrl)
138+
poison-chan (chan)]
139+
(go-loop []
140+
(let [[_ c] (alts! [poison-chan (timeout interval)])]
141+
(when-not (= c poison-chan)
142+
(<! (request-idle-callback-chan!))
143+
(swap! cache* evict-lru cache-size)
144+
(recur))))
145+
(fn []
146+
(close! poison-chan))))
147+
148+
(defmethod ctrl/init :keechma/dataloader [ctrl]
149+
(let [ctrl' (-> (merge default-config ctrl)
150+
(update :keechma.dataloader/request-options #(merge default-request-options %))
151+
(assoc ::cache* (atom {})))]
152+
(assoc ctrl'
153+
::stop-evict-lru! (start-evict-lru! ctrl'))))
154+
155+
(defmethod ctrl/api :keechma/dataloader [ctrl]
156+
(let [{:keys [invoke]} (::pipeline-runtime ctrl)]
157+
(->DataloaderApi ctrl)))
158+
159+
(defmethod ctrl/terminate :keechma/dataloader [ctrl]
160+
(let [stop-evict-lru! (::stop-evict-lru! ctrl)
161+
cache* (::cache* ctrl)]
162+
(stop-evict-lru!)
163+
(reset! cache* nil)))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
(ns keechma.next.controllers.dataloader.protocols)
2+
3+
(defprotocol IDataloaderApi
4+
(req [this loader] [this loader req-opts] [this loader req-opts dataloader-opts])
5+
(cached [this loader req-opts]))
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
(ns keechma.next.controllers.entitydb
2+
(:require [keechma.entitydb.core :as edb]
3+
[keechma.next.controllers.entitydb.protocols :as pt]
4+
[keechma.next.protocols :as keechma-pt]
5+
[keechma.entitydb.query :as q]
6+
[keechma.next.controllers.entitydb.controller]))
7+
8+
(derive :keechma/entitydb :keechma/controller)
9+
10+
(def insert-entity edb/insert-entity)
11+
(def insert-entities edb/insert-entities)
12+
(def insert-named edb/insert-named)
13+
(def insert-collection edb/insert-collection)
14+
(def remove-entity edb/remove-entity)
15+
(def remove-named edb/remove-named)
16+
(def remove-collection edb/remove-collection)
17+
(def get-entity edb/get-entity)
18+
(def get-named edb/get-named)
19+
(def get-collection edb/get-collection)
20+
21+
(def include q/include)
22+
(def reverse-include q/reverse-include)
23+
(def recur-on q/recur-on)
24+
(def switch q/switch)
25+
26+
(def insert-entity! (keechma-pt/make-api-proxy pt/insert-entity!))
27+
(def insert-entities! (keechma-pt/make-api-proxy pt/insert-entities!))
28+
(def insert-named! (keechma-pt/make-api-proxy pt/insert-named!))
29+
(def insert-collection! (keechma-pt/make-api-proxy pt/insert-collection!))
30+
(def remove-entity! (keechma-pt/make-api-proxy pt/remove-entity!))
31+
(def remove-named! (keechma-pt/make-api-proxy pt/remove-named!))
32+
(def remove-collection! (keechma-pt/make-api-proxy pt/remove-collection!))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
(ns keechma.next.controllers.entitydb.controller
2+
(:require [keechma.entitydb.core :as edb]
3+
[keechma.next.controllers.entitydb.protocols :refer [IEntityDbApi]]
4+
[keechma.next.controller :as ctrl]
5+
[cljs.core.async :refer [<! timeout chan alts! close!]]
6+
[goog.object :as gobj])
7+
(:require-macros [cljs.core.async.macros :refer [go-loop]]))
8+
9+
10+
(deftype EntityDbApi [ctrl state*]
11+
IEntityDbApi
12+
(insert-entity! [_ entity-type data]
13+
(ctrl/transact ctrl #(swap! state* edb/insert-entity entity-type data))
14+
nil)
15+
(insert-entities! [_ entity-type entities]
16+
(ctrl/transact ctrl #(swap! state* edb/insert-entities entity-type entities))
17+
nil)
18+
(insert-named! [_ entity-type entity-name data]
19+
(ctrl/transact ctrl #(swap! state* edb/insert-named entity-type entity-name data))
20+
nil)
21+
(insert-named! [_ entity-type entity-name data n-meta]
22+
(ctrl/transact ctrl #(swap! state* edb/insert-named entity-type entity-name data n-meta))
23+
nil)
24+
(insert-collection! [_ entity-type collection-name data]
25+
(ctrl/transact ctrl #(swap! state* edb/insert-collection entity-type collection-name data))
26+
nil)
27+
(insert-collection! [_ entity-type collection-name data c-meta]
28+
(ctrl/transact ctrl #(swap! state* edb/insert-collection entity-type collection-name data c-meta))
29+
nil)
30+
(remove-entity! [_ entity-type id]
31+
(ctrl/transact ctrl #(swap! state* edb/remove-entity entity-type id))
32+
nil)
33+
(remove-named! [_ entity-name]
34+
(ctrl/transact ctrl #(swap! state* edb/remove-named entity-name))
35+
nil)
36+
(remove-collection! [_ collection-name]
37+
(ctrl/transact ctrl #(swap! state* edb/remove-collection collection-name))
38+
nil))
39+
40+
(defn request-idle-callback-chan! []
41+
(let [cb-chan (chan)]
42+
(if-let [req-idle-cb (gobj/get js/window "requestIdleCallback")]
43+
(req-idle-cb #(close! cb-chan))
44+
(close! cb-chan))
45+
cb-chan))
46+
47+
(defn start-vacuum! [{:keys [state*] :as ctrl}]
48+
(let [interval (or (:keechma.entitydb/vacuum-interval ctrl) (* 10 60 1000)) ;; Vacuum EntityDB every 10 minutes
49+
poison-chan (chan)]
50+
(go-loop []
51+
(let [[_ c] (alts! [poison-chan (timeout interval)])]
52+
(when-not (= c poison-chan)
53+
(<! (request-idle-callback-chan!))
54+
(swap! state* edb/vacuum)
55+
(recur))))
56+
(fn []
57+
(close! poison-chan))))
58+
59+
(defmethod ctrl/init :keechma/entitydb [ctrl]
60+
(assoc ctrl :keechma.entitydb.vacuum/stop! (start-vacuum! ctrl)))
61+
62+
(defmethod ctrl/api :keechma/entitydb [{:keys [state*] :as ctrl}]
63+
(->EntityDbApi ctrl state*))
64+
65+
(defmethod ctrl/start :keechma/entitydb [ctrl _ _ _]
66+
(edb/insert-schema {} (:keechma.entitydb/schema ctrl)))
67+
68+
(defmethod ctrl/terminate :keechma/entitydb [ctrl]
69+
(when-let [stop-vacuum! (:keechma.entitydb.vacuum/stop! ctrl)]
70+
(stop-vacuum!)))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
(ns keechma.next.controllers.entitydb.protocols)
2+
3+
(defprotocol IEntityDbApi
4+
(insert-entity! [_ entity-type data])
5+
(insert-entities! [_ entity-type entities])
6+
(insert-named! [_ entity-type entity-name data] [_ entity-type entity-name data n-meta])
7+
(insert-collection! [_ entity-type collection-name data] [_ entity-type collection-name data c-meta])
8+
(remove-entity! [_ entity-type id])
9+
(remove-named! [_ entity-name])
10+
(remove-collection! [_ collection-name]))

0 commit comments

Comments
 (0)