package domain import ( "sync" "time" ) // NotificationChannel represents notification channels for offline parties type NotificationChannel struct { Email string Phone string PushToken string } // HasAnyChannel returns true if any notification channel is configured func (nc *NotificationChannel) HasAnyChannel() bool { return nc != nil && (nc.Email != "" || nc.Phone != "" || nc.PushToken != "") } // RegisteredParty represents a party registered with the router type RegisteredParty struct { PartyID string Role string // persistent, delegate, temporary Version string RegisteredAt time.Time LastSeen time.Time Online bool // Whether the party is currently connected Notification *NotificationChannel // Optional notification channels for offline mode } // IsOfflineMode returns true if the party operates in offline mode (has notification channels) func (p *RegisteredParty) IsOfflineMode() bool { return p.Notification != nil && p.Notification.HasAnyChannel() } // PartyRegistry manages registered parties type PartyRegistry struct { parties map[string]*RegisteredParty mu sync.RWMutex } // NewPartyRegistry creates a new party registry func NewPartyRegistry() *PartyRegistry { return &PartyRegistry{ parties: make(map[string]*RegisteredParty), } } // Register registers a party func (r *PartyRegistry) Register(partyID, role, version string) *RegisteredParty { return r.RegisterWithNotification(partyID, role, version, nil) } // RegisterWithNotification registers a party with optional notification channels func (r *PartyRegistry) RegisterWithNotification(partyID, role, version string, notification *NotificationChannel) *RegisteredParty { r.mu.Lock() defer r.mu.Unlock() now := time.Now() party := &RegisteredParty{ PartyID: partyID, Role: role, Version: version, RegisteredAt: now, LastSeen: now, Online: true, Notification: notification, } r.parties[partyID] = party return party } // Get retrieves a registered party func (r *PartyRegistry) Get(partyID string) (*RegisteredParty, bool) { r.mu.RLock() defer r.mu.RUnlock() party, exists := r.parties[partyID] return party, exists } // GetAll returns all registered parties func (r *PartyRegistry) GetAll() []*RegisteredParty { r.mu.RLock() defer r.mu.RUnlock() parties := make([]*RegisteredParty, 0, len(r.parties)) for _, party := range r.parties { parties = append(parties, party) } return parties } // GetByRole returns registered parties filtered by role func (r *PartyRegistry) GetByRole(role string) []*RegisteredParty { r.mu.RLock() defer r.mu.RUnlock() parties := make([]*RegisteredParty, 0) for _, party := range r.parties { if party.Role == role { parties = append(parties, party) } } return parties } // UpdateLastSeen updates the last seen timestamp func (r *PartyRegistry) UpdateLastSeen(partyID string) { r.mu.Lock() defer r.mu.Unlock() if party, exists := r.parties[partyID]; exists { party.LastSeen = time.Now() } } // Unregister removes a party from the registry func (r *PartyRegistry) Unregister(partyID string) { r.mu.Lock() defer r.mu.Unlock() delete(r.parties, partyID) } // Count returns the number of registered parties func (r *PartyRegistry) Count() int { r.mu.RLock() defer r.mu.RUnlock() return len(r.parties) } // SetOnline sets the online status of a party func (r *PartyRegistry) SetOnline(partyID string, online bool) { r.mu.Lock() defer r.mu.Unlock() if party, exists := r.parties[partyID]; exists { party.Online = online if online { party.LastSeen = time.Now() } } } // IsOnline checks if a party is currently online func (r *PartyRegistry) IsOnline(partyID string) bool { r.mu.RLock() defer r.mu.RUnlock() if party, exists := r.parties[partyID]; exists { return party.Online } return false } // GetOnlineParties returns all online parties func (r *PartyRegistry) GetOnlineParties() []*RegisteredParty { r.mu.RLock() defer r.mu.RUnlock() parties := make([]*RegisteredParty, 0) for _, party := range r.parties { if party.Online { parties = append(parties, party) } } return parties } // GetOfflineParties returns all parties that are offline (have notification channels but not connected) func (r *PartyRegistry) GetOfflineParties() []*RegisteredParty { r.mu.RLock() defer r.mu.RUnlock() parties := make([]*RegisteredParty, 0) for _, party := range r.parties { if !party.Online && party.IsOfflineMode() { parties = append(parties, party) } } return parties } // MarkStalePartiesOffline marks parties as offline if they haven't sent a heartbeat within the timeout // Returns the list of parties that were marked offline func (r *PartyRegistry) MarkStalePartiesOffline(timeout time.Duration) []*RegisteredParty { r.mu.Lock() defer r.mu.Unlock() now := time.Now() staleParties := make([]*RegisteredParty, 0) for _, party := range r.parties { if party.Online && now.Sub(party.LastSeen) > timeout { party.Online = false staleParties = append(staleParties, party) } } return staleParties } // Heartbeat updates the last seen timestamp and marks the party as online func (r *PartyRegistry) Heartbeat(partyID string) bool { r.mu.Lock() defer r.mu.Unlock() if party, exists := r.parties[partyID]; exists { party.LastSeen = time.Now() party.Online = true return true } return false }