@gitkraken/core-gitlens 0.22.0 → 0.23.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 +16 -1
- package/dist/git-cli/exec/git.js +1 -1
- package/dist/git-cli/providers/config.js +2 -2
- package/dist/git-cli/providers/pausedOperations.js +2 -2
- package/dist/git-cli/providers/pausedOperations.js.map +1 -1
- package/dist/git-cli/providers/staging.js +1 -1
- package/dist/ipc/discovery.js +1 -1
- package/dist/plus/agents/providers/claudeCodeTranscript.js +6 -6
- package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
- package/dist/plus/ai/providers/openAICompatibleProviderBase.js +1 -1
- package/dist/plus/integrations/index.d.ts +3 -3
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +10 -2
- package/dist/plus/integrations/integrationService.js +9 -1
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +25 -1
- package/dist/plus/integrations/models/issueReads.d.ts +5 -0
- package/dist/plus/integrations/providers/azure/azure.js +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +2 -2
- package/dist/plus/integrations/providers/jira.js +2 -2
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.d.ts +35 -2
- package/dist/plus/integrations/providers/linear.js +106 -9
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +4 -1
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providerApiClient.js +4 -1
- package/dist/plus/integrations/providers/providerApiClient.js.map +1 -1
- package/dist/plus/integrations/providers/providerErrors.d.ts +4 -1
- package/dist/plus/integrations/providers/providerErrors.js +13 -4
- package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +7 -1
- package/dist/plus/integrations/providers/providersApi.js +9 -1
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/reads/cursors.d.ts +8 -0
- package/dist/plus/integrations/reads/cursors.js +14 -4
- package/dist/plus/integrations/reads/cursors.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.d.ts +8 -2
- package/dist/plus/integrations/reads/hierarchy.js +42 -2
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts +10 -0
- package/dist/plus/integrations/reads/issueTracker.js +46 -5
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/selectionConfirmation.d.ts +5 -3
- package/dist/plus/integrations/reads/selectionConfirmation.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.js +1 -1
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +18 -2
- package/dist/plus/integrations/results.js.map +1 -1
- package/dist/plus/integrations/utils/integration.utils.d.ts +3 -0
- package/dist/plus/integrations/utils/integration.utils.js +5 -1
- package/dist/plus/integrations/utils/integration.utils.js.map +1 -1
- package/dist/utils/env/node/exec.js +2 -2
- package/docs/integrations.md +37 -21
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { PullRequestFilter, PullRequestSearchCriteria, PullRequestShape, PullReq
|
|
|
5
5
|
import { ConfiguredIntegrationDescriptor } from "./authentication/models.js";
|
|
6
6
|
import { IntegrationIds, IssuesCloudHostIntegrationId, IssuesSelfManagedHostIntegrationId } from "./constants.js";
|
|
7
7
|
import { ConfiguredIntegrationsChangeEvent } from "./authentication/configuredIntegrationService.js";
|
|
8
|
-
import { ConnectionStateChangeEvent, ProviderBroadenResult, ProviderOrganization, ProviderPagedResult, ProviderRepositoryShape, ProviderResult, ProviderSweepResult, ResolveRepositoryResult } from "./results.js";
|
|
8
|
+
import { ConnectionStateChangeEvent, ProviderBroadenResult, ProviderLinearProjectShape, ProviderOrganization, ProviderPagedResult, ProviderRepositoryShape, ProviderResult, ProviderSweepResult, ResolveRepositoryResult } from "./results.js";
|
|
9
9
|
import { IssueFilter } from "./providerFilters.js";
|
|
10
10
|
import { IssueEtagInclude, PullRequestEtagInclude } from "./models/integration.js";
|
|
11
11
|
import { IssueCountResult, IssueCountScope, PullRequestCountResult, PullRequestCountScope } from "./reads/counts.js";
|
|
@@ -223,6 +223,12 @@ export type GetAutolinkReferencesOptions = {
|
|
|
223
223
|
/** Drops discovery and awaits a fresh build; retires builds started before the refresh. */
|
|
224
224
|
forceSync?: boolean;
|
|
225
225
|
};
|
|
226
|
+
/** Linear Project discovery reads one Linear connection: the primary, or the one `connectionId` names. */
|
|
227
|
+
export type ListLinearProjectsOptions = {
|
|
228
|
+
connectionId?: string;
|
|
229
|
+
/** Drops the cached Project list and re-syncs the connection before reading, as the other reads' `forceSync`. */
|
|
230
|
+
forceSync?: boolean;
|
|
231
|
+
};
|
|
226
232
|
/**
|
|
227
233
|
* Public, provider-neutral integration facade. Provider clients and integration model instances remain private
|
|
228
234
|
* implementation details so SDK changes don't expand this contract.
|
|
@@ -298,6 +304,16 @@ export interface IntegrationManager {
|
|
|
298
304
|
* references can contain compiled regular expressions and are not a wire format.
|
|
299
305
|
*/
|
|
300
306
|
getAutolinkReferences(options: GetAutolinkReferencesOptions): Promise<RefSet | undefined>;
|
|
307
|
+
/**
|
|
308
|
+
* The Linear workspace's Projects: Linear's own `Project` entity, which can span several teams. Not what
|
|
309
|
+
* {@link listProjects} returns for Linear, which is its teams (the tier a Linear issue read is windowed by).
|
|
310
|
+
* Pass a Project's `id` to {@link listIssueTrackerIssuesPage}'s `linearProjects` to narrow an issue read to it.
|
|
311
|
+
*
|
|
312
|
+
* Drained across Linear's pages (100 Projects each, up to 2,000) and cached per connection like the team list,
|
|
313
|
+
* for five minutes or until `forceSync`. A list the drain could not finish comes back with what it read, a
|
|
314
|
+
* warning and `fetchFailed`, and is not cached.
|
|
315
|
+
*/
|
|
316
|
+
listLinearProjects(options?: ListLinearProjectsOptions): Promise<ProviderResult<ProviderLinearProjectShape>>;
|
|
301
317
|
listRepos(options: {
|
|
302
318
|
providerId: IntegrationIds;
|
|
303
319
|
org?: string;
|
|
@@ -824,6 +840,14 @@ export interface IntegrationManager {
|
|
|
824
840
|
* `getSupportedFilters().issueStates` and are refused otherwise (warning + `fetchFailed`).
|
|
825
841
|
*/
|
|
826
842
|
state?: IssueStateFilter;
|
|
843
|
+
/**
|
|
844
|
+
* Linear only: narrows the read to issues in any of these Linear Projects, by the `id`
|
|
845
|
+
* {@link listLinearProjects} lists. Combines with `org`/`project` (a team), `includeAllAssignees`, `state`
|
|
846
|
+
* and `sort`: the read stays windowed by team, and each team's issues are those in the Projects. Omitted or
|
|
847
|
+
* empty narrows nothing; any other tracker refuses it (warning + `fetchFailed`). Part of a cursor's
|
|
848
|
+
* fingerprint, so drop the cursor when you change it.
|
|
849
|
+
*/
|
|
850
|
+
linearProjects?: string[];
|
|
827
851
|
forceSync?: boolean;
|
|
828
852
|
page?: number;
|
|
829
853
|
/**
|
|
@@ -99,6 +99,11 @@ export type IssuesForProjectOptions = {
|
|
|
99
99
|
* (Trello) refuses anything but `'open'` rather than serving its open cards as if they were the closed ones.
|
|
100
100
|
*/
|
|
101
101
|
state?: IssueStateFilter;
|
|
102
|
+
/**
|
|
103
|
+
* Linear only: narrows the read to issues in any of these Linear Projects (Linear's `Project` entity, by id),
|
|
104
|
+
* server-side. Validated by the facade, which refuses it for any other tracker; omitted or empty adds nothing.
|
|
105
|
+
*/
|
|
106
|
+
linearProjects?: readonly string[];
|
|
102
107
|
};
|
|
103
108
|
/** One project of a batched tracker read (`IssuesIntegration.getIssuesForProjectsWithTruncationResult`). */
|
|
104
109
|
export type ProjectIssuesRequest<T> = {
|
|
@@ -6,9 +6,9 @@ import { trace } from "../../../../utils/decorators/log.js";
|
|
|
6
6
|
import { PromiseCache } from "../../../../utils/promiseCache.js";
|
|
7
7
|
import __decorate from "../../../../_virtual/@oxc-project+runtime/helpers/esm/decorate.js";
|
|
8
8
|
import { AuthenticationError, RequestClientError, RequestNotFoundError } from "../../../../git/errors.js";
|
|
9
|
+
import { sha256, uuid } from "../../../../utils/crypto.js";
|
|
9
10
|
import { maybeStopWatch } from "../../../../utils/stopwatch.js";
|
|
10
11
|
import { base64 } from "../../../../utils/base64.js";
|
|
11
|
-
import { sha256, uuid } from "../../../../utils/crypto.js";
|
|
12
12
|
import { effectiveIssueSort } from "../../../../git/utils/issue.utils.js";
|
|
13
13
|
import { ProviderFetchError, isRateLimitResponse, toRateLimitError } from "../../errors.js";
|
|
14
14
|
import { throwProviderError } from "../providerErrors.js";
|
|
@@ -106,8 +106,8 @@ export declare class JiraIntegration extends IssuesIntegration<IssuesCloudHostIn
|
|
|
106
106
|
*
|
|
107
107
|
* Never proves an absence. Jira silently omits a key it can't find or show (listing it in no `issueErrors` either),
|
|
108
108
|
* and answers a moved issue under its new key, which no longer matches the key asked for. Either way the target's
|
|
109
|
-
* slot is rejected, so the full read
|
|
110
|
-
* nothing failed, the check declines
|
|
109
|
+
* slot is rejected, so the full read distinguishes a missing issue from a site failure. When no target was
|
|
110
|
+
* answered and nothing failed, the check declines, so a batch of only such keys costs a full read, not a failure.
|
|
111
111
|
*/
|
|
112
112
|
protected getProviderIssuesEtagFieldsByResourceId(session: ProviderAuthenticationSession, targets: readonly {
|
|
113
113
|
resourceId: string;
|
|
@@ -428,8 +428,8 @@ var JiraIntegration = class extends IssuesIntegration {
|
|
|
428
428
|
*
|
|
429
429
|
* Never proves an absence. Jira silently omits a key it can't find or show (listing it in no `issueErrors` either),
|
|
430
430
|
* and answers a moved issue under its new key, which no longer matches the key asked for. Either way the target's
|
|
431
|
-
* slot is rejected, so the full read
|
|
432
|
-
* nothing failed, the check declines
|
|
431
|
+
* slot is rejected, so the full read distinguishes a missing issue from a site failure. When no target was
|
|
432
|
+
* answered and nothing failed, the check declines, so a batch of only such keys costs a full read, not a failure.
|
|
433
433
|
*/
|
|
434
434
|
async getProviderIssuesEtagFieldsByResourceId(session, targets) {
|
|
435
435
|
const slots = new Array(targets.length);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jira.js","names":[],"sources":["../../../../../plus/integrations/src/providers/jira.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport { JIRA_MAX_PROJECT_KEYS_PER_REQUEST } from '@gitkraken/provider-apis';\nimport type { Account } from '@gitlens/git/models/author.js';\nimport type { AutolinkReference, DynamicAutolinkReference } from '@gitlens/git/models/autolink.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { chunk } from '@gitlens/utils/array.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport { mapSettledBounded } from '@gitlens/utils/promise.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport type { ProviderRefusal } from '../collectionMetadata.js';\nimport { toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { IssuesCloudHostIntegrationId, providerFanOutConcurrency } from '../constants.js';\nimport type { IssueEtagFields } from '../models/integration.js';\nimport type {\n\tAccountWideIssuesResult,\n\tIssuesForProjectOptions,\n\tProjectIssuesDrain,\n\tProjectIssuesRequest,\n\tSearchMyIssuesOptions,\n} from '../models/issueReads.js';\nimport { groupProjectIssuesSearches, IssuesIntegration } from '../models/issuesIntegration.js';\nimport type { JiraIssueEtagResponse } from './jiraIssueByKey.js';\nimport { jiraBulkFetchMaxKeys, toJiraIssueEtagFields } from './jiraIssueByKey.js';\nimport type { ProviderApiCollectionResult } from './models.js';\nimport { providersMetadata, toAccount, toIssueShape, toProviderIssueStates } from './models.js';\nimport { isJiraMissingProjectError } from './providerErrors.js';\nimport { DiscoveryCache, discoveryCacheTtl } from './utils/discoveryCache.js';\nimport { createIssueAutolinkReferences } from './utils/issueAutolinks.js';\nimport type { JiraDrainOptions, JiraIssuePageFetcher } from './utils/jiraIssueDrains.js';\nimport {\n\tdrainJiraIssues,\n\treadUserScopedJiraIssues,\n\tsearchJiraProjectIssues,\n\tsummarizePageMetadata,\n\ttoUniqueJiraIssueShapes,\n} from './utils/jiraIssueDrains.js';\nimport { collectProviderPagedResult, mergeCollectionMetadata } from './utils/providerPaging.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Jira];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\nconst maxPagesPerRequest = 10;\n/** A numeric issue id, which Jira's issue reads accept in place of a key. */\nconst numericIssueId = /^\\d+$/;\n\n/**\n * `assignee` and `creator` are user FIELDS: Jira resolves an accountId against the directory, which is the identity\n * that keeps matching once a display name cannot be looked up — a deactivated account, or a profile whose visibility\n * hides the name. Measured against a live site: a deactivated assignee returns its issues by accountId and an empty\n * page by display name, and Jira reports that miss as a successful empty search rather than an error, so the list\n * simply appears empty.\n *\n * `mention` is NOT a user field. It is `comment ~ \"...\"`, a free-text search over comment bodies, so an accountId\n * matches nothing there and the display name is the only value that can. Hence the id is applied to the first two\n * and the handle is kept for the third, rather than swapping `user` wholesale.\n */\nfunction toUserIdentities(user: string, options: IssuesForProjectOptions): { userField: string; mention: string } {\n\treturn { userField: options.userId ?? user, mention: user };\n}\n\nexport type JiraBaseDescriptor = IssueResourceDescriptor;\n\nexport interface JiraOrganizationDescriptor extends JiraBaseDescriptor {\n\turl: string;\n\tavatarUrl: string;\n}\n\nexport interface JiraProjectDescriptor extends JiraBaseDescriptor {\n\tresourceId: string;\n}\n\nexport class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Jira> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\treadonly id = IssuesCloudHostIntegrationId.Jira;\n\tprotected readonly key = this.id;\n\treadonly name: string = 'Jira';\n\n\tget domain(): string {\n\t\treturn metadata.domain;\n\t}\n\n\tprotected get apiBaseUrl(): string {\n\t\treturn 'https://api.atlassian.com';\n\t}\n\n\t/**\n\t * Built from the sites and their projects, so it follows them: rebuilt when a site's project list changes or the\n\t * caches are dropped, and once older than {@link discoveryCacheTtl}. An expired set is still served while it is\n\t * rebuilt in the background, because autolinks are read on every render of a commit message and must not wait\n\t * on the network, nor go empty just because nothing read the projects for a while (#5907).\n\t */\n\tprivate readonly _autolinks = new Map<\n\t\tstring,\n\t\t{ autolinks: (AutolinkReference | DynamicAutolinkReference)[]; builtAt: number }\n\t>();\n\tprivate readonly _autolinksBuilds = new Map<string, Promise<(AutolinkReference | DynamicAutolinkReference)[]>>();\n\n\toverride async autolinks(): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst connected = this.maybeConnected ?? (await this.isConnected());\n\t\tif (!connected || this._session == null) return [];\n\n\t\tconst session = this._session;\n\t\tconst cached = this._autolinks.get(session.accessToken);\n\t\t// A session kept through a failed refresh is expired: building with it would send that token to the provider\n\t\t// on every render, so serve whatever was built before (none after a forced re-sync) until a read refreshes it.\n\t\tif (this.connectionExpired === true) return cached?.autolinks ?? [];\n\t\tif (cached == null) return this.buildAutolinks(session);\n\n\t\tif (Date.now() - cached.builtAt >= discoveryCacheTtl) {\n\t\t\tvoid this.buildAutolinks(session);\n\t\t}\n\t\treturn cached.autolinks;\n\t}\n\n\t/** One build per token at a time: concurrent renders share it rather than each re-reading sites and projects. */\n\tprivate buildAutolinks(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst { accessToken } = session;\n\t\tlet build = this._autolinksBuilds.get(accessToken);\n\t\tif (build == null) {\n\t\t\tbuild = this.buildAutolinksCore(session).finally(() => this._autolinksBuilds.delete(accessToken));\n\t\t\tthis._autolinksBuilds.set(accessToken, build);\n\t\t}\n\t\treturn build;\n\t}\n\n\tprivate async buildAutolinksCore(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst { accessToken } = session;\n\t\tconst generation = this._projects.generation;\n\t\ttry {\n\t\t\tconst organizations = await this.getProviderResourcesForUser(session);\n\t\t\tif (!organizations?.length) return [];\n\n\t\t\t// Served from the cache for every site whose projects are still fresh; only the rest are read.\n\t\t\tconst { values: projects, metadata } = await this.getProviderProjectsForResourcesWithMetadata(\n\t\t\t\tsession,\n\t\t\t\torganizations,\n\t\t\t);\n\n\t\t\tconst autolinks: (AutolinkReference | DynamicAutolinkReference)[] = [];\n\t\t\tfor (const project of projects) {\n\t\t\t\tconst organization = organizations.find(o => o.id === project.resourceId);\n\t\t\t\tif (organization == null) continue;\n\n\t\t\t\tautolinks.push(\n\t\t\t\t\t...createIssueAutolinkReferences(\n\t\t\t\t\t\tproject.key,\n\t\t\t\t\t\t`${organization.url}/browse/${project.key}-<num>`,\n\t\t\t\t\t\torganization,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// A set missing a site's projects is served but not kept, so the next call tries that site again; one\n\t\t\t// built from projects read before the caches were dropped is not kept either.\n\t\t\tconst complete = metadata == null || metadata.completeness === 'complete';\n\t\t\tif (complete && this._projects.generation === generation) {\n\t\t\t\tthis._autolinks.set(accessToken, { autolinks: autolinks, builtAt: Date.now() });\n\t\t\t}\n\t\t\treturn autolinks;\n\t\t} catch (ex) {\n\t\t\t// Autolinks are decoration: keep serving the last set rather than failing the render that asked.\n\t\t\tLogger.error(ex, 'JiraIntegration.autolinks');\n\t\t\treturn this._autolinks.get(accessToken)?.autolinks ?? [];\n\t\t}\n\t}\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: JiraOrganizationDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst user = await api.getCurrentUserForResource(toTokenWithInfo(this.id, session), resource.id);\n\n\t\tif (user == null) return undefined;\n\t\treturn toAccount(user, this);\n\t}\n\n\t/** Atlassian's account id is the same on every site, so the first site answers for all of them. */\n\tprotected override async getProviderCurrentAccount(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<Account | undefined> {\n\t\tconst resource = (await this.getProviderResourcesForUser(session))?.[0];\n\t\treturn resource != null ? this.getProviderAccountForResource(session, resource) : undefined;\n\t}\n\n\t/** The site-list request the resource cache would otherwise answer; see `IntegrationBase.validateCredential`. */\n\tprotected override async validateCredential(session: ProviderAuthenticationSession): Promise<void> {\n\t\tconst api = await this.getProvidersApi();\n\t\tif ((await api.getJiraResourcesForCurrentUser(toTokenWithInfo(this.id, session))) == null) {\n\t\t\tthrow new Error('Jira did not confirm the credential');\n\t\t}\n\t}\n\n\t/**\n\t * Atlassian's `401` for a token missing a scope the request needs, which a reconnect fixes by consenting to it\n\t * again; see `IntegrationBase.isCredentialRefusal`. Atlassian gives the same answer to a malformed request URL,\n\t * so a request the SDK built wrong would read as the credential's too.\n\t */\n\tprotected override isCredentialRefusal(refusal: ProviderRefusal): boolean {\n\t\treturn refusal.status === 401 && /scope does not match/i.test(refusal.detail ?? '');\n\t}\n\n\t/**\n\t * The sites and the projects discovered under them, both of which expire and are dropped on a re-sync, so a site\n\t * or project created or deleted mid-session is picked up (#5907). Keyed by token, and projects by token and site.\n\t */\n\tprivate readonly _organizations = new DiscoveryCache<JiraOrganizationDescriptor[]>();\n\tprivate readonly _projects = new DiscoveryCache<JiraProjectDescriptor[]>();\n\t/** When the caches were last dropped; a persisted discovery read before then is as stale as they were. */\n\tprivate _discoveryInvalidatedAt = 0;\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<JiraOrganizationDescriptor[] | undefined> {\n\t\tconst { accessToken } = session;\n\n\t\tconst cachedResources = this._organizations.get(accessToken);\n\t\tif (cachedResources != null && !force) return cachedResources;\n\n\t\tconst generation = this._organizations.generation;\n\t\tconst organizations = await this.readOrganizations(session);\n\t\tif (organizations == null) {\n\t\t\tthis._organizations.delete(accessToken);\n\t\t\treturn undefined;\n\t\t}\n\n\t\tthis._organizations.set(accessToken, organizations, { generation: generation });\n\t\treturn organizations;\n\t}\n\n\tprotected override getProviderUncachedResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<JiraOrganizationDescriptor[] | undefined> {\n\t\treturn this.readOrganizations(session);\n\t}\n\n\tprivate async readOrganizations(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<JiraOrganizationDescriptor[] | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst resources = await api.getJiraResourcesForCurrentUser(toTokenWithInfo(this.id, session));\n\t\treturn resources?.map(r => ({ ...r, key: r.id }));\n\t}\n\n\tprotected override async getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraOrganizationDescriptor[],\n\t\tforce: boolean = false,\n\t): Promise<JiraProjectDescriptor[] | undefined> {\n\t\treturn (await this.getProviderProjectsForResourcesWithMetadata(session, resources, force)).values;\n\t}\n\n\tprotected override async getProviderProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraOrganizationDescriptor[],\n\t\tforce: boolean = false,\n\t): Promise<ProviderApiCollectionResult<JiraProjectDescriptor>> {\n\t\tconst { accessToken } = session;\n\t\tconst projectsCache = this._projects;\n\t\tconst generation = projectsCache.generation;\n\n\t\t// Taken before the read, so a clear while it is in flight can't empty what was cached when it started. A forced\n\t\t// read reads every resource, and still keeps a cached list for one it can't read completely.\n\t\tconst cachedProjects = new Map<string, JiraProjectDescriptor[]>();\n\t\tfor (const resource of resources) {\n\t\t\tconst projects = projectsCache.get(`${accessToken}:${resource.id}`);\n\t\t\tif (projects != null) {\n\t\t\t\tcachedProjects.set(resource.id, projects);\n\t\t\t}\n\t\t}\n\t\tconst resourcesWithoutProjects = force\n\t\t\t? resources\n\t\t\t: resources.filter(resource => !cachedProjects.has(resource.id));\n\n\t\tconst read = await this.readProjects(session, resourcesWithoutProjects);\n\t\tconst values = [...read.partial];\n\t\tfor (const [resourceId, projects] of read.complete) {\n\t\t\t// Returned either way; not stored when the cache was dropped while this read was in flight: it is what this\n\t\t\t// read found, only no longer trusted for the next one.\n\t\t\tif (projectsCache.set(`${accessToken}:${resourceId}`, projects, { generation: generation })) {\n\t\t\t\tthis._autolinks.delete(accessToken);\n\t\t\t}\n\t\t}\n\t\tfor (const resource of resources) {\n\t\t\tconst resourceProjects = read.complete.get(resource.id) ?? cachedProjects.get(resource.id);\n\t\t\tif (resourceProjects != null) {\n\t\t\t\tvalues.push(...resourceProjects);\n\t\t\t}\n\t\t}\n\t\treturn { values: uniqueProjects(values), metadata: read.metadata };\n\t}\n\n\tprotected override async getProviderUncachedProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraOrganizationDescriptor[],\n\t): Promise<ProviderApiCollectionResult<JiraProjectDescriptor>> {\n\t\tconst read = await this.readProjects(session, resources);\n\t\treturn {\n\t\t\tvalues: uniqueProjects([...read.partial, ...[...read.complete.values()].flat()]),\n\t\t\tmetadata: read.metadata,\n\t\t};\n\t}\n\n\t/**\n\t * Reads the projects of each resource from the provider, apart by whether a resource's list came back complete:\n\t * only a complete one may be cached as the resource's project set.\n\t */\n\tprivate async readProjects(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraOrganizationDescriptor[],\n\t): Promise<{\n\t\tcomplete: Map<string, JiraProjectDescriptor[]>;\n\t\tpartial: JiraProjectDescriptor[];\n\t\tmetadata: CollectionMetadata | undefined;\n\t}> {\n\t\tconst complete = new Map<string, JiraProjectDescriptor[]>();\n\t\tconst partial: JiraProjectDescriptor[] = [];\n\t\tlet metadata: CollectionMetadata | undefined;\n\t\tif (resources.length === 0) return { complete: complete, partial: partial, metadata: metadata };\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst drains = await Promise.allSettled(\n\t\t\tresources.map(async resource => ({\n\t\t\t\tresource: resource,\n\t\t\t\tresult: await collectProviderPagedResult(\n\t\t\t\t\tcursor => api.getJiraProjectsForResource(tokenWithInfo, resource.id, { cursor: cursor }),\n\t\t\t\t\tmaxPagesPerRequest,\n\t\t\t\t\t{ providerId: this.id, resourceId: resource.id },\n\t\t\t\t),\n\t\t\t})),\n\t\t);\n\n\t\tdrains.forEach((drain, index) => {\n\t\t\tif (drain.status === 'rejected') {\n\t\t\t\tconst resource = resources[index];\n\t\t\t\tif (resource == null) return;\n\n\t\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\tfailures: [\n\t\t\t\t\t\ttoCollectionScopeFailure({ providerId: this.id, resourceId: resource.id }, drain.reason),\n\t\t\t\t\t],\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst { resource, result } = drain.value;\n\t\t\tmetadata = mergeCollectionMetadata(metadata, result.metadata);\n\t\t\tconst projects = result.values\n\t\t\t\t.filter(project => project.resourceId === resource.id)\n\t\t\t\t.map(project => ({ ...project }));\n\t\t\tconst incomplete =\n\t\t\t\tresult.truncated === true || (result.metadata != null && result.metadata.completeness !== 'complete');\n\t\t\tif (incomplete) {\n\t\t\t\tpartial.push(...projects);\n\t\t\t\tif (\n\t\t\t\t\tresult.truncated === true &&\n\t\t\t\t\t(result.metadata == null || result.metadata.completeness === 'complete')\n\t\t\t\t) {\n\t\t\t\t\tmetadata = mergeCollectionMetadata(metadata, { completeness: 'partial' });\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcomplete.set(resource.id, projects);\n\t\t});\n\t\treturn { complete: complete, partial: partial, metadata: metadata };\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;\n\t}\n\n\tprotected override async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\n\t\tconst api = await this.getProvidersApi();\n\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\t// By key, not name, as the multi-project search: JQL's `project = \"<value>\"` also accepts a name or id, so a\n\t\t// display name equal to another project's key would address that project. The key is the only unambiguous one.\n\t\tconst fetchPage: JiraIssuePageFetcher = (scope, cursor) =>\n\t\t\tapi.getIssuesForProjectPaged(tokenWithInfo, project.key, project.resourceId, {\n\t\t\t\t...scope,\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t\tsort: options?.sort,\n\t\t\t});\n\t\tconst drainOptions: JiraDrainOptions = {\n\t\t\tmaxPages: maxPagesPerRequest,\n\t\t\tfailureScope: { providerId: this.id, resourceId: project.resourceId, projectId: project.key },\n\t\t\tproviderName: 'Jira',\n\t\t\t// A project this token can no longer see (deleted, or its browse permission removed) holds no issues\n\t\t\t// for it, and the site's project list that named it is stale: drop that list so the next read\n\t\t\t// re-reads it, instead of failing the whole provider on every read until a restart (#5907).\n\t\t\tisEmptyFailure: ex => {\n\t\t\t\tif (!isJiraMissingProjectError(ex)) return false;\n\n\t\t\t\tLogger.warn(`Jira project '${project.key}' no longer exists or is not visible`);\n\t\t\t\tthis._projects.delete(`${session.accessToken}:${project.resourceId}`);\n\t\t\t\tthis._autolinks.delete(session.accessToken);\n\t\t\t\treturn true;\n\t\t\t},\n\t\t};\n\n\t\tif (options?.user != null) {\n\t\t\tconst read = await readUserScopedJiraIssues(\n\t\t\t\tfetchPage,\n\t\t\t\toptions.filters,\n\t\t\t\ttoUserIdentities(options.user, options),\n\t\t\t\tdrainOptions,\n\t\t\t);\n\t\t\tconst values = toUniqueJiraIssueShapes(read.issues, this);\n\t\t\tconst drain = { values: values, metadata: read.metadata, scope: drainOptions.failureScope };\n\t\t\treturn read.truncated ? { ...drain, truncated: true, recovery: 'none' } : { ...drain, truncated: false };\n\t\t}\n\n\t\tconst unscoped = await drainJiraIssues(fetchPage, {}, drainOptions);\n\t\tconst values = unscoped.issues\n\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'project' }))\n\t\t\t.filter((result): result is IssueShape => result !== undefined);\n\t\tconst drain = { values: values, metadata: unscoped.metadata, scope: drainOptions.failureScope };\n\t\treturn unscoped.status !== 'complete'\n\t\t\t? { ...drain, truncated: true, recovery: unscoped.status === 'backstop' ? 'narrow-scope' : 'none' }\n\t\t\t: { ...drain, truncated: false };\n\t}\n\n\t/**\n\t * Searches a site's projects together for a user-scoped read: one JQL names up to\n\t * {@link JIRA_MAX_PROJECT_KEYS_PER_REQUEST} projects of one site and one user scope, past which the SDK refuses\n\t * the call rather than risk a query string Jira rejects.\n\t *\n\t * An unscoped read is never searched across projects: it drains every issue in the project, and its backstop is\n\t * reported as `narrow-scope` per project, which one search across projects could not attribute.\n\t */\n\tprotected override getProjectIssuesSearches(\n\t\trequests: readonly ProjectIssuesRequest<JiraProjectDescriptor>[],\n\t): number[][] | undefined {\n\t\treturn groupProjectIssuesSearches(\n\t\t\trequests,\n\t\t\t({ project, options }) =>\n\t\t\t\toptions.user != null\n\t\t\t\t\t? [\n\t\t\t\t\t\t\tproject.resourceId,\n\t\t\t\t\t\t\toptions.user,\n\t\t\t\t\t\t\toptions.userId ?? '',\n\t\t\t\t\t\t\toptions.sort ?? '',\n\t\t\t\t\t\t\toptions.state ?? '',\n\t\t\t\t\t\t\toptions.filters?.join(',') ?? '',\n\t\t\t\t\t\t].join('\\0')\n\t\t\t\t\t: undefined,\n\t\t\tJIRA_MAX_PROJECT_KEYS_PER_REQUEST,\n\t\t);\n\t}\n\n\tprotected override async searchProviderProjectIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\trequests: readonly ProjectIssuesRequest<JiraProjectDescriptor>[],\n\t): Promise<IssueShape[][] | undefined> {\n\t\t// Every request of a search shares its site and user scope (see `getProjectIssuesSearches`).\n\t\tconst { project, options } = requests[0];\n\t\tif (options.user == null) return undefined;\n\t\t// One project's search is its own read, by the same key and user clauses, so it costs no more to read it as one;\n\t\t// and that read attributes a failure to the project and drops a project list that no longer has it.\n\t\tif (requests.length === 1) return undefined;\n\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst api = await this.getProvidersApi();\n\t\t// By key, not name: JQL resolves `project = \"<value>\"` against key first, so a name could read another\n\t\t// project's issues.\n\t\tconst projectKeys = requests.map(request => request.project.key);\n\t\tconst states = toProviderIssueStates(options.state);\n\n\t\treturn searchJiraProjectIssues(\n\t\t\t(userScope, cursor) =>\n\t\t\t\tapi.getIssuesForProjectsPaged(tokenWithInfo, projectKeys, project.resourceId, {\n\t\t\t\t\t...userScope,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tstates: states,\n\t\t\t\t\tsort: options.sort,\n\t\t\t\t}),\n\t\t\trequests.map(request => request.project.id),\n\t\t\toptions.filters,\n\t\t\ttoUserIdentities(options.user, options),\n\t\t\t{\n\t\t\t\tmaxPages: maxPagesPerRequest,\n\t\t\t\tfailureScope: { providerId: this.id, resourceId: project.resourceId },\n\t\t\t\tproviderName: 'Jira',\n\t\t\t},\n\t\t\tthis,\n\t\t);\n\t}\n\n\tprotected override async searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: JiraOrganizationDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\tconst result = await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation);\n\t\t// This read can only return values, so what the account-wide read reports as missing is at least logged.\n\t\tfor (const failure of result?.metadata?.failures ?? []) {\n\t\t\tLogger.warn(\n\t\t\t\t`searchProviderMyIssues: Jira site '${failure.scope?.resourceId ?? '?'}' was not fully read: ${failure.message ?? failure.kind}`,\n\t\t\t);\n\t\t}\n\t\treturn result?.values;\n\t}\n\n\t/**\n\t * Overridden so `state` reaches the per-site read, which the default drops. Each site is drained on its own, so a\n\t * site that fails keeps what it read and is recorded as that site's failure instead of discarding the sites that\n\t * answered; so is an issue a page dropped because it could not be read. A page backstop or a stalled continuation\n\t * makes the read `truncated`. A refused site therefore goes through `confirmScopedAuthFailures`, which publishes it\n\t * as that site's auth warning or, if the credential itself is refused, as the connection's.\n\t */\n\tprotected override async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: JiraOrganizationDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tconst myResources = resources ?? (await this.getProviderResourcesForUser(session));\n\t\tif (!myResources) return undefined;\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\n\t\tconst results: IssueShape[] = [];\n\t\tlet truncated = false;\n\t\tlet metadata: CollectionMetadata | undefined;\n\t\t// What each page reported failing, summarized at the end so a site whose many pages dropped issues reports\n\t\t// it once (see `summarizePageMetadata`); the pages keep the rest of their metadata, so `truncated` holds.\n\t\tconst pages: CollectionMetadata[] = [];\n\t\tconst readPage = async (resourceId: string, cursor: string | undefined) => {\n\t\t\tconst page = await api.getIssuesForResourceForCurrentUser(tokenWithInfo, resourceId, {\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t});\n\t\t\tif (!page.metadata?.failures?.length) return page;\n\n\t\t\tconst { failures: _failures, ...rest } = page.metadata;\n\t\t\tpages.push(page.metadata);\n\t\t\treturn { ...page, metadata: rest };\n\t\t};\n\t\tfor (const resource of myResources) {\n\t\t\tif (cancellation?.aborted) break;\n\n\t\t\tconst site = await collectProviderPagedResult(\n\t\t\t\tcursor =>\n\t\t\t\t\t// Cancellation is not truncation: the caller asked for the stop, so the site ends where it is.\n\t\t\t\t\tcancellation?.aborted\n\t\t\t\t\t\t? Promise.resolve({ values: [], paging: { cursor: '{}', more: false } })\n\t\t\t\t\t\t: readPage(resource.id, cursor),\n\t\t\t\tmaxPagesPerRequest,\n\t\t\t\t{ providerId: this.id, resourceId: resource.id },\n\t\t\t);\n\t\t\tresults.push(\n\t\t\t\t...site.values\n\t\t\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'account' }))\n\t\t\t\t\t.filter((result): result is IssueShape => result != null),\n\t\t\t);\n\t\t\ttruncated ||= site.truncated === true;\n\t\t\tmetadata = mergeCollectionMetadata(metadata, site.metadata);\n\t\t}\n\n\t\treturn {\n\t\t\tvalues: results,\n\t\t\ttruncated: truncated,\n\t\t\tmetadata: mergeCollectionMetadata(summarizePageMetadata(pages), metadata),\n\t\t};\n\t}\n\n\tprotected override async getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: JiraOrganizationDescriptor,\n\t\t{ key }: { id: string; key: string },\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst issue = await api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resource.id,\n\t\t\tnumber: key,\n\t\t});\n\t\treturn issue != null ? toIssueShape(issue, this, { projection: 'point' }) : undefined;\n\t}\n\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: JiraOrganizationDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst apiResult = await api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resource.id,\n\t\t\tnumber: id,\n\t\t});\n\t\tconst issue = apiResult != null ? toIssueShape(apiResult, this, { projection: 'point' }) : undefined;\n\t\treturn issue != null ? { ...issue, type: 'issue' } : undefined;\n\t}\n\n\tprotected override async getProviderIssueByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresourceId: string,\n\t\tid: string,\n\t\tresourceUrl: string | undefined,\n\t): Promise<Issue | undefined> {\n\t\tif (resourceUrl == null) {\n\t\t\tthrow new Error('Jira direct issue reads require a resource URL');\n\t\t}\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst apiResult = await api.getJiraIssueByKey(toTokenWithInfo(this.id, session), resourceId, resourceUrl, id);\n\t\tconst issue = apiResult != null ? toIssueShape(apiResult, this, { projection: 'batch' }) : undefined;\n\t\treturn issue != null ? { ...issue, type: 'issue' } : undefined;\n\t}\n\n\t/**\n\t * The cheap check behind the batch issue read's etags: one bulk fetch per site and {@link jiraBulkFetchMaxKeys}\n\t * distinct keys, where {@link getProviderIssueByResourceId} sends one request per issue. A request that throws\n\t * rejects only its own targets' slots, with its error classified as the full read's would be.\n\t *\n\t * A target identified by a numeric issue id, which both reads accept as well as a key, is matched by the returned\n\t * issue's `id`; every other target by its key.\n\t *\n\t * Never proves an absence. Jira silently omits a key it can't find or show (listing it in no `issueErrors` either),\n\t * and answers a moved issue under its new key, which no longer matches the key asked for. Either way the target's\n\t * slot is rejected, so the full read, whose 404 does prove an absence, decides. When no target was answered and\n\t * nothing failed, the check declines instead, so a batch of only such keys costs a full read, not a failure.\n\t */\n\tprotected override async getProviderIssuesEtagFieldsByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\ttargets: readonly { resourceId: string; identifier: string; resourceUrl?: string }[],\n\t): Promise<PromiseSettledResult<IssueEtagFields | undefined>[] | undefined> {\n\t\tconst slots = new Array<PromiseSettledResult<IssueEtagFields | undefined>>(targets.length);\n\t\tlet answered = false;\n\t\tlet failed = false;\n\n\t\t// Each site's distinct keys, matched case-insensitively as Jira matches them, with the targets asking for each.\n\t\tconst bySite = new Map<string, Map<string, number[]>>();\n\t\tfor (const [index, target] of targets.entries()) {\n\t\t\tif (target.resourceUrl == null) {\n\t\t\t\t// Required as the full read requires it, so a check never answers for a target the full read refuses.\n\t\t\t\tslots[index] = {\n\t\t\t\t\tstatus: 'rejected',\n\t\t\t\t\treason: new Error('Jira direct issue reads require a resource URL'),\n\t\t\t\t};\n\t\t\t\tfailed = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet keys = bySite.get(target.resourceId);\n\t\t\tif (keys == null) {\n\t\t\t\tkeys = new Map();\n\t\t\t\tbySite.set(target.resourceId, keys);\n\t\t\t}\n\n\t\t\tconst key = target.identifier.toUpperCase();\n\t\t\tlet indices = keys.get(key);\n\t\t\tif (indices == null) {\n\t\t\t\tindices = [];\n\t\t\t\tkeys.set(key, indices);\n\t\t\t}\n\t\t\tindices.push(index);\n\t\t}\n\n\t\tconst requests = [...bySite].flatMap(([resourceId, keys]) =>\n\t\t\tchunk([...keys], jiraBulkFetchMaxKeys).map(entries => ({ resourceId: resourceId, entries: entries })),\n\t\t);\n\t\tif (!requests.length) return slots;\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst answers = await mapSettledBounded(requests, providerFanOutConcurrency, r =>\n\t\t\tapi.getJiraIssuesEtagFields(\n\t\t\t\ttokenWithInfo,\n\t\t\t\tr.resourceId,\n\t\t\t\tr.entries.map(([key]) => key),\n\t\t\t),\n\t\t);\n\n\t\tfor (const [i, answer] of answers.entries()) {\n\t\t\tconst { entries } = requests[i];\n\t\t\tif (answer.status === 'rejected') {\n\t\t\t\tfailed = true;\n\t\t\t\tfor (const [, indices] of entries) {\n\t\t\t\t\tfor (const index of indices) {\n\t\t\t\t\t\tslots[index] = answer;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (answer.value.errorCount > 0) {\n\t\t\t\tfailed = true;\n\t\t\t}\n\n\t\t\t// By key or id, never by position: Jira returns the issues in its own order, and only the ones it found.\n\t\t\tconst byKey = new Map<string, JiraIssueEtagResponse>();\n\t\t\tconst byId = new Map<string, JiraIssueEtagResponse>();\n\t\t\tfor (const issue of answer.value.issues) {\n\t\t\t\tbyKey.set(issue.key.toUpperCase(), issue);\n\t\t\t\tbyId.set(issue.id, issue);\n\t\t\t}\n\n\t\t\tfor (const [key, indices] of entries) {\n\t\t\t\t// A key target never matches by id, so an issue returned under a different key (moved) is rejected.\n\t\t\t\tconst issue = numericIssueId.test(key) ? byId.get(key) : byKey.get(key);\n\t\t\t\tlet slot: PromiseSettledResult<IssueEtagFields | undefined>;\n\t\t\t\tif (issue == null) {\n\t\t\t\t\tslot = { status: 'rejected', reason: new Error(`Jira bulk fetch did not return ${key}`) };\n\t\t\t\t} else {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tslot = { status: 'fulfilled', value: toJiraIssueEtagFields(issue) };\n\t\t\t\t\t\tanswered = true;\n\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\tslot = { status: 'rejected', reason: ex };\n\t\t\t\t\t\tfailed = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfor (const index of indices) {\n\t\t\t\t\tslots[index] = slot;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn answered || failed ? slots : undefined;\n\t}\n\n\tprotected override async providerOnConnect(): Promise<void> {\n\t\tthis._autolinks.clear();\n\t\tif (this._session == null) return;\n\n\t\tconst session = this._session;\n\t\tconst { accessToken } = session;\n\t\tconst organizationsGeneration = this._organizations.generation;\n\t\tconst projectsGeneration = this._projects.generation;\n\n\t\t// Persisted discovery seeds the caches only while it would still be fresh in them: read within the TTL, and\n\t\t// after the caches were last dropped. Otherwise a re-sync, which runs this again, would put back the very\n\t\t// project list it just dropped, and a restart would trust one read any time before (#5907).\n\t\tconst isFresh = (timestamp: unknown): timestamp is number =>\n\t\t\ttypeof timestamp === 'number' &&\n\t\t\ttimestamp > this._discoveryInvalidatedAt &&\n\t\t\tDate.now() - timestamp < discoveryCacheTtl;\n\n\t\tconst storedOrganizations = this.ctx.storage.get(`jira:${accessToken}:organizations`);\n\t\t// Projects persisted without their sites belong to a session whose sites were cleared below; ignore them.\n\t\tconst storedProjects =\n\t\t\tstoredOrganizations != null ? this.ctx.storage.get(`jira:${accessToken}:projects`) : undefined;\n\n\t\tlet organizations: JiraOrganizationDescriptor[] | undefined;\n\t\tif (storedOrganizations?.data != null && isFresh(storedOrganizations.timestamp)) {\n\t\t\tconst seeded = storedOrganizations.data.map((o: JiraOrganizationDescriptor) => ({ ...o }));\n\t\t\torganizations = seeded;\n\t\t\tthis._organizations.set(accessToken, seeded, {\n\t\t\t\tgeneration: organizationsGeneration,\n\t\t\t\tstoredAt: storedOrganizations.timestamp,\n\t\t\t});\n\t\t} else {\n\t\t\t// Stamped with when the read started, so the TTL counts from what it actually saw.\n\t\t\tconst readAt = Date.now();\n\t\t\torganizations = await this.getProviderResourcesForUser(session, true);\n\t\t\tif (storedOrganizations == null) {\n\t\t\t\t// Clear all other stored organizations and projects when our session changes\n\t\t\t\tawait this.ctx.storage.deleteWithPrefix('jira');\n\t\t\t}\n\t\t\t// Not persisted when the caches were dropped while it was in flight: it is then a read from before the\n\t\t\t// refresh, and the next start, which knows nothing of that refresh, would take it for fresh.\n\t\t\tif (this._organizations.generation === organizationsGeneration) {\n\t\t\t\tawait this.ctx.storage.store(`jira:${accessToken}:organizations`, {\n\t\t\t\t\tv: 1,\n\t\t\t\t\ttimestamp: readAt,\n\t\t\t\t\tdata: organizations,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tif (storedProjects?.data != null && isFresh(storedProjects.timestamp)) {\n\t\t\tconst projectsByResource = new Map<string, JiraProjectDescriptor[]>();\n\t\t\tfor (const project of storedProjects.data as JiraProjectDescriptor[]) {\n\t\t\t\tconst projects = projectsByResource.get(project.resourceId);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tprojectsByResource.set(project.resourceId, [{ ...project }]);\n\t\t\t\t} else if (!projects.some(p => p.id === project.id)) {\n\t\t\t\t\tprojects.push({ ...project });\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const [resourceId, projects] of projectsByResource) {\n\t\t\t\tthis._projects.set(`${accessToken}:${resourceId}`, projects, {\n\t\t\t\t\tgeneration: projectsGeneration,\n\t\t\t\t\tstoredAt: storedProjects.timestamp,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (organizations?.length) {\n\t\t\tconst readAt = Date.now();\n\t\t\tconst projects = await this.getProviderProjectsForResources(session, organizations, true);\n\t\t\t// See the organizations above.\n\t\t\tif (this._projects.generation === projectsGeneration) {\n\t\t\t\tawait this.ctx.storage.store(`jira:${accessToken}:projects`, {\n\t\t\t\t\tv: 1,\n\t\t\t\t\ttimestamp: readAt,\n\t\t\t\t\tdata: projects,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tprotected override clearDiscoveryCaches(): void {\n\t\tthis._discoveryInvalidatedAt = Date.now();\n\t\tthis._organizations.clear();\n\t\tthis._projects.clear();\n\t\tthis._autolinks.clear();\n\t}\n\n\tprotected override providerOnDisconnect(): void {\n\t\tthis._organizations.clear();\n\t\tthis._projects.clear();\n\t\tthis._autolinks.clear();\n\t}\n}\n\n/** The projects once each, by site and id. */\nfunction uniqueProjects(projects: JiraProjectDescriptor[]): JiraProjectDescriptor[] {\n\tconst projectsByIdentity = new Map<string, JiraProjectDescriptor>();\n\tfor (const project of projects) {\n\t\tconst identity = `${project.resourceId}:${project.id}`;\n\t\tif (!projectsByIdentity.has(identity)) {\n\t\t\tprojectsByIdentity.set(identity, project);\n\t\t}\n\t}\n\treturn [...projectsByIdentity.values()];\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA0CA,MAAM,WAAW,kBAAA;AACjB,MAAM,eAAe,OAAO,OAAO;CAAE,IAAI,SAAS;CAAI,QAAQ,SAAS;AAAO,CAAC;AAC/E,MAAM,qBAAqB;;AAE3B,MAAM,iBAAiB;;;;;;;;;;;;AAavB,SAAS,iBAAiB,MAAc,SAA0E;CACjH,OAAO;EAAE,WAAW,QAAQ,UAAU;EAAM,SAAS;CAAK;AAC3D;AAaA,IAAa,kBAAb,cAAqC,kBAAqD;CACzF,eAAqE;CACrE,KAAS;CACT,MAAyB,KAAK;CAC9B,OAAwB;CAExB,IAAI,SAAiB;EACpB,OAAO,SAAS;CACjB;CAEA,IAAc,aAAqB;EAClC,OAAO;CACR;;;;;;;CAQA,6BAA8B,IAAI,IAGhC;CACF,mCAAoC,IAAI,IAAuE;CAE/G,MAAe,YAAuE;EAErF,IAAI,EADc,KAAK,kBAAmB,MAAM,KAAK,YAAY,MAC/C,KAAK,YAAY,MAAM,OAAO,CAAC;EAEjD,MAAM,UAAU,KAAK;EACrB,MAAM,SAAS,KAAK,WAAW,IAAI,QAAQ,WAAW;EAGtD,IAAI,KAAK,sBAAsB,MAAM,OAAO,QAAQ,aAAa,CAAC;EAClE,IAAI,UAAU,MAAM,OAAO,KAAK,eAAe,OAAO;EAEtD,IAAI,KAAK,IAAI,IAAI,OAAO,WAAA,KACvB,KAAU,eAAe,OAAO;EAEjC,OAAO,OAAO;CACf;;CAGA,eACC,SAC4D;EAC5D,MAAM,EAAE,gBAAgB;EACxB,IAAI,QAAQ,KAAK,iBAAiB,IAAI,WAAW;EACjD,IAAI,SAAS,MAAM;GAClB,QAAQ,KAAK,mBAAmB,OAAO,CAAC,CAAC,cAAc,KAAK,iBAAiB,OAAO,WAAW,CAAC;GAChG,KAAK,iBAAiB,IAAI,aAAa,KAAK;EAC7C;EACA,OAAO;CACR;CAEA,MAAc,mBACb,SAC4D;EAC5D,MAAM,EAAE,gBAAgB;EACxB,MAAM,aAAa,KAAK,UAAU;EAClC,IAAI;GACH,MAAM,gBAAgB,MAAM,KAAK,4BAA4B,OAAO;GACpE,IAAI,CAAC,eAAe,QAAQ,OAAO,CAAC;GAGpC,MAAM,EAAE,QAAQ,UAAU,aAAa,MAAM,KAAK,4CACjD,SACA,aACD;GAEA,MAAM,YAA8D,CAAC;GACrE,KAAK,MAAM,WAAW,UAAU;IAC/B,MAAM,eAAe,cAAc,MAAK,MAAK,EAAE,OAAO,QAAQ,UAAU;IACxE,IAAI,gBAAgB,MAAM;IAE1B,UAAU,KACT,GAAG,8BACF,QAAQ,KACR,GAAG,aAAa,IAAI,UAAU,QAAQ,IAAI,SAC1C,YACD,CACD;GACD;GAKA,KADiB,YAAY,QAAQ,SAAS,iBAAiB,eAC/C,KAAK,UAAU,eAAe,YAC7C,KAAK,WAAW,IAAI,aAAa;IAAa;IAAW,SAAS,KAAK,IAAI;GAAE,CAAC;GAE/E,OAAO;EACR,SAAS,IAAI;GAEZ,QAAO,MAAM,IAAI,2BAA2B;GAC5C,OAAO,KAAK,WAAW,IAAI,WAAW,CAAC,EAAE,aAAa,CAAC;EACxD;CACD;CAEA,MAAyB,8BACxB,SACA,UAC+B;EAE/B,MAAM,OAAO,OAAM,MADD,KAAK,gBAAgB,EAAA,CAChB,0BAA0B,gBAAgB,KAAK,IAAI,OAAO,GAAG,SAAS,EAAE;EAE/F,IAAI,QAAQ,MAAM,OAAO,KAAA;EACzB,OAAO,UAAU,MAAM,IAAI;CAC5B;;CAGA,MAAyB,0BACxB,SAC+B;EAC/B,MAAM,YAAY,MAAM,KAAK,4BAA4B,OAAO,EAAA,GAAK;EACrE,OAAO,YAAY,OAAO,KAAK,8BAA8B,SAAS,QAAQ,IAAI,KAAA;CACnF;;CAGA,MAAyB,mBAAmB,SAAuD;EAElG,IAAK,OAAM,MADO,KAAK,gBAAgB,EAAA,CACxB,+BAA+B,gBAAgB,KAAK,IAAI,OAAO,CAAC,KAAM,MACpF,MAAM,IAAI,MAAM,qCAAqC;CAEvD;;;;;;CAOA,oBAAuC,SAAmC;EACzE,OAAO,QAAQ,WAAW,OAAO,wBAAwB,KAAK,QAAQ,UAAU,EAAE;CACnF;;;;;CAMA,iBAAkC,IAAI,eAA6C;CACnF,YAA6B,IAAI,eAAwC;;CAEzE,0BAAkC;CAElC,MAAyB,4BACxB,SACA,QAAiB,OACmC;EACpD,MAAM,EAAE,gBAAgB;EAExB,MAAM,kBAAkB,KAAK,eAAe,IAAI,WAAW;EAC3D,IAAI,mBAAmB,QAAQ,CAAC,OAAO,OAAO;EAE9C,MAAM,aAAa,KAAK,eAAe;EACvC,MAAM,gBAAgB,MAAM,KAAK,kBAAkB,OAAO;EAC1D,IAAI,iBAAiB,MAAM;GAC1B,KAAK,eAAe,OAAO,WAAW;GACtC;EACD;EAEA,KAAK,eAAe,IAAI,aAAa,eAAe,EAAc,WAAW,CAAC;EAC9E,OAAO;CACR;CAEA,oCACC,SACoD;EACpD,OAAO,KAAK,kBAAkB,OAAO;CACtC;CAEA,MAAc,kBACb,SACoD;EAGpD,QAAO,OADiB,MADN,KAAK,gBAAgB,EAAA,CACX,+BAA+B,gBAAgB,KAAK,IAAI,OAAO,CAAC,EAAA,EAC1E,KAAI,OAAM;GAAE,GAAG;GAAG,KAAK,EAAE;EAAG,EAAE;CACjD;CAEA,MAAyB,gCACxB,SACA,WACA,QAAiB,OAC8B;EAC/C,QAAQ,MAAM,KAAK,4CAA4C,SAAS,WAAW,KAAK,EAAA,CAAG;CAC5F;CAEA,MAAyB,4CACxB,SACA,WACA,QAAiB,OAC6C;EAC9D,MAAM,EAAE,gBAAgB;EACxB,MAAM,gBAAgB,KAAK;EAC3B,MAAM,aAAa,cAAc;EAIjC,MAAM,iCAAiB,IAAI,IAAqC;EAChE,KAAK,MAAM,YAAY,WAAW;GACjC,MAAM,WAAW,cAAc,IAAI,GAAG,YAAY,GAAG,SAAS,IAAI;GAClE,IAAI,YAAY,MACf,eAAe,IAAI,SAAS,IAAI,QAAQ;EAE1C;EACA,MAAM,2BAA2B,QAC9B,YACA,UAAU,QAAO,aAAY,CAAC,eAAe,IAAI,SAAS,EAAE,CAAC;EAEhE,MAAM,OAAO,MAAM,KAAK,aAAa,SAAS,wBAAwB;EACtE,MAAM,SAAS,CAAC,GAAG,KAAK,OAAO;EAC/B,KAAK,MAAM,CAAC,YAAY,aAAa,KAAK,UAGzC,IAAI,cAAc,IAAI,GAAG,YAAY,GAAG,cAAc,UAAU,EAAc,WAAW,CAAC,GACzF,KAAK,WAAW,OAAO,WAAW;EAGpC,KAAK,MAAM,YAAY,WAAW;GACjC,MAAM,mBAAmB,KAAK,SAAS,IAAI,SAAS,EAAE,KAAK,eAAe,IAAI,SAAS,EAAE;GACzF,IAAI,oBAAoB,MACvB,OAAO,KAAK,GAAG,gBAAgB;EAEjC;EACA,OAAO;GAAE,QAAQ,eAAe,MAAM;GAAG,UAAU,KAAK;EAAS;CAClE;CAEA,MAAyB,oDACxB,SACA,WAC8D;EAC9D,MAAM,OAAO,MAAM,KAAK,aAAa,SAAS,SAAS;EACvD,OAAO;GACN,QAAQ,eAAe,CAAC,GAAG,KAAK,SAAS,GAAG,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;GAC/E,UAAU,KAAK;EAChB;CACD;;;;;CAMA,MAAc,aACb,SACA,WAKE;EACF,MAAM,2BAAW,IAAI,IAAqC;EAC1D,MAAM,UAAmC,CAAC;EAC1C,IAAI;EACJ,IAAI,UAAU,WAAW,GAAG,OAAO;GAAY;GAAmB;GAAmB;EAAS;EAE9F,MAAM,MAAM,MAAM,KAAK,gBAAgB;EACvC,MAAM,gBAAgB,gBAAgB,KAAK,IAAI,OAAO;EAYtD,CAAA,MAXqB,QAAQ,WAC5B,UAAU,IAAI,OAAM,cAAa;GACtB;GACV,QAAQ,MAAM,4BACb,WAAU,IAAI,2BAA2B,eAAe,SAAS,IAAI,EAAU,OAAO,CAAC,GACvF,oBACA;IAAE,YAAY,KAAK;IAAI,YAAY,SAAS;GAAG,CAChD;EACD,EAAE,CACH,EAAA,CAEO,SAAS,OAAO,UAAU;GAChC,IAAI,MAAM,WAAW,YAAY;IAChC,MAAM,WAAW,UAAU;IAC3B,IAAI,YAAY,MAAM;IAEtB,WAAW,wBAAwB,UAAU;KAC5C,cAAc;KACd,UAAU,CACT,yBAAyB;MAAE,YAAY,KAAK;MAAI,YAAY,SAAS;KAAG,GAAG,MAAM,MAAM,CACxF;IACD,CAAC;IACD;GACD;GAEA,MAAM,EAAE,UAAU,WAAW,MAAM;GACnC,WAAW,wBAAwB,UAAU,OAAO,QAAQ;GAC5D,MAAM,WAAW,OAAO,OACtB,QAAO,YAAW,QAAQ,eAAe,SAAS,EAAE,CAAC,CACrD,KAAI,aAAY,EAAE,GAAG,QAAQ,EAAE;GAGjC,IADC,OAAO,cAAc,QAAS,OAAO,YAAY,QAAQ,OAAO,SAAS,iBAAiB,YAC3E;IACf,QAAQ,KAAK,GAAG,QAAQ;IACxB,IACC,OAAO,cAAc,SACpB,OAAO,YAAY,QAAQ,OAAO,SAAS,iBAAiB,aAE7D,WAAW,wBAAwB,UAAU,EAAE,cAAc,UAAU,CAAC;IAEzE;GACD;GAEA,SAAS,IAAI,SAAS,IAAI,QAAQ;EACnC,CAAC;EACD,OAAO;GAAY;GAAmB;GAAmB;EAAS;CACnE;CAEA,MAAyB,4BACxB,SACA,SACA,SACoC;EACpC,QAAQ,MAAM,KAAK,0CAA0C,SAAS,SAAS,OAAO,EAAA,EAAI;CAC3F;CAEA,MAAyB,0CACxB,SACA,SACA,SAC0C;EAC1C,MAAM,gBAAgB,gBAAgB,KAAK,IAAI,OAAO;EAEtD,MAAM,MAAM,MAAM,KAAK,gBAAgB;EAEvC,MAAM,SAAS,sBAAsB,SAAS,KAAK;EAGnD,MAAM,aAAmC,OAAO,WAC/C,IAAI,yBAAyB,eAAe,QAAQ,KAAK,QAAQ,YAAY;GAC5E,GAAG;GACK;GACA;GACR,MAAM,SAAS;EAChB,CAAC;EACF,MAAM,eAAiC;GACtC,UAAU;GACV,cAAc;IAAE,YAAY,KAAK;IAAI,YAAY,QAAQ;IAAY,WAAW,QAAQ;GAAI;GAC5F,cAAc;GAId,iBAAgB,OAAM;IACrB,IAAI,CAAC,0BAA0B,EAAE,GAAG,OAAO;IAE3C,QAAO,KAAK,iBAAiB,QAAQ,IAAI,qCAAqC;IAC9E,KAAK,UAAU,OAAO,GAAG,QAAQ,YAAY,GAAG,QAAQ,YAAY;IACpE,KAAK,WAAW,OAAO,QAAQ,WAAW;IAC1C,OAAO;GACR;EACD;EAEA,IAAI,SAAS,QAAQ,MAAM;GAC1B,MAAM,OAAO,MAAM,yBAClB,WACA,QAAQ,SACR,iBAAiB,QAAQ,MAAM,OAAO,GACtC,YACD;GAEA,MAAM,QAAQ;IAAE,QADD,wBAAwB,KAAK,QAAQ,IACvB;IAAG,UAAU,KAAK;IAAU,OAAO,aAAa;GAAa;GAC1F,OAAO,KAAK,YAAY;IAAE,GAAG;IAAO,WAAW;IAAM,UAAU;GAAO,IAAI;IAAE,GAAG;IAAO,WAAW;GAAM;EACxG;EAEA,MAAM,WAAW,MAAM,gBAAgB,WAAW,CAAC,GAAG,YAAY;EAIlE,MAAM,QAAQ;GAAE,QAHD,SAAS,OACtB,KAAI,UAAS,aAAa,OAAO,MAAM,EAAE,YAAY,UAAU,CAAC,CAAC,CAAC,CAClE,QAAQ,WAAiC,WAAW,KAAA,CACzB;GAAG,UAAU,SAAS;GAAU,OAAO,aAAa;EAAa;EAC9F,OAAO,SAAS,WAAW,aACxB;GAAE,GAAG;GAAO,WAAW;GAAM,UAAU,SAAS,WAAW,aAAa,iBAAiB;EAAO,IAChG;GAAE,GAAG;GAAO,WAAW;EAAM;CACjC;;;;;;;;;CAUA,yBACC,UACyB;EACzB,OAAO,2BACN,WACC,EAAE,SAAS,cACX,QAAQ,QAAQ,OACb;GACA,QAAQ;GACR,QAAQ;GACR,QAAQ,UAAU;GAClB,QAAQ,QAAQ;GAChB,QAAQ,SAAS;GACjB,QAAQ,SAAS,KAAK,GAAG,KAAK;EAC/B,CAAC,CAAC,KAAK,IAAI,IACV,KAAA,GACJ,iCACD;CACD;CAEA,MAAyB,4BACxB,SACA,UACsC;EAEtC,MAAM,EAAE,SAAS,YAAY,SAAS;EACtC,IAAI,QAAQ,QAAQ,MAAM,OAAO,KAAA;EAGjC,IAAI,SAAS,WAAW,GAAG,OAAO,KAAA;EAElC,MAAM,gBAAgB,gBAAgB,KAAK,IAAI,OAAO;EACtD,MAAM,MAAM,MAAM,KAAK,gBAAgB;EAGvC,MAAM,cAAc,SAAS,KAAI,YAAW,QAAQ,QAAQ,GAAG;EAC/D,MAAM,SAAS,sBAAsB,QAAQ,KAAK;EAElD,OAAO,yBACL,WAAW,WACX,IAAI,0BAA0B,eAAe,aAAa,QAAQ,YAAY;GAC7E,GAAG;GACK;GACA;GACR,MAAM,QAAQ;EACf,CAAC,GACF,SAAS,KAAI,YAAW,QAAQ,QAAQ,EAAE,GAC1C,QAAQ,SACR,iBAAiB,QAAQ,MAAM,OAAO,GACtC;GACC,UAAU;GACV,cAAc;IAAE,YAAY,KAAK;IAAI,YAAY,QAAQ;GAAW;GACpE,cAAc;EACf,GACA,IACD;CACD;CAEA,MAAyB,uBACxB,SACA,WACA,cACoC;EACpC,MAAM,SAAS,MAAM,KAAK,qCAAqC,SAAS,WAAW,YAAY;EAE/F,KAAK,MAAM,WAAW,QAAQ,UAAU,YAAY,CAAC,GACpD,QAAO,KACN,sCAAsC,QAAQ,OAAO,cAAc,IAAI,wBAAwB,QAAQ,WAAW,QAAQ,MAC3H;EAED,OAAO,QAAQ;CAChB;;;;;;;;CASA,MAAyB,qCACxB,SACA,WACA,cACA,SAC+C;EAC/C,MAAM,SAAS,sBAAsB,SAAS,KAAK;EACnD,MAAM,cAAc,aAAc,MAAM,KAAK,4BAA4B,OAAO;EAChF,IAAI,CAAC,aAAa,OAAO,KAAA;EAEzB,MAAM,MAAM,MAAM,KAAK,gBAAgB;EACvC,MAAM,gBAAgB,gBAAgB,KAAK,IAAI,OAAO;EAEtD,MAAM,UAAwB,CAAC;EAC/B,IAAI,YAAY;EAChB,IAAI;EAGJ,MAAM,QAA8B,CAAC;EACrC,MAAM,WAAW,OAAO,YAAoB,WAA+B;GAC1E,MAAM,OAAO,MAAM,IAAI,mCAAmC,eAAe,YAAY;IAC5E;IACA;GACT,CAAC;GACD,IAAI,CAAC,KAAK,UAAU,UAAU,QAAQ,OAAO;GAE7C,MAAM,EAAE,UAAU,WAAW,GAAG,SAAS,KAAK;GAC9C,MAAM,KAAK,KAAK,QAAQ;GACxB,OAAO;IAAE,GAAG;IAAM,UAAU;GAAK;EAClC;EACA,KAAK,MAAM,YAAY,aAAa;GACnC,IAAI,cAAc,SAAS;GAE3B,MAAM,OAAO,MAAM,4BAClB,WAEC,cAAc,UACX,QAAQ,QAAQ;IAAE,QAAQ,CAAC;IAAG,QAAQ;KAAE,QAAQ;KAAM,MAAM;IAAM;GAAE,CAAC,IACrE,SAAS,SAAS,IAAI,MAAM,GAChC,oBACA;IAAE,YAAY,KAAK;IAAI,YAAY,SAAS;GAAG,CAChD;GACA,QAAQ,KACP,GAAG,KAAK,OACN,KAAI,UAAS,aAAa,OAAO,MAAM,EAAE,YAAY,UAAU,CAAC,CAAC,CAAC,CAClE,QAAQ,WAAiC,UAAU,IAAI,CAC1D;GACA,cAAc,KAAK,cAAc;GACjC,WAAW,wBAAwB,UAAU,KAAK,QAAQ;EAC3D;EAEA,OAAO;GACN,QAAQ;GACG;GACX,UAAU,wBAAwB,sBAAsB,KAAK,GAAG,QAAQ;EACzE;CACD;CAEA,MAAyB,oCACxB,SACA,UACA,EAAE,OACwC;EAE1C,MAAM,QAAQ,OAAM,MADF,KAAK,gBAAgB,EAAA,CACf,SAAS,gBAAgB,KAAK,IAAI,OAAO,GAAG;GACnE,YAAY,SAAS;GACrB,QAAQ;EACT,CAAC;EACD,OAAO,SAAS,OAAO,aAAa,OAAO,MAAM,EAAE,YAAY,QAAQ,CAAC,IAAI,KAAA;CAC7E;CAEA,MAAyB,iBACxB,SACA,UACA,IAC6B;EAE7B,MAAM,YAAY,OAAM,MADN,KAAK,gBAAgB,EAAA,CACX,SAAS,gBAAgB,KAAK,IAAI,OAAO,GAAG;GACvE,YAAY,SAAS;GACrB,QAAQ;EACT,CAAC;EACD,MAAM,QAAQ,aAAa,OAAO,aAAa,WAAW,MAAM,EAAE,YAAY,QAAQ,CAAC,IAAI,KAAA;EAC3F,OAAO,SAAS,OAAO;GAAE,GAAG;GAAO,MAAM;EAAQ,IAAI,KAAA;CACtD;CAEA,MAAyB,6BACxB,SACA,YACA,IACA,aAC6B;EAC7B,IAAI,eAAe,MAClB,MAAM,IAAI,MAAM,gDAAgD;EAIjE,MAAM,YAAY,OAAM,MADN,KAAK,gBAAgB,EAAA,CACX,kBAAkB,gBAAgB,KAAK,IAAI,OAAO,GAAG,YAAY,aAAa,EAAE;EAC5G,MAAM,QAAQ,aAAa,OAAO,aAAa,WAAW,MAAM,EAAE,YAAY,QAAQ,CAAC,IAAI,KAAA;EAC3F,OAAO,SAAS,OAAO;GAAE,GAAG;GAAO,MAAM;EAAQ,IAAI,KAAA;CACtD;;;;;;;;;;;;;;CAeA,MAAyB,wCACxB,SACA,SAC2E;EAC3E,MAAM,QAAQ,IAAI,MAAyD,QAAQ,MAAM;EACzF,IAAI,WAAW;EACf,IAAI,SAAS;EAGb,MAAM,yBAAS,IAAI,IAAmC;EACtD,KAAK,MAAM,CAAC,OAAO,WAAW,QAAQ,QAAQ,GAAG;GAChD,IAAI,OAAO,eAAe,MAAM;IAE/B,MAAM,SAAS;KACd,QAAQ;KACR,wBAAQ,IAAI,MAAM,gDAAgD;IACnE;IACA,SAAS;IACT;GACD;GAEA,IAAI,OAAO,OAAO,IAAI,OAAO,UAAU;GACvC,IAAI,QAAQ,MAAM;IACjB,uBAAO,IAAI,IAAI;IACf,OAAO,IAAI,OAAO,YAAY,IAAI;GACnC;GAEA,MAAM,MAAM,OAAO,WAAW,YAAY;GAC1C,IAAI,UAAU,KAAK,IAAI,GAAG;GAC1B,IAAI,WAAW,MAAM;IACpB,UAAU,CAAC;IACX,KAAK,IAAI,KAAK,OAAO;GACtB;GACA,QAAQ,KAAK,KAAK;EACnB;EAEA,MAAM,WAAW,CAAC,GAAG,MAAM,CAAC,CAAC,SAAS,CAAC,YAAY,UAClD,MAAM,CAAC,GAAG,IAAI,GAAA,GAAuB,CAAC,CAAC,KAAI,aAAY;GAAc;GAAqB;EAAQ,EAAE,CACrG;EACA,IAAI,CAAC,SAAS,QAAQ,OAAO;EAE7B,MAAM,MAAM,MAAM,KAAK,gBAAgB;EACvC,MAAM,gBAAgB,gBAAgB,KAAK,IAAI,OAAO;EACtD,MAAM,UAAU,MAAM,kBAAkB,UAAA,IAAqC,MAC5E,IAAI,wBACH,eACA,EAAE,YACF,EAAE,QAAQ,KAAK,CAAC,SAAS,GAAG,CAC7B,CACD;EAEA,KAAK,MAAM,CAAC,GAAG,WAAW,QAAQ,QAAQ,GAAG;GAC5C,MAAM,EAAE,YAAY,SAAS;GAC7B,IAAI,OAAO,WAAW,YAAY;IACjC,SAAS;IACT,KAAK,MAAM,GAAG,YAAY,SACzB,KAAK,MAAM,SAAS,SACnB,MAAM,SAAS;IAGjB;GACD;GAEA,IAAI,OAAO,MAAM,aAAa,GAC7B,SAAS;GAIV,MAAM,wBAAQ,IAAI,IAAmC;GACrD,MAAM,uBAAO,IAAI,IAAmC;GACpD,KAAK,MAAM,SAAS,OAAO,MAAM,QAAQ;IACxC,MAAM,IAAI,MAAM,IAAI,YAAY,GAAG,KAAK;IACxC,KAAK,IAAI,MAAM,IAAI,KAAK;GACzB;GAEA,KAAK,MAAM,CAAC,KAAK,YAAY,SAAS;IAErC,MAAM,QAAQ,eAAe,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,MAAM,IAAI,GAAG;IACtE,IAAI;IACJ,IAAI,SAAS,MACZ,OAAO;KAAE,QAAQ;KAAY,wBAAQ,IAAI,MAAM,kCAAkC,KAAK;IAAE;SAExF,IAAI;KACH,OAAO;MAAE,QAAQ;MAAa,OAAO,sBAAsB,KAAK;KAAE;KAClE,WAAW;IACZ,SAAS,IAAI;KACZ,OAAO;MAAE,QAAQ;MAAY,QAAQ;KAAG;KACxC,SAAS;IACV;IAGD,KAAK,MAAM,SAAS,SACnB,MAAM,SAAS;GAEjB;EACD;EAEA,OAAO,YAAY,SAAS,QAAQ,KAAA;CACrC;CAEA,MAAyB,oBAAmC;EAC3D,KAAK,WAAW,MAAM;EACtB,IAAI,KAAK,YAAY,MAAM;EAE3B,MAAM,UAAU,KAAK;EACrB,MAAM,EAAE,gBAAgB;EACxB,MAAM,0BAA0B,KAAK,eAAe;EACpD,MAAM,qBAAqB,KAAK,UAAU;EAK1C,MAAM,WAAW,cAChB,OAAO,cAAc,YACrB,YAAY,KAAK,2BACjB,KAAK,IAAI,IAAI,YAAA;EAEd,MAAM,sBAAsB,KAAK,IAAI,QAAQ,IAAI,QAAQ,YAAY,eAAe;EAEpF,MAAM,iBACL,uBAAuB,OAAO,KAAK,IAAI,QAAQ,IAAI,QAAQ,YAAY,UAAU,IAAI,KAAA;EAEtF,IAAI;EACJ,IAAI,qBAAqB,QAAQ,QAAQ,QAAQ,oBAAoB,SAAS,GAAG;GAChF,MAAM,SAAS,oBAAoB,KAAK,KAAK,OAAmC,EAAE,GAAG,EAAE,EAAE;GACzF,gBAAgB;GAChB,KAAK,eAAe,IAAI,aAAa,QAAQ;IAC5C,YAAY;IACZ,UAAU,oBAAoB;GAC/B,CAAC;EACF,OAAO;GAEN,MAAM,SAAS,KAAK,IAAI;GACxB,gBAAgB,MAAM,KAAK,4BAA4B,SAAS,IAAI;GACpE,IAAI,uBAAuB,MAE1B,MAAM,KAAK,IAAI,QAAQ,iBAAiB,MAAM;GAI/C,IAAI,KAAK,eAAe,eAAe,yBACtC,MAAM,KAAK,IAAI,QAAQ,MAAM,QAAQ,YAAY,iBAAiB;IACjE,GAAG;IACH,WAAW;IACX,MAAM;GACP,CAAC;EAEH;EAEA,IAAI,gBAAgB,QAAQ,QAAQ,QAAQ,eAAe,SAAS,GAAG;GACtE,MAAM,qCAAqB,IAAI,IAAqC;GACpE,KAAK,MAAM,WAAW,eAAe,MAAiC;IACrE,MAAM,WAAW,mBAAmB,IAAI,QAAQ,UAAU;IAC1D,IAAI,YAAY,MACf,mBAAmB,IAAI,QAAQ,YAAY,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC;SACrD,IAAI,CAAC,SAAS,MAAK,MAAK,EAAE,OAAO,QAAQ,EAAE,GACjD,SAAS,KAAK,EAAE,GAAG,QAAQ,CAAC;GAE9B;GACA,KAAK,MAAM,CAAC,YAAY,aAAa,oBACpC,KAAK,UAAU,IAAI,GAAG,YAAY,GAAG,cAAc,UAAU;IAC5D,YAAY;IACZ,UAAU,eAAe;GAC1B,CAAC;EAEH,OAAO,IAAI,eAAe,QAAQ;GACjC,MAAM,SAAS,KAAK,IAAI;GACxB,MAAM,WAAW,MAAM,KAAK,gCAAgC,SAAS,eAAe,IAAI;GAExF,IAAI,KAAK,UAAU,eAAe,oBACjC,MAAM,KAAK,IAAI,QAAQ,MAAM,QAAQ,YAAY,YAAY;IAC5D,GAAG;IACH,WAAW;IACX,MAAM;GACP,CAAC;EAEH;CACD;CAEA,uBAAgD;EAC/C,KAAK,0BAA0B,KAAK,IAAI;EACxC,KAAK,eAAe,MAAM;EAC1B,KAAK,UAAU,MAAM;EACrB,KAAK,WAAW,MAAM;CACvB;CAEA,uBAAgD;EAC/C,KAAK,eAAe,MAAM;EAC1B,KAAK,UAAU,MAAM;EACrB,KAAK,WAAW,MAAM;CACvB;AACD;;AAGA,SAAS,eAAe,UAA4D;CACnF,MAAM,qCAAqB,IAAI,IAAmC;CAClE,KAAK,MAAM,WAAW,UAAU;EAC/B,MAAM,WAAW,GAAG,QAAQ,WAAW,GAAG,QAAQ;EAClD,IAAI,CAAC,mBAAmB,IAAI,QAAQ,GACnC,mBAAmB,IAAI,UAAU,OAAO;CAE1C;CACA,OAAO,CAAC,GAAG,mBAAmB,OAAO,CAAC;AACvC"}
|
|
1
|
+
{"version":3,"file":"jira.js","names":[],"sources":["../../../../../plus/integrations/src/providers/jira.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport { JIRA_MAX_PROJECT_KEYS_PER_REQUEST } from '@gitkraken/provider-apis';\nimport type { Account } from '@gitlens/git/models/author.js';\nimport type { AutolinkReference, DynamicAutolinkReference } from '@gitlens/git/models/autolink.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { chunk } from '@gitlens/utils/array.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport { mapSettledBounded } from '@gitlens/utils/promise.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport type { ProviderRefusal } from '../collectionMetadata.js';\nimport { toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { IssuesCloudHostIntegrationId, providerFanOutConcurrency } from '../constants.js';\nimport type { IssueEtagFields } from '../models/integration.js';\nimport type {\n\tAccountWideIssuesResult,\n\tIssuesForProjectOptions,\n\tProjectIssuesDrain,\n\tProjectIssuesRequest,\n\tSearchMyIssuesOptions,\n} from '../models/issueReads.js';\nimport { groupProjectIssuesSearches, IssuesIntegration } from '../models/issuesIntegration.js';\nimport type { JiraIssueEtagResponse } from './jiraIssueByKey.js';\nimport { jiraBulkFetchMaxKeys, toJiraIssueEtagFields } from './jiraIssueByKey.js';\nimport type { ProviderApiCollectionResult } from './models.js';\nimport { providersMetadata, toAccount, toIssueShape, toProviderIssueStates } from './models.js';\nimport { isJiraMissingProjectError } from './providerErrors.js';\nimport { DiscoveryCache, discoveryCacheTtl } from './utils/discoveryCache.js';\nimport { createIssueAutolinkReferences } from './utils/issueAutolinks.js';\nimport type { JiraDrainOptions, JiraIssuePageFetcher } from './utils/jiraIssueDrains.js';\nimport {\n\tdrainJiraIssues,\n\treadUserScopedJiraIssues,\n\tsearchJiraProjectIssues,\n\tsummarizePageMetadata,\n\ttoUniqueJiraIssueShapes,\n} from './utils/jiraIssueDrains.js';\nimport { collectProviderPagedResult, mergeCollectionMetadata } from './utils/providerPaging.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Jira];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\nconst maxPagesPerRequest = 10;\n/** A numeric issue id, which Jira's issue reads accept in place of a key. */\nconst numericIssueId = /^\\d+$/;\n\n/**\n * `assignee` and `creator` are user FIELDS: Jira resolves an accountId against the directory, which is the identity\n * that keeps matching once a display name cannot be looked up — a deactivated account, or a profile whose visibility\n * hides the name. Measured against a live site: a deactivated assignee returns its issues by accountId and an empty\n * page by display name, and Jira reports that miss as a successful empty search rather than an error, so the list\n * simply appears empty.\n *\n * `mention` is NOT a user field. It is `comment ~ \"...\"`, a free-text search over comment bodies, so an accountId\n * matches nothing there and the display name is the only value that can. Hence the id is applied to the first two\n * and the handle is kept for the third, rather than swapping `user` wholesale.\n */\nfunction toUserIdentities(user: string, options: IssuesForProjectOptions): { userField: string; mention: string } {\n\treturn { userField: options.userId ?? user, mention: user };\n}\n\nexport type JiraBaseDescriptor = IssueResourceDescriptor;\n\nexport interface JiraOrganizationDescriptor extends JiraBaseDescriptor {\n\turl: string;\n\tavatarUrl: string;\n}\n\nexport interface JiraProjectDescriptor extends JiraBaseDescriptor {\n\tresourceId: string;\n}\n\nexport class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Jira> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\treadonly id = IssuesCloudHostIntegrationId.Jira;\n\tprotected readonly key = this.id;\n\treadonly name: string = 'Jira';\n\n\tget domain(): string {\n\t\treturn metadata.domain;\n\t}\n\n\tprotected get apiBaseUrl(): string {\n\t\treturn 'https://api.atlassian.com';\n\t}\n\n\t/**\n\t * Built from the sites and their projects, so it follows them: rebuilt when a site's project list changes or the\n\t * caches are dropped, and once older than {@link discoveryCacheTtl}. An expired set is still served while it is\n\t * rebuilt in the background, because autolinks are read on every render of a commit message and must not wait\n\t * on the network, nor go empty just because nothing read the projects for a while (#5907).\n\t */\n\tprivate readonly _autolinks = new Map<\n\t\tstring,\n\t\t{ autolinks: (AutolinkReference | DynamicAutolinkReference)[]; builtAt: number }\n\t>();\n\tprivate readonly _autolinksBuilds = new Map<string, Promise<(AutolinkReference | DynamicAutolinkReference)[]>>();\n\n\toverride async autolinks(): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst connected = this.maybeConnected ?? (await this.isConnected());\n\t\tif (!connected || this._session == null) return [];\n\n\t\tconst session = this._session;\n\t\tconst cached = this._autolinks.get(session.accessToken);\n\t\t// A session kept through a failed refresh is expired: building with it would send that token to the provider\n\t\t// on every render, so serve whatever was built before (none after a forced re-sync) until a read refreshes it.\n\t\tif (this.connectionExpired === true) return cached?.autolinks ?? [];\n\t\tif (cached == null) return this.buildAutolinks(session);\n\n\t\tif (Date.now() - cached.builtAt >= discoveryCacheTtl) {\n\t\t\tvoid this.buildAutolinks(session);\n\t\t}\n\t\treturn cached.autolinks;\n\t}\n\n\t/** One build per token at a time: concurrent renders share it rather than each re-reading sites and projects. */\n\tprivate buildAutolinks(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst { accessToken } = session;\n\t\tlet build = this._autolinksBuilds.get(accessToken);\n\t\tif (build == null) {\n\t\t\tbuild = this.buildAutolinksCore(session).finally(() => this._autolinksBuilds.delete(accessToken));\n\t\t\tthis._autolinksBuilds.set(accessToken, build);\n\t\t}\n\t\treturn build;\n\t}\n\n\tprivate async buildAutolinksCore(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst { accessToken } = session;\n\t\tconst generation = this._projects.generation;\n\t\ttry {\n\t\t\tconst organizations = await this.getProviderResourcesForUser(session);\n\t\t\tif (!organizations?.length) return [];\n\n\t\t\t// Served from the cache for every site whose projects are still fresh; only the rest are read.\n\t\t\tconst { values: projects, metadata } = await this.getProviderProjectsForResourcesWithMetadata(\n\t\t\t\tsession,\n\t\t\t\torganizations,\n\t\t\t);\n\n\t\t\tconst autolinks: (AutolinkReference | DynamicAutolinkReference)[] = [];\n\t\t\tfor (const project of projects) {\n\t\t\t\tconst organization = organizations.find(o => o.id === project.resourceId);\n\t\t\t\tif (organization == null) continue;\n\n\t\t\t\tautolinks.push(\n\t\t\t\t\t...createIssueAutolinkReferences(\n\t\t\t\t\t\tproject.key,\n\t\t\t\t\t\t`${organization.url}/browse/${project.key}-<num>`,\n\t\t\t\t\t\torganization,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// A set missing a site's projects is served but not kept, so the next call tries that site again; one\n\t\t\t// built from projects read before the caches were dropped is not kept either.\n\t\t\tconst complete = metadata == null || metadata.completeness === 'complete';\n\t\t\tif (complete && this._projects.generation === generation) {\n\t\t\t\tthis._autolinks.set(accessToken, { autolinks: autolinks, builtAt: Date.now() });\n\t\t\t}\n\t\t\treturn autolinks;\n\t\t} catch (ex) {\n\t\t\t// Autolinks are decoration: keep serving the last set rather than failing the render that asked.\n\t\t\tLogger.error(ex, 'JiraIntegration.autolinks');\n\t\t\treturn this._autolinks.get(accessToken)?.autolinks ?? [];\n\t\t}\n\t}\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: JiraOrganizationDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst user = await api.getCurrentUserForResource(toTokenWithInfo(this.id, session), resource.id);\n\n\t\tif (user == null) return undefined;\n\t\treturn toAccount(user, this);\n\t}\n\n\t/** Atlassian's account id is the same on every site, so the first site answers for all of them. */\n\tprotected override async getProviderCurrentAccount(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<Account | undefined> {\n\t\tconst resource = (await this.getProviderResourcesForUser(session))?.[0];\n\t\treturn resource != null ? this.getProviderAccountForResource(session, resource) : undefined;\n\t}\n\n\t/** The site-list request the resource cache would otherwise answer; see `IntegrationBase.validateCredential`. */\n\tprotected override async validateCredential(session: ProviderAuthenticationSession): Promise<void> {\n\t\tconst api = await this.getProvidersApi();\n\t\tif ((await api.getJiraResourcesForCurrentUser(toTokenWithInfo(this.id, session))) == null) {\n\t\t\tthrow new Error('Jira did not confirm the credential');\n\t\t}\n\t}\n\n\t/**\n\t * Atlassian's `401` for a token missing a scope the request needs, which a reconnect fixes by consenting to it\n\t * again; see `IntegrationBase.isCredentialRefusal`. Atlassian gives the same answer to a malformed request URL,\n\t * so a request the SDK built wrong would read as the credential's too.\n\t */\n\tprotected override isCredentialRefusal(refusal: ProviderRefusal): boolean {\n\t\treturn refusal.status === 401 && /scope does not match/i.test(refusal.detail ?? '');\n\t}\n\n\t/**\n\t * The sites and the projects discovered under them, both of which expire and are dropped on a re-sync, so a site\n\t * or project created or deleted mid-session is picked up (#5907). Keyed by token, and projects by token and site.\n\t */\n\tprivate readonly _organizations = new DiscoveryCache<JiraOrganizationDescriptor[]>();\n\tprivate readonly _projects = new DiscoveryCache<JiraProjectDescriptor[]>();\n\t/** When the caches were last dropped; a persisted discovery read before then is as stale as they were. */\n\tprivate _discoveryInvalidatedAt = 0;\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<JiraOrganizationDescriptor[] | undefined> {\n\t\tconst { accessToken } = session;\n\n\t\tconst cachedResources = this._organizations.get(accessToken);\n\t\tif (cachedResources != null && !force) return cachedResources;\n\n\t\tconst generation = this._organizations.generation;\n\t\tconst organizations = await this.readOrganizations(session);\n\t\tif (organizations == null) {\n\t\t\tthis._organizations.delete(accessToken);\n\t\t\treturn undefined;\n\t\t}\n\n\t\tthis._organizations.set(accessToken, organizations, { generation: generation });\n\t\treturn organizations;\n\t}\n\n\tprotected override getProviderUncachedResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<JiraOrganizationDescriptor[] | undefined> {\n\t\treturn this.readOrganizations(session);\n\t}\n\n\tprivate async readOrganizations(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<JiraOrganizationDescriptor[] | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst resources = await api.getJiraResourcesForCurrentUser(toTokenWithInfo(this.id, session));\n\t\treturn resources?.map(r => ({ ...r, key: r.id }));\n\t}\n\n\tprotected override async getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraOrganizationDescriptor[],\n\t\tforce: boolean = false,\n\t): Promise<JiraProjectDescriptor[] | undefined> {\n\t\treturn (await this.getProviderProjectsForResourcesWithMetadata(session, resources, force)).values;\n\t}\n\n\tprotected override async getProviderProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraOrganizationDescriptor[],\n\t\tforce: boolean = false,\n\t): Promise<ProviderApiCollectionResult<JiraProjectDescriptor>> {\n\t\tconst { accessToken } = session;\n\t\tconst projectsCache = this._projects;\n\t\tconst generation = projectsCache.generation;\n\n\t\t// Taken before the read, so a clear while it is in flight can't empty what was cached when it started. A forced\n\t\t// read reads every resource, and still keeps a cached list for one it can't read completely.\n\t\tconst cachedProjects = new Map<string, JiraProjectDescriptor[]>();\n\t\tfor (const resource of resources) {\n\t\t\tconst projects = projectsCache.get(`${accessToken}:${resource.id}`);\n\t\t\tif (projects != null) {\n\t\t\t\tcachedProjects.set(resource.id, projects);\n\t\t\t}\n\t\t}\n\t\tconst resourcesWithoutProjects = force\n\t\t\t? resources\n\t\t\t: resources.filter(resource => !cachedProjects.has(resource.id));\n\n\t\tconst read = await this.readProjects(session, resourcesWithoutProjects);\n\t\tconst values = [...read.partial];\n\t\tfor (const [resourceId, projects] of read.complete) {\n\t\t\t// Returned either way; not stored when the cache was dropped while this read was in flight: it is what this\n\t\t\t// read found, only no longer trusted for the next one.\n\t\t\tif (projectsCache.set(`${accessToken}:${resourceId}`, projects, { generation: generation })) {\n\t\t\t\tthis._autolinks.delete(accessToken);\n\t\t\t}\n\t\t}\n\t\tfor (const resource of resources) {\n\t\t\tconst resourceProjects = read.complete.get(resource.id) ?? cachedProjects.get(resource.id);\n\t\t\tif (resourceProjects != null) {\n\t\t\t\tvalues.push(...resourceProjects);\n\t\t\t}\n\t\t}\n\t\treturn { values: uniqueProjects(values), metadata: read.metadata };\n\t}\n\n\tprotected override async getProviderUncachedProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraOrganizationDescriptor[],\n\t): Promise<ProviderApiCollectionResult<JiraProjectDescriptor>> {\n\t\tconst read = await this.readProjects(session, resources);\n\t\treturn {\n\t\t\tvalues: uniqueProjects([...read.partial, ...[...read.complete.values()].flat()]),\n\t\t\tmetadata: read.metadata,\n\t\t};\n\t}\n\n\t/**\n\t * Reads the projects of each resource from the provider, apart by whether a resource's list came back complete:\n\t * only a complete one may be cached as the resource's project set.\n\t */\n\tprivate async readProjects(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraOrganizationDescriptor[],\n\t): Promise<{\n\t\tcomplete: Map<string, JiraProjectDescriptor[]>;\n\t\tpartial: JiraProjectDescriptor[];\n\t\tmetadata: CollectionMetadata | undefined;\n\t}> {\n\t\tconst complete = new Map<string, JiraProjectDescriptor[]>();\n\t\tconst partial: JiraProjectDescriptor[] = [];\n\t\tlet metadata: CollectionMetadata | undefined;\n\t\tif (resources.length === 0) return { complete: complete, partial: partial, metadata: metadata };\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst drains = await Promise.allSettled(\n\t\t\tresources.map(async resource => ({\n\t\t\t\tresource: resource,\n\t\t\t\tresult: await collectProviderPagedResult(\n\t\t\t\t\tcursor => api.getJiraProjectsForResource(tokenWithInfo, resource.id, { cursor: cursor }),\n\t\t\t\t\tmaxPagesPerRequest,\n\t\t\t\t\t{ providerId: this.id, resourceId: resource.id },\n\t\t\t\t),\n\t\t\t})),\n\t\t);\n\n\t\tdrains.forEach((drain, index) => {\n\t\t\tif (drain.status === 'rejected') {\n\t\t\t\tconst resource = resources[index];\n\t\t\t\tif (resource == null) return;\n\n\t\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\tfailures: [\n\t\t\t\t\t\ttoCollectionScopeFailure({ providerId: this.id, resourceId: resource.id }, drain.reason),\n\t\t\t\t\t],\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst { resource, result } = drain.value;\n\t\t\tmetadata = mergeCollectionMetadata(metadata, result.metadata);\n\t\t\tconst projects = result.values\n\t\t\t\t.filter(project => project.resourceId === resource.id)\n\t\t\t\t.map(project => ({ ...project }));\n\t\t\tconst incomplete =\n\t\t\t\tresult.truncated === true || (result.metadata != null && result.metadata.completeness !== 'complete');\n\t\t\tif (incomplete) {\n\t\t\t\tpartial.push(...projects);\n\t\t\t\tif (\n\t\t\t\t\tresult.truncated === true &&\n\t\t\t\t\t(result.metadata == null || result.metadata.completeness === 'complete')\n\t\t\t\t) {\n\t\t\t\t\tmetadata = mergeCollectionMetadata(metadata, { completeness: 'partial' });\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcomplete.set(resource.id, projects);\n\t\t});\n\t\treturn { complete: complete, partial: partial, metadata: metadata };\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;\n\t}\n\n\tprotected override async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\n\t\tconst api = await this.getProvidersApi();\n\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\t// By key, not name, as the multi-project search: JQL's `project = \"<value>\"` also accepts a name or id, so a\n\t\t// display name equal to another project's key would address that project. The key is the only unambiguous one.\n\t\tconst fetchPage: JiraIssuePageFetcher = (scope, cursor) =>\n\t\t\tapi.getIssuesForProjectPaged(tokenWithInfo, project.key, project.resourceId, {\n\t\t\t\t...scope,\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t\tsort: options?.sort,\n\t\t\t});\n\t\tconst drainOptions: JiraDrainOptions = {\n\t\t\tmaxPages: maxPagesPerRequest,\n\t\t\tfailureScope: { providerId: this.id, resourceId: project.resourceId, projectId: project.key },\n\t\t\tproviderName: 'Jira',\n\t\t\t// A project this token can no longer see (deleted, or its browse permission removed) holds no issues\n\t\t\t// for it, and the site's project list that named it is stale: drop that list so the next read\n\t\t\t// re-reads it, instead of failing the whole provider on every read until a restart (#5907).\n\t\t\tisEmptyFailure: ex => {\n\t\t\t\tif (!isJiraMissingProjectError(ex)) return false;\n\n\t\t\t\tLogger.warn(`Jira project '${project.key}' no longer exists or is not visible`);\n\t\t\t\tthis._projects.delete(`${session.accessToken}:${project.resourceId}`);\n\t\t\t\tthis._autolinks.delete(session.accessToken);\n\t\t\t\treturn true;\n\t\t\t},\n\t\t};\n\n\t\tif (options?.user != null) {\n\t\t\tconst read = await readUserScopedJiraIssues(\n\t\t\t\tfetchPage,\n\t\t\t\toptions.filters,\n\t\t\t\ttoUserIdentities(options.user, options),\n\t\t\t\tdrainOptions,\n\t\t\t);\n\t\t\tconst values = toUniqueJiraIssueShapes(read.issues, this);\n\t\t\tconst drain = { values: values, metadata: read.metadata, scope: drainOptions.failureScope };\n\t\t\treturn read.truncated ? { ...drain, truncated: true, recovery: 'none' } : { ...drain, truncated: false };\n\t\t}\n\n\t\tconst unscoped = await drainJiraIssues(fetchPage, {}, drainOptions);\n\t\tconst values = unscoped.issues\n\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'project' }))\n\t\t\t.filter((result): result is IssueShape => result !== undefined);\n\t\tconst drain = { values: values, metadata: unscoped.metadata, scope: drainOptions.failureScope };\n\t\treturn unscoped.status !== 'complete'\n\t\t\t? { ...drain, truncated: true, recovery: unscoped.status === 'backstop' ? 'narrow-scope' : 'none' }\n\t\t\t: { ...drain, truncated: false };\n\t}\n\n\t/**\n\t * Searches a site's projects together for a user-scoped read: one JQL names up to\n\t * {@link JIRA_MAX_PROJECT_KEYS_PER_REQUEST} projects of one site and one user scope, past which the SDK refuses\n\t * the call rather than risk a query string Jira rejects.\n\t *\n\t * An unscoped read is never searched across projects: it drains every issue in the project, and its backstop is\n\t * reported as `narrow-scope` per project, which one search across projects could not attribute.\n\t */\n\tprotected override getProjectIssuesSearches(\n\t\trequests: readonly ProjectIssuesRequest<JiraProjectDescriptor>[],\n\t): number[][] | undefined {\n\t\treturn groupProjectIssuesSearches(\n\t\t\trequests,\n\t\t\t({ project, options }) =>\n\t\t\t\toptions.user != null\n\t\t\t\t\t? [\n\t\t\t\t\t\t\tproject.resourceId,\n\t\t\t\t\t\t\toptions.user,\n\t\t\t\t\t\t\toptions.userId ?? '',\n\t\t\t\t\t\t\toptions.sort ?? '',\n\t\t\t\t\t\t\toptions.state ?? '',\n\t\t\t\t\t\t\toptions.filters?.join(',') ?? '',\n\t\t\t\t\t\t].join('\\0')\n\t\t\t\t\t: undefined,\n\t\t\tJIRA_MAX_PROJECT_KEYS_PER_REQUEST,\n\t\t);\n\t}\n\n\tprotected override async searchProviderProjectIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\trequests: readonly ProjectIssuesRequest<JiraProjectDescriptor>[],\n\t): Promise<IssueShape[][] | undefined> {\n\t\t// Every request of a search shares its site and user scope (see `getProjectIssuesSearches`).\n\t\tconst { project, options } = requests[0];\n\t\tif (options.user == null) return undefined;\n\t\t// One project's search is its own read, by the same key and user clauses, so it costs no more to read it as one;\n\t\t// and that read attributes a failure to the project and drops a project list that no longer has it.\n\t\tif (requests.length === 1) return undefined;\n\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst api = await this.getProvidersApi();\n\t\t// By key, not name: JQL resolves `project = \"<value>\"` against key first, so a name could read another\n\t\t// project's issues.\n\t\tconst projectKeys = requests.map(request => request.project.key);\n\t\tconst states = toProviderIssueStates(options.state);\n\n\t\treturn searchJiraProjectIssues(\n\t\t\t(userScope, cursor) =>\n\t\t\t\tapi.getIssuesForProjectsPaged(tokenWithInfo, projectKeys, project.resourceId, {\n\t\t\t\t\t...userScope,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tstates: states,\n\t\t\t\t\tsort: options.sort,\n\t\t\t\t}),\n\t\t\trequests.map(request => request.project.id),\n\t\t\toptions.filters,\n\t\t\ttoUserIdentities(options.user, options),\n\t\t\t{\n\t\t\t\tmaxPages: maxPagesPerRequest,\n\t\t\t\tfailureScope: { providerId: this.id, resourceId: project.resourceId },\n\t\t\t\tproviderName: 'Jira',\n\t\t\t},\n\t\t\tthis,\n\t\t);\n\t}\n\n\tprotected override async searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: JiraOrganizationDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\tconst result = await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation);\n\t\t// This read can only return values, so what the account-wide read reports as missing is at least logged.\n\t\tfor (const failure of result?.metadata?.failures ?? []) {\n\t\t\tLogger.warn(\n\t\t\t\t`searchProviderMyIssues: Jira site '${failure.scope?.resourceId ?? '?'}' was not fully read: ${failure.message ?? failure.kind}`,\n\t\t\t);\n\t\t}\n\t\treturn result?.values;\n\t}\n\n\t/**\n\t * Overridden so `state` reaches the per-site read, which the default drops. Each site is drained on its own, so a\n\t * site that fails keeps what it read and is recorded as that site's failure instead of discarding the sites that\n\t * answered; so is an issue a page dropped because it could not be read. A page backstop or a stalled continuation\n\t * makes the read `truncated`. A refused site therefore goes through `confirmScopedAuthFailures`, which publishes it\n\t * as that site's auth warning or, if the credential itself is refused, as the connection's.\n\t */\n\tprotected override async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: JiraOrganizationDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tconst myResources = resources ?? (await this.getProviderResourcesForUser(session));\n\t\tif (!myResources) return undefined;\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\n\t\tconst results: IssueShape[] = [];\n\t\tlet truncated = false;\n\t\tlet metadata: CollectionMetadata | undefined;\n\t\t// What each page reported failing, summarized at the end so a site whose many pages dropped issues reports\n\t\t// it once (see `summarizePageMetadata`); the pages keep the rest of their metadata, so `truncated` holds.\n\t\tconst pages: CollectionMetadata[] = [];\n\t\tconst readPage = async (resourceId: string, cursor: string | undefined) => {\n\t\t\tconst page = await api.getIssuesForResourceForCurrentUser(tokenWithInfo, resourceId, {\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t});\n\t\t\tif (!page.metadata?.failures?.length) return page;\n\n\t\t\tconst { failures: _failures, ...rest } = page.metadata;\n\t\t\tpages.push(page.metadata);\n\t\t\treturn { ...page, metadata: rest };\n\t\t};\n\t\tfor (const resource of myResources) {\n\t\t\tif (cancellation?.aborted) break;\n\n\t\t\tconst site = await collectProviderPagedResult(\n\t\t\t\tcursor =>\n\t\t\t\t\t// Cancellation is not truncation: the caller asked for the stop, so the site ends where it is.\n\t\t\t\t\tcancellation?.aborted\n\t\t\t\t\t\t? Promise.resolve({ values: [], paging: { cursor: '{}', more: false } })\n\t\t\t\t\t\t: readPage(resource.id, cursor),\n\t\t\t\tmaxPagesPerRequest,\n\t\t\t\t{ providerId: this.id, resourceId: resource.id },\n\t\t\t);\n\t\t\tresults.push(\n\t\t\t\t...site.values\n\t\t\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'account' }))\n\t\t\t\t\t.filter((result): result is IssueShape => result != null),\n\t\t\t);\n\t\t\ttruncated ||= site.truncated === true;\n\t\t\tmetadata = mergeCollectionMetadata(metadata, site.metadata);\n\t\t}\n\n\t\treturn {\n\t\t\tvalues: results,\n\t\t\ttruncated: truncated,\n\t\t\tmetadata: mergeCollectionMetadata(summarizePageMetadata(pages), metadata),\n\t\t};\n\t}\n\n\tprotected override async getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: JiraOrganizationDescriptor,\n\t\t{ key }: { id: string; key: string },\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst issue = await api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resource.id,\n\t\t\tnumber: key,\n\t\t});\n\t\treturn issue != null ? toIssueShape(issue, this, { projection: 'point' }) : undefined;\n\t}\n\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: JiraOrganizationDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst apiResult = await api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resource.id,\n\t\t\tnumber: id,\n\t\t});\n\t\tconst issue = apiResult != null ? toIssueShape(apiResult, this, { projection: 'point' }) : undefined;\n\t\treturn issue != null ? { ...issue, type: 'issue' } : undefined;\n\t}\n\n\tprotected override async getProviderIssueByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresourceId: string,\n\t\tid: string,\n\t\tresourceUrl: string | undefined,\n\t): Promise<Issue | undefined> {\n\t\tif (resourceUrl == null) {\n\t\t\tthrow new Error('Jira direct issue reads require a resource URL');\n\t\t}\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst apiResult = await api.getJiraIssueByKey(toTokenWithInfo(this.id, session), resourceId, resourceUrl, id);\n\t\tconst issue = apiResult != null ? toIssueShape(apiResult, this, { projection: 'batch' }) : undefined;\n\t\treturn issue != null ? { ...issue, type: 'issue' } : undefined;\n\t}\n\n\t/**\n\t * The cheap check behind the batch issue read's etags: one bulk fetch per site and {@link jiraBulkFetchMaxKeys}\n\t * distinct keys, where {@link getProviderIssueByResourceId} sends one request per issue. A request that throws\n\t * rejects only its own targets' slots, with its error classified as the full read's would be.\n\t *\n\t * A target identified by a numeric issue id, which both reads accept as well as a key, is matched by the returned\n\t * issue's `id`; every other target by its key.\n\t *\n\t * Never proves an absence. Jira silently omits a key it can't find or show (listing it in no `issueErrors` either),\n\t * and answers a moved issue under its new key, which no longer matches the key asked for. Either way the target's\n\t * slot is rejected, so the full read distinguishes a missing issue from a site failure. When no target was\n\t * answered and nothing failed, the check declines, so a batch of only such keys costs a full read, not a failure.\n\t */\n\tprotected override async getProviderIssuesEtagFieldsByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\ttargets: readonly { resourceId: string; identifier: string; resourceUrl?: string }[],\n\t): Promise<PromiseSettledResult<IssueEtagFields | undefined>[] | undefined> {\n\t\tconst slots = new Array<PromiseSettledResult<IssueEtagFields | undefined>>(targets.length);\n\t\tlet answered = false;\n\t\tlet failed = false;\n\n\t\t// Each site's distinct keys, matched case-insensitively as Jira matches them, with the targets asking for each.\n\t\tconst bySite = new Map<string, Map<string, number[]>>();\n\t\tfor (const [index, target] of targets.entries()) {\n\t\t\tif (target.resourceUrl == null) {\n\t\t\t\t// Required as the full read requires it, so a check never answers for a target the full read refuses.\n\t\t\t\tslots[index] = {\n\t\t\t\t\tstatus: 'rejected',\n\t\t\t\t\treason: new Error('Jira direct issue reads require a resource URL'),\n\t\t\t\t};\n\t\t\t\tfailed = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet keys = bySite.get(target.resourceId);\n\t\t\tif (keys == null) {\n\t\t\t\tkeys = new Map();\n\t\t\t\tbySite.set(target.resourceId, keys);\n\t\t\t}\n\n\t\t\tconst key = target.identifier.toUpperCase();\n\t\t\tlet indices = keys.get(key);\n\t\t\tif (indices == null) {\n\t\t\t\tindices = [];\n\t\t\t\tkeys.set(key, indices);\n\t\t\t}\n\t\t\tindices.push(index);\n\t\t}\n\n\t\tconst requests = [...bySite].flatMap(([resourceId, keys]) =>\n\t\t\tchunk([...keys], jiraBulkFetchMaxKeys).map(entries => ({ resourceId: resourceId, entries: entries })),\n\t\t);\n\t\tif (!requests.length) return slots;\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst answers = await mapSettledBounded(requests, providerFanOutConcurrency, r =>\n\t\t\tapi.getJiraIssuesEtagFields(\n\t\t\t\ttokenWithInfo,\n\t\t\t\tr.resourceId,\n\t\t\t\tr.entries.map(([key]) => key),\n\t\t\t),\n\t\t);\n\n\t\tfor (const [i, answer] of answers.entries()) {\n\t\t\tconst { entries } = requests[i];\n\t\t\tif (answer.status === 'rejected') {\n\t\t\t\tfailed = true;\n\t\t\t\tfor (const [, indices] of entries) {\n\t\t\t\t\tfor (const index of indices) {\n\t\t\t\t\t\tslots[index] = answer;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (answer.value.errorCount > 0) {\n\t\t\t\tfailed = true;\n\t\t\t}\n\n\t\t\t// By key or id, never by position: Jira returns the issues in its own order, and only the ones it found.\n\t\t\tconst byKey = new Map<string, JiraIssueEtagResponse>();\n\t\t\tconst byId = new Map<string, JiraIssueEtagResponse>();\n\t\t\tfor (const issue of answer.value.issues) {\n\t\t\t\tbyKey.set(issue.key.toUpperCase(), issue);\n\t\t\t\tbyId.set(issue.id, issue);\n\t\t\t}\n\n\t\t\tfor (const [key, indices] of entries) {\n\t\t\t\t// A key target never matches by id, so an issue returned under a different key (moved) is rejected.\n\t\t\t\tconst issue = numericIssueId.test(key) ? byId.get(key) : byKey.get(key);\n\t\t\t\tlet slot: PromiseSettledResult<IssueEtagFields | undefined>;\n\t\t\t\tif (issue == null) {\n\t\t\t\t\tslot = { status: 'rejected', reason: new Error(`Jira bulk fetch did not return ${key}`) };\n\t\t\t\t} else {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tslot = { status: 'fulfilled', value: toJiraIssueEtagFields(issue) };\n\t\t\t\t\t\tanswered = true;\n\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\tslot = { status: 'rejected', reason: ex };\n\t\t\t\t\t\tfailed = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfor (const index of indices) {\n\t\t\t\t\tslots[index] = slot;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn answered || failed ? slots : undefined;\n\t}\n\n\tprotected override async providerOnConnect(): Promise<void> {\n\t\tthis._autolinks.clear();\n\t\tif (this._session == null) return;\n\n\t\tconst session = this._session;\n\t\tconst { accessToken } = session;\n\t\tconst organizationsGeneration = this._organizations.generation;\n\t\tconst projectsGeneration = this._projects.generation;\n\n\t\t// Persisted discovery seeds the caches only while it would still be fresh in them: read within the TTL, and\n\t\t// after the caches were last dropped. Otherwise a re-sync, which runs this again, would put back the very\n\t\t// project list it just dropped, and a restart would trust one read any time before (#5907).\n\t\tconst isFresh = (timestamp: unknown): timestamp is number =>\n\t\t\ttypeof timestamp === 'number' &&\n\t\t\ttimestamp > this._discoveryInvalidatedAt &&\n\t\t\tDate.now() - timestamp < discoveryCacheTtl;\n\n\t\tconst storedOrganizations = this.ctx.storage.get(`jira:${accessToken}:organizations`);\n\t\t// Projects persisted without their sites belong to a session whose sites were cleared below; ignore them.\n\t\tconst storedProjects =\n\t\t\tstoredOrganizations != null ? this.ctx.storage.get(`jira:${accessToken}:projects`) : undefined;\n\n\t\tlet organizations: JiraOrganizationDescriptor[] | undefined;\n\t\tif (storedOrganizations?.data != null && isFresh(storedOrganizations.timestamp)) {\n\t\t\tconst seeded = storedOrganizations.data.map((o: JiraOrganizationDescriptor) => ({ ...o }));\n\t\t\torganizations = seeded;\n\t\t\tthis._organizations.set(accessToken, seeded, {\n\t\t\t\tgeneration: organizationsGeneration,\n\t\t\t\tstoredAt: storedOrganizations.timestamp,\n\t\t\t});\n\t\t} else {\n\t\t\t// Stamped with when the read started, so the TTL counts from what it actually saw.\n\t\t\tconst readAt = Date.now();\n\t\t\torganizations = await this.getProviderResourcesForUser(session, true);\n\t\t\tif (storedOrganizations == null) {\n\t\t\t\t// Clear all other stored organizations and projects when our session changes\n\t\t\t\tawait this.ctx.storage.deleteWithPrefix('jira');\n\t\t\t}\n\t\t\t// Not persisted when the caches were dropped while it was in flight: it is then a read from before the\n\t\t\t// refresh, and the next start, which knows nothing of that refresh, would take it for fresh.\n\t\t\tif (this._organizations.generation === organizationsGeneration) {\n\t\t\t\tawait this.ctx.storage.store(`jira:${accessToken}:organizations`, {\n\t\t\t\t\tv: 1,\n\t\t\t\t\ttimestamp: readAt,\n\t\t\t\t\tdata: organizations,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tif (storedProjects?.data != null && isFresh(storedProjects.timestamp)) {\n\t\t\tconst projectsByResource = new Map<string, JiraProjectDescriptor[]>();\n\t\t\tfor (const project of storedProjects.data as JiraProjectDescriptor[]) {\n\t\t\t\tconst projects = projectsByResource.get(project.resourceId);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tprojectsByResource.set(project.resourceId, [{ ...project }]);\n\t\t\t\t} else if (!projects.some(p => p.id === project.id)) {\n\t\t\t\t\tprojects.push({ ...project });\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const [resourceId, projects] of projectsByResource) {\n\t\t\t\tthis._projects.set(`${accessToken}:${resourceId}`, projects, {\n\t\t\t\t\tgeneration: projectsGeneration,\n\t\t\t\t\tstoredAt: storedProjects.timestamp,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (organizations?.length) {\n\t\t\tconst readAt = Date.now();\n\t\t\tconst projects = await this.getProviderProjectsForResources(session, organizations, true);\n\t\t\t// See the organizations above.\n\t\t\tif (this._projects.generation === projectsGeneration) {\n\t\t\t\tawait this.ctx.storage.store(`jira:${accessToken}:projects`, {\n\t\t\t\t\tv: 1,\n\t\t\t\t\ttimestamp: readAt,\n\t\t\t\t\tdata: projects,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tprotected override clearDiscoveryCaches(): void {\n\t\tthis._discoveryInvalidatedAt = Date.now();\n\t\tthis._organizations.clear();\n\t\tthis._projects.clear();\n\t\tthis._autolinks.clear();\n\t}\n\n\tprotected override providerOnDisconnect(): void {\n\t\tthis._organizations.clear();\n\t\tthis._projects.clear();\n\t\tthis._autolinks.clear();\n\t}\n}\n\n/** The projects once each, by site and id. */\nfunction uniqueProjects(projects: JiraProjectDescriptor[]): JiraProjectDescriptor[] {\n\tconst projectsByIdentity = new Map<string, JiraProjectDescriptor>();\n\tfor (const project of projects) {\n\t\tconst identity = `${project.resourceId}:${project.id}`;\n\t\tif (!projectsByIdentity.has(identity)) {\n\t\t\tprojectsByIdentity.set(identity, project);\n\t\t}\n\t}\n\treturn [...projectsByIdentity.values()];\n}\n"],"mappings":"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"}
|
|
@@ -5,10 +5,10 @@ import { Issue, IssueShape } from "../../../git/models/issue.js";
|
|
|
5
5
|
import { Account } from "../../../git/models/author.js";
|
|
6
6
|
import { ConnectionResource, ProviderAuthenticationSession } from "../authentication/models.js";
|
|
7
7
|
import { IssuesCloudHostIntegrationId } from "../constants.js";
|
|
8
|
-
import { ProviderApiCollectionResult } from "./models.js";
|
|
8
|
+
import { ProviderApiCollectionResult, ProviderHierarchyResult, ProviderLinearProject } from "./models.js";
|
|
9
9
|
import { AccountWideIssuesResult, IssuesForProjectOptions, ProjectIssuesDrain, ProjectIssuesRequest, SearchMyIssuesOptions } from "../models/issueReads.js";
|
|
10
10
|
import { IssuesIntegration } from "../models/issuesIntegration.js";
|
|
11
|
-
import { IssueEtagFields } from "../models/integration.js";
|
|
11
|
+
import { IntegrationResult, IssueEtagFields } from "../models/integration.js";
|
|
12
12
|
import { IntegrationAuthenticationProviderDescriptor } from "../authentication/integrationAuthenticationProvider.js";
|
|
13
13
|
//#region ../plus/integrations/src/providers/linear.d.ts
|
|
14
14
|
export interface LinearTeamDescriptor extends IssueResourceDescriptor {
|
|
@@ -45,6 +45,34 @@ export declare class LinearIntegration extends IssuesIntegration<IssuesCloudHost
|
|
|
45
45
|
private getTeams;
|
|
46
46
|
private getTeamsWithMetadata;
|
|
47
47
|
private readTeams;
|
|
48
|
+
/**
|
|
49
|
+
* The workspace's Linear Projects (Linear's `Project` entity), drained across their pages. Separate from the
|
|
50
|
+
* teams this integration lists as its projects: a Linear Project can span several teams, and selecting one
|
|
51
|
+
* narrows an issue read (`IssuesForProjectOptions.linearProjects`) rather than naming what is read.
|
|
52
|
+
*
|
|
53
|
+
* Cached like the teams, and only when complete: a drain cut short by its backstop or a failed page is returned
|
|
54
|
+
* with its `truncated`/`metadata` so the facade can say so, and read again next time. A first-page failure
|
|
55
|
+
* rejects, which the result wrapper turns into `{ error }`. `fresh` reads past the cache and any read in flight,
|
|
56
|
+
* for a caller that must not take a list from before what it needs to see (a Project created since).
|
|
57
|
+
*/
|
|
58
|
+
getWorkspaceProjectsResult(connectionId?: string, options?: {
|
|
59
|
+
fresh?: boolean;
|
|
60
|
+
}): Promise<IntegrationResult<ProviderHierarchyResult<ProviderLinearProject>>>;
|
|
61
|
+
/** Expires, and is dropped on a re-sync, so a Project created or archived mid-session is picked up. */
|
|
62
|
+
private readonly _workspaceProjects;
|
|
63
|
+
/**
|
|
64
|
+
* In flight by token, with the cache generation it started under: concurrent cold reads share one drain, but a
|
|
65
|
+
* read after the cache was dropped (a `forceSync`) never joins one started before, which could miss what it asks
|
|
66
|
+
* to see.
|
|
67
|
+
*/
|
|
68
|
+
private readonly _workspaceProjectsReads;
|
|
69
|
+
private getWorkspaceProjects;
|
|
70
|
+
/**
|
|
71
|
+
* Drains the Projects cursor up to {@link maxWorkspaceProjectPages}. A first-page failure rejects, so the read
|
|
72
|
+
* fails as the team read does rather than reading as a workspace with no Projects; a later one keeps the prefix
|
|
73
|
+
* and records the failure, as do a stalled cursor and the backstop with pages left.
|
|
74
|
+
*/
|
|
75
|
+
private readWorkspaceProjects;
|
|
48
76
|
protected getProviderResourcesForUser(session: ProviderAuthenticationSession): Promise<ResourceDescriptor[] | undefined>;
|
|
49
77
|
protected getProviderUncachedResourcesForUser(session: ProviderAuthenticationSession): Promise<ResourceDescriptor[] | undefined>;
|
|
50
78
|
protected getProviderProjectsForResources(session: ProviderAuthenticationSession, _resources: ResourceDescriptor[]): Promise<ResourceDescriptor[] | undefined>;
|
|
@@ -67,6 +95,11 @@ export declare class LinearIntegration extends IssuesIntegration<IssuesCloudHost
|
|
|
67
95
|
* Searches every team of a user-scoped read together: one `getIssues` names all of them and the viewer as
|
|
68
96
|
* assignee, so a team with none of the user's issues costs no request of its own. Linear's filter is a GraphQL
|
|
69
97
|
* body, not a query string, so there is no key bound to chunk at.
|
|
98
|
+
*
|
|
99
|
+
* A read narrowed to Linear Projects is searched together even across every assignee: the Projects already bound
|
|
100
|
+
* it to a few teams' worth of issues, so a team holding none of them costs no request either. An unnarrowed read
|
|
101
|
+
* of every assignee is still read team by team, since a search over all of them would just spend one team's page
|
|
102
|
+
* budget before falling back.
|
|
70
103
|
*/
|
|
71
104
|
protected getProjectIssuesSearches(requests: readonly ProjectIssuesRequest<ResourceDescriptor>[]): number[][] | undefined;
|
|
72
105
|
protected searchProviderProjectIssues(session: ProviderAuthenticationSession, requests: readonly ProjectIssuesRequest<ResourceDescriptor>[]): Promise<IssueShape[][] | undefined>;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { chunk } from "../../../utils/array.js";
|
|
2
2
|
import { Logger as _logger } from "../../../utils/logger.js";
|
|
3
|
+
import { getScopedLogger } from "../../../utils/logger.scoped.js";
|
|
3
4
|
import { mapSettledBounded } from "../../../utils/promise.js";
|
|
5
|
+
import { trace } from "../../../utils/decorators/log.js";
|
|
4
6
|
import { PromiseCache } from "../../../utils/promiseCache.js";
|
|
7
|
+
import __decorate from "../../../_virtual/@oxc-project+runtime/helpers/esm/decorate.js";
|
|
5
8
|
import { isIssueResourceDescriptor } from "../../../git/utils/resourceDescriptor.utils.js";
|
|
6
|
-
import { IntegrationReadUnavailableError } from "../errors.js";
|
|
9
|
+
import { IntegrationReadUnavailableError, toError } from "../errors.js";
|
|
7
10
|
import "../constants.js";
|
|
8
11
|
import { toCollectionScopeFailure } from "../collectionMetadata.js";
|
|
9
12
|
import { toTokenWithInfo } from "../authentication/models.js";
|
|
10
|
-
import { mergeCollectionMetadata } from "./utils/providerPaging.js";
|
|
13
|
+
import { collectProviderPagedResult, mergeCollectionMetadata } from "./utils/providerPaging.js";
|
|
11
14
|
import { fromProviderIssue, providersMetadata, toIssueShape, toProviderIssueStates } from "./models.js";
|
|
15
|
+
import { isReadSessionFailure } from "../models/integration.js";
|
|
12
16
|
import { IssuesIntegration, groupProjectIssuesSearches, splitProjectIssuesSearch } from "../models/issuesIntegration.js";
|
|
13
17
|
import { createIssueAutolinkReferences } from "./utils/issueAutolinks.js";
|
|
14
18
|
import { DiscoveryCache } from "./utils/discoveryCache.js";
|
|
@@ -36,6 +40,8 @@ const maxPagesPerRequest = 10;
|
|
|
36
40
|
*/
|
|
37
41
|
const maxAccountWidePagesPerRequest = 50;
|
|
38
42
|
const linearImplicitTeamsPageSize = 50;
|
|
43
|
+
/** The workspace Projects read's backstop: provider-apis pages them 100 at a time, so 2,000 Projects. */
|
|
44
|
+
const maxWorkspaceProjectPages = 20;
|
|
39
45
|
/** Long enough to span one tracker read, short enough that a profile change shows on the next. */
|
|
40
46
|
const recentAccountTTL = 6e4;
|
|
41
47
|
/** A team key and issue number, as Linear writes an issue's identifier (`ENG-123`). */
|
|
@@ -148,6 +154,84 @@ var LinearIntegration = class extends IssuesIntegration {
|
|
|
148
154
|
};
|
|
149
155
|
return { values: descriptors };
|
|
150
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* The workspace's Linear Projects (Linear's `Project` entity), drained across their pages. Separate from the
|
|
159
|
+
* teams this integration lists as its projects: a Linear Project can span several teams, and selecting one
|
|
160
|
+
* narrows an issue read (`IssuesForProjectOptions.linearProjects`) rather than naming what is read.
|
|
161
|
+
*
|
|
162
|
+
* Cached like the teams, and only when complete: a drain cut short by its backstop or a failed page is returned
|
|
163
|
+
* with its `truncated`/`metadata` so the facade can say so, and read again next time. A first-page failure
|
|
164
|
+
* rejects, which the result wrapper turns into `{ error }`. `fresh` reads past the cache and any read in flight,
|
|
165
|
+
* for a caller that must not take a list from before what it needs to see (a Project created since).
|
|
166
|
+
*/
|
|
167
|
+
async getWorkspaceProjectsResult(connectionId, options) {
|
|
168
|
+
const scope = getScopedLogger();
|
|
169
|
+
const session = await this.resolveReadSession(connectionId, scope);
|
|
170
|
+
if (session == null || isReadSessionFailure(session)) return session && { error: session.error };
|
|
171
|
+
try {
|
|
172
|
+
const projects = await this.getWorkspaceProjects(session, options?.fresh === true);
|
|
173
|
+
await this.confirmScopedAuthFailures(session, projects.metadata);
|
|
174
|
+
this.resetRequestExceptionCount("getProjectsForResources");
|
|
175
|
+
return { value: projects };
|
|
176
|
+
} catch (ex) {
|
|
177
|
+
this.handleProviderException("getProjectsForResources", ex, { connectionId });
|
|
178
|
+
return { error: toError(ex) };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Expires, and is dropped on a re-sync, so a Project created or archived mid-session is picked up. */
|
|
182
|
+
_workspaceProjects = new DiscoveryCache();
|
|
183
|
+
/**
|
|
184
|
+
* In flight by token, with the cache generation it started under: concurrent cold reads share one drain, but a
|
|
185
|
+
* read after the cache was dropped (a `forceSync`) never joins one started before, which could miss what it asks
|
|
186
|
+
* to see.
|
|
187
|
+
*/
|
|
188
|
+
_workspaceProjectsReads = /* @__PURE__ */ new Map();
|
|
189
|
+
getWorkspaceProjects(session, fresh) {
|
|
190
|
+
const { accessToken } = session;
|
|
191
|
+
const generation = this._workspaceProjects.generation;
|
|
192
|
+
if (!fresh) {
|
|
193
|
+
const cached = this._workspaceProjects.get(accessToken);
|
|
194
|
+
if (cached != null) return Promise.resolve({ values: cached });
|
|
195
|
+
const inFlight = this._workspaceProjectsReads.get(accessToken);
|
|
196
|
+
if (inFlight?.generation === generation) return inFlight.read;
|
|
197
|
+
}
|
|
198
|
+
const entry = {
|
|
199
|
+
generation,
|
|
200
|
+
read: this.readWorkspaceProjects(session)
|
|
201
|
+
};
|
|
202
|
+
entry.read = entry.read.then((result) => {
|
|
203
|
+
if (this._workspaceProjectsReads.get(accessToken) === entry && result.truncated !== true && (result.metadata == null || result.metadata.completeness === "complete")) this._workspaceProjects.set(accessToken, result.values, { generation });
|
|
204
|
+
return result;
|
|
205
|
+
});
|
|
206
|
+
this._workspaceProjectsReads.set(accessToken, entry);
|
|
207
|
+
entry.read.finally(() => {
|
|
208
|
+
if (this._workspaceProjectsReads.get(accessToken) === entry) this._workspaceProjectsReads.delete(accessToken);
|
|
209
|
+
}).catch(() => {});
|
|
210
|
+
return entry.read;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Drains the Projects cursor up to {@link maxWorkspaceProjectPages}. A first-page failure rejects, so the read
|
|
214
|
+
* fails as the team read does rather than reading as a workspace with no Projects; a later one keeps the prefix
|
|
215
|
+
* and records the failure, as do a stalled cursor and the backstop with pages left.
|
|
216
|
+
*/
|
|
217
|
+
async readWorkspaceProjects(session) {
|
|
218
|
+
const api = await this.getProvidersApi();
|
|
219
|
+
const tokenWithInfo = toTokenWithInfo(this.id, session);
|
|
220
|
+
let pagesRead = 0;
|
|
221
|
+
let firstPageError;
|
|
222
|
+
const read = await collectProviderPagedResult(async (cursor) => {
|
|
223
|
+
try {
|
|
224
|
+
const page = await api.getLinearProjects(tokenWithInfo, { cursor });
|
|
225
|
+
pagesRead++;
|
|
226
|
+
return page;
|
|
227
|
+
} catch (ex) {
|
|
228
|
+
if (pagesRead === 0) firstPageError = toError(ex);
|
|
229
|
+
throw ex;
|
|
230
|
+
}
|
|
231
|
+
}, maxWorkspaceProjectPages, { providerId: this.id });
|
|
232
|
+
if (firstPageError != null) throw firstPageError;
|
|
233
|
+
return read;
|
|
234
|
+
}
|
|
151
235
|
async getProviderResourcesForUser(session) {
|
|
152
236
|
const organization = await this.getOrganization(session);
|
|
153
237
|
return organization != null ? [organization] : void 0;
|
|
@@ -174,6 +258,8 @@ var LinearIntegration = class extends IssuesIntegration {
|
|
|
174
258
|
clearDiscoveryCaches() {
|
|
175
259
|
this._organizations = void 0;
|
|
176
260
|
this._teams.clear();
|
|
261
|
+
this._workspaceProjects.clear();
|
|
262
|
+
this._workspaceProjectsReads.clear();
|
|
177
263
|
this._autolinks.clear();
|
|
178
264
|
this._recentAccounts.clear();
|
|
179
265
|
}
|
|
@@ -221,6 +307,7 @@ var LinearIntegration = class extends IssuesIntegration {
|
|
|
221
307
|
}
|
|
222
308
|
const drain = await this.drainIssues(session, {
|
|
223
309
|
teams: [project.id],
|
|
310
|
+
...options?.linearProjects?.length ? { projects: [...options.linearProjects] } : {},
|
|
224
311
|
...viewerId != null ? { assignees: [viewerId] } : {},
|
|
225
312
|
states: toProviderIssueStates(options?.state)
|
|
226
313
|
}, options?.sort, {
|
|
@@ -242,25 +329,34 @@ var LinearIntegration = class extends IssuesIntegration {
|
|
|
242
329
|
* Searches every team of a user-scoped read together: one `getIssues` names all of them and the viewer as
|
|
243
330
|
* assignee, so a team with none of the user's issues costs no request of its own. Linear's filter is a GraphQL
|
|
244
331
|
* body, not a query string, so there is no key bound to chunk at.
|
|
332
|
+
*
|
|
333
|
+
* A read narrowed to Linear Projects is searched together even across every assignee: the Projects already bound
|
|
334
|
+
* it to a few teams' worth of issues, so a team holding none of them costs no request either. An unnarrowed read
|
|
335
|
+
* of every assignee is still read team by team, since a search over all of them would just spend one team's page
|
|
336
|
+
* budget before falling back.
|
|
245
337
|
*/
|
|
246
338
|
getProjectIssuesSearches(requests) {
|
|
247
|
-
return groupProjectIssuesSearches(requests, ({ project, options }) => options.user != null && isIssueResourceDescriptor(project) ? [
|
|
248
|
-
options.user,
|
|
339
|
+
return groupProjectIssuesSearches(requests, ({ project, options }) => (options.user != null || options.linearProjects?.length) && isIssueResourceDescriptor(project) ? [
|
|
340
|
+
options.user ?? "",
|
|
249
341
|
options.userId ?? "",
|
|
250
342
|
options.sort ?? "",
|
|
251
|
-
options.state ?? ""
|
|
343
|
+
options.state ?? "",
|
|
344
|
+
...options.linearProjects ?? []
|
|
252
345
|
].join("\0") : void 0);
|
|
253
346
|
}
|
|
254
347
|
async searchProviderProjectIssues(session, requests) {
|
|
255
348
|
const { options } = requests[0];
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
349
|
+
let viewerId;
|
|
350
|
+
if (options.user != null) {
|
|
351
|
+
viewerId = await this.getViewerId(session, options);
|
|
352
|
+
if (viewerId == null) return void 0;
|
|
353
|
+
} else if (!options.linearProjects?.length) return;
|
|
259
354
|
const teams = requests.map((request) => request.project).filter(isIssueResourceDescriptor);
|
|
260
355
|
if (teams.length !== requests.length) return void 0;
|
|
261
356
|
const drain = await this.drainIssues(session, {
|
|
262
357
|
teams: teams.map((team) => team.id),
|
|
263
|
-
|
|
358
|
+
...options.linearProjects?.length ? { projects: [...options.linearProjects] } : {},
|
|
359
|
+
...viewerId != null ? { assignees: [viewerId] } : {},
|
|
264
360
|
states: toProviderIssueStates(options.state)
|
|
265
361
|
}, options.sort, { providerId: this.id });
|
|
266
362
|
if (drain.truncated || drain.metadata != null && drain.metadata.completeness !== "complete") return;
|
|
@@ -532,6 +628,7 @@ var LinearIntegration = class extends IssuesIntegration {
|
|
|
532
628
|
return url.substring(0, url.length - lastSegment.length - 1);
|
|
533
629
|
}
|
|
534
630
|
};
|
|
631
|
+
__decorate([trace()], LinearIntegration.prototype, "getWorkspaceProjectsResult", null);
|
|
535
632
|
//#endregion
|
|
536
633
|
export { LinearIntegration };
|
|
537
634
|
|