This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-09-25
Channels
- # alda (7)
- # aleph (10)
- # announcements (3)
- # babashka (103)
- # beginners (54)
- # calva (62)
- # clerk (2)
- # clj-yaml (27)
- # cljs-dev (1)
- # clojure (61)
- # clojure-europe (64)
- # clojure-nl (3)
- # clojure-norway (34)
- # clojure-sweden (4)
- # clojure-uk (4)
- # conjure (9)
- # cursive (1)
- # data-science (3)
- # fulcro (20)
- # gratitude (1)
- # hyperfiddle (54)
- # lsp (9)
- # malli (7)
- # meander (4)
- # membrane (17)
- # off-topic (23)
- # releases (3)
- # sci (1)
- # shadow-cljs (5)
- # sql (1)
- # tree-sitter (8)
- # vim (6)
Is there a way to use Electric inside chrome extensions (e.g. on the panel or sidebar that execute in chrome devtools). I have a setup that works with shadow-cljs and rum, and I'd like to swap it out with Electric.
Electric's shadow-cljs.edn has :target :browser
but I also need to use :target :chrome-extension
. I have a :browser
build for creating the Electric backend (and starting up Chromium via Playwright's Java API which also loads the chrome extension from the shadow build). The :browser
build works fine on localhost, and the bare bones :chrome-extension
build works as well.
What I need now is a way to compile a devtools_sidebar.js file in the :chrome-extension
build that would connect to the web/Electric backend. In addition to devtools_sidebar.js knowing how to properly connect to the backend, the cljs-runtime files that are created for :chrome-extension
build would need to have the necessary Electric dependencies. I guess you could generalise this question to "how to connect to an external Electric backend from a different cljs build?"
Is this doable in principle? If so, could you give me additional pointers where to look next to understand how to make this happen? Or if anyone has time to help out, I could grant access to my existing code example. Any help is much appreciated!
> How to connect to an external Electric backend from a different cljs build? > Today this is not possible. It might be possible in the future, but it requires more research. Do you really need to share the server side process between the page and the extension. I assume you actually want to share state? If so, you can have two electric programs, one for the page and one for the extension, so two builds. If both electric programs runs on the same server jvm, then they can easily share state. If they share state, they can react to each other's state changes.
Two separate backend processes / shadow builds is fine as well. Is there a way to make a shadow build that has :target :chrome-extension
?
We haven’t tried :target :chrome-extension
yet.
Editing your shadow-cljs.edn
and replacing :target :browser
by :target :chrome-extension
should do the trick.
https://github.com/groundedsage/shadow-cljs-chrome-extension-hello-world/tree/main.
I’m also planning to build a chrome extension soon so I’m following this.
Going to be deploying a native app with webview and chrome extension. So might have to do the double server you said @U2DART3HA
With this approach I'm getting Unexpected protocol
exception when parsing the URL
which refers to the compiled js file which contains the Electric frontend code. Might need to patch the https://github.com/hyperfiddle/electric/blob/0842e64e895b260707a532068dc7f35c147fb65f/src/hyperfiddle/electric_client.cljs#L11 fn temporarily and see if hardcoding it to
or something like that works? What would be the correct hardcoded string to try if my app is accessible at
-
or sth else?
You are right, server-url
hasn’t been designed with chrome extensions in mind. Feel free to patch it (or improve the design) and submit a PR
Hardcoding the response of server-url
to "
actually makes it work, which is pretty awesome!
But I'm not sure how a good PR for this would look like, because the original URL does not indicate what the host/port/protocol of the backend actually is. Sometimes it would be localhost and http, but it might be some remote url over https as well. Would it make sense to be able to (optionally) pass it as a build argument?
Build argument would technically work. Though I’m not sure if it should be hardcoded.
I’d assume a production-ready chrome extension to:
• fetch initial configuration from an API.
• or have the page postMessage the config to the extension
• or always connect to
. Devs would just redirect the DNS resolve to localhost
• etc…
I’ve been outside of extension land for a while. But there is also native messaging for communicating to a local running app.
I’ll be looking into this more when I figure out the desktop setup I am working on.
For me, being able to set the URL at compile time would be enough. But the ideal long-term solution would be:
1. Define a list of backend URLs at compile time (e.g. localhost, dev server, prod server). I guess you could replace this part with "fetch initial configuration from an API".
2. If more than one URL was given, provide an https://developer.chrome.com/docs/extensions/mv2/options/ that enables users to choose the backend. The backend choice is persisted with chrome.storage.sync.set
and updated to different Electric frontends (devtools sidebar and panel, content script) with chrome.runtime.sendMessage
.
3. Might need to have some initializer (background page?) set the default backend in chrome.storage.sync
and emit the initial backend URL with chrome.runtime.sendMessage
.
4. Inside Electric frontends, have chrome.runtime.onMessage
listeners for changes in the backend URL. If it is detected, it reconnects - is that possible? Before it receives a message, it would not connect anywhere.
Not sure if it's feasible with the current architecture though.
> If it is detected, it reconnects - is that possible?
Yes it is possible.
On first message, onMessage
would call e/boot
to start the electric program
On second message, onMessage
would stop the running electric program and restart it by calling e/boot
again with the new domain
I actually got the following setup working, which is the initial thing I asked for:
1. There is a clj backend process (namespace proj.backend
) that starts an Electric server and has two shadow-cljs watches (one for the :web
build frontend and one for :ext
build js). In addition to starting Electric and shadow, it starts a nrepl server, where I connect with Calva. The process can start a Chromium browser with the chrome extension from the :ext
build.
2. The :web
build has :modules {:main {:entries [proj.web.frontend] :init-fn proj.web.frontend/start!}}
, so I can access the web frontend in a browser.
3. The chrome extension manifest.edn
defines outputs like :devtools-panel {:init-fn proj.ext.devtools-panel/start!
for the DevTools panel and sidebar (and could probably work for content script as well).
4. Because I patched server-url
to always returns ws:0.0.0.0:8888
which is the URL that proj.backend
binds to. So now the web frontend, devtools sidebar and devtools panel javascripts connect to the same backend. Which for me makes sense, because they work as a unified whole (you would often use the extension to interact with the web frontend, so syncing state makes sense). proj.backend
has an in-memory XTDB database, and changes from this get propagated to all the different frontends. Not sure if it would behave the same with regular atoms in the backend, or it would create separate state per ws connection (so one for sidebar, one for panel, and one for the web frontend).
I guess the reason the single backend approach here works is because I've hardcoded the server-url
response? I also tried an approach where I have separate backend processes for the :web
and :ext
builds with their respective shadow-cljs watches, but (1) shadow-cljs did not allow 2 simultaneously running shadow processes, and (2) I would need to use an XTDB that is not in-memory (is in a separate process and I'm listening on the transaction log in each (backend ?) process). I would need to do (2) soon anyway when scaling things out, though.
Will keep exploring this to see if I reach some rough edges, but so far it's pretty cool to get auto-reload and auto-saync between my web app and chrome extension.
I pass around a e/fn
, and when I call new
on it crashes the reactor
2023-09-25T08:43:06.039Z machina ERROR [hyperfiddle.electric-httpkit-adapter:326] - Websocket handler failure. nil
java.lang.Thread.run Thread.java: 1623
java.util.concurrent.ThreadPoolExecutor$Worker.run ThreadPoolExecutor.java: 642
java.util.concurrent.ThreadPoolExecutor.runWorker ThreadPoolExecutor.java: 1144
java.util.concurrent.FutureTask.run FutureTask.java: 317
java.util.concurrent.Executors$RunnableAdapter.call Executors.java: 577
org.httpkit.server.LinkingRunnable.run RingHandler.java: 156
org.httpkit.server.WSHandler.run RingHandler.java: 189
org.httpkit.server.AsyncChannel.messageReceived AsyncChannel.java: 153
clojure.core/partial/fn core.clj: 2641
hyperfiddle.electric-httpkit-adapter/handle-electric-ws/on-receive electric_httpkit_adapter.clj: 75
clojure.core/comp/fn core.clj: 2586
missionary.impl.Observe$1.invoke
missionary.impl.Sample$3.invoke Sample.java: 146
missionary.impl.Relieve$1.invoke Relieve.java: 86
missionary.impl.Reactor$1.invoke Reactor.java: 480
missionary.impl.Reactor.event Reactor.java: 398
missionary.impl.Reactor.propagate Reactor.java: 242
missionary.impl.Reactor.touch Reactor.java: 222
missionary.impl.Reactor.pull Reactor.java: 183
missionary.impl.Sample$Process.deref Sample.java: 34
missionary.impl.Sample.transfer Sample.java: 84
missionary.impl.Util.apply Util.java: 29
clojure.core/partial/fn core.clj: 2648
clojure.core/reduce core.clj: 6899
...
hyperfiddle.electric.impl.runtime/parse-event runtime.cljc: 771
clojure.core/reduce core.clj: 6899
...
hyperfiddle.electric.impl.runtime/eval-tree-inst runtime.cljc: 736
...
java.lang.NullPointerException: Cannot load from object array because "xs" is null
this simple example does NOT repro but it's the general idea, will keep digging
(let [!x (atom nil)
x (e/watch !x)]
(when x (new x))
(eui/button (e/fn [] (reset! !x (e/fn [] (ed/text "i die"))))
(ed/text "kill me")))
note that the thunked effect (rendering the text) does go off before the reactor crashes
is a lambda nil? do you have unserializable reference transfer warning? lambdas do not transfer today which is quite easy to accidentally cause using current semantics, you need to be aware of it
once you confirm e/server is not in play,
we have a nasty deep bug called “when true” bug which can cause the body of a when to see an impossible value, it is fixed in next electric but not in master, i’ll get more details for you later today from our issue tracker
there are workarounds
no e/server or serialiization errors. it's not a priority for me, actually thinking about a workaround led to a better data model: dynamic state systems don't need to handle singletons, since those are always known up front and can just be def
'd
i checked our tracker, the "when true" bug is an interaction with Pending, so if you have no e/server then I don't see how this is connected. electric-ui4/button perhaps could emit a pending (i just checked and didn't see one but the infrastructure ui4 uses is really complicated). We can rule it out by writing a barebones dom button with only electric-dom2
I don't understand your comment about singletones
electric has reactive static state. For dynamic state you use something like datascript or an atom holding a collection, but those aren't reactive. I've been struggling on figuring out a good api for my reactive dynamic state system to deal with cardinality and it turns out I can just leave all the cardinality-1 stuff to electric
anyway, my planned workaround was to put the e/fn
in an atom like my simple example but it turns out that still crashes it.. will have to dig further tomorrow
we are disaligned on terminology
you can (def todos-count (atom nil))
and set that at runtime and that's as granular as you'll ever want. you can't (def all-todos)
and have granular reactivity per todo
differential electric maybe can do that? If you are ingesting batch data then someone has to diff, in Electric 2 (today) e/for does the diffing in Electric 3 diffing is built into e/watch (details still firming up) and then diffs flow through the DAG not collections
i am too tired to think right now, but can you efficiently say "do this when :todo/id 1's :todo/title changes"? I was thinking differential electric is like being able to serialize flows
i think i need a clearer example but likely yes, differential electric is insane (good insane)
fwiw, putting e/fn in an atom is valid and reasonable and can be idiomatic, but it's also twisted enough that I am unsurprised if you find Electric evaluation bugs here
i'm not sure if we have any actual instances of that in any of our repos currently, maybe a couple unit tests
this does two pathom queries on mount and reactively subscribes to a datalog store. first [::global ::session/current-user session/current-user]
where session/current-user
is {:user/id 1}
, then the second one does [[_ :user/id 1] [_ :user/name user/name] ...
right now my reactive datalog comes from odoyle, a runtime reactive network. you're telling me that electric will be able to do this at compile time?
it was super cool when I realized that you don't need EQL or datalog joins at all, you can just do them in electric and you get fine grained reactive joins for free
now you're telling me that https://github.com/leonoel/missionary/commit/187ff9eb630b4281d4b1eb1d33f165a0aa5535cf implements differential dataflow in <200loc? what
it's a missionary plugin, it doesn't depend on missionary because missionary flows are "dependency free" you can construct missionary compatible flows out of just lambda
well if you can insert triples into a big array and efficiently get back the diffs, I guess you just need an {a {e #{flow}}
index and you can transfer those diffs to the UI efficiently
https://github.com/hyperfiddle/electric/blob/0842e64e895b260707a532068dc7f35c147fb65f/src/hyperfiddle/incseq.cljc#L1203 was the only example I looked at btw, no luck needed
looking at adding transit types, https://github.com/hyperfiddle/electric-examples-app/blob/main/src/wip/demo_custom_types.cljc says the client only needs read handlers?
No, sorry for the confusion. Both client and server can implement both read and write handlers. If your use case requires the client to send custom types to the server, then you’ll need to implement client write and server read.
Is it possible to set css vars from electric dom?
(dom/div
(dom/props
{:class "slider-container"
:style {"--num-sizes" num-sizes}}) <----- this bit here
...
dom/props
doesn’t support setting CSS vars yet.
In the meantime, this will work:
(dom/div
(dom/props {:class "slider-container"})
(.setProperty (.-style dom/node) "--num-sizes" num-sizes)
(e/on-unmount #(.removeProperty (.-style dom/node) "--num-sizes")))
amazing. Thanks for the workaround!!