Skip to content

Commit 964ccec

Browse files
committed
greatly simplify + document dynamic content handling
1 parent ce86ded commit 964ccec

4 files changed

Lines changed: 86 additions & 214 deletions

File tree

tsunami/app/defaultclient.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,17 @@ func SetGlobalEventHandler(handler func(event vdom.VDomEvent)) {
2626
defaultClient.SetGlobalEventHandler(handler)
2727
}
2828

29-
func AddSetupFn(fn func()) {
30-
defaultClient.AddSetupFn(fn)
29+
// RegisterSetupFn registers a single setup function that is called before the app starts running.
30+
// Only one setup function is allowed, so calling this will replace any previously registered
31+
// setup function.
32+
func RegisterSetupFn(fn func()) {
33+
defaultClient.RegisterSetupFn(fn)
3134
}
3235

36+
// SendAsyncInitiation notifies the frontend that the backend has updated state
37+
// and requires a re-render. Normally the frontend calls the backend in response
38+
// to events, but when the backend changes state independently (e.g., from a
39+
// background process), this function gives the frontend a "nudge" to update.
3340
func SendAsyncInitiation() error {
3441
return defaultClient.SendAsyncInitiation()
3542
}
@@ -46,16 +53,11 @@ func GetAtomVal(name string) any {
4653
return defaultClient.GetAtomVal(name)
4754
}
4855

49-
func RegisterUrlPathHandler(path string, handler http.Handler) {
50-
defaultClient.RegisterUrlPathHandler(path, handler)
51-
}
52-
53-
func RegisterFilePrefixHandler(prefix string, optionProvider func(path string) (*FileHandlerOption, error)) {
54-
defaultClient.RegisterFilePrefixHandler(prefix, optionProvider)
55-
}
56-
57-
func RegisterFileHandler(path string, option FileHandlerOption) {
58-
defaultClient.RegisterFileHandler(path, option)
56+
// HandleDynFunc registers a dynamic HTTP handler function with the internal http.ServeMux.
57+
// The pattern MUST start with "/dyn/" to be valid. This allows registration of dynamic
58+
// routes that can be handled at runtime.
59+
func HandleDynFunc(pattern string, fn func(http.ResponseWriter, *http.Request)) {
60+
defaultClient.HandleDynFunc(pattern, fn)
5961
}
6062

6163
// RunMain is used internally by generated code and should not be called directly.

tsunami/app/serverhandlers.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func init() {
2929
type handlerOpts struct {
3030
AssetsFS fs.FS
3131
StaticFS fs.FS
32-
ManifestFile *FileHandlerOption
32+
ManifestFile []byte
3333
}
3434

3535
type httpHandlers struct {
@@ -377,7 +377,7 @@ func (h *httpHandlers) handleStaticFiles(embeddedFS fs.FS) http.HandlerFunc {
377377
}
378378
}
379379

380-
func (h *httpHandlers) handleManifest(manifestFile *FileHandlerOption) http.HandlerFunc {
380+
func (h *httpHandlers) handleManifest(manifestFileBytes []byte) http.HandlerFunc {
381381
return func(w http.ResponseWriter, r *http.Request) {
382382
defer func() {
383383
panicErr := util.PanicHandler("handleManifest", recover())
@@ -391,12 +391,13 @@ func (h *httpHandlers) handleManifest(manifestFile *FileHandlerOption) http.Hand
391391
return
392392
}
393393

394-
if manifestFile == nil {
394+
if manifestFileBytes == nil {
395395
http.NotFound(w, r)
396396
return
397397
}
398398

399-
serveFileOption(w, r, *manifestFile)
399+
w.Header().Set("Content-Type", "application/json")
400+
w.Write(manifestFileBytes)
400401
}
401402
}
402403

tsunami/app/tsunamiapp.go

Lines changed: 7 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ package app
66
import (
77
"context"
88
"fmt"
9-
"io"
10-
"io/fs"
119
"log"
1210
"net"
1311
"net/http"
@@ -44,7 +42,6 @@ type clientImpl struct {
4442
DoneCh chan struct{}
4543
SSEventCh chan ssEvent
4644
GlobalEventHandler func(event vdom.VDomEvent)
47-
GlobalStylesOption *FileHandlerOption
4845
UrlHandlerMux *http.ServeMux
4946
SetupFn func()
5047
}
@@ -134,7 +131,7 @@ func (c *clientImpl) runMainE() error {
134131
return nil
135132
}
136133

137-
func (c *clientImpl) AddSetupFn(fn func()) {
134+
func (c *clientImpl) RegisterSetupFn(fn func()) {
138135
c.SetupFn = fn
139136
}
140137

@@ -152,14 +149,10 @@ func (c *clientImpl) listenAndServe(ctx context.Context) error {
152149

153150
// Create a new ServeMux and register handlers
154151
mux := http.NewServeMux()
155-
var manifestOption *FileHandlerOption
156-
if len(manifestFileBytes) > 0 {
157-
manifestOption = &FileHandlerOption{Data: manifestFileBytes}
158-
}
159152
handlers.registerHandlers(mux, handlerOpts{
160153
AssetsFS: assetsFS,
161154
StaticFS: staticFS,
162-
ManifestFile: manifestOption,
155+
ManifestFile: manifestFileBytes,
163156
})
164157

165158
// Determine listen address from environment variable or use default
@@ -304,162 +297,10 @@ func (c *clientImpl) incrementalRender() (*rpctypes.VDomBackendUpdate, error) {
304297
}, nil
305298
}
306299

307-
func (c *clientImpl) RegisterUrlPathHandler(path string, handler http.Handler) {
308-
c.UrlHandlerMux.Handle(path, handler)
309-
}
310-
311-
type FileHandlerOption struct {
312-
FilePath string // optional file path on disk
313-
Data []byte // optional byte slice content
314-
Reader io.Reader // optional reader for content
315-
File fs.File // optional embedded or opened file
316-
MimeType string // optional mime type
317-
ETag string // optional ETag (if set, resource may be cached)
318-
}
319-
320-
func determineMimeType(option FileHandlerOption) (string, []byte) {
321-
// If MimeType is set, use it directly
322-
if option.MimeType != "" {
323-
return option.MimeType, nil
324-
}
325-
326-
// Detect from Data if available, no need to buffer
327-
if option.Data != nil {
328-
return http.DetectContentType(option.Data), nil
329-
}
330-
331-
// Detect from FilePath, no buffering necessary
332-
if option.FilePath != "" {
333-
filePath := util.ExpandHomeDirSafe(option.FilePath)
334-
file, err := os.Open(filePath)
335-
if err != nil {
336-
return "application/octet-stream", nil // Fallback on error
337-
}
338-
defer file.Close()
339-
340-
// Read first 512 bytes for MIME detection
341-
buf := make([]byte, 512)
342-
_, err = file.Read(buf)
343-
if err != nil && err != io.EOF {
344-
return "application/octet-stream", nil
345-
}
346-
return http.DetectContentType(buf), nil
347-
}
348-
349-
// Buffer for File (fs.File), since it lacks Seek
350-
if option.File != nil {
351-
buf := make([]byte, 512)
352-
n, err := option.File.Read(buf)
353-
if err != nil && err != io.EOF {
354-
return "application/octet-stream", nil
355-
}
356-
return http.DetectContentType(buf[:n]), buf[:n]
357-
}
358-
359-
// Buffer for Reader (io.Reader), same as File
360-
if option.Reader != nil {
361-
buf := make([]byte, 512)
362-
n, err := option.Reader.Read(buf)
363-
if err != nil && err != io.EOF {
364-
return "application/octet-stream", nil
365-
}
366-
return http.DetectContentType(buf[:n]), buf[:n]
367-
}
368-
369-
// Default MIME type if none specified
370-
return "application/octet-stream", nil
371-
}
372-
373-
// serveFileOption handles serving content based on the provided FileHandlerOption
374-
func serveFileOption(w http.ResponseWriter, r *http.Request, option FileHandlerOption) error {
375-
// Determine MIME type and get buffered data if needed
376-
contentType, bufferedData := determineMimeType(option)
377-
w.Header().Set("Content-Type", contentType)
378-
// Handle ETag
379-
if option.ETag != "" {
380-
w.Header().Set("ETag", option.ETag)
381-
382-
// Check If-None-Match header
383-
if inm := r.Header.Get("If-None-Match"); inm != "" {
384-
// Strip W/ prefix and quotes if present
385-
inm = strings.Trim(inm, `"`)
386-
inm = strings.TrimPrefix(inm, "W/")
387-
etag := strings.Trim(option.ETag, `"`)
388-
etag = strings.TrimPrefix(etag, "W/")
389-
390-
if inm == etag {
391-
// Resource not modified
392-
w.WriteHeader(http.StatusNotModified)
393-
return nil
394-
}
395-
}
396-
}
397-
398-
// Handle the content based on the option type
399-
switch {
400-
case option.FilePath != "":
401-
filePath := util.ExpandHomeDirSafe(option.FilePath)
402-
http.ServeFile(w, r, filePath)
403-
404-
case option.Data != nil:
405-
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(option.Data)))
406-
w.WriteHeader(http.StatusOK)
407-
if _, err := w.Write(option.Data); err != nil {
408-
return fmt.Errorf("failed to write data: %v", err)
409-
}
410-
411-
case option.File != nil:
412-
if bufferedData != nil {
413-
if _, err := w.Write(bufferedData); err != nil {
414-
return fmt.Errorf("failed to write buffered data: %v", err)
415-
}
416-
}
417-
if _, err := io.Copy(w, option.File); err != nil {
418-
return fmt.Errorf("failed to copy from file: %v", err)
419-
}
420-
421-
case option.Reader != nil:
422-
if bufferedData != nil {
423-
if _, err := w.Write(bufferedData); err != nil {
424-
return fmt.Errorf("failed to write buffered data: %v", err)
425-
}
426-
}
427-
if _, err := io.Copy(w, option.Reader); err != nil {
428-
return fmt.Errorf("failed to copy from reader: %v", err)
429-
}
430-
431-
default:
432-
return fmt.Errorf("no content available")
300+
func (c *clientImpl) HandleDynFunc(pattern string, fn func(http.ResponseWriter, *http.Request)) {
301+
if !strings.HasPrefix(pattern, "/dyn/") {
302+
log.Printf("invalid dyn pattern: %s (must start with /dyn/)", pattern)
303+
return
433304
}
434-
435-
return nil
436-
}
437-
438-
func (c *clientImpl) RegisterFilePrefixHandler(prefix string, optionProvider func(path string) (*FileHandlerOption, error)) {
439-
c.UrlHandlerMux.HandleFunc(prefix, func(w http.ResponseWriter, r *http.Request) {
440-
if !strings.HasPrefix(r.URL.Path, prefix) {
441-
http.NotFound(w, r)
442-
return
443-
}
444-
option, err := optionProvider(r.URL.Path)
445-
if err != nil {
446-
http.Error(w, err.Error(), http.StatusInternalServerError)
447-
return
448-
}
449-
if option == nil {
450-
http.Error(w, "no content available", http.StatusNotFound)
451-
return
452-
}
453-
if err := serveFileOption(w, r, *option); err != nil {
454-
http.Error(w, fmt.Sprintf("Failed to serve content: %v", err), http.StatusInternalServerError)
455-
}
456-
})
457-
}
458-
459-
func (c *clientImpl) RegisterFileHandler(path string, option FileHandlerOption) {
460-
c.UrlHandlerMux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
461-
if err := serveFileOption(w, r, option); err != nil {
462-
http.Error(w, err.Error(), http.StatusInternalServerError)
463-
}
464-
})
305+
c.UrlHandlerMux.HandleFunc(pattern, fn)
465306
}

0 commit comments

Comments
 (0)