feat: add inbox
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
package inbox
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func DefaultStorePath() string {
|
||||
return filepath.Join(os.Getenv("HOME"), ".local", "state", "agent-notify", "inbox.jsonl")
|
||||
}
|
||||
|
||||
func NewStore(path string) Store {
|
||||
if path == "" {
|
||||
path = DefaultStorePath()
|
||||
}
|
||||
return Store{path: path}
|
||||
}
|
||||
|
||||
func (s Store) Path() string {
|
||||
return s.path
|
||||
}
|
||||
|
||||
func (s Store) Append(rec Record) error {
|
||||
if err := os.MkdirAll(filepath.Dir(s.path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.OpenFile(s.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if err := json.NewEncoder(f).Encode(rec); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Store) List() ([]Record, error) {
|
||||
f, err := os.Open(s.path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var records []Record
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
var rec Record
|
||||
if err := json.Unmarshal(scanner.Bytes(), &rec); err != nil {
|
||||
continue
|
||||
}
|
||||
records = append(records, rec)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (s Store) Pending() ([]Record, error) {
|
||||
records, err := s.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pending []Record
|
||||
for _, rec := range records {
|
||||
if rec.Status == StatusPending {
|
||||
pending = append(pending, rec)
|
||||
}
|
||||
}
|
||||
return pending, nil
|
||||
}
|
||||
|
||||
func (s Store) MarkDone(ids []string) (int, error) {
|
||||
idSet := makeSet(ids)
|
||||
return s.rewrite(func(rec Record) (Record, bool, bool) {
|
||||
if _, ok := idSet[rec.ID]; !ok {
|
||||
return rec, true, false
|
||||
}
|
||||
if rec.Status == StatusDone {
|
||||
return rec, true, false
|
||||
}
|
||||
rec.Status = StatusDone
|
||||
return rec, true, true
|
||||
})
|
||||
}
|
||||
|
||||
func (s Store) Remove(ids []string) (int, error) {
|
||||
idSet := makeSet(ids)
|
||||
return s.rewrite(func(rec Record) (Record, bool, bool) {
|
||||
if _, ok := idSet[rec.ID]; ok {
|
||||
return rec, false, true
|
||||
}
|
||||
return rec, true, false
|
||||
})
|
||||
}
|
||||
|
||||
func (s Store) ClearDone() (int, error) {
|
||||
return s.rewrite(func(rec Record) (Record, bool, bool) {
|
||||
if rec.Status == StatusDone {
|
||||
return rec, false, true
|
||||
}
|
||||
return rec, true, false
|
||||
})
|
||||
}
|
||||
|
||||
func (s Store) ClearAll() (int, error) {
|
||||
records, err := s.List()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(s.path), 0755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := os.WriteFile(s.path, nil, 0644); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(records), nil
|
||||
}
|
||||
|
||||
func (s Store) rewrite(fn func(Record) (Record, bool, bool)) (int, error) {
|
||||
records, err := s.List()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(s.path), 0755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp(filepath.Dir(s.path), "inbox-*.jsonl")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
changed := 0
|
||||
enc := json.NewEncoder(tmp)
|
||||
for _, rec := range records {
|
||||
next, keep, didChange := fn(rec)
|
||||
if didChange {
|
||||
changed++
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
if err := enc.Encode(next); err != nil {
|
||||
tmp.Close()
|
||||
os.Remove(tmpPath)
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return 0, err
|
||||
}
|
||||
if err := os.Rename(tmpPath, s.path); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return 0, err
|
||||
}
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
func makeSet(values []string) map[string]struct{} {
|
||||
set := make(map[string]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
set[value] = struct{}{}
|
||||
}
|
||||
return set
|
||||
}
|
||||
Reference in New Issue
Block a user