textmachine/backend/internal/store/stopmemory.go

55 lines
1.9 KiB
Go

package store
// stopmemory.go: the bank-stop presented memory (v16) — the surfaces the --verify-bank stop has already
// paid a stop for. Dumb storage like ruby.go: the clustering and the "is anything new" predicate live in
// the pipeline; this layer only accumulates and reads back.
//
// The set only GROWS. There is no delete on purpose: trimming it re-arms the stop on terms the owner has
// already been shown, which is the exact re-stop the memory exists to end.
// StopPresentedSurfaces returns the book's presented set as the pipeline compares it — a lookup of
// normalized surfaces. An empty table is an empty map: nothing has been presented.
func (s *Store) StopPresentedSurfaces(bookID string) (map[string]bool, error) {
ctx, cancel := opContext()
defer cancel()
rows, err := s.r.QueryContext(ctx, `SELECT surface FROM bank_stop_presented WHERE book_id = ?`, bookID)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]bool{}
for rows.Next() {
var surface string
if err := rows.Scan(&surface); err != nil {
return nil, err
}
out[surface] = true
}
return out, rows.Err()
}
// MarkStopPresented adds surfaces to the book's presented set, in one transaction. Idempotent
// (INSERT OR IGNORE): a stop re-presenting a surface it already paid for records nothing new.
func (s *Store) MarkStopPresented(bookID string, surfaces []string) error {
if len(surfaces) == 0 {
return nil
}
ctx, cancel := opContext()
defer cancel()
tx, err := s.w.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
for _, surface := range surfaces {
if surface == "" {
continue // a blank surface must not silently mark everything blank as seen
}
if _, err := tx.ExecContext(ctx, `
INSERT OR IGNORE INTO bank_stop_presented (book_id, surface) VALUES (?, ?)`,
bookID, surface); err != nil {
return err
}
}
return tx.Commit()
}