aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2023-08-23 23:09:16 +0100
committerHayleigh Thompson <me@hayleigh.dev>2023-08-23 23:09:16 +0100
commitc0a3607d41dd24b9ea03425892a361261893d076 (patch)
tree7387b86b55e48649aa5afb45df37193d72aa930b
parent21d59712dd56909293b3804b1d89320216f6906a (diff)
downloadlustre-c0a3607d41dd24b9ea03425892a361261893d076.tar.gz
lustre-c0a3607d41dd24b9ea03425892a361261893d076.zip
:ambulance: Update third-party lustre_websocket package to new api.
-rw-r--r--compat/lustre_websocket/.gitignore1
-rw-r--r--compat/lustre_websocket/README.md51
-rw-r--r--compat/lustre_websocket/gleam.toml18
-rw-r--r--compat/lustre_websocket/manifest.toml13
-rw-r--r--compat/lustre_websocket/src/ffi.mjs39
-rw-r--r--compat/lustre_websocket/src/lustre_websocket.gleam85
-rw-r--r--compat/lustre_websocket/test/lustre_websocket_test.gleam15
7 files changed, 222 insertions, 0 deletions
diff --git a/compat/lustre_websocket/.gitignore b/compat/lustre_websocket/.gitignore
new file mode 100644
index 0000000..378eac2
--- /dev/null
+++ b/compat/lustre_websocket/.gitignore
@@ -0,0 +1 @@
+build
diff --git a/compat/lustre_websocket/README.md b/compat/lustre_websocket/README.md
new file mode 100644
index 0000000..5e30c8e
--- /dev/null
+++ b/compat/lustre_websocket/README.md
@@ -0,0 +1,51 @@
+# lustre_websocket
+
+[![Package Version](https://img.shields.io/hexpm/v/lustre_websocket)](https://hex.pm/packages/lustre_websocket)
+[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/lustre_websocket/)
+
+Use websockets from your `lustre` application!
+
+## Quick start
+
+Add to your Gleam project:
+
+```sh
+gleam add lustre_websocket
+```
+
+Typical usage looks like
+```
+import lustre_websocket as ws
+
+pub type Msg {
+ WsWrapper(ws.WebSocketEvent)
+}
+
+let init = #(Model(None), ws.init("/path", WsWrapper))
+```
+and then you pass `init` as first argument to `lustre.application`.
+But you can create a socket at any time, esp. re-create it after it is closed by the server.
+
+The events can be handled like this:
+```
+update(model, msg) {
+ case msg {
+ WsWrapper(OnOpen(socket)) -> #(Model(..model, ws: Some(socket)), ws.send(socket, "client-init"))
+ WsWrapper(OnMessage(msg)) -> todo
+ WsWrapper(OnClose(reason)) -> #(Model(..model, ws: None), cmd.none())
+ }
+}
+```
+which also demonstrates how you send a text message over the socket.
+
+### Caveat
+
+*This package cannot handle more than 1 socket on a server endpoint*
+
+### TODO:
+ * support protocol choice, including one websocket per protocol per endpoint
+ * support binary data
+ * allow client to close the socket
+ * provide errors to the application, when I have a clue on what those might actually be
+ * prevent sending over closed sockets
+ * maybe auto-reopen sockets that were closed because of Normal
diff --git a/compat/lustre_websocket/gleam.toml b/compat/lustre_websocket/gleam.toml
new file mode 100644
index 0000000..ba11fd3
--- /dev/null
+++ b/compat/lustre_websocket/gleam.toml
@@ -0,0 +1,18 @@
+name = "lustre_websocket"
+version = "0.6.0"
+target = "javascript"
+
+licences = ["MIT"]
+description = "Web Socket requests from lustre"
+repository = { type = "custom", url = "https://git.chmeee.org/lustre_websocket" }
+links = [
+ { title = "Package", href = "https://hex.pm/packages/lustre_websocket" },
+]
+
+[dependencies]
+gleam_stdlib = "~> 0.30"
+lustre = { path = "../../lib" }
+
+
+[dev-dependencies]
+gleeunit = "~> 0.10" \ No newline at end of file
diff --git a/compat/lustre_websocket/manifest.toml b/compat/lustre_websocket/manifest.toml
new file mode 100644
index 0000000..7cf6fc0
--- /dev/null
+++ b/compat/lustre_websocket/manifest.toml
@@ -0,0 +1,13 @@
+# This file was generated by Gleam
+# You typically do not need to edit this file
+
+packages = [
+ { name = "gleam_stdlib", version = "0.30.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "704258528887F95075FFED7AAE1CCF836A9B88E3AADA2F69F9DA15815F94A4F9" },
+ { name = "gleeunit", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "1397E5C4AC4108769EE979939AC39BF7870659C5AFB714630DEEEE16B8272AD5" },
+ { name = "lustre", version = "3.0.0-rc.8", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "/Users/hayleigh/dev/gleam/lustre/lib" },
+]
+
+[requirements]
+gleam_stdlib = { version = "~> 0.30" }
+gleeunit = { version = "~> 0.10" }
+lustre = { path = "../../lib" }
diff --git a/compat/lustre_websocket/src/ffi.mjs b/compat/lustre_websocket/src/ffi.mjs
new file mode 100644
index 0000000..56f11fd
--- /dev/null
+++ b/compat/lustre_websocket/src/ffi.mjs
@@ -0,0 +1,39 @@
+let ws_handler_registry = {}
+
+export const init_websocket = path => {
+ let ws
+ if (typeof WebSocket === "function") {
+ // we're in the browser
+ let url = new URL(document.URL)
+ let protocol = url.protocol === "http:" ? "ws" : "wss"
+ let ws_url = protocol + "://" + url.host + url.pathname + path
+ ws = new WebSocket(ws_url)
+ } else {
+ // we're NOT in the browser, prolly running tests
+ ws = {}
+ }
+ ws_handler_registry[ws.url] = { ws: ws }
+
+ ws.onopen = evt => {
+ ws_handler_registry[ws.url]?.on_open?.()
+ }
+ ws.onclose = evt => {
+ ws_handler_registry[ws.url]?.on_close?.(evt.code)
+ delete ws_handler_registry[ws.url]
+ }
+ ws.onmessage = event => ws_handler_registry[ws.url]?.on_message?.(event.data)
+ ws.onerror = error => console.log("ws", ws.url, "error", error, "no handler, since I have no clue what errors we might be talking about")
+ return ws
+}
+
+export const register_websocket_handler = (ws, on_open, on_message, on_close) => {
+ const reg_entry = ws_handler_registry[ws.url]
+ reg_entry.on_open = on_open
+ reg_entry.on_message = on_message
+ reg_entry.on_close = on_close
+ console.log("ws reg", ws_handler_registry)
+}
+
+export const send_over_websocket = (ws, msg) => {
+ ws.send(msg)
+}
diff --git a/compat/lustre_websocket/src/lustre_websocket.gleam b/compat/lustre_websocket/src/lustre_websocket.gleam
new file mode 100644
index 0000000..ce46a4a
--- /dev/null
+++ b/compat/lustre_websocket/src/lustre_websocket.gleam
@@ -0,0 +1,85 @@
+import lustre/effect.{Effect}
+
+pub type WebSocket
+
+pub type WebSocketCloseReason {
+ // 1000
+ Normal
+ // 1001
+ GoingAway
+ // 1002
+ ProtocolError
+ // 1003
+ UnexpectedTypeOfData
+ // 1004 Reserved
+ // 1005
+ NoCodeFromServer
+ // 1006, no close frame
+ AbnormalClose
+ // 1007
+ IncomprehensibleFrame
+ // 1008
+ PolicyViolated
+ // 1009
+ MessageTooBig
+ // 1010
+ FailedExtensionNegotation
+ // 1011
+ UnexpectedFailure
+ // 1015
+ FailedTLSHandshake
+}
+
+pub type WebSocketEvent {
+ OnOpen(WebSocket)
+ OnMessage(String)
+ OnClose(WebSocketCloseReason)
+}
+
+/// Initialize a websocket. These constructs are fully asynchronous, so you must provide a wrapper
+/// that takes a `WebSocketEvent` and turns it into a lustre message of your application.
+pub fn init(path: String, wrapper: fn(WebSocketEvent) -> a) -> Effect(a) {
+ let ws = do_init(path)
+ use dispatch <- effect.from
+ let on_open = fn() { dispatch(wrapper(OnOpen(ws))) }
+ let on_message = fn(in_msg) { dispatch(wrapper(OnMessage(in_msg))) }
+ let on_close = fn(code) {
+ case code {
+ 1000 -> dispatch(wrapper(OnClose(Normal)))
+ 1001 -> dispatch(wrapper(OnClose(GoingAway)))
+ 1002 -> dispatch(wrapper(OnClose(ProtocolError)))
+ 1003 -> dispatch(wrapper(OnClose(UnexpectedTypeOfData)))
+ 1005 -> dispatch(wrapper(OnClose(NoCodeFromServer)))
+ 1006 -> dispatch(wrapper(OnClose(AbnormalClose)))
+ 1007 -> dispatch(wrapper(OnClose(IncomprehensibleFrame)))
+ 1008 -> dispatch(wrapper(OnClose(PolicyViolated)))
+ 1009 -> dispatch(wrapper(OnClose(MessageTooBig)))
+ 1010 -> dispatch(wrapper(OnClose(FailedExtensionNegotation)))
+ 1011 -> dispatch(wrapper(OnClose(UnexpectedFailure)))
+ 1015 -> dispatch(wrapper(OnClose(FailedTLSHandshake)))
+ }
+ }
+
+ do_register(ws, on_open, on_message, on_close)
+}
+
+external fn do_init(path) -> WebSocket =
+ "./ffi.mjs" "init_websocket"
+
+external fn do_register(
+ ws: WebSocket,
+ on_open: fn() -> Nil,
+ on_message: fn(String) -> Nil,
+ on_close: fn(Int) -> Nil,
+) -> Nil =
+ "./ffi.mjs" "register_websocket_handler"
+
+/// Send a text message over the web socket. This is asynchronous. There is no
+/// expectation of a reply. See `init`. Only works on an Non-Closed socket.
+/// Returns a `Effect(a)` that you must pass as second entry in the lustre `update` return.
+pub fn send(ws: WebSocket, msg: String) -> Effect(a) {
+ effect.from(fn(_) { do_send(ws, msg) })
+}
+
+external fn do_send(ws: WebSocket, msg: String) -> Nil =
+ "./ffi.mjs" "send_over_websocket"
diff --git a/compat/lustre_websocket/test/lustre_websocket_test.gleam b/compat/lustre_websocket/test/lustre_websocket_test.gleam
new file mode 100644
index 0000000..2ce0f90
--- /dev/null
+++ b/compat/lustre_websocket/test/lustre_websocket_test.gleam
@@ -0,0 +1,15 @@
+import gleeunit
+import lustre_websocket as ws
+
+pub fn main() {
+ gleeunit.main()
+}
+
+pub type Wrapper {
+ Wrapper(ws.WebSocketEvent)
+}
+
+pub fn rather_thin_compilation_test() {
+ let _cmd = ws.init("/blah", Wrapper)
+ // We cannot run the resulting lustre.Cmd, but we can at least ensure it can be used/compiled this way
+}