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() }