From 78fe4481099fcf6666f1a259e38d9d828a253347 Mon Sep 17 00:00:00 2001 From: SayaAndy Date: Sat, 30 Aug 2025 00:36:46 +0700 Subject: feat: add persistent storage for likes --- internal/router/client-cache.go | 99 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) (limited to 'internal') diff --git a/internal/router/client-cache.go b/internal/router/client-cache.go index 7d276b1..56cd595 100644 --- a/internal/router/client-cache.go +++ b/internal/router/client-cache.go @@ -1,31 +1,126 @@ package router import ( + "database/sql" "encoding/base64" + "fmt" "log/slog" + "strings" "sync" "golang.org/x/crypto/argon2" ) +type PageLike struct { + PageRef string + UserId string +} + type ClientCache struct { hashMap map[string]string mutexLikeMap map[string]*sync.Mutex mutexHashMap map[string]*sync.Mutex likePageMap map[string]map[string]struct{} salt []byte + db *sql.DB } var CCache *ClientCache -func NewClientCache(salt []byte) *ClientCache { +func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) { + tx, err := db.Begin() + if err != nil { + return nil, fmt.Errorf("fail to init transaction with db to fill cache: %w", err) + } + + rows, err := tx.Query("select * from blog_likes;") + if err != nil { + tx.Rollback() + return nil, fmt.Errorf("fail to query db for blog_likes to fill cache: %w", err) + } + + likePageMap := make(map[string]map[string]struct{}) + + for rows.Next() { + pageRef := make([]byte, 32) + userId := make([]byte, 32) + if err = rows.Scan(&pageRef, &userId); err != nil { + tx.Rollback() + return nil, fmt.Errorf("fail scanning blog_likes to fill cache: %w", err) + } + pageRefString := string(pageRef) + userIdString := base64.RawStdEncoding.EncodeToString(userId) + if _, ok := likePageMap[pageRefString]; !ok { + likePageMap[pageRefString] = make(map[string]struct{}) + } + likePageMap[pageRefString][userIdString] = struct{}{} + } + + if err = tx.Commit(); err != nil { + return nil, fmt.Errorf("fail to commit transaction in db: %w", err) + } + return &ClientCache{ hashMap: make(map[string]string), mutexLikeMap: make(map[string]*sync.Mutex), mutexHashMap: make(map[string]*sync.Mutex), - likePageMap: make(map[string]map[string]struct{}), + likePageMap: likePageMap, salt: salt, + db: db, + }, nil +} + +func (c *ClientCache) Close() error { + tx, err := c.db.Begin() + if err != nil { + return fmt.Errorf("fail to init transaction with db to dump cache: %w", err) + } + + userIdBytes := make(map[string][]byte) + + sqlStatement := fmt.Sprintf(` + INSERT OR IGNORE INTO blog_likes (page_ref, user_id) + VALUES %s(?, ?); + `, strings.Repeat("(?, ?), ", 99)) + sqlStatementVars := make([]interface{}, 0, 200) + + for pageRef, userSet := range c.likePageMap { + pageRefBytes := []byte(pageRef) + + for userId := range userSet { + if _, ok := userIdBytes[userId]; !ok { + userIdBytes[userId], err = base64.RawStdEncoding.DecodeString(userId) + if err != nil { + slog.Warn("couldn't parse one of user hashes into bytes back", slog.String("hash", userId), slog.String("error", err.Error())) + continue + } + } + + sqlStatementVars = append(sqlStatementVars, interface{}(pageRefBytes), interface{}(userIdBytes[userId])) + if len(sqlStatementVars) < 200 { + continue + } + + if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { + slog.Warn("couldn't insert blog like pairs into db", slog.String("error", err.Error())) + } + + sqlStatementVars = make([]interface{}, 0, 200) + } } + + if len(sqlStatementVars) > 0 { + sqlStatement = fmt.Sprintf(` + INSERT OR IGNORE INTO blog_likes (page_ref, user_id) + VALUES %s(?, ?); + `, strings.Repeat("(?, ?), ", len(sqlStatementVars)/2-1)) + + if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { + slog.Warn("couldn't insert blog like pairs into db", slog.String("error", err.Error())) + } + } + + return tx.Commit() } func (c *ClientCache) GetHash(id string) string { -- cgit v1.3.1+13