file.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. package history
  2. import (
  3. "context"
  4. "database/sql"
  5. "fmt"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/google/uuid"
  10. "github.com/opencode-ai/opencode/internal/db"
  11. "github.com/opencode-ai/opencode/internal/pubsub"
  12. )
  13. const (
  14. InitialVersion = "initial"
  15. )
  16. type File struct {
  17. ID string
  18. SessionID string
  19. Path string
  20. Content string
  21. Version string
  22. CreatedAt int64
  23. UpdatedAt int64
  24. }
  25. type Service interface {
  26. pubsub.Suscriber[File]
  27. Create(ctx context.Context, sessionID, path, content string) (File, error)
  28. CreateVersion(ctx context.Context, sessionID, path, content string) (File, error)
  29. Get(ctx context.Context, id string) (File, error)
  30. GetByPathAndSession(ctx context.Context, path, sessionID string) (File, error)
  31. ListBySession(ctx context.Context, sessionID string) ([]File, error)
  32. ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
  33. Update(ctx context.Context, file File) (File, error)
  34. Delete(ctx context.Context, id string) error
  35. DeleteSessionFiles(ctx context.Context, sessionID string) error
  36. }
  37. type service struct {
  38. *pubsub.Broker[File]
  39. db *sql.DB
  40. q *db.Queries
  41. }
  42. func NewService(q *db.Queries, db *sql.DB) Service {
  43. return &service{
  44. Broker: pubsub.NewBroker[File](),
  45. q: q,
  46. db: db,
  47. }
  48. }
  49. func (s *service) Create(ctx context.Context, sessionID, path, content string) (File, error) {
  50. return s.createWithVersion(ctx, sessionID, path, content, InitialVersion)
  51. }
  52. func (s *service) CreateVersion(ctx context.Context, sessionID, path, content string) (File, error) {
  53. // Get the latest version for this path
  54. files, err := s.q.ListFilesByPath(ctx, path)
  55. if err != nil {
  56. return File{}, err
  57. }
  58. if len(files) == 0 {
  59. // No previous versions, create initial
  60. return s.Create(ctx, sessionID, path, content)
  61. }
  62. // Get the latest version
  63. latestFile := files[0] // Files are ordered by created_at DESC
  64. latestVersion := latestFile.Version
  65. // Generate the next version
  66. var nextVersion string
  67. if latestVersion == InitialVersion {
  68. nextVersion = "v1"
  69. } else if strings.HasPrefix(latestVersion, "v") {
  70. versionNum, err := strconv.Atoi(latestVersion[1:])
  71. if err != nil {
  72. // If we can't parse the version, just use a timestamp-based version
  73. nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
  74. } else {
  75. nextVersion = fmt.Sprintf("v%d", versionNum+1)
  76. }
  77. } else {
  78. // If the version format is unexpected, use a timestamp-based version
  79. nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
  80. }
  81. return s.createWithVersion(ctx, sessionID, path, content, nextVersion)
  82. }
  83. func (s *service) createWithVersion(ctx context.Context, sessionID, path, content, version string) (File, error) {
  84. // Maximum number of retries for transaction conflicts
  85. const maxRetries = 3
  86. var file File
  87. var err error
  88. // Retry loop for transaction conflicts
  89. for attempt := range maxRetries {
  90. // Start a transaction
  91. tx, txErr := s.db.Begin()
  92. if txErr != nil {
  93. return File{}, fmt.Errorf("failed to begin transaction: %w", txErr)
  94. }
  95. // Create a new queries instance with the transaction
  96. qtx := s.q.WithTx(tx)
  97. // Try to create the file within the transaction
  98. dbFile, txErr := qtx.CreateFile(ctx, db.CreateFileParams{
  99. ID: uuid.New().String(),
  100. SessionID: sessionID,
  101. Path: path,
  102. Content: content,
  103. Version: version,
  104. })
  105. if txErr != nil {
  106. // Rollback the transaction
  107. tx.Rollback()
  108. // Check if this is a uniqueness constraint violation
  109. if strings.Contains(txErr.Error(), "UNIQUE constraint failed") {
  110. if attempt < maxRetries-1 {
  111. // If we have retries left, generate a new version and try again
  112. if strings.HasPrefix(version, "v") {
  113. versionNum, parseErr := strconv.Atoi(version[1:])
  114. if parseErr == nil {
  115. version = fmt.Sprintf("v%d", versionNum+1)
  116. continue
  117. }
  118. }
  119. // If we can't parse the version, use a timestamp-based version
  120. version = fmt.Sprintf("v%d", time.Now().Unix())
  121. continue
  122. }
  123. }
  124. return File{}, txErr
  125. }
  126. // Commit the transaction
  127. if txErr = tx.Commit(); txErr != nil {
  128. return File{}, fmt.Errorf("failed to commit transaction: %w", txErr)
  129. }
  130. file = s.fromDBItem(dbFile)
  131. s.Publish(pubsub.CreatedEvent, file)
  132. return file, nil
  133. }
  134. return file, err
  135. }
  136. func (s *service) Get(ctx context.Context, id string) (File, error) {
  137. dbFile, err := s.q.GetFile(ctx, id)
  138. if err != nil {
  139. return File{}, err
  140. }
  141. return s.fromDBItem(dbFile), nil
  142. }
  143. func (s *service) GetByPathAndSession(ctx context.Context, path, sessionID string) (File, error) {
  144. dbFile, err := s.q.GetFileByPathAndSession(ctx, db.GetFileByPathAndSessionParams{
  145. Path: path,
  146. SessionID: sessionID,
  147. })
  148. if err != nil {
  149. return File{}, err
  150. }
  151. return s.fromDBItem(dbFile), nil
  152. }
  153. func (s *service) ListBySession(ctx context.Context, sessionID string) ([]File, error) {
  154. dbFiles, err := s.q.ListFilesBySession(ctx, sessionID)
  155. if err != nil {
  156. return nil, err
  157. }
  158. files := make([]File, len(dbFiles))
  159. for i, dbFile := range dbFiles {
  160. files[i] = s.fromDBItem(dbFile)
  161. }
  162. return files, nil
  163. }
  164. func (s *service) ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error) {
  165. dbFiles, err := s.q.ListLatestSessionFiles(ctx, sessionID)
  166. if err != nil {
  167. return nil, err
  168. }
  169. files := make([]File, len(dbFiles))
  170. for i, dbFile := range dbFiles {
  171. files[i] = s.fromDBItem(dbFile)
  172. }
  173. return files, nil
  174. }
  175. func (s *service) Update(ctx context.Context, file File) (File, error) {
  176. dbFile, err := s.q.UpdateFile(ctx, db.UpdateFileParams{
  177. ID: file.ID,
  178. Content: file.Content,
  179. Version: file.Version,
  180. })
  181. if err != nil {
  182. return File{}, err
  183. }
  184. updatedFile := s.fromDBItem(dbFile)
  185. s.Publish(pubsub.UpdatedEvent, updatedFile)
  186. return updatedFile, nil
  187. }
  188. func (s *service) Delete(ctx context.Context, id string) error {
  189. file, err := s.Get(ctx, id)
  190. if err != nil {
  191. return err
  192. }
  193. err = s.q.DeleteFile(ctx, id)
  194. if err != nil {
  195. return err
  196. }
  197. s.Publish(pubsub.DeletedEvent, file)
  198. return nil
  199. }
  200. func (s *service) DeleteSessionFiles(ctx context.Context, sessionID string) error {
  201. files, err := s.ListBySession(ctx, sessionID)
  202. if err != nil {
  203. return err
  204. }
  205. for _, file := range files {
  206. err = s.Delete(ctx, file.ID)
  207. if err != nil {
  208. return err
  209. }
  210. }
  211. return nil
  212. }
  213. func (s *service) fromDBItem(item db.File) File {
  214. return File{
  215. ID: item.ID,
  216. SessionID: item.SessionID,
  217. Path: item.Path,
  218. Content: item.Content,
  219. Version: item.Version,
  220. CreatedAt: item.CreatedAt,
  221. UpdatedAt: item.UpdatedAt,
  222. }
  223. }