import Foundation public protocol ProjectStore: Sendable { func create(_ project: SongProject) async throws func save(_ project: SongProject) async throws func open(id: String) async throws -> SongProject func loadProjectIDs() async throws -> [String] } public actor LocalSongProjectStore: ProjectStore { private let directoryURL: URL private let encoder: JSONEncoder private let decoder: JSONDecoder public init(directoryURL: URL) { self.directoryURL = directoryURL self.encoder = JSONEncoder() self.decoder = JSONDecoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] } public func create(_ project: SongProject) async throws { try ensureDirectoryExists() let url = projectURL(for: project.id) guard !FileManager.default.fileExists(atPath: url.path) else { throw LocalSongProjectStoreError.projectAlreadyExists(project.id) } try saveEncoded(project, to: url) } public func save(_ project: SongProject) async throws { try ensureDirectoryExists() try saveEncoded(project, to: projectURL(for: project.id)) } public func open(id: String) async throws -> SongProject { let url = projectURL(for: id) guard FileManager.default.fileExists(atPath: url.path) else { throw LocalSongProjectStoreError.projectNotFound(id) } let data = try Data(contentsOf: url) return try decoder.decode(SongProject.self, from: data) } public func loadProjectIDs() async throws -> [String] { try ensureDirectoryExists() let fileURLs = try FileManager.default.contentsOfDirectory( at: directoryURL, includingPropertiesForKeys: nil ) return fileURLs .filter { $0.pathExtension == Self.fileExtension } .map { $0.deletingPathExtension().lastPathComponent } .sorted() } private func ensureDirectoryExists() throws { try FileManager.default.createDirectory( at: directoryURL, withIntermediateDirectories: true ) } private func saveEncoded(_ project: SongProject, to url: URL) throws { let data = try encoder.encode(project) try data.write(to: url, options: [.atomic]) } private func projectURL(for id: String) -> URL { directoryURL .appendingPathComponent(id) .appendingPathExtension(Self.fileExtension) } private static let fileExtension = "json" } public enum LocalSongProjectStoreError: Error, Equatable, Sendable { case projectAlreadyExists(String) case projectNotFound(String) }