@@ -58,11 +58,159 @@ extension NSPersistentContainer: ModelStorage {
5858 }
5959}
6060
61+ // MARK: - PersistentContainerStorage
62+
63+ /// Concurrency safe persistent storage
64+ ///
65+ /// Write will happen on a single isolation context using a single background managed object context
66+ @available ( macOS 12 , iOS 15 , watchOS 8 , tvOS 15 , * )
67+ public actor PersistentContainerStorage : ModelStorage , ObservableObject {
68+
69+ // MARK: Initialization
70+
71+ public init ( name: String , model: Model ) {
72+ let managedObjectModel = NSManagedObjectModel ( model: model)
73+ let persistentContainer = NSPersistentContainer (
74+ name: name,
75+ managedObjectModel: managedObjectModel
76+ )
77+ self . persistentContainer = persistentContainer
78+ self . _viewContext = ManagedObjectViewContext ( persistentContainer: persistentContainer)
79+ }
80+
81+ // MARK: Properties
82+
83+ internal let persistentContainer : NSPersistentContainer
84+
85+ internal let _viewContext : ManagedObjectViewContext
86+
87+ @MainActor
88+ public var viewContext : ManagedObjectViewContext {
89+ get throws {
90+ try loadStores ( )
91+ return _viewContext
92+ }
93+ }
94+
95+ private nonisolated ( unsafe) var state: State = . notLoaded
96+
97+ private lazy var backgroundContext = persistentContainer. newBackgroundContext ( )
98+
99+ // MARK: Methods
100+
101+ public func fetch( _ entity: EntityName , for id: ObjectID ) async throws -> ModelData ? {
102+ return try await performBackgroundTask { ( context, model) in
103+ try context. fetch ( entity, for: id)
104+ }
105+ }
106+
107+ public func fetch( _ fetchRequest: FetchRequest ) async throws -> [ ModelData ] {
108+ try await performBackgroundTask { ( context, model) in
109+ try context. fetch ( fetchRequest)
110+ }
111+ }
112+
113+ public func count( _ fetchRequest: FetchRequest ) async throws -> UInt {
114+ try await performBackgroundTask { ( context, model) in
115+ try context. count ( fetchRequest)
116+ }
117+ }
118+
119+ public func insert( _ value: ModelData ) async throws {
120+ try await performBackgroundTask { ( context, model) in
121+ try context. insert ( value, model: model)
122+ }
123+ }
124+
125+ public func insert( _ values: [ ModelData ] ) async throws {
126+ try await performBackgroundTask { ( context, model) in
127+ try context. insert ( values, model: model)
128+ }
129+ }
130+
131+ public func delete( _ entity: EntityName , for id: ObjectID ) async throws {
132+ try await performBackgroundTask { ( context, model) in
133+ try context. delete ( entity, for: id)
134+ }
135+ }
136+
137+ public func fetchID( _ fetchRequest: FetchRequest ) async throws -> [ ObjectID ] {
138+ try await performBackgroundTask { ( context, model) in
139+ try context. fetchID ( fetchRequest)
140+ }
141+ }
142+
143+ private func performBackgroundTask< T> (
144+ schedule: NSManagedObjectContext . ScheduledTaskType = . immediate,
145+ _ task: @escaping ( NSManagedObjectContext , NSManagedObjectModel ) throws -> T
146+ ) async throws -> T {
147+ try await loadStores ( )
148+ let model = persistentContainer. managedObjectModel
149+ let context = backgroundContext
150+ return try await context. perform ( schedule: schedule) {
151+ try task ( context, model)
152+ }
153+ }
154+
155+ @MainActor
156+ private func loadStores( ) async throws {
157+ // lazily load stores
158+ guard state == . notLoaded else {
159+ // already loaded or loading
160+ return
161+ }
162+ state = . loading
163+ do {
164+ for try await store in persistentContainer. loadPersistentStores ( ) {
165+ // continue
166+ _ = store
167+ }
168+ }
169+ catch {
170+ state = . notLoaded
171+ throw error
172+ }
173+ state = . loaded
174+ }
175+
176+ private nonisolated func loadStores( ) throws {
177+ // lazily load stores
178+ guard state == . notLoaded else {
179+ // already loaded or loading
180+ return
181+ }
182+ state = . loading
183+ do {
184+ try persistentContainer. syncLoadPersistentStores ( )
185+ }
186+ catch {
187+ return
188+ }
189+ state = . loaded
190+ }
191+ }
192+
193+ @available ( macOS 12 , iOS 15 , watchOS 8 , tvOS 15 , * )
194+ internal extension PersistentContainerStorage {
195+
196+ enum State : Equatable , Hashable , Sendable {
197+
198+ case notLoaded
199+ case loading
200+ case loaded
201+ }
202+ }
203+
204+ // MARK: - Extensions
205+
61206@available ( macOS 12 , iOS 15 , watchOS 8 , tvOS 15 , * )
62207public extension NSPersistentContainer {
63208
64209 func loadPersistentStores( ) -> AsyncThrowingStream < NSPersistentStoreDescription , Error > {
65210 assert ( self . persistentStoreDescriptions. isEmpty == false )
211+ for store in persistentStoreDescriptions {
212+ store. shouldAddStoreAsynchronously = true
213+ }
66214 return AsyncThrowingStream < NSPersistentStoreDescription , Error > . init ( NSPersistentStoreDescription . self, bufferingPolicy: . unbounded, { continuation in
67215 self . loadPersistentStores { [ unowned self] ( description, error) in
68216 continuation. yield ( description)
@@ -76,6 +224,22 @@ public extension NSPersistentContainer {
76224 }
77225 } )
78226 }
227+
228+ func syncLoadPersistentStores( ) throws {
229+ assert ( self . persistentStoreDescriptions. isEmpty == false )
230+ for store in persistentStoreDescriptions {
231+ store. shouldAddStoreAsynchronously = false
232+ }
233+ var caughtError : Error ?
234+ self . loadPersistentStores { ( description, error) in
235+ if let error {
236+ caughtError = error
237+ }
238+ }
239+ if let error = caughtError {
240+ throw error
241+ }
242+ }
79243}
80244
81245#endif
0 commit comments