@gitkraken/core-gitlens 0.23.0 → 0.25.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 +34 -1
- package/README.md +23 -15
- package/dist/git/errors.d.ts +6 -5
- package/dist/git/errors.js +7 -6
- package/dist/git/errors.js.map +1 -1
- package/dist/git/features.d.ts +1 -1
- package/dist/git/features.js +1 -1
- package/dist/git/features.js.map +1 -1
- package/dist/git/models/remoteProvider.d.ts +4 -1
- package/dist/git/models/remoteProvider.js.map +1 -1
- package/dist/git/providers/provider.d.ts +4 -2
- package/dist/git/utils/branch.utils.d.ts +4 -1
- package/dist/git/utils/branch.utils.js +5 -2
- package/dist/git/utils/branch.utils.js.map +1 -1
- package/dist/git/utils/mergeConflicts.utils.js +2 -2
- package/dist/git/utils/mergeConflicts.utils.js.map +1 -1
- package/dist/git-cli/cliGitProvider.d.ts +14 -3
- package/dist/git-cli/cliGitProvider.js +71 -34
- package/dist/git-cli/cliGitProvider.js.map +1 -1
- package/dist/git-cli/exec/git.d.ts +7 -1
- package/dist/git-cli/exec/git.js +19 -4
- package/dist/git-cli/exec/git.js.map +1 -1
- package/dist/git-cli/providers/branchRemote.js +2 -5
- package/dist/git-cli/providers/branchRemote.js.map +1 -1
- package/dist/git-cli/providers/branches.js +4 -4
- package/dist/git-cli/providers/branches.js.map +1 -1
- package/dist/git-cli/providers/config.js +6 -3
- package/dist/git-cli/providers/config.js.map +1 -1
- package/dist/git-cli/providers/pausedOperations.js +2 -2
- package/dist/git-cli/providers/pausedOperations.js.map +1 -1
- package/dist/git-cli/providers/staging.js +1 -1
- package/dist/ipc/discovery.js +1 -1
- package/dist/plus/agents/providers/claudeCodeTranscript.js +6 -6
- package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
- package/dist/plus/ai/errors.d.ts +4 -1
- package/dist/plus/ai/errors.js +4 -0
- package/dist/plus/ai/errors.js.map +1 -1
- package/dist/plus/ai/providers/mistralProvider.js +1 -1
- package/dist/plus/ai/providers/mistralProvider.js.map +1 -1
- package/dist/plus/ai/providers/openAICompatibleProviderBase.js +2 -1
- package/dist/plus/ai/providers/openAICompatibleProviderBase.js.map +1 -1
- package/dist/plus/git-github/api/github.js +12 -13
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/git-github/api/token.d.ts +2 -0
- package/dist/plus/integrations/authentication/cloudIntegrationService.d.ts +1 -1
- package/dist/plus/integrations/authentication/cloudIntegrationService.js +6 -1
- package/dist/plus/integrations/authentication/cloudIntegrationService.js.map +1 -1
- package/dist/plus/integrations/authentication/configuredIntegrationService.js +2 -2
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +18 -5
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -1
- package/dist/plus/integrations/authentication/integrationAuthenticationService.d.ts +3 -1
- package/dist/plus/integrations/authentication/integrationAuthenticationService.js +4 -0
- package/dist/plus/integrations/authentication/integrationAuthenticationService.js.map +1 -1
- package/dist/plus/integrations/authentication/models.d.ts +2 -0
- package/dist/plus/integrations/authentication/models.js +2 -1
- package/dist/plus/integrations/authentication/models.js.map +1 -1
- package/dist/plus/integrations/authentication/observedAuthenticationProvider.js +1 -1
- package/dist/plus/integrations/authentication/rejectedTokenTracker.d.ts +5 -4
- package/dist/plus/integrations/authentication/rejectedTokenTracker.js +5 -4
- package/dist/plus/integrations/authentication/rejectedTokenTracker.js.map +1 -1
- package/dist/plus/integrations/index.d.ts +2 -2
- package/dist/plus/integrations/index.js +1 -1
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +6 -0
- package/dist/plus/integrations/integrationService.js +3 -3
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/lite.js +1 -1
- package/dist/plus/integrations/manager.d.ts +51 -3
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +14 -0
- package/dist/plus/integrations/models/gitHostIntegration.js +50 -41
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +27 -5
- package/dist/plus/integrations/models/integration.js +57 -15
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issueReads.d.ts +6 -0
- package/dist/plus/integrations/models/issuesIntegration.js +11 -11
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.js +2 -2
- package/dist/plus/integrations/providers/azureDevOps.js +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.js +1 -1
- package/dist/plus/integrations/providers/bitbucket/bitbucket.js +1 -1
- package/dist/plus/integrations/providers/bitbucket/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.js +1 -1
- package/dist/plus/integrations/providers/bitbucket.js +1 -1
- package/dist/plus/integrations/providers/github.js +1 -1
- package/dist/plus/integrations/providers/gitlab/gitlab.js +2 -2
- package/dist/plus/integrations/providers/gitlab/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.js +1 -1
- package/dist/plus/integrations/providers/jira-server.js +2 -2
- package/dist/plus/integrations/providers/jira.js +1 -1
- package/dist/plus/integrations/providers/linear.d.ts +4 -4
- package/dist/plus/integrations/providers/linear.js +26 -9
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/models.js +2 -1
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providerErrors.js +9 -0
- package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +2 -0
- package/dist/plus/integrations/providers/providersApi.js +1 -1
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/trello.js +1 -1
- package/dist/plus/integrations/providers/utils.js +1 -1
- package/dist/plus/integrations/reads/counts.js +1 -1
- package/dist/plus/integrations/reads/cursors.js +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +5 -4
- package/dist/plus/integrations/reads/drains.js +28 -8
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.js +1 -1
- package/dist/plus/integrations/reads/issueBatch.d.ts +1 -1
- package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts +4 -1
- package/dist/plus/integrations/reads/issueTracker.js +1 -1
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/issues.d.ts +2 -0
- package/dist/plus/integrations/reads/issues.js +5 -3
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/paging.d.ts +5 -6
- package/dist/plus/integrations/reads/paging.js +5 -6
- package/dist/plus/integrations/reads/paging.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.d.ts +4 -0
- package/dist/plus/integrations/reads/pullRequests.js +4 -2
- package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/resolveRepository.js +1 -1
- package/dist/plus/integrations/reads/sweeps.js +42 -10
- package/dist/plus/integrations/reads/sweeps.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +23 -3
- package/dist/plus/integrations/results.js.map +1 -1
- package/dist/plus/notifications/api.d.ts +20 -0
- package/dist/plus/notifications/api.js +89 -0
- package/dist/plus/notifications/api.js.map +1 -0
- package/dist/plus/notifications/context.d.ts +28 -0
- package/dist/plus/notifications/context.js +0 -0
- package/dist/plus/notifications/errors.d.ts +13 -0
- package/dist/plus/notifications/errors.js +22 -0
- package/dist/plus/notifications/errors.js.map +1 -0
- package/dist/plus/notifications/models.d.ts +94 -0
- package/dist/plus/notifications/models.js +0 -0
- package/dist/plus/notifications/notificationsService.d.ts +115 -0
- package/dist/plus/notifications/notificationsService.js +381 -0
- package/dist/plus/notifications/notificationsService.js.map +1 -0
- package/dist/plus/notifications/parse.d.ts +12 -0
- package/dist/plus/notifications/parse.js +152 -0
- package/dist/plus/notifications/parse.js.map +1 -0
- package/dist/plus/notifications/presentation.d.ts +28 -0
- package/dist/plus/notifications/presentation.js +55 -0
- package/dist/plus/notifications/presentation.js.map +1 -0
- package/dist/utils/env/node/exec.d.ts +10 -1
- package/dist/utils/env/node/exec.js +41 -4
- package/dist/utils/env/node/exec.js.map +1 -1
- package/docs/integrations.md +25 -3
- package/docs/notifications.md +239 -0
- package/package.json +22 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paging.js","names":[],"sources":["../../../../../plus/integrations/src/reads/paging.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport type { PagedResult } from '@gitlens/utils/paging.js';\nimport { PagingMode } from '../providers/models.js';\nimport { mergeCollectionMetadata, parsePageCursor, toPageCursor } from '../providers/utils/providerPaging.js';\nimport type { ProviderPagedResult, ProviderPageInfo, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\n\n/**\n * The paging mechanics shared by every paged read on the provider facade: translating the SDK's `paging` into\n * the page-oriented shape consumers get, deciding what position and what continuation a page may honestly\n * claim, and walking a cursor-only read forward to a requested page number.\n *\n * Kept in one module because the value of these rules is that they are the SAME on every read — the contract\n * they implement is documented once on {@link ProviderPageInfo} and on `IntegrationManager`'s paging section.\n * Copies of them drifting apart is exactly the defect this consolidation removed.\n */\n\n/**\n * Whether a repo-scoped read for this provider ADVANCES on a page number, so the facade may synthesize a\n * page-number cursor, `page.currentPage` may echo the request, and a next-page number is a usable continuation.\n *\n * Only `PagingMode.Repos` (GitHub/GHE) is excluded: its read is cursor-only, so it accepts a page number and\n * ignores it, answering with page 1 — which is why {@link drainToRequestedPage} exists for that mode.\n *\n * Every other provider, including one with no declared mode (Bitbucket Data Center), reads `page` as a 1-based\n * page number. That last part is only true as of `@gitkraken/provider-apis` 0.54.0: before it, Bitbucket Data\n * Center consumed `page` as a raw `start` ITEM OFFSET, so a synthesized page 3 asked for `start=3` — a window\n * overlapping page 1 — and this facade had to withhold the synthesized cursor from it. The SDK now converts\n * (`start = (page - 1) * limit`) and reports `nextPage` as a page number, so the guard is gone and that host\n * can be advanced by number like the rest.\n */\nexport function isPageNumberAdvanceable(mode: PagingMode | undefined): boolean {\n\treturn mode !== PagingMode.Repos;\n}\n\n/**\n * Encodes a 1-based page number as the opaque cursor the provider paging layer understands, via the same\n * {@link toPageCursor} the providers encode with (its `parsePageCursor` counterpart is what reads these back).\n * Page 1 needs no cursor: it is the read's own starting position.\n */\nexport function pageToCursor(page: number | undefined): string | undefined {\n\treturn page == null || page <= 1 ? undefined : toPageCursor(page);\n}\n\n/**\n * Filters out the provider paging layer's empty-cursor sentinel. `providersApi` seeds its next-cursor with\n * `'{}'` and leaves it there when the SDK reports another page without an `endCursor`, so `'{}'` means \"no\n * usable continuation\" and must never be threaded back as one.\n */\nexport function usableCursor(cursor: string | undefined): string | undefined {\n\treturn cursor == null || cursor === '{}' ? undefined : cursor;\n}\n\n/**\n * The terminal page a paged read returns when it refuses the request outright — the surface doesn't apply (an\n * issue tracker asked for a repo read), the target couldn't be resolved, a capability is missing, or a filter\n * set isn't expressible server-side. Every one of those is \"no page was served\": empty `items`, no continuation,\n * and the reason carried in `warnings`.\n *\n * `currentPage` stays a caller-supplied parameter rather than being derived here because the refusals differ on\n * what position they can honestly claim: a read the provider advances by page number reports the requested page,\n * while a cursor-only account-wide read has no addressable position and reports 1 (see\n * {@link ProviderPageInfo.currentPage}).\n */\nexport function refusedPage<T>(\n\tcurrentPage: number,\n\twarnings: ProviderWarning[],\n\tfetchFailed: boolean,\n): ProviderPagedResult<T> {\n\treturn {\n\t\titems: [],\n\t\twarnings: warnings,\n\t\tpage: { currentPage: currentPage, itemsPerPage: 0 },\n\t\thasMore: false,\n\t\tfetchFailed: fetchFailed || undefined,\n\t};\n}\n\n/** A provider page normalized off the SDK's `paging`, before continuation and position are reconciled. */\nexport interface NormalizedPage {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\tcursor?: string;\n\ttruncated: boolean;\n}\n\n/**\n * Normalizes a `PagedResult.paging` into the page-oriented shape Kepler consumes: `page`, `hasMore`, and an\n * opaque `cursor` retained only for cursor-only hosts (where jumping straight to page N isn't possible, so the\n * caller threads the cursor back instead).\n */\nexport function toProviderPageInfo(\n\titemsPerPage: number,\n\tpaging: { more?: boolean; cursor?: string; page?: number; pageSize?: number; truncated?: boolean } | undefined,\n): NormalizedPage {\n\tlet cursor: string | undefined;\n\tlet cursorPage: number | undefined;\n\tconst raw = paging?.cursor;\n\tif (raw != null && raw !== '{}') {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(raw) as { type?: string; cursors?: unknown; page?: number };\n\t\t\t// Retain opaque cursor strings for cursor-only hosts, per-repo/project cursor bundles for\n\t\t\t// PagingMode.Repo/Project reads, AND page/offset cursors. The latter matters for reads with no\n\t\t\t// caller-visible page param to increment — e.g. Bitbucket Server's account-wide PR read threads\n\t\t\t// its next `start` offset as a `type:'page'` cursor; dropping it left the caller with\n\t\t\t// `hasMore:true` and nothing to continue with. A page cursor is a valid opaque continuation, so\n\t\t\t// threading it back is always safe even where a page number is also reported.\n\t\t\tif (parsed.type === 'cursor' || parsed.type === 'page' || Array.isArray(parsed.cursors)) {\n\t\t\t\tcursor = raw;\n\t\t\t\t// Per-repo/project cursor bundles also carry the current page number so the facade can report the\n\t\t\t\t// real page when the consumer continues using only the cursor.\n\t\t\t\tif (Array.isArray(parsed.cursors) && parsed.page != null) {\n\t\t\t\t\tcursorPage = parsed.page;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {}\n\t}\n\t// Only echo a page number the provider actually honored. Numbered-page hosts report their own\n\t// `paging.page`; cursor-only hosts (e.g. GitHub PR search) report none and ignore a synthesized\n\t// page-number cursor — returning their first page — so echoing the requested `page` would mislabel\n\t// page 1 as page N. Report page 1 in that case rather than the unapplied request. Per-repo/project\n\t// bundles may carry the page explicitly in the cursor.\n\tconst currentPage = paging?.page ?? cursorPage ?? 1;\n\treturn {\n\t\tpage: {\n\t\t\tcurrentPage: Math.max(1, currentPage),\n\t\t\titemsPerPage: paging?.pageSize ?? itemsPerPage,\n\t\t},\n\t\thasMore: paging?.more ?? false,\n\t\tcursor: cursor,\n\t\t// A single-page provider read that couldn't confirm completeness carries `paging.truncated`;\n\t\t// surface it so callers can flag `page.truncated` instead of publishing a partial read as complete.\n\t\ttruncated: paging?.truncated ?? false,\n\t};\n}\n\n/**\n * Reconciles a provider's `hasMore` with the continuation it actually handed back. A provider can report\n * `hasNextPage: true` while omitting the `endCursor` (the paging layer surfaces that as the sentinel `'{}'`,\n * which {@link toProviderPageInfo} drops), leaving `hasMore: true` with no cursor — a consumer that pages while\n * `hasMore` would then re-request the same page forever. Prefer the real provider cursor; else synthesize the\n * next page number, but only for a read the provider actually advances by page number (`nextPage` omitted for\n * cursor-only reads, which ignore it and answer with their first page again); else report the read as\n * terminal-but-incomplete (`hasMore: false` + `truncated`).\n */\nexport function resolveContinuation(\n\tpaged: { hasMore: boolean; cursor?: string; truncated: boolean },\n\tnextPage: number | undefined,\n): { hasMore: boolean; cursor?: string; truncated: boolean } {\n\tif (!paged.hasMore) return { hasMore: false, cursor: undefined, truncated: paged.truncated };\n\tif (paged.cursor != null) return { hasMore: true, cursor: paged.cursor, truncated: paged.truncated };\n\n\tconst synthesized = nextPage != null ? pageToCursor(nextPage) : undefined;\n\tif (synthesized != null) return { hasMore: true, cursor: synthesized, truncated: paged.truncated };\n\n\treturn { hasMore: false, cursor: undefined, truncated: true };\n}\n\n/**\n * Resolves the position a paged read reports as `page.currentPage`, per the single convention documented on\n * {@link ProviderPageInfo.currentPage}. Every paged read routes through here so the field means the same thing\n * on all of them: positional, never constant-1 for one read and positional for another.\n *\n * `providerPage` is what the provider (or the internal drain) established — 1 means \"nothing reported\".\n * `pageAdvanceable` is whether the read honors a page number at all: false for a cursor-only read, which\n * answers with its first page when handed a synthesized page-number cursor, so the requested `page` must NOT be\n * echoed there.\n */\nexport function resolveCurrentPage(options: {\n\tproviderPage: number;\n\trequestedPage: number;\n\tsuppliedCursor: string | undefined;\n\tpageAdvanceable: boolean;\n}): number {\n\tif (options.providerPage > 1) return options.providerPage;\n\t// A caller-threaded cursor DID advance the provider, so the caller's own position is authoritative for a\n\t// provider that reports none: prefer the page the cursor encodes, else the `page` supplied alongside it.\n\tif (options.suppliedCursor != null) return parsePageCursor(options.suppliedCursor) ?? options.requestedPage;\n\n\treturn options.pageAdvanceable ? options.requestedPage : 1;\n}\n\n/**\n * One page of a FLAT cursor-paged provider read — the shape both issue reads' providers return directly.\n *\n * `hasMore` is optional because one of the two providers leaves it off on a terminal page; absent reads as \"no\n * more\", which is what a provider that reported no continuation means.\n */\nexport interface FlatPage {\n\tcursor?: string;\n\thasMore?: boolean;\n\tpage?: number;\n\ttruncated: boolean;\n}\n\n/** What {@link drainFlatPagesToRequestedPage} established by walking. */\nexport interface FlatDrainResult<T extends FlatPage> {\n\t/** The requested page, or `undefined` when a page failed mid-walk (which also sets `fetchFailed`). */\n\tvalue: T | undefined;\n\t/** The position actually reached — 1 when nothing advanced. */\n\tcurrentPage: number;\n\ttruncated: boolean;\n\t/** True when the requested page is past the provider's last one, so it is genuinely EMPTY. */\n\trequestedPageMissing: boolean;\n\tfetchFailed: boolean;\n\t/** A walked page promised continuation but supplied none usable, or repeated one. */\n\tunexpectedGap: boolean;\n}\n\n/**\n * Walks a FLAT cursor-paged read forward to a requested page number, for a provider whose pages are\n * `{cursor, hasMore, page, truncated}` rather than the SDK's `PagedResult` wrapper.\n *\n * The sibling of {@link drainToRequestedPage}, not a replacement for it: that one normalizes SDK `paging` through\n * {@link toProviderPageInfo}, which retains only cursors whose JSON declares `type`/`cursors` — and the composite\n * cursor of a GitHub aliased search declares neither, so it would be dropped and the walk would never advance.\n * Two shapes genuinely need two entry points; what they must NOT have is two copies of the rules below, which is\n * what this exists to prevent (see this module's own note on drift).\n *\n * The rules, all of them subtle enough to be worth stating once:\n * - The empty-cursor sentinel is filtered, so a provider claiming another page without handing back a usable\n * continuation is not re-read with `'{}'` (which it would answer with page 1 again).\n * - A provider that returns the SAME cursor isn't advancing: stop, and report the read as truncated rather than\n * looping forever or publishing a short page as complete.\n * - A page requested past the provider's terminal cursor is that EMPTY page N — never the last available page\n * relabeled, per {@link ProviderPageInfo.currentPage}.\n * - A page that fails mid-walk latches `fetchFailed` and leaves no value, so the caller can tell \"the read broke\"\n * from \"the page was genuinely empty\".\n *\n * `fold` is the one thing the callers differ on — one merges SDK collection metadata across the walked pages, the\n * other keeps the largest reported match count — so it is a callback rather than a branch.\n */\nexport async function drainFlatPagesToRequestedPage<T extends FlatPage>(\n\tfirst: { value?: T; warning?: ProviderWarning },\n\toptions: {\n\t\trequestedPage: number;\n\t\tsuppliedCursor: string | undefined;\n\t\twarnings: ProviderWarning[];\n\t\treadPage: (cursor: string) => Promise<{ value?: T; warning?: ProviderWarning }>;\n\t\tfold?: (page: T) => void;\n\t},\n): Promise<FlatDrainResult<T>> {\n\tlet value = first.value;\n\tlet fetchFailed = first.warning != null && value == null;\n\tlet currentPage = value?.page ?? 1;\n\tlet truncated = value?.truncated ?? false;\n\tlet unexpectedGap = value?.hasMore === true && usableCursor(value.cursor) == null;\n\n\tconst walkable = options.suppliedCursor == null && options.requestedPage > 1;\n\tif (walkable && value != null) {\n\t\tfor (\n\t\t\tlet nextCursor = usableCursor(value.cursor);\n\t\t\tcurrentPage < options.requestedPage && value.hasMore === true && nextCursor != null;\n\t\t\tnextCursor = usableCursor(value.cursor)\n\t\t) {\n\t\t\tconst next = await options.readPage(nextCursor);\n\t\t\tif (next.warning != null) {\n\t\t\t\tappendDedupedWarning(options.warnings, next.warning);\n\t\t\t}\n\t\t\tif (next.value == null) {\n\t\t\t\tfetchFailed = next.warning != null;\n\t\t\t\tvalue = undefined;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tvalue = next.value;\n\t\t\ttruncated = truncated || value.truncated;\n\t\t\toptions.fold?.(value);\n\t\t\tcurrentPage = value.page ?? currentPage + 1;\n\t\t\tunexpectedGap ||= value.hasMore === true && usableCursor(value.cursor) == null;\n\t\t\tif (usableCursor(value.cursor) === nextCursor) {\n\t\t\t\ttruncated = true;\n\t\t\t\tunexpectedGap ||= value.hasMore === true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tvalue: value,\n\t\tcurrentPage: currentPage,\n\t\ttruncated: truncated,\n\t\t// Every case reduces to \"a page was asked for by number and the walk fell short\", including the one where\n\t\t// no walk ran at all because the first page was already terminal.\n\t\trequestedPageMissing: walkable && currentPage < options.requestedPage,\n\t\tfetchFailed: fetchFailed,\n\t\tunexpectedGap: unexpectedGap,\n\t};\n}\n\n/** The mutable state {@link drainToRequestedPage} carries across the pages it walks. */\nexport interface DrainState<T> {\n\titems: T[];\n\tpaged: NormalizedPage;\n\tmetadata: CollectionMetadata | undefined;\n\tfetchFailed: boolean;\n\t/** A walked page promised continuation but supplied none usable, or repeated one. */\n\tunexpectedGap?: boolean;\n}\n\n/**\n * Advances a cursor-only read to the requested page when the caller supplied only `page`.\n *\n * A cursor-only read accepts a synthesized page-number cursor and ignores it, answering with its first page, so\n * the requested page has to be reached by walking the provider's own opaque continuations. Only the last\n * successfully-read page's items are kept — returning pages 1..N as \"page N\" would duplicate items for a normal\n * paged consumer — while warnings and metadata are merged across the whole drained prefix.\n *\n * Shared by the repo-scoped and account-wide PR reads and the repo-scoped issue read so the three cannot drift:\n * a requested page past the provider's terminal cursor is an EMPTY page N on all of them, per\n * {@link ProviderPageInfo.currentPage}, never the last available page relabeled.\n */\nexport async function drainToRequestedPage<T>(\n\tstate: DrainState<T>,\n\toptions: {\n\t\trequestedPage: number;\n\t\titemsPerPage: number | undefined;\n\t\twarnings: ProviderWarning[];\n\t\treadPage: (cursor: string) => Promise<{\n\t\t\tvalue?: (PagedResult<T> & { metadata?: CollectionMetadata }) | undefined;\n\t\t\twarning?: ProviderWarning;\n\t\t}>;\n\t},\n): Promise<DrainState<T>> {\n\tlet { items, metadata, fetchFailed } = state;\n\tlet currentPage = 1;\n\tlet currentCursor = usableCursor(state.paged.hasMore ? state.paged.cursor : undefined);\n\tlet currentHasMore = state.paged.hasMore && currentCursor != null;\n\tlet unexpectedGap = state.paged.hasMore && currentCursor == null;\n\tlet currentTruncated = state.paged.truncated;\n\t// A page requested past the terminal cursor is that empty page N. Distinguished from the first read\n\t// having failed outright, which is already reported as page N with no items by the caller's own state.\n\tconst missRequestedPage = () => {\n\t\titems = [];\n\t\tcurrentPage = options.requestedPage;\n\t\tcurrentCursor = undefined;\n\t\tcurrentHasMore = false;\n\t};\n\n\tif (fetchFailed) {\n\t\tmissRequestedPage();\n\t}\n\n\twhile (currentPage < options.requestedPage && currentHasMore && currentCursor != null) {\n\t\tconst { value, warning } = await options.readPage(currentCursor);\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(options.warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\tfetchFailed = true;\n\t\t\tmissRequestedPage();\n\t\t\tbreak;\n\t\t}\n\n\t\titems = value.values;\n\t\tmetadata = mergeCollectionMetadata(metadata, value.metadata, 'disjoint');\n\t\tconst next = toProviderPageInfo(options.itemsPerPage ?? value.values.length, value.paging);\n\t\tcurrentPage++;\n\t\tcurrentTruncated ||= next.truncated;\n\t\tconst nextCursor = usableCursor(next.cursor);\n\t\tunexpectedGap ||= next.hasMore && (nextCursor == null || nextCursor === currentCursor);\n\t\t// A provider that hands back the same cursor (or none) isn't advancing; stop rather than refetch\n\t\t// the same page forever.\n\t\tif (nextCursor == null || nextCursor === currentCursor) {\n\t\t\tcurrentCursor = undefined;\n\t\t\tcurrentHasMore = false;\n\t\t\tbreak;\n\t\t}\n\n\t\tcurrentCursor = nextCursor;\n\t\tcurrentHasMore = next.hasMore;\n\t}\n\n\tif (currentPage < options.requestedPage) {\n\t\tmissRequestedPage();\n\t}\n\n\treturn {\n\t\titems: items,\n\t\tpaged: {\n\t\t\tpage: { currentPage: currentPage, itemsPerPage: options.itemsPerPage ?? items.length },\n\t\t\thasMore: currentHasMore,\n\t\t\tcursor: currentCursor,\n\t\t\ttruncated: currentTruncated,\n\t\t},\n\t\tmetadata: metadata,\n\t\tfetchFailed: fetchFailed,\n\t\tunexpectedGap: unexpectedGap,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"paging.js","names":[],"sources":["../../../../../plus/integrations/src/reads/paging.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport type { PagedResult } from '@gitlens/utils/paging.js';\nimport { PagingMode } from '../providers/models.js';\nimport { mergeCollectionMetadata, parsePageCursor, toPageCursor } from '../providers/utils/providerPaging.js';\nimport type { ProviderPagedResult, ProviderPageInfo, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\n\n/**\n * The paging mechanics shared by every paged read on the provider facade: translating the SDK's `paging` into\n * the page-oriented shape consumers get, deciding what position and what continuation a page may honestly\n * claim, and walking a cursor-only read forward to a requested page number.\n *\n * Kept in one module because the value of these rules is that they are the SAME on every read — the contract\n * they implement is documented once on {@link ProviderPageInfo} and on `IntegrationManager`'s paging section.\n * Copies of them drifting apart is exactly the defect this consolidation removed.\n */\n\n/**\n * Whether a repo-scoped read for this provider ADVANCES on a page number, so the facade may synthesize a\n * page-number cursor, `page.currentPage` may echo the request, and a next-page number is a usable continuation.\n *\n * Only `PagingMode.Repos` (GitHub/GHE) is excluded: its read is cursor-only, so it accepts a page number and\n * ignores it, answering with page 1 — which is why {@link drainToRequestedPage} exists for that mode.\n *\n * Every other provider reads `page` as a 1-based page number. For Bitbucket Data Center that is only true as of\n * `@gitkraken/provider-apis` 0.54.0: before it, it consumed `page` as a raw `start` ITEM OFFSET, so a synthesized\n * page 3 asked for `start=3` — a window overlapping page 1 — and this facade had to withhold the synthesized cursor\n * from it. The SDK now converts `page` to an offset and reports `nextPage` as a page number, so that host can be\n * advanced by number like the rest.\n */\nexport function isPageNumberAdvanceable(mode: PagingMode | undefined): boolean {\n\treturn mode !== PagingMode.Repos;\n}\n\n/**\n * Encodes a 1-based page number as the opaque cursor the provider paging layer understands, via the same\n * {@link toPageCursor} the providers encode with (its `parsePageCursor` counterpart is what reads these back).\n * Page 1 needs no cursor: it is the read's own starting position.\n */\nexport function pageToCursor(page: number | undefined): string | undefined {\n\treturn page == null || page <= 1 ? undefined : toPageCursor(page);\n}\n\n/**\n * Filters out the provider paging layer's empty-cursor sentinel. `providersApi` seeds its next-cursor with\n * `'{}'` and leaves it there when the SDK reports another page without an `endCursor`, so `'{}'` means \"no\n * usable continuation\" and must never be threaded back as one.\n */\nexport function usableCursor(cursor: string | undefined): string | undefined {\n\treturn cursor == null || cursor === '{}' ? undefined : cursor;\n}\n\n/**\n * The terminal page a paged read returns when it refuses the request outright — the surface doesn't apply (an\n * issue tracker asked for a repo read), the target couldn't be resolved, a capability is missing, or a filter\n * set isn't expressible server-side. Every one of those is \"no page was served\": empty `items`, no continuation,\n * and the reason carried in `warnings`.\n *\n * `currentPage` stays a caller-supplied parameter rather than being derived here because the refusals differ on\n * what position they can honestly claim: a read the provider advances by page number reports the requested page,\n * while a cursor-only account-wide read has no addressable position and reports 1 (see\n * {@link ProviderPageInfo.currentPage}).\n */\nexport function refusedPage<T>(\n\tcurrentPage: number,\n\twarnings: ProviderWarning[],\n\tfetchFailed: boolean,\n): ProviderPagedResult<T> {\n\treturn {\n\t\titems: [],\n\t\twarnings: warnings,\n\t\tpage: { currentPage: currentPage, itemsPerPage: 0 },\n\t\thasMore: false,\n\t\tfetchFailed: fetchFailed || undefined,\n\t};\n}\n\n/** A provider page normalized off the SDK's `paging`, before continuation and position are reconciled. */\nexport interface NormalizedPage {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\tcursor?: string;\n\ttruncated: boolean;\n}\n\n/**\n * Normalizes a `PagedResult.paging` into the page-oriented shape Kepler consumes: `page`, `hasMore`, and an\n * opaque `cursor` retained only for cursor-only hosts (where jumping straight to page N isn't possible, so the\n * caller threads the cursor back instead).\n */\nexport function toProviderPageInfo(\n\titemsPerPage: number,\n\tpaging: { more?: boolean; cursor?: string; page?: number; pageSize?: number; truncated?: boolean } | undefined,\n): NormalizedPage {\n\tlet cursor: string | undefined;\n\tlet cursorPage: number | undefined;\n\tconst raw = paging?.cursor;\n\tif (raw != null && raw !== '{}') {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(raw) as { type?: string; cursors?: unknown; page?: number };\n\t\t\t// Retain opaque cursor strings for cursor-only hosts, per-repo/project cursor bundles for\n\t\t\t// PagingMode.Repo/Project reads, AND page/offset cursors. The latter matters for reads with no\n\t\t\t// caller-visible page param to increment — e.g. Bitbucket Server's account-wide PR read threads\n\t\t\t// its next `start` offset as a `type:'page'` cursor; dropping it left the caller with\n\t\t\t// `hasMore:true` and nothing to continue with. A page cursor is a valid opaque continuation, so\n\t\t\t// threading it back is always safe even where a page number is also reported.\n\t\t\tif (parsed.type === 'cursor' || parsed.type === 'page' || Array.isArray(parsed.cursors)) {\n\t\t\t\tcursor = raw;\n\t\t\t\t// Per-repo/project cursor bundles also carry the current page number so the facade can report the\n\t\t\t\t// real page when the consumer continues using only the cursor.\n\t\t\t\tif (Array.isArray(parsed.cursors) && parsed.page != null) {\n\t\t\t\t\tcursorPage = parsed.page;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {}\n\t}\n\t// Only echo a page number the provider actually honored. Numbered-page hosts report their own\n\t// `paging.page`; cursor-only hosts (e.g. GitHub PR search) report none and ignore a synthesized\n\t// page-number cursor — returning their first page — so echoing the requested `page` would mislabel\n\t// page 1 as page N. Report page 1 in that case rather than the unapplied request. Per-repo/project\n\t// bundles may carry the page explicitly in the cursor.\n\tconst currentPage = paging?.page ?? cursorPage ?? 1;\n\treturn {\n\t\tpage: {\n\t\t\tcurrentPage: Math.max(1, currentPage),\n\t\t\titemsPerPage: paging?.pageSize ?? itemsPerPage,\n\t\t},\n\t\thasMore: paging?.more ?? false,\n\t\tcursor: cursor,\n\t\t// A single-page provider read that couldn't confirm completeness carries `paging.truncated`;\n\t\t// surface it so callers can flag `page.truncated` instead of publishing a partial read as complete.\n\t\ttruncated: paging?.truncated ?? false,\n\t};\n}\n\n/**\n * Reconciles a provider's `hasMore` with the continuation it actually handed back. A provider can report\n * `hasNextPage: true` while omitting the `endCursor` (the paging layer surfaces that as the sentinel `'{}'`,\n * which {@link toProviderPageInfo} drops), leaving `hasMore: true` with no cursor — a consumer that pages while\n * `hasMore` would then re-request the same page forever. Prefer the real provider cursor; else synthesize the\n * next page number, but only for a read the provider actually advances by page number (`nextPage` omitted for\n * cursor-only reads, which ignore it and answer with their first page again); else report the read as\n * terminal-but-incomplete (`hasMore: false` + `truncated`).\n */\nexport function resolveContinuation(\n\tpaged: { hasMore: boolean; cursor?: string; truncated: boolean },\n\tnextPage: number | undefined,\n): { hasMore: boolean; cursor?: string; truncated: boolean } {\n\tif (!paged.hasMore) return { hasMore: false, cursor: undefined, truncated: paged.truncated };\n\tif (paged.cursor != null) return { hasMore: true, cursor: paged.cursor, truncated: paged.truncated };\n\n\tconst synthesized = nextPage != null ? pageToCursor(nextPage) : undefined;\n\tif (synthesized != null) return { hasMore: true, cursor: synthesized, truncated: paged.truncated };\n\n\treturn { hasMore: false, cursor: undefined, truncated: true };\n}\n\n/**\n * Resolves the position a paged read reports as `page.currentPage`, per the single convention documented on\n * {@link ProviderPageInfo.currentPage}. Every paged read routes through here so the field means the same thing\n * on all of them: positional, never constant-1 for one read and positional for another.\n *\n * `providerPage` is what the provider (or the internal drain) established — 1 means \"nothing reported\".\n * `pageAdvanceable` is whether the read honors a page number at all: false for a cursor-only read, which\n * answers with its first page when handed a synthesized page-number cursor, so the requested `page` must NOT be\n * echoed there.\n */\nexport function resolveCurrentPage(options: {\n\tproviderPage: number;\n\trequestedPage: number;\n\tsuppliedCursor: string | undefined;\n\tpageAdvanceable: boolean;\n}): number {\n\tif (options.providerPage > 1) return options.providerPage;\n\t// A caller-threaded cursor DID advance the provider, so the caller's own position is authoritative for a\n\t// provider that reports none: prefer the page the cursor encodes, else the `page` supplied alongside it.\n\tif (options.suppliedCursor != null) return parsePageCursor(options.suppliedCursor) ?? options.requestedPage;\n\n\treturn options.pageAdvanceable ? options.requestedPage : 1;\n}\n\n/**\n * One page of a FLAT cursor-paged provider read — the shape both issue reads' providers return directly.\n *\n * `hasMore` is optional because one of the two providers leaves it off on a terminal page; absent reads as \"no\n * more\", which is what a provider that reported no continuation means.\n */\nexport interface FlatPage {\n\tcursor?: string;\n\thasMore?: boolean;\n\tpage?: number;\n\ttruncated: boolean;\n}\n\n/** What {@link drainFlatPagesToRequestedPage} established by walking. */\nexport interface FlatDrainResult<T extends FlatPage> {\n\t/** The requested page, or `undefined` when a page failed mid-walk (which also sets `fetchFailed`). */\n\tvalue: T | undefined;\n\t/** The position actually reached — 1 when nothing advanced. */\n\tcurrentPage: number;\n\ttruncated: boolean;\n\t/** True when the requested page is past the provider's last one, so it is genuinely EMPTY. */\n\trequestedPageMissing: boolean;\n\tfetchFailed: boolean;\n\t/** A walked page promised continuation but supplied none usable, or repeated one. */\n\tunexpectedGap: boolean;\n}\n\n/**\n * Walks a FLAT cursor-paged read forward to a requested page number, for a provider whose pages are\n * `{cursor, hasMore, page, truncated}` rather than the SDK's `PagedResult` wrapper.\n *\n * The sibling of {@link drainToRequestedPage}, not a replacement for it: that one normalizes SDK `paging` through\n * {@link toProviderPageInfo}, which retains only cursors whose JSON declares `type`/`cursors` — and the composite\n * cursor of a GitHub aliased search declares neither, so it would be dropped and the walk would never advance.\n * Two shapes genuinely need two entry points; what they must NOT have is two copies of the rules below, which is\n * what this exists to prevent (see this module's own note on drift).\n *\n * The rules, all of them subtle enough to be worth stating once:\n * - The empty-cursor sentinel is filtered, so a provider claiming another page without handing back a usable\n * continuation is not re-read with `'{}'` (which it would answer with page 1 again).\n * - A provider that returns the SAME cursor isn't advancing: stop, and report the read as truncated rather than\n * looping forever or publishing a short page as complete.\n * - A page requested past the provider's terminal cursor is that EMPTY page N — never the last available page\n * relabeled, per {@link ProviderPageInfo.currentPage}.\n * - A page that fails mid-walk latches `fetchFailed` and leaves no value, so the caller can tell \"the read broke\"\n * from \"the page was genuinely empty\".\n *\n * `fold` is the one thing the callers differ on — one merges SDK collection metadata across the walked pages, the\n * other keeps the largest reported match count — so it is a callback rather than a branch.\n */\nexport async function drainFlatPagesToRequestedPage<T extends FlatPage>(\n\tfirst: { value?: T; warning?: ProviderWarning },\n\toptions: {\n\t\trequestedPage: number;\n\t\tsuppliedCursor: string | undefined;\n\t\twarnings: ProviderWarning[];\n\t\treadPage: (cursor: string) => Promise<{ value?: T; warning?: ProviderWarning }>;\n\t\tfold?: (page: T) => void;\n\t},\n): Promise<FlatDrainResult<T>> {\n\tlet value = first.value;\n\tlet fetchFailed = first.warning != null && value == null;\n\tlet currentPage = value?.page ?? 1;\n\tlet truncated = value?.truncated ?? false;\n\tlet unexpectedGap = value?.hasMore === true && usableCursor(value.cursor) == null;\n\n\tconst walkable = options.suppliedCursor == null && options.requestedPage > 1;\n\tif (walkable && value != null) {\n\t\tfor (\n\t\t\tlet nextCursor = usableCursor(value.cursor);\n\t\t\tcurrentPage < options.requestedPage && value.hasMore === true && nextCursor != null;\n\t\t\tnextCursor = usableCursor(value.cursor)\n\t\t) {\n\t\t\tconst next = await options.readPage(nextCursor);\n\t\t\tif (next.warning != null) {\n\t\t\t\tappendDedupedWarning(options.warnings, next.warning);\n\t\t\t}\n\t\t\tif (next.value == null) {\n\t\t\t\tfetchFailed = next.warning != null;\n\t\t\t\tvalue = undefined;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tvalue = next.value;\n\t\t\ttruncated = truncated || value.truncated;\n\t\t\toptions.fold?.(value);\n\t\t\tcurrentPage = value.page ?? currentPage + 1;\n\t\t\tunexpectedGap ||= value.hasMore === true && usableCursor(value.cursor) == null;\n\t\t\tif (usableCursor(value.cursor) === nextCursor) {\n\t\t\t\ttruncated = true;\n\t\t\t\tunexpectedGap ||= value.hasMore === true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tvalue: value,\n\t\tcurrentPage: currentPage,\n\t\ttruncated: truncated,\n\t\t// Every case reduces to \"a page was asked for by number and the walk fell short\", including the one where\n\t\t// no walk ran at all because the first page was already terminal.\n\t\trequestedPageMissing: walkable && currentPage < options.requestedPage,\n\t\tfetchFailed: fetchFailed,\n\t\tunexpectedGap: unexpectedGap,\n\t};\n}\n\n/** The mutable state {@link drainToRequestedPage} carries across the pages it walks. */\nexport interface DrainState<T> {\n\titems: T[];\n\tpaged: NormalizedPage;\n\tmetadata: CollectionMetadata | undefined;\n\tfetchFailed: boolean;\n\t/** A walked page promised continuation but supplied none usable, or repeated one. */\n\tunexpectedGap?: boolean;\n}\n\n/**\n * Advances a cursor-only read to the requested page when the caller supplied only `page`.\n *\n * A cursor-only read accepts a synthesized page-number cursor and ignores it, answering with its first page, so\n * the requested page has to be reached by walking the provider's own opaque continuations. Only the last\n * successfully-read page's items are kept — returning pages 1..N as \"page N\" would duplicate items for a normal\n * paged consumer — while warnings and metadata are merged across the whole drained prefix.\n *\n * Shared by the repo-scoped and account-wide PR reads and the repo-scoped issue read so the three cannot drift:\n * a requested page past the provider's terminal cursor is an EMPTY page N on all of them, per\n * {@link ProviderPageInfo.currentPage}, never the last available page relabeled.\n */\nexport async function drainToRequestedPage<T>(\n\tstate: DrainState<T>,\n\toptions: {\n\t\trequestedPage: number;\n\t\titemsPerPage: number | undefined;\n\t\twarnings: ProviderWarning[];\n\t\treadPage: (cursor: string) => Promise<{\n\t\t\tvalue?: (PagedResult<T> & { metadata?: CollectionMetadata }) | undefined;\n\t\t\twarning?: ProviderWarning;\n\t\t}>;\n\t},\n): Promise<DrainState<T>> {\n\tlet { items, metadata, fetchFailed } = state;\n\tlet currentPage = 1;\n\tlet currentCursor = usableCursor(state.paged.hasMore ? state.paged.cursor : undefined);\n\tlet currentHasMore = state.paged.hasMore && currentCursor != null;\n\tlet unexpectedGap = state.paged.hasMore && currentCursor == null;\n\tlet currentTruncated = state.paged.truncated;\n\t// A page requested past the terminal cursor is that empty page N. Distinguished from the first read\n\t// having failed outright, which is already reported as page N with no items by the caller's own state.\n\tconst missRequestedPage = () => {\n\t\titems = [];\n\t\tcurrentPage = options.requestedPage;\n\t\tcurrentCursor = undefined;\n\t\tcurrentHasMore = false;\n\t};\n\n\tif (fetchFailed) {\n\t\tmissRequestedPage();\n\t}\n\n\twhile (currentPage < options.requestedPage && currentHasMore && currentCursor != null) {\n\t\tconst { value, warning } = await options.readPage(currentCursor);\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(options.warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\tfetchFailed = true;\n\t\t\tmissRequestedPage();\n\t\t\tbreak;\n\t\t}\n\n\t\titems = value.values;\n\t\tmetadata = mergeCollectionMetadata(metadata, value.metadata, 'disjoint');\n\t\tconst next = toProviderPageInfo(options.itemsPerPage ?? value.values.length, value.paging);\n\t\tcurrentPage++;\n\t\tcurrentTruncated ||= next.truncated;\n\t\tconst nextCursor = usableCursor(next.cursor);\n\t\tunexpectedGap ||= next.hasMore && (nextCursor == null || nextCursor === currentCursor);\n\t\t// A provider that hands back the same cursor (or none) isn't advancing; stop rather than refetch\n\t\t// the same page forever.\n\t\tif (nextCursor == null || nextCursor === currentCursor) {\n\t\t\tcurrentCursor = undefined;\n\t\t\tcurrentHasMore = false;\n\t\t\tbreak;\n\t\t}\n\n\t\tcurrentCursor = nextCursor;\n\t\tcurrentHasMore = next.hasMore;\n\t}\n\n\tif (currentPage < options.requestedPage) {\n\t\tmissRequestedPage();\n\t}\n\n\treturn {\n\t\titems: items,\n\t\tpaged: {\n\t\t\tpage: { currentPage: currentPage, itemsPerPage: options.itemsPerPage ?? items.length },\n\t\t\thasMore: currentHasMore,\n\t\t\tcursor: currentCursor,\n\t\t\ttruncated: currentTruncated,\n\t\t},\n\t\tmetadata: metadata,\n\t\tfetchFailed: fetchFailed,\n\t\tunexpectedGap: unexpectedGap,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -27,6 +27,10 @@ export declare function listPullRequestsPage(ctx: ProviderReadContext, options:
|
|
|
27
27
|
* shape and keeps doing so, so an explicit `false` does not widen it back to the full projection.
|
|
28
28
|
*/
|
|
29
29
|
summary?: boolean;
|
|
30
|
+
/** See {@link IntegrationManager.listPullRequestsPage}. */
|
|
31
|
+
scopeConcurrency?: number;
|
|
32
|
+
/** See {@link IntegrationManager.listPullRequestsPage}. */
|
|
33
|
+
includeRemoteInfo?: boolean;
|
|
30
34
|
itemsPerPage?: number;
|
|
31
35
|
forceSync?: boolean;
|
|
32
36
|
connectionId?: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { reconcileOmissionsWithFailure } from "../results.js";
|
|
2
2
|
import { mergeAssessmentInto } from "../collectionMetadata.js";
|
|
3
|
-
import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
|
|
4
3
|
import { providersMetadata, toPullRequestRow } from "../providers/models.js";
|
|
4
|
+
import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
|
|
5
5
|
import { drainToRequestedPage, isPageNumberAdvanceable, pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo } from "./paging.js";
|
|
6
6
|
import { gitHostOnlySurfaceWarning, truncationWarning, unsupportedAccountWidePullRequestFiltersWarning, unsupportedFiltersWarning } from "./warnings.js";
|
|
7
7
|
import { getPullRequestViewers, runCaptured } from "./drains.js";
|
|
@@ -32,7 +32,9 @@ async function listPullRequestsPage(ctx, options) {
|
|
|
32
32
|
state: options.states,
|
|
33
33
|
filters: resolvedFilters.filters,
|
|
34
34
|
pageSize: options.itemsPerPage,
|
|
35
|
-
summary: options.summary
|
|
35
|
+
summary: options.summary,
|
|
36
|
+
scopeConcurrency: options.scopeConcurrency,
|
|
37
|
+
includeRemoteInfo: options.includeRemoteInfo
|
|
36
38
|
};
|
|
37
39
|
const readPage = (pageCursor, requestedPage) => runCaptured(options.providerId, domain, options.connectionId, () => accountWide ? integration.getMyPullRequestsForUserResult({
|
|
38
40
|
...accountWideInput,
|
|
@@ -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\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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"}
|
|
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\t/** See {@link IntegrationManager.listPullRequestsPage}. */\n\t\tscopeConcurrency?: number;\n\t\t/** See {@link IntegrationManager.listPullRequestsPage}. */\n\t\tincludeRemoteInfo?: 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, Bitbucket Data Center,\n\t// Azure), whose 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\tscopeConcurrency: options.scopeConcurrency,\n\t\tincludeRemoteInfo: options.includeRemoteInfo,\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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"}
|
|
@@ -5,8 +5,8 @@ import "../constants.js";
|
|
|
5
5
|
import { toProviderWarning } from "../results.js";
|
|
6
6
|
import { mergeAssessmentInto } from "../collectionMetadata.js";
|
|
7
7
|
import { decodePathSegment, getRemoteHostMatcher, hostFromDomain, isWebRemoteScheme } from "../utils/domain.utils.js";
|
|
8
|
-
import { getIntegrationIdForRemote, getSelfManagedBaseUrl, isGitCloudHostIntegrationId, isGitSelfManagedHostIntegrationId, remoteProviderTypeForConfig, remoteProviderTypeForIntegration } from "../utils/integration.utils.js";
|
|
9
8
|
import { isAzureCloudDomain, isBitbucketCloudDomain, isGitHubDotCom, isGitLabDotCom } from "../providers/models.js";
|
|
9
|
+
import { getIntegrationIdForRemote, getSelfManagedBaseUrl, isGitCloudHostIntegrationId, isGitSelfManagedHostIntegrationId, remoteProviderTypeForConfig, remoteProviderTypeForIntegration } from "../utils/integration.utils.js";
|
|
10
10
|
import { isIssuesIntegration } from "../models/issuesIntegration.js";
|
|
11
11
|
import { noConnectionWarning } from "./warnings.js";
|
|
12
12
|
import { getAzureRepositoryIdentity } from "../providers/azure/azure.utils.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mapBounded } from "../../../utils/promise.js";
|
|
2
2
|
import "../constants.js";
|
|
3
3
|
import { appendDedupedWarning } from "../results.js";
|
|
4
|
-
import { isGitHostIntegration, isIssuesHostIntegrationId } from "../utils/integration.utils.js";
|
|
5
4
|
import { toPullRequestRow } from "../providers/models.js";
|
|
5
|
+
import { isGitHostIntegration, isIssuesHostIntegrationId } from "../utils/integration.utils.js";
|
|
6
6
|
import { gitHostOnlySurfaceWarning, noConnectionWarning, unsupportedAccountWidePullRequestFiltersWarning, unsupportedFiltersWarning } from "./warnings.js";
|
|
7
7
|
import { drainPullRequests, getPullRequestViewers, resolvePullRequestSweepTargets } from "./drains.js";
|
|
8
8
|
import { resolveAccountWidePullRequestFilters, resolvePullRequestFilters } from "./filters.js";
|
|
@@ -17,7 +17,7 @@ import { resolveAccountWidePullRequestFilters, resolvePullRequestFilters } from
|
|
|
17
17
|
*/
|
|
18
18
|
async function sweepTarget(ctx, options, target, attributeUnavailableProviders) {
|
|
19
19
|
const { providerId: id, connectionId, domain: requestedDomain } = target;
|
|
20
|
-
const repos = options?.repos ?? [];
|
|
20
|
+
const repos = target.repos ?? options?.repos ?? [];
|
|
21
21
|
const maxPages = options?.maxPages ?? 100;
|
|
22
22
|
/** A target that never reached a drain: the provider itself failed, so its slice is empty and attributed. */
|
|
23
23
|
const rejectedTarget = (warnings) => ({
|
|
@@ -112,23 +112,53 @@ async function sweepPullRequests(ctx, options) {
|
|
|
112
112
|
const reportSettled = beginTarget?.(target);
|
|
113
113
|
const slice = await sweepTarget(ctx, options, target, attributeUnavailableProviders);
|
|
114
114
|
reportSettled?.(slice);
|
|
115
|
-
return
|
|
115
|
+
return {
|
|
116
|
+
target,
|
|
117
|
+
slice
|
|
118
|
+
};
|
|
116
119
|
});
|
|
117
120
|
const items = [];
|
|
118
121
|
const warnings = [];
|
|
119
|
-
const
|
|
120
|
-
const
|
|
122
|
+
const failedTargets = [];
|
|
123
|
+
const incompleteTargets = [];
|
|
124
|
+
const attributed = /* @__PURE__ */ new Map();
|
|
121
125
|
let fetchFailed = false;
|
|
122
126
|
let truncated = false;
|
|
123
|
-
for (const drain of results) {
|
|
127
|
+
for (const { target, slice: drain } of results) {
|
|
124
128
|
if (drain == null) continue;
|
|
125
129
|
items.push(...drain.items);
|
|
126
130
|
for (const w of drain.warnings) appendDedupedWarning(warnings, w);
|
|
127
131
|
if (drain.fetchFailed) fetchFailed = true;
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
let counts = attributed.get(drain.providerId);
|
|
133
|
+
if (counts == null) {
|
|
134
|
+
counts = {
|
|
135
|
+
total: 0,
|
|
136
|
+
failed: 0,
|
|
137
|
+
incomplete: 0
|
|
138
|
+
};
|
|
139
|
+
attributed.set(drain.providerId, counts);
|
|
140
|
+
}
|
|
141
|
+
counts.total++;
|
|
142
|
+
if (drain.failedProvider || drain.fetchFailed || drain.truncated) {
|
|
143
|
+
const ref = {
|
|
144
|
+
providerId: target.providerId,
|
|
145
|
+
connectionId: target.connectionId,
|
|
146
|
+
domain: ctx.resolveDomainForRead(target.providerId, target.connectionId, target.domain)
|
|
147
|
+
};
|
|
148
|
+
if (drain.failedProvider) {
|
|
149
|
+
counts.failed++;
|
|
150
|
+
failedTargets.push(ref);
|
|
151
|
+
} else {
|
|
152
|
+
counts.incomplete++;
|
|
153
|
+
incompleteTargets.push(ref);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
130
156
|
if (drain.truncated) truncated = true;
|
|
131
157
|
}
|
|
158
|
+
const failedProviderIds = [];
|
|
159
|
+
const incompleteProviderIds = [];
|
|
160
|
+
for (const [providerId, { total, failed, incomplete }] of attributed) if (failed === total) failedProviderIds.push(providerId);
|
|
161
|
+
else if (failed + incomplete !== 0) incompleteProviderIds.push(providerId);
|
|
132
162
|
return {
|
|
133
163
|
items,
|
|
134
164
|
warnings,
|
|
@@ -140,8 +170,10 @@ async function sweepPullRequests(ctx, options) {
|
|
|
140
170
|
},
|
|
141
171
|
hasMore: false,
|
|
142
172
|
fetchFailed: fetchFailed || void 0,
|
|
143
|
-
failedProviderIds
|
|
144
|
-
incompleteProviderIds
|
|
173
|
+
failedProviderIds,
|
|
174
|
+
incompleteProviderIds,
|
|
175
|
+
failedTargets,
|
|
176
|
+
incompleteTargets
|
|
145
177
|
};
|
|
146
178
|
}
|
|
147
179
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sweeps.js","names":[],"sources":["../../../../../plus/integrations/src/reads/sweeps.ts"],"sourcesContent":["import type { PullRequestShape } from '@gitlens/git/models/pullRequest.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { providerFanOutConcurrency } from '../constants.js';\nimport type {\n\tClosedPullRequestSweepOptions,\n\tProviderSweepTarget,\n\tProviderSweepTargetEvent,\n\tPullRequestSweepOptions,\n} from '../manager.js';\nimport { toPullRequestRow } from '../providers/models.js';\nimport type { ProviderSweepResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport { isGitHostIntegration, isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { drainPullRequests, getPullRequestViewers, resolvePullRequestSweepTargets } from './drains.js';\nimport { resolveAccountWidePullRequestFilters, resolvePullRequestFilters } from './filters.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tnoConnectionWarning,\n\tunsupportedAccountWidePullRequestFiltersWarning,\n\tunsupportedFiltersWarning,\n} from './warnings.js';\n\n/**\n * The all-pages pull request reads: one drain per provider target, aggregated into a single sweep result.\n *\n * A sweep exposes no continuation of its own — it drains everything it can reach — so its whole contract lives in\n * the completeness signals it returns: `page.allPages` asserts every page of every target was read,\n * `page.truncated` says pages remain unread, and `failedProviderIds`/`incompleteProviderIds` separate a provider\n * that produced nothing from one that produced a slice with a gap. That split is why the aggregation below can't\n * be collapsed into the generic fan-out merge the hierarchy reads use.\n */\n\n/** One provider target's drained slice, before the cross-provider aggregation. */\ninterface SweepSlice {\n\titems: PullRequestShape[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n\tproviderId: IntegrationIds;\n\tfailedProvider: boolean;\n}\n\n/**\n * Drain ONE sweep target. Extracted from the fan-out so the fan-out callback has a single exit: every\n * per-target observation (see `onTargetSettled`) is then reported in one place instead of at each of this\n * function's several early returns, where a missed branch would silently drop a provider's attribution.\n *\n * `undefined` means the target resolved to no reachable connection and is deliberately not attributed in the\n * aggregate result.\n */\nasync function sweepTarget(\n\tctx: ProviderReadContext,\n\toptions: PullRequestSweepOptions | undefined,\n\ttarget: ProviderSweepTarget,\n\tattributeUnavailableProviders: boolean,\n): Promise<SweepSlice | undefined> {\n\tconst { providerId: id, connectionId, domain: requestedDomain } = target;\n\tconst repos = options?.repos ?? [];\n\tconst maxPages = options?.maxPages ?? 100;\n\t/** A target that never reached a drain: the provider itself failed, so its slice is empty and attributed. */\n\tconst rejectedTarget = (warnings: ProviderWarning[]): SweepSlice => ({\n\t\titems: [],\n\t\twarnings: warnings,\n\t\tfetchFailed: true,\n\t\ttruncated: false,\n\t\tproviderId: id,\n\t\tfailedProvider: true,\n\t});\n\n\tif (isIssuesHostIntegrationId(id)) {\n\t\treturn rejectedTarget([gitHostOnlySurfaceWarning(id, requestedDomain, connectionId, 'pull request sweeps')]);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\tif (integration == null) {\n\t\t// A requested connection that can't be resolved is a broken connection — surface it as a\n\t\t// warning + fetchFailed rather than dropping the provider's slice silently.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\tif (early.warnings.length === 0 && !attributeUnavailableProviders) return undefined;\n\n\t\treturn rejectedTarget(\n\t\t\tearly.warnings.length !== 0 ? early.warnings : [noConnectionWarning(id, requestedDomain, connectionId)],\n\t\t);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn rejectedTarget([gitHostOnlySurfaceWarning(id, requestedDomain, connectionId, 'pull request sweeps')]);\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options?.forceSync, connectionId);\n\n\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\tconst accountWide = repos.length === 0;\n\tconst requestedFilters = target.filters ?? options?.filters;\n\tconst resolved = accountWide\n\t\t? resolveAccountWidePullRequestFilters(id, requestedFilters)\n\t\t: resolvePullRequestFilters(id, requestedFilters);\n\tif (resolved.unsupported) {\n\t\treturn rejectedTarget([\n\t\t\taccountWide\n\t\t\t\t? unsupportedAccountWidePullRequestFiltersWarning(id, domain, connectionId, requestedFilters ?? [])\n\t\t\t\t: unsupportedFiltersWarning(id, domain, connectionId),\n\t\t]);\n\t}\n\n\tconst drain = await drainPullRequests(\n\t\tintegration,\n\t\tid,\n\t\tdomain,\n\t\trepos,\n\t\toptions?.states,\n\t\tresolved.filters,\n\t\toptions,\n\t\tconnectionId,\n\t\tmaxPages,\n\t\tattributeUnavailableProviders,\n\t);\n\tconst viewers = await getPullRequestViewers(integration, connectionId, drain.items);\n\t// Mirrors the projection `drainPullRequests` requests.\n\tconst projection = accountWide ? (options?.includeReviews ? 'account' : 'account-summary') : 'repos';\n\t// Normalize the raw rows to the GitLens-owned shape here, where the per-provider `integration` (the\n\t// mapper's provider reference) is in scope; the aggregation below only sees drains.\n\treturn {\n\t\t...drain,\n\t\titems: drain.items.map((pr, i) =>\n\t\t\ttoPullRequestRow(pr, integration, { currentAccount: viewers[i], projection: projection }),\n\t\t),\n\t\tproviderId: id,\n\t};\n}\n\n/**\n * How a target ended, as the consumer buckets it. `failedProvider` outranks `fetchFailed` because a target\n * whose provider failed produced no slice to be partial about, and `undefined` is the target that resolved to\n * no reachable connection at all.\n */\nfunction sliceOutcome(slice: SweepSlice | undefined): ProviderSweepTargetEvent['outcome'] {\n\tif (slice == null) return 'skipped';\n\tif (slice.failedProvider) return 'failed-provider';\n\tif (slice.fetchFailed) return 'fetch-failed';\n\treturn 'ok';\n}\n\n/**\n * Per-target reporting for a sweep's `onTargetSettled`, opened once per sweep: this stamps the fan-out start,\n * the returned function stamps one target's start, and the function IT returns is the only thing that can\n * report that target. The timestamps therefore never cross a boundary, so there is no pair of interchangeable\n * numbers for a call site to transpose, and the whole reporting concern is one value the fan-out either has or\n * does not. That is also what keeps the option free when it is omitted: with no observer there is nothing to\n * open, so no clock is read and no reporting object exists — do not \"simplify\" that into an unconditional\n * open, because a host whose perf gate is off (the default) is entitled to pay nothing for it.\n *\n * The try/catch is what makes the observer observation-only: called from the fan-out's success path, a throwing\n * callback would otherwise propagate out of the `mapBounded` task and reject the entire sweep — corrupting the\n * read, not just the metric. Swallowed silently; the consumer owns its own aggregation. The `void`-typed\n * callback also admits an `async` consumer, whose rejection is created only after `observe` returns — the\n * `Promise.resolve(...).catch` swallows that too, so it cannot escape as an unhandled rejection.\n *\n * The domain is resolved here rather than carried out of {@link sweepTarget}, so every target reports it by the\n * same rule no matter how far it got. `resolveDomainForRead` needs no integration instance, which is what makes\n * that possible: a target rejected by the first guard resolves the same host a fully drained one does.\n */\nfunction startSweepReporting(\n\tctx: ProviderReadContext,\n\tobserve: (event: ProviderSweepTargetEvent) => void | PromiseLike<unknown>,\n) {\n\tconst fanOutStartedAt = performance.now();\n\n\treturn function beginTarget(target: ProviderSweepTarget) {\n\t\tconst startedAt = performance.now();\n\n\t\treturn function reportSettled(slice: SweepSlice | undefined) {\n\t\t\ttry {\n\t\t\t\tvoid Promise.resolve(\n\t\t\t\t\tobserve({\n\t\t\t\t\t\tproviderId: target.providerId,\n\t\t\t\t\t\tdomain: ctx.resolveDomainForRead(target.providerId, target.connectionId, target.domain),\n\t\t\t\t\t\tconnectionId: target.connectionId,\n\t\t\t\t\t\tcount: slice?.items.length ?? 0,\n\t\t\t\t\t\tdurationMs: performance.now() - startedAt,\n\t\t\t\t\t\tqueueWaitMs: startedAt - fanOutStartedAt,\n\t\t\t\t\t\toutcome: sliceOutcome(slice),\n\t\t\t\t\t\ttruncated: slice?.truncated ?? false,\n\t\t\t\t\t}),\n\t\t\t\t).catch(() => {});\n\t\t\t} catch {}\n\t\t};\n\t};\n}\n\nexport async function sweepPullRequests(\n\tctx: ProviderReadContext,\n\toptions?: PullRequestSweepOptions,\n): Promise<ProviderSweepResult<PullRequestShape>> {\n\tconst { targets, attributeUnavailableProviders } = resolvePullRequestSweepTargets(options);\n\n\tconst observe = options?.onTargetSettled;\n\tconst beginTarget = observe != null ? startSweepReporting(ctx, observe) : undefined;\n\n\tconst results = await mapBounded(targets, providerFanOutConcurrency, async target => {\n\t\tconst reportSettled = beginTarget?.(target);\n\t\tconst slice = await sweepTarget(ctx, options, target, attributeUnavailableProviders);\n\t\treportSettled?.(slice);\n\t\treturn slice;\n\t});\n\n\tconst items: PullRequestShape[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tconst failedProviderIds = new Set<IntegrationIds>();\n\tconst incompleteProviderIds = new Set<IntegrationIds>();\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\tfor (const drain of results) {\n\t\tif (drain == null) {\n\t\t\tcontinue;\n\t\t}\n\n\t\titems.push(...drain.items);\n\t\tfor (const w of drain.warnings) {\n\t\t\tappendDedupedWarning(warnings, w);\n\t\t}\n\t\tif (drain.fetchFailed) {\n\t\t\tfetchFailed = true;\n\t\t}\n\t\tif (drain.failedProvider) {\n\t\t\tfailedProviderIds.add(drain.providerId);\n\t\t} else if (drain.fetchFailed || drain.truncated) {\n\t\t\tincompleteProviderIds.add(drain.providerId);\n\t\t}\n\t\tif (drain.truncated) {\n\t\t\ttruncated = true;\n\t\t}\n\t}\n\n\treturn {\n\t\titems: items,\n\t\twarnings: warnings,\n\t\t// `allPages` asserts completeness — it must be false when any provider truncated (a single-page\n\t\t// account-wide read that couldn't confirm it drained everything) OR a drain aborted on a read\n\t\t// failure (its slice is incomplete). Either way the sweep did not read every page.\n\t\tpage: {\n\t\t\tcurrentPage: 1,\n\t\t\titemsPerPage: items.length,\n\t\t\tallPages: !truncated && !fetchFailed,\n\t\t\ttruncated: truncated || undefined,\n\t\t},\n\t\t// A sweep drains every page itself and exposes no cursor to resume — so `hasMore` must be false even\n\t\t// when the read was incomplete. Terminal incompleteness is expressed through `page.truncated` +\n\t\t// `allPages: false` + warnings; setting `hasMore: true` here would make a consumer that drains while\n\t\t// `hasMore` re-run the identical sweep forever with no cursor to advance.\n\t\thasMore: false,\n\t\tfetchFailed: fetchFailed || undefined,\n\t\tfailedProviderIds: [...failedProviderIds],\n\t\tincompleteProviderIds: [...incompleteProviderIds],\n\t};\n}\n\n/**\n * Closed/merged counterpart of {@link sweepPullRequests}, feeding Kepler's Kanban \"done\" column. Applies\n * the native cross-provider state filter (`Closed` + `Merged`) so it works beyond GitHub.\n */\nexport function sweepClosedPullRequests(\n\tctx: ProviderReadContext,\n\toptions?: ClosedPullRequestSweepOptions,\n): Promise<ProviderSweepResult<PullRequestShape>> {\n\treturn sweepPullRequests(ctx, { ...options, states: ['closed', 'merged'] });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sweeps.js","names":[],"sources":["../../../../../plus/integrations/src/reads/sweeps.ts"],"sourcesContent":["import type { PullRequestShape } from '@gitlens/git/models/pullRequest.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { providerFanOutConcurrency } from '../constants.js';\nimport type {\n\tClosedPullRequestSweepOptions,\n\tProviderSweepTarget,\n\tProviderSweepTargetEvent,\n\tPullRequestSweepOptions,\n} from '../manager.js';\nimport { toPullRequestRow } from '../providers/models.js';\nimport type { ProviderSweepResult, ProviderSweepTargetRef, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport { isGitHostIntegration, isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { drainPullRequests, getPullRequestViewers, resolvePullRequestSweepTargets } from './drains.js';\nimport { resolveAccountWidePullRequestFilters, resolvePullRequestFilters } from './filters.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tnoConnectionWarning,\n\tunsupportedAccountWidePullRequestFiltersWarning,\n\tunsupportedFiltersWarning,\n} from './warnings.js';\n\n/**\n * The all-pages pull request reads: one drain per provider target, aggregated into a single sweep result.\n *\n * A sweep exposes no continuation of its own — it drains everything it can reach — so its whole contract lives in\n * the completeness signals it returns: `page.allPages` asserts every page of every target was read,\n * `page.truncated` says pages remain unread, and `failedTargets`/`incompleteTargets` (summarized per provider by\n * `failedProviderIds`/`incompleteProviderIds`) separate a target that produced nothing from one that produced a\n * slice with a gap. That split is why the aggregation below can't be collapsed into the generic fan-out merge\n * the hierarchy reads use.\n */\n\n/** One provider target's drained slice, before the cross-provider aggregation. */\ninterface SweepSlice {\n\titems: PullRequestShape[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n\tproviderId: IntegrationIds;\n\tfailedProvider: boolean;\n}\n\n/**\n * Drain ONE sweep target. Extracted from the fan-out so the fan-out callback has a single exit: every\n * per-target observation (see `onTargetSettled`) is then reported in one place instead of at each of this\n * function's several early returns, where a missed branch would silently drop a provider's attribution.\n *\n * `undefined` means the target resolved to no reachable connection and is deliberately not attributed in the\n * aggregate result.\n */\nasync function sweepTarget(\n\tctx: ProviderReadContext,\n\toptions: PullRequestSweepOptions | undefined,\n\ttarget: ProviderSweepTarget,\n\tattributeUnavailableProviders: boolean,\n): Promise<SweepSlice | undefined> {\n\tconst { providerId: id, connectionId, domain: requestedDomain } = target;\n\tconst repos = target.repos ?? options?.repos ?? [];\n\tconst maxPages = options?.maxPages ?? 100;\n\t/** A target that never reached a drain: the provider itself failed, so its slice is empty and attributed. */\n\tconst rejectedTarget = (warnings: ProviderWarning[]): SweepSlice => ({\n\t\titems: [],\n\t\twarnings: warnings,\n\t\tfetchFailed: true,\n\t\ttruncated: false,\n\t\tproviderId: id,\n\t\tfailedProvider: true,\n\t});\n\n\tif (isIssuesHostIntegrationId(id)) {\n\t\treturn rejectedTarget([gitHostOnlySurfaceWarning(id, requestedDomain, connectionId, 'pull request sweeps')]);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\tif (integration == null) {\n\t\t// A requested connection that can't be resolved is a broken connection — surface it as a\n\t\t// warning + fetchFailed rather than dropping the provider's slice silently.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\tif (early.warnings.length === 0 && !attributeUnavailableProviders) return undefined;\n\n\t\treturn rejectedTarget(\n\t\t\tearly.warnings.length !== 0 ? early.warnings : [noConnectionWarning(id, requestedDomain, connectionId)],\n\t\t);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn rejectedTarget([gitHostOnlySurfaceWarning(id, requestedDomain, connectionId, 'pull request sweeps')]);\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options?.forceSync, connectionId);\n\n\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\tconst accountWide = repos.length === 0;\n\tconst requestedFilters = target.filters ?? options?.filters;\n\tconst resolved = accountWide\n\t\t? resolveAccountWidePullRequestFilters(id, requestedFilters)\n\t\t: resolvePullRequestFilters(id, requestedFilters);\n\tif (resolved.unsupported) {\n\t\treturn rejectedTarget([\n\t\t\taccountWide\n\t\t\t\t? unsupportedAccountWidePullRequestFiltersWarning(id, domain, connectionId, requestedFilters ?? [])\n\t\t\t\t: unsupportedFiltersWarning(id, domain, connectionId),\n\t\t]);\n\t}\n\n\tconst drain = await drainPullRequests(\n\t\tintegration,\n\t\tid,\n\t\tdomain,\n\t\trepos,\n\t\toptions?.states,\n\t\tresolved.filters,\n\t\toptions,\n\t\tconnectionId,\n\t\tmaxPages,\n\t\tattributeUnavailableProviders,\n\t);\n\tconst viewers = await getPullRequestViewers(integration, connectionId, drain.items);\n\t// Mirrors the projection `drainPullRequests` requests.\n\tconst projection = accountWide ? (options?.includeReviews ? 'account' : 'account-summary') : 'repos';\n\t// Normalize the raw rows to the GitLens-owned shape here, where the per-provider `integration` (the\n\t// mapper's provider reference) is in scope; the aggregation below only sees drains.\n\treturn {\n\t\t...drain,\n\t\titems: drain.items.map((pr, i) =>\n\t\t\ttoPullRequestRow(pr, integration, { currentAccount: viewers[i], projection: projection }),\n\t\t),\n\t\tproviderId: id,\n\t};\n}\n\n/**\n * How a target ended, as the consumer buckets it. `failedProvider` outranks `fetchFailed` because a target\n * whose provider failed produced no slice to be partial about, and `undefined` is the target that resolved to\n * no reachable connection at all.\n */\nfunction sliceOutcome(slice: SweepSlice | undefined): ProviderSweepTargetEvent['outcome'] {\n\tif (slice == null) return 'skipped';\n\tif (slice.failedProvider) return 'failed-provider';\n\tif (slice.fetchFailed) return 'fetch-failed';\n\treturn 'ok';\n}\n\n/**\n * Per-target reporting for a sweep's `onTargetSettled`, opened once per sweep: this stamps the fan-out start,\n * the returned function stamps one target's start, and the function IT returns is the only thing that can\n * report that target. The timestamps therefore never cross a boundary, so there is no pair of interchangeable\n * numbers for a call site to transpose, and the whole reporting concern is one value the fan-out either has or\n * does not. That is also what keeps the option free when it is omitted: with no observer there is nothing to\n * open, so no clock is read and no reporting object exists — do not \"simplify\" that into an unconditional\n * open, because a host whose perf gate is off (the default) is entitled to pay nothing for it.\n *\n * The try/catch is what makes the observer observation-only: called from the fan-out's success path, a throwing\n * callback would otherwise propagate out of the `mapBounded` task and reject the entire sweep — corrupting the\n * read, not just the metric. Swallowed silently; the consumer owns its own aggregation. The `void`-typed\n * callback also admits an `async` consumer, whose rejection is created only after `observe` returns — the\n * `Promise.resolve(...).catch` swallows that too, so it cannot escape as an unhandled rejection.\n *\n * The domain is resolved here rather than carried out of {@link sweepTarget}, so every target reports it by the\n * same rule no matter how far it got. `resolveDomainForRead` needs no integration instance, which is what makes\n * that possible: a target rejected by the first guard resolves the same host a fully drained one does.\n */\nfunction startSweepReporting(\n\tctx: ProviderReadContext,\n\tobserve: (event: ProviderSweepTargetEvent) => void | PromiseLike<unknown>,\n) {\n\tconst fanOutStartedAt = performance.now();\n\n\treturn function beginTarget(target: ProviderSweepTarget) {\n\t\tconst startedAt = performance.now();\n\n\t\treturn function reportSettled(slice: SweepSlice | undefined) {\n\t\t\ttry {\n\t\t\t\tvoid Promise.resolve(\n\t\t\t\t\tobserve({\n\t\t\t\t\t\tproviderId: target.providerId,\n\t\t\t\t\t\tdomain: ctx.resolveDomainForRead(target.providerId, target.connectionId, target.domain),\n\t\t\t\t\t\tconnectionId: target.connectionId,\n\t\t\t\t\t\tcount: slice?.items.length ?? 0,\n\t\t\t\t\t\tdurationMs: performance.now() - startedAt,\n\t\t\t\t\t\tqueueWaitMs: startedAt - fanOutStartedAt,\n\t\t\t\t\t\toutcome: sliceOutcome(slice),\n\t\t\t\t\t\ttruncated: slice?.truncated ?? false,\n\t\t\t\t\t}),\n\t\t\t\t).catch(() => {});\n\t\t\t} catch {}\n\t\t};\n\t};\n}\n\nexport async function sweepPullRequests(\n\tctx: ProviderReadContext,\n\toptions?: PullRequestSweepOptions,\n): Promise<ProviderSweepResult<PullRequestShape>> {\n\tconst { targets, attributeUnavailableProviders } = resolvePullRequestSweepTargets(options);\n\n\tconst observe = options?.onTargetSettled;\n\tconst beginTarget = observe != null ? startSweepReporting(ctx, observe) : undefined;\n\n\tconst results = await mapBounded(targets, providerFanOutConcurrency, async target => {\n\t\tconst reportSettled = beginTarget?.(target);\n\t\tconst slice = await sweepTarget(ctx, options, target, attributeUnavailableProviders);\n\t\treportSettled?.(slice);\n\t\treturn { target: target, slice: slice };\n\t});\n\n\tconst items: PullRequestShape[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tconst failedTargets: ProviderSweepTargetRef[] = [];\n\tconst incompleteTargets: ProviderSweepTargetRef[] = [];\n\t// Per provider: attributed (non-skipped) targets, and how many of them failed or came back incomplete.\n\tconst attributed = new Map<IntegrationIds, { total: number; failed: number; incomplete: number }>();\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\tfor (const { target, slice: drain } of results) {\n\t\tif (drain == null) {\n\t\t\tcontinue;\n\t\t}\n\n\t\titems.push(...drain.items);\n\t\tfor (const w of drain.warnings) {\n\t\t\tappendDedupedWarning(warnings, w);\n\t\t}\n\t\tif (drain.fetchFailed) {\n\t\t\tfetchFailed = true;\n\t\t}\n\n\t\tlet counts = attributed.get(drain.providerId);\n\t\tif (counts == null) {\n\t\t\tcounts = { total: 0, failed: 0, incomplete: 0 };\n\t\t\tattributed.set(drain.providerId, counts);\n\t\t}\n\t\tcounts.total++;\n\n\t\tif (drain.failedProvider || drain.fetchFailed || drain.truncated) {\n\t\t\t// The same ref the target's settle event carries, so a consumer can join the two.\n\t\t\tconst ref: ProviderSweepTargetRef = {\n\t\t\t\tproviderId: target.providerId,\n\t\t\t\tconnectionId: target.connectionId,\n\t\t\t\tdomain: ctx.resolveDomainForRead(target.providerId, target.connectionId, target.domain),\n\t\t\t};\n\t\t\tif (drain.failedProvider) {\n\t\t\t\tcounts.failed++;\n\t\t\t\tfailedTargets.push(ref);\n\t\t\t} else {\n\t\t\t\tcounts.incomplete++;\n\t\t\t\tincompleteTargets.push(ref);\n\t\t\t}\n\t\t}\n\t\tif (drain.truncated) {\n\t\t\ttruncated = true;\n\t\t}\n\t}\n\n\t// A provider is failed only when every target of it failed; any lesser trouble leaves it incomplete.\n\tconst failedProviderIds: IntegrationIds[] = [];\n\tconst incompleteProviderIds: IntegrationIds[] = [];\n\tfor (const [providerId, { total, failed, incomplete }] of attributed) {\n\t\tif (failed === total) {\n\t\t\tfailedProviderIds.push(providerId);\n\t\t} else if (failed + incomplete !== 0) {\n\t\t\tincompleteProviderIds.push(providerId);\n\t\t}\n\t}\n\n\treturn {\n\t\titems: items,\n\t\twarnings: warnings,\n\t\t// `allPages` asserts completeness — it must be false when any provider truncated (a single-page\n\t\t// account-wide read that couldn't confirm it drained everything) OR a drain aborted on a read\n\t\t// failure (its slice is incomplete). Either way the sweep did not read every page.\n\t\tpage: {\n\t\t\tcurrentPage: 1,\n\t\t\titemsPerPage: items.length,\n\t\t\tallPages: !truncated && !fetchFailed,\n\t\t\ttruncated: truncated || undefined,\n\t\t},\n\t\t// A sweep drains every page itself and exposes no cursor to resume — so `hasMore` must be false even\n\t\t// when the read was incomplete. Terminal incompleteness is expressed through `page.truncated` +\n\t\t// `allPages: false` + warnings; setting `hasMore: true` here would make a consumer that drains while\n\t\t// `hasMore` re-run the identical sweep forever with no cursor to advance.\n\t\thasMore: false,\n\t\tfetchFailed: fetchFailed || undefined,\n\t\tfailedProviderIds: failedProviderIds,\n\t\tincompleteProviderIds: incompleteProviderIds,\n\t\tfailedTargets: failedTargets,\n\t\tincompleteTargets: incompleteTargets,\n\t};\n}\n\n/**\n * Closed/merged counterpart of {@link sweepPullRequests}, feeding Kepler's Kanban \"done\" column. Applies\n * the native cross-provider state filter (`Closed` + `Merged`) so it works beyond GitHub.\n */\nexport function sweepClosedPullRequests(\n\tctx: ProviderReadContext,\n\toptions?: ClosedPullRequestSweepOptions,\n): Promise<ProviderSweepResult<PullRequestShape>> {\n\treturn sweepPullRequests(ctx, { ...options, states: ['closed', 'merged'] });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAqDA,eAAe,YACd,KACA,SACA,QACA,+BACkC;CAClC,MAAM,EAAE,YAAY,IAAI,cAAc,QAAQ,oBAAoB;CAClE,MAAM,QAAQ,OAAO,SAAS,SAAS,SAAS,CAAC;CACjD,MAAM,WAAW,SAAS,YAAY;;CAEtC,MAAM,kBAAkB,cAA6C;EACpE,OAAO,CAAC;EACE;EACV,aAAa;EACb,WAAW;EACX,YAAY;EACZ,gBAAgB;CACjB;CAEA,IAAI,0BAA0B,EAAE,GAC/B,OAAO,eAAe,CAAC,0BAA0B,IAAI,iBAAiB,cAAc,qBAAqB,CAAC,CAAC;CAG5G,MAAM,cAAc,MAAM,IAAI,sBAAsB,IAAI,cAAc,eAAe;CACrF,IAAI,eAAe,MAAM;EAGxB,MAAM,QAAQ,IAAI,8BAA8B,IAAI,cAAc,eAAe;EACjF,IAAI,MAAM,SAAS,WAAW,KAAK,CAAC,+BAA+B,OAAO,KAAA;EAE1E,OAAO,eACN,MAAM,SAAS,WAAW,IAAI,MAAM,WAAW,CAAC,oBAAoB,IAAI,iBAAiB,YAAY,CAAC,CACvG;CACD;CACA,IAAI,CAAC,qBAAqB,WAAW,GACpC,OAAO,eAAe,CAAC,0BAA0B,IAAI,iBAAiB,cAAc,qBAAqB,CAAC,CAAC;CAG5G,MAAM,IAAI,wBAAwB,aAAa,SAAS,WAAW,YAAY;CAE/E,MAAM,SAAS,IAAI,cAAc,aAAa,IAAI,cAAc,eAAe;CAC/E,MAAM,cAAc,MAAM,WAAW;CACrC,MAAM,mBAAmB,OAAO,WAAW,SAAS;CACpD,MAAM,WAAW,cACd,qCAAqC,IAAI,gBAAgB,IACzD,0BAA0B,IAAI,gBAAgB;CACjD,IAAI,SAAS,aACZ,OAAO,eAAe,CACrB,cACG,gDAAgD,IAAI,QAAQ,cAAc,oBAAoB,CAAC,CAAC,IAChG,0BAA0B,IAAI,QAAQ,YAAY,CACtD,CAAC;CAGF,MAAM,QAAQ,MAAM,kBACnB,aACA,IACA,QACA,OACA,SAAS,QACT,SAAS,SACT,SACA,cACA,UACA,6BACD;CACA,MAAM,UAAU,MAAM,sBAAsB,aAAa,cAAc,MAAM,KAAK;CAElF,MAAM,aAAa,cAAe,SAAS,iBAAiB,YAAY,oBAAqB;CAG7F,OAAO;EACN,GAAG;EACH,OAAO,MAAM,MAAM,KAAK,IAAI,MAC3B,iBAAiB,IAAI,aAAa;GAAE,gBAAgB,QAAQ;GAAgB;EAAW,CAAC,CACzF;EACA,YAAY;CACb;AACD;;;;;;AAOA,SAAS,aAAa,OAAoE;CACzF,IAAI,SAAS,MAAM,OAAO;CAC1B,IAAI,MAAM,gBAAgB,OAAO;CACjC,IAAI,MAAM,aAAa,OAAO;CAC9B,OAAO;AACR;;;;;;;;;;;;;;;;;;;;AAqBA,SAAS,oBACR,KACA,SACC;CACD,MAAM,kBAAkB,YAAY,IAAI;CAExC,OAAO,SAAS,YAAY,QAA6B;EACxD,MAAM,YAAY,YAAY,IAAI;EAElC,OAAO,SAAS,cAAc,OAA+B;GAC5D,IAAI;IACH,QAAa,QACZ,QAAQ;KACP,YAAY,OAAO;KACnB,QAAQ,IAAI,qBAAqB,OAAO,YAAY,OAAO,cAAc,OAAO,MAAM;KACtF,cAAc,OAAO;KACrB,OAAO,OAAO,MAAM,UAAU;KAC9B,YAAY,YAAY,IAAI,IAAI;KAChC,aAAa,YAAY;KACzB,SAAS,aAAa,KAAK;KAC3B,WAAW,OAAO,aAAa;IAChC,CAAC,CACF,CAAC,CAAC,YAAY,CAAC,CAAC;GACjB,QAAQ,CAAC;EACV;CACD;AACD;AAEA,eAAsB,kBACrB,KACA,SACiD;CACjD,MAAM,EAAE,SAAS,kCAAkC,+BAA+B,OAAO;CAEzF,MAAM,UAAU,SAAS;CACzB,MAAM,cAAc,WAAW,OAAO,oBAAoB,KAAK,OAAO,IAAI,KAAA;CAE1E,MAAM,UAAU,MAAM,WAAW,SAAA,GAAoC,OAAM,WAAU;EACpF,MAAM,gBAAgB,cAAc,MAAM;EAC1C,MAAM,QAAQ,MAAM,YAAY,KAAK,SAAS,QAAQ,6BAA6B;EACnF,gBAAgB,KAAK;EACrB,OAAO;GAAU;GAAe;EAAM;CACvC,CAAC;CAED,MAAM,QAA4B,CAAC;CACnC,MAAM,WAA8B,CAAC;CACrC,MAAM,gBAA0C,CAAC;CACjD,MAAM,oBAA8C,CAAC;CAErD,MAAM,6BAAa,IAAI,IAA2E;CAClG,IAAI,cAAc;CAClB,IAAI,YAAY;CAChB,KAAK,MAAM,EAAE,QAAQ,OAAO,WAAW,SAAS;EAC/C,IAAI,SAAS,MACZ;EAGD,MAAM,KAAK,GAAG,MAAM,KAAK;EACzB,KAAK,MAAM,KAAK,MAAM,UACrB,qBAAqB,UAAU,CAAC;EAEjC,IAAI,MAAM,aACT,cAAc;EAGf,IAAI,SAAS,WAAW,IAAI,MAAM,UAAU;EAC5C,IAAI,UAAU,MAAM;GACnB,SAAS;IAAE,OAAO;IAAG,QAAQ;IAAG,YAAY;GAAE;GAC9C,WAAW,IAAI,MAAM,YAAY,MAAM;EACxC;EACA,OAAO;EAEP,IAAI,MAAM,kBAAkB,MAAM,eAAe,MAAM,WAAW;GAEjE,MAAM,MAA8B;IACnC,YAAY,OAAO;IACnB,cAAc,OAAO;IACrB,QAAQ,IAAI,qBAAqB,OAAO,YAAY,OAAO,cAAc,OAAO,MAAM;GACvF;GACA,IAAI,MAAM,gBAAgB;IACzB,OAAO;IACP,cAAc,KAAK,GAAG;GACvB,OAAO;IACN,OAAO;IACP,kBAAkB,KAAK,GAAG;GAC3B;EACD;EACA,IAAI,MAAM,WACT,YAAY;CAEd;CAGA,MAAM,oBAAsC,CAAC;CAC7C,MAAM,wBAA0C,CAAC;CACjD,KAAK,MAAM,CAAC,YAAY,EAAE,OAAO,QAAQ,iBAAiB,YACzD,IAAI,WAAW,OACd,kBAAkB,KAAK,UAAU;MAC3B,IAAI,SAAS,eAAe,GAClC,sBAAsB,KAAK,UAAU;CAIvC,OAAO;EACC;EACG;EAIV,MAAM;GACL,aAAa;GACb,cAAc,MAAM;GACpB,UAAU,CAAC,aAAa,CAAC;GACzB,WAAW,aAAa,KAAA;EACzB;EAKA,SAAS;EACT,aAAa,eAAe,KAAA;EACT;EACI;EACR;EACI;CACpB;AACD;;;;;AAMA,SAAgB,wBACf,KACA,SACiD;CACjD,OAAO,kBAAkB,KAAK;EAAE,GAAG;EAAS,QAAQ,CAAC,UAAU,QAAQ;CAAE,CAAC;AAC3E"}
|
|
@@ -366,22 +366,42 @@ export interface ProviderPagedResult<T> extends ProviderResult<T> {
|
|
|
366
366
|
*/
|
|
367
367
|
cursor?: string;
|
|
368
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* One sweep target, named the way {@link ProviderSweepTargetEvent} names it, so a consumer can join the two.
|
|
371
|
+
* `domain` is the resolved self-managed host (always `undefined` for a cloud provider).
|
|
372
|
+
*/
|
|
373
|
+
export interface ProviderSweepTargetRef {
|
|
374
|
+
providerId: IntegrationIds;
|
|
375
|
+
connectionId: string | undefined;
|
|
376
|
+
domain: string | undefined;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* A sweep may carry several targets of one provider (one per connection), so the provider arrays below can't say
|
|
380
|
+
* which connection failed: `failedTargets` and `incompleteTargets` are the authoritative attribution, and
|
|
381
|
+
* `failedProviderIds`/`incompleteProviderIds` summarize them. With one target per provider the two agree exactly.
|
|
382
|
+
*/
|
|
369
383
|
export interface ProviderSweepResult<T> extends ProviderResult<T> {
|
|
370
384
|
page: ProviderPageInfo;
|
|
371
385
|
hasMore: boolean;
|
|
372
386
|
/**
|
|
373
|
-
* Providers whose top-level read failed before returning any usable page
|
|
374
|
-
*
|
|
387
|
+
* Providers whose top-level read failed before returning any usable page, for EVERY attributed target of the
|
|
388
|
+
* provider. A provider with a failed target and a surviving sibling is in `incompleteProviderIds` instead.
|
|
389
|
+
* Partial per-scope failures stay represented by `warnings` + `fetchFailed` and are intentionally excluded.
|
|
375
390
|
*/
|
|
376
391
|
failedProviderIds: IntegrationIds[];
|
|
377
392
|
/**
|
|
378
393
|
* Providers whose read started successfully but could not produce an authoritative complete slice (for
|
|
379
|
-
* example, a later page failed, collection metadata reported a partial scope, or a paging backstop was hit)
|
|
394
|
+
* example, a later page failed, collection metadata reported a partial scope, or a paging backstop was hit),
|
|
395
|
+
* or that have a failed target beside one that returned something.
|
|
380
396
|
* Kept separate from
|
|
381
397
|
* `failedProviderIds` so consumers can accept healthy sibling providers while preserving an older snapshot
|
|
382
398
|
* for a provider whose returned slice has a gap.
|
|
383
399
|
*/
|
|
384
400
|
incompleteProviderIds: IntegrationIds[];
|
|
401
|
+
/** Targets whose top-level read failed before returning any usable page. A skipped target is in neither list. */
|
|
402
|
+
failedTargets: ProviderSweepTargetRef[];
|
|
403
|
+
/** Targets that returned a slice with a gap: a later page failed, or pages remain unread. Never also in `failedTargets`. */
|
|
404
|
+
incompleteTargets: ProviderSweepTargetRef[];
|
|
385
405
|
}
|
|
386
406
|
export interface ProviderBroadenResult<T> extends ProviderPagedResult<T> {
|
|
387
407
|
broadenedProviderIds: IntegrationIds[];
|