(ns computersandblues.lodestone.database) (defonce +db+ (atom nil)) (defn setup! [namespace db-version migrations] (assert (some? (migrations db-version)) "Will not increase db version as no migration is found") (let [request (js/indexedDB.open (str namespace) db-version)] (js/Promise. (fn [resolve reject] (doto request (.addEventListener "success" (fn [ev] (let [db (.-result request)] ; see https://javascript.info/indexeddb#parallel-update-problem (.addEventListener db "versionchange" (fn [] (.. request -result close) (js/alert "Database is outdated! Please reload the browser window."))) (reset! +db+ db) (resolve @+db+ ev)))) (.addEventListener "upgradeneeded" (fn [ev] (let [db (.-result request) old-version (.-oldVersion ev)] (js/console.log ::upgradeneeded ev db) (doseq [version (range (inc old-version) (inc db-version)) :let [migration (migrations version)]] (migration db))))) ; we don't add a listener for "blocked" events because we handle "versionchange" above (.addEventListener "error" (fn [ev] (reject (.-result request) ev)))))))) (defn open-store ([db store-id] (open-store db store-id "readonly")) ([db store-id permissions] (let [store-id (str store-id) ; simplifies using keywords as store identifiers txn (.transaction db store-id permissions)] (.objectStore txn store-id)))) (defn create-object-store! [db store-id key-opts] (.createObjectStore db (str store-id) (clj->js key-opts))) (defn- promisify [request] (js/Promise. (fn [resolve reject] (doto request (.addEventListener "success" (fn [] (resolve (.-result request)))) (.addEventListener "error" (fn [] (reject (.-error request)))))))) (defn add! [store-id object] (let [store (open-store @+db+ store-id "readwrite") request (.add store (clj->js object))] (promisify request))) (defn put! [store-id object] (let [store (open-store @+db+ store-id "readwrite") request (.put store (clj->js object))] (promisify request))) (defn get! [store-id k] (let [store (open-store @+db+ store-id) request (.get store k)] (promisify request))) (defn get-all! [store-id key-range] (let [store (open-store @+db+ store-id) request (.getAll store key-range)] (promisify request))) (defn open-cursor! ([store-id key-range] (open-cursor! store-id key-range "next")) ([store-id key-range direction] (let [store (open-store @+db+ store-id)] (.openCursor store key-range direction)))) #_(defn logging [f] (let [n (volatile! 0)] (fn [& args] (when (< @n 10) (vswap! n inc) (js/console.log :logging args) (apply f args))))) (defn transduce-cursor "Allows to transduce over all values in a cursor. Takes a transducer `xform`, a reducing function `rf` and an initial `init`. If no `init` is given, it will default to `(rf)`. If no `rf` is given, the resulting value will be a persistent vector containing the result of all steps." ([cursor-req xform] ; optimization: work with a transient vector before returning the final result (-> (transduce-cursor cursor-req xform conj! (transient [])) (.then persistent!))) ([cursor-req xform rf] (transduce-cursor cursor-req xform rf (rf))) ([cursor-req xform rf init] (let [result (volatile! init) xform (xform rf)] (js/Promise. (fn [resolve _] (.addEventListener cursor-req "success" (fn [ev] (if-let [cursor (-> ev .-target .-result)] ; NOTE: each step will work with the raw js value ; to avoid unnecessary conversion costs. (let [step (xform @result (.-value cursor))] (if (reduced? step) (do (vreset! result @step) (resolve @result)) (do (vreset! result step) (.continue cursor)))) (resolve @result))))))))) (def all (js/IDBKeyRange.lowerBound "")) (comment (let [re (js/RegExp. "user" "i")] (do (print "starting…" (js/Date.)) (-> (open-cursor! ::posts all) (transduce-cursor (comp (filter #(re-find re (.-content %))) (take 50) (map #(js->clj % :keywordize-keys true)))) (.then (fn [result] (print "done!" (js/Date.)) (js/console.log (first result))))))) ) (defn delete! [store-id k] (let [store (open-store @+db+ store-id) request (.delete store k)] (promisify request))) (defn count! [store-id] (let [store (open-store @+db+ store-id) request (.count store)] (promisify request)))