Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ public class AnalyticsMetadataInteractorLive: AnalyticsMetadataInteractor {

// Both stores publish non-managed-object values to avoid accessing the managed objects
// from arbitrary threads which happen to call this method
async let flagEnabledPublisher = flagEnabledStore.asyncPublisher()
async let userPublisher = userStore.asyncPublisher()
async let flagEnabledPublisher = flagEnabledStore.asyncValue()
async let userPublisher = userStore.asyncValue()

let isFlagEnabled = try await flagEnabledPublisher
let user = try await userPublisher
Expand Down
109 changes: 109 additions & 0 deletions Core/Core/Common/CommonModels/Store/AsyncFetchedResults.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//
// This file is part of Canvas.
// Copyright (C) 2026-present Instructure, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//

import Foundation
import CoreData

@preconcurrency
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking if we can get rid of this @preconcurrency attribute be used with our newly introduced Async types. How about using it with @preconcurrency import CoreData as that was by Apple, and then move the fetch method to an extension of NSManagedObjectContext.

@preconcurrency import CoreData

public final class AsyncFetchResults<ResultType: NSFetchRequestResult> {
....
    public func fetch() async throws -> [ResultType] {
        try await context.fetch(request)
    }
}

extension NSManagedObjectContext {
    public func fetch<R: NSFetchRequestResult>(_ request: NSFetchRequest<R>) async throws -> [R] {
        try await perform {
            return try self.fetch(request)
        }
    }
}

This way we keep the new code committed to the async/await model, isolating any source of pre-concurrency as much as possible.

public final class AsyncFetchedResults<ResultType: NSFetchRequestResult> {
private let request: NSFetchRequest<ResultType>
private let context: NSManagedObjectContext

public init(
request: NSFetchRequest<ResultType>,
context: NSManagedObjectContext
) {
self.request = request
self.context = context
}

public func fetch() async throws -> [ResultType] {
try await context.perform {
try self.context.fetch(self.request)
}
}

public func stream() -> AsyncThrowingStream<[ResultType], Error> {
AsyncThrowingStream { continuation in
let observer = FetchedResultsObserver(
request: request,
context: context,
continuation: continuation
)

continuation.onTermination = { _ in
observer.cancel()
}
}
}
}

private final class FetchedResultsObserver<ResultType: NSFetchRequestResult>: NSObject, NSFetchedResultsControllerDelegate {
private var controller: NSFetchedResultsController<ResultType>?
private let continuation: AsyncThrowingStream<[ResultType], Error>.Continuation
private let context: NSManagedObjectContext

init(
request: NSFetchRequest<ResultType>,
context: NSManagedObjectContext,
continuation: AsyncThrowingStream<[ResultType], Error>.Continuation
) {
self.continuation = continuation
self.context = context
super.init()

context.perform { [weak self] in
guard let self else { return }

self.controller = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil
)
self.controller?.delegate = self

do {
try self.controller?.performFetch()
self.sendElement()
} catch {
continuation.finish(throwing: NSError.instructureError("Error while reading from Core Data"))
}
}
}

private func sendElement() {
context.perform { [weak self] in
guard let self else { return }
let entities = self.controller?.fetchedObjects ?? []
self.continuation.yield(entities)
}
}

func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
sendElement()
}

func cancel() {
context.perform { [weak self] in
self?.controller?.delegate = nil
self?.controller = nil
self?.continuation.finish()
}
}
}
Loading