Skip to content

chore: create searchContext on demand - WPB-20362#4246

Merged
caldrian merged 21 commits intodevelopfrom
chore/delete-search-context-WPB-20362
Feb 6, 2026
Merged

chore: create searchContext on demand - WPB-20362#4246
caldrian merged 21 commits intodevelopfrom
chore/delete-search-context-WPB-20362

Conversation

@caldrian
Copy link
Contributor

@caldrian caldrian commented Feb 2, 2026

TaskWPB-20362 [iOS/Integration] - Searching for Apps specifically

Issue

This PR removes searchContext from ContextProvider and the Core Data stacks and instead creates a new background context on demand for searching.

Testing

Search for users and/or bots in teams or personal ones.


Checklist

  • Title contains a reference JIRA issue number like [WPB-XXX].
  • Description is filled and free of optional paragraphs.
  • Adds/updates automated tests.

UI accessibility checklist

If your PR includes UI changes, please utilize this checklist:

  • Make sure you use the API for UI elements that support large fonts.
  • All colors are taken from WireDesign.ColorTheme or constructed using WireDesign.BaseColorPalette.
  • New UI elements have Accessibility strings for VoiceOver.

@caldrian caldrian changed the base branch from develop to chore/delete-zmselfuser-inusersession-WPB-20362 February 3, 2026 20:25
@caldrian caldrian changed the title chore: create searchContext on demand - WPB-20362 chore: create searchContext on demand - WPB-20362x Feb 3, 2026
@caldrian caldrian changed the title chore: create searchContext on demand - WPB-20362x chore: create searchContext on demand - WPB-20362 Feb 3, 2026
@caldrian caldrian marked this pull request as ready for review February 3, 2026 20:36
@caldrian caldrian requested review from a team, Copilot, johnxnguyen and samwyndham and removed request for a team February 3, 2026 20:37
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR removes the dedicated Core Data searchContext and refactors search to create background contexts on demand, updating both production code and tests to match the new API surface.

Changes:

  • Remove searchContext from ContextProvider, CoreDataStack, ZMUserSession, and related mocks/tests.
  • Refactor SearchTask/SearchDirectory to rely on contextProvider.newBackgroundContext() instead of an injected/persistent search MOC.
  • Remove “search context” identification/typing from Core Data context helpers/logging.

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
wire-ios/Tests/Mocks/UserSessionMock.swift Drops searchContext conformance from the app’s session mock.
wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift Replaces injected searchContext with ad-hoc background contexts during search operations.
wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift Removes searchContext plumbing; constructs SearchTask without a dedicated MOC.
wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift Removes public searchManagedObjectContext and ContextProvider.searchContext.
wire-ios-data-model/Source/ManagedObjectContext/CoreDataStack.swift Removes searchContext from the stack and ContextProvider protocol.
wire-ios-data-model/Source/ManagedObjectContext/NSManagedObjectContext+zmessaging.(h/m) Removes “search context” keys and helpers from context typing/validation.
wire-ios-sync-engine/Tests/... + wire-ios-data-model/Tests/... Updates tests to stop injecting/using searchContext/searchMOC (in some places).
Comments suppressed due to low confidence (6)

wire-ios-sync-engine/Tests/Source/MessagingTest.m:217

  • searchMOC accessor was removed, but this class still declares/uses searchMOC (e.g. in allManagedObjectContexts). This will either fail to compile (missing getter) or crash at runtime with an unrecognized selector. Update the test base to stop referencing searchMOC (and remove the property from the header) or provide an alternative (e.g. create an on-demand background context for tests that need it).
- (NSManagedObjectContext *)uiMOC
{
    return self.coreDataStack.viewContext;
}

- (NSManagedObjectContext *)syncMOC
{
    return self.coreDataStack.syncContext;
}

- (NSManagedObjectContext *)eventMOC
{
    return self.coreDataStack.eventContext;
}

wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift:442

  • ZMTaskCreatedHandler keeps groupQueue as a weak reference. Since this searchContext is a local variable (not retained by SearchTask), it may be deallocated before the transport layer calls the task-created handlers, so userLookupTaskIdentifier is never set and cancel() won’t be able to cancel the underlying request. Retain a long-lived groupQueue (e.g. a lazily-created context stored on SearchTask, or use an existing long-lived queue/context) for task-created handlers.
        let searchContext = contextProvider.newBackgroundContext()
        searchContext.perform { [self] in
            let request = type(of: self).searchRequestForUser(qualifiedID: qualifiedID, apiVersion: apiVersion)
            request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in
                defer {
                    self?.tasksRemaining -= 1
                }

                guard
                    let self,
                    let payload = response.payload?.asDictionary(),
                    let partialResult = SearchResult(
                        userLookupPayload: payload,
                        contextProvider: contextProvider,
                        searchUsersCache: searchUsersCache
                    )
                else { return }

                let updatedResult = aggregatedResult.union(withDirectoryResult: partialResult)
                aggregatedResult = updatedResult
            })

            request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in
                self?.userLookupTaskIdentifier = taskIdentifier
            })

wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift:508

  • Same issue as above: ZMTaskCreatedHandler holds the groupQueue weakly, but searchContext is not retained beyond this method scope. If the context is deallocated before the task is created, directoryTaskIdentifier won’t be captured and cancellation becomes ineffective. Keep the context/groupQueue alive for the lifetime of the SearchTask (or use a stable groupQueue).
        let searchContext = contextProvider.newBackgroundContext()
        searchContext.perform { [self] in
            let request = Self.searchRequestInDirectory(withRequest: searchRequest, apiVersion: apiVersion)

            request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in
                guard let self else { return }

                guard
                    let payload = response.payload?.asDictionary(),
                    let partialResult = SearchResult(
                        payload: payload,
                        query: searchRequest.query,
                        searchOptions: searchRequest.searchOptions,
                        contextProvider: contextProvider,
                        searchUsersCache: searchUsersCache
                    )
                else {
                    completeRemoteSearch()
                    return
                }

                if searchRequest.searchOptions.contains(.teamMembers) {
                    performTeamMembershipLookup(on: partialResult, searchRequest: searchRequest)
                } else {
                    completeRemoteSearch(searchResult: partialResult)
                }
            })

            request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in
                self?.directoryTaskIdentifier = taskIdentifier
            })

wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift:560

  • searchContext is created only to be passed into ZMTaskCreatedHandler, which stores the queue weakly. This context is very likely to be deallocated immediately after enqueueOneTime, so teamMembershipTaskIdentifier may never be set and cancel() can’t cancel the request. Use a retained groupQueue (e.g. store the context on SearchTask) instead of an ephemeral context here.
        let searchContext = contextProvider.newBackgroundContext()
        request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in
            self?.teamMembershipTaskIdentifier = taskIdentifier
        })

        transportSession.enqueueOneTime(request)

wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift:691

  • ZMTaskCreatedHandler stores groupQueue weakly. Because searchContext is not retained by SearchTask, it can be released before the handler is invoked, preventing handleTaskIdentifier from being recorded (and breaking cancellation). Consider retaining a single lazily-created search context on SearchTask and reuse it for these handlers.
        let searchContext = contextProvider.newBackgroundContext()
        searchContext.perform { [self] in
            let request = type(of: self).searchRequestInDirectory(
                withHandle: searchRequest.query.string,
                apiVersion: apiVersion
            )

            request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in

                defer {
                    self?.tasksRemaining -= 1
                }

                guard
                    let self,
                    let payload = response.payload?.asArray(),
                    let userPayload = (payload.first as? ZMTransportData)?.asDictionary()
                else {
                    return
                }

                guard
                    let handle = userPayload["handle"] as? String,
                    let name = userPayload["name"] as? String,
                    let id = userPayload["id"] as? String
                else {
                    return
                }

                let document = ["handle": handle, "name": name, "id": id]
                let documentPayload = ["documents": [document]]
                guard let partialResult = SearchResult(
                    payload: documentPayload,
                    query: searchRequest.query,
                    searchOptions: searchRequest.searchOptions,
                    contextProvider: contextProvider,
                    searchUsersCache: searchUsersCache
                ) else {
                    return
                }

                if let user = partialResult.directory.first, !user.isSelfUser {
                    let prevResult = aggregatedResult
                    // prepend result to prevResult only if it doesn't contain it
                    if !prevResult.directory.contains(user) {
                        aggregatedResult = SearchResult(
                            context: prevResult.context,
                            contacts: prevResult.contacts,
                            teamMembers: prevResult.teamMembers,
                            directory: partialResult.directory + prevResult.directory,
                            conversations: prevResult.conversations,
                            services: prevResult.services,
                            searchUsersCache: searchUsersCache
                        )
                    }
                }
            })

            request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in
                self?.handleTaskIdentifier = taskIdentifier
            })

wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift:762

  • searchContext is only retained as a local variable, but ZMTaskCreatedHandler keeps the queue weakly. If the context goes away before the task is created, servicesTaskIdentifier won’t be set and cancel() can’t cancel the service search request. Retain a long-lived groupQueue/context for the lifetime of the task.
    func performRemoteSearchForServices() {
        let searchContext = contextProvider.newBackgroundContext()
        let teamIdentifier = searchContext.performAndWait {
            ZMUser.selfUser(in: searchContext).team?.remoteIdentifier
        }
        guard
            let apiVersion,
            let teamIdentifier,
            case let .search(searchRequest) = task,
            !searchRequest.searchOptions.contains(.localResultsOnly),
            searchRequest.searchOptions.contains(.services)
        else { return }

        tasksRemaining += 1

        searchContext.perform { [self] in

            let request = type(of: self).servicesSearchRequest(
                teamIdentifier: teamIdentifier,
                query: searchRequest.query.string,
                apiVersion: apiVersion
            )

            request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in

                defer {
                    self?.tasksRemaining -= 1
                }

                guard
                    let self,
                    let payload = response.payload?.asDictionary(),
                    let partialResult = SearchResult(
                        servicesPayload: payload,
                        query: searchRequest.query.string,
                        contextProvider: contextProvider,
                        searchUsersCache: searchUsersCache
                    )
                else {
                    return
                }

                let updatedResult = aggregatedResult.union(withServiceResult: partialResult)
                aggregatedResult = updatedResult
            })

            request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in
                self?.servicesTaskIdentifier = taskIdentifier
            })

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 3, 2026

Test Results

5 373 tests   5 346 ✅  8m 8s ⏱️
  680 suites     27 💤
    3 files        0 ❌

Results for commit af607dd.

♻️ This comment has been updated with latest results.

Summary: workflow run #21670088645
Allure report (download zip): html-report-27550-chore_delete-search-context-WPB-20362

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…0-7b20975c44be491ed30dc6d915798f5214d023dc' into chore/delete-zmselfuser-inusersession-WPB-20362
@caldrian caldrian requested a review from KaterinaWire February 4, 2026 11:06
Base automatically changed from chore/delete-zmselfuser-inusersession-WPB-20362 to develop February 4, 2026 11:20
@caldrian caldrian enabled auto-merge February 4, 2026 11:42
Copy link
Contributor

@samwyndham samwyndham left a comment

Choose a reason for hiding this comment

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

LGTM

@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 5, 2026

@caldrian caldrian added this pull request to the merge queue Feb 6, 2026
tasksRemaining += 1

searchContext.performGroupedBlock { [self] in
let searchContext = contextProvider.newBackgroundContext()
Copy link
Collaborator

Choose a reason for hiding this comment

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

question: I see that we create the search context multiple times in this file instead of once for the whole search task. Is each use of the context independent from each other? I'm wondering if we are losing accumulative changes to the search context when we perform the different types of lookup.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The search results are mapped to the viewContext, that behavior I haven't changed. So I hope we don't lose results.
However, if we should have a single context or open multiple ones while performing fetch requests, that I'm not sure. I'll think about it for the follow-up PR #4245

Merged via the queue into develop with commit ca786e9 Feb 6, 2026
13 checks passed
@caldrian caldrian deleted the chore/delete-search-context-WPB-20362 branch February 6, 2026 08:31
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.

4 participants