Skip to content

Conversation

@petkybenedek
Copy link
Contributor

What's new

This PR introduces AsyncStore, which makes use of structured concurrency to interact with the API and Core Data.

refs: MBL-19677
builds: Student, Teacher, Parent
affects: Student, Teacher, Parent
release note: none

Checklist

  • Follow-up e2e test ticket created
  • A11y checked
  • Tested on phone
  • Tested on tablet
  • Tested in dark mode
  • Tested in light mode
  • Approve from product

refs: MBL-19677
builds: Student, Teacher, Parent
affects: Student, Teacher, Parent
release note: none
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Review Summary

This PR introduces a new AsyncStore implementation that provides async/await alternatives to the existing reactive Store pattern. The changes align well with Swift's modern concurrency model and follow the project's MVVM architecture.

Issues Found

  • Critical Bug in NSPersistentContainerExtensions.swift:99-103 - The performWriteTask methods are using self.context instead of the computed context property, which defeats the purpose of having a dedicated write context. This could lead to threading issues.

  • Memory Leak Risk in AsyncStore.swift:139 - The streamEntitiesFromDatabase() method returns a non-terminating stream without built-in cancellation handling. While the warning comment is present, consider adding safeguards or helper methods.

  • Silent Error Handling in AsyncStore.swift:147 - The forceRefresh method silently ignores errors with try?. This should at least log errors for debugging purposes.

  • SwiftLint Violation in AsyncStore.swift:54 - Line length exceeds the project's limit and should be broken into multiple lines.

Positive Aspects

  • Clean async/await API: The new AsyncStore provides a modern, ergonomic interface that will improve code readability compared to Combine publishers.

  • Offline mode support: Proper integration with OfflineModeInteractor ensures consistent behavior across online/offline states.

  • Smart caching: The cache validation logic is well-implemented with hasCacheExpired checks before making API calls.

  • Consistent error handling: Most methods properly propagate errors through async throws, giving callers control over error handling.

  • Pagination support: The loadAllPages parameter provides flexibility for different data loading scenarios.

  • Good separation of concerns: Private helper methods like fetchEntitiesFromCache, fetchEntitiesFromAPI, and fetchEntitiesFromDatabase keep the code organized and testable.

Recommendations

  1. Fix the critical bug: The performWriteTask implementation must use the write context, not the view context.

  2. Add unit tests: Given the complexity of async streams and Core Data operations, comprehensive unit tests are essential. Follow the conventions in CLAUDE-unit-tests.md.

  3. Consider adding cancellation helpers: For non-terminating streams, provide convenience methods that handle cancellation automatically (e.g., with timeouts or lifecycle-bound cancellation).

  4. Add logging: At minimum, log errors in forceRefresh to aid debugging.

  5. SwiftLint compliance: Ensure all code passes SwiftLint checks before merging.

public func getEntities(ignoreCache: Bool = false, loadAllPages: Bool = true) async throws -> [U.Model] {
let scope = useCase.scope
let request = NSFetchRequest<U.Model>(entityName: String(describing: U.Model.self))
request.predicate = scope.predicate
Copy link

Choose a reason for hiding this comment

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

SwiftLint violation: This line exceeds the line length limit. Consider breaking it into multiple lines for better readability.


/// - Warning: This stream **does not terminate**. Ensure proper cancellation of its consuming task.
public func streamEntitiesFromDatabase() throws -> AsyncThrowingStream<[U.Model], Error> {
let scope = useCase.scope
Copy link

Choose a reason for hiding this comment

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

Critical: This stream does not terminate and can cause memory leaks if not properly cancelled. Consider:

  1. Adding a timeout mechanism
  2. Providing a helper method that handles cancellation
  3. Adding unit tests that verify proper cleanup when the consuming task is cancelled

return Self.streamEntitiesFromDatabase(fetchRequest: request, context: context)
}

/// Refreshes the entities by requesting the latest data from the API.
Copy link

Choose a reason for hiding this comment

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

The error from forceRefresh is being silently ignored with try?. This could hide important issues. Consider:

  1. Logging the error for debugging
  2. Using a completion handler or throwing the error to the caller
  3. At minimum, adding a comment explaining why it's safe to ignore errors here


@objc open func performWriteTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
let context = writeContext ?? {
context.perform { block(self.context) }
Copy link

Choose a reason for hiding this comment

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

Bug: This implementation is incorrect. The method is supposed to use a write context, but it's always using self.context instead of the actual context variable returned by the context computed property.

The block(self.context) should be block(context) where context is assigned from the context computed property.

}

public func performWriteTask<T>(_ block: @escaping (NSManagedObjectContext) throws -> T) async rethrows -> T {
return try await context.perform { try block(self.context) }
Copy link

Choose a reason for hiding this comment

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

Bug: Same issue here - this should use the write context from the context computed property, not self.context. The correct implementation should be:

let writeContext = context
return try await writeContext.perform { try block(writeContext) }

@petkybenedek petkybenedek changed the title [MBL-19677] [S/P/T] Structured Concurrency foundations [MBL-19677][S/P/T] Structured Concurrency foundations Jan 21, 2026
@inst-danger
Copy link
Contributor

inst-danger commented Jan 21, 2026

Warnings
⚠️ This pull request will not generate a release note.

Affected Apps: Student, Teacher, Parent

Builds: Student, Teacher, Parent

MBL-19677

Coverage New % Master % Delta
Canvas iOS 91.41% 81.06% 10.35%

Generated by 🚫 dangerJS against 666c444

@inst-danger
Copy link
Contributor

inst-danger commented Jan 21, 2026

Builds

Commit: Remove newline (666c444)
Build Number: 1208
Built At: Jan 26 15:48 CET (01/26 07:48 AM MST)

Student
Teacher
Parent

@petkybenedek petkybenedek self-assigned this Jan 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants