Skip to content
Merged
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
8 changes: 8 additions & 0 deletions OpenASO.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@
D32C00000000000000000001 /* Persistence/KeywordResearchProjectStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D32C00000000000000000011 /* Persistence/KeywordResearchProjectStore.swift */; };
D32C00000000000000000002 /* Persistence/KeywordResearchProjectStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D32C00000000000000000011 /* Persistence/KeywordResearchProjectStore.swift */; };
D32C00000000000000000003 /* KeywordResearchProjectStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D32C00000000000000000012 /* KeywordResearchProjectStoreTests.swift */; };
D33A00000000000000000001 /* SearchRanking/KeywordResearchRankingWorkflow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33A00000000000000000011 /* SearchRanking/KeywordResearchRankingWorkflow.swift */; };
D33A00000000000000000002 /* KeywordResearchRankingWorkflowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33A00000000000000000012 /* KeywordResearchRankingWorkflowTests.swift */; };
D20B00000000000000000007 /* Persistence/EstimatedKeywordDifficultyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20B00000000000000000014 /* Persistence/EstimatedKeywordDifficultyStore.swift */; };
D20B00000000000000000008 /* Persistence/EstimatedKeywordDifficultyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20B00000000000000000014 /* Persistence/EstimatedKeywordDifficultyStore.swift */; };
D20B00000000000000000009 /* EstimatedKeywordDifficultyStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20B00000000000000000015 /* EstimatedKeywordDifficultyStoreTests.swift */; };
Expand Down Expand Up @@ -401,6 +403,8 @@
D32A00000000000000000014 /* KeywordResearchPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordResearchPersistenceTests.swift; sourceTree = "<group>"; };
D32C00000000000000000011 /* Persistence/KeywordResearchProjectStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence/KeywordResearchProjectStore.swift; sourceTree = "<group>"; };
D32C00000000000000000012 /* KeywordResearchProjectStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordResearchProjectStoreTests.swift; sourceTree = "<group>"; };
D33A00000000000000000011 /* SearchRanking/KeywordResearchRankingWorkflow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRanking/KeywordResearchRankingWorkflow.swift; sourceTree = "<group>"; };
D33A00000000000000000012 /* KeywordResearchRankingWorkflowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordResearchRankingWorkflowTests.swift; sourceTree = "<group>"; };
D20B00000000000000000014 /* Persistence/EstimatedKeywordDifficultyStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence/EstimatedKeywordDifficultyStore.swift; sourceTree = "<group>"; };
D20B00000000000000000015 /* EstimatedKeywordDifficultyStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EstimatedKeywordDifficultyStoreTests.swift; sourceTree = "<group>"; };
A20000000000000000000011 /* KeywordTableCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordTableCells.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -548,6 +552,7 @@
D77BC4C40000000000000011 /* ExactV4StoreFixtureTests.swift */,
D32A00000000000000000014 /* KeywordResearchPersistenceTests.swift */,
D32C00000000000000000012 /* KeywordResearchProjectStoreTests.swift */,
D33A00000000000000000012 /* KeywordResearchRankingWorkflowTests.swift */,
D09D00000000000000000007 /* TrackedKeywordRefreshStatusTests.swift */,
D20B00000000000000000015 /* EstimatedKeywordDifficultyStoreTests.swift */,
D02000000000000000000004 /* v0.3.2-v1 */,
Expand Down Expand Up @@ -671,6 +676,7 @@
9838D42ED1A3EA1E6DDDD68C /* OpenASOError.swift */,
035877A6E7443B32A3980E39 /* SearchRanking/RankingMatcher.swift */,
26D4B2C87FDED9CFCFA50D7D /* SearchRanking/RankingRefreshCoordinator.swift */,
D33A00000000000000000011 /* SearchRanking/KeywordResearchRankingWorkflow.swift */,
A70000000000000000000002 /* RatingsReviews/ReviewLanguageDetectionService.swift */,
1C1EE58C790084CF7A83CA8A /* SearchRanking/SearchModels.swift */,
F1F48387102DE88B407695C3 /* SearchRanking/SearchRankingProvider.swift */,
Expand Down Expand Up @@ -1178,6 +1184,7 @@
5FAADABB0DA1EEDB3699E66C /* SearchRanking/RankingMatcher.swift in Sources */,
33B2BE83B4CFC8970D7A3482 /* SearchRanking/RankingRefreshCoordinator.swift in Sources */,
D20A00000000000000000001 /* SearchRanking/KeywordDifficultyEstimator.swift in Sources */,
D33A00000000000000000001 /* SearchRanking/KeywordResearchRankingWorkflow.swift in Sources */,
9A6937276AF0E3CAEDA90C36 /* RankingSource.swift in Sources */,
A40000000000000000000003 /* RatingsDetail.swift in Sources */,
A40000000000000000000001 /* RatingsDashboardSupport.swift in Sources */,
Expand Down Expand Up @@ -1224,6 +1231,7 @@
D77BC4C40000000000000001 /* ExactV4StoreFixtureTests.swift in Sources */,
D32A00000000000000000007 /* KeywordResearchPersistenceTests.swift in Sources */,
D32C00000000000000000003 /* KeywordResearchProjectStoreTests.swift in Sources */,
D33A00000000000000000002 /* KeywordResearchRankingWorkflowTests.swift in Sources */,
D09D00000000000000000008 /* TrackedKeywordRefreshStatusTests.swift in Sources */,
D20B00000000000000000009 /* EstimatedKeywordDifficultyStoreTests.swift in Sources */,
D14000000000000000000002 /* AppMetadataRefreshProgressStoreTests.swift in Sources */,
Expand Down
10 changes: 10 additions & 0 deletions OpenASO/App/AppServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ final class AppServices {
let refreshProgressStore: AppRefreshProgressStore
let mcpServerController: OpenASOMCPServerController
let keywordResearchProjectStore: KeywordResearchProjectStore?
let keywordResearchRankingWorkflow: KeywordResearchRankingWorkflow?
private(set) var backgroundModelStore: BackgroundModelStore?
private(set) var backgroundModelStoreRevision = 0

Expand All @@ -55,6 +56,7 @@ final class AppServices {
allowsIconNetworkFetches: Bool = true,
backgroundModelStore: BackgroundModelStore? = nil,
keywordResearchProjectStore: KeywordResearchProjectStore? = nil,
keywordResearchRankingWorkflow: KeywordResearchRankingWorkflow? = nil,
refreshObservationClock: RefreshObservationClock = .live,
refreshMetricsRecorder: RefreshMetricsRecorder? = nil,
providerRequestGateMode: ProviderRequestGateMode? = nil,
Expand Down Expand Up @@ -241,6 +243,13 @@ final class AppServices {
},
metadataEnrichmentHandler: metadataEnrichmentHandler
)
let keywordResearchRankingWorkflow = keywordResearchRankingWorkflow
?? backgroundModelStore.map {
KeywordResearchRankingWorkflow(
backgroundModelStore: $0,
rankingCoordinator: refreshCoordinator
)
}
let appDetailRefreshService = backgroundModelStore.map {
AppDetailRefreshService(
backgroundModelStore: $0,
Expand Down Expand Up @@ -342,6 +351,7 @@ final class AppServices {
self.appDetailRefreshService = appDetailRefreshService
self.refreshProgressStore = refreshProgressStore
self.keywordResearchProjectStore = keywordResearchProjectStore
self.keywordResearchRankingWorkflow = keywordResearchRankingWorkflow
self.mcpServerController = OpenASOMCPServerController(portProvider: {
settingsStore.mcpServerPort
}) {
Expand Down
57 changes: 23 additions & 34 deletions OpenASO/Services/AppDetail/AppDetailRefreshService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ private struct KeywordRefreshResult: Sendable {
private struct RankingPersistenceBatchOutcome: Sendable {
let outcomes: [KeywordBackgroundRefreshOutcome]
let statsRebuildRequests: Set<RankingStatsRebuildRequest>
let successfulPageResults: [RankingRefreshPageResult]
let metadataEnrichmentPageResults: [RankingRefreshPageResult]

var failureCount: Int {
outcomes.filter { $0.error != nil }.count
Expand Down Expand Up @@ -576,7 +576,7 @@ final class AppDetailRefreshService: Sendable {
outcomes.append(contentsOf: batchOutcome.outcomes)
statsRebuildRequests.formUnion(batchOutcome.statsRebuildRequests)
failureCount += batchOutcome.failureCount
for pageResult in batchOutcome.successfulPageResults {
for pageResult in batchOutcome.metadataEnrichmentPageResults {
try Task.checkCancellation()
refreshCoordinator.scheduleTopRankingMetadataEnrichment(for: pageResult)
}
Expand Down Expand Up @@ -682,13 +682,16 @@ final class AppDetailRefreshService: Sendable {
do {
try await backgroundModelStore.write { modelContext in
try Task.checkCancellation()
refreshCoordinator.rebuildDerivedStats(for: requests, in: modelContext)
try refreshCoordinator.rebuildDerivedStats(for: requests, in: modelContext)
}
try Task.checkCancellation()
} catch {
if Task.isCancelled {
throw CancellationError()
}
OpenASOLog.refresh.error(
"Failed to rebuild ranking statistics: \(String(reflecting: error), privacy: .private(mask: .hash))"
)
}
}

Expand All @@ -711,44 +714,30 @@ final class AppDetailRefreshService: Sendable {
try Task.checkCancellation()
var outcomes: [KeywordBackgroundRefreshOutcome] = []
var statsRebuildRequests = Set<RankingStatsRebuildRequest>()
var successfulPageResults: [RankingRefreshPageResult] = []
var metadataEnrichmentPageResults: [RankingRefreshPageResult] = []

for pageResult in pageResults {
do {
_ = try refreshCoordinator.persistRankingPage(
pageResult,
in: modelContext,
rebuildDerivedStats: false,
saveChanges: false,
scheduleMetadataEnrichment: false
)
if let statsRebuildRequest = RankingStatsRebuildRequest(pageRequest: pageResult.request) {
statsRebuildRequests.insert(statsRebuildRequest)
}
successfulPageResults.append(pageResult)
outcomes.append(KeywordBackgroundRefreshOutcome(
trackIdentityKey: pageResult.request.identityKey,
error: nil
))
} catch {
let mappedError = OpenASOError.map(error)
_ = try? refreshCoordinator.recordRefreshFailure(
identityKey: pageResult.request.identityKey,
error: mappedError,
in: modelContext,
saveChanges: false
)
outcomes.append(KeywordBackgroundRefreshOutcome(
trackIdentityKey: pageResult.request.identityKey,
error: mappedError
))
let persistence = try refreshCoordinator.persistRankingPageTransaction(
pageResult,
in: modelContext,
rebuildDerivedStats: false
)
if let statsRebuildRequest = RankingStatsRebuildRequest(pageRequest: pageResult.request) {
statsRebuildRequests.insert(statsRebuildRequest)
}
if persistence.appliedSharedObservation {
metadataEnrichmentPageResults.append(persistence.canonicalPageResult)
}
outcomes.append(KeywordBackgroundRefreshOutcome(
trackIdentityKey: pageResult.request.identityKey,
error: nil
))
}

return RankingPersistenceBatchOutcome(
outcomes: outcomes,
statsRebuildRequests: statsRebuildRequests,
successfulPageResults: successfulPageResults
metadataEnrichmentPageResults: metadataEnrichmentPageResults
)
}
try Task.checkCancellation()
Expand Down Expand Up @@ -781,7 +770,7 @@ final class AppDetailRefreshService: Sendable {
KeywordBackgroundRefreshOutcome(trackIdentityKey: $0.request.identityKey, error: mappedError)
},
statsRebuildRequests: [],
successfulPageResults: []
metadataEnrichmentPageResults: []
)
}
}
Expand Down
77 changes: 51 additions & 26 deletions OpenASO/Services/MCP/OpenASOMCPService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ private struct OpenASOMCPHistoryCursor: Codable, Sendable {
}
}

private struct OpenASOMCPRankingRefreshCommit: Sendable {
let result: OpenASOMCPKeywordRefreshResult
let enrichmentPageResults: [RankingRefreshPageResult]
}

private enum OpenASOMCPHistoryCursorCodec {
private static let version = 2
private static let maximumEncodedCursorLength = 4_096
Expand Down Expand Up @@ -1989,7 +1994,7 @@ final class OpenASOMCPService: Sendable {
default: ResponseLimits.defaultKeywordRefreshTrackLimit,
maximum: ResponseLimits.maximumKeywordRefreshTrackLimit
)
let resultLimit = ResponseLimits.defaultRankingAppLimit
let resultLimit = SearchRankingCrawl.fullKeywordRankingLimit

let reconciliationPlan = await rankingRefreshScheduler.makeReconciliationPlan()
let candidateBatch: (
Expand Down Expand Up @@ -2140,8 +2145,9 @@ final class OpenASOMCPService: Sendable {
try Task.checkCancellation()

let fetchedResults = fetched
let result = try await backgroundModelStore.write { modelContext in
let commit = try await backgroundModelStore.write { modelContext in
var outcomes: [OpenASOMCPKeywordRefreshOutcome] = []
var enrichmentPageResults: [RankingRefreshPageResult] = []
for item in fetchedResults {
if let page = item.page, let observedAt = item.fetchedAt {
let track: TrackedAppKeyword
Expand All @@ -2155,13 +2161,15 @@ final class OpenASOMCPService: Sendable {
winningCount: 0,
confidence: nil
)
track = try rankingRefreshCoordinator.persistRankingPage(
let persistence = try rankingRefreshCoordinator.persistRankingPageTransaction(
pageResult,
in: modelContext,
rebuildDerivedStats: true,
saveChanges: false,
scheduleMetadataEnrichment: true
).keywordTrack
rebuildDerivedStats: true
)
track = persistence.snapshot.keywordTrack
if persistence.appliedSharedObservation {
enrichmentPageResults.append(persistence.canonicalPageResult)
}
} else {
track = try Self.persistRankingPage(
page,
Expand Down Expand Up @@ -2220,23 +2228,31 @@ final class OpenASOMCPService: Sendable {
}
}
let failures = outcomes.filter { $0.error != nil }.count
return OpenASOMCPKeywordRefreshResult(
summary: OpenASOMCPMutationSummary(
inserted: 0,
updated: 0,
skipped: max(0, candidateBatch.totalCount - reservation.requests.count),
refreshed: outcomes.count - failures,
failed: failures
return OpenASOMCPRankingRefreshCommit(
result: OpenASOMCPKeywordRefreshResult(
summary: OpenASOMCPMutationSummary(
inserted: 0,
updated: 0,
skipped: max(0, candidateBatch.totalCount - reservation.requests.count),
refreshed: outcomes.count - failures,
failed: failures
),
outcomes: outcomes,
notes: Self.keywordRankingRefreshNotes(
requestedLimit: requestedLimit,
appliedLimit: trackLimit
)
),
outcomes: outcomes,
notes: Self.keywordRankingRefreshNotes(
requestedLimit: requestedLimit,
appliedLimit: trackLimit
)
enrichmentPageResults: enrichmentPageResults
)
}
if let rankingRefreshCoordinator {
for pageResult in commit.enrichmentPageResults {
rankingRefreshCoordinator.scheduleTopRankingMetadataEnrichment(for: pageResult)
}
}
await rankingRefreshScheduler.release(reservation)
return result
return commit.result
} catch {
await rankingRefreshScheduler.release(reservation)
throw error
Expand Down Expand Up @@ -3278,7 +3294,9 @@ extension OpenASOMCPService {
keyword: seed.keyword,
storefrontCode: storefront,
platform: platform,
limit: ResponseLimits.defaultRankingAppLimit
limit: persistResults
? SearchRankingCrawl.fullKeywordRankingLimit
: ResponseLimits.defaultRankingAppLimit
)
}
} catch {
Expand Down Expand Up @@ -3318,7 +3336,7 @@ extension OpenASOMCPService {
storefront: storefront,
platform: platform
)
try await backgroundModelStore.write { modelContext in
let enrichmentPageResults = try await backgroundModelStore.write { modelContext in
let observedAt = now()
if let rankingRefreshCoordinator {
_ = try Self.ensureTrackedKeyword(
Expand All @@ -3335,13 +3353,14 @@ extension OpenASOMCPService {
winningCount: 0,
confidence: nil
)
_ = try rankingRefreshCoordinator.persistRankingPage(
let persistence = try rankingRefreshCoordinator.persistRankingPageTransaction(
pageResult,
in: modelContext,
rebuildDerivedStats: true,
saveChanges: false,
scheduleMetadataEnrichment: true
rebuildDerivedStats: true
)
return persistence.appliedSharedObservation
? [persistence.canonicalPageResult]
: []
} else {
_ = try Self.persistRankingPage(
page,
Expand All @@ -3351,6 +3370,12 @@ extension OpenASOMCPService {
appCatalogService: appCatalogService,
in: modelContext
)
return []
}
}
if let rankingRefreshCoordinator {
for pageResult in enrichmentPageResults {
rankingRefreshCoordinator.scheduleTopRankingMetadataEnrichment(for: pageResult)
}
}
isTracked = true
Expand Down
Loading
Loading