@gitkraken/core-gitlens 0.12.0 → 0.14.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 +78 -1
- package/dist/git/cache.d.ts +84 -29
- package/dist/git/cache.js +188 -54
- package/dist/git/cache.js.map +1 -1
- package/dist/git/features.d.ts +1 -1
- package/dist/git/features.js +5 -0
- package/dist/git/features.js.map +1 -1
- package/dist/git/models/branch.d.ts +20 -2
- package/dist/git/models/branch.js +28 -6
- package/dist/git/models/branch.js.map +1 -1
- package/dist/git/models/reference.d.ts +3 -0
- package/dist/git/models/status.js +2 -2
- package/dist/git/models/worktree.d.ts +4 -1
- package/dist/git/models/worktree.js +4 -2
- package/dist/git/models/worktree.js.map +1 -1
- package/dist/git/parsers/diffParser.js +1 -1
- package/dist/git/providers/branches.d.ts +73 -8
- package/dist/git/providers/operations.d.ts +3 -2
- package/dist/git/providers/pausedOperations.d.ts +6 -0
- package/dist/git/providers/provider.d.ts +45 -2
- package/dist/git/providers/revision.d.ts +17 -3
- package/dist/git/providers/status.d.ts +8 -2
- package/dist/git/remotes/azure-devops.js +61 -53
- package/dist/git/remotes/azure-devops.js.map +1 -1
- package/dist/git/run.types.d.ts +12 -0
- package/dist/git/service.d.ts +8 -5
- package/dist/git/service.js +16 -8
- package/dist/git/service.js.map +1 -1
- package/dist/git/utils/branch.utils.d.ts +12 -2
- package/dist/git/utils/branch.utils.js +49 -5
- package/dist/git/utils/branch.utils.js.map +1 -1
- package/dist/git/utils/reference.utils.d.ts +1 -0
- package/dist/git/utils/reference.utils.js.map +1 -1
- package/dist/git/watching/changeEvent.d.ts +13 -2
- package/dist/git/watching/changeEvent.js +17 -3
- package/dist/git/watching/changeEvent.js.map +1 -1
- package/dist/git/watching/gitDirPoller.d.ts +111 -0
- package/dist/git/watching/gitDirPoller.js +266 -0
- package/dist/git/watching/gitDirPoller.js.map +1 -0
- package/dist/git/watching/provider.d.ts +16 -0
- package/dist/git/watching/watchGroup.d.ts +40 -4
- package/dist/git/watching/watchGroup.js +143 -17
- package/dist/git/watching/watchGroup.js.map +1 -1
- package/dist/git/watching/watchService.d.ts +70 -1
- package/dist/git/watching/watchService.js +158 -13
- package/dist/git/watching/watchService.js.map +1 -1
- package/dist/git/watching/watchSession.d.ts +44 -0
- package/dist/git/watching/watchSession.js +91 -6
- package/dist/git/watching/watchSession.js.map +1 -1
- package/dist/git/watching/watcherPatterns.d.ts +21 -0
- package/dist/git/watching/watcherPatterns.js +67 -1
- package/dist/git/watching/watcherPatterns.js.map +1 -1
- package/dist/git/watching/worktreeSummary.d.ts +34 -7
- package/dist/git/watching/worktreeSummary.js +39 -12
- package/dist/git/watching/worktreeSummary.js.map +1 -1
- package/dist/git-cli/cliGitProvider.d.ts +40 -2
- package/dist/git-cli/cliGitProvider.js +182 -18
- package/dist/git-cli/cliGitProvider.js.map +1 -1
- package/dist/git-cli/exec/commandClassifier.d.ts +15 -0
- package/dist/git-cli/exec/commandClassifier.js +48 -2
- package/dist/git-cli/exec/commandClassifier.js.map +1 -1
- package/dist/git-cli/exec/git.d.ts +20 -3
- package/dist/git-cli/exec/git.js +63 -19
- package/dist/git-cli/exec/git.js.map +1 -1
- package/dist/git-cli/exec/gitQueue.d.ts +2 -0
- package/dist/git-cli/exec/gitQueue.js +2 -1
- package/dist/git-cli/exec/gitQueue.js.map +1 -1
- package/dist/git-cli/exec/indexLock.d.ts +39 -0
- package/dist/git-cli/exec/indexLock.js +160 -0
- package/dist/git-cli/exec/indexLock.js.map +1 -0
- package/dist/git-cli/parsers/blameParser.js +2 -2
- package/dist/git-cli/parsers/refParser.js +4 -2
- package/dist/git-cli/parsers/refParser.js.map +1 -1
- package/dist/git-cli/parsers/statusParser.d.ts +6 -0
- package/dist/git-cli/parsers/statusParser.js +33 -3
- package/dist/git-cli/parsers/statusParser.js.map +1 -1
- package/dist/git-cli/parsers/worktreeParser.js +42 -2
- package/dist/git-cli/parsers/worktreeParser.js.map +1 -1
- package/dist/git-cli/providers/branchRemote.d.ts +14 -0
- package/dist/git-cli/providers/branchRemote.js +36 -0
- package/dist/git-cli/providers/branchRemote.js.map +1 -0
- package/dist/git-cli/providers/branches.d.ts +109 -10
- package/dist/git-cli/providers/branches.js +630 -134
- package/dist/git-cli/providers/branches.js.map +1 -1
- package/dist/git-cli/providers/commits.js +2 -2
- package/dist/git-cli/providers/config.d.ts +9 -5
- package/dist/git-cli/providers/config.js +58 -25
- package/dist/git-cli/providers/config.js.map +1 -1
- package/dist/git-cli/providers/contributors.js +1 -1
- package/dist/git-cli/providers/graph.js +9 -6
- package/dist/git-cli/providers/graph.js.map +1 -1
- package/dist/git-cli/providers/operations.d.ts +17 -0
- package/dist/git-cli/providers/operations.js +172 -111
- package/dist/git-cli/providers/operations.js.map +1 -1
- package/dist/git-cli/providers/pausedOperations.d.ts +8 -0
- package/dist/git-cli/providers/pausedOperations.js +59 -39
- package/dist/git-cli/providers/pausedOperations.js.map +1 -1
- package/dist/git-cli/providers/refs.js +11 -8
- package/dist/git-cli/providers/refs.js.map +1 -1
- package/dist/git-cli/providers/remotes.js +19 -10
- package/dist/git-cli/providers/remotes.js.map +1 -1
- package/dist/git-cli/providers/revision.d.ts +6 -2
- package/dist/git-cli/providers/revision.js +38 -12
- package/dist/git-cli/providers/revision.js.map +1 -1
- package/dist/git-cli/providers/staging.js +39 -14
- package/dist/git-cli/providers/staging.js.map +1 -1
- package/dist/git-cli/providers/stash.d.ts +2 -0
- package/dist/git-cli/providers/stash.js +33 -18
- package/dist/git-cli/providers/stash.js.map +1 -1
- package/dist/git-cli/providers/status.d.ts +3 -1
- package/dist/git-cli/providers/status.js +40 -22
- package/dist/git-cli/providers/status.js.map +1 -1
- package/dist/git-cli/providers/tags.js +6 -4
- package/dist/git-cli/providers/tags.js.map +1 -1
- package/dist/git-cli/providers/worktrees.d.ts +1 -0
- package/dist/git-cli/providers/worktrees.js +60 -31
- package/dist/git-cli/providers/worktrees.js.map +1 -1
- package/dist/git-cli/service.d.ts +6 -0
- package/dist/git-cli/service.js +5 -2
- package/dist/git-cli/service.js.map +1 -1
- package/dist/plus/git-github/providers/github/revision.d.ts +6 -2
- package/dist/plus/git-github/providers/github/revision.js +15 -10
- package/dist/plus/git-github/providers/github/revision.js.map +1 -1
- package/dist/plus/git-github/providers/github/status.d.ts +3 -1
- package/dist/plus/git-github/providers/github/status.js +2 -2
- package/dist/plus/git-github/providers/github/status.js.map +1 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +13 -2
- package/dist/plus/integrations/collectionMetadata.js +14 -2
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/index.d.ts +2 -2
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +1 -0
- package/dist/plus/integrations/integrationService.js +1 -1
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +1 -0
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +41 -5
- package/dist/plus/integrations/models/gitHostIntegration.js +106 -29
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/issueReads.d.ts +17 -4
- package/dist/plus/integrations/providers/apiClients.d.ts +2 -0
- package/dist/plus/integrations/providers/apiClients.js +7 -1
- package/dist/plus/integrations/providers/apiClients.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.d.ts +5 -4
- package/dist/plus/integrations/providers/azure/azure.js +36 -76
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.utils.js +20 -17
- package/dist/plus/integrations/providers/azure/azure.utils.js.map +1 -1
- package/dist/plus/integrations/providers/azure/models.d.ts +2 -2
- package/dist/plus/integrations/providers/azure/models.js +4 -4
- package/dist/plus/integrations/providers/azure/models.js.map +1 -1
- package/dist/plus/integrations/providers/azure/workItemStateCategories.d.ts +21 -12
- package/dist/plus/integrations/providers/azure/workItemStateCategories.js +74 -17
- package/dist/plus/integrations/providers/azure/workItemStateCategories.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +44 -0
- package/dist/plus/integrations/providers/azureDevOps.js +108 -17
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +45 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.js +121 -6
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
- package/dist/plus/integrations/providers/jira-server.js +18 -14
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.js +18 -14
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +1 -2
- package/dist/plus/integrations/providers/models.js +9 -2
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +7 -17
- package/dist/plus/integrations/providers/providersApi.js +85 -78
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/utils/jiraIssueDrains.d.ts +22 -0
- package/dist/plus/integrations/providers/utils/jiraIssueDrains.js +69 -9
- package/dist/plus/integrations/providers/utils/jiraIssueDrains.js.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.js +8 -2
- package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
- package/dist/plus/integrations/reads/filters.d.ts +3 -3
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/issueBatch.d.ts +2 -1
- package/dist/plus/integrations/reads/issueBatch.js +1 -1
- package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +23 -14
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/issues.d.ts +2 -1
- package/dist/plus/integrations/reads/issues.js +18 -3
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequestBatch.js +1 -1
- package/dist/plus/integrations/reads/resolveRepository.js +12 -3
- package/dist/plus/integrations/reads/resolveRepository.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +11 -4
- package/dist/plus/integrations/reads/warnings.js +15 -5
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +36 -2
- package/dist/plus/integrations/results.js +33 -13
- package/dist/plus/integrations/results.js.map +1 -1
- package/dist/utils/promiseCache.d.ts +10 -0
- package/dist/utils/promiseCache.js +17 -2
- package/dist/utils/promiseCache.js.map +1 -1
- package/docs/integrations.md +52 -20
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"results.js","names":[],"sources":["../../../../plus/integrations/src/results.ts"],"sourcesContent":["import { isUnsupportedSortError } from '@gitkraken/provider-apis';\nimport { AuthenticationError, RequestNotFoundError, RequestRateLimitError } from '@gitlens/git/errors.js';\nimport type { IssueSorting } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSorting } from '@gitlens/git/models/pullRequest.js';\nimport type { IntegrationIds } from './constants.js';\nimport { isProviderUnreachableError } from './errors.js';\n\nexport interface ConnectionStateChangeEvent {\n\tkey: string;\n\treason: 'connected' | 'disconnected';\n}\n\n/**\n * A per-provider, per-connection warning surfaced alongside (partial) read results. Consumers use\n * these to drive auth recovery, retry, or truncation messaging without the read itself throwing.\n *\n * `kind` is the programmatic discriminant (a rate-limit is retryable, a 404 is not, an unsupported capability\n * never succeeds on retry, an auth failure needs re-connection unless {@link ProviderWarning.scope} confines\n * it); `isAuth` is retained as a convenience mirror of `kind === 'auth'`.\n *\n * `'unsupported'`: the provider — or this integration of it — lacks the capability the request asked for: a\n * surface (issues on Bitbucket, pull requests on an issue tracker, a batch read, org/repo discovery,\n * current-account lookup, project-scoped or account-wide issue reads), or a filter, sort, state, search\n * criterion or option combination it cannot express. Retrying the same request never succeeds; a different\n * provider, scope or request may. NOT unsupported (these stay `'other'`): malformed or contradictory caller\n * input the caller fixes in the request itself (a duplicate batch key, invalid target numbers, an empty owner,\n * an unusable scope), incompleteness or omission warnings, and unclassified errors.\n */\nexport type ProviderWarningKind = 'auth' | 'rate-limit' | 'not-found' | 'no-connection' | 'unsupported' | 'other';\n\n/**\n * Why a read that SUCCEEDED still withheld results:\n * - `provider-limit`: the provider refuses to serve past a cap it enforces (GitHub search's 1,000-result\n * ceiling, Trello's `cards_limit`), so the excess is unreachable through that query.\n * - `recovery-budget`: the internal partitioned recovery stopped spending upstream requests before it ran\n * out of partitions to visit.\n * - `pagination-incomplete`: pages were left unread — a sub-scope of a multi-scope read that was not drained,\n * a drain that stopped at its own page backstop, or a provider that advertised another page and gave no way\n * to reach it. These have DIFFERENT remedies, so the kind alone does not say whether more can be fetched;\n * read {@link ProviderWarningOmission.recovery} for that.\n *\n * Mirrors the vocabulary the SDK reports. Declared here rather than imported so this module stays free of\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`); `collectionMetadata.ts` holds the\n * compile-time link, so an SDK bump that adds a kind fails the build at that boundary.\n */\nexport type ProviderWarningOmissionKind = 'provider-limit' | 'recovery-budget' | 'pagination-incomplete';\n\n/**\n * How a consumer can recover omitted results. `page-budget` reruns with a larger budget; `narrow-scope`\n * requests a smaller server-side scope; `none` promises neither. Switch on the exact value because only\n * `page-budget` is a fetch-more action.\n */\nexport type ProviderWarningOmissionRecovery = 'none' | 'page-budget' | 'narrow-scope';\n\n/**\n * The part of a provider a warning is confined to: one resource (organization, workspace, site), project or\n * repository. All fields optional; see {@link ProviderWarning.scope} for when one is forwarded.\n */\nexport interface ProviderWarningScope {\n\t/**\n\t * The resource as the read addressed it: its id on most reads, its name on the few that address it by name\n\t * (Azure DevOps' repo-scoped reads, Bitbucket's workspace reads). Match it against both\n\t * `ProviderOrganization.id` and `name`, and a project's `orgId` where it has one.\n\t */\n\tresourceId?: string;\n\t/**\n\t * The project as the read addressed it. For a Jira project read, the key on Jira Cloud (the project's `name` in\n\t * `listProjects`) and the numeric id on Jira Data Center.\n\t */\n\tprojectId?: string;\n\trepositoryId?: string;\n}\n\n/**\n * Why a provider request failed, when the failure itself says so:\n * - `unreachable`: a transport failure or server-side error prevented the provider from answering. The connection\n * still exists; this accompanies `kind: 'other'`, never an authentication warning.\n * - `oauth-app-not-allowed`: the organization does not let third-party OAuth apps in. On Azure DevOps this is the\n * organization policy \"Third-party application access via OAuth\", off by default for new organizations. An\n * organization admin enables it, or the user connects with a personal access token instead, which the policy\n * does not govern.\n * - `access-denied`: the account has no access to that organization or project (not a member, or lacking the\n * permission). Someone who administers it has to grant access.\n * - `conditional-access`: a Microsoft Entra Conditional Access policy blocked the request (Azure DevOps\n * `VS403463`). Its admin has to exempt the request, e.g. by location or device.\n *\n * None of these is healed by reconnecting, which is the point of naming them.\n */\nexport type ProviderWarningCauseReason =\n\t| 'oauth-app-not-allowed'\n\t| 'access-denied'\n\t| 'conditional-access'\n\t| 'unreachable';\n\n/** See {@link ProviderWarning.cause}. */\nexport interface ProviderWarningCause {\n\treason: ProviderWarningCauseReason;\n\t/** The provider's own error code, when it reports one (Azure DevOps `TF400813`, `VS403463`). */\n\tcode?: string;\n\t/** Where the setting behind the refusal is changed, when this layer can address it. */\n\tremedyUrl?: string;\n}\n\n/** Which repository / project / resource an omission is attributed to. All fields optional; a scope may name none. */\nexport interface ProviderWarningOmissionScope extends ProviderWarningScope {\n\tproviderId?: string;\n}\n\nexport interface ProviderWarningOmission {\n\tkind: ProviderWarningOmissionKind;\n\t/**\n\t * Whether anything would fetch the missing items, and what. Gate a \"load more\" affordance on this rather\n\t * than on `kind` — see {@link ProviderWarningOmissionRecovery}.\n\t */\n\trecovery: ProviderWarningOmissionRecovery;\n\t/**\n\t * The cap the provider enforces, when it reports one. NOT a result count for every kind: on\n\t * `recovery-budget` this is a REQUEST budget and must not be shown to a user as a number of results.\n\t */\n\tlimit?: number;\n\t/** Total matches the provider reported, when it reports one. Only GitHub's search cap does today. */\n\ttotalCount?: number;\n\t/**\n\t * The order the reachable window was selected under, as `field:direction`, when the omission is a result\n\t * ceiling.\n\t *\n\t * Present so a consumer can word its own \"N matched, showing the first M\" without assuming WHICH M: the\n\t * provider serves the top M under this key, so the sentence is only true if it names it. Absent for every\n\t * omission that isn't a ceiling, where no window was selected by an order.\n\t *\n\t * A union of both vocabularies rather than the issue one alone: `PullRequestSorting` happens to be a subset\n\t * today, so narrowing this to {@link IssueSorting} would still compile while describing only half the reads\n\t * that set it — and would break the day the PR search gains a key the issue search has no name for.\n\t */\n\tsort?: IssueSorting | PullRequestSorting;\n\t/** Which repository / project / resource was affected, when one is attributed. */\n\tscope?: ProviderWarningOmissionScope;\n}\n\nexport interface ProviderWarning {\n\tproviderId: IntegrationIds;\n\t/** Disambiguates connections on self-managed hosts (where one provider id spans multiple domains). */\n\tdomain?: string;\n\t/** The specific token/connection the read was pinned to, when a `connectionId` was supplied. */\n\tconnectionId?: string;\n\tmessage: string;\n\tkind: ProviderWarningKind;\n\t/** Convenience mirror of `kind === 'auth'`, scoped or not; read {@link scope} before prompting to reconnect. */\n\tisAuth: boolean;\n\t/**\n\t * Present when the failure was recorded against one sub-scope of the read (an organization, project or\n\t * repository) rather than against the connection. A scoped `auth` failure is that one scope refusing this\n\t * credential, e.g. an Azure DevOps organization that disallows third-party OAuth apps, one in another Entra\n\t * tenant, or a Conditional Access policy. Reconnecting cannot fix those, so this warning alone is no reason\n\t * to prompt for it.\n\t *\n\t * `kind` and `isAuth` are unaffected: a scoped 401 is still an authentication failure, and this field says\n\t * how far it reaches. It names at least one of its IDs, and is never set on an omission (which carries its\n\t * own {@link ProviderWarningOmission.scope}) or on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}), even when that call targeted a single organization. The exception is a batch\n\t * read's refused target, which names its scope: the Azure DevOps organization and project, the repository on\n\t * other git hosts, or the tracker's resource.\n\t *\n\t * Its ABSENCE means account-wide or unattributed, so existing handling stays correct without it.\n\t *\n\t * A scoped `auth` failure also means the credential itself was accepted. A dead token can come back as nothing\n\t * but scoped refusals wherever a read reaches its scopes without an uncached request to the connection first:\n\t * discovery served from a per-token cache (Azure DevOps, Bitbucket, Jira Cloud) or an SDK fan-out across the\n\t * requested repositories (Bitbucket Data Center). So when a read's only auth failures are scoped, those providers\n\t * confirm the credential with one uncached check first, and a refused credential fails the whole read with an\n\t * unscoped `auth` warning. A batch read checks it only when every target was refused, since a target that\n\t * answered already proved it. A refusal the provider attributes to the credential itself, like Bitbucket's or Jira\n\t * Cloud's for a token missing the OAuth scopes a read needs, which a reconnect fixes, is published unscoped too,\n\t * once for the connection, while the scopes that answered keep their results.\n\t *\n\t * So the promise holds for a read that carries no unscoped `auth` warning of its own; one that does already asks\n\t * for a reconnect, and its other scoped refusals are not checked. Two exceptions stay scoped even then. A check\n\t * that could not complete or was denied (a network error, a throttle, a `403`, Bitbucket Data Center refusing a\n\t * credential it authenticated, or a Bitbucket Data Center project access token, whose user the check cannot\n\t * find) proves nothing, so the warnings are published unconfirmed, with no {@link cause}, and the next read\n\t * checks again. And a credential confirmed within the last minute is not probed again, so a revocation can take\n\t * up to a minute to surface.\n\t */\n\tscope?: ProviderWarningScope;\n\t/**\n\t * Why the provider request failed, when the failure itself says so: see {@link ProviderWarningCauseReason}. Switch on\n\t * `reason` to recommend a fix instead of a reconnect; `message` then carries the same explanation as prose.\n\t *\n\t * `unreachable` accompanies a transport or server failure with `kind: 'other'`. Credential refusal causes are\n\t * set only on a scoped `auth` warning whose credential was confirmed first (see {@link scope}), because the\n\t * refusals they name are indistinguishable from a dead credential until then: Azure DevOps answers a\n\t * third-party OAuth app its organization disallows exactly as it answers an expired token. Only Azure DevOps\n\t * reports one today. Its absence proves nothing about the cause.\n\t */\n\tcause?: ProviderWarningCause;\n\t/**\n\t * Present when this warning describes results the read could not return even though the request itself\n\t * SUCCEEDED — a provider-enforced cap, an exhausted recovery budget, a page budget, or a sub-scope the read\n\t * did not drain.\n\t *\n\t * Its presence is the signal: an omission is not a failure. Message it as incompleteness rather than as a\n\t * failed read, and do NOT derive that from `message`, which is English prose and subject to rewording.\n\t * `kind` stays `'other'` for these, so the failure discriminant keeps meaning exactly what it meant before\n\t * this field existed.\n\t *\n\t * Whether anything would fetch the rest is a SEPARATE question — read {@link ProviderWarningOmission.recovery},\n\t * not `kind`, and never assume a retry of the same request returns more.\n\t *\n\t * `limit` / `totalCount` / `scope` are forwarded only when they are reported; most omissions carry none of\n\t * the three, so render correctly without them — and see {@link ProviderWarningOmission.limit} before\n\t * showing that figure as a result count.\n\t *\n\t * Its ABSENCE proves nothing. It is never set on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}) or from a structured scope failure — those are failures, and the field would\n\t * be a lie there. But it is also absent whenever incompleteness was reported without naming what was left\n\t * out, so treat a bare `kind: 'other'` warning as unclassified rather than as a proven failure.\n\t */\n\tomission?: ProviderWarningOmission;\n\t/**\n\t * The keys of the batch targets this warning concerns: the target a per-target failure was refused or failed\n\t * for, or every target of an integration call that failed as a whole. So a caller that sends several callers'\n\t * targets in one call can hand a failure only to the targets it belongs to.\n\t *\n\t * Its ABSENCE means the whole call. It is absent on every read that is not a batch read, and on a batch read's\n\t * refusal of the whole call before any target is read (a duplicate key, an invalid target, an unknown etag\n\t * include, no resolvable connection, the wrong surface), which concerns every target alike.\n\t *\n\t * It is not part of a warning's identity: two warnings that differ only here are one warning, for the union of\n\t * their targets (see {@link appendDedupedWarning}).\n\t */\n\ttargetKeys?: string[];\n}\n\nexport interface ProviderPageInfo {\n\t/**\n\t * The 1-based POSITION of the returned page. One convention across every paged read, page-number and\n\t * cursor paging alike, so a `getNextPageParam`-style consumer can key off it uniformly:\n\t * - the page number the provider itself reported, when it reports one (authoritative);\n\t * - else the position the caller addressed: the page encoded in a threaded page cursor, or the `page`\n\t * supplied alongside an opaque cursor (1 when the caller supplied no `page`);\n\t * - else, for a read that can be advanced by page number, the requested `page` — the provider either\n\t * honored it or the internal drain reached it (see the paging contract on `IntegrationManager`);\n\t * - else 1: a cursor-only read with no cursor threaded back ignores a requested page number and answers\n\t * with its first page, so echoing the request would mislabel page 1 as page N.\n\t *\n\t * A page past the provider's terminal cursor still reports the requested `page` with empty `items` — an\n\t * empty page N, never page N-1 relabeled.\n\t *\n\t * Continue a cursor-paged read from `cursor`, NOT from `currentPage + 1`: a cursor-only host can't be\n\t * addressed by number, so this is a position label and not always a resumable request parameter.\n\t */\n\tcurrentPage: number;\n\t/**\n\t * The page size IN EFFECT, which is not always the one requested — it is whichever of these the read can\n\t * actually vouch for:\n\t * - the size the provider reported for the page it served (numbered-page hosts), else\n\t * - the `itemsPerPage` that was applied to the request (repo-scoped reads, which forward it), else\n\t * - `items.length`, for a read that takes no page size at all: the account-wide PR/issue reads, `listRepos`\n\t * (cursor-only), and `listIssueTrackerIssuesPage`, whose `itemsPerPage` counts PROJECTS rather than issues.\n\t *\n\t * So it is a description of the page, not an echo of the request: don't derive a total from\n\t * `currentPage * itemsPerPage`, and don't read `items.length < itemsPerPage` as \"last page\" — `hasMore` /\n\t * `cursor` are the only continuation signals.\n\t */\n\titemsPerPage: number;\n\t/**\n\t * A COMPLETENESS assertion, not a mode flag: `true` only when a sweep drained every page of every target\n\t * without truncating or failing (`!truncated && !fetchFailed`). Set exclusively by `sweepPullRequests` /\n\t * `sweepClosedPullRequests` and absent on every paged read, so `allPages !== true` does NOT by itself mean\n\t * \"this was a single page\".\n\t *\n\t * This is the signal to gate on before caching a sweep as authoritative: unlike `truncated` it is `false`\n\t * for BOTH kinds of incompleteness, so it can't be misread as a benign cap.\n\t */\n\tallPages?: boolean;\n\t/**\n\t * True when the read stopped before it could confirm it had everything. Deliberately does NOT distinguish\n\t * WHY, and a consumer must not assume it means a provider ceiling — a sweep sets it from three different\n\t * origins:\n\t * - a provider-side ceiling or per-scope backstop propagated from below (e.g. GitHub Search's 1000-result\n\t * cap): the missing items are NOT reachable, so this really is \"complete to the provider's limit\";\n\t * - the sweep's OWN `maxPages` cap (default 100) with a usable cursor still in hand: the missing items ARE\n\t * reachable and were simply not fetched — raise `maxPages` to get them;\n\t * - the provider reporting another page but handing back no usable cursor: not reachable.\n\t *\n\t * Only the second is recoverable, and a sweep exposes no continuation cursor, so treating a truncated sweep\n\t * as a complete set silently drops reachable data in that case. Prefer `allPages` for the cache decision.\n\t */\n\ttruncated?: boolean;\n}\n\nexport interface ProviderResult<T> {\n\titems: T[];\n\twarnings: ProviderWarning[];\n\t/**\n\t * True when `items` is incomplete — distinguishes a failed or truncated flat read from a genuinely empty\n\t * result. Paged results additionally use `page.truncated` when a completed page could not confirm it\n\t * drained everything.\n\t */\n\tfetchFailed?: boolean;\n}\n\nexport interface ProviderPagedResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Opaque cursor for cursor-based paging (GraphQL, per-repo/per-project cursors, etc.). For hosts that\n\t * require the previous page's cursor, consumers should pass this value back to the next `cursor` option.\n\t * A retry-only cursor may remain when `hasMore` is false: `hasMore` means automatic forward progress,\n\t * while explicitly reusing such a cursor retries failed work without creating an infinite paging loop.\n\t */\n\tcursor?: string;\n}\n\nexport interface ProviderSweepResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Providers whose top-level read failed before returning any usable page. Partial per-scope failures stay\n\t * represented by `warnings` + `fetchFailed` and are intentionally excluded.\n\t */\n\tfailedProviderIds: IntegrationIds[];\n\t/**\n\t * Providers whose read started successfully but could not produce an authoritative complete slice (for\n\t * example, a later page failed, collection metadata reported a partial scope, or a paging backstop was hit).\n\t * Kept separate from\n\t * `failedProviderIds` so consumers can accept healthy sibling providers while preserving an older snapshot\n\t * for a provider whose returned slice has a gap.\n\t */\n\tincompleteProviderIds: IntegrationIds[];\n}\n\nexport interface ProviderBroadenResult<T> extends ProviderPagedResult<T> {\n\tbroadenedProviderIds: IntegrationIds[];\n\t/** Providers for which no requested org produced a usable issue result. */\n\tfailedProviderIds: IntegrationIds[];\n\t/** Providers with both a usable org result and at least one failed or truncated org slice. */\n\tincompleteProviderIds: IntegrationIds[];\n\tfanOutCount: number;\n}\n\nexport type RepositoryResolutionStatus =\n\t| 'resolved'\n\t| 'not-found'\n\t| 'unauthorized'\n\t| 'unsupported-provider'\n\t| 'invalid-remote-url'\n\t| 'host-mismatch'\n\t| 'undetermined';\n\nexport interface RepositoryIdentity {\n\tproviderId: IntegrationIds;\n\tdomain: string;\n\t/** The provider's canonical owner/namespace (follows renames), falling back to the parsed remote when omitted. */\n\towner: string;\n\t/** The provider's canonical repo name (follows renames), falling back to the parsed remote when omitted. */\n\tname: string;\n\tproject?: string;\n\t/** The original input remote URL, so the caller can key the resolution back to the remote it asked about. */\n\tremoteUrl: string;\n\t/**\n\t * True when the provider's canonical owner/name differ from what the input remote URL carried — i.e. the\n\t * repo was renamed/moved host-side and the local remote is stale. Case-insensitive, mirroring gkcli's\n\t * `EqualFold` compare so hosts that echo the input casing (e.g. Bitbucket Server/Azure) aren't flagged.\n\t */\n\trenamed: boolean;\n}\n\nexport interface RepositoryResolution {\n\tstatus: RepositoryResolutionStatus;\n\tidentity?: RepositoryIdentity;\n\twarning?: ProviderWarning;\n}\n\nexport interface ResolveRepositoryResult {\n\tresolution: RepositoryResolution;\n}\n\nconst maxProviderWarningMessageLength = 500;\n\n/** A caught error's message as a warning can carry it: an HTML page becomes its status, and prose is capped. */\nexport function providerWarningMessage(ex: unknown): string {\n\tconst raw = (ex instanceof Error ? ex.message : String(ex)).trim();\n\tconst carrier = ex as { status?: unknown; response?: { status?: unknown } } | null | undefined;\n\tconst status =\n\t\ttypeof carrier?.status === 'number'\n\t\t\t? carrier.status\n\t\t\t: typeof carrier?.response?.status === 'number'\n\t\t\t\t? carrier.response.status\n\t\t\t\t: undefined;\n\n\t// Gateways commonly answer API requests with a complete HTML error page. That payload is neither actionable\n\t// nor suitable for a warning DTO consumed by UIs and logs; preserve the status without leaking the document.\n\tif (/<(?:!doctype\\s+html|html|head|body)\\b/i.test(raw)) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\n\tif (!raw) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\treturn raw.length > maxProviderWarningMessageLength\n\t\t? `${raw.slice(0, maxProviderWarningMessageLength - 3)}...`\n\t\t: raw;\n}\n\n/**\n * Normalized org/workspace/group shape returned by the provider facade.\n * `name` is the provider identifier to pass to follow-up reads (a Jira project's key), while `org` is an optional\n * display label of the parent. Where that label can't address the parent, `orgId` is the parent's stable id to pass\n * as `org` instead (a Jira site's id, or a Data Center instance's domain).\n *\n * Jira site rows (`listOrgs`) are the exception: their `name` is the site's label, and a Jira read selects a site only\n * by its `id` (or a project row's `orgId`).\n */\nexport interface ProviderOrganization {\n\tid: string;\n\tproviderId: IntegrationIds;\n\tname: string;\n\torg?: string;\n\torgId?: string;\n\turl: string;\n}\n\n/** Normalized repository shape returned by the provider facade. */\nexport interface ProviderRepositoryShape {\n\tid: string;\n\tnamespace: string;\n\tname: string;\n\t/** Azure DevOps project; `undefined` for hosts without a project layer. */\n\tproject?: string;\n\t/** Web (browser) URL, when the provider exposes it. */\n\turl?: string;\n\t/** HTTPS clone URL, when available. */\n\tcloneUrlHttps?: string;\n\t/** SSH clone URL, when available. */\n\tcloneUrlSsh?: string;\n\t/** Default branch name, when the provider reports it. */\n\tdefaultBranch?: string;\n}\n\n/**\n * Classifies a caught provider exception into a neutral {@link ProviderWarning}. Ordering matters:\n * `instanceof` is checked most-specific-first so a rate-limit isn't mislabeled as a generic error and\n * a 404 isn't mislabeled as auth.\n */\nexport function toProviderWarning(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tex: unknown,\n): ProviderWarning {\n\tlet kind: ProviderWarning['kind'];\n\tif (ex instanceof AuthenticationError) {\n\t\tkind = 'auth';\n\t} else if (ex instanceof RequestRateLimitError) {\n\t\tkind = 'rate-limit';\n\t} else if (ex instanceof RequestNotFoundError) {\n\t\tkind = 'not-found';\n\t} else if (isUnsupportedSortError(ex)) {\n\t\tkind = 'unsupported';\n\t} else {\n\t\tkind = 'other';\n\t}\n\n\treturn {\n\t\tproviderId: providerId,\n\t\tdomain: domain,\n\t\tconnectionId: connectionId,\n\t\tmessage: providerWarningMessage(ex),\n\t\tkind: kind,\n\t\tisAuth: kind === 'auth',\n\t\t...(kind === 'other' && isProviderUnreachableError(ex) ? { cause: { reason: 'unreachable' as const } } : {}),\n\t};\n}\n\n/**\n * A scope's identity, as the stable string that keys it.\n *\n * The single definition of what \"the same scope\" means, shared by the failure and omission dedup keys in\n * `providerPaging.ts` and by {@link providerWarningKey} below, so a scope gaining a field is one edit rather\n * than three. `providerId` is included: the same repository ID under two providers is two scopes.\n *\n * The parameter is structural rather than the SDK's `CollectionScope` so this module keeps naming no\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`) while still serving its SDK-facing\n * callers, which pass that type in unchanged.\n */\nexport function collectionScopeKey(scope: ProviderWarningOmissionScope | undefined): string {\n\treturn [scope?.providerId ?? '', scope?.resourceId ?? '', scope?.projectId ?? '', scope?.repositoryId ?? ''].join(\n\t\t' ',\n\t);\n}\n\n/**\n * The omission's contribution to a warning's identity — empty when there is none, so a warning without an\n * omission keeps deduping exactly as it did before the field existed.\n *\n * Two omissions of different kinds do produce different `message` values today, so message alone would still\n * separate them. That is incidental: the premise of `omission` is that consumers must not depend on prose\n * carrying the distinguishing fact, and this key must not either.\n */\nfunction providerWarningOmissionKey(omission: ProviderWarningOmission | undefined): string {\n\tif (omission == null) return '';\n\n\treturn [\n\t\tomission.kind,\n\t\tomission.recovery,\n\t\tomission.limit ?? '',\n\t\tomission.totalCount ?? '',\n\t\tcollectionScopeKey(omission.scope),\n\t].join(' ');\n}\n\n/**\n * Strips the omission from every warning in `warnings` when the read as a whole failed.\n *\n * An omission asserts the request SUCCEEDED, and a drain only learns it failed AFTER it may have emitted one:\n * an early page can report its own truncation and then a later page can die. Deciding per warning, at the\n * moment each is built, cannot see that future — so the aggregate is reconciled once, here, where\n * `fetchFailed` is final. Call it at the point a read returns its `fetchFailed`.\n *\n * Re-dedupes as it goes: the omission is part of a warning's identity, so two warnings that differed only\n * there become identical once it is gone, and the array's contract is that no two entries are equal.\n *\n * Mutates in place: the warning array is the one being returned, and callers accumulate into it across pages.\n */\nexport function reconcileOmissionsWithFailure(warnings: ProviderWarning[], fetchFailed: boolean): void {\n\tif (!fetchFailed || !warnings.some(w => w.omission != null)) return;\n\n\tconst reconciled: ProviderWarning[] = [];\n\tfor (const warning of warnings) {\n\t\tif (warning.omission == null) {\n\t\t\tappendDedupedWarning(reconciled, warning);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { omission: _omission, ...rest } = warning;\n\t\tappendDedupedWarning(reconciled, rest);\n\t}\n\n\twarnings.splice(0, warnings.length, ...reconciled);\n}\n\n/**\n * A stable key for deduplicating warnings accumulated across drained pages / fan-out scopes.\n *\n * The scope and the cause are keyed for the reason the omission is (see {@link providerWarningOmissionKey}): today's\n * failure messages happen to spell them out, but two failures that differ in either must stay two warnings even if\n * they don't.\n *\n * `message` stays LAST. It is the only free-form segment — provider prose, spaces and all — so anything\n * appended after it could be impersonated by a message that happens to end in the same text.\n */\nfunction providerWarningKey(warning: ProviderWarning): string {\n\treturn [\n\t\twarning.providerId,\n\t\twarning.connectionId ?? '',\n\t\twarning.domain ?? '',\n\t\twarning.kind,\n\t\tcollectionScopeKey(warning.scope),\n\t\twarning.cause?.reason ?? '',\n\t\tproviderWarningOmissionKey(warning.omission),\n\t\twarning.message,\n\t].join(' ');\n}\n\n/**\n * Appends `warning` to `into` only when an equal warning (by provider/connection/domain/kind/message, plus the\n * structured scope, cause and omission when present) is absent. An equal warning already there merges its\n * {@link ProviderWarning.targetKeys} with the new one's instead: the union of both, or none when either concerns\n * the whole call. The existing warning is replaced, never mutated, since it may be shared.\n */\nexport function appendDedupedWarning(into: ProviderWarning[], warning: ProviderWarning): void {\n\tconst key = providerWarningKey(warning);\n\tconst index = into.findIndex(existing => providerWarningKey(existing) === key);\n\tif (index === -1) {\n\t\tinto.push(warning);\n\t\treturn;\n\t}\n\n\tconst existing = into[index];\n\tconst existingKeys = existing.targetKeys;\n\t// Already covers the whole call.\n\tif (existingKeys == null) return;\n\n\tif (warning.targetKeys == null) {\n\t\tconst { targetKeys: _, ...wholeCall } = existing;\n\t\tinto[index] = wholeCall;\n\t\treturn;\n\t}\n\n\tconst added = warning.targetKeys.filter(k => !existingKeys.includes(k));\n\tif (!added.length) return;\n\n\tinto[index] = { ...existing, targetKeys: [...existingKeys, ...new Set(added)] };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"results.js","names":[],"sources":["../../../../plus/integrations/src/results.ts"],"sourcesContent":["import { isUnsupportedSortError } from '@gitkraken/provider-apis';\nimport { AuthenticationError, RequestNotFoundError, RequestRateLimitError } from '@gitlens/git/errors.js';\nimport type { IssueSorting } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSorting } from '@gitlens/git/models/pullRequest.js';\nimport type { IntegrationIds } from './constants.js';\nimport { isProviderUnreachableError } from './errors.js';\n\nexport interface ConnectionStateChangeEvent {\n\tkey: string;\n\treason: 'connected' | 'disconnected';\n}\n\n/**\n * A per-provider, per-connection warning surfaced alongside (partial) read results. Consumers use\n * these to drive auth recovery, retry, or truncation messaging without the read itself throwing.\n *\n * `kind` is the programmatic discriminant (a rate-limit is retryable, a 404 is not, an unsupported capability\n * never succeeds on retry, an auth failure needs re-connection unless {@link ProviderWarning.scope} confines\n * it); `isAuth` is retained as a convenience mirror of `kind === 'auth'`.\n *\n * `'unsupported'`: the provider — or this integration of it — lacks the capability the request asked for: a\n * surface (issues on Bitbucket, pull requests on an issue tracker, a batch read, org/repo discovery,\n * current-account lookup, project-scoped or account-wide issue reads), or a filter, sort, state, search\n * criterion or option combination it cannot express. Retrying the same request never succeeds; a different\n * provider, scope or request may. NOT unsupported (these stay `'other'`): malformed or contradictory caller\n * input the caller fixes in the request itself (a duplicate batch key, invalid target numbers, an empty owner,\n * an unusable scope), incompleteness or omission warnings, and unclassified errors.\n *\n * `'unreadable-items'`: the provider returned items this read could not read (a Jira issue whose fields the SDK cannot\n * parse), so it left them out and read everything else in the warning's {@link ProviderWarning.scope}. It still fails\n * the read (`fetchFailed`), since those items are missing, but retrying returns the same gap. On the reads that publish\n * it (an issue tracker's project reads) it never hides a read that was cut short: a cut always comes with a warning of\n * another kind. {@link ProviderWarning.unreadableItems} names the items left out.\n */\nexport type ProviderWarningKind =\n\t| 'auth'\n\t| 'rate-limit'\n\t| 'not-found'\n\t| 'no-connection'\n\t| 'unsupported'\n\t| 'unreadable-items'\n\t| 'other';\n\n/** The items an `'unreadable-items'` warning left out: see {@link ProviderWarning.unreadableItems}. */\nexport interface ProviderWarningUnreadableItems {\n\t/**\n\t * The provider's identifiers of the items left out (a Jira issue's key), without duplicates, in reported order. Read\n\t * from the provider's prose, it may also hold an identifier that only appeared in an item's error text, never lack\n\t * one it named: rely on an identifier's absence, not on its presence.\n\t */\n\tkeys: string[];\n\t/**\n\t * Whether {@link keys} names every item left out. `false` when the provider named only some of them (Jira names at\n\t * most ten per page), named one by position rather than by key, or reported them in a form this layer could not\n\t * read, so an identifier absent from `keys` may still be one of them.\n\t */\n\tcomplete: boolean;\n}\n\n/**\n * Why a read that SUCCEEDED still withheld results:\n * - `provider-limit`: the provider refuses to serve past a cap it enforces (GitHub search's 1,000-result\n * ceiling, Trello's `cards_limit`), so the excess is unreachable through that query.\n * - `recovery-budget`: the internal partitioned recovery stopped spending upstream requests before it ran\n * out of partitions to visit.\n * - `pagination-incomplete`: pages were left unread — a sub-scope of a multi-scope read that was not drained,\n * a drain that stopped at its own page backstop, or a provider that advertised another page and gave no way\n * to reach it. These have DIFFERENT remedies, so the kind alone does not say whether more can be fetched;\n * read {@link ProviderWarningOmission.recovery} for that.\n *\n * Mirrors the vocabulary the SDK reports. Declared here rather than imported so this module stays free of\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`); `collectionMetadata.ts` holds the\n * compile-time link, so an SDK bump that adds a kind fails the build at that boundary.\n */\nexport type ProviderWarningOmissionKind = 'provider-limit' | 'recovery-budget' | 'pagination-incomplete';\n\n/**\n * How a consumer can recover omitted results. `page-budget` reruns with a larger budget; `narrow-scope`\n * requests a smaller server-side scope; `none` promises neither. Switch on the exact value because only\n * `page-budget` is a fetch-more action.\n */\nexport type ProviderWarningOmissionRecovery = 'none' | 'page-budget' | 'narrow-scope';\n\n/**\n * The part of a provider a warning is confined to: one resource (organization, workspace, site), project or\n * repository. All fields optional; see {@link ProviderWarning.scope} for when one is forwarded.\n */\nexport interface ProviderWarningScope {\n\t/**\n\t * The resource as the read addressed it: its id on most reads, its name on the few that address it by name\n\t * (Azure DevOps' repo-scoped reads, Bitbucket's workspace reads). Match it against both\n\t * `ProviderOrganization.id` and `name`, and a project's `orgId` where it has one.\n\t */\n\tresourceId?: string;\n\t/**\n\t * The project as the read addressed it. For a Jira project read, the key on Jira Cloud (the project's `name` in\n\t * `listProjects`) and the numeric id on Jira Data Center.\n\t */\n\tprojectId?: string;\n\trepositoryId?: string;\n}\n\n/**\n * Why a provider request failed, when the failure itself says so:\n * - `unreachable`: a transport failure or server-side error prevented the provider from answering. The connection\n * still exists; this accompanies `kind: 'other'`, never an authentication warning.\n * - `oauth-app-not-allowed`: the organization does not let third-party OAuth apps in. On Azure DevOps this is the\n * organization policy \"Third-party application access via OAuth\", off by default for new organizations. An\n * organization admin enables it, or the user connects with a personal access token instead, which the policy\n * does not govern.\n * - `access-denied`: the account has no access to that organization or project (not a member, or lacking the\n * permission). Someone who administers it has to grant access.\n * - `conditional-access`: a Microsoft Entra Conditional Access policy blocked the request (Azure DevOps\n * `VS403463`). Its admin has to exempt the request, e.g. by location or device.\n *\n * None of these is healed by reconnecting, which is the point of naming them.\n */\nexport type ProviderWarningCauseReason =\n\t| 'oauth-app-not-allowed'\n\t| 'access-denied'\n\t| 'conditional-access'\n\t| 'unreachable';\n\n/** See {@link ProviderWarning.cause}. */\nexport interface ProviderWarningCause {\n\treason: ProviderWarningCauseReason;\n\t/** The provider's own error code, when it reports one (Azure DevOps `TF400813`, `VS403463`). */\n\tcode?: string;\n\t/** Where the setting behind the refusal is changed, when this layer can address it. */\n\tremedyUrl?: string;\n}\n\n/** Which repository / project / resource an omission is attributed to. All fields optional; a scope may name none. */\nexport interface ProviderWarningOmissionScope extends ProviderWarningScope {\n\tproviderId?: string;\n}\n\nexport interface ProviderWarningOmission {\n\tkind: ProviderWarningOmissionKind;\n\t/**\n\t * Whether anything would fetch the missing items, and what. Gate a \"load more\" affordance on this rather\n\t * than on `kind` — see {@link ProviderWarningOmissionRecovery}.\n\t */\n\trecovery: ProviderWarningOmissionRecovery;\n\t/**\n\t * The cap the provider enforces, when it reports one. NOT a result count for every kind: on\n\t * `recovery-budget` this is a REQUEST budget and must not be shown to a user as a number of results.\n\t */\n\tlimit?: number;\n\t/** Total matches the provider reported, when it reports one. Only GitHub's search cap does today. */\n\ttotalCount?: number;\n\t/**\n\t * The order the reachable window was selected under, as `field:direction`, when the omission is a result\n\t * ceiling.\n\t *\n\t * Present so a consumer can word its own \"N matched, showing the first M\" without assuming WHICH M: the\n\t * provider serves the top M under this key, so the sentence is only true if it names it. Absent for every\n\t * omission that isn't a ceiling, where no window was selected by an order.\n\t *\n\t * A union of both vocabularies rather than the issue one alone: `PullRequestSorting` happens to be a subset\n\t * today, so narrowing this to {@link IssueSorting} would still compile while describing only half the reads\n\t * that set it — and would break the day the PR search gains a key the issue search has no name for.\n\t */\n\tsort?: IssueSorting | PullRequestSorting;\n\t/** Which repository / project / resource was affected, when one is attributed. */\n\tscope?: ProviderWarningOmissionScope;\n}\n\nexport interface ProviderWarning {\n\tproviderId: IntegrationIds;\n\t/** Disambiguates connections on self-managed hosts (where one provider id spans multiple domains). */\n\tdomain?: string;\n\t/** The specific token/connection the read was pinned to, when a `connectionId` was supplied. */\n\tconnectionId?: string;\n\tmessage: string;\n\tkind: ProviderWarningKind;\n\t/** Convenience mirror of `kind === 'auth'`, scoped or not; read {@link scope} before prompting to reconnect. */\n\tisAuth: boolean;\n\t/**\n\t * Present when the failure was recorded against one sub-scope of the read (an organization, project or\n\t * repository) rather than against the connection. A scoped `auth` failure is that one scope refusing this\n\t * credential, e.g. an Azure DevOps organization that disallows third-party OAuth apps, one in another Entra\n\t * tenant, or a Conditional Access policy. Reconnecting cannot fix those, so this warning alone is no reason\n\t * to prompt for it.\n\t *\n\t * `kind` and `isAuth` are unaffected: a scoped 401 is still an authentication failure, and this field says\n\t * how far it reaches. It names at least one of its IDs, and is never set on an omission (which carries its\n\t * own {@link ProviderWarningOmission.scope}) or on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}), even when that call targeted a single organization. The exception is a batch\n\t * read's refused target, which names its scope: the Azure DevOps organization and project, the repository on\n\t * other git hosts, or the tracker's resource.\n\t *\n\t * Its ABSENCE means account-wide or unattributed, so existing handling stays correct without it.\n\t *\n\t * A scoped `auth` failure also means the credential itself was accepted. A dead token can come back as nothing\n\t * but scoped refusals wherever a read reaches its scopes without an uncached request to the connection first:\n\t * discovery served from a per-token cache (Azure DevOps, Bitbucket, Jira Cloud) or an SDK fan-out across the\n\t * requested repositories (Bitbucket Data Center). So when a read's only auth failures are scoped, those providers\n\t * confirm the credential with one uncached check first, and a refused credential fails the whole read with an\n\t * unscoped `auth` warning. A batch read checks it only when every target was refused, since a target that\n\t * answered already proved it. A refusal the provider attributes to the credential itself, like Bitbucket's or Jira\n\t * Cloud's for a token missing the OAuth scopes a read needs, which a reconnect fixes, is published unscoped too,\n\t * once for the connection, while the scopes that answered keep their results.\n\t *\n\t * So the promise holds for a read that carries no unscoped `auth` warning of its own; one that does already asks\n\t * for a reconnect, and its other scoped refusals are not checked. Two exceptions stay scoped even then. A check\n\t * that could not complete or was denied (a network error, a throttle, a `403`, Bitbucket Data Center refusing a\n\t * credential it authenticated, or a Bitbucket Data Center project access token, whose user the check cannot\n\t * find) proves nothing, so the warnings are published unconfirmed, with no {@link cause}, and the next read\n\t * checks again. And a credential confirmed within the last minute is not probed again, so a revocation can take\n\t * up to a minute to surface.\n\t */\n\tscope?: ProviderWarningScope;\n\t/**\n\t * Why the provider request failed, when the failure itself says so: see {@link ProviderWarningCauseReason}. Switch on\n\t * `reason` to recommend a fix instead of a reconnect; `message` then carries the same explanation as prose.\n\t *\n\t * `unreachable` accompanies a transport or server failure with `kind: 'other'`. Credential refusal causes are\n\t * set only on a scoped `auth` warning whose credential was confirmed first (see {@link scope}), because the\n\t * refusals they name are indistinguishable from a dead credential until then: Azure DevOps answers a\n\t * third-party OAuth app its organization disallows exactly as it answers an expired token. Only Azure DevOps\n\t * reports one today. Its absence proves nothing about the cause.\n\t */\n\tcause?: ProviderWarningCause;\n\t/**\n\t * Present when this warning describes results the read could not return even though the request itself\n\t * SUCCEEDED — a provider-enforced cap, an exhausted recovery budget, a page budget, or a sub-scope the read\n\t * did not drain.\n\t *\n\t * Its presence is the signal: an omission is not a failure. Message it as incompleteness rather than as a\n\t * failed read, and do NOT derive that from `message`, which is English prose and subject to rewording.\n\t * `kind` stays `'other'` for these, so the failure discriminant keeps meaning exactly what it meant before\n\t * this field existed.\n\t *\n\t * Whether anything would fetch the rest is a SEPARATE question — read {@link ProviderWarningOmission.recovery},\n\t * not `kind`, and never assume a retry of the same request returns more.\n\t *\n\t * `limit` / `totalCount` / `scope` are forwarded only when they are reported; most omissions carry none of\n\t * the three, so render correctly without them — and see {@link ProviderWarningOmission.limit} before\n\t * showing that figure as a result count.\n\t *\n\t * Its ABSENCE proves nothing. It is never set on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}) or from a structured scope failure — those are failures, and the field would\n\t * be a lie there. But it is also absent whenever incompleteness was reported without naming what was left\n\t * out, so treat a bare `kind: 'other'` warning as unclassified rather than as a proven failure.\n\t */\n\tomission?: ProviderWarningOmission;\n\t/**\n\t * Present exactly when {@link kind} is `'unreadable-items'`: which items the read left out because it could not read\n\t * them. A consumer deciding whether an identifier is absent from the provider, rather than merely unread, must find\n\t * it in no warning's `keys` and see `complete` on every one.\n\t */\n\tunreadableItems?: ProviderWarningUnreadableItems;\n\t/**\n\t * The keys of the batch targets this warning concerns: the target a per-target failure was refused or failed\n\t * for, or every target of an integration call that failed as a whole. So a caller that sends several callers'\n\t * targets in one call can hand a failure only to the targets it belongs to.\n\t *\n\t * Its ABSENCE means the whole call. It is absent on every read that is not a batch read, and on a batch read's\n\t * refusal of the whole call before any target is read (a duplicate key, an invalid target, an unknown etag\n\t * include, no resolvable connection, the wrong surface), which concerns every target alike.\n\t *\n\t * It is not part of a warning's identity: two warnings that differ only here are one warning, for the union of\n\t * their targets (see {@link appendDedupedWarning}).\n\t */\n\ttargetKeys?: string[];\n}\n\nexport interface ProviderPageInfo {\n\t/**\n\t * The 1-based POSITION of the returned page. One convention across every paged read, page-number and\n\t * cursor paging alike, so a `getNextPageParam`-style consumer can key off it uniformly:\n\t * - the page number the provider itself reported, when it reports one (authoritative);\n\t * - else the position the caller addressed: the page encoded in a threaded page cursor, or the `page`\n\t * supplied alongside an opaque cursor (1 when the caller supplied no `page`);\n\t * - else, for a read that can be advanced by page number, the requested `page` — the provider either\n\t * honored it or the internal drain reached it (see the paging contract on `IntegrationManager`);\n\t * - else 1: a cursor-only read with no cursor threaded back ignores a requested page number and answers\n\t * with its first page, so echoing the request would mislabel page 1 as page N.\n\t *\n\t * A page past the provider's terminal cursor still reports the requested `page` with empty `items` — an\n\t * empty page N, never page N-1 relabeled.\n\t *\n\t * Continue a cursor-paged read from `cursor`, NOT from `currentPage + 1`: a cursor-only host can't be\n\t * addressed by number, so this is a position label and not always a resumable request parameter.\n\t */\n\tcurrentPage: number;\n\t/**\n\t * The page size IN EFFECT, which is not always the one requested — it is whichever of these the read can\n\t * actually vouch for:\n\t * - the size the provider reported for the page it served (numbered-page hosts), else\n\t * - the `itemsPerPage` that was applied to the request (repo-scoped reads, which forward it), else\n\t * - `items.length`, for a read that takes no page size at all: the account-wide PR/issue reads, `listRepos`\n\t * (cursor-only), and `listIssueTrackerIssuesPage`, whose `itemsPerPage` counts PROJECTS rather than issues.\n\t *\n\t * So it is a description of the page, not an echo of the request: don't derive a total from\n\t * `currentPage * itemsPerPage`, and don't read `items.length < itemsPerPage` as \"last page\" — `hasMore` /\n\t * `cursor` are the only continuation signals.\n\t */\n\titemsPerPage: number;\n\t/**\n\t * A COMPLETENESS assertion, not a mode flag: `true` only when a sweep drained every page of every target\n\t * without truncating or failing (`!truncated && !fetchFailed`). Set exclusively by `sweepPullRequests` /\n\t * `sweepClosedPullRequests` and absent on every paged read, so `allPages !== true` does NOT by itself mean\n\t * \"this was a single page\".\n\t *\n\t * This is the signal to gate on before caching a sweep as authoritative: unlike `truncated` it is `false`\n\t * for BOTH kinds of incompleteness, so it can't be misread as a benign cap.\n\t */\n\tallPages?: boolean;\n\t/**\n\t * True when the read stopped before it could confirm it had everything. Deliberately does NOT distinguish\n\t * WHY, and a consumer must not assume it means a provider ceiling — a sweep sets it from three different\n\t * origins:\n\t * - a provider-side ceiling or per-scope backstop propagated from below (e.g. GitHub Search's 1000-result\n\t * cap): the missing items are NOT reachable, so this really is \"complete to the provider's limit\";\n\t * - the sweep's OWN `maxPages` cap (default 100) with a usable cursor still in hand: the missing items ARE\n\t * reachable and were simply not fetched — raise `maxPages` to get them;\n\t * - the provider reporting another page but handing back no usable cursor: not reachable.\n\t *\n\t * Only the second is recoverable, and a sweep exposes no continuation cursor, so treating a truncated sweep\n\t * as a complete set silently drops reachable data in that case. Prefer `allPages` for the cache decision.\n\t */\n\ttruncated?: boolean;\n}\n\nexport interface ProviderResult<T> {\n\titems: T[];\n\twarnings: ProviderWarning[];\n\t/**\n\t * True when `items` is incomplete — distinguishes a failed or truncated flat read from a genuinely empty\n\t * result. Paged results additionally use `page.truncated` when a completed page could not confirm it\n\t * drained everything.\n\t */\n\tfetchFailed?: boolean;\n}\n\nexport interface ProviderPagedResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Opaque cursor for cursor-based paging (GraphQL, per-repo/per-project cursors, etc.). For hosts that\n\t * require the previous page's cursor, consumers should pass this value back to the next `cursor` option.\n\t * A retry-only cursor may remain when `hasMore` is false: `hasMore` means automatic forward progress,\n\t * while explicitly reusing such a cursor retries failed work without creating an infinite paging loop.\n\t */\n\tcursor?: string;\n}\n\nexport interface ProviderSweepResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Providers whose top-level read failed before returning any usable page. Partial per-scope failures stay\n\t * represented by `warnings` + `fetchFailed` and are intentionally excluded.\n\t */\n\tfailedProviderIds: IntegrationIds[];\n\t/**\n\t * Providers whose read started successfully but could not produce an authoritative complete slice (for\n\t * example, a later page failed, collection metadata reported a partial scope, or a paging backstop was hit).\n\t * Kept separate from\n\t * `failedProviderIds` so consumers can accept healthy sibling providers while preserving an older snapshot\n\t * for a provider whose returned slice has a gap.\n\t */\n\tincompleteProviderIds: IntegrationIds[];\n}\n\nexport interface ProviderBroadenResult<T> extends ProviderPagedResult<T> {\n\tbroadenedProviderIds: IntegrationIds[];\n\t/** Providers for which no requested org produced a usable issue result. */\n\tfailedProviderIds: IntegrationIds[];\n\t/** Providers with both a usable org result and at least one failed or truncated org slice. */\n\tincompleteProviderIds: IntegrationIds[];\n\tfanOutCount: number;\n}\n\nexport type RepositoryResolutionStatus =\n\t| 'resolved'\n\t| 'not-found'\n\t| 'unauthorized'\n\t| 'unsupported-provider'\n\t| 'invalid-remote-url'\n\t| 'host-mismatch'\n\t| 'undetermined';\n\nexport interface RepositoryIdentity {\n\tproviderId: IntegrationIds;\n\tdomain: string;\n\t/** The provider's canonical owner/namespace (follows renames), falling back to the parsed remote when omitted. */\n\towner: string;\n\t/** The provider's canonical repo name (follows renames), falling back to the parsed remote when omitted. */\n\tname: string;\n\tproject?: string;\n\t/** The original input remote URL, so the caller can key the resolution back to the remote it asked about. */\n\tremoteUrl: string;\n\t/**\n\t * True when the provider's canonical owner/name differ from what the input remote URL carried — i.e. the\n\t * repo was renamed/moved host-side and the local remote is stale. Case-insensitive, mirroring gkcli's\n\t * `EqualFold` compare so hosts that echo the input casing (e.g. Bitbucket Server/Azure) aren't flagged.\n\t */\n\trenamed: boolean;\n}\n\nexport interface RepositoryResolution {\n\tstatus: RepositoryResolutionStatus;\n\tidentity?: RepositoryIdentity;\n\twarning?: ProviderWarning;\n}\n\nexport interface ResolveRepositoryResult {\n\tresolution: RepositoryResolution;\n}\n\nconst maxProviderWarningMessageLength = 500;\n\n/** A caught error's message as a warning can carry it: an HTML page becomes its status, and prose is capped. */\nexport function providerWarningMessage(ex: unknown): string {\n\tconst raw = (ex instanceof Error ? ex.message : String(ex)).trim();\n\tconst carrier = ex as { status?: unknown; response?: { status?: unknown } } | null | undefined;\n\tconst status =\n\t\ttypeof carrier?.status === 'number'\n\t\t\t? carrier.status\n\t\t\t: typeof carrier?.response?.status === 'number'\n\t\t\t\t? carrier.response.status\n\t\t\t\t: undefined;\n\n\t// Gateways commonly answer API requests with a complete HTML error page. That payload is neither actionable\n\t// nor suitable for a warning DTO consumed by UIs and logs; preserve the status without leaking the document.\n\tif (/<(?:!doctype\\s+html|html|head|body)\\b/i.test(raw)) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\n\tif (!raw) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\treturn raw.length > maxProviderWarningMessageLength\n\t\t? `${raw.slice(0, maxProviderWarningMessageLength - 3)}...`\n\t\t: raw;\n}\n\n/**\n * Normalized org/workspace/group shape returned by the provider facade.\n * `name` is the provider identifier to pass to follow-up reads (a Jira project's key), while `org` is an optional\n * display label of the parent. Where that label can't address the parent, `orgId` is the parent's stable id to pass\n * as `org` instead (a Jira site's id, or a Data Center instance's domain).\n *\n * Jira site rows (`listOrgs`) are the exception: their `name` is the site's label, and a Jira read selects a site only\n * by its `id` (or a project row's `orgId`).\n */\nexport interface ProviderOrganization {\n\tid: string;\n\tproviderId: IntegrationIds;\n\tname: string;\n\torg?: string;\n\torgId?: string;\n\turl: string;\n}\n\n/** Normalized repository shape returned by the provider facade. */\nexport interface ProviderRepositoryShape {\n\tid: string;\n\tnamespace: string;\n\tname: string;\n\t/** Azure DevOps project; `undefined` for hosts without a project layer. */\n\tproject?: string;\n\t/** Web (browser) URL, when the provider exposes it. */\n\turl?: string;\n\t/** HTTPS clone URL, when available. */\n\tcloneUrlHttps?: string;\n\t/** SSH clone URL, when available. */\n\tcloneUrlSsh?: string;\n\t/** Default branch name, when the provider reports it. */\n\tdefaultBranch?: string;\n}\n\n/**\n * Classifies a caught provider exception into a neutral {@link ProviderWarning}. Ordering matters:\n * `instanceof` is checked most-specific-first so a rate-limit isn't mislabeled as a generic error and\n * a 404 isn't mislabeled as auth.\n */\nexport function toProviderWarning(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tex: unknown,\n): ProviderWarning {\n\tlet kind: ProviderWarning['kind'];\n\tif (ex instanceof AuthenticationError) {\n\t\tkind = 'auth';\n\t} else if (ex instanceof RequestRateLimitError) {\n\t\tkind = 'rate-limit';\n\t} else if (ex instanceof RequestNotFoundError) {\n\t\tkind = 'not-found';\n\t} else if (isUnsupportedSortError(ex)) {\n\t\tkind = 'unsupported';\n\t} else {\n\t\tkind = 'other';\n\t}\n\n\treturn {\n\t\tproviderId: providerId,\n\t\tdomain: domain,\n\t\tconnectionId: connectionId,\n\t\tmessage: providerWarningMessage(ex),\n\t\tkind: kind,\n\t\tisAuth: kind === 'auth',\n\t\t...(kind === 'other' && isProviderUnreachableError(ex) ? { cause: { reason: 'unreachable' as const } } : {}),\n\t};\n}\n\n/**\n * A scope's identity, as the stable string that keys it.\n *\n * The single definition of what \"the same scope\" means, shared by the failure and omission dedup keys in\n * `providerPaging.ts` and by {@link providerWarningKey} below, so a scope gaining a field is one edit rather\n * than three. `providerId` is included: the same repository ID under two providers is two scopes.\n *\n * The parameter is structural rather than the SDK's `CollectionScope` so this module keeps naming no\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`) while still serving its SDK-facing\n * callers, which pass that type in unchanged.\n */\nexport function collectionScopeKey(scope: ProviderWarningOmissionScope | undefined): string {\n\treturn [scope?.providerId ?? '', scope?.resourceId ?? '', scope?.projectId ?? '', scope?.repositoryId ?? ''].join(\n\t\t' ',\n\t);\n}\n\n/**\n * The omission's contribution to a warning's identity — empty when there is none, so a warning without an\n * omission keeps deduping exactly as it did before the field existed.\n *\n * Two omissions of different kinds do produce different `message` values today, so message alone would still\n * separate them. That is incidental: the premise of `omission` is that consumers must not depend on prose\n * carrying the distinguishing fact, and this key must not either.\n */\nfunction providerWarningOmissionKey(omission: ProviderWarningOmission | undefined): string {\n\tif (omission == null) return '';\n\n\treturn [\n\t\tomission.kind,\n\t\tomission.recovery,\n\t\tomission.limit ?? '',\n\t\tomission.totalCount ?? '',\n\t\tcollectionScopeKey(omission.scope),\n\t].join(' ');\n}\n\n/**\n * Strips the omission from every warning in `warnings` when the read as a whole failed.\n *\n * An omission asserts the request SUCCEEDED, and a drain only learns it failed AFTER it may have emitted one:\n * an early page can report its own truncation and then a later page can die. Deciding per warning, at the\n * moment each is built, cannot see that future — so the aggregate is reconciled once, here, where\n * `fetchFailed` is final. Call it at the point a read returns its `fetchFailed`.\n *\n * Re-dedupes as it goes: the omission is part of a warning's identity, so two warnings that differed only\n * there become identical once it is gone, and the array's contract is that no two entries are equal.\n *\n * Mutates in place: the warning array is the one being returned, and callers accumulate into it across pages.\n */\nexport function reconcileOmissionsWithFailure(warnings: ProviderWarning[], fetchFailed: boolean): void {\n\tif (!fetchFailed || !warnings.some(w => w.omission != null)) return;\n\n\tconst reconciled: ProviderWarning[] = [];\n\tfor (const warning of warnings) {\n\t\tif (warning.omission == null) {\n\t\t\tappendDedupedWarning(reconciled, warning);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { omission: _omission, ...rest } = warning;\n\t\tappendDedupedWarning(reconciled, rest);\n\t}\n\n\twarnings.splice(0, warnings.length, ...reconciled);\n}\n\n/**\n * A stable key for deduplicating warnings accumulated across drained pages / fan-out scopes.\n *\n * The scope and the cause are keyed for the reason the omission is (see {@link providerWarningOmissionKey}): today's\n * failure messages happen to spell them out, but two failures that differ in either must stay two warnings even if\n * they don't.\n *\n * `message` stays LAST. It is the only free-form segment — provider prose, spaces and all — so anything\n * appended after it could be impersonated by a message that happens to end in the same text.\n */\nfunction providerWarningKey(warning: ProviderWarning): string {\n\treturn [\n\t\twarning.providerId,\n\t\twarning.connectionId ?? '',\n\t\twarning.domain ?? '',\n\t\twarning.kind,\n\t\tcollectionScopeKey(warning.scope),\n\t\twarning.cause?.reason ?? '',\n\t\tproviderWarningOmissionKey(warning.omission),\n\t\twarning.message,\n\t].join(' ');\n}\n\n/**\n * Appends `warning` to `into` only when an equal warning (by provider/connection/domain/kind/message, plus the\n * structured scope, cause and omission when present) is absent. An equal warning already there merges its\n * {@link ProviderWarning.targetKeys} with the new one's instead: the union of both, or none when either concerns\n * the whole call. Its {@link ProviderWarning.unreadableItems} merge too (see {@link mergeUnreadableItems}): two reads\n * can report the same text for different items. The existing warning is replaced, never mutated, since it may be\n * shared.\n */\nexport function appendDedupedWarning(into: ProviderWarning[], warning: ProviderWarning): void {\n\tconst key = providerWarningKey(warning);\n\tconst index = into.findIndex(existing => providerWarningKey(existing) === key);\n\tif (index === -1) {\n\t\tinto.push(warning);\n\t\treturn;\n\t}\n\n\tconst existing = into[index];\n\tconst targetKeys = mergeTargetKeys(existing.targetKeys, warning.targetKeys);\n\tconst unreadableItems = mergeUnreadableItems(existing.unreadableItems, warning.unreadableItems);\n\tif (targetKeys === existing.targetKeys && unreadableItems === existing.unreadableItems) return;\n\n\tconst { targetKeys: _, ...rest } = existing;\n\tinto[index] = {\n\t\t...rest,\n\t\t...(targetKeys != null ? { targetKeys: targetKeys } : {}),\n\t\t...(unreadableItems != null ? { unreadableItems: unreadableItems } : {}),\n\t};\n}\n\n/** The union of two warnings' target keys, or none when either concerns the whole call. `existing` when unchanged. */\nfunction mergeTargetKeys(existing: string[] | undefined, next: string[] | undefined): string[] | undefined {\n\t// Either concerns the whole call.\n\tif (existing == null || next == null) return undefined;\n\n\tconst added = next.filter(k => !existing.includes(k));\n\tif (!added.length) return existing;\n\n\treturn [...existing, ...new Set(added)];\n}\n\n/**\n * The items two reports of unreadable items left out together: the union of their keys, complete only when both are.\n * `existing` when `next` adds nothing to it.\n */\nexport function mergeUnreadableItems(\n\texisting: ProviderWarningUnreadableItems | undefined,\n\tnext: ProviderWarningUnreadableItems | undefined,\n): ProviderWarningUnreadableItems | undefined {\n\tif (existing == null) return next;\n\tif (next == null) return existing;\n\n\tconst added = next.keys.filter(k => !existing.keys.includes(k));\n\tconst complete = existing.complete && next.complete;\n\tif (!added.length && complete === existing.complete) return existing;\n\n\treturn { keys: [...existing.keys, ...new Set(added)], complete: complete };\n}\n"],"mappings":"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"}
|
|
@@ -130,6 +130,8 @@ export declare class PromiseMap<K, V> {
|
|
|
130
130
|
private readonly cache;
|
|
131
131
|
private readonly aborts;
|
|
132
132
|
private readonly controllers;
|
|
133
|
+
/** Each cached promise's value once it fulfills, for {@link getSettled} */
|
|
134
|
+
private readonly settled;
|
|
133
135
|
get [Symbol.toStringTag](): string;
|
|
134
136
|
/**
|
|
135
137
|
* Gets a promise from the cache or creates a new one using the factory function.
|
|
@@ -165,6 +167,14 @@ export declare class PromiseMap<K, V> {
|
|
|
165
167
|
* Sets a promise in the cache. The promise will be automatically removed if it fails.
|
|
166
168
|
*/
|
|
167
169
|
set(key: K, promise: Promise<V>): this;
|
|
170
|
+
/**
|
|
171
|
+
* The value `key`'s cached promise fulfilled with, or `undefined` while it is pending (or nothing is cached), so a
|
|
172
|
+
* caller can tell a settled entry from a read still in flight without joining it.
|
|
173
|
+
*/
|
|
174
|
+
getSettled(key: K): {
|
|
175
|
+
value: V;
|
|
176
|
+
} | undefined;
|
|
177
|
+
private track;
|
|
168
178
|
/**
|
|
169
179
|
* Checks if a key exists in the cache.
|
|
170
180
|
*/
|
|
@@ -247,6 +247,8 @@ var PromiseMap = class {
|
|
|
247
247
|
cache = /* @__PURE__ */ new Map();
|
|
248
248
|
aborts = /* @__PURE__ */ new Map();
|
|
249
249
|
controllers = /* @__PURE__ */ new Map();
|
|
250
|
+
/** Each cached promise's value once it fulfills, for {@link getSettled} */
|
|
251
|
+
settled = /* @__PURE__ */ new WeakMap();
|
|
250
252
|
get [Symbol.toStringTag]() {
|
|
251
253
|
return "PromiseMap";
|
|
252
254
|
}
|
|
@@ -279,7 +281,7 @@ var PromiseMap = class {
|
|
|
279
281
|
this.aborts.set(key, aborts);
|
|
280
282
|
this.controllers.set(key, cacheable);
|
|
281
283
|
const promise = factory(cacheable, aborts.signal);
|
|
282
|
-
|
|
284
|
+
this.track(key, promise);
|
|
283
285
|
promise.finally(() => {
|
|
284
286
|
if (cacheable.invalidated) deleteIfOwned(this.cache, key, promise);
|
|
285
287
|
aborts.dispose();
|
|
@@ -319,10 +321,23 @@ var PromiseMap = class {
|
|
|
319
321
|
*/
|
|
320
322
|
set(key, promise) {
|
|
321
323
|
this.cache.set(key, promise);
|
|
322
|
-
|
|
324
|
+
this.track(key, promise);
|
|
323
325
|
return this;
|
|
324
326
|
}
|
|
325
327
|
/**
|
|
328
|
+
* The value `key`'s cached promise fulfilled with, or `undefined` while it is pending (or nothing is cached), so a
|
|
329
|
+
* caller can tell a settled entry from a read still in flight without joining it.
|
|
330
|
+
*/
|
|
331
|
+
getSettled(key) {
|
|
332
|
+
const promise = this.cache.get(key);
|
|
333
|
+
return promise != null ? this.settled.get(promise) : void 0;
|
|
334
|
+
}
|
|
335
|
+
track(key, promise) {
|
|
336
|
+
promise.then((value) => {
|
|
337
|
+
this.settled.set(promise, { value });
|
|
338
|
+
}, () => deleteIfOwned(this.cache, key, promise));
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
326
341
|
* Checks if a key exists in the cache.
|
|
327
342
|
*/
|
|
328
343
|
has(key) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"promiseCache.js","names":[],"sources":["../../../utils/src/promiseCache.ts"],"sourcesContent":["import { AbortAggregate, CancellationError, raceWithSignal } from './cancellation.js';\n\n/** Deletes `key` from `map` only if it still maps to `owner`. A `delete(key)` + fresh `getOrCreate` can install\n * a successor under the same key while our factory is in flight; our late settle must not evict the successor. */\nfunction deleteIfOwned<K, V>(map: Map<K, V>, key: K, owner: V): void {\n\tif (map.get(key) === owner) {\n\t\tmap.delete(key);\n\t}\n}\n\ninterface CacheEntry<V> {\n\tpromise: Promise<V>;\n\taccessed: number;\n\tcreated: number;\n\n\tcreateTTL: number | undefined;\n\taccessTTL: number | undefined;\n}\n\nexport class CacheController {\n\t#invalidated = false;\n\tget invalidated(): boolean {\n\t\treturn this.#invalidated;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.#invalidated = true;\n\t}\n}\n\ninterface PromiseCacheOptions {\n\t/** TTL (time-to-live) in milliseconds since creation */\n\tcreateTTL?: number;\n\t/** TTL (time-to-live) in milliseconds since last access */\n\taccessTTL?: number;\n\t/** Whether to expire the entry if the promise fails (default: true) */\n\texpireOnError?: boolean;\n\t/**\n\t * TTL (time-to-live) in milliseconds for caching errors. When the factory rejects and `errorTTL` is set,\n\t * the cache entry is replaced with `Promise.resolve(undefined)` for subsequent callers, and the error\n\t * is re-thrown to the first caller. After `errorTTL` expires, the next call retries.\n\t * Only used when `onError` is not provided. Mutually exclusive with `expireOnError`.\n\t */\n\terrorTTL?: number;\n\t/** Maximum number of entries in the cache (LRU eviction when exceeded) */\n\tcapacity?: number;\n}\n\nexport class PromiseCache<K, V> {\n\tprivate readonly cache = new Map<K, CacheEntry<V>>();\n\tprivate readonly aborts = new Map<K, AbortAggregate>();\n\tprivate readonly controllers = new Map<K, CacheController>();\n\tprivate readonly options: PromiseCacheOptions;\n\n\tconstructor(options?: PromiseCacheOptions) {\n\t\tthis.options = { expireOnError: true, ...options };\n\t}\n\n\t/** Clears all entries. Like `delete`, does NOT dispose in-flight abort aggregates — that would detach a running\n\t * op's cancellation (a later caller-abort couldn't cancel it); each factory's settle handler disposes its own. */\n\tclear(): void {\n\t\tthis.aborts.clear();\n\t\tthis.controllers.clear();\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Removes a promise from the cache.\n\t * Does NOT dispose the abort aggregate: while a factory is in flight, disposing would detach\n\t * its callers' cancellation and orphan the underlying op (a later caller-abort could no longer cancel the\n\t * running `git status`). The factory's own settle handler disposes the aggregate (see `getOrCreate`). */\n\tdelete(key: K): void {\n\t\tthis.aborts.delete(key);\n\t\tthis.controllers.delete(key);\n\t\tthis.cache.delete(key);\n\t}\n\n\t/**\n\t * Marks a cached entry as invalidated.\n\t * - If the entry has a `CacheController` (created via `getOrCreate`), it is soft-invalidated:\n\t * existing waiters complete normally, new callers joining during the remainder of the\n\t * in-flight operation share the same promise, and the entry self-evicts on settle.\n\t * - If the entry was set via plain `.set()` (no controller), it is hard-deleted — there is\n\t * no in-flight factory to ride along with.\n\t *\n\t * No-op if the key is not cached.\n\t */\n\tinvalidate(key: K): void {\n\t\tif (this.controllers.has(key)) {\n\t\t\tthis.controllers.get(key)!.invalidate();\n\t\t} else {\n\t\t\tthis.delete(key);\n\t\t}\n\t}\n\n\tkeys(): IterableIterator<K> {\n\t\treturn this.cache.keys();\n\t}\n\n\t/**\n\t * Returns the number of entries in the cache, including expired entries that haven't been evicted yet\n\t * (they are only evicted on access), so this reflects what's actually retained in memory.\n\t */\n\tget size(): number {\n\t\treturn this.cache.size;\n\t}\n\n\t/**\n\t * Gets a promise from the cache without creating it and updates accessed time\n\t * @param key - The cache key\n\t * @returns The cached promise, or undefined if not cached or expired\n\t */\n\tget(key: K): Promise<V> | undefined {\n\t\tconst now = Date.now();\n\t\tconst entry = this.cache.get(key);\n\t\tif (entry == null || this.expired(entry, now)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// Update accessed time\n\t\tentry.accessed = now;\n\t\treturn entry.promise;\n\t}\n\n\t/**\n\t * Gets a promise from the cache or creates a new one using the factory function.\n\t *\n\t * When `cancellation` is provided:\n\t * - The factory receives an aggregate `AbortSignal` that only fires when **all** callers cancel\n\t * - The returned promise rejects with `CancellationError` if this caller's signal fires\n\t * - Other callers sharing the same cached promise are unaffected\n\t *\n\t * @param key - The cache key\n\t * @param factory - Factory function to create the value if not cached\n\t * @param cancellation - Optional per-caller AbortSignal\n\t * @param options - Optional TTL and error handling options that override the defaults\n\t */\n\tasync getOrCreate(\n\t\tkey: K,\n\t\tfactory: (cacheable: CacheController, cancellation?: AbortSignal) => Promise<V>,\n\t\toptions?: {\n\t\t\t/** Per-caller AbortSignal — the factory receives an aggregate signal that only fires when all callers cancel */\n\t\t\tcancellation?: AbortSignal;\n\t\t\t/** TTL (time-to-live) in milliseconds since creation */\n\t\t\tcreateTTL?: number;\n\t\t\t/** TTL (time-to-live) in milliseconds since last access */\n\t\t\taccessTTL?: number;\n\t\t\t/** Whether to expire the entry if the promise fails */\n\t\t\texpireOnError?: boolean;\n\t\t\t/**\n\t\t\t * Called after the factory resolves. If the predicate resolves to true, the entry is evicted while\n\t\t\t * the current caller still receives the resolved value.\n\t\t\t */\n\t\t\tevictWhen?: (value: V) => boolean | Promise<boolean>;\n\t\t\t/**\n\t\t\t * TTL (time-to-live) in milliseconds for caching errors. When the factory rejects and `errorTTL` is set,\n\t\t\t * the cache entry is replaced with `Promise.resolve(undefined)` for subsequent callers, and the error\n\t\t\t * is re-thrown to the first caller. After `errorTTL` expires, the next call retries.\n\t\t\t * Only used when `onError` is not provided.\n\t\t\t */\n\t\t\terrorTTL?: number;\n\t\t\t/**\n\t\t\t * Called when the factory promise rejects. Return a fallback result to cache\n\t\t\t * instead of expiring. The fallback value is cached with the specified `createTTL`\n\t\t\t * (defaults to the cache's `createTTL`). By default, the error is re-thrown to the\n\t\t\t * first caller after caching the fallback. Set `suppress: true` to return the fallback\n\t\t\t * value to the first caller instead. Return `undefined` to expire normally and re-throw.\n\t\t\t */\n\t\t\tonError?: (error: unknown) => { value: V; createTTL?: number; suppress?: boolean } | undefined;\n\t\t},\n\t): Promise<V> {\n\t\tconst cancellation = options?.cancellation;\n\t\tif (cancellation?.aborted) return Promise.reject(new CancellationError());\n\n\t\tconst now = Date.now();\n\n\t\toptions = { ...this.options, ...options };\n\n\t\tlet entry = this.cache.get(key);\n\t\tif (entry != null && !this.expired(entry, now)) {\n\t\t\t// Cache hit — but don't join an entry whose aggregate has already permanently aborted (all prior\n\t\t\t// callers cancelled, so the shared run is failing): that would hand this caller a doomed promise.\n\t\t\t// Fall through to create a fresh entry; the doomed entry's late settle is `deleteIfOwned`-guarded.\n\t\t\tconst existingAborts = this.aborts.get(key);\n\t\t\tif (existingAborts == null || !existingAborts.signal.aborted) {\n\t\t\t\tentry.accessed = now;\n\t\t\t\tconst cleanup = existingAborts != null ? existingAborts.add(cancellation) : undefined;\n\t\t\t\treturn this.attachCallerLifecycle(entry.promise, cancellation, cleanup);\n\t\t\t}\n\t\t}\n\n\t\tconst cacheable = new CacheController();\n\t\t// Always create the aggregate so subsequent callers (with or without signals) can contribute,\n\t\t// and so delete/invalidate has a controller to interact with\n\t\tconst abortAgg = new AbortAggregate();\n\t\tconst cleanup = abortAgg.add(cancellation);\n\t\tthis.aborts.set(key, abortAgg);\n\t\tthis.controllers.set(key, cacheable);\n\n\t\tconst promise = factory(cacheable, abortAgg.signal);\n\t\tvoid promise\n\t\t\t.finally(() => {\n\t\t\t\t// All three deletes are ownership-guarded — a hard `delete(key)` + fresh `getOrCreate` while our\n\t\t\t\t// factory is in flight installs a successor under `key`; our late settle must not evict it.\n\t\t\t\tif (cacheable.invalidated) {\n\t\t\t\t\tdeleteIfOwned(this.cache, key, entry!);\n\t\t\t\t}\n\t\t\t\tabortAgg.dispose();\n\t\t\t\tdeleteIfOwned(this.aborts, key, abortAgg);\n\t\t\t\tdeleteIfOwned(this.controllers, key, cacheable);\n\t\t\t})\n\t\t\t// Swallow the cleanup chain's rejection so it doesn't surface as an unhandled rejection\n\t\t\t// separate from the caller-facing promise's own handling (e.g. on cancellation).\n\t\t\t.catch(() => {});\n\n\t\tif (options?.onError != null) {\n\t\t\t// Wrap the promise to handle errors with the onError callback\n\t\t\tconst errorHandled = promise.catch((ex: unknown) => {\n\t\t\t\tconst result = options.onError!(ex);\n\t\t\t\t// A hard `delete(key)` + fresh `getOrCreate` while we were in flight installs a successor;\n\t\t\t\t// don't clobber it. But an EMPTY key (our entry already evicted, e.g. by the invalidated-finally)\n\t\t\t\t// still takes the fallback — preserving base behavior of caching the error fallback.\n\t\t\t\tconst occupant = this.cache.get(key);\n\t\t\t\tconst clobbersSuccessor = occupant != null && occupant !== entry;\n\t\t\t\tif (result != null) {\n\t\t\t\t\t// Replace the entry with a resolved fallback value for subsequent callers\n\t\t\t\t\tif (!clobbersSuccessor) {\n\t\t\t\t\t\tconst errorNow = Date.now();\n\t\t\t\t\t\tthis.cache.set(key, {\n\t\t\t\t\t\t\tpromise: Promise.resolve(result.value),\n\t\t\t\t\t\t\tcreated: errorNow,\n\t\t\t\t\t\t\taccessed: errorNow,\n\t\t\t\t\t\t\tcreateTTL: result.createTTL ?? options.createTTL,\n\t\t\t\t\t\t\taccessTTL: options.accessTTL,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\t// suppress: return fallback to first caller; otherwise re-throw\n\t\t\t\t\tif (result.suppress) return result.value;\n\t\t\t\t} else {\n\t\t\t\t\tdeleteIfOwned(this.cache, key, entry!);\n\t\t\t\t}\n\t\t\t\tthrow ex;\n\t\t\t});\n\n\t\t\tentry = {\n\t\t\t\tpromise: errorHandled,\n\t\t\t\tcreated: now,\n\t\t\t\taccessed: now,\n\t\t\t\tcreateTTL: options.createTTL,\n\t\t\t\taccessTTL: options.accessTTL,\n\t\t\t};\n\t\t\tthis.cache.set(key, entry);\n\t\t} else if (options?.errorTTL != null) {\n\t\t\t// Cache resolved undefined for subsequent callers, re-throw to first caller\n\t\t\tconst errorTTL = options.errorTTL;\n\t\t\tconst errorHandled = promise.catch((ex: unknown) => {\n\t\t\t\t// Overwrite unless a successor now owns the key (empty key still takes it — see onError above).\n\t\t\t\tconst occupant = this.cache.get(key);\n\t\t\t\tif (occupant == null || occupant === entry) {\n\t\t\t\t\tconst errorNow = Date.now();\n\t\t\t\t\tthis.cache.set(key, {\n\t\t\t\t\t\tpromise: Promise.resolve(undefined as V),\n\t\t\t\t\t\tcreated: errorNow,\n\t\t\t\t\t\taccessed: errorNow,\n\t\t\t\t\t\tcreateTTL: errorTTL,\n\t\t\t\t\t\taccessTTL: options.accessTTL,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tthrow ex;\n\t\t\t});\n\n\t\t\tentry = {\n\t\t\t\tpromise: errorHandled,\n\t\t\t\tcreated: now,\n\t\t\t\taccessed: now,\n\t\t\t\tcreateTTL: options.createTTL,\n\t\t\t\taccessTTL: options.accessTTL,\n\t\t\t};\n\t\t\tthis.cache.set(key, entry);\n\t\t} else {\n\t\t\tentry = {\n\t\t\t\tpromise: promise,\n\t\t\t\tcreated: now,\n\t\t\t\taccessed: now,\n\t\t\t\tcreateTTL: options.createTTL,\n\t\t\t\taccessTTL: options.accessTTL,\n\t\t\t};\n\t\t\tthis.cache.set(key, entry);\n\n\t\t\tif (options?.expireOnError ?? true) {\n\t\t\t\t// Ownership-guarded: a hard `delete(key)` + fresh `getOrCreate` replacing us while in flight must\n\t\t\t\t// not have its entry evicted by our rejection.\n\t\t\t\tconst ourEntry = entry;\n\t\t\t\tpromise.catch(() => deleteIfOwned(this.cache, key, ourEntry));\n\t\t\t}\n\t\t}\n\n\t\tconst evictWhen = options?.evictWhen;\n\t\tif (evictWhen != null) {\n\t\t\tconst candidate = entry;\n\t\t\tvoid candidate.promise\n\t\t\t\t.then(value => evictWhen(value))\n\t\t\t\t.then(evict => {\n\t\t\t\t\tif (evict) {\n\t\t\t\t\t\tdeleteIfOwned(this.cache, key, candidate);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t// A retention policy must not surface a rejection independently of the caller-facing operation.\n\t\t\t\t.catch(() => {});\n\t\t}\n\n\t\t// Clean up expired entries and enforce capacity limit in one pass\n\t\tif (this.cache.size > 1) {\n\t\t\tqueueMicrotask(() => this.cleanup());\n\t\t}\n\n\t\treturn this.attachCallerLifecycle(entry.promise, cancellation, cleanup);\n\t}\n\n\tprivate attachCallerLifecycle(\n\t\tpromise: Promise<V>,\n\t\tcancellation: AbortSignal | undefined,\n\t\tcleanup: (() => void) | undefined,\n\t): Promise<V> {\n\t\tconst returned = cancellation != null ? raceWithSignal(promise, cancellation) : promise;\n\t\tif (cleanup != null) {\n\t\t\t// Release this caller's aggregate slot when their wait ends.\n\t\t\t// Attach .catch to swallow any rejection in the cleanup chain so it doesn't surface\n\t\t\t// as an unhandled rejection separate from the caller's own handling of `returned`.\n\t\t\tvoid returned.finally(cleanup).catch(() => {});\n\t\t}\n\t\treturn returned;\n\t}\n\n\tprivate cleanup(): void {\n\t\tconst now = Date.now();\n\t\tconst capacity = this.options.capacity;\n\n\t\t// If no capacity limit, just remove expired entries\n\t\tif (capacity == null) {\n\t\t\tfor (const [key, entry] of this.cache.entries()) {\n\t\t\t\tif (this.expired(entry, now)) {\n\t\t\t\t\tthis.cache.delete(key);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Single pass: collect non-expired entries and find LRU candidates\n\t\tconst entries: Array<[K, CacheEntry<V>]> = [];\n\t\tfor (const [key, entry] of this.cache.entries()) {\n\t\t\tif (!this.expired(entry, now)) {\n\t\t\t\tentries.push([key, entry]);\n\t\t\t} else {\n\t\t\t\tthis.cache.delete(key);\n\t\t\t}\n\t\t}\n\n\t\t// If still over capacity, remove LRU entries\n\t\tconst excess = entries.length - capacity;\n\t\tif (excess > 0) {\n\t\t\t// Sort by accessed time (oldest first) and remove the excess\n\t\t\t// Note: O(N log N) sort — acceptable at current cache sizes; replace with a linked-list LRU if capacity grows\n\t\t\tentries.sort((a, b) => a[1].accessed - b[1].accessed);\n\t\t\tfor (let i = 0; i < excess; i++) {\n\t\t\t\tthis.cache.delete(entries[i][0]);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate expired(entry: CacheEntry<V>, now: number): boolean {\n\t\treturn (\n\t\t\t(entry.createTTL != null && now - entry.created >= entry.createTTL) ||\n\t\t\t(entry.accessTTL != null && now - entry.accessed >= entry.accessTTL)\n\t\t);\n\t}\n\n\t/**\n\t * Sets a promise in the cache.\n\t * The promise will use the default options from the cache constructor, unless overridden.\n\t *\n\t * @param key - The cache key\n\t * @param promise - The promise to cache\n\t * @param options - Optional TTL options that override the defaults\n\t */\n\tset(key: K, promise: Promise<V>, options?: { accessTTL?: number; createTTL?: number }): void {\n\t\tconst now = Date.now();\n\t\tconst entry: CacheEntry<V> = {\n\t\t\tpromise: promise,\n\t\t\tcreated: now,\n\t\t\taccessed: now,\n\t\t\tcreateTTL: options?.createTTL ?? this.options.createTTL,\n\t\t\taccessTTL: options?.accessTTL ?? this.options.accessTTL,\n\t\t};\n\t\tthis.cache.set(key, entry);\n\n\t\tif (this.options.expireOnError ?? true) {\n\t\t\tpromise.catch(() => deleteIfOwned(this.cache, key, entry));\n\t\t}\n\t}\n}\n\n/**\n * A Map-like wrapper for promises that automatically removes failed promises from the cache.\n * This provides a drop-in replacement for Map<K, Promise<V>> with automatic error cleanup.\n */\nexport class PromiseMap<K, V> {\n\tprivate readonly cache = new Map<K, Promise<V>>();\n\tprivate readonly aborts = new Map<K, AbortAggregate>();\n\tprivate readonly controllers = new Map<K, CacheController>();\n\n\tget [Symbol.toStringTag](): string {\n\t\treturn 'PromiseMap';\n\t}\n\n\t/**\n\t * Gets a promise from the cache or creates a new one using the factory function.\n\t * Failed promises are automatically removed from the cache.\n\t *\n\t * When `cancellation` is provided:\n\t * - The factory receives an aggregate `AbortSignal` that only fires when **all** callers cancel\n\t * - The returned promise rejects with `CancellationError` if this caller's signal fires\n\t * - Other callers sharing the same cached promise are unaffected\n\t *\n\t * Callers without a `cancellation` signal count as \"permanent\" subscribers — the aggregate\n\t * cannot fire while at least one is active. The slot is released when the caller's returned\n\t * promise settles.\n\t */\n\tgetOrCreate(\n\t\tkey: K,\n\t\tfactory: (cacheable: CacheController, cancellation?: AbortSignal) => Promise<V>,\n\t\tcancellation?: AbortSignal,\n\t): Promise<V> {\n\t\tif (cancellation?.aborted) return Promise.reject(new CancellationError());\n\n\t\tconst cached = this.cache.get(key);\n\t\tif (cached != null) {\n\t\t\t// Cache hit — but don't join an entry whose aggregate has already permanently aborted (all prior\n\t\t\t// callers cancelled, so the shared run is failing): joining hands this caller a doomed promise.\n\t\t\t// Fall through to a fresh entry; the doomed entry's late settle is `deleteIfOwned`-guarded.\n\t\t\tconst existingAborts = this.aborts.get(key);\n\t\t\tif (existingAborts == null || !existingAborts.signal.aborted) {\n\t\t\t\tconst cleanup = existingAborts != null ? existingAborts.add(cancellation) : undefined;\n\t\t\t\treturn this.attachCallerLifecycle(cached, cancellation, cleanup);\n\t\t\t}\n\t\t}\n\n\t\t// Cache miss — always create the aggregate so subsequent callers (with or without signals)\n\t\t// can contribute, and so delete/invalidate has a controller to interact with\n\t\tconst cacheable = new CacheController();\n\t\tconst aborts = new AbortAggregate();\n\t\tconst cleanup = aborts.add(cancellation);\n\t\tthis.aborts.set(key, aborts);\n\t\tthis.controllers.set(key, cacheable);\n\n\t\tconst promise = factory(cacheable, aborts.signal);\n\n\t\t// Automatically remove failed promises from the cache (ownership-guarded — see `deleteIfOwned`).\n\t\tpromise.catch(() => deleteIfOwned(this.cache, key, promise));\n\t\tvoid promise\n\t\t\t.finally(() => {\n\t\t\t\tif (cacheable.invalidated) {\n\t\t\t\t\tdeleteIfOwned(this.cache, key, promise);\n\t\t\t\t}\n\t\t\t\taborts.dispose();\n\t\t\t\tdeleteIfOwned(this.aborts, key, aborts);\n\t\t\t\tdeleteIfOwned(this.controllers, key, cacheable);\n\t\t\t})\n\t\t\t// Swallow the cleanup chain's rejection so it doesn't surface as an unhandled rejection\n\t\t\t// separate from the caller-facing promise's own handling (e.g. on cancellation).\n\t\t\t.catch(() => {});\n\n\t\tthis.cache.set(key, promise);\n\n\t\treturn this.attachCallerLifecycle(promise, cancellation, cleanup);\n\t}\n\n\tprivate attachCallerLifecycle(\n\t\tpromise: Promise<V>,\n\t\tcancellation: AbortSignal | undefined,\n\t\tcleanup: (() => void) | undefined,\n\t): Promise<V> {\n\t\tconst returned = cancellation != null ? raceWithSignal(promise, cancellation) : promise;\n\t\tif (cleanup != null) {\n\t\t\t// Release this caller's aggregate slot when their wait ends.\n\t\t\t// Attach .catch to swallow any rejection in the cleanup chain so it doesn't surface\n\t\t\t// as an unhandled rejection separate from the caller's own handling of `returned`.\n\t\t\tvoid returned.finally(cleanup).catch(() => {});\n\t\t}\n\t\treturn returned;\n\t}\n\n\t/**\n\t * Marks a cached entry as invalidated.\n\t * - If the entry has a `CacheController` (created via `getOrCreate`), it is soft-invalidated:\n\t * existing waiters complete normally, new callers joining during the remainder of the\n\t * in-flight operation share the same promise, and the entry self-evicts on settle.\n\t * - If the entry was set via plain `.set()` (no controller), it is hard-deleted — there is\n\t * no in-flight factory to ride along with.\n\t *\n\t * No-op if the key is not cached.\n\t */\n\tinvalidate(key: K): void {\n\t\tif (this.controllers.has(key)) {\n\t\t\tthis.controllers.get(key)!.invalidate();\n\t\t} else {\n\t\t\tthis.delete(key);\n\t\t}\n\t}\n\n\t/**\n\t * Gets a promise from the cache, or undefined if not found.\n\t */\n\tget(key: K): Promise<V> | undefined {\n\t\treturn this.cache.get(key);\n\t}\n\n\t/**\n\t * Sets a promise in the cache. The promise will be automatically removed if it fails.\n\t */\n\tset(key: K, promise: Promise<V>): this {\n\t\tthis.cache.set(key, promise);\n\n\t\t// Automatically remove failed promises from the cache (ownership-guarded — see `deleteIfOwned`).\n\t\tpromise.catch(() => deleteIfOwned(this.cache, key, promise));\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t */\n\thas(key: K): boolean {\n\t\treturn this.cache.has(key);\n\t}\n\n\t/**\n\t * Removes a key from the cache.\n\t * Does NOT dispose the abort aggregate: while a factory is in flight, disposing would detach\n\t * its callers' cancellation and orphan the underlying op (a later caller-abort could no longer cancel the\n\t * running `git status`). The factory's own settle handler disposes the aggregate (see `getOrCreate`). */\n\tdelete(key: K): boolean {\n\t\tthis.aborts.delete(key);\n\t\tthis.controllers.delete(key);\n\t\treturn this.cache.delete(key);\n\t}\n\n\t/**\n\t * Clears all entries. Like `delete`, does NOT dispose in-flight abort aggregates — that would detach a running\n\t * op's cancellation (a later caller-abort couldn't cancel it); each factory's settle handler disposes its own.\n\t */\n\tclear(): void {\n\t\tthis.aborts.clear();\n\t\tthis.controllers.clear();\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Returns the number of entries in the cache.\n\t */\n\tget size(): number {\n\t\treturn this.cache.size;\n\t}\n\n\t/**\n\t * Returns an iterator for the cache keys.\n\t */\n\tkeys(): IterableIterator<K> {\n\t\treturn this.cache.keys();\n\t}\n\n\t/**\n\t * Returns an iterator for the cache values.\n\t */\n\tvalues(): IterableIterator<Promise<V>> {\n\t\treturn this.cache.values();\n\t}\n\n\t/**\n\t * Returns an iterator for the cache entries.\n\t */\n\tentries(): IterableIterator<[K, Promise<V>]> {\n\t\treturn this.cache.entries();\n\t}\n\n\t/**\n\t * Executes a provided function once for each cache entry.\n\t */\n\tforEach(callbackfn: (value: Promise<V>, key: K, map: Map<K, Promise<V>>) => void, thisArg?: any): void {\n\t\tthis.cache.forEach(callbackfn, thisArg);\n\t}\n\n\t[Symbol.iterator](): IterableIterator<[K, Promise<V>]> {\n\t\treturn this.cache[Symbol.iterator]();\n\t}\n}\n\n/**\n * A two-level cache that organizes PromiseCaches by repository path.\n * Automatically creates and manages inner PromiseCaches per repository.\n *\n * This is useful for caching data that varies by both repository and some other key,\n * with TTL and capacity management provided by PromiseCache.\n */\nexport class RepoPromiseCacheMap<K, V> {\n\tprivate readonly cache = new Map<string, PromiseCache<K, V>>();\n\tprivate readonly options: PromiseCacheOptions;\n\n\tconstructor(options?: PromiseCacheOptions) {\n\t\tthis.options = { expireOnError: true, ...options };\n\t}\n\n\tget [Symbol.toStringTag](): string {\n\t\treturn 'RepoPromiseCacheMap';\n\t}\n\n\t/**\n\t * Gets a promise from the cache or creates a new one using the factory function.\n\t * Automatically creates the inner PromiseCache for the repository if it doesn't exist.\n\t *\n\t * @param repoPath - The repository path (outer key)\n\t * @param key - The cache key within the repository (inner key)\n\t * @param factory - Factory function to create the value if not cached\n\t * @param options - Optional TTL and error handling options that override the defaults\n\t */\n\tgetOrCreate(\n\t\trepoPath: string,\n\t\tkey: K,\n\t\tfactory: (cacheable: CacheController, cancellation?: AbortSignal) => Promise<V>,\n\t\toptions?: {\n\t\t\t/** Per-caller AbortSignal — the factory receives an aggregate signal that only fires when all callers cancel */\n\t\t\tcancellation?: AbortSignal;\n\t\t\t/** TTL (time-to-live) in milliseconds since creation */\n\t\t\tcreateTTL?: number;\n\t\t\t/** TTL (time-to-live) in milliseconds since last access */\n\t\t\taccessTTL?: number;\n\t\t\t/** Whether to expire the entry if the promise fails */\n\t\t\texpireOnError?: boolean;\n\t\t\t/**\n\t\t\t * Called after the factory resolves. If the predicate resolves to true, the entry is evicted while\n\t\t\t * the current caller still receives the resolved value.\n\t\t\t */\n\t\t\tevictWhen?: (value: V) => boolean | Promise<boolean>;\n\t\t\t/**\n\t\t\t * TTL (time-to-live) in milliseconds for caching errors. When the factory rejects and `errorTTL` is set,\n\t\t\t * the cache entry is replaced with `Promise.resolve(undefined)` for subsequent callers, and the error\n\t\t\t * is re-thrown to the first caller. After `errorTTL` expires, the next call retries.\n\t\t\t * Only used when `onError` is not provided.\n\t\t\t */\n\t\t\terrorTTL?: number;\n\t\t\t/**\n\t\t\t * Called when the factory promise rejects. Return a fallback result to cache\n\t\t\t * instead of expiring. The fallback value is cached with the specified `createTTL`\n\t\t\t * (defaults to the cache's `createTTL`). By default, the error is re-thrown to the\n\t\t\t * first caller after caching the fallback. Set `suppress: true` to return the fallback\n\t\t\t * value to the first caller instead. Return `undefined` to expire normally and re-throw.\n\t\t\t */\n\t\t\tonError?: (error: unknown) => { value: V; createTTL?: number; suppress?: boolean } | undefined;\n\t\t},\n\t): Promise<V> {\n\t\tlet repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) {\n\t\t\trepoCache = new PromiseCache<K, V>(this.options);\n\t\t\tthis.cache.set(repoPath, repoCache);\n\t\t}\n\n\t\treturn repoCache.getOrCreate(key, factory, options);\n\t}\n\n\t/**\n\t * Gets a promise from the cache without creating it.\n\t *\n\t * @param repoPath - The repository path (outer key)\n\t * @param key - The cache key within the repository (inner key)\n\t * @returns The cached promise, or undefined if not cached\n\t */\n\tget(repoPath: string, key: K): Promise<V> | undefined {\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) return undefined;\n\n\t\treturn repoCache.get(key);\n\t}\n\n\t/**\n\t * Sets a promise in the cache.\n\t *\n\t * @param repoPath - The repository path (outer key)\n\t * @param key - The cache key within the repository (inner key)\n\t * @param promise - The promise to cache\n\t * @param options - Optional TTL options that override the defaults\n\t */\n\tset(repoPath: string, key: K, promise: Promise<V>, options?: { accessTTL?: number; createTTL?: number }): void {\n\t\tlet repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) {\n\t\t\trepoCache = new PromiseCache<K, V>(this.options);\n\t\t\tthis.cache.set(repoPath, repoCache);\n\t\t}\n\n\t\trepoCache.set(key, promise, options);\n\t}\n\n\t/**\n\t * Deletes a specific key from a repository's cache, or all keys for a repository.\n\t *\n\t * @param repoPath - The repository path\n\t * @param key - Optional specific key to delete. If omitted, deletes the entire repository cache.\n\t * @returns true if something was deleted, false otherwise\n\t */\n\tdelete(repoPath: string, key?: K): boolean {\n\t\tif (key === undefined) {\n\t\t\treturn this.cache.delete(repoPath);\n\t\t}\n\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) return false;\n\n\t\trepoCache.delete(key);\n\t\treturn true;\n\t}\n\n\t/**\n\t * Marks a cached entry as invalidated. See `PromiseCache.invalidate` for semantics.\n\t *\n\t * @param repoPath - The repository path\n\t * @param key - Optional specific key to invalidate. If omitted, invalidates every entry for the repository.\n\t */\n\tinvalidate(repoPath: string, key?: K): void {\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) return;\n\n\t\tif (key === undefined) {\n\t\t\tfor (const k of repoCache.keys()) {\n\t\t\t\trepoCache.invalidate(k);\n\t\t\t}\n\t\t} else {\n\t\t\trepoCache.invalidate(key);\n\t\t}\n\t}\n\n\t/**\n\t * Deletes all keys matching a prefix from a repository's cache.\n\t * @param repoPath - The repository path\n\t * @param prefix - Key prefix to match\n\t * @returns true if something was deleted\n\t */\n\tdeleteByKeyPrefix(this: RepoPromiseCacheMap<string, V>, repoPath: string, prefix: string): boolean {\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) return false;\n\n\t\tlet deleted = false;\n\t\tfor (const key of repoCache.keys()) {\n\t\t\tif (key.startsWith(prefix)) {\n\t\t\t\trepoCache.delete(key);\n\t\t\t\tdeleted = true;\n\t\t\t}\n\t\t}\n\t\treturn deleted;\n\t}\n\n\t/**\n\t * Invalidates all keys matching a prefix from a repository's cache. See `PromiseCache.invalidate`\n\t * for semantics — in-flight entries are marked invalidated and shared with new callers; settled\n\t * entries are hard-deleted. Prefer over `deleteByKeyPrefix` when the eviction is a \"data has\n\t * changed\" event and in-flight factories' results would still be valid for current waiters.\n\t * @param repoPath - The repository path\n\t * @param prefix - Key prefix to match\n\t * @returns true if something was invalidated\n\t */\n\tinvalidateByKeyPrefix(this: RepoPromiseCacheMap<string, V>, repoPath: string, prefix: string): boolean {\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) return false;\n\n\t\tlet invalidated = false;\n\t\tfor (const key of repoCache.keys()) {\n\t\t\tif (key.startsWith(prefix)) {\n\t\t\t\trepoCache.invalidate(key);\n\t\t\t\tinvalidated = true;\n\t\t\t}\n\t\t}\n\t\treturn invalidated;\n\t}\n\n\t/**\n\t * Clears all caches for all repositories.\n\t */\n\tclear(): void {\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Returns the total number of entries across all repository caches.\n\t */\n\tget entryCount(): number {\n\t\tlet count = 0;\n\t\tfor (const repoCache of this.cache.values()) {\n\t\t\tcount += repoCache.size;\n\t\t}\n\t\treturn count;\n\t}\n\n\t/**\n\t * Returns the number of repositories in the cache.\n\t */\n\tget size(): number {\n\t\treturn this.cache.size;\n\t}\n}\n\n/**\n * A two-level cache that organizes PromiseMaps by repository path.\n * Automatically creates and manages inner PromiseMaps per repository.\n *\n * This is useful for caching data that varies by both repository and some other key,\n * with simple promise deduplication and automatic error cleanup.\n */\nexport class RepoPromiseMap<K, V> {\n\tprivate readonly cache = new Map<string, PromiseMap<K, V>>();\n\n\tget [Symbol.toStringTag](): string {\n\t\treturn 'RepoPromiseMap';\n\t}\n\n\t/**\n\t * Gets a promise from the cache or creates a new one using the factory function.\n\t * Automatically creates the inner PromiseMap for the repository if it doesn't exist.\n\t *\n\t * @param repoPath - The repository path (outer key)\n\t * @param key - The cache key within the repository (inner key)\n\t * @param factory - Factory function to create the value if not cached\n\t */\n\tgetOrCreate(\n\t\trepoPath: string,\n\t\tkey: K,\n\t\tfactory: (cacheable: CacheController, cancellation?: AbortSignal) => Promise<V>,\n\t\tcancellation?: AbortSignal,\n\t): Promise<V> {\n\t\tlet repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) {\n\t\t\trepoCache = new PromiseMap<K, V>();\n\t\t\tthis.cache.set(repoPath, repoCache);\n\t\t}\n\n\t\treturn repoCache.getOrCreate(key, factory, cancellation);\n\t}\n\n\t/**\n\t * Gets a promise from the cache without creating it.\n\t *\n\t * @param repoPath - The repository path (outer key)\n\t * @param key - The cache key within the repository (inner key)\n\t * @returns The cached promise, or undefined if not found\n\t */\n\tget(repoPath: string, key: K): Promise<V> | undefined {\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\treturn repoCache?.get(key);\n\t}\n\n\t/**\n\t * Deletes a specific key from a repository's cache, or all keys for a repository.\n\t *\n\t * @param repoPath - The repository path\n\t * @param key - Optional specific key to delete. If omitted, deletes the entire repository cache.\n\t * @returns true if something was deleted, false otherwise\n\t */\n\tdelete(repoPath: string, key?: K): boolean {\n\t\tif (key === undefined) {\n\t\t\treturn this.cache.delete(repoPath);\n\t\t}\n\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) return false;\n\n\t\treturn repoCache.delete(key);\n\t}\n\n\t/**\n\t * Marks a cached entry as invalidated. See `PromiseMap.invalidate` for semantics.\n\t *\n\t * @param repoPath - The repository path\n\t * @param key - Optional specific key to invalidate. If omitted, invalidates every entry for the repository.\n\t */\n\tinvalidate(repoPath: string, key?: K): void {\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) return;\n\n\t\tif (key === undefined) {\n\t\t\tfor (const k of repoCache.keys()) {\n\t\t\t\trepoCache.invalidate(k);\n\t\t\t}\n\t\t} else {\n\t\t\trepoCache.invalidate(key);\n\t\t}\n\t}\n\n\t/**\n\t * Clears all caches for all repositories.\n\t */\n\tclear(): void {\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Returns the number of repositories in the cache.\n\t */\n\tget size(): number {\n\t\treturn this.cache.size;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"promiseCache.js","names":[],"sources":["../../../utils/src/promiseCache.ts"],"sourcesContent":["import { AbortAggregate, CancellationError, raceWithSignal } from './cancellation.js';\n\n/** Deletes `key` from `map` only if it still maps to `owner`. A `delete(key)` + fresh `getOrCreate` can install\n * a successor under the same key while our factory is in flight; our late settle must not evict the successor. */\nfunction deleteIfOwned<K, V>(map: Map<K, V>, key: K, owner: V): void {\n\tif (map.get(key) === owner) {\n\t\tmap.delete(key);\n\t}\n}\n\ninterface CacheEntry<V> {\n\tpromise: Promise<V>;\n\taccessed: number;\n\tcreated: number;\n\n\tcreateTTL: number | undefined;\n\taccessTTL: number | undefined;\n}\n\nexport class CacheController {\n\t#invalidated = false;\n\tget invalidated(): boolean {\n\t\treturn this.#invalidated;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.#invalidated = true;\n\t}\n}\n\ninterface PromiseCacheOptions {\n\t/** TTL (time-to-live) in milliseconds since creation */\n\tcreateTTL?: number;\n\t/** TTL (time-to-live) in milliseconds since last access */\n\taccessTTL?: number;\n\t/** Whether to expire the entry if the promise fails (default: true) */\n\texpireOnError?: boolean;\n\t/**\n\t * TTL (time-to-live) in milliseconds for caching errors. When the factory rejects and `errorTTL` is set,\n\t * the cache entry is replaced with `Promise.resolve(undefined)` for subsequent callers, and the error\n\t * is re-thrown to the first caller. After `errorTTL` expires, the next call retries.\n\t * Only used when `onError` is not provided. Mutually exclusive with `expireOnError`.\n\t */\n\terrorTTL?: number;\n\t/** Maximum number of entries in the cache (LRU eviction when exceeded) */\n\tcapacity?: number;\n}\n\nexport class PromiseCache<K, V> {\n\tprivate readonly cache = new Map<K, CacheEntry<V>>();\n\tprivate readonly aborts = new Map<K, AbortAggregate>();\n\tprivate readonly controllers = new Map<K, CacheController>();\n\tprivate readonly options: PromiseCacheOptions;\n\n\tconstructor(options?: PromiseCacheOptions) {\n\t\tthis.options = { expireOnError: true, ...options };\n\t}\n\n\t/** Clears all entries. Like `delete`, does NOT dispose in-flight abort aggregates — that would detach a running\n\t * op's cancellation (a later caller-abort couldn't cancel it); each factory's settle handler disposes its own. */\n\tclear(): void {\n\t\tthis.aborts.clear();\n\t\tthis.controllers.clear();\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Removes a promise from the cache.\n\t * Does NOT dispose the abort aggregate: while a factory is in flight, disposing would detach\n\t * its callers' cancellation and orphan the underlying op (a later caller-abort could no longer cancel the\n\t * running `git status`). The factory's own settle handler disposes the aggregate (see `getOrCreate`). */\n\tdelete(key: K): void {\n\t\tthis.aborts.delete(key);\n\t\tthis.controllers.delete(key);\n\t\tthis.cache.delete(key);\n\t}\n\n\t/**\n\t * Marks a cached entry as invalidated.\n\t * - If the entry has a `CacheController` (created via `getOrCreate`), it is soft-invalidated:\n\t * existing waiters complete normally, new callers joining during the remainder of the\n\t * in-flight operation share the same promise, and the entry self-evicts on settle.\n\t * - If the entry was set via plain `.set()` (no controller), it is hard-deleted — there is\n\t * no in-flight factory to ride along with.\n\t *\n\t * No-op if the key is not cached.\n\t */\n\tinvalidate(key: K): void {\n\t\tif (this.controllers.has(key)) {\n\t\t\tthis.controllers.get(key)!.invalidate();\n\t\t} else {\n\t\t\tthis.delete(key);\n\t\t}\n\t}\n\n\tkeys(): IterableIterator<K> {\n\t\treturn this.cache.keys();\n\t}\n\n\t/**\n\t * Returns the number of entries in the cache, including expired entries that haven't been evicted yet\n\t * (they are only evicted on access), so this reflects what's actually retained in memory.\n\t */\n\tget size(): number {\n\t\treturn this.cache.size;\n\t}\n\n\t/**\n\t * Gets a promise from the cache without creating it and updates accessed time\n\t * @param key - The cache key\n\t * @returns The cached promise, or undefined if not cached or expired\n\t */\n\tget(key: K): Promise<V> | undefined {\n\t\tconst now = Date.now();\n\t\tconst entry = this.cache.get(key);\n\t\tif (entry == null || this.expired(entry, now)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// Update accessed time\n\t\tentry.accessed = now;\n\t\treturn entry.promise;\n\t}\n\n\t/**\n\t * Gets a promise from the cache or creates a new one using the factory function.\n\t *\n\t * When `cancellation` is provided:\n\t * - The factory receives an aggregate `AbortSignal` that only fires when **all** callers cancel\n\t * - The returned promise rejects with `CancellationError` if this caller's signal fires\n\t * - Other callers sharing the same cached promise are unaffected\n\t *\n\t * @param key - The cache key\n\t * @param factory - Factory function to create the value if not cached\n\t * @param cancellation - Optional per-caller AbortSignal\n\t * @param options - Optional TTL and error handling options that override the defaults\n\t */\n\tasync getOrCreate(\n\t\tkey: K,\n\t\tfactory: (cacheable: CacheController, cancellation?: AbortSignal) => Promise<V>,\n\t\toptions?: {\n\t\t\t/** Per-caller AbortSignal — the factory receives an aggregate signal that only fires when all callers cancel */\n\t\t\tcancellation?: AbortSignal;\n\t\t\t/** TTL (time-to-live) in milliseconds since creation */\n\t\t\tcreateTTL?: number;\n\t\t\t/** TTL (time-to-live) in milliseconds since last access */\n\t\t\taccessTTL?: number;\n\t\t\t/** Whether to expire the entry if the promise fails */\n\t\t\texpireOnError?: boolean;\n\t\t\t/**\n\t\t\t * Called after the factory resolves. If the predicate resolves to true, the entry is evicted while\n\t\t\t * the current caller still receives the resolved value.\n\t\t\t */\n\t\t\tevictWhen?: (value: V) => boolean | Promise<boolean>;\n\t\t\t/**\n\t\t\t * TTL (time-to-live) in milliseconds for caching errors. When the factory rejects and `errorTTL` is set,\n\t\t\t * the cache entry is replaced with `Promise.resolve(undefined)` for subsequent callers, and the error\n\t\t\t * is re-thrown to the first caller. After `errorTTL` expires, the next call retries.\n\t\t\t * Only used when `onError` is not provided.\n\t\t\t */\n\t\t\terrorTTL?: number;\n\t\t\t/**\n\t\t\t * Called when the factory promise rejects. Return a fallback result to cache\n\t\t\t * instead of expiring. The fallback value is cached with the specified `createTTL`\n\t\t\t * (defaults to the cache's `createTTL`). By default, the error is re-thrown to the\n\t\t\t * first caller after caching the fallback. Set `suppress: true` to return the fallback\n\t\t\t * value to the first caller instead. Return `undefined` to expire normally and re-throw.\n\t\t\t */\n\t\t\tonError?: (error: unknown) => { value: V; createTTL?: number; suppress?: boolean } | undefined;\n\t\t},\n\t): Promise<V> {\n\t\tconst cancellation = options?.cancellation;\n\t\tif (cancellation?.aborted) return Promise.reject(new CancellationError());\n\n\t\tconst now = Date.now();\n\n\t\toptions = { ...this.options, ...options };\n\n\t\tlet entry = this.cache.get(key);\n\t\tif (entry != null && !this.expired(entry, now)) {\n\t\t\t// Cache hit — but don't join an entry whose aggregate has already permanently aborted (all prior\n\t\t\t// callers cancelled, so the shared run is failing): that would hand this caller a doomed promise.\n\t\t\t// Fall through to create a fresh entry; the doomed entry's late settle is `deleteIfOwned`-guarded.\n\t\t\tconst existingAborts = this.aborts.get(key);\n\t\t\tif (existingAborts == null || !existingAborts.signal.aborted) {\n\t\t\t\tentry.accessed = now;\n\t\t\t\tconst cleanup = existingAborts != null ? existingAborts.add(cancellation) : undefined;\n\t\t\t\treturn this.attachCallerLifecycle(entry.promise, cancellation, cleanup);\n\t\t\t}\n\t\t}\n\n\t\tconst cacheable = new CacheController();\n\t\t// Always create the aggregate so subsequent callers (with or without signals) can contribute,\n\t\t// and so delete/invalidate has a controller to interact with\n\t\tconst abortAgg = new AbortAggregate();\n\t\tconst cleanup = abortAgg.add(cancellation);\n\t\tthis.aborts.set(key, abortAgg);\n\t\tthis.controllers.set(key, cacheable);\n\n\t\tconst promise = factory(cacheable, abortAgg.signal);\n\t\tvoid promise\n\t\t\t.finally(() => {\n\t\t\t\t// All three deletes are ownership-guarded — a hard `delete(key)` + fresh `getOrCreate` while our\n\t\t\t\t// factory is in flight installs a successor under `key`; our late settle must not evict it.\n\t\t\t\tif (cacheable.invalidated) {\n\t\t\t\t\tdeleteIfOwned(this.cache, key, entry!);\n\t\t\t\t}\n\t\t\t\tabortAgg.dispose();\n\t\t\t\tdeleteIfOwned(this.aborts, key, abortAgg);\n\t\t\t\tdeleteIfOwned(this.controllers, key, cacheable);\n\t\t\t})\n\t\t\t// Swallow the cleanup chain's rejection so it doesn't surface as an unhandled rejection\n\t\t\t// separate from the caller-facing promise's own handling (e.g. on cancellation).\n\t\t\t.catch(() => {});\n\n\t\tif (options?.onError != null) {\n\t\t\t// Wrap the promise to handle errors with the onError callback\n\t\t\tconst errorHandled = promise.catch((ex: unknown) => {\n\t\t\t\tconst result = options.onError!(ex);\n\t\t\t\t// A hard `delete(key)` + fresh `getOrCreate` while we were in flight installs a successor;\n\t\t\t\t// don't clobber it. But an EMPTY key (our entry already evicted, e.g. by the invalidated-finally)\n\t\t\t\t// still takes the fallback — preserving base behavior of caching the error fallback.\n\t\t\t\tconst occupant = this.cache.get(key);\n\t\t\t\tconst clobbersSuccessor = occupant != null && occupant !== entry;\n\t\t\t\tif (result != null) {\n\t\t\t\t\t// Replace the entry with a resolved fallback value for subsequent callers\n\t\t\t\t\tif (!clobbersSuccessor) {\n\t\t\t\t\t\tconst errorNow = Date.now();\n\t\t\t\t\t\tthis.cache.set(key, {\n\t\t\t\t\t\t\tpromise: Promise.resolve(result.value),\n\t\t\t\t\t\t\tcreated: errorNow,\n\t\t\t\t\t\t\taccessed: errorNow,\n\t\t\t\t\t\t\tcreateTTL: result.createTTL ?? options.createTTL,\n\t\t\t\t\t\t\taccessTTL: options.accessTTL,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\t// suppress: return fallback to first caller; otherwise re-throw\n\t\t\t\t\tif (result.suppress) return result.value;\n\t\t\t\t} else {\n\t\t\t\t\tdeleteIfOwned(this.cache, key, entry!);\n\t\t\t\t}\n\t\t\t\tthrow ex;\n\t\t\t});\n\n\t\t\tentry = {\n\t\t\t\tpromise: errorHandled,\n\t\t\t\tcreated: now,\n\t\t\t\taccessed: now,\n\t\t\t\tcreateTTL: options.createTTL,\n\t\t\t\taccessTTL: options.accessTTL,\n\t\t\t};\n\t\t\tthis.cache.set(key, entry);\n\t\t} else if (options?.errorTTL != null) {\n\t\t\t// Cache resolved undefined for subsequent callers, re-throw to first caller\n\t\t\tconst errorTTL = options.errorTTL;\n\t\t\tconst errorHandled = promise.catch((ex: unknown) => {\n\t\t\t\t// Overwrite unless a successor now owns the key (empty key still takes it — see onError above).\n\t\t\t\tconst occupant = this.cache.get(key);\n\t\t\t\tif (occupant == null || occupant === entry) {\n\t\t\t\t\tconst errorNow = Date.now();\n\t\t\t\t\tthis.cache.set(key, {\n\t\t\t\t\t\tpromise: Promise.resolve(undefined as V),\n\t\t\t\t\t\tcreated: errorNow,\n\t\t\t\t\t\taccessed: errorNow,\n\t\t\t\t\t\tcreateTTL: errorTTL,\n\t\t\t\t\t\taccessTTL: options.accessTTL,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tthrow ex;\n\t\t\t});\n\n\t\t\tentry = {\n\t\t\t\tpromise: errorHandled,\n\t\t\t\tcreated: now,\n\t\t\t\taccessed: now,\n\t\t\t\tcreateTTL: options.createTTL,\n\t\t\t\taccessTTL: options.accessTTL,\n\t\t\t};\n\t\t\tthis.cache.set(key, entry);\n\t\t} else {\n\t\t\tentry = {\n\t\t\t\tpromise: promise,\n\t\t\t\tcreated: now,\n\t\t\t\taccessed: now,\n\t\t\t\tcreateTTL: options.createTTL,\n\t\t\t\taccessTTL: options.accessTTL,\n\t\t\t};\n\t\t\tthis.cache.set(key, entry);\n\n\t\t\tif (options?.expireOnError ?? true) {\n\t\t\t\t// Ownership-guarded: a hard `delete(key)` + fresh `getOrCreate` replacing us while in flight must\n\t\t\t\t// not have its entry evicted by our rejection.\n\t\t\t\tconst ourEntry = entry;\n\t\t\t\tpromise.catch(() => deleteIfOwned(this.cache, key, ourEntry));\n\t\t\t}\n\t\t}\n\n\t\tconst evictWhen = options?.evictWhen;\n\t\tif (evictWhen != null) {\n\t\t\tconst candidate = entry;\n\t\t\tvoid candidate.promise\n\t\t\t\t.then(value => evictWhen(value))\n\t\t\t\t.then(evict => {\n\t\t\t\t\tif (evict) {\n\t\t\t\t\t\tdeleteIfOwned(this.cache, key, candidate);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t// A retention policy must not surface a rejection independently of the caller-facing operation.\n\t\t\t\t.catch(() => {});\n\t\t}\n\n\t\t// Clean up expired entries and enforce capacity limit in one pass\n\t\tif (this.cache.size > 1) {\n\t\t\tqueueMicrotask(() => this.cleanup());\n\t\t}\n\n\t\treturn this.attachCallerLifecycle(entry.promise, cancellation, cleanup);\n\t}\n\n\tprivate attachCallerLifecycle(\n\t\tpromise: Promise<V>,\n\t\tcancellation: AbortSignal | undefined,\n\t\tcleanup: (() => void) | undefined,\n\t): Promise<V> {\n\t\tconst returned = cancellation != null ? raceWithSignal(promise, cancellation) : promise;\n\t\tif (cleanup != null) {\n\t\t\t// Release this caller's aggregate slot when their wait ends.\n\t\t\t// Attach .catch to swallow any rejection in the cleanup chain so it doesn't surface\n\t\t\t// as an unhandled rejection separate from the caller's own handling of `returned`.\n\t\t\tvoid returned.finally(cleanup).catch(() => {});\n\t\t}\n\t\treturn returned;\n\t}\n\n\tprivate cleanup(): void {\n\t\tconst now = Date.now();\n\t\tconst capacity = this.options.capacity;\n\n\t\t// If no capacity limit, just remove expired entries\n\t\tif (capacity == null) {\n\t\t\tfor (const [key, entry] of this.cache.entries()) {\n\t\t\t\tif (this.expired(entry, now)) {\n\t\t\t\t\tthis.cache.delete(key);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Single pass: collect non-expired entries and find LRU candidates\n\t\tconst entries: Array<[K, CacheEntry<V>]> = [];\n\t\tfor (const [key, entry] of this.cache.entries()) {\n\t\t\tif (!this.expired(entry, now)) {\n\t\t\t\tentries.push([key, entry]);\n\t\t\t} else {\n\t\t\t\tthis.cache.delete(key);\n\t\t\t}\n\t\t}\n\n\t\t// If still over capacity, remove LRU entries\n\t\tconst excess = entries.length - capacity;\n\t\tif (excess > 0) {\n\t\t\t// Sort by accessed time (oldest first) and remove the excess\n\t\t\t// Note: O(N log N) sort — acceptable at current cache sizes; replace with a linked-list LRU if capacity grows\n\t\t\tentries.sort((a, b) => a[1].accessed - b[1].accessed);\n\t\t\tfor (let i = 0; i < excess; i++) {\n\t\t\t\tthis.cache.delete(entries[i][0]);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate expired(entry: CacheEntry<V>, now: number): boolean {\n\t\treturn (\n\t\t\t(entry.createTTL != null && now - entry.created >= entry.createTTL) ||\n\t\t\t(entry.accessTTL != null && now - entry.accessed >= entry.accessTTL)\n\t\t);\n\t}\n\n\t/**\n\t * Sets a promise in the cache.\n\t * The promise will use the default options from the cache constructor, unless overridden.\n\t *\n\t * @param key - The cache key\n\t * @param promise - The promise to cache\n\t * @param options - Optional TTL options that override the defaults\n\t */\n\tset(key: K, promise: Promise<V>, options?: { accessTTL?: number; createTTL?: number }): void {\n\t\tconst now = Date.now();\n\t\tconst entry: CacheEntry<V> = {\n\t\t\tpromise: promise,\n\t\t\tcreated: now,\n\t\t\taccessed: now,\n\t\t\tcreateTTL: options?.createTTL ?? this.options.createTTL,\n\t\t\taccessTTL: options?.accessTTL ?? this.options.accessTTL,\n\t\t};\n\t\tthis.cache.set(key, entry);\n\n\t\tif (this.options.expireOnError ?? true) {\n\t\t\tpromise.catch(() => deleteIfOwned(this.cache, key, entry));\n\t\t}\n\t}\n}\n\n/**\n * A Map-like wrapper for promises that automatically removes failed promises from the cache.\n * This provides a drop-in replacement for Map<K, Promise<V>> with automatic error cleanup.\n */\nexport class PromiseMap<K, V> {\n\tprivate readonly cache = new Map<K, Promise<V>>();\n\tprivate readonly aborts = new Map<K, AbortAggregate>();\n\tprivate readonly controllers = new Map<K, CacheController>();\n\t/** Each cached promise's value once it fulfills, for {@link getSettled} */\n\tprivate readonly settled = new WeakMap<Promise<V>, { value: V }>();\n\n\tget [Symbol.toStringTag](): string {\n\t\treturn 'PromiseMap';\n\t}\n\n\t/**\n\t * Gets a promise from the cache or creates a new one using the factory function.\n\t * Failed promises are automatically removed from the cache.\n\t *\n\t * When `cancellation` is provided:\n\t * - The factory receives an aggregate `AbortSignal` that only fires when **all** callers cancel\n\t * - The returned promise rejects with `CancellationError` if this caller's signal fires\n\t * - Other callers sharing the same cached promise are unaffected\n\t *\n\t * Callers without a `cancellation` signal count as \"permanent\" subscribers — the aggregate\n\t * cannot fire while at least one is active. The slot is released when the caller's returned\n\t * promise settles.\n\t */\n\tgetOrCreate(\n\t\tkey: K,\n\t\tfactory: (cacheable: CacheController, cancellation?: AbortSignal) => Promise<V>,\n\t\tcancellation?: AbortSignal,\n\t): Promise<V> {\n\t\tif (cancellation?.aborted) return Promise.reject(new CancellationError());\n\n\t\tconst cached = this.cache.get(key);\n\t\tif (cached != null) {\n\t\t\t// Cache hit — but don't join an entry whose aggregate has already permanently aborted (all prior\n\t\t\t// callers cancelled, so the shared run is failing): joining hands this caller a doomed promise.\n\t\t\t// Fall through to a fresh entry; the doomed entry's late settle is `deleteIfOwned`-guarded.\n\t\t\tconst existingAborts = this.aborts.get(key);\n\t\t\tif (existingAborts == null || !existingAborts.signal.aborted) {\n\t\t\t\tconst cleanup = existingAborts != null ? existingAborts.add(cancellation) : undefined;\n\t\t\t\treturn this.attachCallerLifecycle(cached, cancellation, cleanup);\n\t\t\t}\n\t\t}\n\n\t\t// Cache miss — always create the aggregate so subsequent callers (with or without signals)\n\t\t// can contribute, and so delete/invalidate has a controller to interact with\n\t\tconst cacheable = new CacheController();\n\t\tconst aborts = new AbortAggregate();\n\t\tconst cleanup = aborts.add(cancellation);\n\t\tthis.aborts.set(key, aborts);\n\t\tthis.controllers.set(key, cacheable);\n\n\t\tconst promise = factory(cacheable, aborts.signal);\n\t\tthis.track(key, promise);\n\t\tvoid promise\n\t\t\t.finally(() => {\n\t\t\t\tif (cacheable.invalidated) {\n\t\t\t\t\tdeleteIfOwned(this.cache, key, promise);\n\t\t\t\t}\n\t\t\t\taborts.dispose();\n\t\t\t\tdeleteIfOwned(this.aborts, key, aborts);\n\t\t\t\tdeleteIfOwned(this.controllers, key, cacheable);\n\t\t\t})\n\t\t\t// Swallow the cleanup chain's rejection so it doesn't surface as an unhandled rejection\n\t\t\t// separate from the caller-facing promise's own handling (e.g. on cancellation).\n\t\t\t.catch(() => {});\n\n\t\tthis.cache.set(key, promise);\n\n\t\treturn this.attachCallerLifecycle(promise, cancellation, cleanup);\n\t}\n\n\tprivate attachCallerLifecycle(\n\t\tpromise: Promise<V>,\n\t\tcancellation: AbortSignal | undefined,\n\t\tcleanup: (() => void) | undefined,\n\t): Promise<V> {\n\t\tconst returned = cancellation != null ? raceWithSignal(promise, cancellation) : promise;\n\t\tif (cleanup != null) {\n\t\t\t// Release this caller's aggregate slot when their wait ends.\n\t\t\t// Attach .catch to swallow any rejection in the cleanup chain so it doesn't surface\n\t\t\t// as an unhandled rejection separate from the caller's own handling of `returned`.\n\t\t\tvoid returned.finally(cleanup).catch(() => {});\n\t\t}\n\t\treturn returned;\n\t}\n\n\t/**\n\t * Marks a cached entry as invalidated.\n\t * - If the entry has a `CacheController` (created via `getOrCreate`), it is soft-invalidated:\n\t * existing waiters complete normally, new callers joining during the remainder of the\n\t * in-flight operation share the same promise, and the entry self-evicts on settle.\n\t * - If the entry was set via plain `.set()` (no controller), it is hard-deleted — there is\n\t * no in-flight factory to ride along with.\n\t *\n\t * No-op if the key is not cached.\n\t */\n\tinvalidate(key: K): void {\n\t\tif (this.controllers.has(key)) {\n\t\t\tthis.controllers.get(key)!.invalidate();\n\t\t} else {\n\t\t\tthis.delete(key);\n\t\t}\n\t}\n\n\t/**\n\t * Gets a promise from the cache, or undefined if not found.\n\t */\n\tget(key: K): Promise<V> | undefined {\n\t\treturn this.cache.get(key);\n\t}\n\n\t/**\n\t * Sets a promise in the cache. The promise will be automatically removed if it fails.\n\t */\n\tset(key: K, promise: Promise<V>): this {\n\t\tthis.cache.set(key, promise);\n\t\tthis.track(key, promise);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * The value `key`'s cached promise fulfilled with, or `undefined` while it is pending (or nothing is cached), so a\n\t * caller can tell a settled entry from a read still in flight without joining it.\n\t */\n\tgetSettled(key: K): { value: V } | undefined {\n\t\tconst promise = this.cache.get(key);\n\t\treturn promise != null ? this.settled.get(promise) : undefined;\n\t}\n\n\tprivate track(key: K, promise: Promise<V>): void {\n\t\tpromise.then(\n\t\t\tvalue => {\n\t\t\t\tthis.settled.set(promise, { value: value });\n\t\t\t},\n\t\t\t// Automatically remove failed promises from the cache (ownership-guarded — see `deleteIfOwned`).\n\t\t\t() => deleteIfOwned(this.cache, key, promise),\n\t\t);\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t */\n\thas(key: K): boolean {\n\t\treturn this.cache.has(key);\n\t}\n\n\t/**\n\t * Removes a key from the cache.\n\t * Does NOT dispose the abort aggregate: while a factory is in flight, disposing would detach\n\t * its callers' cancellation and orphan the underlying op (a later caller-abort could no longer cancel the\n\t * running `git status`). The factory's own settle handler disposes the aggregate (see `getOrCreate`). */\n\tdelete(key: K): boolean {\n\t\tthis.aborts.delete(key);\n\t\tthis.controllers.delete(key);\n\t\treturn this.cache.delete(key);\n\t}\n\n\t/**\n\t * Clears all entries. Like `delete`, does NOT dispose in-flight abort aggregates — that would detach a running\n\t * op's cancellation (a later caller-abort couldn't cancel it); each factory's settle handler disposes its own.\n\t */\n\tclear(): void {\n\t\tthis.aborts.clear();\n\t\tthis.controllers.clear();\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Returns the number of entries in the cache.\n\t */\n\tget size(): number {\n\t\treturn this.cache.size;\n\t}\n\n\t/**\n\t * Returns an iterator for the cache keys.\n\t */\n\tkeys(): IterableIterator<K> {\n\t\treturn this.cache.keys();\n\t}\n\n\t/**\n\t * Returns an iterator for the cache values.\n\t */\n\tvalues(): IterableIterator<Promise<V>> {\n\t\treturn this.cache.values();\n\t}\n\n\t/**\n\t * Returns an iterator for the cache entries.\n\t */\n\tentries(): IterableIterator<[K, Promise<V>]> {\n\t\treturn this.cache.entries();\n\t}\n\n\t/**\n\t * Executes a provided function once for each cache entry.\n\t */\n\tforEach(callbackfn: (value: Promise<V>, key: K, map: Map<K, Promise<V>>) => void, thisArg?: any): void {\n\t\tthis.cache.forEach(callbackfn, thisArg);\n\t}\n\n\t[Symbol.iterator](): IterableIterator<[K, Promise<V>]> {\n\t\treturn this.cache[Symbol.iterator]();\n\t}\n}\n\n/**\n * A two-level cache that organizes PromiseCaches by repository path.\n * Automatically creates and manages inner PromiseCaches per repository.\n *\n * This is useful for caching data that varies by both repository and some other key,\n * with TTL and capacity management provided by PromiseCache.\n */\nexport class RepoPromiseCacheMap<K, V> {\n\tprivate readonly cache = new Map<string, PromiseCache<K, V>>();\n\tprivate readonly options: PromiseCacheOptions;\n\n\tconstructor(options?: PromiseCacheOptions) {\n\t\tthis.options = { expireOnError: true, ...options };\n\t}\n\n\tget [Symbol.toStringTag](): string {\n\t\treturn 'RepoPromiseCacheMap';\n\t}\n\n\t/**\n\t * Gets a promise from the cache or creates a new one using the factory function.\n\t * Automatically creates the inner PromiseCache for the repository if it doesn't exist.\n\t *\n\t * @param repoPath - The repository path (outer key)\n\t * @param key - The cache key within the repository (inner key)\n\t * @param factory - Factory function to create the value if not cached\n\t * @param options - Optional TTL and error handling options that override the defaults\n\t */\n\tgetOrCreate(\n\t\trepoPath: string,\n\t\tkey: K,\n\t\tfactory: (cacheable: CacheController, cancellation?: AbortSignal) => Promise<V>,\n\t\toptions?: {\n\t\t\t/** Per-caller AbortSignal — the factory receives an aggregate signal that only fires when all callers cancel */\n\t\t\tcancellation?: AbortSignal;\n\t\t\t/** TTL (time-to-live) in milliseconds since creation */\n\t\t\tcreateTTL?: number;\n\t\t\t/** TTL (time-to-live) in milliseconds since last access */\n\t\t\taccessTTL?: number;\n\t\t\t/** Whether to expire the entry if the promise fails */\n\t\t\texpireOnError?: boolean;\n\t\t\t/**\n\t\t\t * Called after the factory resolves. If the predicate resolves to true, the entry is evicted while\n\t\t\t * the current caller still receives the resolved value.\n\t\t\t */\n\t\t\tevictWhen?: (value: V) => boolean | Promise<boolean>;\n\t\t\t/**\n\t\t\t * TTL (time-to-live) in milliseconds for caching errors. When the factory rejects and `errorTTL` is set,\n\t\t\t * the cache entry is replaced with `Promise.resolve(undefined)` for subsequent callers, and the error\n\t\t\t * is re-thrown to the first caller. After `errorTTL` expires, the next call retries.\n\t\t\t * Only used when `onError` is not provided.\n\t\t\t */\n\t\t\terrorTTL?: number;\n\t\t\t/**\n\t\t\t * Called when the factory promise rejects. Return a fallback result to cache\n\t\t\t * instead of expiring. The fallback value is cached with the specified `createTTL`\n\t\t\t * (defaults to the cache's `createTTL`). By default, the error is re-thrown to the\n\t\t\t * first caller after caching the fallback. Set `suppress: true` to return the fallback\n\t\t\t * value to the first caller instead. Return `undefined` to expire normally and re-throw.\n\t\t\t */\n\t\t\tonError?: (error: unknown) => { value: V; createTTL?: number; suppress?: boolean } | undefined;\n\t\t},\n\t): Promise<V> {\n\t\tlet repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) {\n\t\t\trepoCache = new PromiseCache<K, V>(this.options);\n\t\t\tthis.cache.set(repoPath, repoCache);\n\t\t}\n\n\t\treturn repoCache.getOrCreate(key, factory, options);\n\t}\n\n\t/**\n\t * Gets a promise from the cache without creating it.\n\t *\n\t * @param repoPath - The repository path (outer key)\n\t * @param key - The cache key within the repository (inner key)\n\t * @returns The cached promise, or undefined if not cached\n\t */\n\tget(repoPath: string, key: K): Promise<V> | undefined {\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) return undefined;\n\n\t\treturn repoCache.get(key);\n\t}\n\n\t/**\n\t * Sets a promise in the cache.\n\t *\n\t * @param repoPath - The repository path (outer key)\n\t * @param key - The cache key within the repository (inner key)\n\t * @param promise - The promise to cache\n\t * @param options - Optional TTL options that override the defaults\n\t */\n\tset(repoPath: string, key: K, promise: Promise<V>, options?: { accessTTL?: number; createTTL?: number }): void {\n\t\tlet repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) {\n\t\t\trepoCache = new PromiseCache<K, V>(this.options);\n\t\t\tthis.cache.set(repoPath, repoCache);\n\t\t}\n\n\t\trepoCache.set(key, promise, options);\n\t}\n\n\t/**\n\t * Deletes a specific key from a repository's cache, or all keys for a repository.\n\t *\n\t * @param repoPath - The repository path\n\t * @param key - Optional specific key to delete. If omitted, deletes the entire repository cache.\n\t * @returns true if something was deleted, false otherwise\n\t */\n\tdelete(repoPath: string, key?: K): boolean {\n\t\tif (key === undefined) {\n\t\t\treturn this.cache.delete(repoPath);\n\t\t}\n\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) return false;\n\n\t\trepoCache.delete(key);\n\t\treturn true;\n\t}\n\n\t/**\n\t * Marks a cached entry as invalidated. See `PromiseCache.invalidate` for semantics.\n\t *\n\t * @param repoPath - The repository path\n\t * @param key - Optional specific key to invalidate. If omitted, invalidates every entry for the repository.\n\t */\n\tinvalidate(repoPath: string, key?: K): void {\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) return;\n\n\t\tif (key === undefined) {\n\t\t\tfor (const k of repoCache.keys()) {\n\t\t\t\trepoCache.invalidate(k);\n\t\t\t}\n\t\t} else {\n\t\t\trepoCache.invalidate(key);\n\t\t}\n\t}\n\n\t/**\n\t * Deletes all keys matching a prefix from a repository's cache.\n\t * @param repoPath - The repository path\n\t * @param prefix - Key prefix to match\n\t * @returns true if something was deleted\n\t */\n\tdeleteByKeyPrefix(this: RepoPromiseCacheMap<string, V>, repoPath: string, prefix: string): boolean {\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) return false;\n\n\t\tlet deleted = false;\n\t\tfor (const key of repoCache.keys()) {\n\t\t\tif (key.startsWith(prefix)) {\n\t\t\t\trepoCache.delete(key);\n\t\t\t\tdeleted = true;\n\t\t\t}\n\t\t}\n\t\treturn deleted;\n\t}\n\n\t/**\n\t * Invalidates all keys matching a prefix from a repository's cache. See `PromiseCache.invalidate`\n\t * for semantics — in-flight entries are marked invalidated and shared with new callers; settled\n\t * entries are hard-deleted. Prefer over `deleteByKeyPrefix` when the eviction is a \"data has\n\t * changed\" event and in-flight factories' results would still be valid for current waiters.\n\t * @param repoPath - The repository path\n\t * @param prefix - Key prefix to match\n\t * @returns true if something was invalidated\n\t */\n\tinvalidateByKeyPrefix(this: RepoPromiseCacheMap<string, V>, repoPath: string, prefix: string): boolean {\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) return false;\n\n\t\tlet invalidated = false;\n\t\tfor (const key of repoCache.keys()) {\n\t\t\tif (key.startsWith(prefix)) {\n\t\t\t\trepoCache.invalidate(key);\n\t\t\t\tinvalidated = true;\n\t\t\t}\n\t\t}\n\t\treturn invalidated;\n\t}\n\n\t/**\n\t * Clears all caches for all repositories.\n\t */\n\tclear(): void {\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Returns the total number of entries across all repository caches.\n\t */\n\tget entryCount(): number {\n\t\tlet count = 0;\n\t\tfor (const repoCache of this.cache.values()) {\n\t\t\tcount += repoCache.size;\n\t\t}\n\t\treturn count;\n\t}\n\n\t/**\n\t * Returns the number of repositories in the cache.\n\t */\n\tget size(): number {\n\t\treturn this.cache.size;\n\t}\n}\n\n/**\n * A two-level cache that organizes PromiseMaps by repository path.\n * Automatically creates and manages inner PromiseMaps per repository.\n *\n * This is useful for caching data that varies by both repository and some other key,\n * with simple promise deduplication and automatic error cleanup.\n */\nexport class RepoPromiseMap<K, V> {\n\tprivate readonly cache = new Map<string, PromiseMap<K, V>>();\n\n\tget [Symbol.toStringTag](): string {\n\t\treturn 'RepoPromiseMap';\n\t}\n\n\t/**\n\t * Gets a promise from the cache or creates a new one using the factory function.\n\t * Automatically creates the inner PromiseMap for the repository if it doesn't exist.\n\t *\n\t * @param repoPath - The repository path (outer key)\n\t * @param key - The cache key within the repository (inner key)\n\t * @param factory - Factory function to create the value if not cached\n\t */\n\tgetOrCreate(\n\t\trepoPath: string,\n\t\tkey: K,\n\t\tfactory: (cacheable: CacheController, cancellation?: AbortSignal) => Promise<V>,\n\t\tcancellation?: AbortSignal,\n\t): Promise<V> {\n\t\tlet repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) {\n\t\t\trepoCache = new PromiseMap<K, V>();\n\t\t\tthis.cache.set(repoPath, repoCache);\n\t\t}\n\n\t\treturn repoCache.getOrCreate(key, factory, cancellation);\n\t}\n\n\t/**\n\t * Gets a promise from the cache without creating it.\n\t *\n\t * @param repoPath - The repository path (outer key)\n\t * @param key - The cache key within the repository (inner key)\n\t * @returns The cached promise, or undefined if not found\n\t */\n\tget(repoPath: string, key: K): Promise<V> | undefined {\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\treturn repoCache?.get(key);\n\t}\n\n\t/**\n\t * Deletes a specific key from a repository's cache, or all keys for a repository.\n\t *\n\t * @param repoPath - The repository path\n\t * @param key - Optional specific key to delete. If omitted, deletes the entire repository cache.\n\t * @returns true if something was deleted, false otherwise\n\t */\n\tdelete(repoPath: string, key?: K): boolean {\n\t\tif (key === undefined) {\n\t\t\treturn this.cache.delete(repoPath);\n\t\t}\n\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) return false;\n\n\t\treturn repoCache.delete(key);\n\t}\n\n\t/**\n\t * Marks a cached entry as invalidated. See `PromiseMap.invalidate` for semantics.\n\t *\n\t * @param repoPath - The repository path\n\t * @param key - Optional specific key to invalidate. If omitted, invalidates every entry for the repository.\n\t */\n\tinvalidate(repoPath: string, key?: K): void {\n\t\tconst repoCache = this.cache.get(repoPath);\n\t\tif (repoCache == null) return;\n\n\t\tif (key === undefined) {\n\t\t\tfor (const k of repoCache.keys()) {\n\t\t\t\trepoCache.invalidate(k);\n\t\t\t}\n\t\t} else {\n\t\t\trepoCache.invalidate(key);\n\t\t}\n\t}\n\n\t/**\n\t * Clears all caches for all repositories.\n\t */\n\tclear(): void {\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Returns the number of repositories in the cache.\n\t */\n\tget size(): number {\n\t\treturn this.cache.size;\n\t}\n}\n"],"mappings":"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"}
|
package/docs/integrations.md
CHANGED
|
@@ -746,14 +746,22 @@ result** instead of rejecting the call. One provider's expired token never blank
|
|
|
746
746
|
`ProviderWarning.kind` (also exported as `ProviderWarningKind`) carries the classifications the facade can
|
|
747
747
|
prove from structured errors:
|
|
748
748
|
|
|
749
|
-
| `kind`
|
|
750
|
-
|
|
|
751
|
-
| `auth`
|
|
752
|
-
| `rate-limit`
|
|
753
|
-
| `not-found`
|
|
754
|
-
| `no-connection`
|
|
755
|
-
| `unsupported`
|
|
756
|
-
| `
|
|
749
|
+
| `kind` | Meaning | Reasonable response |
|
|
750
|
+
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
751
|
+
| `auth` | Token rejected (401/403 that isn't a throttle). | Prompt to reconnect that connection. A scoped one is narrower: see `scope` below. |
|
|
752
|
+
| `rate-limit` | Throttled (429, or a 403 whose body says so). | Back off and retry; keep the last snapshot. |
|
|
753
|
+
| `not-found` | 404/410/422 on the requested scope. | Drop that scope; don't reconnect. |
|
|
754
|
+
| `no-connection` | The requested `connectionId`/`domain` doesn't resolve. | Re-resolve the target or re-authenticate. |
|
|
755
|
+
| `unsupported` | The provider can't serve this capability: a surface, filter, sort, state, search criterion or option combination it can't express. | Hide or disable the feature for that provider; don't retry or reconnect. |
|
|
756
|
+
| `unreadable-items` | Items the provider returned that couldn't be read were left out (a Jira issue `@gitkraken/provider-apis` can't parse); the rest of its `scope` was read. `unreadableItems` names them. | Show the read as incomplete; retrying returns the same gap. A cut read always adds a warning of another kind. |
|
|
757
|
+
| `other` | Catch-all: malformed or contradictory input, truncation, upstream/network failure, or an unclassified error. Read `omission` before treating one as a failure. | Preserve the warning and use the result flags; do not assume it is benign or non-retryable. |
|
|
758
|
+
|
|
759
|
+
An `unreadable-items` warning carries `unreadableItems: { keys, complete }`: the identifiers of the items left out (Jira
|
|
760
|
+
issue keys), and whether `keys` names all of them. `complete` is `false` when the provider named only some (Jira names
|
|
761
|
+
at most ten per page) or named one by its position, so an identifier absent from `keys` is proven read only when every
|
|
762
|
+
such warning is `complete`. The read still sets `fetchFailed`, but it retracts no omission: no request failed. On an
|
|
763
|
+
issue tracker's project reads it never stands for a read that was also cut short: a project or relationship stopped
|
|
764
|
+
by the page backstop, a stalled continuation or a failed page always adds a warning of another kind for it.
|
|
757
765
|
|
|
758
766
|
`isAuth` is a convenience mirror of `kind === 'auth'`. **Collapsing `kind` into that boolean loses the
|
|
759
767
|
rate-limit and not-found distinctions**, which then have to be re-derived from raw provider prose.
|
|
@@ -1218,13 +1226,20 @@ requires), keyed on the raw `System.State`, and caches each read for five minute
|
|
|
1218
1226
|
credential. The category, never the name, decides: a state named `Canceled` in the `Completed` category is `DONE`, and
|
|
1219
1227
|
a state its type doesn't list has no category. When a states read fails, a project list keeps its rows (the failed
|
|
1220
1228
|
type's without a category) and reports the project as a partial collection with a failure scoped to it; a read by id
|
|
1221
|
-
and the cheap etag check fail instead.
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1229
|
+
and the cheap etag check fail instead. List selection uses the same metadata before pagination, matching both
|
|
1230
|
+
work item type and state name. A closed read includes Removed, Completed and Resolved categories even without closed
|
|
1231
|
+
or resolved dates. Failed workflows fall back to dates with partial metadata; failures are cached for 30 seconds
|
|
1232
|
+
before retrying. List, identity and native presentation reads share one cache, including workflow expiry and HTTP
|
|
1233
|
+
proxy invalidation.
|
|
1225
1234
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1235
|
+
`listIssuesPage` accepts `state: 'open' | 'closed' | 'all'` for Azure DevOps Services and Server, on repository,
|
|
1236
|
+
project and account-wide reads. Omitted means open only. `getSupportedFilters(providerId).issueStates` declares this
|
|
1237
|
+
capability. Other git hosts retain their open-only list default and refuse non-open state requests on this facade.
|
|
1238
|
+
|
|
1239
|
+
A known Azure workflow category determines normalized `state` and `closed`, with `CANCELED` closing like `DONE`.
|
|
1240
|
+
A historical closed date remains available as data but does not close an issue whose category is now open.
|
|
1241
|
+
Identity and list reads fall back to the closed date only when the category is unknown. Other providers keep their
|
|
1242
|
+
existing derivation.
|
|
1228
1243
|
|
|
1229
1244
|
### Sprints and iterations
|
|
1230
1245
|
|
|
@@ -1302,14 +1317,27 @@ Markdown by this package.
|
|
|
1302
1317
|
- **Azure DevOps** — org + project scoped. Repo-scoped reads accept one org per call. Account-wide reads
|
|
1303
1318
|
drain every project of every org and return one aggregate page; a failed project becomes a scoped warning
|
|
1304
1319
|
while its siblings survive. Only Azure can narrow an account-wide issue read by `org`/`project`.
|
|
1305
|
-
`resolveRepository` needs a project in the remote URL
|
|
1320
|
+
`resolveRepository` needs a project in the remote URL; a repository named after its project is also served without
|
|
1321
|
+
it (`{collection}/_git/{repo}`, the clone URL TFS 2018 reports), which reads as that project. Azure DevOps Server uses the trusted connection's
|
|
1306
1322
|
`baseUrl`, including its installation path. A repository's virtual directory must match that path and is
|
|
1307
1323
|
applied once; a connection addressed at the host root takes the repository's own virtual directory. An address
|
|
1308
1324
|
that also names a collection (`https://server/tfs/DefaultCollection`) reads that collection without repeating it:
|
|
1309
1325
|
discovery reports it as the one collection visible there, since Azure only lists collections at the server level,
|
|
1310
1326
|
and another collection's repositories need an address without the collection. Installation paths match case-insensitively, as IIS serves them. An SSH remote
|
|
1311
1327
|
that names no virtual directory is read against the whole address, since it can't tell a virtual directory from
|
|
1312
|
-
another collection named there.
|
|
1328
|
+
another collection named there. Behind a virtual directory the project-less form, `{vdir}/{collection}/_git/{repo}`,
|
|
1329
|
+
is equally a full path whose collection is `{vdir}` and project `{collection}`. When the connection's address is that
|
|
1330
|
+
virtual directory (or its parent, for an address naming the collection), the integration settles it against the
|
|
1331
|
+
collections discovery reports, over HTTPS and SSH alike, reading it as the project-less form only when the segment
|
|
1332
|
+
before `_git` is a listed collection and the one before it isn't; any other path reads as parsed, without discovery.
|
|
1333
|
+
When the collections can't be read the path is read as parsed, and a read that finds nothing then fails rather than
|
|
1334
|
+
answer, so the miss isn't cached.
|
|
1335
|
+
A repository scope a consumer passes (`repos` on the repo-scoped, search and count reads, and the `owner`/`project`/`repo`
|
|
1336
|
+
of the batch and branch reads) is settled against the same collections when it splits the path as the remote parser
|
|
1337
|
+
does: collection `{vdir}` and project `{collection}`, with the connection addressed at `{vdir}` (or at
|
|
1338
|
+
`{vdir}/{collection}`). The repository name is required for it, since it names the project. A scope that names its
|
|
1339
|
+
collection and project is read as given. So is one whose collections can't be read, and a batch or branch read that
|
|
1340
|
+
then finds nothing for it fails that target rather than report it absent.
|
|
1313
1341
|
|
|
1314
1342
|
Azure DevOps Server also has the filtered searches and their counts, with the collection applied exactly once
|
|
1315
1343
|
whichever way the address is written. Scope names are encoded as URL segments (collections, projects,
|
|
@@ -1351,10 +1379,13 @@ Markdown by this package.
|
|
|
1351
1379
|
|
|
1352
1380
|
- **Jira (Cloud + Data Center) / Linear / Trello** — paged by **project**, not by issue: `itemsPerPage` counts projects (default
|
|
1353
1381
|
20), each drained in full. Passing none of `page`/`cursor`/`itemsPerPage` aggregates every matched project
|
|
1354
|
-
in one page. A single project exceeding its internal drain backstop shows up as `page.truncated`.
|
|
1382
|
+
in one page. A single project exceeding its internal drain backstop shows up as `page.truncated`. On Jira it gets a
|
|
1383
|
+
warning of its own, naming it in its message and its `omission.scope`, published even when another project failed
|
|
1384
|
+
(when a request of the read failed, the omission is retracted as on every read, and only the message names it);
|
|
1385
|
+
Linear and Trello report one warning for the page, only when nothing failed.
|
|
1355
1386
|
A Jira issue `@gitkraken/provider-apis` can't read is dropped from its page and reported instead: its siblings are
|
|
1356
|
-
served, with `fetchFailed`,
|
|
1357
|
-
that dropped one is not retried.
|
|
1387
|
+
served, with `fetchFailed`, an `unreadable-items` warning scoped to its project and naming its key, and
|
|
1388
|
+
`page.truncated` (never `narrow-scope`); a project that dropped one is not retried.
|
|
1358
1389
|
Trello's issue `id` is the card's `idShort` — unique per board only, so correlate across boards by
|
|
1359
1390
|
`nodeId`. For Trello, boards are both the resource and the project, so `listOrgs` and `listProjects`
|
|
1360
1391
|
return the same set; Linear's resource is the organization and its projects are teams.
|
|
@@ -1417,7 +1448,8 @@ const metadata = await api.getRepositoryMetadata('gitkraken', 'vscode-gitlens');
|
|
|
1417
1448
|
Supports `getRepositoryMetadata` + `getDefaultBranch` for GitHub/GHE, GitLab/self-hosted, Bitbucket Cloud,
|
|
1418
1449
|
and Azure DevOps (+ Server). **Not** Bitbucket Data Center. A self-managed id requires `token.domain`
|
|
1419
1450
|
(it throws otherwise, rather than building a malformed base URL). Azure encodes the repo as
|
|
1420
|
-
`"{project}/_git/{repoName}"
|
|
1451
|
+
`"{project}/_git/{repoName}"`; a repository named after its project and addressed without the project is
|
|
1452
|
+
`"{repoName}/_git/{repoName}"`.
|
|
1421
1453
|
|
|
1422
1454
|
## 11. Checklist for a new consumer
|
|
1423
1455
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitkraken/core-gitlens",
|
|
3
3
|
"description": "GitLens core — shared Git / AI / GitHub primitives for internal GitKraken consumption",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.14.0",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "GitKraken",
|
|
@@ -239,7 +239,7 @@
|
|
|
239
239
|
}
|
|
240
240
|
},
|
|
241
241
|
"dependencies": {
|
|
242
|
-
"@gitkraken/provider-apis": "0.65.
|
|
242
|
+
"@gitkraken/provider-apis": "0.65.3",
|
|
243
243
|
"@octokit/graphql": "9.0.4",
|
|
244
244
|
"@octokit/request": "10.0.14",
|
|
245
245
|
"@octokit/request-error": "7.1.1",
|