Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.
Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.
Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.
Expected source tree: 0a4a7528e9.
78 lines
2.4 KiB
Go
78 lines
2.4 KiB
Go
package services
|
|
|
|
import (
|
|
"agent-desk/internal/models"
|
|
"agent-desk/internal/pkg/constants"
|
|
"agent-desk/internal/pkg/enums"
|
|
"agent-desk/internal/repositories"
|
|
"github.com/mlogclub/simple/sqls"
|
|
"log/slog"
|
|
"slices"
|
|
"time"
|
|
)
|
|
|
|
const agentPresenceLeaseTTL = 90 * time.Second
|
|
const agentPresenceDisconnectGrace = 15 * time.Second
|
|
const agentPresenceRenewInterval = 15 * time.Second
|
|
|
|
func (s *wsService) renewAgentPresence(session *ClientSession) {
|
|
if session == nil || session.Closed.Load() || session.Principal == nil || (session.Role != realtimeRoleAdmin && session.Role != realtimeRoleNotification) {
|
|
return
|
|
}
|
|
session.presenceMu.Lock()
|
|
dispatchPending := false
|
|
defer func() {
|
|
session.presenceMu.Unlock()
|
|
// Finish the presence write before dispatching, and keep ownership in
|
|
// this call rather than launching an untracked database goroutine.
|
|
if dispatchPending {
|
|
_, _ = ConversationDispatchService.DispatchPendingConversations(0)
|
|
}
|
|
}()
|
|
if session.PresenceEnded {
|
|
return
|
|
}
|
|
principal := session.Principal
|
|
if principal.UserType != enums.UserTypeEmployee || !slices.Contains(principal.Permissions, constants.PermissionConversationView.Code) {
|
|
return
|
|
}
|
|
now := time.Now()
|
|
last := session.PresenceRenewedAt.Load()
|
|
if now.Unix()-last < int64(agentPresenceRenewInterval/time.Second) {
|
|
return
|
|
}
|
|
if !session.PresenceRenewedAt.CompareAndSwap(last, now.Unix()) {
|
|
return
|
|
}
|
|
// The internal ID is generated independently per connection, across processes.
|
|
expires := now.Add(agentPresenceLeaseTTL)
|
|
if principal.ExpiresAt.Before(expires) {
|
|
expires = principal.ExpiresAt
|
|
}
|
|
lease := models.AgentPresenceLease{ID: session.PresenceLeaseID, UserID: principal.UserID, LoginSessionID: principal.LoginSessionID, ExpiresAt: expires}
|
|
if lease.ID == "" {
|
|
return
|
|
}
|
|
if err := repositories.SupportAvailabilityRepository.RenewPresence(sqls.DB(), &lease); err != nil {
|
|
slog.Warn("support presence renewal failed")
|
|
return
|
|
}
|
|
if last == 0 {
|
|
dispatchPending = true
|
|
}
|
|
}
|
|
func (s *wsService) endAgentPresence(session *ClientSession) {
|
|
if session == nil {
|
|
return
|
|
}
|
|
session.presenceMu.Lock()
|
|
defer session.presenceMu.Unlock()
|
|
session.PresenceEnded = true
|
|
if session.PresenceLeaseID == "" || session.PresenceRenewedAt.Load() == 0 {
|
|
return
|
|
}
|
|
if err := repositories.SupportAvailabilityRepository.EndPresence(sqls.DB(), session.PresenceLeaseID, time.Now().Add(agentPresenceDisconnectGrace)); err != nil {
|
|
slog.Warn("support presence disconnect update failed")
|
|
}
|
|
}
|