@gitkraken/core-gitlens 0.9.8 → 0.9.9

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 (30) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/dist/plus/integrations/collectionMetadata.d.ts +7 -0
  3. package/dist/plus/integrations/collectionMetadata.js +11 -1
  4. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  5. package/dist/plus/integrations/models/issueReads.d.ts +5 -0
  6. package/dist/plus/integrations/models/issuesIntegration.d.ts +37 -1
  7. package/dist/plus/integrations/models/issuesIntegration.js +118 -2
  8. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  9. package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
  10. package/dist/plus/integrations/providers/jira-server.d.ts +8 -1
  11. package/dist/plus/integrations/providers/jira-server.js +90 -138
  12. package/dist/plus/integrations/providers/jira-server.js.map +1 -1
  13. package/dist/plus/integrations/providers/jira.d.ts +11 -1
  14. package/dist/plus/integrations/providers/jira.js +88 -122
  15. package/dist/plus/integrations/providers/jira.js.map +1 -1
  16. package/dist/plus/integrations/providers/linear.d.ts +22 -1
  17. package/dist/plus/integrations/providers/linear.js +84 -35
  18. package/dist/plus/integrations/providers/linear.js.map +1 -1
  19. package/dist/plus/integrations/providers/models.d.ts +5 -9
  20. package/dist/plus/integrations/providers/models.js.map +1 -1
  21. package/dist/plus/integrations/providers/providersApi.d.ts +24 -3
  22. package/dist/plus/integrations/providers/providersApi.js +56 -4
  23. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  24. package/dist/plus/integrations/providers/utils/jiraIssueDrains.d.ts +86 -0
  25. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js +139 -0
  26. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js.map +1 -0
  27. package/dist/plus/integrations/reads/drains.d.ts +0 -1
  28. package/dist/plus/integrations/reads/issueTracker.js +20 -9
  29. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  30. package/package.json +2 -2
@@ -3,15 +3,16 @@ import { Logger as _logger } from "../../../utils/logger.js";
3
3
  import { mapSettledBounded } from "../../../utils/promise.js";
4
4
  import "../constants.js";
5
5
  import { isJiraMissingProjectError } from "./providerErrors.js";
6
- import { throwIfCallerContractError, toCollectionScopeFailure } from "../collectionMetadata.js";
6
+ import { toCollectionScopeFailure } from "../collectionMetadata.js";
7
7
  import { toTokenWithInfo } from "../authentication/models.js";
8
- import "../providerFilters.js";
9
8
  import { collectProviderPagedResult, mergeCollectionMetadata } from "./utils/providerPaging.js";
10
9
  import { providersMetadata, toAccount, toIssueShape, toProviderIssueStates } from "./models.js";
11
- import { IssuesIntegration } from "../models/issuesIntegration.js";
10
+ import { IssuesIntegration, groupProjectIssuesSearches } from "../models/issuesIntegration.js";
12
11
  import { DiscoveryCache } from "./utils/discoveryCache.js";
12
+ import { drainJiraIssues, readUserScopedJiraIssues, searchJiraProjectIssues, toUniqueJiraIssueShapes } from "./utils/jiraIssueDrains.js";
13
13
  import { toJiraIssueEtagFields } from "./jiraIssueByKey.js";
14
14
  import * as l10n from "@vscode/l10n";
15
+ import { JIRA_MAX_PROJECT_KEYS_PER_REQUEST } from "@gitkraken/provider-apis";
15
16
  //#region ../plus/integrations/src/providers/jira.ts
16
17
  const metadata = providersMetadata["jira"];
17
18
  const authProvider = Object.freeze({
@@ -21,6 +22,23 @@ const authProvider = Object.freeze({
21
22
  const maxPagesPerRequest = 10;
22
23
  /** A numeric issue id, which Jira's issue reads accept in place of a key. */
23
24
  const numericIssueId = /^\d+$/;
25
+ /**
26
+ * `assignee` and `creator` are user FIELDS: Jira resolves an accountId against the directory, which is the identity
27
+ * that keeps matching once a display name cannot be looked up — a deactivated account, or a profile whose visibility
28
+ * hides the name. Measured against a live site: a deactivated assignee returns its issues by accountId and an empty
29
+ * page by display name, and Jira reports that miss as a successful empty search rather than an error, so the list
30
+ * simply appears empty.
31
+ *
32
+ * `mention` is NOT a user field. It is `comment ~ "..."`, a free-text search over comment bodies, so an accountId
33
+ * matches nothing there and the display name is the only value that can. Hence the id is applied to the first two
34
+ * and the handle is kept for the third, rather than swapping `user` wholesale.
35
+ */
36
+ function toUserIdentities(user, options) {
37
+ return {
38
+ userField: options.userId ?? user,
39
+ mention: user
40
+ };
41
+ }
24
42
  var JiraIntegration = class extends IssuesIntegration {
25
43
  authProvider = authProvider;
26
44
  id = "jira";
@@ -217,135 +235,44 @@ var JiraIntegration = class extends IssuesIntegration {
217
235
  async getProviderIssuesForProjectWithTruncation(session, project, options) {
218
236
  const tokenWithInfo = toTokenWithInfo(this.id, session);
219
237
  const api = await this.getProvidersApi();
220
- const projectScope = {
221
- providerId: this.id,
222
- resourceId: project.resourceId,
223
- projectId: project.name
224
- };
225
238
  const states = toProviderIssueStates(options?.state);
226
- const drainIssues = async (scope) => {
227
- const collected = [];
228
- let cursor;
229
- let status = "complete";
230
- let metadata;
231
- for (let i = 0; i < maxPagesPerRequest; i++) {
232
- let result;
233
- try {
234
- result = await api.getIssuesForProjectPaged(tokenWithInfo, project.name, project.resourceId, {
235
- ...scope,
236
- cursor,
237
- states,
238
- sort: options?.sort
239
- });
240
- } catch (ex) {
241
- if (collected.length === 0 && isJiraMissingProjectError(ex)) {
242
- _logger.warn(`Jira project '${project.key}' no longer exists or is not visible`);
243
- this._projects.delete(`${session.accessToken}:${project.resourceId}`);
244
- this._autolinks.delete(session.accessToken);
245
- return {
246
- issues: [],
247
- status: "complete"
248
- };
249
- }
250
- if (collected.length === 0) throw ex;
251
- status = "incomplete";
252
- metadata = mergeCollectionMetadata(metadata, {
253
- completeness: "partial",
254
- failures: [toCollectionScopeFailure(projectScope, ex)]
255
- });
256
- break;
257
- }
258
- if (result == null) {
259
- if (cursor == null) break;
260
- status = "incomplete";
261
- metadata = mergeCollectionMetadata(metadata, {
262
- completeness: "partial",
263
- failures: [toCollectionScopeFailure(projectScope, /* @__PURE__ */ new Error("Jira returned no page after advertising a continuation"))]
264
- });
265
- break;
266
- }
267
- collected.push(...result.data);
268
- if (!result.hasMore) break;
269
- if (result.nextCursor == null || result.nextCursor === cursor) {
270
- status = "incomplete";
271
- metadata = mergeCollectionMetadata(metadata, {
272
- completeness: "partial",
273
- failures: [toCollectionScopeFailure(projectScope, /* @__PURE__ */ new Error("Jira returned no advancing issue continuation"))]
274
- });
275
- break;
276
- }
277
- cursor = result.nextCursor;
278
- if (i === 9) status = "backstop";
239
+ const fetchPage = (scope, cursor) => api.getIssuesForProjectPaged(tokenWithInfo, project.name, project.resourceId, {
240
+ ...scope,
241
+ cursor,
242
+ states,
243
+ sort: options?.sort
244
+ });
245
+ const drainOptions = {
246
+ maxPages: maxPagesPerRequest,
247
+ failureScope: {
248
+ providerId: this.id,
249
+ resourceId: project.resourceId,
250
+ projectId: project.name
251
+ },
252
+ providerName: "Jira",
253
+ isEmptyFailure: (ex) => {
254
+ if (!isJiraMissingProjectError(ex)) return false;
255
+ _logger.warn(`Jira project '${project.key}' no longer exists or is not visible`);
256
+ this._projects.delete(`${session.accessToken}:${project.resourceId}`);
257
+ this._autolinks.delete(session.accessToken);
258
+ return true;
279
259
  }
280
- return {
281
- issues: collected,
282
- status,
283
- metadata
284
- };
285
- };
286
- const getSearchedUserIssuesForFilter = async (user, filter) => {
287
- const userField = options?.userId ?? user;
288
- const result = await drainIssues({
289
- authorLogin: filter === "author" ? userField : void 0,
290
- assigneeLogins: filter === "assignee" ? [userField] : void 0,
291
- mentionLogin: filter === "mention" ? user : void 0
292
- });
293
- return {
294
- issues: result.issues.map((issue) => toIssueShape(issue, this, { projection: "project" })).filter((r) => r !== void 0),
295
- status: result.status,
296
- metadata: result.metadata
297
- };
298
260
  };
299
261
  if (options?.user != null) {
300
- const user = options.user;
301
- const filters = options.filters?.length ? options.filters : ["assignee"];
302
- const settled = await Promise.allSettled(filters.map((filter) => getSearchedUserIssuesForFilter(user, filter)));
303
- if (settled.every((r) => r.status === "rejected")) {
304
- for (let i = 1; i < settled.length; i++) {
305
- const outcome = settled[i];
306
- if (outcome.status === "rejected") _logger.error(outcome.reason, `getProviderIssuesForProjectWithTruncation: filter '${filters[i]}' failed`);
307
- }
308
- throw settled[0].status === "rejected" ? settled[0].reason : /* @__PURE__ */ new Error("Jira issue read failed");
309
- }
310
- let truncated = false;
311
- let metadata;
312
- const resultsById = /* @__PURE__ */ new Map();
313
- for (let i = 0; i < settled.length; i++) {
314
- const outcome = settled[i];
315
- const filter = filters[i];
316
- if (outcome.status !== "fulfilled") {
317
- throwIfCallerContractError(outcome.reason);
318
- truncated = true;
319
- const failure = toCollectionScopeFailure({
320
- providerId: this.id,
321
- resourceId: project.resourceId,
322
- projectId: project.name
323
- }, outcome.reason);
324
- metadata = mergeCollectionMetadata(metadata, {
325
- completeness: "partial",
326
- failures: [{
327
- ...failure,
328
- message: `Issue filter '${filter}' could not be read${failure.message != null ? `: ${failure.message}` : ""}`
329
- }]
330
- });
331
- continue;
332
- }
333
- if (outcome.value.status !== "complete") truncated = true;
334
- if (outcome.value.metadata != null) metadata = mergeCollectionMetadata(metadata, outcome.value.metadata);
335
- for (const resultIssue of outcome.value.issues) if (!resultsById.has(resultIssue.id)) resultsById.set(resultIssue.id, resultIssue);
336
- }
337
- return truncated ? {
338
- values: [...resultsById.values()],
262
+ const read = await readUserScopedJiraIssues(fetchPage, options.filters, toUserIdentities(options.user, options), drainOptions);
263
+ const values = toUniqueJiraIssueShapes(read.issues, this);
264
+ return read.truncated ? {
265
+ values,
339
266
  truncated: true,
340
267
  recovery: "none",
341
- metadata
268
+ metadata: read.metadata
342
269
  } : {
343
- values: [...resultsById.values()],
270
+ values,
344
271
  truncated: false,
345
- metadata
272
+ metadata: read.metadata
346
273
  };
347
274
  }
348
- const unscoped = await drainIssues({});
275
+ const unscoped = await drainJiraIssues(fetchPage, {}, drainOptions);
349
276
  const values = unscoped.issues.map((issue) => toIssueShape(issue, this, { projection: "project" })).filter((result) => result !== void 0);
350
277
  return unscoped.status !== "complete" ? {
351
278
  values,
@@ -358,6 +285,45 @@ var JiraIntegration = class extends IssuesIntegration {
358
285
  metadata: unscoped.metadata
359
286
  };
360
287
  }
288
+ /**
289
+ * Searches a site's projects together for a user-scoped read: one JQL names up to
290
+ * {@link JIRA_MAX_PROJECT_KEYS_PER_REQUEST} projects of one site and one user scope, past which the SDK refuses
291
+ * the call rather than risk a query string Jira rejects.
292
+ *
293
+ * An unscoped read is never searched across projects: it drains every issue in the project, and its backstop is
294
+ * reported as `narrow-scope` per project, which one search across projects could not attribute.
295
+ */
296
+ getProjectIssuesSearches(requests) {
297
+ return groupProjectIssuesSearches(requests, ({ project, options }) => options.user != null ? [
298
+ project.resourceId,
299
+ options.user,
300
+ options.userId ?? "",
301
+ options.sort ?? "",
302
+ options.state ?? "",
303
+ options.filters?.join(",") ?? ""
304
+ ].join("\0") : void 0, JIRA_MAX_PROJECT_KEYS_PER_REQUEST);
305
+ }
306
+ async searchProviderProjectIssues(session, requests) {
307
+ const { project, options } = requests[0];
308
+ if (options.user == null) return void 0;
309
+ const tokenWithInfo = toTokenWithInfo(this.id, session);
310
+ const api = await this.getProvidersApi();
311
+ const projectKeys = requests.map((request) => request.project.key);
312
+ const states = toProviderIssueStates(options.state);
313
+ return searchJiraProjectIssues((userScope, cursor) => api.getIssuesForProjectsPaged(tokenWithInfo, projectKeys, project.resourceId, {
314
+ ...userScope,
315
+ cursor,
316
+ states,
317
+ sort: options.sort
318
+ }), requests.map((request) => request.project.id), options.filters, toUserIdentities(options.user, options), {
319
+ maxPages: maxPagesPerRequest,
320
+ failureScope: {
321
+ providerId: this.id,
322
+ resourceId: project.resourceId
323
+ },
324
+ providerName: "Jira"
325
+ }, this);
326
+ }
361
327
  async searchProviderMyIssues(session, resources, cancellation) {
362
328
  return (await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation))?.values;
363
329
  }
@@ -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 * as l10n from '@vscode/l10n';\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 { throwIfCallerContractError, 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\tSearchMyIssuesOptions,\n} from '../models/issueReads.js';\nimport { IssuesIntegration } from '../models/issuesIntegration.js';\nimport type { JiraIssueEtagResponse } from './jiraIssueByKey.js';\nimport { jiraBulkFetchMaxKeys, toJiraIssueEtagFields } from './jiraIssueByKey.js';\nimport type { ProviderApiCollectionResult, ProviderIssue } from './models.js';\nimport { IssueFilter, providersMetadata, toAccount, toIssueShape, toProviderIssueStates } from './models.js';\nimport { isJiraMissingProjectError } from './providerErrors.js';\nimport { DiscoveryCache, discoveryCacheTtl } from './utils/discoveryCache.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\ntype JiraProjectIssuesDrain<T> = {\n\tissues: T[];\n\tstatus: 'complete' | 'backstop' | 'incomplete';\n\tmetadata?: CollectionMetadata;\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\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\tconst dashedPrefix = `${project.key}-`;\n\t\t\t\tconst underscoredPrefix = `${project.key}_`;\n\t\t\t\tautolinks.push({\n\t\t\t\t\tprefix: dashedPrefix,\n\t\t\t\t\turl: `${organization.url}/browse/${dashedPrefix}<num>`,\n\t\t\t\t\talphanumeric: false,\n\t\t\t\t\tignoreCase: false,\n\t\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\t\ttype: 'issue',\n\t\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\t\tdescriptor: { ...organization },\n\t\t\t\t});\n\t\t\t\tautolinks.push({\n\t\t\t\t\tprefix: underscoredPrefix,\n\t\t\t\t\turl: `${organization.url}/browse/${dashedPrefix}<num>`,\n\t\t\t\t\talphanumeric: false,\n\t\t\t\t\tignoreCase: false,\n\t\t\t\t\treferenceType: 'branch',\n\t\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\t\ttype: 'issue',\n\t\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\t\tdescriptor: { ...organization },\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/** 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 api = await this.getProvidersApi();\n\t\tconst resources = await api.getJiraResourcesForCurrentUser(toTokenWithInfo(this.id, session));\n\t\tconst organizations = resources?.map(r => ({ ...r, key: r.id }));\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 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\tlet resourcesWithoutProjects = [];\n\t\tif (force) {\n\t\t\tresourcesWithoutProjects = resources;\n\t\t} else {\n\t\t\tfor (const resource of resources) {\n\t\t\t\tconst resourceKey = `${accessToken}:${resource.id}`;\n\t\t\t\tconst cachedProjects = projectsCache.get(resourceKey);\n\t\t\t\tif (cachedProjects == null) {\n\t\t\t\t\tresourcesWithoutProjects.push(resource);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlet metadata: CollectionMetadata | undefined;\n\t\tconst partialProjects: JiraProjectDescriptor[] = [];\n\t\tif (resourcesWithoutProjects.length > 0) {\n\t\t\tconst api = await this.getProvidersApi();\n\t\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\t\tconst drains = await Promise.allSettled(\n\t\t\t\tresourcesWithoutProjects.map(async resource => ({\n\t\t\t\t\tresource: resource,\n\t\t\t\t\tresult: await collectProviderPagedResult(\n\t\t\t\t\t\tcursor => api.getJiraProjectsForResource(tokenWithInfo, resource.id, { cursor: cursor }),\n\t\t\t\t\t\tmaxPagesPerRequest,\n\t\t\t\t\t\t{ providerId: this.id, resourceId: resource.id },\n\t\t\t\t\t),\n\t\t\t\t})),\n\t\t\t);\n\n\t\t\tdrains.forEach((drain, index) => {\n\t\t\t\tif (drain.status === 'rejected') {\n\t\t\t\t\tconst resource = resourcesWithoutProjects[index];\n\t\t\t\t\tif (resource == null) return;\n\n\t\t\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\t\tfailures: [\n\t\t\t\t\t\t\ttoCollectionScopeFailure({ providerId: this.id, resourceId: resource.id }, drain.reason),\n\t\t\t\t\t\t],\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst { resource, result } = drain.value;\n\t\t\t\tmetadata = mergeCollectionMetadata(metadata, result.metadata);\n\t\t\t\tconst projects = result.values\n\t\t\t\t\t.filter(project => project.resourceId === resource.id)\n\t\t\t\t\t.map(project => ({ ...project }));\n\t\t\t\tconst incomplete =\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\tif (incomplete) {\n\t\t\t\t\tpartialProjects.push(...projects);\n\t\t\t\t\tif (\n\t\t\t\t\t\tresult.truncated === true &&\n\t\t\t\t\t\t(result.metadata == null || result.metadata.completeness === 'complete')\n\t\t\t\t\t) {\n\t\t\t\t\t\tmetadata = mergeCollectionMetadata(metadata, { completeness: 'partial' });\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Not stored when the cache was dropped while this read was in flight, but still returned below: it\n\t\t\t\t// is what this read found, only no longer trusted for the next one.\n\t\t\t\tif (!projectsCache.set(`${accessToken}:${resource.id}`, projects, { generation: generation })) {\n\t\t\t\t\tpartialProjects.push(...projects);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tthis._autolinks.delete(accessToken);\n\t\t\t});\n\t\t}\n\n\t\tconst values = resources.reduce<JiraProjectDescriptor[]>((projects, resource) => {\n\t\t\tconst resourceProjects = projectsCache.get(`${accessToken}:${resource.id}`);\n\t\t\tif (resourceProjects != null) {\n\t\t\t\tprojects.push(...resourceProjects);\n\t\t\t}\n\t\t\treturn projects;\n\t\t}, partialProjects);\n\n\t\tconst projectsByIdentity = new Map<string, JiraProjectDescriptor>();\n\t\tfor (const project of values) {\n\t\t\tconst identity = `${project.resourceId}:${project.id}`;\n\t\t\tif (!projectsByIdentity.has(identity)) {\n\t\t\t\tprojectsByIdentity.set(identity, project);\n\t\t\t}\n\t\t}\n\t\treturn { values: [...projectsByIdentity.values()], 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 projectScope = {\n\t\t\tproviderId: this.id,\n\t\t\tresourceId: project.resourceId,\n\t\t\tprojectId: project.name,\n\t\t};\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tconst drainIssues = async (scope: {\n\t\t\tauthorLogin?: string;\n\t\t\tassigneeLogins?: string[];\n\t\t\tmentionLogin?: string;\n\t\t}): Promise<JiraProjectIssuesDrain<ProviderIssue>> => {\n\t\t\tconst collected: ProviderIssue[] = [];\n\t\t\tlet cursor: string | undefined;\n\t\t\tlet status: JiraProjectIssuesDrain<ProviderIssue>['status'] = 'complete';\n\t\t\tlet metadata: CollectionMetadata | undefined;\n\t\t\tfor (let i = 0; i < maxPagesPerRequest; i++) {\n\t\t\t\tlet result: Awaited<ReturnType<typeof api.getIssuesForProjectPaged>> | undefined;\n\t\t\t\ttry {\n\t\t\t\t\tresult = await api.getIssuesForProjectPaged(tokenWithInfo, project.name, project.resourceId, {\n\t\t\t\t\t\t...scope,\n\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\tstates: states,\n\t\t\t\t\t\tsort: options?.sort,\n\t\t\t\t\t});\n\t\t\t\t} catch (ex) {\n\t\t\t\t\t// A project this token can no longer see (deleted, or its browse permission removed) holds no issues\n\t\t\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\t\t// re-reads it, instead of failing the whole provider on every read until a restart (#5907).\n\t\t\t\t\tif (collected.length === 0 && isJiraMissingProjectError(ex)) {\n\t\t\t\t\t\tLogger.warn(`Jira project '${project.key}' no longer exists or is not visible`);\n\t\t\t\t\t\tthis._projects.delete(`${session.accessToken}:${project.resourceId}`);\n\t\t\t\t\t\tthis._autolinks.delete(session.accessToken);\n\t\t\t\t\t\treturn { issues: [], status: 'complete' };\n\t\t\t\t\t}\n\n\t\t\t\t\t// A page failure after the first page leaves the already-drained prefix intact; record the\n\t\t\t\t\t// failure at the project scope instead of re-throwing and discarding the prefix. If nothing was\n\t\t\t\t\t// fetched yet, the original throw behavior is preserved so the caller sees a hard error rather\n\t\t\t\t\t// than an empty partial success.\n\t\t\t\t\tif (collected.length === 0) throw ex;\n\n\t\t\t\t\tstatus = 'incomplete';\n\t\t\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\t\tfailures: [toCollectionScopeFailure(projectScope, ex)],\n\t\t\t\t\t});\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (result == null) {\n\t\t\t\t\tif (cursor == null) break;\n\n\t\t\t\t\tstatus = 'incomplete';\n\t\t\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\t\tfailures: [\n\t\t\t\t\t\t\ttoCollectionScopeFailure(\n\t\t\t\t\t\t\t\tprojectScope,\n\t\t\t\t\t\t\t\tnew Error('Jira returned no page after advertising a continuation'),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t],\n\t\t\t\t\t});\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcollected.push(...result.data);\n\t\t\t\tif (!result.hasMore) break;\n\n\t\t\t\t// The provider claims more pages but gave no advancing cursor: we can't continue, so the drain\n\t\t\t\t// is incomplete — flag it rather than silently stopping (matches drainPullRequests/Repositories).\n\t\t\t\tif (result.nextCursor == null || result.nextCursor === cursor) {\n\t\t\t\t\tstatus = 'incomplete';\n\t\t\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\t\tfailures: [\n\t\t\t\t\t\t\ttoCollectionScopeFailure(\n\t\t\t\t\t\t\t\tprojectScope,\n\t\t\t\t\t\t\t\tnew Error('Jira returned no advancing issue continuation'),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t],\n\t\t\t\t\t});\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcursor = result.nextCursor;\n\t\t\t\tif (i === maxPagesPerRequest - 1) {\n\t\t\t\t\tstatus = 'backstop';\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tissues: collected,\n\t\t\t\tstatus: status,\n\t\t\t\tmetadata: metadata,\n\t\t\t};\n\t\t};\n\n\t\tconst getSearchedUserIssuesForFilter = async (\n\t\t\tuser: string,\n\t\t\tfilter: IssueFilter,\n\t\t): Promise<JiraProjectIssuesDrain<IssueShape>> => {\n\t\t\t// `assignee` and `creator` are user FIELDS: Jira resolves an accountId against the directory, which is\n\t\t\t// the identity that keeps matching once a display name cannot be looked up — a deactivated account, or\n\t\t\t// a profile whose visibility hides the name. Measured against a live site: a deactivated assignee\n\t\t\t// returns its issues by accountId and an empty page by display name, and Jira reports that miss as a\n\t\t\t// successful empty search rather than an error, so the list simply appears empty.\n\t\t\t//\n\t\t\t// `mention` is NOT a user field. It is `comment ~ \"...\"`, a free-text search over comment bodies, so an\n\t\t\t// accountId matches nothing there and the display name is the only value that can. Hence the id is\n\t\t\t// applied to the first two and the handle is kept for the third, rather than swapping `user` wholesale.\n\t\t\tconst userField = options?.userId ?? user;\n\t\t\tconst result = await drainIssues({\n\t\t\t\tauthorLogin: filter === IssueFilter.Author ? userField : undefined,\n\t\t\t\tassigneeLogins: filter === IssueFilter.Assignee ? [userField] : undefined,\n\t\t\t\tmentionLogin: filter === IssueFilter.Mention ? user : undefined,\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\tissues: result.issues\n\t\t\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'project' }))\n\t\t\t\t\t.filter((r): r is IssueShape => r !== undefined),\n\t\t\t\tstatus: result.status,\n\t\t\t\tmetadata: result.metadata,\n\t\t\t};\n\t\t};\n\n\t\tif (options?.user != null) {\n\t\t\tconst user = options.user;\n\t\t\t// A resolved user always scopes the read. Default to the assignee filter (\"my issues\") when no\n\t\t\t// explicit filters are given — otherwise a caller that scopes by user but omits filters would fall\n\t\t\t// through to the unscoped fetch below and get every issue in the project instead of the user's.\n\t\t\tconst filters = options.filters?.length ? options.filters : [IssueFilter.Assignee];\n\t\t\tconst settled = await Promise.allSettled(\n\t\t\t\tfilters.map(filter => getSearchedUserIssuesForFilter(user, filter)),\n\t\t\t);\n\n\t\t\t// If every filter branch rejected, the read failed outright — propagate the first rejection instead\n\t\t\t// of returning an empty list, which the facade (getIssuesForProjectResult → runCaptured) would\n\t\t\t// otherwise surface as a successful \"no issues\" rather than a warning + fetchFailed. The first\n\t\t\t// reason is re-thrown as-is (not wrapped in an AggregateError) so the facade can still classify it\n\t\t\t// by type (auth/rate-limit) — wrapping would collapse every failure to a generic 'other'. The\n\t\t\t// remaining reasons would otherwise be discarded, so log them here to keep them diagnosable.\n\t\t\tif (settled.every(r => r.status === 'rejected')) {\n\t\t\t\tfor (let i = 1; i < settled.length; i++) {\n\t\t\t\t\tconst outcome = settled[i];\n\t\t\t\t\tif (outcome.status === 'rejected') {\n\t\t\t\t\t\tLogger.error(\n\t\t\t\t\t\t\toutcome.reason,\n\t\t\t\t\t\t\t`getProviderIssuesForProjectWithTruncation: filter '${filters[i]}' failed`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthrow settled[0].status === 'rejected' ? settled[0].reason : new Error('Jira issue read failed');\n\t\t\t}\n\n\t\t\tlet truncated = false;\n\t\t\tlet metadata: CollectionMetadata | undefined;\n\t\t\tconst resultsById = new Map<string, IssueShape>();\n\t\t\tfor (let i = 0; i < settled.length; i++) {\n\t\t\t\tconst outcome = settled[i];\n\t\t\t\tconst filter = filters[i];\n\t\t\t\t// A rejected filter branch (with at least one sibling succeeding) means this project's issues are\n\t\t\t\t// incomplete: keep the sibling results but record a structured failure so the facade can warn on\n\t\t\t\t// the specific filter (auth/rate-limit) instead of just a generic truncation flag.\n\t\t\t\tif (outcome.status !== 'fulfilled') {\n\t\t\t\t\t// Identical for every filter branch, so degrading it would report one failure per branch for a\n\t\t\t\t\t// single invalid call — see `throwIfCallerContractError`.\n\t\t\t\t\tthrowIfCallerContractError(outcome.reason);\n\n\t\t\t\t\ttruncated = true;\n\t\t\t\t\tconst failure = toCollectionScopeFailure(\n\t\t\t\t\t\t{ providerId: this.id, resourceId: project.resourceId, projectId: project.name },\n\t\t\t\t\t\toutcome.reason,\n\t\t\t\t\t);\n\t\t\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\t\tfailures: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t...failure,\n\t\t\t\t\t\t\t\tmessage: `Issue filter '${filter}' could not be read${\n\t\t\t\t\t\t\t\t\tfailure.message != null ? `: ${failure.message}` : ''\n\t\t\t\t\t\t\t\t}`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (outcome.value.status !== 'complete') {\n\t\t\t\t\ttruncated = true;\n\t\t\t\t}\n\t\t\t\tif (outcome.value.metadata != null) {\n\t\t\t\t\tmetadata = mergeCollectionMetadata(metadata, outcome.value.metadata);\n\t\t\t\t}\n\t\t\t\tfor (const resultIssue of outcome.value.issues) {\n\t\t\t\t\tif (!resultsById.has(resultIssue.id)) {\n\t\t\t\t\t\tresultsById.set(resultIssue.id, resultIssue);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn truncated\n\t\t\t\t? { values: [...resultsById.values()], truncated: true, recovery: 'none', metadata: metadata }\n\t\t\t\t: { values: [...resultsById.values()], truncated: false, metadata: metadata };\n\t\t}\n\n\t\tconst unscoped = await drainIssues({});\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\treturn unscoped.status !== 'complete'\n\t\t\t? {\n\t\t\t\t\tvalues: values,\n\t\t\t\t\ttruncated: true,\n\t\t\t\t\trecovery: unscoped.status === 'backstop' ? 'narrow-scope' : 'none',\n\t\t\t\t\tmetadata: unscoped.metadata,\n\t\t\t\t}\n\t\t\t: { values: values, truncated: false, metadata: unscoped.metadata };\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\treturn (await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation))?.values;\n\t}\n\n\t/**\n\t * Overridden so `state` reaches the per-site read, which the default drops. Still reports `truncated: false`,\n\t * as the default did: the per-site loop has no way to say it stopped early.\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\n\t\tconst results: IssueShape[] = [];\n\t\tfor (const resource of myResources) {\n\t\t\tif (cancellation?.aborted) break;\n\n\t\t\ttry {\n\t\t\t\tlet cursor = undefined;\n\t\t\t\tlet hasMore = false;\n\t\t\t\tlet requestCount = 0;\n\t\t\t\tdo {\n\t\t\t\t\tif (cancellation?.aborted) break;\n\n\t\t\t\t\tconst resourceIssues = await api.getIssuesForResourceForCurrentUser(\n\t\t\t\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\t\t\t\tresource.id,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\t\tstates: states,\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t\trequestCount += 1;\n\t\t\t\t\thasMore = resourceIssues.paging?.more ?? false;\n\t\t\t\t\tcursor = resourceIssues.paging?.cursor;\n\t\t\t\t\tconst formattedIssues = resourceIssues.values\n\t\t\t\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'account' }))\n\t\t\t\t\t\t.filter((result): result is IssueShape => result != null);\n\t\t\t\t\tif (formattedIssues.length > 0) {\n\t\t\t\t\t\tresults.push(...formattedIssues);\n\t\t\t\t\t}\n\t\t\t\t} while (requestCount < maxPagesPerRequest && hasMore);\n\t\t\t} catch (ex) {\n\t\t\t\t// TODO: We need a better way to message the failure to the user here.\n\t\t\t\t// This is a stopgap to prevent one bag org from throwing and preventing any issues from being returned.\n\t\t\t\tLogger.error(ex, 'searchProviderMyIssues');\n\t\t\t}\n\t\t}\n\n\t\treturn { values: results, truncated: false };\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 =\n\t\t\tapiResult != null\n\t\t\t\t? toIssueShape(apiResult, this, { reliableStateCategory: true, projection: 'batch' })\n\t\t\t\t: 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\toverride invalidateDiscoveryCaches(): 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"],"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 * as l10n from '@vscode/l10n';\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 type { JiraDrainOptions, JiraIssuePageFetcher } from './utils/jiraIssueDrains.js';\nimport {\n\tdrainJiraIssues,\n\treadUserScopedJiraIssues,\n\tsearchJiraProjectIssues,\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\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\tconst dashedPrefix = `${project.key}-`;\n\t\t\t\tconst underscoredPrefix = `${project.key}_`;\n\t\t\t\tautolinks.push({\n\t\t\t\t\tprefix: dashedPrefix,\n\t\t\t\t\turl: `${organization.url}/browse/${dashedPrefix}<num>`,\n\t\t\t\t\talphanumeric: false,\n\t\t\t\t\tignoreCase: false,\n\t\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\t\ttype: 'issue',\n\t\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\t\tdescriptor: { ...organization },\n\t\t\t\t});\n\t\t\t\tautolinks.push({\n\t\t\t\t\tprefix: underscoredPrefix,\n\t\t\t\t\turl: `${organization.url}/browse/${dashedPrefix}<num>`,\n\t\t\t\t\talphanumeric: false,\n\t\t\t\t\tignoreCase: false,\n\t\t\t\t\treferenceType: 'branch',\n\t\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\t\ttype: 'issue',\n\t\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\t\tdescriptor: { ...organization },\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/** 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 api = await this.getProvidersApi();\n\t\tconst resources = await api.getJiraResourcesForCurrentUser(toTokenWithInfo(this.id, session));\n\t\tconst organizations = resources?.map(r => ({ ...r, key: r.id }));\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 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\tlet resourcesWithoutProjects = [];\n\t\tif (force) {\n\t\t\tresourcesWithoutProjects = resources;\n\t\t} else {\n\t\t\tfor (const resource of resources) {\n\t\t\t\tconst resourceKey = `${accessToken}:${resource.id}`;\n\t\t\t\tconst cachedProjects = projectsCache.get(resourceKey);\n\t\t\t\tif (cachedProjects == null) {\n\t\t\t\t\tresourcesWithoutProjects.push(resource);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlet metadata: CollectionMetadata | undefined;\n\t\tconst partialProjects: JiraProjectDescriptor[] = [];\n\t\tif (resourcesWithoutProjects.length > 0) {\n\t\t\tconst api = await this.getProvidersApi();\n\t\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\t\tconst drains = await Promise.allSettled(\n\t\t\t\tresourcesWithoutProjects.map(async resource => ({\n\t\t\t\t\tresource: resource,\n\t\t\t\t\tresult: await collectProviderPagedResult(\n\t\t\t\t\t\tcursor => api.getJiraProjectsForResource(tokenWithInfo, resource.id, { cursor: cursor }),\n\t\t\t\t\t\tmaxPagesPerRequest,\n\t\t\t\t\t\t{ providerId: this.id, resourceId: resource.id },\n\t\t\t\t\t),\n\t\t\t\t})),\n\t\t\t);\n\n\t\t\tdrains.forEach((drain, index) => {\n\t\t\t\tif (drain.status === 'rejected') {\n\t\t\t\t\tconst resource = resourcesWithoutProjects[index];\n\t\t\t\t\tif (resource == null) return;\n\n\t\t\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\t\tfailures: [\n\t\t\t\t\t\t\ttoCollectionScopeFailure({ providerId: this.id, resourceId: resource.id }, drain.reason),\n\t\t\t\t\t\t],\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst { resource, result } = drain.value;\n\t\t\t\tmetadata = mergeCollectionMetadata(metadata, result.metadata);\n\t\t\t\tconst projects = result.values\n\t\t\t\t\t.filter(project => project.resourceId === resource.id)\n\t\t\t\t\t.map(project => ({ ...project }));\n\t\t\t\tconst incomplete =\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\tif (incomplete) {\n\t\t\t\t\tpartialProjects.push(...projects);\n\t\t\t\t\tif (\n\t\t\t\t\t\tresult.truncated === true &&\n\t\t\t\t\t\t(result.metadata == null || result.metadata.completeness === 'complete')\n\t\t\t\t\t) {\n\t\t\t\t\t\tmetadata = mergeCollectionMetadata(metadata, { completeness: 'partial' });\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Not stored when the cache was dropped while this read was in flight, but still returned below: it\n\t\t\t\t// is what this read found, only no longer trusted for the next one.\n\t\t\t\tif (!projectsCache.set(`${accessToken}:${resource.id}`, projects, { generation: generation })) {\n\t\t\t\t\tpartialProjects.push(...projects);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tthis._autolinks.delete(accessToken);\n\t\t\t});\n\t\t}\n\n\t\tconst values = resources.reduce<JiraProjectDescriptor[]>((projects, resource) => {\n\t\t\tconst resourceProjects = projectsCache.get(`${accessToken}:${resource.id}`);\n\t\t\tif (resourceProjects != null) {\n\t\t\t\tprojects.push(...resourceProjects);\n\t\t\t}\n\t\t\treturn projects;\n\t\t}, partialProjects);\n\n\t\tconst projectsByIdentity = new Map<string, JiraProjectDescriptor>();\n\t\tfor (const project of values) {\n\t\t\tconst identity = `${project.resourceId}:${project.id}`;\n\t\t\tif (!projectsByIdentity.has(identity)) {\n\t\t\t\tprojectsByIdentity.set(identity, project);\n\t\t\t}\n\t\t}\n\t\treturn { values: [...projectsByIdentity.values()], 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\tconst fetchPage: JiraIssuePageFetcher = (scope, cursor) =>\n\t\t\tapi.getIssuesForProjectPaged(tokenWithInfo, project.name, 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.name },\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\treturn read.truncated\n\t\t\t\t? { values: values, truncated: true, recovery: 'none', metadata: read.metadata }\n\t\t\t\t: { values: values, truncated: false, metadata: read.metadata };\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\treturn unscoped.status !== 'complete'\n\t\t\t? {\n\t\t\t\t\tvalues: values,\n\t\t\t\t\ttruncated: true,\n\t\t\t\t\trecovery: unscoped.status === 'backstop' ? 'narrow-scope' : 'none',\n\t\t\t\t\tmetadata: unscoped.metadata,\n\t\t\t\t}\n\t\t\t: { values: values, truncated: false, metadata: unscoped.metadata };\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\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\treturn (await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation))?.values;\n\t}\n\n\t/**\n\t * Overridden so `state` reaches the per-site read, which the default drops. Still reports `truncated: false`,\n\t * as the default did: the per-site loop has no way to say it stopped early.\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\n\t\tconst results: IssueShape[] = [];\n\t\tfor (const resource of myResources) {\n\t\t\tif (cancellation?.aborted) break;\n\n\t\t\ttry {\n\t\t\t\tlet cursor = undefined;\n\t\t\t\tlet hasMore = false;\n\t\t\t\tlet requestCount = 0;\n\t\t\t\tdo {\n\t\t\t\t\tif (cancellation?.aborted) break;\n\n\t\t\t\t\tconst resourceIssues = await api.getIssuesForResourceForCurrentUser(\n\t\t\t\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\t\t\t\tresource.id,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\t\tstates: states,\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t\trequestCount += 1;\n\t\t\t\t\thasMore = resourceIssues.paging?.more ?? false;\n\t\t\t\t\tcursor = resourceIssues.paging?.cursor;\n\t\t\t\t\tconst formattedIssues = resourceIssues.values\n\t\t\t\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'account' }))\n\t\t\t\t\t\t.filter((result): result is IssueShape => result != null);\n\t\t\t\t\tif (formattedIssues.length > 0) {\n\t\t\t\t\t\tresults.push(...formattedIssues);\n\t\t\t\t\t}\n\t\t\t\t} while (requestCount < maxPagesPerRequest && hasMore);\n\t\t\t} catch (ex) {\n\t\t\t\t// TODO: We need a better way to message the failure to the user here.\n\t\t\t\t// This is a stopgap to prevent one bag org from throwing and preventing any issues from being returned.\n\t\t\t\tLogger.error(ex, 'searchProviderMyIssues');\n\t\t\t}\n\t\t}\n\n\t\treturn { values: results, truncated: false };\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 =\n\t\t\tapiResult != null\n\t\t\t\t? toIssueShape(apiResult, this, { reliableStateCategory: true, projection: 'batch' })\n\t\t\t\t: 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\toverride invalidateDiscoveryCaches(): 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"],"mappings":"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"}
@@ -6,7 +6,7 @@ import { Account } from "../../../git/models/author.js";
6
6
  import { ProviderAuthenticationSession } from "../authentication/models.js";
7
7
  import { IssuesCloudHostIntegrationId } from "../constants.js";
8
8
  import { ProviderApiCollectionResult } from "./models.js";
9
- import { AccountWideIssuesResult, IssuesForProjectOptions, ProjectIssuesDrain, SearchMyIssuesOptions } from "../models/issueReads.js";
9
+ import { AccountWideIssuesResult, IssuesForProjectOptions, ProjectIssuesDrain, ProjectIssuesRequest, SearchMyIssuesOptions } from "../models/issueReads.js";
10
10
  import { IssuesIntegration } from "../models/issuesIntegration.js";
11
11
  import { IssueEtagFields } from "../models/integration.js";
12
12
  import { IntegrationAuthenticationProviderDescriptor } from "../authentication/integrationAuthenticationProvider.js";
@@ -44,6 +44,27 @@ export declare class LinearIntegration extends IssuesIntegration<IssuesCloudHost
44
44
  protected getProviderAccountForResource(session: ProviderAuthenticationSession, _resource: ResourceDescriptor): Promise<Account | undefined>;
45
45
  protected getProviderIssuesForProject(session: ProviderAuthenticationSession, project: ResourceDescriptor, options?: IssuesForProjectOptions): Promise<IssueShape[] | undefined>;
46
46
  protected getProviderIssuesForProjectWithTruncation(session: ProviderAuthenticationSession, project: ResourceDescriptor, options?: IssuesForProjectOptions): Promise<ProjectIssuesDrain | undefined>;
47
+ /**
48
+ * Searches every team of a user-scoped read together: one `getIssues` names all of them and the viewer as
49
+ * assignee, so a team with none of the user's issues costs no request of its own. Linear's filter is a GraphQL
50
+ * body, not a query string, so there is no key bound to chunk at.
51
+ */
52
+ protected getProjectIssuesSearches(requests: readonly ProjectIssuesRequest<ResourceDescriptor>[]): number[][] | undefined;
53
+ protected searchProviderProjectIssues(session: ProviderAuthenticationSession, requests: readonly ProjectIssuesRequest<ResourceDescriptor>[]): Promise<IssueShape[][] | undefined>;
54
+ private readonly _viewerIds;
55
+ /**
56
+ * The viewer's Linear user id: the caller's resolved `userId` when it has one, else the viewer query, shared per
57
+ * token. A tracker read hands each team the id of the team's own resource, which Linear's team descriptors don't
58
+ * name, so without sharing every team read (and a search's fallback) would ask for the viewer again.
59
+ */
60
+ private getViewerId;
61
+ /**
62
+ * Follows `getIssues`' cursor up to {@link maxPagesPerRequest}. `truncated` is set when that backstop stopped
63
+ * the drain with more pages still available, when the provider stalled its cursor or flagged its paging as
64
+ * truncated, or when a page after the first failed, which is recorded at `failureScope` while the prefix is
65
+ * kept. A first-page failure throws, so the caller sees a hard error rather than an empty success.
66
+ */
67
+ private drainIssues;
47
68
  get id(): IssuesCloudHostIntegrationId.Linear;
48
69
  protected get key(): 'linear';
49
70
  get name(): string;