1
0
Fork 0
forked from Mirrors/gomuks

web/main: automatically reload page if version changes

This commit is contained in:
Tulir Asokan 2024-12-07 16:12:47 +02:00
parent 5ee25b83d4
commit 5638adf6bc
9 changed files with 124 additions and 17 deletions

View file

@ -81,6 +81,9 @@ func (c *CommandHandler) Init() {
Data: marshaledPayload,
})
}
if ctx.Err() != nil {
return
}
c.App.EmitEvent("hicli_event", &hicli.JSONCommand{
Command: "init_complete",
RequestID: 0,

View file

@ -56,6 +56,8 @@ type Gomuks struct {
LogDir string
FrontendFS embed.FS
indexWithETag []byte
frontendETag string
Config Config
DisableAuth bool

View file

@ -17,15 +17,19 @@
package gomuks
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"html"
"io"
"io/fs"
"net/http"
_ "net/http/pprof"
"strconv"
"strings"
"time"
@ -73,6 +77,25 @@ func (gmx *Gomuks) StartServer() {
gmx.Log.Warn().Msg("Frontend not found")
} else {
router.Handle("/", gmx.FrontendCacheMiddleware(http.FileServerFS(frontend)))
if gmx.Commit != "unknown" && !gmx.BuildTime.IsZero() {
gmx.frontendETag = fmt.Sprintf(`"%s-%s"`, gmx.Commit, gmx.BuildTime.Format(time.RFC3339))
indexFile, err := frontend.Open("index.html")
if err != nil {
gmx.Log.Err(err).Msg("Failed to open index.html")
} else {
data, err := io.ReadAll(indexFile)
_ = indexFile.Close()
if err == nil {
gmx.indexWithETag = bytes.Replace(
data,
[]byte("<!-- etag placeholder -->"),
[]byte(fmt.Sprintf(`<meta name="gomuks-frontend-etag" content="%s">`, html.EscapeString(gmx.frontendETag))),
1,
)
}
}
}
}
gmx.Server = &http.Server{
Addr: gmx.Config.Web.ListenAddress,
@ -88,20 +111,23 @@ func (gmx *Gomuks) StartServer() {
}
func (gmx *Gomuks) FrontendCacheMiddleware(next http.Handler) http.Handler {
var frontendCacheETag string
if gmx.Commit != "unknown" && !gmx.BuildTime.IsZero() {
frontendCacheETag = fmt.Sprintf(`"%s-%s"`, gmx.Commit, gmx.BuildTime.Format(time.RFC3339))
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("If-None-Match") == frontendCacheETag {
if gmx.frontendETag != "" && r.Header.Get("If-None-Match") == gmx.frontendETag {
w.WriteHeader(http.StatusNotModified)
return
}
if strings.HasPrefix(r.URL.Path, "/assets/") {
w.Header().Set("Cache-Control", "max-age=604800, immutable")
}
if frontendCacheETag != "" {
w.Header().Set("ETag", frontendCacheETag)
if gmx.frontendETag != "" {
w.Header().Set("ETag", gmx.frontendETag)
if r.URL.Path == "/" && gmx.indexWithETag != nil {
w.Header().Set("Content-Type", "text/html")
w.Header().Set("Content-Length", strconv.Itoa(len(gmx.indexWithETag)))
w.WriteHeader(http.StatusOK)
_, _ = w.Write(gmx.indexWithETag)
return
}
}
next.ServeHTTP(w, r)
})

View file

@ -59,6 +59,11 @@ type PingRequestData struct {
var runID = time.Now().UnixNano()
type RunData struct {
RunID string `json:"run_id"`
ETag string `json:"etag"`
}
func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
var conn *websocket.Conn
log := zerolog.Ctx(r.Context())
@ -233,9 +238,12 @@ func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
log.Trace().Int64("req_id", cmd.RequestID).Msg("Sent response to command")
}
}
initErr := writeCmd(ctx, conn, &hicli.JSONCommandCustom[string]{
initErr := writeCmd(ctx, conn, &hicli.JSONCommandCustom[*RunData]{
Command: "run_id",
Data: strconv.FormatInt(runID, 10),
Data: &RunData{
RunID: strconv.FormatInt(runID, 10),
ETag: gmx.frontendETag,
},
})
if initErr != nil {
log.Err(initErr).Msg("Failed to write init client state message")
@ -315,6 +323,9 @@ func (gmx *Gomuks) sendInitialData(ctx context.Context, conn *websocket.Conn) {
return
}
}
if ctx.Err() != nil {
return
}
err := writeCmd(ctx, conn, &hicli.JSONCommand{
Command: "init_complete",
RequestID: 0,

View file

@ -23,6 +23,9 @@ func (h *HiClient) getInitialSyncRoom(ctx context.Context, room *database.Room)
ad, err := h.DB.AccountData.GetAllRoom(ctx, h.Account.UserID, room.ID)
if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("room_id", room.ID).Msg("Failed to get room account data")
if ctx.Err() != nil {
return nil
}
syncRoom.AccountData = make(map[event.Type]*database.AccountData)
} else {
syncRoom.AccountData = make(map[event.Type]*database.AccountData, len(ad))
@ -34,6 +37,9 @@ func (h *HiClient) getInitialSyncRoom(ctx context.Context, room *database.Room)
previewEvent, err := h.DB.Event.GetByRowID(ctx, room.PreviewEventRowID)
if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("room_id", room.ID).Msg("Failed to get preview event for room")
if ctx.Err() != nil {
return nil
}
} else if previewEvent != nil {
h.ReprocessExistingEvent(ctx, previewEvent)
previewMember, err := h.DB.CurrentState.Get(ctx, room.ID, event.StateMember, previewEvent.Sender.String())
@ -85,6 +91,9 @@ func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[*
}
maxTS = room.SortingTimestamp.Time
payload.Rooms[room.ID] = h.getInitialSyncRoom(ctx, room)
if ctx.Err() != nil {
return
}
}
if !yield(&payload) || len(rooms) < batchSize {
break

View file

@ -5,6 +5,7 @@
<link rel="icon" type="image/png" href="gomuks.png"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>gomuks web</title>
<!-- etag placeholder -->
</head>
<body>
<div id="root"></div>

View file

@ -124,7 +124,12 @@ export interface InitCompleteEvent extends BaseRPCCommand<void> {
command: "init_complete"
}
export interface RunIDEvent extends BaseRPCCommand<string> {
export interface RunData {
run_id: string
etag: string
}
export interface RunIDEvent extends BaseRPCCommand<RunData> {
command: "run_id"
}

View file

@ -19,6 +19,35 @@ import type { RPCCommand } from "./types"
const PING_INTERVAL = 15_000
const RECV_TIMEOUT = 4 * PING_INTERVAL
function checkUpdate(etag: string) {
if (!import.meta.env.PROD) {
return
} else if (!etag) {
console.log("Not checking for update, frontend etag not found in websocket init")
return
}
const currentETag = (
document.querySelector("meta[name=gomuks-frontend-etag]") as HTMLMetaElement
)?.content
if (!currentETag) {
console.log("Not checking for update, frontend etag not found in head")
} else if (currentETag === etag) {
console.log("Frontend is up to date")
} else if (localStorage.lastUpdateTo === etag) {
console.warn(
`Frontend etag mismatch ${currentETag} !== ${etag}, `,
"but localstorage says an update was already attempted",
)
} else {
console.info(`Frontend etag mismatch ${currentETag} !== ${etag}, reloading`)
localStorage.lastUpdateTo = etag
location.search = "?" + new URLSearchParams({
updateTo: etag,
state: JSON.stringify(history.state),
})
}
}
export default class WSClient extends RPCClient {
#conn: WebSocket | null = null
#lastMessage: number = 0
@ -105,7 +134,9 @@ export default class WSClient extends RPCClient {
if (parsed.request_id < 0) {
this.#lastReceivedEvt = parsed.request_id
} else if (parsed.command === "run_id") {
this.#resumeRunID = parsed.data
console.log("Received run ID", parsed.data)
this.#resumeRunID = parsed.data.run_id
checkUpdate(parsed.data.etag)
}
this.onCommand(parsed)
}

View file

@ -151,17 +151,35 @@ const SYNC_ERROR_HIDE_DELAY = 30 * 1000
const handleURLHash = (client: Client, context: ContextFields) => {
if (!location.hash.startsWith("#/uri/")) {
return
if (location.search) {
const currentETag = (
document.querySelector("meta[name=gomuks-frontend-etag]") as HTMLMetaElement
)?.content
const newURL = new URL(location.href)
const updateTo = newURL.searchParams.get("updateTo")
if (updateTo === currentETag) {
console.info("Update to etag", updateTo, "successful")
} else {
console.warn("Update to etag", updateTo, "failed, got", currentETag)
}
const state = JSON.parse(newURL.searchParams.get("state") || "{}")
newURL.search = ""
history.replaceState(state, "", newURL.toString())
return state
}
return history.state
}
const decodedURI = decodeURIComponent(location.hash.slice("#/uri/".length))
const uri = parseMatrixURI(decodedURI)
if (!uri) {
console.error("Invalid matrix URI", decodedURI)
return
return history.state
}
console.log("Handling URI", uri)
const newURL = new URL(location.href)
newURL.hash = ""
newURL.search = ""
if (uri.identifier.startsWith("@")) {
const right_panel = {
type: "user",
@ -184,6 +202,7 @@ const handleURLHash = (client: Client, context: ContextFields) => {
} else {
console.error("Invalid matrix URI", uri)
}
return null
}
const MainScreen = () => {
@ -208,8 +227,8 @@ const MainScreen = () => {
}
window.addEventListener("popstate", listener)
const initHandle = () => {
listener({ state: history.state } as PopStateEvent)
handleURLHash(client, context)
const state = handleURLHash(client, context)
listener({ state } as PopStateEvent)
}
let cancel = () => {}
if (client.initComplete.current) {