Skip to content
quarry Artifact [29a1d4355f]
Login

Artifact [29a1d4355f]

Artifact 29a1d4355f485ee3a6093306ac9f0b72bd93c9234154c87070be942d7228149d:


// chatread.go stores chat read-state (RFC 0011 §3): the highest chat
// sequence an account has seen per repository. It is the forum_read
// shape narrowed to one row per (repo, account) instead of one per
// thread, because chat has no threads to track separately — a channel
// is one stream, addressed by JetStream sequence, and "read" is simply
// "has seen up to seq N".
//
// Like forum_read this is forge-local social state: per-account,
// private, never part of the artifact DAG (RFC 0001 §11 disposability —
// wiping it loses convenience, not truth).
package oltp

import "time"

const chatReadSchema = `
CREATE TABLE IF NOT EXISTS chat_read (
	repo    TEXT NOT NULL,
	account TEXT NOT NULL,
	seq     INTEGER NOT NULL, -- highest seq the account has seen
	at      TEXT NOT NULL,
	PRIMARY KEY (repo, account)
);`

// MarkChatRead records that an account has seen a repository's chat
// channel up to seq — but only when that advances the pointer. A
// re-visit with nothing new since the last mark is a read, not a
// write, the same guard MarkForumRead uses and for the same reason:
// opening a chat channel is the single most common act on it and
// SQLite has one writer.
func (d *DB) MarkChatRead(repo, account string, seq uint64) error {
	if account == "" {
		return nil
	}
	_, err := d.db.Exec(`INSERT INTO chat_read (repo, account, seq, at)
		VALUES (?, ?, ?, ?)
		ON CONFLICT (repo, account) DO UPDATE SET seq = excluded.seq, at = excluded.at
		WHERE chat_read.seq < excluded.seq`,
		repo, account, seq, time.Now().UTC().Format(time.RFC3339Nano))
	return err
}

// ChatReadSeq answers the highest seq an account has marked read in one
// repository's channel — 0 for an account that has never marked it
// (which reads as "everything visible is unread", the correct answer
// for a channel nobody has opened). Anonymous has no read-state.
func (d *DB) ChatReadSeq(repo, account string) uint64 {
	if account == "" {
		return 0
	}
	var seq uint64
	d.db.QueryRow(`SELECT seq FROM chat_read WHERE repo = ? AND account = ?`,
		repo, account).Scan(&seq)
	return seq
}

// ChatReadAll answers every repository's read pointer for one account,
// keyed by repo — the shape chat.unread needs to answer "how much is
// unread everywhere" in one query instead of one per repository.
func (d *DB) ChatReadAll(account string) (map[string]uint64, error) {
	out := map[string]uint64{}
	if account == "" {
		return out, nil
	}
	rows, err := d.db.Query(`SELECT repo, seq FROM chat_read WHERE account = ?`, account)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	for rows.Next() {
		var repo string
		var seq uint64
		if err := rows.Scan(&repo, &seq); err != nil {
			return nil, err
		}
		out[repo] = seq
	}
	return out, rows.Err()
}