package ironnotify import ( "context" "errors" "fmt" "sync" ) // Errors var ( ErrNotInitialized = errors.New("ironnotify: client not initialized") ErrTitleRequired = errors.New("ironnotify: notification title is required") ErrAPIKeyRequired = errors.New("ironnotify: API key is required") ) // Client is the IronNotify client. type Client struct { options Options transport *Transport queue *OfflineQueue isOnline bool connectionState ConnectionState onNotification NotificationHandler onUnreadCount UnreadCountHandler onConnectionChange ConnectionStateHandler mu sync.RWMutex } // Global client instance var globalClient *Client var globalMu sync.RWMutex // NewClient creates a new IronNotify client. func NewClient(options Options) (*Client, error) { if options.APIKey == "" { return nil, ErrAPIKeyRequired } opts := options.WithDefaults() client := &Client{ options: opts, transport: NewTransport(opts.APIBaseURL, opts.APIKey, opts.HTTPTimeout, opts.Debug), isOnline: true, connectionState: StateDisconnected, } if opts.EnableOfflineQueue { client.queue = NewOfflineQueue(opts.MaxOfflineQueueSize, opts.Debug) } if opts.Debug { fmt.Println("[IronNotify] Client initialized") } return client, nil } // Init initializes the global client. func Init(apiKey string, opts ...func(*Options)) error { options := DefaultOptions() options.APIKey = apiKey for _, opt := range opts { opt(&options) } client, err := NewClient(options) if err != nil { return err } globalMu.Lock() globalClient = client globalMu.Unlock() return nil } // WithDebug enables debug mode. func WithDebug(debug bool) func(*Options) { return func(o *Options) { o.Debug = debug } } // WithAPIBaseURL sets the API base URL. func WithAPIBaseURL(url string) func(*Options) { return func(o *Options) { o.APIBaseURL = url } } // WithOfflineQueue enables or disables the offline queue. func WithOfflineQueue(enabled bool) func(*Options) { return func(o *Options) { o.EnableOfflineQueue = enabled } } // getGlobalClient returns the global client. func getGlobalClient() (*Client, error) { globalMu.RLock() defer globalMu.RUnlock() if globalClient == nil { return nil, ErrNotInitialized } return globalClient, nil } // Notify sends a notification using the global client. func Notify(eventType, title string, opts ...NotifyOption) SendResult { client, err := getGlobalClient() if err != nil { return SendResult{Success: false, Error: err.Error()} } return client.Notify(eventType, title, opts...) } // NotifyOption is a function that modifies a NotificationPayload. type NotifyOption func(*NotificationPayload) // WithMessage sets the message. func WithMessage(message string) NotifyOption { return func(p *NotificationPayload) { p.Message = message } } // WithSeverity sets the severity. func WithSeverity(severity SeverityLevel) NotifyOption { return func(p *NotificationPayload) { p.Severity = severity } } // WithUserID sets the user ID. func WithUserID(userID string) NotifyOption { return func(p *NotificationPayload) { p.UserID = userID } } // WithMetadataOpt sets metadata. func WithMetadataOpt(metadata map[string]any) NotifyOption { return func(p *NotificationPayload) { p.Metadata = metadata } } // Notify sends a notification. func (c *Client) Notify(eventType, title string, opts ...NotifyOption) SendResult { payload := &NotificationPayload{ EventType: eventType, Title: title, Severity: SeverityInfo, } for _, opt := range opts { opt(payload) } return c.SendPayload(payload) } // Event creates an event builder using the global client. func Event(eventType string) *EventBuilder { client, err := getGlobalClient() if err != nil { // Return a builder that will fail on send return &EventBuilder{eventType: eventType} } return client.Event(eventType) } // Event creates an event builder. func (c *Client) Event(eventType string) *EventBuilder { return newEventBuilder(c, eventType) } // SendPayload sends a notification payload. func (c *Client) SendPayload(payload *NotificationPayload) SendResult { result := c.transport.Send(context.Background(), payload) if !result.Success && c.options.EnableOfflineQueue && c.queue != nil { c.queue.Add(*payload) c.mu.Lock() c.isOnline = false c.mu.Unlock() return SendResult{ Success: result.Success, NotificationID: result.NotificationID, Error: result.Error, Queued: true, } } return result } // GetNotifications retrieves notifications. func (c *Client) GetNotifications(limit, offset int, unreadOnly bool) ([]Notification, error) { return c.transport.GetNotifications(context.Background(), limit, offset, unreadOnly) } // GetNotificationsContext retrieves notifications with context. func (c *Client) GetNotificationsContext(ctx context.Context, limit, offset int, unreadOnly bool) ([]Notification, error) { return c.transport.GetNotifications(ctx, limit, offset, unreadOnly) } // GetUnreadCount returns the unread notification count. func (c *Client) GetUnreadCount() (int, error) { return c.transport.GetUnreadCount(context.Background()) } // MarkAsRead marks a notification as read. func (c *Client) MarkAsRead(notificationID string) error { return c.transport.MarkAsRead(context.Background(), notificationID) } // MarkAllAsRead marks all notifications as read. func (c *Client) MarkAllAsRead() error { return c.transport.MarkAllAsRead(context.Background()) } // OnNotification sets the notification handler. func (c *Client) OnNotification(handler NotificationHandler) { c.mu.Lock() defer c.mu.Unlock() c.onNotification = handler } // OnUnreadCountChange sets the unread count change handler. func (c *Client) OnUnreadCountChange(handler UnreadCountHandler) { c.mu.Lock() defer c.mu.Unlock() c.onUnreadCount = handler } // OnConnectionStateChange sets the connection state change handler. func (c *Client) OnConnectionStateChange(handler ConnectionStateHandler) { c.mu.Lock() defer c.mu.Unlock() c.onConnectionChange = handler } // ConnectionState returns the current connection state. func (c *Client) ConnectionState() ConnectionState { c.mu.RLock() defer c.mu.RUnlock() return c.connectionState } // Connect connects to real-time notifications. func (c *Client) Connect() { c.setConnectionState(StateConnected) if c.options.Debug { fmt.Println("[IronNotify] Connected (WebSocket not implemented)") } } // Disconnect disconnects from real-time notifications. func (c *Client) Disconnect() { c.setConnectionState(StateDisconnected) } // SubscribeToUser subscribes to a user's notifications. func (c *Client) SubscribeToUser(userID string) { if c.options.Debug { fmt.Printf("[IronNotify] Subscribed to user: %s\n", userID) } } // SubscribeToApp subscribes to app-wide notifications. func (c *Client) SubscribeToApp() { if c.options.Debug { fmt.Println("[IronNotify] Subscribed to app notifications") } } // Flush flushes the offline queue. func (c *Client) Flush() { if c.queue == nil || c.queue.IsEmpty() { return } if !c.transport.IsOnline(context.Background()) { return } c.mu.Lock() c.isOnline = true c.mu.Unlock() notifications := c.queue.GetAll() for i := len(notifications) - 1; i >= 0; i-- { result := c.transport.Send(context.Background(), ¬ifications[i]) if result.Success { c.queue.Remove(i) } else { break } } } // FlushContext flushes the offline queue with context. func (c *Client) FlushContext(ctx context.Context) { if c.queue == nil || c.queue.IsEmpty() { return } if !c.transport.IsOnline(ctx) { return } c.mu.Lock() c.isOnline = true c.mu.Unlock() notifications := c.queue.GetAll() for i := len(notifications) - 1; i >= 0; i-- { result := c.transport.Send(ctx, ¬ifications[i]) if result.Success { c.queue.Remove(i) } else { break } } } // Close closes the client. func (c *Client) Close() { c.Disconnect() c.transport.Close() } // setConnectionState sets the connection state and calls the handler. func (c *Client) setConnectionState(state ConnectionState) { c.mu.Lock() c.connectionState = state handler := c.onConnectionChange c.mu.Unlock() if handler != nil { handler(state) } } // Flush flushes the global client's offline queue. func Flush() { client, err := getGlobalClient() if err != nil { return } client.Flush() } // Close closes the global client. func Close() { globalMu.Lock() defer globalMu.Unlock() if globalClient != nil { globalClient.Close() globalClient = nil } }