Files
browser/platform/service/control.go
T
2026-07-11 14:56:10 +08:00

122 lines
4.0 KiB
Go

package service
import (
"errors"
"fmt"
"time"
"browser.local/platform/domain"
"browser.local/platform/repo"
"browser.local/platform/validator"
)
const (
defaultHeartbeatIntervalSeconds = 15
)
func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.RunControlHelloResult, error) {
hello = domain.CopyRunControlHello(hello)
if err := validator.ValidateRunControlHello(hello); err != nil {
return domain.RunControlHelloResult{}, err
}
stamp := svc.now()
endpoint := domain.RunEndpoint{
ID: hello.RunEndpointID,
DisplayName: hello.DisplayName,
Version: hello.Version,
Status: domain.RunEndpointStatusOnline,
Capabilities: domain.CopyStringSlice(hello.CapabilityReport.Capabilities),
Capacity: hello.Capacity,
LastHeartbeatAt: stamp,
}
if err := validator.ValidateRunEndpoint(endpoint); err != nil {
return domain.RunControlHelloResult{}, err
}
svc.controlMu.Lock()
defer svc.controlMu.Unlock()
if err := svc.upsertRunEndpoint(endpoint); err != nil {
return domain.RunControlHelloResult{}, err
}
sessionToken := svc.nextSessionToken(hello.RunEndpointID, stamp)
svc.runSessions[hello.RunEndpointID] = domain.RunControlSession{
RunEndpointID: hello.RunEndpointID,
SessionToken: sessionToken,
CapabilityFingerprint: hello.CapabilityReport.Fingerprint,
HeartbeatIntervalSeconds: defaultHeartbeatIntervalSeconds,
CreatedAt: stamp,
UpdatedAt: stamp,
}
return domain.CopyRunControlHelloResult(domain.RunControlHelloResult{
Accepted: true,
RunEndpointID: hello.RunEndpointID,
SessionToken: sessionToken,
ServerTime: stamp,
HeartbeatIntervalSeconds: defaultHeartbeatIntervalSeconds,
FeatureFlags: []string{"control.hello", "control.heartbeat"},
}), nil
}
func (svc *CoreService) AcceptRunHeartbeat(heartbeat domain.RunControlHeartbeat) (domain.RunControlHeartbeatResult, error) {
heartbeat = domain.CopyRunControlHeartbeat(heartbeat)
if err := validator.ValidateRunControlHeartbeat(heartbeat); err != nil {
return domain.RunControlHeartbeatResult{}, err
}
stamp := svc.now()
svc.controlMu.Lock()
defer svc.controlMu.Unlock()
session, exists := svc.runSessions[heartbeat.RunEndpointID]
if !exists || session.SessionToken != heartbeat.SessionToken {
return domain.RunControlHeartbeatResult{}, validationError("sessionToken is invalid")
}
endpoint, err := svc.store.RunEndpoints().Get(heartbeat.RunEndpointID)
if err != nil {
return domain.RunControlHeartbeatResult{}, err
}
endpoint.Version = heartbeat.Version
endpoint.Status = heartbeat.Status
endpoint.Capacity = heartbeat.Capacity
endpoint.LastHeartbeatAt = stamp
if err := validator.ValidateRunEndpoint(endpoint); err != nil {
return domain.RunControlHeartbeatResult{}, err
}
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
return domain.RunControlHeartbeatResult{}, err
}
refreshCapabilities := session.CapabilityFingerprint != heartbeat.CapabilityFingerprint
session.CapabilityFingerprint = heartbeat.CapabilityFingerprint
session.UpdatedAt = stamp
svc.runSessions[heartbeat.RunEndpointID] = session
return domain.CopyRunControlHeartbeatResult(domain.RunControlHeartbeatResult{
Accepted: true,
RunEndpointID: heartbeat.RunEndpointID,
NextHeartbeatSeconds: session.HeartbeatIntervalSeconds,
RefreshCapabilities: refreshCapabilities,
ServerTime: stamp,
}), nil
}
func (svc *CoreService) upsertRunEndpoint(endpoint domain.RunEndpoint) error {
if _, err := svc.store.RunEndpoints().Get(endpoint.ID); err != nil {
if errors.Is(err, repo.ErrNotFound) {
return svc.store.RunEndpoints().Create(endpoint)
}
return err
}
return svc.store.RunEndpoints().Update(endpoint)
}
func (svc *CoreService) nextSessionToken(runEndpointID string, stamp time.Time) string {
svc.runSessionSeq++
return fmt.Sprintf("session:%s:%d:%d", runEndpointID, stamp.UnixNano(), svc.runSessionSeq)
}