@gitkraken/core-gitlens 0.9.3 → 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 +34 -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 +13 -4
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +62 -7
- 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/issueReads.d.ts +14 -0
- 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 +45 -8
- package/dist/plus/integrations/providers/bitbucket/models.js +32 -9
- 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/jira-server.d.ts +3 -2
- package/dist/plus/integrations/providers/jira-server.js +18 -9
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +26 -2
- package/dist/plus/integrations/providers/jira.js +120 -9
- 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 +105 -7
- 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 +70 -8
- package/dist/plus/integrations/providers/models.js +59 -21
- 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 +70 -4
- package/dist/plus/integrations/providers/providersApi.js +148 -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 +11 -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/cursors.d.ts +8 -0
- package/dist/plus/integrations/reads/cursors.js +5 -3
- package/dist/plus/integrations/reads/cursors.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +13 -4
- 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/filters.d.ts +8 -0
- package/dist/plus/integrations/reads/filters.js +2 -1
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- 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 +10 -2
- package/dist/plus/integrations/reads/issueTracker.js +23 -6
- 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 +389 -23
- package/docs/kepler-read-api-parity.md +16 -2
- package/package.json +2 -2
|
@@ -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":";;;;;;;;;;;;;;;;;;AAyCA,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;CAQxG,KAAK,MAAM,UAAU,eAAe,UAAU,KAAK,KAAK,CAAC,4BAA4B,UAAU,GAC9F,OAAO,aACN,YACA,QACA,cACA,6BAA6B,MAAM,IAAI,wKACxC;AAIF"}
|
|
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":"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"}
|
|
@@ -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,3 +1,4 @@
|
|
|
1
|
+
import { IssueStateFilter } from "../../../git/models/issue.js";
|
|
1
2
|
import { IntegrationIds } from "../constants.js";
|
|
2
3
|
//#region ../plus/integrations/src/reads/cursors.d.ts
|
|
3
4
|
/**
|
|
@@ -21,6 +22,12 @@ export interface IssueTrackerPageCursor {
|
|
|
21
22
|
retryPages?: number[];
|
|
22
23
|
retryProjects?: string[];
|
|
23
24
|
completedProjects?: string[];
|
|
25
|
+
/**
|
|
26
|
+
* The `state` the read that minted this cursor asked for, omitted for open. `completedProjects` records what was
|
|
27
|
+
* already drained UNDER that state, so resuming it under another would skip projects whose other-state issues
|
|
28
|
+
* were never read.
|
|
29
|
+
*/
|
|
30
|
+
state?: 'closed' | 'all';
|
|
24
31
|
}
|
|
25
32
|
export declare function parseIssueTrackerPageCursor(cursor: string | undefined): IssueTrackerPageCursor | undefined;
|
|
26
33
|
export declare function toIssueTrackerPageCursor(options: {
|
|
@@ -30,6 +37,7 @@ export declare function toIssueTrackerPageCursor(options: {
|
|
|
30
37
|
retryPages: readonly number[];
|
|
31
38
|
retryProjects: readonly string[];
|
|
32
39
|
completedProjects?: readonly string[];
|
|
40
|
+
state?: IssueStateFilter;
|
|
33
41
|
}): string | undefined;
|
|
34
42
|
/** Identifies one org slice of the fan-out. Connection and domain are part of the key: two accounts, or two
|
|
35
43
|
* self-managed hosts, can each have an org of the same name. */
|
|
@@ -29,7 +29,8 @@ function parseIssueTrackerPageCursor(cursor) {
|
|
|
29
29
|
...typeof parsed.nextPage === "number" && Number.isSafeInteger(parsed.nextPage) && parsed.nextPage > 0 ? { nextPage: parsed.nextPage } : {},
|
|
30
30
|
...retryPages != null ? { retryPages } : {},
|
|
31
31
|
...retryProjects != null ? { retryProjects } : {},
|
|
32
|
-
...completedProjects != null ? { completedProjects } : {}
|
|
32
|
+
...completedProjects != null ? { completedProjects } : {},
|
|
33
|
+
...parsed.state === "closed" || parsed.state === "all" ? { state: parsed.state } : {}
|
|
33
34
|
};
|
|
34
35
|
} catch {
|
|
35
36
|
return;
|
|
@@ -40,7 +41,7 @@ function toIssueTrackerPageCursor(options) {
|
|
|
40
41
|
const retryProjects = [...new Set(options.retryProjects)].sort();
|
|
41
42
|
const completedProjects = retryPages.length > 0 || retryProjects.length > 0 || options.nextPage != null ? [...new Set(options.completedProjects ?? [])].sort() : [];
|
|
42
43
|
if (retryPages.length === 0 && retryProjects.length === 0 && options.nextPage == null) return;
|
|
43
|
-
if (retryPages.length === 0 && retryProjects.length === 0 && completedProjects.length === 0 && options.nextPage != null && options.unpaged !== true) return toPageCursor(options.nextPage);
|
|
44
|
+
if (retryPages.length === 0 && retryProjects.length === 0 && completedProjects.length === 0 && options.nextPage != null && options.unpaged !== true && (options.state == null || options.state === "open")) return toPageCursor(options.nextPage);
|
|
44
45
|
return JSON.stringify({
|
|
45
46
|
type: "issue-tracker-page",
|
|
46
47
|
currentPage: options.currentPage,
|
|
@@ -48,7 +49,8 @@ function toIssueTrackerPageCursor(options) {
|
|
|
48
49
|
...options.nextPage != null ? { nextPage: options.nextPage } : {},
|
|
49
50
|
...retryPages.length > 0 ? { retryPages } : {},
|
|
50
51
|
...retryProjects.length > 0 ? { retryProjects } : {},
|
|
51
|
-
...completedProjects.length > 0 ? { completedProjects } : {}
|
|
52
|
+
...completedProjects.length > 0 ? { completedProjects } : {},
|
|
53
|
+
...options.state === "closed" || options.state === "all" ? { state: options.state } : {}
|
|
52
54
|
});
|
|
53
55
|
}
|
|
54
56
|
/** Whether a bundle entry addresses the same org slice as `org`, comparing domains by host. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cursors.js","names":[],"sources":["../../../../../plus/integrations/src/reads/cursors.ts"],"sourcesContent":["import type { IntegrationIds } from '../constants.js';\nimport { toPageCursor } from '../providers/utils/providerPaging.js';\nimport { hostFromDomain } from '../utils/domain.utils.js';\nimport { pageToCursor } from './paging.js';\n\n/**\n * The COMPOSITE cursors, for the two reads a single provider cursor can't address.\n *\n * A plain paged read threads the provider's own opaque continuation. These two fan out instead — the\n * issue-tracker read over a window of projects, the broaden read over a set of orgs — so one round produces\n * several provider positions plus the bookkeeping needed to resume: which scopes failed and should be retried,\n * and which are already drained and must NOT be re-read (a cursor-only provider handed a fresh page-1 request\n * answers with its first page again, duplicating items).\n *\n * Both encodings are opaque to consumers and are parsed defensively: a cursor is caller-supplied data that may\n * be stale, truncated, or from another read entirely, so anything unrecognized degrades to \"no cursor\" (which\n * falls back to page paging) rather than throwing or being forwarded to a provider as a foreign string.\n */\n\n// ---------------------------------------------------------------------------------------------------------\n// Issue-tracker page cursor (Jira/Linear/Trello: resource → project, paginated by a window of PROJECTS)\n// ---------------------------------------------------------------------------------------------------------\n\nexport interface IssueTrackerPageCursor {\n\ttype: 'issue-tracker-page';\n\tcurrentPage: number;\n\tunpaged?: boolean;\n\tnextPage?: number;\n\tretryPages?: number[];\n\tretryProjects?: string[];\n\tcompletedProjects?: string[];\n}\n\n/** Positive safe integers only, deduped; `undefined` when nothing survives (so the field is omitted). */\nfunction positiveIntegers(values: unknown): number[] | undefined {\n\tif (!Array.isArray(values)) return undefined;\n\n\tconst valid = values.filter(\n\t\t(value): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value > 0,\n\t);\n\treturn valid.length > 0 ? [...new Set(valid)] : undefined;\n}\n\n/** Non-empty strings only, deduped; `undefined` when nothing survives (so the field is omitted). */\nfunction nonEmptyStrings(values: unknown): string[] | undefined {\n\tif (!Array.isArray(values)) return undefined;\n\n\tconst valid = values.filter((value): value is string => typeof value === 'string' && value.length > 0);\n\treturn valid.length > 0 ? [...new Set(valid)] : undefined;\n}\n\nexport function parseIssueTrackerPageCursor(cursor: string | undefined): IssueTrackerPageCursor | undefined {\n\tif (cursor == null) return undefined;\n\n\ttry {\n\t\tconst parsed = JSON.parse(cursor) as Partial<IssueTrackerPageCursor>;\n\t\tif (\n\t\t\tparsed.type !== 'issue-tracker-page' ||\n\t\t\ttypeof parsed.currentPage !== 'number' ||\n\t\t\t!Number.isSafeInteger(parsed.currentPage) ||\n\t\t\tparsed.currentPage < 1\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst retryPages = positiveIntegers(parsed.retryPages);\n\t\tconst retryProjects = nonEmptyStrings(parsed.retryProjects);\n\t\tconst completedProjects = nonEmptyStrings(parsed.completedProjects);\n\t\treturn {\n\t\t\ttype: 'issue-tracker-page',\n\t\t\tcurrentPage: parsed.currentPage,\n\t\t\t...(parsed.unpaged === true ? { unpaged: true } : {}),\n\t\t\t...(typeof parsed.nextPage === 'number' && Number.isSafeInteger(parsed.nextPage) && parsed.nextPage > 0\n\t\t\t\t? { nextPage: parsed.nextPage }\n\t\t\t\t: {}),\n\t\t\t...(retryPages != null ? { retryPages: retryPages } : {}),\n\t\t\t...(retryProjects != null ? { retryProjects: retryProjects } : {}),\n\t\t\t...(completedProjects != null ? { completedProjects: completedProjects } : {}),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function toIssueTrackerPageCursor(options: {\n\tcurrentPage: number;\n\tunpaged?: boolean;\n\tnextPage?: number;\n\tretryPages: readonly number[];\n\tretryProjects: readonly string[];\n\tcompletedProjects?: readonly string[];\n}): string | undefined {\n\tconst retryPages = [...new Set(options.retryPages)].sort((a, b) => a - b);\n\tconst retryProjects = [...new Set(options.retryProjects)].sort();\n\tconst completedProjects =\n\t\tretryPages.length > 0 || retryProjects.length > 0 || options.nextPage != null\n\t\t\t? [...new Set(options.completedProjects ?? [])].sort()\n\t\t\t: [];\n\tif (retryPages.length === 0 && retryProjects.length === 0 && options.nextPage == null) {\n\t\treturn undefined;\n\t}\n\t// Nothing to carry beyond the next window: emit the plain page cursor instead of a composite one, so an\n\t// ordinary forward read round-trips through the same encoding every other paged read uses.\n\tif (\n\t\tretryPages.length === 0 &&\n\t\tretryProjects.length === 0 &&\n\t\tcompletedProjects.length === 0 &&\n\t\toptions.nextPage != null &&\n\t\toptions.unpaged !== true\n\t) {\n\t\treturn toPageCursor(options.nextPage);\n\t}\n\treturn JSON.stringify({\n\t\ttype: 'issue-tracker-page',\n\t\tcurrentPage: options.currentPage,\n\t\t...(options.unpaged === true ? { unpaged: true } : {}),\n\t\t...(options.nextPage != null ? { nextPage: options.nextPage } : {}),\n\t\t...(retryPages.length > 0 ? { retryPages: retryPages } : {}),\n\t\t...(retryProjects.length > 0 ? { retryProjects: retryProjects } : {}),\n\t\t...(completedProjects.length > 0 ? { completedProjects: completedProjects } : {}),\n\t} satisfies IssueTrackerPageCursor);\n}\n\n// ---------------------------------------------------------------------------------------------------------\n// Broaden-issues cursor bundle (one provider position per org in the fan-out)\n// ---------------------------------------------------------------------------------------------------------\n\n/** Identifies one org slice of the fan-out. Connection and domain are part of the key: two accounts, or two\n * self-managed hosts, can each have an org of the same name. */\nexport interface BroadenIssuesOrgKey {\n\tproviderId: IntegrationIds;\n\tname: string;\n\tconnectionId?: string;\n\tdomain?: string;\n}\n\n/** One org's position in the bundle: either the provider's own cursor, or a page to retry after a failure. */\nexport interface BroadenIssuesOrgCursor {\n\tproviderId: IntegrationIds;\n\torg: string;\n\tconnectionId?: string;\n\tdomain?: string;\n\tcursor?: string;\n\tretryPage?: number;\n}\n\nexport type BroadenIssuesExhaustedOrg = Omit<BroadenIssuesOrgCursor, 'cursor' | 'retryPage'>;\n\n/** Whether a bundle entry addresses the same org slice as `org`, comparing domains by host. */\nfunction matchesOrg(\n\tentry: { providerId?: IntegrationIds; org?: string; connectionId?: string; domain?: string },\n\torg: BroadenIssuesOrgKey,\n): boolean {\n\treturn (\n\t\tentry.providerId === org.providerId &&\n\t\tentry.org === org.name &&\n\t\tentry.connectionId === org.connectionId &&\n\t\t(hostFromDomain(entry.domain) ?? entry.domain) === (hostFromDomain(org.domain) ?? org.domain)\n\t);\n}\n\n/**\n * The cursor to send for one org in this round: its entry in the bundle, or a synthesized page cursor.\n *\n * A single-org fan-out has no bundle to key into — its cursor IS the provider's, threaded straight through.\n */\nexport function getBroadenIssuesCursor(\n\tcursor: string | undefined,\n\torg: BroadenIssuesOrgKey,\n\tpage: number,\n\torgCount: number,\n): string | undefined {\n\tif (orgCount === 1) return cursor ?? pageToCursor(page);\n\n\tif (cursor != null) {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(cursor) as { cursors?: BroadenIssuesOrgCursor[] };\n\t\t\tconst match = parsed.cursors?.find(c => matchesOrg(c, org));\n\t\t\tif (match?.cursor != null) return match.cursor;\n\t\t\tif (typeof match?.retryPage === 'number' && Number.isSafeInteger(match.retryPage) && match.retryPage > 0) {\n\t\t\t\treturn toPageCursor(match.retryPage);\n\t\t\t}\n\t\t} catch {}\n\t}\n\n\treturn pageToCursor(page);\n}\n\n/**\n * Whether a prior round already drained this org (multi-org fan-out only). Once an org runs out of pages while\n * another org keeps paging, the bundle records it as exhausted so the next round skips it instead of re-issuing\n * a page-1 read — which cursor-only providers (having no page-number cursor to honor) would answer with their\n * first page again, duplicating results.\n */\nexport function isBroadenIssuesOrgExhausted(\n\tcursor: string | undefined,\n\torg: BroadenIssuesOrgKey,\n\torgCount: number,\n): boolean {\n\tif (orgCount === 1 || cursor == null) return false;\n\n\ttry {\n\t\tconst parsed = JSON.parse(cursor) as { exhausted?: BroadenIssuesExhaustedOrg[] };\n\t\treturn parsed.exhausted?.some(e => matchesOrg(e, org)) ?? false;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Encodes the round's per-org positions. A single-org fan-out emits that org's cursor directly, unwrapped. */\nexport function toBroadenIssuesCursor(\n\tcursors: BroadenIssuesOrgCursor[],\n\texhausted: BroadenIssuesExhaustedOrg[],\n\torgCount: number,\n): string | undefined {\n\tif (cursors.length === 0) return undefined;\n\tif (orgCount === 1) {\n\t\treturn cursors[0].cursor ?? (cursors[0].retryPage != null ? toPageCursor(cursors[0].retryPage) : undefined);\n\t}\n\n\t// Carry the exhausted orgs alongside the still-active cursors so the next round can skip them (see\n\t// isBroadenIssuesOrgExhausted). Only meaningful while at least one org still has more to read.\n\treturn JSON.stringify({ cursors: cursors, exhausted: exhausted });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cursors.js","names":[],"sources":["../../../../../plus/integrations/src/reads/cursors.ts"],"sourcesContent":["import type { IssueStateFilter } from '@gitlens/git/models/issue.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { toPageCursor } from '../providers/utils/providerPaging.js';\nimport { hostFromDomain } from '../utils/domain.utils.js';\nimport { pageToCursor } from './paging.js';\n\n/**\n * The COMPOSITE cursors, for the two reads a single provider cursor can't address.\n *\n * A plain paged read threads the provider's own opaque continuation. These two fan out instead — the\n * issue-tracker read over a window of projects, the broaden read over a set of orgs — so one round produces\n * several provider positions plus the bookkeeping needed to resume: which scopes failed and should be retried,\n * and which are already drained and must NOT be re-read (a cursor-only provider handed a fresh page-1 request\n * answers with its first page again, duplicating items).\n *\n * Both encodings are opaque to consumers and are parsed defensively: a cursor is caller-supplied data that may\n * be stale, truncated, or from another read entirely, so anything unrecognized degrades to \"no cursor\" (which\n * falls back to page paging) rather than throwing or being forwarded to a provider as a foreign string.\n */\n\n// ---------------------------------------------------------------------------------------------------------\n// Issue-tracker page cursor (Jira/Linear/Trello: resource → project, paginated by a window of PROJECTS)\n// ---------------------------------------------------------------------------------------------------------\n\nexport interface IssueTrackerPageCursor {\n\ttype: 'issue-tracker-page';\n\tcurrentPage: number;\n\tunpaged?: boolean;\n\tnextPage?: number;\n\tretryPages?: number[];\n\tretryProjects?: string[];\n\tcompletedProjects?: string[];\n\t/**\n\t * The `state` the read that minted this cursor asked for, omitted for open. `completedProjects` records what was\n\t * already drained UNDER that state, so resuming it under another would skip projects whose other-state issues\n\t * were never read.\n\t */\n\tstate?: 'closed' | 'all';\n}\n\n/** Positive safe integers only, deduped; `undefined` when nothing survives (so the field is omitted). */\nfunction positiveIntegers(values: unknown): number[] | undefined {\n\tif (!Array.isArray(values)) return undefined;\n\n\tconst valid = values.filter(\n\t\t(value): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value > 0,\n\t);\n\treturn valid.length > 0 ? [...new Set(valid)] : undefined;\n}\n\n/** Non-empty strings only, deduped; `undefined` when nothing survives (so the field is omitted). */\nfunction nonEmptyStrings(values: unknown): string[] | undefined {\n\tif (!Array.isArray(values)) return undefined;\n\n\tconst valid = values.filter((value): value is string => typeof value === 'string' && value.length > 0);\n\treturn valid.length > 0 ? [...new Set(valid)] : undefined;\n}\n\nexport function parseIssueTrackerPageCursor(cursor: string | undefined): IssueTrackerPageCursor | undefined {\n\tif (cursor == null) return undefined;\n\n\ttry {\n\t\tconst parsed = JSON.parse(cursor) as Partial<IssueTrackerPageCursor>;\n\t\tif (\n\t\t\tparsed.type !== 'issue-tracker-page' ||\n\t\t\ttypeof parsed.currentPage !== 'number' ||\n\t\t\t!Number.isSafeInteger(parsed.currentPage) ||\n\t\t\tparsed.currentPage < 1\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst retryPages = positiveIntegers(parsed.retryPages);\n\t\tconst retryProjects = nonEmptyStrings(parsed.retryProjects);\n\t\tconst completedProjects = nonEmptyStrings(parsed.completedProjects);\n\t\treturn {\n\t\t\ttype: 'issue-tracker-page',\n\t\t\tcurrentPage: parsed.currentPage,\n\t\t\t...(parsed.unpaged === true ? { unpaged: true } : {}),\n\t\t\t...(typeof parsed.nextPage === 'number' && Number.isSafeInteger(parsed.nextPage) && parsed.nextPage > 0\n\t\t\t\t? { nextPage: parsed.nextPage }\n\t\t\t\t: {}),\n\t\t\t...(retryPages != null ? { retryPages: retryPages } : {}),\n\t\t\t...(retryProjects != null ? { retryProjects: retryProjects } : {}),\n\t\t\t...(completedProjects != null ? { completedProjects: completedProjects } : {}),\n\t\t\t...(parsed.state === 'closed' || parsed.state === 'all' ? { state: parsed.state } : {}),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function toIssueTrackerPageCursor(options: {\n\tcurrentPage: number;\n\tunpaged?: boolean;\n\tnextPage?: number;\n\tretryPages: readonly number[];\n\tretryProjects: readonly string[];\n\tcompletedProjects?: readonly string[];\n\tstate?: IssueStateFilter;\n}): string | undefined {\n\tconst retryPages = [...new Set(options.retryPages)].sort((a, b) => a - b);\n\tconst retryProjects = [...new Set(options.retryProjects)].sort();\n\tconst completedProjects =\n\t\tretryPages.length > 0 || retryProjects.length > 0 || options.nextPage != null\n\t\t\t? [...new Set(options.completedProjects ?? [])].sort()\n\t\t\t: [];\n\tif (retryPages.length === 0 && retryProjects.length === 0 && options.nextPage == null) {\n\t\treturn undefined;\n\t}\n\t// Nothing to carry beyond the next window: emit the plain page cursor instead of a composite one, so an\n\t// ordinary forward read round-trips through the same encoding every other paged read uses.\n\tif (\n\t\tretryPages.length === 0 &&\n\t\tretryProjects.length === 0 &&\n\t\tcompletedProjects.length === 0 &&\n\t\toptions.nextPage != null &&\n\t\toptions.unpaged !== true &&\n\t\t// A page cursor carries no state, so a narrowed read keeps the composite one: resumed under another state, a\n\t\t// plain cursor would start at its window and skip the earlier projects' issues of that state.\n\t\t(options.state == null || options.state === 'open')\n\t) {\n\t\treturn toPageCursor(options.nextPage);\n\t}\n\treturn JSON.stringify({\n\t\ttype: 'issue-tracker-page',\n\t\tcurrentPage: options.currentPage,\n\t\t...(options.unpaged === true ? { unpaged: true } : {}),\n\t\t...(options.nextPage != null ? { nextPage: options.nextPage } : {}),\n\t\t...(retryPages.length > 0 ? { retryPages: retryPages } : {}),\n\t\t...(retryProjects.length > 0 ? { retryProjects: retryProjects } : {}),\n\t\t...(completedProjects.length > 0 ? { completedProjects: completedProjects } : {}),\n\t\t...(options.state === 'closed' || options.state === 'all' ? { state: options.state } : {}),\n\t} satisfies IssueTrackerPageCursor);\n}\n\n// ---------------------------------------------------------------------------------------------------------\n// Broaden-issues cursor bundle (one provider position per org in the fan-out)\n// ---------------------------------------------------------------------------------------------------------\n\n/** Identifies one org slice of the fan-out. Connection and domain are part of the key: two accounts, or two\n * self-managed hosts, can each have an org of the same name. */\nexport interface BroadenIssuesOrgKey {\n\tproviderId: IntegrationIds;\n\tname: string;\n\tconnectionId?: string;\n\tdomain?: string;\n}\n\n/** One org's position in the bundle: either the provider's own cursor, or a page to retry after a failure. */\nexport interface BroadenIssuesOrgCursor {\n\tproviderId: IntegrationIds;\n\torg: string;\n\tconnectionId?: string;\n\tdomain?: string;\n\tcursor?: string;\n\tretryPage?: number;\n}\n\nexport type BroadenIssuesExhaustedOrg = Omit<BroadenIssuesOrgCursor, 'cursor' | 'retryPage'>;\n\n/** Whether a bundle entry addresses the same org slice as `org`, comparing domains by host. */\nfunction matchesOrg(\n\tentry: { providerId?: IntegrationIds; org?: string; connectionId?: string; domain?: string },\n\torg: BroadenIssuesOrgKey,\n): boolean {\n\treturn (\n\t\tentry.providerId === org.providerId &&\n\t\tentry.org === org.name &&\n\t\tentry.connectionId === org.connectionId &&\n\t\t(hostFromDomain(entry.domain) ?? entry.domain) === (hostFromDomain(org.domain) ?? org.domain)\n\t);\n}\n\n/**\n * The cursor to send for one org in this round: its entry in the bundle, or a synthesized page cursor.\n *\n * A single-org fan-out has no bundle to key into — its cursor IS the provider's, threaded straight through.\n */\nexport function getBroadenIssuesCursor(\n\tcursor: string | undefined,\n\torg: BroadenIssuesOrgKey,\n\tpage: number,\n\torgCount: number,\n): string | undefined {\n\tif (orgCount === 1) return cursor ?? pageToCursor(page);\n\n\tif (cursor != null) {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(cursor) as { cursors?: BroadenIssuesOrgCursor[] };\n\t\t\tconst match = parsed.cursors?.find(c => matchesOrg(c, org));\n\t\t\tif (match?.cursor != null) return match.cursor;\n\t\t\tif (typeof match?.retryPage === 'number' && Number.isSafeInteger(match.retryPage) && match.retryPage > 0) {\n\t\t\t\treturn toPageCursor(match.retryPage);\n\t\t\t}\n\t\t} catch {}\n\t}\n\n\treturn pageToCursor(page);\n}\n\n/**\n * Whether a prior round already drained this org (multi-org fan-out only). Once an org runs out of pages while\n * another org keeps paging, the bundle records it as exhausted so the next round skips it instead of re-issuing\n * a page-1 read — which cursor-only providers (having no page-number cursor to honor) would answer with their\n * first page again, duplicating results.\n */\nexport function isBroadenIssuesOrgExhausted(\n\tcursor: string | undefined,\n\torg: BroadenIssuesOrgKey,\n\torgCount: number,\n): boolean {\n\tif (orgCount === 1 || cursor == null) return false;\n\n\ttry {\n\t\tconst parsed = JSON.parse(cursor) as { exhausted?: BroadenIssuesExhaustedOrg[] };\n\t\treturn parsed.exhausted?.some(e => matchesOrg(e, org)) ?? false;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Encodes the round's per-org positions. A single-org fan-out emits that org's cursor directly, unwrapped. */\nexport function toBroadenIssuesCursor(\n\tcursors: BroadenIssuesOrgCursor[],\n\texhausted: BroadenIssuesExhaustedOrg[],\n\torgCount: number,\n): string | undefined {\n\tif (cursors.length === 0) return undefined;\n\tif (orgCount === 1) {\n\t\treturn cursors[0].cursor ?? (cursors[0].retryPage != null ? toPageCursor(cursors[0].retryPage) : undefined);\n\t}\n\n\t// Carry the exhausted orgs alongside the still-active cursors so the next round can skip them (see\n\t// isBroadenIssuesOrgExhausted). Only meaningful while at least one org still has more to read.\n\treturn JSON.stringify({ cursors: cursors, exhausted: exhausted });\n}\n"],"mappings":"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"}
|
|
@@ -1,10 +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
|
-
import "../providerFilters.js";
|
|
4
3
|
import { ProviderWarning } from "../results.js";
|
|
4
|
+
import "../providerFilters.js";
|
|
5
|
+
import { ProviderPullRequest, ProviderReposInput, ProviderRepository } from "../providers/models.js";
|
|
5
6
|
import { GitHostIntegration } from "../models/gitHostIntegration.js";
|
|
6
7
|
import { IntegrationResult } from "../models/integration.js";
|
|
7
|
-
import { ProviderPullRequest, ProviderReposInput, ProviderRepository } from "../providers/models.js";
|
|
8
8
|
import { ProviderSweepTarget, PullRequestSweepOptions } from "../manager.js";
|
|
9
9
|
//#region ../plus/integrations/src/reads/drains.d.ts
|
|
10
10
|
/**
|
|
@@ -42,7 +42,7 @@ export declare function drainPullRequests(integration: GitHostIntegration, id: I
|
|
|
42
42
|
* options rather than restated so a third opt-in is declared once.
|
|
43
43
|
*/
|
|
44
44
|
accountWideOptions: Pick<PullRequestSweepOptions, 'includeReviewRequested' | 'includeReviews'> | undefined, connectionId: string | undefined, maxPages: number, attributeUnavailableProvider: boolean): Promise<{
|
|
45
|
-
items: ProviderPullRequest[];
|
|
45
|
+
items: (ProviderPullRequest | PullRequest)[];
|
|
46
46
|
warnings: ProviderWarning[];
|
|
47
47
|
fetchFailed: boolean;
|
|
48
48
|
truncated: boolean;
|
|
@@ -67,5 +67,14 @@ export declare function getCurrentAccountIdentity(integration: GitHostIntegratio
|
|
|
67
67
|
id: string;
|
|
68
68
|
username?: string;
|
|
69
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)[]>;
|
|
70
79
|
//#endregion
|
|
71
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
|