mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
websocket: add support for resuming sessions
This commit is contained in:
parent
0930e94fb2
commit
7d6bbe77b9
13 changed files with 288 additions and 72 deletions
|
@ -152,7 +152,7 @@ func main() {
|
||||||
URL: "/",
|
URL: "/",
|
||||||
})
|
})
|
||||||
|
|
||||||
gmx.SubscribeEvents(nil, func(command *hicli.JSONCommand) {
|
gmx.EventBuffer.Subscribe(0, nil, func(command *hicli.JSONCommand) {
|
||||||
app.EmitEvent("hicli_event", command)
|
app.EmitEvent("hicli_event", command)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
157
pkg/gomuks/buffer.go
Normal file
157
pkg/gomuks/buffer.go
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2024 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package gomuks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/coder/websocket"
|
||||||
|
|
||||||
|
"go.mau.fi/gomuks/pkg/hicli"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebsocketCloseFunc func(websocket.StatusCode, string)
|
||||||
|
|
||||||
|
type EventBuffer struct {
|
||||||
|
lock sync.RWMutex
|
||||||
|
buf []*hicli.JSONCommand
|
||||||
|
minID int64
|
||||||
|
maxID int64
|
||||||
|
MaxSize int
|
||||||
|
|
||||||
|
websocketClosers map[uint64]WebsocketCloseFunc
|
||||||
|
lastAckedID map[uint64]int64
|
||||||
|
eventListeners map[uint64]func(*hicli.JSONCommand)
|
||||||
|
nextListenerID uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEventBuffer(maxSize int) *EventBuffer {
|
||||||
|
return &EventBuffer{
|
||||||
|
websocketClosers: make(map[uint64]WebsocketCloseFunc),
|
||||||
|
lastAckedID: make(map[uint64]int64),
|
||||||
|
eventListeners: make(map[uint64]func(*hicli.JSONCommand)),
|
||||||
|
buf: make([]*hicli.JSONCommand, 0, maxSize*2),
|
||||||
|
MaxSize: maxSize,
|
||||||
|
minID: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *EventBuffer) HicliEventHandler(evt any) {
|
||||||
|
data, err := json.Marshal(evt)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to marshal event %T: %w", evt, err))
|
||||||
|
}
|
||||||
|
allowCache := true
|
||||||
|
if syncComplete, ok := evt.(*hicli.SyncComplete); ok && syncComplete.Since != nil && *syncComplete.Since == "" {
|
||||||
|
// Don't cache initial sync responses
|
||||||
|
allowCache = false
|
||||||
|
} else if _, ok := evt.(*hicli.Typing); ok {
|
||||||
|
// Also don't cache typing events
|
||||||
|
allowCache = false
|
||||||
|
}
|
||||||
|
eb.lock.Lock()
|
||||||
|
defer eb.lock.Unlock()
|
||||||
|
jc := &hicli.JSONCommand{
|
||||||
|
Command: hicli.EventTypeName(evt),
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
if allowCache {
|
||||||
|
eb.addToBuffer(jc)
|
||||||
|
}
|
||||||
|
for _, listener := range eb.eventListeners {
|
||||||
|
listener(jc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *EventBuffer) GetClosers() []WebsocketCloseFunc {
|
||||||
|
eb.lock.Lock()
|
||||||
|
defer eb.lock.Unlock()
|
||||||
|
return slices.Collect(maps.Values(eb.websocketClosers))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *EventBuffer) Unsubscribe(listenerID uint64) {
|
||||||
|
eb.lock.Lock()
|
||||||
|
defer eb.lock.Unlock()
|
||||||
|
delete(eb.eventListeners, listenerID)
|
||||||
|
delete(eb.websocketClosers, listenerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *EventBuffer) addToBuffer(evt *hicli.JSONCommand) {
|
||||||
|
eb.maxID--
|
||||||
|
evt.RequestID = eb.maxID
|
||||||
|
if len(eb.lastAckedID) > 0 {
|
||||||
|
eb.buf = append(eb.buf, evt)
|
||||||
|
} else {
|
||||||
|
eb.minID = eb.maxID - 1
|
||||||
|
}
|
||||||
|
if len(eb.buf) > eb.MaxSize {
|
||||||
|
eb.buf = eb.buf[len(eb.buf)-eb.MaxSize:]
|
||||||
|
eb.minID = eb.buf[0].RequestID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *EventBuffer) ClearListenerLastAckedID(listenerID uint64) {
|
||||||
|
eb.lock.Lock()
|
||||||
|
defer eb.lock.Unlock()
|
||||||
|
delete(eb.lastAckedID, listenerID)
|
||||||
|
eb.gc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *EventBuffer) SetLastAckedID(listenerID uint64, ackedID int64) {
|
||||||
|
eb.lock.Lock()
|
||||||
|
defer eb.lock.Unlock()
|
||||||
|
eb.lastAckedID[listenerID] = ackedID
|
||||||
|
eb.gc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *EventBuffer) gc() {
|
||||||
|
neededMinID := eb.maxID
|
||||||
|
for lid, evtID := range eb.lastAckedID {
|
||||||
|
if evtID > eb.minID {
|
||||||
|
delete(eb.lastAckedID, lid)
|
||||||
|
} else if evtID > neededMinID {
|
||||||
|
neededMinID = evtID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if neededMinID < eb.minID {
|
||||||
|
eb.buf = eb.buf[eb.minID-neededMinID:]
|
||||||
|
eb.minID = neededMinID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *EventBuffer) Subscribe(resumeFrom int64, closeForRestart WebsocketCloseFunc, cb func(*hicli.JSONCommand)) (uint64, []*hicli.JSONCommand) {
|
||||||
|
eb.lock.Lock()
|
||||||
|
defer eb.lock.Unlock()
|
||||||
|
eb.nextListenerID++
|
||||||
|
id := eb.nextListenerID
|
||||||
|
eb.eventListeners[id] = cb
|
||||||
|
if closeForRestart != nil {
|
||||||
|
eb.websocketClosers[id] = closeForRestart
|
||||||
|
}
|
||||||
|
var resumeData []*hicli.JSONCommand
|
||||||
|
if resumeFrom < eb.minID {
|
||||||
|
resumeData = eb.buf[eb.minID-resumeFrom+1:]
|
||||||
|
eb.lastAckedID[id] = resumeFrom
|
||||||
|
} else {
|
||||||
|
eb.lastAckedID[id] = eb.maxID
|
||||||
|
}
|
||||||
|
return id, resumeData
|
||||||
|
}
|
|
@ -20,13 +20,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
@ -65,17 +63,13 @@ type Gomuks struct {
|
||||||
stopOnce sync.Once
|
stopOnce sync.Once
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
|
|
||||||
websocketClosers map[uint64]WebsocketCloseFunc
|
EventBuffer *EventBuffer
|
||||||
eventListeners map[uint64]func(*hicli.JSONCommand)
|
|
||||||
nextListenerID uint64
|
|
||||||
eventListenersLock sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGomuks() *Gomuks {
|
func NewGomuks() *Gomuks {
|
||||||
return &Gomuks{
|
return &Gomuks{
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
eventListeners: make(map[uint64]func(*hicli.JSONCommand)),
|
EventBuffer: NewEventBuffer(512),
|
||||||
websocketClosers: make(map[uint64]WebsocketCloseFunc),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,7 +170,7 @@ func (gmx *Gomuks) StartClient() {
|
||||||
nil,
|
nil,
|
||||||
gmx.Log.With().Str("component", "hicli").Logger(),
|
gmx.Log.With().Str("component", "hicli").Logger(),
|
||||||
[]byte("meow"),
|
[]byte("meow"),
|
||||||
hicli.JSONEventHandler(gmx.OnEvent).HandleEvent,
|
gmx.EventBuffer.HicliEventHandler,
|
||||||
)
|
)
|
||||||
gmx.Client.LogoutFunc = gmx.Logout
|
gmx.Client.LogoutFunc = gmx.Logout
|
||||||
httpClient := gmx.Client.Client.Client
|
httpClient := gmx.Client.Client.Client
|
||||||
|
@ -218,10 +212,7 @@ func (gmx *Gomuks) WaitForInterrupt() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gmx *Gomuks) DirectStop() {
|
func (gmx *Gomuks) DirectStop() {
|
||||||
gmx.eventListenersLock.Lock()
|
for _, closer := range gmx.EventBuffer.GetClosers() {
|
||||||
closers := slices.Collect(maps.Values(gmx.websocketClosers))
|
|
||||||
gmx.eventListenersLock.Unlock()
|
|
||||||
for _, closer := range closers {
|
|
||||||
closer(websocket.StatusServiceRestart, "Server shutting down")
|
closer(websocket.StatusServiceRestart, "Server shutting down")
|
||||||
}
|
}
|
||||||
gmx.Client.Stop()
|
gmx.Client.Stop()
|
||||||
|
@ -231,33 +222,6 @@ func (gmx *Gomuks) DirectStop() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gmx *Gomuks) OnEvent(evt *hicli.JSONCommand) {
|
|
||||||
gmx.eventListenersLock.RLock()
|
|
||||||
defer gmx.eventListenersLock.RUnlock()
|
|
||||||
for _, listener := range gmx.eventListeners {
|
|
||||||
listener(evt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebsocketCloseFunc func(websocket.StatusCode, string)
|
|
||||||
|
|
||||||
func (gmx *Gomuks) SubscribeEvents(closeForRestart WebsocketCloseFunc, cb func(command *hicli.JSONCommand)) func() {
|
|
||||||
gmx.eventListenersLock.Lock()
|
|
||||||
defer gmx.eventListenersLock.Unlock()
|
|
||||||
gmx.nextListenerID++
|
|
||||||
id := gmx.nextListenerID
|
|
||||||
gmx.eventListeners[id] = cb
|
|
||||||
if closeForRestart != nil {
|
|
||||||
gmx.websocketClosers[id] = closeForRestart
|
|
||||||
}
|
|
||||||
return func() {
|
|
||||||
gmx.eventListenersLock.Lock()
|
|
||||||
defer gmx.eventListenersLock.Unlock()
|
|
||||||
delete(gmx.eventListeners, id)
|
|
||||||
delete(gmx.websocketClosers, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gmx *Gomuks) Run() {
|
func (gmx *Gomuks) Run() {
|
||||||
gmx.InitDirectories()
|
gmx.InitDirectories()
|
||||||
err := gmx.LoadConfig()
|
err := gmx.LoadConfig()
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
@ -52,6 +53,12 @@ const (
|
||||||
|
|
||||||
var emptyObject = json.RawMessage("{}")
|
var emptyObject = json.RawMessage("{}")
|
||||||
|
|
||||||
|
type PingRequestData struct {
|
||||||
|
LastReceivedID int64 `json:"last_received_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var runID = time.Now().UnixNano()
|
||||||
|
|
||||||
func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
var conn *websocket.Conn
|
var conn *websocket.Conn
|
||||||
log := zerolog.Ctx(r.Context())
|
log := zerolog.Ctx(r.Context())
|
||||||
|
@ -80,15 +87,23 @@ func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Warn().Err(acceptErr).Msg("Failed to accept websocket connection")
|
log.Warn().Err(acceptErr).Msg("Failed to accept websocket connection")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Info().Msg("Accepted new websocket connection")
|
resumeFrom, _ := strconv.ParseInt(r.URL.Query().Get("last_received_event"), 10, 64)
|
||||||
|
resumeRunID, _ := strconv.ParseInt(r.URL.Query().Get("run_id"), 10, 64)
|
||||||
|
log.Info().
|
||||||
|
Int64("resume_from", resumeFrom).
|
||||||
|
Int64("resume_run_id", resumeRunID).
|
||||||
|
Int64("current_run_id", runID).
|
||||||
|
Msg("Accepted new websocket connection")
|
||||||
conn.SetReadLimit(128 * 1024)
|
conn.SetReadLimit(128 * 1024)
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
ctx = log.WithContext(ctx)
|
ctx = log.WithContext(ctx)
|
||||||
unsubscribe := func() {}
|
var listenerID uint64
|
||||||
evts := make(chan *hicli.JSONCommand, 32)
|
evts := make(chan *hicli.JSONCommand, 32)
|
||||||
forceClose := func() {
|
forceClose := func() {
|
||||||
cancel()
|
cancel()
|
||||||
unsubscribe()
|
if listenerID != 0 {
|
||||||
|
gmx.EventBuffer.Unsubscribe(listenerID)
|
||||||
|
}
|
||||||
_ = conn.CloseNow()
|
_ = conn.CloseNow()
|
||||||
close(evts)
|
close(evts)
|
||||||
}
|
}
|
||||||
|
@ -99,7 +114,11 @@ func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = conn.Close(statusCode, reason)
|
_ = conn.Close(statusCode, reason)
|
||||||
closeOnce.Do(forceClose)
|
closeOnce.Do(forceClose)
|
||||||
}
|
}
|
||||||
unsubscribe = gmx.SubscribeEvents(closeManually, func(evt *hicli.JSONCommand) {
|
if resumeRunID != runID {
|
||||||
|
resumeFrom = 0
|
||||||
|
}
|
||||||
|
var resumeData []*hicli.JSONCommand
|
||||||
|
listenerID, resumeData = gmx.EventBuffer.Subscribe(resumeFrom, closeManually, func(evt *hicli.JSONCommand) {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -115,6 +134,7 @@ func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
didResume := resumeData != nil
|
||||||
|
|
||||||
lastDataReceived := &atomic.Int64{}
|
lastDataReceived := &atomic.Int64{}
|
||||||
lastDataReceived.Store(time.Now().UnixMilli())
|
lastDataReceived.Store(time.Now().UnixMilli())
|
||||||
|
@ -133,6 +153,16 @@ func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
go func() {
|
go func() {
|
||||||
defer recoverPanic("event loop")
|
defer recoverPanic("event loop")
|
||||||
defer closeOnce.Do(forceClose)
|
defer closeOnce.Do(forceClose)
|
||||||
|
for _, cmd := range resumeData {
|
||||||
|
err := writeCmd(ctx, conn, cmd)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Int64("req_id", cmd.RequestID).Msg("Failed to write outgoing event from resume data")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
log.Trace().Int64("req_id", cmd.RequestID).Msg("Sent outgoing event from resume data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resumeData = nil
|
||||||
ticker := time.NewTicker(60 * time.Second)
|
ticker := time.NewTicker(60 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
ctxDone := ctx.Done()
|
ctxDone := ctx.Done()
|
||||||
|
@ -176,7 +206,22 @@ func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
Str("command", cmd.Command).
|
Str("command", cmd.Command).
|
||||||
RawJSON("data", cmd.Data).
|
RawJSON("data", cmd.Data).
|
||||||
Msg("Received command")
|
Msg("Received command")
|
||||||
resp := gmx.Client.SubmitJSONCommand(ctx, cmd)
|
var resp *hicli.JSONCommand
|
||||||
|
if cmd.Command == "ping" {
|
||||||
|
resp = &hicli.JSONCommand{
|
||||||
|
Command: "pong",
|
||||||
|
RequestID: cmd.RequestID,
|
||||||
|
}
|
||||||
|
var pingData PingRequestData
|
||||||
|
err := json.Unmarshal(cmd.Data, &pingData)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Failed to parse ping data")
|
||||||
|
} else if pingData.LastReceivedID != 0 {
|
||||||
|
gmx.EventBuffer.SetLastAckedID(listenerID, pingData.LastReceivedID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resp = gmx.Client.SubmitJSONCommand(ctx, cmd)
|
||||||
|
}
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -188,7 +233,15 @@ func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Trace().Int64("req_id", cmd.RequestID).Msg("Sent response to command")
|
log.Trace().Int64("req_id", cmd.RequestID).Msg("Sent response to command")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initErr := writeCmd(ctx, conn, &hicli.JSONCommandCustom[*hicli.ClientState]{
|
initErr := writeCmd(ctx, conn, &hicli.JSONCommandCustom[string]{
|
||||||
|
Command: "run_id",
|
||||||
|
Data: strconv.FormatInt(runID, 10),
|
||||||
|
})
|
||||||
|
if initErr != nil {
|
||||||
|
log.Err(initErr).Msg("Failed to write init client state message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
initErr = writeCmd(ctx, conn, &hicli.JSONCommandCustom[*hicli.ClientState]{
|
||||||
Command: "client_state",
|
Command: "client_state",
|
||||||
Data: gmx.Client.State(),
|
Data: gmx.Client.State(),
|
||||||
})
|
})
|
||||||
|
@ -205,10 +258,10 @@ func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go sendImageAuthToken()
|
go sendImageAuthToken()
|
||||||
if gmx.Client.IsLoggedIn() {
|
if gmx.Client.IsLoggedIn() && !didResume {
|
||||||
go gmx.sendInitialData(ctx, conn)
|
go gmx.sendInitialData(ctx, conn)
|
||||||
}
|
}
|
||||||
log.Debug().Msg("Connection initialization complete")
|
log.Debug().Bool("did_resume", didResume).Msg("Connection initialization complete")
|
||||||
var closeErr websocket.CloseError
|
var closeErr websocket.CloseError
|
||||||
for {
|
for {
|
||||||
msgType, reader, err := conn.Reader(ctx)
|
msgType, reader, err := conn.Reader(ctx)
|
||||||
|
@ -218,6 +271,9 @@ func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
Stringer("status_code", closeErr.Code).
|
Stringer("status_code", closeErr.Code).
|
||||||
Str("reason", closeErr.Reason).
|
Str("reason", closeErr.Reason).
|
||||||
Msg("Connection closed")
|
Msg("Connection closed")
|
||||||
|
if closeErr.Code == websocket.StatusGoingAway {
|
||||||
|
gmx.EventBuffer.ClearListenerLastAckedID(listenerID)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Err(err).Msg("Failed to read message")
|
log.Err(err).Msg("Failed to read message")
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@ type SyncNotification struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyncComplete struct {
|
type SyncComplete struct {
|
||||||
|
Since *string `json:"since,omitempty"`
|
||||||
|
ClearState bool `json:"clear_state,omitempty"`
|
||||||
Rooms map[id.RoomID]*SyncRoom `json:"rooms"`
|
Rooms map[id.RoomID]*SyncRoom `json:"rooms"`
|
||||||
AccountData map[event.Type]*database.AccountData `json:"account_data"`
|
AccountData map[event.Type]*database.AccountData `json:"account_data"`
|
||||||
LeftRooms []id.RoomID `json:"left_rooms"`
|
LeftRooms []id.RoomID `json:"left_rooms"`
|
||||||
|
|
|
@ -76,6 +76,9 @@ func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[*
|
||||||
LeftRooms: make([]id.RoomID, 0),
|
LeftRooms: make([]id.RoomID, 0),
|
||||||
AccountData: make(map[event.Type]*database.AccountData),
|
AccountData: make(map[event.Type]*database.AccountData),
|
||||||
}
|
}
|
||||||
|
if i == 0 {
|
||||||
|
payload.ClearState = true
|
||||||
|
}
|
||||||
for _, room := range rooms {
|
for _, room := range rooms {
|
||||||
if room.SortingTimestamp == rooms[len(rooms)-1].SortingTimestamp {
|
if room.SortingTimestamp == rooms[len(rooms)-1].SortingTimestamp {
|
||||||
break
|
break
|
||||||
|
|
|
@ -76,12 +76,6 @@ func (h *HiClient) dispatchCurrentState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HiClient) SubmitJSONCommand(ctx context.Context, req *JSONCommand) *JSONCommand {
|
func (h *HiClient) SubmitJSONCommand(ctx context.Context, req *JSONCommand) *JSONCommand {
|
||||||
if req.Command == "ping" {
|
|
||||||
return &JSONCommand{
|
|
||||||
Command: "pong",
|
|
||||||
RequestID: req.RequestID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log := h.Log.With().Int64("request_id", req.RequestID).Str("command", req.Command).Logger()
|
log := h.Log.With().Int64("request_id", req.RequestID).Str("command", req.Command).Logger()
|
||||||
ctx, cancel := context.WithCancelCause(ctx)
|
ctx, cancel := context.WithCancelCause(ctx)
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
|
@ -29,6 +29,7 @@ func (h *hiSyncer) ProcessResponse(ctx context.Context, resp *mautrix.RespSync,
|
||||||
c := (*HiClient)(h)
|
c := (*HiClient)(h)
|
||||||
c.lastSync = time.Now()
|
c.lastSync = time.Now()
|
||||||
ctx = context.WithValue(ctx, syncContextKey, &syncContext{evt: &SyncComplete{
|
ctx = context.WithValue(ctx, syncContextKey, &syncContext{evt: &SyncComplete{
|
||||||
|
Since: &since,
|
||||||
Rooms: make(map[id.RoomID]*SyncRoom, len(resp.Rooms.Join)),
|
Rooms: make(map[id.RoomID]*SyncRoom, len(resp.Rooms.Join)),
|
||||||
LeftRooms: make([]id.RoomID, 0, len(resp.Rooms.Leave)),
|
LeftRooms: make([]id.RoomID, 0, len(resp.Rooms.Leave)),
|
||||||
}})
|
}})
|
||||||
|
|
|
@ -324,12 +324,12 @@ export default class Client {
|
||||||
this.initComplete.emit(false)
|
this.initComplete.emit(false)
|
||||||
this.syncStatus.emit({ type: "waiting", error_count: 0 })
|
this.syncStatus.emit({ type: "waiting", error_count: 0 })
|
||||||
this.state.clearCache()
|
this.state.clearCache()
|
||||||
localStorage.clear()
|
|
||||||
this.store.clear()
|
this.store.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
await this.rpc.logout()
|
await this.rpc.logout()
|
||||||
this.clearState()
|
this.clearState()
|
||||||
|
localStorage.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ export default abstract class RPCClient {
|
||||||
public abstract start(): void
|
public abstract start(): void
|
||||||
public abstract stop(): void
|
public abstract stop(): void
|
||||||
|
|
||||||
protected onCommand(data: RPCCommand<unknown>) {
|
protected onCommand(data: RPCCommand) {
|
||||||
if (data.command === "response" || data.command === "error") {
|
if (data.command === "response" || data.command === "error") {
|
||||||
const target = this.pendingRequests.get(data.request_id)
|
const target = this.pendingRequests.get(data.request_id)
|
||||||
if (!target) {
|
if (!target) {
|
||||||
|
|
|
@ -154,6 +154,10 @@ export class StateStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
applySync(sync: SyncCompleteData) {
|
applySync(sync: SyncCompleteData) {
|
||||||
|
if (sync.clear_state && this.rooms.size > 0) {
|
||||||
|
console.info("Clearing state store as sync told to reset and there are rooms in the store")
|
||||||
|
this.clear()
|
||||||
|
}
|
||||||
const resyncRoomList = this.roomList.current.length === 0
|
const resyncRoomList = this.roomList.current.length === 0
|
||||||
const changedRoomListEntries = new Map<RoomID, RoomListEntry | null>()
|
const changedRoomListEntries = new Map<RoomID, RoomListEntry | null>()
|
||||||
for (const [roomID, data] of Object.entries(sync.rooms)) {
|
for (const [roomID, data] of Object.entries(sync.rooms)) {
|
||||||
|
|
|
@ -28,7 +28,7 @@ import {
|
||||||
UserID,
|
UserID,
|
||||||
} from "./mxtypes.ts"
|
} from "./mxtypes.ts"
|
||||||
|
|
||||||
export interface RPCCommand<T> {
|
interface BaseRPCCommand<T> {
|
||||||
command: string
|
command: string
|
||||||
request_id: number
|
request_id: number
|
||||||
data: T
|
data: T
|
||||||
|
@ -39,7 +39,7 @@ export interface TypingEventData {
|
||||||
user_ids: UserID[]
|
user_ids: UserID[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TypingEvent extends RPCCommand<TypingEventData> {
|
export interface TypingEvent extends BaseRPCCommand<TypingEventData> {
|
||||||
command: "typing"
|
command: "typing"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ export interface SendCompleteData {
|
||||||
error: string | null
|
error: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendCompleteEvent extends RPCCommand<SendCompleteData> {
|
export interface SendCompleteEvent extends BaseRPCCommand<SendCompleteData> {
|
||||||
command: "send_complete"
|
command: "send_complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,11 +58,11 @@ export interface EventsDecryptedData {
|
||||||
events: RawDBEvent[]
|
events: RawDBEvent[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventsDecryptedEvent extends RPCCommand<EventsDecryptedData> {
|
export interface EventsDecryptedEvent extends BaseRPCCommand<EventsDecryptedData> {
|
||||||
command: "events_decrypted"
|
command: "events_decrypted"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImageAuthTokenEvent extends RPCCommand<string> {
|
export interface ImageAuthTokenEvent extends BaseRPCCommand<string> {
|
||||||
command: "image_auth_token"
|
command: "image_auth_token"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,9 +85,11 @@ export interface SyncCompleteData {
|
||||||
rooms: Record<RoomID, SyncRoom>
|
rooms: Record<RoomID, SyncRoom>
|
||||||
left_rooms: RoomID[]
|
left_rooms: RoomID[]
|
||||||
account_data: Record<EventType, DBAccountData>
|
account_data: Record<EventType, DBAccountData>
|
||||||
|
since?: string
|
||||||
|
clear_state?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SyncCompleteEvent extends RPCCommand<SyncCompleteData> {
|
export interface SyncCompleteEvent extends BaseRPCCommand<SyncCompleteData> {
|
||||||
command: "sync_complete"
|
command: "sync_complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +105,7 @@ export type ClientState = {
|
||||||
homeserver_url: string
|
homeserver_url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientStateEvent extends RPCCommand<ClientState> {
|
export interface ClientStateEvent extends BaseRPCCommand<ClientState> {
|
||||||
command: "client_state"
|
command: "client_state"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,14 +116,26 @@ export interface SyncStatus {
|
||||||
last_sync?: number
|
last_sync?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SyncStatusEvent extends RPCCommand<SyncStatus> {
|
export interface SyncStatusEvent extends BaseRPCCommand<SyncStatus> {
|
||||||
command: "sync_status"
|
command: "sync_status"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InitCompleteEvent extends RPCCommand<void> {
|
export interface InitCompleteEvent extends BaseRPCCommand<void> {
|
||||||
command: "init_complete"
|
command: "init_complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RunIDEvent extends BaseRPCCommand<string> {
|
||||||
|
command: "run_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResponseCommand extends BaseRPCCommand<unknown> {
|
||||||
|
command: "response"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorCommand extends BaseRPCCommand<unknown> {
|
||||||
|
command: "error"
|
||||||
|
}
|
||||||
|
|
||||||
export type RPCEvent =
|
export type RPCEvent =
|
||||||
ClientStateEvent |
|
ClientStateEvent |
|
||||||
SyncStatusEvent |
|
SyncStatusEvent |
|
||||||
|
@ -130,4 +144,7 @@ export type RPCEvent =
|
||||||
EventsDecryptedEvent |
|
EventsDecryptedEvent |
|
||||||
SyncCompleteEvent |
|
SyncCompleteEvent |
|
||||||
ImageAuthTokenEvent |
|
ImageAuthTokenEvent |
|
||||||
InitCompleteEvent
|
InitCompleteEvent |
|
||||||
|
RunIDEvent
|
||||||
|
|
||||||
|
export type RPCCommand = RPCEvent | ResponseCommand | ErrorCommand
|
||||||
|
|
|
@ -23,6 +23,8 @@ export default class WSClient extends RPCClient {
|
||||||
#conn: WebSocket | null = null
|
#conn: WebSocket | null = null
|
||||||
#lastMessage: number = 0
|
#lastMessage: number = 0
|
||||||
#pingInterval: number | null = null
|
#pingInterval: number | null = null
|
||||||
|
#lastReceivedEvt: number = 0
|
||||||
|
#resumeRunID: string = ""
|
||||||
|
|
||||||
constructor(readonly addr: string) {
|
constructor(readonly addr: string) {
|
||||||
super()
|
super()
|
||||||
|
@ -31,8 +33,13 @@ export default class WSClient extends RPCClient {
|
||||||
start() {
|
start() {
|
||||||
try {
|
try {
|
||||||
this.#lastMessage = Date.now()
|
this.#lastMessage = Date.now()
|
||||||
console.info("Connecting to websocket", this.addr)
|
const params = new URLSearchParams({
|
||||||
this.#conn = new WebSocket(this.addr)
|
run_id: this.#resumeRunID,
|
||||||
|
last_received_event: this.#lastReceivedEvt.toString(),
|
||||||
|
}).toString()
|
||||||
|
const addr = this.#lastReceivedEvt && this.#resumeRunID ? `${this.addr}?${params}` : this.addr
|
||||||
|
console.info("Connecting to websocket", addr)
|
||||||
|
this.#conn = new WebSocket(addr)
|
||||||
this.#conn.onmessage = this.#onMessage
|
this.#conn.onmessage = this.#onMessage
|
||||||
this.#conn.onopen = this.#onOpen
|
this.#conn.onopen = this.#onOpen
|
||||||
this.#conn.onerror = this.#onError
|
this.#conn.onerror = this.#onError
|
||||||
|
@ -49,7 +56,13 @@ export default class WSClient extends RPCClient {
|
||||||
this.#conn?.close(4002, "Ping timeout")
|
this.#conn?.close(4002, "Ping timeout")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.send(JSON.stringify({ command: "ping", request_id: this.nextRequestID }))
|
this.send(JSON.stringify({
|
||||||
|
command: "ping",
|
||||||
|
data: {
|
||||||
|
last_received_id: this.#lastReceivedEvt,
|
||||||
|
},
|
||||||
|
request_id: this.nextRequestID,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
|
@ -72,7 +85,7 @@ export default class WSClient extends RPCClient {
|
||||||
|
|
||||||
#onMessage = (ev: MessageEvent) => {
|
#onMessage = (ev: MessageEvent) => {
|
||||||
this.#lastMessage = Date.now()
|
this.#lastMessage = Date.now()
|
||||||
let parsed: RPCCommand<unknown>
|
let parsed: RPCCommand
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(ev.data)
|
parsed = JSON.parse(ev.data)
|
||||||
if (!parsed.command) {
|
if (!parsed.command) {
|
||||||
|
@ -84,6 +97,11 @@ export default class WSClient extends RPCClient {
|
||||||
this.#conn?.close(1003, "Malformed JSON")
|
this.#conn?.close(1003, "Malformed JSON")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (parsed.request_id < 0) {
|
||||||
|
this.#lastReceivedEvt = parsed.request_id
|
||||||
|
} else if (parsed.command === "run_id") {
|
||||||
|
this.#resumeRunID = parsed.data
|
||||||
|
}
|
||||||
this.onCommand(parsed)
|
this.onCommand(parsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue