SuperCoder/internal/history/file.go

253 lines
6.4 KiB
Go
Raw Normal View History

2025-04-13 11:29:20 +02:00
package history
import (
"context"
2025-04-17 13:45:26 +02:00
"database/sql"
2025-04-13 11:29:20 +02:00
"fmt"
"strconv"
"strings"
2025-04-17 13:45:26 +02:00
"time"
2025-04-13 11:29:20 +02:00
"github.com/google/uuid"
2025-04-24 18:25:52 +02:00
"github.com/opencode-ai/opencode/internal/db"
"github.com/opencode-ai/opencode/internal/pubsub"
2025-04-13 11:29:20 +02:00
)
const (
InitialVersion = "initial"
)
type File struct {
ID string
SessionID string
Path string
Content string
Version string
CreatedAt int64
UpdatedAt int64
}
type Service interface {
pubsub.Suscriber[File]
2025-04-13 13:17:17 +02:00
Create(ctx context.Context, sessionID, path, content string) (File, error)
CreateVersion(ctx context.Context, sessionID, path, content string) (File, error)
Get(ctx context.Context, id string) (File, error)
GetByPathAndSession(ctx context.Context, path, sessionID string) (File, error)
ListBySession(ctx context.Context, sessionID string) ([]File, error)
ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
Update(ctx context.Context, file File) (File, error)
Delete(ctx context.Context, id string) error
DeleteSessionFiles(ctx context.Context, sessionID string) error
2025-04-13 11:29:20 +02:00
}
type service struct {
*pubsub.Broker[File]
2025-04-17 13:45:26 +02:00
db *sql.DB
q *db.Queries
2025-04-13 11:29:20 +02:00
}
2025-04-17 13:45:26 +02:00
func NewService(q *db.Queries, db *sql.DB) Service {
2025-04-13 11:29:20 +02:00
return &service{
Broker: pubsub.NewBroker[File](),
q: q,
db: db,
2025-04-13 11:29:20 +02:00
}
}
2025-04-13 13:17:17 +02:00
func (s *service) Create(ctx context.Context, sessionID, path, content string) (File, error) {
return s.createWithVersion(ctx, sessionID, path, content, InitialVersion)
2025-04-13 11:29:20 +02:00
}
2025-04-13 13:17:17 +02:00
func (s *service) CreateVersion(ctx context.Context, sessionID, path, content string) (File, error) {
2025-04-13 11:29:20 +02:00
// Get the latest version for this path
2025-04-13 13:17:17 +02:00
files, err := s.q.ListFilesByPath(ctx, path)
2025-04-13 11:29:20 +02:00
if err != nil {
return File{}, err
}
if len(files) == 0 {
// No previous versions, create initial
2025-04-13 13:17:17 +02:00
return s.Create(ctx, sessionID, path, content)
2025-04-13 11:29:20 +02:00
}
// Get the latest version
latestFile := files[0] // Files are ordered by created_at DESC
latestVersion := latestFile.Version
// Generate the next version
var nextVersion string
if latestVersion == InitialVersion {
nextVersion = "v1"
} else if strings.HasPrefix(latestVersion, "v") {
versionNum, err := strconv.Atoi(latestVersion[1:])
if err != nil {
// If we can't parse the version, just use a timestamp-based version
nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
} else {
nextVersion = fmt.Sprintf("v%d", versionNum+1)
}
} else {
// If the version format is unexpected, use a timestamp-based version
nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
}
2025-04-13 13:17:17 +02:00
return s.createWithVersion(ctx, sessionID, path, content, nextVersion)
2025-04-13 11:29:20 +02:00
}
2025-04-13 13:17:17 +02:00
func (s *service) createWithVersion(ctx context.Context, sessionID, path, content, version string) (File, error) {
2025-04-17 13:45:26 +02:00
// Maximum number of retries for transaction conflicts
const maxRetries = 3
var file File
var err error
// Retry loop for transaction conflicts
for attempt := range maxRetries {
2025-04-17 13:45:26 +02:00
// Start a transaction
tx, txErr := s.db.Begin()
if txErr != nil {
return File{}, fmt.Errorf("failed to begin transaction: %w", txErr)
2025-04-17 13:45:26 +02:00
}
// Create a new queries instance with the transaction
qtx := s.q.WithTx(tx)
// Try to create the file within the transaction
dbFile, txErr := qtx.CreateFile(ctx, db.CreateFileParams{
2025-04-17 13:45:26 +02:00
ID: uuid.New().String(),
SessionID: sessionID,
Path: path,
Content: content,
Version: version,
})
if txErr != nil {
2025-04-17 13:45:26 +02:00
// Rollback the transaction
tx.Rollback()
// Check if this is a uniqueness constraint violation
if strings.Contains(txErr.Error(), "UNIQUE constraint failed") {
2025-04-17 13:45:26 +02:00
if attempt < maxRetries-1 {
// If we have retries left, generate a new version and try again
if strings.HasPrefix(version, "v") {
versionNum, parseErr := strconv.Atoi(version[1:])
if parseErr == nil {
version = fmt.Sprintf("v%d", versionNum+1)
continue
}
}
// If we can't parse the version, use a timestamp-based version
version = fmt.Sprintf("v%d", time.Now().Unix())
continue
}
}
return File{}, txErr
2025-04-17 13:45:26 +02:00
}
// Commit the transaction
if txErr = tx.Commit(); txErr != nil {
return File{}, fmt.Errorf("failed to commit transaction: %w", txErr)
2025-04-17 13:45:26 +02:00
}
file = s.fromDBItem(dbFile)
s.Publish(pubsub.CreatedEvent, file)
return file, nil
2025-04-13 11:29:20 +02:00
}
2025-04-17 13:45:26 +02:00
return file, err
2025-04-13 11:29:20 +02:00
}
2025-04-13 13:17:17 +02:00
func (s *service) Get(ctx context.Context, id string) (File, error) {
dbFile, err := s.q.GetFile(ctx, id)
2025-04-13 11:29:20 +02:00
if err != nil {
return File{}, err
}
return s.fromDBItem(dbFile), nil
}
2025-04-13 13:17:17 +02:00
func (s *service) GetByPathAndSession(ctx context.Context, path, sessionID string) (File, error) {
dbFile, err := s.q.GetFileByPathAndSession(ctx, db.GetFileByPathAndSessionParams{
2025-04-13 11:29:20 +02:00
Path: path,
SessionID: sessionID,
})
if err != nil {
return File{}, err
}
return s.fromDBItem(dbFile), nil
}
2025-04-13 13:17:17 +02:00
func (s *service) ListBySession(ctx context.Context, sessionID string) ([]File, error) {
dbFiles, err := s.q.ListFilesBySession(ctx, sessionID)
2025-04-13 11:29:20 +02:00
if err != nil {
return nil, err
}
files := make([]File, len(dbFiles))
for i, dbFile := range dbFiles {
files[i] = s.fromDBItem(dbFile)
}
return files, nil
}
2025-04-13 13:17:17 +02:00
func (s *service) ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error) {
dbFiles, err := s.q.ListLatestSessionFiles(ctx, sessionID)
2025-04-13 11:29:20 +02:00
if err != nil {
return nil, err
}
files := make([]File, len(dbFiles))
for i, dbFile := range dbFiles {
files[i] = s.fromDBItem(dbFile)
}
return files, nil
}
2025-04-13 13:17:17 +02:00
func (s *service) Update(ctx context.Context, file File) (File, error) {
dbFile, err := s.q.UpdateFile(ctx, db.UpdateFileParams{
2025-04-13 11:29:20 +02:00
ID: file.ID,
Content: file.Content,
Version: file.Version,
})
if err != nil {
return File{}, err
}
updatedFile := s.fromDBItem(dbFile)
s.Publish(pubsub.UpdatedEvent, updatedFile)
return updatedFile, nil
}
2025-04-13 13:17:17 +02:00
func (s *service) Delete(ctx context.Context, id string) error {
file, err := s.Get(ctx, id)
2025-04-13 11:29:20 +02:00
if err != nil {
return err
}
2025-04-13 13:17:17 +02:00
err = s.q.DeleteFile(ctx, id)
2025-04-13 11:29:20 +02:00
if err != nil {
return err
}
s.Publish(pubsub.DeletedEvent, file)
return nil
}
2025-04-13 13:17:17 +02:00
func (s *service) DeleteSessionFiles(ctx context.Context, sessionID string) error {
files, err := s.ListBySession(ctx, sessionID)
2025-04-13 11:29:20 +02:00
if err != nil {
return err
}
for _, file := range files {
2025-04-13 13:17:17 +02:00
err = s.Delete(ctx, file.ID)
2025-04-13 11:29:20 +02:00
if err != nil {
return err
}
}
return nil
}
func (s *service) fromDBItem(item db.File) File {
return File{
ID: item.ID,
SessionID: item.SessionID,
Path: item.Path,
Content: item.Content,
Version: item.Version,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
}
}