@gitkraken/core-gitlens 0.24.0 → 0.26.0

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.
Files changed (176) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +23 -15
  3. package/dist/git/errors.d.ts +6 -5
  4. package/dist/git/errors.js +7 -6
  5. package/dist/git/errors.js.map +1 -1
  6. package/dist/git/features.d.ts +1 -1
  7. package/dist/git/features.js +1 -1
  8. package/dist/git/features.js.map +1 -1
  9. package/dist/git/models/remoteProvider.d.ts +4 -1
  10. package/dist/git/models/remoteProvider.js.map +1 -1
  11. package/dist/git/providers/operations.d.ts +15 -0
  12. package/dist/git/providers/provider.d.ts +4 -2
  13. package/dist/git/utils/branch.utils.d.ts +4 -1
  14. package/dist/git/utils/branch.utils.js +5 -2
  15. package/dist/git/utils/branch.utils.js.map +1 -1
  16. package/dist/git/utils/mergeConflicts.utils.js +2 -2
  17. package/dist/git/utils/mergeConflicts.utils.js.map +1 -1
  18. package/dist/git/watching/worktreeSummary.d.ts +15 -3
  19. package/dist/git/watching/worktreeSummary.js +179 -31
  20. package/dist/git/watching/worktreeSummary.js.map +1 -1
  21. package/dist/git-cli/cliGitProvider.d.ts +30 -3
  22. package/dist/git-cli/cliGitProvider.js +102 -40
  23. package/dist/git-cli/cliGitProvider.js.map +1 -1
  24. package/dist/git-cli/exec/git.d.ts +9 -1
  25. package/dist/git-cli/exec/git.js +23 -4
  26. package/dist/git-cli/exec/git.js.map +1 -1
  27. package/dist/git-cli/exec/gitDirResolver.d.ts +26 -0
  28. package/dist/git-cli/exec/gitDirResolver.js +181 -0
  29. package/dist/git-cli/exec/gitDirResolver.js.map +1 -0
  30. package/dist/git-cli/exec/headState.js +3 -3
  31. package/dist/git-cli/exec/headState.js.map +1 -1
  32. package/dist/git-cli/exec/indexRefresh.d.ts +78 -0
  33. package/dist/git-cli/exec/indexRefresh.js +159 -0
  34. package/dist/git-cli/exec/indexRefresh.js.map +1 -0
  35. package/dist/git-cli/providers/branchRemote.js +2 -5
  36. package/dist/git-cli/providers/branchRemote.js.map +1 -1
  37. package/dist/git-cli/providers/branches.js +4 -4
  38. package/dist/git-cli/providers/branches.js.map +1 -1
  39. package/dist/git-cli/providers/config.d.ts +2 -0
  40. package/dist/git-cli/providers/config.js +26 -4
  41. package/dist/git-cli/providers/config.js.map +1 -1
  42. package/dist/git-cli/providers/maintenance.js +1 -1
  43. package/dist/git-cli/providers/operations.d.ts +4 -1
  44. package/dist/git-cli/providers/operations.js +33 -25
  45. package/dist/git-cli/providers/operations.js.map +1 -1
  46. package/dist/git-cli/providers/pausedOperations.d.ts +3 -1
  47. package/dist/git-cli/providers/pausedOperations.js +10 -7
  48. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  49. package/dist/git-cli/providers/refs.js +25 -1
  50. package/dist/git-cli/providers/refs.js.map +1 -1
  51. package/dist/git-cli/providers/staging.js +1 -1
  52. package/dist/git-cli/providers/stash.d.ts +3 -1
  53. package/dist/git-cli/providers/stash.js +8 -5
  54. package/dist/git-cli/providers/stash.js.map +1 -1
  55. package/dist/git-cli/providers/worktrees.d.ts +6 -1
  56. package/dist/git-cli/providers/worktrees.js +6 -2
  57. package/dist/git-cli/providers/worktrees.js.map +1 -1
  58. package/dist/git-cli/service.d.ts +2 -0
  59. package/dist/git-cli/service.js +2 -1
  60. package/dist/git-cli/service.js.map +1 -1
  61. package/dist/ipc/discovery.js +1 -1
  62. package/dist/plus/agents/providers/claudeCodeTranscript.js +6 -6
  63. package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
  64. package/dist/plus/ai/errors.d.ts +4 -1
  65. package/dist/plus/ai/errors.js +4 -0
  66. package/dist/plus/ai/errors.js.map +1 -1
  67. package/dist/plus/ai/providers/mistralProvider.js +1 -1
  68. package/dist/plus/ai/providers/mistralProvider.js.map +1 -1
  69. package/dist/plus/ai/providers/openAICompatibleProviderBase.js +2 -1
  70. package/dist/plus/ai/providers/openAICompatibleProviderBase.js.map +1 -1
  71. package/dist/plus/git-github/api/github.js +12 -13
  72. package/dist/plus/git-github/api/github.js.map +1 -1
  73. package/dist/plus/git-github/api/token.d.ts +2 -0
  74. package/dist/plus/integrations/authentication/cloudIntegrationService.d.ts +1 -1
  75. package/dist/plus/integrations/authentication/cloudIntegrationService.js +6 -1
  76. package/dist/plus/integrations/authentication/cloudIntegrationService.js.map +1 -1
  77. package/dist/plus/integrations/authentication/configuredIntegrationService.js +2 -2
  78. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +18 -5
  79. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -1
  80. package/dist/plus/integrations/authentication/integrationAuthenticationService.d.ts +3 -1
  81. package/dist/plus/integrations/authentication/integrationAuthenticationService.js +4 -0
  82. package/dist/plus/integrations/authentication/integrationAuthenticationService.js.map +1 -1
  83. package/dist/plus/integrations/authentication/models.d.ts +2 -0
  84. package/dist/plus/integrations/authentication/models.js +2 -1
  85. package/dist/plus/integrations/authentication/models.js.map +1 -1
  86. package/dist/plus/integrations/authentication/observedAuthenticationProvider.js +1 -1
  87. package/dist/plus/integrations/authentication/rejectedTokenTracker.d.ts +5 -4
  88. package/dist/plus/integrations/authentication/rejectedTokenTracker.js +5 -4
  89. package/dist/plus/integrations/authentication/rejectedTokenTracker.js.map +1 -1
  90. package/dist/plus/integrations/index.d.ts +2 -2
  91. package/dist/plus/integrations/index.js +1 -1
  92. package/dist/plus/integrations/index.js.map +1 -1
  93. package/dist/plus/integrations/integrationService.d.ts +6 -0
  94. package/dist/plus/integrations/integrationService.js +3 -3
  95. package/dist/plus/integrations/integrationService.js.map +1 -1
  96. package/dist/plus/integrations/lite.js +1 -1
  97. package/dist/plus/integrations/manager.d.ts +51 -3
  98. package/dist/plus/integrations/models/gitHostIntegration.d.ts +14 -0
  99. package/dist/plus/integrations/models/gitHostIntegration.js +50 -41
  100. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  101. package/dist/plus/integrations/models/integration.d.ts +27 -5
  102. package/dist/plus/integrations/models/integration.js +57 -15
  103. package/dist/plus/integrations/models/integration.js.map +1 -1
  104. package/dist/plus/integrations/models/issueReads.d.ts +6 -0
  105. package/dist/plus/integrations/models/issuesIntegration.js +11 -11
  106. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  107. package/dist/plus/integrations/providers/azure/azure.js +2 -2
  108. package/dist/plus/integrations/providers/azureDevOps.js +1 -1
  109. package/dist/plus/integrations/providers/azureDevOpsServer.js +1 -1
  110. package/dist/plus/integrations/providers/bitbucket/bitbucket.js +1 -1
  111. package/dist/plus/integrations/providers/bitbucket/bitbucket.js.map +1 -1
  112. package/dist/plus/integrations/providers/bitbucket-server.js +1 -1
  113. package/dist/plus/integrations/providers/bitbucket.js +1 -1
  114. package/dist/plus/integrations/providers/github.js +1 -1
  115. package/dist/plus/integrations/providers/gitlab/gitlab.js +2 -2
  116. package/dist/plus/integrations/providers/gitlab/gitlab.js.map +1 -1
  117. package/dist/plus/integrations/providers/gitlab.js +1 -1
  118. package/dist/plus/integrations/providers/jira-server.js +2 -2
  119. package/dist/plus/integrations/providers/jira.js +1 -1
  120. package/dist/plus/integrations/providers/linear.js +2 -2
  121. package/dist/plus/integrations/providers/linear.js.map +1 -1
  122. package/dist/plus/integrations/providers/models.js +2 -1
  123. package/dist/plus/integrations/providers/models.js.map +1 -1
  124. package/dist/plus/integrations/providers/providerErrors.js +9 -0
  125. package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
  126. package/dist/plus/integrations/providers/providersApi.js +1 -1
  127. package/dist/plus/integrations/providers/trello.js +1 -1
  128. package/dist/plus/integrations/providers/utils.js +1 -1
  129. package/dist/plus/integrations/reads/counts.js +1 -1
  130. package/dist/plus/integrations/reads/cursors.js +1 -1
  131. package/dist/plus/integrations/reads/drains.d.ts +5 -4
  132. package/dist/plus/integrations/reads/drains.js +28 -8
  133. package/dist/plus/integrations/reads/drains.js.map +1 -1
  134. package/dist/plus/integrations/reads/hierarchy.js +1 -1
  135. package/dist/plus/integrations/reads/issueBatch.d.ts +1 -1
  136. package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
  137. package/dist/plus/integrations/reads/issueTracker.js +1 -1
  138. package/dist/plus/integrations/reads/issues.d.ts +2 -0
  139. package/dist/plus/integrations/reads/issues.js +5 -3
  140. package/dist/plus/integrations/reads/issues.js.map +1 -1
  141. package/dist/plus/integrations/reads/paging.d.ts +5 -6
  142. package/dist/plus/integrations/reads/paging.js +5 -6
  143. package/dist/plus/integrations/reads/paging.js.map +1 -1
  144. package/dist/plus/integrations/reads/pullRequests.d.ts +4 -0
  145. package/dist/plus/integrations/reads/pullRequests.js +4 -2
  146. package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
  147. package/dist/plus/integrations/reads/resolveRepository.js +1 -1
  148. package/dist/plus/integrations/reads/sweeps.js +42 -10
  149. package/dist/plus/integrations/reads/sweeps.js.map +1 -1
  150. package/dist/plus/integrations/results.d.ts +23 -3
  151. package/dist/plus/integrations/results.js.map +1 -1
  152. package/dist/plus/notifications/api.d.ts +20 -0
  153. package/dist/plus/notifications/api.js +89 -0
  154. package/dist/plus/notifications/api.js.map +1 -0
  155. package/dist/plus/notifications/context.d.ts +28 -0
  156. package/dist/plus/notifications/context.js +0 -0
  157. package/dist/plus/notifications/errors.d.ts +13 -0
  158. package/dist/plus/notifications/errors.js +22 -0
  159. package/dist/plus/notifications/errors.js.map +1 -0
  160. package/dist/plus/notifications/models.d.ts +94 -0
  161. package/dist/plus/notifications/models.js +0 -0
  162. package/dist/plus/notifications/notificationsService.d.ts +115 -0
  163. package/dist/plus/notifications/notificationsService.js +381 -0
  164. package/dist/plus/notifications/notificationsService.js.map +1 -0
  165. package/dist/plus/notifications/parse.d.ts +12 -0
  166. package/dist/plus/notifications/parse.js +152 -0
  167. package/dist/plus/notifications/parse.js.map +1 -0
  168. package/dist/plus/notifications/presentation.d.ts +28 -0
  169. package/dist/plus/notifications/presentation.js +55 -0
  170. package/dist/plus/notifications/presentation.js.map +1 -0
  171. package/dist/utils/env/node/exec.d.ts +10 -1
  172. package/dist/utils/env/node/exec.js +41 -4
  173. package/dist/utils/env/node/exec.js.map +1 -1
  174. package/docs/integrations.md +21 -2
  175. package/docs/notifications.md +239 -0
  176. package/package.json +21 -1
@@ -1 +1 @@
1
- {"version":3,"file":"issueBatch.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issueBatch.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IntegrationIds, IssuesHostIntegrationIds } from '../constants.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport type { IssueEtagInclude } from '../models/integration.js';\nimport { issueEtagIncludes } from '../models/integration.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { githubGraphQLInt32Max, isAzureProviderId, isGitHubProviderId } from '../providers/providerErrors.js';\nimport type { ProviderResult, ProviderWarning } from '../results.js';\nimport { areDomainsOnSameHost, hostFromDomain } from '../utils/domain.utils.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\tisIssuesSelfManagedHostIntegrationId,\n} from '../utils/integration.utils.js';\nimport type { EtaggedBatchRow } from './batchEtags.js';\nimport { readEtaggedBatch } from './batchEtags.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tfindInvalidIssueEtagInclude,\n\tissueEtag,\n\tissueEtagFieldsFromShape,\n\tnormalizeIssueEtagIncludes,\n} from './etag.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissuesUnsupportedWarning,\n\tissueTrackerOnlySurfaceWarning,\n\tnoConnectionWarning,\n\totherWarning,\n\tunsupportedWarning,\n} from './warnings.js';\n\n/**\n * The BATCH issue read: resolve N issues BY IDENTITY in one call — `(owner, repo, number)` coordinates on a git\n * host, `(resourceId, ABC-123)` identifiers on an issue tracker.\n *\n * A sibling of the issue searches rather than a mode of one, because it answers a different question. A search\n * asks \"what matches\"; this asks \"does this exact issue exist\", which is what a caller correlating a branch name\n * to the issue it references is actually asking. Three consequences follow, and they are the reason this exists:\n *\n * - It resolves by EXACT IDENTIFIER, so relevance never enters into it.\n * - No result ceiling applies, so there is no partial window to reason about and no omission to report.\n * - An absent slot is a PROVEN ABSENCE, not \"not found within a page budget\". That is the property that matters\n * most: a caller can CACHE a miss. Emulating this with a paged list cannot prove absence without walking the\n * whole scope, so a miss stays unproven and the walk repeats on every pass, forever.\n *\n * Modeled on `countIssues` for its shape — caller-owned `key` echoed back, per-target isolation, as few requests\n * as the provider allows — because both take a set of independent questions and answer them together.\n *\n * ONE integration call per invocation when no target carries an `etag`, whatever the target count, as in\n * `getPullRequestsBatch`: the integration fans its targets out (chunked for GitHub/GHE, one request per target with\n * bounded concurrency on GitLab and Azure DevOps and on the trackers) and settles each independently, so failing\n * targets spend at most one strike of the integration's failure budget.\n *\n * With etags, up to THREE on a host with a cheap check (GitHub/GHE, GitLab and Azure DevOps; Jira Cloud and Linear\n * among the trackers): the cheap check of the targets that carry one, a full read of the rest started alongside it,\n * and a full read of the targets whose etag no longer matches — see `readEtaggedBatch`. Every other tracker still makes\n * one call and etags its rows.\n */\n\n/** One issue to resolve, echoed back under the caller's own `key`. Which form a call takes depends on its provider. */\nexport type IssueBatchTarget =\n\t/**\n\t * A git host's issue, by repository coordinate. GitHub/GHE, GitLab (and self-managed) and Azure DevOps (and\n\t * Server); Bitbucket and Bitbucket Data Center have no issues and refuse.\n\t */\n\t| {\n\t\t\tkey: string;\n\t\t\t/** GitHub owner, GitLab namespace path (may be nested), or Azure DevOps organization (or collection). */\n\t\t\towner: string;\n\t\t\t/**\n\t\t\t * Repository name. Required, but ignored on Azure DevOps, whose work items belong to the project rather\n\t\t\t * than to a repository; there it only names the project of a project-less Azure DevOps Server path read\n\t\t\t * behind a virtual directory (`{vdir}/{collection}/_git/{repo}` as owner `{vdir}`, project `{collection}`).\n\t\t\t */\n\t\t\trepo: string;\n\t\t\t/** Issue number, GitLab issue iid, or Azure DevOps work item id. */\n\t\t\tnumber: number;\n\t\t\t/** Azure DevOps project. Required there, ignored elsewhere. */\n\t\t\tproject?: string;\n\t\t\t/** The {@link IssueBatchResult.etag} of the copy the caller holds; see {@link IssueBatchResult}. */\n\t\t\tetag?: string;\n\t }\n\t/**\n\t * An issue tracker's issue, by its own identifier (e.g. `ABC-123`) within a resource. Jira (Cloud and Data\n\t * Center) and Linear. For Jira Data Center `resourceId` is the host — the instance's single resource, as\n\t * `listOrgs` reports it — and `resourceUrl` is not needed, since the browser link is built from the\n\t * connection's own base URL; Jira Cloud requires it.\n\t */\n\t| {\n\t\t\tkey: string;\n\t\t\tresourceId: string;\n\t\t\tresourceUrl?: string;\n\t\t\tidentifier: string;\n\t\t\t/**\n\t\t\t * The {@link IssueBatchResult.etag} of the copy the caller holds. Checked cheaply on Jira Cloud and Linear;\n\t\t\t * every other tracker accepts it but reads the issue in full.\n\t\t\t */\n\t\t\tetag?: string;\n\t };\n\ntype CoordinateTarget = Extract<IssueBatchTarget, { owner: string }>;\ntype TrackerTarget = Extract<IssueBatchTarget, { resourceId: string }>;\n\n/**\n * The answer for one {@link IssueBatchTarget}, in one of four states:\n * - `{ key, issue, etag }` — read in full. Every fully read row carries an `etag`, whether or not the caller sent\n * one, so a caller can seed its etags from the reads it already makes.\n * - `{ key, unchanged: true, etag }` — the cheap check proved the caller's copy current, and nothing else was\n * fetched. Only for a target that sent an `etag`, on a host with a cheap check.\n * - `{ key }` — PROVEN ABSENT.\n * - no row, with `fetchFailed` and a warning — the read could not check. Never treat that as absent.\n */\nexport interface IssueBatchResult {\n\tkey: string;\n\t/**\n\t * The resolved issue, or `undefined` when it PROVABLY does not exist (or is not visible to this connection) —\n\t * or when it is `unchanged`.\n\t *\n\t * Absent is an answer here, unlike every paged read on this facade: a target that FAILED — its own request\n\t * outright, or just its own alias within an otherwise-answering GitHub chunk (e.g. an org enforcing SAML SSO\n\t * the token isn't authorized for) — is not returned at all and sets `fetchFailed`, so a caller can tell\n\t * \"proven absent\" from \"unknown\" and cache the first without ever caching the second.\n\t */\n\tissue?: IssueShape;\n\t/**\n\t * Opaque: compare for equality only, never parse. Computed by core from the issue's change state — its state and\n\t * update time, plus each input the call's `etagIncludes` listed — and NOT the provider's HTTP ETag. Send it back\n\t * as the target's `etag`. An etag from another scheme or another `etagIncludes` set simply compares unequal and\n\t * costs a full read, never a false `unchanged`. A host can add a reaction without moving the update time, so\n\t * unless `'reactions'` is listed, an `unchanged` copy's `thumbsUpCount` may be stale.\n\t */\n\tetag?: string;\n\t/** The caller's copy is current (its `etag` matched); `issue` is absent because nothing was read. */\n\tunchanged?: true;\n}\n\nconst surface = 'Batch issue resolution';\n\nfunction refused(warning: ProviderWarning): ProviderResult<IssueBatchResult> {\n\treturn { items: [], warnings: [warning], fetchFailed: true };\n}\n\nexport async function getIssuesBatch(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\ttargets: readonly IssueBatchTarget[];\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\t/**\n\t\t * Widens every etag to the listed inputs, each of which a host changes without moving the issue's update\n\t\t * time, and each of which costs its own fields in the cheap check. `'reactions'` is the thumbs-up count; it\n\t\t * widens nothing on a host whose rows carry no real count (Azure DevOps work items and the trackers). Order\n\t\t * and repeats don't matter. An unknown value refuses the whole call. Changes only which etag is computed; the\n\t\t * full read is the same. Calls that differ in this set never match each other's etags.\n\t\t */\n\t\tetagIncludes?: readonly IssueEtagInclude[];\n\t},\n): Promise<ProviderResult<IssueBatchResult>> {\n\t// Nothing was asked for, so nothing is missing: an empty success, not a refusal.\n\tif (options.targets.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.targets);\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 target, not just\n\t\t// the 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 batch target key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// Same as a bad target: refused whole before any request, naming the value and what's allowed.\n\tconst invalidInclude = findInvalidIssueEtagInclude(options.etagIncludes ?? []);\n\tif (invalidInclude != null) {\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`Unknown issue etag include '${invalidInclude}'; expected one of ${issueEtagIncludes.map(i => `'${i}'`).join(', ')}.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst etagIncludes = normalizeIssueEtagIncludes(options.etagIncludes ?? []);\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn getIssuesBatchForTracker(\n\t\t\tctx,\n\t\t\toptions.providerId,\n\t\t\toptions.targets,\n\t\t\toptions.connectionId,\n\t\t\toptions.domain,\n\t\t\tetagIncludes,\n\t\t);\n\t}\n\n\tconst targets = options.targets;\n\tif (!targets.every(isCoordinateTarget)) {\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`${surface} for '${options.providerId}' takes repository coordinates { key, owner, repo, number, project? }, not tracker identifiers.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\tconst trimmedTargets = targets.map(trimCoordinateFields);\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream, where a blank owner or repo\n\t// would come back as a missing repository — a proven absence the caller would cache.\n\tconst invalid = findInvalidCoordinateTarget(options.providerId, trimmedTargets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(options.providerId, undefined, options.connectionId, invalid));\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\treturn unresolvedIntegration(ctx, options.providerId, options.connectionId, options.domain);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, surface));\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tif (!integration.supportsIssues) {\n\t\treturn refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));\n\t}\n\n\tconst toCoordinate = (t: CoordinateTarget) => ({\n\t\towner: t.owner,\n\t\trepo: t.repo,\n\t\tnumber: t.number,\n\t\tproject: t.project,\n\t});\n\tconst result = await readEtaggedBatch({\n\t\tproviderId: options.providerId,\n\t\tdomain: domain,\n\t\tconnectionId: options.connectionId,\n\t\ttargets: trimmedTargets,\n\t\tsupportsEtags: integration.supportsIssueEtags,\n\t\treadFull: batch =>\n\t\t\trunCaptured(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t() => integration.getIssuesBatchResult(batch.map(toCoordinate), undefined, options.connectionId),\n\t\t\t\t// A missing session must surface as a connection warning, including on the primary path, which\n\t\t\t\t// otherwise reads it as \"not connected\" and says nothing — and this read would then call it\n\t\t\t\t// unsupported.\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\treadEtagFields: batch =>\n\t\t\trunCaptured(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t() =>\n\t\t\t\t\tintegration.getIssuesEtagFieldsResult(\n\t\t\t\t\t\tbatch.map(toCoordinate),\n\t\t\t\t\t\t{ etagIncludes: etagIncludes },\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t),\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\titemEtag: issue => issueEtag(issueEtagFieldsFromShape(issue), etagIncludes),\n\t\tfieldsEtag: fields => issueEtag(fields, etagIncludes),\n\t\t// A provider that doesn't implement the batch hook answers `undefined` with no error. Either way its targets\n\t\t// are DROPPED rather than reported as absent — the difference between \"unknown\" and \"proven absent\" is the\n\t\t// read's whole value, and a failure must never be cached as an answer.\n\t\tunsupportedWarning: () =>\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`${surface} is not supported by '${options.providerId}'; resolve issues individually instead.`,\n\t\t\t),\n\t});\n\n\treturn toIssueBatchResult(result);\n}\n\n/**\n * The tracker form. ONE integration call per invocation when no target carries an `etag`, whatever the target count:\n * the integration makes one single-issue request per target, with bounded concurrency, and settles each independently\n * — see `IssuesIntegration.getIssuesByResourceIdBatchResult`. With etags, a tracker with a cheap check (Jira Cloud's\n * bulk fetch, Linear's per-team query) takes the coordinate form's `readEtaggedBatch` flow; every other tracker\n * ignores a target's `etag` and reads and etags every found row in full.\n *\n * `resourceId` is trusted and the read does no resource discovery. It is handed to the provider's by-resource-id\n * read as-is, never wrapped into a synthesized descriptor: Linear and Trello answer `undefined` for a descriptor\n * that fails `isIssueResourceDescriptor`, which would be published as a proven absence.\n *\n * `domain` selects the host of a self-managed tracker (Jira Data Center), as it does for\n * `listIssueTrackerIssuesPage`, and is ignored for the cloud trackers, which have a single canonical host. Unlike\n * the paged reads, this one never falls back to the primary connection for a self-managed tracker: it requires a\n * `domain` or a `connectionId` with a configured host and refuses otherwise. Two self-hosted instances routinely\n * issue the same project and issue keys, and `issue: undefined` is a proven absence a caller may cache — an answer\n * from whichever host happens to be primary would be cached under a key that names a different instance (#5872).\n */\nasync function getIssuesBatchForTracker(\n\tctx: ProviderReadContext,\n\tproviderId: IssuesHostIntegrationIds,\n\ttargets: readonly IssueBatchTarget[],\n\tconnectionId: string | undefined,\n\tdomain: string | undefined,\n\tetagIncludes: readonly IssueEtagInclude[],\n): Promise<ProviderResult<IssueBatchResult>> {\n\tif (!targets.every(isTrackerTarget)) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tundefined,\n\t\t\t\tconnectionId,\n\t\t\t\t`${surface} for '${providerId}' takes tracker identifiers { key, resourceId, resourceUrl?, identifier }, not repository coordinates.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\tconst trimmedTargets = targets.map(trimTrackerTarget);\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream to fail one by one.\n\tconst invalid = findInvalidTrackerTarget(providerId, trimmedTargets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(providerId, undefined, connectionId, invalid));\n\t}\n\n\t// A `domain` only selects a host when it parses to one, and a `connectionId` only when it names a configured\n\t// connection that has one; anything else would resolve the primary host instead, which is the fallback this\n\t// read refuses.\n\tif (\n\t\tisIssuesSelfManagedHostIntegrationId(providerId) &&\n\t\t(domain != null\n\t\t\t? hostFromDomain(domain) == null\n\t\t\t: connectionId == null ||\n\t\t\t\t!ctx.getConfigured(providerId).some(c => c.id === connectionId && hostFromDomain(c.domain) != null))\n\t) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tundefined,\n\t\t\t\tconnectionId,\n\t\t\t\t`${surface} requires a domain or a configured connection id for '${providerId}': every configured host can hold a different issue under the same key, so the read does not answer from the primary connection.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(providerId, connectionId, domain);\n\tif (integration == null) return unresolvedIntegration(ctx, providerId, connectionId, domain);\n\tif (!isIssuesIntegration(integration)) {\n\t\treturn refused(issueTrackerOnlySurfaceWarning(providerId, connectionId, surface));\n\t}\n\n\tconst unsupported = (): ProviderWarning =>\n\t\tunsupportedWarning(\n\t\t\tproviderId,\n\t\t\tundefined,\n\t\t\tconnectionId,\n\t\t\t`${surface} is not supported by '${providerId}'; its single-issue read cannot prove an absence, so a miss would not be safe to cache.`,\n\t\t);\n\tif (!integration.supportsIssueLookupByResourceId) return refused(unsupported());\n\n\t// A self-managed tracker's single resource is its host, and a caller keys (and caches) each answer by\n\t// `resourceId` while the read is addressed to the host `domain`/`connectionId` resolved. When they disagree,\n\t// host B's answer — including a cacheable absence — would be stored under host A's key. Refused whole, like\n\t// the caller bugs above: a target naming another host means the call selected a host its caller did not mean.\n\tif (isIssuesSelfManagedHostIntegrationId(providerId)) {\n\t\tconst mismatched = trimmedTargets.find(t => !areDomainsOnSameHost(t.resourceId, integration.domain));\n\t\tif (mismatched != null) {\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\tproviderId,\n\t\t\t\t\tundefined,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\t`Issue batch target '${mismatched.key}' requires the resource id of '${providerId}' to name the host the read resolved to.`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\n\tconst resolvedDomain = ctx.domainForRead(integration, providerId, connectionId, domain);\n\n\tconst toResourceTarget = (t: TrackerTarget) => ({\n\t\tresourceId: t.resourceId,\n\t\tidentifier: t.identifier,\n\t\tresourceUrl: t.resourceUrl,\n\t});\n\tconst result = await readEtaggedBatch({\n\t\tproviderId: providerId,\n\t\tdomain: resolvedDomain,\n\t\tconnectionId: connectionId,\n\t\ttargets: trimmedTargets,\n\t\tsupportsEtags: integration.supportsIssueEtagsByResourceId,\n\t\treadFull: batch =>\n\t\t\trunCaptured(\n\t\t\t\tproviderId,\n\t\t\t\tresolvedDomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getIssuesByResourceIdBatchResult(batch.map(toResourceTarget), connectionId),\n\t\t\t\t// The read core answers `undefined` when it cannot resolve a session. That must surface as a connection\n\t\t\t\t// warning, including on the primary path, rather than as nothing — or worse, be read as absence.\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\treadEtagFields: batch =>\n\t\t\trunCaptured(\n\t\t\t\tproviderId,\n\t\t\t\tresolvedDomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() =>\n\t\t\t\t\tintegration.getIssuesEtagFieldsByResourceIdBatchResult(\n\t\t\t\t\t\tbatch.map(toResourceTarget),\n\t\t\t\t\t\t{ etagIncludes: etagIncludes },\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t),\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\titemEtag: issue => issueEtag(issueEtagFieldsFromShape(issue), etagIncludes),\n\t\tfieldsEtag: fields => issueEtag(fields, etagIncludes),\n\t\tunsupportedWarning: unsupported,\n\t});\n\n\treturn toIssueBatchResult(result);\n}\n\n/** The etag flow's rows as this read's results, under the `issue` name the result type gives the item. */\nfunction toIssueBatchResult(result: ProviderResult<EtaggedBatchRow<IssueShape>>): ProviderResult<IssueBatchResult> {\n\treturn {\n\t\titems: result.items.map(row => ({\n\t\t\tkey: row.key,\n\t\t\t...(row.value != null ? { issue: row.value } : {}),\n\t\t\t...(row.unchanged ? { unchanged: true as const } : {}),\n\t\t\t...(row.etag != null ? { etag: row.etag } : {}),\n\t\t})),\n\t\twarnings: result.warnings,\n\t\tfetchFailed: result.fetchFailed,\n\t};\n}\n\nfunction isCoordinateTarget(target: IssueBatchTarget): target is CoordinateTarget {\n\treturn 'owner' in target;\n}\n\nfunction isTrackerTarget(target: IssueBatchTarget): target is TrackerTarget {\n\treturn 'resourceId' in target;\n}\n\n/**\n * Trims `owner`, `repo` and `project` once, shared by every coordinate-form batch read (this one, the pull request\n * batch and the pull-requests-by-branch read): leading/trailing whitespace can't change which repository a\n * coordinate names, so it's stripped and accepted rather than refused, matching #5821's scope-name precedent.\n */\nexport function trimCoordinateFields<T extends { owner: string; repo: string; project?: string }>(target: T): T {\n\treturn { ...target, owner: target.owner.trim(), repo: target.repo.trim(), project: target.project?.trim() };\n}\n\n/** Trims `resourceId`, `resourceUrl` and `identifier` once; the validated value must be the one sent to the provider. */\nfunction trimTrackerTarget(target: TrackerTarget): TrackerTarget {\n\treturn {\n\t\t...target,\n\t\tresourceId: target.resourceId.trim(),\n\t\tresourceUrl: target.resourceUrl?.trim() || undefined,\n\t\tidentifier: target.identifier.trim(),\n\t};\n}\n\nfunction findInvalidCoordinateTarget(\n\tproviderId: IntegrationIds,\n\ttargets: readonly CoordinateTarget[],\n): string | undefined {\n\tconst isAzure = isAzureProviderId(providerId);\n\tconst isGitHub = isGitHubProviderId(providerId);\n\tfor (const target of targets) {\n\t\tif (!Number.isSafeInteger(target.number) || target.number <= 0) {\n\t\t\treturn `Issue batch target '${target.key}' has number ${target.number}; expected a positive integer.`;\n\t\t}\n\t\tif (isGitHub && target.number > githubGraphQLInt32Max) {\n\t\t\treturn `Issue batch target '${target.key}' has number ${target.number}; GitHub numbers are 32-bit and cannot exceed ${githubGraphQLInt32Max}.`;\n\t\t}\n\t\tif (target.owner.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a non-empty owner.`;\n\t\t}\n\t\tif (isAzure) {\n\t\t\tif (!target.project) {\n\t\t\t\treturn `Issue batch target '${target.key}' requires a project for '${providerId}'.`;\n\t\t\t}\n\t\t} else if (target.repo.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a non-empty repo.`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction findInvalidTrackerTarget(\n\tproviderId: IssuesHostIntegrationIds,\n\ttargets: readonly TrackerTarget[],\n): string | undefined {\n\tfor (const target of targets) {\n\t\tif (target.resourceId.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a resource id.`;\n\t\t}\n\t\tif (target.identifier.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires an issue identifier.`;\n\t\t}\n\t\tif (providerId === IssuesCloudHostIntegrationId.Jira && !target.resourceUrl) {\n\t\t\treturn `Issue batch target '${target.key}' requires the Jira resource URL so the result contains a browser link without resource discovery.`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * The answer when no integration resolves for a batch read. Targets were asked about, so this is never a silent\n * empty success: a supplied connection or domain that no longer resolves gets its own warning, and the untargeted\n * primary path (e.g. a self-managed provider with no configured host) a connection warning, as `getCurrentAccount`\n * gives — either way `fetchFailed`, so no caller mistakes the dropped targets for an answer.\n */\nexport function unresolvedIntegration<T>(\n\tctx: ProviderReadContext,\n\tproviderId: IntegrationIds,\n\tconnectionId: string | undefined,\n\tdomain: string | undefined,\n): ProviderResult<T> {\n\tconst early = ctx.earlyReturnConnectionWarnings(providerId, connectionId, domain);\n\tif (early.warnings.length > 0) return { items: [], warnings: early.warnings, fetchFailed: true };\n\n\tconst resolvedDomain = ctx.resolveDomainForRead(providerId, connectionId, domain);\n\treturn { items: [], warnings: [noConnectionWarning(providerId, resolvedDomain, connectionId)], fetchFailed: true };\n}\n\nexport function findDuplicateKey(targets: readonly { key: string }[]): string | undefined {\n\tconst seen = new Set<string>();\n\tfor (const target of targets) {\n\t\tif (seen.has(target.key)) return target.key;\n\n\t\tseen.add(target.key);\n\t}\n\treturn undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"issueBatch.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issueBatch.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IntegrationIds, IssuesHostIntegrationIds } from '../constants.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport type { IssueEtagInclude } from '../models/integration.js';\nimport { issueEtagIncludes } from '../models/integration.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { githubGraphQLInt32Max, isAzureProviderId, isGitHubProviderId } from '../providers/providerErrors.js';\nimport type { ProviderResult, ProviderWarning } from '../results.js';\nimport { areDomainsOnSameHost, hostFromDomain } from '../utils/domain.utils.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\tisIssuesSelfManagedHostIntegrationId,\n} from '../utils/integration.utils.js';\nimport type { EtaggedBatchRow } from './batchEtags.js';\nimport { readEtaggedBatch } from './batchEtags.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tfindInvalidIssueEtagInclude,\n\tissueEtag,\n\tissueEtagFieldsFromShape,\n\tnormalizeIssueEtagIncludes,\n} from './etag.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissuesUnsupportedWarning,\n\tissueTrackerOnlySurfaceWarning,\n\tnoConnectionWarning,\n\totherWarning,\n\tunsupportedWarning,\n} from './warnings.js';\n\n/**\n * The BATCH issue read: resolve N issues BY IDENTITY in one call — `(owner, repo, number)` coordinates on a git\n * host, `(resourceId, ABC-123)` identifiers on an issue tracker.\n *\n * A sibling of the issue searches rather than a mode of one, because it answers a different question. A search\n * asks \"what matches\"; this asks \"does this exact issue exist\", which is what a caller correlating a branch name\n * to the issue it references is actually asking. Three consequences follow, and they are the reason this exists:\n *\n * - It resolves by EXACT IDENTIFIER, so relevance never enters into it.\n * - No result ceiling applies, so there is no partial window to reason about and no omission to report.\n * - An absent slot is a PROVEN ABSENCE, not \"not found within a page budget\". That is the property that matters\n * most: a caller can CACHE a miss. Emulating this with a paged list cannot prove absence without walking the\n * whole scope, so a miss stays unproven and the walk repeats on every pass, forever.\n *\n * Modeled on `countIssues` for its shape — caller-owned `key` echoed back, per-target isolation, as few requests\n * as the provider allows — because both take a set of independent questions and answer them together.\n *\n * ONE integration call per invocation when no target carries an `etag`, whatever the target count, as in\n * `getPullRequestsBatch`: the integration fans its targets out (chunked for GitHub/GHE, one request per target with\n * bounded concurrency on GitLab and Azure DevOps and on the trackers) and settles each independently, so failing\n * targets spend at most one strike of the connection's failure budget.\n *\n * With etags, up to THREE on a host with a cheap check (GitHub/GHE, GitLab and Azure DevOps; Jira Cloud and Linear\n * among the trackers): the cheap check of the targets that carry one, a full read of the rest started alongside it,\n * and a full read of the targets whose etag no longer matches — see `readEtaggedBatch`. Every other tracker still makes\n * one call and etags its rows.\n */\n\n/** One issue to resolve, echoed back under the caller's own `key`. Which form a call takes depends on its provider. */\nexport type IssueBatchTarget =\n\t/**\n\t * A git host's issue, by repository coordinate. GitHub/GHE, GitLab (and self-managed) and Azure DevOps (and\n\t * Server); Bitbucket and Bitbucket Data Center have no issues and refuse.\n\t */\n\t| {\n\t\t\tkey: string;\n\t\t\t/** GitHub owner, GitLab namespace path (may be nested), or Azure DevOps organization (or collection). */\n\t\t\towner: string;\n\t\t\t/**\n\t\t\t * Repository name. Required, but ignored on Azure DevOps, whose work items belong to the project rather\n\t\t\t * than to a repository; there it only names the project of a project-less Azure DevOps Server path read\n\t\t\t * behind a virtual directory (`{vdir}/{collection}/_git/{repo}` as owner `{vdir}`, project `{collection}`).\n\t\t\t */\n\t\t\trepo: string;\n\t\t\t/** Issue number, GitLab issue iid, or Azure DevOps work item id. */\n\t\t\tnumber: number;\n\t\t\t/** Azure DevOps project. Required there, ignored elsewhere. */\n\t\t\tproject?: string;\n\t\t\t/** The {@link IssueBatchResult.etag} of the copy the caller holds; see {@link IssueBatchResult}. */\n\t\t\tetag?: string;\n\t }\n\t/**\n\t * An issue tracker's issue, by its own identifier (e.g. `ABC-123`) within a resource. Jira (Cloud and Data\n\t * Center) and Linear. For Jira Data Center `resourceId` is the host — the instance's single resource, as\n\t * `listOrgs` reports it — and `resourceUrl` is not needed, since the browser link is built from the\n\t * connection's own base URL; Jira Cloud requires it.\n\t */\n\t| {\n\t\t\tkey: string;\n\t\t\tresourceId: string;\n\t\t\tresourceUrl?: string;\n\t\t\tidentifier: string;\n\t\t\t/**\n\t\t\t * The {@link IssueBatchResult.etag} of the copy the caller holds. Checked cheaply on Jira Cloud and Linear;\n\t\t\t * every other tracker accepts it but reads the issue in full.\n\t\t\t */\n\t\t\tetag?: string;\n\t };\n\ntype CoordinateTarget = Extract<IssueBatchTarget, { owner: string }>;\ntype TrackerTarget = Extract<IssueBatchTarget, { resourceId: string }>;\n\n/**\n * The answer for one {@link IssueBatchTarget}, in one of four states:\n * - `{ key, issue, etag }` — read in full. Every fully read row carries an `etag`, whether or not the caller sent\n * one, so a caller can seed its etags from the reads it already makes.\n * - `{ key, unchanged: true, etag }` — the cheap check proved the caller's copy current, and nothing else was\n * fetched. Only for a target that sent an `etag`, on a host with a cheap check.\n * - `{ key }` — PROVEN ABSENT.\n * - no row, with `fetchFailed` and a warning — the read could not check. Never treat that as absent.\n */\nexport interface IssueBatchResult {\n\tkey: string;\n\t/**\n\t * The resolved issue, or `undefined` when it PROVABLY does not exist (or is not visible to this connection) —\n\t * or when it is `unchanged`.\n\t *\n\t * Absent is an answer here, unlike every paged read on this facade: a target that FAILED — its own request\n\t * outright, or just its own alias within an otherwise-answering GitHub chunk (e.g. an org enforcing SAML SSO\n\t * the token isn't authorized for) — is not returned at all and sets `fetchFailed`, so a caller can tell\n\t * \"proven absent\" from \"unknown\" and cache the first without ever caching the second.\n\t */\n\tissue?: IssueShape;\n\t/**\n\t * Opaque: compare for equality only, never parse. Computed by core from the issue's change state — its state and\n\t * update time, plus each input the call's `etagIncludes` listed — and NOT the provider's HTTP ETag. Send it back\n\t * as the target's `etag`. An etag from another scheme or another `etagIncludes` set simply compares unequal and\n\t * costs a full read, never a false `unchanged`. A host can add a reaction without moving the update time, so\n\t * unless `'reactions'` is listed, an `unchanged` copy's `thumbsUpCount` may be stale.\n\t */\n\tetag?: string;\n\t/** The caller's copy is current (its `etag` matched); `issue` is absent because nothing was read. */\n\tunchanged?: true;\n}\n\nconst surface = 'Batch issue resolution';\n\nfunction refused(warning: ProviderWarning): ProviderResult<IssueBatchResult> {\n\treturn { items: [], warnings: [warning], fetchFailed: true };\n}\n\nexport async function getIssuesBatch(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\ttargets: readonly IssueBatchTarget[];\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\t/**\n\t\t * Widens every etag to the listed inputs, each of which a host changes without moving the issue's update\n\t\t * time, and each of which costs its own fields in the cheap check. `'reactions'` is the thumbs-up count; it\n\t\t * widens nothing on a host whose rows carry no real count (Azure DevOps work items and the trackers). Order\n\t\t * and repeats don't matter. An unknown value refuses the whole call. Changes only which etag is computed; the\n\t\t * full read is the same. Calls that differ in this set never match each other's etags.\n\t\t */\n\t\tetagIncludes?: readonly IssueEtagInclude[];\n\t},\n): Promise<ProviderResult<IssueBatchResult>> {\n\t// Nothing was asked for, so nothing is missing: an empty success, not a refusal.\n\tif (options.targets.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.targets);\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 target, not just\n\t\t// the 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 batch target key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// Same as a bad target: refused whole before any request, naming the value and what's allowed.\n\tconst invalidInclude = findInvalidIssueEtagInclude(options.etagIncludes ?? []);\n\tif (invalidInclude != null) {\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`Unknown issue etag include '${invalidInclude}'; expected one of ${issueEtagIncludes.map(i => `'${i}'`).join(', ')}.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst etagIncludes = normalizeIssueEtagIncludes(options.etagIncludes ?? []);\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn getIssuesBatchForTracker(\n\t\t\tctx,\n\t\t\toptions.providerId,\n\t\t\toptions.targets,\n\t\t\toptions.connectionId,\n\t\t\toptions.domain,\n\t\t\tetagIncludes,\n\t\t);\n\t}\n\n\tconst targets = options.targets;\n\tif (!targets.every(isCoordinateTarget)) {\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`${surface} for '${options.providerId}' takes repository coordinates { key, owner, repo, number, project? }, not tracker identifiers.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\tconst trimmedTargets = targets.map(trimCoordinateFields);\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream, where a blank owner or repo\n\t// would come back as a missing repository — a proven absence the caller would cache.\n\tconst invalid = findInvalidCoordinateTarget(options.providerId, trimmedTargets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(options.providerId, undefined, options.connectionId, invalid));\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\treturn unresolvedIntegration(ctx, options.providerId, options.connectionId, options.domain);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, surface));\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tif (!integration.supportsIssues) {\n\t\treturn refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));\n\t}\n\n\tconst toCoordinate = (t: CoordinateTarget) => ({\n\t\towner: t.owner,\n\t\trepo: t.repo,\n\t\tnumber: t.number,\n\t\tproject: t.project,\n\t});\n\tconst result = await readEtaggedBatch({\n\t\tproviderId: options.providerId,\n\t\tdomain: domain,\n\t\tconnectionId: options.connectionId,\n\t\ttargets: trimmedTargets,\n\t\tsupportsEtags: integration.supportsIssueEtags,\n\t\treadFull: batch =>\n\t\t\trunCaptured(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t() => integration.getIssuesBatchResult(batch.map(toCoordinate), undefined, options.connectionId),\n\t\t\t\t// A missing session must surface as a connection warning, including on the primary path, which\n\t\t\t\t// otherwise reads it as \"not connected\" and says nothing — and this read would then call it\n\t\t\t\t// unsupported.\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\treadEtagFields: batch =>\n\t\t\trunCaptured(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t() =>\n\t\t\t\t\tintegration.getIssuesEtagFieldsResult(\n\t\t\t\t\t\tbatch.map(toCoordinate),\n\t\t\t\t\t\t{ etagIncludes: etagIncludes },\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t),\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\titemEtag: issue => issueEtag(issueEtagFieldsFromShape(issue), etagIncludes),\n\t\tfieldsEtag: fields => issueEtag(fields, etagIncludes),\n\t\t// A provider that doesn't implement the batch hook answers `undefined` with no error. Either way its targets\n\t\t// are DROPPED rather than reported as absent — the difference between \"unknown\" and \"proven absent\" is the\n\t\t// read's whole value, and a failure must never be cached as an answer.\n\t\tunsupportedWarning: () =>\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`${surface} is not supported by '${options.providerId}'; resolve issues individually instead.`,\n\t\t\t),\n\t});\n\n\treturn toIssueBatchResult(result);\n}\n\n/**\n * The tracker form. ONE integration call per invocation when no target carries an `etag`, whatever the target count:\n * the integration makes one single-issue request per target, with bounded concurrency, and settles each independently\n * — see `IssuesIntegration.getIssuesByResourceIdBatchResult`. With etags, a tracker with a cheap check (Jira Cloud's\n * bulk fetch, Linear's per-team query) takes the coordinate form's `readEtaggedBatch` flow; every other tracker\n * ignores a target's `etag` and reads and etags every found row in full.\n *\n * `resourceId` is trusted and the read does no resource discovery. It is handed to the provider's by-resource-id\n * read as-is, never wrapped into a synthesized descriptor: Linear and Trello answer `undefined` for a descriptor\n * that fails `isIssueResourceDescriptor`, which would be published as a proven absence.\n *\n * `domain` selects the host of a self-managed tracker (Jira Data Center), as it does for\n * `listIssueTrackerIssuesPage`, and is ignored for the cloud trackers, which have a single canonical host. Unlike\n * the paged reads, this one never falls back to the primary connection for a self-managed tracker: it requires a\n * `domain` or a `connectionId` with a configured host and refuses otherwise. Two self-hosted instances routinely\n * issue the same project and issue keys, and `issue: undefined` is a proven absence a caller may cache — an answer\n * from whichever host happens to be primary would be cached under a key that names a different instance (#5872).\n */\nasync function getIssuesBatchForTracker(\n\tctx: ProviderReadContext,\n\tproviderId: IssuesHostIntegrationIds,\n\ttargets: readonly IssueBatchTarget[],\n\tconnectionId: string | undefined,\n\tdomain: string | undefined,\n\tetagIncludes: readonly IssueEtagInclude[],\n): Promise<ProviderResult<IssueBatchResult>> {\n\tif (!targets.every(isTrackerTarget)) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tundefined,\n\t\t\t\tconnectionId,\n\t\t\t\t`${surface} for '${providerId}' takes tracker identifiers { key, resourceId, resourceUrl?, identifier }, not repository coordinates.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\tconst trimmedTargets = targets.map(trimTrackerTarget);\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream to fail one by one.\n\tconst invalid = findInvalidTrackerTarget(providerId, trimmedTargets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(providerId, undefined, connectionId, invalid));\n\t}\n\n\t// A `domain` only selects a host when it parses to one, and a `connectionId` only when it names a configured\n\t// connection that has one; anything else would resolve the primary host instead, which is the fallback this\n\t// read refuses.\n\tif (\n\t\tisIssuesSelfManagedHostIntegrationId(providerId) &&\n\t\t(domain != null\n\t\t\t? hostFromDomain(domain) == null\n\t\t\t: connectionId == null ||\n\t\t\t\t!ctx.getConfigured(providerId).some(c => c.id === connectionId && hostFromDomain(c.domain) != null))\n\t) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tundefined,\n\t\t\t\tconnectionId,\n\t\t\t\t`${surface} requires a domain or a configured connection id for '${providerId}': every configured host can hold a different issue under the same key, so the read does not answer from the primary connection.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(providerId, connectionId, domain);\n\tif (integration == null) return unresolvedIntegration(ctx, providerId, connectionId, domain);\n\tif (!isIssuesIntegration(integration)) {\n\t\treturn refused(issueTrackerOnlySurfaceWarning(providerId, connectionId, surface));\n\t}\n\n\tconst unsupported = (): ProviderWarning =>\n\t\tunsupportedWarning(\n\t\t\tproviderId,\n\t\t\tundefined,\n\t\t\tconnectionId,\n\t\t\t`${surface} is not supported by '${providerId}'; its single-issue read cannot prove an absence, so a miss would not be safe to cache.`,\n\t\t);\n\tif (!integration.supportsIssueLookupByResourceId) return refused(unsupported());\n\n\t// A self-managed tracker's single resource is its host, and a caller keys (and caches) each answer by\n\t// `resourceId` while the read is addressed to the host `domain`/`connectionId` resolved. When they disagree,\n\t// host B's answer — including a cacheable absence — would be stored under host A's key. Refused whole, like\n\t// the caller bugs above: a target naming another host means the call selected a host its caller did not mean.\n\tif (isIssuesSelfManagedHostIntegrationId(providerId)) {\n\t\tconst mismatched = trimmedTargets.find(t => !areDomainsOnSameHost(t.resourceId, integration.domain));\n\t\tif (mismatched != null) {\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\tproviderId,\n\t\t\t\t\tundefined,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\t`Issue batch target '${mismatched.key}' requires the resource id of '${providerId}' to name the host the read resolved to.`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\n\tconst resolvedDomain = ctx.domainForRead(integration, providerId, connectionId, domain);\n\n\tconst toResourceTarget = (t: TrackerTarget) => ({\n\t\tresourceId: t.resourceId,\n\t\tidentifier: t.identifier,\n\t\tresourceUrl: t.resourceUrl,\n\t});\n\tconst result = await readEtaggedBatch({\n\t\tproviderId: providerId,\n\t\tdomain: resolvedDomain,\n\t\tconnectionId: connectionId,\n\t\ttargets: trimmedTargets,\n\t\tsupportsEtags: integration.supportsIssueEtagsByResourceId,\n\t\treadFull: batch =>\n\t\t\trunCaptured(\n\t\t\t\tproviderId,\n\t\t\t\tresolvedDomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getIssuesByResourceIdBatchResult(batch.map(toResourceTarget), connectionId),\n\t\t\t\t// The read core answers `undefined` when it cannot resolve a session. That must surface as a connection\n\t\t\t\t// warning, including on the primary path, rather than as nothing — or worse, be read as absence.\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\treadEtagFields: batch =>\n\t\t\trunCaptured(\n\t\t\t\tproviderId,\n\t\t\t\tresolvedDomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() =>\n\t\t\t\t\tintegration.getIssuesEtagFieldsByResourceIdBatchResult(\n\t\t\t\t\t\tbatch.map(toResourceTarget),\n\t\t\t\t\t\t{ etagIncludes: etagIncludes },\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t),\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\titemEtag: issue => issueEtag(issueEtagFieldsFromShape(issue), etagIncludes),\n\t\tfieldsEtag: fields => issueEtag(fields, etagIncludes),\n\t\tunsupportedWarning: unsupported,\n\t});\n\n\treturn toIssueBatchResult(result);\n}\n\n/** The etag flow's rows as this read's results, under the `issue` name the result type gives the item. */\nfunction toIssueBatchResult(result: ProviderResult<EtaggedBatchRow<IssueShape>>): ProviderResult<IssueBatchResult> {\n\treturn {\n\t\titems: result.items.map(row => ({\n\t\t\tkey: row.key,\n\t\t\t...(row.value != null ? { issue: row.value } : {}),\n\t\t\t...(row.unchanged ? { unchanged: true as const } : {}),\n\t\t\t...(row.etag != null ? { etag: row.etag } : {}),\n\t\t})),\n\t\twarnings: result.warnings,\n\t\tfetchFailed: result.fetchFailed,\n\t};\n}\n\nfunction isCoordinateTarget(target: IssueBatchTarget): target is CoordinateTarget {\n\treturn 'owner' in target;\n}\n\nfunction isTrackerTarget(target: IssueBatchTarget): target is TrackerTarget {\n\treturn 'resourceId' in target;\n}\n\n/**\n * Trims `owner`, `repo` and `project` once, shared by every coordinate-form batch read (this one, the pull request\n * batch and the pull-requests-by-branch read): leading/trailing whitespace can't change which repository a\n * coordinate names, so it's stripped and accepted rather than refused, matching #5821's scope-name precedent.\n */\nexport function trimCoordinateFields<T extends { owner: string; repo: string; project?: string }>(target: T): T {\n\treturn { ...target, owner: target.owner.trim(), repo: target.repo.trim(), project: target.project?.trim() };\n}\n\n/** Trims `resourceId`, `resourceUrl` and `identifier` once; the validated value must be the one sent to the provider. */\nfunction trimTrackerTarget(target: TrackerTarget): TrackerTarget {\n\treturn {\n\t\t...target,\n\t\tresourceId: target.resourceId.trim(),\n\t\tresourceUrl: target.resourceUrl?.trim() || undefined,\n\t\tidentifier: target.identifier.trim(),\n\t};\n}\n\nfunction findInvalidCoordinateTarget(\n\tproviderId: IntegrationIds,\n\ttargets: readonly CoordinateTarget[],\n): string | undefined {\n\tconst isAzure = isAzureProviderId(providerId);\n\tconst isGitHub = isGitHubProviderId(providerId);\n\tfor (const target of targets) {\n\t\tif (!Number.isSafeInteger(target.number) || target.number <= 0) {\n\t\t\treturn `Issue batch target '${target.key}' has number ${target.number}; expected a positive integer.`;\n\t\t}\n\t\tif (isGitHub && target.number > githubGraphQLInt32Max) {\n\t\t\treturn `Issue batch target '${target.key}' has number ${target.number}; GitHub numbers are 32-bit and cannot exceed ${githubGraphQLInt32Max}.`;\n\t\t}\n\t\tif (target.owner.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a non-empty owner.`;\n\t\t}\n\t\tif (isAzure) {\n\t\t\tif (!target.project) {\n\t\t\t\treturn `Issue batch target '${target.key}' requires a project for '${providerId}'.`;\n\t\t\t}\n\t\t} else if (target.repo.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a non-empty repo.`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction findInvalidTrackerTarget(\n\tproviderId: IssuesHostIntegrationIds,\n\ttargets: readonly TrackerTarget[],\n): string | undefined {\n\tfor (const target of targets) {\n\t\tif (target.resourceId.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a resource id.`;\n\t\t}\n\t\tif (target.identifier.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires an issue identifier.`;\n\t\t}\n\t\tif (providerId === IssuesCloudHostIntegrationId.Jira && !target.resourceUrl) {\n\t\t\treturn `Issue batch target '${target.key}' requires the Jira resource URL so the result contains a browser link without resource discovery.`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * The answer when no integration resolves for a batch read. Targets were asked about, so this is never a silent\n * empty success: a supplied connection or domain that no longer resolves gets its own warning, and the untargeted\n * primary path (e.g. a self-managed provider with no configured host) a connection warning, as `getCurrentAccount`\n * gives — either way `fetchFailed`, so no caller mistakes the dropped targets for an answer.\n */\nexport function unresolvedIntegration<T>(\n\tctx: ProviderReadContext,\n\tproviderId: IntegrationIds,\n\tconnectionId: string | undefined,\n\tdomain: string | undefined,\n): ProviderResult<T> {\n\tconst early = ctx.earlyReturnConnectionWarnings(providerId, connectionId, domain);\n\tif (early.warnings.length > 0) return { items: [], warnings: early.warnings, fetchFailed: true };\n\n\tconst resolvedDomain = ctx.resolveDomainForRead(providerId, connectionId, domain);\n\treturn { items: [], warnings: [noConnectionWarning(providerId, resolvedDomain, connectionId)], fetchFailed: true };\n}\n\nexport function findDuplicateKey(targets: readonly { key: string }[]): string | undefined {\n\tconst seen = new Set<string>();\n\tfor (const target of targets) {\n\t\tif (seen.has(target.key)) return target.key;\n\n\t\tseen.add(target.key);\n\t}\n\treturn undefined;\n}\n"],"mappings":"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"}
@@ -4,9 +4,9 @@ import "../constants.js";
4
4
  import { appendDedupedWarning, reconcileOmissionsWithFailure } from "../results.js";
5
5
  import { assessCollectionMetadata, mergeAssessmentInto } from "../collectionMetadata.js";
6
6
  import "../providerFilters.js";
7
- import { isIssuesHostIntegrationId, isLinearIntegration } from "../utils/integration.utils.js";
8
7
  import { mergeCollectionMetadata, parsePageCursor } from "../providers/utils/providerPaging.js";
9
8
  import { providersMetadata } from "../providers/models.js";
9
+ import { isIssuesHostIntegrationId, isLinearIntegration } from "../utils/integration.utils.js";
10
10
  import { isIssuesIntegration } from "../models/issuesIntegration.js";
11
11
  import { linearProjectIds, parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from "./cursors.js";
12
12
  import { incompleteReadWarning, issueTrackerOnlySurfaceWarning, otherWarning, projectCutWarning, unmatchedSelectorWarning, unmergeableIssueSortWarning, unsupportedIssueSortWarning, unsupportedWarning } from "./warnings.js";
@@ -49,6 +49,8 @@ export interface ListIssuesPageOptions {
49
49
  page?: number;
50
50
  cursor?: string;
51
51
  itemsPerPage?: number;
52
+ /** See {@link IntegrationManager.listIssuesPage}. */
53
+ scopeConcurrency?: number;
52
54
  forceSync?: boolean;
53
55
  connectionId?: string;
54
56
  /**
@@ -1,9 +1,9 @@
1
1
  import "../constants.js";
2
2
  import { reconcileOmissionsWithFailure } from "../results.js";
3
3
  import { mergeAssessmentInto } from "../collectionMetadata.js";
4
- import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
5
4
  import { mergeCollectionMetadata } from "../providers/utils/providerPaging.js";
6
5
  import { isRepoIdsInput, providersMetadata } from "../providers/models.js";
6
+ import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
7
7
  import { drainFlatPagesToRequestedPage, drainToRequestedPage, isPageNumberAdvanceable, pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo, usableCursor } from "./paging.js";
8
8
  import { gitHostOnlySurfaceWarning, issuesUnsupportedWarning, otherWarning, truncationWarning, unmergeableIssueSortWarning, unsupportedAccountWideIssueFiltersWarning, unsupportedIssueSortWarning, unsupportedWarning } from "./warnings.js";
9
9
  import { runCaptured } from "./drains.js";
@@ -169,7 +169,8 @@ async function readRepoScopedIssuesPage({ options, integration, domain, warnOnMi
169
169
  cursor,
170
170
  page: options.page != null ? page : void 0,
171
171
  pageSize: options.itemsPerPage,
172
- sort: ordering.sort
172
+ sort: ordering.sort,
173
+ scopeConcurrency: options.scopeConcurrency
173
174
  }, options.connectionId), { warnOnMissingSession });
174
175
  let items = value?.values ?? [];
175
176
  const warnings = warning != null ? [warning] : [];
@@ -193,7 +194,8 @@ async function readRepoScopedIssuesPage({ options, integration, domain, warnOnMi
193
194
  state: options.state,
194
195
  cursor,
195
196
  pageSize: options.itemsPerPage,
196
- sort: ordering.sort
197
+ sort: ordering.sort,
198
+ scopeConcurrency: options.scopeConcurrency
197
199
  }, options.connectionId), { warnOnMissingSession })
198
200
  });
199
201
  items = drained.items;
@@ -1 +1 @@
1
- {"version":3,"file":"issues.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issues.ts"],"sourcesContent":["import type { IssueShape, IssueSorting, IssueStateFilter } from '@gitlens/git/models/issue.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../constants.js';\nimport type { GitHostIntegration, SettledRepositoryScopes } from '../models/gitHostIntegration.js';\nimport type { IssueFilter, ProviderReposInput } from '../providers/models.js';\nimport { isRepoIdsInput, PagingMode, providersMetadata } from '../providers/models.js';\nimport { mergeCollectionMetadata } from '../providers/utils/providerPaging.js';\nimport type { ProviderPagedResult, ProviderWarning } from '../results.js';\nimport { reconcileOmissionsWithFailure } 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 { resolveAccountWideIssueFilters } from './filters.js';\nimport { resolveIssueSort, toIssueOrdering } from './ordering.js';\nimport {\n\tdrainFlatPagesToRequestedPage,\n\tdrainToRequestedPage,\n\tisPageNumberAdvanceable,\n\tpageToCursor,\n\trefusedPage,\n\tresolveContinuation,\n\tresolveCurrentPage,\n\ttoProviderPageInfo,\n\tusableCursor,\n} from './paging.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissuesUnsupportedWarning,\n\totherWarning,\n\ttruncationWarning,\n\tunmergeableIssueSortWarning,\n\tunsupportedAccountWideIssueFiltersWarning,\n\tunsupportedIssueSortWarning,\n\tunsupportedWarning,\n} from './warnings.js';\n\nconst issueStateFilters: readonly IssueStateFilter[] = ['open', 'closed', 'all'];\n\n/**\n * Whether a repo-scoped page is assembled from SEVERAL provider queries and merged here — which is what decides\n * whether the requested order can be honored at all.\n *\n * `PagingMode.Repos` (GitHub) sends one search however many repositories are named, so its page arrives ordered by\n * the provider. `Repo` (GitLab) issues one query per repository, and `Project` (Azure) one per PROJECT — several\n * repositories of the same project are still one query, which is why that case counts distinct projects rather than\n * repositories.\n *\n * The `Repo` case has a second way to merge that has nothing to do with the count: given repository IDS rather than\n * descriptors, `getMyIssuesForReposResult` skips its per-repository fan-out entirely (that branch is guarded on\n * `!isRepoIdsInput`) and calls the SDK's `getIssuesForRepos`, which for GitLab is the multi-project aggregate that\n * merges in the SDK and refuses `priority`/`dueDate` however few scopes it was given. Treating that as merged is\n * what makes this facade refuse it up front, with the message that names the reason, instead of letting the SDK\n * reject a read the capability table had just promised.\n */\nfunction mergesProviderQueries(pagingMode: PagingMode | undefined, repos: ProviderReposInput | undefined): boolean {\n\tconst scopes = repos ?? [];\n\tswitch (pagingMode) {\n\t\tcase PagingMode.Repo:\n\t\t\t// `isRepoIdsInput` is the same predicate `ProvidersApi` selects the SDK aggregate with, so this cannot\n\t\t\t// disagree with the branch it is predicting.\n\t\t\treturn isRepoIdsInput(scopes) ? scopes.length > 0 : scopes.length > 1;\n\t\tcase PagingMode.Project:\n\t\t\t// Only the descriptor form carries a project; the id form is refused by the Azure read before it runs.\n\t\t\treturn new Set(scopes.map(r => (typeof r === 'object' ? r.project : undefined))).size > 1;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/**\n * What {@link listIssuesPage} accepts, shared by name with the two functions it dispatches to.\n *\n * Named rather than left inline: three functions now take this exact shape, and reaching it from the other two as\n * `Parameters<typeof listIssuesPage>[1]` would define the contract in terms of the function that consumes it —\n * inside out, and unreadable at the point where the fields are actually used.\n */\nexport interface ListIssuesPageOptions {\n\tstate?: IssueStateFilter;\n\tproviderId: IntegrationIds;\n\trepos?: ProviderReposInput;\n\t/** Narrows the account-wide read to one org/account. Requires a host with a project layer (Azure). */\n\torg?: string;\n\t/** Narrows the account-wide read to one project. Requires a host with a project layer (Azure). */\n\tproject?: string;\n\t/**\n\t * Narrows to the requested relationship(s). On the account-wide path this replaces the provider's own\n\t * definition of \"my issues\" (GitHub/GHE: authored ∪ assigned ∪ mentioned; Azure: assigned ∪ authored;\n\t * GitLab: assigned-to-me), so `[Assignee]` yields `assignee:@me` everywhere it's expressible. A set the\n\t * provider can't express server-side is refused whole (warning + `fetchFailed`), never widened — check\n\t * getSupportedFilters first to avoid that path.\n\t */\n\tfilters?: IssueFilter[];\n\t/** Broadens the read to every assignee. Contradicts `filters`; passing both is refused. */\n\tincludeAllAssignees?: boolean;\n\t/**\n\t * How to order the page, as `field:direction`. Omitted orders most-recently-updated-first wherever the\n\t * provider can express that, which is this facade's default rather than the provider's own.\n\t *\n\t * Validated against `getSupportedFilters().issueSorts` on the repo-scoped path and `.issueSortsAccountWide`\n\t * on the account-wide one — two genuinely different vocabularies for GitLab, whose two reads are different\n\t * APIs. A key the provider can't express refuses the read (warning + `fetchFailed`) rather than serving a\n\t * differently-ordered list, for the same reason an inexpressible filter does: the reachable window is\n\t * bounded, so another order is another subset.\n\t *\n\t * On the repo-scoped path with SEVERAL scopes the page is a merge of one query per repository/project, so\n\t * only a key a normalized issue carries can be honored: `priority`, `dueDate` and `resolved` are refused\n\t * there even where the provider supports them on a single scope. Order is per page, and per scope across\n\t * pages — the merge orders what a page contains, not the sequence of pages.\n\t */\n\tsort?: IssueSorting;\n\tpage?: number;\n\tcursor?: string;\n\titemsPerPage?: number;\n\tforceSync?: boolean;\n\tconnectionId?: string;\n\t/**\n\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t */\n\tdomain?: string;\n}\n\n/**\n * What both halves of {@link listIssuesPage} need from its shared prologue.\n *\n * A bundle rather than six parameters because the two reads take exactly the same inputs and differ only in what\n * they do with them; threading them positionally would make the two call sites read as two different reads of two\n * different things, which is the opposite of what is true.\n *\n * Note `ctx` is absent: every use of it — resolving the integration, the domain, the forced refresh, the\n * connection warnings — is in the prologue, which is what makes this the right cut.\n */\ninterface IssueReadContext {\n\toptions: ListIssuesPageOptions;\n\tintegration: GitHostIntegration;\n\tdomain: string | undefined;\n\twarnOnMissingSession: boolean;\n\tpage: number;\n}\n\nexport async function listIssuesPage(\n\tctx: ProviderReadContext,\n\toptions: ListIssuesPageOptions,\n): Promise<ProviderPagedResult<IssueShape>> {\n\t// Truncated as well as floored, like every other paged read here: `page` is reported back as\n\t// `page.currentPage` and compared against the position a drain reached, and neither can be fractional.\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository issue reads')],\n\t\t\ttrue,\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\t// surface a no-connection warning + fetchFailed rather than a silent empty page.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(page, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository issue reads')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);\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 git host whose issue tracker is deprecated (Bitbucket, superseded by dedicated issue integrations)\n\t// reports issues as explicitly unsupported rather than serving a partial/legacy source or a silent empty.\n\tif (!integration.supportsIssues) {\n\t\treturn refusedPage(page, [issuesUnsupportedWarning(options.providerId, domain, options.connectionId)], true);\n\t}\n\n\tif (\n\t\toptions.state != null &&\n\t\t(!issueStateFilters.includes(options.state) ||\n\t\t\t(options.state !== 'open' && !providersMetadata[options.providerId]?.supportsIssueStates))\n\t) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tissueStateFilters.includes(options.state)\n\t\t\t\t\t? unsupportedWarning(\n\t\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\t\tdomain,\n\t\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t\t`Issue state '${options.state}' is not supported by '${options.providerId}'.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t: otherWarning(\n\t\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\t\tdomain,\n\t\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t\t`Unknown issue state; expected one of ${issueStateFilters.join(', ')}.`,\n\t\t\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\t// `repos` is what selects between the two: with none there is no scope to query per repository, so the\n\t// provider's own account-wide search answers instead. They are different provider queries with different\n\t// capability tables, cursors and completeness signals — see each function.\n\tconst context: IssueReadContext = {\n\t\toptions: options,\n\t\tintegration: integration,\n\t\tdomain: domain,\n\t\twarnOnMissingSession: warnOnMissingSession,\n\t\tpage: page,\n\t};\n\treturn (options.repos?.length ?? 0) === 0 ? readAccountWideIssuesPage(context) : readRepoScopedIssuesPage(context);\n}\n\n/**\n * The account-wide read: the provider's own definition of \"my issues\", behind a composite cursor that spans\n * several provider searches.\n *\n * Its own function rather than a branch of the paged read, because almost nothing is shared past the guards: it\n * validates a different filter table and a different sort table, it is cursor-only (a page number cannot address\n * a composite cursor), it accumulates per-scope failures across the pages it walks, and it always merges several\n * queries. The two used to share one 460-line function behind a mode flag, and the comments explaining how far\n * apart they are were the clue that they wanted separating.\n */\nasync function readAccountWideIssuesPage({\n\toptions,\n\tintegration,\n\tdomain,\n\twarnOnMissingSession,\n\tpage,\n}: IssueReadContext): Promise<ProviderPagedResult<IssueShape>> {\n\t// The account-wide read is cursor-only, so a refusal can't claim the requested position — it reports\n\t// page 1, per ProviderPageInfo.currentPage.\n\tconst refused = (warning: ProviderWarning) => refusedPage<IssueShape>(1, [warning], true);\n\n\t// Checked before the provider-specific guards below: a caller passing both has a contradictory request\n\t// whatever the provider, and saying so is more useful than reporting one half of it as unsupported.\n\t// `filters` narrows this read to a relationship (`[Assignee]` ⇒ just assigned-to-me); `includeAllAssignees`\n\t// broadens it to every assignee. Honoring either silently would answer a question the caller didn't ask.\n\tif (options.filters?.length && options.includeAllAssignees === true) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'`filters` and `includeAllAssignees` are contradictory for an account-wide issue read; pass only one.',\n\t\t\t),\n\t\t);\n\t}\n\n\t// GitHub expresses \"any assignee\" as `assignee:*`, which is meaningless without a scope: unscoped it\n\t// matches millions of issues across all of GitHub instead of the user's own world. ANY scope makes it\n\t// meaningful (one repository, several, or an org) — it is specifically THIS branch, the account-wide read,\n\t// that has none to offer. Scope the read to repositories (or use `searchIssuesPage`, whose criteria model\n\t// takes `any-assignee` alongside a `repos`/`org` scope) rather than reading unscoped.\n\tif (\n\t\toptions.includeAllAssignees === true &&\n\t\t(options.providerId === GitCloudHostIntegrationId.GitHub ||\n\t\t\toptions.providerId === GitSelfManagedHostIntegrationId.CloudGitHubEnterprise)\n\t) {\n\t\treturn refused(\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'`includeAllAssignees` is not supported for account-wide GitHub issue reads; scope the read to repositories instead.',\n\t\t\t),\n\t\t);\n\t}\n\n\t// Only a host with a project layer can narrow server-side. Reject the request rather than serving an\n\t// unscoped list as if it had been narrowed: the caller would otherwise have to filter client-side,\n\t// which desynchronizes the filtered `items` from this read's `hasMore`/`currentPage` and shows\n\t// \"no issues\" for a page that simply held none of the requested project's.\n\tif ((options.org != null || options.project != null) && !integration.supportsProjectDiscovery) {\n\t\treturn refused(\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Project-scoped issue reads are not supported by '${options.providerId}'; scope the read to repositories instead.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// Narrowing the account-wide read is only honest when the provider can express it server-side: its\n\t// per-relationship queries produced the page and the cursor together, so dropping items afterward would\n\t// leave `items` describing a different result set than `hasMore`/`currentPage`.\n\tconst resolvedIssueFilters = resolveAccountWideIssueFilters(options.providerId, options.filters);\n\tif (resolvedIssueFilters.unsupported) {\n\t\treturn refused(\n\t\t\tunsupportedAccountWideIssueFiltersWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\toptions.filters!,\n\t\t\t),\n\t\t);\n\t}\n\n\t// Every provider's account-wide read is a UNION of several queries — GitHub's three `@me` searches,\n\t// GitLab's one REST call per relationship, Azure's (project x relationship) drains — so it always merges,\n\t// and `supportedAccountWideIssueSorts` already lists only keys a merge can honor. No separate\n\t// mergeability refusal is needed here, unlike on the repo-scoped path below.\n\tconst resolvedSort = resolveIssueSort(\n\t\tprovidersMetadata[options.providerId]?.supportedAccountWideIssueSorts,\n\t\toptions.sort,\n\t);\n\tif (resolvedSort.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSortWarning(options.providerId, domain, options.connectionId, resolvedSort.rejection),\n\t\t);\n\t}\n\n\t// `merged: true` unconditionally: this read has no scope count for a caller to reduce. It is also why no\n\t// `unmergeable` check follows — `supportedAccountWideIssueSorts` declares only keys a merge can honor.\n\tconst accountWideOrdering = toIssueOrdering(resolvedSort.sort, true);\n\n\t// The repo-scoped core rejects empty repos (GitHub/Bitbucket/Azure); read the account-wide,\n\t// already-user-scoped core instead. GitHub exposes a composite cursor across its authored,\n\t// assigned, and mentioned searches. Walk it internally when the caller supplies only page N.\n\tconst readAccountWidePage = (cursor: string | undefined) =>\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.searchMyIssuesWithTruncationResult(undefined, undefined, options.connectionId, {\n\t\t\t\t\tincludeAllAssignees: options.includeAllAssignees,\n\t\t\t\t\tstate: options.state,\n\t\t\t\t\tfilters: resolvedIssueFilters.filters,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tpageSize: options.itemsPerPage,\n\t\t\t\t\torg: options.org,\n\t\t\t\t\tproject: options.project,\n\t\t\t\t\tsort: accountWideOrdering.sort,\n\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\tconst first = await readAccountWidePage(options.cursor);\n\tconst warnings = first.warning != null ? [first.warning] : [];\n\t// Merged across the walked pages, so a per-scope failure reported on an earlier page isn't lost by\n\t// paging past it — the one thing this read accumulates that the filtered search doesn't.\n\tlet allMetadata = first.value?.metadata;\n\tconst drained = await drainFlatPagesToRequestedPage(first, {\n\t\trequestedPage: page,\n\t\tsuppliedCursor: options.cursor,\n\t\twarnings: warnings,\n\t\treadPage: readAccountWidePage,\n\t\tfold: p => {\n\t\t\tallMetadata = mergeCollectionMetadata(allMetadata, p.metadata, 'disjoint');\n\t\t},\n\t});\n\tconst { value, currentPage, requestedPageMissing } = drained;\n\tconst currentTruncated = drained.truncated;\n\tconst pageFetchFailed = drained.fetchFailed;\n\n\t// GitHub, GitLab, and Azure implement an account-wide issue search; a provider that doesn't (Bitbucket\n\t// exposes no issues at all, and `supportsIssues` already short-circuits it above) returns `undefined`\n\t// with no error. Surface that as an explicit unsupported warning + fetchFailed rather than a silent\n\t// empty success — the caller must fall back (e.g. broadenIssues over repos).\n\tif (value == null && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Account-wide issue search is not supported by '${options.providerId}'; scope the read to repositories instead.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst items = requestedPageMissing ? [] : (value?.values ?? []);\n\t// Each query arrived ordered by the provider; their union is not, and the union is what this read publishes.\n\t// Idempotent for GitHub, whose aliased-search engine already ordered the same merge — one call here rather\n\t// than the same logic repeated in each of the three provider implementations.\n\tconst orderedItems = accountWideOrdering.order(items);\n\t// Fold in structured per-scope failures from the account-wide fan-out (e.g. Azure across projects):\n\t// scope-aware warnings + `fetchFailed` when a scope failed, without discarding the successful items.\n\tconst assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, allMetadata);\n\t// An account-wide search that couldn't confirm completeness (a provider cap with no cursor, or a\n\t// per-scope backstop/failure) is incomplete and can't be paged; report it as truncated (+ a\n\t// provider-neutral warning, unless a structured failure already explains it) rather than a complete\n\t// list. Don't hard-code GitHub's \"100 per category\" cap here — Azure reaches this via a per-project\n\t// backstop, and other providers may cap differently.\n\t// The account-wide read is cursor-only (its composite cursor spans several provider searches, so a\n\t// page number can't address it): `hasMore` without a real cursor is a dead end, so report it as\n\t// terminal-but-incomplete rather than inviting the caller to page forever.\n\tconst continuation = resolveContinuation(\n\t\t{\n\t\t\thasMore: requestedPageMissing ? false : (value?.hasMore ?? false),\n\t\t\tcursor: requestedPageMissing ? undefined : usableCursor(value?.cursor),\n\t\t\ttruncated: currentTruncated,\n\t\t},\n\t\tundefined,\n\t);\n\tconst truncated = continuation.truncated || assessment.truncated;\n\tif (truncated && warnings.length === 0) {\n\t\twarnings.push(\n\t\t\ttruncationWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'Account-wide issue search',\n\t\t\t\t// `exhausted`: this composite read exposes no budget the caller can raise, so nothing it\n\t\t\t\t// could call would return the withheld items.\n\t\t\t\tassessment.fetchFailed || pageFetchFailed ? 'interrupted' : 'exhausted',\n\t\t\t),\n\t\t);\n\t}\n\t// A metadata omission from an earlier page asserts the read succeeded; a later page may since have failed.\n\treconcileOmissionsWithFailure(warnings, assessment.fetchFailed || pageFetchFailed);\n\treturn {\n\t\titems: orderedItems,\n\t\twarnings: warnings,\n\t\tpage: {\n\t\t\t// Positional, per ProviderPageInfo.currentPage. `currentPage` already carries what the provider\n\t\t\t// reported or what the internal drain counted; a requested page past the terminal cursor is\n\t\t\t// reported as that empty page N. The account-wide read is cursor-only, so a `page` the caller\n\t\t\t// didn't pair with a cursor is never echoed.\n\t\t\tcurrentPage: requestedPageMissing\n\t\t\t\t? page\n\t\t\t\t: resolveCurrentPage({\n\t\t\t\t\t\tproviderPage: currentPage,\n\t\t\t\t\t\trequestedPage: page,\n\t\t\t\t\t\tsuppliedCursor: options.cursor,\n\t\t\t\t\t\tpageAdvanceable: false,\n\t\t\t\t\t}),\n\t\t\titemsPerPage: orderedItems.length,\n\t\t\ttruncated: truncated || undefined,\n\t\t},\n\t\thasMore: continuation.hasMore,\n\t\tcursor: continuation.cursor,\n\t\tfetchFailed: assessment.fetchFailed || pageFetchFailed || undefined,\n\t\trequestFailed: assessment.requestFailed || pageFetchFailed,\n\t\tunexpectedGap: drained.unexpectedGap,\n\t};\n}\n\n/**\n * The repository-scoped read: one provider query per repository or project, paged by the provider's own\n * continuation and drained internally when the caller supplied only a page number.\n *\n * See {@link readAccountWideIssuesPage} for why the two are separate functions.\n */\nasync function readRepoScopedIssuesPage({\n\toptions,\n\tintegration,\n\tdomain,\n\twarnOnMissingSession,\n\tpage,\n}: IssueReadContext): Promise<ProviderPagedResult<IssueShape>> {\n\tconst metadata = providersMetadata[options.providerId];\n\n\t// How many separate provider queries this page is assembled from, which decides whether the requested order can\n\t// be honored at all. `PagingMode.Repos` (GitHub) sends ONE search however many repositories are named, so its\n\t// page arrives ordered by the provider; `Repo` (GitLab) and `Project` (Azure) issue one query per scope and are\n\t// concatenated here, so a page spanning several of them is only as orderable as a normalized issue is.\n\tconst resolvedRepoScopedSort = resolveIssueSort(metadata?.supportedIssueSorts, options.sort);\n\tif (resolvedRepoScopedSort.rejection != null) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tunsupportedIssueSortWarning(\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\tresolvedRepoScopedSort.rejection,\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst issuesPagingMode = metadata?.issuesPagingMode;\n\t// Counted, and read, as the host settles them: a scope can name another project than the one it was parsed with\n\t// (see `GitHostIntegration.resolveRepositoryScopes`), which changes how many queries a per-project read makes. The\n\t// read is handed the settled scopes, flags included, so it reads what was counted rather than settling again.\n\tconst scopes = options.repos ?? [];\n\tlet repos: ProviderReposInput | SettledRepositoryScopes = scopes;\n\tlet counted = scopes;\n\tif (issuesPagingMode === PagingMode.Project && scopes.length) {\n\t\tconst settled = await runCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() => integration.resolveRepositoryScopes(scopes, options.connectionId),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\t\t// No session to read with, or the read's own failure resolving it: answered as that read would, without it.\n\t\tif (settled.value == null) {\n\t\t\treturn refusedPage(page, settled.warning != null ? [settled.warning] : [], settled.warning != null);\n\t\t}\n\n\t\trepos = settled.value;\n\t\tcounted = settled.value.repos;\n\t}\n\tconst ordering = toIssueOrdering(resolvedRepoScopedSort.sort, mergesProviderQueries(issuesPagingMode, counted));\n\t// Refused rather than served as concatenated per-scope runs, which would look ordered without being so — and\n\t// only where it actually merges, since the same key against the same provider is perfectly answerable for a\n\t// single repository or project.\n\tif (ordering.unmergeable != null) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tunmergeableIssueSortWarning(\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\tordering.unmergeable,\n\t\t\t\t\tissuesPagingMode === PagingMode.Project ? 'projects' : 'repositories',\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst cursor = options.cursor ?? pageToCursor(page);\n\tconst { value, warning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() =>\n\t\t\t// The shapes seam returns normalized IssueShape, and is overridable by a provider whose only issue\n\t\t\t// client already yields shapes (serving this path without a raw ProviderIssue round-trip).\n\t\t\tintegration.getMyIssuesForReposAsShapesResult(\n\t\t\t\trepos,\n\t\t\t\t// Forward `page`/`pageSize` alongside the cursor so PagingMode.Repo/Project hosts honor the\n\t\t\t\t// requested page and page size rather than ignoring a synthesized page-number cursor.\n\t\t\t\t{\n\t\t\t\t\tfilters: options.filters,\n\t\t\t\t\tincludeAllAssignees: options.includeAllAssignees,\n\t\t\t\t\tstate: options.state,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t// The normalized `page`, so the number forwarded to the provider can't disagree with the\n\t\t\t\t\t// page-number cursor beside it — both are built from the same value. Only when the caller asked\n\t\t\t\t\t// for one: an omitted page is the provider's own first page.\n\t\t\t\t\tpage: options.page != null ? page : undefined,\n\t\t\t\t\tpageSize: options.itemsPerPage,\n\t\t\t\t\tsort: ordering.sort,\n\t\t\t\t},\n\t\t\t\toptions.connectionId,\n\t\t\t),\n\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t);\n\n\tlet items = value?.values ?? [];\n\tconst warnings = warning != null ? [warning] : [];\n\tlet pageFetchFailed = warning != null && value == null;\n\tlet paged = toProviderPageInfo(options.itemsPerPage ?? items.length, value?.paging);\n\tlet allMetadata = value?.metadata;\n\tlet drainedGap = false;\n\n\t// Cursor-only repo-scoped hosts (e.g. GitHub) ignore a synthesized page-number cursor, so a page-only\n\t// request is advanced through the provider's own continuations (see drainToRequestedPage).\n\tif (\n\t\tissuesPagingMode === PagingMode.Repos &&\n\t\toptions.page != null &&\n\t\tpage > 1 &&\n\t\toptions.cursor == null &&\n\t\tpaged.page.currentPage === 1\n\t) {\n\t\tconst drained = await drainToRequestedPage(\n\t\t\t{ items: items, paged: paged, metadata: allMetadata, fetchFailed: pageFetchFailed },\n\t\t\t{\n\t\t\t\trequestedPage: page,\n\t\t\t\titemsPerPage: options.itemsPerPage,\n\t\t\t\twarnings: warnings,\n\t\t\t\treadPage: (cursor: string) =>\n\t\t\t\t\trunCaptured(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t() =>\n\t\t\t\t\t\t\tintegration.getMyIssuesForReposAsShapesResult(\n\t\t\t\t\t\t\t\trepos,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tfilters: options.filters,\n\t\t\t\t\t\t\t\t\tincludeAllAssignees: options.includeAllAssignees,\n\t\t\t\t\t\t\t\t\tstate: options.state,\n\t\t\t\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\t\t\t\tpageSize: options.itemsPerPage,\n\t\t\t\t\t\t\t\t\tsort: ordering.sort,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t\t\t),\n\t\t\t},\n\t\t);\n\t\titems = drained.items;\n\t\tpaged = drained.paged;\n\t\tallMetadata = drained.metadata;\n\t\tpageFetchFailed = drained.fetchFailed;\n\t\tdrainedGap = drained.unexpectedGap === true;\n\t}\n\n\t// Ordered once the drain (above) has assembled whichever pages this position spans.\n\titems = ordering.order(items);\n\n\t// Convert the SDK collection metadata into scope-aware warnings + failure/truncation flags, appending\n\t// them to any captured thrown-error warning without discarding the partial result's items.\n\tconst assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, allMetadata);\n\t// Never advertise `hasMore` without a continuation the caller can act on, and only synthesize a page\n\t// number for a host that reads it as one (see `isPageNumberAdvanceable`).\n\tconst issuesPageAdvanceable = isPageNumberAdvanceable(issuesPagingMode);\n\tconst continuation = resolveContinuation(paged, issuesPageAdvanceable ? paged.page.currentPage + 1 : undefined);\n\t// A provider read that couldn't confirm completeness (e.g. Bitbucket's single-page repo issue read\n\t// that dropped a repo) sets `paging.truncated`; surface it as a terminal `page.truncated` so a partial\n\t// page isn't published as complete. Metadata incompleteness is an independent source of the same signal.\n\tconst truncated = continuation.truncated || assessment.truncated;\n\tif (truncated && warnings.length === 0) {\n\t\twarnings.push(\n\t\t\ttruncationWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'Issue',\n\t\t\t\t// `exhausted`, never `page-budget`: a paged read has no budget the caller can raise, and\n\t\t\t\t// ordinary continuation is already expressed by `hasMore`/`cursor`.\n\t\t\t\tassessment.fetchFailed || pageFetchFailed ? 'interrupted' : 'exhausted',\n\t\t\t),\n\t\t);\n\t}\n\t// A metadata omission from an earlier page asserts the read succeeded; a later page may since have failed.\n\treconcileOmissionsWithFailure(warnings, assessment.fetchFailed || pageFetchFailed);\n\treturn {\n\t\titems: items,\n\t\twarnings: warnings,\n\t\tpage: {\n\t\t\t...paged.page,\n\t\t\t// Positional, per ProviderPageInfo.currentPage: the drain (above) already advanced `paged.page` for\n\t\t\t// a cursor-only host, and a caller-threaded cursor advanced the provider without one.\n\t\t\tcurrentPage: resolveCurrentPage({\n\t\t\t\tproviderPage: paged.page.currentPage,\n\t\t\t\trequestedPage: page,\n\t\t\t\tsuppliedCursor: options.cursor,\n\t\t\t\tpageAdvanceable: issuesPageAdvanceable,\n\t\t\t}),\n\t\t\ttruncated: truncated || undefined,\n\t\t},\n\t\thasMore: continuation.hasMore,\n\t\tcursor: continuation.cursor,\n\t\tfetchFailed: assessment.fetchFailed || pageFetchFailed || undefined,\n\t\trequestFailed: assessment.requestFailed || pageFetchFailed,\n\t\tunexpectedGap:\n\t\t\tdrainedGap || (paged.hasMore === true && usableCursor(paged.cursor) == null && !issuesPageAdvanceable),\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"issues.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issues.ts"],"sourcesContent":["import type { IssueShape, IssueSorting, IssueStateFilter } from '@gitlens/git/models/issue.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../constants.js';\nimport type { GitHostIntegration, SettledRepositoryScopes } from '../models/gitHostIntegration.js';\nimport type { IssueFilter, ProviderReposInput } from '../providers/models.js';\nimport { isRepoIdsInput, PagingMode, providersMetadata } from '../providers/models.js';\nimport { mergeCollectionMetadata } from '../providers/utils/providerPaging.js';\nimport type { ProviderPagedResult, ProviderWarning } from '../results.js';\nimport { reconcileOmissionsWithFailure } 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 { resolveAccountWideIssueFilters } from './filters.js';\nimport { resolveIssueSort, toIssueOrdering } from './ordering.js';\nimport {\n\tdrainFlatPagesToRequestedPage,\n\tdrainToRequestedPage,\n\tisPageNumberAdvanceable,\n\tpageToCursor,\n\trefusedPage,\n\tresolveContinuation,\n\tresolveCurrentPage,\n\ttoProviderPageInfo,\n\tusableCursor,\n} from './paging.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissuesUnsupportedWarning,\n\totherWarning,\n\ttruncationWarning,\n\tunmergeableIssueSortWarning,\n\tunsupportedAccountWideIssueFiltersWarning,\n\tunsupportedIssueSortWarning,\n\tunsupportedWarning,\n} from './warnings.js';\n\nconst issueStateFilters: readonly IssueStateFilter[] = ['open', 'closed', 'all'];\n\n/**\n * Whether a repo-scoped page is assembled from SEVERAL provider queries and merged here — which is what decides\n * whether the requested order can be honored at all.\n *\n * `PagingMode.Repos` (GitHub) sends one search however many repositories are named, so its page arrives ordered by\n * the provider. `Repo` (GitLab) issues one query per repository, and `Project` (Azure) one per PROJECT — several\n * repositories of the same project are still one query, which is why that case counts distinct projects rather than\n * repositories.\n *\n * The `Repo` case has a second way to merge that has nothing to do with the count: given repository IDS rather than\n * descriptors, `getMyIssuesForReposResult` skips its per-repository fan-out entirely (that branch is guarded on\n * `!isRepoIdsInput`) and calls the SDK's `getIssuesForRepos`, which for GitLab is the multi-project aggregate that\n * merges in the SDK and refuses `priority`/`dueDate` however few scopes it was given. Treating that as merged is\n * what makes this facade refuse it up front, with the message that names the reason, instead of letting the SDK\n * reject a read the capability table had just promised.\n */\nfunction mergesProviderQueries(pagingMode: PagingMode | undefined, repos: ProviderReposInput | undefined): boolean {\n\tconst scopes = repos ?? [];\n\tswitch (pagingMode) {\n\t\tcase PagingMode.Repo:\n\t\t\t// `isRepoIdsInput` is the same predicate `ProvidersApi` selects the SDK aggregate with, so this cannot\n\t\t\t// disagree with the branch it is predicting.\n\t\t\treturn isRepoIdsInput(scopes) ? scopes.length > 0 : scopes.length > 1;\n\t\tcase PagingMode.Project:\n\t\t\t// Only the descriptor form carries a project; the id form is refused by the Azure read before it runs.\n\t\t\treturn new Set(scopes.map(r => (typeof r === 'object' ? r.project : undefined))).size > 1;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/**\n * What {@link listIssuesPage} accepts, shared by name with the two functions it dispatches to.\n *\n * Named rather than left inline: three functions now take this exact shape, and reaching it from the other two as\n * `Parameters<typeof listIssuesPage>[1]` would define the contract in terms of the function that consumes it —\n * inside out, and unreadable at the point where the fields are actually used.\n */\nexport interface ListIssuesPageOptions {\n\tstate?: IssueStateFilter;\n\tproviderId: IntegrationIds;\n\trepos?: ProviderReposInput;\n\t/** Narrows the account-wide read to one org/account. Requires a host with a project layer (Azure). */\n\torg?: string;\n\t/** Narrows the account-wide read to one project. Requires a host with a project layer (Azure). */\n\tproject?: string;\n\t/**\n\t * Narrows to the requested relationship(s). On the account-wide path this replaces the provider's own\n\t * definition of \"my issues\" (GitHub/GHE: authored ∪ assigned ∪ mentioned; Azure: assigned ∪ authored;\n\t * GitLab: assigned-to-me), so `[Assignee]` yields `assignee:@me` everywhere it's expressible. A set the\n\t * provider can't express server-side is refused whole (warning + `fetchFailed`), never widened — check\n\t * getSupportedFilters first to avoid that path.\n\t */\n\tfilters?: IssueFilter[];\n\t/** Broadens the read to every assignee. Contradicts `filters`; passing both is refused. */\n\tincludeAllAssignees?: boolean;\n\t/**\n\t * How to order the page, as `field:direction`. Omitted orders most-recently-updated-first wherever the\n\t * provider can express that, which is this facade's default rather than the provider's own.\n\t *\n\t * Validated against `getSupportedFilters().issueSorts` on the repo-scoped path and `.issueSortsAccountWide`\n\t * on the account-wide one — two genuinely different vocabularies for GitLab, whose two reads are different\n\t * APIs. A key the provider can't express refuses the read (warning + `fetchFailed`) rather than serving a\n\t * differently-ordered list, for the same reason an inexpressible filter does: the reachable window is\n\t * bounded, so another order is another subset.\n\t *\n\t * On the repo-scoped path with SEVERAL scopes the page is a merge of one query per repository/project, so\n\t * only a key a normalized issue carries can be honored: `priority`, `dueDate` and `resolved` are refused\n\t * there even where the provider supports them on a single scope. Order is per page, and per scope across\n\t * pages — the merge orders what a page contains, not the sequence of pages.\n\t */\n\tsort?: IssueSorting;\n\tpage?: number;\n\tcursor?: string;\n\titemsPerPage?: number;\n\t/** See {@link IntegrationManager.listIssuesPage}. */\n\tscopeConcurrency?: number;\n\tforceSync?: boolean;\n\tconnectionId?: string;\n\t/**\n\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t */\n\tdomain?: string;\n}\n\n/**\n * What both halves of {@link listIssuesPage} need from its shared prologue.\n *\n * A bundle rather than six parameters because the two reads take exactly the same inputs and differ only in what\n * they do with them; threading them positionally would make the two call sites read as two different reads of two\n * different things, which is the opposite of what is true.\n *\n * Note `ctx` is absent: every use of it — resolving the integration, the domain, the forced refresh, the\n * connection warnings — is in the prologue, which is what makes this the right cut.\n */\ninterface IssueReadContext {\n\toptions: ListIssuesPageOptions;\n\tintegration: GitHostIntegration;\n\tdomain: string | undefined;\n\twarnOnMissingSession: boolean;\n\tpage: number;\n}\n\nexport async function listIssuesPage(\n\tctx: ProviderReadContext,\n\toptions: ListIssuesPageOptions,\n): Promise<ProviderPagedResult<IssueShape>> {\n\t// Truncated as well as floored, like every other paged read here: `page` is reported back as\n\t// `page.currentPage` and compared against the position a drain reached, and neither can be fractional.\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository issue reads')],\n\t\t\ttrue,\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\t// surface a no-connection warning + fetchFailed rather than a silent empty page.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(page, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository issue reads')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);\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 git host whose issue tracker is deprecated (Bitbucket, superseded by dedicated issue integrations)\n\t// reports issues as explicitly unsupported rather than serving a partial/legacy source or a silent empty.\n\tif (!integration.supportsIssues) {\n\t\treturn refusedPage(page, [issuesUnsupportedWarning(options.providerId, domain, options.connectionId)], true);\n\t}\n\n\tif (\n\t\toptions.state != null &&\n\t\t(!issueStateFilters.includes(options.state) ||\n\t\t\t(options.state !== 'open' && !providersMetadata[options.providerId]?.supportsIssueStates))\n\t) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tissueStateFilters.includes(options.state)\n\t\t\t\t\t? unsupportedWarning(\n\t\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\t\tdomain,\n\t\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t\t`Issue state '${options.state}' is not supported by '${options.providerId}'.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t: otherWarning(\n\t\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\t\tdomain,\n\t\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t\t`Unknown issue state; expected one of ${issueStateFilters.join(', ')}.`,\n\t\t\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\t// `repos` is what selects between the two: with none there is no scope to query per repository, so the\n\t// provider's own account-wide search answers instead. They are different provider queries with different\n\t// capability tables, cursors and completeness signals — see each function.\n\tconst context: IssueReadContext = {\n\t\toptions: options,\n\t\tintegration: integration,\n\t\tdomain: domain,\n\t\twarnOnMissingSession: warnOnMissingSession,\n\t\tpage: page,\n\t};\n\treturn (options.repos?.length ?? 0) === 0 ? readAccountWideIssuesPage(context) : readRepoScopedIssuesPage(context);\n}\n\n/**\n * The account-wide read: the provider's own definition of \"my issues\", behind a composite cursor that spans\n * several provider searches.\n *\n * Its own function rather than a branch of the paged read, because almost nothing is shared past the guards: it\n * validates a different filter table and a different sort table, it is cursor-only (a page number cannot address\n * a composite cursor), it accumulates per-scope failures across the pages it walks, and it always merges several\n * queries. The two used to share one 460-line function behind a mode flag, and the comments explaining how far\n * apart they are were the clue that they wanted separating.\n */\nasync function readAccountWideIssuesPage({\n\toptions,\n\tintegration,\n\tdomain,\n\twarnOnMissingSession,\n\tpage,\n}: IssueReadContext): Promise<ProviderPagedResult<IssueShape>> {\n\t// The account-wide read is cursor-only, so a refusal can't claim the requested position — it reports\n\t// page 1, per ProviderPageInfo.currentPage.\n\tconst refused = (warning: ProviderWarning) => refusedPage<IssueShape>(1, [warning], true);\n\n\t// Checked before the provider-specific guards below: a caller passing both has a contradictory request\n\t// whatever the provider, and saying so is more useful than reporting one half of it as unsupported.\n\t// `filters` narrows this read to a relationship (`[Assignee]` ⇒ just assigned-to-me); `includeAllAssignees`\n\t// broadens it to every assignee. Honoring either silently would answer a question the caller didn't ask.\n\tif (options.filters?.length && options.includeAllAssignees === true) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'`filters` and `includeAllAssignees` are contradictory for an account-wide issue read; pass only one.',\n\t\t\t),\n\t\t);\n\t}\n\n\t// GitHub expresses \"any assignee\" as `assignee:*`, which is meaningless without a scope: unscoped it\n\t// matches millions of issues across all of GitHub instead of the user's own world. ANY scope makes it\n\t// meaningful (one repository, several, or an org) — it is specifically THIS branch, the account-wide read,\n\t// that has none to offer. Scope the read to repositories (or use `searchIssuesPage`, whose criteria model\n\t// takes `any-assignee` alongside a `repos`/`org` scope) rather than reading unscoped.\n\tif (\n\t\toptions.includeAllAssignees === true &&\n\t\t(options.providerId === GitCloudHostIntegrationId.GitHub ||\n\t\t\toptions.providerId === GitSelfManagedHostIntegrationId.CloudGitHubEnterprise)\n\t) {\n\t\treturn refused(\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'`includeAllAssignees` is not supported for account-wide GitHub issue reads; scope the read to repositories instead.',\n\t\t\t),\n\t\t);\n\t}\n\n\t// Only a host with a project layer can narrow server-side. Reject the request rather than serving an\n\t// unscoped list as if it had been narrowed: the caller would otherwise have to filter client-side,\n\t// which desynchronizes the filtered `items` from this read's `hasMore`/`currentPage` and shows\n\t// \"no issues\" for a page that simply held none of the requested project's.\n\tif ((options.org != null || options.project != null) && !integration.supportsProjectDiscovery) {\n\t\treturn refused(\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Project-scoped issue reads are not supported by '${options.providerId}'; scope the read to repositories instead.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// Narrowing the account-wide read is only honest when the provider can express it server-side: its\n\t// per-relationship queries produced the page and the cursor together, so dropping items afterward would\n\t// leave `items` describing a different result set than `hasMore`/`currentPage`.\n\tconst resolvedIssueFilters = resolveAccountWideIssueFilters(options.providerId, options.filters);\n\tif (resolvedIssueFilters.unsupported) {\n\t\treturn refused(\n\t\t\tunsupportedAccountWideIssueFiltersWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\toptions.filters!,\n\t\t\t),\n\t\t);\n\t}\n\n\t// Every provider's account-wide read is a UNION of several queries — GitHub's three `@me` searches,\n\t// GitLab's one REST call per relationship, Azure's (project x relationship) drains — so it always merges,\n\t// and `supportedAccountWideIssueSorts` already lists only keys a merge can honor. No separate\n\t// mergeability refusal is needed here, unlike on the repo-scoped path below.\n\tconst resolvedSort = resolveIssueSort(\n\t\tprovidersMetadata[options.providerId]?.supportedAccountWideIssueSorts,\n\t\toptions.sort,\n\t);\n\tif (resolvedSort.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSortWarning(options.providerId, domain, options.connectionId, resolvedSort.rejection),\n\t\t);\n\t}\n\n\t// `merged: true` unconditionally: this read has no scope count for a caller to reduce. It is also why no\n\t// `unmergeable` check follows — `supportedAccountWideIssueSorts` declares only keys a merge can honor.\n\tconst accountWideOrdering = toIssueOrdering(resolvedSort.sort, true);\n\n\t// The repo-scoped core rejects empty repos (GitHub/Bitbucket/Azure); read the account-wide,\n\t// already-user-scoped core instead. GitHub exposes a composite cursor across its authored,\n\t// assigned, and mentioned searches. Walk it internally when the caller supplies only page N.\n\tconst readAccountWidePage = (cursor: string | undefined) =>\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.searchMyIssuesWithTruncationResult(undefined, undefined, options.connectionId, {\n\t\t\t\t\tincludeAllAssignees: options.includeAllAssignees,\n\t\t\t\t\tstate: options.state,\n\t\t\t\t\tfilters: resolvedIssueFilters.filters,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tpageSize: options.itemsPerPage,\n\t\t\t\t\torg: options.org,\n\t\t\t\t\tproject: options.project,\n\t\t\t\t\tsort: accountWideOrdering.sort,\n\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\tconst first = await readAccountWidePage(options.cursor);\n\tconst warnings = first.warning != null ? [first.warning] : [];\n\t// Merged across the walked pages, so a per-scope failure reported on an earlier page isn't lost by\n\t// paging past it — the one thing this read accumulates that the filtered search doesn't.\n\tlet allMetadata = first.value?.metadata;\n\tconst drained = await drainFlatPagesToRequestedPage(first, {\n\t\trequestedPage: page,\n\t\tsuppliedCursor: options.cursor,\n\t\twarnings: warnings,\n\t\treadPage: readAccountWidePage,\n\t\tfold: p => {\n\t\t\tallMetadata = mergeCollectionMetadata(allMetadata, p.metadata, 'disjoint');\n\t\t},\n\t});\n\tconst { value, currentPage, requestedPageMissing } = drained;\n\tconst currentTruncated = drained.truncated;\n\tconst pageFetchFailed = drained.fetchFailed;\n\n\t// GitHub, GitLab, and Azure implement an account-wide issue search; a provider that doesn't (Bitbucket\n\t// exposes no issues at all, and `supportsIssues` already short-circuits it above) returns `undefined`\n\t// with no error. Surface that as an explicit unsupported warning + fetchFailed rather than a silent\n\t// empty success — the caller must fall back (e.g. broadenIssues over repos).\n\tif (value == null && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Account-wide issue search is not supported by '${options.providerId}'; scope the read to repositories instead.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst items = requestedPageMissing ? [] : (value?.values ?? []);\n\t// Each query arrived ordered by the provider; their union is not, and the union is what this read publishes.\n\t// Idempotent for GitHub, whose aliased-search engine already ordered the same merge — one call here rather\n\t// than the same logic repeated in each of the three provider implementations.\n\tconst orderedItems = accountWideOrdering.order(items);\n\t// Fold in structured per-scope failures from the account-wide fan-out (e.g. Azure across projects):\n\t// scope-aware warnings + `fetchFailed` when a scope failed, without discarding the successful items.\n\tconst assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, allMetadata);\n\t// An account-wide search that couldn't confirm completeness (a provider cap with no cursor, or a\n\t// per-scope backstop/failure) is incomplete and can't be paged; report it as truncated (+ a\n\t// provider-neutral warning, unless a structured failure already explains it) rather than a complete\n\t// list. Don't hard-code GitHub's \"100 per category\" cap here — Azure reaches this via a per-project\n\t// backstop, and other providers may cap differently.\n\t// The account-wide read is cursor-only (its composite cursor spans several provider searches, so a\n\t// page number can't address it): `hasMore` without a real cursor is a dead end, so report it as\n\t// terminal-but-incomplete rather than inviting the caller to page forever.\n\tconst continuation = resolveContinuation(\n\t\t{\n\t\t\thasMore: requestedPageMissing ? false : (value?.hasMore ?? false),\n\t\t\tcursor: requestedPageMissing ? undefined : usableCursor(value?.cursor),\n\t\t\ttruncated: currentTruncated,\n\t\t},\n\t\tundefined,\n\t);\n\tconst truncated = continuation.truncated || assessment.truncated;\n\tif (truncated && warnings.length === 0) {\n\t\twarnings.push(\n\t\t\ttruncationWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'Account-wide issue search',\n\t\t\t\t// `exhausted`: this composite read exposes no budget the caller can raise, so nothing it\n\t\t\t\t// could call would return the withheld items.\n\t\t\t\tassessment.fetchFailed || pageFetchFailed ? 'interrupted' : 'exhausted',\n\t\t\t),\n\t\t);\n\t}\n\t// A metadata omission from an earlier page asserts the read succeeded; a later page may since have failed.\n\treconcileOmissionsWithFailure(warnings, assessment.fetchFailed || pageFetchFailed);\n\treturn {\n\t\titems: orderedItems,\n\t\twarnings: warnings,\n\t\tpage: {\n\t\t\t// Positional, per ProviderPageInfo.currentPage. `currentPage` already carries what the provider\n\t\t\t// reported or what the internal drain counted; a requested page past the terminal cursor is\n\t\t\t// reported as that empty page N. The account-wide read is cursor-only, so a `page` the caller\n\t\t\t// didn't pair with a cursor is never echoed.\n\t\t\tcurrentPage: requestedPageMissing\n\t\t\t\t? page\n\t\t\t\t: resolveCurrentPage({\n\t\t\t\t\t\tproviderPage: currentPage,\n\t\t\t\t\t\trequestedPage: page,\n\t\t\t\t\t\tsuppliedCursor: options.cursor,\n\t\t\t\t\t\tpageAdvanceable: false,\n\t\t\t\t\t}),\n\t\t\titemsPerPage: orderedItems.length,\n\t\t\ttruncated: truncated || undefined,\n\t\t},\n\t\thasMore: continuation.hasMore,\n\t\tcursor: continuation.cursor,\n\t\tfetchFailed: assessment.fetchFailed || pageFetchFailed || undefined,\n\t\trequestFailed: assessment.requestFailed || pageFetchFailed,\n\t\tunexpectedGap: drained.unexpectedGap,\n\t};\n}\n\n/**\n * The repository-scoped read: one provider query per repository or project, paged by the provider's own\n * continuation and drained internally when the caller supplied only a page number.\n *\n * See {@link readAccountWideIssuesPage} for why the two are separate functions.\n */\nasync function readRepoScopedIssuesPage({\n\toptions,\n\tintegration,\n\tdomain,\n\twarnOnMissingSession,\n\tpage,\n}: IssueReadContext): Promise<ProviderPagedResult<IssueShape>> {\n\tconst metadata = providersMetadata[options.providerId];\n\n\t// How many separate provider queries this page is assembled from, which decides whether the requested order can\n\t// be honored at all. `PagingMode.Repos` (GitHub) sends ONE search however many repositories are named, so its\n\t// page arrives ordered by the provider; `Repo` (GitLab) and `Project` (Azure) issue one query per scope and are\n\t// concatenated here, so a page spanning several of them is only as orderable as a normalized issue is.\n\tconst resolvedRepoScopedSort = resolveIssueSort(metadata?.supportedIssueSorts, options.sort);\n\tif (resolvedRepoScopedSort.rejection != null) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tunsupportedIssueSortWarning(\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\tresolvedRepoScopedSort.rejection,\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst issuesPagingMode = metadata?.issuesPagingMode;\n\t// Counted, and read, as the host settles them: a scope can name another project than the one it was parsed with\n\t// (see `GitHostIntegration.resolveRepositoryScopes`), which changes how many queries a per-project read makes. The\n\t// read is handed the settled scopes, flags included, so it reads what was counted rather than settling again.\n\tconst scopes = options.repos ?? [];\n\tlet repos: ProviderReposInput | SettledRepositoryScopes = scopes;\n\tlet counted = scopes;\n\tif (issuesPagingMode === PagingMode.Project && scopes.length) {\n\t\tconst settled = await runCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() => integration.resolveRepositoryScopes(scopes, options.connectionId),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\t\t// No session to read with, or the read's own failure resolving it: answered as that read would, without it.\n\t\tif (settled.value == null) {\n\t\t\treturn refusedPage(page, settled.warning != null ? [settled.warning] : [], settled.warning != null);\n\t\t}\n\n\t\trepos = settled.value;\n\t\tcounted = settled.value.repos;\n\t}\n\tconst ordering = toIssueOrdering(resolvedRepoScopedSort.sort, mergesProviderQueries(issuesPagingMode, counted));\n\t// Refused rather than served as concatenated per-scope runs, which would look ordered without being so — and\n\t// only where it actually merges, since the same key against the same provider is perfectly answerable for a\n\t// single repository or project.\n\tif (ordering.unmergeable != null) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tunmergeableIssueSortWarning(\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\tordering.unmergeable,\n\t\t\t\t\tissuesPagingMode === PagingMode.Project ? 'projects' : 'repositories',\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst cursor = options.cursor ?? pageToCursor(page);\n\tconst { value, warning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() =>\n\t\t\t// The shapes seam returns normalized IssueShape, and is overridable by a provider whose only issue\n\t\t\t// client already yields shapes (serving this path without a raw ProviderIssue round-trip).\n\t\t\tintegration.getMyIssuesForReposAsShapesResult(\n\t\t\t\trepos,\n\t\t\t\t// Forward `page`/`pageSize` alongside the cursor so PagingMode.Repo/Project hosts honor the\n\t\t\t\t// requested page and page size rather than ignoring a synthesized page-number cursor.\n\t\t\t\t{\n\t\t\t\t\tfilters: options.filters,\n\t\t\t\t\tincludeAllAssignees: options.includeAllAssignees,\n\t\t\t\t\tstate: options.state,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t// The normalized `page`, so the number forwarded to the provider can't disagree with the\n\t\t\t\t\t// page-number cursor beside it — both are built from the same value. Only when the caller asked\n\t\t\t\t\t// for one: an omitted page is the provider's own first page.\n\t\t\t\t\tpage: options.page != null ? page : undefined,\n\t\t\t\t\tpageSize: options.itemsPerPage,\n\t\t\t\t\tsort: ordering.sort,\n\t\t\t\t\tscopeConcurrency: options.scopeConcurrency,\n\t\t\t\t},\n\t\t\t\toptions.connectionId,\n\t\t\t),\n\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t);\n\n\tlet items = value?.values ?? [];\n\tconst warnings = warning != null ? [warning] : [];\n\tlet pageFetchFailed = warning != null && value == null;\n\tlet paged = toProviderPageInfo(options.itemsPerPage ?? items.length, value?.paging);\n\tlet allMetadata = value?.metadata;\n\tlet drainedGap = false;\n\n\t// Cursor-only repo-scoped hosts (e.g. GitHub) ignore a synthesized page-number cursor, so a page-only\n\t// request is advanced through the provider's own continuations (see drainToRequestedPage).\n\tif (\n\t\tissuesPagingMode === PagingMode.Repos &&\n\t\toptions.page != null &&\n\t\tpage > 1 &&\n\t\toptions.cursor == null &&\n\t\tpaged.page.currentPage === 1\n\t) {\n\t\tconst drained = await drainToRequestedPage(\n\t\t\t{ items: items, paged: paged, metadata: allMetadata, fetchFailed: pageFetchFailed },\n\t\t\t{\n\t\t\t\trequestedPage: page,\n\t\t\t\titemsPerPage: options.itemsPerPage,\n\t\t\t\twarnings: warnings,\n\t\t\t\treadPage: (cursor: string) =>\n\t\t\t\t\trunCaptured(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t() =>\n\t\t\t\t\t\t\tintegration.getMyIssuesForReposAsShapesResult(\n\t\t\t\t\t\t\t\trepos,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tfilters: options.filters,\n\t\t\t\t\t\t\t\t\tincludeAllAssignees: options.includeAllAssignees,\n\t\t\t\t\t\t\t\t\tstate: options.state,\n\t\t\t\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\t\t\t\tpageSize: options.itemsPerPage,\n\t\t\t\t\t\t\t\t\tsort: ordering.sort,\n\t\t\t\t\t\t\t\t\tscopeConcurrency: options.scopeConcurrency,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t\t\t),\n\t\t\t},\n\t\t);\n\t\titems = drained.items;\n\t\tpaged = drained.paged;\n\t\tallMetadata = drained.metadata;\n\t\tpageFetchFailed = drained.fetchFailed;\n\t\tdrainedGap = drained.unexpectedGap === true;\n\t}\n\n\t// Ordered once the drain (above) has assembled whichever pages this position spans.\n\titems = ordering.order(items);\n\n\t// Convert the SDK collection metadata into scope-aware warnings + failure/truncation flags, appending\n\t// them to any captured thrown-error warning without discarding the partial result's items.\n\tconst assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, allMetadata);\n\t// Never advertise `hasMore` without a continuation the caller can act on, and only synthesize a page\n\t// number for a host that reads it as one (see `isPageNumberAdvanceable`).\n\tconst issuesPageAdvanceable = isPageNumberAdvanceable(issuesPagingMode);\n\tconst continuation = resolveContinuation(paged, issuesPageAdvanceable ? paged.page.currentPage + 1 : undefined);\n\t// A provider read that couldn't confirm completeness (e.g. Bitbucket's single-page repo issue read\n\t// that dropped a repo) sets `paging.truncated`; surface it as a terminal `page.truncated` so a partial\n\t// page isn't published as complete. Metadata incompleteness is an independent source of the same signal.\n\tconst truncated = continuation.truncated || assessment.truncated;\n\tif (truncated && warnings.length === 0) {\n\t\twarnings.push(\n\t\t\ttruncationWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'Issue',\n\t\t\t\t// `exhausted`, never `page-budget`: a paged read has no budget the caller can raise, and\n\t\t\t\t// ordinary continuation is already expressed by `hasMore`/`cursor`.\n\t\t\t\tassessment.fetchFailed || pageFetchFailed ? 'interrupted' : 'exhausted',\n\t\t\t),\n\t\t);\n\t}\n\t// A metadata omission from an earlier page asserts the read succeeded; a later page may since have failed.\n\treconcileOmissionsWithFailure(warnings, assessment.fetchFailed || pageFetchFailed);\n\treturn {\n\t\titems: items,\n\t\twarnings: warnings,\n\t\tpage: {\n\t\t\t...paged.page,\n\t\t\t// Positional, per ProviderPageInfo.currentPage: the drain (above) already advanced `paged.page` for\n\t\t\t// a cursor-only host, and a caller-threaded cursor advanced the provider without one.\n\t\t\tcurrentPage: resolveCurrentPage({\n\t\t\t\tproviderPage: paged.page.currentPage,\n\t\t\t\trequestedPage: page,\n\t\t\t\tsuppliedCursor: options.cursor,\n\t\t\t\tpageAdvanceable: issuesPageAdvanceable,\n\t\t\t}),\n\t\t\ttruncated: truncated || undefined,\n\t\t},\n\t\thasMore: continuation.hasMore,\n\t\tcursor: continuation.cursor,\n\t\tfetchFailed: assessment.fetchFailed || pageFetchFailed || undefined,\n\t\trequestFailed: assessment.requestFailed || pageFetchFailed,\n\t\tunexpectedGap:\n\t\t\tdrainedGap || (paged.hasMore === true && usableCursor(paged.cursor) == null && !issuesPageAdvanceable),\n\t};\n}\n"],"mappings":"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"}
@@ -19,12 +19,11 @@ import { CollectionMetadata } from "@gitkraken/provider-apis";
19
19
  * Only `PagingMode.Repos` (GitHub/GHE) is excluded: its read is cursor-only, so it accepts a page number and
20
20
  * ignores it, answering with page 1 — which is why {@link drainToRequestedPage} exists for that mode.
21
21
  *
22
- * Every other provider, including one with no declared mode (Bitbucket Data Center), reads `page` as a 1-based
23
- * page number. That last part is only true as of `@gitkraken/provider-apis` 0.54.0: before it, Bitbucket Data
24
- * Center consumed `page` as a raw `start` ITEM OFFSET, so a synthesized page 3 asked for `start=3` — a window
25
- * overlapping page 1 — and this facade had to withhold the synthesized cursor from it. The SDK now converts
26
- * (`start = (page - 1) * limit`) and reports `nextPage` as a page number, so the guard is gone and that host
27
- * can be advanced by number like the rest.
22
+ * Every other provider reads `page` as a 1-based page number. For Bitbucket Data Center that is only true as of
23
+ * `@gitkraken/provider-apis` 0.54.0: before it, it consumed `page` as a raw `start` ITEM OFFSET, so a synthesized
24
+ * page 3 asked for `start=3` — a window overlapping page 1 — and this facade had to withhold the synthesized cursor
25
+ * from it. The SDK now converts `page` to an offset and reports `nextPage` as a page number, so that host can be
26
+ * advanced by number like the rest.
28
27
  */
29
28
  export declare function isPageNumberAdvanceable(mode: PagingMode | undefined): boolean;
30
29
  /**
@@ -18,12 +18,11 @@ import "../providers/models.js";
18
18
  * Only `PagingMode.Repos` (GitHub/GHE) is excluded: its read is cursor-only, so it accepts a page number and
19
19
  * ignores it, answering with page 1 — which is why {@link drainToRequestedPage} exists for that mode.
20
20
  *
21
- * Every other provider, including one with no declared mode (Bitbucket Data Center), reads `page` as a 1-based
22
- * page number. That last part is only true as of `@gitkraken/provider-apis` 0.54.0: before it, Bitbucket Data
23
- * Center consumed `page` as a raw `start` ITEM OFFSET, so a synthesized page 3 asked for `start=3` — a window
24
- * overlapping page 1 — and this facade had to withhold the synthesized cursor from it. The SDK now converts
25
- * (`start = (page - 1) * limit`) and reports `nextPage` as a page number, so the guard is gone and that host
26
- * can be advanced by number like the rest.
21
+ * Every other provider reads `page` as a 1-based page number. For Bitbucket Data Center that is only true as of
22
+ * `@gitkraken/provider-apis` 0.54.0: before it, it consumed `page` as a raw `start` ITEM OFFSET, so a synthesized
23
+ * page 3 asked for `start=3` — a window overlapping page 1 — and this facade had to withhold the synthesized cursor
24
+ * from it. The SDK now converts `page` to an offset and reports `nextPage` as a page number, so that host can be
25
+ * advanced by number like the rest.
27
26
  */
28
27
  function isPageNumberAdvanceable(mode) {
29
28
  return mode !== "repos";
@@ -1 +1 @@
1
- {"version":3,"file":"paging.js","names":[],"sources":["../../../../../plus/integrations/src/reads/paging.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport type { PagedResult } from '@gitlens/utils/paging.js';\nimport { PagingMode } from '../providers/models.js';\nimport { mergeCollectionMetadata, parsePageCursor, toPageCursor } from '../providers/utils/providerPaging.js';\nimport type { ProviderPagedResult, ProviderPageInfo, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\n\n/**\n * The paging mechanics shared by every paged read on the provider facade: translating the SDK's `paging` into\n * the page-oriented shape consumers get, deciding what position and what continuation a page may honestly\n * claim, and walking a cursor-only read forward to a requested page number.\n *\n * Kept in one module because the value of these rules is that they are the SAME on every read — the contract\n * they implement is documented once on {@link ProviderPageInfo} and on `IntegrationManager`'s paging section.\n * Copies of them drifting apart is exactly the defect this consolidation removed.\n */\n\n/**\n * Whether a repo-scoped read for this provider ADVANCES on a page number, so the facade may synthesize a\n * page-number cursor, `page.currentPage` may echo the request, and a next-page number is a usable continuation.\n *\n * Only `PagingMode.Repos` (GitHub/GHE) is excluded: its read is cursor-only, so it accepts a page number and\n * ignores it, answering with page 1 — which is why {@link drainToRequestedPage} exists for that mode.\n *\n * Every other provider, including one with no declared mode (Bitbucket Data Center), reads `page` as a 1-based\n * page number. That last part is only true as of `@gitkraken/provider-apis` 0.54.0: before it, Bitbucket Data\n * Center consumed `page` as a raw `start` ITEM OFFSET, so a synthesized page 3 asked for `start=3` — a window\n * overlapping page 1 — and this facade had to withhold the synthesized cursor from it. The SDK now converts\n * (`start = (page - 1) * limit`) and reports `nextPage` as a page number, so the guard is gone and that host\n * can be advanced by number like the rest.\n */\nexport function isPageNumberAdvanceable(mode: PagingMode | undefined): boolean {\n\treturn mode !== PagingMode.Repos;\n}\n\n/**\n * Encodes a 1-based page number as the opaque cursor the provider paging layer understands, via the same\n * {@link toPageCursor} the providers encode with (its `parsePageCursor` counterpart is what reads these back).\n * Page 1 needs no cursor: it is the read's own starting position.\n */\nexport function pageToCursor(page: number | undefined): string | undefined {\n\treturn page == null || page <= 1 ? undefined : toPageCursor(page);\n}\n\n/**\n * Filters out the provider paging layer's empty-cursor sentinel. `providersApi` seeds its next-cursor with\n * `'{}'` and leaves it there when the SDK reports another page without an `endCursor`, so `'{}'` means \"no\n * usable continuation\" and must never be threaded back as one.\n */\nexport function usableCursor(cursor: string | undefined): string | undefined {\n\treturn cursor == null || cursor === '{}' ? undefined : cursor;\n}\n\n/**\n * The terminal page a paged read returns when it refuses the request outright — the surface doesn't apply (an\n * issue tracker asked for a repo read), the target couldn't be resolved, a capability is missing, or a filter\n * set isn't expressible server-side. Every one of those is \"no page was served\": empty `items`, no continuation,\n * and the reason carried in `warnings`.\n *\n * `currentPage` stays a caller-supplied parameter rather than being derived here because the refusals differ on\n * what position they can honestly claim: a read the provider advances by page number reports the requested page,\n * while a cursor-only account-wide read has no addressable position and reports 1 (see\n * {@link ProviderPageInfo.currentPage}).\n */\nexport function refusedPage<T>(\n\tcurrentPage: number,\n\twarnings: ProviderWarning[],\n\tfetchFailed: boolean,\n): ProviderPagedResult<T> {\n\treturn {\n\t\titems: [],\n\t\twarnings: warnings,\n\t\tpage: { currentPage: currentPage, itemsPerPage: 0 },\n\t\thasMore: false,\n\t\tfetchFailed: fetchFailed || undefined,\n\t};\n}\n\n/** A provider page normalized off the SDK's `paging`, before continuation and position are reconciled. */\nexport interface NormalizedPage {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\tcursor?: string;\n\ttruncated: boolean;\n}\n\n/**\n * Normalizes a `PagedResult.paging` into the page-oriented shape Kepler consumes: `page`, `hasMore`, and an\n * opaque `cursor` retained only for cursor-only hosts (where jumping straight to page N isn't possible, so the\n * caller threads the cursor back instead).\n */\nexport function toProviderPageInfo(\n\titemsPerPage: number,\n\tpaging: { more?: boolean; cursor?: string; page?: number; pageSize?: number; truncated?: boolean } | undefined,\n): NormalizedPage {\n\tlet cursor: string | undefined;\n\tlet cursorPage: number | undefined;\n\tconst raw = paging?.cursor;\n\tif (raw != null && raw !== '{}') {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(raw) as { type?: string; cursors?: unknown; page?: number };\n\t\t\t// Retain opaque cursor strings for cursor-only hosts, per-repo/project cursor bundles for\n\t\t\t// PagingMode.Repo/Project reads, AND page/offset cursors. The latter matters for reads with no\n\t\t\t// caller-visible page param to increment — e.g. Bitbucket Server's account-wide PR read threads\n\t\t\t// its next `start` offset as a `type:'page'` cursor; dropping it left the caller with\n\t\t\t// `hasMore:true` and nothing to continue with. A page cursor is a valid opaque continuation, so\n\t\t\t// threading it back is always safe even where a page number is also reported.\n\t\t\tif (parsed.type === 'cursor' || parsed.type === 'page' || Array.isArray(parsed.cursors)) {\n\t\t\t\tcursor = raw;\n\t\t\t\t// Per-repo/project cursor bundles also carry the current page number so the facade can report the\n\t\t\t\t// real page when the consumer continues using only the cursor.\n\t\t\t\tif (Array.isArray(parsed.cursors) && parsed.page != null) {\n\t\t\t\t\tcursorPage = parsed.page;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {}\n\t}\n\t// Only echo a page number the provider actually honored. Numbered-page hosts report their own\n\t// `paging.page`; cursor-only hosts (e.g. GitHub PR search) report none and ignore a synthesized\n\t// page-number cursor — returning their first page — so echoing the requested `page` would mislabel\n\t// page 1 as page N. Report page 1 in that case rather than the unapplied request. Per-repo/project\n\t// bundles may carry the page explicitly in the cursor.\n\tconst currentPage = paging?.page ?? cursorPage ?? 1;\n\treturn {\n\t\tpage: {\n\t\t\tcurrentPage: Math.max(1, currentPage),\n\t\t\titemsPerPage: paging?.pageSize ?? itemsPerPage,\n\t\t},\n\t\thasMore: paging?.more ?? false,\n\t\tcursor: cursor,\n\t\t// A single-page provider read that couldn't confirm completeness carries `paging.truncated`;\n\t\t// surface it so callers can flag `page.truncated` instead of publishing a partial read as complete.\n\t\ttruncated: paging?.truncated ?? false,\n\t};\n}\n\n/**\n * Reconciles a provider's `hasMore` with the continuation it actually handed back. A provider can report\n * `hasNextPage: true` while omitting the `endCursor` (the paging layer surfaces that as the sentinel `'{}'`,\n * which {@link toProviderPageInfo} drops), leaving `hasMore: true` with no cursor — a consumer that pages while\n * `hasMore` would then re-request the same page forever. Prefer the real provider cursor; else synthesize the\n * next page number, but only for a read the provider actually advances by page number (`nextPage` omitted for\n * cursor-only reads, which ignore it and answer with their first page again); else report the read as\n * terminal-but-incomplete (`hasMore: false` + `truncated`).\n */\nexport function resolveContinuation(\n\tpaged: { hasMore: boolean; cursor?: string; truncated: boolean },\n\tnextPage: number | undefined,\n): { hasMore: boolean; cursor?: string; truncated: boolean } {\n\tif (!paged.hasMore) return { hasMore: false, cursor: undefined, truncated: paged.truncated };\n\tif (paged.cursor != null) return { hasMore: true, cursor: paged.cursor, truncated: paged.truncated };\n\n\tconst synthesized = nextPage != null ? pageToCursor(nextPage) : undefined;\n\tif (synthesized != null) return { hasMore: true, cursor: synthesized, truncated: paged.truncated };\n\n\treturn { hasMore: false, cursor: undefined, truncated: true };\n}\n\n/**\n * Resolves the position a paged read reports as `page.currentPage`, per the single convention documented on\n * {@link ProviderPageInfo.currentPage}. Every paged read routes through here so the field means the same thing\n * on all of them: positional, never constant-1 for one read and positional for another.\n *\n * `providerPage` is what the provider (or the internal drain) established — 1 means \"nothing reported\".\n * `pageAdvanceable` is whether the read honors a page number at all: false for a cursor-only read, which\n * answers with its first page when handed a synthesized page-number cursor, so the requested `page` must NOT be\n * echoed there.\n */\nexport function resolveCurrentPage(options: {\n\tproviderPage: number;\n\trequestedPage: number;\n\tsuppliedCursor: string | undefined;\n\tpageAdvanceable: boolean;\n}): number {\n\tif (options.providerPage > 1) return options.providerPage;\n\t// A caller-threaded cursor DID advance the provider, so the caller's own position is authoritative for a\n\t// provider that reports none: prefer the page the cursor encodes, else the `page` supplied alongside it.\n\tif (options.suppliedCursor != null) return parsePageCursor(options.suppliedCursor) ?? options.requestedPage;\n\n\treturn options.pageAdvanceable ? options.requestedPage : 1;\n}\n\n/**\n * One page of a FLAT cursor-paged provider read — the shape both issue reads' providers return directly.\n *\n * `hasMore` is optional because one of the two providers leaves it off on a terminal page; absent reads as \"no\n * more\", which is what a provider that reported no continuation means.\n */\nexport interface FlatPage {\n\tcursor?: string;\n\thasMore?: boolean;\n\tpage?: number;\n\ttruncated: boolean;\n}\n\n/** What {@link drainFlatPagesToRequestedPage} established by walking. */\nexport interface FlatDrainResult<T extends FlatPage> {\n\t/** The requested page, or `undefined` when a page failed mid-walk (which also sets `fetchFailed`). */\n\tvalue: T | undefined;\n\t/** The position actually reached — 1 when nothing advanced. */\n\tcurrentPage: number;\n\ttruncated: boolean;\n\t/** True when the requested page is past the provider's last one, so it is genuinely EMPTY. */\n\trequestedPageMissing: boolean;\n\tfetchFailed: boolean;\n\t/** A walked page promised continuation but supplied none usable, or repeated one. */\n\tunexpectedGap: boolean;\n}\n\n/**\n * Walks a FLAT cursor-paged read forward to a requested page number, for a provider whose pages are\n * `{cursor, hasMore, page, truncated}` rather than the SDK's `PagedResult` wrapper.\n *\n * The sibling of {@link drainToRequestedPage}, not a replacement for it: that one normalizes SDK `paging` through\n * {@link toProviderPageInfo}, which retains only cursors whose JSON declares `type`/`cursors` — and the composite\n * cursor of a GitHub aliased search declares neither, so it would be dropped and the walk would never advance.\n * Two shapes genuinely need two entry points; what they must NOT have is two copies of the rules below, which is\n * what this exists to prevent (see this module's own note on drift).\n *\n * The rules, all of them subtle enough to be worth stating once:\n * - The empty-cursor sentinel is filtered, so a provider claiming another page without handing back a usable\n * continuation is not re-read with `'{}'` (which it would answer with page 1 again).\n * - A provider that returns the SAME cursor isn't advancing: stop, and report the read as truncated rather than\n * looping forever or publishing a short page as complete.\n * - A page requested past the provider's terminal cursor is that EMPTY page N — never the last available page\n * relabeled, per {@link ProviderPageInfo.currentPage}.\n * - A page that fails mid-walk latches `fetchFailed` and leaves no value, so the caller can tell \"the read broke\"\n * from \"the page was genuinely empty\".\n *\n * `fold` is the one thing the callers differ on — one merges SDK collection metadata across the walked pages, the\n * other keeps the largest reported match count — so it is a callback rather than a branch.\n */\nexport async function drainFlatPagesToRequestedPage<T extends FlatPage>(\n\tfirst: { value?: T; warning?: ProviderWarning },\n\toptions: {\n\t\trequestedPage: number;\n\t\tsuppliedCursor: string | undefined;\n\t\twarnings: ProviderWarning[];\n\t\treadPage: (cursor: string) => Promise<{ value?: T; warning?: ProviderWarning }>;\n\t\tfold?: (page: T) => void;\n\t},\n): Promise<FlatDrainResult<T>> {\n\tlet value = first.value;\n\tlet fetchFailed = first.warning != null && value == null;\n\tlet currentPage = value?.page ?? 1;\n\tlet truncated = value?.truncated ?? false;\n\tlet unexpectedGap = value?.hasMore === true && usableCursor(value.cursor) == null;\n\n\tconst walkable = options.suppliedCursor == null && options.requestedPage > 1;\n\tif (walkable && value != null) {\n\t\tfor (\n\t\t\tlet nextCursor = usableCursor(value.cursor);\n\t\t\tcurrentPage < options.requestedPage && value.hasMore === true && nextCursor != null;\n\t\t\tnextCursor = usableCursor(value.cursor)\n\t\t) {\n\t\t\tconst next = await options.readPage(nextCursor);\n\t\t\tif (next.warning != null) {\n\t\t\t\tappendDedupedWarning(options.warnings, next.warning);\n\t\t\t}\n\t\t\tif (next.value == null) {\n\t\t\t\tfetchFailed = next.warning != null;\n\t\t\t\tvalue = undefined;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tvalue = next.value;\n\t\t\ttruncated = truncated || value.truncated;\n\t\t\toptions.fold?.(value);\n\t\t\tcurrentPage = value.page ?? currentPage + 1;\n\t\t\tunexpectedGap ||= value.hasMore === true && usableCursor(value.cursor) == null;\n\t\t\tif (usableCursor(value.cursor) === nextCursor) {\n\t\t\t\ttruncated = true;\n\t\t\t\tunexpectedGap ||= value.hasMore === true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tvalue: value,\n\t\tcurrentPage: currentPage,\n\t\ttruncated: truncated,\n\t\t// Every case reduces to \"a page was asked for by number and the walk fell short\", including the one where\n\t\t// no walk ran at all because the first page was already terminal.\n\t\trequestedPageMissing: walkable && currentPage < options.requestedPage,\n\t\tfetchFailed: fetchFailed,\n\t\tunexpectedGap: unexpectedGap,\n\t};\n}\n\n/** The mutable state {@link drainToRequestedPage} carries across the pages it walks. */\nexport interface DrainState<T> {\n\titems: T[];\n\tpaged: NormalizedPage;\n\tmetadata: CollectionMetadata | undefined;\n\tfetchFailed: boolean;\n\t/** A walked page promised continuation but supplied none usable, or repeated one. */\n\tunexpectedGap?: boolean;\n}\n\n/**\n * Advances a cursor-only read to the requested page when the caller supplied only `page`.\n *\n * A cursor-only read accepts a synthesized page-number cursor and ignores it, answering with its first page, so\n * the requested page has to be reached by walking the provider's own opaque continuations. Only the last\n * successfully-read page's items are kept — returning pages 1..N as \"page N\" would duplicate items for a normal\n * paged consumer — while warnings and metadata are merged across the whole drained prefix.\n *\n * Shared by the repo-scoped and account-wide PR reads and the repo-scoped issue read so the three cannot drift:\n * a requested page past the provider's terminal cursor is an EMPTY page N on all of them, per\n * {@link ProviderPageInfo.currentPage}, never the last available page relabeled.\n */\nexport async function drainToRequestedPage<T>(\n\tstate: DrainState<T>,\n\toptions: {\n\t\trequestedPage: number;\n\t\titemsPerPage: number | undefined;\n\t\twarnings: ProviderWarning[];\n\t\treadPage: (cursor: string) => Promise<{\n\t\t\tvalue?: (PagedResult<T> & { metadata?: CollectionMetadata }) | undefined;\n\t\t\twarning?: ProviderWarning;\n\t\t}>;\n\t},\n): Promise<DrainState<T>> {\n\tlet { items, metadata, fetchFailed } = state;\n\tlet currentPage = 1;\n\tlet currentCursor = usableCursor(state.paged.hasMore ? state.paged.cursor : undefined);\n\tlet currentHasMore = state.paged.hasMore && currentCursor != null;\n\tlet unexpectedGap = state.paged.hasMore && currentCursor == null;\n\tlet currentTruncated = state.paged.truncated;\n\t// A page requested past the terminal cursor is that empty page N. Distinguished from the first read\n\t// having failed outright, which is already reported as page N with no items by the caller's own state.\n\tconst missRequestedPage = () => {\n\t\titems = [];\n\t\tcurrentPage = options.requestedPage;\n\t\tcurrentCursor = undefined;\n\t\tcurrentHasMore = false;\n\t};\n\n\tif (fetchFailed) {\n\t\tmissRequestedPage();\n\t}\n\n\twhile (currentPage < options.requestedPage && currentHasMore && currentCursor != null) {\n\t\tconst { value, warning } = await options.readPage(currentCursor);\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(options.warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\tfetchFailed = true;\n\t\t\tmissRequestedPage();\n\t\t\tbreak;\n\t\t}\n\n\t\titems = value.values;\n\t\tmetadata = mergeCollectionMetadata(metadata, value.metadata, 'disjoint');\n\t\tconst next = toProviderPageInfo(options.itemsPerPage ?? value.values.length, value.paging);\n\t\tcurrentPage++;\n\t\tcurrentTruncated ||= next.truncated;\n\t\tconst nextCursor = usableCursor(next.cursor);\n\t\tunexpectedGap ||= next.hasMore && (nextCursor == null || nextCursor === currentCursor);\n\t\t// A provider that hands back the same cursor (or none) isn't advancing; stop rather than refetch\n\t\t// the same page forever.\n\t\tif (nextCursor == null || nextCursor === currentCursor) {\n\t\t\tcurrentCursor = undefined;\n\t\t\tcurrentHasMore = false;\n\t\t\tbreak;\n\t\t}\n\n\t\tcurrentCursor = nextCursor;\n\t\tcurrentHasMore = next.hasMore;\n\t}\n\n\tif (currentPage < options.requestedPage) {\n\t\tmissRequestedPage();\n\t}\n\n\treturn {\n\t\titems: items,\n\t\tpaged: {\n\t\t\tpage: { currentPage: currentPage, itemsPerPage: options.itemsPerPage ?? items.length },\n\t\t\thasMore: currentHasMore,\n\t\t\tcursor: currentCursor,\n\t\t\ttruncated: currentTruncated,\n\t\t},\n\t\tmetadata: metadata,\n\t\tfetchFailed: fetchFailed,\n\t\tunexpectedGap: unexpectedGap,\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"paging.js","names":[],"sources":["../../../../../plus/integrations/src/reads/paging.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport type { PagedResult } from '@gitlens/utils/paging.js';\nimport { PagingMode } from '../providers/models.js';\nimport { mergeCollectionMetadata, parsePageCursor, toPageCursor } from '../providers/utils/providerPaging.js';\nimport type { ProviderPagedResult, ProviderPageInfo, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\n\n/**\n * The paging mechanics shared by every paged read on the provider facade: translating the SDK's `paging` into\n * the page-oriented shape consumers get, deciding what position and what continuation a page may honestly\n * claim, and walking a cursor-only read forward to a requested page number.\n *\n * Kept in one module because the value of these rules is that they are the SAME on every read — the contract\n * they implement is documented once on {@link ProviderPageInfo} and on `IntegrationManager`'s paging section.\n * Copies of them drifting apart is exactly the defect this consolidation removed.\n */\n\n/**\n * Whether a repo-scoped read for this provider ADVANCES on a page number, so the facade may synthesize a\n * page-number cursor, `page.currentPage` may echo the request, and a next-page number is a usable continuation.\n *\n * Only `PagingMode.Repos` (GitHub/GHE) is excluded: its read is cursor-only, so it accepts a page number and\n * ignores it, answering with page 1 — which is why {@link drainToRequestedPage} exists for that mode.\n *\n * Every other provider reads `page` as a 1-based page number. For Bitbucket Data Center that is only true as of\n * `@gitkraken/provider-apis` 0.54.0: before it, it consumed `page` as a raw `start` ITEM OFFSET, so a synthesized\n * page 3 asked for `start=3` — a window overlapping page 1 — and this facade had to withhold the synthesized cursor\n * from it. The SDK now converts `page` to an offset and reports `nextPage` as a page number, so that host can be\n * advanced by number like the rest.\n */\nexport function isPageNumberAdvanceable(mode: PagingMode | undefined): boolean {\n\treturn mode !== PagingMode.Repos;\n}\n\n/**\n * Encodes a 1-based page number as the opaque cursor the provider paging layer understands, via the same\n * {@link toPageCursor} the providers encode with (its `parsePageCursor` counterpart is what reads these back).\n * Page 1 needs no cursor: it is the read's own starting position.\n */\nexport function pageToCursor(page: number | undefined): string | undefined {\n\treturn page == null || page <= 1 ? undefined : toPageCursor(page);\n}\n\n/**\n * Filters out the provider paging layer's empty-cursor sentinel. `providersApi` seeds its next-cursor with\n * `'{}'` and leaves it there when the SDK reports another page without an `endCursor`, so `'{}'` means \"no\n * usable continuation\" and must never be threaded back as one.\n */\nexport function usableCursor(cursor: string | undefined): string | undefined {\n\treturn cursor == null || cursor === '{}' ? undefined : cursor;\n}\n\n/**\n * The terminal page a paged read returns when it refuses the request outright — the surface doesn't apply (an\n * issue tracker asked for a repo read), the target couldn't be resolved, a capability is missing, or a filter\n * set isn't expressible server-side. Every one of those is \"no page was served\": empty `items`, no continuation,\n * and the reason carried in `warnings`.\n *\n * `currentPage` stays a caller-supplied parameter rather than being derived here because the refusals differ on\n * what position they can honestly claim: a read the provider advances by page number reports the requested page,\n * while a cursor-only account-wide read has no addressable position and reports 1 (see\n * {@link ProviderPageInfo.currentPage}).\n */\nexport function refusedPage<T>(\n\tcurrentPage: number,\n\twarnings: ProviderWarning[],\n\tfetchFailed: boolean,\n): ProviderPagedResult<T> {\n\treturn {\n\t\titems: [],\n\t\twarnings: warnings,\n\t\tpage: { currentPage: currentPage, itemsPerPage: 0 },\n\t\thasMore: false,\n\t\tfetchFailed: fetchFailed || undefined,\n\t};\n}\n\n/** A provider page normalized off the SDK's `paging`, before continuation and position are reconciled. */\nexport interface NormalizedPage {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\tcursor?: string;\n\ttruncated: boolean;\n}\n\n/**\n * Normalizes a `PagedResult.paging` into the page-oriented shape Kepler consumes: `page`, `hasMore`, and an\n * opaque `cursor` retained only for cursor-only hosts (where jumping straight to page N isn't possible, so the\n * caller threads the cursor back instead).\n */\nexport function toProviderPageInfo(\n\titemsPerPage: number,\n\tpaging: { more?: boolean; cursor?: string; page?: number; pageSize?: number; truncated?: boolean } | undefined,\n): NormalizedPage {\n\tlet cursor: string | undefined;\n\tlet cursorPage: number | undefined;\n\tconst raw = paging?.cursor;\n\tif (raw != null && raw !== '{}') {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(raw) as { type?: string; cursors?: unknown; page?: number };\n\t\t\t// Retain opaque cursor strings for cursor-only hosts, per-repo/project cursor bundles for\n\t\t\t// PagingMode.Repo/Project reads, AND page/offset cursors. The latter matters for reads with no\n\t\t\t// caller-visible page param to increment — e.g. Bitbucket Server's account-wide PR read threads\n\t\t\t// its next `start` offset as a `type:'page'` cursor; dropping it left the caller with\n\t\t\t// `hasMore:true` and nothing to continue with. A page cursor is a valid opaque continuation, so\n\t\t\t// threading it back is always safe even where a page number is also reported.\n\t\t\tif (parsed.type === 'cursor' || parsed.type === 'page' || Array.isArray(parsed.cursors)) {\n\t\t\t\tcursor = raw;\n\t\t\t\t// Per-repo/project cursor bundles also carry the current page number so the facade can report the\n\t\t\t\t// real page when the consumer continues using only the cursor.\n\t\t\t\tif (Array.isArray(parsed.cursors) && parsed.page != null) {\n\t\t\t\t\tcursorPage = parsed.page;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {}\n\t}\n\t// Only echo a page number the provider actually honored. Numbered-page hosts report their own\n\t// `paging.page`; cursor-only hosts (e.g. GitHub PR search) report none and ignore a synthesized\n\t// page-number cursor — returning their first page — so echoing the requested `page` would mislabel\n\t// page 1 as page N. Report page 1 in that case rather than the unapplied request. Per-repo/project\n\t// bundles may carry the page explicitly in the cursor.\n\tconst currentPage = paging?.page ?? cursorPage ?? 1;\n\treturn {\n\t\tpage: {\n\t\t\tcurrentPage: Math.max(1, currentPage),\n\t\t\titemsPerPage: paging?.pageSize ?? itemsPerPage,\n\t\t},\n\t\thasMore: paging?.more ?? false,\n\t\tcursor: cursor,\n\t\t// A single-page provider read that couldn't confirm completeness carries `paging.truncated`;\n\t\t// surface it so callers can flag `page.truncated` instead of publishing a partial read as complete.\n\t\ttruncated: paging?.truncated ?? false,\n\t};\n}\n\n/**\n * Reconciles a provider's `hasMore` with the continuation it actually handed back. A provider can report\n * `hasNextPage: true` while omitting the `endCursor` (the paging layer surfaces that as the sentinel `'{}'`,\n * which {@link toProviderPageInfo} drops), leaving `hasMore: true` with no cursor — a consumer that pages while\n * `hasMore` would then re-request the same page forever. Prefer the real provider cursor; else synthesize the\n * next page number, but only for a read the provider actually advances by page number (`nextPage` omitted for\n * cursor-only reads, which ignore it and answer with their first page again); else report the read as\n * terminal-but-incomplete (`hasMore: false` + `truncated`).\n */\nexport function resolveContinuation(\n\tpaged: { hasMore: boolean; cursor?: string; truncated: boolean },\n\tnextPage: number | undefined,\n): { hasMore: boolean; cursor?: string; truncated: boolean } {\n\tif (!paged.hasMore) return { hasMore: false, cursor: undefined, truncated: paged.truncated };\n\tif (paged.cursor != null) return { hasMore: true, cursor: paged.cursor, truncated: paged.truncated };\n\n\tconst synthesized = nextPage != null ? pageToCursor(nextPage) : undefined;\n\tif (synthesized != null) return { hasMore: true, cursor: synthesized, truncated: paged.truncated };\n\n\treturn { hasMore: false, cursor: undefined, truncated: true };\n}\n\n/**\n * Resolves the position a paged read reports as `page.currentPage`, per the single convention documented on\n * {@link ProviderPageInfo.currentPage}. Every paged read routes through here so the field means the same thing\n * on all of them: positional, never constant-1 for one read and positional for another.\n *\n * `providerPage` is what the provider (or the internal drain) established — 1 means \"nothing reported\".\n * `pageAdvanceable` is whether the read honors a page number at all: false for a cursor-only read, which\n * answers with its first page when handed a synthesized page-number cursor, so the requested `page` must NOT be\n * echoed there.\n */\nexport function resolveCurrentPage(options: {\n\tproviderPage: number;\n\trequestedPage: number;\n\tsuppliedCursor: string | undefined;\n\tpageAdvanceable: boolean;\n}): number {\n\tif (options.providerPage > 1) return options.providerPage;\n\t// A caller-threaded cursor DID advance the provider, so the caller's own position is authoritative for a\n\t// provider that reports none: prefer the page the cursor encodes, else the `page` supplied alongside it.\n\tif (options.suppliedCursor != null) return parsePageCursor(options.suppliedCursor) ?? options.requestedPage;\n\n\treturn options.pageAdvanceable ? options.requestedPage : 1;\n}\n\n/**\n * One page of a FLAT cursor-paged provider read — the shape both issue reads' providers return directly.\n *\n * `hasMore` is optional because one of the two providers leaves it off on a terminal page; absent reads as \"no\n * more\", which is what a provider that reported no continuation means.\n */\nexport interface FlatPage {\n\tcursor?: string;\n\thasMore?: boolean;\n\tpage?: number;\n\ttruncated: boolean;\n}\n\n/** What {@link drainFlatPagesToRequestedPage} established by walking. */\nexport interface FlatDrainResult<T extends FlatPage> {\n\t/** The requested page, or `undefined` when a page failed mid-walk (which also sets `fetchFailed`). */\n\tvalue: T | undefined;\n\t/** The position actually reached — 1 when nothing advanced. */\n\tcurrentPage: number;\n\ttruncated: boolean;\n\t/** True when the requested page is past the provider's last one, so it is genuinely EMPTY. */\n\trequestedPageMissing: boolean;\n\tfetchFailed: boolean;\n\t/** A walked page promised continuation but supplied none usable, or repeated one. */\n\tunexpectedGap: boolean;\n}\n\n/**\n * Walks a FLAT cursor-paged read forward to a requested page number, for a provider whose pages are\n * `{cursor, hasMore, page, truncated}` rather than the SDK's `PagedResult` wrapper.\n *\n * The sibling of {@link drainToRequestedPage}, not a replacement for it: that one normalizes SDK `paging` through\n * {@link toProviderPageInfo}, which retains only cursors whose JSON declares `type`/`cursors` — and the composite\n * cursor of a GitHub aliased search declares neither, so it would be dropped and the walk would never advance.\n * Two shapes genuinely need two entry points; what they must NOT have is two copies of the rules below, which is\n * what this exists to prevent (see this module's own note on drift).\n *\n * The rules, all of them subtle enough to be worth stating once:\n * - The empty-cursor sentinel is filtered, so a provider claiming another page without handing back a usable\n * continuation is not re-read with `'{}'` (which it would answer with page 1 again).\n * - A provider that returns the SAME cursor isn't advancing: stop, and report the read as truncated rather than\n * looping forever or publishing a short page as complete.\n * - A page requested past the provider's terminal cursor is that EMPTY page N — never the last available page\n * relabeled, per {@link ProviderPageInfo.currentPage}.\n * - A page that fails mid-walk latches `fetchFailed` and leaves no value, so the caller can tell \"the read broke\"\n * from \"the page was genuinely empty\".\n *\n * `fold` is the one thing the callers differ on — one merges SDK collection metadata across the walked pages, the\n * other keeps the largest reported match count — so it is a callback rather than a branch.\n */\nexport async function drainFlatPagesToRequestedPage<T extends FlatPage>(\n\tfirst: { value?: T; warning?: ProviderWarning },\n\toptions: {\n\t\trequestedPage: number;\n\t\tsuppliedCursor: string | undefined;\n\t\twarnings: ProviderWarning[];\n\t\treadPage: (cursor: string) => Promise<{ value?: T; warning?: ProviderWarning }>;\n\t\tfold?: (page: T) => void;\n\t},\n): Promise<FlatDrainResult<T>> {\n\tlet value = first.value;\n\tlet fetchFailed = first.warning != null && value == null;\n\tlet currentPage = value?.page ?? 1;\n\tlet truncated = value?.truncated ?? false;\n\tlet unexpectedGap = value?.hasMore === true && usableCursor(value.cursor) == null;\n\n\tconst walkable = options.suppliedCursor == null && options.requestedPage > 1;\n\tif (walkable && value != null) {\n\t\tfor (\n\t\t\tlet nextCursor = usableCursor(value.cursor);\n\t\t\tcurrentPage < options.requestedPage && value.hasMore === true && nextCursor != null;\n\t\t\tnextCursor = usableCursor(value.cursor)\n\t\t) {\n\t\t\tconst next = await options.readPage(nextCursor);\n\t\t\tif (next.warning != null) {\n\t\t\t\tappendDedupedWarning(options.warnings, next.warning);\n\t\t\t}\n\t\t\tif (next.value == null) {\n\t\t\t\tfetchFailed = next.warning != null;\n\t\t\t\tvalue = undefined;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tvalue = next.value;\n\t\t\ttruncated = truncated || value.truncated;\n\t\t\toptions.fold?.(value);\n\t\t\tcurrentPage = value.page ?? currentPage + 1;\n\t\t\tunexpectedGap ||= value.hasMore === true && usableCursor(value.cursor) == null;\n\t\t\tif (usableCursor(value.cursor) === nextCursor) {\n\t\t\t\ttruncated = true;\n\t\t\t\tunexpectedGap ||= value.hasMore === true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tvalue: value,\n\t\tcurrentPage: currentPage,\n\t\ttruncated: truncated,\n\t\t// Every case reduces to \"a page was asked for by number and the walk fell short\", including the one where\n\t\t// no walk ran at all because the first page was already terminal.\n\t\trequestedPageMissing: walkable && currentPage < options.requestedPage,\n\t\tfetchFailed: fetchFailed,\n\t\tunexpectedGap: unexpectedGap,\n\t};\n}\n\n/** The mutable state {@link drainToRequestedPage} carries across the pages it walks. */\nexport interface DrainState<T> {\n\titems: T[];\n\tpaged: NormalizedPage;\n\tmetadata: CollectionMetadata | undefined;\n\tfetchFailed: boolean;\n\t/** A walked page promised continuation but supplied none usable, or repeated one. */\n\tunexpectedGap?: boolean;\n}\n\n/**\n * Advances a cursor-only read to the requested page when the caller supplied only `page`.\n *\n * A cursor-only read accepts a synthesized page-number cursor and ignores it, answering with its first page, so\n * the requested page has to be reached by walking the provider's own opaque continuations. Only the last\n * successfully-read page's items are kept — returning pages 1..N as \"page N\" would duplicate items for a normal\n * paged consumer — while warnings and metadata are merged across the whole drained prefix.\n *\n * Shared by the repo-scoped and account-wide PR reads and the repo-scoped issue read so the three cannot drift:\n * a requested page past the provider's terminal cursor is an EMPTY page N on all of them, per\n * {@link ProviderPageInfo.currentPage}, never the last available page relabeled.\n */\nexport async function drainToRequestedPage<T>(\n\tstate: DrainState<T>,\n\toptions: {\n\t\trequestedPage: number;\n\t\titemsPerPage: number | undefined;\n\t\twarnings: ProviderWarning[];\n\t\treadPage: (cursor: string) => Promise<{\n\t\t\tvalue?: (PagedResult<T> & { metadata?: CollectionMetadata }) | undefined;\n\t\t\twarning?: ProviderWarning;\n\t\t}>;\n\t},\n): Promise<DrainState<T>> {\n\tlet { items, metadata, fetchFailed } = state;\n\tlet currentPage = 1;\n\tlet currentCursor = usableCursor(state.paged.hasMore ? state.paged.cursor : undefined);\n\tlet currentHasMore = state.paged.hasMore && currentCursor != null;\n\tlet unexpectedGap = state.paged.hasMore && currentCursor == null;\n\tlet currentTruncated = state.paged.truncated;\n\t// A page requested past the terminal cursor is that empty page N. Distinguished from the first read\n\t// having failed outright, which is already reported as page N with no items by the caller's own state.\n\tconst missRequestedPage = () => {\n\t\titems = [];\n\t\tcurrentPage = options.requestedPage;\n\t\tcurrentCursor = undefined;\n\t\tcurrentHasMore = false;\n\t};\n\n\tif (fetchFailed) {\n\t\tmissRequestedPage();\n\t}\n\n\twhile (currentPage < options.requestedPage && currentHasMore && currentCursor != null) {\n\t\tconst { value, warning } = await options.readPage(currentCursor);\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(options.warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\tfetchFailed = true;\n\t\t\tmissRequestedPage();\n\t\t\tbreak;\n\t\t}\n\n\t\titems = value.values;\n\t\tmetadata = mergeCollectionMetadata(metadata, value.metadata, 'disjoint');\n\t\tconst next = toProviderPageInfo(options.itemsPerPage ?? value.values.length, value.paging);\n\t\tcurrentPage++;\n\t\tcurrentTruncated ||= next.truncated;\n\t\tconst nextCursor = usableCursor(next.cursor);\n\t\tunexpectedGap ||= next.hasMore && (nextCursor == null || nextCursor === currentCursor);\n\t\t// A provider that hands back the same cursor (or none) isn't advancing; stop rather than refetch\n\t\t// the same page forever.\n\t\tif (nextCursor == null || nextCursor === currentCursor) {\n\t\t\tcurrentCursor = undefined;\n\t\t\tcurrentHasMore = false;\n\t\t\tbreak;\n\t\t}\n\n\t\tcurrentCursor = nextCursor;\n\t\tcurrentHasMore = next.hasMore;\n\t}\n\n\tif (currentPage < options.requestedPage) {\n\t\tmissRequestedPage();\n\t}\n\n\treturn {\n\t\titems: items,\n\t\tpaged: {\n\t\t\tpage: { currentPage: currentPage, itemsPerPage: options.itemsPerPage ?? items.length },\n\t\t\thasMore: currentHasMore,\n\t\t\tcursor: currentCursor,\n\t\t\ttruncated: currentTruncated,\n\t\t},\n\t\tmetadata: metadata,\n\t\tfetchFailed: fetchFailed,\n\t\tunexpectedGap: unexpectedGap,\n\t};\n}\n"],"mappings":"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"}
@@ -27,6 +27,10 @@ export declare function listPullRequestsPage(ctx: ProviderReadContext, options:
27
27
  * shape and keeps doing so, so an explicit `false` does not widen it back to the full projection.
28
28
  */
29
29
  summary?: boolean;
30
+ /** See {@link IntegrationManager.listPullRequestsPage}. */
31
+ scopeConcurrency?: number;
32
+ /** See {@link IntegrationManager.listPullRequestsPage}. */
33
+ includeRemoteInfo?: boolean;
30
34
  itemsPerPage?: number;
31
35
  forceSync?: boolean;
32
36
  connectionId?: string;
@@ -1,7 +1,7 @@
1
1
  import { reconcileOmissionsWithFailure } from "../results.js";
2
2
  import { mergeAssessmentInto } from "../collectionMetadata.js";
3
- import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
4
3
  import { providersMetadata, toPullRequestRow } from "../providers/models.js";
4
+ import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
5
5
  import { drainToRequestedPage, isPageNumberAdvanceable, pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo } from "./paging.js";
6
6
  import { gitHostOnlySurfaceWarning, truncationWarning, unsupportedAccountWidePullRequestFiltersWarning, unsupportedFiltersWarning } from "./warnings.js";
7
7
  import { getPullRequestViewers, runCaptured } from "./drains.js";
@@ -32,7 +32,9 @@ async function listPullRequestsPage(ctx, options) {
32
32
  state: options.states,
33
33
  filters: resolvedFilters.filters,
34
34
  pageSize: options.itemsPerPage,
35
- summary: options.summary
35
+ summary: options.summary,
36
+ scopeConcurrency: options.scopeConcurrency,
37
+ includeRemoteInfo: options.includeRemoteInfo
36
38
  };
37
39
  const readPage = (pageCursor, requestedPage) => runCaptured(options.providerId, domain, options.connectionId, () => accountWide ? integration.getMyPullRequestsForUserResult({
38
40
  ...accountWideInput,