@gitkraken/core-gitlens 0.10.1 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -1
- package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts +40 -27
- package/dist/plus/integrations/authentication/configuredIntegrationService.js +92 -22
- package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +10 -6
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +1 -1
- package/dist/plus/integrations/collectionMetadata.js +19 -8
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/errors.d.ts +2 -0
- package/dist/plus/integrations/errors.js +45 -1
- package/dist/plus/integrations/errors.js.map +1 -1
- package/dist/plus/integrations/integrationService.js +2 -1
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +18 -16
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +46 -8
- package/dist/plus/integrations/models/integration.js +159 -54
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.js +2 -6
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +1 -2
- package/dist/plus/integrations/providers/azureDevOps.js +12 -5
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
- package/dist/plus/integrations/providers/github.js +1 -4
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
- package/dist/plus/integrations/providers/jira.js +1 -0
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.js +1 -0
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +0 -1
- package/dist/plus/integrations/results.d.ts +8 -5
- package/dist/plus/integrations/results.js +3 -1
- package/dist/plus/integrations/results.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linear.js","names":[],"sources":["../../../../../plus/integrations/src/providers/linear.ts"],"sourcesContent":["import type { CollectionMetadata, CollectionScopeFailure, GitIssueState } 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, IssueOrPullRequestType } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor, ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { isIssueResourceDescriptor } from '@gitlens/git/utils/resourceDescriptor.utils.js';\nimport { chunk } from '@gitlens/utils/array.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport { mapSettledBounded } from '@gitlens/utils/promise.js';\nimport { PromiseCache } from '@gitlens/utils/promiseCache.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { IssuesCloudHostIntegrationId, providerFanOutConcurrency } from '../constants.js';\nimport { IntegrationReadUnavailableError } from '../errors.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 {\n\tgroupProjectIssuesSearches,\n\tIssuesIntegration,\n\tsplitProjectIssuesSearch,\n} from '../models/issuesIntegration.js';\nimport type { LinearIssueEtagNode } from './linearIssuesEtag.js';\nimport { linearIssuesEtagMaxNumbers, toLinearIssueEtagFields } from './linearIssuesEtag.js';\nimport type { ProviderApiCollectionResult, ProviderIssue } from './models.js';\nimport { fromProviderIssue, providersMetadata, toIssueShape, toProviderIssueStates } from './models.js';\nimport { DiscoveryCache, discoveryCacheTtl } from './utils/discoveryCache.js';\nimport { mergeCollectionMetadata } from './utils/providerPaging.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Linear];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\nconst maxPagesPerRequest = 10;\n/**\n * The account-wide drain's own backstop, separate from {@link maxPagesPerRequest}.\n *\n * Raised to 50 (5,000 issues at the SDK's 100-per-page) because 10 no longer means what it used to. That read\n * once fanned out over four overlapping relationship queries and merged them, so most of what a page cost was\n * rows already returned — a low budget capped the waste. It is now a single server-ordered query over an `or`\n * filter, so every page is 100 issues the caller has not seen, and the same budget just truncates real results\n * at 1,000.\n *\n * Reaching it is reported rather than silent: {@link LinearIntegration.searchProviderMyIssuesWithTruncation}\n * returns `truncated`, so the facade can say the read was incomplete instead of publishing a capped list as a\n * whole account. The bound stays — a runaway cursor must not spend requests forever — but it is now set where a\n * real Linear account is unlikely to reach it rather than where the duplication used to hurt.\n */\nconst maxAccountWidePagesPerRequest = 50;\nconst linearImplicitTeamsPageSize = 50;\n/** A team key and issue number, as Linear writes an issue's identifier (`ENG-123`). */\nconst linearIdentifier = /^([A-Za-z0-9]+)-(\\d+)$/;\n\nexport interface LinearTeamDescriptor extends IssueResourceDescriptor {\n\tavatarUrl: string | undefined;\n}\n\nexport interface LinearOrganizationDescriptor extends IssueResourceDescriptor {\n\turl: string;\n}\n\nexport interface LinearProjectDescriptor extends IssueResourceDescriptor {}\n\nexport class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Linear> {\n\t/**\n\t * Built from the teams, so it follows them: rebuilt when the team list changes or the caches are dropped, and once\n\t * older than {@link discoveryCacheTtl}. An expired set is still served while it is rebuilt in the background, because\n\t * autolinks are read on every render of a commit message and must not wait on the network (#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) {\n\t\t\treturn [];\n\t\t}\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).catch(() => {});\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 the teams. */\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 generation = this._teams.generation;\n\t\tconst organization = await this.getOrganization(session);\n\t\tif (organization == null) return [];\n\n\t\tconst autolinks: (AutolinkReference | DynamicAutolinkReference)[] = [];\n\n\t\tconst teams = await this.getTeams(session);\n\t\tfor (const team of teams ?? []) {\n\t\t\tconst dashedPrefix = `${team.key}-`;\n\t\t\tconst underscoredPrefix = `${team.key}_`;\n\n\t\t\tautolinks.push({\n\t\t\t\tprefix: dashedPrefix,\n\t\t\t\turl: `${organization.url}/issue/${dashedPrefix}<num>`,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\ttype: 'issue',\n\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\tdescriptor: { ...organization },\n\t\t\t});\n\t\t\tautolinks.push({\n\t\t\t\tprefix: underscoredPrefix,\n\t\t\t\turl: `${organization.url}/issue/${dashedPrefix}<num>`,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\treferenceType: 'branch',\n\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\ttype: 'issue',\n\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\tdescriptor: { ...organization },\n\t\t\t});\n\t\t}\n\n\t\t// Not kept when built from teams read before the caches were dropped.\n\t\tif (this._teams.generation === generation) {\n\t\t\tthis._autolinks.set(session.accessToken, { autolinks: autolinks, builtAt: Date.now() });\n\t\t}\n\t\treturn autolinks;\n\t}\n\n\tprivate _organizations: Map<string, LinearOrganizationDescriptor | undefined> | undefined;\n\tprivate async getOrganization(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<LinearOrganizationDescriptor | undefined> {\n\t\tconst { accessToken } = session;\n\t\tthis._organizations ??= new Map<string, LinearOrganizationDescriptor | undefined>();\n\n\t\tconst cachedResources = this._organizations.get(accessToken);\n\n\t\tif (cachedResources == null || force) {\n\t\t\tconst api = await this.getProvidersApi();\n\t\t\tconst organization = await api.getLinearOrganization(toTokenWithInfo(this.id, session));\n\t\t\tconst descriptor: LinearOrganizationDescriptor | undefined = organization && {\n\t\t\t\tid: organization.id,\n\t\t\t\tkey: organization.key,\n\t\t\t\tname: organization.name,\n\t\t\t\turl: organization.url,\n\t\t\t};\n\t\t\tif (descriptor) {\n\t\t\t\tthis._organizations.set(accessToken, descriptor);\n\t\t\t}\n\t\t}\n\n\t\treturn this._organizations.get(accessToken);\n\t}\n\n\t/** Expires, and is dropped on a re-sync, so a team created or left mid-session is picked up (#5907). */\n\tprivate readonly _teams = new DiscoveryCache<LinearTeamDescriptor[]>();\n\tprivate async getTeams(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<LinearTeamDescriptor[] | undefined> {\n\t\treturn (await this.getTeamsWithMetadata(session, force))?.values;\n\t}\n\n\tprivate async getTeamsWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<ProviderApiCollectionResult<LinearTeamDescriptor> | undefined> {\n\t\tconst { accessToken } = session;\n\n\t\tconst cachedResources = this._teams.get(accessToken);\n\t\tif (cachedResources != null && !force) return { values: cachedResources };\n\n\t\tconst generation = this._teams.generation;\n\t\tconst api = await this.getProvidersApi();\n\t\tconst teams = await api.getLinearTeamsForCurrentUser(toTokenWithInfo(this.id, session));\n\t\tconst descriptors: LinearTeamDescriptor[] | undefined = teams?.map(t => ({\n\t\t\tid: t.id,\n\t\t\tkey: t.key,\n\t\t\tname: t.name,\n\t\t\tavatarUrl: t.iconUrl,\n\t\t}));\n\t\tif (descriptors == null) return undefined;\n\n\t\t// provider-apis currently requests Linear's teams connection without pageInfo or a cursor. Linear's\n\t\t// implicit page size is 50, so exactly a full page cannot prove there is no team 51. Preserve the useful\n\t\t// prefix, but mark it unknown and leave it uncached so the next read retries instead of treating it as\n\t\t// authoritative. Once provider-apis exposes a paged primitive, replace this fail-closed guard with a drain.\n\t\tif (descriptors.length >= linearImplicitTeamsPageSize) {\n\t\t\treturn { values: descriptors, metadata: { completeness: 'unknown' } };\n\t\t}\n\n\t\tif (this._teams.set(accessToken, descriptors, { generation: generation })) {\n\t\t\t// Built from the teams; a new set means new prefixes.\n\t\t\tthis._autolinks.delete(accessToken);\n\t\t}\n\t\treturn { values: descriptors };\n\t}\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<ResourceDescriptor[] | undefined> {\n\t\tconst organization = await this.getOrganization(session);\n\t\treturn organization != null ? [organization] : undefined;\n\t}\n\tprotected override getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources: ResourceDescriptor[],\n\t): Promise<ResourceDescriptor[] | undefined> {\n\t\treturn this.getTeams(session);\n\t}\n\tprotected override async getProviderProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources: ResourceDescriptor[],\n\t): Promise<ProviderApiCollectionResult<ResourceDescriptor>> {\n\t\treturn (await this.getTeamsWithMetadata(session)) ?? { values: [] };\n\t}\n\n\toverride invalidateDiscoveryCaches(): void {\n\t\tthis._organizations = undefined;\n\t\tthis._teams.clear();\n\t\tthis._autolinks.clear();\n\t}\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\t// Linear's viewer isn't a ProviderAccount (no username/avatar), so build the Account manually\n\t\t// (Trello-style) from the fields the viewer query returns.\n\t\tconst user = await api.getLinearCurrentUser(toTokenWithInfo(this.id, session));\n\t\tif (user == null) return undefined;\n\n\t\treturn {\n\t\t\tprovider: this,\n\t\t\tid: user.id,\n\t\t\tname: user.name ?? user.displayName ?? undefined,\n\t\t\tusername: user.displayName ?? undefined,\n\t\t\temail: user.email ?? undefined,\n\t\t\tavatarUrl: undefined,\n\t\t};\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\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: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tif (!isIssueResourceDescriptor(project)) return undefined;\n\n\t\t// Scope to \"my issues\" by the viewer's stable id, not the passed display name: Linear's `name` (full name)\n\t\t// and `displayName` (nickname) are distinct fields, and assignees are normalized with `.name` = the full\n\t\t// name while the caller's `user` is the displayName — so a name string can miss. The assignee `.id` is the\n\t\t// Linear user id, which is unambiguous.\n\t\tlet viewerId: string | undefined;\n\t\tif (options?.user != null) {\n\t\t\tviewerId = await this.getViewerId(session, options);\n\t\t\t// If the viewer can't be resolved we can't scope to \"my issues\" — returning the unfiltered team\n\t\t\t// issues would leak everyone else's, and returning [] is indistinguishable from \"no issues assigned\n\t\t\t// to me\". Throw so the facade (getIssuesForProjectResult → runCaptured) surfaces a warning +\n\t\t\t// fetchFailed the caller can act on, instead of a silent empty.\n\t\t\tif (viewerId == null) {\n\t\t\t\tthrow new IntegrationReadUnavailableError(\n\t\t\t\t\tmetadata.name,\n\t\t\t\t\t'could not resolve the current user to scope issues to',\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// `getProviderProjectsForResources` returns Linear teams, so `project.id` is a team id here.\n\t\t//\n\t\t// The state and the viewer are narrowed server-side, not after the drain: a team's done work and other\n\t\t// people's issues would otherwise count against the backstop, so a busy team runs out of pages before the\n\t\t// viewer's open issues and hands back a page that looks complete.\n\t\tconst drain = await this.drainIssues(\n\t\t\tsession,\n\t\t\t{\n\t\t\t\tteams: [project.id],\n\t\t\t\t...(viewerId != null ? { assignees: [viewerId] } : {}),\n\t\t\t\tstates: toProviderIssueStates(options?.state),\n\t\t\t},\n\t\t\toptions?.sort,\n\t\t\t{ providerId: this.id, projectId: project.id },\n\t\t);\n\n\t\treturn drain.truncated\n\t\t\t? { values: drain.issues, truncated: true, recovery: 'none', metadata: drain.metadata }\n\t\t\t: { values: drain.issues, truncated: false, metadata: drain.metadata };\n\t}\n\n\t/**\n\t * Searches every team of a user-scoped read together: one `getIssues` names all of them and the viewer as\n\t * assignee, so a team with none of the user's issues costs no request of its own. Linear's filter is a GraphQL\n\t * body, not a query string, so there is no key bound to chunk at.\n\t */\n\tprotected override getProjectIssuesSearches(\n\t\trequests: readonly ProjectIssuesRequest<ResourceDescriptor>[],\n\t): number[][] | undefined {\n\t\treturn groupProjectIssuesSearches(requests, ({ project, options }) =>\n\t\t\toptions.user != null && isIssueResourceDescriptor(project)\n\t\t\t\t? [options.user, options.userId ?? '', options.sort ?? '', options.state ?? ''].join('\\0')\n\t\t\t\t: undefined,\n\t\t);\n\t}\n\n\tprotected override async searchProviderProjectIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\trequests: readonly ProjectIssuesRequest<ResourceDescriptor>[],\n\t): Promise<IssueShape[][] | undefined> {\n\t\t// Every request of a search shares its user scope (see `getProjectIssuesSearches`).\n\t\tconst { options } = requests[0];\n\t\tif (options.user == null) return undefined;\n\n\t\t// Unresolvable here means each team's own read throws the same warning, so let them.\n\t\tconst viewerId = await this.getViewerId(session, options);\n\t\tif (viewerId == null) return undefined;\n\n\t\tconst teams = requests.map(request => request.project).filter(isIssueResourceDescriptor);\n\t\tif (teams.length !== requests.length) return undefined;\n\n\t\tconst drain = await this.drainIssues(\n\t\t\tsession,\n\t\t\t{\n\t\t\t\tteams: teams.map(team => team.id),\n\t\t\t\tassignees: [viewerId],\n\t\t\t\tstates: toProviderIssueStates(options.state),\n\t\t\t},\n\t\t\toptions.sort,\n\t\t\t// Never published: an incomplete search is read again per team, which records its own failures.\n\t\t\t{ providerId: this.id },\n\t\t);\n\t\t// Deliberately discarded rather than served: an incomplete search says nothing about which of its teams it\n\t\t// covered, so only the per-team reads can report each one's completeness. The waste is bounded by one team's\n\t\t// page budget, and paid only by a search that needed more than that or failed partway.\n\t\tif (drain.truncated || (drain.metadata != null && drain.metadata.completeness !== 'complete')) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// A normalized Linear issue carries its team only inside `project`, which is absent for an issue in no\n\t\t// Linear project, so split by the identifier instead: Linear numbers every issue `<team key>-<n>`, and\n\t\t// renumbers it under its new team's key when it moves.\n\t\treturn splitProjectIssuesSearch(\n\t\t\tteams.map(team => team.key),\n\t\t\tdrain.issues,\n\t\t\tissue => {\n\t\t\t\tconst separator = issue.id.lastIndexOf('-');\n\t\t\t\treturn separator > 0 ? issue.id.slice(0, separator) : undefined;\n\t\t\t},\n\t\t);\n\t}\n\n\tprivate readonly _viewerIds = new PromiseCache<string, string | undefined>({ capacity: 10 });\n\t/**\n\t * The viewer's Linear user id: the caller's resolved `userId` when it has one, else the viewer query, shared per\n\t * token. A tracker read hands each team the id of the team's own resource, which Linear's team descriptors don't\n\t * name, so without sharing every team read (and a search's fallback) would ask for the viewer again.\n\t */\n\tprivate getViewerId(\n\t\tsession: ProviderAuthenticationSession,\n\t\toptions: IssuesForProjectOptions,\n\t): Promise<string | undefined> {\n\t\tif (options.userId) return Promise.resolve(options.userId);\n\n\t\treturn this._viewerIds.getOrCreate(\n\t\t\tsession.accessToken,\n\t\t\tasync () => {\n\t\t\t\tconst api = await this.getProvidersApi();\n\t\t\t\treturn (await api.getLinearCurrentUser(toTokenWithInfo(this.id, session)))?.id;\n\t\t\t},\n\t\t\t// Only a resolved id is worth keeping; an unresolved viewer is asked for again next time, as is a failure.\n\t\t\t{ evictWhen: id => id == null },\n\t\t);\n\t}\n\n\t/**\n\t * Follows `getIssues`' cursor up to {@link maxPagesPerRequest}. `truncated` is set when that backstop stopped\n\t * the drain with more pages still available, when the provider stalled its cursor or flagged its paging as\n\t * truncated, or when a page after the first failed, which is recorded at `failureScope` while the prefix is\n\t * kept. A first-page failure throws, so the caller sees a hard error rather than an empty success.\n\t */\n\tprivate async drainIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tfilter: { teams: string[]; assignees?: string[]; states?: GitIssueState[] },\n\t\tsort: IssuesForProjectOptions['sort'],\n\t\tfailureScope: CollectionScopeFailure['scope'],\n\t): Promise<{ issues: IssueShape[]; truncated: boolean; metadata?: CollectionMetadata }> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst assignees = filter.assignees?.length ? filter.assignees : undefined;\n\t\tlet cursor: string | undefined;\n\t\tlet hasMore: boolean;\n\t\tlet requestCount = 0;\n\t\tlet truncated = false;\n\t\tlet collectionMetadata: CollectionMetadata | undefined;\n\t\tconst issues: IssueShape[] = [];\n\t\tdo {\n\t\t\tlet result: Awaited<ReturnType<typeof api.getLinearIssues>>;\n\t\t\ttry {\n\t\t\t\tresult = await api.getLinearIssues(toTokenWithInfo(this.id, session), filter, {\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tsort: sort,\n\t\t\t\t});\n\t\t\t} catch (ex) {\n\t\t\t\tif (issues.length === 0) throw ex;\n\n\t\t\t\ttruncated = true;\n\t\t\t\tcollectionMetadata = mergeCollectionMetadata(collectionMetadata, {\n\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\tfailures: [toCollectionScopeFailure(failureScope, ex)],\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\trequestCount += 1;\n\t\t\thasMore = result.paging?.more ?? false;\n\t\t\tconst nextCursor = result.paging?.cursor;\n\t\t\ttruncated ||= result.paging?.truncated === true;\n\t\t\tcollectionMetadata = mergeCollectionMetadata(collectionMetadata, result.metadata);\n\t\t\tfor (const issue of result.values) {\n\t\t\t\tconst shape = toIssueShape(issue, this, { projection: 'project' });\n\t\t\t\t// The query already scopes to the assignees; checking again keeps another user's issue out of \"my\n\t\t\t\t// issues\" should the provider ever return one.\n\t\t\t\tif (shape != null && (assignees == null || shape.assignees?.some(a => assignees.includes(a.id)))) {\n\t\t\t\t\tissues.push(shape);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// The provider claims more but returns no advancing cursor: we can't continue without re-reading the\n\t\t\t// same page, so the drain is incomplete — flag it rather than silently stopping.\n\t\t\tif (hasMore && (nextCursor == null || nextCursor === cursor)) {\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcursor = nextCursor;\n\t\t\tif (hasMore && requestCount >= maxPagesPerRequest) {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t} while (requestCount < maxPagesPerRequest && hasMore);\n\n\t\treturn { issues: issues, truncated: truncated, metadata: collectionMetadata };\n\t}\n\n\toverride get id(): IssuesCloudHostIntegrationId.Linear {\n\t\treturn IssuesCloudHostIntegrationId.Linear;\n\t}\n\tprotected override get key(): 'linear' {\n\t\treturn 'linear';\n\t}\n\toverride get name(): string {\n\t\treturn metadata.name;\n\t}\n\toverride get domain(): string {\n\t\treturn metadata.domain;\n\t}\n\tprotected override async searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\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 * Account-wide \"my issues\" for Linear, drained to exhaustion and reporting whether it got there.\n\t *\n\t * The SDK read is one server-ordered query over an `or` filter across the four relationships (assigned,\n\t * created, named in an issue's content, named in a comment), so each page is 100 issues the caller has not\n\t * seen and the pages are ordered as one sequence. Draining it is therefore just following the cursor.\n\t *\n\t * Overridden rather than left to the base default because that default hardcodes `truncated: false`, and a\n\t * bounded drain that stops early is exactly the case a caller must be able to see. Three things end the\n\t * drain, and only one of them is completion:\n\t *\n\t * - the provider says there is no next page — complete;\n\t * - the backstop is reached — `truncated`, because issues beyond it exist and were not read;\n\t * - the provider claims a next page but hands back a cursor that does not advance — also `truncated`, since\n\t * the read cannot continue and the remainder is unreachable rather than absent.\n\t *\n\t * Cancellation is deliberately NOT truncation: the caller asked for the read to stop, so the partial list is\n\t * what it asked for, and flagging it would surface an incompleteness warning for a user action.\n\t */\n\tprotected override async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\tif (resources != null) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tlet cursor = undefined;\n\t\t// Starts false so an immediate cancellation, which leaves the loop before the first response, reads as\n\t\t// \"no more pages known\" rather than as an unfinished drain.\n\t\tlet hasMore = false;\n\t\tlet requestCount = 0;\n\t\tlet truncated = false;\n\t\tconst issues = [];\n\t\ttry {\n\t\t\tdo {\n\t\t\t\tif (cancellation?.aborted) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tconst result = await api.getIssuesForCurrentUser(toTokenWithInfo(this.id, session), {\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\t\trequestCount += 1;\n\t\t\t\thasMore = result.paging?.more ?? false;\n\t\t\t\tconst nextCursor = result.paging?.cursor;\n\n\t\t\t\t// Keep this page before deciding whether to continue: the request is already paid for, so\n\t\t\t\t// dropping its rows would lose real results to save nothing.\n\t\t\t\tconst formattedIssues = result.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\tif (formattedIssues.length > 0) {\n\t\t\t\t\tissues.push(...formattedIssues);\n\t\t\t\t}\n\n\t\t\t\t// The provider claims more but hands back no advancing cursor: continuing would re-read the page\n\t\t\t\t// just returned. `ProvidersApi.getPagedResult` already normalizes this into `more: false`, so\n\t\t\t\t// this is a backstop for if that ever stops holding rather than a case seen today -- but the rest\n\t\t\t\t// of the account is unreachable either way, which is what `truncated` says.\n\t\t\t\tif (hasMore && (nextCursor == null || nextCursor === cursor)) {\n\t\t\t\t\ttruncated = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcursor = nextCursor;\n\t\t\t} while (requestCount < maxAccountWidePagesPerRequest && hasMore);\n\n\t\t\t// Ran out of budget with pages still to come.\n\t\t\tif (hasMore && requestCount >= maxAccountWidePagesPerRequest) {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t} catch (ex) {\n\t\t\tif (issues.length === 0) {\n\t\t\t\tthrow ex;\n\t\t\t}\n\n\t\t\t// Kept what was already fetched, so the list is real but short of the account.\n\t\t\ttruncated = true;\n\t\t\tLogger.error(ex, 'searchProviderMyIssues');\n\t\t}\n\t\treturn { values: issues, truncated: truncated };\n\t}\n\tprotected override async getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\t{ key }: { id: string; key: string },\n\t\t_type: undefined | IssueOrPullRequestType,\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst issue = await this.getRawProviderIssue(session, resource, key);\n\t\tconst autolinkableIssue: ProviderIssue | undefined = issue && {\n\t\t\t...issue,\n\t\t\turl: this.getIssueAutolinkLikeUrl(issue),\n\t\t};\n\t\treturn autolinkableIssue && toIssueShape(autolinkableIssue, this, { projection: 'point' });\n\t}\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tconst result = await this.getRawProviderIssue(session, resource, id);\n\t\treturn result && fromProviderIssue(result, this, { projection: 'point' });\n\t}\n\n\tprotected override async getProviderIssueByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresourceId: string,\n\t\tid: string,\n\t\t_resourceUrl: string | undefined,\n\t): Promise<Issue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst result = await api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resourceId,\n\t\t\tnumber: id,\n\t\t});\n\t\treturn result && fromProviderIssue(result, this, { projection: 'batch' });\n\t}\n\n\t/**\n\t * The cheap check behind the batch issue read's etags: one `issues` query per team and\n\t * {@link linearIssuesEtagMaxNumbers} distinct numbers, where {@link getProviderIssueByResourceId} sends one request\n\t * per issue. A request that throws rejects only its own targets' slots, with its error classified as the full\n\t * read's would be.\n\t *\n\t * Never proves an absence. The query filters by the team's CURRENT key and asks for archived issues too, but the\n\t * full read's `issue(id:)` may also resolve an identifier the issue no longer carries (its team was renamed, or it\n\t * moved to another team), so an issue the query leaves out may still exist. Its target's slot is rejected, as is a\n\t * target whose identifier isn't a team key and a number, so the full read decides. When no target was answered and\n\t * nothing failed, the check declines instead, so a batch of only such targets 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 workspace and team's distinct numbers, with the targets asking for each.\n\t\tconst byTeam = new Map<string, { teamKey: string; numbers: Map<number, number[]> }>();\n\t\tfor (const [index, target] of targets.entries()) {\n\t\t\tconst match = linearIdentifier.exec(target.identifier);\n\t\t\tconst number = match != null ? Number(match[2]) : Number.NaN;\n\t\t\t// Only a number written as Linear writes it, so `ENG-012` never answers for `ENG-12`.\n\t\t\tif (match == null || !Number.isSafeInteger(number) || String(number) !== match[2]) {\n\t\t\t\tslots[index] = {\n\t\t\t\t\tstatus: 'rejected',\n\t\t\t\t\treason: new Error(`Not a Linear issue identifier: ${target.identifier}`),\n\t\t\t\t};\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst teamKey = match[1].toUpperCase();\n\t\t\tconst groupKey = `${target.resourceId}\\n${teamKey}`;\n\t\t\tlet group = byTeam.get(groupKey);\n\t\t\tif (group == null) {\n\t\t\t\tgroup = { teamKey: teamKey, numbers: new Map() };\n\t\t\t\tbyTeam.set(groupKey, group);\n\t\t\t}\n\n\t\t\tlet indices = group.numbers.get(number);\n\t\t\tif (indices == null) {\n\t\t\t\tindices = [];\n\t\t\t\tgroup.numbers.set(number, indices);\n\t\t\t}\n\t\t\tindices.push(index);\n\t\t}\n\n\t\tconst requests = [...byTeam.values()].flatMap(({ teamKey, numbers }) =>\n\t\t\tchunk([...numbers], linearIssuesEtagMaxNumbers).map(entries => ({ teamKey: teamKey, entries: entries })),\n\t\t);\n\t\tif (requests.length) {\n\t\t\tconst api = await this.getProvidersApi();\n\t\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\t\tconst answers = await mapSettledBounded(requests, providerFanOutConcurrency, r =>\n\t\t\t\tapi.getLinearIssuesEtagFields(\n\t\t\t\t\ttokenWithInfo,\n\t\t\t\t\tr.teamKey,\n\t\t\t\t\tr.entries.map(([number]) => number),\n\t\t\t\t),\n\t\t\t);\n\n\t\t\tfor (const [i, answer] of answers.entries()) {\n\t\t\t\tconst { teamKey, entries } = requests[i];\n\t\t\t\tif (answer.status === 'rejected') {\n\t\t\t\t\tfailed = true;\n\t\t\t\t\tfor (const [, indices] of entries) {\n\t\t\t\t\t\tfor (const index of indices) {\n\t\t\t\t\t\t\tslots[index] = answer;\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\t// By number, never by position: Linear returns the issues in its own order, and only the ones it found.\n\t\t\t\tconst byNumber = new Map<number, LinearIssueEtagNode>();\n\t\t\t\tfor (const issue of answer.value) {\n\t\t\t\t\tbyNumber.set(issue.number, issue);\n\t\t\t\t}\n\n\t\t\t\tfor (const [number, indices] of entries) {\n\t\t\t\t\tconst issue = byNumber.get(number);\n\t\t\t\t\tlet slot: PromiseSettledResult<IssueEtagFields | undefined>;\n\t\t\t\t\tif (issue == null) {\n\t\t\t\t\t\tslot = { status: 'rejected', reason: new Error(`Linear did not return ${teamKey}-${number}`) };\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tslot = { status: 'fulfilled', value: toLinearIssueEtagFields(issue) };\n\t\t\t\t\t\t\tanswered = true;\n\t\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\t\tslot = { status: 'rejected', reason: ex };\n\t\t\t\t\t\t\tfailed = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const index of indices) {\n\t\t\t\t\t\tslots[index] = slot;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn answered || failed ? slots : undefined;\n\t}\n\n\tprivate async getRawProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<ProviderIssue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tif (!isIssueResourceDescriptor(resource)) {\n\t\t\tLogger.error(undefined, 'getProviderIssue: resource is not an IssueResourceDescriptor');\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resource.id,\n\t\t\tnumber: id,\n\t\t});\n\t}\n\tprivate getIssueAutolinkLikeUrl(issue: ProviderIssue): string | null {\n\t\tconst url = issue.url;\n\t\tif (url == null) return null;\n\n\t\tconst lastSegment = url.split('/').pop();\n\t\tif (!lastSegment || issue.number === lastSegment) {\n\t\t\treturn url;\n\t\t}\n\t\treturn url.substring(0, url.length - lastSegment.length - 1);\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"linear.js","names":[],"sources":["../../../../../plus/integrations/src/providers/linear.ts"],"sourcesContent":["import type { CollectionMetadata, CollectionScopeFailure, GitIssueState } 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, IssueOrPullRequestType } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor, ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { isIssueResourceDescriptor } from '@gitlens/git/utils/resourceDescriptor.utils.js';\nimport { chunk } from '@gitlens/utils/array.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport { mapSettledBounded } from '@gitlens/utils/promise.js';\nimport { PromiseCache } from '@gitlens/utils/promiseCache.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { IssuesCloudHostIntegrationId, providerFanOutConcurrency } from '../constants.js';\nimport { IntegrationReadUnavailableError } from '../errors.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 {\n\tgroupProjectIssuesSearches,\n\tIssuesIntegration,\n\tsplitProjectIssuesSearch,\n} from '../models/issuesIntegration.js';\nimport type { LinearIssueEtagNode } from './linearIssuesEtag.js';\nimport { linearIssuesEtagMaxNumbers, toLinearIssueEtagFields } from './linearIssuesEtag.js';\nimport type { ProviderApiCollectionResult, ProviderIssue } from './models.js';\nimport { fromProviderIssue, providersMetadata, toIssueShape, toProviderIssueStates } from './models.js';\nimport { DiscoveryCache, discoveryCacheTtl } from './utils/discoveryCache.js';\nimport { mergeCollectionMetadata } from './utils/providerPaging.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Linear];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\nconst maxPagesPerRequest = 10;\n/**\n * The account-wide drain's own backstop, separate from {@link maxPagesPerRequest}.\n *\n * Raised to 50 (5,000 issues at the SDK's 100-per-page) because 10 no longer means what it used to. That read\n * once fanned out over four overlapping relationship queries and merged them, so most of what a page cost was\n * rows already returned — a low budget capped the waste. It is now a single server-ordered query over an `or`\n * filter, so every page is 100 issues the caller has not seen, and the same budget just truncates real results\n * at 1,000.\n *\n * Reaching it is reported rather than silent: {@link LinearIntegration.searchProviderMyIssuesWithTruncation}\n * returns `truncated`, so the facade can say the read was incomplete instead of publishing a capped list as a\n * whole account. The bound stays — a runaway cursor must not spend requests forever — but it is now set where a\n * real Linear account is unlikely to reach it rather than where the duplication used to hurt.\n */\nconst maxAccountWidePagesPerRequest = 50;\nconst linearImplicitTeamsPageSize = 50;\n/** A team key and issue number, as Linear writes an issue's identifier (`ENG-123`). */\nconst linearIdentifier = /^([A-Za-z0-9]+)-(\\d+)$/;\n\nexport interface LinearTeamDescriptor extends IssueResourceDescriptor {\n\tavatarUrl: string | undefined;\n}\n\nexport interface LinearOrganizationDescriptor extends IssueResourceDescriptor {\n\turl: string;\n}\n\nexport interface LinearProjectDescriptor extends IssueResourceDescriptor {}\n\nexport class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Linear> {\n\t/**\n\t * Built from the teams, so it follows them: rebuilt when the team list changes or the caches are dropped, and once\n\t * older than {@link discoveryCacheTtl}. An expired set is still served while it is rebuilt in the background, because\n\t * autolinks are read on every render of a commit message and must not wait on the network (#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) {\n\t\t\treturn [];\n\t\t}\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).catch(() => {});\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 the teams. */\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 generation = this._teams.generation;\n\t\tconst organization = await this.getOrganization(session);\n\t\tif (organization == null) return [];\n\n\t\tconst autolinks: (AutolinkReference | DynamicAutolinkReference)[] = [];\n\n\t\tconst teams = await this.getTeams(session);\n\t\tfor (const team of teams ?? []) {\n\t\t\tconst dashedPrefix = `${team.key}-`;\n\t\t\tconst underscoredPrefix = `${team.key}_`;\n\n\t\t\tautolinks.push({\n\t\t\t\tprefix: dashedPrefix,\n\t\t\t\turl: `${organization.url}/issue/${dashedPrefix}<num>`,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\ttype: 'issue',\n\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\tdescriptor: { ...organization },\n\t\t\t});\n\t\t\tautolinks.push({\n\t\t\t\tprefix: underscoredPrefix,\n\t\t\t\turl: `${organization.url}/issue/${dashedPrefix}<num>`,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\treferenceType: 'branch',\n\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\ttype: 'issue',\n\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\tdescriptor: { ...organization },\n\t\t\t});\n\t\t}\n\n\t\t// Not kept when built from teams read before the caches were dropped.\n\t\tif (this._teams.generation === generation) {\n\t\t\tthis._autolinks.set(session.accessToken, { autolinks: autolinks, builtAt: Date.now() });\n\t\t}\n\t\treturn autolinks;\n\t}\n\n\tprivate _organizations: Map<string, LinearOrganizationDescriptor | undefined> | undefined;\n\tprivate async getOrganization(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<LinearOrganizationDescriptor | undefined> {\n\t\tconst { accessToken } = session;\n\t\tthis._organizations ??= new Map<string, LinearOrganizationDescriptor | undefined>();\n\n\t\tconst cachedResources = this._organizations.get(accessToken);\n\n\t\tif (cachedResources == null || force) {\n\t\t\tconst api = await this.getProvidersApi();\n\t\t\tconst organization = await api.getLinearOrganization(toTokenWithInfo(this.id, session));\n\t\t\tconst descriptor: LinearOrganizationDescriptor | undefined = organization && {\n\t\t\t\tid: organization.id,\n\t\t\t\tkey: organization.key,\n\t\t\t\tname: organization.name,\n\t\t\t\turl: organization.url,\n\t\t\t};\n\t\t\tif (descriptor) {\n\t\t\t\tthis._organizations.set(accessToken, descriptor);\n\t\t\t}\n\t\t}\n\n\t\treturn this._organizations.get(accessToken);\n\t}\n\n\t/** Expires, and is dropped on a re-sync, so a team created or left mid-session is picked up (#5907). */\n\tprivate readonly _teams = new DiscoveryCache<LinearTeamDescriptor[]>();\n\tprivate async getTeams(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<LinearTeamDescriptor[] | undefined> {\n\t\treturn (await this.getTeamsWithMetadata(session, force))?.values;\n\t}\n\n\tprivate async getTeamsWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<ProviderApiCollectionResult<LinearTeamDescriptor> | undefined> {\n\t\tconst { accessToken } = session;\n\n\t\tconst cachedResources = this._teams.get(accessToken);\n\t\tif (cachedResources != null && !force) return { values: cachedResources };\n\n\t\tconst generation = this._teams.generation;\n\t\tconst api = await this.getProvidersApi();\n\t\tconst teams = await api.getLinearTeamsForCurrentUser(toTokenWithInfo(this.id, session));\n\t\tconst descriptors: LinearTeamDescriptor[] | undefined = teams?.map(t => ({\n\t\t\tid: t.id,\n\t\t\tkey: t.key,\n\t\t\tname: t.name,\n\t\t\tavatarUrl: t.iconUrl,\n\t\t}));\n\t\tif (descriptors == null) return undefined;\n\n\t\t// provider-apis currently requests Linear's teams connection without pageInfo or a cursor. Linear's\n\t\t// implicit page size is 50, so exactly a full page cannot prove there is no team 51. Preserve the useful\n\t\t// prefix, but mark it unknown and leave it uncached so the next read retries instead of treating it as\n\t\t// authoritative. Once provider-apis exposes a paged primitive, replace this fail-closed guard with a drain.\n\t\tif (descriptors.length >= linearImplicitTeamsPageSize) {\n\t\t\treturn { values: descriptors, metadata: { completeness: 'unknown' } };\n\t\t}\n\n\t\tif (this._teams.set(accessToken, descriptors, { generation: generation })) {\n\t\t\t// Built from the teams; a new set means new prefixes.\n\t\t\tthis._autolinks.delete(accessToken);\n\t\t}\n\t\treturn { values: descriptors };\n\t}\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<ResourceDescriptor[] | undefined> {\n\t\tconst organization = await this.getOrganization(session);\n\t\treturn organization != null ? [organization] : undefined;\n\t}\n\tprotected override getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources: ResourceDescriptor[],\n\t): Promise<ResourceDescriptor[] | undefined> {\n\t\treturn this.getTeams(session);\n\t}\n\tprotected override async getProviderProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources: ResourceDescriptor[],\n\t): Promise<ProviderApiCollectionResult<ResourceDescriptor>> {\n\t\treturn (await this.getTeamsWithMetadata(session)) ?? { values: [] };\n\t}\n\n\toverride invalidateDiscoveryCaches(): void {\n\t\tthis._organizations = undefined;\n\t\tthis._teams.clear();\n\t\tthis._autolinks.clear();\n\t}\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\t// Linear's viewer isn't a ProviderAccount (no username/avatar), so build the Account manually\n\t\t// (Trello-style) from the fields the viewer query returns.\n\t\tconst user = await api.getLinearCurrentUser(toTokenWithInfo(this.id, session));\n\t\tif (user == null) return undefined;\n\n\t\treturn {\n\t\t\tprovider: this,\n\t\t\tid: user.id,\n\t\t\tname: user.name ?? user.displayName ?? undefined,\n\t\t\tusername: user.displayName ?? undefined,\n\t\t\temail: user.email ?? undefined,\n\t\t\tavatarUrl: undefined,\n\t\t};\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\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: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tif (!isIssueResourceDescriptor(project)) return undefined;\n\n\t\t// Scope to \"my issues\" by the viewer's stable id, not the passed display name: Linear's `name` (full name)\n\t\t// and `displayName` (nickname) are distinct fields, and assignees are normalized with `.name` = the full\n\t\t// name while the caller's `user` is the displayName — so a name string can miss. The assignee `.id` is the\n\t\t// Linear user id, which is unambiguous.\n\t\tlet viewerId: string | undefined;\n\t\tif (options?.user != null) {\n\t\t\tviewerId = await this.getViewerId(session, options);\n\t\t\t// If the viewer can't be resolved we can't scope to \"my issues\" — returning the unfiltered team\n\t\t\t// issues would leak everyone else's, and returning [] is indistinguishable from \"no issues assigned\n\t\t\t// to me\". Throw so the facade (getIssuesForProjectResult → runCaptured) surfaces a warning +\n\t\t\t// fetchFailed the caller can act on, instead of a silent empty.\n\t\t\tif (viewerId == null) {\n\t\t\t\tthrow new IntegrationReadUnavailableError(\n\t\t\t\t\tmetadata.name,\n\t\t\t\t\t'could not resolve the current user to scope issues to',\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// `getProviderProjectsForResources` returns Linear teams, so `project.id` is a team id here.\n\t\t//\n\t\t// The state and the viewer are narrowed server-side, not after the drain: a team's done work and other\n\t\t// people's issues would otherwise count against the backstop, so a busy team runs out of pages before the\n\t\t// viewer's open issues and hands back a page that looks complete.\n\t\tconst drain = await this.drainIssues(\n\t\t\tsession,\n\t\t\t{\n\t\t\t\tteams: [project.id],\n\t\t\t\t...(viewerId != null ? { assignees: [viewerId] } : {}),\n\t\t\t\tstates: toProviderIssueStates(options?.state),\n\t\t\t},\n\t\t\toptions?.sort,\n\t\t\t{ providerId: this.id, projectId: project.id },\n\t\t);\n\n\t\treturn drain.truncated\n\t\t\t? { values: drain.issues, truncated: true, recovery: 'none', metadata: drain.metadata }\n\t\t\t: { values: drain.issues, truncated: false, metadata: drain.metadata };\n\t}\n\n\t/**\n\t * Searches every team of a user-scoped read together: one `getIssues` names all of them and the viewer as\n\t * assignee, so a team with none of the user's issues costs no request of its own. Linear's filter is a GraphQL\n\t * body, not a query string, so there is no key bound to chunk at.\n\t */\n\tprotected override getProjectIssuesSearches(\n\t\trequests: readonly ProjectIssuesRequest<ResourceDescriptor>[],\n\t): number[][] | undefined {\n\t\treturn groupProjectIssuesSearches(requests, ({ project, options }) =>\n\t\t\toptions.user != null && isIssueResourceDescriptor(project)\n\t\t\t\t? [options.user, options.userId ?? '', options.sort ?? '', options.state ?? ''].join('\\0')\n\t\t\t\t: undefined,\n\t\t);\n\t}\n\n\tprotected override async searchProviderProjectIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\trequests: readonly ProjectIssuesRequest<ResourceDescriptor>[],\n\t): Promise<IssueShape[][] | undefined> {\n\t\t// Every request of a search shares its user scope (see `getProjectIssuesSearches`).\n\t\tconst { options } = requests[0];\n\t\tif (options.user == null) return undefined;\n\n\t\t// Unresolvable here means each team's own read throws the same warning, so let them.\n\t\tconst viewerId = await this.getViewerId(session, options);\n\t\tif (viewerId == null) return undefined;\n\n\t\tconst teams = requests.map(request => request.project).filter(isIssueResourceDescriptor);\n\t\tif (teams.length !== requests.length) return undefined;\n\n\t\tconst drain = await this.drainIssues(\n\t\t\tsession,\n\t\t\t{\n\t\t\t\tteams: teams.map(team => team.id),\n\t\t\t\tassignees: [viewerId],\n\t\t\t\tstates: toProviderIssueStates(options.state),\n\t\t\t},\n\t\t\toptions.sort,\n\t\t\t// Never published: an incomplete search is read again per team, which records its own failures.\n\t\t\t{ providerId: this.id },\n\t\t);\n\t\t// Deliberately discarded rather than served: an incomplete search says nothing about which of its teams it\n\t\t// covered, so only the per-team reads can report each one's completeness. The waste is bounded by one team's\n\t\t// page budget, and paid only by a search that needed more than that or failed partway.\n\t\tif (drain.truncated || (drain.metadata != null && drain.metadata.completeness !== 'complete')) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// A normalized Linear issue carries its team only inside `project`, which is absent for an issue in no\n\t\t// Linear project, so split by the identifier instead: Linear numbers every issue `<team key>-<n>`, and\n\t\t// renumbers it under its new team's key when it moves.\n\t\treturn splitProjectIssuesSearch(\n\t\t\tteams.map(team => team.key),\n\t\t\tdrain.issues,\n\t\t\tissue => {\n\t\t\t\tconst separator = issue.id.lastIndexOf('-');\n\t\t\t\treturn separator > 0 ? issue.id.slice(0, separator) : undefined;\n\t\t\t},\n\t\t);\n\t}\n\n\tprivate readonly _viewerIds = new PromiseCache<string, string | undefined>({ capacity: 10 });\n\t/**\n\t * The viewer's Linear user id: the caller's resolved `userId` when it has one, else the viewer query, shared per\n\t * token. A tracker read hands each team the id of the team's own resource, which Linear's team descriptors don't\n\t * name, so without sharing every team read (and a search's fallback) would ask for the viewer again.\n\t */\n\tprivate getViewerId(\n\t\tsession: ProviderAuthenticationSession,\n\t\toptions: IssuesForProjectOptions,\n\t): Promise<string | undefined> {\n\t\tif (options.userId) return Promise.resolve(options.userId);\n\n\t\treturn this._viewerIds.getOrCreate(\n\t\t\tsession.accessToken,\n\t\t\tasync () => {\n\t\t\t\tconst api = await this.getProvidersApi();\n\t\t\t\treturn (await api.getLinearCurrentUser(toTokenWithInfo(this.id, session)))?.id;\n\t\t\t},\n\t\t\t// Only a resolved id is worth keeping; an unresolved viewer is asked for again next time, as is a failure.\n\t\t\t{ evictWhen: id => id == null },\n\t\t);\n\t}\n\n\t/**\n\t * Follows `getIssues`' cursor up to {@link maxPagesPerRequest}. `truncated` is set when that backstop stopped\n\t * the drain with more pages still available, when the provider stalled its cursor or flagged its paging as\n\t * truncated, or when a page after the first failed, which is recorded at `failureScope` while the prefix is\n\t * kept. A first-page failure throws, so the caller sees a hard error rather than an empty success.\n\t */\n\tprivate async drainIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tfilter: { teams: string[]; assignees?: string[]; states?: GitIssueState[] },\n\t\tsort: IssuesForProjectOptions['sort'],\n\t\tfailureScope: CollectionScopeFailure['scope'],\n\t): Promise<{ issues: IssueShape[]; truncated: boolean; metadata?: CollectionMetadata }> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst assignees = filter.assignees?.length ? filter.assignees : undefined;\n\t\tlet cursor: string | undefined;\n\t\tlet hasMore: boolean;\n\t\tlet requestCount = 0;\n\t\tlet truncated = false;\n\t\tlet collectionMetadata: CollectionMetadata | undefined;\n\t\tconst issues: IssueShape[] = [];\n\t\tdo {\n\t\t\tlet result: Awaited<ReturnType<typeof api.getLinearIssues>>;\n\t\t\ttry {\n\t\t\t\tresult = await api.getLinearIssues(toTokenWithInfo(this.id, session), filter, {\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tsort: sort,\n\t\t\t\t});\n\t\t\t} catch (ex) {\n\t\t\t\tif (issues.length === 0) throw ex;\n\n\t\t\t\ttruncated = true;\n\t\t\t\tcollectionMetadata = mergeCollectionMetadata(collectionMetadata, {\n\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\tfailures: [toCollectionScopeFailure(failureScope, ex)],\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\trequestCount += 1;\n\t\t\thasMore = result.paging?.more ?? false;\n\t\t\tconst nextCursor = result.paging?.cursor;\n\t\t\ttruncated ||= result.paging?.truncated === true;\n\t\t\tcollectionMetadata = mergeCollectionMetadata(collectionMetadata, result.metadata);\n\t\t\tfor (const issue of result.values) {\n\t\t\t\tconst shape = toIssueShape(issue, this, { projection: 'project' });\n\t\t\t\t// The query already scopes to the assignees; checking again keeps another user's issue out of \"my\n\t\t\t\t// issues\" should the provider ever return one.\n\t\t\t\tif (shape != null && (assignees == null || shape.assignees?.some(a => assignees.includes(a.id)))) {\n\t\t\t\t\tissues.push(shape);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// The provider claims more but returns no advancing cursor: we can't continue without re-reading the\n\t\t\t// same page, so the drain is incomplete — flag it rather than silently stopping.\n\t\t\tif (hasMore && (nextCursor == null || nextCursor === cursor)) {\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcursor = nextCursor;\n\t\t\tif (hasMore && requestCount >= maxPagesPerRequest) {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t} while (requestCount < maxPagesPerRequest && hasMore);\n\n\t\treturn { issues: issues, truncated: truncated, metadata: collectionMetadata };\n\t}\n\n\toverride get id(): IssuesCloudHostIntegrationId.Linear {\n\t\treturn IssuesCloudHostIntegrationId.Linear;\n\t}\n\tprotected override get key(): 'linear' {\n\t\treturn 'linear';\n\t}\n\toverride get name(): string {\n\t\treturn metadata.name;\n\t}\n\toverride get domain(): string {\n\t\treturn metadata.domain;\n\t}\n\tprotected override async searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\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 * Account-wide \"my issues\" for Linear, drained to exhaustion and reporting whether it got there.\n\t *\n\t * The SDK read is one server-ordered query over an `or` filter across the four relationships (assigned,\n\t * created, named in an issue's content, named in a comment), so each page is 100 issues the caller has not\n\t * seen and the pages are ordered as one sequence. Draining it is therefore just following the cursor.\n\t *\n\t * Overridden rather than left to the base default because that default hardcodes `truncated: false`, and a\n\t * bounded drain that stops early is exactly the case a caller must be able to see. Three things end the\n\t * drain, and only one of them is completion:\n\t *\n\t * - the provider says there is no next page — complete;\n\t * - the backstop is reached — `truncated`, because issues beyond it exist and were not read;\n\t * - the provider claims a next page but hands back a cursor that does not advance — also `truncated`, since\n\t * the read cannot continue and the remainder is unreachable rather than absent.\n\t *\n\t * Cancellation is deliberately NOT truncation: the caller asked for the read to stop, so the partial list is\n\t * what it asked for, and flagging it would surface an incompleteness warning for a user action.\n\t */\n\tprotected override async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\tif (resources != null) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tlet cursor = undefined;\n\t\t// Starts false so an immediate cancellation, which leaves the loop before the first response, reads as\n\t\t// \"no more pages known\" rather than as an unfinished drain.\n\t\tlet hasMore = false;\n\t\tlet requestCount = 0;\n\t\tlet truncated = false;\n\t\tconst issues = [];\n\t\ttry {\n\t\t\tdo {\n\t\t\t\tif (cancellation?.aborted) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tconst result = await api.getIssuesForCurrentUser(toTokenWithInfo(this.id, session), {\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\t\trequestCount += 1;\n\t\t\t\thasMore = result.paging?.more ?? false;\n\t\t\t\tconst nextCursor = result.paging?.cursor;\n\n\t\t\t\t// Keep this page before deciding whether to continue: the request is already paid for, so\n\t\t\t\t// dropping its rows would lose real results to save nothing.\n\t\t\t\tconst formattedIssues = result.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\tif (formattedIssues.length > 0) {\n\t\t\t\t\tissues.push(...formattedIssues);\n\t\t\t\t}\n\n\t\t\t\t// The provider claims more but hands back no advancing cursor: continuing would re-read the page\n\t\t\t\t// just returned. `ProvidersApi.getPagedResult` already normalizes this into `more: false`, so\n\t\t\t\t// this is a backstop for if that ever stops holding rather than a case seen today -- but the rest\n\t\t\t\t// of the account is unreachable either way, which is what `truncated` says.\n\t\t\t\tif (hasMore && (nextCursor == null || nextCursor === cursor)) {\n\t\t\t\t\ttruncated = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcursor = nextCursor;\n\t\t\t} while (requestCount < maxAccountWidePagesPerRequest && hasMore);\n\n\t\t\t// Ran out of budget with pages still to come.\n\t\t\tif (hasMore && requestCount >= maxAccountWidePagesPerRequest) {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t} catch (ex) {\n\t\t\tif (issues.length === 0) {\n\t\t\t\tthrow ex;\n\t\t\t}\n\n\t\t\t// Kept what was already fetched, so the list is real but short of the account.\n\t\t\ttruncated = true;\n\t\t\tLogger.error(ex, 'searchProviderMyIssues');\n\t\t}\n\t\treturn { values: issues, truncated: truncated };\n\t}\n\tprotected override async getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\t{ key }: { id: string; key: string },\n\t\t_type: undefined | IssueOrPullRequestType,\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst issue = await this.getRawProviderIssue(session, resource, key);\n\t\tconst autolinkableIssue: ProviderIssue | undefined = issue && {\n\t\t\t...issue,\n\t\t\turl: this.getIssueAutolinkLikeUrl(issue),\n\t\t};\n\t\treturn autolinkableIssue && toIssueShape(autolinkableIssue, this, { projection: 'point' });\n\t}\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tconst result = await this.getRawProviderIssue(session, resource, id);\n\t\treturn result && fromProviderIssue(result, this, { projection: 'point' });\n\t}\n\n\tprotected override async getProviderIssueByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresourceId: string,\n\t\tid: string,\n\t\t_resourceUrl: string | undefined,\n\t): Promise<Issue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst result = await api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resourceId,\n\t\t\tnumber: id,\n\t\t});\n\t\treturn result && fromProviderIssue(result, this, { projection: 'batch' });\n\t}\n\n\t/**\n\t * The cheap check behind the batch issue read's etags: one `issues` query per team and\n\t * {@link linearIssuesEtagMaxNumbers} distinct numbers, where {@link getProviderIssueByResourceId} sends one request\n\t * per issue. A request that throws rejects only its own targets' slots, with its error classified as the full\n\t * read's would be.\n\t *\n\t * Never proves an absence. The query filters by the team's CURRENT key and asks for archived issues too, but the\n\t * full read's `issue(id:)` may also resolve an identifier the issue no longer carries (its team was renamed, or it\n\t * moved to another team), so an issue the query leaves out may still exist. Its target's slot is rejected, as is a\n\t * target whose identifier isn't a team key and a number, so the full read decides. When no target was answered and\n\t * nothing failed, the check declines instead, so a batch of only such targets 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 workspace and team's distinct numbers, with the targets asking for each.\n\t\tconst byTeam = new Map<string, { teamKey: string; numbers: Map<number, number[]> }>();\n\t\tfor (const [index, target] of targets.entries()) {\n\t\t\tconst match = linearIdentifier.exec(target.identifier);\n\t\t\tconst number = match != null ? Number(match[2]) : Number.NaN;\n\t\t\t// Only a number written as Linear writes it, so `ENG-012` never answers for `ENG-12`.\n\t\t\tif (match == null || !Number.isSafeInteger(number) || String(number) !== match[2]) {\n\t\t\t\tslots[index] = {\n\t\t\t\t\tstatus: 'rejected',\n\t\t\t\t\treason: new Error(`Not a Linear issue identifier: ${target.identifier}`),\n\t\t\t\t};\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst teamKey = match[1].toUpperCase();\n\t\t\tconst groupKey = `${target.resourceId}\\n${teamKey}`;\n\t\t\tlet group = byTeam.get(groupKey);\n\t\t\tif (group == null) {\n\t\t\t\tgroup = { teamKey: teamKey, numbers: new Map() };\n\t\t\t\tbyTeam.set(groupKey, group);\n\t\t\t}\n\n\t\t\tlet indices = group.numbers.get(number);\n\t\t\tif (indices == null) {\n\t\t\t\tindices = [];\n\t\t\t\tgroup.numbers.set(number, indices);\n\t\t\t}\n\t\t\tindices.push(index);\n\t\t}\n\n\t\tconst requests = [...byTeam.values()].flatMap(({ teamKey, numbers }) =>\n\t\t\tchunk([...numbers], linearIssuesEtagMaxNumbers).map(entries => ({ teamKey: teamKey, entries: entries })),\n\t\t);\n\t\tif (requests.length) {\n\t\t\tconst api = await this.getProvidersApi();\n\t\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\t\tconst answers = await mapSettledBounded(requests, providerFanOutConcurrency, r =>\n\t\t\t\tapi.getLinearIssuesEtagFields(\n\t\t\t\t\ttokenWithInfo,\n\t\t\t\t\tr.teamKey,\n\t\t\t\t\tr.entries.map(([number]) => number),\n\t\t\t\t),\n\t\t\t);\n\n\t\t\tfor (const [i, answer] of answers.entries()) {\n\t\t\t\tconst { teamKey, entries } = requests[i];\n\t\t\t\tif (answer.status === 'rejected') {\n\t\t\t\t\tfailed = true;\n\t\t\t\t\tfor (const [, indices] of entries) {\n\t\t\t\t\t\tfor (const index of indices) {\n\t\t\t\t\t\t\tslots[index] = answer;\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\t// By number, never by position: Linear returns the issues in its own order, and only the ones it found.\n\t\t\t\tconst byNumber = new Map<number, LinearIssueEtagNode>();\n\t\t\t\tfor (const issue of answer.value) {\n\t\t\t\t\tbyNumber.set(issue.number, issue);\n\t\t\t\t}\n\n\t\t\t\tfor (const [number, indices] of entries) {\n\t\t\t\t\tconst issue = byNumber.get(number);\n\t\t\t\t\tlet slot: PromiseSettledResult<IssueEtagFields | undefined>;\n\t\t\t\t\tif (issue == null) {\n\t\t\t\t\t\tslot = { status: 'rejected', reason: new Error(`Linear did not return ${teamKey}-${number}`) };\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tslot = { status: 'fulfilled', value: toLinearIssueEtagFields(issue) };\n\t\t\t\t\t\t\tanswered = true;\n\t\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\t\tslot = { status: 'rejected', reason: ex };\n\t\t\t\t\t\t\tfailed = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const index of indices) {\n\t\t\t\t\t\tslots[index] = slot;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn answered || failed ? slots : undefined;\n\t}\n\n\tprivate async getRawProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<ProviderIssue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tif (!isIssueResourceDescriptor(resource)) {\n\t\t\tLogger.error(undefined, 'getProviderIssue: resource is not an IssueResourceDescriptor');\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resource.id,\n\t\t\tnumber: id,\n\t\t});\n\t}\n\tprivate getIssueAutolinkLikeUrl(issue: ProviderIssue): string | null {\n\t\tconst url = issue.url;\n\t\tif (url == null) return null;\n\n\t\tconst lastSegment = url.split('/').pop();\n\t\tif (!lastSegment || issue.number === lastSegment) {\n\t\t\treturn url;\n\t\t}\n\t\treturn url.substring(0, url.length - lastSegment.length - 1);\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { PullRequest, PullRequestFilter, PullRequestStateFilter } from "../../../git/models/pullRequest.js";
|
|
2
2
|
import { IntegrationIds } from "../constants.js";
|
|
3
3
|
import { ProviderWarning } from "../results.js";
|
|
4
|
-
import "../providerFilters.js";
|
|
5
4
|
import { ProviderPullRequest, ProviderReposInput, ProviderRepository } from "../providers/models.js";
|
|
6
5
|
import { GitHostIntegration } from "../models/gitHostIntegration.js";
|
|
7
6
|
import { IntegrationResult } from "../models/integration.js";
|
|
@@ -60,7 +60,9 @@ export interface ProviderWarningScope {
|
|
|
60
60
|
repositoryId?: string;
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
|
-
* Why a provider
|
|
63
|
+
* Why a provider request failed, when the failure itself says so:
|
|
64
|
+
* - `unreachable`: a transport failure or server-side error prevented the provider from answering. The connection
|
|
65
|
+
* still exists; this accompanies `kind: 'other'`, never an authentication warning.
|
|
64
66
|
* - `oauth-app-not-allowed`: the organization does not let third-party OAuth apps in. On Azure DevOps this is the
|
|
65
67
|
* organization policy "Third-party application access via OAuth", off by default for new organizations. An
|
|
66
68
|
* organization admin enables it, or the user connects with a personal access token instead, which the policy
|
|
@@ -72,7 +74,7 @@ export interface ProviderWarningScope {
|
|
|
72
74
|
*
|
|
73
75
|
* None of these is healed by reconnecting, which is the point of naming them.
|
|
74
76
|
*/
|
|
75
|
-
export type ProviderWarningCauseReason = 'oauth-app-not-allowed' | 'access-denied' | 'conditional-access';
|
|
77
|
+
export type ProviderWarningCauseReason = 'oauth-app-not-allowed' | 'access-denied' | 'conditional-access' | 'unreachable';
|
|
76
78
|
/** See {@link ProviderWarning.cause}. */
|
|
77
79
|
export interface ProviderWarningCause {
|
|
78
80
|
reason: ProviderWarningCauseReason;
|
|
@@ -161,11 +163,12 @@ export interface ProviderWarning {
|
|
|
161
163
|
*/
|
|
162
164
|
scope?: ProviderWarningScope;
|
|
163
165
|
/**
|
|
164
|
-
* Why the provider
|
|
166
|
+
* Why the provider request failed, when the failure itself says so: see {@link ProviderWarningCauseReason}. Switch on
|
|
165
167
|
* `reason` to recommend a fix instead of a reconnect; `message` then carries the same explanation as prose.
|
|
166
168
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
+
* `unreachable` accompanies a transport or server failure with `kind: 'other'`. Credential refusal causes are
|
|
170
|
+
* set only on a scoped `auth` warning whose credential was confirmed first (see {@link scope}), because the
|
|
171
|
+
* refusals they name are indistinguishable from a dead credential until then: Azure DevOps answers a
|
|
169
172
|
* third-party OAuth app its organization disallows exactly as it answers an expired token. Only Azure DevOps
|
|
170
173
|
* reports one today. Its absence proves nothing about the cause.
|
|
171
174
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AuthenticationError, RequestNotFoundError, RequestRateLimitError } from "../../git/errors.js";
|
|
2
|
+
import { isProviderUnreachableError } from "./errors.js";
|
|
2
3
|
import { isUnsupportedSortError } from "@gitkraken/provider-apis";
|
|
3
4
|
//#region ../plus/integrations/src/results.ts
|
|
4
5
|
const maxProviderWarningMessageLength = 500;
|
|
@@ -29,7 +30,8 @@ function toProviderWarning(providerId, domain, connectionId, ex) {
|
|
|
29
30
|
connectionId,
|
|
30
31
|
message: providerWarningMessage(ex),
|
|
31
32
|
kind,
|
|
32
|
-
isAuth: kind === "auth"
|
|
33
|
+
isAuth: kind === "auth",
|
|
34
|
+
...kind === "other" && isProviderUnreachableError(ex) ? { cause: { reason: "unreachable" } } : {}
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
37
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"results.js","names":[],"sources":["../../../../plus/integrations/src/results.ts"],"sourcesContent":["import { isUnsupportedSortError } from '@gitkraken/provider-apis';\nimport { AuthenticationError, RequestNotFoundError, RequestRateLimitError } from '@gitlens/git/errors.js';\nimport type { IssueSorting } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSorting } from '@gitlens/git/models/pullRequest.js';\nimport type { IntegrationIds } from './constants.js';\n\nexport interface ConnectionStateChangeEvent {\n\tkey: string;\n\treason: 'connected' | 'disconnected';\n}\n\n/**\n * A per-provider, per-connection warning surfaced alongside (partial) read results. Consumers use\n * these to drive auth recovery, retry, or truncation messaging without the read itself throwing.\n *\n * `kind` is the programmatic discriminant (a rate-limit is retryable, a 404 is not, an unsupported capability\n * never succeeds on retry, an auth failure needs re-connection unless {@link ProviderWarning.scope} confines\n * it); `isAuth` is retained as a convenience mirror of `kind === 'auth'`.\n *\n * `'unsupported'`: the provider — or this integration of it — lacks the capability the request asked for: a\n * surface (issues on Bitbucket, pull requests on an issue tracker, a batch read, org/repo discovery,\n * current-account lookup, project-scoped or account-wide issue reads), or a filter, sort, state, search\n * criterion or option combination it cannot express. Retrying the same request never succeeds; a different\n * provider, scope or request may. NOT unsupported (these stay `'other'`): malformed or contradictory caller\n * input the caller fixes in the request itself (a duplicate batch key, invalid target numbers, an empty owner,\n * an unusable scope), incompleteness or omission warnings, and unclassified errors.\n */\nexport type ProviderWarningKind = 'auth' | 'rate-limit' | 'not-found' | 'no-connection' | 'unsupported' | 'other';\n\n/**\n * Why a read that SUCCEEDED still withheld results:\n * - `provider-limit`: the provider refuses to serve past a cap it enforces (GitHub search's 1,000-result\n * ceiling, Trello's `cards_limit`), so the excess is unreachable through that query.\n * - `recovery-budget`: the internal partitioned recovery stopped spending upstream requests before it ran\n * out of partitions to visit.\n * - `pagination-incomplete`: pages were left unread — a sub-scope of a multi-scope read that was not drained,\n * a drain that stopped at its own page backstop, or a provider that advertised another page and gave no way\n * to reach it. These have DIFFERENT remedies, so the kind alone does not say whether more can be fetched;\n * read {@link ProviderWarningOmission.recovery} for that.\n *\n * Mirrors the vocabulary the SDK reports. Declared here rather than imported so this module stays free of\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`); `collectionMetadata.ts` holds the\n * compile-time link, so an SDK bump that adds a kind fails the build at that boundary.\n */\nexport type ProviderWarningOmissionKind = 'provider-limit' | 'recovery-budget' | 'pagination-incomplete';\n\n/**\n * How a consumer can recover omitted results. `page-budget` reruns with a larger budget; `narrow-scope`\n * requests a smaller server-side scope; `none` promises neither. Switch on the exact value because only\n * `page-budget` is a fetch-more action.\n */\nexport type ProviderWarningOmissionRecovery = 'none' | 'page-budget' | 'narrow-scope';\n\n/**\n * The part of a provider a warning is confined to: one resource (organization, workspace, site), project or\n * repository. All fields optional; see {@link ProviderWarning.scope} for when one is forwarded.\n */\nexport interface ProviderWarningScope {\n\t/**\n\t * The resource as the read addressed it: its id on most reads, its name on the few that address it by name\n\t * (Azure DevOps' repo-scoped reads, Bitbucket's workspace reads). Match it against both\n\t * `ProviderOrganization.id` and `name`.\n\t */\n\tresourceId?: string;\n\tprojectId?: string;\n\trepositoryId?: string;\n}\n\n/**\n * Why a provider refused a credential it otherwise accepts, when the refusal itself says so:\n * - `oauth-app-not-allowed`: the organization does not let third-party OAuth apps in. On Azure DevOps this is the\n * organization policy \"Third-party application access via OAuth\", off by default for new organizations. An\n * organization admin enables it, or the user connects with a personal access token instead, which the policy\n * does not govern.\n * - `access-denied`: the account has no access to that organization or project (not a member, or lacking the\n * permission). Someone who administers it has to grant access.\n * - `conditional-access`: a Microsoft Entra Conditional Access policy blocked the request (Azure DevOps\n * `VS403463`). Its admin has to exempt the request, e.g. by location or device.\n *\n * None of these is healed by reconnecting, which is the point of naming them.\n */\nexport type ProviderWarningCauseReason = 'oauth-app-not-allowed' | 'access-denied' | 'conditional-access';\n\n/** See {@link ProviderWarning.cause}. */\nexport interface ProviderWarningCause {\n\treason: ProviderWarningCauseReason;\n\t/** The provider's own error code, when it reports one (Azure DevOps `TF400813`, `VS403463`). */\n\tcode?: string;\n\t/** Where the setting behind the refusal is changed, when this layer can address it. */\n\tremedyUrl?: string;\n}\n\n/** Which repository / project / resource an omission is attributed to. All fields optional; a scope may name none. */\nexport interface ProviderWarningOmissionScope extends ProviderWarningScope {\n\tproviderId?: string;\n}\n\nexport interface ProviderWarningOmission {\n\tkind: ProviderWarningOmissionKind;\n\t/**\n\t * Whether anything would fetch the missing items, and what. Gate a \"load more\" affordance on this rather\n\t * than on `kind` — see {@link ProviderWarningOmissionRecovery}.\n\t */\n\trecovery: ProviderWarningOmissionRecovery;\n\t/**\n\t * The cap the provider enforces, when it reports one. NOT a result count for every kind: on\n\t * `recovery-budget` this is a REQUEST budget and must not be shown to a user as a number of results.\n\t */\n\tlimit?: number;\n\t/** Total matches the provider reported, when it reports one. Only GitHub's search cap does today. */\n\ttotalCount?: number;\n\t/**\n\t * The order the reachable window was selected under, as `field:direction`, when the omission is a result\n\t * ceiling.\n\t *\n\t * Present so a consumer can word its own \"N matched, showing the first M\" without assuming WHICH M: the\n\t * provider serves the top M under this key, so the sentence is only true if it names it. Absent for every\n\t * omission that isn't a ceiling, where no window was selected by an order.\n\t *\n\t * A union of both vocabularies rather than the issue one alone: `PullRequestSorting` happens to be a subset\n\t * today, so narrowing this to {@link IssueSorting} would still compile while describing only half the reads\n\t * that set it — and would break the day the PR search gains a key the issue search has no name for.\n\t */\n\tsort?: IssueSorting | PullRequestSorting;\n\t/** Which repository / project / resource was affected, when one is attributed. */\n\tscope?: ProviderWarningOmissionScope;\n}\n\nexport interface ProviderWarning {\n\tproviderId: IntegrationIds;\n\t/** Disambiguates connections on self-managed hosts (where one provider id spans multiple domains). */\n\tdomain?: string;\n\t/** The specific token/connection the read was pinned to, when a `connectionId` was supplied. */\n\tconnectionId?: string;\n\tmessage: string;\n\tkind: ProviderWarningKind;\n\t/** Convenience mirror of `kind === 'auth'`, scoped or not; read {@link scope} before prompting to reconnect. */\n\tisAuth: boolean;\n\t/**\n\t * Present when the failure was recorded against one sub-scope of the read (an organization, project or\n\t * repository) rather than against the connection. A scoped `auth` failure is that one scope refusing this\n\t * credential, e.g. an Azure DevOps organization that disallows third-party OAuth apps, one in another Entra\n\t * tenant, or a Conditional Access policy. Reconnecting cannot fix those, so this warning alone is no reason\n\t * to prompt for it.\n\t *\n\t * `kind` and `isAuth` are unaffected: a scoped 401 is still an authentication failure, and this field says\n\t * how far it reaches. It names at least one of its IDs, and is never set on an omission (which carries its\n\t * own {@link ProviderWarningOmission.scope}) or on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}), even when that call targeted a single organization. The exception is a batch\n\t * read's refused target, which names its scope: the Azure DevOps organization and project, the repository on\n\t * other git hosts, or the tracker's resource.\n\t *\n\t * Its ABSENCE means account-wide or unattributed, so existing handling stays correct without it.\n\t *\n\t * A scoped `auth` failure also means the credential itself was accepted. A dead token can come back as nothing\n\t * but scoped refusals wherever a read reaches its scopes without an uncached request to the connection first:\n\t * discovery served from a per-token cache (Azure DevOps, Bitbucket, Jira Cloud) or an SDK fan-out across the\n\t * requested repositories (Bitbucket Data Center). So when a read's only auth failures are scoped, those providers\n\t * confirm the credential with one uncached check first, and a refused credential fails the whole read with an\n\t * unscoped `auth` warning. A batch read checks it only when every target was refused, since a target that\n\t * answered already proved it. A refusal the provider attributes to the credential itself, like Bitbucket's or Jira\n\t * Cloud's for a token missing the OAuth scopes a read needs, which a reconnect fixes, is published unscoped too,\n\t * once for the connection, while the scopes that answered keep their results.\n\t *\n\t * So the promise holds for a read that carries no unscoped `auth` warning of its own; one that does already asks\n\t * for a reconnect, and its other scoped refusals are not checked. Two exceptions stay scoped even then. A check\n\t * that could not complete or was denied (a network error, a throttle, a `403`, Bitbucket Data Center refusing a\n\t * credential it authenticated, or a Bitbucket Data Center project access token, whose user the check cannot\n\t * find) proves nothing, so the warnings are published unconfirmed, with no {@link cause}, and the next read\n\t * checks again. And a credential confirmed within the last minute is not probed again, so a revocation can take\n\t * up to a minute to surface.\n\t */\n\tscope?: ProviderWarningScope;\n\t/**\n\t * Why the provider refused, when the refusal itself says so: see {@link ProviderWarningCauseReason}. Switch on\n\t * `reason` to recommend a fix instead of a reconnect; `message` then carries the same explanation as prose.\n\t *\n\t * Set only on a scoped `auth` warning whose credential was confirmed first (see {@link scope}), because the\n\t * refusals it names are indistinguishable from a dead credential until then: Azure DevOps answers a\n\t * third-party OAuth app its organization disallows exactly as it answers an expired token. Only Azure DevOps\n\t * reports one today. Its absence proves nothing about the cause.\n\t */\n\tcause?: ProviderWarningCause;\n\t/**\n\t * Present when this warning describes results the read could not return even though the request itself\n\t * SUCCEEDED — a provider-enforced cap, an exhausted recovery budget, a page budget, or a sub-scope the read\n\t * did not drain.\n\t *\n\t * Its presence is the signal: an omission is not a failure. Message it as incompleteness rather than as a\n\t * failed read, and do NOT derive that from `message`, which is English prose and subject to rewording.\n\t * `kind` stays `'other'` for these, so the failure discriminant keeps meaning exactly what it meant before\n\t * this field existed.\n\t *\n\t * Whether anything would fetch the rest is a SEPARATE question — read {@link ProviderWarningOmission.recovery},\n\t * not `kind`, and never assume a retry of the same request returns more.\n\t *\n\t * `limit` / `totalCount` / `scope` are forwarded only when they are reported; most omissions carry none of\n\t * the three, so render correctly without them — and see {@link ProviderWarningOmission.limit} before\n\t * showing that figure as a result count.\n\t *\n\t * Its ABSENCE proves nothing. It is never set on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}) or from a structured scope failure — those are failures, and the field would\n\t * be a lie there. But it is also absent whenever incompleteness was reported without naming what was left\n\t * out, so treat a bare `kind: 'other'` warning as unclassified rather than as a proven failure.\n\t */\n\tomission?: ProviderWarningOmission;\n}\n\nexport interface ProviderPageInfo {\n\t/**\n\t * The 1-based POSITION of the returned page. One convention across every paged read, page-number and\n\t * cursor paging alike, so a `getNextPageParam`-style consumer can key off it uniformly:\n\t * - the page number the provider itself reported, when it reports one (authoritative);\n\t * - else the position the caller addressed: the page encoded in a threaded page cursor, or the `page`\n\t * supplied alongside an opaque cursor (1 when the caller supplied no `page`);\n\t * - else, for a read that can be advanced by page number, the requested `page` — the provider either\n\t * honored it or the internal drain reached it (see the paging contract on `IntegrationManager`);\n\t * - else 1: a cursor-only read with no cursor threaded back ignores a requested page number and answers\n\t * with its first page, so echoing the request would mislabel page 1 as page N.\n\t *\n\t * A page past the provider's terminal cursor still reports the requested `page` with empty `items` — an\n\t * empty page N, never page N-1 relabeled.\n\t *\n\t * Continue a cursor-paged read from `cursor`, NOT from `currentPage + 1`: a cursor-only host can't be\n\t * addressed by number, so this is a position label and not always a resumable request parameter.\n\t */\n\tcurrentPage: number;\n\t/**\n\t * The page size IN EFFECT, which is not always the one requested — it is whichever of these the read can\n\t * actually vouch for:\n\t * - the size the provider reported for the page it served (numbered-page hosts), else\n\t * - the `itemsPerPage` that was applied to the request (repo-scoped reads, which forward it), else\n\t * - `items.length`, for a read that takes no page size at all: the account-wide PR/issue reads, `listRepos`\n\t * (cursor-only), and `listIssueTrackerIssuesPage`, whose `itemsPerPage` counts PROJECTS rather than issues.\n\t *\n\t * So it is a description of the page, not an echo of the request: don't derive a total from\n\t * `currentPage * itemsPerPage`, and don't read `items.length < itemsPerPage` as \"last page\" — `hasMore` /\n\t * `cursor` are the only continuation signals.\n\t */\n\titemsPerPage: number;\n\t/**\n\t * A COMPLETENESS assertion, not a mode flag: `true` only when a sweep drained every page of every target\n\t * without truncating or failing (`!truncated && !fetchFailed`). Set exclusively by `sweepPullRequests` /\n\t * `sweepClosedPullRequests` and absent on every paged read, so `allPages !== true` does NOT by itself mean\n\t * \"this was a single page\".\n\t *\n\t * This is the signal to gate on before caching a sweep as authoritative: unlike `truncated` it is `false`\n\t * for BOTH kinds of incompleteness, so it can't be misread as a benign cap.\n\t */\n\tallPages?: boolean;\n\t/**\n\t * True when the read stopped before it could confirm it had everything. Deliberately does NOT distinguish\n\t * WHY, and a consumer must not assume it means a provider ceiling — a sweep sets it from three different\n\t * origins:\n\t * - a provider-side ceiling or per-scope backstop propagated from below (e.g. GitHub Search's 1000-result\n\t * cap): the missing items are NOT reachable, so this really is \"complete to the provider's limit\";\n\t * - the sweep's OWN `maxPages` cap (default 100) with a usable cursor still in hand: the missing items ARE\n\t * reachable and were simply not fetched — raise `maxPages` to get them;\n\t * - the provider reporting another page but handing back no usable cursor: not reachable.\n\t *\n\t * Only the second is recoverable, and a sweep exposes no continuation cursor, so treating a truncated sweep\n\t * as a complete set silently drops reachable data in that case. Prefer `allPages` for the cache decision.\n\t */\n\ttruncated?: boolean;\n}\n\nexport interface ProviderResult<T> {\n\titems: T[];\n\twarnings: ProviderWarning[];\n\t/**\n\t * True when `items` is incomplete — distinguishes a failed or truncated flat read from a genuinely empty\n\t * result. Paged results additionally use `page.truncated` when a completed page could not confirm it\n\t * drained everything.\n\t */\n\tfetchFailed?: boolean;\n}\n\nexport interface ProviderPagedResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Opaque cursor for cursor-based paging (GraphQL, per-repo/per-project cursors, etc.). For hosts that\n\t * require the previous page's cursor, consumers should pass this value back to the next `cursor` option.\n\t * A retry-only cursor may remain when `hasMore` is false: `hasMore` means automatic forward progress,\n\t * while explicitly reusing such a cursor retries failed work without creating an infinite paging loop.\n\t */\n\tcursor?: string;\n}\n\nexport interface ProviderSweepResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Providers whose top-level read failed before returning any usable page. Partial per-scope failures stay\n\t * represented by `warnings` + `fetchFailed` and are intentionally excluded.\n\t */\n\tfailedProviderIds: IntegrationIds[];\n\t/**\n\t * Providers whose read started successfully but could not produce an authoritative complete slice (for\n\t * example, a later page failed, collection metadata reported a partial scope, or a paging backstop was hit).\n\t * Kept separate from\n\t * `failedProviderIds` so consumers can accept healthy sibling providers while preserving an older snapshot\n\t * for a provider whose returned slice has a gap.\n\t */\n\tincompleteProviderIds: IntegrationIds[];\n}\n\nexport interface ProviderBroadenResult<T> extends ProviderPagedResult<T> {\n\tbroadenedProviderIds: IntegrationIds[];\n\t/** Providers for which no requested org produced a usable issue result. */\n\tfailedProviderIds: IntegrationIds[];\n\t/** Providers with both a usable org result and at least one failed or truncated org slice. */\n\tincompleteProviderIds: IntegrationIds[];\n\tfanOutCount: number;\n}\n\nexport type RepositoryResolutionStatus =\n\t| 'resolved'\n\t| 'not-found'\n\t| 'unauthorized'\n\t| 'unsupported-provider'\n\t| 'invalid-remote-url'\n\t| 'host-mismatch'\n\t| 'undetermined';\n\nexport interface RepositoryIdentity {\n\tproviderId: IntegrationIds;\n\tdomain: string;\n\t/** The provider's canonical owner/namespace (follows renames), falling back to the parsed remote when omitted. */\n\towner: string;\n\t/** The provider's canonical repo name (follows renames), falling back to the parsed remote when omitted. */\n\tname: string;\n\tproject?: string;\n\t/** The original input remote URL, so the caller can key the resolution back to the remote it asked about. */\n\tremoteUrl: string;\n\t/**\n\t * True when the provider's canonical owner/name differ from what the input remote URL carried — i.e. the\n\t * repo was renamed/moved host-side and the local remote is stale. Case-insensitive, mirroring gkcli's\n\t * `EqualFold` compare so hosts that echo the input casing (e.g. Bitbucket Server/Azure) aren't flagged.\n\t */\n\trenamed: boolean;\n}\n\nexport interface RepositoryResolution {\n\tstatus: RepositoryResolutionStatus;\n\tidentity?: RepositoryIdentity;\n\twarning?: ProviderWarning;\n}\n\nexport interface ResolveRepositoryResult {\n\tresolution: RepositoryResolution;\n}\n\nconst maxProviderWarningMessageLength = 500;\n\n/** A caught error's message as a warning can carry it: an HTML page becomes its status, and prose is capped. */\nexport function providerWarningMessage(ex: unknown): string {\n\tconst raw = (ex instanceof Error ? ex.message : String(ex)).trim();\n\tconst carrier = ex as { status?: unknown; response?: { status?: unknown } } | null | undefined;\n\tconst status =\n\t\ttypeof carrier?.status === 'number'\n\t\t\t? carrier.status\n\t\t\t: typeof carrier?.response?.status === 'number'\n\t\t\t\t? carrier.response.status\n\t\t\t\t: undefined;\n\n\t// Gateways commonly answer API requests with a complete HTML error page. That payload is neither actionable\n\t// nor suitable for a warning DTO consumed by UIs and logs; preserve the status without leaking the document.\n\tif (/<(?:!doctype\\s+html|html|head|body)\\b/i.test(raw)) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\n\tif (!raw) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\treturn raw.length > maxProviderWarningMessageLength\n\t\t? `${raw.slice(0, maxProviderWarningMessageLength - 3)}...`\n\t\t: raw;\n}\n\n/**\n * Normalized org/workspace/group shape returned by the provider facade.\n * `name` is the provider identifier to pass to follow-up reads, while `org` is an optional display label.\n */\nexport interface ProviderOrganization {\n\tid: string;\n\tproviderId: IntegrationIds;\n\tname: string;\n\torg?: string;\n\turl: string;\n}\n\n/** Normalized repository shape returned by the provider facade. */\nexport interface ProviderRepositoryShape {\n\tid: string;\n\tnamespace: string;\n\tname: string;\n\t/** Azure DevOps project; `undefined` for hosts without a project layer. */\n\tproject?: string;\n\t/** Web (browser) URL, when the provider exposes it. */\n\turl?: string;\n\t/** HTTPS clone URL, when available. */\n\tcloneUrlHttps?: string;\n\t/** SSH clone URL, when available. */\n\tcloneUrlSsh?: string;\n\t/** Default branch name, when the provider reports it. */\n\tdefaultBranch?: string;\n}\n\n/**\n * Classifies a caught provider exception into a neutral {@link ProviderWarning}. Ordering matters:\n * `instanceof` is checked most-specific-first so a rate-limit isn't mislabeled as a generic error and\n * a 404 isn't mislabeled as auth.\n */\nexport function toProviderWarning(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tex: unknown,\n): ProviderWarning {\n\tlet kind: ProviderWarning['kind'];\n\tif (ex instanceof AuthenticationError) {\n\t\tkind = 'auth';\n\t} else if (ex instanceof RequestRateLimitError) {\n\t\tkind = 'rate-limit';\n\t} else if (ex instanceof RequestNotFoundError) {\n\t\tkind = 'not-found';\n\t} else if (isUnsupportedSortError(ex)) {\n\t\tkind = 'unsupported';\n\t} else {\n\t\tkind = 'other';\n\t}\n\n\treturn {\n\t\tproviderId: providerId,\n\t\tdomain: domain,\n\t\tconnectionId: connectionId,\n\t\tmessage: providerWarningMessage(ex),\n\t\tkind: kind,\n\t\tisAuth: kind === 'auth',\n\t};\n}\n\n/**\n * A scope's identity, as the stable string that keys it.\n *\n * The single definition of what \"the same scope\" means, shared by the failure and omission dedup keys in\n * `providerPaging.ts` and by {@link providerWarningKey} below, so a scope gaining a field is one edit rather\n * than three. `providerId` is included: the same repository ID under two providers is two scopes.\n *\n * The parameter is structural rather than the SDK's `CollectionScope` so this module keeps naming no\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`) while still serving its SDK-facing\n * callers, which pass that type in unchanged.\n */\nexport function collectionScopeKey(scope: ProviderWarningOmissionScope | undefined): string {\n\treturn [scope?.providerId ?? '', scope?.resourceId ?? '', scope?.projectId ?? '', scope?.repositoryId ?? ''].join(\n\t\t' ',\n\t);\n}\n\n/**\n * The omission's contribution to a warning's identity — empty when there is none, so a warning without an\n * omission keeps deduping exactly as it did before the field existed.\n *\n * Two omissions of different kinds do produce different `message` values today, so message alone would still\n * separate them. That is incidental: the premise of `omission` is that consumers must not depend on prose\n * carrying the distinguishing fact, and this key must not either.\n */\nfunction providerWarningOmissionKey(omission: ProviderWarningOmission | undefined): string {\n\tif (omission == null) return '';\n\n\treturn [\n\t\tomission.kind,\n\t\tomission.recovery,\n\t\tomission.limit ?? '',\n\t\tomission.totalCount ?? '',\n\t\tcollectionScopeKey(omission.scope),\n\t].join(' ');\n}\n\n/**\n * Strips the omission from every warning in `warnings` when the read as a whole failed.\n *\n * An omission asserts the request SUCCEEDED, and a drain only learns it failed AFTER it may have emitted one:\n * an early page can report its own truncation and then a later page can die. Deciding per warning, at the\n * moment each is built, cannot see that future — so the aggregate is reconciled once, here, where\n * `fetchFailed` is final. Call it at the point a read returns its `fetchFailed`.\n *\n * Re-dedupes as it goes: the omission is part of a warning's identity, so two warnings that differed only\n * there become identical once it is gone, and the array's contract is that no two entries are equal.\n *\n * Mutates in place: the warning array is the one being returned, and callers accumulate into it across pages.\n */\nexport function reconcileOmissionsWithFailure(warnings: ProviderWarning[], fetchFailed: boolean): void {\n\tif (!fetchFailed || !warnings.some(w => w.omission != null)) return;\n\n\tconst reconciled: ProviderWarning[] = [];\n\tfor (const warning of warnings) {\n\t\tif (warning.omission == null) {\n\t\t\tappendDedupedWarning(reconciled, warning);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { omission: _omission, ...rest } = warning;\n\t\tappendDedupedWarning(reconciled, rest);\n\t}\n\n\twarnings.splice(0, warnings.length, ...reconciled);\n}\n\n/**\n * A stable key for deduplicating warnings accumulated across drained pages / fan-out scopes.\n *\n * The scope and the cause are keyed for the reason the omission is (see {@link providerWarningOmissionKey}): today's\n * failure messages happen to spell them out, but two failures that differ in either must stay two warnings even if\n * they don't.\n *\n * `message` stays LAST. It is the only free-form segment — provider prose, spaces and all — so anything\n * appended after it could be impersonated by a message that happens to end in the same text.\n */\nfunction providerWarningKey(warning: ProviderWarning): string {\n\treturn [\n\t\twarning.providerId,\n\t\twarning.connectionId ?? '',\n\t\twarning.domain ?? '',\n\t\twarning.kind,\n\t\tcollectionScopeKey(warning.scope),\n\t\twarning.cause?.reason ?? '',\n\t\tproviderWarningOmissionKey(warning.omission),\n\t\twarning.message,\n\t].join(' ');\n}\n\n/**\n * Appends `warning` to `into` only when an equal warning (by provider/connection/domain/kind/message, plus the\n * structured scope, cause and omission when present) is absent.\n */\nexport function appendDedupedWarning(into: ProviderWarning[], warning: ProviderWarning): void {\n\tconst key = providerWarningKey(warning);\n\tif (into.some(existing => providerWarningKey(existing) === key)) return;\n\n\tinto.push(warning);\n}\n"],"mappings":";;;AAiWA,MAAM,kCAAkC;;AAGxC,SAAgB,uBAAuB,IAAqB;CAC3D,MAAM,OAAO,cAAc,QAAQ,GAAG,UAAU,OAAO,EAAE,EAAA,CAAG,KAAK;CACjE,MAAM,UAAU;CAChB,MAAM,SACL,OAAO,SAAS,WAAW,WACxB,QAAQ,SACR,OAAO,SAAS,UAAU,WAAW,WACpC,QAAQ,SAAS,SACjB,KAAA;CAIL,IAAI,yCAAyC,KAAK,GAAG,GACpD,OAAO,UAAU,OAAO,uCAAuC,OAAO,KAAK;CAG5E,IAAI,CAAC,KACJ,OAAO,UAAU,OAAO,uCAAuC,OAAO,KAAK;CAE5E,OAAO,IAAI,SAAS,kCACjB,GAAG,IAAI,MAAM,GAAG,GAAmC,EAAE,OACrD;AACJ;;;;;;AAoCA,SAAgB,kBACf,YACA,QACA,cACA,IACkB;CAClB,IAAI;CACJ,IAAI,cAAc,qBACjB,OAAO;MACD,IAAI,cAAc,uBACxB,OAAO;MACD,IAAI,cAAc,sBACxB,OAAO;MACD,IAAI,uBAAuB,EAAE,GACnC,OAAO;MAEP,OAAO;CAGR,OAAO;EACM;EACJ;EACM;EACd,SAAS,uBAAuB,EAAE;EAC5B;EACN,QAAQ,SAAS;CAClB;AACD;;;;;;;;;;;;AAaA,SAAgB,mBAAmB,OAAyD;CAC3F,OAAO;EAAC,OAAO,cAAc;EAAI,OAAO,cAAc;EAAI,OAAO,aAAa;EAAI,OAAO,gBAAgB;CAAE,CAAC,CAAC,KAC5G,GACD;AACD;;;;;;;;;AAUA,SAAS,2BAA2B,UAAuD;CAC1F,IAAI,YAAY,MAAM,OAAO;CAE7B,OAAO;EACN,SAAS;EACT,SAAS;EACT,SAAS,SAAS;EAClB,SAAS,cAAc;EACvB,mBAAmB,SAAS,KAAK;CAClC,CAAC,CAAC,KAAK,GAAG;AACX;;;;;;;;;;;;;;AAeA,SAAgB,8BAA8B,UAA6B,aAA4B;CACtG,IAAI,CAAC,eAAe,CAAC,SAAS,MAAK,MAAK,EAAE,YAAY,IAAI,GAAG;CAE7D,MAAM,aAAgC,CAAC;CACvC,KAAK,MAAM,WAAW,UAAU;EAC/B,IAAI,QAAQ,YAAY,MAAM;GAC7B,qBAAqB,YAAY,OAAO;GACxC;EACD;EAEA,MAAM,EAAE,UAAU,WAAW,GAAG,SAAS;EACzC,qBAAqB,YAAY,IAAI;CACtC;CAEA,SAAS,OAAO,GAAG,SAAS,QAAQ,GAAG,UAAU;AAClD;;;;;;;;;;;AAYA,SAAS,mBAAmB,SAAkC;CAC7D,OAAO;EACN,QAAQ;EACR,QAAQ,gBAAgB;EACxB,QAAQ,UAAU;EAClB,QAAQ;EACR,mBAAmB,QAAQ,KAAK;EAChC,QAAQ,OAAO,UAAU;EACzB,2BAA2B,QAAQ,QAAQ;EAC3C,QAAQ;CACT,CAAC,CAAC,KAAK,GAAG;AACX;;;;;AAMA,SAAgB,qBAAqB,MAAyB,SAAgC;CAC7F,MAAM,MAAM,mBAAmB,OAAO;CACtC,IAAI,KAAK,MAAK,aAAY,mBAAmB,QAAQ,MAAM,GAAG,GAAG;CAEjE,KAAK,KAAK,OAAO;AAClB"}
|
|
1
|
+
{"version":3,"file":"results.js","names":[],"sources":["../../../../plus/integrations/src/results.ts"],"sourcesContent":["import { isUnsupportedSortError } from '@gitkraken/provider-apis';\nimport { AuthenticationError, RequestNotFoundError, RequestRateLimitError } from '@gitlens/git/errors.js';\nimport type { IssueSorting } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSorting } from '@gitlens/git/models/pullRequest.js';\nimport type { IntegrationIds } from './constants.js';\nimport { isProviderUnreachableError } from './errors.js';\n\nexport interface ConnectionStateChangeEvent {\n\tkey: string;\n\treason: 'connected' | 'disconnected';\n}\n\n/**\n * A per-provider, per-connection warning surfaced alongside (partial) read results. Consumers use\n * these to drive auth recovery, retry, or truncation messaging without the read itself throwing.\n *\n * `kind` is the programmatic discriminant (a rate-limit is retryable, a 404 is not, an unsupported capability\n * never succeeds on retry, an auth failure needs re-connection unless {@link ProviderWarning.scope} confines\n * it); `isAuth` is retained as a convenience mirror of `kind === 'auth'`.\n *\n * `'unsupported'`: the provider — or this integration of it — lacks the capability the request asked for: a\n * surface (issues on Bitbucket, pull requests on an issue tracker, a batch read, org/repo discovery,\n * current-account lookup, project-scoped or account-wide issue reads), or a filter, sort, state, search\n * criterion or option combination it cannot express. Retrying the same request never succeeds; a different\n * provider, scope or request may. NOT unsupported (these stay `'other'`): malformed or contradictory caller\n * input the caller fixes in the request itself (a duplicate batch key, invalid target numbers, an empty owner,\n * an unusable scope), incompleteness or omission warnings, and unclassified errors.\n */\nexport type ProviderWarningKind = 'auth' | 'rate-limit' | 'not-found' | 'no-connection' | 'unsupported' | 'other';\n\n/**\n * Why a read that SUCCEEDED still withheld results:\n * - `provider-limit`: the provider refuses to serve past a cap it enforces (GitHub search's 1,000-result\n * ceiling, Trello's `cards_limit`), so the excess is unreachable through that query.\n * - `recovery-budget`: the internal partitioned recovery stopped spending upstream requests before it ran\n * out of partitions to visit.\n * - `pagination-incomplete`: pages were left unread — a sub-scope of a multi-scope read that was not drained,\n * a drain that stopped at its own page backstop, or a provider that advertised another page and gave no way\n * to reach it. These have DIFFERENT remedies, so the kind alone does not say whether more can be fetched;\n * read {@link ProviderWarningOmission.recovery} for that.\n *\n * Mirrors the vocabulary the SDK reports. Declared here rather than imported so this module stays free of\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`); `collectionMetadata.ts` holds the\n * compile-time link, so an SDK bump that adds a kind fails the build at that boundary.\n */\nexport type ProviderWarningOmissionKind = 'provider-limit' | 'recovery-budget' | 'pagination-incomplete';\n\n/**\n * How a consumer can recover omitted results. `page-budget` reruns with a larger budget; `narrow-scope`\n * requests a smaller server-side scope; `none` promises neither. Switch on the exact value because only\n * `page-budget` is a fetch-more action.\n */\nexport type ProviderWarningOmissionRecovery = 'none' | 'page-budget' | 'narrow-scope';\n\n/**\n * The part of a provider a warning is confined to: one resource (organization, workspace, site), project or\n * repository. All fields optional; see {@link ProviderWarning.scope} for when one is forwarded.\n */\nexport interface ProviderWarningScope {\n\t/**\n\t * The resource as the read addressed it: its id on most reads, its name on the few that address it by name\n\t * (Azure DevOps' repo-scoped reads, Bitbucket's workspace reads). Match it against both\n\t * `ProviderOrganization.id` and `name`.\n\t */\n\tresourceId?: string;\n\tprojectId?: string;\n\trepositoryId?: string;\n}\n\n/**\n * Why a provider request failed, when the failure itself says so:\n * - `unreachable`: a transport failure or server-side error prevented the provider from answering. The connection\n * still exists; this accompanies `kind: 'other'`, never an authentication warning.\n * - `oauth-app-not-allowed`: the organization does not let third-party OAuth apps in. On Azure DevOps this is the\n * organization policy \"Third-party application access via OAuth\", off by default for new organizations. An\n * organization admin enables it, or the user connects with a personal access token instead, which the policy\n * does not govern.\n * - `access-denied`: the account has no access to that organization or project (not a member, or lacking the\n * permission). Someone who administers it has to grant access.\n * - `conditional-access`: a Microsoft Entra Conditional Access policy blocked the request (Azure DevOps\n * `VS403463`). Its admin has to exempt the request, e.g. by location or device.\n *\n * None of these is healed by reconnecting, which is the point of naming them.\n */\nexport type ProviderWarningCauseReason =\n\t| 'oauth-app-not-allowed'\n\t| 'access-denied'\n\t| 'conditional-access'\n\t| 'unreachable';\n\n/** See {@link ProviderWarning.cause}. */\nexport interface ProviderWarningCause {\n\treason: ProviderWarningCauseReason;\n\t/** The provider's own error code, when it reports one (Azure DevOps `TF400813`, `VS403463`). */\n\tcode?: string;\n\t/** Where the setting behind the refusal is changed, when this layer can address it. */\n\tremedyUrl?: string;\n}\n\n/** Which repository / project / resource an omission is attributed to. All fields optional; a scope may name none. */\nexport interface ProviderWarningOmissionScope extends ProviderWarningScope {\n\tproviderId?: string;\n}\n\nexport interface ProviderWarningOmission {\n\tkind: ProviderWarningOmissionKind;\n\t/**\n\t * Whether anything would fetch the missing items, and what. Gate a \"load more\" affordance on this rather\n\t * than on `kind` — see {@link ProviderWarningOmissionRecovery}.\n\t */\n\trecovery: ProviderWarningOmissionRecovery;\n\t/**\n\t * The cap the provider enforces, when it reports one. NOT a result count for every kind: on\n\t * `recovery-budget` this is a REQUEST budget and must not be shown to a user as a number of results.\n\t */\n\tlimit?: number;\n\t/** Total matches the provider reported, when it reports one. Only GitHub's search cap does today. */\n\ttotalCount?: number;\n\t/**\n\t * The order the reachable window was selected under, as `field:direction`, when the omission is a result\n\t * ceiling.\n\t *\n\t * Present so a consumer can word its own \"N matched, showing the first M\" without assuming WHICH M: the\n\t * provider serves the top M under this key, so the sentence is only true if it names it. Absent for every\n\t * omission that isn't a ceiling, where no window was selected by an order.\n\t *\n\t * A union of both vocabularies rather than the issue one alone: `PullRequestSorting` happens to be a subset\n\t * today, so narrowing this to {@link IssueSorting} would still compile while describing only half the reads\n\t * that set it — and would break the day the PR search gains a key the issue search has no name for.\n\t */\n\tsort?: IssueSorting | PullRequestSorting;\n\t/** Which repository / project / resource was affected, when one is attributed. */\n\tscope?: ProviderWarningOmissionScope;\n}\n\nexport interface ProviderWarning {\n\tproviderId: IntegrationIds;\n\t/** Disambiguates connections on self-managed hosts (where one provider id spans multiple domains). */\n\tdomain?: string;\n\t/** The specific token/connection the read was pinned to, when a `connectionId` was supplied. */\n\tconnectionId?: string;\n\tmessage: string;\n\tkind: ProviderWarningKind;\n\t/** Convenience mirror of `kind === 'auth'`, scoped or not; read {@link scope} before prompting to reconnect. */\n\tisAuth: boolean;\n\t/**\n\t * Present when the failure was recorded against one sub-scope of the read (an organization, project or\n\t * repository) rather than against the connection. A scoped `auth` failure is that one scope refusing this\n\t * credential, e.g. an Azure DevOps organization that disallows third-party OAuth apps, one in another Entra\n\t * tenant, or a Conditional Access policy. Reconnecting cannot fix those, so this warning alone is no reason\n\t * to prompt for it.\n\t *\n\t * `kind` and `isAuth` are unaffected: a scoped 401 is still an authentication failure, and this field says\n\t * how far it reaches. It names at least one of its IDs, and is never set on an omission (which carries its\n\t * own {@link ProviderWarningOmission.scope}) or on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}), even when that call targeted a single organization. The exception is a batch\n\t * read's refused target, which names its scope: the Azure DevOps organization and project, the repository on\n\t * other git hosts, or the tracker's resource.\n\t *\n\t * Its ABSENCE means account-wide or unattributed, so existing handling stays correct without it.\n\t *\n\t * A scoped `auth` failure also means the credential itself was accepted. A dead token can come back as nothing\n\t * but scoped refusals wherever a read reaches its scopes without an uncached request to the connection first:\n\t * discovery served from a per-token cache (Azure DevOps, Bitbucket, Jira Cloud) or an SDK fan-out across the\n\t * requested repositories (Bitbucket Data Center). So when a read's only auth failures are scoped, those providers\n\t * confirm the credential with one uncached check first, and a refused credential fails the whole read with an\n\t * unscoped `auth` warning. A batch read checks it only when every target was refused, since a target that\n\t * answered already proved it. A refusal the provider attributes to the credential itself, like Bitbucket's or Jira\n\t * Cloud's for a token missing the OAuth scopes a read needs, which a reconnect fixes, is published unscoped too,\n\t * once for the connection, while the scopes that answered keep their results.\n\t *\n\t * So the promise holds for a read that carries no unscoped `auth` warning of its own; one that does already asks\n\t * for a reconnect, and its other scoped refusals are not checked. Two exceptions stay scoped even then. A check\n\t * that could not complete or was denied (a network error, a throttle, a `403`, Bitbucket Data Center refusing a\n\t * credential it authenticated, or a Bitbucket Data Center project access token, whose user the check cannot\n\t * find) proves nothing, so the warnings are published unconfirmed, with no {@link cause}, and the next read\n\t * checks again. And a credential confirmed within the last minute is not probed again, so a revocation can take\n\t * up to a minute to surface.\n\t */\n\tscope?: ProviderWarningScope;\n\t/**\n\t * Why the provider request failed, when the failure itself says so: see {@link ProviderWarningCauseReason}. Switch on\n\t * `reason` to recommend a fix instead of a reconnect; `message` then carries the same explanation as prose.\n\t *\n\t * `unreachable` accompanies a transport or server failure with `kind: 'other'`. Credential refusal causes are\n\t * set only on a scoped `auth` warning whose credential was confirmed first (see {@link scope}), because the\n\t * refusals they name are indistinguishable from a dead credential until then: Azure DevOps answers a\n\t * third-party OAuth app its organization disallows exactly as it answers an expired token. Only Azure DevOps\n\t * reports one today. Its absence proves nothing about the cause.\n\t */\n\tcause?: ProviderWarningCause;\n\t/**\n\t * Present when this warning describes results the read could not return even though the request itself\n\t * SUCCEEDED — a provider-enforced cap, an exhausted recovery budget, a page budget, or a sub-scope the read\n\t * did not drain.\n\t *\n\t * Its presence is the signal: an omission is not a failure. Message it as incompleteness rather than as a\n\t * failed read, and do NOT derive that from `message`, which is English prose and subject to rewording.\n\t * `kind` stays `'other'` for these, so the failure discriminant keeps meaning exactly what it meant before\n\t * this field existed.\n\t *\n\t * Whether anything would fetch the rest is a SEPARATE question — read {@link ProviderWarningOmission.recovery},\n\t * not `kind`, and never assume a retry of the same request returns more.\n\t *\n\t * `limit` / `totalCount` / `scope` are forwarded only when they are reported; most omissions carry none of\n\t * the three, so render correctly without them — and see {@link ProviderWarningOmission.limit} before\n\t * showing that figure as a result count.\n\t *\n\t * Its ABSENCE proves nothing. It is never set on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}) or from a structured scope failure — those are failures, and the field would\n\t * be a lie there. But it is also absent whenever incompleteness was reported without naming what was left\n\t * out, so treat a bare `kind: 'other'` warning as unclassified rather than as a proven failure.\n\t */\n\tomission?: ProviderWarningOmission;\n}\n\nexport interface ProviderPageInfo {\n\t/**\n\t * The 1-based POSITION of the returned page. One convention across every paged read, page-number and\n\t * cursor paging alike, so a `getNextPageParam`-style consumer can key off it uniformly:\n\t * - the page number the provider itself reported, when it reports one (authoritative);\n\t * - else the position the caller addressed: the page encoded in a threaded page cursor, or the `page`\n\t * supplied alongside an opaque cursor (1 when the caller supplied no `page`);\n\t * - else, for a read that can be advanced by page number, the requested `page` — the provider either\n\t * honored it or the internal drain reached it (see the paging contract on `IntegrationManager`);\n\t * - else 1: a cursor-only read with no cursor threaded back ignores a requested page number and answers\n\t * with its first page, so echoing the request would mislabel page 1 as page N.\n\t *\n\t * A page past the provider's terminal cursor still reports the requested `page` with empty `items` — an\n\t * empty page N, never page N-1 relabeled.\n\t *\n\t * Continue a cursor-paged read from `cursor`, NOT from `currentPage + 1`: a cursor-only host can't be\n\t * addressed by number, so this is a position label and not always a resumable request parameter.\n\t */\n\tcurrentPage: number;\n\t/**\n\t * The page size IN EFFECT, which is not always the one requested — it is whichever of these the read can\n\t * actually vouch for:\n\t * - the size the provider reported for the page it served (numbered-page hosts), else\n\t * - the `itemsPerPage` that was applied to the request (repo-scoped reads, which forward it), else\n\t * - `items.length`, for a read that takes no page size at all: the account-wide PR/issue reads, `listRepos`\n\t * (cursor-only), and `listIssueTrackerIssuesPage`, whose `itemsPerPage` counts PROJECTS rather than issues.\n\t *\n\t * So it is a description of the page, not an echo of the request: don't derive a total from\n\t * `currentPage * itemsPerPage`, and don't read `items.length < itemsPerPage` as \"last page\" — `hasMore` /\n\t * `cursor` are the only continuation signals.\n\t */\n\titemsPerPage: number;\n\t/**\n\t * A COMPLETENESS assertion, not a mode flag: `true` only when a sweep drained every page of every target\n\t * without truncating or failing (`!truncated && !fetchFailed`). Set exclusively by `sweepPullRequests` /\n\t * `sweepClosedPullRequests` and absent on every paged read, so `allPages !== true` does NOT by itself mean\n\t * \"this was a single page\".\n\t *\n\t * This is the signal to gate on before caching a sweep as authoritative: unlike `truncated` it is `false`\n\t * for BOTH kinds of incompleteness, so it can't be misread as a benign cap.\n\t */\n\tallPages?: boolean;\n\t/**\n\t * True when the read stopped before it could confirm it had everything. Deliberately does NOT distinguish\n\t * WHY, and a consumer must not assume it means a provider ceiling — a sweep sets it from three different\n\t * origins:\n\t * - a provider-side ceiling or per-scope backstop propagated from below (e.g. GitHub Search's 1000-result\n\t * cap): the missing items are NOT reachable, so this really is \"complete to the provider's limit\";\n\t * - the sweep's OWN `maxPages` cap (default 100) with a usable cursor still in hand: the missing items ARE\n\t * reachable and were simply not fetched — raise `maxPages` to get them;\n\t * - the provider reporting another page but handing back no usable cursor: not reachable.\n\t *\n\t * Only the second is recoverable, and a sweep exposes no continuation cursor, so treating a truncated sweep\n\t * as a complete set silently drops reachable data in that case. Prefer `allPages` for the cache decision.\n\t */\n\ttruncated?: boolean;\n}\n\nexport interface ProviderResult<T> {\n\titems: T[];\n\twarnings: ProviderWarning[];\n\t/**\n\t * True when `items` is incomplete — distinguishes a failed or truncated flat read from a genuinely empty\n\t * result. Paged results additionally use `page.truncated` when a completed page could not confirm it\n\t * drained everything.\n\t */\n\tfetchFailed?: boolean;\n}\n\nexport interface ProviderPagedResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Opaque cursor for cursor-based paging (GraphQL, per-repo/per-project cursors, etc.). For hosts that\n\t * require the previous page's cursor, consumers should pass this value back to the next `cursor` option.\n\t * A retry-only cursor may remain when `hasMore` is false: `hasMore` means automatic forward progress,\n\t * while explicitly reusing such a cursor retries failed work without creating an infinite paging loop.\n\t */\n\tcursor?: string;\n}\n\nexport interface ProviderSweepResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Providers whose top-level read failed before returning any usable page. Partial per-scope failures stay\n\t * represented by `warnings` + `fetchFailed` and are intentionally excluded.\n\t */\n\tfailedProviderIds: IntegrationIds[];\n\t/**\n\t * Providers whose read started successfully but could not produce an authoritative complete slice (for\n\t * example, a later page failed, collection metadata reported a partial scope, or a paging backstop was hit).\n\t * Kept separate from\n\t * `failedProviderIds` so consumers can accept healthy sibling providers while preserving an older snapshot\n\t * for a provider whose returned slice has a gap.\n\t */\n\tincompleteProviderIds: IntegrationIds[];\n}\n\nexport interface ProviderBroadenResult<T> extends ProviderPagedResult<T> {\n\tbroadenedProviderIds: IntegrationIds[];\n\t/** Providers for which no requested org produced a usable issue result. */\n\tfailedProviderIds: IntegrationIds[];\n\t/** Providers with both a usable org result and at least one failed or truncated org slice. */\n\tincompleteProviderIds: IntegrationIds[];\n\tfanOutCount: number;\n}\n\nexport type RepositoryResolutionStatus =\n\t| 'resolved'\n\t| 'not-found'\n\t| 'unauthorized'\n\t| 'unsupported-provider'\n\t| 'invalid-remote-url'\n\t| 'host-mismatch'\n\t| 'undetermined';\n\nexport interface RepositoryIdentity {\n\tproviderId: IntegrationIds;\n\tdomain: string;\n\t/** The provider's canonical owner/namespace (follows renames), falling back to the parsed remote when omitted. */\n\towner: string;\n\t/** The provider's canonical repo name (follows renames), falling back to the parsed remote when omitted. */\n\tname: string;\n\tproject?: string;\n\t/** The original input remote URL, so the caller can key the resolution back to the remote it asked about. */\n\tremoteUrl: string;\n\t/**\n\t * True when the provider's canonical owner/name differ from what the input remote URL carried — i.e. the\n\t * repo was renamed/moved host-side and the local remote is stale. Case-insensitive, mirroring gkcli's\n\t * `EqualFold` compare so hosts that echo the input casing (e.g. Bitbucket Server/Azure) aren't flagged.\n\t */\n\trenamed: boolean;\n}\n\nexport interface RepositoryResolution {\n\tstatus: RepositoryResolutionStatus;\n\tidentity?: RepositoryIdentity;\n\twarning?: ProviderWarning;\n}\n\nexport interface ResolveRepositoryResult {\n\tresolution: RepositoryResolution;\n}\n\nconst maxProviderWarningMessageLength = 500;\n\n/** A caught error's message as a warning can carry it: an HTML page becomes its status, and prose is capped. */\nexport function providerWarningMessage(ex: unknown): string {\n\tconst raw = (ex instanceof Error ? ex.message : String(ex)).trim();\n\tconst carrier = ex as { status?: unknown; response?: { status?: unknown } } | null | undefined;\n\tconst status =\n\t\ttypeof carrier?.status === 'number'\n\t\t\t? carrier.status\n\t\t\t: typeof carrier?.response?.status === 'number'\n\t\t\t\t? carrier.response.status\n\t\t\t\t: undefined;\n\n\t// Gateways commonly answer API requests with a complete HTML error page. That payload is neither actionable\n\t// nor suitable for a warning DTO consumed by UIs and logs; preserve the status without leaking the document.\n\tif (/<(?:!doctype\\s+html|html|head|body)\\b/i.test(raw)) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\n\tif (!raw) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\treturn raw.length > maxProviderWarningMessageLength\n\t\t? `${raw.slice(0, maxProviderWarningMessageLength - 3)}...`\n\t\t: raw;\n}\n\n/**\n * Normalized org/workspace/group shape returned by the provider facade.\n * `name` is the provider identifier to pass to follow-up reads, while `org` is an optional display label.\n */\nexport interface ProviderOrganization {\n\tid: string;\n\tproviderId: IntegrationIds;\n\tname: string;\n\torg?: string;\n\turl: string;\n}\n\n/** Normalized repository shape returned by the provider facade. */\nexport interface ProviderRepositoryShape {\n\tid: string;\n\tnamespace: string;\n\tname: string;\n\t/** Azure DevOps project; `undefined` for hosts without a project layer. */\n\tproject?: string;\n\t/** Web (browser) URL, when the provider exposes it. */\n\turl?: string;\n\t/** HTTPS clone URL, when available. */\n\tcloneUrlHttps?: string;\n\t/** SSH clone URL, when available. */\n\tcloneUrlSsh?: string;\n\t/** Default branch name, when the provider reports it. */\n\tdefaultBranch?: string;\n}\n\n/**\n * Classifies a caught provider exception into a neutral {@link ProviderWarning}. Ordering matters:\n * `instanceof` is checked most-specific-first so a rate-limit isn't mislabeled as a generic error and\n * a 404 isn't mislabeled as auth.\n */\nexport function toProviderWarning(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tex: unknown,\n): ProviderWarning {\n\tlet kind: ProviderWarning['kind'];\n\tif (ex instanceof AuthenticationError) {\n\t\tkind = 'auth';\n\t} else if (ex instanceof RequestRateLimitError) {\n\t\tkind = 'rate-limit';\n\t} else if (ex instanceof RequestNotFoundError) {\n\t\tkind = 'not-found';\n\t} else if (isUnsupportedSortError(ex)) {\n\t\tkind = 'unsupported';\n\t} else {\n\t\tkind = 'other';\n\t}\n\n\treturn {\n\t\tproviderId: providerId,\n\t\tdomain: domain,\n\t\tconnectionId: connectionId,\n\t\tmessage: providerWarningMessage(ex),\n\t\tkind: kind,\n\t\tisAuth: kind === 'auth',\n\t\t...(kind === 'other' && isProviderUnreachableError(ex) ? { cause: { reason: 'unreachable' as const } } : {}),\n\t};\n}\n\n/**\n * A scope's identity, as the stable string that keys it.\n *\n * The single definition of what \"the same scope\" means, shared by the failure and omission dedup keys in\n * `providerPaging.ts` and by {@link providerWarningKey} below, so a scope gaining a field is one edit rather\n * than three. `providerId` is included: the same repository ID under two providers is two scopes.\n *\n * The parameter is structural rather than the SDK's `CollectionScope` so this module keeps naming no\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`) while still serving its SDK-facing\n * callers, which pass that type in unchanged.\n */\nexport function collectionScopeKey(scope: ProviderWarningOmissionScope | undefined): string {\n\treturn [scope?.providerId ?? '', scope?.resourceId ?? '', scope?.projectId ?? '', scope?.repositoryId ?? ''].join(\n\t\t' ',\n\t);\n}\n\n/**\n * The omission's contribution to a warning's identity — empty when there is none, so a warning without an\n * omission keeps deduping exactly as it did before the field existed.\n *\n * Two omissions of different kinds do produce different `message` values today, so message alone would still\n * separate them. That is incidental: the premise of `omission` is that consumers must not depend on prose\n * carrying the distinguishing fact, and this key must not either.\n */\nfunction providerWarningOmissionKey(omission: ProviderWarningOmission | undefined): string {\n\tif (omission == null) return '';\n\n\treturn [\n\t\tomission.kind,\n\t\tomission.recovery,\n\t\tomission.limit ?? '',\n\t\tomission.totalCount ?? '',\n\t\tcollectionScopeKey(omission.scope),\n\t].join(' ');\n}\n\n/**\n * Strips the omission from every warning in `warnings` when the read as a whole failed.\n *\n * An omission asserts the request SUCCEEDED, and a drain only learns it failed AFTER it may have emitted one:\n * an early page can report its own truncation and then a later page can die. Deciding per warning, at the\n * moment each is built, cannot see that future — so the aggregate is reconciled once, here, where\n * `fetchFailed` is final. Call it at the point a read returns its `fetchFailed`.\n *\n * Re-dedupes as it goes: the omission is part of a warning's identity, so two warnings that differed only\n * there become identical once it is gone, and the array's contract is that no two entries are equal.\n *\n * Mutates in place: the warning array is the one being returned, and callers accumulate into it across pages.\n */\nexport function reconcileOmissionsWithFailure(warnings: ProviderWarning[], fetchFailed: boolean): void {\n\tif (!fetchFailed || !warnings.some(w => w.omission != null)) return;\n\n\tconst reconciled: ProviderWarning[] = [];\n\tfor (const warning of warnings) {\n\t\tif (warning.omission == null) {\n\t\t\tappendDedupedWarning(reconciled, warning);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { omission: _omission, ...rest } = warning;\n\t\tappendDedupedWarning(reconciled, rest);\n\t}\n\n\twarnings.splice(0, warnings.length, ...reconciled);\n}\n\n/**\n * A stable key for deduplicating warnings accumulated across drained pages / fan-out scopes.\n *\n * The scope and the cause are keyed for the reason the omission is (see {@link providerWarningOmissionKey}): today's\n * failure messages happen to spell them out, but two failures that differ in either must stay two warnings even if\n * they don't.\n *\n * `message` stays LAST. It is the only free-form segment — provider prose, spaces and all — so anything\n * appended after it could be impersonated by a message that happens to end in the same text.\n */\nfunction providerWarningKey(warning: ProviderWarning): string {\n\treturn [\n\t\twarning.providerId,\n\t\twarning.connectionId ?? '',\n\t\twarning.domain ?? '',\n\t\twarning.kind,\n\t\tcollectionScopeKey(warning.scope),\n\t\twarning.cause?.reason ?? '',\n\t\tproviderWarningOmissionKey(warning.omission),\n\t\twarning.message,\n\t].join(' ');\n}\n\n/**\n * Appends `warning` to `into` only when an equal warning (by provider/connection/domain/kind/message, plus the\n * structured scope, cause and omission when present) is absent.\n */\nexport function appendDedupedWarning(into: ProviderWarning[], warning: ProviderWarning): void {\n\tconst key = providerWarningKey(warning);\n\tif (into.some(existing => providerWarningKey(existing) === key)) return;\n\n\tinto.push(warning);\n}\n"],"mappings":";;;;AAyWA,MAAM,kCAAkC;;AAGxC,SAAgB,uBAAuB,IAAqB;CAC3D,MAAM,OAAO,cAAc,QAAQ,GAAG,UAAU,OAAO,EAAE,EAAA,CAAG,KAAK;CACjE,MAAM,UAAU;CAChB,MAAM,SACL,OAAO,SAAS,WAAW,WACxB,QAAQ,SACR,OAAO,SAAS,UAAU,WAAW,WACpC,QAAQ,SAAS,SACjB,KAAA;CAIL,IAAI,yCAAyC,KAAK,GAAG,GACpD,OAAO,UAAU,OAAO,uCAAuC,OAAO,KAAK;CAG5E,IAAI,CAAC,KACJ,OAAO,UAAU,OAAO,uCAAuC,OAAO,KAAK;CAE5E,OAAO,IAAI,SAAS,kCACjB,GAAG,IAAI,MAAM,GAAG,GAAmC,EAAE,OACrD;AACJ;;;;;;AAoCA,SAAgB,kBACf,YACA,QACA,cACA,IACkB;CAClB,IAAI;CACJ,IAAI,cAAc,qBACjB,OAAO;MACD,IAAI,cAAc,uBACxB,OAAO;MACD,IAAI,cAAc,sBACxB,OAAO;MACD,IAAI,uBAAuB,EAAE,GACnC,OAAO;MAEP,OAAO;CAGR,OAAO;EACM;EACJ;EACM;EACd,SAAS,uBAAuB,EAAE;EAC5B;EACN,QAAQ,SAAS;EACjB,GAAI,SAAS,WAAW,2BAA2B,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,cAAuB,EAAE,IAAI,CAAC;CAC3G;AACD;;;;;;;;;;;;AAaA,SAAgB,mBAAmB,OAAyD;CAC3F,OAAO;EAAC,OAAO,cAAc;EAAI,OAAO,cAAc;EAAI,OAAO,aAAa;EAAI,OAAO,gBAAgB;CAAE,CAAC,CAAC,KAC5G,GACD;AACD;;;;;;;;;AAUA,SAAS,2BAA2B,UAAuD;CAC1F,IAAI,YAAY,MAAM,OAAO;CAE7B,OAAO;EACN,SAAS;EACT,SAAS;EACT,SAAS,SAAS;EAClB,SAAS,cAAc;EACvB,mBAAmB,SAAS,KAAK;CAClC,CAAC,CAAC,KAAK,GAAG;AACX;;;;;;;;;;;;;;AAeA,SAAgB,8BAA8B,UAA6B,aAA4B;CACtG,IAAI,CAAC,eAAe,CAAC,SAAS,MAAK,MAAK,EAAE,YAAY,IAAI,GAAG;CAE7D,MAAM,aAAgC,CAAC;CACvC,KAAK,MAAM,WAAW,UAAU;EAC/B,IAAI,QAAQ,YAAY,MAAM;GAC7B,qBAAqB,YAAY,OAAO;GACxC;EACD;EAEA,MAAM,EAAE,UAAU,WAAW,GAAG,SAAS;EACzC,qBAAqB,YAAY,IAAI;CACtC;CAEA,SAAS,OAAO,GAAG,SAAS,QAAQ,GAAG,UAAU;AAClD;;;;;;;;;;;AAYA,SAAS,mBAAmB,SAAkC;CAC7D,OAAO;EACN,QAAQ;EACR,QAAQ,gBAAgB;EACxB,QAAQ,UAAU;EAClB,QAAQ;EACR,mBAAmB,QAAQ,KAAK;EAChC,QAAQ,OAAO,UAAU;EACzB,2BAA2B,QAAQ,QAAQ;EAC3C,QAAQ;CACT,CAAC,CAAC,KAAK,GAAG;AACX;;;;;AAMA,SAAgB,qBAAqB,MAAyB,SAAgC;CAC7F,MAAM,MAAM,mBAAmB,OAAO;CACtC,IAAI,KAAK,MAAK,aAAY,mBAAmB,QAAQ,MAAM,GAAG,GAAG;CAEjE,KAAK,KAAK,OAAO;AAClB"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitkraken/core-gitlens",
|
|
3
3
|
"description": "GitLens core — shared Git / AI / GitHub primitives for internal GitKraken consumption",
|
|
4
|
-
"version": "0.10.
|
|
4
|
+
"version": "0.10.3",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "GitKraken",
|