Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
c4147f5f38
commit
52034946af
30
README.md
30
README.md
@ -150,6 +150,36 @@ extension StorageKeys {
|
||||
}
|
||||
}
|
||||
|
||||
// ... other properties
|
||||
}
|
||||
}
|
||||
|
||||
## Data Migration
|
||||
|
||||
LocalData supports migrating data between different storage keys and domains (e.g., from a legacy `UserDefaults` string key to a modern secure `Keychain` key).
|
||||
|
||||
### 1. Automatic Fallback (Lazy)
|
||||
When you define `migrationSources` on a key, `StorageRouter.get(key)` will automatically check those sources if the primary key is not found. If data exists in a source:
|
||||
- It is retrieved using the source's metadata.
|
||||
- It is saved to the new key using the new security policy.
|
||||
- It is deleted from the source.
|
||||
- It is returned to the caller.
|
||||
|
||||
### 2. Proactive Sweep (Drain)
|
||||
To ensure no "ghost data" remains in legacy keys (e.g., if a bug causes old code to write to them again), you can use either a manual call or an automated startup sweep.
|
||||
|
||||
#### Manual Call
|
||||
```swift
|
||||
try await StorageRouter.shared.migrate(for: StorageKeys.ModernKey())
|
||||
```
|
||||
|
||||
#### Automated Startup Sweep
|
||||
When registering a catalog, you can enable `migrateImmediately` to perform a global sweep of all legacy keys for every key in the catalog. This ensures your storage is clean at every app launch.
|
||||
|
||||
```swift
|
||||
try await StorageRouter.shared.registerCatalog(AppStorageCatalog.self, migrateImmediately: true)
|
||||
```
|
||||
|
||||
## Storage Design Philosophy
|
||||
|
||||
This app intentionally uses a **Type-Safe Storage Design**. Unlike standard iOS development which uses string keys (e.g., `UserDefaults.standard.string(forKey: "user_name")`), this library requires you to define a `StorageKey` type.
|
||||
|
||||
@ -1,11 +1,20 @@
|
||||
public struct AnyStorageKey: Sendable {
|
||||
public let descriptor: StorageKeyDescriptor
|
||||
private let migrateAction: @Sendable (StorageRouter) async throws -> Void
|
||||
|
||||
public init<Key: StorageKey>(_ key: Key) {
|
||||
self.descriptor = .from(key)
|
||||
self.migrateAction = { router in
|
||||
try await router.migrate(for: key)
|
||||
}
|
||||
}
|
||||
|
||||
public static func key<Key: StorageKey>(_ key: Key) -> AnyStorageKey {
|
||||
AnyStorageKey(key)
|
||||
}
|
||||
|
||||
/// Internal use: Triggers the migration logic for this key.
|
||||
internal func migrate(on router: StorageRouter) async throws {
|
||||
try await migrateAction(router)
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ public actor StorageRouter: StorageProviding {
|
||||
public static let shared = StorageRouter()
|
||||
|
||||
private var registeredKeyNames: Set<String> = []
|
||||
private var registeredEntries: [AnyStorageKey] = []
|
||||
private var storageConfiguration: StorageConfiguration = .default
|
||||
|
||||
private init() {}
|
||||
@ -52,11 +53,29 @@ public actor StorageRouter: StorageProviding {
|
||||
|
||||
/// Registers a catalog of known storage keys for audit and validation.
|
||||
/// When registered, all storage operations will verify keys are listed.
|
||||
public func registerCatalog<C: StorageKeyCatalog>(_ catalog: C.Type) throws {
|
||||
/// - Parameters:
|
||||
/// - catalog: The catalog type to register.
|
||||
/// - migrateImmediately: If true, triggers a proactive migration (sweep) for all keys in the catalog.
|
||||
public func registerCatalog<C: StorageKeyCatalog>(_ catalog: C.Type, migrateImmediately: Bool = false) async throws {
|
||||
let entries = catalog.allKeys
|
||||
try validateDescription(entries)
|
||||
try validateUniqueKeys(entries)
|
||||
registeredKeyNames = Set(entries.map { $0.descriptor.name })
|
||||
registeredEntries = entries
|
||||
|
||||
if migrateImmediately {
|
||||
try await migrateAllRegisteredKeys()
|
||||
}
|
||||
}
|
||||
|
||||
/// Triggers a proactive migration (sweep) for all registered storage keys.
|
||||
/// This "drains" any legacy data into the modern storage locations.
|
||||
public func migrateAllRegisteredKeys() async throws {
|
||||
Logger.debug(">>> [STORAGE] STARTING GLOBAL MIGRATION SWEEP")
|
||||
for entry in registeredEntries {
|
||||
try await entry.migrate(on: self)
|
||||
}
|
||||
Logger.debug("<<< [STORAGE] GLOBAL MIGRATION SWEEP COMPLETE")
|
||||
}
|
||||
|
||||
// MARK: - StorageProviding Implementation
|
||||
@ -126,6 +145,31 @@ public actor StorageRouter: StorageProviding {
|
||||
throw StorageError.notFound
|
||||
}
|
||||
|
||||
/// Manually triggers migration from legacy sources for a specific key.
|
||||
/// This is useful for "draining" old keys at app startup even if the destination already has data.
|
||||
/// - Parameter key: The storage key to migrate.
|
||||
/// - Throws: Various storage and serialization errors.
|
||||
public func migrate<Key: StorageKey>(for key: Key) async throws {
|
||||
Logger.debug(">>> [STORAGE] MANUAL MIGRATION: \(key.name)")
|
||||
|
||||
for source in key.migrationSources {
|
||||
let descriptor = source.descriptor
|
||||
if let securedOldData = try await retrieve(for: descriptor) {
|
||||
Logger.info("!!! [STORAGE] MANUAL MIGRATION: Migrating found data from '\(descriptor.name)' to '\(key.name)'")
|
||||
|
||||
let oldData = try await applySecurity(securedOldData, for: descriptor, isEncrypt: false)
|
||||
let value = try deserialize(oldData, with: key.serializer)
|
||||
|
||||
// Store in NEW location
|
||||
try await self.set(value, for: key)
|
||||
|
||||
// Delete OLD data
|
||||
try await delete(for: descriptor)
|
||||
}
|
||||
}
|
||||
Logger.debug("<<< [STORAGE] MANUAL MIGRATION COMPLETE: \(key.name)")
|
||||
}
|
||||
|
||||
/// Removes the value for the given key.
|
||||
/// - Parameter key: The storage key to remove.
|
||||
/// - Throws: Domain-specific errors if removal fails.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user