@gitkraken/core-gitlens 0.9.8 → 0.10.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 +29 -1
- package/dist/git/errors.d.ts +7 -0
- package/dist/git/errors.js +14 -1
- package/dist/git/errors.js.map +1 -1
- package/dist/git/providers/status.d.ts +6 -1
- package/dist/git/run.types.d.ts +9 -4
- package/dist/git-cli/exec/git.js +36 -16
- package/dist/git-cli/exec/git.js.map +1 -1
- package/dist/git-cli/providers/diff.d.ts +5 -2
- package/dist/git-cli/providers/diff.js +21 -9
- package/dist/git-cli/providers/diff.js.map +1 -1
- package/dist/git-cli/providers/patch.js +5 -7
- package/dist/git-cli/providers/patch.js.map +1 -1
- package/dist/git-cli/providers/status.d.ts +0 -1
- package/dist/git-cli/providers/status.js +119 -142
- package/dist/git-cli/providers/status.js.map +1 -1
- package/dist/plus/git-github/providers/github/status.d.ts +0 -1
- package/dist/plus/git-github/providers/github/status.js.map +1 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +7 -0
- package/dist/plus/integrations/collectionMetadata.js +11 -1
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/models/issueReads.d.ts +5 -0
- package/dist/plus/integrations/models/issuesIntegration.d.ts +37 -1
- package/dist/plus/integrations/models/issuesIntegration.js +118 -2
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/providers/jira-server.d.ts +8 -1
- package/dist/plus/integrations/providers/jira-server.js +90 -138
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +11 -1
- package/dist/plus/integrations/providers/jira.js +88 -122
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.d.ts +22 -1
- package/dist/plus/integrations/providers/linear.js +84 -35
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +5 -9
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +24 -3
- package/dist/plus/integrations/providers/providersApi.js +56 -4
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/utils/jiraIssueDrains.d.ts +86 -0
- package/dist/plus/integrations/providers/utils/jiraIssueDrains.js +139 -0
- package/dist/plus/integrations/providers/utils/jiraIssueDrains.js.map +1 -0
- package/dist/plus/integrations/reads/issueTracker.js +20 -9
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/utils/cancellation.d.ts +4 -1
- package/dist/utils/cancellation.js +4 -2
- package/dist/utils/cancellation.js.map +1 -1
- package/dist/utils/env/node/exec.d.ts +5 -1
- package/dist/utils/env/node/exec.js +8 -5
- package/dist/utils/env/node/exec.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collectionMetadata.js","names":[],"sources":["../../../../plus/integrations/src/collectionMetadata.ts"],"sourcesContent":["import type {\n\tCollectionMetadata,\n\tCollectionOmission,\n\tCollectionScope,\n\tCollectionScopeFailure,\n} from '@gitkraken/provider-apis';\n// Matched by its `code` discriminator rather than by `instanceof`: the SDK ships one bundle per entry point, so\n// the class reached through the root is not the same object as the one reached through `/providers`.\nimport { isInvalidRequestError, isUnsupportedSortError } from '@gitkraken/provider-apis';\nimport {\n\tAuthenticationError,\n\tRequestClientError,\n\tRequestNotFoundError,\n\tRequestRateLimitError,\n} from '@gitlens/git/errors.js';\nimport type { TokenWithInfo } from './authentication/models.js';\nimport type { IntegrationIds } from './constants.js';\nimport type { ResponseHeaders } from './errors.js';\nimport { getResponseHeader, isRateLimitResponse } from './errors.js';\nimport { getProviderResponseBodyMessage } from './providers/providerErrors.js';\nimport type {\n\tProviderWarning,\n\tProviderWarningCause,\n\tProviderWarningOmission,\n\tProviderWarningScope,\n} from './results.js';\nimport { appendDedupedWarning, providerWarningMessage } from './results.js';\n\n/**\n * Re-throws an error that is a fact about the CALL rather than about one scope of a fan-out.\n *\n * Every provider fan-out catches per-scope rejections and records them as {@link CollectionScopeFailure}s, which is\n * right for auth, rate limits and a missing project: those really did happen to one scope, and the siblings that\n * succeeded must survive. A caller-contract error is the opposite — it is identical for every scope and was decided\n * before any request went out — so degrading it would hand back N indistinguishable failures and an empty `partial`\n * page, describing an invalid call as an incomplete read.\n *\n * Today that means the SDK's `UnsupportedSortError` and `InvalidRequestError` — the two codes it lists in its own\n * `CALLER_CONTRACT_ERROR_CODES`, both documented as \"never degraded into a `CollectionScopeFailure` or an\n * omission\". The second covers arguments rejected before the first request goes out: a project key list longer\n * than one Jira search can carry (or an empty one), a non-integer `pageSize`, an unknown current-user filter, or a\n * Jira Server read with no `baseUrl` to address. Called from every fan-out rather than spelled out in each, so a\n * fan-out added later inherits the rule instead of silently omitting it — which is exactly how Azure's\n * account-wide drain came to degrade it while its three siblings did not.\n */\nexport function throwIfCallerContractError(ex: unknown): void {\n\tif (isUnsupportedSortError(ex) || isInvalidRequestError(ex)) throw ex;\n}\n\n/**\n * Maps a caught GitLens request error to the SDK collection failure vocabulary used inside provider fan-outs.\n */\nexport function toCollectionFailureKind(ex: unknown): CollectionScopeFailure['kind'] {\n\tif (ex instanceof AuthenticationError) return 'authentication';\n\tif (ex instanceof RequestRateLimitError) return 'rate-limit';\n\tif (ex instanceof RequestNotFoundError) return 'not-found';\n\treturn 'provider';\n}\n\n/**\n * What an authentication refusal's response said, kept on the failure so the provider can name its cause once the\n * credential behind it is confirmed (see `IntegrationBase.confirmScopedAuthFailures`). Only these facts are kept,\n * never the response: its headers carry the provider's session cookies.\n */\nexport interface ProviderRefusal {\n\tstatus: number;\n\t/** The provider's own explanation: Azure DevOps' `X-TFS-ServiceError`, or the error body's message. */\n\tdetail?: string;\n\t/** The provider's error type, when the body names one (Azure DevOps `typeKey`). */\n\ttypeKey?: string;\n\t/**\n\t * The OAuth app the refused token belongs to, when the provider says (GitHub's `X-OAuth-Client-Id`). Not a\n\t * secret: it is the app's public client id, which the page where a user asks an organization to approve the app\n\t * is addressed by.\n\t */\n\toauthClientId?: string;\n}\n\n/** A scope failure as recorded here: the SDK's shape, plus what the refusal said and, once it is known, why. */\nexport interface ProviderScopeFailure extends CollectionScopeFailure {\n\trefusal?: ProviderRefusal;\n\tcause?: ProviderWarningCause;\n\t/** The provider pinned the refusal on the credential itself, so it is published for the connection, unscoped. */\n\tcredentialRefused?: true;\n}\n\n/**\n * Reads the refusal off an `AuthenticationError` that carries the provider's response: the SDK adapter's plain\n * `{ status, headers, body }` (`providersApi.ts`), Octokit's `{ status, headers, data }` (the GitHub client's REST\n * reads), or the `Response` of a `ProviderFetchError`, whose body was already read into its message.\n */\nfunction toProviderRefusal(ex: unknown): ProviderRefusal | undefined {\n\tif (!(ex instanceof AuthenticationError)) return undefined;\n\n\tconst response = (ex.original as { response?: unknown } | undefined)?.response as\n\t\t| { status?: unknown; headers?: ResponseHeaders; body?: unknown; data?: unknown }\n\t\t| undefined;\n\tif (typeof response?.status !== 'number') return undefined;\n\n\t// Only a structured body: a string one is a page (a proxy's, a sign-in form), not an explanation.\n\tconst raw = response.body ?? response.data;\n\tconst body = typeof raw === 'object' && raw != null ? raw : undefined;\n\tconst detail =\n\t\tdecodeServiceError(getResponseHeader(response.headers, 'x-tfs-serviceerror')) ??\n\t\tgetProviderResponseBodyMessage(body);\n\tconst typeKey = body != null && 'typeKey' in body ? body.typeKey : undefined;\n\tconst oauthClientId = getResponseHeader(response.headers, 'x-oauth-client-id');\n\n\treturn {\n\t\tstatus: response.status,\n\t\t...(detail ? { detail: detail } : {}),\n\t\t...(typeof typeKey === 'string' ? { typeKey: typeKey } : {}),\n\t\t...(typeof oauthClientId === 'string' && oauthClientId ? { oauthClientId: oauthClientId } : {}),\n\t};\n}\n\n/** Azure DevOps sends `X-TFS-ServiceError` percent-encoded. */\nfunction decodeServiceError(value: unknown): string | undefined {\n\tif (typeof value !== 'string' || !value) return undefined;\n\n\ttry {\n\t\treturn decodeURIComponent(value).trim() || undefined;\n\t} catch {\n\t\treturn value.trim() || undefined;\n\t}\n}\n\n/**\n * Builds a structured SDK scope failure from a caught GitLens request error.\n *\n * An authentication refusal is described in the provider's own words when it gives any, and otherwise by the\n * error the provider's response raised, sanitized like any warning's (a page becomes its status), rather than by\n * `AuthenticationError`'s generic \"credentials are either invalid or expired\": a scope refusing a sound credential\n * is the case this failure exists to tell apart, and that sentence says the opposite.\n */\nexport function toCollectionScopeFailure(scope: CollectionScopeFailure['scope'], ex: unknown): ProviderScopeFailure {\n\tconst refusal = toProviderRefusal(ex);\n\tconst message =\n\t\trefusal != null ? (refusal.detail ?? providerWarningMessage((ex as AuthenticationError).original)) : undefined;\n\treturn {\n\t\tscope: scope,\n\t\tkind: toCollectionFailureKind(ex),\n\t\t...(message ? { message: message } : ex instanceof Error && ex.message ? { message: ex.message } : {}),\n\t\t...(refusal != null ? { refusal: refusal } : {}),\n\t};\n}\n\nfunction collectionFailureKindToWarningKind(kind: CollectionScopeFailure['kind']): ProviderWarning['kind'] {\n\tswitch (kind) {\n\t\tcase 'authentication':\n\t\t\treturn 'auth';\n\t\tcase 'rate-limit':\n\t\t\treturn 'rate-limit';\n\t\tcase 'not-found':\n\t\t\treturn 'not-found';\n\t\tdefault:\n\t\t\treturn 'other';\n\t}\n}\n\nfunction getCollectionFailureStatus(message: string | undefined): number | undefined {\n\tif (message == null) return undefined;\n\n\tconst match = /\\((\\d{3})\\)/.exec(message) ?? /^(?:HTTP\\s+)?(\\d{3})\\b/i.exec(message.trim());\n\treturn match != null ? Number(match[1]) : undefined;\n}\n\nfunction toCollectionFailureWarningKind(failure: CollectionScopeFailure): ProviderWarning['kind'] {\n\tconst mapped = collectionFailureKindToWarningKind(failure.kind);\n\tif (mapped !== 'other' || (failure.kind !== 'provider' && failure.kind !== 'unknown')) return mapped;\n\n\t// provider-apis currently groups several HTTP statuses under `provider` in partial fan-outs. The custom\n\t// fetch adapter retains the status in `message`, so recover the same neutral classification the thrown-error\n\t// path exposes instead of downgrading these structured failures to `other`.\n\tconst status = getCollectionFailureStatus(failure.message);\n\tswitch (status) {\n\t\tcase 401:\n\t\t\treturn 'auth';\n\t\tcase 403:\n\t\t\treturn isRateLimitResponse({ status: status, message: failure.message }) ? 'rate-limit' : 'auth';\n\t\tcase 404:\n\t\tcase 410:\n\t\tcase 422:\n\t\t\treturn 'not-found';\n\t\tcase 429:\n\t\t\treturn 'rate-limit';\n\t\tdefault:\n\t\t\treturn mapped;\n\t}\n}\n\nexport function toCollectionFailureError(failure: CollectionScopeFailure, tokenWithInfo: TokenWithInfo): Error {\n\tconst error = new Error(failure.message ?? 'Provider request failed');\n\tconst kind = toCollectionFailureWarningKind(failure);\n\tconst { accessToken, ...tokenInfo } = tokenWithInfo;\n\tif (kind === 'auth') return new AuthenticationError(tokenInfo, error.message, error);\n\tif (kind === 'rate-limit') return new RequestRateLimitError(error, accessToken, undefined);\n\tif (kind === 'not-found') return new RequestNotFoundError(error);\n\n\tconst status = getCollectionFailureStatus(failure.message);\n\tif (kind === 'other' && status != null && status >= 400 && status < 500) {\n\t\treturn new RequestClientError(error);\n\t}\n\n\treturn error;\n}\n\n/** ` (resource r, project p, repository o/n)` for the scope IDs present; empty when the scope names none. */\nfunction collectionScopeText(scope: CollectionScope | undefined): string {\n\tconst parts: string[] = [];\n\tif (scope?.resourceId != null) {\n\t\tparts.push(`resource ${scope.resourceId}`);\n\t}\n\tif (scope?.projectId != null) {\n\t\tparts.push(`project ${scope.projectId}`);\n\t}\n\tif (scope?.repositoryId != null) {\n\t\tparts.push(`repository ${scope.repositoryId}`);\n\t}\n\n\treturn parts.length ? ` (${parts.join(', ')})` : '';\n}\n\n/** The cause in the consumer's terms, for the prose that goes with the structured {@link ProviderWarningCause}. */\nfunction causeText(cause: ProviderWarningCause): string {\n\tswitch (cause.reason) {\n\t\tcase 'oauth-app-not-allowed':\n\t\t\treturn 'the organization does not allow third-party OAuth apps';\n\t\tcase 'access-denied':\n\t\t\treturn 'the account has no access to it';\n\t\tcase 'conditional-access':\n\t\t\treturn 'a Conditional Access policy blocked the request';\n\t\tdefault:\n\t\t\tcause.reason satisfies never;\n\t\t\treturn 'the provider refused the request';\n\t}\n}\n\nfunction collectionFailureMessage(failure: ProviderScopeFailure): string {\n\tconst text = failure.cause != null ? causeText(failure.cause) : failure.message;\n\tconst detail = text != null ? `: ${text}` : '';\n\treturn `Failed to read ${failure.kind} scope${collectionScopeText(failure.scope)}${detail}`;\n}\n\n/**\n * Forwards the part of a failure's scope below the provider, so a consumer can tell \"one organization refused\n * this token\" from \"the connection's token is dead\" without parsing {@link collectionFailureMessage}.\n *\n * `providerId` is dropped: the warning already names its provider, and a scope naming nothing below it is a\n * failure of the connection itself. That is the distinction `getOrganizationsForUserResult` also draws before\n * counting an auth failure against the connection, though it reads a scope with no keys but `providerId`, so an\n * absent scope does not count there. Forwarding such a scope would make an account-wide failure look confined.\n *\n * Copied field by field, for the reason {@link toProviderWarningOmission} copies its scope.\n */\nfunction toProviderWarningScope(scope: CollectionScope | undefined): ProviderWarningScope | undefined {\n\tif (scope == null) return undefined;\n\n\tconst { resourceId, projectId, repositoryId } = scope;\n\tif (resourceId == null && projectId == null && repositoryId == null) return undefined;\n\n\treturn {\n\t\t...(resourceId != null ? { resourceId: resourceId } : {}),\n\t\t...(projectId != null ? { projectId: projectId } : {}),\n\t\t...(repositoryId != null ? { repositoryId: repositoryId } : {}),\n\t};\n}\n\n/**\n * Whether `metadata` reports authentication failures only for scopes below the provider, with none for the\n * connection itself. A credential revoked since a provider cached its discovery produces exactly this shape,\n * the same one a scope's own refusal does; `IntegrationBase.confirmScopedAuthFailures` tells them apart.\n *\n * Classified exactly as {@link assessCollectionMetadata} classifies the warnings, so it agrees with what a\n * consumer would be shown.\n */\nexport function hasOnlyScopedAuthFailures(metadata: CollectionMetadata | undefined): boolean {\n\tlet scoped = false;\n\tfor (const failure of metadata?.failures ?? []) {\n\t\tif (toCollectionFailureWarningKind(failure) !== 'auth') continue;\n\t\tif (toProviderWarningScope(failure.scope) == null || (failure as ProviderScopeFailure).credentialRefused) {\n\t\t\treturn false;\n\t\t}\n\n\t\tscoped = true;\n\t}\n\treturn scoped;\n}\n\n/** The scoped authentication failures in `metadata` that kept their refusal, with the scope they are forwarded as. */\nfunction* refusedScopes(\n\tmetadata: CollectionMetadata | undefined,\n): Generator<{ failure: ProviderScopeFailure & { refusal: ProviderRefusal }; scope: ProviderWarningScope }> {\n\tfor (const failure of (metadata?.failures ?? []) as ProviderScopeFailure[]) {\n\t\tif (\n\t\t\tfailure.refusal == null ||\n\t\t\tfailure.credentialRefused ||\n\t\t\ttoCollectionFailureWarningKind(failure) !== 'auth'\n\t\t) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst scope = toProviderWarningScope(failure.scope);\n\t\tif (scope == null) continue;\n\n\t\tyield { failure: failure as ProviderScopeFailure & { refusal: ProviderRefusal }, scope: scope };\n\t}\n}\n\n/**\n * Marks each scoped authentication failure in `metadata` whose refusal the provider pins on the credential, so it is\n * published for the connection rather than for its scope. Returns whether any was.\n */\nexport function markCredentialRefusals(\n\tmetadata: CollectionMetadata | undefined,\n\tisCredentialRefusal: (refusal: ProviderRefusal) => boolean,\n): boolean {\n\tlet marked = false;\n\tfor (const { failure } of refusedScopes(metadata)) {\n\t\tif (!isCredentialRefusal(failure.refusal)) continue;\n\n\t\tfailure.credentialRefused = true;\n\t\tmarked = true;\n\t}\n\treturn marked;\n}\n\n/**\n * Names the cause of each scoped authentication failure in `metadata` that kept its refusal. Only for a read whose\n * credential was just confirmed: before that, the refusals this can name look exactly like a dead credential.\n */\nexport function attributeScopedAuthFailures(\n\tmetadata: CollectionMetadata | undefined,\n\tdescribe: (refusal: ProviderRefusal, scope: ProviderWarningScope) => ProviderWarningCause | undefined,\n): void {\n\tfor (const { failure, scope } of refusedScopes(metadata)) {\n\t\tif (failure.cause != null) continue;\n\n\t\tconst cause = describe(failure.refusal, scope);\n\t\tif (cause != null) {\n\t\t\tfailure.cause = cause;\n\t\t}\n\t}\n}\n\n/**\n * Explains one omission in the consumer's terms — what was left out and, where the SDK reports it, how much.\n *\n * An omission is a completeness fact, never a failure: the read succeeded, and the provider (or the SDK's own\n * recovery budget) is what withheld results. That is why these never contribute to `fetchFailed`. Whether a\n * retry would recover anything is a separate question this layer cannot answer — see the `recovery` note on\n * {@link toProviderWarningOmission}.\n */\nfunction collectionOmissionMessage(omission: CollectionOmission): string {\n\tconst scopeText = collectionScopeText(omission.scope);\n\tswitch (omission.kind) {\n\t\tcase 'provider-limit':\n\t\t\t// `totalCount` is the only figure saying how much was withheld; both fields are optional, so degrade\n\t\t\t// through the shapes the SDK can actually emit rather than printing `undefined`.\n\t\t\tif (omission.totalCount != null && omission.limit != null) {\n\t\t\t\treturn `Search${scopeText} matched ${omission.totalCount} results, but the provider exposes at most ${omission.limit}`;\n\t\t\t}\n\t\t\tif (omission.limit != null) {\n\t\t\t\treturn `Search${scopeText} exceeded the provider limit of ${omission.limit} results`;\n\t\t\t}\n\t\t\treturn `Search${scopeText} exceeded the provider's result limit`;\n\t\tcase 'recovery-budget':\n\t\t\treturn `Stopped recovering omitted results${scopeText} after reaching the request budget${\n\t\t\t\tomission.limit != null ? ` of ${omission.limit} requests` : ''\n\t\t\t}`;\n\t\tcase 'pagination-incomplete':\n\t\t\treturn `More results are available${scopeText} than this read returned`;\n\t\tdefault:\n\t\t\t// `CollectionOmissionKind` is a closed union, so this is unreachable today. Kept as a compile-time\n\t\t\t// guard rather than a silent fallback: an SDK bump that adds a kind breaks the build here instead\n\t\t\t// of quietly degrading it to a vague message.\n\t\t\tomission.kind satisfies never;\n\t\t\treturn `Some results were omitted${scopeText}; the read is incomplete`;\n\t}\n}\n\n/**\n * Forwards an SDK omission as the structured signal consumers read instead of parsing {@link\n * collectionOmissionMessage}'s prose.\n *\n * `results.ts` re-spells `CollectionOmissionKind` rather than importing it, so the published warning surface\n * carries no `@gitkraken/provider-apis` types (see the export block in `index.ts`). The return type is what\n * keeps the two unions honest: an SDK bump that adds a member fails to compile here, alongside\n * `collectionOmissionMessage`'s `satisfies never`.\n *\n * `scope` is copied because the SDK's own object is retained and re-merged across drained pages\n * (`providerPaging.ts`), and this one crosses the package boundary.\n *\n * `recovery` is always `'none'`, deliberately, and a scoped `pagination-incomplete` is why it looks wrong: the\n * SDK emits that one shape from situations with opposite remedies and says so itself — \"Either the read\n * deliberately took one page per scope, or the provider advertised another page it gave no way to reach.\" Its\n * `collectAcrossScopes` producers (Azure, Bitbucket, Bitbucket Server) are the first, its `drainAcrossScopes`\n * producers (GitLab, Jira) the second, and those report an omission only when a cursor STALLED — so re-reading\n * that scope stalls at the identical page. Nothing in `CollectionOmission` separates the two, so claiming\n * recoverability would ship the dead-end button `recovery` exists to prevent. Making the recoverable case\n * claimable needs provider-apis to say whether a scope was drained or merely sampled: a fact to forward, not\n * to guess here.\n */\nfunction toProviderWarningOmission(omission: CollectionOmission): ProviderWarningOmission {\n\treturn {\n\t\tkind: omission.kind,\n\t\trecovery: 'none',\n\t\t...(omission.limit != null ? { limit: omission.limit } : {}),\n\t\t// The SDK types this `number | null | undefined`; normalize to one absence for consumers.\n\t\t...(omission.totalCount != null ? { totalCount: omission.totalCount } : {}),\n\t\t...(omission.scope != null ? { scope: { ...omission.scope } } : {}),\n\t};\n}\n\n/**\n * Whether SDK metadata describes a read that may be missing results.\n *\n * A reported omission counts on its own rather than deferring to `completeness`. The SDK does couple the\n * two — every site that emits an omission degrades completeness in the same call — so today the omission\n * clause changes no outcome. It is defensive: the two facts arrive as independent fields, and a read that\n * names what it left out must not be publishable as whole if a future producer sets only one of them.\n *\n * This is the single definition of \"may be missing results\", shared with `getPagedResult`, so one metadata\n * object cannot be truncated at one layer and complete at another.\n */\nexport function isIncompleteCollection(metadata: CollectionMetadata | undefined): boolean {\n\tif (metadata == null) return false;\n\n\treturn metadata.completeness !== 'complete' || (metadata.omissions?.length ?? 0) > 0;\n}\n\n/** Converts internal SDK collection metadata into neutral provider facade signals. */\nexport function assessCollectionMetadata(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tmetadata: CollectionMetadata | undefined,\n): { warnings: ProviderWarning[]; fetchFailed: boolean; truncated: boolean } {\n\tif (metadata == null) return { warnings: [], fetchFailed: false, truncated: false };\n\n\tconst warnings: ProviderWarning[] = [];\n\tconst failures: ProviderScopeFailure[] = metadata.failures ?? [];\n\tfor (const failure of failures) {\n\t\tconst kind = toCollectionFailureWarningKind(failure);\n\t\tconst scope = failure.credentialRefused ? undefined : toProviderWarningScope(failure.scope);\n\t\tappendDedupedWarning(warnings, {\n\t\t\tproviderId: providerId,\n\t\t\tdomain: domain,\n\t\t\tconnectionId: connectionId,\n\t\t\t// A credential-level refusal names no scope in prose either, so every scope's copy of it is one warning.\n\t\t\tmessage: collectionFailureMessage(failure.credentialRefused ? { ...failure, scope: undefined } : failure),\n\t\t\tkind: kind,\n\t\t\tisAuth: kind === 'auth',\n\t\t\t...(scope != null ? { scope: scope } : {}),\n\t\t\t// Copied for the reason the scope is: the failure object is retained and re-merged across pages.\n\t\t\t...(failure.cause != null ? { cause: { ...failure.cause } } : {}),\n\t\t});\n\t}\n\n\t// Omissions explain WHY a read is incomplete when nothing failed — a provider cap, an exhausted recovery\n\t// budget, an undrained scope. They classify as `other`, never `auth`, and are deliberately excluded from\n\t// `fetchFailed` below: the request itself succeeded. `omission` carries that same fact structurally, so a\n\t// consumer can act on it without parsing the message.\n\tconst omissions = metadata.omissions ?? [];\n\tfor (const omission of omissions) {\n\t\tappendDedupedWarning(warnings, {\n\t\t\tproviderId: providerId,\n\t\t\tdomain: domain,\n\t\t\tconnectionId: connectionId,\n\t\t\tmessage: collectionOmissionMessage(omission),\n\t\t\tkind: 'other',\n\t\t\tisAuth: false,\n\t\t\tomission: toProviderWarningOmission(omission),\n\t\t});\n\t}\n\n\tconst incomplete = isIncompleteCollection(metadata);\n\t// Only fall back to the generic message when nothing more specific was reported; an omission already\n\t// explains the incompleteness in the consumer's terms, so adding this on top would be noise.\n\t//\n\t// This one deliberately carries no `omission`: it fires precisely when the SDK reported incompleteness\n\t// WITHOUT saying what was left out, so there is no structured fact to forward and synthesizing one would\n\t// assert a specificity this layer does not have.\n\tif (incomplete && failures.length === 0 && omissions.length === 0) {\n\t\tappendDedupedWarning(warnings, {\n\t\t\tproviderId: providerId,\n\t\t\tdomain: domain,\n\t\t\tconnectionId: connectionId,\n\t\t\tmessage:\n\t\t\t\tmetadata.completeness === 'partial'\n\t\t\t\t\t? 'Some results were omitted; the read is incomplete'\n\t\t\t\t\t: 'Result completeness could not be confirmed',\n\t\t\tkind: 'other',\n\t\t\tisAuth: false,\n\t\t});\n\t}\n\n\treturn { warnings: warnings, fetchFailed: failures.length > 0, truncated: incomplete };\n}\n\n/**\n * Appends neutral warnings derived from SDK metadata and returns its failure/truncation assessment.\n *\n * `reported` says whether the metadata described this read's incompleteness at all. A caller that would\n * otherwise add its own generic \"this was truncated\" warning uses it to stay quiet — the same rule\n * {@link assessCollectionMetadata} applies to its own generic fallback, one layer up: whatever the metadata\n * reported already names this provider and, unless it was that same fallback, says something a consumer can\n * act on. Restating it in a weaker vocabulary is how one gap becomes two warnings with two different verdicts.\n *\n * `truncated` is the near-synonym that must NOT be used for that question: it comes from\n * {@link isIncompleteCollection}, which does not consider failures, so metadata carrying a typed failure\n * alongside `completeness: 'complete'` reports `truncated: false` while having already appended the warning\n * that explains everything. Gating on it would restate exactly the case this field exists to keep quiet.\n */\nexport function mergeAssessmentInto(\n\twarnings: ProviderWarning[],\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tmetadata: CollectionMetadata | undefined,\n): { fetchFailed: boolean; truncated: boolean; reported: boolean } {\n\tconst assessment = assessCollectionMetadata(providerId, domain, connectionId, metadata);\n\tfor (const warning of assessment.warnings) {\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\treturn {\n\t\tfetchFailed: assessment.fetchFailed,\n\t\ttruncated: assessment.truncated,\n\t\t// What the assessment PRODUCED, not what `appendDedupedWarning` accepted: a warning dropped as a\n\t\t// duplicate is still one the consumer will see, so it still makes a restatement redundant.\n\t\treported: assessment.warnings.length > 0,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"collectionMetadata.js","names":[],"sources":["../../../../plus/integrations/src/collectionMetadata.ts"],"sourcesContent":["import type {\n\tCollectionMetadata,\n\tCollectionOmission,\n\tCollectionScope,\n\tCollectionScopeFailure,\n} from '@gitkraken/provider-apis';\n// Matched by its `code` discriminator rather than by `instanceof`: the SDK ships one bundle per entry point, so\n// the class reached through the root is not the same object as the one reached through `/providers`.\nimport { isInvalidRequestError, isUnsupportedSortError } from '@gitkraken/provider-apis';\nimport {\n\tAuthenticationError,\n\tRequestClientError,\n\tRequestNotFoundError,\n\tRequestRateLimitError,\n} from '@gitlens/git/errors.js';\nimport type { TokenWithInfo } from './authentication/models.js';\nimport type { IntegrationIds } from './constants.js';\nimport type { ResponseHeaders } from './errors.js';\nimport { getResponseHeader, isRateLimitResponse } from './errors.js';\nimport { getProviderResponseBodyMessage } from './providers/providerErrors.js';\nimport type {\n\tProviderWarning,\n\tProviderWarningCause,\n\tProviderWarningOmission,\n\tProviderWarningScope,\n} from './results.js';\nimport { appendDedupedWarning, providerWarningMessage } from './results.js';\n\n/**\n * Re-throws an error that is a fact about the CALL rather than about one scope of a fan-out.\n *\n * Every provider fan-out catches per-scope rejections and records them as {@link CollectionScopeFailure}s, which is\n * right for auth, rate limits and a missing project: those really did happen to one scope, and the siblings that\n * succeeded must survive. A caller-contract error is the opposite — it is identical for every scope and was decided\n * before any request went out — so degrading it would hand back N indistinguishable failures and an empty `partial`\n * page, describing an invalid call as an incomplete read.\n *\n * Today that means the SDK's `UnsupportedSortError` and `InvalidRequestError` — the two codes it lists in its own\n * `CALLER_CONTRACT_ERROR_CODES`, both documented as \"never degraded into a `CollectionScopeFailure` or an\n * omission\". The second covers arguments rejected before the first request goes out: a project key list longer\n * than one Jira search can carry (or an empty one), a non-integer `pageSize`, an unknown current-user filter, or a\n * Jira Server read with no `baseUrl` to address. Called from every fan-out rather than spelled out in each, so a\n * fan-out added later inherits the rule instead of silently omitting it — which is exactly how Azure's\n * account-wide drain came to degrade it while its three siblings did not.\n */\nexport function throwIfCallerContractError(ex: unknown): void {\n\tif (isUnsupportedSortError(ex) || isInvalidRequestError(ex)) throw ex;\n}\n\n/**\n * Whether a failed query that covered several scopes would fail each scope's own read the same way: a rejected or\n * rate-limited token, or a call the SDK refused before sending. Anything else (a query the provider rejects because\n * one scope is gone or no longer visible, a server error) may be one scope's problem, so the scopes are worth\n * reading on their own.\n */\nexport function failsEveryScope(ex: unknown): boolean {\n\tconst kind = toCollectionFailureKind(ex);\n\treturn (\n\t\tkind === 'authentication' || kind === 'rate-limit' || isUnsupportedSortError(ex) || isInvalidRequestError(ex)\n\t);\n}\n\n/**\n * Maps a caught GitLens request error to the SDK collection failure vocabulary used inside provider fan-outs.\n */\nexport function toCollectionFailureKind(ex: unknown): CollectionScopeFailure['kind'] {\n\tif (ex instanceof AuthenticationError) return 'authentication';\n\tif (ex instanceof RequestRateLimitError) return 'rate-limit';\n\tif (ex instanceof RequestNotFoundError) return 'not-found';\n\treturn 'provider';\n}\n\n/**\n * What an authentication refusal's response said, kept on the failure so the provider can name its cause once the\n * credential behind it is confirmed (see `IntegrationBase.confirmScopedAuthFailures`). Only these facts are kept,\n * never the response: its headers carry the provider's session cookies.\n */\nexport interface ProviderRefusal {\n\tstatus: number;\n\t/** The provider's own explanation: Azure DevOps' `X-TFS-ServiceError`, or the error body's message. */\n\tdetail?: string;\n\t/** The provider's error type, when the body names one (Azure DevOps `typeKey`). */\n\ttypeKey?: string;\n\t/**\n\t * The OAuth app the refused token belongs to, when the provider says (GitHub's `X-OAuth-Client-Id`). Not a\n\t * secret: it is the app's public client id, which the page where a user asks an organization to approve the app\n\t * is addressed by.\n\t */\n\toauthClientId?: string;\n}\n\n/** A scope failure as recorded here: the SDK's shape, plus what the refusal said and, once it is known, why. */\nexport interface ProviderScopeFailure extends CollectionScopeFailure {\n\trefusal?: ProviderRefusal;\n\tcause?: ProviderWarningCause;\n\t/** The provider pinned the refusal on the credential itself, so it is published for the connection, unscoped. */\n\tcredentialRefused?: true;\n}\n\n/**\n * Reads the refusal off an `AuthenticationError` that carries the provider's response: the SDK adapter's plain\n * `{ status, headers, body }` (`providersApi.ts`), Octokit's `{ status, headers, data }` (the GitHub client's REST\n * reads), or the `Response` of a `ProviderFetchError`, whose body was already read into its message.\n */\nfunction toProviderRefusal(ex: unknown): ProviderRefusal | undefined {\n\tif (!(ex instanceof AuthenticationError)) return undefined;\n\n\tconst response = (ex.original as { response?: unknown } | undefined)?.response as\n\t\t| { status?: unknown; headers?: ResponseHeaders; body?: unknown; data?: unknown }\n\t\t| undefined;\n\tif (typeof response?.status !== 'number') return undefined;\n\n\t// Only a structured body: a string one is a page (a proxy's, a sign-in form), not an explanation.\n\tconst raw = response.body ?? response.data;\n\tconst body = typeof raw === 'object' && raw != null ? raw : undefined;\n\tconst detail =\n\t\tdecodeServiceError(getResponseHeader(response.headers, 'x-tfs-serviceerror')) ??\n\t\tgetProviderResponseBodyMessage(body);\n\tconst typeKey = body != null && 'typeKey' in body ? body.typeKey : undefined;\n\tconst oauthClientId = getResponseHeader(response.headers, 'x-oauth-client-id');\n\n\treturn {\n\t\tstatus: response.status,\n\t\t...(detail ? { detail: detail } : {}),\n\t\t...(typeof typeKey === 'string' ? { typeKey: typeKey } : {}),\n\t\t...(typeof oauthClientId === 'string' && oauthClientId ? { oauthClientId: oauthClientId } : {}),\n\t};\n}\n\n/** Azure DevOps sends `X-TFS-ServiceError` percent-encoded. */\nfunction decodeServiceError(value: unknown): string | undefined {\n\tif (typeof value !== 'string' || !value) return undefined;\n\n\ttry {\n\t\treturn decodeURIComponent(value).trim() || undefined;\n\t} catch {\n\t\treturn value.trim() || undefined;\n\t}\n}\n\n/**\n * Builds a structured SDK scope failure from a caught GitLens request error.\n *\n * An authentication refusal is described in the provider's own words when it gives any, and otherwise by the\n * error the provider's response raised, sanitized like any warning's (a page becomes its status), rather than by\n * `AuthenticationError`'s generic \"credentials are either invalid or expired\": a scope refusing a sound credential\n * is the case this failure exists to tell apart, and that sentence says the opposite.\n */\nexport function toCollectionScopeFailure(scope: CollectionScopeFailure['scope'], ex: unknown): ProviderScopeFailure {\n\tconst refusal = toProviderRefusal(ex);\n\tconst message =\n\t\trefusal != null ? (refusal.detail ?? providerWarningMessage((ex as AuthenticationError).original)) : undefined;\n\treturn {\n\t\tscope: scope,\n\t\tkind: toCollectionFailureKind(ex),\n\t\t...(message ? { message: message } : ex instanceof Error && ex.message ? { message: ex.message } : {}),\n\t\t...(refusal != null ? { refusal: refusal } : {}),\n\t};\n}\n\nfunction collectionFailureKindToWarningKind(kind: CollectionScopeFailure['kind']): ProviderWarning['kind'] {\n\tswitch (kind) {\n\t\tcase 'authentication':\n\t\t\treturn 'auth';\n\t\tcase 'rate-limit':\n\t\t\treturn 'rate-limit';\n\t\tcase 'not-found':\n\t\t\treturn 'not-found';\n\t\tdefault:\n\t\t\treturn 'other';\n\t}\n}\n\nfunction getCollectionFailureStatus(message: string | undefined): number | undefined {\n\tif (message == null) return undefined;\n\n\tconst match = /\\((\\d{3})\\)/.exec(message) ?? /^(?:HTTP\\s+)?(\\d{3})\\b/i.exec(message.trim());\n\treturn match != null ? Number(match[1]) : undefined;\n}\n\nfunction toCollectionFailureWarningKind(failure: CollectionScopeFailure): ProviderWarning['kind'] {\n\tconst mapped = collectionFailureKindToWarningKind(failure.kind);\n\tif (mapped !== 'other' || (failure.kind !== 'provider' && failure.kind !== 'unknown')) return mapped;\n\n\t// provider-apis currently groups several HTTP statuses under `provider` in partial fan-outs. The custom\n\t// fetch adapter retains the status in `message`, so recover the same neutral classification the thrown-error\n\t// path exposes instead of downgrading these structured failures to `other`.\n\tconst status = getCollectionFailureStatus(failure.message);\n\tswitch (status) {\n\t\tcase 401:\n\t\t\treturn 'auth';\n\t\tcase 403:\n\t\t\treturn isRateLimitResponse({ status: status, message: failure.message }) ? 'rate-limit' : 'auth';\n\t\tcase 404:\n\t\tcase 410:\n\t\tcase 422:\n\t\t\treturn 'not-found';\n\t\tcase 429:\n\t\t\treturn 'rate-limit';\n\t\tdefault:\n\t\t\treturn mapped;\n\t}\n}\n\nexport function toCollectionFailureError(failure: CollectionScopeFailure, tokenWithInfo: TokenWithInfo): Error {\n\tconst error = new Error(failure.message ?? 'Provider request failed');\n\tconst kind = toCollectionFailureWarningKind(failure);\n\tconst { accessToken, ...tokenInfo } = tokenWithInfo;\n\tif (kind === 'auth') return new AuthenticationError(tokenInfo, error.message, error);\n\tif (kind === 'rate-limit') return new RequestRateLimitError(error, accessToken, undefined);\n\tif (kind === 'not-found') return new RequestNotFoundError(error);\n\n\tconst status = getCollectionFailureStatus(failure.message);\n\tif (kind === 'other' && status != null && status >= 400 && status < 500) {\n\t\treturn new RequestClientError(error);\n\t}\n\n\treturn error;\n}\n\n/** ` (resource r, project p, repository o/n)` for the scope IDs present; empty when the scope names none. */\nfunction collectionScopeText(scope: CollectionScope | undefined): string {\n\tconst parts: string[] = [];\n\tif (scope?.resourceId != null) {\n\t\tparts.push(`resource ${scope.resourceId}`);\n\t}\n\tif (scope?.projectId != null) {\n\t\tparts.push(`project ${scope.projectId}`);\n\t}\n\tif (scope?.repositoryId != null) {\n\t\tparts.push(`repository ${scope.repositoryId}`);\n\t}\n\n\treturn parts.length ? ` (${parts.join(', ')})` : '';\n}\n\n/** The cause in the consumer's terms, for the prose that goes with the structured {@link ProviderWarningCause}. */\nfunction causeText(cause: ProviderWarningCause): string {\n\tswitch (cause.reason) {\n\t\tcase 'oauth-app-not-allowed':\n\t\t\treturn 'the organization does not allow third-party OAuth apps';\n\t\tcase 'access-denied':\n\t\t\treturn 'the account has no access to it';\n\t\tcase 'conditional-access':\n\t\t\treturn 'a Conditional Access policy blocked the request';\n\t\tdefault:\n\t\t\tcause.reason satisfies never;\n\t\t\treturn 'the provider refused the request';\n\t}\n}\n\nfunction collectionFailureMessage(failure: ProviderScopeFailure): string {\n\tconst text = failure.cause != null ? causeText(failure.cause) : failure.message;\n\tconst detail = text != null ? `: ${text}` : '';\n\treturn `Failed to read ${failure.kind} scope${collectionScopeText(failure.scope)}${detail}`;\n}\n\n/**\n * Forwards the part of a failure's scope below the provider, so a consumer can tell \"one organization refused\n * this token\" from \"the connection's token is dead\" without parsing {@link collectionFailureMessage}.\n *\n * `providerId` is dropped: the warning already names its provider, and a scope naming nothing below it is a\n * failure of the connection itself. That is the distinction `getOrganizationsForUserResult` also draws before\n * counting an auth failure against the connection, though it reads a scope with no keys but `providerId`, so an\n * absent scope does not count there. Forwarding such a scope would make an account-wide failure look confined.\n *\n * Copied field by field, for the reason {@link toProviderWarningOmission} copies its scope.\n */\nfunction toProviderWarningScope(scope: CollectionScope | undefined): ProviderWarningScope | undefined {\n\tif (scope == null) return undefined;\n\n\tconst { resourceId, projectId, repositoryId } = scope;\n\tif (resourceId == null && projectId == null && repositoryId == null) return undefined;\n\n\treturn {\n\t\t...(resourceId != null ? { resourceId: resourceId } : {}),\n\t\t...(projectId != null ? { projectId: projectId } : {}),\n\t\t...(repositoryId != null ? { repositoryId: repositoryId } : {}),\n\t};\n}\n\n/**\n * Whether `metadata` reports authentication failures only for scopes below the provider, with none for the\n * connection itself. A credential revoked since a provider cached its discovery produces exactly this shape,\n * the same one a scope's own refusal does; `IntegrationBase.confirmScopedAuthFailures` tells them apart.\n *\n * Classified exactly as {@link assessCollectionMetadata} classifies the warnings, so it agrees with what a\n * consumer would be shown.\n */\nexport function hasOnlyScopedAuthFailures(metadata: CollectionMetadata | undefined): boolean {\n\tlet scoped = false;\n\tfor (const failure of metadata?.failures ?? []) {\n\t\tif (toCollectionFailureWarningKind(failure) !== 'auth') continue;\n\t\tif (toProviderWarningScope(failure.scope) == null || (failure as ProviderScopeFailure).credentialRefused) {\n\t\t\treturn false;\n\t\t}\n\n\t\tscoped = true;\n\t}\n\treturn scoped;\n}\n\n/** The scoped authentication failures in `metadata` that kept their refusal, with the scope they are forwarded as. */\nfunction* refusedScopes(\n\tmetadata: CollectionMetadata | undefined,\n): Generator<{ failure: ProviderScopeFailure & { refusal: ProviderRefusal }; scope: ProviderWarningScope }> {\n\tfor (const failure of (metadata?.failures ?? []) as ProviderScopeFailure[]) {\n\t\tif (\n\t\t\tfailure.refusal == null ||\n\t\t\tfailure.credentialRefused ||\n\t\t\ttoCollectionFailureWarningKind(failure) !== 'auth'\n\t\t) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst scope = toProviderWarningScope(failure.scope);\n\t\tif (scope == null) continue;\n\n\t\tyield { failure: failure as ProviderScopeFailure & { refusal: ProviderRefusal }, scope: scope };\n\t}\n}\n\n/**\n * Marks each scoped authentication failure in `metadata` whose refusal the provider pins on the credential, so it is\n * published for the connection rather than for its scope. Returns whether any was.\n */\nexport function markCredentialRefusals(\n\tmetadata: CollectionMetadata | undefined,\n\tisCredentialRefusal: (refusal: ProviderRefusal) => boolean,\n): boolean {\n\tlet marked = false;\n\tfor (const { failure } of refusedScopes(metadata)) {\n\t\tif (!isCredentialRefusal(failure.refusal)) continue;\n\n\t\tfailure.credentialRefused = true;\n\t\tmarked = true;\n\t}\n\treturn marked;\n}\n\n/**\n * Names the cause of each scoped authentication failure in `metadata` that kept its refusal. Only for a read whose\n * credential was just confirmed: before that, the refusals this can name look exactly like a dead credential.\n */\nexport function attributeScopedAuthFailures(\n\tmetadata: CollectionMetadata | undefined,\n\tdescribe: (refusal: ProviderRefusal, scope: ProviderWarningScope) => ProviderWarningCause | undefined,\n): void {\n\tfor (const { failure, scope } of refusedScopes(metadata)) {\n\t\tif (failure.cause != null) continue;\n\n\t\tconst cause = describe(failure.refusal, scope);\n\t\tif (cause != null) {\n\t\t\tfailure.cause = cause;\n\t\t}\n\t}\n}\n\n/**\n * Explains one omission in the consumer's terms — what was left out and, where the SDK reports it, how much.\n *\n * An omission is a completeness fact, never a failure: the read succeeded, and the provider (or the SDK's own\n * recovery budget) is what withheld results. That is why these never contribute to `fetchFailed`. Whether a\n * retry would recover anything is a separate question this layer cannot answer — see the `recovery` note on\n * {@link toProviderWarningOmission}.\n */\nfunction collectionOmissionMessage(omission: CollectionOmission): string {\n\tconst scopeText = collectionScopeText(omission.scope);\n\tswitch (omission.kind) {\n\t\tcase 'provider-limit':\n\t\t\t// `totalCount` is the only figure saying how much was withheld; both fields are optional, so degrade\n\t\t\t// through the shapes the SDK can actually emit rather than printing `undefined`.\n\t\t\tif (omission.totalCount != null && omission.limit != null) {\n\t\t\t\treturn `Search${scopeText} matched ${omission.totalCount} results, but the provider exposes at most ${omission.limit}`;\n\t\t\t}\n\t\t\tif (omission.limit != null) {\n\t\t\t\treturn `Search${scopeText} exceeded the provider limit of ${omission.limit} results`;\n\t\t\t}\n\t\t\treturn `Search${scopeText} exceeded the provider's result limit`;\n\t\tcase 'recovery-budget':\n\t\t\treturn `Stopped recovering omitted results${scopeText} after reaching the request budget${\n\t\t\t\tomission.limit != null ? ` of ${omission.limit} requests` : ''\n\t\t\t}`;\n\t\tcase 'pagination-incomplete':\n\t\t\treturn `More results are available${scopeText} than this read returned`;\n\t\tdefault:\n\t\t\t// `CollectionOmissionKind` is a closed union, so this is unreachable today. Kept as a compile-time\n\t\t\t// guard rather than a silent fallback: an SDK bump that adds a kind breaks the build here instead\n\t\t\t// of quietly degrading it to a vague message.\n\t\t\tomission.kind satisfies never;\n\t\t\treturn `Some results were omitted${scopeText}; the read is incomplete`;\n\t}\n}\n\n/**\n * Forwards an SDK omission as the structured signal consumers read instead of parsing {@link\n * collectionOmissionMessage}'s prose.\n *\n * `results.ts` re-spells `CollectionOmissionKind` rather than importing it, so the published warning surface\n * carries no `@gitkraken/provider-apis` types (see the export block in `index.ts`). The return type is what\n * keeps the two unions honest: an SDK bump that adds a member fails to compile here, alongside\n * `collectionOmissionMessage`'s `satisfies never`.\n *\n * `scope` is copied because the SDK's own object is retained and re-merged across drained pages\n * (`providerPaging.ts`), and this one crosses the package boundary.\n *\n * `recovery` is always `'none'`, deliberately, and a scoped `pagination-incomplete` is why it looks wrong: the\n * SDK emits that one shape from situations with opposite remedies and says so itself — \"Either the read\n * deliberately took one page per scope, or the provider advertised another page it gave no way to reach.\" Its\n * `collectAcrossScopes` producers (Azure, Bitbucket, Bitbucket Server) are the first, its `drainAcrossScopes`\n * producers (GitLab, Jira) the second, and those report an omission only when a cursor STALLED — so re-reading\n * that scope stalls at the identical page. Nothing in `CollectionOmission` separates the two, so claiming\n * recoverability would ship the dead-end button `recovery` exists to prevent. Making the recoverable case\n * claimable needs provider-apis to say whether a scope was drained or merely sampled: a fact to forward, not\n * to guess here.\n */\nfunction toProviderWarningOmission(omission: CollectionOmission): ProviderWarningOmission {\n\treturn {\n\t\tkind: omission.kind,\n\t\trecovery: 'none',\n\t\t...(omission.limit != null ? { limit: omission.limit } : {}),\n\t\t// The SDK types this `number | null | undefined`; normalize to one absence for consumers.\n\t\t...(omission.totalCount != null ? { totalCount: omission.totalCount } : {}),\n\t\t...(omission.scope != null ? { scope: { ...omission.scope } } : {}),\n\t};\n}\n\n/**\n * Whether SDK metadata describes a read that may be missing results.\n *\n * A reported omission counts on its own rather than deferring to `completeness`. The SDK does couple the\n * two — every site that emits an omission degrades completeness in the same call — so today the omission\n * clause changes no outcome. It is defensive: the two facts arrive as independent fields, and a read that\n * names what it left out must not be publishable as whole if a future producer sets only one of them.\n *\n * This is the single definition of \"may be missing results\", shared with `getPagedResult`, so one metadata\n * object cannot be truncated at one layer and complete at another.\n */\nexport function isIncompleteCollection(metadata: CollectionMetadata | undefined): boolean {\n\tif (metadata == null) return false;\n\n\treturn metadata.completeness !== 'complete' || (metadata.omissions?.length ?? 0) > 0;\n}\n\n/** Converts internal SDK collection metadata into neutral provider facade signals. */\nexport function assessCollectionMetadata(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tmetadata: CollectionMetadata | undefined,\n): { warnings: ProviderWarning[]; fetchFailed: boolean; truncated: boolean } {\n\tif (metadata == null) return { warnings: [], fetchFailed: false, truncated: false };\n\n\tconst warnings: ProviderWarning[] = [];\n\tconst failures: ProviderScopeFailure[] = metadata.failures ?? [];\n\tfor (const failure of failures) {\n\t\tconst kind = toCollectionFailureWarningKind(failure);\n\t\tconst scope = failure.credentialRefused ? undefined : toProviderWarningScope(failure.scope);\n\t\tappendDedupedWarning(warnings, {\n\t\t\tproviderId: providerId,\n\t\t\tdomain: domain,\n\t\t\tconnectionId: connectionId,\n\t\t\t// A credential-level refusal names no scope in prose either, so every scope's copy of it is one warning.\n\t\t\tmessage: collectionFailureMessage(failure.credentialRefused ? { ...failure, scope: undefined } : failure),\n\t\t\tkind: kind,\n\t\t\tisAuth: kind === 'auth',\n\t\t\t...(scope != null ? { scope: scope } : {}),\n\t\t\t// Copied for the reason the scope is: the failure object is retained and re-merged across pages.\n\t\t\t...(failure.cause != null ? { cause: { ...failure.cause } } : {}),\n\t\t});\n\t}\n\n\t// Omissions explain WHY a read is incomplete when nothing failed — a provider cap, an exhausted recovery\n\t// budget, an undrained scope. They classify as `other`, never `auth`, and are deliberately excluded from\n\t// `fetchFailed` below: the request itself succeeded. `omission` carries that same fact structurally, so a\n\t// consumer can act on it without parsing the message.\n\tconst omissions = metadata.omissions ?? [];\n\tfor (const omission of omissions) {\n\t\tappendDedupedWarning(warnings, {\n\t\t\tproviderId: providerId,\n\t\t\tdomain: domain,\n\t\t\tconnectionId: connectionId,\n\t\t\tmessage: collectionOmissionMessage(omission),\n\t\t\tkind: 'other',\n\t\t\tisAuth: false,\n\t\t\tomission: toProviderWarningOmission(omission),\n\t\t});\n\t}\n\n\tconst incomplete = isIncompleteCollection(metadata);\n\t// Only fall back to the generic message when nothing more specific was reported; an omission already\n\t// explains the incompleteness in the consumer's terms, so adding this on top would be noise.\n\t//\n\t// This one deliberately carries no `omission`: it fires precisely when the SDK reported incompleteness\n\t// WITHOUT saying what was left out, so there is no structured fact to forward and synthesizing one would\n\t// assert a specificity this layer does not have.\n\tif (incomplete && failures.length === 0 && omissions.length === 0) {\n\t\tappendDedupedWarning(warnings, {\n\t\t\tproviderId: providerId,\n\t\t\tdomain: domain,\n\t\t\tconnectionId: connectionId,\n\t\t\tmessage:\n\t\t\t\tmetadata.completeness === 'partial'\n\t\t\t\t\t? 'Some results were omitted; the read is incomplete'\n\t\t\t\t\t: 'Result completeness could not be confirmed',\n\t\t\tkind: 'other',\n\t\t\tisAuth: false,\n\t\t});\n\t}\n\n\treturn { warnings: warnings, fetchFailed: failures.length > 0, truncated: incomplete };\n}\n\n/**\n * Appends neutral warnings derived from SDK metadata and returns its failure/truncation assessment.\n *\n * `reported` says whether the metadata described this read's incompleteness at all. A caller that would\n * otherwise add its own generic \"this was truncated\" warning uses it to stay quiet — the same rule\n * {@link assessCollectionMetadata} applies to its own generic fallback, one layer up: whatever the metadata\n * reported already names this provider and, unless it was that same fallback, says something a consumer can\n * act on. Restating it in a weaker vocabulary is how one gap becomes two warnings with two different verdicts.\n *\n * `truncated` is the near-synonym that must NOT be used for that question: it comes from\n * {@link isIncompleteCollection}, which does not consider failures, so metadata carrying a typed failure\n * alongside `completeness: 'complete'` reports `truncated: false` while having already appended the warning\n * that explains everything. Gating on it would restate exactly the case this field exists to keep quiet.\n */\nexport function mergeAssessmentInto(\n\twarnings: ProviderWarning[],\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tmetadata: CollectionMetadata | undefined,\n): { fetchFailed: boolean; truncated: boolean; reported: boolean } {\n\tconst assessment = assessCollectionMetadata(providerId, domain, connectionId, metadata);\n\tfor (const warning of assessment.warnings) {\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\treturn {\n\t\tfetchFailed: assessment.fetchFailed,\n\t\ttruncated: assessment.truncated,\n\t\t// What the assessment PRODUCED, not what `appendDedupedWarning` accepted: a warning dropped as a\n\t\t// duplicate is still one the consumer will see, so it still makes a restatement redundant.\n\t\treported: assessment.warnings.length > 0,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -100,6 +100,11 @@ export type IssuesForProjectOptions = {
|
|
|
100
100
|
*/
|
|
101
101
|
state?: IssueStateFilter;
|
|
102
102
|
};
|
|
103
|
+
/** One project of a batched tracker read (`IssuesIntegration.getIssuesForProjectsWithTruncationResult`). */
|
|
104
|
+
export type ProjectIssuesRequest<T> = {
|
|
105
|
+
project: T;
|
|
106
|
+
options: IssuesForProjectOptions;
|
|
107
|
+
};
|
|
103
108
|
export type ProjectIssuesDrain = {
|
|
104
109
|
values: IssueShape[];
|
|
105
110
|
metadata?: CollectionMetadata;
|
|
@@ -4,9 +4,20 @@ import { Account } from "../../../git/models/author.js";
|
|
|
4
4
|
import { ProviderAuthenticationSession } from "../authentication/models.js";
|
|
5
5
|
import { IntegrationIds } from "../constants.js";
|
|
6
6
|
import { ProviderApiCollectionResult } from "../providers/models.js";
|
|
7
|
-
import { IssuesForProjectOptions, ProjectIssuesDrain } from "./issueReads.js";
|
|
7
|
+
import { IssuesForProjectOptions, ProjectIssuesDrain, ProjectIssuesRequest } from "./issueReads.js";
|
|
8
8
|
import { BatchSlot, Integration, IntegrationBase, IntegrationResult, IntegrationType, IssueEtagFields, IssueEtagInclude } from "./integration.js";
|
|
9
9
|
//#region ../plus/integrations/src/models/issuesIntegration.d.ts
|
|
10
|
+
/**
|
|
11
|
+
* Groups requests into searches of at most `maxPerSearch` projects sharing `searchKey`. Undefined, reading
|
|
12
|
+
* every project on its own, when any request has no key, i.e. cannot be searched together with others.
|
|
13
|
+
*/
|
|
14
|
+
export declare function groupProjectIssuesSearches<P>(requests: readonly ProjectIssuesRequest<P>[], searchKey: (request: ProjectIssuesRequest<P>) => string | undefined, maxPerSearch?: number): number[][] | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Splits one search's issues back to its projects, in the order of `projectKeys`. Undefined when an issue belongs to
|
|
17
|
+
* no requested project (or `projectKeyOf` can't tell), since the split then can't be trusted and the search's
|
|
18
|
+
* projects are read one by one instead.
|
|
19
|
+
*/
|
|
20
|
+
export declare function splitProjectIssuesSearch<I>(projectKeys: readonly string[], issues: readonly I[], projectKeyOf: (issue: I) => string | undefined): I[][] | undefined;
|
|
10
21
|
export declare function isIssuesIntegration(integration: Integration): integration is IssuesIntegration;
|
|
11
22
|
export declare abstract class IssuesIntegration<ID extends IntegrationIds = IntegrationIds, T extends ResourceDescriptor = ResourceDescriptor> extends IntegrationBase<ID> {
|
|
12
23
|
readonly type: IntegrationType;
|
|
@@ -117,6 +128,31 @@ export declare abstract class IssuesIntegration<ID extends IntegrationIds = Inte
|
|
|
117
128
|
* publishing it as complete. The default reports `truncated: false`.
|
|
118
129
|
*/
|
|
119
130
|
getIssuesForProjectWithTruncationResult(project: T, options?: IssuesForProjectOptions, connectionId?: string): Promise<IntegrationResult<ProjectIssuesDrain | undefined>>;
|
|
131
|
+
/**
|
|
132
|
+
* Reads several projects' issues, one result per request in request order, each with the contract of
|
|
133
|
+
* {@link getIssuesForProjectWithTruncationResult}, so one project failing leaves its siblings readable.
|
|
134
|
+
*
|
|
135
|
+
* A tracker that can read several projects in one query (Jira, Linear) groups them through
|
|
136
|
+
* {@link getProjectIssuesSearches}, so a project with none of the user's issues costs no request of its own.
|
|
137
|
+
* Every other request, and every project of a search that did not complete, is read on its own.
|
|
138
|
+
*
|
|
139
|
+
* A search's pages are global to its project set, so only a search that completes says something about each
|
|
140
|
+
* of its projects: one that does not, or that fails for a reason that may be one project's
|
|
141
|
+
* ({@link failsEveryScope}), is read again project by project, which reports completeness, failures and
|
|
142
|
+
* retries per project. Only a failure every project would hit on its own read too fails them all at once.
|
|
143
|
+
*/
|
|
144
|
+
getIssuesForProjectsWithTruncationResult(requests: readonly ProjectIssuesRequest<T>[], connectionId?: string): Promise<IntegrationResult<ProjectIssuesDrain | undefined>[]>;
|
|
145
|
+
/**
|
|
146
|
+
* Groups requests into searches, as lists of request indices, that {@link searchProviderProjectIssues} can read
|
|
147
|
+
* in one query each. Undefined reads every project on its own, which is the default.
|
|
148
|
+
*/
|
|
149
|
+
protected getProjectIssuesSearches(_requests: readonly ProjectIssuesRequest<T>[]): number[][] | undefined;
|
|
150
|
+
/**
|
|
151
|
+
* Reads the projects of one search in one query: each project's issues in request order, or undefined when the
|
|
152
|
+
* query did not complete, which reads them one by one instead. A throw is classified by {@link failsEveryScope}.
|
|
153
|
+
*/
|
|
154
|
+
protected searchProviderProjectIssues(_session: ProviderAuthenticationSession, _requests: readonly ProjectIssuesRequest<T>[]): Promise<IssueShape[][] | undefined>;
|
|
155
|
+
private readProjectsOneByOne;
|
|
120
156
|
protected abstract getProviderIssuesForProject(session: ProviderAuthenticationSession, project: T, options?: IssuesForProjectOptions): Promise<IssueShape[] | undefined>;
|
|
121
157
|
/**
|
|
122
158
|
* Truncation-aware core of {@link getProviderIssuesForProject}. The default wraps the plain read and
|
|
@@ -1,12 +1,53 @@
|
|
|
1
|
+
import { Logger as _logger } from "../../../utils/logger.js";
|
|
1
2
|
import { getScopedLogger } from "../../../utils/logger.scoped.js";
|
|
2
|
-
import { mapSettledBounded } from "../../../utils/promise.js";
|
|
3
|
+
import { mapBounded, mapSettledBounded } from "../../../utils/promise.js";
|
|
3
4
|
import { trace } from "../../../utils/decorators/log.js";
|
|
4
5
|
import __decorate from "../../../_virtual/@oxc-project+runtime/helpers/esm/decorate.js";
|
|
5
6
|
import { gate } from "../../../utils/decorators/gate.js";
|
|
6
7
|
import { toError } from "../errors.js";
|
|
7
8
|
import "../constants.js";
|
|
9
|
+
import { failsEveryScope } from "../collectionMetadata.js";
|
|
8
10
|
import { IntegrationBase } from "./integration.js";
|
|
9
11
|
//#region ../plus/integrations/src/models/issuesIntegration.ts
|
|
12
|
+
/**
|
|
13
|
+
* Groups requests into searches of at most `maxPerSearch` projects sharing `searchKey`. Undefined, reading
|
|
14
|
+
* every project on its own, when any request has no key, i.e. cannot be searched together with others.
|
|
15
|
+
*/
|
|
16
|
+
function groupProjectIssuesSearches(requests, searchKey, maxPerSearch = Number.POSITIVE_INFINITY) {
|
|
17
|
+
const searchesByKey = /* @__PURE__ */ new Map();
|
|
18
|
+
for (let index = 0; index < requests.length; index++) {
|
|
19
|
+
const key = searchKey(requests[index]);
|
|
20
|
+
if (key == null) return void 0;
|
|
21
|
+
let searches = searchesByKey.get(key);
|
|
22
|
+
if (searches == null) {
|
|
23
|
+
searches = [];
|
|
24
|
+
searchesByKey.set(key, searches);
|
|
25
|
+
}
|
|
26
|
+
let search = searches.at(-1);
|
|
27
|
+
if (search == null || search.length >= maxPerSearch) {
|
|
28
|
+
search = [];
|
|
29
|
+
searches.push(search);
|
|
30
|
+
}
|
|
31
|
+
search.push(index);
|
|
32
|
+
}
|
|
33
|
+
return [...searchesByKey.values()].flat();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Splits one search's issues back to its projects, in the order of `projectKeys`. Undefined when an issue belongs to
|
|
37
|
+
* no requested project (or `projectKeyOf` can't tell), since the split then can't be trusted and the search's
|
|
38
|
+
* projects are read one by one instead.
|
|
39
|
+
*/
|
|
40
|
+
function splitProjectIssuesSearch(projectKeys, issues, projectKeyOf) {
|
|
41
|
+
const indexByKey = new Map(projectKeys.map((key, index) => [key, index]));
|
|
42
|
+
const split = projectKeys.map(() => []);
|
|
43
|
+
for (const issue of issues) {
|
|
44
|
+
const key = projectKeyOf(issue);
|
|
45
|
+
const index = key != null ? indexByKey.get(key) : void 0;
|
|
46
|
+
if (index == null) return void 0;
|
|
47
|
+
split[index].push(issue);
|
|
48
|
+
}
|
|
49
|
+
return split;
|
|
50
|
+
}
|
|
10
51
|
function isIssuesIntegration(integration) {
|
|
11
52
|
return integration.type === "issues";
|
|
12
53
|
}
|
|
@@ -218,6 +259,81 @@ var IssuesIntegration = class extends IntegrationBase {
|
|
|
218
259
|
}
|
|
219
260
|
}
|
|
220
261
|
/**
|
|
262
|
+
* Reads several projects' issues, one result per request in request order, each with the contract of
|
|
263
|
+
* {@link getIssuesForProjectWithTruncationResult}, so one project failing leaves its siblings readable.
|
|
264
|
+
*
|
|
265
|
+
* A tracker that can read several projects in one query (Jira, Linear) groups them through
|
|
266
|
+
* {@link getProjectIssuesSearches}, so a project with none of the user's issues costs no request of its own.
|
|
267
|
+
* Every other request, and every project of a search that did not complete, is read on its own.
|
|
268
|
+
*
|
|
269
|
+
* A search's pages are global to its project set, so only a search that completes says something about each
|
|
270
|
+
* of its projects: one that does not, or that fails for a reason that may be one project's
|
|
271
|
+
* ({@link failsEveryScope}), is read again project by project, which reports completeness, failures and
|
|
272
|
+
* retries per project. Only a failure every project would hit on its own read too fails them all at once.
|
|
273
|
+
*/
|
|
274
|
+
async getIssuesForProjectsWithTruncationResult(requests, connectionId) {
|
|
275
|
+
const searches = this.getProjectIssuesSearches(requests);
|
|
276
|
+
if (searches == null) return this.readProjectsOneByOne(requests, connectionId);
|
|
277
|
+
const scope = getScopedLogger();
|
|
278
|
+
const session = await this.resolveReadSession(connectionId, scope);
|
|
279
|
+
if (session == null) return requests.map(() => void 0);
|
|
280
|
+
const results = requests.map(() => void 0);
|
|
281
|
+
const unsearchedIndices = [];
|
|
282
|
+
await mapBounded(searches, 6, async (indices) => {
|
|
283
|
+
try {
|
|
284
|
+
const values = await this.searchProviderProjectIssues(session, indices.map((index) => requests[index]));
|
|
285
|
+
if (values == null) {
|
|
286
|
+
unsearchedIndices.push(...indices);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.resetRequestExceptionCount("getIssuesForProject");
|
|
290
|
+
indices.forEach((index, i) => {
|
|
291
|
+
results[index] = { value: {
|
|
292
|
+
values: values[i],
|
|
293
|
+
truncated: false
|
|
294
|
+
} };
|
|
295
|
+
});
|
|
296
|
+
} catch (ex) {
|
|
297
|
+
if (!failsEveryScope(ex)) {
|
|
298
|
+
_logger.warn(scope, `Project search failed (${String(ex)}); reading its projects one by one`);
|
|
299
|
+
unsearchedIndices.push(...indices);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
this.handleProviderException("getIssuesForProject", ex, { connectionId });
|
|
303
|
+
const error = toError(ex);
|
|
304
|
+
for (const index of indices) results[index] = { error };
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
if (unsearchedIndices.length > 0) {
|
|
308
|
+
const fallback = await this.readProjectsOneByOne(unsearchedIndices.map((index) => requests[index]), connectionId);
|
|
309
|
+
unsearchedIndices.forEach((index, i) => {
|
|
310
|
+
results[index] = fallback[i];
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return results;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Groups requests into searches, as lists of request indices, that {@link searchProviderProjectIssues} can read
|
|
317
|
+
* in one query each. Undefined reads every project on its own, which is the default.
|
|
318
|
+
*/
|
|
319
|
+
getProjectIssuesSearches(_requests) {}
|
|
320
|
+
/**
|
|
321
|
+
* Reads the projects of one search in one query: each project's issues in request order, or undefined when the
|
|
322
|
+
* query did not complete, which reads them one by one instead. A throw is classified by {@link failsEveryScope}.
|
|
323
|
+
*/
|
|
324
|
+
searchProviderProjectIssues(_session, _requests) {
|
|
325
|
+
return Promise.resolve(void 0);
|
|
326
|
+
}
|
|
327
|
+
readProjectsOneByOne(requests, connectionId) {
|
|
328
|
+
return mapBounded(requests, 6, async (request) => {
|
|
329
|
+
try {
|
|
330
|
+
return await this.getIssuesForProjectWithTruncationResult(request.project, request.options, connectionId);
|
|
331
|
+
} catch (ex) {
|
|
332
|
+
return { error: toError(ex) };
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
221
337
|
* Truncation-aware core of {@link getProviderIssuesForProject}. The default wraps the plain read and
|
|
222
338
|
* reports `truncated: false`; a provider whose per-project drain is capped by a page backstop overrides
|
|
223
339
|
* this to report incompleteness. Optional metadata lets providers surface structured per-project failures
|
|
@@ -239,6 +355,6 @@ __decorate([gate()], IssuesIntegration.prototype, "getResourcesForUserResult", n
|
|
|
239
355
|
__decorate([trace()], IssuesIntegration.prototype, "getProjectsForResourcesWithMetadataResult", null);
|
|
240
356
|
__decorate([trace()], IssuesIntegration.prototype, "getIssuesForProject", null);
|
|
241
357
|
//#endregion
|
|
242
|
-
export { IssuesIntegration, isIssuesIntegration };
|
|
358
|
+
export { IssuesIntegration, groupProjectIssuesSearches, isIssuesIntegration, splitProjectIssuesSearch };
|
|
243
359
|
|
|
244
360
|
//# sourceMappingURL=issuesIntegration.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issuesIntegration.js","names":[],"sources":["../../../../../plus/integrations/src/models/issuesIntegration.ts"],"sourcesContent":["import type { Account } from '@gitlens/git/models/author.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { gate } from '@gitlens/utils/decorators/gate.js';\nimport { trace } from '@gitlens/utils/decorators/log.js';\nimport { getScopedLogger } from '@gitlens/utils/logger.scoped.js';\nimport { mapSettledBounded } from '@gitlens/utils/promise.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { providerFanOutConcurrency } from '../constants.js';\nimport { toError } from '../errors.js';\nimport type { ProviderApiCollectionResult } from '../providers/models.js';\nimport type {\n\tBatchSlot,\n\tIntegration,\n\tIntegrationResult,\n\tIntegrationType,\n\tIssueEtagFields,\n\tIssueEtagInclude,\n} from './integration.js';\nimport { IntegrationBase } from './integration.js';\nimport type { IssuesForProjectOptions, ProjectIssuesDrain } from './issueReads.js';\n\nexport function isIssuesIntegration(integration: Integration): integration is IssuesIntegration {\n\treturn integration.type === 'issues';\n}\n\nexport abstract class IssuesIntegration<\n\tID extends IntegrationIds = IntegrationIds,\n\tT extends ResourceDescriptor = ResourceDescriptor,\n> extends IntegrationBase<ID> {\n\treadonly type: IntegrationType = 'issues';\n\n\tget supportsIssueLookupByResourceId(): boolean {\n\t\treturn this.getProviderIssueByResourceId != null;\n\t}\n\n\t/**\n\t * Whether this tracker has the cheap check behind the batch issue read's etags\n\t * ({@link getProviderIssuesEtagFieldsByResourceId}). The read decides before calling anything, so a tracker without\n\t * one makes one call.\n\t */\n\tget supportsIssueEtagsByResourceId(): boolean {\n\t\treturn this.getProviderIssuesEtagFieldsByResourceId != null;\n\t}\n\n\t/**\n\t * Result-returning wrapper for the BATCH tracker issue read: resolves several `(resourceId, identifier)` targets\n\t * with one {@link getProviderIssueByResourceId} request each, run with bounded concurrency. One settled slot per\n\t * input target: `fulfilled` with `undefined` means the issue does not exist or is not visible to this token;\n\t * `rejected` means that target could not be checked, never that it is absent.\n\t *\n\t * Failure isolation happens PER TARGET, mirroring `GitHostIntegration.getPullRequestsBatchResult`: the whole\n\t * call counts as a failure against the integration's request-exception budget only when EVERY slot rejected,\n\t * and not even then when a credential that checks out was refused by each target's own resource\n\t * (`settleBatchRefusals`), so a batch of mostly-good targets never spends more than one strike. A batch read with\n\t * etags makes up to two such calls (a full read of the targets without an etag, and a full read of the ones that\n\t * changed), so it can spend two; its cheap check ({@link getIssuesEtagFieldsByResourceIdBatchResult}) spends\n\t * nothing unless the credential is refused.\n\t *\n\t * Uncached on purpose: the caller owns caching, as with the git hosts' batch reads.\n\t */\n\tasync getIssuesByResourceIdBatchResult(\n\t\ttargets: readonly { resourceId: string; identifier: string; resourceUrl?: string }[],\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<BatchSlot<IssueShape | undefined>[] | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\tconst getProviderIssue = this.getProviderIssueByResourceId;\n\t\tif (getProviderIssue == null) return { value: undefined };\n\n\t\tconst start = performance.now();\n\t\ttry {\n\t\t\tconst slots = await mapSettledBounded(targets, providerFanOutConcurrency, t =>\n\t\t\t\tgetProviderIssue.call(this, session, t.resourceId, t.identifier, t.resourceUrl),\n\t\t\t);\n\n\t\t\tconst settled = await this.settleBatchRefusals(session, targets, slots);\n\n\t\t\tthis.resetRequestExceptionCount('getIssue');\n\t\t\treturn { value: settled, duration: performance.now() - start };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getIssue', ex, { scope: scope, connectionId: connectionId });\n\t\t\treturn { error: toError(ex), duration: performance.now() - start };\n\t\t}\n\t}\n\n\tprotected getProviderIssueByResourceId?(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resourceId: string,\n\t\t_id: string,\n\t\t_resourceUrl: string | undefined,\n\t): Promise<Issue | undefined>;\n\n\t/**\n\t * Result-returning wrapper for the cheap check behind the batch tracker issue read's etags: each target's change\n\t * state ({@link IssueEtagFields}), in ONE call to {@link getProviderIssuesEtagFieldsByResourceId}, which is handed\n\t * `options.etagIncludes` as the git hosts' check is. The slots mean\n\t * what {@link getIssuesByResourceIdBatchResult}'s do — `fulfilled` with `undefined` is a PROVEN ABSENCE, `rejected`\n\t * means that target could not be checked. Failures are judged and budgeted as in\n\t * `GitHostIntegration.getIssuesEtagFieldsResult`: every slot comes back as it settled, even when none answered, and\n\t * only a refused credential fails the call or spends a strike.\n\t */\n\tasync getIssuesEtagFieldsByResourceIdBatchResult(\n\t\ttargets: readonly { resourceId: string; identifier: string; resourceUrl?: string }[],\n\t\toptions: { etagIncludes?: readonly IssueEtagInclude[] },\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<BatchSlot<IssueEtagFields | undefined>[] | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\tconst getProviderEtagFields = this.getProviderIssuesEtagFieldsByResourceId;\n\t\tif (getProviderEtagFields == null) return { value: undefined };\n\n\t\tconst start = performance.now();\n\t\ttry {\n\t\t\tconst slots = await getProviderEtagFields.call(this, session, targets, options);\n\t\t\tif (slots == null) return { value: undefined, duration: performance.now() - start };\n\n\t\t\tconst settled = await this.settleBatchRefusals(session, targets, slots, { keepOtherFailures: true });\n\n\t\t\tif (slots.some(slot => slot.status === 'fulfilled')) {\n\t\t\t\tthis.resetRequestExceptionCount('getIssuesEtagFields');\n\t\t\t}\n\t\t\treturn { value: settled, duration: performance.now() - start };\n\t\t} catch (ex) {\n\t\t\tthis.handleEtagCheckException('getIssuesEtagFields', ex, { scope: scope, connectionId: connectionId });\n\t\t\treturn { error: toError(ex), duration: performance.now() - start };\n\t\t}\n\t}\n\n\t/**\n\t * OPTIONAL: the cheap check behind the batch tracker issue read's etags — one settled slot per input target, in\n\t * order, holding the issue's change state in the vocabulary {@link getProviderIssueByResourceId}'s issues end in,\n\t * so both reads compute the same etag. `fulfilled` with `undefined` is a PROVEN ABSENCE, as there; a tracker whose\n\t * cheap read can't prove one rejects the slot instead, so the full read decides. `undefined` declines the whole\n\t * check, sending every target to the full read without counting as a failure. A tracker implements this only\n\t * where it is cheaper than the full read. No tracker's row carries a real reaction count, so `'reactions'` in\n\t * `options.etagIncludes` reads nothing here.\n\t */\n\tprotected getProviderIssuesEtagFieldsByResourceId?(\n\t\tsession: ProviderAuthenticationSession,\n\t\ttargets: readonly { resourceId: string; identifier: string; resourceUrl?: string }[],\n\t\toptions: { etagIncludes?: readonly IssueEtagInclude[] },\n\t): Promise<PromiseSettledResult<IssueEtagFields | undefined>[] | undefined>;\n\n\t@trace()\n\tasync getAccountForResource(resource: T, connectionId?: string): Promise<Account | undefined> {\n\t\treturn (await this.getAccountForResourceResult(resource, connectionId))?.value;\n\t}\n\n\t/**\n\t * Result-returning core of {@link getAccountForResource}. Recovers a thrown error into `{ error }` so\n\t * callers can preserve its classification (e.g. a 401/403 → an `auth` warning that drives re-auth)\n\t * instead of collapsing every failure into an untyped `undefined`. Gated here (not on the wrapper) so\n\t * direct callers such as the ProviderBackend facade share the same dedup as `getAccountForResource`.\n\t */\n\t@gate()\n\tasync getAccountForResourceResult(\n\t\tresource: T,\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<Account | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst account = await this.getProviderAccountForResource(session, resource);\n\t\t\tthis.resetRequestExceptionCount('getAccountForResource');\n\t\t\treturn { value: account };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getAccountForResource', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\tprotected abstract getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: T,\n\t): Promise<Account | undefined>;\n\n\t@trace()\n\tasync getResourcesForUser(connectionId?: string): Promise<T[] | undefined> {\n\t\treturn (await this.getResourcesForUserResult(connectionId))?.value;\n\t}\n\n\t/**\n\t * Result-returning core of {@link getResourcesForUser}. Recovers thrown errors into `{ error }` so callers\n\t * can surface them as warnings rather than silently swallowing them to `undefined`. Gated here (not on the\n\t * wrapper) so direct callers such as the ProviderBackend facade share the same dedup as `getResourcesForUser`.\n\t */\n\t@gate()\n\tasync getResourcesForUserResult(connectionId?: string): Promise<IntegrationResult<T[] | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst resources = await this.getProviderResourcesForUser(session);\n\t\t\tthis.resetRequestExceptionCount('getResourcesForUser');\n\t\t\treturn { value: resources };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getResourcesForUser', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\tprotected abstract getProviderResourcesForUser(session: ProviderAuthenticationSession): Promise<T[] | undefined>;\n\n\t/**\n\t * Project discovery for a set of resources, returning the SDK collection `{ values, metadata }` (completeness\n\t * + per-resource failures) so callers can warn on failed resources and set `fetchFailed` without discarding\n\t * the resources that succeeded. Thrown errors are recovered into `{ error }` for the same reason: a caller\n\t * surfaces them as a warning rather than swallowing them to `undefined`.\n\t */\n\t@trace()\n\tasync getProjectsForResourcesWithMetadataResult(\n\t\tresources: T[],\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<ProviderApiCollectionResult<T> | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst projects = await this.getProviderProjectsForResourcesWithMetadata(session, resources);\n\t\t\tawait this.confirmScopedAuthFailures(session, projects.metadata);\n\t\t\tthis.resetRequestExceptionCount('getProjectsForResources');\n\t\t\treturn { value: projects };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getProjectsForResources', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\t/**\n\t * Metadata-aware provider project discovery. The default wraps the array-returning\n\t * {@link getProviderProjectsForResources} in `{ values }` with no metadata, so providers without a\n\t * fan-out completeness signal (Linear, Trello) need no change; Jira overrides it to preserve the SDK's\n\t * per-resource completeness/failures.\n\t */\n\tprotected async getProviderProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: T[],\n\t): Promise<ProviderApiCollectionResult<T>> {\n\t\tconst projects = await this.getProviderProjectsForResources(session, resources);\n\t\treturn { values: projects ?? [] };\n\t}\n\n\tprotected abstract getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: T[],\n\t): Promise<T[] | undefined>;\n\n\t@trace()\n\tasync getIssuesForProject(\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t\tconnectionId?: string,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getIssuesForProjectResult(project, options, connectionId))?.value;\n\t}\n\n\t/**\n\t * Result-returning core of {@link getIssuesForProject}. Recovers thrown errors into `{ error }` so callers\n\t * (e.g. the ProviderBackend facade) can surface a per-provider warning instead of a silent empty read —\n\t * important for providers that throw on unsupported operations (e.g. Linear's not-implemented issue read).\n\t */\n\tasync getIssuesForProjectResult(\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<IssueShape[] | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst issues = await this.getProviderIssuesForProject(session, project, options);\n\t\t\tthis.resetRequestExceptionCount('getIssuesForProject');\n\t\t\treturn { value: issues };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getIssuesForProject', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\t/**\n\t * Truncation-aware variant of {@link getIssuesForProjectResult}. A provider that drains a project's issues\n\t * with an internal page backstop (Jira/Linear) overrides {@link getProviderIssuesForProjectWithTruncation}\n\t * to report when that backstop was hit, so the facade can surface an incomplete project read instead of\n\t * publishing it as complete. The default reports `truncated: false`.\n\t */\n\tasync getIssuesForProjectWithTruncationResult(\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<ProjectIssuesDrain | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst result = await this.getProviderIssuesForProjectWithTruncation(session, project, options);\n\t\t\tawait this.confirmScopedAuthFailures(session, result?.metadata);\n\t\t\tthis.resetRequestExceptionCount('getIssuesForProject');\n\t\t\treturn { value: result };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getIssuesForProject', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\tprotected abstract getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined>;\n\n\t/**\n\t * Truncation-aware core of {@link getProviderIssuesForProject}. The default wraps the plain read and\n\t * reports `truncated: false`; a provider whose per-project drain is capped by a page backstop overrides\n\t * this to report incompleteness. Optional metadata lets providers surface structured per-project failures\n\t * (e.g. a page-level auth rejection) without discarding the already-fetched prefix.\n\t */\n\tprotected async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tconst values = await this.getProviderIssuesForProject(session, project, options);\n\t\tif (values == null) return undefined;\n\t\treturn { values: values, truncated: false };\n\t}\n}\n"],"mappings":";;;;;;;;;AAuBA,SAAgB,oBAAoB,aAA4D;CAC/F,OAAO,YAAY,SAAS;AAC7B;AAEA,IAAsB,oBAAtB,cAGU,gBAAoB;CAC7B,OAAiC;CAEjC,IAAI,kCAA2C;EAC9C,OAAO,KAAK,gCAAgC;CAC7C;;;;;;CAOA,IAAI,iCAA0C;EAC7C,OAAO,KAAK,2CAA2C;CACxD;;;;;;;;;;;;;;;;;CAkBA,MAAM,iCACL,SACA,cAC8E;EAC9E,MAAM,QAAQ,gBAAgB;EAE9B,MAAM,UAAU,MAAM,KAAK,mBAAmB,cAAc,KAAK;EACjE,IAAI,WAAW,MAAM,OAAO,KAAA;EAE5B,MAAM,mBAAmB,KAAK;EAC9B,IAAI,oBAAoB,MAAM,OAAO,EAAE,OAAO,KAAA,EAAU;EAExD,MAAM,QAAQ,YAAY,IAAI;EAC9B,IAAI;GACH,MAAM,QAAQ,MAAM,kBAAkB,SAAA,IAAoC,MACzE,iBAAiB,KAAK,MAAM,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,CAC/E;GAEA,MAAM,UAAU,MAAM,KAAK,oBAAoB,SAAS,SAAS,KAAK;GAEtE,KAAK,2BAA2B,UAAU;GAC1C,OAAO;IAAE,OAAO;IAAS,UAAU,YAAY,IAAI,IAAI;GAAM;EAC9D,SAAS,IAAI;GACZ,KAAK,wBAAwB,YAAY,IAAI;IAAS;IAAqB;GAAa,CAAC;GACzF,OAAO;IAAE,OAAO,QAAQ,EAAE;IAAG,UAAU,YAAY,IAAI,IAAI;GAAM;EAClE;CACD;;;;;;;;;;CAkBA,MAAM,2CACL,SACA,SACA,cACmF;EACnF,MAAM,QAAQ,gBAAgB;EAE9B,MAAM,UAAU,MAAM,KAAK,mBAAmB,cAAc,KAAK;EACjE,IAAI,WAAW,MAAM,OAAO,KAAA;EAE5B,MAAM,wBAAwB,KAAK;EACnC,IAAI,yBAAyB,MAAM,OAAO,EAAE,OAAO,KAAA,EAAU;EAE7D,MAAM,QAAQ,YAAY,IAAI;EAC9B,IAAI;GACH,MAAM,QAAQ,MAAM,sBAAsB,KAAK,MAAM,SAAS,SAAS,OAAO;GAC9E,IAAI,SAAS,MAAM,OAAO;IAAE,OAAO,KAAA;IAAW,UAAU,YAAY,IAAI,IAAI;GAAM;GAElF,MAAM,UAAU,MAAM,KAAK,oBAAoB,SAAS,SAAS,OAAO,EAAE,mBAAmB,KAAK,CAAC;GAEnG,IAAI,MAAM,MAAK,SAAQ,KAAK,WAAW,WAAW,GACjD,KAAK,2BAA2B,qBAAqB;GAEtD,OAAO;IAAE,OAAO;IAAS,UAAU,YAAY,IAAI,IAAI;GAAM;EAC9D,SAAS,IAAI;GACZ,KAAK,yBAAyB,uBAAuB,IAAI;IAAS;IAAqB;GAAa,CAAC;GACrG,OAAO;IAAE,OAAO,QAAQ,EAAE;IAAG,UAAU,YAAY,IAAI,IAAI;GAAM;EAClE;CACD;CAiBA,MACM,sBAAsB,UAAa,cAAqD;EAC7F,QAAQ,MAAM,KAAK,4BAA4B,UAAU,YAAY,EAAA,EAAI;CAC1E;;;;;;;CAQA,MACM,4BACL,UACA,cACkD;EAClD,MAAM,QAAQ,gBAAgB;EAE9B,MAAM,UAAU,MAAM,KAAK,mBAAmB,cAAc,KAAK;EACjE,IAAI,WAAW,MAAM,OAAO,KAAA;EAE5B,IAAI;GACH,MAAM,UAAU,MAAM,KAAK,8BAA8B,SAAS,QAAQ;GAC1E,KAAK,2BAA2B,uBAAuB;GACvD,OAAO,EAAE,OAAO,QAAQ;EACzB,SAAS,IAAI;GACZ,KAAK,wBAAwB,yBAAyB,IAAI,EAAgB,aAAa,CAAC;GACxF,OAAO,EAAE,OAAO,QAAQ,EAAE,EAAE;EAC7B;CACD;CAOA,MACM,oBAAoB,cAAiD;EAC1E,QAAQ,MAAM,KAAK,0BAA0B,YAAY,EAAA,EAAI;CAC9D;;;;;;CAOA,MACM,0BAA0B,cAAoE;EACnG,MAAM,QAAQ,gBAAgB;EAE9B,MAAM,UAAU,MAAM,KAAK,mBAAmB,cAAc,KAAK;EACjE,IAAI,WAAW,MAAM,OAAO,KAAA;EAE5B,IAAI;GACH,MAAM,YAAY,MAAM,KAAK,4BAA4B,OAAO;GAChE,KAAK,2BAA2B,qBAAqB;GACrD,OAAO,EAAE,OAAO,UAAU;EAC3B,SAAS,IAAI;GACZ,KAAK,wBAAwB,uBAAuB,IAAI,EAAgB,aAAa,CAAC;GACtF,OAAO,EAAE,OAAO,QAAQ,EAAE,EAAE;EAC7B;CACD;;;;;;;CAUA,MACM,0CACL,WACA,cACyE;EACzE,MAAM,QAAQ,gBAAgB;EAE9B,MAAM,UAAU,MAAM,KAAK,mBAAmB,cAAc,KAAK;EACjE,IAAI,WAAW,MAAM,OAAO,KAAA;EAE5B,IAAI;GACH,MAAM,WAAW,MAAM,KAAK,4CAA4C,SAAS,SAAS;GAC1F,MAAM,KAAK,0BAA0B,SAAS,SAAS,QAAQ;GAC/D,KAAK,2BAA2B,yBAAyB;GACzD,OAAO,EAAE,OAAO,SAAS;EAC1B,SAAS,IAAI;GACZ,KAAK,wBAAwB,2BAA2B,IAAI,EAAgB,aAAa,CAAC;GAC1F,OAAO,EAAE,OAAO,QAAQ,EAAE,EAAE;EAC7B;CACD;;;;;;;CAQA,MAAgB,4CACf,SACA,WAC0C;EAE1C,OAAO,EAAE,QAAQ,MADM,KAAK,gCAAgC,SAAS,SAAS,KACjD,CAAC,EAAE;CACjC;CAOA,MACM,oBACL,SACA,SACA,cACoC;EACpC,QAAQ,MAAM,KAAK,0BAA0B,SAAS,SAAS,YAAY,EAAA,EAAI;CAChF;;;;;;CAOA,MAAM,0BACL,SACA,SACA,cACuD;EACvD,MAAM,QAAQ,gBAAgB;EAE9B,MAAM,UAAU,MAAM,KAAK,mBAAmB,cAAc,KAAK;EACjE,IAAI,WAAW,MAAM,OAAO,KAAA;EAE5B,IAAI;GACH,MAAM,SAAS,MAAM,KAAK,4BAA4B,SAAS,SAAS,OAAO;GAC/E,KAAK,2BAA2B,qBAAqB;GACrD,OAAO,EAAE,OAAO,OAAO;EACxB,SAAS,IAAI;GACZ,KAAK,wBAAwB,uBAAuB,IAAI,EAAgB,aAAa,CAAC;GACtF,OAAO,EAAE,OAAO,QAAQ,EAAE,EAAE;EAC7B;CACD;;;;;;;CAQA,MAAM,wCACL,SACA,SACA,cAC6D;EAC7D,MAAM,QAAQ,gBAAgB;EAC9B,MAAM,UAAU,MAAM,KAAK,mBAAmB,cAAc,KAAK;EACjE,IAAI,WAAW,MAAM,OAAO,KAAA;EAE5B,IAAI;GACH,MAAM,SAAS,MAAM,KAAK,0CAA0C,SAAS,SAAS,OAAO;GAC7F,MAAM,KAAK,0BAA0B,SAAS,QAAQ,QAAQ;GAC9D,KAAK,2BAA2B,qBAAqB;GACrD,OAAO,EAAE,OAAO,OAAO;EACxB,SAAS,IAAI;GACZ,KAAK,wBAAwB,uBAAuB,IAAI,EAAgB,aAAa,CAAC;GACtF,OAAO,EAAE,OAAO,QAAQ,EAAE,EAAE;EAC7B;CACD;;;;;;;CAcA,MAAgB,0CACf,SACA,SACA,SAC0C;EAC1C,MAAM,SAAS,MAAM,KAAK,4BAA4B,SAAS,SAAS,OAAO;EAC/E,IAAI,UAAU,MAAM,OAAO,KAAA;EAC3B,OAAO;GAAU;GAAQ,WAAW;EAAM;CAC3C;AACD;AAhME,WAAA,CAAA,MAAM,CAAA,GAAA,kBAAA,WAAA,yBAAA,IAAA;AAWN,WAAA,CAAA,KAAK,CAAA,GAAA,kBAAA,WAAA,+BAAA,IAAA;AAyBL,WAAA,CAAA,MAAM,CAAA,GAAA,kBAAA,WAAA,uBAAA,IAAA;AAUN,WAAA,CAAA,KAAK,CAAA,GAAA,kBAAA,WAAA,6BAAA,IAAA;AAyBL,WAAA,CAAA,MAAM,CAAA,GAAA,kBAAA,WAAA,6CAAA,IAAA;AAwCN,WAAA,CAAA,MAAM,CAAA,GAAA,kBAAA,WAAA,uBAAA,IAAA"}
|
|
1
|
+
{"version":3,"file":"issuesIntegration.js","names":[],"sources":["../../../../../plus/integrations/src/models/issuesIntegration.ts"],"sourcesContent":["import type { Account } from '@gitlens/git/models/author.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { gate } from '@gitlens/utils/decorators/gate.js';\nimport { trace } from '@gitlens/utils/decorators/log.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport { getScopedLogger } from '@gitlens/utils/logger.scoped.js';\nimport { mapBounded, mapSettledBounded } from '@gitlens/utils/promise.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { failsEveryScope } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { providerFanOutConcurrency } from '../constants.js';\nimport { toError } from '../errors.js';\nimport type { ProviderApiCollectionResult } from '../providers/models.js';\nimport type {\n\tBatchSlot,\n\tIntegration,\n\tIntegrationResult,\n\tIntegrationType,\n\tIssueEtagFields,\n\tIssueEtagInclude,\n} from './integration.js';\nimport { IntegrationBase } from './integration.js';\nimport type { IssuesForProjectOptions, ProjectIssuesDrain, ProjectIssuesRequest } from './issueReads.js';\n\n/**\n * Groups requests into searches of at most `maxPerSearch` projects sharing `searchKey`. Undefined, reading\n * every project on its own, when any request has no key, i.e. cannot be searched together with others.\n */\nexport function groupProjectIssuesSearches<P>(\n\trequests: readonly ProjectIssuesRequest<P>[],\n\tsearchKey: (request: ProjectIssuesRequest<P>) => string | undefined,\n\tmaxPerSearch: number = Number.POSITIVE_INFINITY,\n): number[][] | undefined {\n\tconst searchesByKey = new Map<string, number[][]>();\n\tfor (let index = 0; index < requests.length; index++) {\n\t\tconst key = searchKey(requests[index]);\n\t\tif (key == null) return undefined;\n\n\t\tlet searches = searchesByKey.get(key);\n\t\tif (searches == null) {\n\t\t\tsearches = [];\n\t\t\tsearchesByKey.set(key, searches);\n\t\t}\n\n\t\tlet search = searches.at(-1);\n\t\tif (search == null || search.length >= maxPerSearch) {\n\t\t\tsearch = [];\n\t\t\tsearches.push(search);\n\t\t}\n\t\tsearch.push(index);\n\t}\n\treturn [...searchesByKey.values()].flat();\n}\n\n/**\n * Splits one search's issues back to its projects, in the order of `projectKeys`. Undefined when an issue belongs to\n * no requested project (or `projectKeyOf` can't tell), since the split then can't be trusted and the search's\n * projects are read one by one instead.\n */\nexport function splitProjectIssuesSearch<I>(\n\tprojectKeys: readonly string[],\n\tissues: readonly I[],\n\tprojectKeyOf: (issue: I) => string | undefined,\n): I[][] | undefined {\n\tconst indexByKey = new Map(projectKeys.map((key, index) => [key, index]));\n\tconst split = projectKeys.map((): I[] => []);\n\tfor (const issue of issues) {\n\t\tconst key = projectKeyOf(issue);\n\t\tconst index = key != null ? indexByKey.get(key) : undefined;\n\t\tif (index == null) return undefined;\n\n\t\tsplit[index].push(issue);\n\t}\n\treturn split;\n}\n\nexport function isIssuesIntegration(integration: Integration): integration is IssuesIntegration {\n\treturn integration.type === 'issues';\n}\n\nexport abstract class IssuesIntegration<\n\tID extends IntegrationIds = IntegrationIds,\n\tT extends ResourceDescriptor = ResourceDescriptor,\n> extends IntegrationBase<ID> {\n\treadonly type: IntegrationType = 'issues';\n\n\tget supportsIssueLookupByResourceId(): boolean {\n\t\treturn this.getProviderIssueByResourceId != null;\n\t}\n\n\t/**\n\t * Whether this tracker has the cheap check behind the batch issue read's etags\n\t * ({@link getProviderIssuesEtagFieldsByResourceId}). The read decides before calling anything, so a tracker without\n\t * one makes one call.\n\t */\n\tget supportsIssueEtagsByResourceId(): boolean {\n\t\treturn this.getProviderIssuesEtagFieldsByResourceId != null;\n\t}\n\n\t/**\n\t * Result-returning wrapper for the BATCH tracker issue read: resolves several `(resourceId, identifier)` targets\n\t * with one {@link getProviderIssueByResourceId} request each, run with bounded concurrency. One settled slot per\n\t * input target: `fulfilled` with `undefined` means the issue does not exist or is not visible to this token;\n\t * `rejected` means that target could not be checked, never that it is absent.\n\t *\n\t * Failure isolation happens PER TARGET, mirroring `GitHostIntegration.getPullRequestsBatchResult`: the whole\n\t * call counts as a failure against the integration's request-exception budget only when EVERY slot rejected,\n\t * and not even then when a credential that checks out was refused by each target's own resource\n\t * (`settleBatchRefusals`), so a batch of mostly-good targets never spends more than one strike. A batch read with\n\t * etags makes up to two such calls (a full read of the targets without an etag, and a full read of the ones that\n\t * changed), so it can spend two; its cheap check ({@link getIssuesEtagFieldsByResourceIdBatchResult}) spends\n\t * nothing unless the credential is refused.\n\t *\n\t * Uncached on purpose: the caller owns caching, as with the git hosts' batch reads.\n\t */\n\tasync getIssuesByResourceIdBatchResult(\n\t\ttargets: readonly { resourceId: string; identifier: string; resourceUrl?: string }[],\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<BatchSlot<IssueShape | undefined>[] | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\tconst getProviderIssue = this.getProviderIssueByResourceId;\n\t\tif (getProviderIssue == null) return { value: undefined };\n\n\t\tconst start = performance.now();\n\t\ttry {\n\t\t\tconst slots = await mapSettledBounded(targets, providerFanOutConcurrency, t =>\n\t\t\t\tgetProviderIssue.call(this, session, t.resourceId, t.identifier, t.resourceUrl),\n\t\t\t);\n\n\t\t\tconst settled = await this.settleBatchRefusals(session, targets, slots);\n\n\t\t\tthis.resetRequestExceptionCount('getIssue');\n\t\t\treturn { value: settled, duration: performance.now() - start };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getIssue', ex, { scope: scope, connectionId: connectionId });\n\t\t\treturn { error: toError(ex), duration: performance.now() - start };\n\t\t}\n\t}\n\n\tprotected getProviderIssueByResourceId?(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resourceId: string,\n\t\t_id: string,\n\t\t_resourceUrl: string | undefined,\n\t): Promise<Issue | undefined>;\n\n\t/**\n\t * Result-returning wrapper for the cheap check behind the batch tracker issue read's etags: each target's change\n\t * state ({@link IssueEtagFields}), in ONE call to {@link getProviderIssuesEtagFieldsByResourceId}, which is handed\n\t * `options.etagIncludes` as the git hosts' check is. The slots mean\n\t * what {@link getIssuesByResourceIdBatchResult}'s do — `fulfilled` with `undefined` is a PROVEN ABSENCE, `rejected`\n\t * means that target could not be checked. Failures are judged and budgeted as in\n\t * `GitHostIntegration.getIssuesEtagFieldsResult`: every slot comes back as it settled, even when none answered, and\n\t * only a refused credential fails the call or spends a strike.\n\t */\n\tasync getIssuesEtagFieldsByResourceIdBatchResult(\n\t\ttargets: readonly { resourceId: string; identifier: string; resourceUrl?: string }[],\n\t\toptions: { etagIncludes?: readonly IssueEtagInclude[] },\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<BatchSlot<IssueEtagFields | undefined>[] | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\tconst getProviderEtagFields = this.getProviderIssuesEtagFieldsByResourceId;\n\t\tif (getProviderEtagFields == null) return { value: undefined };\n\n\t\tconst start = performance.now();\n\t\ttry {\n\t\t\tconst slots = await getProviderEtagFields.call(this, session, targets, options);\n\t\t\tif (slots == null) return { value: undefined, duration: performance.now() - start };\n\n\t\t\tconst settled = await this.settleBatchRefusals(session, targets, slots, { keepOtherFailures: true });\n\n\t\t\tif (slots.some(slot => slot.status === 'fulfilled')) {\n\t\t\t\tthis.resetRequestExceptionCount('getIssuesEtagFields');\n\t\t\t}\n\t\t\treturn { value: settled, duration: performance.now() - start };\n\t\t} catch (ex) {\n\t\t\tthis.handleEtagCheckException('getIssuesEtagFields', ex, { scope: scope, connectionId: connectionId });\n\t\t\treturn { error: toError(ex), duration: performance.now() - start };\n\t\t}\n\t}\n\n\t/**\n\t * OPTIONAL: the cheap check behind the batch tracker issue read's etags — one settled slot per input target, in\n\t * order, holding the issue's change state in the vocabulary {@link getProviderIssueByResourceId}'s issues end in,\n\t * so both reads compute the same etag. `fulfilled` with `undefined` is a PROVEN ABSENCE, as there; a tracker whose\n\t * cheap read can't prove one rejects the slot instead, so the full read decides. `undefined` declines the whole\n\t * check, sending every target to the full read without counting as a failure. A tracker implements this only\n\t * where it is cheaper than the full read. No tracker's row carries a real reaction count, so `'reactions'` in\n\t * `options.etagIncludes` reads nothing here.\n\t */\n\tprotected getProviderIssuesEtagFieldsByResourceId?(\n\t\tsession: ProviderAuthenticationSession,\n\t\ttargets: readonly { resourceId: string; identifier: string; resourceUrl?: string }[],\n\t\toptions: { etagIncludes?: readonly IssueEtagInclude[] },\n\t): Promise<PromiseSettledResult<IssueEtagFields | undefined>[] | undefined>;\n\n\t@trace()\n\tasync getAccountForResource(resource: T, connectionId?: string): Promise<Account | undefined> {\n\t\treturn (await this.getAccountForResourceResult(resource, connectionId))?.value;\n\t}\n\n\t/**\n\t * Result-returning core of {@link getAccountForResource}. Recovers a thrown error into `{ error }` so\n\t * callers can preserve its classification (e.g. a 401/403 → an `auth` warning that drives re-auth)\n\t * instead of collapsing every failure into an untyped `undefined`. Gated here (not on the wrapper) so\n\t * direct callers such as the ProviderBackend facade share the same dedup as `getAccountForResource`.\n\t */\n\t@gate()\n\tasync getAccountForResourceResult(\n\t\tresource: T,\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<Account | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst account = await this.getProviderAccountForResource(session, resource);\n\t\t\tthis.resetRequestExceptionCount('getAccountForResource');\n\t\t\treturn { value: account };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getAccountForResource', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\tprotected abstract getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: T,\n\t): Promise<Account | undefined>;\n\n\t@trace()\n\tasync getResourcesForUser(connectionId?: string): Promise<T[] | undefined> {\n\t\treturn (await this.getResourcesForUserResult(connectionId))?.value;\n\t}\n\n\t/**\n\t * Result-returning core of {@link getResourcesForUser}. Recovers thrown errors into `{ error }` so callers\n\t * can surface them as warnings rather than silently swallowing them to `undefined`. Gated here (not on the\n\t * wrapper) so direct callers such as the ProviderBackend facade share the same dedup as `getResourcesForUser`.\n\t */\n\t@gate()\n\tasync getResourcesForUserResult(connectionId?: string): Promise<IntegrationResult<T[] | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst resources = await this.getProviderResourcesForUser(session);\n\t\t\tthis.resetRequestExceptionCount('getResourcesForUser');\n\t\t\treturn { value: resources };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getResourcesForUser', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\tprotected abstract getProviderResourcesForUser(session: ProviderAuthenticationSession): Promise<T[] | undefined>;\n\n\t/**\n\t * Project discovery for a set of resources, returning the SDK collection `{ values, metadata }` (completeness\n\t * + per-resource failures) so callers can warn on failed resources and set `fetchFailed` without discarding\n\t * the resources that succeeded. Thrown errors are recovered into `{ error }` for the same reason: a caller\n\t * surfaces them as a warning rather than swallowing them to `undefined`.\n\t */\n\t@trace()\n\tasync getProjectsForResourcesWithMetadataResult(\n\t\tresources: T[],\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<ProviderApiCollectionResult<T> | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst projects = await this.getProviderProjectsForResourcesWithMetadata(session, resources);\n\t\t\tawait this.confirmScopedAuthFailures(session, projects.metadata);\n\t\t\tthis.resetRequestExceptionCount('getProjectsForResources');\n\t\t\treturn { value: projects };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getProjectsForResources', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\t/**\n\t * Metadata-aware provider project discovery. The default wraps the array-returning\n\t * {@link getProviderProjectsForResources} in `{ values }` with no metadata, so providers without a\n\t * fan-out completeness signal (Linear, Trello) need no change; Jira overrides it to preserve the SDK's\n\t * per-resource completeness/failures.\n\t */\n\tprotected async getProviderProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: T[],\n\t): Promise<ProviderApiCollectionResult<T>> {\n\t\tconst projects = await this.getProviderProjectsForResources(session, resources);\n\t\treturn { values: projects ?? [] };\n\t}\n\n\tprotected abstract getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: T[],\n\t): Promise<T[] | undefined>;\n\n\t@trace()\n\tasync getIssuesForProject(\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t\tconnectionId?: string,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getIssuesForProjectResult(project, options, connectionId))?.value;\n\t}\n\n\t/**\n\t * Result-returning core of {@link getIssuesForProject}. Recovers thrown errors into `{ error }` so callers\n\t * (e.g. the ProviderBackend facade) can surface a per-provider warning instead of a silent empty read —\n\t * important for providers that throw on unsupported operations (e.g. Linear's not-implemented issue read).\n\t */\n\tasync getIssuesForProjectResult(\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<IssueShape[] | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst issues = await this.getProviderIssuesForProject(session, project, options);\n\t\t\tthis.resetRequestExceptionCount('getIssuesForProject');\n\t\t\treturn { value: issues };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getIssuesForProject', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\t/**\n\t * Truncation-aware variant of {@link getIssuesForProjectResult}. A provider that drains a project's issues\n\t * with an internal page backstop (Jira/Linear) overrides {@link getProviderIssuesForProjectWithTruncation}\n\t * to report when that backstop was hit, so the facade can surface an incomplete project read instead of\n\t * publishing it as complete. The default reports `truncated: false`.\n\t */\n\tasync getIssuesForProjectWithTruncationResult(\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<ProjectIssuesDrain | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst result = await this.getProviderIssuesForProjectWithTruncation(session, project, options);\n\t\t\tawait this.confirmScopedAuthFailures(session, result?.metadata);\n\t\t\tthis.resetRequestExceptionCount('getIssuesForProject');\n\t\t\treturn { value: result };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getIssuesForProject', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\t/**\n\t * Reads several projects' issues, one result per request in request order, each with the contract of\n\t * {@link getIssuesForProjectWithTruncationResult}, so one project failing leaves its siblings readable.\n\t *\n\t * A tracker that can read several projects in one query (Jira, Linear) groups them through\n\t * {@link getProjectIssuesSearches}, so a project with none of the user's issues costs no request of its own.\n\t * Every other request, and every project of a search that did not complete, is read on its own.\n\t *\n\t * A search's pages are global to its project set, so only a search that completes says something about each\n\t * of its projects: one that does not, or that fails for a reason that may be one project's\n\t * ({@link failsEveryScope}), is read again project by project, which reports completeness, failures and\n\t * retries per project. Only a failure every project would hit on its own read too fails them all at once.\n\t */\n\tasync getIssuesForProjectsWithTruncationResult(\n\t\trequests: readonly ProjectIssuesRequest<T>[],\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<ProjectIssuesDrain | undefined>[]> {\n\t\tconst searches = this.getProjectIssuesSearches(requests);\n\t\tif (searches == null) return this.readProjectsOneByOne(requests, connectionId);\n\n\t\tconst scope = getScopedLogger();\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return requests.map(() => undefined);\n\n\t\tconst results: IntegrationResult<ProjectIssuesDrain | undefined>[] = requests.map(() => undefined);\n\t\tconst unsearchedIndices: number[] = [];\n\t\tawait mapBounded(searches, providerFanOutConcurrency, async indices => {\n\t\t\ttry {\n\t\t\t\tconst values = await this.searchProviderProjectIssues(\n\t\t\t\t\tsession,\n\t\t\t\t\tindices.map(index => requests[index]),\n\t\t\t\t);\n\t\t\t\tif (values == null) {\n\t\t\t\t\tunsearchedIndices.push(...indices);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tthis.resetRequestExceptionCount('getIssuesForProject');\n\t\t\t\tindices.forEach((index, i) => {\n\t\t\t\t\tresults[index] = { value: { values: values[i], truncated: false } };\n\t\t\t\t});\n\t\t\t} catch (ex) {\n\t\t\t\tif (!failsEveryScope(ex)) {\n\t\t\t\t\tLogger.warn(scope, `Project search failed (${String(ex)}); reading its projects one by one`);\n\t\t\t\t\tunsearchedIndices.push(...indices);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tthis.handleProviderException('getIssuesForProject', ex, { connectionId: connectionId });\n\t\t\t\tconst error = toError(ex);\n\t\t\t\tfor (const index of indices) {\n\t\t\t\t\tresults[index] = { error: error };\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\t// After every search rather than inside each, so the fallback reads share one concurrency bound.\n\t\tif (unsearchedIndices.length > 0) {\n\t\t\tconst fallback = await this.readProjectsOneByOne(\n\t\t\t\tunsearchedIndices.map(index => requests[index]),\n\t\t\t\tconnectionId,\n\t\t\t);\n\t\t\tunsearchedIndices.forEach((index, i) => {\n\t\t\t\tresults[index] = fallback[i];\n\t\t\t});\n\t\t}\n\t\treturn results;\n\t}\n\n\t/**\n\t * Groups requests into searches, as lists of request indices, that {@link searchProviderProjectIssues} can read\n\t * in one query each. Undefined reads every project on its own, which is the default.\n\t */\n\tprotected getProjectIssuesSearches(_requests: readonly ProjectIssuesRequest<T>[]): number[][] | undefined {\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Reads the projects of one search in one query: each project's issues in request order, or undefined when the\n\t * query did not complete, which reads them one by one instead. A throw is classified by {@link failsEveryScope}.\n\t */\n\tprotected searchProviderProjectIssues(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_requests: readonly ProjectIssuesRequest<T>[],\n\t): Promise<IssueShape[][] | undefined> {\n\t\treturn Promise.resolve(undefined);\n\t}\n\n\tprivate readProjectsOneByOne(\n\t\trequests: readonly ProjectIssuesRequest<T>[],\n\t\tconnectionId: string | undefined,\n\t): Promise<IntegrationResult<ProjectIssuesDrain | undefined>[]> {\n\t\treturn mapBounded(requests, providerFanOutConcurrency, async request => {\n\t\t\ttry {\n\t\t\t\treturn await this.getIssuesForProjectWithTruncationResult(\n\t\t\t\t\trequest.project,\n\t\t\t\t\trequest.options,\n\t\t\t\t\tconnectionId,\n\t\t\t\t);\n\t\t\t} catch (ex) {\n\t\t\t\treturn { error: toError(ex) };\n\t\t\t}\n\t\t});\n\t}\n\n\tprotected abstract getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined>;\n\n\t/**\n\t * Truncation-aware core of {@link getProviderIssuesForProject}. The default wraps the plain read and\n\t * reports `truncated: false`; a provider whose per-project drain is capped by a page backstop overrides\n\t * this to report incompleteness. Optional metadata lets providers surface structured per-project failures\n\t * (e.g. a page-level auth rejection) without discarding the already-fetched prefix.\n\t */\n\tprotected async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tconst values = await this.getProviderIssuesForProject(session, project, options);\n\t\tif (values == null) return undefined;\n\t\treturn { values: values, truncated: false };\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@ import { Account } from "../../../git/models/author.js";
|
|
|
6
6
|
import { ProviderAuthenticationSession } from "../authentication/models.js";
|
|
7
7
|
import { IssuesSelfManagedHostIntegrationId } from "../constants.js";
|
|
8
8
|
import { ProvidersApi } from "./providersApi.js";
|
|
9
|
-
import { AccountWideIssuesResult, IssuesForProjectOptions, ProjectIssuesDrain, SearchMyIssuesOptions } from "../models/issueReads.js";
|
|
9
|
+
import { AccountWideIssuesResult, IssuesForProjectOptions, ProjectIssuesDrain, ProjectIssuesRequest, SearchMyIssuesOptions } from "../models/issueReads.js";
|
|
10
10
|
import { IssuesIntegration } from "../models/issuesIntegration.js";
|
|
11
11
|
import { IntegrationKey } from "../models/integration.js";
|
|
12
12
|
import { IntegrationConnectionChangeEvent } from "../integrationService.js";
|
|
@@ -61,6 +61,13 @@ export declare class JiraServerIntegration extends IssuesIntegration<IssuesSelfM
|
|
|
61
61
|
protected getProviderProjectsForResources(session: ProviderAuthenticationSession, resources: JiraServerResourceDescriptor[], force?: boolean): Promise<JiraServerProjectDescriptor[] | undefined>;
|
|
62
62
|
protected getProviderIssuesForProject(session: ProviderAuthenticationSession, project: JiraServerProjectDescriptor, options?: IssuesForProjectOptions): Promise<IssueShape[] | undefined>;
|
|
63
63
|
protected getProviderIssuesForProjectWithTruncation(session: ProviderAuthenticationSession, project: JiraServerProjectDescriptor, options?: IssuesForProjectOptions): Promise<ProjectIssuesDrain | undefined>;
|
|
64
|
+
/**
|
|
65
|
+
* Searches the instance's projects together for a user-scoped read, as Jira Cloud does per site: one JQL names
|
|
66
|
+
* up to {@link JIRA_MAX_PROJECT_KEYS_PER_REQUEST} projects of one user scope. An unscoped read stays per project,
|
|
67
|
+
* since its backstop is reported as `narrow-scope` per project.
|
|
68
|
+
*/
|
|
69
|
+
protected getProjectIssuesSearches(requests: readonly ProjectIssuesRequest<JiraServerProjectDescriptor>[]): number[][] | undefined;
|
|
70
|
+
protected searchProviderProjectIssues(session: ProviderAuthenticationSession, requests: readonly ProjectIssuesRequest<JiraServerProjectDescriptor>[]): Promise<IssueShape[][] | undefined>;
|
|
64
71
|
protected searchProviderMyIssues(session: ProviderAuthenticationSession, resources?: JiraServerResourceDescriptor[], cancellation?: AbortSignal): Promise<IssueShape[] | undefined>;
|
|
65
72
|
/**
|
|
66
73
|
* Account-wide "my issues" for this instance, drained to the backstop and reporting whether it got there.
|