Add more QoL and high prio features
This commit is contained in:
249
cache.go
Normal file
249
cache.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type cacheEnvelope[T any] struct {
|
||||
SavedAt time.Time `json:"saved_at"`
|
||||
ContentHash string `json:"content_hash,omitempty"`
|
||||
Value T `json:"value"`
|
||||
}
|
||||
|
||||
type CachedGitHubService struct {
|
||||
remote GitHubService
|
||||
dir string
|
||||
maxAge time.Duration
|
||||
}
|
||||
|
||||
type cachedSnapshotService interface {
|
||||
CachedPullRequests(string, string, int, bool) ([]PullRequest, error)
|
||||
CachedPullRequest(string, string, int) (PRDetails, error)
|
||||
}
|
||||
|
||||
type liveGitHubService interface {
|
||||
LivePullRequests(context.Context, string, string, int, bool) ([]PullRequest, error)
|
||||
LivePullRequest(context.Context, string, string, int) (PRDetails, error)
|
||||
}
|
||||
|
||||
func NewCachedGitHubService(remote GitHubService, dir string, maxAge time.Duration) *CachedGitHubService {
|
||||
return &CachedGitHubService{remote: remote, dir: dir, maxAge: maxAge}
|
||||
}
|
||||
|
||||
func (c *CachedGitHubService) CachedPullRequests(
|
||||
owner, repo string, limit int, showAll bool,
|
||||
) ([]PullRequest, error) {
|
||||
var cached cacheEnvelope[[]PullRequest]
|
||||
savedAt, err := c.read(c.pullRequestsKey(owner, repo, limit, showAll), &cached)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cached.SavedAt = savedAt
|
||||
for i := range cached.Value {
|
||||
cached.Value[i].FromCache, cached.Value[i].CachedAt = true, cached.SavedAt
|
||||
}
|
||||
return cached.Value, nil
|
||||
}
|
||||
|
||||
func (c *CachedGitHubService) CachedPullRequest(owner, repo string, number int) (PRDetails, error) {
|
||||
var cached cacheEnvelope[PRDetails]
|
||||
savedAt, err := c.read(c.pullRequestKey(owner, repo, number), &cached)
|
||||
if err != nil {
|
||||
return PRDetails{}, err
|
||||
}
|
||||
cached.SavedAt = savedAt
|
||||
cached.Value.FromCache, cached.Value.CachedAt = true, cached.SavedAt
|
||||
return cached.Value, nil
|
||||
}
|
||||
|
||||
func (c *CachedGitHubService) ListPullRequests(
|
||||
ctx context.Context, owner, repo string, limit int, showAll bool,
|
||||
) ([]PullRequest, error) {
|
||||
prs, err := c.LivePullRequests(ctx, owner, repo, limit, showAll)
|
||||
if err == nil {
|
||||
return prs, nil
|
||||
}
|
||||
cached, cacheErr := c.CachedPullRequests(owner, repo, limit, showAll)
|
||||
if cacheErr != nil {
|
||||
return nil, fmt.Errorf("%w (cache unavailable: %v)", err, cacheErr)
|
||||
}
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
func (c *CachedGitHubService) GetPullRequest(
|
||||
ctx context.Context, owner, repo string, number int,
|
||||
) (PRDetails, error) {
|
||||
details, err := c.LivePullRequest(ctx, owner, repo, number)
|
||||
if err == nil {
|
||||
return details, nil
|
||||
}
|
||||
cached, cacheErr := c.CachedPullRequest(owner, repo, number)
|
||||
if cacheErr != nil {
|
||||
return PRDetails{}, fmt.Errorf("%w (cache unavailable: %v)", err, cacheErr)
|
||||
}
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
func (c *CachedGitHubService) LivePullRequests(
|
||||
ctx context.Context, owner, repo string, limit int, showAll bool,
|
||||
) ([]PullRequest, error) {
|
||||
prs, err := c.remote.ListPullRequests(ctx, owner, repo, limit, showAll)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range prs {
|
||||
prs[i].FromCache, prs[i].CachedAt = false, time.Time{}
|
||||
}
|
||||
_ = c.write(c.pullRequestsKey(owner, repo, limit, showAll), prs)
|
||||
return prs, nil
|
||||
}
|
||||
|
||||
func (c *CachedGitHubService) LivePullRequest(
|
||||
ctx context.Context, owner, repo string, number int,
|
||||
) (PRDetails, error) {
|
||||
details, err := c.remote.GetPullRequest(ctx, owner, repo, number)
|
||||
if err != nil {
|
||||
return PRDetails{}, err
|
||||
}
|
||||
details.FromCache, details.CachedAt = false, time.Time{}
|
||||
_ = c.write(c.pullRequestKey(owner, repo, number), details)
|
||||
return details, nil
|
||||
}
|
||||
|
||||
func (c *CachedGitHubService) pullRequestsKey(owner, repo string, limit int, showAll bool) string {
|
||||
return fmt.Sprintf("prs:%s/%s:%d:%t", owner, repo, limit, showAll)
|
||||
}
|
||||
|
||||
func (c *CachedGitHubService) pullRequestKey(owner, repo string, number int) string {
|
||||
return fmt.Sprintf("pr:%s/%s:%d", owner, repo, number)
|
||||
}
|
||||
|
||||
func (c *CachedGitHubService) file(key string) string {
|
||||
sum := sha256.Sum256([]byte(key))
|
||||
return filepath.Join(c.dir, hex.EncodeToString(sum[:])+".json")
|
||||
}
|
||||
|
||||
func (c *CachedGitHubService) write(key string, value any) error {
|
||||
if err := os.MkdirAll(c.dir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
valueData, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sum := sha256.Sum256(valueData)
|
||||
contentHash := hex.EncodeToString(sum[:])
|
||||
target := c.file(key)
|
||||
if existing, err := os.ReadFile(target); err == nil {
|
||||
var metadata struct {
|
||||
ContentHash string `json:"content_hash"`
|
||||
}
|
||||
if json.Unmarshal(existing, &metadata) == nil && metadata.ContentHash == contentHash {
|
||||
if info, statErr := os.Stat(target); statErr == nil &&
|
||||
time.Since(info.ModTime()) >= c.cacheTouchInterval() {
|
||||
now := time.Now()
|
||||
_ = os.Chtimes(target, now, now)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
data, err := json.Marshal(cacheEnvelope[any]{
|
||||
SavedAt: time.Now(), ContentHash: contentHash, Value: value,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
temp, err := os.CreateTemp(c.dir, ".cache-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := temp.Name()
|
||||
defer os.Remove(name)
|
||||
if err := temp.Chmod(0o600); err != nil {
|
||||
temp.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := temp.Write(data); err != nil {
|
||||
temp.Close()
|
||||
return err
|
||||
}
|
||||
if err := temp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(name, target)
|
||||
}
|
||||
|
||||
func (c *CachedGitHubService) cacheTouchInterval() time.Duration {
|
||||
interval := 24 * time.Hour
|
||||
if c.maxAge > 0 && c.maxAge/2 < interval {
|
||||
interval = c.maxAge / 2
|
||||
}
|
||||
return interval
|
||||
}
|
||||
|
||||
func (c *CachedGitHubService) read(key string, target any) (time.Time, error) {
|
||||
path := c.file(key)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if err := json.Unmarshal(data, target); err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
var metadata struct {
|
||||
SavedAt time.Time `json:"saved_at"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &metadata); err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
savedAt := metadata.SavedAt
|
||||
if info, statErr := os.Stat(path); statErr == nil && info.ModTime().After(savedAt) {
|
||||
savedAt = info.ModTime()
|
||||
}
|
||||
if c.maxAge > 0 && time.Since(savedAt) > c.maxAge {
|
||||
return time.Time{}, errors.New("cached data expired")
|
||||
}
|
||||
return savedAt, nil
|
||||
}
|
||||
|
||||
type readStateStore struct {
|
||||
path string
|
||||
Data map[string]readPRState `json:"pull_requests"`
|
||||
}
|
||||
|
||||
type readPRState struct {
|
||||
Initialized bool `json:"initialized"`
|
||||
Threads map[string]bool `json:"threads"`
|
||||
Comments map[string]bool `json:"comments"`
|
||||
}
|
||||
|
||||
func loadReadState(path string) *readStateStore {
|
||||
store := &readStateStore{path: path, Data: make(map[string]readPRState)}
|
||||
data, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
_ = json.Unmarshal(data, &store.Data)
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
func (s *readStateStore) save() error {
|
||||
if s == nil || s.path == "" {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(s.path), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := json.MarshalIndent(s.Data, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(s.path, data, 0o600)
|
||||
}
|
||||
Reference in New Issue
Block a user