gtting closer, now must support ports

This commit is contained in:
Yehowshua Immanuel 2024-12-31 20:50:26 -05:00
parent 38bfd3bf18
commit 0412bc04c8
21 changed files with 2109 additions and 16 deletions

14
.gitignore vendored
View file

@ -1,6 +1,8 @@
elm-stuff/
elm.js
elm.min.js
node_modules/
package-lock.json
package.json
frontend/elm-stuff/
frontend/elm.js
frontend/elm.min.js
frontend/node_modules/
frontend/package-lock.json
frontend/package.json
backend/target
public/*

View file

@ -1,10 +1,36 @@
SRC_FILES := $(shell find src -name "*.elm")
# Directories
FRONTEND_DIR := ./frontend
BACKEND_DIR := ./backend
PUBLIC_DIR := ./public
ASSET_DIR := ./assets
all: elm.min.js
# Commands
ELM_MAKE := elm make
CARGO_BUILD := cargo build
CARGO_RUN := cargo run
# Targets
.PHONY: all frontend backend clean serve
all: frontend backend
frontend:
make -C $(FRONTEND_DIR) elm.min.js
cp $(FRONTEND_DIR)/index.html $(PUBLIC_DIR)/
cp $(FRONTEND_DIR)/elm.min.js $(PUBLIC_DIR)/
cp $(FRONTEND_DIR)/src/ports.websocket.js $(PUBLIC_DIR)/
mkdir -p $(PUBLIC_DIR)/assets
backend:
$(CARGO_BUILD) --manifest-path=$(BACKEND_DIR)/Cargo.toml
serve: all
python3 -m http.server
$(CARGO_RUN) --manifest-path=$(BACKEND_DIR)/Cargo.toml
serve_debug: all
RUST_LOG=info,actix_web=debug $(CARGO_RUN) --manifest-path=$(BACKEND_DIR)/Cargo.toml
clean:
rm -rf $(PUBLIC_DIR)/*
rm -rf $(BACKEND_DIR)/target
elm.min.js: $(SRC_FILES)
rm -f elm.min.js elm.js
./optimize.sh src/Main.elm

View file

@ -18,7 +18,8 @@ Now open `http://localhost:8000` in your browser.
# TODO
- [x] Add Makefile
- [ ] Determine if `src/Body.elm` or pages in `sr/Page` should have subscription functions
- [ ] Add GPLV3 License
- [ ] Determine if `src/Body.elm` or pages in `src/Page` should have subscription functions
- [ ] use actix backend that maps most root requests to serve `actix_file::Files`
- [ ] Submit to slack for feedback...
- [ ] Refactor into router page

1846
backend/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

15
backend/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "backend"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.0"
actix-files = "0.6"
actix-web-actors = "4.0"
actix = "0.13"
chrono = "0.4" # For timestamp generation
env_logger = "0.10" # For logging
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
log = "0.4"

82
backend/src/main.rs Normal file
View file

@ -0,0 +1,82 @@
use actix_files::Files;
use actix_web::{web, App, HttpServer, Responder, HttpRequest, HttpResponse, Error};
use actix_web_actors::ws;
use log::{info};
use serde::{Deserialize, Serialize};
use std::{fs, time::Duration};
use actix::prelude::*;
/// Greeting API structures
#[derive(Serialize, Deserialize)]
struct GreetingRequest {
name: String,
}
#[derive(Serialize)]
struct GreetingResponse {
message: String,
}
async fn greet(req: web::Json<GreetingRequest>) -> impl Responder {
info!("Received request to /api/greet with name: {}", req.name);
let message = format!("Hello, {}!", req.name);
web::Json(GreetingResponse { message })
}
/// WebSocket actor
struct MyWebSocket;
impl Actor for MyWebSocket {
type Context = ws::WebsocketContext<Self>;
fn started(&mut self, ctx: &mut Self::Context) {
info!("WebSocket actor started");
// Send messages every second
ctx.run_interval(Duration::from_secs(1), |_, ctx| {
let message = format!("{{\"time\" : \"{:?}\" }}", chrono::Local::now());
ctx.text(message);
});
info!("Leaving started");
}
}
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWebSocket {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut ws::WebsocketContext<Self>) {
if let Ok(ws::Message::Ping(msg)) = msg {
ctx.pong(&msg);
}
}
}
async fn websocket_handler(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
ws::start(MyWebSocket {}, &req, stream)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
let address = "127.0.0.1";
let port = 8080;
info!("Starting server at http://{}:{}", address, port);
HttpServer::new(|| {
App::new()
.route("/api/greet", web::post().to(greet)) // Greeting API
.route("/ws/", web::get().to(websocket_handler)) // WebSocket endpoint
.service(Files::new("/assets", "./public/assets"))
.service(Files::new("/", "./public").index_file("index.html")) // Serve frontend
.default_service(web::route().to(|| async {
// Serve the `index.html` file
let index_html = fs::read_to_string("./public/index.html")
.unwrap_or_else(|_| "404 Not Found".to_string());
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(index_html)
}))
})
.bind((address, port))?
.run()
.await
}

7
frontend/Makefile Normal file
View file

@ -0,0 +1,7 @@
SRC_FILES := $(shell find src -name "*.elm")
all: elm.min.js
elm.min.js: $(SRC_FILES)
rm -f elm.min.js elm.js
./optimize.sh src/Main.elm

View file

@ -7,10 +7,12 @@
<body>
<div id="elm"></div>
<script src="elm.min.js"></script>
<script src="ports.websocket.js"></script>
<script>
var app = Elm.Main.init({
node: document.getElementById('elm')
});
initSockets(app);
</script>
</body>
</html>

View file

@ -1,4 +1,4 @@
module Body exposing (Msg(..), Model(..), init, update, view)
module Body exposing (Msg(..), Model(..), init, update, view, handleRoute)
import Element
@ -7,6 +7,7 @@ import Page.Contact
import Page.Landing
import Page.Products
import Page.Resources
import Page.NotFound
type Msg
= MsgLanding Page.Landing.Msg
@ -14,6 +15,7 @@ type Msg
| MsgResources Page.Resources.Msg
| MsgAbout Page.About.Msg
| MsgContact Page.Contact.Msg
| MsgNotFound Page.NotFound.Msg
type Model
= ModelLanding Page.Landing.Model
@ -21,10 +23,25 @@ type Model
| ModelResources Page.Resources.Model
| ModelAbout Page.About.Model
| ModelContact Page.Contact.Model
| ModelNotFound Page.NotFound.Model
init : () -> Model
init flags = ModelLanding (Page.Landing.init flags)
handleRoute : String -> Model
handleRoute path =
let
page =
case path of
"/" -> ModelLanding <| Page.Landing.init ()
"/Products" -> ModelProducts <| Page.Products.init ()
"/Resources" -> ModelResources <| Page.Resources.init ()
"/About" -> ModelAbout <| Page.About.init ()
"/Contact" -> ModelContact <| Page.Contact.init ()
_ -> ModelNotFound <| Page.NotFound.init ()
in
page
update : Msg -> Model -> (Model, Cmd Msg)
update bodyMsg bodyModel =
let
@ -62,5 +79,6 @@ view model =
ModelResources m -> Page.Resources.view m |> Element.map MsgResources
ModelAbout m -> Page.About.view m |> Element.map MsgAbout
ModelContact m -> Page.Contact.view m |> Element.map MsgContact
ModelNotFound m -> Page.NotFound.view m |> Element.map MsgNotFound
in
Element.el [Element.centerY ,Element.centerX] content

View file

@ -34,12 +34,13 @@ init flags url key =
model =
{ key = key
, url = url
, page = page
, page = Body.handleRoute url.path
, header = header
}
in
(model, Cmd.none)
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
@ -51,7 +52,11 @@ update msg model =
UrlChanged url ->
( {model | url = url}, Cmd.none )
UrlRequest (Browser.Internal url) ->
( model, Browser.Navigation.pushUrl model.key (Url.toString url) )
let
-- newModel = Body.handleRoute url.path
newModel = {model | page = Body.handleRoute url.path}
in
( newModel, Browser.Navigation.pushUrl model.key (Url.toString url) )
_ -> (model, Cmd.none)
subscriptions : Model -> Sub Msg

View file

@ -0,0 +1,16 @@
module Page.NotFound exposing (Model, Msg, view, init, update)
import Element exposing (Element)
type alias Model = {}
type alias Msg = {}
init : () -> Model
init flags = {}
update : Msg -> Model -> (Model, Cmd Msg)
update msg model = (model, Cmd.none)
view : Model -> Element Msg
view model =
Element.el []
<| Element.text "I'm sorry, it seems we don't have that page!"

13
frontend/src/Ports.elm Normal file
View file

@ -0,0 +1,13 @@
port module Ports exposing (socket)
import Websockets
port webSocketCommand : Websockets.CommandPort msg
port webSocketEvent : Websockets.EventPort msg
socket : Websockets.Methods msg
socket =
Websockets.withPorts
{ command = webSocketCommand
, event = webSocketEvent
}

View file

@ -0,0 +1,60 @@
function initSockets(app) {
var sockets = new Map();
app.ports.webSocketCommand.subscribe(function (command) {
switch (command.type) {
case "open": {
var name_1 = command.name, url = command.url, meta_1 = command.meta;
var oldSocket = sockets.get(name_1);
if (oldSocket) {
oldSocket.ws.close(-1, "New socket opened with the same name");
sockets.delete(name_1);
}
var socket = {
ws: new WebSocket(url),
name: name_1,
meta: meta_1,
};
sockets.set(name_1, socket);
socket.ws.addEventListener("open", function (ev) {
app.ports.webSocketEvent.send({ type: "opened", name: name_1, meta: meta_1 });
});
socket.ws.addEventListener("message", function (_a) {
var data = _a.data;
app.ports.webSocketEvent.send({ type: "message", name: name_1, meta: meta_1, data: data });
});
socket.ws.addEventListener("close", function (_a) {
var reason = _a.reason;
app.ports.webSocketEvent.send({ type: "closed", name: name_1, meta: meta_1, reason: reason });
sockets.delete(name_1);
});
socket.ws.addEventListener("error", function (ev) {
app.ports.webSocketEvent.send({
type: "error",
name: name_1,
meta: meta_1,
error: null,
});
sockets.delete(name_1);
});
break;
}
case "send": {
var name_2 = command.name, data = command.data;
var socket = sockets.get(name_2);
if (socket) {
socket.ws.send(typeof data === "object" ? JSON.stringify(data) : data);
}
break;
}
case "close": {
var name_3 = command.name;
var socket = sockets.get(name_3);
if (socket) {
socket.ws.close();
sockets.delete(name_3);
}
break;
}
}
});
}