@gitkraken/core-gitlens 0.9.4 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -1
- package/dist/git/models/issue.d.ts +25 -2
- package/dist/git/models/issue.js +3 -1
- package/dist/git/models/issue.js.map +1 -1
- package/dist/git/models/pullRequest.d.ts +55 -5
- package/dist/git/models/pullRequest.js +5 -1
- package/dist/git/models/pullRequest.js.map +1 -1
- package/dist/git/utils/issue.utils.d.ts +4 -0
- package/dist/git/utils/issue.utils.js +4 -0
- package/dist/git/utils/issue.utils.js.map +1 -1
- package/dist/git/utils/pullRequest.utils.d.ts +4 -0
- package/dist/git/utils/pullRequest.utils.js +8 -0
- package/dist/git/utils/pullRequest.utils.js.map +1 -1
- package/dist/plus/git-github/api/github.d.ts +64 -4
- package/dist/plus/git-github/api/github.js +232 -15
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/git-github/models.d.ts +48 -6
- package/dist/plus/git-github/models.js +11 -10
- package/dist/plus/git-github/models.js.map +1 -1
- package/dist/plus/integrations/fieldPresence.d.ts +75 -0
- package/dist/plus/integrations/fieldPresence.js +381 -0
- package/dist/plus/integrations/fieldPresence.js.map +1 -0
- package/dist/plus/integrations/index.d.ts +6 -4
- package/dist/plus/integrations/index.js +2 -1
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +7 -3
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +56 -6
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +103 -9
- package/dist/plus/integrations/models/gitHostIntegration.js +116 -5
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +91 -4
- package/dist/plus/integrations/models/integration.js +56 -7
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issuesIntegration.d.ts +44 -3
- package/dist/plus/integrations/models/issuesIntegration.js +51 -1
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.js +3 -3
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azure/models.d.ts +36 -4
- package/dist/plus/integrations/providers/azure/models.js +118 -8
- package/dist/plus/integrations/providers/azure/models.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +102 -5
- package/dist/plus/integrations/providers/azureDevOps.js +264 -45
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +30 -6
- package/dist/plus/integrations/providers/azureDevOpsServer.js +82 -21
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket/bitbucket.d.ts +21 -1
- package/dist/plus/integrations/providers/bitbucket/bitbucket.js +74 -10
- package/dist/plus/integrations/providers/bitbucket/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket/models.d.ts +40 -3
- package/dist/plus/integrations/providers/bitbucket/models.js +30 -7
- package/dist/plus/integrations/providers/bitbucket/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server/models.d.ts +2 -6
- package/dist/plus/integrations/providers/bitbucket-server/models.js +1 -3
- package/dist/plus/integrations/providers/bitbucket-server/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.d.ts +3 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js +4 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +2 -2
- package/dist/plus/integrations/providers/bitbucket-server.js +7 -5
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.d.ts +22 -2
- package/dist/plus/integrations/providers/bitbucket.js +29 -6
- package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +40 -3
- package/dist/plus/integrations/providers/github.js +133 -11
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab/gitlab.d.ts +34 -1
- package/dist/plus/integrations/providers/gitlab/gitlab.js +118 -4
- package/dist/plus/integrations/providers/gitlab/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab/models.d.ts +70 -5
- package/dist/plus/integrations/providers/gitlab/models.js +106 -13
- package/dist/plus/integrations/providers/gitlab/models.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.d.ts +31 -2
- package/dist/plus/integrations/providers/gitlab.js +48 -8
- package/dist/plus/integrations/providers/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +1 -0
- package/dist/plus/integrations/providers/jira-server.d.ts +1 -0
- package/dist/plus/integrations/providers/jira-server.js +9 -6
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +20 -1
- package/dist/plus/integrations/providers/jira.js +101 -6
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/jiraIssueByKey.d.ts +79 -0
- package/dist/plus/integrations/providers/jiraIssueByKey.js +58 -7
- package/dist/plus/integrations/providers/jiraIssueByKey.js.map +1 -1
- package/dist/plus/integrations/providers/linear.d.ts +19 -1
- package/dist/plus/integrations/providers/linear.js +97 -5
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/linearIssuesEtag.d.ts +42 -0
- package/dist/plus/integrations/providers/linearIssuesEtag.js +94 -0
- package/dist/plus/integrations/providers/linearIssuesEtag.js.map +1 -0
- package/dist/plus/integrations/providers/models.d.ts +51 -7
- package/dist/plus/integrations/providers/models.js +45 -16
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providerErrors.js +8 -1
- package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +62 -3
- package/dist/plus/integrations/providers/providersApi.js +145 -4
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/pullRequestReviews.d.ts +13 -0
- package/dist/plus/integrations/providers/pullRequestReviews.js +30 -6
- package/dist/plus/integrations/providers/pullRequestReviews.js.map +1 -1
- package/dist/plus/integrations/providers/trello.js +10 -7
- package/dist/plus/integrations/providers/trello.js.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts +11 -0
- package/dist/plus/integrations/providers/utils/providerPaging.js +47 -1
- package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
- package/dist/plus/integrations/providers/utils.d.ts +1 -1
- package/dist/plus/integrations/reads/batchEtags.d.ts +69 -0
- package/dist/plus/integrations/reads/batchEtags.js +114 -0
- package/dist/plus/integrations/reads/batchEtags.js.map +1 -0
- package/dist/plus/integrations/reads/counts.js +2 -2
- package/dist/plus/integrations/reads/counts.js.map +1 -1
- package/dist/plus/integrations/reads/currentAccount.js +2 -2
- package/dist/plus/integrations/reads/currentAccount.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +13 -3
- package/dist/plus/integrations/reads/drains.js +29 -5
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/etag.d.ts +26 -0
- package/dist/plus/integrations/reads/etag.js +108 -0
- package/dist/plus/integrations/reads/etag.js.map +1 -0
- package/dist/plus/integrations/reads/hierarchy.js +3 -3
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/issueBatch.d.ts +47 -16
- package/dist/plus/integrations/reads/issueBatch.js +55 -87
- package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +4 -4
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/issues.d.ts +1 -1
- package/dist/plus/integrations/reads/issues.js +4 -4
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequestBatch.d.ts +44 -6
- package/dist/plus/integrations/reads/pullRequestBatch.js +31 -34
- package/dist/plus/integrations/reads/pullRequestBatch.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequestBranches.d.ts +5 -2
- package/dist/plus/integrations/reads/pullRequestBranches.js +4 -3
- package/dist/plus/integrations/reads/pullRequestBranches.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.js +8 -4
- package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/sweeps.js +8 -4
- package/dist/plus/integrations/reads/sweeps.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +23 -8
- package/dist/plus/integrations/reads/warnings.js +43 -19
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +12 -4
- package/dist/plus/integrations/results.js +2 -0
- package/dist/plus/integrations/results.js.map +1 -1
- package/docs/integrations.md +385 -23
- package/docs/kepler-read-api-parity.md +16 -2
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batchEtags.js","names":[],"sources":["../../../../../plus/integrations/src/reads/batchEtags.ts"],"sourcesContent":["import { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport type { BatchSlot } from '../models/integration.js';\nimport type { ProviderResult, ProviderWarning, ProviderWarningKind } from '../results.js';\nimport { appendDedupedWarning, toProviderWarning } from '../results.js';\n\n/**\n * The etag flow shared by the batch reads (`getPullRequestsBatch`, and both forms of `getIssuesBatch`): targets that\n * carry the etag of the caller's copy are first asked a cheap \"what is your change state\" question, and only the ones\n * that moved are read in full.\n *\n * - No target carries an etag, or the integration has no cheap check: ONE full read of every target, exactly as\n * before etags existed, with an etag added to each row.\n * - Otherwise up to three integration calls: the full read of the targets with no etag starts at once, in parallel\n * with the cheap check of the others, and the targets whose etag no longer matches get a second full read as soon\n * as the check settles. A call takes the longer of the two paths, never their sum.\n *\n * The cheap check never answers for a target it couldn't check: a target it failed falls through to the full read,\n * which then has the final word (and supplies the warning, should it fail too) — except when the failure was a\n * refused credential or a spent rate limit, which the full read would only hit again; those targets are dropped with\n * the cheap check's warning, as a failed full read drops them. Each target is judged by its own slot's reason, so one\n * target's rate limit never drops another target the check merely failed to answer, nor sends a rate-limited target\n * to the full read; the check's wrappers return every slot as it settled, failing as a whole only on a refused\n * credential (or when the request itself failed).\n *\n * Failure budget: the cheap check spends no strike toward disconnecting, and shows no notice, unless the credential\n * is refused. Only the full reads spend, each at most one, so a call spends at most two: in a total outage of a batch\n * whose every target carries an etag, one, for the full read every target falls through to.\n */\n\n/** One integration call's slots, as `runCaptured` captures them. */\nexport interface CapturedBatch<T> {\n\tvalue?: BatchSlot<T | undefined>[];\n\twarning?: ProviderWarning;\n}\n\n/**\n * One target's answer: read in full (`value`, or proven absent without one), or `unchanged`, when the cheap check\n * proved the caller's copy current. Every row with a `value` carries its `etag`, as does every `unchanged` one.\n */\nexport interface EtaggedBatchRow<T> {\n\tkey: string;\n\tvalue?: T;\n\tetag?: string;\n\tunchanged?: true;\n}\n\n/** Failures a full read would only hit again: a refused credential, a spent rate limit, a connection that's gone. */\nconst noRetryKinds: readonly ProviderWarningKind[] = ['auth', 'rate-limit', 'no-connection'];\n\nexport async function readEtaggedBatch<Target extends { key: string; etag?: string }, Item, Fields>(options: {\n\tproviderId: IntegrationIds;\n\tdomain: string | undefined;\n\tconnectionId: string | undefined;\n\ttargets: readonly Target[];\n\t/** Whether the integration has a cheap check. Decided up front, so a host without one makes one call. */\n\tsupportsEtags: boolean;\n\treadFull: (targets: readonly Target[]) => Promise<CapturedBatch<Item>>;\n\treadEtagFields: (targets: readonly Target[]) => Promise<CapturedBatch<Fields>>;\n\titemEtag: (item: Item) => string;\n\tfieldsEtag: (fields: Fields) => string;\n\t/** The warning for a full read that answered neither slots nor an error: the provider can't batch at all. */\n\tunsupportedWarning: () => ProviderWarning;\n}): Promise<ProviderResult<EtaggedBatchRow<Item>>> {\n\tconst { providerId, domain, connectionId, targets } = options;\n\tconst warnings: ProviderWarning[] = [];\n\tconst rows = new Map<number, EtaggedBatchRow<Item>>();\n\tlet fetchFailed = false;\n\n\tconst settleFull = (indices: readonly number[], { value: slots, warning }: CapturedBatch<Item>): void => {\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\n\t\tif (slots == null) {\n\t\t\t// Dropped, never reported absent: \"unknown\" and \"proven absent\" must stay distinguishable.\n\t\t\tif (warning == null) {\n\t\t\t\tappendDedupedWarning(warnings, options.unsupportedWarning());\n\t\t\t}\n\t\t\tfetchFailed = true;\n\t\t\treturn;\n\t\t}\n\n\t\tfor (let i = 0; i < indices.length; i++) {\n\t\t\tconst slot = slots[i];\n\t\t\tif (slot.status === 'rejected') {\n\t\t\t\t// Dropped, never reported absent, like a whole-call failure: only THIS target failed.\n\t\t\t\tappendBatchSlotWarning(warnings, providerId, domain, connectionId, slot);\n\t\t\t\tfetchFailed = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst item = slot.value;\n\t\t\tconst key = targets[indices[i]].key;\n\t\t\trows.set(indices[i], item != null ? { key: key, value: item, etag: options.itemEtag(item) } : { key: key });\n\t\t}\n\t};\n\n\tconst cheap: number[] = [];\n\tconst full: number[] = [];\n\tfor (let i = 0; i < targets.length; i++) {\n\t\t(options.supportsEtags && targets[i].etag ? cheap : full).push(i);\n\t}\n\n\tconst select = (indices: readonly number[]): Target[] => indices.map(i => targets[i]);\n\n\tif (!cheap.length) {\n\t\tsettleFull(full, await options.readFull(targets));\n\t} else {\n\t\t// Started before the cheap check is even sent, so the targets it can't answer never wait on it.\n\t\tconst fullRead = full.length ? options.readFull(select(full)) : undefined;\n\n\t\tconst changed: number[] = [];\n\t\tconst { value: slots, warning } = await options.readEtagFields(select(cheap));\n\t\tif (warning != null && noRetryKinds.includes(warning.kind)) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t\tfetchFailed = true;\n\t\t} else if (slots == null) {\n\t\t\t// The check failed some other way, or the provider declined it; the full read decides instead.\n\t\t\tchanged.push(...cheap);\n\t\t} else {\n\t\t\tfor (let i = 0; i < cheap.length; i++) {\n\t\t\t\tconst index = cheap[i];\n\t\t\t\tconst slot = slots[i];\n\t\t\t\tif (slot.status === 'rejected') {\n\t\t\t\t\t// A scoped refusal (`failure`) is the credential refused for that target's scope, which a full read\n\t\t\t\t\t// would hit as well.\n\t\t\t\t\tif (slot.failure != null) {\n\t\t\t\t\t\tappendBatchSlotWarning(warnings, providerId, domain, connectionId, slot);\n\t\t\t\t\t\tfetchFailed = true;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst slotWarning = toProviderWarning(providerId, domain, connectionId, slot.reason);\n\t\t\t\t\tif (noRetryKinds.includes(slotWarning.kind)) {\n\t\t\t\t\t\tappendDedupedWarning(warnings, slotWarning);\n\t\t\t\t\t\tfetchFailed = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged.push(index);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst key = targets[index].key;\n\t\t\t\tconst fields = slot.value;\n\t\t\t\tif (fields == null) {\n\t\t\t\t\trows.set(index, { key: key });\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst etag = options.fieldsEtag(fields);\n\t\t\t\tif (etag === targets[index].etag) {\n\t\t\t\t\trows.set(index, { key: key, unchanged: true, etag: etag });\n\t\t\t\t} else {\n\t\t\t\t\tchanged.push(index);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst changedRead = changed.length ? options.readFull(select(changed)) : undefined;\n\t\tif (fullRead != null) {\n\t\t\tsettleFull(full, await fullRead);\n\t\t}\n\t\tif (changedRead != null) {\n\t\t\tsettleFull(changed, await changedRead);\n\t\t}\n\t}\n\n\tconst items: EtaggedBatchRow<Item>[] = [];\n\tfor (let i = 0; i < targets.length; i++) {\n\t\tconst row = rows.get(i);\n\t\tif (row != null) {\n\t\t\titems.push(row);\n\t\t}\n\t}\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\n/**\n * Appends the warning for a batch target that could not be checked, shared by every batch read: for a refusal, the\n * scope failure it was recorded as (see `IntegrationBase.settleBatchRefusals`), published as an account-wide read\n * publishes one, scoped and with its cause; otherwise, the reason itself.\n */\nexport function appendBatchSlotWarning(\n\twarnings: ProviderWarning[],\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tslot: Extract<BatchSlot<unknown>, { status: 'rejected' }>,\n): void {\n\tif (slot.failure != null) {\n\t\tmergeAssessmentInto(warnings, providerId, domain, connectionId, {\n\t\t\tcompleteness: 'partial',\n\t\t\tfailures: [slot.failure],\n\t\t});\n\t\treturn;\n\t}\n\n\tappendDedupedWarning(warnings, toProviderWarning(providerId, domain, connectionId, slot.reason));\n}\n"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import "../constants.js";
|
|
|
4
4
|
import { appendDedupedWarning, toProviderWarning } from "../results.js";
|
|
5
5
|
import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
|
|
6
6
|
import { providersMetadata } from "../providers/models.js";
|
|
7
|
-
import { gitHostOnlySurfaceWarning, issuesUnsupportedWarning, otherWarning, unsupportedIssueSearchCriteriaWarning, unsupportedPullRequestSearchCriteriaWarning, unusableSearchScopeMessage } from "./warnings.js";
|
|
7
|
+
import { gitHostOnlySurfaceWarning, issuesUnsupportedWarning, otherWarning, unsupportedIssueSearchCriteriaWarning, unsupportedPullRequestSearchCriteriaWarning, unsupportedWarning, unusableSearchScopeMessage } from "./warnings.js";
|
|
8
8
|
import { runCaptured } from "./drains.js";
|
|
9
9
|
import { resolveIssueSearchCriteria, resolveIssueSearchScope, resolvePullRequestSearchCriteria, resolvePullRequestSearchScope } from "./filters.js";
|
|
10
10
|
//#region ../plus/integrations/src/reads/counts.ts
|
|
@@ -294,7 +294,7 @@ function rejectPullRequestScope(providerId, domain, connectionId, scope) {
|
|
|
294
294
|
}
|
|
295
295
|
const resolved = resolvePullRequestSearchCriteria(providerId, scope.criteria);
|
|
296
296
|
if (resolved.rejection != null) return unsupportedPullRequestSearchCriteriaWarning(providerId, domain, connectionId, resolved.rejection);
|
|
297
|
-
if ((scope.criteria?.relationships?.length ?? 0) > 1 && !countsPullRequestsByReading(providerId)) return
|
|
297
|
+
if ((scope.criteria?.relationships?.length ?? 0) > 1 && !countsPullRequestsByReading(providerId)) return unsupportedWarning(providerId, domain, connectionId, `Pull request count scope '${scope.key}' requests several relationships, which a single count can't express (they are OR-ed, so overlapping matches would be double-counted); pass one scope per relationship.`);
|
|
298
298
|
}
|
|
299
299
|
//#endregion
|
|
300
300
|
export { countIssues, countPullRequests };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"counts.js","names":[],"sources":["../../../../../plus/integrations/src/reads/counts.ts"],"sourcesContent":["import type { IssueSearchCriteria } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSearchCriteria } from '@gitlens/git/models/pullRequest.js';\nimport { chunk } from '@gitlens/utils/array.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { GitSelfManagedHostIntegrationId, providerFanOutConcurrency } from '../constants.js';\nimport type { ProviderSearchCount } from '../models/integration.js';\nimport type { ProviderRepoInput, ProviderReposInput } from '../providers/models.js';\nimport { providersMetadata } from '../providers/models.js';\nimport type { ProviderResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, toProviderWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tresolveIssueSearchCriteria,\n\tresolveIssueSearchScope,\n\tresolvePullRequestSearchCriteria,\n\tresolvePullRequestSearchScope,\n} from './filters.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissuesUnsupportedWarning,\n\totherWarning,\n\tunsupportedIssueSearchCriteriaWarning,\n\tunsupportedPullRequestSearchCriteriaWarning,\n\tunusableSearchScopeMessage,\n} from './warnings.js';\n\n/**\n * How many scopes go into one upstream request.\n *\n * Measured against the live GitHub API, 30 aliased counts cost a single rate-limit point and ~2s, and 60 cost the\n * same point and roughly twice as long — so the constraint isn't rate limit, it's latency and the provider's\n * (uncontracted, so not to be leaned on) query-complexity limits. 25 keeps a chunk comfortably inside both while\n * still collapsing a realistic filter matrix into one or two requests.\n */\nconst issueCountChunkSize = 25;\n\n/**\n * How many pull-request scopes go into one upstream request.\n *\n * The same latency/complexity reasoning as {@link issueCountChunkSize}, kept slightly smaller because a pull-request\n * scope can fan out into one aliased count PER requested state (open/closed/merged), so a chunk of scopes carries a\n * small multiple of that many `search` aliases.\n *\n * A provider that counts by reading batches nothing, so it is chunked one scope at a time instead: a chunk is then\n * just a scope, and the facade's bounded concurrency is what paces the requests.\n */\nconst pullRequestCountChunkSize = 15;\n\n/**\n * Whether a provider counts pull requests by READING the matches rather than asking for a total — Bitbucket Data\n * Center, which has no count query. Two rules follow from it: it batches nothing, and it deduplicates the rows\n * themselves, so a relationship set is an exact union it can count in one scope rather than a refusal.\n */\nfunction countsPullRequestsByReading(providerId: IntegrationIds): boolean {\n\treturn providerId === GitSelfManagedHostIntegrationId.BitbucketServer;\n}\n\n/**\n * One scope to count.\n *\n * `key` is caller-owned and echoed back verbatim, so a batch result needs no positional matching by the consumer.\n * It never reaches the provider query — the aliases in the upstream document are generated — so it can be any\n * string the caller finds meaningful.\n */\nexport interface IssueCountScope {\n\tkey: string;\n\t/** Repositories to count over. Combines with `org`, exactly as in `searchIssuesPage`. */\n\trepos?: ProviderReposInput;\n\torg?: string;\n\t/**\n\t * Same criteria model as `searchIssuesPage`, validated against the same capability table.\n\t *\n\t * `criteria.sort` does not affect the count — a total is invariant under ordering — but it is still VALIDATED,\n\t * and an unsupported key refuses this scope exactly as it would refuse the read. That is the point of the\n\t * count: it previews the constraints the read will apply, so one that accepted what the read refuses would\n\t * promise a fetch that can't happen, which is worse than no preview at all.\n\t */\n\tcriteria?: IssueSearchCriteria;\n}\n\n/** The count for one {@link IssueCountScope}, echoed back under the caller's own `key`. */\nexport interface IssueCountResult {\n\tkey: string;\n\t/**\n\t * Total matches the provider reports. `undefined` when the provider didn't report one for this scope — NEVER\n\t * zero, which is a real answer. Render the difference: an unreported count means \"unknown\", and showing it as\n\t * 0 would tell the user this filter matches nothing.\n\t */\n\tcount?: number;\n\t/**\n\t * True when `count` exceeds the provider's own result ceiling, so a full read CANNOT return everything no\n\t * matter how it is paged. This is the signal to warn before starting an expensive fetch.\n\t */\n\texceedsProviderLimit: boolean;\n\t/** The ceiling itself, when the provider declares one. */\n\tproviderLimit?: number;\n}\n\n/**\n * One scope to count pull requests over — the PR twin of {@link IssueCountScope}.\n *\n * `key` is caller-owned and echoed back verbatim, never reaching the provider query (the aliases are generated), so\n * it can be any string the caller finds meaningful.\n */\nexport interface PullRequestCountScope {\n\tkey: string;\n\t/** Repositories to count over. Combines with `org`, exactly as in `searchPullRequestsPage`. */\n\trepos?: ProviderReposInput;\n\torg?: string;\n\t/**\n\t * Same criteria model as `searchPullRequestsPage`, validated against the same capability table — so a count\n\t * refuses exactly what the read would, and never previews a fetch that can't happen.\n\t */\n\tcriteria?: PullRequestSearchCriteria;\n}\n\n/** The count for one {@link PullRequestCountScope}, echoed back under the caller's own `key`. */\nexport interface PullRequestCountResult {\n\tkey: string;\n\t/**\n\t * Total matches the provider reports. `undefined` when the provider didn't report one for this scope — NEVER\n\t * zero, which is a real answer.\n\t *\n\t * For a multi-state scope, GitHub/GHE report the LARGEST of their per-state counts (the same total\n\t * `searchPullRequestsPage` surfaces, since each state is its own capped search), not their sum. Bitbucket Data\n\t * Center has no ceiling to stay under, so it reports the exact union — the number of rows the search returns —\n\t * and so does Azure DevOps Server, which reads every state in one drain.\n\t */\n\tcount?: number;\n\t/**\n\t * True when `count` is a FLOOR rather than the total: the provider has no count query (Bitbucket Data Center),\n\t * so it counts by reading within a budget, and this scope had more than the budget read. Render it as \"N+\", and\n\t * treat the scope as at least that expensive. Absent means `count` is exact — the provider's own total, or for a\n\t * provider that counts by reading, every match it read.\n\t */\n\tlowerBound?: boolean;\n\t/**\n\t * True when `count` exceeds the provider's own per-search result ceiling, so a full read CANNOT return\n\t * everything no matter how it is paged. This is the signal to warn before starting an expensive fetch.\n\t */\n\texceedsProviderLimit: boolean;\n\t/** The ceiling itself, when the provider declares one. */\n\tproviderLimit?: number;\n}\n\n/**\n * Counts issues for many scopes without fetching any — the probe behind a \"this will fetch ~N issues\" preview.\n *\n * A distinct method rather than a `countOnly` flag on `searchIssuesPage`, because transferring ZERO issues is the\n * entire value: a flag would return a paged result whose `items`, `cursor` and `hasMore` are all meaningless, and\n * every consumer would have to know which fields to ignore. A separate method has a return type that only\n * describes counts.\n *\n * Scopes are batched into as few upstream requests as possible ({@link issueCountChunkSize} each). A chunk that\n * fails warns and drops only its own scopes, leaving the successful chunks' counts intact — so a partial answer is\n * still useful — with `fetchFailed` set. A scope refused for its own reasons (unscoped, inexpressible criteria) is\n * likewise isolated: its siblings are still counted.\n *\n * Cheap is not free: each chunk is a network request, so a caller driving this from UI state is expected to\n * debounce and cache.\n */\nexport async function countIssues(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\tscopes: readonly IssueCountScope[];\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderResult<IssueCountResult>> {\n\tconst refused = (warning: ProviderWarning): ProviderResult<IssueCountResult> => ({\n\t\titems: [],\n\t\twarnings: [warning],\n\t\tfetchFailed: true,\n\t});\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Issue counts'));\n\t}\n\n\t// Nothing was asked for, so nothing is missing: an empty success, not a refusal.\n\tif (options.scopes.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.scopes);\n\tif (duplicateKey != null) {\n\t\t// Refuses the whole call rather than deduping: `key` exists so the caller can match results without\n\t\t// positional bookkeeping, and two results under one key make that ambiguous for EVERY scope, not just the\n\t\t// repeated one.\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Duplicate issue count scope key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn { items: [], warnings: early.warnings, fetchFailed: early.fetchFailed || undefined };\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Issue counts'));\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\n\tif (!integration.supportsIssues) {\n\t\treturn refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));\n\t}\n\n\t// A provider with no filtered issue search has no count either: refuse ONCE for the provider rather than\n\t// letting every scope repeat the same rejection. `undefined` criteria probes only the search's existence.\n\t// Matches `countPullRequests`. Refusing it here rather than per scope also keeps the loop's warnings from\n\t// being dominated by N copies of one provider-level fact — see the dedupe below, which handles the\n\t// keyless refusals that remain.\n\tconst searchSupport = resolveIssueSearchCriteria(options.providerId, undefined);\n\tif (searchSupport.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSearchCriteriaWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tsearchSupport.rejection,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst providerLimit = providersMetadata[options.providerId]?.issueSearchResultLimit;\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\n\t// Validate every scope first, so a refusal costs no request at all and the countable ones are still batched\n\t// together.\n\tconst countable: IssueCountScope[] = [];\n\tfor (const scope of options.scopes) {\n\t\tconst warning = rejectScope(options.providerId, domain, options.connectionId, scope);\n\t\tif (warning != null) {\n\t\t\t// Deduped rather than pushed. The rejections that name the offending SCOPE embed its own key, and\n\t\t\t// duplicate keys were already refused above, so those can never collapse. But the ones that report a\n\t\t\t// PROVIDER-level fact — a criterion it cannot express, a contradictory relationship pair — name no\n\t\t\t// scope, so several scopes failing the same way produce byte-identical warnings, and one refusal\n\t\t\t// reported N times reads as N different problems.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tcountable.push(scope);\n\t}\n\n\t// Chunks are independent requests over their own slice of scopes — nothing in one reads what another produced,\n\t// and `runCaptured` never throws — so they run concurrently, bounded like every other fan-out on the facade.\n\t// Sequentially they would spend exactly the resource this probe is tuned to conserve: latency, ~2s per chunk.\n\t// `mapBounded` returns in input order, so `items` and `warnings` stay in scope order.\n\tconst batches = await mapBounded(chunk(countable, issueCountChunkSize), providerFanOutConcurrency, batch =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\tintegration.countIssuesResult(\n\t\t\t\t\tbatch.map(s => ({\n\t\t\t\t\t\trepos: s.repos as ProviderRepoInput[] | undefined,\n\t\t\t\t\t\torg: s.org,\n\t\t\t\t\t\tcriteria: s.criteria,\n\t\t\t\t\t})),\n\t\t\t\t\tundefined,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t).then(result => ({ batch: batch, ...result })),\n\t);\n\n\tconst items: IssueCountResult[] = [];\n\tfor (const { batch, value, warning } of batches) {\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// This batch contributes nothing, but the batches around it still do. Drop only these scopes.\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tfetchFailed =\n\t\t\tcollectCounts(batch, value, providerLimit, items, (key, error) => {\n\t\t\t\tconst warning = toProviderWarning(options.providerId, domain, options.connectionId, error);\n\t\t\t\tappendDedupedWarning(warnings, {\n\t\t\t\t\t...warning,\n\t\t\t\t\tmessage: `Issue count scope '${key}': ${warning.message}`,\n\t\t\t\t});\n\t\t\t}) || fetchFailed;\n\t}\n\n\t// A provider with no count support returns `undefined` with no error, which lands as an empty `items` and no\n\t// warning. Say so explicitly rather than letting it read as \"every scope matched nothing\".\n\tif (items.length === 0 && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSearchCriteriaWarning(options.providerId, domain, options.connectionId, {\n\t\t\t\treason: 'unsupported-search',\n\t\t\t}),\n\t\t);\n\t}\n\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\n/**\n * Turns one batch of positional issue counts into keyed results, returning whether any scope was refused.\n *\n * A slot holding an `Error` is a scope the PROVIDER refused for its own reasons (a scope naming a collection it\n * can't search, say): reported and dropped exactly like a scope the facade refuses, so its siblings in the batch\n * still come back. `'exceeds-limit'` is a count the provider can only bound — more than its ceiling matched — which\n * is reported as `exceedsProviderLimit` with no `count` rather than as the ceiling, which would understate it.\n */\nfunction collectCounts<T extends { key: string }>(\n\tbatch: readonly T[],\n\tvalue: readonly ProviderSearchCount[],\n\tproviderLimit: number | undefined,\n\titems: { key: string; count?: number; exceedsProviderLimit: boolean; providerLimit?: number }[],\n\tonRefused: (key: string, error: Error) => void,\n): boolean {\n\tlet refused = false;\n\tfor (let i = 0; i < batch.length; i++) {\n\t\tconst count = value[i];\n\t\tif (count instanceof Error) {\n\t\t\tonRefused(batch[i].key, count);\n\t\t\trefused = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst exceeds = count === 'exceeds-limit';\n\t\titems.push({\n\t\t\tkey: batch[i].key,\n\t\t\tcount: exceeds ? undefined : count,\n\t\t\t// Only a reported count can exceed a declared ceiling; unknown-vs-limit is not a comparison.\n\t\t\texceedsProviderLimit: exceeds || (count != null && providerLimit != null && count > providerLimit),\n\t\t\tproviderLimit: providerLimit,\n\t\t});\n\t}\n\treturn refused;\n}\n\n/** The first key that appears twice, or `undefined` when every key is unique. */\nfunction findDuplicateKey(scopes: readonly { key: string }[]): string | undefined {\n\tconst seen = new Set<string>();\n\tfor (const scope of scopes) {\n\t\tif (seen.has(scope.key)) return scope.key;\n\n\t\tseen.add(scope.key);\n\t}\n\treturn undefined;\n}\n\n/**\n * Why one scope can't be counted, as the warning to report — or `undefined` when it can.\n *\n * The scope and criteria rules come from {@link resolveIssueSearchScope} / {@link resolveIssueSearchCriteria},\n * the same validators `searchIssuesPage` uses, so a count always previews the constraints the read would apply.\n * That is not cosmetic: a count computed under different constraints is a WRONG number rather than a missing\n * one, which is worse. Only the wording is local, because these name the offending scope's key.\n */\nfunction rejectScope(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tscope: IssueCountScope,\n): ProviderWarning | undefined {\n\tconst resolved = resolveIssueSearchCriteria(providerId, scope.criteria);\n\tif (resolved.rejection != null) {\n\t\treturn unsupportedIssueSearchCriteriaWarning(providerId, domain, connectionId, resolved.rejection);\n\t}\n\n\t// AFTER the criteria check, mirroring `searchIssuesPage`: a provider with no filtered issue search is the more\n\t// fundamental refusal, and a count must preview the refusal its read would give.\n\tconst scoping = resolveIssueSearchScope(providerId, scope.repos, scope.org, scope.criteria);\n\tswitch (scoping.rejection?.reason) {\n\t\tcase 'repo-ids':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Issue count scope '${scope.key}' cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.`,\n\t\t\t);\n\t\tcase 'unscoped':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Issue count scope '${scope.key}' is unscoped; pass \\`repos\\`, \\`org\\`, or a relationship to the current user. \\`any-assignee\\` and \\`unassigned\\` are not scopes.`,\n\t\t\t);\n\t\t// A count applies exactly the qualifiers its search would, so an unusable scope would make it agree with\n\t\t// the wrong search rather than expose it — see `isUsableSearchScopeName`.\n\t\tcase 'unusable-scope':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\tunusableSearchScopeMessage('scopes', scoping.rejection.scopes, `Issue count scope '${scope.key}'`),\n\t\t\t);\n\t}\n\n\t// Count-only, with no counterpart in the read: a relationship set is an OR across several searches, which one\n\t// count can't express — summing them would double-count anything matching two, and taking the max would\n\t// under-report. Rather than answer with a wrong number, ask the caller to count each relationship as its own\n\t// scope, where the keys make the OR explicit.\n\tif ((scope.criteria?.relationships?.length ?? 0) > 1) {\n\t\treturn otherWarning(\n\t\t\tproviderId,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t`Issue count scope '${scope.key}' requests several relationships, which a single count can't express (they are OR-ed, so overlapping matches would be double-counted); pass one scope per relationship.`,\n\t\t);\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Counts pull requests for many scopes without fetching any — the PR twin of {@link countIssues}, and the probe\n * behind a \"this will fetch ~N pull requests\" preview.\n *\n * Identical batching, per-scope isolation, and `key`-echo contract as {@link countIssues}; see it for the cost\n * model and the `count: undefined` ≠ zero rule. The one difference is inherent to pull requests: a scope's criteria\n * can name several STATES, which GitHub/GHE count as independent searches — the reported `count` is the LARGEST\n * of them (mirroring `searchPullRequestsPage`'s total), so an unpaged read still can't exceed a single search's\n * ceiling undetected. Bitbucket Data Center has no ceiling and no count query: it counts the exact union by reading\n * each scope within a budget, and flags a scope that had more as `lowerBound`. Because it deduplicates rows, it is\n * also the one provider that accepts several relationships in one scope. Azure DevOps Server counts the union of the\n * states from the one drain its search pages through.\n */\nexport async function countPullRequests(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\tscopes: readonly PullRequestCountScope[];\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderResult<PullRequestCountResult>> {\n\tconst refused = (warning: ProviderWarning): ProviderResult<PullRequestCountResult> => ({\n\t\titems: [],\n\t\twarnings: [warning],\n\t\tfetchFailed: true,\n\t});\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Pull request counts'),\n\t\t);\n\t}\n\n\t// Nothing was asked for, so nothing is missing: an empty success, not a refusal.\n\tif (options.scopes.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.scopes);\n\tif (duplicateKey != null) {\n\t\t// Refuses the whole call rather than deduping: `key` exists so the caller can match results without\n\t\t// positional bookkeeping, and two results under one key make that ambiguous for EVERY scope, not just the\n\t\t// repeated one.\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Duplicate pull request count scope key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn { items: [], warnings: early.warnings, fetchFailed: early.fetchFailed || undefined };\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Pull request counts'),\n\t\t);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\n\t// A provider with no filtered pull-request search has no count either: refuse ONCE for the provider rather than\n\t// letting every scope repeat the same rejection. `undefined` criteria probes only the search's existence.\n\tconst searchSupport = resolvePullRequestSearchCriteria(options.providerId, undefined);\n\tif (searchSupport.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedPullRequestSearchCriteriaWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tsearchSupport.rejection,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst providerLimit = providersMetadata[options.providerId]?.pullRequestSearchResultLimit;\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\n\t// Validate every scope first, so a refusal costs no request at all and the countable ones are still batched\n\t// together.\n\tconst countable: PullRequestCountScope[] = [];\n\tfor (const scope of options.scopes) {\n\t\tconst warning = rejectPullRequestScope(options.providerId, domain, options.connectionId, scope);\n\t\tif (warning != null) {\n\t\t\t// Deduped rather than pushed: see the issue twin. Here the keyless set is wider — besides the criteria\n\t\t\t// rejections, `unsupported-repository-scope` and `unsupported-organization-scope` are scope rejections\n\t\t\t// that route through the criteria warning and name no key, and both are provider-level facts every\n\t\t\t// scope would repeat.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tcountable.push(scope);\n\t}\n\n\t// Chunks are independent requests over their own slice of scopes, `runCaptured` never throws, so they run\n\t// concurrently, bounded like every other fan-out on the facade. `mapBounded` returns in input order, so `items`\n\t// and `warnings` stay in scope order.\n\tconst chunks = chunk(countable, countsPullRequestsByReading(options.providerId) ? 1 : pullRequestCountChunkSize);\n\tconst batches = await mapBounded(chunks, providerFanOutConcurrency, batch =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\tintegration.countPullRequestsResult(\n\t\t\t\t\tbatch.map(s => ({\n\t\t\t\t\t\trepos: s.repos as ProviderRepoInput[] | undefined,\n\t\t\t\t\t\torg: s.org,\n\t\t\t\t\t\tcriteria: s.criteria,\n\t\t\t\t\t})),\n\t\t\t\t\tundefined,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t).then(result => ({ batch: batch, ...result })),\n\t);\n\n\tconst items: PullRequestCountResult[] = [];\n\tfor (const { batch, value, warning } of batches) {\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// This batch contributes nothing, but the batches around it still do. Drop only these scopes.\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (let i = 0; i < batch.length; i++) {\n\t\t\tconst slot = value[i];\n\t\t\t// A scope the provider refused for its own reasons: reported and dropped like one the facade refuses.\n\t\t\tif (slot instanceof Error) {\n\t\t\t\tconst warning = toProviderWarning(options.providerId, domain, options.connectionId, slot);\n\t\t\t\tappendDedupedWarning(warnings, {\n\t\t\t\t\t...warning,\n\t\t\t\t\tmessage: `Pull request count scope '${batch[i].key}': ${warning.message}`,\n\t\t\t\t});\n\t\t\t\tfetchFailed = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst count = slot?.count;\n\t\t\titems.push({\n\t\t\t\tkey: batch[i].key,\n\t\t\t\tcount: count,\n\t\t\t\t...(count != null && slot?.lowerBound === true ? { lowerBound: true } : {}),\n\t\t\t\t// Only a reported count can exceed a declared ceiling; unknown-vs-limit is not a comparison.\n\t\t\t\texceedsProviderLimit: count != null && providerLimit != null && count > providerLimit,\n\t\t\t\tproviderLimit: providerLimit,\n\t\t\t});\n\t\t}\n\t}\n\n\t// A provider that reported nothing for any scope and raised no warning: say so explicitly rather than letting it\n\t// read as \"every scope matched nothing\".\n\tif (items.length === 0 && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedPullRequestSearchCriteriaWarning(options.providerId, domain, options.connectionId, {\n\t\t\t\treason: 'unsupported-search',\n\t\t\t}),\n\t\t);\n\t}\n\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\n/**\n * Why one pull-request scope can't be counted, as the warning to report — or `undefined` when it can. The PR twin\n * of {@link rejectScope}, validating against the same {@link resolvePullRequestSearchScope} /\n * {@link resolvePullRequestSearchCriteria} the read uses, so a count always previews the constraints the read\n * would apply.\n */\nfunction rejectPullRequestScope(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tscope: PullRequestCountScope,\n): ProviderWarning | undefined {\n\tconst scoping = resolvePullRequestSearchScope(providerId, scope.repos, scope.org, scope.criteria);\n\tswitch (scoping.rejection?.reason) {\n\t\tcase 'repo-ids':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Pull request count scope '${scope.key}' cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.`,\n\t\t\t);\n\t\tcase 'unscoped':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Pull request count scope '${scope.key}' is unscoped; pass \\`repos\\`, \\`org\\`, or at least one current-user relationship.`,\n\t\t\t);\n\t\tcase 'unsupported-repository-scope':\n\t\tcase 'unsupported-organization-scope':\n\t\t\treturn unsupportedPullRequestSearchCriteriaWarning(providerId, domain, connectionId, {\n\t\t\t\treason: 'unsupported-criteria',\n\t\t\t\tcriteria: [\n\t\t\t\t\tscoping.rejection.reason === 'unsupported-repository-scope'\n\t\t\t\t\t\t? 'repositoryScope'\n\t\t\t\t\t\t: 'organizationScope',\n\t\t\t\t],\n\t\t\t});\n\t\t// See the issue twin, and `isUsableSearchScopeName` for the rule.\n\t\tcase 'unusable-scope':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\tunusableSearchScopeMessage(\n\t\t\t\t\t'scopes',\n\t\t\t\t\tscoping.rejection.scopes,\n\t\t\t\t\t`Pull request count scope '${scope.key}'`,\n\t\t\t\t),\n\t\t\t);\n\t}\n\n\tconst resolved = resolvePullRequestSearchCriteria(providerId, scope.criteria);\n\tif (resolved.rejection != null) {\n\t\treturn unsupportedPullRequestSearchCriteriaWarning(providerId, domain, connectionId, resolved.rejection);\n\t}\n\n\t// Count-only, with no counterpart in the read: a relationship set is an OR across several searches, which one\n\t// count can't express — summing would double-count overlaps and max would under-report. Ask the caller to count\n\t// each relationship as its own scope, where the keys make the OR explicit. (States are disjoint, so a scope may\n\t// still name several — the provider counts them as the max, not a refusal.) A provider that counts by reading\n\t// deduplicates the matching rows themselves, so it counts the OR exactly, as the search it previews returns it.\n\tif ((scope.criteria?.relationships?.length ?? 0) > 1 && !countsPullRequestsByReading(providerId)) {\n\t\treturn otherWarning(\n\t\t\tproviderId,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t`Pull request count scope '${scope.key}' requests several relationships, which a single count can't express (they are OR-ed, so overlapping matches would be double-counted); pass one scope per relationship.`,\n\t\t);\n\t}\n\n\treturn undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"counts.js","names":[],"sources":["../../../../../plus/integrations/src/reads/counts.ts"],"sourcesContent":["import type { IssueSearchCriteria } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSearchCriteria } from '@gitlens/git/models/pullRequest.js';\nimport { chunk } from '@gitlens/utils/array.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { GitSelfManagedHostIntegrationId, providerFanOutConcurrency } from '../constants.js';\nimport type { ProviderSearchCount } from '../models/integration.js';\nimport type { ProviderRepoInput, ProviderReposInput } from '../providers/models.js';\nimport { providersMetadata } from '../providers/models.js';\nimport type { ProviderResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, toProviderWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tresolveIssueSearchCriteria,\n\tresolveIssueSearchScope,\n\tresolvePullRequestSearchCriteria,\n\tresolvePullRequestSearchScope,\n} from './filters.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissuesUnsupportedWarning,\n\totherWarning,\n\tunsupportedIssueSearchCriteriaWarning,\n\tunsupportedPullRequestSearchCriteriaWarning,\n\tunsupportedWarning,\n\tunusableSearchScopeMessage,\n} from './warnings.js';\n\n/**\n * How many scopes go into one upstream request.\n *\n * Measured against the live GitHub API, 30 aliased counts cost a single rate-limit point and ~2s, and 60 cost the\n * same point and roughly twice as long — so the constraint isn't rate limit, it's latency and the provider's\n * (uncontracted, so not to be leaned on) query-complexity limits. 25 keeps a chunk comfortably inside both while\n * still collapsing a realistic filter matrix into one or two requests.\n */\nconst issueCountChunkSize = 25;\n\n/**\n * How many pull-request scopes go into one upstream request.\n *\n * The same latency/complexity reasoning as {@link issueCountChunkSize}, kept slightly smaller because a pull-request\n * scope can fan out into one aliased count PER requested state (open/closed/merged), so a chunk of scopes carries a\n * small multiple of that many `search` aliases.\n *\n * A provider that counts by reading batches nothing, so it is chunked one scope at a time instead: a chunk is then\n * just a scope, and the facade's bounded concurrency is what paces the requests.\n */\nconst pullRequestCountChunkSize = 15;\n\n/**\n * Whether a provider counts pull requests by READING the matches rather than asking for a total — Bitbucket Data\n * Center, which has no count query. Two rules follow from it: it batches nothing, and it deduplicates the rows\n * themselves, so a relationship set is an exact union it can count in one scope rather than a refusal.\n */\nfunction countsPullRequestsByReading(providerId: IntegrationIds): boolean {\n\treturn providerId === GitSelfManagedHostIntegrationId.BitbucketServer;\n}\n\n/**\n * One scope to count.\n *\n * `key` is caller-owned and echoed back verbatim, so a batch result needs no positional matching by the consumer.\n * It never reaches the provider query — the aliases in the upstream document are generated — so it can be any\n * string the caller finds meaningful.\n */\nexport interface IssueCountScope {\n\tkey: string;\n\t/** Repositories to count over. Combines with `org`, exactly as in `searchIssuesPage`. */\n\trepos?: ProviderReposInput;\n\torg?: string;\n\t/**\n\t * Same criteria model as `searchIssuesPage`, validated against the same capability table.\n\t *\n\t * `criteria.sort` does not affect the count — a total is invariant under ordering — but it is still VALIDATED,\n\t * and an unsupported key refuses this scope exactly as it would refuse the read. That is the point of the\n\t * count: it previews the constraints the read will apply, so one that accepted what the read refuses would\n\t * promise a fetch that can't happen, which is worse than no preview at all.\n\t */\n\tcriteria?: IssueSearchCriteria;\n}\n\n/** The count for one {@link IssueCountScope}, echoed back under the caller's own `key`. */\nexport interface IssueCountResult {\n\tkey: string;\n\t/**\n\t * Total matches the provider reports. `undefined` when the provider didn't report one for this scope — NEVER\n\t * zero, which is a real answer. Render the difference: an unreported count means \"unknown\", and showing it as\n\t * 0 would tell the user this filter matches nothing.\n\t */\n\tcount?: number;\n\t/**\n\t * True when `count` exceeds the provider's own result ceiling, so a full read CANNOT return everything no\n\t * matter how it is paged. This is the signal to warn before starting an expensive fetch.\n\t */\n\texceedsProviderLimit: boolean;\n\t/** The ceiling itself, when the provider declares one. */\n\tproviderLimit?: number;\n}\n\n/**\n * One scope to count pull requests over — the PR twin of {@link IssueCountScope}.\n *\n * `key` is caller-owned and echoed back verbatim, never reaching the provider query (the aliases are generated), so\n * it can be any string the caller finds meaningful.\n */\nexport interface PullRequestCountScope {\n\tkey: string;\n\t/** Repositories to count over. Combines with `org`, exactly as in `searchPullRequestsPage`. */\n\trepos?: ProviderReposInput;\n\torg?: string;\n\t/**\n\t * Same criteria model as `searchPullRequestsPage`, validated against the same capability table — so a count\n\t * refuses exactly what the read would, and never previews a fetch that can't happen.\n\t */\n\tcriteria?: PullRequestSearchCriteria;\n}\n\n/** The count for one {@link PullRequestCountScope}, echoed back under the caller's own `key`. */\nexport interface PullRequestCountResult {\n\tkey: string;\n\t/**\n\t * Total matches the provider reports. `undefined` when the provider didn't report one for this scope — NEVER\n\t * zero, which is a real answer.\n\t *\n\t * For a multi-state scope, GitHub/GHE report the LARGEST of their per-state counts (the same total\n\t * `searchPullRequestsPage` surfaces, since each state is its own capped search), not their sum. Bitbucket Data\n\t * Center has no ceiling to stay under, so it reports the exact union — the number of rows the search returns —\n\t * and so does Azure DevOps Server, which reads every state in one drain.\n\t */\n\tcount?: number;\n\t/**\n\t * True when `count` is a FLOOR rather than the total: the provider has no count query (Bitbucket Data Center),\n\t * so it counts by reading within a budget, and this scope had more than the budget read. Render it as \"N+\", and\n\t * treat the scope as at least that expensive. Absent means `count` is exact — the provider's own total, or for a\n\t * provider that counts by reading, every match it read.\n\t */\n\tlowerBound?: boolean;\n\t/**\n\t * True when `count` exceeds the provider's own per-search result ceiling, so a full read CANNOT return\n\t * everything no matter how it is paged. This is the signal to warn before starting an expensive fetch.\n\t */\n\texceedsProviderLimit: boolean;\n\t/** The ceiling itself, when the provider declares one. */\n\tproviderLimit?: number;\n}\n\n/**\n * Counts issues for many scopes without fetching any — the probe behind a \"this will fetch ~N issues\" preview.\n *\n * A distinct method rather than a `countOnly` flag on `searchIssuesPage`, because transferring ZERO issues is the\n * entire value: a flag would return a paged result whose `items`, `cursor` and `hasMore` are all meaningless, and\n * every consumer would have to know which fields to ignore. A separate method has a return type that only\n * describes counts.\n *\n * Scopes are batched into as few upstream requests as possible ({@link issueCountChunkSize} each). A chunk that\n * fails warns and drops only its own scopes, leaving the successful chunks' counts intact — so a partial answer is\n * still useful — with `fetchFailed` set. A scope refused for its own reasons (unscoped, inexpressible criteria) is\n * likewise isolated: its siblings are still counted.\n *\n * Cheap is not free: each chunk is a network request, so a caller driving this from UI state is expected to\n * debounce and cache.\n */\nexport async function countIssues(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\tscopes: readonly IssueCountScope[];\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderResult<IssueCountResult>> {\n\tconst refused = (warning: ProviderWarning): ProviderResult<IssueCountResult> => ({\n\t\titems: [],\n\t\twarnings: [warning],\n\t\tfetchFailed: true,\n\t});\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Issue counts'));\n\t}\n\n\t// Nothing was asked for, so nothing is missing: an empty success, not a refusal.\n\tif (options.scopes.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.scopes);\n\tif (duplicateKey != null) {\n\t\t// Refuses the whole call rather than deduping: `key` exists so the caller can match results without\n\t\t// positional bookkeeping, and two results under one key make that ambiguous for EVERY scope, not just the\n\t\t// repeated one.\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Duplicate issue count scope key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn { items: [], warnings: early.warnings, fetchFailed: early.fetchFailed || undefined };\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Issue counts'));\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\n\tif (!integration.supportsIssues) {\n\t\treturn refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));\n\t}\n\n\t// A provider with no filtered issue search has no count either: refuse ONCE for the provider rather than\n\t// letting every scope repeat the same rejection. `undefined` criteria probes only the search's existence.\n\t// Matches `countPullRequests`. Refusing it here rather than per scope also keeps the loop's warnings from\n\t// being dominated by N copies of one provider-level fact — see the dedupe below, which handles the\n\t// keyless refusals that remain.\n\tconst searchSupport = resolveIssueSearchCriteria(options.providerId, undefined);\n\tif (searchSupport.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSearchCriteriaWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tsearchSupport.rejection,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst providerLimit = providersMetadata[options.providerId]?.issueSearchResultLimit;\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\n\t// Validate every scope first, so a refusal costs no request at all and the countable ones are still batched\n\t// together.\n\tconst countable: IssueCountScope[] = [];\n\tfor (const scope of options.scopes) {\n\t\tconst warning = rejectScope(options.providerId, domain, options.connectionId, scope);\n\t\tif (warning != null) {\n\t\t\t// Deduped rather than pushed. The rejections that name the offending SCOPE embed its own key, and\n\t\t\t// duplicate keys were already refused above, so those can never collapse. But the ones that report a\n\t\t\t// PROVIDER-level fact — a criterion it cannot express, a contradictory relationship pair — name no\n\t\t\t// scope, so several scopes failing the same way produce byte-identical warnings, and one refusal\n\t\t\t// reported N times reads as N different problems.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tcountable.push(scope);\n\t}\n\n\t// Chunks are independent requests over their own slice of scopes — nothing in one reads what another produced,\n\t// and `runCaptured` never throws — so they run concurrently, bounded like every other fan-out on the facade.\n\t// Sequentially they would spend exactly the resource this probe is tuned to conserve: latency, ~2s per chunk.\n\t// `mapBounded` returns in input order, so `items` and `warnings` stay in scope order.\n\tconst batches = await mapBounded(chunk(countable, issueCountChunkSize), providerFanOutConcurrency, batch =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\tintegration.countIssuesResult(\n\t\t\t\t\tbatch.map(s => ({\n\t\t\t\t\t\trepos: s.repos as ProviderRepoInput[] | undefined,\n\t\t\t\t\t\torg: s.org,\n\t\t\t\t\t\tcriteria: s.criteria,\n\t\t\t\t\t})),\n\t\t\t\t\tundefined,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t).then(result => ({ batch: batch, ...result })),\n\t);\n\n\tconst items: IssueCountResult[] = [];\n\tfor (const { batch, value, warning } of batches) {\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// This batch contributes nothing, but the batches around it still do. Drop only these scopes.\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tfetchFailed =\n\t\t\tcollectCounts(batch, value, providerLimit, items, (key, error) => {\n\t\t\t\tconst warning = toProviderWarning(options.providerId, domain, options.connectionId, error);\n\t\t\t\tappendDedupedWarning(warnings, {\n\t\t\t\t\t...warning,\n\t\t\t\t\tmessage: `Issue count scope '${key}': ${warning.message}`,\n\t\t\t\t});\n\t\t\t}) || fetchFailed;\n\t}\n\n\t// A provider with no count support returns `undefined` with no error, which lands as an empty `items` and no\n\t// warning. Say so explicitly rather than letting it read as \"every scope matched nothing\".\n\tif (items.length === 0 && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSearchCriteriaWarning(options.providerId, domain, options.connectionId, {\n\t\t\t\treason: 'unsupported-search',\n\t\t\t}),\n\t\t);\n\t}\n\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\n/**\n * Turns one batch of positional issue counts into keyed results, returning whether any scope was refused.\n *\n * A slot holding an `Error` is a scope the PROVIDER refused for its own reasons (a scope naming a collection it\n * can't search, say): reported and dropped exactly like a scope the facade refuses, so its siblings in the batch\n * still come back. `'exceeds-limit'` is a count the provider can only bound — more than its ceiling matched — which\n * is reported as `exceedsProviderLimit` with no `count` rather than as the ceiling, which would understate it.\n */\nfunction collectCounts<T extends { key: string }>(\n\tbatch: readonly T[],\n\tvalue: readonly ProviderSearchCount[],\n\tproviderLimit: number | undefined,\n\titems: { key: string; count?: number; exceedsProviderLimit: boolean; providerLimit?: number }[],\n\tonRefused: (key: string, error: Error) => void,\n): boolean {\n\tlet refused = false;\n\tfor (let i = 0; i < batch.length; i++) {\n\t\tconst count = value[i];\n\t\tif (count instanceof Error) {\n\t\t\tonRefused(batch[i].key, count);\n\t\t\trefused = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst exceeds = count === 'exceeds-limit';\n\t\titems.push({\n\t\t\tkey: batch[i].key,\n\t\t\tcount: exceeds ? undefined : count,\n\t\t\t// Only a reported count can exceed a declared ceiling; unknown-vs-limit is not a comparison.\n\t\t\texceedsProviderLimit: exceeds || (count != null && providerLimit != null && count > providerLimit),\n\t\t\tproviderLimit: providerLimit,\n\t\t});\n\t}\n\treturn refused;\n}\n\n/** The first key that appears twice, or `undefined` when every key is unique. */\nfunction findDuplicateKey(scopes: readonly { key: string }[]): string | undefined {\n\tconst seen = new Set<string>();\n\tfor (const scope of scopes) {\n\t\tif (seen.has(scope.key)) return scope.key;\n\n\t\tseen.add(scope.key);\n\t}\n\treturn undefined;\n}\n\n/**\n * Why one scope can't be counted, as the warning to report — or `undefined` when it can.\n *\n * The scope and criteria rules come from {@link resolveIssueSearchScope} / {@link resolveIssueSearchCriteria},\n * the same validators `searchIssuesPage` uses, so a count always previews the constraints the read would apply.\n * That is not cosmetic: a count computed under different constraints is a WRONG number rather than a missing\n * one, which is worse. Only the wording is local, because these name the offending scope's key.\n */\nfunction rejectScope(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tscope: IssueCountScope,\n): ProviderWarning | undefined {\n\tconst resolved = resolveIssueSearchCriteria(providerId, scope.criteria);\n\tif (resolved.rejection != null) {\n\t\treturn unsupportedIssueSearchCriteriaWarning(providerId, domain, connectionId, resolved.rejection);\n\t}\n\n\t// AFTER the criteria check, mirroring `searchIssuesPage`: a provider with no filtered issue search is the more\n\t// fundamental refusal, and a count must preview the refusal its read would give.\n\tconst scoping = resolveIssueSearchScope(providerId, scope.repos, scope.org, scope.criteria);\n\tswitch (scoping.rejection?.reason) {\n\t\tcase 'repo-ids':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Issue count scope '${scope.key}' cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.`,\n\t\t\t);\n\t\tcase 'unscoped':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Issue count scope '${scope.key}' is unscoped; pass \\`repos\\`, \\`org\\`, or a relationship to the current user. \\`any-assignee\\` and \\`unassigned\\` are not scopes.`,\n\t\t\t);\n\t\t// A count applies exactly the qualifiers its search would, so an unusable scope would make it agree with\n\t\t// the wrong search rather than expose it — see `isUsableSearchScopeName`.\n\t\tcase 'unusable-scope':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\tunusableSearchScopeMessage('scopes', scoping.rejection.scopes, `Issue count scope '${scope.key}'`),\n\t\t\t);\n\t}\n\n\t// Count-only, with no counterpart in the read: a relationship set is an OR across several searches, which one\n\t// count can't express — summing them would double-count anything matching two, and taking the max would\n\t// under-report. Rather than answer with a wrong number, ask the caller to count each relationship as its own\n\t// scope, where the keys make the OR explicit.\n\tif ((scope.criteria?.relationships?.length ?? 0) > 1) {\n\t\treturn otherWarning(\n\t\t\tproviderId,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t`Issue count scope '${scope.key}' requests several relationships, which a single count can't express (they are OR-ed, so overlapping matches would be double-counted); pass one scope per relationship.`,\n\t\t);\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Counts pull requests for many scopes without fetching any — the PR twin of {@link countIssues}, and the probe\n * behind a \"this will fetch ~N pull requests\" preview.\n *\n * Identical batching, per-scope isolation, and `key`-echo contract as {@link countIssues}; see it for the cost\n * model and the `count: undefined` ≠ zero rule. The one difference is inherent to pull requests: a scope's criteria\n * can name several STATES, which GitHub/GHE count as independent searches — the reported `count` is the LARGEST\n * of them (mirroring `searchPullRequestsPage`'s total), so an unpaged read still can't exceed a single search's\n * ceiling undetected. Bitbucket Data Center has no ceiling and no count query: it counts the exact union by reading\n * each scope within a budget, and flags a scope that had more as `lowerBound`. Because it deduplicates rows, it is\n * also the one provider that accepts several relationships in one scope. Azure DevOps Server counts the union of the\n * states from the one drain its search pages through.\n */\nexport async function countPullRequests(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\tscopes: readonly PullRequestCountScope[];\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderResult<PullRequestCountResult>> {\n\tconst refused = (warning: ProviderWarning): ProviderResult<PullRequestCountResult> => ({\n\t\titems: [],\n\t\twarnings: [warning],\n\t\tfetchFailed: true,\n\t});\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Pull request counts'),\n\t\t);\n\t}\n\n\t// Nothing was asked for, so nothing is missing: an empty success, not a refusal.\n\tif (options.scopes.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.scopes);\n\tif (duplicateKey != null) {\n\t\t// Refuses the whole call rather than deduping: `key` exists so the caller can match results without\n\t\t// positional bookkeeping, and two results under one key make that ambiguous for EVERY scope, not just the\n\t\t// repeated one.\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Duplicate pull request count scope key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn { items: [], warnings: early.warnings, fetchFailed: early.fetchFailed || undefined };\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Pull request counts'),\n\t\t);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\n\t// A provider with no filtered pull-request search has no count either: refuse ONCE for the provider rather than\n\t// letting every scope repeat the same rejection. `undefined` criteria probes only the search's existence.\n\tconst searchSupport = resolvePullRequestSearchCriteria(options.providerId, undefined);\n\tif (searchSupport.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedPullRequestSearchCriteriaWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tsearchSupport.rejection,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst providerLimit = providersMetadata[options.providerId]?.pullRequestSearchResultLimit;\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\n\t// Validate every scope first, so a refusal costs no request at all and the countable ones are still batched\n\t// together.\n\tconst countable: PullRequestCountScope[] = [];\n\tfor (const scope of options.scopes) {\n\t\tconst warning = rejectPullRequestScope(options.providerId, domain, options.connectionId, scope);\n\t\tif (warning != null) {\n\t\t\t// Deduped rather than pushed: see the issue twin. Here the keyless set is wider — besides the criteria\n\t\t\t// rejections, `unsupported-repository-scope` and `unsupported-organization-scope` are scope rejections\n\t\t\t// that route through the criteria warning and name no key, and both are provider-level facts every\n\t\t\t// scope would repeat.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tcountable.push(scope);\n\t}\n\n\t// Chunks are independent requests over their own slice of scopes, `runCaptured` never throws, so they run\n\t// concurrently, bounded like every other fan-out on the facade. `mapBounded` returns in input order, so `items`\n\t// and `warnings` stay in scope order.\n\tconst chunks = chunk(countable, countsPullRequestsByReading(options.providerId) ? 1 : pullRequestCountChunkSize);\n\tconst batches = await mapBounded(chunks, providerFanOutConcurrency, batch =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\tintegration.countPullRequestsResult(\n\t\t\t\t\tbatch.map(s => ({\n\t\t\t\t\t\trepos: s.repos as ProviderRepoInput[] | undefined,\n\t\t\t\t\t\torg: s.org,\n\t\t\t\t\t\tcriteria: s.criteria,\n\t\t\t\t\t})),\n\t\t\t\t\tundefined,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t).then(result => ({ batch: batch, ...result })),\n\t);\n\n\tconst items: PullRequestCountResult[] = [];\n\tfor (const { batch, value, warning } of batches) {\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// This batch contributes nothing, but the batches around it still do. Drop only these scopes.\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (let i = 0; i < batch.length; i++) {\n\t\t\tconst slot = value[i];\n\t\t\t// A scope the provider refused for its own reasons: reported and dropped like one the facade refuses.\n\t\t\tif (slot instanceof Error) {\n\t\t\t\tconst warning = toProviderWarning(options.providerId, domain, options.connectionId, slot);\n\t\t\t\tappendDedupedWarning(warnings, {\n\t\t\t\t\t...warning,\n\t\t\t\t\tmessage: `Pull request count scope '${batch[i].key}': ${warning.message}`,\n\t\t\t\t});\n\t\t\t\tfetchFailed = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst count = slot?.count;\n\t\t\titems.push({\n\t\t\t\tkey: batch[i].key,\n\t\t\t\tcount: count,\n\t\t\t\t...(count != null && slot?.lowerBound === true ? { lowerBound: true } : {}),\n\t\t\t\t// Only a reported count can exceed a declared ceiling; unknown-vs-limit is not a comparison.\n\t\t\t\texceedsProviderLimit: count != null && providerLimit != null && count > providerLimit,\n\t\t\t\tproviderLimit: providerLimit,\n\t\t\t});\n\t\t}\n\t}\n\n\t// A provider that reported nothing for any scope and raised no warning: say so explicitly rather than letting it\n\t// read as \"every scope matched nothing\".\n\tif (items.length === 0 && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedPullRequestSearchCriteriaWarning(options.providerId, domain, options.connectionId, {\n\t\t\t\treason: 'unsupported-search',\n\t\t\t}),\n\t\t);\n\t}\n\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\n/**\n * Why one pull-request scope can't be counted, as the warning to report — or `undefined` when it can. The PR twin\n * of {@link rejectScope}, validating against the same {@link resolvePullRequestSearchScope} /\n * {@link resolvePullRequestSearchCriteria} the read uses, so a count always previews the constraints the read\n * would apply.\n */\nfunction rejectPullRequestScope(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tscope: PullRequestCountScope,\n): ProviderWarning | undefined {\n\tconst scoping = resolvePullRequestSearchScope(providerId, scope.repos, scope.org, scope.criteria);\n\tswitch (scoping.rejection?.reason) {\n\t\tcase 'repo-ids':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Pull request count scope '${scope.key}' cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.`,\n\t\t\t);\n\t\tcase 'unscoped':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Pull request count scope '${scope.key}' is unscoped; pass \\`repos\\`, \\`org\\`, or at least one current-user relationship.`,\n\t\t\t);\n\t\tcase 'unsupported-repository-scope':\n\t\tcase 'unsupported-organization-scope':\n\t\t\treturn unsupportedPullRequestSearchCriteriaWarning(providerId, domain, connectionId, {\n\t\t\t\treason: 'unsupported-criteria',\n\t\t\t\tcriteria: [\n\t\t\t\t\tscoping.rejection.reason === 'unsupported-repository-scope'\n\t\t\t\t\t\t? 'repositoryScope'\n\t\t\t\t\t\t: 'organizationScope',\n\t\t\t\t],\n\t\t\t});\n\t\t// See the issue twin, and `isUsableSearchScopeName` for the rule.\n\t\tcase 'unusable-scope':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\tunusableSearchScopeMessage(\n\t\t\t\t\t'scopes',\n\t\t\t\t\tscoping.rejection.scopes,\n\t\t\t\t\t`Pull request count scope '${scope.key}'`,\n\t\t\t\t),\n\t\t\t);\n\t}\n\n\tconst resolved = resolvePullRequestSearchCriteria(providerId, scope.criteria);\n\tif (resolved.rejection != null) {\n\t\treturn unsupportedPullRequestSearchCriteriaWarning(providerId, domain, connectionId, resolved.rejection);\n\t}\n\n\t// Count-only, with no counterpart in the read: a relationship set is an OR across several searches, which one\n\t// count can't express — summing would double-count overlaps and max would under-report. Ask the caller to count\n\t// each relationship as its own scope, where the keys make the OR explicit. (States are disjoint, so a scope may\n\t// still name several — the provider counts them as the max, not a refusal.) A provider that counts by reading\n\t// deduplicates the matching rows themselves, so it counts the OR exactly, as the search it previews returns it.\n\t// That makes the refusal the provider's capability, so `unsupported`; the issue twin refuses on every provider.\n\tif ((scope.criteria?.relationships?.length ?? 0) > 1 && !countsPullRequestsByReading(providerId)) {\n\t\treturn unsupportedWarning(\n\t\t\tproviderId,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t`Pull request count scope '${scope.key}' requests several relationships, which a single count can't express (they are OR-ed, so overlapping matches would be double-counted); pass one scope per relationship.`,\n\t\t);\n\t}\n\n\treturn undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA0CA,MAAM,sBAAsB;;;;;;;;;;;AAY5B,MAAM,4BAA4B;;;;;;AAOlC,SAAS,4BAA4B,YAAqC;CACzE,OAAO,eAAA;AACR;;;;;;;;;;;;;;;;;AA0GA,eAAsB,YACrB,KACA,SAU4C;CAC5C,MAAM,WAAW,aAAgE;EAChF,OAAO,CAAC;EACR,UAAU,CAAC,OAAO;EAClB,aAAa;CACd;CAEA,IAAI,0BAA0B,QAAQ,UAAU,GAC/C,OAAO,QAAQ,0BAA0B,QAAQ,YAAY,KAAA,GAAW,QAAQ,cAAc,cAAc,CAAC;CAI9G,IAAI,QAAQ,OAAO,WAAW,GAAG,OAAO;EAAE,OAAO,CAAC;EAAG,UAAU,CAAC;CAAE;CAElE,MAAM,eAAe,iBAAiB,QAAQ,MAAM;CACpD,IAAI,gBAAgB,MAInB,OAAO,QACN,aACC,QAAQ,YACR,KAAA,GACA,QAAQ,cACR,oCAAoC,aAAa,kDAClD,CACD;CAGD,MAAM,cAAc,MAAM,IAAI,sBAAsB,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CAC5G,IAAI,eAAe,MAAM;EAExB,MAAM,QAAQ,IAAI,8BAA8B,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;EACxG,OAAO;GAAE,OAAO,CAAC;GAAG,UAAU,MAAM;GAAU,aAAa,MAAM,eAAe,KAAA;EAAU;CAC3F;CACA,IAAI,CAAC,qBAAqB,WAAW,GACpC,OAAO,QAAQ,0BAA0B,QAAQ,YAAY,KAAA,GAAW,QAAQ,cAAc,cAAc,CAAC;CAG9G,MAAM,SAAS,IAAI,cAAc,aAAa,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CACtG,MAAM,uBAAuB,8BAA8B,QAAQ,YAAY,QAAQ,MAAM;CAE7F,IAAI,CAAC,YAAY,gBAChB,OAAO,QAAQ,yBAAyB,QAAQ,YAAY,QAAQ,QAAQ,YAAY,CAAC;CAQ1F,MAAM,gBAAgB,2BAA2B,QAAQ,YAAY,KAAA,CAAS;CAC9E,IAAI,cAAc,aAAa,MAC9B,OAAO,QACN,sCACC,QAAQ,YACR,QACA,QAAQ,cACR,cAAc,SACf,CACD;CAGD,MAAM,gBAAgB,kBAAkB,QAAQ,WAAW,EAAE;CAC7D,MAAM,WAA8B,CAAC;CACrC,IAAI,cAAc;CAIlB,MAAM,YAA+B,CAAC;CACtC,KAAK,MAAM,SAAS,QAAQ,QAAQ;EACnC,MAAM,UAAU,YAAY,QAAQ,YAAY,QAAQ,QAAQ,cAAc,KAAK;EACnF,IAAI,WAAW,MAAM;GAMpB,qBAAqB,UAAU,OAAO;GACtC,cAAc;GACd;EACD;EAEA,UAAU,KAAK,KAAK;CACrB;CAMA,MAAM,UAAU,MAAM,WAAW,MAAM,WAAW,mBAAmB,GAAA,IAA8B,UAClG,YACC,QAAQ,YACR,QACA,QAAQ,oBAEP,YAAY,kBACX,MAAM,KAAI,OAAM;EACf,OAAO,EAAE;EACT,KAAK,EAAE;EACP,UAAU,EAAE;CACb,EAAE,GACF,KAAA,GACA,QAAQ,YACT,GACD,EAAwB,qBAAqB,CAC9C,CAAC,CAAC,MAAK,YAAW;EAAS;EAAO,GAAG;CAAO,EAAE,CAC/C;CAEA,MAAM,QAA4B,CAAC;CACnC,KAAK,MAAM,EAAE,OAAO,OAAO,aAAa,SAAS;EAChD,IAAI,WAAW,MACd,qBAAqB,UAAU,OAAO;EAEvC,IAAI,SAAS,MAAM;GAElB,cAAc;GACd;EACD;EAEA,cACC,cAAc,OAAO,OAAO,eAAe,QAAQ,KAAK,UAAU;GACjE,MAAM,UAAU,kBAAkB,QAAQ,YAAY,QAAQ,QAAQ,cAAc,KAAK;GACzF,qBAAqB,UAAU;IAC9B,GAAG;IACH,SAAS,sBAAsB,IAAI,KAAK,QAAQ;GACjD,CAAC;EACF,CAAC,KAAK;CACR;CAIA,IAAI,MAAM,WAAW,KAAK,SAAS,WAAW,GAC7C,OAAO,QACN,sCAAsC,QAAQ,YAAY,QAAQ,QAAQ,cAAc,EACvF,QAAQ,qBACT,CAAC,CACF;CAGD,OAAO;EAAS;EAAiB;EAAU,aAAa,eAAe,KAAA;CAAU;AAClF;;;;;;;;;AAUA,SAAS,cACR,OACA,OACA,eACA,OACA,WACU;CACV,IAAI,UAAU;CACd,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACtC,MAAM,QAAQ,MAAM;EACpB,IAAI,iBAAiB,OAAO;GAC3B,UAAU,MAAM,EAAE,CAAC,KAAK,KAAK;GAC7B,UAAU;GACV;EACD;EAEA,MAAM,UAAU,UAAU;EAC1B,MAAM,KAAK;GACV,KAAK,MAAM,EAAE,CAAC;GACd,OAAO,UAAU,KAAA,IAAY;GAE7B,sBAAsB,WAAY,SAAS,QAAQ,iBAAiB,QAAQ,QAAQ;GACrE;EAChB,CAAC;CACF;CACA,OAAO;AACR;;AAGA,SAAS,iBAAiB,QAAwD;CACjF,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,MAAM,SAAS,QAAQ;EAC3B,IAAI,KAAK,IAAI,MAAM,GAAG,GAAG,OAAO,MAAM;EAEtC,KAAK,IAAI,MAAM,GAAG;CACnB;AAED;;;;;;;;;AAUA,SAAS,YACR,YACA,QACA,cACA,OAC8B;CAC9B,MAAM,WAAW,2BAA2B,YAAY,MAAM,QAAQ;CACtE,IAAI,SAAS,aAAa,MACzB,OAAO,sCAAsC,YAAY,QAAQ,cAAc,SAAS,SAAS;CAKlG,MAAM,UAAU,wBAAwB,YAAY,MAAM,OAAO,MAAM,KAAK,MAAM,QAAQ;CAC1F,QAAQ,QAAQ,WAAW,QAA3B;EACC,KAAK,YACJ,OAAO,aACN,YACA,QACA,cACA,sBAAsB,MAAM,IAAI,6FACjC;EACD,KAAK,YACJ,OAAO,aACN,YACA,QACA,cACA,sBAAsB,MAAM,IAAI,mIACjC;EAGD,KAAK,kBACJ,OAAO,aACN,YACA,QACA,cACA,2BAA2B,UAAU,QAAQ,UAAU,QAAQ,sBAAsB,MAAM,IAAI,EAAE,CAClG;CACF;CAMA,KAAK,MAAM,UAAU,eAAe,UAAU,KAAK,GAClD,OAAO,aACN,YACA,QACA,cACA,sBAAsB,MAAM,IAAI,wKACjC;AAIF;;;;;;;;;;;;;;AAeA,eAAsB,kBACrB,KACA,SAUkD;CAClD,MAAM,WAAW,aAAsE;EACtF,OAAO,CAAC;EACR,UAAU,CAAC,OAAO;EAClB,aAAa;CACd;CAEA,IAAI,0BAA0B,QAAQ,UAAU,GAC/C,OAAO,QACN,0BAA0B,QAAQ,YAAY,KAAA,GAAW,QAAQ,cAAc,qBAAqB,CACrG;CAID,IAAI,QAAQ,OAAO,WAAW,GAAG,OAAO;EAAE,OAAO,CAAC;EAAG,UAAU,CAAC;CAAE;CAElE,MAAM,eAAe,iBAAiB,QAAQ,MAAM;CACpD,IAAI,gBAAgB,MAInB,OAAO,QACN,aACC,QAAQ,YACR,KAAA,GACA,QAAQ,cACR,2CAA2C,aAAa,kDACzD,CACD;CAGD,MAAM,cAAc,MAAM,IAAI,sBAAsB,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CAC5G,IAAI,eAAe,MAAM;EAExB,MAAM,QAAQ,IAAI,8BAA8B,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;EACxG,OAAO;GAAE,OAAO,CAAC;GAAG,UAAU,MAAM;GAAU,aAAa,MAAM,eAAe,KAAA;EAAU;CAC3F;CACA,IAAI,CAAC,qBAAqB,WAAW,GACpC,OAAO,QACN,0BAA0B,QAAQ,YAAY,KAAA,GAAW,QAAQ,cAAc,qBAAqB,CACrG;CAGD,MAAM,SAAS,IAAI,cAAc,aAAa,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CACtG,MAAM,uBAAuB,8BAA8B,QAAQ,YAAY,QAAQ,MAAM;CAI7F,MAAM,gBAAgB,iCAAiC,QAAQ,YAAY,KAAA,CAAS;CACpF,IAAI,cAAc,aAAa,MAC9B,OAAO,QACN,4CACC,QAAQ,YACR,QACA,QAAQ,cACR,cAAc,SACf,CACD;CAGD,MAAM,gBAAgB,kBAAkB,QAAQ,WAAW,EAAE;CAC7D,MAAM,WAA8B,CAAC;CACrC,IAAI,cAAc;CAIlB,MAAM,YAAqC,CAAC;CAC5C,KAAK,MAAM,SAAS,QAAQ,QAAQ;EACnC,MAAM,UAAU,uBAAuB,QAAQ,YAAY,QAAQ,QAAQ,cAAc,KAAK;EAC9F,IAAI,WAAW,MAAM;GAKpB,qBAAqB,UAAU,OAAO;GACtC,cAAc;GACd;EACD;EAEA,UAAU,KAAK,KAAK;CACrB;CAKA,MAAM,SAAS,MAAM,WAAW,4BAA4B,QAAQ,UAAU,IAAI,IAAI,yBAAyB;CAC/G,MAAM,UAAU,MAAM,WAAW,QAAA,IAAmC,UACnE,YACC,QAAQ,YACR,QACA,QAAQ,oBAEP,YAAY,wBACX,MAAM,KAAI,OAAM;EACf,OAAO,EAAE;EACT,KAAK,EAAE;EACP,UAAU,EAAE;CACb,EAAE,GACF,KAAA,GACA,QAAQ,YACT,GACD,EAAwB,qBAAqB,CAC9C,CAAC,CAAC,MAAK,YAAW;EAAS;EAAO,GAAG;CAAO,EAAE,CAC/C;CAEA,MAAM,QAAkC,CAAC;CACzC,KAAK,MAAM,EAAE,OAAO,OAAO,aAAa,SAAS;EAChD,IAAI,WAAW,MACd,qBAAqB,UAAU,OAAO;EAEvC,IAAI,SAAS,MAAM;GAElB,cAAc;GACd;EACD;EAEA,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;GACtC,MAAM,OAAO,MAAM;GAEnB,IAAI,gBAAgB,OAAO;IAC1B,MAAM,UAAU,kBAAkB,QAAQ,YAAY,QAAQ,QAAQ,cAAc,IAAI;IACxF,qBAAqB,UAAU;KAC9B,GAAG;KACH,SAAS,6BAA6B,MAAM,EAAE,CAAC,IAAI,KAAK,QAAQ;IACjE,CAAC;IACD,cAAc;IACd;GACD;GAEA,MAAM,QAAQ,MAAM;GACpB,MAAM,KAAK;IACV,KAAK,MAAM,EAAE,CAAC;IACP;IACP,GAAI,SAAS,QAAQ,MAAM,eAAe,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IAEzE,sBAAsB,SAAS,QAAQ,iBAAiB,QAAQ,QAAQ;IACzD;GAChB,CAAC;EACF;CACD;CAIA,IAAI,MAAM,WAAW,KAAK,SAAS,WAAW,GAC7C,OAAO,QACN,4CAA4C,QAAQ,YAAY,QAAQ,QAAQ,cAAc,EAC7F,QAAQ,qBACT,CAAC,CACF;CAGD,OAAO;EAAS;EAAiB;EAAU,aAAa,eAAe,KAAA;CAAU;AAClF;;;;;;;AAQA,SAAS,uBACR,YACA,QACA,cACA,OAC8B;CAC9B,MAAM,UAAU,8BAA8B,YAAY,MAAM,OAAO,MAAM,KAAK,MAAM,QAAQ;CAChG,QAAQ,QAAQ,WAAW,QAA3B;EACC,KAAK,YACJ,OAAO,aACN,YACA,QACA,cACA,6BAA6B,MAAM,IAAI,6FACxC;EACD,KAAK,YACJ,OAAO,aACN,YACA,QACA,cACA,6BAA6B,MAAM,IAAI,mFACxC;EACD,KAAK;EACL,KAAK,kCACJ,OAAO,4CAA4C,YAAY,QAAQ,cAAc;GACpF,QAAQ;GACR,UAAU,CACT,QAAQ,UAAU,WAAW,iCAC1B,oBACA,mBACJ;EACD,CAAC;EAEF,KAAK,kBACJ,OAAO,aACN,YACA,QACA,cACA,2BACC,UACA,QAAQ,UAAU,QAClB,6BAA6B,MAAM,IAAI,EACxC,CACD;CACF;CAEA,MAAM,WAAW,iCAAiC,YAAY,MAAM,QAAQ;CAC5E,IAAI,SAAS,aAAa,MACzB,OAAO,4CAA4C,YAAY,QAAQ,cAAc,SAAS,SAAS;CASxG,KAAK,MAAM,UAAU,eAAe,UAAU,KAAK,KAAK,CAAC,4BAA4B,UAAU,GAC9F,OAAO,mBACN,YACA,QACA,cACA,6BAA6B,MAAM,IAAI,wKACxC;AAIF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { appendDedupedWarning } from "../results.js";
|
|
2
|
-
import { noConnectionWarning,
|
|
2
|
+
import { noConnectionWarning, unsupportedWarning } from "./warnings.js";
|
|
3
3
|
import { runCaptured } from "./drains.js";
|
|
4
4
|
//#region ../plus/integrations/src/reads/currentAccount.ts
|
|
5
5
|
/**
|
|
@@ -25,7 +25,7 @@ async function getCurrentAccount(ctx, options) {
|
|
|
25
25
|
}
|
|
26
26
|
const domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);
|
|
27
27
|
if (!integration.supportsCurrentAccount) return {
|
|
28
|
-
warnings: [
|
|
28
|
+
warnings: [unsupportedWarning(options.providerId, domain, options.connectionId, `Current account lookup is not supported by '${options.providerId}'.`)],
|
|
29
29
|
fetchFailed: true
|
|
30
30
|
};
|
|
31
31
|
const { value: account, warning } = await runCaptured(options.providerId, domain, options.connectionId, async () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"currentAccount.js","names":[],"sources":["../../../../../plus/integrations/src/reads/currentAccount.ts"],"sourcesContent":["import type { Account } from '@gitlens/git/models/author.js';\nimport type { IntegrationIds } from '../constants.js';\nimport type { ProviderWarning } from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport { noConnectionWarning,
|
|
1
|
+
{"version":3,"file":"currentAccount.js","names":[],"sources":["../../../../../plus/integrations/src/reads/currentAccount.ts"],"sourcesContent":["import type { Account } from '@gitlens/git/models/author.js';\nimport type { IntegrationIds } from '../constants.js';\nimport type { ProviderWarning } from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport { noConnectionWarning, unsupportedWarning } from './warnings.js';\n\n/**\n * \"Who am I on this provider / connection\" — {@link IntegrationBase.getCurrentAccount}'s own answer, exposed on\n * the manager so a caller can ask for it directly instead of waiting for some other read to warm it as a side\n * effect.\n */\nexport interface CurrentAccountResult {\n\t/** The signed-in account, when the provider answered. */\n\taccount?: Account;\n\twarnings: ProviderWarning[];\n\t/**\n\t * Set whenever `account` is absent. Unlike a list/batch read, this one has no benign empty answer — \"who am\n\t * I\" is either resolved or it couldn't be told, never a legitimately empty result — so `account` is never\n\t * missing without a warning explaining why.\n\t */\n\tfetchFailed?: boolean;\n}\n\n/**\n * Resolves the viewer's own account for a git host connection. Issue trackers refuse: they only have a per-resource account.\n *\n * Goes THROUGH `Integration.getCurrentAccount`'s own cache (the hook a host supplies as\n * `IntegrationManagerCacheProvider.getCurrentAccount`) rather than adding a second one, so this read and\n * whatever else warms that cache — today, authorship enrichment on the pull request reads — stay one cache.\n */\nexport async function getCurrentAccount(\n\tctx: ProviderReadContext,\n\toptions: { providerId: IntegrationIds; connectionId?: string; domain?: string },\n): Promise<CurrentAccountResult> {\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\tif (early.warnings.length > 0) return { warnings: early.warnings, fetchFailed: true };\n\n\t\t// The untargeted primary path reads a disconnected provider as a silent empty result on the list reads; an\n\t\t// absent account must still carry a warning here, as on the batch reads — see `CurrentAccountResult.fetchFailed`.\n\t\tconst domain = ctx.resolveDomainForRead(options.providerId, options.connectionId, options.domain);\n\t\treturn { warnings: [noConnectionWarning(options.providerId, domain, options.connectionId)], fetchFailed: true };\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tif (!integration.supportsCurrentAccount) {\n\t\treturn {\n\t\t\twarnings: [\n\t\t\t\tunsupportedWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t`Current account lookup is not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t],\n\t\t\tfetchFailed: true,\n\t\t};\n\t}\n\n\tconst { value: account, warning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\tasync () => {\n\t\t\tconst resolved = await integration.getCurrentAccount({ connectionId: options.connectionId });\n\t\t\t// `undefined` is returned bare (not `{ value: undefined }`) so `runCaptured` treats it the same as\n\t\t\t// a session it couldn't resolve — the only other way this read core answers nothing.\n\t\t\treturn resolved != null ? { value: resolved } : undefined;\n\t\t},\n\t\t{ warnOnMissingSession: true },\n\t);\n\n\tconst warnings: ProviderWarning[] = [];\n\tif (warning != null) {\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\n\treturn {\n\t\t...(account != null ? { account: account } : {}),\n\t\twarnings: warnings,\n\t\tfetchFailed: account == null ? true : undefined,\n\t};\n}\n"],"mappings":";;;;;;;;;;;AAgCA,eAAsB,kBACrB,KACA,SACgC;CAChC,MAAM,cAAc,MAAM,IAAI,sBAAsB,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CAC5G,IAAI,eAAe,MAAM;EACxB,MAAM,QAAQ,IAAI,8BAA8B,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;EACxG,IAAI,MAAM,SAAS,SAAS,GAAG,OAAO;GAAE,UAAU,MAAM;GAAU,aAAa;EAAK;EAIpF,MAAM,SAAS,IAAI,qBAAqB,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;EAChG,OAAO;GAAE,UAAU,CAAC,oBAAoB,QAAQ,YAAY,QAAQ,QAAQ,YAAY,CAAC;GAAG,aAAa;EAAK;CAC/G;CAEA,MAAM,SAAS,IAAI,cAAc,aAAa,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CAEtG,IAAI,CAAC,YAAY,wBAChB,OAAO;EACN,UAAU,CACT,mBACC,QAAQ,YACR,QACA,QAAQ,cACR,+CAA+C,QAAQ,WAAW,GACnE,CACD;EACA,aAAa;CACd;CAGD,MAAM,EAAE,OAAO,SAAS,YAAY,MAAM,YACzC,QAAQ,YACR,QACA,QAAQ,cACR,YAAY;EACX,MAAM,WAAW,MAAM,YAAY,kBAAkB,EAAE,cAAc,QAAQ,aAAa,CAAC;EAG3F,OAAO,YAAY,OAAO,EAAE,OAAO,SAAS,IAAI,KAAA;CACjD,GACA,EAAE,sBAAsB,KAAK,CAC9B;CAEA,MAAM,WAA8B,CAAC;CACrC,IAAI,WAAW,MACd,qBAAqB,UAAU,OAAO;CAGvC,OAAO;EACN,GAAI,WAAW,OAAO,EAAW,QAAQ,IAAI,CAAC;EACpC;EACV,aAAa,WAAW,OAAO,OAAO,KAAA;CACvC;AACD"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { PullRequestFilter, PullRequestStateFilter } from "../../../git/models/pullRequest.js";
|
|
1
|
+
import { PullRequest, PullRequestFilter, PullRequestStateFilter } from "../../../git/models/pullRequest.js";
|
|
2
2
|
import { IntegrationIds } from "../constants.js";
|
|
3
3
|
import { ProviderWarning } from "../results.js";
|
|
4
|
+
import "../providerFilters.js";
|
|
5
|
+
import { ProviderPullRequest, ProviderReposInput, ProviderRepository } from "../providers/models.js";
|
|
4
6
|
import { GitHostIntegration } from "../models/gitHostIntegration.js";
|
|
5
7
|
import { IntegrationResult } from "../models/integration.js";
|
|
6
|
-
import { ProviderPullRequest, ProviderReposInput, ProviderRepository } from "../providers/models.js";
|
|
7
8
|
import { ProviderSweepTarget, PullRequestSweepOptions } from "../manager.js";
|
|
8
9
|
//#region ../plus/integrations/src/reads/drains.d.ts
|
|
9
10
|
/**
|
|
@@ -41,7 +42,7 @@ export declare function drainPullRequests(integration: GitHostIntegration, id: I
|
|
|
41
42
|
* options rather than restated so a third opt-in is declared once.
|
|
42
43
|
*/
|
|
43
44
|
accountWideOptions: Pick<PullRequestSweepOptions, 'includeReviewRequested' | 'includeReviews'> | undefined, connectionId: string | undefined, maxPages: number, attributeUnavailableProvider: boolean): Promise<{
|
|
44
|
-
items: ProviderPullRequest[];
|
|
45
|
+
items: (ProviderPullRequest | PullRequest)[];
|
|
45
46
|
warnings: ProviderWarning[];
|
|
46
47
|
fetchFailed: boolean;
|
|
47
48
|
truncated: boolean;
|
|
@@ -66,5 +67,14 @@ export declare function getCurrentAccountIdentity(integration: GitHostIntegratio
|
|
|
66
67
|
id: string;
|
|
67
68
|
username?: string;
|
|
68
69
|
} | undefined>;
|
|
70
|
+
/**
|
|
71
|
+
* The current account to map each raw row against, in order (see {@link GitHostIntegration.getPullRequestViewers}).
|
|
72
|
+
* Rows with no author can't be matched, so a read of only those resolves nothing; a failed lookup leaves every row's
|
|
73
|
+
* authorship unknown rather than failing the read.
|
|
74
|
+
*/
|
|
75
|
+
export declare function getPullRequestViewers(integration: GitHostIntegration, connectionId: string | undefined, pullRequests: readonly (ProviderPullRequest | PullRequest)[]): Promise<({
|
|
76
|
+
id: string;
|
|
77
|
+
username?: string;
|
|
78
|
+
} | undefined)[]>;
|
|
69
79
|
//#endregion
|
|
70
80
|
//# sourceMappingURL=drains.d.ts.map
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { defaultPullRequestSort } from "../../../git/models/pullRequest.js";
|
|
1
2
|
import { supportedOrderedCloudIntegrationIds } from "../constants.js";
|
|
2
3
|
import { appendDedupedWarning, reconcileOmissionsWithFailure, toProviderWarning } from "../results.js";
|
|
3
4
|
import { mergeAssessmentInto } from "../collectionMetadata.js";
|
|
4
5
|
import { isIssuesHostIntegrationId } from "../utils/integration.utils.js";
|
|
5
6
|
import { getProviderPullRequestIdentity } from "../providers/models.js";
|
|
6
|
-
import { cappedBesideBudgetWarning, noConnectionWarning, truncationWarning } from "./warnings.js";
|
|
7
|
+
import { cappedBesideBudgetWarning, noConnectionWarning, pullRequestSearchCapResultWarning, truncationWarning } from "./warnings.js";
|
|
7
8
|
//#region ../plus/integrations/src/reads/drains.ts
|
|
8
9
|
/**
|
|
9
10
|
* The all-pages reads: a provider read run repeatedly until it runs out of pages, as opposed to the single-page
|
|
@@ -49,6 +50,13 @@ async function drainPullRequests(integration, id, domain, repos, state, filters,
|
|
|
49
50
|
let fetchFailed = false;
|
|
50
51
|
let truncated = false;
|
|
51
52
|
let unexplainedTruncation = false;
|
|
53
|
+
let truncatedTotalCount;
|
|
54
|
+
const capWarning = () => pullRequestSearchCapResultWarning(id, domain, connectionId, truncatedTotalCount, defaultPullRequestSort);
|
|
55
|
+
const appendTruncationWarnings = (cause) => {
|
|
56
|
+
const cap = capWarning();
|
|
57
|
+
if (cap != null) appendDedupedWarning(warnings, cap);
|
|
58
|
+
if (unexplainedTruncation && (cap == null || cause === "interrupted")) appendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, "Pull request", cause));
|
|
59
|
+
};
|
|
52
60
|
const accountWide = repos.length === 0;
|
|
53
61
|
const includeReviewRequested = accountWide && (accountWideOptions?.includeReviewRequested ?? false);
|
|
54
62
|
const includeReviews = accountWide && (accountWideOptions?.includeReviews ?? false);
|
|
@@ -74,7 +82,7 @@ async function drainPullRequests(integration, id, domain, repos, state, filters,
|
|
|
74
82
|
const unavailable = warning == null && (attributeUnavailableProvider || sessionLostAfterProgress);
|
|
75
83
|
if (unavailable) appendDedupedWarning(warnings, noConnectionWarning(id, domain, connectionId));
|
|
76
84
|
const failed = fetchFailed || warning != null || unavailable;
|
|
77
|
-
|
|
85
|
+
appendTruncationWarnings(failed ? "interrupted" : "exhausted");
|
|
78
86
|
reconcileOmissionsWithFailure(warnings, failed);
|
|
79
87
|
return {
|
|
80
88
|
items,
|
|
@@ -101,8 +109,9 @@ async function drainPullRequests(integration, id, domain, repos, state, filters,
|
|
|
101
109
|
const pageTruncated = value.truncated === true || value.paging?.truncated === true || assessment.truncated;
|
|
102
110
|
truncated = truncated || pageTruncated;
|
|
103
111
|
unexplainedTruncation = unexplainedTruncation || pageTruncated && !assessment.truncated;
|
|
112
|
+
if (accountWide && pageTruncated && value.paging?.totalCount != null) truncatedTotalCount = Math.max(truncatedTotalCount ?? 0, value.paging.totalCount);
|
|
104
113
|
if (!(value.paging?.more ?? false)) {
|
|
105
|
-
|
|
114
|
+
appendTruncationWarnings(fetchFailed ? "interrupted" : "exhausted");
|
|
106
115
|
reconcileOmissionsWithFailure(warnings, fetchFailed);
|
|
107
116
|
return {
|
|
108
117
|
items,
|
|
@@ -117,7 +126,9 @@ async function drainPullRequests(integration, id, domain, repos, state, filters,
|
|
|
117
126
|
if (!continuable || page >= maxPages) {
|
|
118
127
|
const budgetStop = !fetchFailed && continuable;
|
|
119
128
|
appendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, "Pull request", fetchFailed ? "interrupted" : budgetStop ? "page-budget" : "exhausted"));
|
|
120
|
-
|
|
129
|
+
const cap = capWarning();
|
|
130
|
+
if (cap != null) appendDedupedWarning(warnings, cap);
|
|
131
|
+
else if (budgetStop && unexplainedTruncation) appendDedupedWarning(warnings, cappedBesideBudgetWarning(id, domain, connectionId, "Pull request"));
|
|
121
132
|
reconcileOmissionsWithFailure(warnings, fetchFailed);
|
|
122
133
|
return {
|
|
123
134
|
items,
|
|
@@ -233,7 +244,20 @@ async function getCurrentAccountIdentity(integration, connectionId) {
|
|
|
233
244
|
return;
|
|
234
245
|
}
|
|
235
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* The current account to map each raw row against, in order (see {@link GitHostIntegration.getPullRequestViewers}).
|
|
249
|
+
* Rows with no author can't be matched, so a read of only those resolves nothing; a failed lookup leaves every row's
|
|
250
|
+
* authorship unknown rather than failing the read.
|
|
251
|
+
*/
|
|
252
|
+
async function getPullRequestViewers(integration, connectionId, pullRequests) {
|
|
253
|
+
if (!pullRequests.some((pr) => pr.author != null)) return pullRequests.map(() => void 0);
|
|
254
|
+
try {
|
|
255
|
+
return await integration.getPullRequestViewers(pullRequests, connectionId);
|
|
256
|
+
} catch {
|
|
257
|
+
return pullRequests.map(() => void 0);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
236
260
|
//#endregion
|
|
237
|
-
export { drainPullRequests, drainRepositories, getCurrentAccountIdentity, resolvePullRequestSweepTargets, runCaptured };
|
|
261
|
+
export { drainPullRequests, drainRepositories, getCurrentAccountIdentity, getPullRequestViewers, resolvePullRequestSweepTargets, runCaptured };
|
|
238
262
|
|
|
239
263
|
//# sourceMappingURL=drains.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drains.js","names":[],"sources":["../../../../../plus/integrations/src/reads/drains.ts"],"sourcesContent":["import type { PullRequestStateFilter } from '@gitlens/git/models/pullRequest.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { supportedOrderedCloudIntegrationIds } from '../constants.js';\nimport type { ProviderSweepTarget, PullRequestSweepOptions } from '../manager.js';\nimport type { GitHostIntegration } from '../models/gitHostIntegration.js';\nimport type { IntegrationResult } from '../models/integration.js';\nimport type { PullRequestFilter } from '../providerFilters.js';\nimport type { ProviderPullRequest, ProviderReposInput, ProviderRepository } from '../providers/models.js';\nimport { getProviderPullRequestIdentity } from '../providers/models.js';\nimport type { ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, reconcileOmissionsWithFailure, toProviderWarning } from '../results.js';\nimport { isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport { cappedBesideBudgetWarning, noConnectionWarning, truncationWarning } from './warnings.js';\n\n/**\n * The all-pages reads: a provider read run repeatedly until it runs out of pages, as opposed to the single-page\n * reads the facade's paged surface exposes. `sweepPullRequests` and `broadenIssues` are built on these.\n *\n * A drain's job is to keep going where a single page would stop, so what distinguishes it is its failure\n * bookkeeping rather than its loop: it must return the prefix it did read plus enough signal for the caller to\n * know the result is not authoritative — `truncated` when pages remain unread (a page backstop, or a provider\n * claiming another page while handing back no usable cursor), `fetchFailed` when a page or scope failed\n * outright. Discarding the prefix on a late failure, or returning it as if complete, are the two mistakes these\n * exist to prevent.\n */\n\n/**\n * Runs a result-returning read and captures failure as a neutral {@link ProviderWarning} rather than\n * letting it throw or silently vanish. Handles both a returned `{ error }` (the read cores' contract)\n * and a hard throw; a soft warning (`{ value, error }`) yields the value *and* a warning.\n */\nexport async function runCaptured<T>(\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tfn: () => Promise<IntegrationResult<T>>,\n\toptions?: { warnOnMissingSession?: boolean },\n): Promise<{ value?: T; warning?: ProviderWarning }> {\n\ttry {\n\t\tconst result = await fn();\n\t\tif (result == null) {\n\t\t\t// The read core returns undefined only when it couldn't resolve a session. For a per-connection\n\t\t\t// or explicit-domain read that means the requested target is gone or its authentication is invalid,\n\t\t\t// which must not be reported as an empty account. The untargeted primary path legitimately yields\n\t\t\t// nothing when the provider isn't connected, so leave it as an empty result.\n\t\t\treturn connectionId != null || options?.warnOnMissingSession\n\t\t\t\t? { warning: noConnectionWarning(id, domain, connectionId) }\n\t\t\t\t: {};\n\t\t}\n\t\tif (result.error != null) {\n\t\t\treturn { value: result.value, warning: toProviderWarning(id, domain, connectionId, result.error) };\n\t\t}\n\t\treturn { value: result.value };\n\t} catch (ex) {\n\t\treturn { warning: toProviderWarning(id, domain, connectionId, ex) };\n\t}\n}\n\n/**\n * Drains every page of the user's pull requests for one git-host integration, threading the opaque\n * next-cursor the provider returns (so it works for both page- and cursor-based hosts). Stops at\n * `maxPages` (marking `truncated`) or on a hard read failure (marking `fetchFailed`), keeping the\n * pages fetched so far. A soft warning (`{ value, error }`) is recorded but the drain continues.\n */\nexport async function drainPullRequests(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\trepos: ProviderReposInput,\n\tstate: PullRequestStateFilter[] | undefined,\n\tfilters: PullRequestFilter[] | undefined,\n\t/**\n\t * Breadth opt-ins that only the account-wide read can express. Grouped rather than passed positionally\n\t * because they are same-typed neighbors a transposition would silently swap, and picked from the sweep\n\t * options rather than restated so a third opt-in is declared once.\n\t */\n\taccountWideOptions: Pick<PullRequestSweepOptions, 'includeReviewRequested' | 'includeReviews'> | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n\tattributeUnavailableProvider: boolean,\n): Promise<{\n\titems: ProviderPullRequest[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n\tfailedProvider: boolean;\n}> {\n\tconst items: ProviderPullRequest[] = [];\n\tconst itemIndexByIdentity = new Map<string, number>();\n\tconst warnings: ProviderWarning[] = [];\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\t// SDK metadata failures across pages mean the collection is incomplete even when no page threw; carry\n\t// this through the terminal returns instead of resetting it to false at the last page.\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\t// A page the provider capped or couldn't vouch for, and which no other warning already explains — decides\n\t// whether this drain raises one of its own for that part.\n\tlet unexplainedTruncation = false;\n\n\t// With no repos this is an account-wide \"my PRs\" sweep. The repo-scoped core rejects an empty `repos`\n\t// input, so read the provider-native account-wide core instead.\n\tconst accountWide = repos.length === 0;\n\t// The breadth opt-ins are only expressible on the account-wide core, so gate them here — the single place\n\t// that already knows which core it is about to call.\n\tconst includeReviewRequested = accountWide && (accountWideOptions?.includeReviewRequested ?? false);\n\tconst includeReviews = accountWide && (accountWideOptions?.includeReviews ?? false);\n\t/** Every cursor already followed, so a provider that cycles them can't keep the drain walking in circles. */\n\tconst seenCursors = new Set<string>();\n\n\tfor (;;) {\n\t\tpage++;\n\t\t// Snapshot the mutable loop cursor so the read closure doesn't capture a later-reassigned value.\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\taccountWide\n\t\t\t\t? integration.getMyPullRequestsForUserResult(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstate: state,\n\t\t\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\t\t\tincludeReviewRequested: includeReviewRequested,\n\t\t\t\t\t\t\tfilters: filters,\n\t\t\t\t\t\t\t// Lite by default; the full projection (reviews) only when a caller opts in.\n\t\t\t\t\t\t\tsummary: !includeReviews,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t)\n\t\t\t\t: integration.getMyPullRequestsForReposResult(\n\t\t\t\t\t\trepos,\n\t\t\t\t\t\t{ state: state, filters: filters, cursor: pageCursor },\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t),\n\t\t);\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// An implicit sweep may silently skip a provider that has no session before it yields data.\n\t\t\t// Once a page has been returned, however, losing that session leaves an unread tail and must\n\t\t\t// be attributed even when the provider wasn't explicitly requested.\n\t\t\tconst sessionLostAfterProgress = warning == null && page > 1;\n\t\t\tconst unavailable = warning == null && (attributeUnavailableProvider || sessionLostAfterProgress);\n\t\t\tif (unavailable) {\n\t\t\t\tappendDedupedWarning(warnings, noConnectionWarning(id, domain, connectionId));\n\t\t\t}\n\t\t\t// `warning` set → a hard read failure (incomplete items); otherwise not connected / no session.\n\t\t\tconst failed = fetchFailed || warning != null || unavailable;\n\t\t\t// A cap seen on an earlier page still left results out, so it is still worth saying — but as part\n\t\t\t// of a read that failed, never as an omission. (`failed` is always true here when anything was\n\t\t\t// latched: reaching this exit past page 1 means a later page was lost.)\n\t\t\tif (unexplainedTruncation) {\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(id, domain, connectionId, 'Pull request', failed ? 'interrupted' : 'exhausted'),\n\t\t\t\t);\n\t\t\t}\n\t\t\t// An earlier page may already have emitted an omission before this one died; it asserts the read\n\t\t\t// succeeded, which is no longer true.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || sessionLostAfterProgress,\n\t\t\t\t// Only a top-level first-page rejection means the provider itself failed. A later-page or\n\t\t\t\t// per-scope failure still yielded a usable provider slice and stays represented separately.\n\t\t\t\tfailedProvider: page === 1 && (warning != null || unavailable),\n\t\t\t};\n\t\t}\n\n\t\t// Composite account-wide queries can surface the same PR on different relationship/state pages\n\t\t// (for example authored + review-requested, or closed + merged). Keep the first stable position but\n\t\t// replace its value with the latest representation so a later, richer merged state wins. Repository\n\t\t// identity is what keys a row (see `getProviderPullRequestIdentity`), falling back to the canonical\n\t\t// URL; a row with neither stays unkeyed and is retained so unrelated incomplete rows are never\n\t\t// collapsed.\n\t\tfor (const pullRequest of value.values) {\n\t\t\tconst identity = getProviderPullRequestIdentity(pullRequest);\n\t\t\tif (identity == null) {\n\t\t\t\titems.push(pullRequest);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst existingIndex = itemIndexByIdentity.get(identity);\n\t\t\tif (existingIndex == null) {\n\t\t\t\titemIndexByIdentity.set(identity, items.length);\n\t\t\t\titems.push(pullRequest);\n\t\t\t} else {\n\t\t\t\titems[existingIndex] = pullRequest;\n\t\t\t}\n\t\t}\n\n\t\t// Assess this page's SDK metadata: append scope-aware warnings (deduped across pages), and remember\n\t\t// whether a structured failure or incompleteness occurred.\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\tconst pageTruncated =\n\t\t\t(value as { truncated?: boolean }).truncated === true ||\n\t\t\tvalue.paging?.truncated === true ||\n\t\t\tassessment.truncated;\n\t\ttruncated = truncated || pageTruncated;\n\t\t// Whether this drain owes its OWN warning for a capped page — `mergeAssessmentInto` has already appended\n\t\t// one when the fact came from SDK metadata, and repeating it would be noise.\n\t\tunexplainedTruncation = unexplainedTruncation || (pageTruncated && !assessment.truncated);\n\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\t// A read that can't confirm completeness (single-page provider reads with no `hasNextPage`)\n\t\t\t// sets `paging.truncated`; propagate it (and any top-level `truncated` and SDK incompleteness)\n\t\t\t// so the sweep doesn't claim an all-pages result.\n\t\t\tif (unexplainedTruncation) {\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t\t'Pull request',\n\t\t\t\t\t\tfetchFailed ? 'interrupted' : 'exhausted',\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\t// Resolve the continuation BEFORE deciding why the drain stops. `page-budget` claims the missing items\n\t\t// are reachable, which is only true with a usable cursor in hand — and a provider can report another\n\t\t// page while handing back none (Bitbucket Server does, when it omits `nextPageStart`). Checking the\n\t\t// budget first would label that unreachable tail as merely unfetched, so raising `maxPages` would\n\t\t// return the identical set.\n\t\tconst nextCursor = value.paging?.cursor;\n\t\t// A cursor already used isn't a continuation either — following it refetches a page we have, and a\n\t\t// provider that cycles (A→B→A) would otherwise burn the whole budget and then be reported as merely\n\t\t// out of budget. Tracked as a SET rather than compared one-back, matching the SDK's own `followCursors`:\n\t\t// `drainToRequestedPage` and `collectProviderPagedResult` compare only the previous cursor, which a\n\t\t// cycle slips past. That is tolerable there and not here, because only this drain reports `page-budget`.\n\t\tconst continuable = nextCursor != null && nextCursor !== '{}' && !seenCursors.has(nextCursor);\n\t\tif (!continuable || page >= maxPages) {\n\t\t\t// A usable cursor in hand means `page-budget`, even when an earlier page was capped: a composite\n\t\t\t// read caps its facets independently, so one facet reaching the provider's ceiling leaves its\n\t\t\t// siblings' cursors live, and raising the budget returns more of them. What no budget returns is the\n\t\t\t// capped part, so that is reported BESIDE the budget stop rather than in place of it — unless an SDK\n\t\t\t// omission already says so. Neither warning then misstates the part the other describes.\n\t\t\tconst budgetStop = !fetchFailed && continuable;\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\ttruncationWarning(\n\t\t\t\t\tid,\n\t\t\t\t\tdomain,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\t'Pull request',\n\t\t\t\t\tfetchFailed ? 'interrupted' : budgetStop ? 'page-budget' : 'exhausted',\n\t\t\t\t),\n\t\t\t);\n\t\t\tif (budgetStop && unexplainedTruncation) {\n\t\t\t\tappendDedupedWarning(warnings, cappedBesideBudgetWarning(id, domain, connectionId, 'Pull request'));\n\t\t\t}\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: true,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\tseenCursors.add(nextCursor);\n\t\tcursor = nextCursor;\n\t}\n}\n\n/**\n * Drains every page of repositories under an org for one git-host integration, threading the opaque\n * next-cursor the provider returns. Stops at `maxPages` (marking `truncated`) or on a hard read failure\n * (marking `fetchFailed`), keeping the pages fetched so far.\n */\nexport async function drainRepositories(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\torg: string,\n\tproject: string | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n): Promise<{\n\trepos: ProviderRepository[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n}> {\n\tconst repos: ProviderRepository[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\n\tfor (;;) {\n\t\tpage++;\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(\n\t\t\tid,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t() =>\n\t\t\t\tintegration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\tproject: project,\n\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\tconnectionId: connectionId,\n\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: true },\n\t\t);\n\t\tif (warning != null) {\n\t\t\t// Deduped, like every other accumulation here: a soft warning (`{ value, error }`) repeats verbatim on\n\t\t\t// each page that hits the same condition, and the array's contract is that no two entries are equal.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\tconst interruptedAfterProgress = page > 1;\n\t\t\tif (interruptedAfterProgress && warning == null) {\n\t\t\t\t// Not a backstop: the read was cut short mid-drain, which is why it also sets `fetchFailed`\n\t\t\t\t// below. A retry may complete it, so this must not claim the succeeded-but-capped omission.\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(id, domain, connectionId, 'Repository', 'interrupted'),\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst failed = fetchFailed || warning != null || interruptedAfterProgress;\n\t\t\t// An SDK omission from an earlier page asserts the read succeeded; this one didn't.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || interruptedAfterProgress,\n\t\t\t};\n\t\t}\n\n\t\trepos.push(...value.values);\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\ttruncated = truncated || value.truncated === true || value.paging?.truncated === true || assessment.truncated;\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t};\n\t\t}\n\n\t\tif (page >= maxPages) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tconst nextCursor = value.paging?.cursor;\n\t\tif (nextCursor == null || nextCursor === '{}') {\n\t\t\t// Provider says there is more but didn't return a usable cursor; stop rather than refetch the same page.\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tcursor = nextCursor;\n\t}\n}\n\nexport function resolvePullRequestSweepTargets(options: PullRequestSweepOptions | undefined): {\n\ttargets: readonly ProviderSweepTarget[];\n\tattributeUnavailableProviders: boolean;\n} {\n\tif (options?.targets != null) {\n\t\tif (options.providerIds != null || options.connectionId != null) {\n\t\t\tthrow new TypeError(\"Pull request sweep 'targets' cannot be combined with 'providerIds' or 'connectionId'\");\n\t\t}\n\n\t\tconst seenProviderIds = new Set<IntegrationIds>();\n\t\tfor (const target of options.targets) {\n\t\t\tif (seenProviderIds.has(target.providerId)) {\n\t\t\t\tthrow new TypeError(\n\t\t\t\t\t`Pull request sweep targets must contain at most one target per provider; duplicate '${target.providerId}'`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tseenProviderIds.add(target.providerId);\n\t\t}\n\n\t\treturn { targets: options.targets, attributeUnavailableProviders: true };\n\t}\n\n\tconst providerIds =\n\t\toptions?.providerIds ?? supportedOrderedCloudIntegrationIds.filter(id => !isIssuesHostIntegrationId(id));\n\tconst connectionId = providerIds.length === 1 ? options?.connectionId : undefined;\n\treturn {\n\t\ttargets: providerIds.map(providerId => ({ providerId: providerId, connectionId: connectionId })),\n\t\tattributeUnavailableProviders: options?.providerIds != null,\n\t};\n}\n\nexport async function getCurrentAccountIdentity(\n\tintegration: GitHostIntegration,\n\tconnectionId: string | undefined,\n): Promise<{ id: string; username?: string } | undefined> {\n\ttry {\n\t\tconst account = await integration.getCurrentAccount({ connectionId: connectionId });\n\t\treturn account != null ? { id: account.id, username: account.username } : undefined;\n\t} catch {\n\t\t// Authorship is optional enrichment; don't turn a successful PR read into a failure if identity lookup fails.\n\t\treturn undefined;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"drains.js","names":[],"sources":["../../../../../plus/integrations/src/reads/drains.ts"],"sourcesContent":["import type { PullRequest, PullRequestStateFilter } from '@gitlens/git/models/pullRequest.js';\nimport { defaultPullRequestSort } from '@gitlens/git/models/pullRequest.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { supportedOrderedCloudIntegrationIds } from '../constants.js';\nimport type { ProviderSweepTarget, PullRequestSweepOptions } from '../manager.js';\nimport type { GitHostIntegration } from '../models/gitHostIntegration.js';\nimport type { IntegrationResult } from '../models/integration.js';\nimport type { PullRequestFilter } from '../providerFilters.js';\nimport type { ProviderPullRequest, ProviderReposInput, ProviderRepository } from '../providers/models.js';\nimport { getProviderPullRequestIdentity } from '../providers/models.js';\nimport type { ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, reconcileOmissionsWithFailure, toProviderWarning } from '../results.js';\nimport { isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport {\n\tcappedBesideBudgetWarning,\n\tnoConnectionWarning,\n\tpullRequestSearchCapResultWarning,\n\ttruncationWarning,\n} from './warnings.js';\n\n/**\n * The all-pages reads: a provider read run repeatedly until it runs out of pages, as opposed to the single-page\n * reads the facade's paged surface exposes. `sweepPullRequests` and `broadenIssues` are built on these.\n *\n * A drain's job is to keep going where a single page would stop, so what distinguishes it is its failure\n * bookkeeping rather than its loop: it must return the prefix it did read plus enough signal for the caller to\n * know the result is not authoritative — `truncated` when pages remain unread (a page backstop, or a provider\n * claiming another page while handing back no usable cursor), `fetchFailed` when a page or scope failed\n * outright. Discarding the prefix on a late failure, or returning it as if complete, are the two mistakes these\n * exist to prevent.\n */\n\n/**\n * Runs a result-returning read and captures failure as a neutral {@link ProviderWarning} rather than\n * letting it throw or silently vanish. Handles both a returned `{ error }` (the read cores' contract)\n * and a hard throw; a soft warning (`{ value, error }`) yields the value *and* a warning.\n */\nexport async function runCaptured<T>(\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tfn: () => Promise<IntegrationResult<T>>,\n\toptions?: { warnOnMissingSession?: boolean },\n): Promise<{ value?: T; warning?: ProviderWarning }> {\n\ttry {\n\t\tconst result = await fn();\n\t\tif (result == null) {\n\t\t\t// The read core returns undefined only when it couldn't resolve a session. For a per-connection\n\t\t\t// or explicit-domain read that means the requested target is gone or its authentication is invalid,\n\t\t\t// which must not be reported as an empty account. The untargeted primary path legitimately yields\n\t\t\t// nothing when the provider isn't connected, so leave it as an empty result.\n\t\t\treturn connectionId != null || options?.warnOnMissingSession\n\t\t\t\t? { warning: noConnectionWarning(id, domain, connectionId) }\n\t\t\t\t: {};\n\t\t}\n\t\tif (result.error != null) {\n\t\t\treturn { value: result.value, warning: toProviderWarning(id, domain, connectionId, result.error) };\n\t\t}\n\t\treturn { value: result.value };\n\t} catch (ex) {\n\t\treturn { warning: toProviderWarning(id, domain, connectionId, ex) };\n\t}\n}\n\n/**\n * Drains every page of the user's pull requests for one git-host integration, threading the opaque\n * next-cursor the provider returns (so it works for both page- and cursor-based hosts). Stops at\n * `maxPages` (marking `truncated`) or on a hard read failure (marking `fetchFailed`), keeping the\n * pages fetched so far. A soft warning (`{ value, error }`) is recorded but the drain continues.\n */\nexport async function drainPullRequests(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\trepos: ProviderReposInput,\n\tstate: PullRequestStateFilter[] | undefined,\n\tfilters: PullRequestFilter[] | undefined,\n\t/**\n\t * Breadth opt-ins that only the account-wide read can express. Grouped rather than passed positionally\n\t * because they are same-typed neighbors a transposition would silently swap, and picked from the sweep\n\t * options rather than restated so a third opt-in is declared once.\n\t */\n\taccountWideOptions: Pick<PullRequestSweepOptions, 'includeReviewRequested' | 'includeReviews'> | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n\tattributeUnavailableProvider: boolean,\n): Promise<{\n\titems: (ProviderPullRequest | PullRequest)[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n\tfailedProvider: boolean;\n}> {\n\tconst items: (ProviderPullRequest | PullRequest)[] = [];\n\tconst itemIndexByIdentity = new Map<string, number>();\n\tconst warnings: ProviderWarning[] = [];\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\t// SDK metadata failures across pages mean the collection is incomplete even when no page threw; carry\n\t// this through the terminal returns instead of resetting it to false at the last page.\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\t// A page the provider capped or couldn't vouch for, and which no other warning already explains — decides\n\t// whether this drain raises one of its own for that part.\n\tlet unexplainedTruncation = false;\n\t// The largest match count reported on a truncated page, from which the drain derives the provider's cap.\n\tlet truncatedTotalCount: number | undefined;\n\tconst capWarning = (): ProviderWarning | undefined =>\n\t\tpullRequestSearchCapResultWarning(id, domain, connectionId, truncatedTotalCount, defaultPullRequestSort);\n\tconst appendTruncationWarnings = (cause: 'interrupted' | 'exhausted'): void => {\n\t\tconst cap = capWarning();\n\t\tif (cap != null) {\n\t\t\tappendDedupedWarning(warnings, cap);\n\t\t}\n\t\t// A cap explains the missing tail of a read that succeeded; a failed read still owes its own warning.\n\t\tif (unexplainedTruncation && (cap == null || cause === 'interrupted')) {\n\t\t\tappendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, 'Pull request', cause));\n\t\t}\n\t};\n\n\t// With no repos this is an account-wide \"my PRs\" sweep. The repo-scoped core rejects an empty `repos`\n\t// input, so read the provider-native account-wide core instead.\n\tconst accountWide = repos.length === 0;\n\t// The breadth opt-ins are only expressible on the account-wide core, so gate them here — the single place\n\t// that already knows which core it is about to call.\n\tconst includeReviewRequested = accountWide && (accountWideOptions?.includeReviewRequested ?? false);\n\tconst includeReviews = accountWide && (accountWideOptions?.includeReviews ?? false);\n\t/** Every cursor already followed, so a provider that cycles them can't keep the drain walking in circles. */\n\tconst seenCursors = new Set<string>();\n\n\tfor (;;) {\n\t\tpage++;\n\t\t// Snapshot the mutable loop cursor so the read closure doesn't capture a later-reassigned value.\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\taccountWide\n\t\t\t\t? integration.getMyPullRequestsForUserResult(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstate: state,\n\t\t\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\t\t\tincludeReviewRequested: includeReviewRequested,\n\t\t\t\t\t\t\tfilters: filters,\n\t\t\t\t\t\t\t// Lite by default; the full projection (reviews) only when a caller opts in.\n\t\t\t\t\t\t\tsummary: !includeReviews,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t)\n\t\t\t\t: integration.getMyPullRequestsForReposResult(\n\t\t\t\t\t\trepos,\n\t\t\t\t\t\t{ state: state, filters: filters, cursor: pageCursor },\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t),\n\t\t);\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// An implicit sweep may silently skip a provider that has no session before it yields data.\n\t\t\t// Once a page has been returned, however, losing that session leaves an unread tail and must\n\t\t\t// be attributed even when the provider wasn't explicitly requested.\n\t\t\tconst sessionLostAfterProgress = warning == null && page > 1;\n\t\t\tconst unavailable = warning == null && (attributeUnavailableProvider || sessionLostAfterProgress);\n\t\t\tif (unavailable) {\n\t\t\t\tappendDedupedWarning(warnings, noConnectionWarning(id, domain, connectionId));\n\t\t\t}\n\t\t\t// `warning` set → a hard read failure (incomplete items); otherwise not connected / no session.\n\t\t\tconst failed = fetchFailed || warning != null || unavailable;\n\t\t\t// A cap seen on an earlier page still left results out, so it is still worth saying — but as part\n\t\t\t// of a read that failed, never as an omission. (`failed` is always true here when anything was\n\t\t\t// latched: reaching this exit past page 1 means a later page was lost.)\n\t\t\tappendTruncationWarnings(failed ? 'interrupted' : 'exhausted');\n\t\t\t// An earlier page may already have emitted an omission before this one died; it asserts the read\n\t\t\t// succeeded, which is no longer true.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || sessionLostAfterProgress,\n\t\t\t\t// Only a top-level first-page rejection means the provider itself failed. A later-page or\n\t\t\t\t// per-scope failure still yielded a usable provider slice and stays represented separately.\n\t\t\t\tfailedProvider: page === 1 && (warning != null || unavailable),\n\t\t\t};\n\t\t}\n\n\t\t// Composite account-wide queries can surface the same PR on different relationship/state pages\n\t\t// (for example authored + review-requested, or closed + merged). Keep the first stable position but\n\t\t// replace its value with the latest representation so a later, richer merged state wins. Repository\n\t\t// identity is what keys a row (see `getProviderPullRequestIdentity`), falling back to the canonical\n\t\t// URL; a row with neither stays unkeyed and is retained so unrelated incomplete rows are never\n\t\t// collapsed.\n\t\tfor (const pullRequest of value.values) {\n\t\t\tconst identity = getProviderPullRequestIdentity(pullRequest);\n\t\t\tif (identity == null) {\n\t\t\t\titems.push(pullRequest);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst existingIndex = itemIndexByIdentity.get(identity);\n\t\t\tif (existingIndex == null) {\n\t\t\t\titemIndexByIdentity.set(identity, items.length);\n\t\t\t\titems.push(pullRequest);\n\t\t\t} else {\n\t\t\t\titems[existingIndex] = pullRequest;\n\t\t\t}\n\t\t}\n\n\t\t// Assess this page's SDK metadata: append scope-aware warnings (deduped across pages), and remember\n\t\t// whether a structured failure or incompleteness occurred.\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\tconst pageTruncated =\n\t\t\t(value as { truncated?: boolean }).truncated === true ||\n\t\t\tvalue.paging?.truncated === true ||\n\t\t\tassessment.truncated;\n\t\ttruncated = truncated || pageTruncated;\n\t\t// Whether this drain owes its OWN warning for a capped page — `mergeAssessmentInto` has already appended\n\t\t// one when the fact came from SDK metadata, and repeating it would be noise.\n\t\tunexplainedTruncation = unexplainedTruncation || (pageTruncated && !assessment.truncated);\n\t\t// Only the account-wide read reports a search match count here; the repo-scoped one reports the item\n\t\t// total, which no cap is derived from. Latched even when SDK metadata explains the page: a composite page\n\t\t// reports a failed or cursorless sibling facet there, which says nothing about the cap another reached.\n\t\tif (accountWide && pageTruncated && value.paging?.totalCount != null) {\n\t\t\ttruncatedTotalCount = Math.max(truncatedTotalCount ?? 0, value.paging.totalCount);\n\t\t}\n\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\t// A read that can't confirm completeness (single-page provider reads with no `hasNextPage`)\n\t\t\t// sets `paging.truncated`; propagate it (and any top-level `truncated` and SDK incompleteness)\n\t\t\t// so the sweep doesn't claim an all-pages result.\n\t\t\tappendTruncationWarnings(fetchFailed ? 'interrupted' : 'exhausted');\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\t// Resolve the continuation BEFORE deciding why the drain stops. `page-budget` claims the missing items\n\t\t// are reachable, which is only true with a usable cursor in hand — and a provider can report another\n\t\t// page while handing back none (Bitbucket Server does, when it omits `nextPageStart`). Checking the\n\t\t// budget first would label that unreachable tail as merely unfetched, so raising `maxPages` would\n\t\t// return the identical set.\n\t\tconst nextCursor = value.paging?.cursor;\n\t\t// A cursor already used isn't a continuation either — following it refetches a page we have, and a\n\t\t// provider that cycles (A→B→A) would otherwise burn the whole budget and then be reported as merely\n\t\t// out of budget. Tracked as a SET rather than compared one-back, matching the SDK's own `followCursors`:\n\t\t// `drainToRequestedPage` and `collectProviderPagedResult` compare only the previous cursor, which a\n\t\t// cycle slips past. That is tolerable there and not here, because only this drain reports `page-budget`.\n\t\tconst continuable = nextCursor != null && nextCursor !== '{}' && !seenCursors.has(nextCursor);\n\t\tif (!continuable || page >= maxPages) {\n\t\t\t// A usable cursor in hand means `page-budget`, even when an earlier page was capped: a composite\n\t\t\t// read caps its facets independently, so one facet reaching the provider's ceiling leaves its\n\t\t\t// siblings' cursors live, and raising the budget returns more of them. What no budget returns is the\n\t\t\t// capped part, so that is reported BESIDE the budget stop rather than in place of it — unless an SDK\n\t\t\t// omission already says so. Neither warning then misstates the part the other describes.\n\t\t\tconst budgetStop = !fetchFailed && continuable;\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\ttruncationWarning(\n\t\t\t\t\tid,\n\t\t\t\t\tdomain,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\t'Pull request',\n\t\t\t\t\tfetchFailed ? 'interrupted' : budgetStop ? 'page-budget' : 'exhausted',\n\t\t\t\t),\n\t\t\t);\n\t\t\tconst cap = capWarning();\n\t\t\tif (cap != null) {\n\t\t\t\tappendDedupedWarning(warnings, cap);\n\t\t\t} else if (budgetStop && unexplainedTruncation) {\n\t\t\t\tappendDedupedWarning(warnings, cappedBesideBudgetWarning(id, domain, connectionId, 'Pull request'));\n\t\t\t}\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: true,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\tseenCursors.add(nextCursor);\n\t\tcursor = nextCursor;\n\t}\n}\n\n/**\n * Drains every page of repositories under an org for one git-host integration, threading the opaque\n * next-cursor the provider returns. Stops at `maxPages` (marking `truncated`) or on a hard read failure\n * (marking `fetchFailed`), keeping the pages fetched so far.\n */\nexport async function drainRepositories(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\torg: string,\n\tproject: string | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n): Promise<{\n\trepos: ProviderRepository[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n}> {\n\tconst repos: ProviderRepository[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\n\tfor (;;) {\n\t\tpage++;\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(\n\t\t\tid,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t() =>\n\t\t\t\tintegration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\tproject: project,\n\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\tconnectionId: connectionId,\n\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: true },\n\t\t);\n\t\tif (warning != null) {\n\t\t\t// Deduped, like every other accumulation here: a soft warning (`{ value, error }`) repeats verbatim on\n\t\t\t// each page that hits the same condition, and the array's contract is that no two entries are equal.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\tconst interruptedAfterProgress = page > 1;\n\t\t\tif (interruptedAfterProgress && warning == null) {\n\t\t\t\t// Not a backstop: the read was cut short mid-drain, which is why it also sets `fetchFailed`\n\t\t\t\t// below. A retry may complete it, so this must not claim the succeeded-but-capped omission.\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(id, domain, connectionId, 'Repository', 'interrupted'),\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst failed = fetchFailed || warning != null || interruptedAfterProgress;\n\t\t\t// An SDK omission from an earlier page asserts the read succeeded; this one didn't.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || interruptedAfterProgress,\n\t\t\t};\n\t\t}\n\n\t\trepos.push(...value.values);\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\ttruncated = truncated || value.truncated === true || value.paging?.truncated === true || assessment.truncated;\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t};\n\t\t}\n\n\t\tif (page >= maxPages) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tconst nextCursor = value.paging?.cursor;\n\t\tif (nextCursor == null || nextCursor === '{}') {\n\t\t\t// Provider says there is more but didn't return a usable cursor; stop rather than refetch the same page.\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tcursor = nextCursor;\n\t}\n}\n\nexport function resolvePullRequestSweepTargets(options: PullRequestSweepOptions | undefined): {\n\ttargets: readonly ProviderSweepTarget[];\n\tattributeUnavailableProviders: boolean;\n} {\n\tif (options?.targets != null) {\n\t\tif (options.providerIds != null || options.connectionId != null) {\n\t\t\tthrow new TypeError(\"Pull request sweep 'targets' cannot be combined with 'providerIds' or 'connectionId'\");\n\t\t}\n\n\t\tconst seenProviderIds = new Set<IntegrationIds>();\n\t\tfor (const target of options.targets) {\n\t\t\tif (seenProviderIds.has(target.providerId)) {\n\t\t\t\tthrow new TypeError(\n\t\t\t\t\t`Pull request sweep targets must contain at most one target per provider; duplicate '${target.providerId}'`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tseenProviderIds.add(target.providerId);\n\t\t}\n\n\t\treturn { targets: options.targets, attributeUnavailableProviders: true };\n\t}\n\n\tconst providerIds =\n\t\toptions?.providerIds ?? supportedOrderedCloudIntegrationIds.filter(id => !isIssuesHostIntegrationId(id));\n\tconst connectionId = providerIds.length === 1 ? options?.connectionId : undefined;\n\treturn {\n\t\ttargets: providerIds.map(providerId => ({ providerId: providerId, connectionId: connectionId })),\n\t\tattributeUnavailableProviders: options?.providerIds != null,\n\t};\n}\n\nexport async function getCurrentAccountIdentity(\n\tintegration: GitHostIntegration,\n\tconnectionId: string | undefined,\n): Promise<{ id: string; username?: string } | undefined> {\n\ttry {\n\t\tconst account = await integration.getCurrentAccount({ connectionId: connectionId });\n\t\treturn account != null ? { id: account.id, username: account.username } : undefined;\n\t} catch {\n\t\t// Authorship is optional enrichment; don't turn a successful PR read into a failure if identity lookup fails.\n\t\treturn undefined;\n\t}\n}\n\n/**\n * The current account to map each raw row against, in order (see {@link GitHostIntegration.getPullRequestViewers}).\n * Rows with no author can't be matched, so a read of only those resolves nothing; a failed lookup leaves every row's\n * authorship unknown rather than failing the read.\n */\nexport async function getPullRequestViewers(\n\tintegration: GitHostIntegration,\n\tconnectionId: string | undefined,\n\tpullRequests: readonly (ProviderPullRequest | PullRequest)[],\n): Promise<({ id: string; username?: string } | undefined)[]> {\n\tif (!pullRequests.some(pr => pr.author != null)) return pullRequests.map(() => undefined);\n\n\ttry {\n\t\treturn await integration.getPullRequestViewers(pullRequests, connectionId);\n\t} catch {\n\t\treturn pullRequests.map(() => undefined);\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { IssueShape } from "../../../git/models/issue.js";
|
|
2
|
+
import { PullRequestShape } from "../../../git/models/pullRequest.js";
|
|
3
|
+
import { IssueEtagFields, IssueEtagInclude, PullRequestEtagFields, PullRequestEtagInclude } from "../models/integration.js";
|
|
4
|
+
//#region ../plus/integrations/src/reads/etag.d.ts
|
|
5
|
+
export declare function pullRequestEtagFieldsFromShape(pr: PullRequestShape): PullRequestEtagFields;
|
|
6
|
+
export declare function issueEtagFieldsFromShape(issue: IssueShape): IssueEtagFields;
|
|
7
|
+
/**
|
|
8
|
+
* The distinct `includes` in canonical order, whatever order and repetition the caller gave. Values it doesn't know
|
|
9
|
+
* are dropped, so a caller validates them first (see {@link findInvalidPullRequestEtagInclude}).
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizePullRequestEtagIncludes(includes: readonly PullRequestEtagInclude[]): readonly PullRequestEtagInclude[];
|
|
12
|
+
/** The first of `includes` that isn't a {@link PullRequestEtagInclude}, if any. */
|
|
13
|
+
export declare function findInvalidPullRequestEtagInclude(includes: readonly string[]): string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Each of `includes` adds one input — mergeability, review decision or check rollup — which a host may change
|
|
16
|
+
* without moving `updatedDate`. An input that isn't listed is left out, so its churn never forces a full read.
|
|
17
|
+
*/
|
|
18
|
+
export declare function pullRequestEtag(fields: PullRequestEtagFields, includes: readonly PullRequestEtagInclude[]): string;
|
|
19
|
+
/** The issue twin of {@link normalizePullRequestEtagIncludes}; validate with {@link findInvalidIssueEtagInclude}. */
|
|
20
|
+
export declare function normalizeIssueEtagIncludes(includes: readonly IssueEtagInclude[]): readonly IssueEtagInclude[];
|
|
21
|
+
/** The first of `includes` that isn't an {@link IssueEtagInclude}, if any. */
|
|
22
|
+
export declare function findInvalidIssueEtagInclude(includes: readonly string[]): string | undefined;
|
|
23
|
+
/** `'reactions'` adds the thumbs-up count, which a host changes without moving `updatedDate`. */
|
|
24
|
+
export declare function issueEtag(fields: IssueEtagFields, includes: readonly IssueEtagInclude[]): string;
|
|
25
|
+
//#endregion
|
|
26
|
+
//# sourceMappingURL=etag.d.ts.map
|