gtting closer, now must support ports
This commit is contained in:
parent
38bfd3bf18
commit
0412bc04c8
14
.gitignore
vendored
14
.gitignore
vendored
|
@ -1,6 +1,8 @@
|
||||||
elm-stuff/
|
frontend/elm-stuff/
|
||||||
elm.js
|
frontend/elm.js
|
||||||
elm.min.js
|
frontend/elm.min.js
|
||||||
node_modules/
|
frontend/node_modules/
|
||||||
package-lock.json
|
frontend/package-lock.json
|
||||||
package.json
|
frontend/package.json
|
||||||
|
backend/target
|
||||||
|
public/*
|
||||||
|
|
38
Makefile
38
Makefile
|
@ -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
|
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
|
|
||||||
|
|
|
@ -18,7 +18,8 @@ Now open `http://localhost:8000` in your browser.
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
- [x] Add Makefile
|
- [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`
|
- [ ] use actix backend that maps most root requests to serve `actix_file::Files`
|
||||||
- [ ] Submit to slack for feedback...
|
- [ ] Submit to slack for feedback...
|
||||||
- [ ] Refactor into router page
|
- [ ] Refactor into router page
|
||||||
|
|
1846
backend/Cargo.lock
generated
Normal file
1846
backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
15
backend/Cargo.toml
Normal file
15
backend/Cargo.toml
Normal 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
82
backend/src/main.rs
Normal 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
7
frontend/Makefile
Normal 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
|
|
@ -7,10 +7,12 @@
|
||||||
<body>
|
<body>
|
||||||
<div id="elm"></div>
|
<div id="elm"></div>
|
||||||
<script src="elm.min.js"></script>
|
<script src="elm.min.js"></script>
|
||||||
|
<script src="ports.websocket.js"></script>
|
||||||
<script>
|
<script>
|
||||||
var app = Elm.Main.init({
|
var app = Elm.Main.init({
|
||||||
node: document.getElementById('elm')
|
node: document.getElementById('elm')
|
||||||
});
|
});
|
||||||
|
initSockets(app);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -1,4 +1,4 @@
|
||||||
module Body exposing (Msg(..), Model(..), init, update, view)
|
module Body exposing (Msg(..), Model(..), init, update, view, handleRoute)
|
||||||
|
|
||||||
import Element
|
import Element
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import Page.Contact
|
||||||
import Page.Landing
|
import Page.Landing
|
||||||
import Page.Products
|
import Page.Products
|
||||||
import Page.Resources
|
import Page.Resources
|
||||||
|
import Page.NotFound
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= MsgLanding Page.Landing.Msg
|
= MsgLanding Page.Landing.Msg
|
||||||
|
@ -14,6 +15,7 @@ type Msg
|
||||||
| MsgResources Page.Resources.Msg
|
| MsgResources Page.Resources.Msg
|
||||||
| MsgAbout Page.About.Msg
|
| MsgAbout Page.About.Msg
|
||||||
| MsgContact Page.Contact.Msg
|
| MsgContact Page.Contact.Msg
|
||||||
|
| MsgNotFound Page.NotFound.Msg
|
||||||
|
|
||||||
type Model
|
type Model
|
||||||
= ModelLanding Page.Landing.Model
|
= ModelLanding Page.Landing.Model
|
||||||
|
@ -21,10 +23,25 @@ type Model
|
||||||
| ModelResources Page.Resources.Model
|
| ModelResources Page.Resources.Model
|
||||||
| ModelAbout Page.About.Model
|
| ModelAbout Page.About.Model
|
||||||
| ModelContact Page.Contact.Model
|
| ModelContact Page.Contact.Model
|
||||||
|
| ModelNotFound Page.NotFound.Model
|
||||||
|
|
||||||
init : () -> Model
|
init : () -> Model
|
||||||
init flags = ModelLanding (Page.Landing.init flags)
|
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 : Msg -> Model -> (Model, Cmd Msg)
|
||||||
update bodyMsg bodyModel =
|
update bodyMsg bodyModel =
|
||||||
let
|
let
|
||||||
|
@ -62,5 +79,6 @@ view model =
|
||||||
ModelResources m -> Page.Resources.view m |> Element.map MsgResources
|
ModelResources m -> Page.Resources.view m |> Element.map MsgResources
|
||||||
ModelAbout m -> Page.About.view m |> Element.map MsgAbout
|
ModelAbout m -> Page.About.view m |> Element.map MsgAbout
|
||||||
ModelContact m -> Page.Contact.view m |> Element.map MsgContact
|
ModelContact m -> Page.Contact.view m |> Element.map MsgContact
|
||||||
|
ModelNotFound m -> Page.NotFound.view m |> Element.map MsgNotFound
|
||||||
in
|
in
|
||||||
Element.el [Element.centerY ,Element.centerX] content
|
Element.el [Element.centerY ,Element.centerX] content
|
|
@ -34,12 +34,13 @@ init flags url key =
|
||||||
model =
|
model =
|
||||||
{ key = key
|
{ key = key
|
||||||
, url = url
|
, url = url
|
||||||
, page = page
|
, page = Body.handleRoute url.path
|
||||||
, header = header
|
, header = header
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
(model, Cmd.none)
|
(model, Cmd.none)
|
||||||
|
|
||||||
|
|
||||||
update : Msg -> Model -> (Model, Cmd Msg)
|
update : Msg -> Model -> (Model, Cmd Msg)
|
||||||
update msg model =
|
update msg model =
|
||||||
case msg of
|
case msg of
|
||||||
|
@ -51,7 +52,11 @@ update msg model =
|
||||||
UrlChanged url ->
|
UrlChanged url ->
|
||||||
( {model | url = url}, Cmd.none )
|
( {model | url = url}, Cmd.none )
|
||||||
UrlRequest (Browser.Internal url) ->
|
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)
|
_ -> (model, Cmd.none)
|
||||||
|
|
||||||
subscriptions : Model -> Sub Msg
|
subscriptions : Model -> Sub Msg
|
16
frontend/src/Page/NotFound.elm
Normal file
16
frontend/src/Page/NotFound.elm
Normal 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
13
frontend/src/Ports.elm
Normal 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
|
||||||
|
}
|
60
frontend/src/ports.websocket.js
Normal file
60
frontend/src/ports.websocket.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue