133 lines
3.7 KiB
Go
133 lines
3.7 KiB
Go
// Package memory is Keel's durable, cross-run memory port. Modes Record derived
|
|
// events and read them back with Recent; the AW-backed adapter persists them to
|
|
// an ActivityWatch bucket. Event-kind names are owned by the modes that write
|
|
// them — this package stays a generic event store.
|
|
package memory
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"keel/internal/aw"
|
|
)
|
|
|
|
// Event is one derived event Keel chooses to remember. Kind is the event type
|
|
// (e.g. "proposal_made"); Data carries pointers and small values, not copies of
|
|
// other tools' source of truth.
|
|
type Event struct {
|
|
Kind string
|
|
At time.Time
|
|
Data map[string]any
|
|
}
|
|
|
|
// Store records derived events and reads recent ones back by kind.
|
|
type Store interface {
|
|
Record(ctx context.Context, e Event) error
|
|
Recent(ctx context.Context, kind string, n int) ([]Event, error)
|
|
}
|
|
|
|
// nopStore is the no-memory fallback used when AW is unreachable or disabled.
|
|
type nopStore struct{}
|
|
|
|
// NewNop returns a Store that drops writes and returns no history.
|
|
func NewNop() Store { return nopStore{} }
|
|
|
|
func (nopStore) Record(context.Context, Event) error { return nil }
|
|
func (nopStore) Recent(context.Context, string, int) ([]Event, error) { return nil, nil }
|
|
|
|
// Fake is an in-memory Store for tests.
|
|
type Fake struct {
|
|
mu sync.Mutex
|
|
events []Event
|
|
}
|
|
|
|
// NewFake returns an empty in-memory Store.
|
|
func NewFake() *Fake { return &Fake{} }
|
|
|
|
func (f *Fake) Record(_ context.Context, e Event) error {
|
|
f.mu.Lock()
|
|
f.events = append(f.events, e)
|
|
f.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (f *Fake) Recent(_ context.Context, kind string, n int) ([]Event, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
var out []Event
|
|
for i := len(f.events) - 1; i >= 0 && len(out) < n; i-- {
|
|
if f.events[i].Kind == kind {
|
|
out = append(out, f.events[i])
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Events returns a snapshot of everything recorded, for test assertions.
|
|
func (f *Fake) Events() []Event {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return append([]Event(nil), f.events...)
|
|
}
|
|
|
|
// overfetch bounds how many recent bucket events awStore pulls before filtering
|
|
// by kind, because keel.events interleaves all kinds. Events older than this
|
|
// window are not seen — acceptable for "recent".
|
|
const overfetch = 200
|
|
|
|
// kindKey is the data field carrying the Event.Kind inside an AW event.
|
|
const kindKey = "_kind"
|
|
|
|
// AWClient is the subset of *aw.Client that awStore needs. Declaring it here
|
|
// lets tests inject a fake without HTTP; *aw.Client satisfies it structurally.
|
|
type AWClient interface {
|
|
Insert(ctx context.Context, bucketID string, e aw.Event) error
|
|
Recent(ctx context.Context, bucketID string, limit int) ([]aw.Event, error)
|
|
}
|
|
|
|
type awStore struct {
|
|
c AWClient
|
|
bucketID string
|
|
}
|
|
|
|
// NewAWStore returns a Store that persists events to the given AW bucket.
|
|
func NewAWStore(c AWClient, bucketID string) Store {
|
|
return &awStore{c: c, bucketID: bucketID}
|
|
}
|
|
|
|
func (s *awStore) Record(ctx context.Context, e Event) error {
|
|
data := make(map[string]any, len(e.Data)+1)
|
|
for k, v := range e.Data {
|
|
data[k] = v
|
|
}
|
|
data[kindKey] = e.Kind
|
|
return s.c.Insert(ctx, s.bucketID, aw.Event{Timestamp: e.At, Duration: 0, Data: data})
|
|
}
|
|
|
|
func (s *awStore) Recent(ctx context.Context, kind string, n int) ([]Event, error) {
|
|
if n <= 0 {
|
|
return nil, nil
|
|
}
|
|
raw, err := s.c.Recent(ctx, s.bucketID, overfetch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []Event
|
|
for _, ev := range raw {
|
|
if len(out) >= n {
|
|
break
|
|
}
|
|
if k, _ := ev.Data[kindKey].(string); k == kind {
|
|
data := make(map[string]any, len(ev.Data))
|
|
for dk, dv := range ev.Data {
|
|
if dk != kindKey {
|
|
data[dk] = dv
|
|
}
|
|
}
|
|
out = append(out, Event{Kind: kind, At: ev.Timestamp, Data: data})
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|