biff

jf 2024-08-13T10:05:59.348569Z

is there a way to not do a restart of the app (deployed remotely) every time I do an update of the code? I have no issues doing clj -M:dev deploy..., but I would much prefer (as of right now, because of my hosting situation) the sort of "live update" experience of Clojure, vs the safer systemctl restart app .

2024-08-13T13:14:46.942729Z

yep! clj -M:dev soft-deploy

🎉 1
2024-08-13T13:15:40.967079Z

some things will still require a hard deploy, like if you add a dependency. but that'll work most of the time.

🙏 1
2024-08-13T00:10:26.945879Z

I'm due for another newsletter update, but until then, a small milestone: Just got a non-toy https://biffweb.com/p/indexes-prerelease/ set up in Yakread. Currently to load a list of all the newsletter subscriptions on my own account, the query takes 13 seconds; with the index, it takes 130 ms. Hallelujah. I've added a handful of functions for developing indexes when the prod transaction log is large. e.g. it takes about 6 hours to go through Yakread's entire transaction log and build this new index. The developer experience is pretty convenient though.

👏 6
2024-08-13T00:11:32.811839Z

The application code, which could be cleaned up a bit:

(defattr newsletter-subs :user/email-subscriptions [:vector :biff.attr/ref]
  {:biff.attr/input [:xt/id]
   :biff.attr/output [{:user/email-subscriptions [:sub/user
                                                  :sub/newsletter
                                                  :sub/kind
                                                  :sub/unread
                                                  :sub/last-published
                                                  :sub/pinned
                                                  :sub/total
                                                  :sub/read]}]}
  (fn [{:keys [biff.index/snapshots]} {user-id :xt/id}]
    (->> (q (:subscriptions snapshots)
            '{:find (pull sub [*])
              :in [user]
              :where [[sub :sub/user user]]}
            user-id)
         (remove (fn [{:keys [sub/hidden sub/total]}]
                   (= hidden total)))
         vec)))

(defn email-indexer [{:biff.index/keys [get-doc op doc]}]
  (cond
    (and (= op ::xt/put) (:item.email/user doc))
    (let [{:keys [item.email/user
                  item/author-name
                  item.email/hidden]} doc
          item-id (:xt/id doc)
          sub-id {:sub/user user :sub/newsletter author-name}
          old-sub (get-doc sub-id)
          last-published-ms (or (some-> (:sub/last-published old-sub) inst-ms) 0)
          new-sub (merge {:xt/id sub-id
                          :sub/user user
                          :sub/newsletter author-name
                          :sub/kind :sub.kind/email
                          :sub/pinned false}
                         old-sub
                         (when (< last-published-ms (inst-ms (:item/fetched-at doc)))
                           {:sub/last-published (:item/fetched-at doc)
                            :sub/total (inc (:sub/total old-sub 0))}))

          item-exists (some? (get-doc item-id))

          item-state-id {:item-state/user user :item-state/item item-id}
          old-state (when item-exists
                      (get-doc item-state-id))
          new-state (merge {:xt/id item-state-id}
                           old-state
                           {:item-state/hidden (boolean hidden)})
          hidden-changed (not= (boolean (:item-state/hidden old-state))
                               (boolean hidden))
          read? (fn [{:item-state/keys [read hidden]}]
                  (boolean (or read hidden)))
          read-changed (not= (read? old-state) (read? new-state))
          new-sub (cond-> new-sub
                    hidden-changed (update :sub/hidden (fnil + 0) (if hidden 1 -1))
                    read-changed (update :sub/read (fnil + 0) (if (read? new-state) 1 -1)))]
      (concat (when (not= old-sub new-sub)
                [[::xt/put new-sub]])
              (when-not item-exists
                [[::xt/put (select-keys doc [:xt/id :item/author-name])]])))

    (and (= op ::xt/put) (:pinned/user doc))
    (let [{:keys [pinned/newsletters pinned/user xt/id]} doc
          old-newsletters (:pinned/newsletters (get-doc id) #{})
          new-pins (set/difference newsletters old-newsletters)
          deleted-pins (set/difference old-newsletters newsletters)]
      (for [[pinned coll] [[true new-pins]
                           [false deleted-pins]]
            newsletter coll
            :let [sub-id {:sub/user user :sub/newsletter newsletter}
                  old-sub (get-doc sub-id)]
            :when old-sub]
        [::xt/put (assoc old-sub :sub/pinned pinned)]))

    (:rec/user doc)
    (let [{:keys [rec/user] item-id :rec/item} doc
          item-state-id {:item-state/user user :item-state/item item-id}
          old-state (get-doc item-state-id)
          read-state (= op ::xt/put)
          new-state (merge {:xt/id item-state-id}
                           old-state
                           {:item-state/read read-state})
          changed (not= read-state (boolean (:item-state/read old-state)))

          item (get-doc item-id)
          sub-id {:sub/user user :sub/newsletter (:item/author-name item)}
          old-sub (get-doc sub-id)
          read? (fn [{:item-state/keys [read hidden]}]
                  (boolean (or read hidden)))
          read-changed (not= (read? old-state) (read? new-state))
          new-sub (cond-> old-sub
                    read-changed (update :sub/read (fnil (if read-state inc dec) 0)))]
      (concat (when (not= old-state new-state)
                [[::xt/put new-state]])
              (when (and old-sub read-changed)
                [[::xt/put new-sub]])))))

(def module {:attrs [newsletter-subs]
             :indexes [{:id :subscriptions
                        :indexer email-indexer
                        :version 0}]})