(ns computersandblues.lodestone.database (:refer-clojure :exclude [count get])) (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) txn (-> ev .-target .-transaction)] (js/console.log ::upgradeneeded ev db) (doseq [version (range (inc old-version) (inc db-version)) :let [migration (migrations version)]] (migration db txn))))) ; we don't add a listener for "blocked" events because we handle "versionchange" above (.addEventListener "error" (fn [ev] (reject (.-result request) ev)))))))) (def transaction? (partial instance? js/IDBTransaction)) (def store? (partial instance? js/IDBObjectStore)) (defn open-store ([store-id permissions] (open-store @+db+ store-id permissions)) ([db store-id permissions] (let [store-id (str store-id) ; simplifies using keywords as store identifiers txn (if (transaction? db) db (.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 create-index! ([store idx-name fields] (create-index! store idx-name fields {})) ([store idx-name fields opts] (.createIndex store (str idx-name) (clj->js fields) (clj->js 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] (add! @+db+ store-id object)) ([db store-id object] (let [store (open-store db store-id "readwrite") request (.add store (clj->js object))] (promisify request)))) (defn put! ([store-id object] (put! @+db+ store-id object)) ([db store-id object] (let [store (open-store db store-id "readwrite") request (.put store (clj->js object))] (promisify request)))) (defn get ([store-id k] (get @+db+ store-id k)) ([db store-id k] (let [store (open-store db store-id "readonly") request (.get store k)] (promisify request)))) (defn get-all ([store-id key-range] (get-all @+db+ store-id key-range)) ([db store-id key-range] (let [store (open-store db store-id "readonly") request (.getAll store key-range)] (promisify request)))) (defn clear! ([store-id] (clear! @+db+ store-id)) ([db store-id] (let [store (open-store db store-id "readwrite") request (.clear store)] (promisify request)))) (defn delete! ([store-id k] (get @+db+ store-id k)) ([db store-id k] (let [store (open-store db store-id "readwrite") request (.delete store k)] (promisify request)))) (defn count ([store-id] (count @+db+ store-id)) ([db store-id] (let [store (open-store db store-id "readonly") request (.count store)] (promisify request)))) (defn open-cursor ([store-id key-range] (open-cursor @+db+ store-id key-range {:direction :asc})) ([store-id key-range opts] (open-cursor @+db+ store-id key-range opts)) ([db store-id key-range {:keys [direction index]}] (let [store (open-store db store-id "readonly") key-range (if (= key-range ::all) (js/IDBKeyRange.lowerBound js/Number.NEGATIVE_INFINITY) key-range) direction ({:asc "next" :desc "prev"} direction direction)] (if index (.openCursor (.index store (str index)) key-range direction) (.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 iterate-cursor [f cursor-req] (.addEventListener cursor-req "success" (fn [ev] (if-let [cursor (-> ev .-target .-result)] (do (f cursor) (.continue cursor)) (f))))) (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." ([xform cursor-req] ; optimization: work with a transient vector before returning the final result (-> (transduce-cursor xform conj! (transient []) cursor-req) (.then persistent!))) ([xform rf cursor-req] (transduce-cursor xform rf (rf) cursor-req)) ([xform rf init cursor-req] (let [result (volatile! init) xform (xform rf)] (js/Promise. (fn [resolve _] (iterate-cursor (fn [cursor] (if cursor (let [step (xform @result (.-value cursor))] (if (reduced? step) (resolve @step) (vreset! result step))) (resolve @result))) cursor-req)))))) (defn first-result "Given a cursor, will return a promise that resolves to the first result. Optionally takes an `xform` that can be used to filter or transform values returned by the cursor." ([cursor-req] (first-result (map identity) cursor-req)) ([xform cursor-req] (transduce-cursor (comp xform (take 1)) (fn [_ x] x) nil cursor-req))) (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))))))) )