@gitkraken/core-gitlens 0.15.0 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -1
- package/dist/git/models/branch.d.ts +7 -0
- package/dist/git/models/branch.js +1 -0
- package/dist/git/models/branch.js.map +1 -1
- package/dist/git/models/graph.d.ts +2 -0
- package/dist/git/models/graph.js.map +1 -1
- package/dist/git/models/reference.d.ts +4 -0
- package/dist/git/providers/provider.d.ts +3 -1
- package/dist/git/providers/revision.d.ts +2 -1
- package/dist/git/run.types.d.ts +3 -2
- package/dist/git/utils/branch.utils.d.ts +15 -3
- package/dist/git/utils/branch.utils.js +34 -8
- package/dist/git/utils/branch.utils.js.map +1 -1
- package/dist/git/utils/reference.utils.d.ts +2 -0
- package/dist/git/utils/reference.utils.js.map +1 -1
- package/dist/git/utils/revision.utils.d.ts +3 -0
- package/dist/git/utils/revision.utils.js +5 -1
- package/dist/git/utils/revision.utils.js.map +1 -1
- package/dist/git/watching/worktreeSummary.d.ts +7 -4
- package/dist/git/watching/worktreeSummary.js +22 -3
- package/dist/git/watching/worktreeSummary.js.map +1 -1
- package/dist/git-cli/cliGitProvider.d.ts +6 -0
- package/dist/git-cli/cliGitProvider.js +48 -2
- package/dist/git-cli/cliGitProvider.js.map +1 -1
- package/dist/git-cli/exec/git.d.ts +1 -0
- package/dist/git-cli/exec/git.js +8 -10
- package/dist/git-cli/exec/git.js.map +1 -1
- package/dist/git-cli/exec/indexLock.d.ts +9 -1
- package/dist/git-cli/exec/indexLock.js +19 -5
- package/dist/git-cli/exec/indexLock.js.map +1 -1
- package/dist/git-cli/parsers/logParser.d.ts +4 -0
- package/dist/git-cli/parsers/logParser.js +12 -2
- package/dist/git-cli/parsers/logParser.js.map +1 -1
- package/dist/git-cli/parsers/refParser.js +4 -2
- package/dist/git-cli/parsers/refParser.js.map +1 -1
- package/dist/git-cli/providers/branchRemote.d.ts +13 -1
- package/dist/git-cli/providers/branchRemote.js +41 -12
- package/dist/git-cli/providers/branchRemote.js.map +1 -1
- package/dist/git-cli/providers/branches.js +55 -21
- package/dist/git-cli/providers/branches.js.map +1 -1
- package/dist/git-cli/providers/graph.js +53 -32
- package/dist/git-cli/providers/graph.js.map +1 -1
- package/dist/git-cli/providers/operations.js +22 -4
- package/dist/git-cli/providers/operations.js.map +1 -1
- package/dist/git-cli/providers/revision.d.ts +8 -1
- package/dist/git-cli/providers/revision.js +22 -8
- package/dist/git-cli/providers/revision.js.map +1 -1
- package/dist/git-cli/providers/stash.js +3 -3
- package/dist/git-cli/providers/stash.js.map +1 -1
- package/dist/git-cli/providers/worktrees.js +3 -3
- package/dist/git-cli/providers/worktrees.js.map +1 -1
- package/dist/plus/git-github/api/github.d.ts +4 -0
- package/dist/plus/git-github/api/github.js +31 -4
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/git-github/api/types.d.ts +4 -0
- package/dist/plus/git-github/providers/github/revision.js +12 -1
- package/dist/plus/git-github/providers/github/revision.js.map +1 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +22 -7
- package/dist/plus/integrations/collectionMetadata.js +49 -6
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.js +5 -4
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/utils/jiraIssueDrains.d.ts +4 -21
- package/dist/plus/integrations/providers/utils/jiraIssueDrains.js +13 -48
- package/dist/plus/integrations/providers/utils/jiraIssueDrains.js.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts +20 -6
- package/dist/plus/integrations/providers/utils/providerPaging.js +50 -13
- package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
- package/dist/plus/integrations/reads/issues.js +1 -1
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/paging.js +1 -1
- package/dist/plus/integrations/reads/paging.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +12 -7
- package/dist/plus/integrations/results.js +3 -4
- package/dist/plus/integrations/results.js.map +1 -1
- package/docs/integrations.md +9 -5
- package/package.json +1 -1
|
@@ -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 } 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);\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};\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.\n\tlet repos = options.repos ?? [];\n\tif (issuesPagingMode === PagingMode.Project && repos.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(repos, 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}\n\tconst ordering = toIssueOrdering(resolvedRepoScopedSort.sort, mergesProviderQueries(issuesPagingMode, repos));\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\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}\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};\n}\n"],"mappings":";;;;;;;;;;;;AAyCA,MAAM,oBAAiD;CAAC;CAAQ;CAAU;AAAK;;;;;;;;;;;;;;;;;AAkB/E,SAAS,sBAAsB,YAAoC,OAAgD;CAClH,MAAM,SAAS,SAAS,CAAC;CACzB,QAAQ,YAAR;EACC,KAAA,QAGC,OAAO,eAAe,MAAM,IAAI,OAAO,SAAS,IAAI,OAAO,SAAS;EACrE,KAAA,WAEC,OAAO,IAAI,IAAI,OAAO,KAAI,MAAM,OAAO,MAAM,WAAW,EAAE,UAAU,KAAA,CAAU,CAAC,CAAC,CAAC,OAAO;EACzF,SACC,OAAO;CACT;AACD;AAyEA,eAAsB,eACrB,KACA,SAC2C;CAG3C,MAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,QAAQ,CAAC,CAAC;CACtD,IAAI,0BAA0B,QAAQ,UAAU,GAC/C,OAAO,YACN,MACA,CAAC,0BAA0B,QAAQ,YAAY,KAAA,GAAW,QAAQ,cAAc,wBAAwB,CAAC,GACzG,IACD;CAGD,MAAM,cAAc,MAAM,IAAI,sBAAsB,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CAC5G,IAAI,eAAe,MAAM;EAGxB,MAAM,QAAQ,IAAI,8BAA8B,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;EACxG,OAAO,YAAY,MAAM,MAAM,UAAU,MAAM,WAAW;CAC3D;CACA,IAAI,CAAC,qBAAqB,WAAW,GACpC,OAAO,YACN,MACA,CAAC,0BAA0B,QAAQ,YAAY,KAAA,GAAW,QAAQ,cAAc,wBAAwB,CAAC,GACzG,IACD;CAGD,MAAM,IAAI,wBAAwB,aAAa,QAAQ,WAAW,QAAQ,YAAY;CAEtF,MAAM,SAAS,IAAI,cAAc,aAAa,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CACtG,MAAM,uBAAuB,8BAA8B,QAAQ,YAAY,QAAQ,MAAM;CAI7F,IAAI,CAAC,YAAY,gBAChB,OAAO,YAAY,MAAM,CAAC,yBAAyB,QAAQ,YAAY,QAAQ,QAAQ,YAAY,CAAC,GAAG,IAAI;CAG5G,IACC,QAAQ,SAAS,SAChB,CAAC,kBAAkB,SAAS,QAAQ,KAAK,KACxC,QAAQ,UAAU,UAAU,CAAC,kBAAkB,QAAQ,WAAW,EAAE,sBAEtE,OAAO,YACN,MACA,CACC,kBAAkB,SAAS,QAAQ,KAAK,IACrC,mBACA,QAAQ,YACR,QACA,QAAQ,cACR,gBAAgB,QAAQ,MAAM,yBAAyB,QAAQ,WAAW,GAC3E,IACC,aACA,QAAQ,YACR,QACA,QAAQ,cACR,wCAAwC,kBAAkB,KAAK,IAAI,EAAE,EACtE,CACH,GACA,IACD;CAMD,MAAM,UAA4B;EACxB;EACI;EACL;EACc;EAChB;CACP;CACA,QAAQ,QAAQ,OAAO,UAAU,OAAO,IAAI,0BAA0B,OAAO,IAAI,yBAAyB,OAAO;AAClH;;;;;;;;;;;AAYA,eAAe,0BAA0B,EACxC,SACA,aACA,QACA,sBACA,QAC8D;CAG9D,MAAM,WAAW,YAA6B,YAAwB,GAAG,CAAC,OAAO,GAAG,IAAI;CAMxF,IAAI,QAAQ,SAAS,UAAU,QAAQ,wBAAwB,MAC9D,OAAO,QACN,aACC,QAAQ,YACR,QACA,QAAQ,cACR,sGACD,CACD;CAQD,IACC,QAAQ,wBAAwB,SAC/B,QAAQ,eAAA,YACR,QAAQ,eAAA,4BAET,OAAO,QACN,mBACC,QAAQ,YACR,QACA,QAAQ,cACR,qHACD,CACD;CAOD,KAAK,QAAQ,OAAO,QAAQ,QAAQ,WAAW,SAAS,CAAC,YAAY,0BACpE,OAAO,QACN,mBACC,QAAQ,YACR,QACA,QAAQ,cACR,oDAAoD,QAAQ,WAAW,2CACxE,CACD;CAMD,MAAM,uBAAuB,+BAA+B,QAAQ,YAAY,QAAQ,OAAO;CAC/F,IAAI,qBAAqB,aACxB,OAAO,QACN,0CACC,QAAQ,YACR,QACA,QAAQ,cACR,QAAQ,OACT,CACD;CAOD,MAAM,eAAe,iBACpB,kBAAkB,QAAQ,WAAW,EAAE,gCACvC,QAAQ,IACT;CACA,IAAI,aAAa,aAAa,MAC7B,OAAO,QACN,4BAA4B,QAAQ,YAAY,QAAQ,QAAQ,cAAc,aAAa,SAAS,CACrG;CAKD,MAAM,sBAAsB,gBAAgB,aAAa,MAAM,IAAI;CAKnE,MAAM,uBAAuB,WAC5B,YACC,QAAQ,YACR,QACA,QAAQ,oBAEP,YAAY,mCAAmC,KAAA,GAAW,KAAA,GAAW,QAAQ,cAAc;EAC1F,qBAAqB,QAAQ;EAC7B,OAAO,QAAQ;EACf,SAAS,qBAAqB;EACtB;EACR,UAAU,QAAQ;EAClB,KAAK,QAAQ;EACb,SAAS,QAAQ;EACjB,MAAM,oBAAoB;CAC3B,CAAC,GACF,EAAwB,qBAAqB,CAC9C;CACD,MAAM,QAAQ,MAAM,oBAAoB,QAAQ,MAAM;CACtD,MAAM,WAAW,MAAM,WAAW,OAAO,CAAC,MAAM,OAAO,IAAI,CAAC;CAG5D,IAAI,cAAc,MAAM,OAAO;CAC/B,MAAM,UAAU,MAAM,8BAA8B,OAAO;EAC1D,eAAe;EACf,gBAAgB,QAAQ;EACd;EACV,UAAU;EACV,OAAM,MAAK;GACV,cAAc,wBAAwB,aAAa,EAAE,QAAQ;EAC9D;CACD,CAAC;CACD,MAAM,EAAE,OAAO,aAAa,yBAAyB;CACrD,MAAM,mBAAmB,QAAQ;CACjC,MAAM,kBAAkB,QAAQ;CAMhC,IAAI,SAAS,QAAQ,SAAS,WAAW,GACxC,OAAO,QACN,mBACC,QAAQ,YACR,QACA,QAAQ,cACR,kDAAkD,QAAQ,WAAW,2CACtE,CACD;CAGD,MAAM,QAAQ,uBAAuB,CAAC,IAAK,OAAO,UAAU,CAAC;CAI7D,MAAM,eAAe,oBAAoB,MAAM,KAAK;CAGpD,MAAM,aAAa,oBAAoB,UAAU,QAAQ,YAAY,QAAQ,QAAQ,cAAc,WAAW;CAS9G,MAAM,eAAe,oBACpB;EACC,SAAS,uBAAuB,QAAS,OAAO,WAAW;EAC3D,QAAQ,uBAAuB,KAAA,IAAY,aAAa,OAAO,MAAM;EACrE,WAAW;CACZ,GACA,KAAA,CACD;CACA,MAAM,YAAY,aAAa,aAAa,WAAW;CACvD,IAAI,aAAa,SAAS,WAAW,GACpC,SAAS,KACR,kBACC,QAAQ,YACR,QACA,QAAQ,cACR,6BAGA,WAAW,eAAe,kBAAkB,gBAAgB,WAC7D,CACD;CAGD,8BAA8B,UAAU,WAAW,eAAe,eAAe;CACjF,OAAO;EACN,OAAO;EACG;EACV,MAAM;GAKL,aAAa,uBACV,OACA,mBAAmB;IACnB,cAAc;IACd,eAAe;IACf,gBAAgB,QAAQ;IACxB,iBAAiB;GAClB,CAAC;GACH,cAAc,aAAa;GAC3B,WAAW,aAAa,KAAA;EACzB;EACA,SAAS,aAAa;EACtB,QAAQ,aAAa;EACrB,aAAa,WAAW,eAAe,mBAAmB,KAAA;CAC3D;AACD;;;;;;;AAQA,eAAe,yBAAyB,EACvC,SACA,aACA,QACA,sBACA,QAC8D;CAC9D,MAAM,WAAW,kBAAkB,QAAQ;CAM3C,MAAM,yBAAyB,iBAAiB,UAAU,qBAAqB,QAAQ,IAAI;CAC3F,IAAI,uBAAuB,aAAa,MACvC,OAAO,YACN,MACA,CACC,4BACC,QAAQ,YACR,QACA,QAAQ,cACR,uBAAuB,SACxB,CACD,GACA,IACD;CAGD,MAAM,mBAAmB,UAAU;CAGnC,IAAI,QAAQ,QAAQ,SAAS,CAAC;CAC9B,IAAI,qBAAA,aAA2C,MAAM,QAAQ;EAC5D,MAAM,UAAU,MAAM,YACrB,QAAQ,YACR,QACA,QAAQ,oBACF,YAAY,wBAAwB,OAAO,QAAQ,YAAY,GACrE,EAAwB,qBAAqB,CAC9C;EAEA,IAAI,QAAQ,SAAS,MACpB,OAAO,YAAY,MAAM,QAAQ,WAAW,OAAO,CAAC,QAAQ,OAAO,IAAI,CAAC,GAAG,QAAQ,WAAW,IAAI;EAGnG,QAAQ,QAAQ;CACjB;CACA,MAAM,WAAW,gBAAgB,uBAAuB,MAAM,sBAAsB,kBAAkB,KAAK,CAAC;CAI5G,IAAI,SAAS,eAAe,MAC3B,OAAO,YACN,MACA,CACC,4BACC,QAAQ,YACR,QACA,QAAQ,cACR,SAAS,aACT,qBAAA,YAA0C,aAAa,cACxD,CACD,GACA,IACD;CAGD,MAAM,SAAS,QAAQ,UAAU,aAAa,IAAI;CAClD,MAAM,EAAE,OAAO,YAAY,MAAM,YAChC,QAAQ,YACR,QACA,QAAQ,oBAIP,YAAY,kCACX,OAGA;EACC,SAAS,QAAQ;EACjB,qBAAqB,QAAQ;EAC7B,OAAO,QAAQ;EACP;EAIR,MAAM,QAAQ,QAAQ,OAAO,OAAO,KAAA;EACpC,UAAU,QAAQ;EAClB,MAAM,SAAS;CAChB,GACA,QAAQ,YACT,GACD,EAAwB,qBAAqB,CAC9C;CAEA,IAAI,QAAQ,OAAO,UAAU,CAAC;CAC9B,MAAM,WAAW,WAAW,OAAO,CAAC,OAAO,IAAI,CAAC;CAChD,IAAI,kBAAkB,WAAW,QAAQ,SAAS;CAClD,IAAI,QAAQ,mBAAmB,QAAQ,gBAAgB,MAAM,QAAQ,OAAO,MAAM;CAClF,IAAI,cAAc,OAAO;CAIzB,IACC,qBAAA,WACA,QAAQ,QAAQ,QAChB,OAAO,KACP,QAAQ,UAAU,QAClB,MAAM,KAAK,gBAAgB,GAC1B;EACD,MAAM,UAAU,MAAM,qBACrB;GAAS;GAAc;GAAO,UAAU;GAAa,aAAa;EAAgB,GAClF;GACC,eAAe;GACf,cAAc,QAAQ;GACZ;GACV,WAAW,WACV,YACC,QAAQ,YACR,QACA,QAAQ,oBAEP,YAAY,kCACX,OACA;IACC,SAAS,QAAQ;IACjB,qBAAqB,QAAQ;IAC7B,OAAO,QAAQ;IACP;IACR,UAAU,QAAQ;IAClB,MAAM,SAAS;GAChB,GACA,QAAQ,YACT,GACD,EAAwB,qBAAqB,CAC9C;EACF,CACD;EACA,QAAQ,QAAQ;EAChB,QAAQ,QAAQ;EAChB,cAAc,QAAQ;EACtB,kBAAkB,QAAQ;CAC3B;CAGA,QAAQ,SAAS,MAAM,KAAK;CAI5B,MAAM,aAAa,oBAAoB,UAAU,QAAQ,YAAY,QAAQ,QAAQ,cAAc,WAAW;CAG9G,MAAM,wBAAwB,wBAAwB,gBAAgB;CACtE,MAAM,eAAe,oBAAoB,OAAO,wBAAwB,MAAM,KAAK,cAAc,IAAI,KAAA,CAAS;CAI9G,MAAM,YAAY,aAAa,aAAa,WAAW;CACvD,IAAI,aAAa,SAAS,WAAW,GACpC,SAAS,KACR,kBACC,QAAQ,YACR,QACA,QAAQ,cACR,SAGA,WAAW,eAAe,kBAAkB,gBAAgB,WAC7D,CACD;CAGD,8BAA8B,UAAU,WAAW,eAAe,eAAe;CACjF,OAAO;EACC;EACG;EACV,MAAM;GACL,GAAG,MAAM;GAGT,aAAa,mBAAmB;IAC/B,cAAc,MAAM,KAAK;IACzB,eAAe;IACf,gBAAgB,QAAQ;IACxB,iBAAiB;GAClB,CAAC;GACD,WAAW,aAAa,KAAA;EACzB;EACA,SAAS,aAAa;EACtB,QAAQ,aAAa;EACrB,aAAa,WAAW,eAAe,mBAAmB,KAAA;CAC3D;AACD"}
|
|
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 } 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};\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.\n\tlet repos = options.repos ?? [];\n\tif (issuesPagingMode === PagingMode.Project && repos.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(repos, 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}\n\tconst ordering = toIssueOrdering(resolvedRepoScopedSort.sort, mergesProviderQueries(issuesPagingMode, repos));\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\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}\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};\n}\n"],"mappings":"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"}
|
|
@@ -229,7 +229,7 @@ async function drainToRequestedPage(state, options) {
|
|
|
229
229
|
break;
|
|
230
230
|
}
|
|
231
231
|
items = value.values;
|
|
232
|
-
metadata = mergeCollectionMetadata(metadata, value.metadata);
|
|
232
|
+
metadata = mergeCollectionMetadata(metadata, value.metadata, "disjoint");
|
|
233
233
|
const next = toProviderPageInfo(options.itemsPerPage ?? value.values.length, value.paging);
|
|
234
234
|
currentPage++;
|
|
235
235
|
currentTruncated ||= next.truncated;
|
|
@@ -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}\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\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\tif (usableCursor(value.cursor) === nextCursor) {\n\t\t\t\ttruncated = 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};\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}\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 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);\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\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};\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, 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}\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\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\tif (usableCursor(value.cursor) === nextCursor) {\n\t\t\t\ttruncated = 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};\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}\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 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\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};\n}\n"],"mappings":"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"}
|
|
@@ -32,15 +32,20 @@ export type ProviderWarningKind = 'auth' | 'rate-limit' | 'not-found' | 'no-conn
|
|
|
32
32
|
/** The items an `'unreadable-items'` warning left out: see {@link ProviderWarning.unreadableItems}. */
|
|
33
33
|
export interface ProviderWarningUnreadableItems {
|
|
34
34
|
/**
|
|
35
|
-
* The provider's identifiers of the items left out (a Jira issue's key), without duplicates, in reported order.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* The provider's identifiers of the items left out (a Jira issue's key), without duplicates, in reported order. Each
|
|
36
|
+
* one is an item the provider reported as skipped, so an identifier present is one that was left out, and one absent
|
|
37
|
+
* was read when {@link complete}.
|
|
38
|
+
*
|
|
39
|
+
* Both hold only for the spelling the provider returned, which is also how a published issue's id spells its key: `keys`
|
|
40
|
+
* doesn't answer a lookup by another spelling, such as a different case or the key an issue had before it moved. A
|
|
41
|
+
* consumer comparing a key from elsewhere (one parsed from a branch name, say) must normalize the case of both sides,
|
|
42
|
+
* and can't take an issue under a key it once had as read.
|
|
38
43
|
*/
|
|
39
44
|
keys: string[];
|
|
40
45
|
/**
|
|
41
|
-
* Whether {@link keys} names every item left out. `false` when
|
|
42
|
-
*
|
|
43
|
-
*
|
|
46
|
+
* Whether {@link keys} names every item left out. `false` when an item left out had no key to report, when the
|
|
47
|
+
* provider's report of them was malformed (no list of keys, or more than keys in it), or when it said items were left
|
|
48
|
+
* out and named none, so an identifier absent from `keys` may still be one of them.
|
|
44
49
|
*/
|
|
45
50
|
complete: boolean;
|
|
46
51
|
}
|
|
@@ -477,6 +482,6 @@ export declare function appendDedupedWarning(into: ProviderWarning[], warning: P
|
|
|
477
482
|
* The items two reports of unreadable items left out together: the union of their keys, complete only when both are.
|
|
478
483
|
* `existing` when `next` adds nothing to it.
|
|
479
484
|
*/
|
|
480
|
-
export declare function mergeUnreadableItems(existing: ProviderWarningUnreadableItems
|
|
485
|
+
export declare function mergeUnreadableItems(existing: ProviderWarningUnreadableItems, next: ProviderWarningUnreadableItems): ProviderWarningUnreadableItems;
|
|
481
486
|
//#endregion
|
|
482
487
|
//# sourceMappingURL=results.d.ts.map
|
|
@@ -137,7 +137,7 @@ function appendDedupedWarning(into, warning) {
|
|
|
137
137
|
}
|
|
138
138
|
const existing = into[index];
|
|
139
139
|
const targetKeys = mergeTargetKeys(existing.targetKeys, warning.targetKeys);
|
|
140
|
-
const unreadableItems = mergeUnreadableItems(existing.unreadableItems, warning.unreadableItems);
|
|
140
|
+
const unreadableItems = existing.unreadableItems != null && warning.unreadableItems != null ? mergeUnreadableItems(existing.unreadableItems, warning.unreadableItems) : existing.unreadableItems ?? warning.unreadableItems;
|
|
141
141
|
if (targetKeys === existing.targetKeys && unreadableItems === existing.unreadableItems) return;
|
|
142
142
|
const { targetKeys: _, ...rest } = existing;
|
|
143
143
|
into[index] = {
|
|
@@ -158,9 +158,8 @@ function mergeTargetKeys(existing, next) {
|
|
|
158
158
|
* `existing` when `next` adds nothing to it.
|
|
159
159
|
*/
|
|
160
160
|
function mergeUnreadableItems(existing, next) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const added = next.keys.filter((k) => !existing.keys.includes(k));
|
|
161
|
+
const known = new Set(existing.keys);
|
|
162
|
+
const added = next.keys.filter((k) => !known.has(k));
|
|
164
163
|
const complete = existing.complete && next.complete;
|
|
165
164
|
if (!added.length && complete === existing.complete) return existing;
|
|
166
165
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"results.js","names":[],"sources":["../../../../plus/integrations/src/results.ts"],"sourcesContent":["import { isUnsupportedSortError } from '@gitkraken/provider-apis';\nimport { AuthenticationError, RequestNotFoundError, RequestRateLimitError } from '@gitlens/git/errors.js';\nimport type { IssueSorting } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSorting } from '@gitlens/git/models/pullRequest.js';\nimport type { IntegrationIds } from './constants.js';\nimport { isProviderUnreachableError } from './errors.js';\n\nexport interface ConnectionStateChangeEvent {\n\tkey: string;\n\treason: 'connected' | 'disconnected';\n}\n\n/**\n * A per-provider, per-connection warning surfaced alongside (partial) read results. Consumers use\n * these to drive auth recovery, retry, or truncation messaging without the read itself throwing.\n *\n * `kind` is the programmatic discriminant (a rate-limit is retryable, a 404 is not, an unsupported capability\n * never succeeds on retry, an auth failure needs re-connection unless {@link ProviderWarning.scope} confines\n * it); `isAuth` is retained as a convenience mirror of `kind === 'auth'`.\n *\n * `'unsupported'`: the provider — or this integration of it — lacks the capability the request asked for: a\n * surface (issues on Bitbucket, pull requests on an issue tracker, a batch read, org/repo discovery,\n * current-account lookup, project-scoped or account-wide issue reads), or a filter, sort, state, search\n * criterion or option combination it cannot express. Retrying the same request never succeeds; a different\n * provider, scope or request may. NOT unsupported (these stay `'other'`): malformed or contradictory caller\n * input the caller fixes in the request itself (a duplicate batch key, invalid target numbers, an empty owner,\n * an unusable scope), incompleteness or omission warnings, and unclassified errors.\n *\n * `'unreadable-items'`: the provider returned items this read could not read (a Jira issue whose fields the SDK cannot\n * parse), so it left them out and read everything else in the warning's {@link ProviderWarning.scope}. It still fails\n * the read (`fetchFailed`), since those items are missing, but retrying returns the same gap. On the reads that publish\n * it (an issue tracker's project reads) it never hides a read that was cut short: a cut always comes with a warning of\n * another kind. {@link ProviderWarning.unreadableItems} names the items left out.\n */\nexport type ProviderWarningKind =\n\t| 'auth'\n\t| 'rate-limit'\n\t| 'not-found'\n\t| 'no-connection'\n\t| 'unsupported'\n\t| 'unreadable-items'\n\t| 'other';\n\n/** The items an `'unreadable-items'` warning left out: see {@link ProviderWarning.unreadableItems}. */\nexport interface ProviderWarningUnreadableItems {\n\t/**\n\t * The provider's identifiers of the items left out (a Jira issue's key), without duplicates, in reported order. Read\n\t * from the provider's prose, it may also hold an identifier that only appeared in an item's error text, never lack\n\t * one it named: rely on an identifier's absence, not on its presence.\n\t */\n\tkeys: string[];\n\t/**\n\t * Whether {@link keys} names every item left out. `false` when the provider named only some of them (Jira names at\n\t * most ten per page), named one by position rather than by key, or reported them in a form this layer could not\n\t * read, so an identifier absent from `keys` may still be one of them.\n\t */\n\tcomplete: boolean;\n}\n\n/**\n * Why a read that SUCCEEDED still withheld results:\n * - `provider-limit`: the provider refuses to serve past a cap it enforces (GitHub search's 1,000-result\n * ceiling, Trello's `cards_limit`), so the excess is unreachable through that query.\n * - `recovery-budget`: the internal partitioned recovery stopped spending upstream requests before it ran\n * out of partitions to visit.\n * - `pagination-incomplete`: pages were left unread — a sub-scope of a multi-scope read that was not drained,\n * a drain that stopped at its own page backstop, or a provider that advertised another page and gave no way\n * to reach it. These have DIFFERENT remedies, so the kind alone does not say whether more can be fetched;\n * read {@link ProviderWarningOmission.recovery} for that.\n *\n * Mirrors the vocabulary the SDK reports. Declared here rather than imported so this module stays free of\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`); `collectionMetadata.ts` holds the\n * compile-time link, so an SDK bump that adds a kind fails the build at that boundary.\n */\nexport type ProviderWarningOmissionKind = 'provider-limit' | 'recovery-budget' | 'pagination-incomplete';\n\n/**\n * How a consumer can recover omitted results. `page-budget` reruns with a larger budget; `narrow-scope`\n * requests a smaller server-side scope; `none` promises neither. Switch on the exact value because only\n * `page-budget` is a fetch-more action.\n */\nexport type ProviderWarningOmissionRecovery = 'none' | 'page-budget' | 'narrow-scope';\n\n/**\n * The part of a provider a warning is confined to: one resource (organization, workspace, site), project or\n * repository. All fields optional; see {@link ProviderWarning.scope} for when one is forwarded.\n */\nexport interface ProviderWarningScope {\n\t/**\n\t * The resource as the read addressed it: its id on most reads, its name on the few that address it by name\n\t * (Azure DevOps' repo-scoped reads, Bitbucket's workspace reads). Match it against both\n\t * `ProviderOrganization.id` and `name`, and a project's `orgId` where it has one.\n\t */\n\tresourceId?: string;\n\t/**\n\t * The project as the read addressed it. For a Jira project read, the key, on Jira Cloud and Jira Data Center alike\n\t * (the project's `name` in `listProjects`).\n\t */\n\tprojectId?: string;\n\trepositoryId?: string;\n}\n\n/**\n * The selector of an issue-tracker project read that matched nothing: see {@link ProviderWarning.selector}. `kind`\n * names which one missed, and `value` is that selector exactly as the caller passed it.\n */\nexport interface ProviderWarningSelector {\n\tkind: 'org' | 'project';\n\tvalue: string;\n}\n\n/**\n * Why a provider request failed, when the failure itself says so:\n * - `unreachable`: a transport failure or server-side error prevented the provider from answering. The connection\n * still exists; this accompanies `kind: 'other'`, never an authentication warning.\n * - `oauth-app-not-allowed`: the organization does not let third-party OAuth apps in. On Azure DevOps this is the\n * organization policy \"Third-party application access via OAuth\", off by default for new organizations. An\n * organization admin enables it, or the user connects with a personal access token instead, which the policy\n * does not govern.\n * - `access-denied`: the account has no access to that organization or project (not a member, or lacking the\n * permission). Someone who administers it has to grant access.\n * - `conditional-access`: a Microsoft Entra Conditional Access policy blocked the request (Azure DevOps\n * `VS403463`). Its admin has to exempt the request, e.g. by location or device.\n *\n * None of these is healed by reconnecting, which is the point of naming them.\n */\nexport type ProviderWarningCauseReason =\n\t| 'oauth-app-not-allowed'\n\t| 'access-denied'\n\t| 'conditional-access'\n\t| 'unreachable';\n\n/** See {@link ProviderWarning.cause}. */\nexport interface ProviderWarningCause {\n\treason: ProviderWarningCauseReason;\n\t/** The provider's own error code, when it reports one (Azure DevOps `TF400813`, `VS403463`). */\n\tcode?: string;\n\t/** Where the setting behind the refusal is changed, when this layer can address it. */\n\tremedyUrl?: string;\n}\n\n/** Which repository / project / resource an omission is attributed to. All fields optional; a scope may name none. */\nexport interface ProviderWarningOmissionScope extends ProviderWarningScope {\n\tproviderId?: string;\n}\n\nexport interface ProviderWarningOmission {\n\tkind: ProviderWarningOmissionKind;\n\t/**\n\t * Whether anything would fetch the missing items, and what. Gate a \"load more\" affordance on this rather\n\t * than on `kind` — see {@link ProviderWarningOmissionRecovery}.\n\t */\n\trecovery: ProviderWarningOmissionRecovery;\n\t/**\n\t * The cap the provider enforces, when it reports one. NOT a result count for every kind: on\n\t * `recovery-budget` this is a REQUEST budget and must not be shown to a user as a number of results.\n\t */\n\tlimit?: number;\n\t/** Total matches the provider reported, when it reports one. Only GitHub's search cap does today. */\n\ttotalCount?: number;\n\t/**\n\t * The order the reachable window was selected under, as `field:direction`, when the omission is a result\n\t * ceiling.\n\t *\n\t * Present so a consumer can word its own \"N matched, showing the first M\" without assuming WHICH M: the\n\t * provider serves the top M under this key, so the sentence is only true if it names it. Absent for every\n\t * omission that isn't a ceiling, where no window was selected by an order.\n\t *\n\t * A union of both vocabularies rather than the issue one alone: `PullRequestSorting` happens to be a subset\n\t * today, so narrowing this to {@link IssueSorting} would still compile while describing only half the reads\n\t * that set it — and would break the day the PR search gains a key the issue search has no name for.\n\t */\n\tsort?: IssueSorting | PullRequestSorting;\n\t/** Which repository / project / resource was affected, when one is attributed. */\n\tscope?: ProviderWarningOmissionScope;\n}\n\nexport interface ProviderWarning {\n\tproviderId: IntegrationIds;\n\t/** Disambiguates connections on self-managed hosts (where one provider id spans multiple domains). */\n\tdomain?: string;\n\t/** The specific token/connection the read was pinned to, when a `connectionId` was supplied. */\n\tconnectionId?: string;\n\tmessage: string;\n\tkind: ProviderWarningKind;\n\t/** Convenience mirror of `kind === 'auth'`, scoped or not; read {@link scope} before prompting to reconnect. */\n\tisAuth: boolean;\n\t/**\n\t * Present when the failure was recorded against one sub-scope of the read (an organization, project or\n\t * repository) rather than against the connection. A scoped `auth` failure is that one scope refusing this\n\t * credential, e.g. an Azure DevOps organization that disallows third-party OAuth apps, one in another Entra\n\t * tenant, or a Conditional Access policy. Reconnecting cannot fix those, so this warning alone is no reason\n\t * to prompt for it.\n\t *\n\t * `kind` and `isAuth` are unaffected: a scoped 401 is still an authentication failure, and this field says\n\t * how far it reaches. It names at least one of its IDs, and is never set on an omission (which carries its\n\t * own {@link ProviderWarningOmission.scope}) or on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}), even when that call targeted a single organization. The exception is a batch\n\t * read's refused target, which names its scope: the Azure DevOps organization and project, the repository on\n\t * other git hosts, or the tracker's resource.\n\t *\n\t * Its ABSENCE means account-wide or unattributed, so existing handling stays correct without it.\n\t *\n\t * A scoped `auth` failure also means the credential itself was accepted. A dead token can come back as nothing\n\t * but scoped refusals wherever a read reaches its scopes without an uncached request to the connection first:\n\t * discovery served from a per-token cache (Azure DevOps, Bitbucket, Jira Cloud) or an SDK fan-out across the\n\t * requested repositories (Bitbucket Data Center). So when a read's only auth failures are scoped, those providers\n\t * confirm the credential with one uncached check first, and a refused credential fails the whole read with an\n\t * unscoped `auth` warning. A batch read checks it only when every target was refused, since a target that\n\t * answered already proved it. A refusal the provider attributes to the credential itself, like Bitbucket's or Jira\n\t * Cloud's for a token missing the OAuth scopes a read needs, which a reconnect fixes, is published unscoped too,\n\t * once for the connection, while the scopes that answered keep their results.\n\t *\n\t * So the promise holds for a read that carries no unscoped `auth` warning of its own; one that does already asks\n\t * for a reconnect, and its other scoped refusals are not checked. Two exceptions stay scoped even then. A check\n\t * that could not complete or was denied (a network error, a throttle, a `403`, Bitbucket Data Center refusing a\n\t * credential it authenticated, or a Bitbucket Data Center project access token, whose user the check cannot\n\t * find) proves nothing, so the warnings are published unconfirmed, with no {@link cause}, and the next read\n\t * checks again. And a credential confirmed within the last minute is not probed again, so a revocation can take\n\t * up to a minute to surface.\n\t */\n\tscope?: ProviderWarningScope;\n\t/**\n\t * Why the provider request failed, when the failure itself says so: see {@link ProviderWarningCauseReason}. Switch on\n\t * `reason` to recommend a fix instead of a reconnect; `message` then carries the same explanation as prose.\n\t *\n\t * `unreachable` accompanies a transport or server failure with `kind: 'other'`. Credential refusal causes are\n\t * set only on a scoped `auth` warning whose credential was confirmed first (see {@link scope}), because the\n\t * refusals they name are indistinguishable from a dead credential until then: Azure DevOps answers a\n\t * third-party OAuth app its organization disallows exactly as it answers an expired token. Only Azure DevOps\n\t * reports one today. Its absence proves nothing about the cause.\n\t */\n\tcause?: ProviderWarningCause;\n\t/**\n\t * Present when this warning describes results the read could not return even though the request itself\n\t * SUCCEEDED — a provider-enforced cap, an exhausted recovery budget, a page budget, or a sub-scope the read\n\t * did not drain.\n\t *\n\t * Its presence is the signal: an omission is not a failure. Message it as incompleteness rather than as a\n\t * failed read, and do NOT derive that from `message`, which is English prose and subject to rewording.\n\t * `kind` stays `'other'` for these, so the failure discriminant keeps meaning exactly what it meant before\n\t * this field existed.\n\t *\n\t * Whether anything would fetch the rest is a SEPARATE question — read {@link ProviderWarningOmission.recovery},\n\t * not `kind`, and never assume a retry of the same request returns more.\n\t *\n\t * `limit` / `totalCount` / `scope` are forwarded only when they are reported; most omissions carry none of\n\t * the three, so render correctly without them — and see {@link ProviderWarningOmission.limit} before\n\t * showing that figure as a result count.\n\t *\n\t * Its ABSENCE proves nothing. It is never set on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}) or from a structured scope failure — those are failures, and the field would\n\t * be a lie there. But it is also absent whenever incompleteness was reported without naming what was left\n\t * out, so treat a bare `kind: 'other'` warning as unclassified rather than as a proven failure.\n\t */\n\tomission?: ProviderWarningOmission;\n\t/**\n\t * Present exactly when {@link kind} is `'unreadable-items'`: which items the read left out because it could not read\n\t * them. A consumer deciding whether an identifier is absent from the provider, rather than merely unread, must find\n\t * it in no warning's `keys` and see `complete` on every one.\n\t */\n\tunreadableItems?: ProviderWarningUnreadableItems;\n\t/**\n\t * Present when an issue-tracker project read's `org` or `project` selector matched nothing the connection\n\t * discovered: the selection was removed, renamed, or is no longer visible to this connection. `kind` is\n\t * `'not-found'`, the page is empty and not `fetchFailed`, and it carries no cursor, since a retry finds the same.\n\t *\n\t * Set only when discovery was complete. A read whose discovery failed or was truncated cannot tell a missing\n\t * selection from one it did not reach, so it keeps reporting that failure instead, without this field. A miss is\n\t * confirmed against the connection's lists as the provider holds them now, read without the cache, since a cached\n\t * one predates what was created since. A cached project list can still name a project deleted since: the tracker\n\t * refuses it (Jira's \"does not exist for the field 'project'\"), it reads as empty, and the refusal drops that list,\n\t * so the next read reports the miss. Jira Data Center's refusal counts only when it names the user it\n\t * authenticated: it answers a rejected token the same way, as the anonymous user. The miss waits on an account\n\t * lookup on a resource those lists name, which confirms the\n\t * connection's credential, and a failed lookup is reported as the read's failure instead; an account that lists no\n\t * resource at all has none to look up, and is judged on its lists alone.\n\t *\n\t * It is the discriminant: a `not-found` without it is a request that failed with a 404, 410 or 422.\n\t */\n\tselector?: ProviderWarningSelector;\n\t/**\n\t * The keys of the batch targets this warning concerns: the target a per-target failure was refused or failed\n\t * for, or every target of an integration call that failed as a whole. So a caller that sends several callers'\n\t * targets in one call can hand a failure only to the targets it belongs to.\n\t *\n\t * Its ABSENCE means the whole call. It is absent on every read that is not a batch read, and on a batch read's\n\t * refusal of the whole call before any target is read (a duplicate key, an invalid target, an unknown etag\n\t * include, no resolvable connection, the wrong surface), which concerns every target alike.\n\t *\n\t * It is not part of a warning's identity: two warnings that differ only here are one warning, for the union of\n\t * their targets (see {@link appendDedupedWarning}).\n\t */\n\ttargetKeys?: string[];\n}\n\nexport interface ProviderPageInfo {\n\t/**\n\t * The 1-based POSITION of the returned page. One convention across every paged read, page-number and\n\t * cursor paging alike, so a `getNextPageParam`-style consumer can key off it uniformly:\n\t * - the page number the provider itself reported, when it reports one (authoritative);\n\t * - else the position the caller addressed: the page encoded in a threaded page cursor, or the `page`\n\t * supplied alongside an opaque cursor (1 when the caller supplied no `page`);\n\t * - else, for a read that can be advanced by page number, the requested `page` — the provider either\n\t * honored it or the internal drain reached it (see the paging contract on `IntegrationManager`);\n\t * - else 1: a cursor-only read with no cursor threaded back ignores a requested page number and answers\n\t * with its first page, so echoing the request would mislabel page 1 as page N.\n\t *\n\t * A page past the provider's terminal cursor still reports the requested `page` with empty `items` — an\n\t * empty page N, never page N-1 relabeled.\n\t *\n\t * Continue a cursor-paged read from `cursor`, NOT from `currentPage + 1`: a cursor-only host can't be\n\t * addressed by number, so this is a position label and not always a resumable request parameter.\n\t */\n\tcurrentPage: number;\n\t/**\n\t * The page size IN EFFECT, which is not always the one requested — it is whichever of these the read can\n\t * actually vouch for:\n\t * - the size the provider reported for the page it served (numbered-page hosts), else\n\t * - the `itemsPerPage` that was applied to the request (repo-scoped reads, which forward it), else\n\t * - `items.length`, for a read that takes no page size at all: the account-wide PR/issue reads, `listRepos`\n\t * (cursor-only), and `listIssueTrackerIssuesPage`, whose `itemsPerPage` counts PROJECTS rather than issues.\n\t *\n\t * So it is a description of the page, not an echo of the request: don't derive a total from\n\t * `currentPage * itemsPerPage`, and don't read `items.length < itemsPerPage` as \"last page\" — `hasMore` /\n\t * `cursor` are the only continuation signals.\n\t */\n\titemsPerPage: number;\n\t/**\n\t * A COMPLETENESS assertion, not a mode flag: `true` only when a sweep drained every page of every target\n\t * without truncating or failing (`!truncated && !fetchFailed`). Set exclusively by `sweepPullRequests` /\n\t * `sweepClosedPullRequests` and absent on every paged read, so `allPages !== true` does NOT by itself mean\n\t * \"this was a single page\".\n\t *\n\t * This is the signal to gate on before caching a sweep as authoritative: unlike `truncated` it is `false`\n\t * for BOTH kinds of incompleteness, so it can't be misread as a benign cap.\n\t */\n\tallPages?: boolean;\n\t/**\n\t * True when the read stopped before it could confirm it had everything. Deliberately does NOT distinguish\n\t * WHY, and a consumer must not assume it means a provider ceiling — a sweep sets it from three different\n\t * origins:\n\t * - a provider-side ceiling or per-scope backstop propagated from below (e.g. GitHub Search's 1000-result\n\t * cap): the missing items are NOT reachable, so this really is \"complete to the provider's limit\";\n\t * - the sweep's OWN `maxPages` cap (default 100) with a usable cursor still in hand: the missing items ARE\n\t * reachable and were simply not fetched — raise `maxPages` to get them;\n\t * - the provider reporting another page but handing back no usable cursor: not reachable.\n\t *\n\t * Only the second is recoverable, and a sweep exposes no continuation cursor, so treating a truncated sweep\n\t * as a complete set silently drops reachable data in that case. Prefer `allPages` for the cache decision.\n\t */\n\ttruncated?: boolean;\n}\n\nexport interface ProviderResult<T> {\n\titems: T[];\n\twarnings: ProviderWarning[];\n\t/**\n\t * True when `items` is incomplete — distinguishes a failed or truncated flat read from a genuinely empty\n\t * result. Paged results additionally use `page.truncated` when a completed page could not confirm it\n\t * drained everything.\n\t */\n\tfetchFailed?: boolean;\n}\n\nexport interface ProviderPagedResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Opaque cursor for cursor-based paging (GraphQL, per-repo/per-project cursors, etc.). For hosts that\n\t * require the previous page's cursor, consumers should pass this value back to the next `cursor` option.\n\t * A retry-only cursor may remain when `hasMore` is false: `hasMore` means automatic forward progress,\n\t * while explicitly reusing such a cursor retries failed work without creating an infinite paging loop.\n\t */\n\tcursor?: string;\n}\n\nexport interface ProviderSweepResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Providers whose top-level read failed before returning any usable page. Partial per-scope failures stay\n\t * represented by `warnings` + `fetchFailed` and are intentionally excluded.\n\t */\n\tfailedProviderIds: IntegrationIds[];\n\t/**\n\t * Providers whose read started successfully but could not produce an authoritative complete slice (for\n\t * example, a later page failed, collection metadata reported a partial scope, or a paging backstop was hit).\n\t * Kept separate from\n\t * `failedProviderIds` so consumers can accept healthy sibling providers while preserving an older snapshot\n\t * for a provider whose returned slice has a gap.\n\t */\n\tincompleteProviderIds: IntegrationIds[];\n}\n\nexport interface ProviderBroadenResult<T> extends ProviderPagedResult<T> {\n\tbroadenedProviderIds: IntegrationIds[];\n\t/** Providers for which no requested org produced a usable issue result. */\n\tfailedProviderIds: IntegrationIds[];\n\t/** Providers with both a usable org result and at least one failed or truncated org slice. */\n\tincompleteProviderIds: IntegrationIds[];\n\tfanOutCount: number;\n}\n\nexport type RepositoryResolutionStatus =\n\t| 'resolved'\n\t| 'not-found'\n\t| 'unauthorized'\n\t| 'unsupported-provider'\n\t| 'invalid-remote-url'\n\t| 'host-mismatch'\n\t| 'undetermined';\n\nexport interface RepositoryIdentity {\n\tproviderId: IntegrationIds;\n\tdomain: string;\n\t/** The provider's canonical owner/namespace (follows renames), falling back to the parsed remote when omitted. */\n\towner: string;\n\t/** The provider's canonical repo name (follows renames), falling back to the parsed remote when omitted. */\n\tname: string;\n\tproject?: string;\n\t/** The original input remote URL, so the caller can key the resolution back to the remote it asked about. */\n\tremoteUrl: string;\n\t/**\n\t * True when the provider's canonical owner/name differ from what the input remote URL carried — i.e. the\n\t * repo was renamed/moved host-side and the local remote is stale. Case-insensitive, mirroring gkcli's\n\t * `EqualFold` compare so hosts that echo the input casing (e.g. Bitbucket Server/Azure) aren't flagged.\n\t */\n\trenamed: boolean;\n}\n\nexport interface RepositoryResolution {\n\tstatus: RepositoryResolutionStatus;\n\tidentity?: RepositoryIdentity;\n\twarning?: ProviderWarning;\n}\n\nexport interface ResolveRepositoryResult {\n\tresolution: RepositoryResolution;\n}\n\nconst maxProviderWarningMessageLength = 500;\n\n/** A caught error's message as a warning can carry it: an HTML page becomes its status, and prose is capped. */\nexport function providerWarningMessage(ex: unknown): string {\n\tconst raw = (ex instanceof Error ? ex.message : String(ex)).trim();\n\tconst carrier = ex as { status?: unknown; response?: { status?: unknown } } | null | undefined;\n\tconst status =\n\t\ttypeof carrier?.status === 'number'\n\t\t\t? carrier.status\n\t\t\t: typeof carrier?.response?.status === 'number'\n\t\t\t\t? carrier.response.status\n\t\t\t\t: undefined;\n\n\t// Gateways commonly answer API requests with a complete HTML error page. That payload is neither actionable\n\t// nor suitable for a warning DTO consumed by UIs and logs; preserve the status without leaking the document.\n\tif (/<(?:!doctype\\s+html|html|head|body)\\b/i.test(raw)) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\n\tif (!raw) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\treturn raw.length > maxProviderWarningMessageLength\n\t\t? `${raw.slice(0, maxProviderWarningMessageLength - 3)}...`\n\t\t: raw;\n}\n\n/**\n * Normalized org/workspace/group shape returned by the provider facade.\n * `name` is the provider identifier to pass to follow-up reads (a Jira project's key), while `org` is an optional\n * display label of the parent. Where that label can't address the parent, `orgId` is the parent's stable id to pass\n * as `org` instead (a Jira site's id, or a Data Center instance's domain).\n *\n * Jira site rows (`listOrgs`) are the exception: their `name` is the site's label, and a Jira read selects a site only\n * by its `id` (or a project row's `orgId`).\n */\nexport interface ProviderOrganization {\n\tid: string;\n\tproviderId: IntegrationIds;\n\tname: string;\n\torg?: string;\n\torgId?: string;\n\turl: string;\n}\n\n/** Normalized repository shape returned by the provider facade. */\nexport interface ProviderRepositoryShape {\n\tid: string;\n\tnamespace: string;\n\tname: string;\n\t/** Azure DevOps project; `undefined` for hosts without a project layer. */\n\tproject?: string;\n\t/** Web (browser) URL, when the provider exposes it. */\n\turl?: string;\n\t/** HTTPS clone URL, when available. */\n\tcloneUrlHttps?: string;\n\t/** SSH clone URL, when available. */\n\tcloneUrlSsh?: string;\n\t/** Default branch name, when the provider reports it. */\n\tdefaultBranch?: string;\n}\n\n/**\n * Classifies a caught provider exception into a neutral {@link ProviderWarning}. Ordering matters:\n * `instanceof` is checked most-specific-first so a rate-limit isn't mislabeled as a generic error and\n * a 404 isn't mislabeled as auth.\n */\nexport function toProviderWarning(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tex: unknown,\n): ProviderWarning {\n\tlet kind: ProviderWarning['kind'];\n\tif (ex instanceof AuthenticationError) {\n\t\tkind = 'auth';\n\t} else if (ex instanceof RequestRateLimitError) {\n\t\tkind = 'rate-limit';\n\t} else if (ex instanceof RequestNotFoundError) {\n\t\tkind = 'not-found';\n\t} else if (isUnsupportedSortError(ex)) {\n\t\tkind = 'unsupported';\n\t} else {\n\t\tkind = 'other';\n\t}\n\n\treturn {\n\t\tproviderId: providerId,\n\t\tdomain: domain,\n\t\tconnectionId: connectionId,\n\t\tmessage: providerWarningMessage(ex),\n\t\tkind: kind,\n\t\tisAuth: kind === 'auth',\n\t\t...(kind === 'other' && isProviderUnreachableError(ex) ? { cause: { reason: 'unreachable' as const } } : {}),\n\t};\n}\n\n/**\n * A scope's identity, as the stable string that keys it.\n *\n * The single definition of what \"the same scope\" means, shared by the failure and omission dedup keys in\n * `providerPaging.ts` and by {@link providerWarningKey} below, so a scope gaining a field is one edit rather\n * than three. `providerId` is included: the same repository ID under two providers is two scopes.\n *\n * The parameter is structural rather than the SDK's `CollectionScope` so this module keeps naming no\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`) while still serving its SDK-facing\n * callers, which pass that type in unchanged.\n */\nexport function collectionScopeKey(scope: ProviderWarningOmissionScope | undefined): string {\n\treturn [scope?.providerId ?? '', scope?.resourceId ?? '', scope?.projectId ?? '', scope?.repositoryId ?? ''].join(\n\t\t' ',\n\t);\n}\n\n/**\n * The omission's contribution to a warning's identity — empty when there is none, so a warning without an\n * omission keeps deduping exactly as it did before the field existed.\n *\n * Two omissions of different kinds do produce different `message` values today, so message alone would still\n * separate them. That is incidental: the premise of `omission` is that consumers must not depend on prose\n * carrying the distinguishing fact, and this key must not either.\n */\nfunction providerWarningOmissionKey(omission: ProviderWarningOmission | undefined): string {\n\tif (omission == null) return '';\n\n\treturn [\n\t\tomission.kind,\n\t\tomission.recovery,\n\t\tomission.limit ?? '',\n\t\tomission.totalCount ?? '',\n\t\tcollectionScopeKey(omission.scope),\n\t].join(' ');\n}\n\n/**\n * Strips the omission from every warning in `warnings` when the read as a whole failed.\n *\n * An omission asserts the request SUCCEEDED, and a drain only learns it failed AFTER it may have emitted one:\n * an early page can report its own truncation and then a later page can die. Deciding per warning, at the\n * moment each is built, cannot see that future — so the aggregate is reconciled once, here, where\n * `fetchFailed` is final. Call it at the point a read returns its `fetchFailed`.\n *\n * Re-dedupes as it goes: the omission is part of a warning's identity, so two warnings that differed only\n * there become identical once it is gone, and the array's contract is that no two entries are equal.\n *\n * Mutates in place: the warning array is the one being returned, and callers accumulate into it across pages.\n */\nexport function reconcileOmissionsWithFailure(warnings: ProviderWarning[], fetchFailed: boolean): void {\n\tif (!fetchFailed || !warnings.some(w => w.omission != null)) return;\n\n\tconst reconciled: ProviderWarning[] = [];\n\tfor (const warning of warnings) {\n\t\tif (warning.omission == null) {\n\t\t\tappendDedupedWarning(reconciled, warning);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { omission: _omission, ...rest } = warning;\n\t\tappendDedupedWarning(reconciled, rest);\n\t}\n\n\twarnings.splice(0, warnings.length, ...reconciled);\n}\n\n/**\n * A stable key for deduplicating warnings accumulated across drained pages / fan-out scopes.\n *\n * The scope and the cause are keyed for the reason the omission is (see {@link providerWarningOmissionKey}): today's\n * failure messages happen to spell them out, but two failures that differ in either must stay two warnings even if\n * they don't.\n *\n * `message` stays LAST. It is the only free-form segment — provider prose, spaces and all — so anything\n * appended after it could be impersonated by a message that happens to end in the same text.\n */\nfunction providerWarningKey(warning: ProviderWarning): string {\n\treturn [\n\t\twarning.providerId,\n\t\twarning.connectionId ?? '',\n\t\twarning.domain ?? '',\n\t\twarning.kind,\n\t\tcollectionScopeKey(warning.scope),\n\t\twarning.cause?.reason ?? '',\n\t\tproviderWarningOmissionKey(warning.omission),\n\t\twarning.selector != null ? JSON.stringify([warning.selector.kind, warning.selector.value]) : '',\n\t\twarning.message,\n\t].join(' ');\n}\n\n/**\n * Appends `warning` to `into` only when an equal warning (by provider/connection/domain/kind/message, plus the\n * structured scope, cause, omission and selector when present) is absent. An equal warning already there merges its\n * {@link ProviderWarning.targetKeys} with the new one's instead: the union of both, or none when either concerns\n * the whole call. Its {@link ProviderWarning.unreadableItems} merge too (see {@link mergeUnreadableItems}): two reads\n * can report the same text for different items. The existing warning is replaced, never mutated, since it may be\n * shared.\n */\nexport function appendDedupedWarning(into: ProviderWarning[], warning: ProviderWarning): void {\n\tconst key = providerWarningKey(warning);\n\tconst index = into.findIndex(existing => providerWarningKey(existing) === key);\n\tif (index === -1) {\n\t\tinto.push(warning);\n\t\treturn;\n\t}\n\n\tconst existing = into[index];\n\tconst targetKeys = mergeTargetKeys(existing.targetKeys, warning.targetKeys);\n\tconst unreadableItems = mergeUnreadableItems(existing.unreadableItems, warning.unreadableItems);\n\tif (targetKeys === existing.targetKeys && unreadableItems === existing.unreadableItems) return;\n\n\tconst { targetKeys: _, ...rest } = existing;\n\tinto[index] = {\n\t\t...rest,\n\t\t...(targetKeys != null ? { targetKeys: targetKeys } : {}),\n\t\t...(unreadableItems != null ? { unreadableItems: unreadableItems } : {}),\n\t};\n}\n\n/** The union of two warnings' target keys, or none when either concerns the whole call. `existing` when unchanged. */\nfunction mergeTargetKeys(existing: string[] | undefined, next: string[] | undefined): string[] | undefined {\n\t// Either concerns the whole call.\n\tif (existing == null || next == null) return undefined;\n\n\tconst added = next.filter(k => !existing.includes(k));\n\tif (!added.length) return existing;\n\n\treturn [...existing, ...new Set(added)];\n}\n\n/**\n * The items two reports of unreadable items left out together: the union of their keys, complete only when both are.\n * `existing` when `next` adds nothing to it.\n */\nexport function mergeUnreadableItems(\n\texisting: ProviderWarningUnreadableItems | undefined,\n\tnext: ProviderWarningUnreadableItems | undefined,\n): ProviderWarningUnreadableItems | undefined {\n\tif (existing == null) return next;\n\tif (next == null) return existing;\n\n\tconst added = next.keys.filter(k => !existing.keys.includes(k));\n\tconst complete = existing.complete && next.complete;\n\tif (!added.length && complete === existing.complete) return existing;\n\n\treturn { keys: [...existing.keys, ...new Set(added)], complete: complete };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"results.js","names":[],"sources":["../../../../plus/integrations/src/results.ts"],"sourcesContent":["import { isUnsupportedSortError } from '@gitkraken/provider-apis';\nimport { AuthenticationError, RequestNotFoundError, RequestRateLimitError } from '@gitlens/git/errors.js';\nimport type { IssueSorting } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSorting } from '@gitlens/git/models/pullRequest.js';\nimport type { IntegrationIds } from './constants.js';\nimport { isProviderUnreachableError } from './errors.js';\n\nexport interface ConnectionStateChangeEvent {\n\tkey: string;\n\treason: 'connected' | 'disconnected';\n}\n\n/**\n * A per-provider, per-connection warning surfaced alongside (partial) read results. Consumers use\n * these to drive auth recovery, retry, or truncation messaging without the read itself throwing.\n *\n * `kind` is the programmatic discriminant (a rate-limit is retryable, a 404 is not, an unsupported capability\n * never succeeds on retry, an auth failure needs re-connection unless {@link ProviderWarning.scope} confines\n * it); `isAuth` is retained as a convenience mirror of `kind === 'auth'`.\n *\n * `'unsupported'`: the provider — or this integration of it — lacks the capability the request asked for: a\n * surface (issues on Bitbucket, pull requests on an issue tracker, a batch read, org/repo discovery,\n * current-account lookup, project-scoped or account-wide issue reads), or a filter, sort, state, search\n * criterion or option combination it cannot express. Retrying the same request never succeeds; a different\n * provider, scope or request may. NOT unsupported (these stay `'other'`): malformed or contradictory caller\n * input the caller fixes in the request itself (a duplicate batch key, invalid target numbers, an empty owner,\n * an unusable scope), incompleteness or omission warnings, and unclassified errors.\n *\n * `'unreadable-items'`: the provider returned items this read could not read (a Jira issue whose fields the SDK cannot\n * parse), so it left them out and read everything else in the warning's {@link ProviderWarning.scope}. It still fails\n * the read (`fetchFailed`), since those items are missing, but retrying returns the same gap. On the reads that publish\n * it (an issue tracker's project reads) it never hides a read that was cut short: a cut always comes with a warning of\n * another kind. {@link ProviderWarning.unreadableItems} names the items left out.\n */\nexport type ProviderWarningKind =\n\t| 'auth'\n\t| 'rate-limit'\n\t| 'not-found'\n\t| 'no-connection'\n\t| 'unsupported'\n\t| 'unreadable-items'\n\t| 'other';\n\n/** The items an `'unreadable-items'` warning left out: see {@link ProviderWarning.unreadableItems}. */\nexport interface ProviderWarningUnreadableItems {\n\t/**\n\t * The provider's identifiers of the items left out (a Jira issue's key), without duplicates, in reported order. Each\n\t * one is an item the provider reported as skipped, so an identifier present is one that was left out, and one absent\n\t * was read when {@link complete}.\n\t *\n\t * Both hold only for the spelling the provider returned, which is also how a published issue's id spells its key: `keys`\n\t * doesn't answer a lookup by another spelling, such as a different case or the key an issue had before it moved. A\n\t * consumer comparing a key from elsewhere (one parsed from a branch name, say) must normalize the case of both sides,\n\t * and can't take an issue under a key it once had as read.\n\t */\n\tkeys: string[];\n\t/**\n\t * Whether {@link keys} names every item left out. `false` when an item left out had no key to report, when the\n\t * provider's report of them was malformed (no list of keys, or more than keys in it), or when it said items were left\n\t * out and named none, so an identifier absent from `keys` may still be one of them.\n\t */\n\tcomplete: boolean;\n}\n\n/**\n * Why a read that SUCCEEDED still withheld results:\n * - `provider-limit`: the provider refuses to serve past a cap it enforces (GitHub search's 1,000-result\n * ceiling, Trello's `cards_limit`), so the excess is unreachable through that query.\n * - `recovery-budget`: the internal partitioned recovery stopped spending upstream requests before it ran\n * out of partitions to visit.\n * - `pagination-incomplete`: pages were left unread — a sub-scope of a multi-scope read that was not drained,\n * a drain that stopped at its own page backstop, or a provider that advertised another page and gave no way\n * to reach it. These have DIFFERENT remedies, so the kind alone does not say whether more can be fetched;\n * read {@link ProviderWarningOmission.recovery} for that.\n *\n * Mirrors the vocabulary the SDK reports. Declared here rather than imported so this module stays free of\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`); `collectionMetadata.ts` holds the\n * compile-time link, so an SDK bump that adds a kind fails the build at that boundary.\n */\nexport type ProviderWarningOmissionKind = 'provider-limit' | 'recovery-budget' | 'pagination-incomplete';\n\n/**\n * How a consumer can recover omitted results. `page-budget` reruns with a larger budget; `narrow-scope`\n * requests a smaller server-side scope; `none` promises neither. Switch on the exact value because only\n * `page-budget` is a fetch-more action.\n */\nexport type ProviderWarningOmissionRecovery = 'none' | 'page-budget' | 'narrow-scope';\n\n/**\n * The part of a provider a warning is confined to: one resource (organization, workspace, site), project or\n * repository. All fields optional; see {@link ProviderWarning.scope} for when one is forwarded.\n */\nexport interface ProviderWarningScope {\n\t/**\n\t * The resource as the read addressed it: its id on most reads, its name on the few that address it by name\n\t * (Azure DevOps' repo-scoped reads, Bitbucket's workspace reads). Match it against both\n\t * `ProviderOrganization.id` and `name`, and a project's `orgId` where it has one.\n\t */\n\tresourceId?: string;\n\t/**\n\t * The project as the read addressed it. For a Jira project read, the key, on Jira Cloud and Jira Data Center alike\n\t * (the project's `name` in `listProjects`).\n\t */\n\tprojectId?: string;\n\trepositoryId?: string;\n}\n\n/**\n * The selector of an issue-tracker project read that matched nothing: see {@link ProviderWarning.selector}. `kind`\n * names which one missed, and `value` is that selector exactly as the caller passed it.\n */\nexport interface ProviderWarningSelector {\n\tkind: 'org' | 'project';\n\tvalue: string;\n}\n\n/**\n * Why a provider request failed, when the failure itself says so:\n * - `unreachable`: a transport failure or server-side error prevented the provider from answering. The connection\n * still exists; this accompanies `kind: 'other'`, never an authentication warning.\n * - `oauth-app-not-allowed`: the organization does not let third-party OAuth apps in. On Azure DevOps this is the\n * organization policy \"Third-party application access via OAuth\", off by default for new organizations. An\n * organization admin enables it, or the user connects with a personal access token instead, which the policy\n * does not govern.\n * - `access-denied`: the account has no access to that organization or project (not a member, or lacking the\n * permission). Someone who administers it has to grant access.\n * - `conditional-access`: a Microsoft Entra Conditional Access policy blocked the request (Azure DevOps\n * `VS403463`). Its admin has to exempt the request, e.g. by location or device.\n *\n * None of these is healed by reconnecting, which is the point of naming them.\n */\nexport type ProviderWarningCauseReason =\n\t| 'oauth-app-not-allowed'\n\t| 'access-denied'\n\t| 'conditional-access'\n\t| 'unreachable';\n\n/** See {@link ProviderWarning.cause}. */\nexport interface ProviderWarningCause {\n\treason: ProviderWarningCauseReason;\n\t/** The provider's own error code, when it reports one (Azure DevOps `TF400813`, `VS403463`). */\n\tcode?: string;\n\t/** Where the setting behind the refusal is changed, when this layer can address it. */\n\tremedyUrl?: string;\n}\n\n/** Which repository / project / resource an omission is attributed to. All fields optional; a scope may name none. */\nexport interface ProviderWarningOmissionScope extends ProviderWarningScope {\n\tproviderId?: string;\n}\n\nexport interface ProviderWarningOmission {\n\tkind: ProviderWarningOmissionKind;\n\t/**\n\t * Whether anything would fetch the missing items, and what. Gate a \"load more\" affordance on this rather\n\t * than on `kind` — see {@link ProviderWarningOmissionRecovery}.\n\t */\n\trecovery: ProviderWarningOmissionRecovery;\n\t/**\n\t * The cap the provider enforces, when it reports one. NOT a result count for every kind: on\n\t * `recovery-budget` this is a REQUEST budget and must not be shown to a user as a number of results.\n\t */\n\tlimit?: number;\n\t/** Total matches the provider reported, when it reports one. Only GitHub's search cap does today. */\n\ttotalCount?: number;\n\t/**\n\t * The order the reachable window was selected under, as `field:direction`, when the omission is a result\n\t * ceiling.\n\t *\n\t * Present so a consumer can word its own \"N matched, showing the first M\" without assuming WHICH M: the\n\t * provider serves the top M under this key, so the sentence is only true if it names it. Absent for every\n\t * omission that isn't a ceiling, where no window was selected by an order.\n\t *\n\t * A union of both vocabularies rather than the issue one alone: `PullRequestSorting` happens to be a subset\n\t * today, so narrowing this to {@link IssueSorting} would still compile while describing only half the reads\n\t * that set it — and would break the day the PR search gains a key the issue search has no name for.\n\t */\n\tsort?: IssueSorting | PullRequestSorting;\n\t/** Which repository / project / resource was affected, when one is attributed. */\n\tscope?: ProviderWarningOmissionScope;\n}\n\nexport interface ProviderWarning {\n\tproviderId: IntegrationIds;\n\t/** Disambiguates connections on self-managed hosts (where one provider id spans multiple domains). */\n\tdomain?: string;\n\t/** The specific token/connection the read was pinned to, when a `connectionId` was supplied. */\n\tconnectionId?: string;\n\tmessage: string;\n\tkind: ProviderWarningKind;\n\t/** Convenience mirror of `kind === 'auth'`, scoped or not; read {@link scope} before prompting to reconnect. */\n\tisAuth: boolean;\n\t/**\n\t * Present when the failure was recorded against one sub-scope of the read (an organization, project or\n\t * repository) rather than against the connection. A scoped `auth` failure is that one scope refusing this\n\t * credential, e.g. an Azure DevOps organization that disallows third-party OAuth apps, one in another Entra\n\t * tenant, or a Conditional Access policy. Reconnecting cannot fix those, so this warning alone is no reason\n\t * to prompt for it.\n\t *\n\t * `kind` and `isAuth` are unaffected: a scoped 401 is still an authentication failure, and this field says\n\t * how far it reaches. It names at least one of its IDs, and is never set on an omission (which carries its\n\t * own {@link ProviderWarningOmission.scope}) or on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}), even when that call targeted a single organization. The exception is a batch\n\t * read's refused target, which names its scope: the Azure DevOps organization and project, the repository on\n\t * other git hosts, or the tracker's resource.\n\t *\n\t * Its ABSENCE means account-wide or unattributed, so existing handling stays correct without it.\n\t *\n\t * A scoped `auth` failure also means the credential itself was accepted. A dead token can come back as nothing\n\t * but scoped refusals wherever a read reaches its scopes without an uncached request to the connection first:\n\t * discovery served from a per-token cache (Azure DevOps, Bitbucket, Jira Cloud) or an SDK fan-out across the\n\t * requested repositories (Bitbucket Data Center). So when a read's only auth failures are scoped, those providers\n\t * confirm the credential with one uncached check first, and a refused credential fails the whole read with an\n\t * unscoped `auth` warning. A batch read checks it only when every target was refused, since a target that\n\t * answered already proved it. A refusal the provider attributes to the credential itself, like Bitbucket's or Jira\n\t * Cloud's for a token missing the OAuth scopes a read needs, which a reconnect fixes, is published unscoped too,\n\t * once for the connection, while the scopes that answered keep their results.\n\t *\n\t * So the promise holds for a read that carries no unscoped `auth` warning of its own; one that does already asks\n\t * for a reconnect, and its other scoped refusals are not checked. Two exceptions stay scoped even then. A check\n\t * that could not complete or was denied (a network error, a throttle, a `403`, Bitbucket Data Center refusing a\n\t * credential it authenticated, or a Bitbucket Data Center project access token, whose user the check cannot\n\t * find) proves nothing, so the warnings are published unconfirmed, with no {@link cause}, and the next read\n\t * checks again. And a credential confirmed within the last minute is not probed again, so a revocation can take\n\t * up to a minute to surface.\n\t */\n\tscope?: ProviderWarningScope;\n\t/**\n\t * Why the provider request failed, when the failure itself says so: see {@link ProviderWarningCauseReason}. Switch on\n\t * `reason` to recommend a fix instead of a reconnect; `message` then carries the same explanation as prose.\n\t *\n\t * `unreachable` accompanies a transport or server failure with `kind: 'other'`. Credential refusal causes are\n\t * set only on a scoped `auth` warning whose credential was confirmed first (see {@link scope}), because the\n\t * refusals they name are indistinguishable from a dead credential until then: Azure DevOps answers a\n\t * third-party OAuth app its organization disallows exactly as it answers an expired token. Only Azure DevOps\n\t * reports one today. Its absence proves nothing about the cause.\n\t */\n\tcause?: ProviderWarningCause;\n\t/**\n\t * Present when this warning describes results the read could not return even though the request itself\n\t * SUCCEEDED — a provider-enforced cap, an exhausted recovery budget, a page budget, or a sub-scope the read\n\t * did not drain.\n\t *\n\t * Its presence is the signal: an omission is not a failure. Message it as incompleteness rather than as a\n\t * failed read, and do NOT derive that from `message`, which is English prose and subject to rewording.\n\t * `kind` stays `'other'` for these, so the failure discriminant keeps meaning exactly what it meant before\n\t * this field existed.\n\t *\n\t * Whether anything would fetch the rest is a SEPARATE question — read {@link ProviderWarningOmission.recovery},\n\t * not `kind`, and never assume a retry of the same request returns more.\n\t *\n\t * `limit` / `totalCount` / `scope` are forwarded only when they are reported; most omissions carry none of\n\t * the three, so render correctly without them — and see {@link ProviderWarningOmission.limit} before\n\t * showing that figure as a result count.\n\t *\n\t * Its ABSENCE proves nothing. It is never set on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}) or from a structured scope failure — those are failures, and the field would\n\t * be a lie there. But it is also absent whenever incompleteness was reported without naming what was left\n\t * out, so treat a bare `kind: 'other'` warning as unclassified rather than as a proven failure.\n\t */\n\tomission?: ProviderWarningOmission;\n\t/**\n\t * Present exactly when {@link kind} is `'unreadable-items'`: which items the read left out because it could not read\n\t * them. A consumer deciding whether an identifier is absent from the provider, rather than merely unread, must find\n\t * it in no warning's `keys` and see `complete` on every one.\n\t */\n\tunreadableItems?: ProviderWarningUnreadableItems;\n\t/**\n\t * Present when an issue-tracker project read's `org` or `project` selector matched nothing the connection\n\t * discovered: the selection was removed, renamed, or is no longer visible to this connection. `kind` is\n\t * `'not-found'`, the page is empty and not `fetchFailed`, and it carries no cursor, since a retry finds the same.\n\t *\n\t * Set only when discovery was complete. A read whose discovery failed or was truncated cannot tell a missing\n\t * selection from one it did not reach, so it keeps reporting that failure instead, without this field. A miss is\n\t * confirmed against the connection's lists as the provider holds them now, read without the cache, since a cached\n\t * one predates what was created since. A cached project list can still name a project deleted since: the tracker\n\t * refuses it (Jira's \"does not exist for the field 'project'\"), it reads as empty, and the refusal drops that list,\n\t * so the next read reports the miss. Jira Data Center's refusal counts only when it names the user it\n\t * authenticated: it answers a rejected token the same way, as the anonymous user. The miss waits on an account\n\t * lookup on a resource those lists name, which confirms the\n\t * connection's credential, and a failed lookup is reported as the read's failure instead; an account that lists no\n\t * resource at all has none to look up, and is judged on its lists alone.\n\t *\n\t * It is the discriminant: a `not-found` without it is a request that failed with a 404, 410 or 422.\n\t */\n\tselector?: ProviderWarningSelector;\n\t/**\n\t * The keys of the batch targets this warning concerns: the target a per-target failure was refused or failed\n\t * for, or every target of an integration call that failed as a whole. So a caller that sends several callers'\n\t * targets in one call can hand a failure only to the targets it belongs to.\n\t *\n\t * Its ABSENCE means the whole call. It is absent on every read that is not a batch read, and on a batch read's\n\t * refusal of the whole call before any target is read (a duplicate key, an invalid target, an unknown etag\n\t * include, no resolvable connection, the wrong surface), which concerns every target alike.\n\t *\n\t * It is not part of a warning's identity: two warnings that differ only here are one warning, for the union of\n\t * their targets (see {@link appendDedupedWarning}).\n\t */\n\ttargetKeys?: string[];\n}\n\nexport interface ProviderPageInfo {\n\t/**\n\t * The 1-based POSITION of the returned page. One convention across every paged read, page-number and\n\t * cursor paging alike, so a `getNextPageParam`-style consumer can key off it uniformly:\n\t * - the page number the provider itself reported, when it reports one (authoritative);\n\t * - else the position the caller addressed: the page encoded in a threaded page cursor, or the `page`\n\t * supplied alongside an opaque cursor (1 when the caller supplied no `page`);\n\t * - else, for a read that can be advanced by page number, the requested `page` — the provider either\n\t * honored it or the internal drain reached it (see the paging contract on `IntegrationManager`);\n\t * - else 1: a cursor-only read with no cursor threaded back ignores a requested page number and answers\n\t * with its first page, so echoing the request would mislabel page 1 as page N.\n\t *\n\t * A page past the provider's terminal cursor still reports the requested `page` with empty `items` — an\n\t * empty page N, never page N-1 relabeled.\n\t *\n\t * Continue a cursor-paged read from `cursor`, NOT from `currentPage + 1`: a cursor-only host can't be\n\t * addressed by number, so this is a position label and not always a resumable request parameter.\n\t */\n\tcurrentPage: number;\n\t/**\n\t * The page size IN EFFECT, which is not always the one requested — it is whichever of these the read can\n\t * actually vouch for:\n\t * - the size the provider reported for the page it served (numbered-page hosts), else\n\t * - the `itemsPerPage` that was applied to the request (repo-scoped reads, which forward it), else\n\t * - `items.length`, for a read that takes no page size at all: the account-wide PR/issue reads, `listRepos`\n\t * (cursor-only), and `listIssueTrackerIssuesPage`, whose `itemsPerPage` counts PROJECTS rather than issues.\n\t *\n\t * So it is a description of the page, not an echo of the request: don't derive a total from\n\t * `currentPage * itemsPerPage`, and don't read `items.length < itemsPerPage` as \"last page\" — `hasMore` /\n\t * `cursor` are the only continuation signals.\n\t */\n\titemsPerPage: number;\n\t/**\n\t * A COMPLETENESS assertion, not a mode flag: `true` only when a sweep drained every page of every target\n\t * without truncating or failing (`!truncated && !fetchFailed`). Set exclusively by `sweepPullRequests` /\n\t * `sweepClosedPullRequests` and absent on every paged read, so `allPages !== true` does NOT by itself mean\n\t * \"this was a single page\".\n\t *\n\t * This is the signal to gate on before caching a sweep as authoritative: unlike `truncated` it is `false`\n\t * for BOTH kinds of incompleteness, so it can't be misread as a benign cap.\n\t */\n\tallPages?: boolean;\n\t/**\n\t * True when the read stopped before it could confirm it had everything. Deliberately does NOT distinguish\n\t * WHY, and a consumer must not assume it means a provider ceiling — a sweep sets it from three different\n\t * origins:\n\t * - a provider-side ceiling or per-scope backstop propagated from below (e.g. GitHub Search's 1000-result\n\t * cap): the missing items are NOT reachable, so this really is \"complete to the provider's limit\";\n\t * - the sweep's OWN `maxPages` cap (default 100) with a usable cursor still in hand: the missing items ARE\n\t * reachable and were simply not fetched — raise `maxPages` to get them;\n\t * - the provider reporting another page but handing back no usable cursor: not reachable.\n\t *\n\t * Only the second is recoverable, and a sweep exposes no continuation cursor, so treating a truncated sweep\n\t * as a complete set silently drops reachable data in that case. Prefer `allPages` for the cache decision.\n\t */\n\ttruncated?: boolean;\n}\n\nexport interface ProviderResult<T> {\n\titems: T[];\n\twarnings: ProviderWarning[];\n\t/**\n\t * True when `items` is incomplete — distinguishes a failed or truncated flat read from a genuinely empty\n\t * result. Paged results additionally use `page.truncated` when a completed page could not confirm it\n\t * drained everything.\n\t */\n\tfetchFailed?: boolean;\n}\n\nexport interface ProviderPagedResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Opaque cursor for cursor-based paging (GraphQL, per-repo/per-project cursors, etc.). For hosts that\n\t * require the previous page's cursor, consumers should pass this value back to the next `cursor` option.\n\t * A retry-only cursor may remain when `hasMore` is false: `hasMore` means automatic forward progress,\n\t * while explicitly reusing such a cursor retries failed work without creating an infinite paging loop.\n\t */\n\tcursor?: string;\n}\n\nexport interface ProviderSweepResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Providers whose top-level read failed before returning any usable page. Partial per-scope failures stay\n\t * represented by `warnings` + `fetchFailed` and are intentionally excluded.\n\t */\n\tfailedProviderIds: IntegrationIds[];\n\t/**\n\t * Providers whose read started successfully but could not produce an authoritative complete slice (for\n\t * example, a later page failed, collection metadata reported a partial scope, or a paging backstop was hit).\n\t * Kept separate from\n\t * `failedProviderIds` so consumers can accept healthy sibling providers while preserving an older snapshot\n\t * for a provider whose returned slice has a gap.\n\t */\n\tincompleteProviderIds: IntegrationIds[];\n}\n\nexport interface ProviderBroadenResult<T> extends ProviderPagedResult<T> {\n\tbroadenedProviderIds: IntegrationIds[];\n\t/** Providers for which no requested org produced a usable issue result. */\n\tfailedProviderIds: IntegrationIds[];\n\t/** Providers with both a usable org result and at least one failed or truncated org slice. */\n\tincompleteProviderIds: IntegrationIds[];\n\tfanOutCount: number;\n}\n\nexport type RepositoryResolutionStatus =\n\t| 'resolved'\n\t| 'not-found'\n\t| 'unauthorized'\n\t| 'unsupported-provider'\n\t| 'invalid-remote-url'\n\t| 'host-mismatch'\n\t| 'undetermined';\n\nexport interface RepositoryIdentity {\n\tproviderId: IntegrationIds;\n\tdomain: string;\n\t/** The provider's canonical owner/namespace (follows renames), falling back to the parsed remote when omitted. */\n\towner: string;\n\t/** The provider's canonical repo name (follows renames), falling back to the parsed remote when omitted. */\n\tname: string;\n\tproject?: string;\n\t/** The original input remote URL, so the caller can key the resolution back to the remote it asked about. */\n\tremoteUrl: string;\n\t/**\n\t * True when the provider's canonical owner/name differ from what the input remote URL carried — i.e. the\n\t * repo was renamed/moved host-side and the local remote is stale. Case-insensitive, mirroring gkcli's\n\t * `EqualFold` compare so hosts that echo the input casing (e.g. Bitbucket Server/Azure) aren't flagged.\n\t */\n\trenamed: boolean;\n}\n\nexport interface RepositoryResolution {\n\tstatus: RepositoryResolutionStatus;\n\tidentity?: RepositoryIdentity;\n\twarning?: ProviderWarning;\n}\n\nexport interface ResolveRepositoryResult {\n\tresolution: RepositoryResolution;\n}\n\nconst maxProviderWarningMessageLength = 500;\n\n/** A caught error's message as a warning can carry it: an HTML page becomes its status, and prose is capped. */\nexport function providerWarningMessage(ex: unknown): string {\n\tconst raw = (ex instanceof Error ? ex.message : String(ex)).trim();\n\tconst carrier = ex as { status?: unknown; response?: { status?: unknown } } | null | undefined;\n\tconst status =\n\t\ttypeof carrier?.status === 'number'\n\t\t\t? carrier.status\n\t\t\t: typeof carrier?.response?.status === 'number'\n\t\t\t\t? carrier.response.status\n\t\t\t\t: undefined;\n\n\t// Gateways commonly answer API requests with a complete HTML error page. That payload is neither actionable\n\t// nor suitable for a warning DTO consumed by UIs and logs; preserve the status without leaking the document.\n\tif (/<(?:!doctype\\s+html|html|head|body)\\b/i.test(raw)) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\n\tif (!raw) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\treturn raw.length > maxProviderWarningMessageLength\n\t\t? `${raw.slice(0, maxProviderWarningMessageLength - 3)}...`\n\t\t: raw;\n}\n\n/**\n * Normalized org/workspace/group shape returned by the provider facade.\n * `name` is the provider identifier to pass to follow-up reads (a Jira project's key), while `org` is an optional\n * display label of the parent. Where that label can't address the parent, `orgId` is the parent's stable id to pass\n * as `org` instead (a Jira site's id, or a Data Center instance's domain).\n *\n * Jira site rows (`listOrgs`) are the exception: their `name` is the site's label, and a Jira read selects a site only\n * by its `id` (or a project row's `orgId`).\n */\nexport interface ProviderOrganization {\n\tid: string;\n\tproviderId: IntegrationIds;\n\tname: string;\n\torg?: string;\n\torgId?: string;\n\turl: string;\n}\n\n/** Normalized repository shape returned by the provider facade. */\nexport interface ProviderRepositoryShape {\n\tid: string;\n\tnamespace: string;\n\tname: string;\n\t/** Azure DevOps project; `undefined` for hosts without a project layer. */\n\tproject?: string;\n\t/** Web (browser) URL, when the provider exposes it. */\n\turl?: string;\n\t/** HTTPS clone URL, when available. */\n\tcloneUrlHttps?: string;\n\t/** SSH clone URL, when available. */\n\tcloneUrlSsh?: string;\n\t/** Default branch name, when the provider reports it. */\n\tdefaultBranch?: string;\n}\n\n/**\n * Classifies a caught provider exception into a neutral {@link ProviderWarning}. Ordering matters:\n * `instanceof` is checked most-specific-first so a rate-limit isn't mislabeled as a generic error and\n * a 404 isn't mislabeled as auth.\n */\nexport function toProviderWarning(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tex: unknown,\n): ProviderWarning {\n\tlet kind: ProviderWarning['kind'];\n\tif (ex instanceof AuthenticationError) {\n\t\tkind = 'auth';\n\t} else if (ex instanceof RequestRateLimitError) {\n\t\tkind = 'rate-limit';\n\t} else if (ex instanceof RequestNotFoundError) {\n\t\tkind = 'not-found';\n\t} else if (isUnsupportedSortError(ex)) {\n\t\tkind = 'unsupported';\n\t} else {\n\t\tkind = 'other';\n\t}\n\n\treturn {\n\t\tproviderId: providerId,\n\t\tdomain: domain,\n\t\tconnectionId: connectionId,\n\t\tmessage: providerWarningMessage(ex),\n\t\tkind: kind,\n\t\tisAuth: kind === 'auth',\n\t\t...(kind === 'other' && isProviderUnreachableError(ex) ? { cause: { reason: 'unreachable' as const } } : {}),\n\t};\n}\n\n/**\n * A scope's identity, as the stable string that keys it.\n *\n * The single definition of what \"the same scope\" means, shared by the failure and omission dedup keys in\n * `providerPaging.ts` and by {@link providerWarningKey} below, so a scope gaining a field is one edit rather\n * than three. `providerId` is included: the same repository ID under two providers is two scopes.\n *\n * The parameter is structural rather than the SDK's `CollectionScope` so this module keeps naming no\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`) while still serving its SDK-facing\n * callers, which pass that type in unchanged.\n */\nexport function collectionScopeKey(scope: ProviderWarningOmissionScope | undefined): string {\n\treturn [scope?.providerId ?? '', scope?.resourceId ?? '', scope?.projectId ?? '', scope?.repositoryId ?? ''].join(\n\t\t' ',\n\t);\n}\n\n/**\n * The omission's contribution to a warning's identity — empty when there is none, so a warning without an\n * omission keeps deduping exactly as it did before the field existed.\n *\n * Two omissions of different kinds do produce different `message` values today, so message alone would still\n * separate them. That is incidental: the premise of `omission` is that consumers must not depend on prose\n * carrying the distinguishing fact, and this key must not either.\n */\nfunction providerWarningOmissionKey(omission: ProviderWarningOmission | undefined): string {\n\tif (omission == null) return '';\n\n\treturn [\n\t\tomission.kind,\n\t\tomission.recovery,\n\t\tomission.limit ?? '',\n\t\tomission.totalCount ?? '',\n\t\tcollectionScopeKey(omission.scope),\n\t].join(' ');\n}\n\n/**\n * Strips the omission from every warning in `warnings` when the read as a whole failed.\n *\n * An omission asserts the request SUCCEEDED, and a drain only learns it failed AFTER it may have emitted one:\n * an early page can report its own truncation and then a later page can die. Deciding per warning, at the\n * moment each is built, cannot see that future — so the aggregate is reconciled once, here, where\n * `fetchFailed` is final. Call it at the point a read returns its `fetchFailed`.\n *\n * Re-dedupes as it goes: the omission is part of a warning's identity, so two warnings that differed only\n * there become identical once it is gone, and the array's contract is that no two entries are equal.\n *\n * Mutates in place: the warning array is the one being returned, and callers accumulate into it across pages.\n */\nexport function reconcileOmissionsWithFailure(warnings: ProviderWarning[], fetchFailed: boolean): void {\n\tif (!fetchFailed || !warnings.some(w => w.omission != null)) return;\n\n\tconst reconciled: ProviderWarning[] = [];\n\tfor (const warning of warnings) {\n\t\tif (warning.omission == null) {\n\t\t\tappendDedupedWarning(reconciled, warning);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { omission: _omission, ...rest } = warning;\n\t\tappendDedupedWarning(reconciled, rest);\n\t}\n\n\twarnings.splice(0, warnings.length, ...reconciled);\n}\n\n/**\n * A stable key for deduplicating warnings accumulated across drained pages / fan-out scopes.\n *\n * The scope and the cause are keyed for the reason the omission is (see {@link providerWarningOmissionKey}): today's\n * failure messages happen to spell them out, but two failures that differ in either must stay two warnings even if\n * they don't.\n *\n * `message` stays LAST. It is the only free-form segment — provider prose, spaces and all — so anything\n * appended after it could be impersonated by a message that happens to end in the same text.\n */\nfunction providerWarningKey(warning: ProviderWarning): string {\n\treturn [\n\t\twarning.providerId,\n\t\twarning.connectionId ?? '',\n\t\twarning.domain ?? '',\n\t\twarning.kind,\n\t\tcollectionScopeKey(warning.scope),\n\t\twarning.cause?.reason ?? '',\n\t\tproviderWarningOmissionKey(warning.omission),\n\t\twarning.selector != null ? JSON.stringify([warning.selector.kind, warning.selector.value]) : '',\n\t\twarning.message,\n\t].join(' ');\n}\n\n/**\n * Appends `warning` to `into` only when an equal warning (by provider/connection/domain/kind/message, plus the\n * structured scope, cause, omission and selector when present) is absent. An equal warning already there merges its\n * {@link ProviderWarning.targetKeys} with the new one's instead: the union of both, or none when either concerns\n * the whole call. Its {@link ProviderWarning.unreadableItems} merge too (see {@link mergeUnreadableItems}): two reads\n * can report the same text for different items. The existing warning is replaced, never mutated, since it may be\n * shared.\n */\nexport function appendDedupedWarning(into: ProviderWarning[], warning: ProviderWarning): void {\n\tconst key = providerWarningKey(warning);\n\tconst index = into.findIndex(existing => providerWarningKey(existing) === key);\n\tif (index === -1) {\n\t\tinto.push(warning);\n\t\treturn;\n\t}\n\n\tconst existing = into[index];\n\tconst targetKeys = mergeTargetKeys(existing.targetKeys, warning.targetKeys);\n\tconst unreadableItems =\n\t\texisting.unreadableItems != null && warning.unreadableItems != null\n\t\t\t? mergeUnreadableItems(existing.unreadableItems, warning.unreadableItems)\n\t\t\t: (existing.unreadableItems ?? warning.unreadableItems);\n\tif (targetKeys === existing.targetKeys && unreadableItems === existing.unreadableItems) return;\n\n\tconst { targetKeys: _, ...rest } = existing;\n\tinto[index] = {\n\t\t...rest,\n\t\t...(targetKeys != null ? { targetKeys: targetKeys } : {}),\n\t\t...(unreadableItems != null ? { unreadableItems: unreadableItems } : {}),\n\t};\n}\n\n/** The union of two warnings' target keys, or none when either concerns the whole call. `existing` when unchanged. */\nfunction mergeTargetKeys(existing: string[] | undefined, next: string[] | undefined): string[] | undefined {\n\t// Either concerns the whole call.\n\tif (existing == null || next == null) return undefined;\n\n\tconst added = next.filter(k => !existing.includes(k));\n\tif (!added.length) return existing;\n\n\treturn [...existing, ...new Set(added)];\n}\n\n/**\n * The items two reports of unreadable items left out together: the union of their keys, complete only when both are.\n * `existing` when `next` adds nothing to it.\n */\nexport function mergeUnreadableItems(\n\texisting: ProviderWarningUnreadableItems,\n\tnext: ProviderWarningUnreadableItems,\n): ProviderWarningUnreadableItems {\n\tconst known = new Set(existing.keys);\n\tconst added = next.keys.filter(k => !known.has(k));\n\tconst complete = existing.complete && next.complete;\n\tif (!added.length && complete === existing.complete) return existing;\n\n\treturn { keys: [...existing.keys, ...new Set(added)], complete: complete };\n}\n"],"mappings":"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"}
|
package/docs/integrations.md
CHANGED
|
@@ -757,11 +757,15 @@ prove from structured errors:
|
|
|
757
757
|
| `other` | Catch-all: malformed or contradictory input, truncation, upstream/network failure, or an unclassified error. Read `omission` before treating one as a failure. | Preserve the warning and use the result flags; do not assume it is benign or non-retryable. |
|
|
758
758
|
|
|
759
759
|
An `unreadable-items` warning carries `unreadableItems: { keys, complete }`: the identifiers of the items left out (Jira
|
|
760
|
-
issue keys), and whether `keys` names all of them. `complete` is `false` when
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
760
|
+
issue keys), and whether `keys` names all of them. `complete` is `false` when an item left out had no key to report,
|
|
761
|
+
when the provider's report of them was malformed (no list of keys, or more than keys in it), or when it said items were
|
|
762
|
+
left out and named none, so an identifier absent from `keys` is proven read only when every such warning is `complete`;
|
|
763
|
+
one present in `keys` was reported as skipped. Both hold for the spelling the provider returned only, which is also how
|
|
764
|
+
a published issue's id spells its key: `keys` doesn't answer a lookup by another case or by a key the issue had before
|
|
765
|
+
it moved, so a consumer comparing a key from elsewhere (one parsed from a branch name, say) normalizes the case of both
|
|
766
|
+
sides. The read still sets `fetchFailed`, but it retracts no omission: no request failed. On an issue tracker's project
|
|
767
|
+
reads it never stands for a read that was also cut short: a project or relationship stopped by the page backstop, a
|
|
768
|
+
stalled continuation or a failed page always adds a warning of another kind for it.
|
|
765
769
|
|
|
766
770
|
`isAuth` is a convenience mirror of `kind === 'auth'`. **Collapsing `kind` into that boolean loses the
|
|
767
771
|
rate-limit and not-found distinctions**, which then have to be re-derived from raw provider prose.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitkraken/core-gitlens",
|
|
3
3
|
"description": "GitLens core — shared Git / AI / GitHub primitives for internal GitKraken consumption",
|
|
4
|
-
"version": "0.15.
|
|
4
|
+
"version": "0.15.2",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "GitKraken",
|