feat(store): keep an in-memory newest-first message store
Provide a concurrency-safe, per-topic message store with id-based deduplication, delete, clear, and a search predicate.
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
// Package store keeps an in-memory, newest-first view of ntfy messages.
|
||||
package store
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Attachment is a file linked from a message.
|
||||
type Attachment struct {
|
||||
Name string
|
||||
URL string
|
||||
Type string
|
||||
Size int64
|
||||
}
|
||||
|
||||
// Message is one delivered ntfy notification.
|
||||
type Message struct {
|
||||
ID string
|
||||
Time int64
|
||||
Expires int64
|
||||
Topic string
|
||||
Title string
|
||||
Body string
|
||||
Tags []string
|
||||
Priority int
|
||||
Click string
|
||||
Attachment *Attachment
|
||||
Actions string
|
||||
}
|
||||
|
||||
// Store is safe for concurrent readers and writers.
|
||||
type Store struct {
|
||||
mu sync.Mutex
|
||||
byTopic map[string][]Message
|
||||
index map[string]string
|
||||
order []string
|
||||
}
|
||||
|
||||
// New returns an empty store.
|
||||
func New() *Store {
|
||||
return &Store{
|
||||
byTopic: map[string][]Message{},
|
||||
index: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
// Put inserts a message. A repeated id is ignored.
|
||||
func (s *Store) Put(m Message) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if m.ID != "" {
|
||||
if _, ok := s.index[m.ID]; ok {
|
||||
return
|
||||
}
|
||||
s.index[m.ID] = m.Topic
|
||||
}
|
||||
s.ensure(m.Topic)
|
||||
s.byTopic[m.Topic] = insertNewest(s.byTopic[m.Topic], m)
|
||||
}
|
||||
|
||||
// Delete removes the message with id.
|
||||
func (s *Store) Delete(id string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
topic, ok := s.index[id]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
list := s.byTopic[topic]
|
||||
for i, m := range list {
|
||||
if m.ID == id {
|
||||
s.byTopic[topic] = append(list[:i], list[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
delete(s.index, id)
|
||||
}
|
||||
|
||||
// Clear removes every message for a topic.
|
||||
func (s *Store) Clear(topic string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for _, m := range s.byTopic[topic] {
|
||||
if m.ID != "" {
|
||||
delete(s.index, m.ID)
|
||||
}
|
||||
}
|
||||
s.byTopic[topic] = nil
|
||||
}
|
||||
|
||||
// List returns a copy of one topic, newest first.
|
||||
func (s *Store) List(topic string) []Message {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
src := s.byTopic[topic]
|
||||
out := make([]Message, len(src))
|
||||
copy(out, src)
|
||||
return out
|
||||
}
|
||||
|
||||
// Topics returns topic names in the order they were first seen.
|
||||
func (s *Store) Topics() []string {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
out := make([]string, len(s.order))
|
||||
copy(out, s.order)
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Store) ensure(topic string) {
|
||||
if _, ok := s.byTopic[topic]; ok {
|
||||
return
|
||||
}
|
||||
s.byTopic[topic] = nil
|
||||
s.order = append(s.order, topic)
|
||||
}
|
||||
|
||||
func insertNewest(list []Message, m Message) []Message {
|
||||
i := 0
|
||||
for i < len(list) && list[i].Time > m.Time {
|
||||
i++
|
||||
}
|
||||
list = append(list, Message{})
|
||||
copy(list[i+1:], list[i:])
|
||||
list[i] = m
|
||||
return list
|
||||
}
|
||||
|
||||
// Match reports whether needle is contained in the message's visible fields.
|
||||
func Match(m Message, title, needle string) bool {
|
||||
needle = strings.ToLower(strings.TrimSpace(needle))
|
||||
if needle == "" {
|
||||
return true
|
||||
}
|
||||
hay := strings.ToLower(strings.Join([]string{
|
||||
title,
|
||||
m.Title,
|
||||
m.Body,
|
||||
m.ID,
|
||||
strings.Join(m.Tags, " "),
|
||||
}, " "))
|
||||
return strings.Contains(hay, needle)
|
||||
}
|
||||
Reference in New Issue
Block a user