Skip to content

Commit 9ca6946

Browse files
Added starting implementation to migration and warmup methods
1 parent e269222 commit 9ca6946

File tree

1 file changed

+101
-5
lines changed

1 file changed

+101
-5
lines changed

Sources/CodableDatastore/Datastore/Datastore.swift

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ public actor Datastore<
1818
let persistence: any Persistence
1919
let key: String
2020

21+
fileprivate var warmupStatus: TaskStatus = .waiting
22+
fileprivate var warmupProgressHandlers: [ProgressHandler] = []
23+
24+
fileprivate var storeMigrationStatus: TaskStatus = .waiting
25+
fileprivate var storeMigrationProgressHandlers: [ProgressHandler] = []
26+
27+
fileprivate var indexMigrationStatus: [IndexPath<CodedType> : TaskStatus] = [:]
28+
fileprivate var indexMigrationProgressHandlers: [IndexPath<CodedType> : ProgressHandler] = [:]
29+
2130
public init(
2231
persistence: some Persistence<AccessMode>,
2332
key: String,
@@ -54,10 +63,46 @@ public actor Datastore<
5463
/// It is recommended you call this method before accessing any data, as it will offer you an opportunity to show a loading screen during potentially long migrations, rather than leaving it for the first read or write on the data store.
5564
///
5665
/// - Parameter progressHandler: A closure that will be regularly called with progress during the migration. If no migration needs to occur, it won't be called, so setup and tear down any UI within the handler.
57-
public func warm(progressHandler: (_ progress: Progress) -> Void = { _ in }) async throws {
58-
66+
public func warm(progressHandler: @escaping ProgressHandler = { _ in }) async throws {
67+
try await warmupIfNeeded(progressHandler: progressHandler)
5968
}
6069

70+
func warmupIfNeeded(progressHandler: @escaping ProgressHandler = { _ in }) async throws {
71+
switch warmupStatus {
72+
case .complete: return
73+
case .inProgress(let task):
74+
warmupProgressHandlers.append(progressHandler)
75+
try await task.value
76+
case .waiting:
77+
let warmupTask = Task {
78+
let descriptor = try await persistence.register(datastore: self)
79+
print("\(String(describing: descriptor))")
80+
81+
/// Only operate on read-write datastores beyond this point.
82+
guard let self = self as? Datastore<Version, CodedType, IdentifierType, ReadWrite> else { return }
83+
print("\(self)")
84+
85+
for handler in warmupProgressHandlers {
86+
handler(.evaluating)
87+
}
88+
89+
// TODO: Migrate any incompatible indexes by calling the internal methods below as needed.
90+
await Task.yield() // The "work"
91+
92+
for handler in warmupProgressHandlers {
93+
handler(.complete(total: 0))
94+
}
95+
96+
warmupProgressHandlers.removeAll()
97+
warmupStatus = .complete
98+
}
99+
warmupStatus = .inProgress(warmupTask)
100+
try await warmupTask.value
101+
}
102+
}
103+
}
104+
105+
extension Datastore where AccessMode == ReadWrite {
61106
/// Manually migrate an index if the version persisted is less than a given minimum version.
62107
///
63108
/// Only use this if you must force an index to be re-calculated, which is sometimes necessary when the implementation of the compare method changes between releases.
@@ -66,8 +111,48 @@ public actor Datastore<
66111
/// - index: The index to migrate.
67112
/// - minimumVersion: The minimum valid version for an index to not be migrated.
68113
/// - progressHandler: A closure that will be regularly called with progress during the migration. If no migration needs to occur, it won't be called, so setup and tear down any UI within the handler.
69-
public func migrate(index: IndexPath<CodedType>, ifLessThan minimumVersion: Version, progressHandler: (_ progress: Progress) -> Void = { _ in }) async throws {
114+
public func migrate(index: IndexPath<CodedType>, ifLessThan minimumVersion: Version, progressHandler: @escaping ProgressHandler = { _ in }) async throws {
115+
guard
116+
/// If we have no descriptor, then no data exists to be migrated.
117+
let descriptor = try await persistence.datastoreDescriptor(for: self),
118+
descriptor.size > 0,
119+
/// If we don't have an index stored, there is nothing to do here. This means we can skip checking it on the type.
120+
let matchingIndex = descriptor.directIndexes[index.path] ?? descriptor.secondaryIndexes[index.path],
121+
/// We don't care in this method of the version is incompatible — the index will be discarded.
122+
let version = try? Version(matchingIndex.version),
123+
/// Make sure the stored version is smaller than the one we require, otherwise stop early.
124+
version.rawValue < minimumVersion.rawValue
125+
else { return }
70126

127+
var warmUpProgress: Progress = .complete(total: 0)
128+
try await warmupIfNeeded { progress in
129+
warmUpProgress = progress
130+
progressHandler(progress.adding(current: 0, total: descriptor.size))
131+
}
132+
133+
/// Make sure we still need to do the work, as the warm up may have made changes anyways due to incompatible types.
134+
guard
135+
/// If we have no descriptor, then no data exists to be migrated.
136+
let descriptor = try await persistence.datastoreDescriptor(for: self),
137+
descriptor.size > 0,
138+
/// If we don't have an index stored, there is nothing to do here. This means we can skip checking it on the type.
139+
let matchingIndex = descriptor.directIndexes[index.path] ?? descriptor.secondaryIndexes[index.path],
140+
/// We don't care in this method of the version is incompatible — the index will be discarded.
141+
let version = try? Version(matchingIndex.version),
142+
/// Make sure the stored version is smaller than the one we require, otherwise stop early.
143+
version.rawValue < minimumVersion.rawValue
144+
else {
145+
progressHandler(warmUpProgress.adding(current: descriptor.size, total: descriptor.size))
146+
return
147+
}
148+
149+
try await migrate(index: index) { migrateProgress in
150+
progressHandler(warmUpProgress.adding(migrateProgress))
151+
}
152+
}
153+
154+
func migrate(index: IndexPath<CodedType>, progressHandler: @escaping ProgressHandler = { _ in }) async throws {
155+
// TODO: Migrate just that index, use indexMigrationStatus and indexMigrationProgressHandlers to record progress.
71156
}
72157

73158
/// Manually migrate the entire store if the primary index version persisted is less than a given minimum version.
@@ -77,8 +162,12 @@ public actor Datastore<
77162
/// - Parameters:
78163
/// - minimumVersion: The minimum valid version for an index to not be migrated.
79164
/// - progressHandler: A closure that will be regularly called with progress during the migration. If no migration needs to occur, it won't be called, so setup and tear down any UI within the handler.
80-
public func migrateEntireStore(ifLessThan minimumVersion: Version, progressHandler: (_ progress: Progress) -> Void = { _ in }) async throws {
81-
165+
public func migrateEntireStore(ifLessThan minimumVersion: Version, progressHandler: @escaping ProgressHandler = { _ in }) async throws {
166+
// TODO: Like the method above, check the description to see if a migration is needed
167+
}
168+
169+
func migrateEntireStore(progressHandler: @escaping ProgressHandler = { _ in }) async throws {
170+
// TODO: Migrate all indexes, use storeMigrationStatus and storeMigrationProgressHandlers to record progress.
82171
}
83172
}
84173

@@ -440,3 +529,10 @@ extension Datastore where CodedType: Identifiable, IdentifierType == CodedType.I
440529
}
441530
}
442531

532+
// MARK: - Helper Types
533+
534+
private enum TaskStatus {
535+
case waiting
536+
case inProgress(Task<Void, Error>)
537+
case complete
538+
}

0 commit comments

Comments
 (0)