@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/git-cli/exec/git.js +1 -1
  3. package/dist/git-cli/providers/config.js +2 -2
  4. package/dist/git-cli/providers/pausedOperations.js +2 -2
  5. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  6. package/dist/git-cli/providers/staging.js +1 -1
  7. package/dist/ipc/discovery.js +1 -1
  8. package/dist/plus/agents/providers/claudeCodeTranscript.js +6 -6
  9. package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
  10. package/dist/plus/ai/providers/openAICompatibleProviderBase.js +1 -1
  11. package/dist/plus/integrations/index.d.ts +3 -3
  12. package/dist/plus/integrations/index.js.map +1 -1
  13. package/dist/plus/integrations/integrationService.d.ts +10 -2
  14. package/dist/plus/integrations/integrationService.js +9 -1
  15. package/dist/plus/integrations/integrationService.js.map +1 -1
  16. package/dist/plus/integrations/manager.d.ts +25 -1
  17. package/dist/plus/integrations/models/issueReads.d.ts +5 -0
  18. package/dist/plus/integrations/providers/azure/azure.js +1 -1
  19. package/dist/plus/integrations/providers/jira.d.ts +2 -2
  20. package/dist/plus/integrations/providers/jira.js +2 -2
  21. package/dist/plus/integrations/providers/jira.js.map +1 -1
  22. package/dist/plus/integrations/providers/linear.d.ts +35 -2
  23. package/dist/plus/integrations/providers/linear.js +106 -9
  24. package/dist/plus/integrations/providers/linear.js.map +1 -1
  25. package/dist/plus/integrations/providers/models.d.ts +4 -1
  26. package/dist/plus/integrations/providers/models.js.map +1 -1
  27. package/dist/plus/integrations/providers/providerApiClient.js +4 -1
  28. package/dist/plus/integrations/providers/providerApiClient.js.map +1 -1
  29. package/dist/plus/integrations/providers/providerErrors.d.ts +4 -1
  30. package/dist/plus/integrations/providers/providerErrors.js +13 -4
  31. package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
  32. package/dist/plus/integrations/providers/providersApi.d.ts +7 -1
  33. package/dist/plus/integrations/providers/providersApi.js +9 -1
  34. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  35. package/dist/plus/integrations/reads/cursors.d.ts +8 -0
  36. package/dist/plus/integrations/reads/cursors.js +14 -4
  37. package/dist/plus/integrations/reads/cursors.js.map +1 -1
  38. package/dist/plus/integrations/reads/hierarchy.d.ts +8 -2
  39. package/dist/plus/integrations/reads/hierarchy.js +42 -2
  40. package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
  41. package/dist/plus/integrations/reads/issueTracker.d.ts +10 -0
  42. package/dist/plus/integrations/reads/issueTracker.js +46 -5
  43. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  44. package/dist/plus/integrations/reads/selectionConfirmation.d.ts +5 -3
  45. package/dist/plus/integrations/reads/selectionConfirmation.js.map +1 -1
  46. package/dist/plus/integrations/reads/warnings.js +1 -1
  47. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  48. package/dist/plus/integrations/results.d.ts +18 -2
  49. package/dist/plus/integrations/results.js.map +1 -1
  50. package/dist/plus/integrations/utils/integration.utils.d.ts +3 -0
  51. package/dist/plus/integrations/utils/integration.utils.js +5 -1
  52. package/dist/plus/integrations/utils/integration.utils.js.map +1 -1
  53. package/dist/utils/env/node/exec.js +2 -2
  54. package/docs/integrations.md +37 -21
  55. 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, whose 404 does prove an absence, decides. When no target was answered and
110
- * nothing failed, the check declines instead, so a batch of only such keys costs a full read, not a failure.
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, whose 404 does prove an absence, decides. When no target was answered and
432
- * nothing failed, the check declines instead, so a batch of only such keys costs a full read, not a failure.
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":"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"}
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
- if (options.user == null) return void 0;
257
- const viewerId = await this.getViewerId(session, options);
258
- if (viewerId == null) return void 0;
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
- assignees: [viewerId],
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