├── .gitbook └── assets │ └── readme-image.jpg ├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── SUMMARY.md ├── assets └── readme-image.jpg ├── back └── back.go ├── backend-entry.go ├── bin └── placeholer.txt ├── front ├── front.go └── natsclient.go ├── frontent-wasm.go ├── go.mod ├── go.sum ├── mage.go ├── magefiles └── magefile.go ├── main.go ├── page-1.md └── web ├── index.css ├── logo-192.png ├── logo-512.png ├── logo.svg └── robots.txt /.gitbook/assets/readme-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oderwat/go-nats-app/e5eafd92c43db87840e284877e58578479b4bbd1/.gitbook/assets/readme-image.jpg -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: mage 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | # Runs on pushes targeting the default branch 9 | push: 10 | branches: 11 | - 'master' 12 | tags: 13 | - 'v*' 14 | # Runs on PRS targeting any branch 15 | pull_request: 16 | 17 | # Allows you to run this workflow manually from the Actions tab 18 | workflow_dispatch: 19 | 20 | env: 21 | BASE_GWD: ${{ github.workspace }} 22 | BASE_GWD_BIN: ${{ github.workspace }}/.bin 23 | 24 | jobs: 25 | 26 | # 27 | # Tests for all platforms. Runs a matrix build on Windows, Linux and Mac, 28 | # with the list of expected supported Go versions (current, previous). 29 | # 30 | 31 | build: 32 | name: Build and test 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | os: [ ubuntu-latest, macos-latest, windows-latest ] 37 | go-version: [ 1.22.0 ] 38 | target: [ "build", ] 39 | runs-on: ${{ matrix.os }} 40 | steps: 41 | 42 | # Install go: https://github.com/actions/setup-go/releases/tag/v5.0.0 43 | - name: Install go 44 | uses: actions/setup-go@v5.0.0 45 | with: 46 | go-version: ${{ matrix.go-version }} 47 | id: go 48 | 49 | # Set go bin 50 | #- name: Setup Go binary path 51 | # shell: bash 52 | # run: | 53 | # echo "GOPATH=${{ github.workspace }}" >> $GITHUB_ENV 54 | # echo "${{ github.workspace }}/bin" >> $GITHUB_PATH 55 | 56 | # Fix git line endings 57 | - name: Git line endings 58 | shell: bash 59 | run: | 60 | git config --global core.autocrlf false 61 | git config --global core.eol lf 62 | 63 | # Checkout code: https://github.com/actions/checkout/releases/tag/v4.1.1 64 | - name: Check out main code into the Go module directory 65 | uses: actions/checkout@v4.1.1 66 | with: 67 | ref: ${{ github.event.pull_request.head.sha }} 68 | path: ${{ github.workspace }}/go/src/github.com/${{ github.repository }} 69 | 70 | # Check workspace 71 | - name: check workspace ${{ matrix.target }} 72 | shell: bash 73 | run: | 74 | echo ${{ github.workspace }} 75 | echo $GITHUB_WORKSPACE 76 | 77 | # Build using make 78 | - name: make ${{ matrix.target }} 79 | shell: bash 80 | run: | 81 | go install github.com/magefile/mage@latest 82 | mage $target 83 | working-directory: ${{ github.workspace }}/go/src/github.com/${{ github.repository }} 84 | env: 85 | # CONFIG_PASSWORD: secretzSoSecureYouWontBelieveIt999 86 | target: ${{ matrix.target }} 87 | # CONFIG_PASSWORD: ${{ secrets.CONFIG_PASSWORD }} 88 | 89 | ## Test 90 | - name: Test with Go ( fake for noe ) 91 | shell: bash 92 | run: touch test-${{ matrix.os }}.json 93 | 94 | ## Upload other artifacts 95 | - name: upload other 96 | uses: actions/upload-artifact@v4 97 | with: 98 | name: other-${{ matrix.os }} 99 | path: ${{ env.BASE_GWD_BIN }} 100 | 101 | ## Upload bin artifacts 102 | - name: upload bins 103 | uses: actions/upload-artifact@v4 104 | with: 105 | name: bin-${{ matrix.os }} 106 | path: ${{ env.BASE_GWD_BIN }} 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/go-nats-app 2 | bin/go-nats-app.exe 3 | web/app.wasm 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hans Raaf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go-Nats-App 2 | 3 | This is a demo of a [Go-App](https://github.com/maxence-charriere/go-app) based PWA that uses [NATS](https://nats.io/) for the communication between frontend and backend. 4 | 5 | ### How to run it? 6 | 7 | Clone the repository, cd into it and run `go run mage.go run` (or `mage run` if you installed mage already). 8 | 9 | Then open http://127.0.0.1:8500 once or multiple times in your browser and chat with yourself. 10 | 11 | ![preview](.gitbook/assets/readme-image.jpg) 12 | 13 | ### Features of this demo 14 | 15 | * There is no java-script! Everything is Go code. 16 | * Frontend & Backend uses [Go-App](https://github.com/maxence-charriere/go-app). 17 | * Build tooling made with [Mage](https://magefile.org/). 18 | * The frontend is a PWA and can be installed on your phone or desktop. It runs in the browser as WASM code with a service-worker. 19 | * We are using an embedded NATS-Server in the backend to offer three services: 20 | * [Govatar](https://github.com/o1egl/govatar) image (JPEG / random / female). 21 | * Chat broker (3 lines of code + error handling = 10 lines). 22 | * (New) Each PWA has an echo "req" service under the subject "echo.". Like "echo.late-meadow" in our example picture. An example command-line is shown in the site. 23 | * We use the original nats.go client in the frontend. 24 | * Go-App code is smaller when building WASM and normal code separately. 25 | * We compress the WASM code on the fly. 26 | * You can run the embedded nats-server as leaf-node of a cluster (that is what we do in another proof of concept). 27 | 28 | ### What does not work? 29 | 30 | * This will not work with TLS (`wss://`) with before the next release of [Nats.go](https://github.com/nats-io/nats.go) (after 1.20.0). If you need TLS for the web socket you can use `go get https://github.com/nats-io/nats.go@main` which should work for that. The code in the demo does contain everything needed though (implementation of `SkipTLSHandshake()` on the `CustomDialer`). 31 | * The IPs and ports are hard-coded and as everything binds to localhost it will not work behind reverse proxies or through tunnels like [sish](https://github.com/antoniomika/sish) or ngrok. 32 | * A lot more. It is just a proof of concept / demo. 33 | * [#second-tab](page-1.md#second-tab "mention") 34 | 35 | ### Disclaimer 36 | 37 | * We do not care what you do with the code as long as you do not bug us or destroy humanity with it :) 38 | * This uses the MIT License and is as it is what it is. 39 | * Parts of the code were quickly grabbed from other internal prototypes or written without much though. It is most likely full of bugs :) 40 | 41 | ### Greetings 42 | 43 | * to the NATS Team 44 | * to the Go-App developer 45 | * all the contributors 46 | * and to everybody else :) 47 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [Go-Nats-App](README.md) 4 | * [Page 1](page-1.md) 5 | -------------------------------------------------------------------------------- /assets/readme-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oderwat/go-nats-app/e5eafd92c43db87840e284877e58578479b4bbd1/assets/readme-image.jpg -------------------------------------------------------------------------------- /back/back.go: -------------------------------------------------------------------------------- 1 | package back 2 | 3 | import ( 4 | "bytes" 5 | "compress/flate" 6 | "context" 7 | "encoding/base64" 8 | "fmt" 9 | "github.com/go-chi/chi/v5" 10 | "github.com/go-chi/chi/v5/middleware" 11 | "github.com/maxence-charriere/go-app/v10/pkg/app" 12 | "github.com/nats-io/nats-server/v2/server" 13 | "github.com/nats-io/nats.go" 14 | "github.com/o1egl/govatar" 15 | "golang.org/x/net/netutil" 16 | "image" 17 | "image/jpeg" 18 | "log" 19 | "net" 20 | "net/http" 21 | "net/url" 22 | "os" 23 | "os/signal" 24 | "time" 25 | ) 26 | 27 | // AppServer implements the Backend service 28 | type AppServer struct { 29 | mux *chi.Mux 30 | } 31 | 32 | func (srv *AppServer) Mux() *chi.Mux { 33 | return srv.mux 34 | } 35 | 36 | type Muxer interface { 37 | Mux() *chi.Mux 38 | } 39 | 40 | func Create(ah *app.Handler) { 41 | var srv AppServer 42 | 43 | opts := &server.Options{ 44 | ServerName: "Your friendly backend", 45 | Host: "127.0.0.1", 46 | Port: 8501, 47 | NoLog: true, 48 | NoSigs: true, 49 | MaxControlLine: 4096, 50 | Accounts: []*server.Account{ 51 | { 52 | Name: "cluster", 53 | }, 54 | }, 55 | Websocket: server.WebsocketOpts{ 56 | Host: "127.0.0.1", 57 | Port: 8502, 58 | NoTLS: true, 59 | SameOrigin: false, 60 | Compression: false, 61 | HandshakeTimeout: 5 * time.Second, 62 | }, 63 | DisableShortFirstPing: true, 64 | } 65 | // Initialize new server with options 66 | ns, err := server.NewServer(opts) 67 | if err != nil { 68 | panic(err) 69 | } 70 | 71 | // Start the server via goroutine 72 | go ns.Start() 73 | 74 | // Wait for server to be ready for connections 75 | if !ns.ReadyForConnections(2 * time.Second) { 76 | panic("could not start the server (is another instance running on the same port?)") 77 | } 78 | 79 | fmt.Printf("Nats AppServer Name: %q\n", ns.Name()) 80 | fmt.Printf("Nats AppServer Addr: %q\n", ns.Addr()) 81 | 82 | // Connect to server 83 | nc, err := nats.Connect(ns.ClientURL()) 84 | if err != nil { 85 | panic(err) 86 | } 87 | 88 | // This is the chat broker 89 | _, err = nc.Subscribe("chat.say", func(msg *nats.Msg) { 90 | fmt.Printf("Got: %q\n", msg.Data) 91 | err = nc.Publish("chat.room", msg.Data) 92 | if err != nil { 93 | panic(err) 94 | } 95 | }) 96 | if err != nil { 97 | panic(err) 98 | } 99 | 100 | _, err = nc.Subscribe("govatar.female", func(msg *nats.Msg) { 101 | if err != nil { 102 | panic(err) 103 | } 104 | var img image.Image 105 | // always female and random 106 | img, err = govatar.Generate(govatar.FEMALE) 107 | if err != nil { 108 | panic(err) 109 | } 110 | var buf bytes.Buffer 111 | err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: 80}) 112 | if err != nil { 113 | panic(err) 114 | } 115 | err = msg.Respond([]byte("data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()))) 116 | if err != nil { 117 | log.Printf("Respond error: %s", err) 118 | } 119 | //noErr(err) 120 | }) 121 | if err != nil { 122 | panic(err) 123 | } 124 | 125 | r := chi.NewRouter() 126 | srv.mux = r 127 | 128 | r.Use(middleware.CleanPath) 129 | r.Use(middleware.Logger) 130 | compressor := middleware.NewCompressor(flate.DefaultCompression, 131 | "application/wasm", "text/css", "image/svg+xml" /*, "application/javascript"*/) 132 | r.Use(compressor.Handler) 133 | r.Use(middleware.Recoverer) 134 | 135 | listener, err := net.Listen("tcp", "127.0.0.1:8500") 136 | if err != nil { 137 | panic(err) 138 | } 139 | 140 | mainServer := &http.Server{ 141 | Addr: listener.Addr().String(), 142 | } 143 | 144 | // try to build the final browser location 145 | var hostURL = url.URL{ 146 | Scheme: "http", 147 | Host: mainServer.Addr, 148 | } 149 | 150 | log.Printf("Serving at %s\n", hostURL.String()) 151 | 152 | // This is the frontend handler (go-app) and will "pick up" any route 153 | // which is not handled by the backend. It then will load the frontend 154 | // and navigate it to this router. 155 | srv.mux.Handle("/*", ah) 156 | 157 | // registering our stack as the handler for the http server 158 | mainServer.Handler = srv.Mux() 159 | 160 | // Creating some graceful shutdown system 161 | shutdown := make(chan struct{}) 162 | go func() { 163 | listener = netutil.LimitListener(listener, 100) 164 | defer func() { _ = listener.Close() }() 165 | 166 | err := mainServer.Serve(listener) 167 | if err != nil { 168 | if err == http.ErrServerClosed { 169 | fmt.Println("AppServer was stopped!") 170 | } else { 171 | log.Fatal(err) 172 | } 173 | } 174 | // when run by "mage watch/run" it will break mage 175 | // before actually exiting the server, which just looks 176 | // strange but is okay after all 177 | //time.Sleep(2 * time.Second) // just for testing 178 | close(shutdown) 179 | }() 180 | 181 | // Setting up signal capturing 182 | stop := make(chan os.Signal, 1) 183 | signal.Notify(stop, os.Interrupt) 184 | // Waiting for SIGINT (kill -SIGINT or Ctrl+c ) 185 | <-stop 186 | shutdownServer(mainServer, shutdown) 187 | fmt.Println("Program ended") 188 | } 189 | 190 | // shutdownServer stops the server gracefully 191 | func shutdownServer(server *http.Server, shutdown chan struct{}) { 192 | fmt.Println("\nStopping AppServer gracefully") 193 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 194 | defer cancel() 195 | fmt.Println("Send shutdown!") 196 | if err := server.Shutdown(ctx); err != nil { 197 | log.Fatal(err) 198 | } 199 | fmt.Println("Waiting for server to shutdown!") 200 | <-shutdown 201 | fmt.Println("AppServer is shutdown!") 202 | } 203 | -------------------------------------------------------------------------------- /backend-entry.go: -------------------------------------------------------------------------------- 1 | // Our empty version of the httpServer for usage with the wasm target 2 | // this way we will not include any of the related code 3 | //go:build !wasm 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/maxence-charriere/go-app/v10/pkg/app" 9 | "go-nats-app/back" 10 | ) 11 | 12 | type empty struct{ app.Compo } 13 | 14 | // Render implements the "source" code our App is showing before WASM is loaded and runs. 15 | // Actually the WASM loader takes over if javascript is available. So this is mainly 16 | // what robots and scrappers get to see. 17 | func (c *empty) Render() app.UI { 18 | return app.Div().Text("The application is loading...") 19 | } 20 | 21 | func Front() { 22 | // This will skip most of the frontend code in the server binary 23 | // but keeps the routing intact. It is important that all possible 24 | // routes that are reachable in the frontend are here. Any route that 25 | // is not covered results in a (raw) 404 error from the server. 26 | 27 | // For the Skelly demo app, we handle all routes in the frontend itself 28 | app.RouteWithRegexp("/.*", app.NewZeroComponentFactory(&empty{})) 29 | } 30 | 31 | func Back(ah *app.Handler) { 32 | back.Create(ah) 33 | } 34 | -------------------------------------------------------------------------------- /bin/placeholer.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /front/front.go: -------------------------------------------------------------------------------- 1 | package front 2 | 3 | import ( 4 | "github.com/goombaio/namegenerator" 5 | "github.com/maxence-charriere/go-app/v10/pkg/app" 6 | "github.com/nats-io/nats.go" 7 | "nhooyr.io/websocket" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | type appControl struct { 13 | app.Compo 14 | whoami string // keeps our name 15 | avatar string // data base64 jpeg image 16 | nc *nats.Conn // is the nats server connection 17 | input string // the current input line 18 | messages []string // the last 10 messages we received 19 | echoCount int // we count how many echos we sent on the echo service 20 | } 21 | 22 | var _ app.Initializer = (*appControl)(nil) // Verify the implementation 23 | var _ app.Mounter = (*appControl)(nil) // Verify the implementation 24 | var _ app.AppUpdater = (*appControl)(nil) // Verify the implementation 25 | 26 | // OnInit is called before the component gets mounted 27 | // This is before Render was called for the first time 28 | func (uc *appControl) OnInit() { 29 | app.Log("OnInit") 30 | uc.whoami = namegenerator.NewNameGenerator(time.Now().UTC().UnixNano()).Generate() 31 | // a blank image 32 | uc.avatar = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" 33 | } 34 | 35 | // OnMount gets called when the component is mounted 36 | // This is after Render was called for the first time 37 | func (uc *appControl) OnMount(ctx app.Context) { 38 | app.Log("OnMount") 39 | go func() { 40 | var ncw WasmNatsConnectionWrapper 41 | app.Log("NatsConnect") 42 | var err error 43 | uc.nc, err = nats.Connect("localhost:8502", // our websocket port 44 | nats.Name("PWA-"+uc.whoami), 45 | nats.SetCustomDialer(ncw), 46 | ) 47 | if err != nil { 48 | app.Logf("Native Go Connect did fail: %#v", err) 49 | return 50 | } 51 | app.Log("Nats connected through websocket and netConn wrapper!") 52 | // now we add a subscription to the chat.room 53 | // Subscribe to the subject 54 | _, err = uc.nc.Subscribe("chat.room", func(msg *nats.Msg) { 55 | // Print message data 56 | ctx.Dispatch(func(ctx app.Context) { 57 | if len(uc.messages) < 10 { 58 | uc.messages = append(uc.messages, string(msg.Data)) 59 | } else { 60 | uc.messages = append(uc.messages[1:], string(msg.Data)) 61 | } 62 | }) 63 | }) 64 | 65 | // grab an avatar 66 | msg, err := uc.nc.Request("govatar.female", []byte(""), 200*time.Millisecond) 67 | if err != nil { 68 | app.Logf("govatar request error %s", err) 69 | } else { 70 | ctx.Dispatch(func(ctx app.Context) { 71 | // set it 72 | uc.avatar = string(msg.Data) 73 | }) 74 | } 75 | 76 | // tell them we are here 77 | err = uc.nc.Publish("chat.say", []byte(uc.whoami+" entered the room")) 78 | if err != nil { 79 | app.Logf("Publish entry message error %s", err) 80 | } 81 | 82 | // create an echo service in this browser :) 83 | _, err = uc.nc.Subscribe("echo."+uc.whoami, func(msg *nats.Msg) { 84 | _ = msg.Respond(msg.Data) 85 | ctx.Dispatch(func(ctx app.Context) { 86 | uc.echoCount++ 87 | }) 88 | }) 89 | ctx.Update() 90 | }() 91 | 92 | app.Window().GetElementByID("inp").Call("focus") 93 | 94 | // Notice: We do not care about OnDismount which would be needed 95 | // when working with a more complex app. 96 | } 97 | 98 | func (uc *appControl) OnAppUpdate(ctx app.Context) { 99 | // This will be called when the service worker gets updated 100 | // With this demo it checks for this about every 10 seconds 101 | // This would normally just a fallback for using NATS to tell 102 | // the frontend that there is a new version of the app runnning 103 | if ctx.AppUpdateAvailable() { 104 | // hard update immediately :) 105 | // You would not do that with a real app but ask the user or 106 | // give a countdown before it updates 107 | ctx.Reload() 108 | } 109 | } 110 | 111 | func (uc *appControl) Render() app.UI { 112 | return app.Div().Body( 113 | app.H1().Text("Go-Nats-App"), 114 | app.Div().Text(func() string { 115 | if uc.nc == nil { 116 | return "Not connected to the nats server" 117 | } else { 118 | return "Connected to: " + uc.nc.ConnectedServerName() 119 | } 120 | }()), 121 | app.Div().Body(app.Img().Src(uc.avatar).Width(250).Height(250)), 122 | app.H4().Text("Chat:"), 123 | app.Form().Body( 124 | app.Div().Body( 125 | app.Span().Text(uc.whoami+": "), 126 | app.Input().Value(uc.input).ID("inp").OnInput(uc.ValueTo(&uc.input)), 127 | ), 128 | ).OnSubmit(func(ctx app.Context, e app.Event) { 129 | e.PreventDefault() 130 | if uc.nc != nil { 131 | err := uc.nc.Publish("chat.say", []byte(uc.whoami+": "+uc.input)) 132 | if err != nil { 133 | app.Logf("Publish error %s", err) 134 | } 135 | uc.input = "" // clear the message entry 136 | } 137 | }), 138 | app.Range(uc.messages).Slice(func(i int) app.UI { 139 | return app.Div().Text(uc.messages[len(uc.messages)-1-i]) 140 | }), 141 | app.H4().Text("For an echo use:"), 142 | app.Pre().Text(`nats -s 127.0.0.1:8501 req echo.`+uc.whoami+` '{{ Random 10 100 }}'`), 143 | app.Div().Text("Echos sent: "+strconv.Itoa(uc.echoCount)), 144 | ) 145 | } 146 | 147 | func Create() { 148 | app.RouteWithRegexp("/.*", app.NewZeroComponentFactory(&appControl{})) 149 | // add a very simple update checker that checks for updates every 5 seconds 150 | // this lets us modify the code and restart the server more easily 151 | // For production this should be changed to a longer interval. 152 | intervalUpdater(time.Second * 5) 153 | } 154 | 155 | func intervalUpdater(delay time.Duration) { 156 | time.AfterFunc(delay, func() { 157 | app.Log("checking for update") 158 | app.TryUpdate() 159 | intervalUpdater(delay) 160 | }) 161 | } 162 | 163 | type WasmNatsConnectionWrapper struct { 164 | ws *websocket.Conn 165 | } 166 | -------------------------------------------------------------------------------- /front/natsclient.go: -------------------------------------------------------------------------------- 1 | package front 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/maxence-charriere/go-app/v10/pkg/app" 7 | "github.com/nats-io/nats.go" 8 | "net" 9 | "nhooyr.io/websocket" 10 | "time" 11 | ) 12 | 13 | var _ nats.CustomDialer = (*WasmNatsConnectionWrapper)(nil) // Verify the implementation 14 | 15 | func (cw WasmNatsConnectionWrapper) Dial(network, address string) (net.Conn, error) { 16 | // we actually do not care about the adress given here 17 | app.Logf("Got Request for Network: %q / Address: %q", network, address) 18 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 19 | defer cancel() 20 | 21 | app.Logf("Dialing Address: ws://%s", address) 22 | c, _, err := websocket.Dial(ctx, "ws://"+address, nil) 23 | if err != nil { 24 | app.Logf("websocket.Dial failed %#v", err) 25 | return nil, fmt.Errorf("websocket.Dial failed %w", err) 26 | } 27 | cw.ws = c 28 | nconn := websocket.NetConn(context.Background(), c, websocket.MessageBinary) 29 | return nconn, nil 30 | } 31 | 32 | func (cw WasmNatsConnectionWrapper) SkipTLSHandshake() bool { 33 | return true 34 | } 35 | -------------------------------------------------------------------------------- /frontent-wasm.go: -------------------------------------------------------------------------------- 1 | // Our empty version of the httpServer for usage with the wasm target 2 | // this way we will not include any of the related code 3 | //go:build wasm 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/maxence-charriere/go-app/v10/pkg/app" 9 | "go-nats-app/front" 10 | ) 11 | 12 | // This is a dummy for the AppServer (back end) code so it does 13 | // not get included in the WASM code 14 | func Back(_ *app.Handler) { 15 | } 16 | 17 | // This is the actual frontend. It 18 | func Front() { 19 | front.Create() 20 | } 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go-nats-app 2 | 3 | go 1.21 4 | 5 | toolchain go1.22.3 6 | 7 | require ( 8 | github.com/go-chi/chi/v5 v5.0.12 9 | github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e 10 | github.com/magefile/mage v1.15.0 11 | github.com/maxence-charriere/go-app/v10 v10.0.5-0.20240531023508-d310db9298a0 12 | github.com/nats-io/nats-server/v2 v2.10.16 13 | github.com/nats-io/nats.go v1.35.0 14 | github.com/o1egl/govatar v0.4.1 15 | golang.org/x/net v0.25.0 16 | nhooyr.io/websocket v1.8.11 17 | ) 18 | 19 | require ( 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/klauspost/compress v1.17.8 // indirect 22 | github.com/minio/highwayhash v1.0.2 // indirect 23 | github.com/nats-io/jwt/v2 v2.5.7 // indirect 24 | github.com/nats-io/nkeys v0.4.7 // indirect 25 | github.com/nats-io/nuid v1.0.1 // indirect 26 | golang.org/x/crypto v0.23.0 // indirect 27 | golang.org/x/sys v0.20.0 // indirect 28 | golang.org/x/time v0.5.0 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= 6 | github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 7 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 8 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 9 | github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40= 10 | github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4= 11 | github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= 12 | github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 13 | github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= 14 | github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= 15 | github.com/maxence-charriere/go-app/v10 v10.0.5-0.20240531023508-d310db9298a0 h1:9xzoM3ISp0TirBUURSMbj3k5elUvB8ObgEHsq8RJqBs= 16 | github.com/maxence-charriere/go-app/v10 v10.0.5-0.20240531023508-d310db9298a0/go.mod h1:+BMrOhRAAXKUE7WFFC5Xp+Y+8atKUVvGJNePO4iMydM= 17 | github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= 18 | github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= 19 | github.com/nats-io/jwt/v2 v2.5.7 h1:j5lH1fUXCnJnY8SsQeB/a/z9Azgu2bYIDvtPVNdxe2c= 20 | github.com/nats-io/jwt/v2 v2.5.7/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A= 21 | github.com/nats-io/nats-server/v2 v2.10.16 h1:2jXaiydp5oB/nAx/Ytf9fdCi9QN6ItIc9eehX8kwVV0= 22 | github.com/nats-io/nats-server/v2 v2.10.16/go.mod h1:Pksi38H2+6xLe1vQx0/EA4bzetM0NqyIHcIbmgXSkIU= 23 | github.com/nats-io/nats.go v1.35.0 h1:XFNqNM7v5B+MQMKqVGAyHwYhyKb48jrenXNxIU20ULk= 24 | github.com/nats-io/nats.go v1.35.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= 25 | github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= 26 | github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= 27 | github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= 28 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 29 | github.com/o1egl/govatar v0.4.1 h1:RRzAxm52WpZMSEoWgAXrTcXWKhIUPpgpI54KP+UI0Ew= 30 | github.com/o1egl/govatar v0.4.1/go.mod h1:cSBJjpgYiKmQ8E+C4zNBcsbuDwy9UH4HS8BwE4m6JmQ= 31 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 34 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 35 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 36 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 37 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 38 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 39 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 40 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 41 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 42 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 43 | golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 44 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 45 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 46 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 47 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 48 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 49 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 50 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 51 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 52 | nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0= 53 | nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= 54 | -------------------------------------------------------------------------------- /mage.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | "github.com/magefile/mage/mage" 8 | ) 9 | 10 | func main() { os.Exit(mage.Main()) } 11 | -------------------------------------------------------------------------------- /magefiles/magefile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/magefile/mage/mg" 6 | "github.com/magefile/mage/sh" 7 | "github.com/magefile/mage/target" 8 | "runtime" 9 | ) 10 | 11 | var appExecutable = "bin/go-nats-app" 12 | var appDir = "." 13 | 14 | const goCompiler = "go" 15 | 16 | var appGlobs = []string{ 17 | "magefiles/magefile.go", 18 | "*.go", 19 | "*/*.go", 20 | } 21 | 22 | //goland:noinspection GoBoolExpressions 23 | func init() { 24 | if runtime.GOOS == "windows" { 25 | appExecutable += ".exe" 26 | } 27 | } 28 | 29 | func buildApp() error { 30 | changes, err := target.Glob(appExecutable, appGlobs...) 31 | if err != nil { 32 | return err 33 | } 34 | changes = true 35 | if !changes { 36 | return nil 37 | } 38 | fmt.Println("> Building App...") 39 | return sh.RunV(goCompiler, "build", "-o", appExecutable, appDir) 40 | } 41 | 42 | func BuildWasm() error { 43 | changes, err := target.Glob("web/app.wasm", appGlobs...) 44 | if err != nil { 45 | return err 46 | } 47 | if !changes { 48 | return nil 49 | } 50 | fmt.Println("> Building WASM...") 51 | return sh.RunWithV(map[string]string{"GOOS": "js", "GOARCH": "wasm"}, goCompiler, "build", "-o", 52 | "web/app.wasm", appDir) 53 | } 54 | 55 | func Build() error { 56 | mg.Deps(BuildWasm) 57 | return buildApp() 58 | } 59 | 60 | func Run() error { 61 | mg.Deps(Build) 62 | return sh.RunV(appExecutable) 63 | } 64 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/maxence-charriere/go-app/v10/pkg/app" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | func main() { 10 | rand.Seed(time.Now().UnixNano()) // needs to be better for a real app 11 | 12 | // check the system for int64 fitting into int (so we can convert int64 to int safely) 13 | if uint64(^uint(0)) < ^uint64(0) { 14 | panic("int does not fit int64") 15 | } 16 | 17 | // This is behind a WASM build tag so the business logic of the UI does not increase 18 | // the size of our server executable. 19 | // It also has an empty component with its own Render in "server.go" 20 | Front() 21 | 22 | // this concludes the part which goes into the front-end 23 | app.RunWhenOnBrowser() 24 | 25 | // I declare this here because of the "logic" to find it in 26 | // the backend code still is a mindbender for me :) 27 | ah := &app.Handler{ 28 | Name: "Go-Nats-App", 29 | Lang: "de", 30 | Author: "Hans Raaf - METATEXX GmbH", 31 | Title: "Go Nats App", 32 | Description: "NATS in a PWA", 33 | Image: "/web/logo-512.png", 34 | LoadingLabel: "Loading...", 35 | Icon: app.Icon{ 36 | Default: "/web/logo-192.png", 37 | Large: "/web/logo-512.png", 38 | Maskable: "/web/logo-192.png", 39 | }, 40 | Styles: []string{ 41 | "/web/index.css", 42 | }, 43 | CacheableResources: []string{ 44 | "/web/logo.svg", 45 | }, 46 | } 47 | 48 | // this will depend on the target (wasm or not wasm) and 49 | // it starts the servers if it is not the wasm target. 50 | Back(ah) 51 | } 52 | -------------------------------------------------------------------------------- /page-1.md: -------------------------------------------------------------------------------- 1 | # Page 1 2 | 3 | {% tabs %} 4 | {% tab title="First Tab" %} 5 | Test 1 6 | {% endtab %} 7 | 8 | {% tab title="Second Tab" %} 9 | Test 2 10 | {% endtab %} 11 | {% endtabs %} 12 | -------------------------------------------------------------------------------- /web/index.css: -------------------------------------------------------------------------------- 1 | /* Basis CSS */ 2 | 3 | body { 4 | font-family: sans-serif; 5 | } 6 | 7 | div { 8 | margin-bottom: 1rem; 9 | } -------------------------------------------------------------------------------- /web/logo-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oderwat/go-nats-app/e5eafd92c43db87840e284877e58578479b4bbd1/web/logo-192.png -------------------------------------------------------------------------------- /web/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oderwat/go-nats-app/e5eafd92c43db87840e284877e58578479b4bbd1/web/logo-512.png -------------------------------------------------------------------------------- /web/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | --------------------------------------------------------------------------------