Skip to content

Commit d23852a

Browse files
committed
feat: (WIP) SSC Server Side Computed
replacing SSR Server Side Rendering
1 parent d6e2d7f commit d23852a

File tree

16 files changed

+314
-7
lines changed

16 files changed

+314
-7
lines changed

cmd/rfw/build/build.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,28 @@ func Build() error {
2525
return fmt.Errorf("failed to copy wasm_exec.js: %w", err)
2626
}
2727

28-
cmd := exec.Command("go", "build", "-o", "main.wasm", "main.go")
28+
cmd := exec.Command("go", "build", "-o", "app.wasm", "main.go")
2929
cmd.Env = append(os.Environ(), "GOARCH=wasm", "GOOS=js")
3030
output, err := cmd.CombinedOutput()
3131
if err != nil {
3232
return fmt.Errorf("failed to build project: %s: %w", output, err)
3333
}
3434

3535
var manifest struct {
36+
Build struct {
37+
Type string `json:"type"`
38+
} `json:"build"`
3639
Plugins map[string]json.RawMessage `json:"plugins"`
3740
}
3841
if data, err := os.ReadFile("rfw.json"); err == nil {
3942
_ = json.Unmarshal(data, &manifest)
4043
}
44+
if strings.EqualFold(manifest.Build.Type, "ssc") {
45+
hostCmd := exec.Command("go", "build", "-o", "host/host", "./host")
46+
if hostOutput, err := hostCmd.CombinedOutput(); err != nil {
47+
return fmt.Errorf("failed to build host components: %s: %w", hostOutput, err)
48+
}
49+
}
4150
if err := plugins.Configure(manifest.Plugins); err != nil {
4251
return fmt.Errorf("failed to run plugins: %w", err)
4352
}

cmd/rfw/server/server.go

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package server
22

33
import (
44
"bufio"
5+
"encoding/json"
56
"expvar"
67
"fmt"
78
"net/http"
89
"net/http/pprof"
910
"os"
11+
"os/exec"
1012
"os/signal"
1113
"path/filepath"
1214
"strings"
@@ -21,11 +23,13 @@ import (
2123
var rebuilds = expvar.NewInt("rebuilds")
2224

2325
type Server struct {
24-
Port string
25-
Host bool
26-
Debug bool
27-
stopCh chan os.Signal
28-
watcher *fsnotify.Watcher
26+
Port string
27+
Host bool
28+
Debug bool
29+
stopCh chan os.Signal
30+
watcher *fsnotify.Watcher
31+
hostCmd *exec.Cmd
32+
buildType string
2933
}
3034

3135
func NewServer(port string, host, debug bool) *Server {
@@ -43,6 +47,14 @@ func (s *Server) Start() error {
4347
return err
4448
}
4549

50+
// Detect build type from manifest to know if host components are enabled.
51+
s.buildType = readBuildType()
52+
if s.buildType == "ssc" {
53+
if err := s.startHost(); err != nil {
54+
return err
55+
}
56+
}
57+
4658
mux := http.NewServeMux()
4759

4860
fs := http.FileServer(http.Dir("."))
@@ -93,6 +105,7 @@ func (s *Server) Start() error {
93105

94106
<-s.stopCh
95107
utils.Info("Server stopped.")
108+
s.stopHost()
96109
return nil
97110
}
98111

@@ -171,6 +184,12 @@ func (s *Server) watchFiles() {
171184
if err := build.Build(); err != nil {
172185
utils.Fatal("Failed to rebuild project: ", err)
173186
}
187+
if s.buildType == "ssc" {
188+
s.stopHost()
189+
if err := s.startHost(); err != nil {
190+
utils.Fatal("Failed to restart host server: ", err)
191+
}
192+
}
174193
}
175194
case err, ok := <-s.watcher.Errors:
176195
if !ok {
@@ -179,7 +198,38 @@ func (s *Server) watchFiles() {
179198
utils.Info(fmt.Sprintf("Watcher error: %v", err))
180199
case <-s.stopCh:
181200
s.watcher.Close()
201+
s.stopHost()
182202
return
183203
}
184204
}
185205
}
206+
207+
// readBuildType reads the build type from rfw.json if present.
208+
func readBuildType() string {
209+
var manifest struct {
210+
Build struct {
211+
Type string `json:"type"`
212+
} `json:"build"`
213+
}
214+
data, err := os.ReadFile("rfw.json")
215+
if err != nil {
216+
return ""
217+
}
218+
_ = json.Unmarshal(data, &manifest)
219+
return strings.ToLower(manifest.Build.Type)
220+
}
221+
222+
func (s *Server) startHost() error {
223+
s.hostCmd = exec.Command("./host/host")
224+
s.hostCmd.Stdout = os.Stdout
225+
s.hostCmd.Stderr = os.Stderr
226+
return s.hostCmd.Start()
227+
}
228+
229+
func (s *Server) stopHost() {
230+
if s.hostCmd != nil && s.hostCmd.Process != nil {
231+
_ = s.hostCmd.Process.Kill()
232+
_, _ = s.hostCmd.Process.Wait()
233+
}
234+
s.hostCmd = nil
235+
}

docs/articles/guide/ssc.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Server Side Computed
2+
3+
The project manifest (`rfw.json`) can declare the build type. Setting it to `ssc` enables Server Side Computed builds.
4+
5+
```json
6+
{
7+
"build": {
8+
"type": "ssc"
9+
}
10+
}
11+
```
12+
13+
When `ssc` is active, `rfw build` compiles the Wasm bundle and also builds the Go sources in the `host` directory into a server binary. The server keeps variables and commands prefixed with `h:` synchronized with the client through a persistent WebSocket connection.

docs/articles/sidebar.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
{ "title": "API Integration", "path": "guide/api-integration.md" },
1818
{ "title": "Animations", "path": "guide/animation.md" },
1919
{ "title": "Plugins", "path": "guide/plugins.md" },
20-
{ "title": "Server-Side Rendering", "path": "guide/ssr.md" }
20+
{ "title": "Server-Side Rendering", "path": "guide/ssr.md" },
21+
{ "title": "Server Side Computed", "path": "guide/ssc.md" }
2122
]
2223
},
2324
{

docs/components/home_component.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var homeTpl []byte
1616

1717
func NewHomeComponent() *core.HTMLComponent {
1818
c := core.NewComponent("HomeComponent", homeTpl, nil)
19+
c.AddHostComponent("HomeHost")
1920
c.SetOnMount(func(cmp *core.HTMLComponent) {
2021
doc := js.Document()
2122
add := func(sel, path string) {

docs/components/templates/home_component.rtml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<section class="text-center py-20 bg-gradient-to-r from-blue-500 to-purple-600 text-white">
1111
<h1 class="text-5xl font-extrabold mb-4">Build reactive apps in Go</h1>
1212
<p class="text-lg mb-8">rfw lets you write modern web UIs with Go and WebAssembly.</p>
13+
<p class="mt-4">{h:welcome}</p>
1314
<a href="/docs/getting-started" class="px-6 py-3 bg-white text-blue-600 font-semibold rounded shadow">Get Started</a>
1415
</section>
1516
<section class="py-20 bg-gray-50">

docs/host/host

8.35 MB
Binary file not shown.

docs/host/main.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"log"
7+
"net/http"
8+
9+
"golang.org/x/net/websocket"
10+
11+
"github.com/rfwlab/rfw/v1/host"
12+
)
13+
14+
type inbound struct {
15+
Component string `json:"component"`
16+
Payload json.RawMessage `json:"payload"`
17+
}
18+
19+
func main() {
20+
http.Handle("/ws", websocket.Handler(func(ws *websocket.Conn) {
21+
defer ws.Close()
22+
for {
23+
var raw []byte
24+
if err := websocket.Message.Receive(ws, &raw); err != nil {
25+
if err == io.EOF {
26+
break
27+
}
28+
log.Printf("recv: %v", err)
29+
return
30+
}
31+
var msg inbound
32+
if err := json.Unmarshal(raw, &msg); err != nil {
33+
log.Printf("unmarshal: %v", err)
34+
continue
35+
}
36+
if hc, ok := host.Get(msg.Component); ok {
37+
ctx := host.NewContext(ws, msg.Component)
38+
hc.Handle(ctx, msg.Payload)
39+
}
40+
}
41+
}))
42+
log.Fatal(http.ListenAndServe(":8090", nil))
43+
}
44+
45+
func init() {
46+
host.Register(host.NewHostComponent("HomeHost", func(ctx *host.Context, payload json.RawMessage) {
47+
_ = ctx.Notify(map[string]any{"welcome": "hello from host"})
48+
}))
49+
}

docs/main.wasm

-11.5 MB
Binary file not shown.

docs/rfw.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
{
2+
"build": {
3+
"type": "ssc"
4+
},
25
"plugins": {
36
"tailwind": {
47
"input": "input.css",

0 commit comments

Comments
 (0)