@@ -12,14 +12,226 @@ typealias SnapshotIdentifier = Identifier<Snapshot<ReadOnly>>
1212
1313/// A type that manages access to a snapshot on disk.
1414actor Snapshot < AccessMode: _AccessMode > {
15- let identifier : SnapshotIdentifier
16- weak var persistence : DiskPersistence < AccessMode > ?
15+ /// The identifier of the snapshot.
16+ ///
17+ /// This is used to determine where on disk the snapshot is stored.
18+ let id : SnapshotIdentifier
19+
20+ /// The persistence the stapshot is a part of.
21+ ///
22+ /// Prefer to access ``Snapshot/persistence`` instead, which offers non-optional access to the same persistence.
23+ private weak var _persistence : DiskPersistence < AccessMode > ?
24+
25+ /// A flag indicating if this is a backup snapshot.
26+ ///
27+ /// This is used to determine where on disk the snapshot is stored.
28+ let isBackup : Bool
29+
30+ /// A cached instance of the manifest as last loaded from disk.
31+ var cachedManifest : SnapshotManifest ?
32+
33+ /// A pointer to the last manifest updater, so updates can be serialized after the last request
34+ var lastUpdateManifestTask : Task < Any , Error > ?
1735
1836 init (
19- identifier: SnapshotIdentifier ,
20- persistence: DiskPersistence < AccessMode >
37+ id: SnapshotIdentifier ,
38+ persistence: DiskPersistence < AccessMode > ,
39+ isBackup: Bool = false
2140 ) {
22- self . identifier = identifier
23- self . persistence = persistence
41+ self . id = id
42+ self . _persistence = persistence
43+ self . isBackup = isBackup
44+ }
45+ }
46+
47+ extension Snapshot {
48+ @inlinable
49+ var persistence : DiskPersistence < AccessMode > {
50+ guard let persistence = _persistence else { preconditionFailure ( " Persistence is no longer allocated for this snapshot. " ) }
51+ return persistence
52+ }
53+ }
54+
55+ // MARK: - Common URL Accessors
56+ extension Snapshot {
57+ /// The URL that points to the Snapshot directory.
58+ var snapshotURL : URL {
59+ guard let components = try ? id. components else { preconditionFailure ( " Components could not be determined for Snapshot. " ) }
60+
61+ let baseURL = isBackup ? persistence. backupsURL : persistence. snapshotsURL
62+
63+ return baseURL
64+ . appendingPathComponent ( components. year, isDirectory: true )
65+ . appendingPathComponent ( components. monthDay, isDirectory: true )
66+ . appendingPathComponent ( components. hourMinute, isDirectory: true )
67+ . appendingPathComponent ( " \( id) .snapshot " , isDirectory: true )
68+ }
69+
70+ /// The URL that points to the Manifest.json file.
71+ var manifestURL : URL {
72+ snapshotURL. appendingPathComponent ( " Manifest.json " , isDirectory: false )
73+ }
74+
75+ /// The URL that points to the Dirty file.
76+ var dirtyURL : URL {
77+ snapshotURL. appendingPathComponent ( " Dirty " , isDirectory: false )
78+ }
79+
80+ /// The URL that points to the Datastores directory.
81+ var datastoresURL : URL {
82+ snapshotURL. appendingPathComponent ( " Datastores " , isDirectory: true )
83+ }
84+
85+ /// The URL that points to the Inbox directory.
86+ var inboxURL : URL {
87+ snapshotURL. appendingPathComponent ( " Inbox " , isDirectory: true )
88+ }
89+ }
90+
91+ // MARK: - Snapshot Manifest Management
92+ extension Snapshot {
93+ /// Load the manifest from disk, or create a suitable starting value if such a file does not exist.
94+ private func loadManifest( ) throws -> SnapshotManifest {
95+ do {
96+ let data = try Data ( contentsOf: snapshotURL)
97+
98+ let manifestDecoder = JSONDecoder ( )
99+ manifestDecoder. dateDecodingStrategy = . iso8601WithMilliseconds
100+ let manifest = try manifestDecoder. decode ( SnapshotManifest . self, from: data)
101+
102+ cachedManifest = manifest
103+ return manifest
104+ } catch URLError . fileDoesNotExist , CocoaError. fileReadNoSuchFile {
105+ return SnapshotManifest ( id: id, modificationDate: Date ( ) )
106+ } catch let error as NSError where error. domain == NSPOSIXErrorDomain && error. code == Int ( POSIXError . ENOENT. rawValue) {
107+ return SnapshotManifest ( id: id, modificationDate: Date ( ) )
108+ } catch {
109+ throw error
110+ }
111+ }
112+
113+ /// Write the specified manifest to the store, and cache the results in ``Snapshot/cachedManifest``.
114+ private func write( manifest: SnapshotManifest ) throws where AccessMode == ReadWrite {
115+ /// Make sure the directories exists first.
116+ if cachedManifest == nil {
117+ try FileManager . default. createDirectory ( at: snapshotURL, withIntermediateDirectories: true )
118+ try FileManager . default. createDirectory ( at: datastoresURL, withIntermediateDirectories: true )
119+ try FileManager . default. createDirectory ( at: inboxURL, withIntermediateDirectories: true )
120+ }
121+
122+ /// Encode the provided manifest, and write it to disk.
123+ let manifestEncoder = JSONEncoder ( )
124+ manifestEncoder. dateEncodingStrategy = . iso8601WithMilliseconds
125+ let data = try manifestEncoder. encode ( manifest)
126+ try data. write ( to: manifestURL, options: . atomic)
127+
128+ /// Update the cache since we know what it should be.
129+ cachedManifest = manifest
130+ }
131+
132+ /// Load and update the manifest in an updater, returning the task for the updater.
133+ ///
134+ /// This method loads the ``SnapshotManifest`` from cache, offers it to be mutated, then writes it back to disk, if it changed. It is up to the caller to update the modification date of the store.
135+ ///
136+ /// - Note: Calling this method when no manifest exists on disk will create it, even if no changes occur in the block.
137+ /// - Parameter updater: An updater that takes a mutable reference to a manifest, and will forward the returned value to the caller.
138+ /// - Returns: A ``/Swift/Task`` which contains the value of the updater upon completion.
139+ func updateManifest< T> ( updater: @escaping ( _ manifest: inout SnapshotManifest ) async throws -> T ) -> Task < T , Error > where AccessMode == ReadWrite {
140+
141+ if let manifest = SnapshotTaskLocals . manifest {
142+ return Task {
143+ var updatedManifest = manifest
144+ let returnValue = try await updater ( & updatedManifest)
145+
146+ guard updatedManifest == manifest else {
147+ throw DiskPersistenceInternalError . nestedSnapshotWrite
148+ }
149+
150+ return returnValue
151+ }
152+ }
153+
154+ /// Grab the last task so we can chain off of it in a serial manner.
155+ let lastUpdaterTask = lastUpdateManifestTask
156+ let updaterTask = Task {
157+ /// We don't care if the last request throws an error or not, but we do want it to complete first.
158+ _ = try ? await lastUpdaterTask? . value
159+
160+ /// Load the manifest so we have a fresh copy, unless we have a cached copy already.
161+ var manifest = try cachedManifest ?? self . loadManifest ( )
162+
163+ /// Let the updater do something with the manifest, storing the variable on the Task Local stack.
164+ let returnValue = try await SnapshotTaskLocals . $manifest. withValue ( manifest) {
165+ try await updater ( & manifest)
166+ }
167+
168+ /// Only write to the store if we changed the manifest for any reason
169+ if manifest != cachedManifest {
170+ try write ( manifest: manifest)
171+ }
172+ return returnValue
173+ }
174+ /// Assign the task to our pointer so we can depend on it the next time. Also, re-wrap it so we can keep proper type information when returning from this method.
175+ lastUpdateManifestTask = Task { try await updaterTask. value }
176+
177+ return updaterTask
24178 }
179+
180+ /// Load the manifest in an accessor, returning the task for the updater.
181+ ///
182+ /// This method loads the ``SnapshotManifest`` from cache.
183+ ///
184+ /// - Parameter accessor: An accessor that takes an immutable reference to a manifest, and will forward the returned value to the caller.
185+ /// - Returns: A ``/Swift/Task`` which contains the value of the updater upon completion.
186+ func updateManifest< T> ( accessor: @escaping ( _ manifest: SnapshotManifest ) async throws -> T ) -> Task < T , Error > where AccessMode == ReadOnly {
187+
188+ if let manifest = SnapshotTaskLocals . manifest {
189+ return Task { try await accessor ( manifest) }
190+ }
191+
192+ /// Grab the last task so we can chain off of it in a serial manner.
193+ let lastUpdaterTask = lastUpdateManifestTask
194+ let updaterTask = Task {
195+ /// We don't care if the last request throws an error or not, but we do want it to complete first.
196+ _ = try ? await lastUpdaterTask? . value
197+
198+ /// Load the manifest so we have a fresh copy, unless we have a cached copy already.
199+ let manifest = try cachedManifest ?? self . loadManifest ( )
200+
201+ /// Let the accessor do something with the manifest, storing the variable on the Task Local stack.
202+ return try await SnapshotTaskLocals . $manifest. withValue ( manifest) {
203+ try await accessor ( manifest)
204+ }
205+ }
206+ /// Assign the task to our pointer so we can depend on it the next time. Also, re-wrap it so we can keep proper type information when returning from this method.
207+ lastUpdateManifestTask = Task { try await updaterTask. value }
208+
209+ return updaterTask
210+ }
211+
212+ /// Load and update the manifest in an updater.
213+ ///
214+ /// This method loads the ``SnapshotManifest`` from cache, offers it to be mutated, then writes it back to disk, if it changed. It is up to the caller to update the modification date of the store.
215+ ///
216+ /// - Note: Calling this method when no manifest exists on disk will create it, even if no changes occur in the block.
217+ /// - Parameter updater: An updater that takes a mutable reference to a manifest, and will forward the returned value to the caller.
218+ /// - Returns: The value returned from the `updater`.
219+ func withManifest< T> ( updater: @escaping ( _ manifest: inout SnapshotManifest ) async throws -> T ) async throws -> T where AccessMode == ReadWrite {
220+ try await updateManifest ( updater: updater) . value
221+ }
222+
223+ /// Load the manifest in an updater.
224+ ///
225+ /// This method loads the ``SnapshotManifest`` from cache.
226+ ///
227+ /// - Parameter accessor: An accessor that takes an immutable reference to a manifest, and will forward the returned value to the caller.
228+ /// - Returns: The value returned from the `accessor`.
229+ func withManifest< T> ( accessor: @escaping ( _ manifest: SnapshotManifest ) async throws -> T ) async throws -> T where AccessMode == ReadOnly {
230+ try await updateManifest ( accessor: accessor) . value
231+ }
232+ }
233+
234+ private enum SnapshotTaskLocals {
235+ @TaskLocal
236+ static var manifest : SnapshotManifest ?
25237}
0 commit comments