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
|
## 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.
|
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 struct AnyStorageKey: Sendable {
|
||||||
public let descriptor: StorageKeyDescriptor
|
public let descriptor: StorageKeyDescriptor
|
||||||
|
private let migrateAction: @Sendable (StorageRouter) async throws -> Void
|
||||||
|
|
||||||
public init<Key: StorageKey>(_ key: Key) {
|
public init<Key: StorageKey>(_ key: Key) {
|
||||||
self.descriptor = .from(key)
|
self.descriptor = .from(key)
|
||||||
|
self.migrateAction = { router in
|
||||||
|
try await router.migrate(for: key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func key<Key: StorageKey>(_ key: Key) -> AnyStorageKey {
|
public static func key<Key: StorageKey>(_ key: Key) -> AnyStorageKey {
|
||||||
AnyStorageKey(key)
|
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()
|
public static let shared = StorageRouter()
|
||||||
|
|
||||||
private var registeredKeyNames: Set<String> = []
|
private var registeredKeyNames: Set<String> = []
|
||||||
|
private var registeredEntries: [AnyStorageKey] = []
|
||||||
private var storageConfiguration: StorageConfiguration = .default
|
private var storageConfiguration: StorageConfiguration = .default
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
@ -52,11 +53,29 @@ public actor StorageRouter: StorageProviding {
|
|||||||
|
|
||||||
/// Registers a catalog of known storage keys for audit and validation.
|
/// Registers a catalog of known storage keys for audit and validation.
|
||||||
/// When registered, all storage operations will verify keys are listed.
|
/// 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
|
let entries = catalog.allKeys
|
||||||
try validateDescription(entries)
|
try validateDescription(entries)
|
||||||
try validateUniqueKeys(entries)
|
try validateUniqueKeys(entries)
|
||||||
registeredKeyNames = Set(entries.map { $0.descriptor.name })
|
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
|
// MARK: - StorageProviding Implementation
|
||||||
@ -126,6 +145,31 @@ public actor StorageRouter: StorageProviding {
|
|||||||
throw StorageError.notFound
|
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.
|
/// Removes the value for the given key.
|
||||||
/// - Parameter key: The storage key to remove.
|
/// - Parameter key: The storage key to remove.
|
||||||
/// - Throws: Domain-specific errors if removal fails.
|
/// - Throws: Domain-specific errors if removal fails.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user