Fork me on GitHub
#hyperfiddle
<
2023-05-23
>
jjttjj14:05:29

Is there a nice way to reuse an e/fn across other e/fns?

;; save-label is an e/fn that i want to reuse with multiple dom/on event triggers
(let [save-label
      (e/fn [e]
        (if-let [label (not-empty (.. e -target -textContent))]
          (e/server (do-stuff! label))
          (println "empty label")))]
  (dom/on "blur" (e/fn [e] (save-label e)))
  (dom/on "keydown"
    (e/fn [e]
      (when (= "Enter" (.-key e))
        (.preventDefault e)
        (save-label e)
        (.. e -target blur)))))

2
markaddleman14:05:35

I think this will work for you:

(let [save-label
      (e/fn [e]
        (if-let [label (not-empty (.. e -target -textContent))]
          (e/server (do-stuff! label))
          (println "empty label")))]
  (dom/on "blur" (e/fn [e] (save-label e)))
  (dom/on "keydown"
    (e/fn [e]
      (when (= "Enter" (.-key e))
        (.preventDefault e)
        (new save-label e)
        (.. e -target blur)))))

markaddleman14:05:52

The calling convention for electric functions is to use new

jjttjj14:05:20

that does it, thanks!

markaddleman14:05:28

I guess it’s more than a convention, it’s the law 🙂

jjttjj14:05:15

question that's been on the back of my mind, does new correspond directly to some missionary concept? Does e/fn create a task that new essentially calls m/? on (I think that can't be exactly it but maybe is it something like that)? I'm overdue to do a missionary refresher and I've been curious how exactly it relates to electric. I know it's implemented with it and deeply integrated, but I'm not sure where it is directly used vs wrapped.

👀 2
markaddleman14:05:21

As I recall a comment that @U09K620SG made, it has to do with a limitation of clojure to supply the necessary symbol / fn metadata so that the electric compiler can properly identify electric functions in all cases (related to anonymous functions, maybe?)

markaddleman14:05:35

Regardless, you’re not newing up something in the javascript/java sense. You’re simply letting the electric compiler know that you’re referencing an electric function rather than a regular clojure fn

👍 2
jjttjj15:05:58

Yup that makes sense

Dustin Getz16:05:39

new (thinking from the missionary layer) corresponds to monadic join on missionary flows, which looks something like (m/cp (m/?< (m/?< <<x)) - given a flow-of-flows, it returns a flow, removing one layer. This is the same concept as async/await on promises

Dustin Getz16:05:35

the fact that new also boots e/fn is revealing compiler internals, e/fn compiles down literally to a missionary flow. the e/fn parameters are compiled down to flows in dynamic scope that the e/fn flow will access

Dustin Getz16:05:55

TLDR • new is await for flows - (let [x (new (m/observe ...)) ...) • new boots Electric e/fns - (let [x (new (e/fn [] ...)) ...) • it turns out these two operators are the same thing due to compiler internals

braai engineer15:05:03

When deploying an Electric app to Fly via GH actions, the build alias deps don’t seem to be cached.

#12 [clojure-deps 6/6] RUN clojure -T:build noop           # preload build deps
#12 CACHED

# further down:
#19 [build 9/9] RUN clojure -X:build uberjar :jar-name "app.jar" :verbose true :version '"'9f539d3'"'
#19 1.611 Cloning: 
#19 2.874 Cloning: 
#19 4.432 Checking out:  at 9bd8b8a3c459966954b21c136e7b1084cb5e0fb0
#19 4.458 Checking out:  at ba1a2bf421838802e7bdefc541b41f57582e53b6
#19 5.063 Cloning: 
#19 6.216 Checking out:  at e3e353262072e95ccac314a9b935b1bc42412a40
#19 6.239 Checking out:  at 55fb6f63ea3cc5344e67e87d2322570d4dddd3d5
#19 6.669 Downloading: commons-io/commons-io/2.10.0/commons-io-2.10.0.pom from central
#19 6.814 Downloading: org/codehaus/plexus/plexus-interpolation/1.25/plexus-interpolation-1.25.pom from central
#19 6.818 Downloading: org/apache/maven/maven-builder-support/3.8.2/maven-builder-support-3.8.2.pom from central
Is this the right command to trigger fetching :build alias deps?
RUN clojure -T:build noop
It is the right alias flag.

2
Geoffrey Gaillard15:05:53

This is the right alias flag. The issue is git deps are prefetched but aren’t carried over to the next build step. So they are fetched a second time. Unlike maven deps (Dockerfile has a COPY instruction for .m2).

braai engineer15:05:44

Update: resolved by calling (time-literals.read-write/print-time-literals-clj!) in prod.clj. There is probably a #time/date in a namespace that entrypoint depends on. I’m heavily using https://github.com/henryw374/time-literals/ in an Electric project. Runs fine locally in dev, but when I deploy to http://Fly.io, it throws a java.lang.ExceptionInInitializerError on startup despite calling (time-literals.read-write/print-time-literals-clj!) at top of my app namespace:

2023-05-23T15:03:40.205 app[1781756a99ed89] jnb [info] at clojure.lang.Util.loadWithClass(Util.java:251)

2023-05-23T15:03:40.205 app[1781756a99ed89] jnb [info] at prod.<clinit>(Unknown Source)

2023-05-23T15:03:40.205 app[1781756a99ed89] jnb [info] Caused by: java.lang.IllegalStateException: Attempting to call unbound fn: #'time-literals.data-readers/date

2023-05-23T15:03:40.205 app[1781756a99ed89] jnb [info] at clojure.lang.Var$Unbound.throwArity(Var.java:45)

2023-05-23T15:03:40.205 app[1781756a99ed89] jnb [info] at clojure.lang.AFn.invoke(AFn.java:32)

2
Geoffrey Gaillard15:05:01

Is the namespace containing (time-literals.read-write/print-time-literals-clj!) at the top actually loaded/required in your prod setup?

braai engineer15:05:09

Thanks @U2DART3HA, I just moved that call to prod.clj (+ require) and now it seems to pass health check on Fly. Now getting 502 due to Connection state changed (MAX_CONCURRENT_STREAMS == 32)! on Fly, which is not Electric-related (I don’t think).

👀 2
braai engineer15:05:20

How much memory does my Electric XTDB app need on http://Fly.io? My deployed app ran out of memory (256MB).

2
Geoffrey Gaillard15:05:48

I don’t know how much your app is going to use. 256MB is low for a JVM app anyway. Bumping it to 1 or 2G is not expensive.

braai engineer15:05:22

OK so 512MB seems to have worked, but now I’m getting Missing client program manifest. Must be missing a file…

Dustin Getz16:05:32

Electric needs 512mb, your database likely needs a lot more

👍 2
braai engineer16:05:05

rn Electric only works with in-process XT. Why is that? Can it poll XT HTTP server so I can put XT on a bigger machine?

braai engineer16:05:10

(and also to share state)

Dustin Getz16:05:48

That's a question for the XTDB folks

Dustin Getz16:05:31

Electric can integrate any event stream that XTDB provides

braai engineer17:05:12

Has anyone tried to poll XT’s HTTP tx-log and turn it into a missionary stream?

braai engineer15:05:40

Deploying to http://Fly.io via GH actions frequently blocks because Waiting for remote builder fly-builder-wispy-wildflower-9951... When I run fly deploy on my machine, it seems to release the builder and unblock the GH build job, at which point I can Ctrl+C my local fly deploy.

2
Geoffrey Gaillard15:05:09

We have seen similar intermittent issues with fly. It’s not related to electric. Maybe have a look at the --remote-only and --local-only fly deploy flags?

braai engineer15:05:57

Deleted my Fly builder machine and retriggered GH deploy job to see if that fixes it.

Dustin Getz16:05:59

To echo what Geoffrey said - fly goes down a lot

braai engineer16:05:16

(the above seemed to work)

👍 2
braai engineer15:05:51

What does Missing client program manifest mean in prod?

Geoffrey Gaillard15:05:24

It means the client-side code might not have been compiled, or failed to compile. Look at your fly deploy output for cljs compilation errors. Prefix your fly deploy command with NO_COLOR=1 to get the full logs : NO_COLOR=1 flyctl deploy … When building the client-side code, the cljs compiler generates a manifest.edn file, sitting next to the compiled .js files. In prod, js artifacts are fingerprinted (`app.<SHA256>.js`) for proper cache invalidation. The server needs to read the manifest file to serve the js file.

braai engineer15:05:31

Ah, thanks. I was missing :prod build in shadow-cljs.edn and didn’t see it because previous failure was cached. Surprised it did not error out, just showed:

#19 24.30 Building client. Version: e7d2c50
#19 33.68 shadow-cljs - server version: 2.20.1 running at 
#19 33.68 shadow-cljs - nREPL server started on port 9001
#19 33.68 No configuration for build ":prod" found.
#19 34.69 Bundling sources
#19 34.73 Compiling server. Version: e7d2c50

Geoffrey Gaillard16:05:02

Agree. A cljs build failure should always fail the docker build.

☝️ 2
Aldo Solorzano15:05:17

Hello, I'm new to Electric and want to give it a try. The situation I have is the following: • The back-end is already built and has an endpoint Get /events that returns streams SSE • I want to make a GET request to /events and constantly react and render something based on what the end-point is returning. The demos I saw render stuff based on the db changes and from what I understand websockets are happening under-the hood. I don't know if electric would be suitable for my case, any guidance on how to start are very helpful.

2
jjttjj16:05:14

I've been doing something like this with my first electric project and it definitely works. I don't quite have a super great understanding how it works and have been curious about some things related to it. I've been using the OpenAI api to get streaming responses. So on my server I make an api request to them, and get a SSE response, which I turn into a lazy sequence from of the streaming results. I keep the state like this:

#?(:clj (defonce !msg-id->frags (atom {})))
(e/def msg-id->frags (e/server (e/watch !msg-id->frags)))
Each streamed fragment I get is conj'd into the atom like:
(swap! !msg-id->frags update resp-msg-id (fnil conj []) frag)
Then in any electric code I can just refer to msg-id->frags it it will reflect the latest changes. I have been curious if, under the hood, this entire map is sent across the wire every time a change is made, or if just a diff of the old and new map is sent. Either way, I could imagine for various reasons wanting to just stream something directly from the server to the client. From browsing the source I'm pretty sure this is possible but too advanced for me at the moment, was hoping to learn more of electric via osmosis before attempting it. I think this stuff might be relevant: https://github.com/hyperfiddle/electric/blob/a65ed416fb0ec8320b43b135bf4c5c1df0a6a607/src/hyperfiddle/electric.cljc#L435-L490 For my use case just watching the atom works well for now.

Aldo Solorzano16:05:48

Thank you very much, this indeed works as a starting point. I'll give it a try, it is good enough for the moment if all the seq is passed, later on we can see how to optimize it if need it. I'll spend some time playing around with the content yous shared. The example you mentioned is public code that you could share?

jjttjj16:05:22

I'm working on polishing it off to open source as we speak, possibly today or tomorrow

Aldo Solorzano16:05:29

good luck there

👍 2
Dustin Getz16:05:35

there is no diffing on msg-id->frags, the only Electric primitive that performs diffing is e/for-by

Dustin Getz16:05:35

Don't use e/for-event from master (don't use master at all actually), this is WIP. That is a low level API that is hard to use and we are trying to rework before releasing it.

Dustin Getz16:05:28

As to how to integrate OpenAI streaming responses, there is certainly a better way to do it than accumulating an atom and watching that, can you share some example responses and how you are trying to render them?

Dustin Getz16:05:24

But to be clear, the atom is fine if it's working for you

Aldo Solorzano17:05:57

What if we use datascript to persist everytime a event is received? and query for the events to render them in the client? similar to the https://electric.hyperfiddle.net/user.demo-todos-simple!TodoList it uses the e/for-by

Dustin Getz17:05:02

that strategy consumes unbounded memory (same as using (atom []) as a queue)

Dustin Getz17:05:17

If it works for you, it's fine

Aldo Solorzano17:05:33

how would you approach the problem? different solutions work fine and probably for the scale of this problem it's okay.

Dustin Getz17:05:29

I need to see some example responses and how you are trying to render them then i'll tell you 🙂

Aldo Solorzano17:05:02

fair enough, I'll code something and come back 🙂

jjttjj21:05:45

> I'm working on polishing [my openai electric demo app] off to open source as we speak, possibly today or tomorrow > As to how to integrate OpenAI streaming responses, there is certainly a better way to do it than accumulating an atom and watching that, can you share some example responses and how you are trying to render them? Got my electric demo out here: https://github.com/jjttjj/chatCLJ Definitely still needs some rounds of refactoring which I'll keep doing over time. In particular this streaming response stuff. Basically on the server I have a function that makes the streaming api request and returns an eduction of message fragments

(into []
    (comp (map #(select-keys % [:choices])) ;; I only use this key
          (take 4))
    (cljgpt.openai/streaming-request example-request))

  [{:choices
    [{:delta {:role "assistant"}, :index 0, :finish_reason nil}]}
   {:choices [{:delta {:content "Why"}, :index 0, :finish_reason nil}]}
   {:choices [{:delta {:content " did"}, :index 0, :finish_reason nil}]}
   {:choices [{:delta {:content " the"}, :index 0, :finish_reason nil}]}]
I just conj each of these fragments into a map of msg-id->frag, in an atom that is `e/watched, (while also building up a completed message that will eventually be saved) https://github.com/jjttjj/chatCLJ/blob/78629d13f97522d3e47f590573b9ff51ada5e778/src/chatclj/app.cljc#L181C8-L191 Then I want to render these fragments so when listing all messages for a chat, if one doesn't yet have a content key I lookup the fragments in msg-id->frags and do a (for-by identity ...) to show each fragment https://github.com/jjttjj/chatCLJ/blob/78629d13f97522d3e47f590573b9ff51ada5e778/src/chatclj/app.cljc#L267C21-L270

👀 2
jjttjj21:05:02

So at a super high level in bad "pseudocode", what I'm doing looks like this:

(def db (atom {1 "msg1" 2 "msg2" 3 :placeholder})) ;;<- this is the db in the real app
(let [;;frags isn't in the db, it's only temporary and could have it's scope limited to just the code below
      frags (atom {3 "m"}) ;; eventually "msg3"
      user-question "what's 1+1"]
  (for [[id msg] db]
    (if (= msg :placeholder)
      (show-fragments id)
      (show-msg msg)))
  (input
    (on :enter-keypress
      (fn [e]
        (let [msg-id 3
              answer-reducible (streaming-request user-question)
              ;;ie ["a" "b" "c" ...] 
              ]
          ;; the most straightforward thing 
          (run! #(swap! frags update msg-id str %) answer-reducible))))))
I guess te question is, is there a way in electric to directly stream something on the server to something atom-like that can be watched on the client?

jjttjj21:05:34

Actually, sorry. I realized this was already answered in my earlier slack question that I even linked to above, and I just need to take a serious stab at those methods (I got this working and moved on to other stuff before trying them deeply). But I guess I'll leave this here because it was sort of useful to re-state the situation

Dustin Getz16:05:19

I tried the chatclj app on my machine, I get 404 errors in the log when I submit a prompt. The api key is set as you see in bearers. Have I done something wrong?

clojure.lang.ExceptionInfo: Exceptional status code: 404
       body: #object[java.util.zip.GZIPInputStream 0x410d7a00 "java.util.zip.GZIPInputStream@410d7a00"]
    headers: {"content-encoding" "gzip",
              "server" "cloudflare",
              "content-type" "application/json; charset=utf-8",
              "alt-svc" "h3=\":443\"; ma=86400",
              "strict-transport-security" "max-age=15724800; includeSubDomains",
              "cf-cache-status" "DYNAMIC",
              "cf-ray" "7cdfc40dd9ca8ccc-EWR",
              ":status" "404",
              "date" "Sat, 27 May 2023 16:52:00 GMT",
"vary" "Origin",
              ...}
    request: {:headers
              {:accept "*/*",
               :accept-encoding ["gzip" "deflate"],
               :user-agent "babashka.http-client/0.1.4",
               "Content-Type" "application/json",
               "Authorization"
               "Bearer sk-qfi...H1tX"},                // Open API key
              :body
              "{\"model\":\"gpt-4\",\"messages\":[{\"role\":\"system\",\"content\":\"You are a helpful assistant.\"},{\"role\":\"user\",\"content\":\"what is \\\"ring\\\" in category theory?\"},{\"role\":\"user\",\"content\":\"translate \\\"hello\\\" to french\"}],\"stream\":true}",
              :as :stream,
              :uri "",
              :method :post}
     status: 404
    version: :http2

Dustin Getz17:05:01

Ok, I read the thread and looked at the code in the links. The stated question: "is there a way in electric to directly stream something on the server to something atom-like that can be watched on the client?" I don't understand the question and I also don't understand why you would ask that, I think there is missing context. Can we start with the problem – what problem are you facing that makes you ask this question? Please answer in the language of the end user, please do not answer in the language of programming

jjttjj14:05:44

> I tried the chatclj app on my machine, I get 404 errors in the log when I submit a prompt. Hmm, just to rule some things out, you have a funded openAI account right? It looks like that request you made is using gpt-4 which I think still requires getting on a waitlist and isn't generally available. I should put this in my readme. When you start a chat with gpt 3.5 do you still get the 404? You should be able to try out api requests here to confirm that they work for your openai account: https://platform.openai.com/playground?mode=chat > The stated question: "is there a way in electric to directly stream something on the server to something atom-like that can be watched on the client?" > Can we start with the problem – what problem are you facing that makes you ask this question? Good point, it's definitely not a user-facing problem. Partly just asking scattershot questions to get up to speed on Electric, and curious if your earlier point of: > As to how to integrate OpenAI streaming responses, there is certainly a better way to do it than accumulating an atom and watching that, can you share some example responses and how you are trying to render them? still applied. I was also a bit "worried" that the entire accumulated sequence of fragments was being sent over the wire each time an new fragment was received on the server, but I think that is avoided due to using e/for-by to iterate over the fragments here: https://github.com/jjttjj/chatCLJ/blob/c875af3fa030459edc60ac8c35681b37eb32897c/src/chatclj/app.cljc#L313-L316 This would be a premature-optimization at this point in any case, what I have works perfectly here. I have done things in the past that required direct server to client ui streaming of large amounts of data, so just a little curious. I do think I'm at a point now where I have enough context with Electric and a backlog of examples to try out that I've found in the repo that I could make an attempt at this when the need arises and have more specific questions then 🙂

Dustin Getz15:05:23

yes I pay for ChatGPT and have GPT4 access

Dustin Getz15:05:16

Ah, I guess paying for the OpenAI API is different than paying for ChatGPT ?

Dustin Getz15:05:04

I now see that it is, and my "free usage has expired" thanks

Dustin Getz15:05:41

you'll want to wrap that e/for-by in an e/server for (get msg-id->frags id) to run on the server and therefore individual frag to stream; currently the e/for-by is on the client so (get msg-id->frags id) is running on the client, which means (e/def msg-id->frags (e/server (e/watch !msg-id->frags))) the entire atom value is streaming to the client so the get can run on the client

Dustin Getz15:05:16

The point is that Electric does give you what you need to reason directly about efficient network transfers

Dustin Getz15:05:30

If your question is: "How do I stream a sequence of server events/delta/frags to the client efficiently": Two things to consider: • minimizing network traffic • cost of diffing

Dustin Getz15:05:33

Minimizing network traffic can be done with e/for-by as I described a moment ago

Dustin Getz15:05:36

Cost of diffing is: if OpenAI is giving us a stream of events/deltas/frags, why would we collect them into an atom with (swap! !msg-id->frags update resp-msg-id (fnil conj []) frag) (specifically conj) only to pass the collection to e/for-by which is going to separate them back into deltas by computing diffs?

Dustin Getz15:05:27

Also, storing the whole collection in an atom on the server costs unbounded memory, and if that collection gets huge, and you have to diff it back into frags each time it changes, ...

jjttjj15:05:56

Ah, the e/server wrapping now makes perfect sense. This worked on the first go and reads very nicely in that it's clear that it only streams the changes

👍 2
Dustin Getz15:05:13

The solution here (today) is tricky, it would involve dropping down to missionary and bypassing e/for-by entirely. We're working on an alternate primitive to e/for-by, perhaps called e/for-integrate-by or something which accepts a discrete event stream as an input (thereby skipping the diffing step)

Dustin Getz15:05:52

So for now I would continue doing what you're doing, as obviously it works well enough despite the theoretical flaws

👍 2
braai engineer16:05:45

Maybe this is shadow-cljs specific, but GH deployment to Fly does not stop if Cljs build fails due to a missing package.json dependency. This causes CI to deploy a broken image that passes health check.

braai engineer16:05:20

Where should I add a call to yarn or npm install in the client build pipeline? Seems like my cljs dependencies in package.json are not being installed. According to Shadow-cljs, docs supposed to call it: https://shadow-cljs.github.io/docs/UsersGuide.html#npm-install

2
braai engineer16:05:21

I see there is a node-deps step on example app now: https://github.com/hyperfiddle/electric-examples-app/blob/60e2e6f8c34c6397da91af7d067bd977a75240c1/Dockerfile#L1-L4 What’s the latest? Is there a reference Dockerfile for Electric XT?

👍 2
Geoffrey Gaillard16:05:50

Also look at line 8.

Geoffrey Gaillard16:05:45

Your link points to the latest.

braai engineer16:05:10

This is what I have:

FROM node:14.7-stretch AS node-deps
WORKDIR /app
COPY package.json package.json
RUN npm install

FROM clojure:openjdk-11-tools-deps AS clojure-deps
WORKDIR /app
COPY deps.edn deps.edn
COPY src-build src-build
RUN clojure -A:dev -M -e :ok        # preload deps
RUN clojure -T:build noop           # preload build deps

FROM clojure:openjdk-11-tools-deps AS build
WORKDIR /app
COPY --from=clojure-deps /root/.m2 /root/.m2
COPY --from=node-deps /app/node_modules /app/node_modules
COPY shadow-cljs.edn shadow-cljs.edn
COPY deps.edn deps.edn
COPY src src
COPY src-build src-build
COPY resources resources
ARG REBUILD=unknown
ARG VERSION
RUN clojure -X:build uberjar :jar-name "app.jar" :verbose true :version '"'$VERSION'"'

FROM amazoncorretto:11 AS app
WORKDIR /app
COPY --from=build /app/app.jar app.jar
EXPOSE 8080
ARG VERSION
ENV VERSION=$VERSION
CMD java -DHYPERFIDDLE_ELECTRIC_SERVER_VERSION=$VERSION -jar app.jar
Can I replace it with the reference? (for Electric XT) Note this is for XT, not Datomic.

Geoffrey Gaillard16:05:07

You cannot just replace it, lines 10 to 28 are not relevant outside of the electric-examples-app repo.

braai engineer16:05:36

> [build  7/13] COPY .m2 /root/.m2:
------
Error: failed to fetch an image or build from source: error building: failed to compute cache key: "/.m2" not found: not found
^ also

braai engineer16:05:36

currently blocked on this. should it be COPY /root/.m2 /root/.m2?

Geoffrey Gaillard16:05:00

There is a .m2 in electric-examples-app, but probably not in your project.

braai engineer16:05:26

should I create it? isn’t that supposed to be my machine’s m2 cache?

Geoffrey Gaillard16:05:24

Unless you need to set maven repository credentials, then you don’t need it and can drop the line in the Dockerfile.

braai engineer16:05:57

Isn’t .m2 related to caching maven deps? I’m trying with this step added back:

FROM clojure:openjdk-11-tools-deps AS clojure-deps
WORKDIR /app
COPY deps.edn deps.edn
COPY src-build src-build
RUN clojure -A:dev -M -e :ok        # preload deps
RUN clojure -T:build noop           # preload build deps

FROM clojure:openjdk-11-tools-deps AS build
WORKDIR /app
COPY --from=clojure-deps /root/.m2 /root/.m2
...

braai engineer16:05:50

fuuu image size is now 766MB, but it fixes my missing node deps. now stuck on required namespace "user" is not available (src/user.cljs is committed):

#22 19.42 [:prod] Compiling ...
#22 19.95 -> build target: :browser stage: :configure
#22 19.96 <- build target: :browser stage: :configure (3 ms)
#22 19.96 -> Resolving Module: :main
#22 19.96 The required namespace "user" is not available.
#22 19.96 
#22 DONE 20.5s

#23 exporting to image
Build still continues and deploys with missing manifest error.

Geoffrey Gaillard17:05:16

Maybe a classpath issue. Maybe some of your prod code references user/something .

braai engineer17:05:27

hmm nope. Here is src/user.cljs:

(ns ^:dev/always user ; Electric currently needs to rebuild everything when any file changes. Will fix
  (:require
    app.electric ;; my app entrypoint
    hyperfiddle.electric
    hyperfiddle.electric-dom2))

(prn "loading user namespace")

(def electric-main
  (hyperfiddle.electric/boot ; Electric macroexpansion - Clojure to signals compiler
    (binding [hyperfiddle.electric-dom2/node js/document.body]
      (app.electric/MyApp.))))

(defonce reactor nil)

(defn ^:dev/after-load ^:export start! []
  (prn "start")
  (assert (nil? reactor) "reactor already running")
  (set! reactor (electric-main
                  #(js/console.log "Reactor success:" %)
                  #(js/console.error "Reactor failure:" %))))

(defn ^:dev/before-load stop! []
  (prn "stop")
  (when reactor (reactor)) ; teardown
  (set! reactor nil))

braai engineer17:05:55

Oh you said prod code…Hmm here is src/prod.clj:

(ns prod
  (:gen-class)
  (:require
    time-literals.read-write
    app.electric
    clojure.string
    ;electric-server-java11-jetty10
    electric-server-java8-jetty9))

(time-literals.read-write/print-time-literals-clj!) ;; here because  complains on startup if in Electric.

(def electric-server-config
  {:host "0.0.0.0", :port 8080, :resources-path "public"})

(defn -main [& args]                                        ; run with `clj -M -m prod`
  (when (clojure.string/blank? (System/getProperty "HYPERFIDDLE_ELECTRIC_SERVER_VERSION"))
    (throw (ex-info "HYPERFIDDLE_ELECTRIC_SERVER_VERSION jvm property must be set in prod" {})))
  (electric-server-java8-jetty9/start-server! electric-server-config))

; On CLJS side we reuse src/user.cljs for prod entrypoint
the only thing that changed recently is time-literals.

Dustin Getz19:05:32

> the only thing that changed recently is time-literals. are you saying this used to work and then stopped working at a specific commit?

braai engineer16:05:41

Would it be possible to run the Electric cljs build and the clj uberjar build in parallel?

2
braai engineer17:05:01

Why would prod shadow-cljs build complain that The required namespace "user" is not available when src/user.cljs is committed? My shadow-cljs.edn matches https://github.com/hyperfiddle/electric-examples-app/blob/60e2e6f8c34c6397da91af7d067bd977a75240c1/shadow-cljs.edn.

2
braai engineer18:05:47

When I run clojure -X:build build-client locally, prod build seems to hang:

Building client. Version: 0e62a92-dirty
shadow-cljs - server version: 2.20.1 running at 
shadow-cljs - nREPL server started on port 9001
[:prod] Compiling ...
TRACE hyperfiddle.electric.impl.env: initial load opentax.electric
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external java.math.RoundingMode
TRACE hyperfiddle.electric.impl.env: loading external java.math.RoundingMode
TRACE hyperfiddle.electric.impl.env: loading external java.math.RoundingMode
# it just waits here forever.
Gonna try with verbose & debug.

braai engineer18:05:26

So everything runs fine locally, but on GH actions it fails:

#22 [build 10/10] RUN clojure -T:build build-client :verbose true :version '"'fdf113d'"'
#22 1.236 Cloning: 
#22 1.667 Cloning: 
#22 2.061 Checking out:  at ba1a2bf421838802e7bdefc541b41f57582e53b6
#22 2.081 Checking out:  at 9bd8b8a3c459966954b21c136e7b1084cb5e0fb0
#22 2.248 Cloning: 
#22 2.565 Checking out:  at e3e353262072e95ccac314a9b935b1bc42412a40
#22 2.582 Checking out:  at 55fb6f63ea3cc5344e67e87d2322570d4dddd3d5
#22 12.79 fatal: not a git repository (or any of the parent directories): .git
#22 14.02 Building client. Version: fdf113d
#22 19.44 shadow-cljs - server version: 2.20.1 running at 
#22 19.44 shadow-cljs - nREPL server started on port 9001
#22 19.45 [:prod] Compiling ...
#22 19.97 -> build target: :browser stage: :configure
#22 19.97 <- build target: :browser stage: :configure (4 ms)
#22 19.97 -> Resolving Module: :main
#22 19.98 The required namespace "user" is not available.
#22 19.98 
#22 DONE 20.5s

braai engineer22:05:24

Resolved by using the Dockerfile from electric-starter-app, not the commands from electric-examples-app. So instead of this from electric-examples-app: RUN clojure -T:build build-client :verbose true :version '"'$VERSION'"' Use: RUN clojure -X:build uberjar :jar-name "app.jar" :verbose true :version '"'$VERSION'"'

braai engineer17:05:54

How long does advanced cljs build typically take? dev build takes ~30s on my machine, but prod seems to take forever (or hangs).

braai engineer18:05:05

Trying now with clojure -X:build build-client verbose=true:

[:prod] Compiling ...
-> build target: :browser stage: :configure
<- build target: :browser stage: :configure (3 ms)
-> Resolving Module: :main
<- Resolving Module: :main (1145 ms)
...
<- Cache write: com/rpl/specter.cljc (129 ms)
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external clojure.lang.PersistentArrayMap
TRACE hyperfiddle.electric.impl.env: loading external java.math.RoundingMode
<- Compile CLJS: user.cljs (17209 ms)
-> build target: :browser stage: :compile-finish
<- build target: :browser stage: :compile-finish (1 ms)
-> build target: :browser stage: :optimize-prepare
<- build target: :browser stage: :optimize-prepare (0 ms)
-> Closure - Optimizing ...
Optimizing CLJS Constants took 1071ms
... ;; hangs here.

braai engineer18:05:44

<- Closure - Optimizing ... (186481 ms)
-> build target: :browser stage: :optimize-finish
<- build target: :browser stage: :optimize-finish (0 ms)
-> build target: :browser stage: :flush
-> Flushing optimized modules
Flushing: main.D52B65600F20EE2C84CB5486269BBD07.js (22972665 bytes)
<- Flushing optimized modules (1723 ms)
<- build target: :browser stage: :flush (2384 ms)
[:prod] Build completed. (223 files, 91 compiled, 0 warnings, 232.74s)
phew OK, advanced build takes about 4 minutes (with debug setting).

braai engineer18:05:04

any tips to speed this up? maybe ditch Specter in client?

braai engineer18:05:36

Aside, I get these Missionary warnings on advanced build:

Optimizing CLJS Constants took 1071ms
------ WARNING #1 -  -----------------------------------------------------------
 File: ~/.m2/repository/org/clojure/clojurescript/1.11.60/clojurescript-1.11.60.jar!/cljs/pprint.cljs:260
--------------------------------------------------------------------------------
 257 |
 258 | (deftype end-block-t :logical-block :start-pos :end-pos)
 259 |
 260 | (deftype indent-t :logical-block :relative-to :offset :start-pos :end-pos)
--------------------------------------------------------------------------------
 variable G__35634__$1 is undeclared
--------------------------------------------------------------------------------
 261 |
 262 | (def ^:private pp-newline (fn [] "\n"))
 263 |
 264 | (declare emit-nl)
--------------------------------------------------------------------------------
------ WARNING #2 -  -----------------------------------------------------------
 File: ~/.m2/repository/missionary/missionary/b.27-SNAPSHOT/missionary-b.27-SNAPSHOT.jar!/missionary/impl.cljs:161:15
--------------------------------------------------------------------------------
 158 |     (when (== n (alength (.-result j)))
 159 |       (let [w (.-race j)]
 160 |         (if (neg? w)
 161 |           (try ((.-joincb j) (.apply (.-combinator j) nil (.-result j)))
---------------------^----------------------------------------------------------
 variable $fexpr__35624 is undeclared
--------------------------------------------------------------------------------
 162 |                (catch :default e ((.-racecb j) e)))
 163 |           ((.-racecb j) (aget (.-result j) w)))))))
 164 |
 165 | (defn race-join [r c ts s f]
--------------------------------------------------------------------------------
nil
<- Closure - Optimizing ... (186481 ms)

braai engineer18:05:20

In Fly build step on GH I get a “fatal: not a git repository” error. Could that be causing my “required namespace “user” is not available issue?

#22 [build 10/10] RUN clojure -T:build build-client :verbose true :version '"'fdf113d'"'
#22 1.236 Cloning:  ;; ...
;; ...
#22 2.582 Checking out:  at 55fb6f63ea3cc5344e67e87d2322570d4dddd3d5
#22 12.79 fatal: not a git repository (or any of the parent directories): .git
#22 14.02 Building client. Version: fdf113d

2
Geoffrey Gaillard18:05:49

This message seems like a false positive. We have seen it and had no impact. I don’t think it is related to your issue.

👍 2
braai engineer22:05:27

How do I debug Reactor failure: Remote error - 1011? Finally got my app deployed to Fly, but when I open the app in browser, I see a brief flash of text and then: Remote error - 1011 org.eclipse.jetty.websocket.core.exception.WebSocketException, which I think means error on server. The app has not run out of memory on Fly (268MB/496MB) and is still running, but I don’t see any logs on Fly’s monitoring. First thought is to do a simple (not advanced) client build, because JS stacktrace is inscrutable. Logback.xml has these two <logger> entries:

<logger name="hyperfiddle" level="DEBUG" additivity="false"><appender-ref ref="STDOUT" /></logger>
<logger name="hyperfiddle.electric-jetty-adapter" level="WARN" additivity="false"><appender-ref ref="STDOUT" /></logger>

2
Dustin Getz22:05:30

the first thing i do is run the prod build locally to repro outside of fly

Dustin Getz22:05:58

i don’t know why it isn’t logging the exception, i don’t think we have seen that issue yet

Dustin Getz22:05:15

websocket exception may be different than application exception on server as well, on mobile so can’t look at code path easily for that error

braai engineer23:05:15

Ran the prod build locally. It starts, throws a different TypeError, but reactor does not crash:

in.3BE3A164204AC9A7A9881BC3DE98B078.js:1699 Uncaught TypeError: Cannot set properties of null (setting 'X')
    at B_ (main.3BE3A164204AC9A7A9881BC3DE98B078.js:1699:290)
    at p_ (main.3BE3A164204AC9A7A9881BC3DE98B078.js:1705:57)
    at o_.o (main.3BE3A164204AC9A7A9881BC3DE98B078.js:1695:291)
    at f0 (main.3BE3A164204AC9A7A9881BC3DE98B078.js:1717:77)
    at e0 (main.3BE3A164204AC9A7A9881BC3DE98B078.js:1718:31)
    at S_ (main.3BE3A164204AC9A7A9881BC3DE98B078.js:1716:390)
    at R_.o (main.3BE3A164204AC9A7A9881BC3DE98B078.js:1713:780)
    at f0 (main.3BE3A164204AC9A7A9881BC3DE98B078.js:1717:77)
    at e0 (main.3BE3A164204AC9A7A9881BC3DE98B078.js:1718:31)
    at S_ (main.3BE3A164204AC9A7A9881BC3DE98B078.js:1716:390)
B
This happens even for a simple view that does nothing other than render a static text message (no DB queries).

Dustin Getz23:05:42

and the dev build works?

braai engineer23:05:59

Both dev and prod work fine locally. They both throw Only prod throws the TypeError, but it does not seem to cause any problems. I nee to populate the DB in this folder to be 100% sure tho. I also did prod with :optimizations :simple - same outcome. Nothing in server logs.

braai engineer23:05:48

here is the error in dev prod with optimizations :simple

in.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:6943 Uncaught TypeError: Cannot set properties of null (setting 'prev')
    at missionary.impl.Reactor.propagate (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:6943:259)
    at missionary.impl.Reactor.event (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:6954:168)
    at missionary.impl.Reactor.Process.cljs$core$IFn$_invoke$arity$0 (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:6934:245)
    at missionary.impl.Ambiguous.cancel (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:7009:157)
    at missionary.impl.Ambiguous.walk (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:7011:85)
    at missionary.impl.Ambiguous.kill (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:7008:459)
    at missionary.impl.Ambiguous.Process.cljs$core$IFn$_invoke$arity$0 (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:6989:374)
    at missionary.impl.Ambiguous.cancel (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:7009:157)
    at missionary.impl.Ambiguous.walk (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:7011:85)
    at missionary.impl.Ambiguous.kill (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:7008:459)
m

braai engineer23:05:58

@U09K620SG so an interesting thing happens if I start up dev, load up the client (all works as expected in dev - no errors), then I stop dev and start prod while keeping client open (built from same source - should be same hash right? not sure). client then attempts reconnection and sees a reactor failure that might be the same error I’m seeing on Fly, but I can’t be 100% sure and maybe it’s something totally different:

electric_client.cljs:31 WebSocket connection to '' failed: 
eval @ electric_client.cljs:31
eval @ Sequential.cljs:29
missionary$impl$Sequential$suspend @ Sequential.cljs:29
eval @ Sequential.cljs:13
missionary$core$park @ core.cljc:162
hyperfiddle$electric_client$connector_$_cr62862_block_0 @ electric_client.cljs:78
eval @ impl.cljc:60
G__59769__0 @ impl.cljc:60
eval @ Sequential.cljs:40
missionary$impl$Sequential$step @ Sequential.cljs:40
missionary$impl$Sequential$run @ Sequential.cljs:59
missionary$core$sp_run @ core.cljc:175
G__59769__3 @ impl.cljc:65
G__46710__2 @ core.cljs:4358
eval @ Sequential.cljs:29
missionary$impl$Sequential$suspend @ Sequential.cljs:29
eval @ Sequential.cljs:13
missionary$core$park @ core.cljc:162
hyperfiddle$electric_client$boot_with_retry_$_cr63073_block_2 @ electric_client.cljs:103
eval @ impl.cljc:60
G__59769__0 @ impl.cljc:60
eval @ Sequential.cljs:40
missionary$impl$Sequential$step @ Sequential.cljs:40
eval @ Sequential.cljs:53
eval @ impl.cljs:205
01:26:08.032 electric_client.cljs:126 Failed to connect.
01:26:08.033 electric_client.cljs:127 Next attempt in 5.5 seconds.
01:26:08.758 websocket.cljs:12 WebSocket connection to '' failed: 
shadow$cljs$devtools$client$websocket$start @ websocket.cljs:12
eval @ shared.cljs:324
eval @ shared.cljs:345
01:26:08.758 shared.cljs:305 shadow-cljs - remote-error Event {isTrusted: true, type: 'error', target: WebSocket, currentTarget: WebSocket, eventPhase: 2, …}
eval @ shared.cljs:305
shadow$cljs$devtools$client$shared$remote_error @ shared.cljs:18
eval @ websocket.cljs:29
error (async)
shadow$cljs$devtools$client$websocket$start @ websocket.cljs:26
eval @ shared.cljs:324
eval @ shared.cljs:345
setTimeout (async)
eval @ shared.cljs:341
eval @ shared.cljs:297
shadow$cljs$devtools$client$shared$remote_close @ shared.cljs:17
eval @ websocket.cljs:24
01:26:13.542 electric_client.cljs:107 Connecting...
01:26:13.762 electric_client.cljs:112 Connected.
01:26:13.954 electric_client.cljs:66 WebSocket is already in CLOSING or CLOSED state.
hyperfiddle$electric_client$send_BANG_ @ electric_client.cljs:66
eval @ electric_client.cljs:69
hyperfiddle$electric_client$send_all_$_cr62775_block_1 @ electric_client.cljs:69
eval @ impl.cljc:60
G__59769__0 @ impl.cljc:60
eval @ Ambiguous.cljs:262
missionary$impl$Ambiguous$ready @ Ambiguous.cljs:264
missionary$impl$Ambiguous$boot @ Ambiguous.cljs:28
G__59769__2 @ impl.cljc:64
missionary$impl$Ambiguous$backtrack @ Ambiguous.cljs:34
missionary$impl$Ambiguous$branch @ Ambiguous.cljs:73
missionary$impl$Ambiguous$discard @ Ambiguous.cljs:184
missionary$impl$Ambiguous$done @ Ambiguous.cljs:212
eval @ Ambiguous.cljs:233
missionary$impl$Ambiguous$transfer @ Ambiguous.cljs:237
eval @ Ambiguous.cljs:11
cljs$core$_deref @ core.cljs:688
cljs$core$deref @ core.cljs:1477
eval @ Reduce.cljs:16
eval @ Reduce.cljs:16
missionary$impl$Reduce$transfer @ Reduce.cljs:21
missionary$impl$Reduce$ready @ Reduce.cljs:30
G__56331 @ Reduce.cljs:36
G__57122__1 @ Ambiguous.cljs:334
missionary$impl$Sequential$step @ Sequential.cljs:45
eval @ Sequential.cljs:58
missionary$impl$sleep_cancel @ impl.cljs:201
eval @ impl.cljs:195
missionary$impl$Sequential$kill @ Sequential.cljs:26
eval @ Sequential.cljs:11
missionary$impl$Ambiguous$cancel @ Ambiguous.cljs:92
missionary$impl$Ambiguous$walk @ Ambiguous.cljs:116
missionary$impl$Ambiguous$cancel @ Ambiguous.cljs:102
missionary$impl$Ambiguous$walk @ Ambiguous.cljs:116
missionary$impl$Ambiguous$kill @ Ambiguous.cljs:85
eval @ Ambiguous.cljs:9
eval @ Reduce.cljs:8
missionary$impl$racejoin_cancel @ impl.cljs:153
eval @ impl.cljs:177
eval @ electric_client.cljs:56
01:26:13.954 user.cljs:21 Reactor failure: cljs$core$ExceptionInfo {message: 'Remote error - 1011 …apter$WebSocketPingPongListener$12d400b6 OPEN met', data: {…}, cause: null, name: 'Error', description: undefined, …}cause: nullcolumnNumber: undefineddata: {meta: {…}, cnt: 0, arr: Array(0), __hash: -15128758, cljs$lang$protocol_mask$partition0$: 16647951, …}description: undefinedfileName: undefinedlineNumber: undefinedmessage: "Remote error - 1011 org.eclipse.jetty.websocket.core.exception.WebSocketException: WebSocketAdapter$WebSocketPingPongListener$12d400b6 OPEN met"name: "Error"number: undefinedstack: "Error: Remote error - 1011 org.eclipse.jetty.websocket.core.exception.WebSocketException: WebSocketAdapter$WebSocketPingPongListener$12d400b6 OPEN met\n    at new cljs$core$ExceptionInfo ()\n    at Function.eval [as cljs$core$IFn$_invoke$arity$3] ()\n    at Function.eval [as cljs$core$IFn$_invoke$arity$2] ()\n    at eval ()\n    at hyperfiddle$electric_client$boot_with_retry_$_cr63073_block_10 ()\n    at eval ()\n    at Function.G__59769__0 [as cljs$core$IFn$_invoke$arity$0] ()\n    at eval ()\n    at Object.missionary$impl$Sequential$step [as step] ()\n    at eval ()"[[Prototype]]: Errorcljs$core$IPrintWithWriter$: {}cljs$core$IPrintWithWriter$_pr_writer$arity$3: ƒ (obj,writer,opts)toString: ƒ ()constructor: ƒ cljs$core$ExceptionInfo(message,data,cause)[[Prototype]]: Object
eval @ user.cljs:21
missionary$impl$Sequential$step @ Sequential.cljs:45
eval @ Sequential.cljs:53
missionary$impl$Sequential$step @ Sequential.cljs:43
eval @ Sequential.cljs:53
missionary$impl$racejoin_terminated @ impl.cljs:163
eval @ impl.cljs:172
eval @ impl.cljs:178
eval @ electric_client.cljs:56
01:26:13.957 websocket.cljs:12 WebSocket connection to '' failed: 
s

braai engineer23:05:34

So symptoms are: • deploy to Fly — Reactor failure • local prod build w/optimizations :simple throw Uncaught TypeError: Cannot set properties of null (setting 'prev') on startup & inputs throw this on click or type:

in.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:7996 Uncaught Error: No matching clause: 
    at hyperfiddle.electric_dom2.happen (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:7996:487)
    at cljs.core.swap_BANG_.cljs$core$IFn$_invoke$arity$3 (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:1575:215)
    at Function.f [as cljs$core$IFn$_invoke$arity$1] (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:1531:256)
    at HTMLInputElement.<anonymous> (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:7995:339)
h
• local dev - everything works, no errors. • no server logs visible throughout. Is there a way to get more logs?

Dustin Getz00:05:41

once the first low level NPE has happened the app is crashed, no further errors produced from this undefined state are interesting

Dustin Getz00:05:16

do you have a git clone url?

braai engineer00:05:36

I’ll DM (it’s a private repo)

braai engineer23:05:28

Has anyone seen an error like this in prod when interacting with UI components, even just clicking on input (focus change)?

Uncaught Error: No matching clause: 
    at hyperfiddle.electric_dom2.happen (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:7996:487)
    at cljs.core.swap_BANG_.cljs$core$IFn$_invoke$arity$3 (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:1575:215)
    at Function.f [as cljs$core$IFn$_invoke$arity$1] (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:1531:256)
    at HTMLInputElement.<anonymous> (main.362B9B6A4E66C2E4112AB2DE5B5C5B41.js:7995:339)
h

2
braai engineer23:05:36

This function:

(defn happen [s e]
  ; Todo, we need a buffer (unbounded) to force a nil in between overlapping events to fix race
  ; Buffer is unbounded because all events matter. (This is sequential unbounded queue)
  (case (:status s)
    :idle {:status :impulse :event e} ; rising edge
    :pending {:status :impulse :event e} ; supersede the outstanding event with a new event
    :impulse (assert false "two events in the same frame? that's weird and wrong")))
I assume (:status s) is nil.

Dustin Getz00:05:08

for this to happen, iiuc, either the app is already crashed and you interacted again from the crashed state, or you ran into a very subtle electric bug that we call the “when true” bug in which an impossible nil can be observed in situations like (when (not= nil x) (prn x)). i believe the bug is fixed in the next major Electric runtime version that Leo has been working on, we have a workaround for this as well. Please confirm that the app wasn’t already broken when you saw this?

braai engineer03:05:08

How can I tell if the app has crashed if there are no logs and web requests still get served? Is there maybe a special Electric endpoint that can tell me the state of the reactor?