Release 3.8.2.5: cheaper idle UI poll and tighter hot paths.
ci / test (macos-latest) (push) Waiting to run
ci / test (ubuntu-latest) (push) Waiting to run
ci / test (windows-latest) (push) Waiting to run

Delta getState by rev, pause when hidden, cancellable background loops,
hostCache bounds, unlock PollUI around engine status, corebin singleflight,
PingAll worker pool.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
M4
2026-08-01 16:13:59 +03:00
co-authored by Cursor
parent ff5d87ab9f
commit 2ad376b49e
25 changed files with 277 additions and 67 deletions
+91 -19
View File
@@ -3,6 +3,7 @@ package apphost
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
@@ -39,6 +40,8 @@ type App struct {
OpenURL func(string) error
OnAfterUpdate func()
APIToken string
ctx context.Context
cancel context.CancelFunc
}
type UIState struct {
@@ -63,6 +66,8 @@ type UIState struct {
ConnectOnLaunch bool `json:"connect_on_launch"`
AutoReconnect bool `json:"auto_reconnect"`
LogTail string `json:"log_tail,omitempty"`
Rev string `json:"rev,omitempty"`
Unchanged bool `json:"unchanged,omitempty"`
}
type PingBestResult struct {
@@ -74,10 +79,13 @@ type PingBestResult struct {
}
func New(mgr *core.Manager, cfgPath string, logBuf *logbuf.Buffer) *App {
ctx, cancel := context.WithCancel(context.Background())
return &App{
Mgr: mgr,
CfgPath: cfgPath,
LogBuf: logBuf,
ctx: ctx,
cancel: cancel,
UpdateStatus: update.Status{
Current: update.DisplayVersion(),
ManifestURL: update.DefaultManifestURL,
@@ -85,13 +93,40 @@ func New(mgr *core.Manager, cfgPath string, logBuf *logbuf.Buffer) *App {
}
}
// Shutdown cancels background loops (dock icon / watchdog). Safe to call more than once.
func (a *App) Shutdown() {
if a == nil || a.cancel == nil {
return
}
a.cancel()
}
func (a *App) GetState() (UIState, error) {
return a.getState(false)
return a.GetStateSince("")
}
// GetStateSince returns a full poll snapshot, or {rev, unchanged:true} when since
// matches the current fingerprint (cheap idle HTTP polls).
func (a *App) GetStateSince(since string) (UIState, error) {
out, err := a.getState(false)
if err != nil {
return out, err
}
out.Rev = fingerprintState(out)
if since != "" && since == out.Rev {
return UIState{Rev: out.Rev, Unchanged: true}, nil
}
return out, nil
}
// GetEditState returns full active proxy / hy2 secrets for the editor form (not for polls).
func (a *App) GetEditState() (UIState, error) {
return a.getState(true)
out, err := a.getState(true)
if err != nil {
return out, err
}
out.Rev = fingerprintState(out)
return out, nil
}
func (a *App) getState(includeSecrets bool) (UIState, error) {
@@ -155,6 +190,23 @@ func (a *App) getState(includeSecrets bool) (UIState, error) {
return out, nil
}
func fingerprintState(s UIState) string {
var b strings.Builder
b.Grow(256)
fmt.Fprintf(&b, "%t|%s|%s|%s|%t|%s|%t|%t|%t|%s|",
s.Connected, s.Profile, s.ActiveProfile, s.Protocol, s.CoreReady, s.LastError,
s.SystemProxy, s.ConnectOnLaunch, s.AutoReconnect, s.Subscription)
fmt.Fprintf(&b, "%s|%s|%t|", s.Update.Latest, s.Update.Current, s.Update.Available)
for _, p := range s.Profiles {
fmt.Fprintf(&b, "%s,%s,%s,%t;", p.Name, p.Protocol, p.Host, p.Active)
}
for _, p := range s.Pings {
fmt.Fprintf(&b, "%s,%t,%t,%d,%s;", p.Name, p.OK, p.Soft, p.Ms, p.Error)
}
sum := sha256.Sum256([]byte(b.String()))
return hex.EncodeToString(sum[:8])
}
func trimLogTail(s string, max int) string {
if max <= 0 || len(s) <= max {
return s
@@ -318,7 +370,11 @@ func (a *App) StartBackground() {
prefs := a.Mgr.Prefs()
if prefs.ConnectOnLaunch {
go func() {
time.Sleep(1200 * time.Millisecond)
select {
case <-a.ctx.Done():
return
case <-time.After(1200 * time.Millisecond):
}
if a.Mgr.Status().Connected {
return
}
@@ -331,29 +387,45 @@ func (a *App) dockIconLoop() {
dockicon.SetConnected(false)
var last bool
first := true
t := time.NewTicker(400 * time.Millisecond)
defer t.Stop()
for {
time.Sleep(400 * time.Millisecond)
up := a.Mgr.Status().Connected
if first || up != last {
first = false
last = up
dockicon.SetConnected(up)
select {
case <-a.ctx.Done():
return
case <-t.C:
up := a.Mgr.Status().Connected
if first || up != last {
first = false
last = up
dockicon.SetConnected(up)
}
}
}
}
func (a *App) watchdogLoop() {
t := time.NewTicker(2 * time.Second)
defer t.Stop()
for {
time.Sleep(2 * time.Second)
if !a.Mgr.TakeUnexpectedDrop() {
continue
select {
case <-a.ctx.Done():
return
case <-t.C:
if !a.Mgr.TakeUnexpectedDrop() {
continue
}
prefs := a.Mgr.Prefs()
if !prefs.AutoReconnect {
continue
}
select {
case <-a.ctx.Done():
return
case <-time.After(800 * time.Millisecond):
}
_ = a.Connect()
}
prefs := a.Mgr.Prefs()
if !prefs.AutoReconnect {
continue
}
time.Sleep(800 * time.Millisecond)
_ = a.Connect()
}
}
@@ -602,7 +674,7 @@ func arg[T any](args []json.RawMessage, i int, def T) T {
func (a *App) dispatch(name string, args []json.RawMessage) (any, error) {
switch name {
case "getState":
return a.GetState()
return a.GetStateSince(arg(args, 0, ""))
case "getEditState":
return a.GetEditState()
case "connect":
+27
View File
@@ -50,6 +50,33 @@ func TestHTTPBridgeGetState(t *testing.T) {
}
}
func TestHTTPBridgeGetStateUnchanged(t *testing.T) {
cfg := &config.Config{
Active: "demo",
Profiles: []config.Profile{
{Name: "demo", Protocol: config.ProtocolNaive, Proxy: "https://u:p@example.com"},
},
}
mgr, err := core.NewManager("", cfg, logbuf.New(0))
if err != nil {
t.Fatal(err)
}
a := New(mgr, "", logbuf.New(0))
a.APIToken = "test-token"
full, err := a.GetStateSince("")
if err != nil || full.Unchanged || full.Rev == "" {
t.Fatalf("full: unchanged=%v rev=%q err=%v", full.Unchanged, full.Rev, err)
}
again, err := a.GetStateSince(full.Rev)
if err != nil || !again.Unchanged || again.Rev != full.Rev {
t.Fatalf("delta: %+v err=%v", again, err)
}
if len(again.Profiles) != 0 {
t.Fatalf("unchanged payload should omit profiles, got %d", len(again.Profiles))
}
}
func TestHTTPBridgeRejectsBadToken(t *testing.T) {
cfg := &config.Config{}
mgr, err := core.NewManager("", cfg, logbuf.New(0))