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 )))
0 commit comments