diff options
author | Hayleigh Thompson <me@hayleigh.dev> | 2023-08-23 23:09:16 +0100 |
---|---|---|
committer | Hayleigh Thompson <me@hayleigh.dev> | 2023-08-23 23:09:16 +0100 |
commit | c0a3607d41dd24b9ea03425892a361261893d076 (patch) | |
tree | 7387b86b55e48649aa5afb45df37193d72aa930b | |
parent | 21d59712dd56909293b3804b1d89320216f6906a (diff) | |
download | lustre-c0a3607d41dd24b9ea03425892a361261893d076.tar.gz lustre-c0a3607d41dd24b9ea03425892a361261893d076.zip |
:ambulance: Update third-party lustre_websocket package to new api.
-rw-r--r-- | compat/lustre_websocket/.gitignore | 1 | ||||
-rw-r--r-- | compat/lustre_websocket/README.md | 51 | ||||
-rw-r--r-- | compat/lustre_websocket/gleam.toml | 18 | ||||
-rw-r--r-- | compat/lustre_websocket/manifest.toml | 13 | ||||
-rw-r--r-- | compat/lustre_websocket/src/ffi.mjs | 39 | ||||
-rw-r--r-- | compat/lustre_websocket/src/lustre_websocket.gleam | 85 | ||||
-rw-r--r-- | compat/lustre_websocket/test/lustre_websocket_test.gleam | 15 |
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 + +[](https://hex.pm/packages/lustre_websocket) +[](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 +} |