@gitkraken/core-gitlens 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -1
- package/dist/git/models/issue.d.ts +13 -3
- package/dist/git/models/issue.js +3 -1
- package/dist/git/models/issue.js.map +1 -1
- package/dist/git/models/pullRequest.d.ts +13 -2
- package/dist/git/models/pullRequest.js +3 -1
- package/dist/git/models/pullRequest.js.map +1 -1
- package/dist/git/utils/issue.utils.js +2 -1
- package/dist/git/utils/issue.utils.js.map +1 -1
- package/dist/git/utils/pullRequest.utils.js +1 -0
- package/dist/git/utils/pullRequest.utils.js.map +1 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +1 -0
- package/dist/plus/integrations/collectionMetadata.js +1 -0
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +7 -6
- package/dist/plus/integrations/models/gitHostIntegration.js +4 -3
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +5 -0
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.d.ts +8 -6
- package/dist/plus/integrations/providers/azure/azure.js +3 -2
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azure/models.d.ts +4 -2
- package/dist/plus/integrations/providers/azure/models.js +6 -5
- package/dist/plus/integrations/providers/azure/models.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +15 -13
- package/dist/plus/integrations/providers/azureDevOps.js +26 -22
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.js +6 -3
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +1 -0
- package/dist/plus/integrations/providers/models.d.ts +26 -4
- package/dist/plus/integrations/providers/models.js +27 -5
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +2 -1
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/etag.js +3 -1
- package/dist/plus/integrations/reads/etag.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +3 -1
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/issues.js +8 -2
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/paging.d.ts +4 -0
- package/dist/plus/integrations/reads/paging.js +9 -2
- package/dist/plus/integrations/reads/paging.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.js +5 -1
- package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +7 -0
- package/dist/plus/integrations/results.js.map +1 -1
- package/docs/integrations.md +21 -2
- 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, SettledRepositoryScopes } from '../models/gitHostIntegration.js';\nimport type { IssueFilter, ProviderReposInput } from '../providers/models.js';\nimport { isRepoIdsInput, PagingMode, providersMetadata } from '../providers/models.js';\nimport { mergeCollectionMetadata } from '../providers/utils/providerPaging.js';\nimport type { ProviderPagedResult, ProviderWarning } from '../results.js';\nimport { reconcileOmissionsWithFailure } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport { resolveAccountWideIssueFilters } from './filters.js';\nimport { resolveIssueSort, toIssueOrdering } from './ordering.js';\nimport {\n\tdrainFlatPagesToRequestedPage,\n\tdrainToRequestedPage,\n\tisPageNumberAdvanceable,\n\tpageToCursor,\n\trefusedPage,\n\tresolveContinuation,\n\tresolveCurrentPage,\n\ttoProviderPageInfo,\n\tusableCursor,\n} from './paging.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissuesUnsupportedWarning,\n\totherWarning,\n\ttruncationWarning,\n\tunmergeableIssueSortWarning,\n\tunsupportedAccountWideIssueFiltersWarning,\n\tunsupportedIssueSortWarning,\n\tunsupportedWarning,\n} from './warnings.js';\n\nconst issueStateFilters: readonly IssueStateFilter[] = ['open', 'closed', 'all'];\n\n/**\n * Whether a repo-scoped page is assembled from SEVERAL provider queries and merged here — which is what decides\n * whether the requested order can be honored at all.\n *\n * `PagingMode.Repos` (GitHub) sends one search however many repositories are named, so its page arrives ordered by\n * the provider. `Repo` (GitLab) issues one query per repository, and `Project` (Azure) one per PROJECT — several\n * repositories of the same project are still one query, which is why that case counts distinct projects rather than\n * repositories.\n *\n * The `Repo` case has a second way to merge that has nothing to do with the count: given repository IDS rather than\n * descriptors, `getMyIssuesForReposResult` skips its per-repository fan-out entirely (that branch is guarded on\n * `!isRepoIdsInput`) and calls the SDK's `getIssuesForRepos`, which for GitLab is the multi-project aggregate that\n * merges in the SDK and refuses `priority`/`dueDate` however few scopes it was given. Treating that as merged is\n * what makes this facade refuse it up front, with the message that names the reason, instead of letting the SDK\n * reject a read the capability table had just promised.\n */\nfunction mergesProviderQueries(pagingMode: PagingMode | undefined, repos: ProviderReposInput | undefined): boolean {\n\tconst scopes = repos ?? [];\n\tswitch (pagingMode) {\n\t\tcase PagingMode.Repo:\n\t\t\t// `isRepoIdsInput` is the same predicate `ProvidersApi` selects the SDK aggregate with, so this cannot\n\t\t\t// disagree with the branch it is predicting.\n\t\t\treturn isRepoIdsInput(scopes) ? scopes.length > 0 : scopes.length > 1;\n\t\tcase PagingMode.Project:\n\t\t\t// Only the descriptor form carries a project; the id form is refused by the Azure read before it runs.\n\t\t\treturn new Set(scopes.map(r => (typeof r === 'object' ? r.project : undefined))).size > 1;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/**\n * What {@link listIssuesPage} accepts, shared by name with the two functions it dispatches to.\n *\n * Named rather than left inline: three functions now take this exact shape, and reaching it from the other two as\n * `Parameters<typeof listIssuesPage>[1]` would define the contract in terms of the function that consumes it —\n * inside out, and unreadable at the point where the fields are actually used.\n */\nexport interface ListIssuesPageOptions {\n\tstate?: IssueStateFilter;\n\tproviderId: IntegrationIds;\n\trepos?: ProviderReposInput;\n\t/** Narrows the account-wide read to one org/account. Requires a host with a project layer (Azure). */\n\torg?: string;\n\t/** Narrows the account-wide read to one project. Requires a host with a project layer (Azure). */\n\tproject?: string;\n\t/**\n\t * Narrows to the requested relationship(s). On the account-wide path this replaces the provider's own\n\t * definition of \"my issues\" (GitHub/GHE: authored ∪ assigned ∪ mentioned; Azure: assigned ∪ authored;\n\t * GitLab: assigned-to-me), so `[Assignee]` yields `assignee:@me` everywhere it's expressible. A set the\n\t * provider can't express server-side is refused whole (warning + `fetchFailed`), never widened — check\n\t * getSupportedFilters first to avoid that path.\n\t */\n\tfilters?: IssueFilter[];\n\t/** Broadens the read to every assignee. Contradicts `filters`; passing both is refused. */\n\tincludeAllAssignees?: boolean;\n\t/**\n\t * How to order the page, as `field:direction`. Omitted orders most-recently-updated-first wherever the\n\t * provider can express that, which is this facade's default rather than the provider's own.\n\t *\n\t * Validated against `getSupportedFilters().issueSorts` on the repo-scoped path and `.issueSortsAccountWide`\n\t * on the account-wide one — two genuinely different vocabularies for GitLab, whose two reads are different\n\t * APIs. A key the provider can't express refuses the read (warning + `fetchFailed`) rather than serving a\n\t * differently-ordered list, for the same reason an inexpressible filter does: the reachable window is\n\t * bounded, so another order is another subset.\n\t *\n\t * On the repo-scoped path with SEVERAL scopes the page is a merge of one query per repository/project, so\n\t * only a key a normalized issue carries can be honored: `priority`, `dueDate` and `resolved` are refused\n\t * there even where the provider supports them on a single scope. Order is per page, and per scope across\n\t * pages — the merge orders what a page contains, not the sequence of pages.\n\t */\n\tsort?: IssueSorting;\n\tpage?: number;\n\tcursor?: string;\n\titemsPerPage?: number;\n\tforceSync?: boolean;\n\tconnectionId?: string;\n\t/**\n\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t */\n\tdomain?: string;\n}\n\n/**\n * What both halves of {@link listIssuesPage} need from its shared prologue.\n *\n * A bundle rather than six parameters because the two reads take exactly the same inputs and differ only in what\n * they do with them; threading them positionally would make the two call sites read as two different reads of two\n * different things, which is the opposite of what is true.\n *\n * Note `ctx` is absent: every use of it — resolving the integration, the domain, the forced refresh, the\n * connection warnings — is in the prologue, which is what makes this the right cut.\n */\ninterface IssueReadContext {\n\toptions: ListIssuesPageOptions;\n\tintegration: GitHostIntegration;\n\tdomain: string | undefined;\n\twarnOnMissingSession: boolean;\n\tpage: number;\n}\n\nexport async function listIssuesPage(\n\tctx: ProviderReadContext,\n\toptions: ListIssuesPageOptions,\n): Promise<ProviderPagedResult<IssueShape>> {\n\t// Truncated as well as floored, like every other paged read here: `page` is reported back as\n\t// `page.currentPage` and compared against the position a drain reached, and neither can be fractional.\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository issue reads')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account —\n\t\t// surface a no-connection warning + fetchFailed rather than a silent empty page.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(page, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository issue reads')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\n\t// A git host whose issue tracker is deprecated (Bitbucket, superseded by dedicated issue integrations)\n\t// reports issues as explicitly unsupported rather than serving a partial/legacy source or a silent empty.\n\tif (!integration.supportsIssues) {\n\t\treturn refusedPage(page, [issuesUnsupportedWarning(options.providerId, domain, options.connectionId)], true);\n\t}\n\n\tif (\n\t\toptions.state != null &&\n\t\t(!issueStateFilters.includes(options.state) ||\n\t\t\t(options.state !== 'open' && !providersMetadata[options.providerId]?.supportsIssueStates))\n\t) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tissueStateFilters.includes(options.state)\n\t\t\t\t\t? unsupportedWarning(\n\t\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\t\tdomain,\n\t\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t\t`Issue state '${options.state}' is not supported by '${options.providerId}'.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t: otherWarning(\n\t\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\t\tdomain,\n\t\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t\t`Unknown issue state; expected one of ${issueStateFilters.join(', ')}.`,\n\t\t\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\t// `repos` is what selects between the two: with none there is no scope to query per repository, so the\n\t// provider's own account-wide search answers instead. They are different provider queries with different\n\t// capability tables, cursors and completeness signals — see each function.\n\tconst context: IssueReadContext = {\n\t\toptions: options,\n\t\tintegration: integration,\n\t\tdomain: domain,\n\t\twarnOnMissingSession: warnOnMissingSession,\n\t\tpage: page,\n\t};\n\treturn (options.repos?.length ?? 0) === 0 ? readAccountWideIssuesPage(context) : readRepoScopedIssuesPage(context);\n}\n\n/**\n * The account-wide read: the provider's own definition of \"my issues\", behind a composite cursor that spans\n * several provider searches.\n *\n * Its own function rather than a branch of the paged read, because almost nothing is shared past the guards: it\n * validates a different filter table and a different sort table, it is cursor-only (a page number cannot address\n * a composite cursor), it accumulates per-scope failures across the pages it walks, and it always merges several\n * queries. The two used to share one 460-line function behind a mode flag, and the comments explaining how far\n * apart they are were the clue that they wanted separating.\n */\nasync function readAccountWideIssuesPage({\n\toptions,\n\tintegration,\n\tdomain,\n\twarnOnMissingSession,\n\tpage,\n}: IssueReadContext): Promise<ProviderPagedResult<IssueShape>> {\n\t// The account-wide read is cursor-only, so a refusal can't claim the requested position — it reports\n\t// page 1, per ProviderPageInfo.currentPage.\n\tconst refused = (warning: ProviderWarning) => refusedPage<IssueShape>(1, [warning], true);\n\n\t// Checked before the provider-specific guards below: a caller passing both has a contradictory request\n\t// whatever the provider, and saying so is more useful than reporting one half of it as unsupported.\n\t// `filters` narrows this read to a relationship (`[Assignee]` ⇒ just assigned-to-me); `includeAllAssignees`\n\t// broadens it to every assignee. Honoring either silently would answer a question the caller didn't ask.\n\tif (options.filters?.length && options.includeAllAssignees === true) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'`filters` and `includeAllAssignees` are contradictory for an account-wide issue read; pass only one.',\n\t\t\t),\n\t\t);\n\t}\n\n\t// GitHub expresses \"any assignee\" as `assignee:*`, which is meaningless without a scope: unscoped it\n\t// matches millions of issues across all of GitHub instead of the user's own world. ANY scope makes it\n\t// meaningful (one repository, several, or an org) — it is specifically THIS branch, the account-wide read,\n\t// that has none to offer. Scope the read to repositories (or use `searchIssuesPage`, whose criteria model\n\t// takes `any-assignee` alongside a `repos`/`org` scope) rather than reading unscoped.\n\tif (\n\t\toptions.includeAllAssignees === true &&\n\t\t(options.providerId === GitCloudHostIntegrationId.GitHub ||\n\t\t\toptions.providerId === GitSelfManagedHostIntegrationId.CloudGitHubEnterprise)\n\t) {\n\t\treturn refused(\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'`includeAllAssignees` is not supported for account-wide GitHub issue reads; scope the read to repositories instead.',\n\t\t\t),\n\t\t);\n\t}\n\n\t// Only a host with a project layer can narrow server-side. Reject the request rather than serving an\n\t// unscoped list as if it had been narrowed: the caller would otherwise have to filter client-side,\n\t// which desynchronizes the filtered `items` from this read's `hasMore`/`currentPage` and shows\n\t// \"no issues\" for a page that simply held none of the requested project's.\n\tif ((options.org != null || options.project != null) && !integration.supportsProjectDiscovery) {\n\t\treturn refused(\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Project-scoped issue reads are not supported by '${options.providerId}'; scope the read to repositories instead.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// Narrowing the account-wide read is only honest when the provider can express it server-side: its\n\t// per-relationship queries produced the page and the cursor together, so dropping items afterward would\n\t// leave `items` describing a different result set than `hasMore`/`currentPage`.\n\tconst resolvedIssueFilters = resolveAccountWideIssueFilters(options.providerId, options.filters);\n\tif (resolvedIssueFilters.unsupported) {\n\t\treturn refused(\n\t\t\tunsupportedAccountWideIssueFiltersWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\toptions.filters!,\n\t\t\t),\n\t\t);\n\t}\n\n\t// Every provider's account-wide read is a UNION of several queries — GitHub's three `@me` searches,\n\t// GitLab's one REST call per relationship, Azure's (project x relationship) drains — so it always merges,\n\t// and `supportedAccountWideIssueSorts` already lists only keys a merge can honor. No separate\n\t// mergeability refusal is needed here, unlike on the repo-scoped path below.\n\tconst resolvedSort = resolveIssueSort(\n\t\tprovidersMetadata[options.providerId]?.supportedAccountWideIssueSorts,\n\t\toptions.sort,\n\t);\n\tif (resolvedSort.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSortWarning(options.providerId, domain, options.connectionId, resolvedSort.rejection),\n\t\t);\n\t}\n\n\t// `merged: true` unconditionally: this read has no scope count for a caller to reduce. It is also why no\n\t// `unmergeable` check follows — `supportedAccountWideIssueSorts` declares only keys a merge can honor.\n\tconst accountWideOrdering = toIssueOrdering(resolvedSort.sort, true);\n\n\t// The repo-scoped core rejects empty repos (GitHub/Bitbucket/Azure); read the account-wide,\n\t// already-user-scoped core instead. GitHub exposes a composite cursor across its authored,\n\t// assigned, and mentioned searches. Walk it internally when the caller supplies only page N.\n\tconst readAccountWidePage = (cursor: string | undefined) =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\tintegration.searchMyIssuesWithTruncationResult(undefined, undefined, options.connectionId, {\n\t\t\t\t\tincludeAllAssignees: options.includeAllAssignees,\n\t\t\t\t\tstate: options.state,\n\t\t\t\t\tfilters: resolvedIssueFilters.filters,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tpageSize: options.itemsPerPage,\n\t\t\t\t\torg: options.org,\n\t\t\t\t\tproject: options.project,\n\t\t\t\t\tsort: accountWideOrdering.sort,\n\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\tconst first = await readAccountWidePage(options.cursor);\n\tconst warnings = first.warning != null ? [first.warning] : [];\n\t// Merged across the walked pages, so a per-scope failure reported on an earlier page isn't lost by\n\t// paging past it — the one thing this read accumulates that the filtered search doesn't.\n\tlet allMetadata = first.value?.metadata;\n\tconst drained = await drainFlatPagesToRequestedPage(first, {\n\t\trequestedPage: page,\n\t\tsuppliedCursor: options.cursor,\n\t\twarnings: warnings,\n\t\treadPage: readAccountWidePage,\n\t\tfold: p => {\n\t\t\tallMetadata = mergeCollectionMetadata(allMetadata, p.metadata, 'disjoint');\n\t\t},\n\t});\n\tconst { value, currentPage, requestedPageMissing } = drained;\n\tconst currentTruncated = drained.truncated;\n\tconst pageFetchFailed = drained.fetchFailed;\n\n\t// GitHub, GitLab, and Azure implement an account-wide issue search; a provider that doesn't (Bitbucket\n\t// exposes no issues at all, and `supportsIssues` already short-circuits it above) returns `undefined`\n\t// with no error. Surface that as an explicit unsupported warning + fetchFailed rather than a silent\n\t// empty success — the caller must fall back (e.g. broadenIssues over repos).\n\tif (value == null && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Account-wide issue search is not supported by '${options.providerId}'; scope the read to repositories instead.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst items = requestedPageMissing ? [] : (value?.values ?? []);\n\t// Each query arrived ordered by the provider; their union is not, and the union is what this read publishes.\n\t// Idempotent for GitHub, whose aliased-search engine already ordered the same merge — one call here rather\n\t// than the same logic repeated in each of the three provider implementations.\n\tconst orderedItems = accountWideOrdering.order(items);\n\t// Fold in structured per-scope failures from the account-wide fan-out (e.g. Azure across projects):\n\t// scope-aware warnings + `fetchFailed` when a scope failed, without discarding the successful items.\n\tconst assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, allMetadata);\n\t// An account-wide search that couldn't confirm completeness (a provider cap with no cursor, or a\n\t// per-scope backstop/failure) is incomplete and can't be paged; report it as truncated (+ a\n\t// provider-neutral warning, unless a structured failure already explains it) rather than a complete\n\t// list. Don't hard-code GitHub's \"100 per category\" cap here — Azure reaches this via a per-project\n\t// backstop, and other providers may cap differently.\n\t// The account-wide read is cursor-only (its composite cursor spans several provider searches, so a\n\t// page number can't address it): `hasMore` without a real cursor is a dead end, so report it as\n\t// terminal-but-incomplete rather than inviting the caller to page forever.\n\tconst continuation = resolveContinuation(\n\t\t{\n\t\t\thasMore: requestedPageMissing ? false : (value?.hasMore ?? false),\n\t\t\tcursor: requestedPageMissing ? undefined : usableCursor(value?.cursor),\n\t\t\ttruncated: currentTruncated,\n\t\t},\n\t\tundefined,\n\t);\n\tconst truncated = continuation.truncated || assessment.truncated;\n\tif (truncated && warnings.length === 0) {\n\t\twarnings.push(\n\t\t\ttruncationWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'Account-wide issue search',\n\t\t\t\t// `exhausted`: this composite read exposes no budget the caller can raise, so nothing it\n\t\t\t\t// could call would return the withheld items.\n\t\t\t\tassessment.fetchFailed || pageFetchFailed ? 'interrupted' : 'exhausted',\n\t\t\t),\n\t\t);\n\t}\n\t// A metadata omission from an earlier page asserts the read succeeded; a later page may since have failed.\n\treconcileOmissionsWithFailure(warnings, assessment.fetchFailed || pageFetchFailed);\n\treturn {\n\t\titems: orderedItems,\n\t\twarnings: warnings,\n\t\tpage: {\n\t\t\t// Positional, per ProviderPageInfo.currentPage. `currentPage` already carries what the provider\n\t\t\t// reported or what the internal drain counted; a requested page past the terminal cursor is\n\t\t\t// reported as that empty page N. The account-wide read is cursor-only, so a `page` the caller\n\t\t\t// didn't pair with a cursor is never echoed.\n\t\t\tcurrentPage: requestedPageMissing\n\t\t\t\t? page\n\t\t\t\t: resolveCurrentPage({\n\t\t\t\t\t\tproviderPage: currentPage,\n\t\t\t\t\t\trequestedPage: page,\n\t\t\t\t\t\tsuppliedCursor: options.cursor,\n\t\t\t\t\t\tpageAdvanceable: false,\n\t\t\t\t\t}),\n\t\t\titemsPerPage: orderedItems.length,\n\t\t\ttruncated: truncated || undefined,\n\t\t},\n\t\thasMore: continuation.hasMore,\n\t\tcursor: continuation.cursor,\n\t\tfetchFailed: assessment.fetchFailed || pageFetchFailed || undefined,\n\t};\n}\n\n/**\n * The repository-scoped read: one provider query per repository or project, paged by the provider's own\n * continuation and drained internally when the caller supplied only a page number.\n *\n * See {@link readAccountWideIssuesPage} for why the two are separate functions.\n */\nasync function readRepoScopedIssuesPage({\n\toptions,\n\tintegration,\n\tdomain,\n\twarnOnMissingSession,\n\tpage,\n}: IssueReadContext): Promise<ProviderPagedResult<IssueShape>> {\n\tconst metadata = providersMetadata[options.providerId];\n\n\t// How many separate provider queries this page is assembled from, which decides whether the requested order can\n\t// be honored at all. `PagingMode.Repos` (GitHub) sends ONE search however many repositories are named, so its\n\t// page arrives ordered by the provider; `Repo` (GitLab) and `Project` (Azure) issue one query per scope and are\n\t// concatenated here, so a page spanning several of them is only as orderable as a normalized issue is.\n\tconst resolvedRepoScopedSort = resolveIssueSort(metadata?.supportedIssueSorts, options.sort);\n\tif (resolvedRepoScopedSort.rejection != null) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tunsupportedIssueSortWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\tresolvedRepoScopedSort.rejection,\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst issuesPagingMode = metadata?.issuesPagingMode;\n\t// Counted, and read, as the host settles them: a scope can name another project than the one it was parsed with\n\t// (see `GitHostIntegration.resolveRepositoryScopes`), which changes how many queries a per-project read makes. The\n\t// read is handed the settled scopes, flags included, so it reads what was counted rather than settling again.\n\tconst scopes = options.repos ?? [];\n\tlet repos: ProviderReposInput | SettledRepositoryScopes = scopes;\n\tlet counted = scopes;\n\tif (issuesPagingMode === PagingMode.Project && scopes.length) {\n\t\tconst settled = await runCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() => integration.resolveRepositoryScopes(scopes, options.connectionId),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\t\t// No session to read with, or the read's own failure resolving it: answered as that read would, without it.\n\t\tif (settled.value == null) {\n\t\t\treturn refusedPage(page, settled.warning != null ? [settled.warning] : [], settled.warning != null);\n\t\t}\n\n\t\trepos = settled.value;\n\t\tcounted = settled.value.repos;\n\t}\n\tconst ordering = toIssueOrdering(resolvedRepoScopedSort.sort, mergesProviderQueries(issuesPagingMode, counted));\n\t// Refused rather than served as concatenated per-scope runs, which would look ordered without being so — and\n\t// only where it actually merges, since the same key against the same provider is perfectly answerable for a\n\t// single repository or project.\n\tif (ordering.unmergeable != null) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tunmergeableIssueSortWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\tordering.unmergeable,\n\t\t\t\t\tissuesPagingMode === PagingMode.Project ? 'projects' : 'repositories',\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst cursor = options.cursor ?? pageToCursor(page);\n\tconst { value, warning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() =>\n\t\t\t// The shapes seam returns normalized IssueShape, and is overridable by a provider whose only issue\n\t\t\t// client already yields shapes (serving this path without a raw ProviderIssue round-trip).\n\t\t\tintegration.getMyIssuesForReposAsShapesResult(\n\t\t\t\trepos,\n\t\t\t\t// Forward `page`/`pageSize` alongside the cursor so PagingMode.Repo/Project hosts honor the\n\t\t\t\t// requested page and page size rather than ignoring a synthesized page-number cursor.\n\t\t\t\t{\n\t\t\t\t\tfilters: options.filters,\n\t\t\t\t\tincludeAllAssignees: options.includeAllAssignees,\n\t\t\t\t\tstate: options.state,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t// The normalized `page`, so the number forwarded to the provider can't disagree with the\n\t\t\t\t\t// page-number cursor beside it — both are built from the same value. Only when the caller asked\n\t\t\t\t\t// for one: an omitted page is the provider's own first page.\n\t\t\t\t\tpage: options.page != null ? page : undefined,\n\t\t\t\t\tpageSize: options.itemsPerPage,\n\t\t\t\t\tsort: ordering.sort,\n\t\t\t\t},\n\t\t\t\toptions.connectionId,\n\t\t\t),\n\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t);\n\n\tlet items = value?.values ?? [];\n\tconst warnings = warning != null ? [warning] : [];\n\tlet pageFetchFailed = warning != null && value == null;\n\tlet paged = toProviderPageInfo(options.itemsPerPage ?? items.length, value?.paging);\n\tlet allMetadata = value?.metadata;\n\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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"}
|
|
1
|
+
{"version":3,"file":"issues.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issues.ts"],"sourcesContent":["import type { IssueShape, IssueSorting, IssueStateFilter } from '@gitlens/git/models/issue.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../constants.js';\nimport type { GitHostIntegration, SettledRepositoryScopes } from '../models/gitHostIntegration.js';\nimport type { IssueFilter, ProviderReposInput } from '../providers/models.js';\nimport { isRepoIdsInput, PagingMode, providersMetadata } from '../providers/models.js';\nimport { mergeCollectionMetadata } from '../providers/utils/providerPaging.js';\nimport type { ProviderPagedResult, ProviderWarning } from '../results.js';\nimport { reconcileOmissionsWithFailure } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport { resolveAccountWideIssueFilters } from './filters.js';\nimport { resolveIssueSort, toIssueOrdering } from './ordering.js';\nimport {\n\tdrainFlatPagesToRequestedPage,\n\tdrainToRequestedPage,\n\tisPageNumberAdvanceable,\n\tpageToCursor,\n\trefusedPage,\n\tresolveContinuation,\n\tresolveCurrentPage,\n\ttoProviderPageInfo,\n\tusableCursor,\n} from './paging.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissuesUnsupportedWarning,\n\totherWarning,\n\ttruncationWarning,\n\tunmergeableIssueSortWarning,\n\tunsupportedAccountWideIssueFiltersWarning,\n\tunsupportedIssueSortWarning,\n\tunsupportedWarning,\n} from './warnings.js';\n\nconst issueStateFilters: readonly IssueStateFilter[] = ['open', 'closed', 'all'];\n\n/**\n * Whether a repo-scoped page is assembled from SEVERAL provider queries and merged here — which is what decides\n * whether the requested order can be honored at all.\n *\n * `PagingMode.Repos` (GitHub) sends one search however many repositories are named, so its page arrives ordered by\n * the provider. `Repo` (GitLab) issues one query per repository, and `Project` (Azure) one per PROJECT — several\n * repositories of the same project are still one query, which is why that case counts distinct projects rather than\n * repositories.\n *\n * The `Repo` case has a second way to merge that has nothing to do with the count: given repository IDS rather than\n * descriptors, `getMyIssuesForReposResult` skips its per-repository fan-out entirely (that branch is guarded on\n * `!isRepoIdsInput`) and calls the SDK's `getIssuesForRepos`, which for GitLab is the multi-project aggregate that\n * merges in the SDK and refuses `priority`/`dueDate` however few scopes it was given. Treating that as merged is\n * what makes this facade refuse it up front, with the message that names the reason, instead of letting the SDK\n * reject a read the capability table had just promised.\n */\nfunction mergesProviderQueries(pagingMode: PagingMode | undefined, repos: ProviderReposInput | undefined): boolean {\n\tconst scopes = repos ?? [];\n\tswitch (pagingMode) {\n\t\tcase PagingMode.Repo:\n\t\t\t// `isRepoIdsInput` is the same predicate `ProvidersApi` selects the SDK aggregate with, so this cannot\n\t\t\t// disagree with the branch it is predicting.\n\t\t\treturn isRepoIdsInput(scopes) ? scopes.length > 0 : scopes.length > 1;\n\t\tcase PagingMode.Project:\n\t\t\t// Only the descriptor form carries a project; the id form is refused by the Azure read before it runs.\n\t\t\treturn new Set(scopes.map(r => (typeof r === 'object' ? r.project : undefined))).size > 1;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/**\n * What {@link listIssuesPage} accepts, shared by name with the two functions it dispatches to.\n *\n * Named rather than left inline: three functions now take this exact shape, and reaching it from the other two as\n * `Parameters<typeof listIssuesPage>[1]` would define the contract in terms of the function that consumes it —\n * inside out, and unreadable at the point where the fields are actually used.\n */\nexport interface ListIssuesPageOptions {\n\tstate?: IssueStateFilter;\n\tproviderId: IntegrationIds;\n\trepos?: ProviderReposInput;\n\t/** Narrows the account-wide read to one org/account. Requires a host with a project layer (Azure). */\n\torg?: string;\n\t/** Narrows the account-wide read to one project. Requires a host with a project layer (Azure). */\n\tproject?: string;\n\t/**\n\t * Narrows to the requested relationship(s). On the account-wide path this replaces the provider's own\n\t * definition of \"my issues\" (GitHub/GHE: authored ∪ assigned ∪ mentioned; Azure: assigned ∪ authored;\n\t * GitLab: assigned-to-me), so `[Assignee]` yields `assignee:@me` everywhere it's expressible. A set the\n\t * provider can't express server-side is refused whole (warning + `fetchFailed`), never widened — check\n\t * getSupportedFilters first to avoid that path.\n\t */\n\tfilters?: IssueFilter[];\n\t/** Broadens the read to every assignee. Contradicts `filters`; passing both is refused. */\n\tincludeAllAssignees?: boolean;\n\t/**\n\t * How to order the page, as `field:direction`. Omitted orders most-recently-updated-first wherever the\n\t * provider can express that, which is this facade's default rather than the provider's own.\n\t *\n\t * Validated against `getSupportedFilters().issueSorts` on the repo-scoped path and `.issueSortsAccountWide`\n\t * on the account-wide one — two genuinely different vocabularies for GitLab, whose two reads are different\n\t * APIs. A key the provider can't express refuses the read (warning + `fetchFailed`) rather than serving a\n\t * differently-ordered list, for the same reason an inexpressible filter does: the reachable window is\n\t * bounded, so another order is another subset.\n\t *\n\t * On the repo-scoped path with SEVERAL scopes the page is a merge of one query per repository/project, so\n\t * only a key a normalized issue carries can be honored: `priority`, `dueDate` and `resolved` are refused\n\t * there even where the provider supports them on a single scope. Order is per page, and per scope across\n\t * pages — the merge orders what a page contains, not the sequence of pages.\n\t */\n\tsort?: IssueSorting;\n\tpage?: number;\n\tcursor?: string;\n\titemsPerPage?: number;\n\tforceSync?: boolean;\n\tconnectionId?: string;\n\t/**\n\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t */\n\tdomain?: string;\n}\n\n/**\n * What both halves of {@link listIssuesPage} need from its shared prologue.\n *\n * A bundle rather than six parameters because the two reads take exactly the same inputs and differ only in what\n * they do with them; threading them positionally would make the two call sites read as two different reads of two\n * different things, which is the opposite of what is true.\n *\n * Note `ctx` is absent: every use of it — resolving the integration, the domain, the forced refresh, the\n * connection warnings — is in the prologue, which is what makes this the right cut.\n */\ninterface IssueReadContext {\n\toptions: ListIssuesPageOptions;\n\tintegration: GitHostIntegration;\n\tdomain: string | undefined;\n\twarnOnMissingSession: boolean;\n\tpage: number;\n}\n\nexport async function listIssuesPage(\n\tctx: ProviderReadContext,\n\toptions: ListIssuesPageOptions,\n): Promise<ProviderPagedResult<IssueShape>> {\n\t// Truncated as well as floored, like every other paged read here: `page` is reported back as\n\t// `page.currentPage` and compared against the position a drain reached, and neither can be fractional.\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository issue reads')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account —\n\t\t// surface a no-connection warning + fetchFailed rather than a silent empty page.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(page, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository issue reads')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\n\t// A git host whose issue tracker is deprecated (Bitbucket, superseded by dedicated issue integrations)\n\t// reports issues as explicitly unsupported rather than serving a partial/legacy source or a silent empty.\n\tif (!integration.supportsIssues) {\n\t\treturn refusedPage(page, [issuesUnsupportedWarning(options.providerId, domain, options.connectionId)], true);\n\t}\n\n\tif (\n\t\toptions.state != null &&\n\t\t(!issueStateFilters.includes(options.state) ||\n\t\t\t(options.state !== 'open' && !providersMetadata[options.providerId]?.supportsIssueStates))\n\t) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tissueStateFilters.includes(options.state)\n\t\t\t\t\t? unsupportedWarning(\n\t\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\t\tdomain,\n\t\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t\t`Issue state '${options.state}' is not supported by '${options.providerId}'.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t: otherWarning(\n\t\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\t\tdomain,\n\t\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t\t`Unknown issue state; expected one of ${issueStateFilters.join(', ')}.`,\n\t\t\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\t// `repos` is what selects between the two: with none there is no scope to query per repository, so the\n\t// provider's own account-wide search answers instead. They are different provider queries with different\n\t// capability tables, cursors and completeness signals — see each function.\n\tconst context: IssueReadContext = {\n\t\toptions: options,\n\t\tintegration: integration,\n\t\tdomain: domain,\n\t\twarnOnMissingSession: warnOnMissingSession,\n\t\tpage: page,\n\t};\n\treturn (options.repos?.length ?? 0) === 0 ? readAccountWideIssuesPage(context) : readRepoScopedIssuesPage(context);\n}\n\n/**\n * The account-wide read: the provider's own definition of \"my issues\", behind a composite cursor that spans\n * several provider searches.\n *\n * Its own function rather than a branch of the paged read, because almost nothing is shared past the guards: it\n * validates a different filter table and a different sort table, it is cursor-only (a page number cannot address\n * a composite cursor), it accumulates per-scope failures across the pages it walks, and it always merges several\n * queries. The two used to share one 460-line function behind a mode flag, and the comments explaining how far\n * apart they are were the clue that they wanted separating.\n */\nasync function readAccountWideIssuesPage({\n\toptions,\n\tintegration,\n\tdomain,\n\twarnOnMissingSession,\n\tpage,\n}: IssueReadContext): Promise<ProviderPagedResult<IssueShape>> {\n\t// The account-wide read is cursor-only, so a refusal can't claim the requested position — it reports\n\t// page 1, per ProviderPageInfo.currentPage.\n\tconst refused = (warning: ProviderWarning) => refusedPage<IssueShape>(1, [warning], true);\n\n\t// Checked before the provider-specific guards below: a caller passing both has a contradictory request\n\t// whatever the provider, and saying so is more useful than reporting one half of it as unsupported.\n\t// `filters` narrows this read to a relationship (`[Assignee]` ⇒ just assigned-to-me); `includeAllAssignees`\n\t// broadens it to every assignee. Honoring either silently would answer a question the caller didn't ask.\n\tif (options.filters?.length && options.includeAllAssignees === true) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'`filters` and `includeAllAssignees` are contradictory for an account-wide issue read; pass only one.',\n\t\t\t),\n\t\t);\n\t}\n\n\t// GitHub expresses \"any assignee\" as `assignee:*`, which is meaningless without a scope: unscoped it\n\t// matches millions of issues across all of GitHub instead of the user's own world. ANY scope makes it\n\t// meaningful (one repository, several, or an org) — it is specifically THIS branch, the account-wide read,\n\t// that has none to offer. Scope the read to repositories (or use `searchIssuesPage`, whose criteria model\n\t// takes `any-assignee` alongside a `repos`/`org` scope) rather than reading unscoped.\n\tif (\n\t\toptions.includeAllAssignees === true &&\n\t\t(options.providerId === GitCloudHostIntegrationId.GitHub ||\n\t\t\toptions.providerId === GitSelfManagedHostIntegrationId.CloudGitHubEnterprise)\n\t) {\n\t\treturn refused(\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'`includeAllAssignees` is not supported for account-wide GitHub issue reads; scope the read to repositories instead.',\n\t\t\t),\n\t\t);\n\t}\n\n\t// Only a host with a project layer can narrow server-side. Reject the request rather than serving an\n\t// unscoped list as if it had been narrowed: the caller would otherwise have to filter client-side,\n\t// which desynchronizes the filtered `items` from this read's `hasMore`/`currentPage` and shows\n\t// \"no issues\" for a page that simply held none of the requested project's.\n\tif ((options.org != null || options.project != null) && !integration.supportsProjectDiscovery) {\n\t\treturn refused(\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Project-scoped issue reads are not supported by '${options.providerId}'; scope the read to repositories instead.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// Narrowing the account-wide read is only honest when the provider can express it server-side: its\n\t// per-relationship queries produced the page and the cursor together, so dropping items afterward would\n\t// leave `items` describing a different result set than `hasMore`/`currentPage`.\n\tconst resolvedIssueFilters = resolveAccountWideIssueFilters(options.providerId, options.filters);\n\tif (resolvedIssueFilters.unsupported) {\n\t\treturn refused(\n\t\t\tunsupportedAccountWideIssueFiltersWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\toptions.filters!,\n\t\t\t),\n\t\t);\n\t}\n\n\t// Every provider's account-wide read is a UNION of several queries — GitHub's three `@me` searches,\n\t// GitLab's one REST call per relationship, Azure's (project x relationship) drains — so it always merges,\n\t// and `supportedAccountWideIssueSorts` already lists only keys a merge can honor. No separate\n\t// mergeability refusal is needed here, unlike on the repo-scoped path below.\n\tconst resolvedSort = resolveIssueSort(\n\t\tprovidersMetadata[options.providerId]?.supportedAccountWideIssueSorts,\n\t\toptions.sort,\n\t);\n\tif (resolvedSort.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSortWarning(options.providerId, domain, options.connectionId, resolvedSort.rejection),\n\t\t);\n\t}\n\n\t// `merged: true` unconditionally: this read has no scope count for a caller to reduce. It is also why no\n\t// `unmergeable` check follows — `supportedAccountWideIssueSorts` declares only keys a merge can honor.\n\tconst accountWideOrdering = toIssueOrdering(resolvedSort.sort, true);\n\n\t// The repo-scoped core rejects empty repos (GitHub/Bitbucket/Azure); read the account-wide,\n\t// already-user-scoped core instead. GitHub exposes a composite cursor across its authored,\n\t// assigned, and mentioned searches. Walk it internally when the caller supplies only page N.\n\tconst readAccountWidePage = (cursor: string | undefined) =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\tintegration.searchMyIssuesWithTruncationResult(undefined, undefined, options.connectionId, {\n\t\t\t\t\tincludeAllAssignees: options.includeAllAssignees,\n\t\t\t\t\tstate: options.state,\n\t\t\t\t\tfilters: resolvedIssueFilters.filters,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tpageSize: options.itemsPerPage,\n\t\t\t\t\torg: options.org,\n\t\t\t\t\tproject: options.project,\n\t\t\t\t\tsort: accountWideOrdering.sort,\n\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\tconst first = await readAccountWidePage(options.cursor);\n\tconst warnings = first.warning != null ? [first.warning] : [];\n\t// Merged across the walked pages, so a per-scope failure reported on an earlier page isn't lost by\n\t// paging past it — the one thing this read accumulates that the filtered search doesn't.\n\tlet allMetadata = first.value?.metadata;\n\tconst drained = await drainFlatPagesToRequestedPage(first, {\n\t\trequestedPage: page,\n\t\tsuppliedCursor: options.cursor,\n\t\twarnings: warnings,\n\t\treadPage: readAccountWidePage,\n\t\tfold: p => {\n\t\t\tallMetadata = mergeCollectionMetadata(allMetadata, p.metadata, 'disjoint');\n\t\t},\n\t});\n\tconst { value, currentPage, requestedPageMissing } = drained;\n\tconst currentTruncated = drained.truncated;\n\tconst pageFetchFailed = drained.fetchFailed;\n\n\t// GitHub, GitLab, and Azure implement an account-wide issue search; a provider that doesn't (Bitbucket\n\t// exposes no issues at all, and `supportsIssues` already short-circuits it above) returns `undefined`\n\t// with no error. Surface that as an explicit unsupported warning + fetchFailed rather than a silent\n\t// empty success — the caller must fall back (e.g. broadenIssues over repos).\n\tif (value == null && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Account-wide issue search is not supported by '${options.providerId}'; scope the read to repositories instead.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst items = requestedPageMissing ? [] : (value?.values ?? []);\n\t// Each query arrived ordered by the provider; their union is not, and the union is what this read publishes.\n\t// Idempotent for GitHub, whose aliased-search engine already ordered the same merge — one call here rather\n\t// than the same logic repeated in each of the three provider implementations.\n\tconst orderedItems = accountWideOrdering.order(items);\n\t// Fold in structured per-scope failures from the account-wide fan-out (e.g. Azure across projects):\n\t// scope-aware warnings + `fetchFailed` when a scope failed, without discarding the successful items.\n\tconst assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, allMetadata);\n\t// An account-wide search that couldn't confirm completeness (a provider cap with no cursor, or a\n\t// per-scope backstop/failure) is incomplete and can't be paged; report it as truncated (+ a\n\t// provider-neutral warning, unless a structured failure already explains it) rather than a complete\n\t// list. Don't hard-code GitHub's \"100 per category\" cap here — Azure reaches this via a per-project\n\t// backstop, and other providers may cap differently.\n\t// The account-wide read is cursor-only (its composite cursor spans several provider searches, so a\n\t// page number can't address it): `hasMore` without a real cursor is a dead end, so report it as\n\t// terminal-but-incomplete rather than inviting the caller to page forever.\n\tconst continuation = resolveContinuation(\n\t\t{\n\t\t\thasMore: requestedPageMissing ? false : (value?.hasMore ?? false),\n\t\t\tcursor: requestedPageMissing ? undefined : usableCursor(value?.cursor),\n\t\t\ttruncated: currentTruncated,\n\t\t},\n\t\tundefined,\n\t);\n\tconst truncated = continuation.truncated || assessment.truncated;\n\tif (truncated && warnings.length === 0) {\n\t\twarnings.push(\n\t\t\ttruncationWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'Account-wide issue search',\n\t\t\t\t// `exhausted`: this composite read exposes no budget the caller can raise, so nothing it\n\t\t\t\t// could call would return the withheld items.\n\t\t\t\tassessment.fetchFailed || pageFetchFailed ? 'interrupted' : 'exhausted',\n\t\t\t),\n\t\t);\n\t}\n\t// A metadata omission from an earlier page asserts the read succeeded; a later page may since have failed.\n\treconcileOmissionsWithFailure(warnings, assessment.fetchFailed || pageFetchFailed);\n\treturn {\n\t\titems: orderedItems,\n\t\twarnings: warnings,\n\t\tpage: {\n\t\t\t// Positional, per ProviderPageInfo.currentPage. `currentPage` already carries what the provider\n\t\t\t// reported or what the internal drain counted; a requested page past the terminal cursor is\n\t\t\t// reported as that empty page N. The account-wide read is cursor-only, so a `page` the caller\n\t\t\t// didn't pair with a cursor is never echoed.\n\t\t\tcurrentPage: requestedPageMissing\n\t\t\t\t? page\n\t\t\t\t: resolveCurrentPage({\n\t\t\t\t\t\tproviderPage: currentPage,\n\t\t\t\t\t\trequestedPage: page,\n\t\t\t\t\t\tsuppliedCursor: options.cursor,\n\t\t\t\t\t\tpageAdvanceable: false,\n\t\t\t\t\t}),\n\t\t\titemsPerPage: orderedItems.length,\n\t\t\ttruncated: truncated || undefined,\n\t\t},\n\t\thasMore: continuation.hasMore,\n\t\tcursor: continuation.cursor,\n\t\tfetchFailed: assessment.fetchFailed || pageFetchFailed || undefined,\n\t\trequestFailed: assessment.requestFailed || pageFetchFailed,\n\t\tunexpectedGap: drained.unexpectedGap,\n\t};\n}\n\n/**\n * The repository-scoped read: one provider query per repository or project, paged by the provider's own\n * continuation and drained internally when the caller supplied only a page number.\n *\n * See {@link readAccountWideIssuesPage} for why the two are separate functions.\n */\nasync function readRepoScopedIssuesPage({\n\toptions,\n\tintegration,\n\tdomain,\n\twarnOnMissingSession,\n\tpage,\n}: IssueReadContext): Promise<ProviderPagedResult<IssueShape>> {\n\tconst metadata = providersMetadata[options.providerId];\n\n\t// How many separate provider queries this page is assembled from, which decides whether the requested order can\n\t// be honored at all. `PagingMode.Repos` (GitHub) sends ONE search however many repositories are named, so its\n\t// page arrives ordered by the provider; `Repo` (GitLab) and `Project` (Azure) issue one query per scope and are\n\t// concatenated here, so a page spanning several of them is only as orderable as a normalized issue is.\n\tconst resolvedRepoScopedSort = resolveIssueSort(metadata?.supportedIssueSorts, options.sort);\n\tif (resolvedRepoScopedSort.rejection != null) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tunsupportedIssueSortWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\tresolvedRepoScopedSort.rejection,\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst issuesPagingMode = metadata?.issuesPagingMode;\n\t// Counted, and read, as the host settles them: a scope can name another project than the one it was parsed with\n\t// (see `GitHostIntegration.resolveRepositoryScopes`), which changes how many queries a per-project read makes. The\n\t// read is handed the settled scopes, flags included, so it reads what was counted rather than settling again.\n\tconst scopes = options.repos ?? [];\n\tlet repos: ProviderReposInput | SettledRepositoryScopes = scopes;\n\tlet counted = scopes;\n\tif (issuesPagingMode === PagingMode.Project && scopes.length) {\n\t\tconst settled = await runCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() => integration.resolveRepositoryScopes(scopes, options.connectionId),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\t\t// No session to read with, or the read's own failure resolving it: answered as that read would, without it.\n\t\tif (settled.value == null) {\n\t\t\treturn refusedPage(page, settled.warning != null ? [settled.warning] : [], settled.warning != null);\n\t\t}\n\n\t\trepos = settled.value;\n\t\tcounted = settled.value.repos;\n\t}\n\tconst ordering = toIssueOrdering(resolvedRepoScopedSort.sort, mergesProviderQueries(issuesPagingMode, counted));\n\t// Refused rather than served as concatenated per-scope runs, which would look ordered without being so — and\n\t// only where it actually merges, since the same key against the same provider is perfectly answerable for a\n\t// single repository or project.\n\tif (ordering.unmergeable != null) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tunmergeableIssueSortWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\tordering.unmergeable,\n\t\t\t\t\tissuesPagingMode === PagingMode.Project ? 'projects' : 'repositories',\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst cursor = options.cursor ?? pageToCursor(page);\n\tconst { value, warning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() =>\n\t\t\t// The shapes seam returns normalized IssueShape, and is overridable by a provider whose only issue\n\t\t\t// client already yields shapes (serving this path without a raw ProviderIssue round-trip).\n\t\t\tintegration.getMyIssuesForReposAsShapesResult(\n\t\t\t\trepos,\n\t\t\t\t// Forward `page`/`pageSize` alongside the cursor so PagingMode.Repo/Project hosts honor the\n\t\t\t\t// requested page and page size rather than ignoring a synthesized page-number cursor.\n\t\t\t\t{\n\t\t\t\t\tfilters: options.filters,\n\t\t\t\t\tincludeAllAssignees: options.includeAllAssignees,\n\t\t\t\t\tstate: options.state,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t// The normalized `page`, so the number forwarded to the provider can't disagree with the\n\t\t\t\t\t// page-number cursor beside it — both are built from the same value. Only when the caller asked\n\t\t\t\t\t// for one: an omitted page is the provider's own first page.\n\t\t\t\t\tpage: options.page != null ? page : undefined,\n\t\t\t\t\tpageSize: options.itemsPerPage,\n\t\t\t\t\tsort: ordering.sort,\n\t\t\t\t},\n\t\t\t\toptions.connectionId,\n\t\t\t),\n\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t);\n\n\tlet items = value?.values ?? [];\n\tconst warnings = warning != null ? [warning] : [];\n\tlet pageFetchFailed = warning != null && value == null;\n\tlet paged = toProviderPageInfo(options.itemsPerPage ?? items.length, value?.paging);\n\tlet allMetadata = value?.metadata;\n\tlet drainedGap = false;\n\n\t// Cursor-only repo-scoped hosts (e.g. GitHub) ignore a synthesized page-number cursor, so a page-only\n\t// request is advanced through the provider's own continuations (see drainToRequestedPage).\n\tif (\n\t\tissuesPagingMode === PagingMode.Repos &&\n\t\toptions.page != null &&\n\t\tpage > 1 &&\n\t\toptions.cursor == null &&\n\t\tpaged.page.currentPage === 1\n\t) {\n\t\tconst drained = await drainToRequestedPage(\n\t\t\t{ items: items, paged: paged, metadata: allMetadata, fetchFailed: pageFetchFailed },\n\t\t\t{\n\t\t\t\trequestedPage: page,\n\t\t\t\titemsPerPage: options.itemsPerPage,\n\t\t\t\twarnings: warnings,\n\t\t\t\treadPage: (cursor: string) =>\n\t\t\t\t\trunCaptured(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t() =>\n\t\t\t\t\t\t\tintegration.getMyIssuesForReposAsShapesResult(\n\t\t\t\t\t\t\t\trepos,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tfilters: options.filters,\n\t\t\t\t\t\t\t\t\tincludeAllAssignees: options.includeAllAssignees,\n\t\t\t\t\t\t\t\t\tstate: options.state,\n\t\t\t\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\t\t\t\tpageSize: options.itemsPerPage,\n\t\t\t\t\t\t\t\t\tsort: ordering.sort,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t\t\t),\n\t\t\t},\n\t\t);\n\t\titems = drained.items;\n\t\tpaged = drained.paged;\n\t\tallMetadata = drained.metadata;\n\t\tpageFetchFailed = drained.fetchFailed;\n\t\tdrainedGap = drained.unexpectedGap === true;\n\t}\n\n\t// Ordered once the drain (above) has assembled whichever pages this position spans.\n\titems = ordering.order(items);\n\n\t// Convert the SDK collection metadata into scope-aware warnings + failure/truncation flags, appending\n\t// them to any captured thrown-error warning without discarding the partial result's items.\n\tconst assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, allMetadata);\n\t// Never advertise `hasMore` without a continuation the caller can act on, and only synthesize a page\n\t// number for a host that reads it as one (see `isPageNumberAdvanceable`).\n\tconst issuesPageAdvanceable = isPageNumberAdvanceable(issuesPagingMode);\n\tconst continuation = resolveContinuation(paged, issuesPageAdvanceable ? paged.page.currentPage + 1 : undefined);\n\t// A provider read that couldn't confirm completeness (e.g. Bitbucket's single-page repo issue read\n\t// that dropped a repo) sets `paging.truncated`; surface it as a terminal `page.truncated` so a partial\n\t// page isn't published as complete. Metadata incompleteness is an independent source of the same signal.\n\tconst truncated = continuation.truncated || assessment.truncated;\n\tif (truncated && warnings.length === 0) {\n\t\twarnings.push(\n\t\t\ttruncationWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'Issue',\n\t\t\t\t// `exhausted`, never `page-budget`: a paged read has no budget the caller can raise, and\n\t\t\t\t// ordinary continuation is already expressed by `hasMore`/`cursor`.\n\t\t\t\tassessment.fetchFailed || pageFetchFailed ? 'interrupted' : 'exhausted',\n\t\t\t),\n\t\t);\n\t}\n\t// A metadata omission from an earlier page asserts the read succeeded; a later page may since have failed.\n\treconcileOmissionsWithFailure(warnings, assessment.fetchFailed || pageFetchFailed);\n\treturn {\n\t\titems: items,\n\t\twarnings: warnings,\n\t\tpage: {\n\t\t\t...paged.page,\n\t\t\t// Positional, per ProviderPageInfo.currentPage: the drain (above) already advanced `paged.page` for\n\t\t\t// a cursor-only host, and a caller-threaded cursor advanced the provider without one.\n\t\t\tcurrentPage: resolveCurrentPage({\n\t\t\t\tproviderPage: paged.page.currentPage,\n\t\t\t\trequestedPage: page,\n\t\t\t\tsuppliedCursor: options.cursor,\n\t\t\t\tpageAdvanceable: issuesPageAdvanceable,\n\t\t\t}),\n\t\t\ttruncated: truncated || undefined,\n\t\t},\n\t\thasMore: continuation.hasMore,\n\t\tcursor: continuation.cursor,\n\t\tfetchFailed: assessment.fetchFailed || pageFetchFailed || undefined,\n\t\trequestFailed: assessment.requestFailed || pageFetchFailed,\n\t\tunexpectedGap:\n\t\t\tdrainedGap || (paged.hasMore === true && usableCursor(paged.cursor) == null && !issuesPageAdvanceable),\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -126,6 +126,8 @@ export interface FlatDrainResult<T extends FlatPage> {
|
|
|
126
126
|
/** True when the requested page is past the provider's last one, so it is genuinely EMPTY. */
|
|
127
127
|
requestedPageMissing: boolean;
|
|
128
128
|
fetchFailed: boolean;
|
|
129
|
+
/** A walked page promised continuation but supplied none usable, or repeated one. */
|
|
130
|
+
unexpectedGap: boolean;
|
|
129
131
|
}
|
|
130
132
|
/**
|
|
131
133
|
* Walks a FLAT cursor-paged read forward to a requested page number, for a provider whose pages are
|
|
@@ -169,6 +171,8 @@ export interface DrainState<T> {
|
|
|
169
171
|
paged: NormalizedPage;
|
|
170
172
|
metadata: CollectionMetadata | undefined;
|
|
171
173
|
fetchFailed: boolean;
|
|
174
|
+
/** A walked page promised continuation but supplied none usable, or repeated one. */
|
|
175
|
+
unexpectedGap?: boolean;
|
|
172
176
|
}
|
|
173
177
|
/**
|
|
174
178
|
* Advances a cursor-only read to the requested page when the caller supplied only `page`.
|
|
@@ -169,6 +169,7 @@ async function drainFlatPagesToRequestedPage(first, options) {
|
|
|
169
169
|
let fetchFailed = first.warning != null && value == null;
|
|
170
170
|
let currentPage = value?.page ?? 1;
|
|
171
171
|
let truncated = value?.truncated ?? false;
|
|
172
|
+
let unexpectedGap = value?.hasMore === true && usableCursor(value.cursor) == null;
|
|
172
173
|
const walkable = options.suppliedCursor == null && options.requestedPage > 1;
|
|
173
174
|
if (walkable && value != null) for (let nextCursor = usableCursor(value.cursor); currentPage < options.requestedPage && value.hasMore === true && nextCursor != null; nextCursor = usableCursor(value.cursor)) {
|
|
174
175
|
const next = await options.readPage(nextCursor);
|
|
@@ -182,8 +183,10 @@ async function drainFlatPagesToRequestedPage(first, options) {
|
|
|
182
183
|
truncated = truncated || value.truncated;
|
|
183
184
|
options.fold?.(value);
|
|
184
185
|
currentPage = value.page ?? currentPage + 1;
|
|
186
|
+
unexpectedGap ||= value.hasMore === true && usableCursor(value.cursor) == null;
|
|
185
187
|
if (usableCursor(value.cursor) === nextCursor) {
|
|
186
188
|
truncated = true;
|
|
189
|
+
unexpectedGap ||= value.hasMore === true;
|
|
187
190
|
break;
|
|
188
191
|
}
|
|
189
192
|
}
|
|
@@ -192,7 +195,8 @@ async function drainFlatPagesToRequestedPage(first, options) {
|
|
|
192
195
|
currentPage,
|
|
193
196
|
truncated,
|
|
194
197
|
requestedPageMissing: walkable && currentPage < options.requestedPage,
|
|
195
|
-
fetchFailed
|
|
198
|
+
fetchFailed,
|
|
199
|
+
unexpectedGap
|
|
196
200
|
};
|
|
197
201
|
}
|
|
198
202
|
/**
|
|
@@ -212,6 +216,7 @@ async function drainToRequestedPage(state, options) {
|
|
|
212
216
|
let currentPage = 1;
|
|
213
217
|
let currentCursor = usableCursor(state.paged.hasMore ? state.paged.cursor : void 0);
|
|
214
218
|
let currentHasMore = state.paged.hasMore && currentCursor != null;
|
|
219
|
+
let unexpectedGap = state.paged.hasMore && currentCursor == null;
|
|
215
220
|
let currentTruncated = state.paged.truncated;
|
|
216
221
|
const missRequestedPage = () => {
|
|
217
222
|
items = [];
|
|
@@ -234,6 +239,7 @@ async function drainToRequestedPage(state, options) {
|
|
|
234
239
|
currentPage++;
|
|
235
240
|
currentTruncated ||= next.truncated;
|
|
236
241
|
const nextCursor = usableCursor(next.cursor);
|
|
242
|
+
unexpectedGap ||= next.hasMore && (nextCursor == null || nextCursor === currentCursor);
|
|
237
243
|
if (nextCursor == null || nextCursor === currentCursor) {
|
|
238
244
|
currentCursor = void 0;
|
|
239
245
|
currentHasMore = false;
|
|
@@ -255,7 +261,8 @@ async function drainToRequestedPage(state, options) {
|
|
|
255
261
|
truncated: currentTruncated
|
|
256
262
|
},
|
|
257
263
|
metadata,
|
|
258
|
-
fetchFailed
|
|
264
|
+
fetchFailed,
|
|
265
|
+
unexpectedGap
|
|
259
266
|
};
|
|
260
267
|
}
|
|
261
268
|
//#endregion
|
|
@@ -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, '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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"}
|
|
1
|
+
{"version":3,"file":"paging.js","names":[],"sources":["../../../../../plus/integrations/src/reads/paging.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport type { PagedResult } from '@gitlens/utils/paging.js';\nimport { PagingMode } from '../providers/models.js';\nimport { mergeCollectionMetadata, parsePageCursor, toPageCursor } from '../providers/utils/providerPaging.js';\nimport type { ProviderPagedResult, ProviderPageInfo, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\n\n/**\n * The paging mechanics shared by every paged read on the provider facade: translating the SDK's `paging` into\n * the page-oriented shape consumers get, deciding what position and what continuation a page may honestly\n * claim, and walking a cursor-only read forward to a requested page number.\n *\n * Kept in one module because the value of these rules is that they are the SAME on every read — the contract\n * they implement is documented once on {@link ProviderPageInfo} and on `IntegrationManager`'s paging section.\n * Copies of them drifting apart is exactly the defect this consolidation removed.\n */\n\n/**\n * Whether a repo-scoped read for this provider ADVANCES on a page number, so the facade may synthesize a\n * page-number cursor, `page.currentPage` may echo the request, and a next-page number is a usable continuation.\n *\n * Only `PagingMode.Repos` (GitHub/GHE) is excluded: its read is cursor-only, so it accepts a page number and\n * ignores it, answering with page 1 — which is why {@link drainToRequestedPage} exists for that mode.\n *\n * Every other provider, including one with no declared mode (Bitbucket Data Center), reads `page` as a 1-based\n * page number. That last part is only true as of `@gitkraken/provider-apis` 0.54.0: before it, Bitbucket Data\n * Center consumed `page` as a raw `start` ITEM OFFSET, so a synthesized page 3 asked for `start=3` — a window\n * overlapping page 1 — and this facade had to withhold the synthesized cursor from it. The SDK now converts\n * (`start = (page - 1) * limit`) and reports `nextPage` as a page number, so the guard is gone and that host\n * can be advanced by number like the rest.\n */\nexport function isPageNumberAdvanceable(mode: PagingMode | undefined): boolean {\n\treturn mode !== PagingMode.Repos;\n}\n\n/**\n * Encodes a 1-based page number as the opaque cursor the provider paging layer understands, via the same\n * {@link toPageCursor} the providers encode with (its `parsePageCursor` counterpart is what reads these back).\n * Page 1 needs no cursor: it is the read's own starting position.\n */\nexport function pageToCursor(page: number | undefined): string | undefined {\n\treturn page == null || page <= 1 ? undefined : toPageCursor(page);\n}\n\n/**\n * Filters out the provider paging layer's empty-cursor sentinel. `providersApi` seeds its next-cursor with\n * `'{}'` and leaves it there when the SDK reports another page without an `endCursor`, so `'{}'` means \"no\n * usable continuation\" and must never be threaded back as one.\n */\nexport function usableCursor(cursor: string | undefined): string | undefined {\n\treturn cursor == null || cursor === '{}' ? undefined : cursor;\n}\n\n/**\n * The terminal page a paged read returns when it refuses the request outright — the surface doesn't apply (an\n * issue tracker asked for a repo read), the target couldn't be resolved, a capability is missing, or a filter\n * set isn't expressible server-side. Every one of those is \"no page was served\": empty `items`, no continuation,\n * and the reason carried in `warnings`.\n *\n * `currentPage` stays a caller-supplied parameter rather than being derived here because the refusals differ on\n * what position they can honestly claim: a read the provider advances by page number reports the requested page,\n * while a cursor-only account-wide read has no addressable position and reports 1 (see\n * {@link ProviderPageInfo.currentPage}).\n */\nexport function refusedPage<T>(\n\tcurrentPage: number,\n\twarnings: ProviderWarning[],\n\tfetchFailed: boolean,\n): ProviderPagedResult<T> {\n\treturn {\n\t\titems: [],\n\t\twarnings: warnings,\n\t\tpage: { currentPage: currentPage, itemsPerPage: 0 },\n\t\thasMore: false,\n\t\tfetchFailed: fetchFailed || undefined,\n\t};\n}\n\n/** A provider page normalized off the SDK's `paging`, before continuation and position are reconciled. */\nexport interface NormalizedPage {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\tcursor?: string;\n\ttruncated: boolean;\n}\n\n/**\n * Normalizes a `PagedResult.paging` into the page-oriented shape Kepler consumes: `page`, `hasMore`, and an\n * opaque `cursor` retained only for cursor-only hosts (where jumping straight to page N isn't possible, so the\n * caller threads the cursor back instead).\n */\nexport function toProviderPageInfo(\n\titemsPerPage: number,\n\tpaging: { more?: boolean; cursor?: string; page?: number; pageSize?: number; truncated?: boolean } | undefined,\n): NormalizedPage {\n\tlet cursor: string | undefined;\n\tlet cursorPage: number | undefined;\n\tconst raw = paging?.cursor;\n\tif (raw != null && raw !== '{}') {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(raw) as { type?: string; cursors?: unknown; page?: number };\n\t\t\t// Retain opaque cursor strings for cursor-only hosts, per-repo/project cursor bundles for\n\t\t\t// PagingMode.Repo/Project reads, AND page/offset cursors. The latter matters for reads with no\n\t\t\t// caller-visible page param to increment — e.g. Bitbucket Server's account-wide PR read threads\n\t\t\t// its next `start` offset as a `type:'page'` cursor; dropping it left the caller with\n\t\t\t// `hasMore:true` and nothing to continue with. A page cursor is a valid opaque continuation, so\n\t\t\t// threading it back is always safe even where a page number is also reported.\n\t\t\tif (parsed.type === 'cursor' || parsed.type === 'page' || Array.isArray(parsed.cursors)) {\n\t\t\t\tcursor = raw;\n\t\t\t\t// Per-repo/project cursor bundles also carry the current page number so the facade can report the\n\t\t\t\t// real page when the consumer continues using only the cursor.\n\t\t\t\tif (Array.isArray(parsed.cursors) && parsed.page != null) {\n\t\t\t\t\tcursorPage = parsed.page;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {}\n\t}\n\t// Only echo a page number the provider actually honored. Numbered-page hosts report their own\n\t// `paging.page`; cursor-only hosts (e.g. GitHub PR search) report none and ignore a synthesized\n\t// page-number cursor — returning their first page — so echoing the requested `page` would mislabel\n\t// page 1 as page N. Report page 1 in that case rather than the unapplied request. Per-repo/project\n\t// bundles may carry the page explicitly in the cursor.\n\tconst currentPage = paging?.page ?? cursorPage ?? 1;\n\treturn {\n\t\tpage: {\n\t\t\tcurrentPage: Math.max(1, currentPage),\n\t\t\titemsPerPage: paging?.pageSize ?? itemsPerPage,\n\t\t},\n\t\thasMore: paging?.more ?? false,\n\t\tcursor: cursor,\n\t\t// A single-page provider read that couldn't confirm completeness carries `paging.truncated`;\n\t\t// surface it so callers can flag `page.truncated` instead of publishing a partial read as complete.\n\t\ttruncated: paging?.truncated ?? false,\n\t};\n}\n\n/**\n * Reconciles a provider's `hasMore` with the continuation it actually handed back. A provider can report\n * `hasNextPage: true` while omitting the `endCursor` (the paging layer surfaces that as the sentinel `'{}'`,\n * which {@link toProviderPageInfo} drops), leaving `hasMore: true` with no cursor — a consumer that pages while\n * `hasMore` would then re-request the same page forever. Prefer the real provider cursor; else synthesize the\n * next page number, but only for a read the provider actually advances by page number (`nextPage` omitted for\n * cursor-only reads, which ignore it and answer with their first page again); else report the read as\n * terminal-but-incomplete (`hasMore: false` + `truncated`).\n */\nexport function resolveContinuation(\n\tpaged: { hasMore: boolean; cursor?: string; truncated: boolean },\n\tnextPage: number | undefined,\n): { hasMore: boolean; cursor?: string; truncated: boolean } {\n\tif (!paged.hasMore) return { hasMore: false, cursor: undefined, truncated: paged.truncated };\n\tif (paged.cursor != null) return { hasMore: true, cursor: paged.cursor, truncated: paged.truncated };\n\n\tconst synthesized = nextPage != null ? pageToCursor(nextPage) : undefined;\n\tif (synthesized != null) return { hasMore: true, cursor: synthesized, truncated: paged.truncated };\n\n\treturn { hasMore: false, cursor: undefined, truncated: true };\n}\n\n/**\n * Resolves the position a paged read reports as `page.currentPage`, per the single convention documented on\n * {@link ProviderPageInfo.currentPage}. Every paged read routes through here so the field means the same thing\n * on all of them: positional, never constant-1 for one read and positional for another.\n *\n * `providerPage` is what the provider (or the internal drain) established — 1 means \"nothing reported\".\n * `pageAdvanceable` is whether the read honors a page number at all: false for a cursor-only read, which\n * answers with its first page when handed a synthesized page-number cursor, so the requested `page` must NOT be\n * echoed there.\n */\nexport function resolveCurrentPage(options: {\n\tproviderPage: number;\n\trequestedPage: number;\n\tsuppliedCursor: string | undefined;\n\tpageAdvanceable: boolean;\n}): number {\n\tif (options.providerPage > 1) return options.providerPage;\n\t// A caller-threaded cursor DID advance the provider, so the caller's own position is authoritative for a\n\t// provider that reports none: prefer the page the cursor encodes, else the `page` supplied alongside it.\n\tif (options.suppliedCursor != null) return parsePageCursor(options.suppliedCursor) ?? options.requestedPage;\n\n\treturn options.pageAdvanceable ? options.requestedPage : 1;\n}\n\n/**\n * One page of a FLAT cursor-paged provider read — the shape both issue reads' providers return directly.\n *\n * `hasMore` is optional because one of the two providers leaves it off on a terminal page; absent reads as \"no\n * more\", which is what a provider that reported no continuation means.\n */\nexport interface FlatPage {\n\tcursor?: string;\n\thasMore?: boolean;\n\tpage?: number;\n\ttruncated: boolean;\n}\n\n/** What {@link drainFlatPagesToRequestedPage} established by walking. */\nexport interface FlatDrainResult<T extends FlatPage> {\n\t/** The requested page, or `undefined` when a page failed mid-walk (which also sets `fetchFailed`). */\n\tvalue: T | undefined;\n\t/** The position actually reached — 1 when nothing advanced. */\n\tcurrentPage: number;\n\ttruncated: boolean;\n\t/** True when the requested page is past the provider's last one, so it is genuinely EMPTY. */\n\trequestedPageMissing: boolean;\n\tfetchFailed: boolean;\n\t/** A walked page promised continuation but supplied none usable, or repeated one. */\n\tunexpectedGap: boolean;\n}\n\n/**\n * Walks a FLAT cursor-paged read forward to a requested page number, for a provider whose pages are\n * `{cursor, hasMore, page, truncated}` rather than the SDK's `PagedResult` wrapper.\n *\n * The sibling of {@link drainToRequestedPage}, not a replacement for it: that one normalizes SDK `paging` through\n * {@link toProviderPageInfo}, which retains only cursors whose JSON declares `type`/`cursors` — and the composite\n * cursor of a GitHub aliased search declares neither, so it would be dropped and the walk would never advance.\n * Two shapes genuinely need two entry points; what they must NOT have is two copies of the rules below, which is\n * what this exists to prevent (see this module's own note on drift).\n *\n * The rules, all of them subtle enough to be worth stating once:\n * - The empty-cursor sentinel is filtered, so a provider claiming another page without handing back a usable\n * continuation is not re-read with `'{}'` (which it would answer with page 1 again).\n * - A provider that returns the SAME cursor isn't advancing: stop, and report the read as truncated rather than\n * looping forever or publishing a short page as complete.\n * - A page requested past the provider's terminal cursor is that EMPTY page N — never the last available page\n * relabeled, per {@link ProviderPageInfo.currentPage}.\n * - A page that fails mid-walk latches `fetchFailed` and leaves no value, so the caller can tell \"the read broke\"\n * from \"the page was genuinely empty\".\n *\n * `fold` is the one thing the callers differ on — one merges SDK collection metadata across the walked pages, the\n * other keeps the largest reported match count — so it is a callback rather than a branch.\n */\nexport async function drainFlatPagesToRequestedPage<T extends FlatPage>(\n\tfirst: { value?: T; warning?: ProviderWarning },\n\toptions: {\n\t\trequestedPage: number;\n\t\tsuppliedCursor: string | undefined;\n\t\twarnings: ProviderWarning[];\n\t\treadPage: (cursor: string) => Promise<{ value?: T; warning?: ProviderWarning }>;\n\t\tfold?: (page: T) => void;\n\t},\n): Promise<FlatDrainResult<T>> {\n\tlet value = first.value;\n\tlet fetchFailed = first.warning != null && value == null;\n\tlet currentPage = value?.page ?? 1;\n\tlet truncated = value?.truncated ?? false;\n\tlet unexpectedGap = value?.hasMore === true && usableCursor(value.cursor) == null;\n\n\tconst walkable = options.suppliedCursor == null && options.requestedPage > 1;\n\tif (walkable && value != null) {\n\t\tfor (\n\t\t\tlet nextCursor = usableCursor(value.cursor);\n\t\t\tcurrentPage < options.requestedPage && value.hasMore === true && nextCursor != null;\n\t\t\tnextCursor = usableCursor(value.cursor)\n\t\t) {\n\t\t\tconst next = await options.readPage(nextCursor);\n\t\t\tif (next.warning != null) {\n\t\t\t\tappendDedupedWarning(options.warnings, next.warning);\n\t\t\t}\n\t\t\tif (next.value == null) {\n\t\t\t\tfetchFailed = next.warning != null;\n\t\t\t\tvalue = undefined;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tvalue = next.value;\n\t\t\ttruncated = truncated || value.truncated;\n\t\t\toptions.fold?.(value);\n\t\t\tcurrentPage = value.page ?? currentPage + 1;\n\t\t\tunexpectedGap ||= value.hasMore === true && usableCursor(value.cursor) == null;\n\t\t\tif (usableCursor(value.cursor) === nextCursor) {\n\t\t\t\ttruncated = true;\n\t\t\t\tunexpectedGap ||= value.hasMore === true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tvalue: value,\n\t\tcurrentPage: currentPage,\n\t\ttruncated: truncated,\n\t\t// Every case reduces to \"a page was asked for by number and the walk fell short\", including the one where\n\t\t// no walk ran at all because the first page was already terminal.\n\t\trequestedPageMissing: walkable && currentPage < options.requestedPage,\n\t\tfetchFailed: fetchFailed,\n\t\tunexpectedGap: unexpectedGap,\n\t};\n}\n\n/** The mutable state {@link drainToRequestedPage} carries across the pages it walks. */\nexport interface DrainState<T> {\n\titems: T[];\n\tpaged: NormalizedPage;\n\tmetadata: CollectionMetadata | undefined;\n\tfetchFailed: boolean;\n\t/** A walked page promised continuation but supplied none usable, or repeated one. */\n\tunexpectedGap?: boolean;\n}\n\n/**\n * Advances a cursor-only read to the requested page when the caller supplied only `page`.\n *\n * A cursor-only read accepts a synthesized page-number cursor and ignores it, answering with its first page, so\n * the requested page has to be reached by walking the provider's own opaque continuations. Only the last\n * successfully-read page's items are kept — returning pages 1..N as \"page N\" would duplicate items for a normal\n * paged consumer — while warnings and metadata are merged across the whole drained prefix.\n *\n * Shared by the repo-scoped and account-wide PR reads and the repo-scoped issue read so the three cannot drift:\n * a requested page past the provider's terminal cursor is an EMPTY page N on all of them, per\n * {@link ProviderPageInfo.currentPage}, never the last available page relabeled.\n */\nexport async function drainToRequestedPage<T>(\n\tstate: DrainState<T>,\n\toptions: {\n\t\trequestedPage: number;\n\t\titemsPerPage: number | undefined;\n\t\twarnings: ProviderWarning[];\n\t\treadPage: (cursor: string) => Promise<{\n\t\t\tvalue?: (PagedResult<T> & { metadata?: CollectionMetadata }) | undefined;\n\t\t\twarning?: ProviderWarning;\n\t\t}>;\n\t},\n): Promise<DrainState<T>> {\n\tlet { items, metadata, fetchFailed } = state;\n\tlet currentPage = 1;\n\tlet currentCursor = usableCursor(state.paged.hasMore ? state.paged.cursor : undefined);\n\tlet currentHasMore = state.paged.hasMore && currentCursor != null;\n\tlet unexpectedGap = state.paged.hasMore && currentCursor == null;\n\tlet currentTruncated = state.paged.truncated;\n\t// A page requested past the terminal cursor is that empty page N. Distinguished from the first read\n\t// having failed outright, which is already reported as page N with no items by the caller's own state.\n\tconst missRequestedPage = () => {\n\t\titems = [];\n\t\tcurrentPage = options.requestedPage;\n\t\tcurrentCursor = undefined;\n\t\tcurrentHasMore = false;\n\t};\n\n\tif (fetchFailed) {\n\t\tmissRequestedPage();\n\t}\n\n\twhile (currentPage < options.requestedPage && currentHasMore && currentCursor != null) {\n\t\tconst { value, warning } = await options.readPage(currentCursor);\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(options.warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\tfetchFailed = true;\n\t\t\tmissRequestedPage();\n\t\t\tbreak;\n\t\t}\n\n\t\titems = value.values;\n\t\tmetadata = mergeCollectionMetadata(metadata, value.metadata, 'disjoint');\n\t\tconst next = toProviderPageInfo(options.itemsPerPage ?? value.values.length, value.paging);\n\t\tcurrentPage++;\n\t\tcurrentTruncated ||= next.truncated;\n\t\tconst nextCursor = usableCursor(next.cursor);\n\t\tunexpectedGap ||= next.hasMore && (nextCursor == null || nextCursor === currentCursor);\n\t\t// A provider that hands back the same cursor (or none) isn't advancing; stop rather than refetch\n\t\t// the same page forever.\n\t\tif (nextCursor == null || nextCursor === currentCursor) {\n\t\t\tcurrentCursor = undefined;\n\t\t\tcurrentHasMore = false;\n\t\t\tbreak;\n\t\t}\n\n\t\tcurrentCursor = nextCursor;\n\t\tcurrentHasMore = next.hasMore;\n\t}\n\n\tif (currentPage < options.requestedPage) {\n\t\tmissRequestedPage();\n\t}\n\n\treturn {\n\t\titems: items,\n\t\tpaged: {\n\t\t\tpage: { currentPage: currentPage, itemsPerPage: options.itemsPerPage ?? items.length },\n\t\t\thasMore: currentHasMore,\n\t\t\tcursor: currentCursor,\n\t\t\ttruncated: currentTruncated,\n\t\t},\n\t\tmetadata: metadata,\n\t\tfetchFailed: fetchFailed,\n\t\tunexpectedGap: unexpectedGap,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -48,6 +48,7 @@ async function listPullRequestsPage(ctx, options) {
|
|
|
48
48
|
let allMetadata = value?.metadata;
|
|
49
49
|
const warnings = warning != null ? [warning] : [];
|
|
50
50
|
let pageFetchFailed = warning != null && value == null;
|
|
51
|
+
let drainedGap = false;
|
|
51
52
|
if ((accountWide || providersMetadata[options.providerId]?.pullRequestsPagingMode === "repos") && options.page != null && page > 1 && options.cursor == null && paged.page.currentPage === 1) {
|
|
52
53
|
const drained = await drainToRequestedPage({
|
|
53
54
|
items,
|
|
@@ -64,6 +65,7 @@ async function listPullRequestsPage(ctx, options) {
|
|
|
64
65
|
paged = drained.paged;
|
|
65
66
|
allMetadata = drained.metadata;
|
|
66
67
|
pageFetchFailed = drained.fetchFailed;
|
|
68
|
+
drainedGap = drained.unexpectedGap === true;
|
|
67
69
|
}
|
|
68
70
|
const assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, allMetadata);
|
|
69
71
|
const pageAdvanceable = !accountWide && isPageNumberAdvanceable(providersMetadata[options.providerId]?.pullRequestsPagingMode);
|
|
@@ -92,7 +94,9 @@ async function listPullRequestsPage(ctx, options) {
|
|
|
92
94
|
},
|
|
93
95
|
hasMore: continuation.hasMore,
|
|
94
96
|
cursor: continuation.cursor,
|
|
95
|
-
fetchFailed: assessment.fetchFailed || pageFetchFailed || void 0
|
|
97
|
+
fetchFailed: assessment.fetchFailed || pageFetchFailed || void 0,
|
|
98
|
+
requestFailed: assessment.requestFailed || pageFetchFailed,
|
|
99
|
+
unexpectedGap: drainedGap || paged.hasMore === true && paged.cursor == null && !pageAdvanceable
|
|
96
100
|
};
|
|
97
101
|
}
|
|
98
102
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pullRequests.js","names":[],"sources":["../../../../../plus/integrations/src/reads/pullRequests.ts"],"sourcesContent":["import type { PullRequest, PullRequestShape, PullRequestStateFilter } from '@gitlens/git/models/pullRequest.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport type { ProviderPullRequest, ProviderReposInput, PullRequestFilter } from '../providers/models.js';\nimport { PagingMode, providersMetadata, toPullRequestRow } from '../providers/models.js';\nimport type { ProviderPagedResult } 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 { getPullRequestViewers, runCaptured } from './drains.js';\nimport { resolveAccountWidePullRequestFilters, resolvePullRequestFilters } from './filters.js';\nimport {\n\tdrainToRequestedPage,\n\tisPageNumberAdvanceable,\n\tpageToCursor,\n\trefusedPage,\n\tresolveContinuation,\n\tresolveCurrentPage,\n\ttoProviderPageInfo,\n} from './paging.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\ttruncationWarning,\n\tunsupportedAccountWidePullRequestFiltersWarning,\n\tunsupportedFiltersWarning,\n} from './warnings.js';\n\nexport async function listPullRequestsPage(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\trepos?: ProviderReposInput;\n\t\tstates?: PullRequestStateFilter[];\n\t\t/**\n\t\t * PR relationship filters (e.g. `[Author, Assignee, ReviewRequested]`). On repo-scoped reads members\n\t\t * combine as provider query constraints (normally an intersection); on account-wide reads they form an\n\t\t * exact OR union. The whole set is validated against the selected path's capability.\n\t\t */\n\t\tfilters?: PullRequestFilter[];\n\t\t/**\n\t\t * Account-wide only: include review-requested PRs when the provider's native \"my PRs\" query omits them.\n\t\t * This extends the provider-native result; it is not a narrowing filter.\n\t\t */\n\t\tincludeReviewRequested?: boolean;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\t/**\n\t\t * Requests the lightweight row shape (see {@link IntegrationManager.listPullRequestsPage}). Opt-in\n\t\t * only, and only on the repo-scoped branch: the account-wide branch has always read the lightweight\n\t\t * shape and keeps doing so, so an explicit `false` does not widen it back to the full projection.\n\t\t */\n\t\tsummary?: boolean;\n\t\titemsPerPage?: number;\n\t\tforceSync?: boolean;\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain when no configured connection supplies one. The value must come\n\t\t * from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<PullRequestShape>> {\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, 'pull request 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, 'pull request 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\t// With no repos this is an account-wide \"my PRs\" read; the repo-scoped core rejects an empty `repos`\n\t// input, so route to the account-wide, inherently user-scoped core instead (see drainPullRequests).\n\t// That provider-defined path takes no `pageSize`. A page-only request is handled by walking any opaque\n\t// continuations it supplies below. Do NOT synthesize a page-number cursor for it: cursor-based queries\n\t// (e.g. GitHub `involves:`) ignore a page number and would return their first page.\n\tconst accountWide = (options.repos?.length ?? 0) === 0;\n\tconst cursor = accountWide ? options.cursor : (options.cursor ?? pageToCursor(page));\n\n\tconst resolvedFilters = accountWide\n\t\t? resolveAccountWidePullRequestFilters(options.providerId, options.filters)\n\t\t: resolvePullRequestFilters(options.providerId, options.filters);\n\tif (resolvedFilters.unsupported) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\taccountWide\n\t\t\t\t\t? unsupportedAccountWidePullRequestFiltersWarning(\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\toptions.filters ?? [],\n\t\t\t\t\t\t)\n\t\t\t\t\t: unsupportedFiltersWarning(options.providerId, domain, options.connectionId),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\t// Everything but the cursor, shared by the initial read and the drain's re-reads below so the two cannot\n\t// drift. `summary` is the reason this matters beyond tidiness: a re-read that dropped it would both\n\t// revert to the full payload — on the very path that issues the most requests — and return rows of a\n\t// different shape than the first page, since a summary row reports `headCommit`/`commitCount` as absent.\n\t// `summary` is pinned true here rather than forwarded: this branch has never had a full projection to\n\t// fall back to (see the option's doc), so honoring an explicit `false` would promise a widening the read\n\t// cannot deliver.\n\tconst accountWideInput = {\n\t\tstate: options.states,\n\t\tincludeReviewRequested: options.includeReviewRequested ?? false,\n\t\tfilters: resolvedFilters.filters,\n\t\tsummary: true,\n\t};\n\t// `filters` scopes the read to the current user (the core resolves the account for these), so it returns\n\t// the user's PRs. `pageSize` rides along so PagingMode.Repo hosts (GitLab, Bitbucket, Azure), whose\n\t// per-repo cursor path ignores a synthesized page-number cursor, honor the requested size.\n\tconst scopedInput = {\n\t\tstate: options.states,\n\t\tfilters: resolvedFilters.filters,\n\t\tpageSize: options.itemsPerPage,\n\t\tsummary: options.summary,\n\t};\n\tconst readPage = (pageCursor: string | undefined, requestedPage?: number) =>\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\taccountWide\n\t\t\t\t\t? integration.getMyPullRequestsForUserResult(\n\t\t\t\t\t\t\t{ ...accountWideInput, cursor: pageCursor },\n\t\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t)\n\t\t\t\t\t: integration.getMyPullRequestsForReposResult(\n\t\t\t\t\t\t\toptions.repos ?? [],\n\t\t\t\t\t\t\t{ ...scopedInput, cursor: pageCursor, page: requestedPage },\n\t\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t),\n\t\t\t{\n\t\t\t\twarnOnMissingSession: warnOnMissingSessionForDomain(options.providerId, options.domain),\n\t\t\t},\n\t\t);\n\tconst { value, warning } = await readPage(cursor, options.page != null ? page : undefined);\n\n\tlet items = value?.values ?? [];\n\t// Cursor-only account-wide reads start at page 1; a page-only request is advanced through opaque\n\t// continuations below. Repo-scoped reads report the requested page unless the provider reports its own.\n\tlet paged = toProviderPageInfo(items.length, value?.paging);\n\tlet allMetadata = value?.metadata;\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 warnings = warning != null ? [warning] : [];\n\tlet pageFetchFailed = warning != null && value == null;\n\n\t// Cursor-only reads ignore a synthesized page-number cursor, so a page-only request is advanced through\n\t// the provider's own continuations (see drainToRequestedPage).\n\tif (\n\t\t(accountWide || providersMetadata[options.providerId]?.pullRequestsPagingMode === 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<ProviderPullRequest | PullRequest>(\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\t// A drain advances through provider continuations, so only the cursor positions these reads.\n\t\t\t\treadPage: (cursor: string) => readPage(cursor),\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\tconst assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, allMetadata);\n\t// Never advertise `hasMore` without a continuation the caller can act on. The account-wide read has no\n\t// page to synthesize, and neither does a host that doesn't honor a page number — see\n\t// {@link IntegrationService.isPageNumberAdvanceable} for which do and why absence of a declared mode\n\t// does NOT count as one.\n\tconst pageAdvanceable =\n\t\t!accountWide && isPageNumberAdvanceable(providersMetadata[options.providerId]?.pullRequestsPagingMode);\n\tconst continuation = resolveContinuation(paged, pageAdvanceable ? paged.page.currentPage + 1 : undefined);\n\t// A single-page provider read that couldn't confirm completeness sets `paging.truncated`; surface it\n\t// as a terminal `page.truncated` (not `hasMore`, which has no cursor to advance) so the caller knows\n\t// the page may be incomplete. 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\t// `exhausted`, never `page-budget`: a paged read has no budget the caller can raise. Ordinary\n\t\t// continuation is already expressed by `hasMore`/`cursor`, so reaching here means this page itself\n\t\t// couldn't be completed — raising anything would return the same page.\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'Pull request',\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\tconst viewers = await getPullRequestViewers(integration, options.connectionId, items);\n\tconst projection = accountWide ? 'account-summary' : options.summary ? 'repos-summary' : 'repos';\n\treturn {\n\t\t// Normalize the raw rows to the GitLens-owned shape at the surface boundary.\n\t\titems: items.map((pr, i) =>\n\t\t\ttoPullRequestRow(pr, integration, { currentAccount: viewers[i], projection: projection }),\n\t\t),\n\t\twarnings: warnings,\n\t\t// The account-wide read can't take a page size, so don't echo the requested `itemsPerPage` as if it\n\t\t// had been applied — report what came back.\n\t\tpage: {\n\t\t\t...paged.page,\n\t\t\t// Positional, per ProviderPageInfo.currentPage: the drain (above) already advanced `paged.page`,\n\t\t\t// and a caller-threaded cursor advanced the provider, so neither leaves a cursor-only read stuck\n\t\t\t// reporting page 1.\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: pageAdvanceable,\n\t\t\t}),\n\t\t\titemsPerPage: accountWide ? items.length : paged.page.itemsPerPage,\n\t\t\ttruncated: truncated || undefined,\n\t\t},\n\t\thasMore: continuation.hasMore,\n\t\tcursor: continuation.cursor,\n\t\t// A metadata failure means items are incomplete even when the read didn't throw; a thrown error with\n\t\t// no recovered value is the pre-existing failure case.\n\t\tfetchFailed: assessment.fetchFailed || pageFetchFailed || undefined,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pullRequests.js","names":[],"sources":["../../../../../plus/integrations/src/reads/pullRequests.ts"],"sourcesContent":["import type { PullRequest, PullRequestShape, PullRequestStateFilter } from '@gitlens/git/models/pullRequest.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport type { ProviderPullRequest, ProviderReposInput, PullRequestFilter } from '../providers/models.js';\nimport { PagingMode, providersMetadata, toPullRequestRow } from '../providers/models.js';\nimport type { ProviderPagedResult } 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 { getPullRequestViewers, runCaptured } from './drains.js';\nimport { resolveAccountWidePullRequestFilters, resolvePullRequestFilters } from './filters.js';\nimport {\n\tdrainToRequestedPage,\n\tisPageNumberAdvanceable,\n\tpageToCursor,\n\trefusedPage,\n\tresolveContinuation,\n\tresolveCurrentPage,\n\ttoProviderPageInfo,\n} from './paging.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\ttruncationWarning,\n\tunsupportedAccountWidePullRequestFiltersWarning,\n\tunsupportedFiltersWarning,\n} from './warnings.js';\n\nexport async function listPullRequestsPage(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\trepos?: ProviderReposInput;\n\t\tstates?: PullRequestStateFilter[];\n\t\t/**\n\t\t * PR relationship filters (e.g. `[Author, Assignee, ReviewRequested]`). On repo-scoped reads members\n\t\t * combine as provider query constraints (normally an intersection); on account-wide reads they form an\n\t\t * exact OR union. The whole set is validated against the selected path's capability.\n\t\t */\n\t\tfilters?: PullRequestFilter[];\n\t\t/**\n\t\t * Account-wide only: include review-requested PRs when the provider's native \"my PRs\" query omits them.\n\t\t * This extends the provider-native result; it is not a narrowing filter.\n\t\t */\n\t\tincludeReviewRequested?: boolean;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\t/**\n\t\t * Requests the lightweight row shape (see {@link IntegrationManager.listPullRequestsPage}). Opt-in\n\t\t * only, and only on the repo-scoped branch: the account-wide branch has always read the lightweight\n\t\t * shape and keeps doing so, so an explicit `false` does not widen it back to the full projection.\n\t\t */\n\t\tsummary?: boolean;\n\t\titemsPerPage?: number;\n\t\tforceSync?: boolean;\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain when no configured connection supplies one. The value must come\n\t\t * from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<PullRequestShape>> {\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, 'pull request 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, 'pull request 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\t// With no repos this is an account-wide \"my PRs\" read; the repo-scoped core rejects an empty `repos`\n\t// input, so route to the account-wide, inherently user-scoped core instead (see drainPullRequests).\n\t// That provider-defined path takes no `pageSize`. A page-only request is handled by walking any opaque\n\t// continuations it supplies below. Do NOT synthesize a page-number cursor for it: cursor-based queries\n\t// (e.g. GitHub `involves:`) ignore a page number and would return their first page.\n\tconst accountWide = (options.repos?.length ?? 0) === 0;\n\tconst cursor = accountWide ? options.cursor : (options.cursor ?? pageToCursor(page));\n\n\tconst resolvedFilters = accountWide\n\t\t? resolveAccountWidePullRequestFilters(options.providerId, options.filters)\n\t\t: resolvePullRequestFilters(options.providerId, options.filters);\n\tif (resolvedFilters.unsupported) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\taccountWide\n\t\t\t\t\t? unsupportedAccountWidePullRequestFiltersWarning(\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\toptions.filters ?? [],\n\t\t\t\t\t\t)\n\t\t\t\t\t: unsupportedFiltersWarning(options.providerId, domain, options.connectionId),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\t// Everything but the cursor, shared by the initial read and the drain's re-reads below so the two cannot\n\t// drift. `summary` is the reason this matters beyond tidiness: a re-read that dropped it would both\n\t// revert to the full payload — on the very path that issues the most requests — and return rows of a\n\t// different shape than the first page, since a summary row reports `headCommit`/`commitCount` as absent.\n\t// `summary` is pinned true here rather than forwarded: this branch has never had a full projection to\n\t// fall back to (see the option's doc), so honoring an explicit `false` would promise a widening the read\n\t// cannot deliver.\n\tconst accountWideInput = {\n\t\tstate: options.states,\n\t\tincludeReviewRequested: options.includeReviewRequested ?? false,\n\t\tfilters: resolvedFilters.filters,\n\t\tsummary: true,\n\t};\n\t// `filters` scopes the read to the current user (the core resolves the account for these), so it returns\n\t// the user's PRs. `pageSize` rides along so PagingMode.Repo hosts (GitLab, Bitbucket, Azure), whose\n\t// per-repo cursor path ignores a synthesized page-number cursor, honor the requested size.\n\tconst scopedInput = {\n\t\tstate: options.states,\n\t\tfilters: resolvedFilters.filters,\n\t\tpageSize: options.itemsPerPage,\n\t\tsummary: options.summary,\n\t};\n\tconst readPage = (pageCursor: string | undefined, requestedPage?: number) =>\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\taccountWide\n\t\t\t\t\t? integration.getMyPullRequestsForUserResult(\n\t\t\t\t\t\t\t{ ...accountWideInput, cursor: pageCursor },\n\t\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t)\n\t\t\t\t\t: integration.getMyPullRequestsForReposResult(\n\t\t\t\t\t\t\toptions.repos ?? [],\n\t\t\t\t\t\t\t{ ...scopedInput, cursor: pageCursor, page: requestedPage },\n\t\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t),\n\t\t\t{\n\t\t\t\twarnOnMissingSession: warnOnMissingSessionForDomain(options.providerId, options.domain),\n\t\t\t},\n\t\t);\n\tconst { value, warning } = await readPage(cursor, options.page != null ? page : undefined);\n\n\tlet items = value?.values ?? [];\n\t// Cursor-only account-wide reads start at page 1; a page-only request is advanced through opaque\n\t// continuations below. Repo-scoped reads report the requested page unless the provider reports its own.\n\tlet paged = toProviderPageInfo(items.length, value?.paging);\n\tlet allMetadata = value?.metadata;\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 warnings = warning != null ? [warning] : [];\n\tlet pageFetchFailed = warning != null && value == null;\n\tlet drainedGap = false;\n\n\t// Cursor-only reads ignore a synthesized page-number cursor, so a page-only request is advanced through\n\t// the provider's own continuations (see drainToRequestedPage).\n\tif (\n\t\t(accountWide || providersMetadata[options.providerId]?.pullRequestsPagingMode === 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<ProviderPullRequest | PullRequest>(\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\t// A drain advances through provider continuations, so only the cursor positions these reads.\n\t\t\t\treadPage: (cursor: string) => readPage(cursor),\n\t\t\t},\n\t\t);\n\t\titems = drained.items;\n\t\tpaged = drained.paged;\n\t\tallMetadata = drained.metadata;\n\t\tpageFetchFailed = drained.fetchFailed;\n\t\tdrainedGap = drained.unexpectedGap === true;\n\t}\n\n\tconst assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, allMetadata);\n\t// Never advertise `hasMore` without a continuation the caller can act on. The account-wide read has no\n\t// page to synthesize, and neither does a host that doesn't honor a page number — see\n\t// {@link IntegrationService.isPageNumberAdvanceable} for which do and why absence of a declared mode\n\t// does NOT count as one.\n\tconst pageAdvanceable =\n\t\t!accountWide && isPageNumberAdvanceable(providersMetadata[options.providerId]?.pullRequestsPagingMode);\n\tconst continuation = resolveContinuation(paged, pageAdvanceable ? paged.page.currentPage + 1 : undefined);\n\t// A single-page provider read that couldn't confirm completeness sets `paging.truncated`; surface it\n\t// as a terminal `page.truncated` (not `hasMore`, which has no cursor to advance) so the caller knows\n\t// the page may be incomplete. 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\t// `exhausted`, never `page-budget`: a paged read has no budget the caller can raise. Ordinary\n\t\t// continuation is already expressed by `hasMore`/`cursor`, so reaching here means this page itself\n\t\t// couldn't be completed — raising anything would return the same page.\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'Pull request',\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\tconst viewers = await getPullRequestViewers(integration, options.connectionId, items);\n\tconst projection = accountWide ? 'account-summary' : options.summary ? 'repos-summary' : 'repos';\n\treturn {\n\t\t// Normalize the raw rows to the GitLens-owned shape at the surface boundary.\n\t\titems: items.map((pr, i) =>\n\t\t\ttoPullRequestRow(pr, integration, { currentAccount: viewers[i], projection: projection }),\n\t\t),\n\t\twarnings: warnings,\n\t\t// The account-wide read can't take a page size, so don't echo the requested `itemsPerPage` as if it\n\t\t// had been applied — report what came back.\n\t\tpage: {\n\t\t\t...paged.page,\n\t\t\t// Positional, per ProviderPageInfo.currentPage: the drain (above) already advanced `paged.page`,\n\t\t\t// and a caller-threaded cursor advanced the provider, so neither leaves a cursor-only read stuck\n\t\t\t// reporting page 1.\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: pageAdvanceable,\n\t\t\t}),\n\t\t\titemsPerPage: accountWide ? items.length : paged.page.itemsPerPage,\n\t\t\ttruncated: truncated || undefined,\n\t\t},\n\t\thasMore: continuation.hasMore,\n\t\tcursor: continuation.cursor,\n\t\t// A metadata failure means items are incomplete even when the read didn't throw; a thrown error with\n\t\t// no recovered value is the pre-existing failure case.\n\t\tfetchFailed: assessment.fetchFailed || pageFetchFailed || undefined,\n\t\trequestFailed: assessment.requestFailed || pageFetchFailed,\n\t\tunexpectedGap: drainedGap || (paged.hasMore === true && paged.cursor == null && !pageAdvanceable),\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -346,6 +346,13 @@ export interface ProviderResult<T> {
|
|
|
346
346
|
fetchFailed?: boolean;
|
|
347
347
|
}
|
|
348
348
|
export interface ProviderPagedResult<T> extends ProviderResult<T> {
|
|
349
|
+
/**
|
|
350
|
+
* True when a request failed. Unlike `fetchFailed`, a collection read successfully that only left some of its items
|
|
351
|
+
* unreadable doesn't set it; those items are named by their own warnings.
|
|
352
|
+
*/
|
|
353
|
+
requestFailed?: boolean;
|
|
354
|
+
/** True when the provider advertised continuation without a usable cursor or page position, or repeated one. */
|
|
355
|
+
unexpectedGap?: boolean;
|
|
349
356
|
page: ProviderPageInfo;
|
|
350
357
|
hasMore: boolean;
|
|
351
358
|
/**
|