mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
feat(skills): add swift-actor-persistence skill
Thread-safe data persistence patterns using Swift actors with in-memory cache and file-backed storage.
This commit is contained in:
142
skills/swift-actor-persistence/SKILL.md
Normal file
142
skills/swift-actor-persistence/SKILL.md
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
name: swift-actor-persistence
|
||||
description: Thread-safe data persistence in Swift using actors — in-memory cache with file-backed storage, eliminating data races by design.
|
||||
---
|
||||
|
||||
# Swift Actors for Thread-Safe Persistence
|
||||
|
||||
Patterns for building thread-safe data persistence layers using Swift actors. Combines in-memory caching with file-backed storage, leveraging the actor model to eliminate data races at compile time.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Building a data persistence layer in Swift 5.5+
|
||||
- Need thread-safe access to shared mutable state
|
||||
- Want to eliminate manual synchronization (locks, DispatchQueues)
|
||||
- Building offline-first apps with local storage
|
||||
|
||||
## Core Pattern
|
||||
|
||||
### Actor-Based Repository
|
||||
|
||||
The actor model guarantees serialized access — no data races, enforced by the compiler.
|
||||
|
||||
```swift
|
||||
public actor LocalRepository<T: Codable & Identifiable> where T.ID == String {
|
||||
private var cache: [String: T] = [:]
|
||||
private let fileURL: URL
|
||||
|
||||
public init(directory: URL = .documentsDirectory, filename: String = "data.json") {
|
||||
self.fileURL = directory.appendingPathComponent(filename)
|
||||
// Synchronous load during init (actor isolation not yet active)
|
||||
self.cache = Self.loadSynchronously(from: fileURL)
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
public func save(_ item: T) throws {
|
||||
cache[item.id] = item
|
||||
try persistToFile()
|
||||
}
|
||||
|
||||
public func delete(_ id: String) throws {
|
||||
cache[id] = nil
|
||||
try persistToFile()
|
||||
}
|
||||
|
||||
public func find(by id: String) -> T? {
|
||||
cache[id]
|
||||
}
|
||||
|
||||
public func loadAll() -> [T] {
|
||||
Array(cache.values)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func persistToFile() throws {
|
||||
let data = try JSONEncoder().encode(Array(cache.values))
|
||||
try data.write(to: fileURL, options: .atomic)
|
||||
}
|
||||
|
||||
private static func loadSynchronously(from url: URL) -> [String: T] {
|
||||
guard let data = try? Data(contentsOf: url),
|
||||
let items = try? JSONDecoder().decode([T].self, from: data) else {
|
||||
return [:]
|
||||
}
|
||||
return Dictionary(uniqueKeysWithValues: items.map { ($0.id, $0) })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
All calls are automatically async due to actor isolation:
|
||||
|
||||
```swift
|
||||
let repository = LocalRepository<Question>()
|
||||
|
||||
// Read — fast O(1) lookup from in-memory cache
|
||||
let question = await repository.find(by: "q-001")
|
||||
let allQuestions = await repository.loadAll()
|
||||
|
||||
// Write — updates cache and persists to file atomically
|
||||
try await repository.save(newQuestion)
|
||||
try await repository.delete("q-001")
|
||||
```
|
||||
|
||||
### Combining with @Observable ViewModel
|
||||
|
||||
```swift
|
||||
@Observable
|
||||
final class QuestionListViewModel {
|
||||
private(set) var questions: [Question] = []
|
||||
private let repository: LocalRepository<Question>
|
||||
|
||||
init(repository: LocalRepository<Question> = LocalRepository()) {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
func load() async {
|
||||
questions = await repository.loadAll()
|
||||
}
|
||||
|
||||
func add(_ question: Question) async throws {
|
||||
try await repository.save(question)
|
||||
questions = await repository.loadAll()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| Actor (not class + lock) | Compiler-enforced thread safety, no manual synchronization |
|
||||
| In-memory cache + file persistence | Fast reads from cache, durable writes to disk |
|
||||
| Synchronous init loading | Avoids async initialization complexity |
|
||||
| Dictionary keyed by ID | O(1) lookups by identifier |
|
||||
| Generic over `Codable & Identifiable` | Reusable across any model type |
|
||||
| Atomic file writes (`.atomic`) | Prevents partial writes on crash |
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Use `Sendable` types** for all data crossing actor boundaries
|
||||
- **Keep the actor's public API minimal** — only expose domain operations, not persistence details
|
||||
- **Use `.atomic` writes** to prevent data corruption if the app crashes mid-write
|
||||
- **Load synchronously in `init`** — async initializers add complexity with minimal benefit for local files
|
||||
- **Combine with `@Observable`** ViewModels for reactive UI updates
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- Using `DispatchQueue` or `NSLock` instead of actors for new Swift concurrency code
|
||||
- Exposing the internal cache dictionary to external callers
|
||||
- Making the file URL configurable without validation
|
||||
- Forgetting that all actor method calls are `await` — callers must handle async context
|
||||
- Using `nonisolated` to bypass actor isolation (defeats the purpose)
|
||||
|
||||
## When to Use
|
||||
|
||||
- Local data storage in iOS/macOS apps (user data, settings, cached content)
|
||||
- Offline-first architectures that sync to a server later
|
||||
- Any shared mutable state that multiple parts of the app access concurrently
|
||||
- Replacing legacy `DispatchQueue`-based thread safety with modern Swift concurrency
|
||||
Reference in New Issue
Block a user