@gitkraken/core-gitlens 0.9.2 → 0.9.4
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 +26 -1
- package/dist/plus/git-github/api/github.js +1 -1
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +8 -2
- package/dist/plus/integrations/integrationService.js +3 -1
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +6 -1
- package/dist/plus/integrations/models/integration.d.ts +8 -0
- package/dist/plus/integrations/models/integration.js +9 -0
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issueReads.d.ts +14 -0
- package/dist/plus/integrations/providers/azure/azure.d.ts +3 -0
- package/dist/plus/integrations/providers/azure/azure.js +18 -0
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.utils.d.ts +12 -0
- package/dist/plus/integrations/providers/azure/azure.utils.js +46 -0
- package/dist/plus/integrations/providers/azure/azure.utils.js.map +1 -0
- package/dist/plus/integrations/providers/azure/models.d.ts +1 -0
- package/dist/plus/integrations/providers/azure/models.js +1 -1
- package/dist/plus/integrations/providers/azure/models.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +3 -0
- package/dist/plus/integrations/providers/azureDevOps.js +7 -0
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket/bitbucket.utils.d.ts +17 -0
- package/dist/plus/integrations/providers/bitbucket/bitbucket.utils.js +25 -0
- package/dist/plus/integrations/providers/bitbucket/bitbucket.utils.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket/models.d.ts +5 -5
- package/dist/plus/integrations/providers/bitbucket/models.js +2 -2
- package/dist/plus/integrations/providers/bitbucket/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +3 -0
- package/dist/plus/integrations/providers/bitbucket-server.js +7 -0
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.d.ts +3 -0
- package/dist/plus/integrations/providers/bitbucket.js +7 -0
- package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
- package/dist/plus/integrations/providers/jira-server.d.ts +5 -3
- package/dist/plus/integrations/providers/jira-server.js +28 -8
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +26 -4
- package/dist/plus/integrations/providers/jira.js +148 -53
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.d.ts +13 -2
- package/dist/plus/integrations/providers/linear.js +49 -12
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +19 -1
- package/dist/plus/integrations/providers/models.js +14 -5
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providerErrors.d.ts +11 -0
- package/dist/plus/integrations/providers/providerErrors.js +22 -1
- package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +8 -1
- package/dist/plus/integrations/providers/providersApi.js +3 -0
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/trello.js +1 -0
- package/dist/plus/integrations/providers/trello.js.map +1 -1
- package/dist/plus/integrations/providers/utils/discoveryCache.d.ts +36 -0
- package/dist/plus/integrations/providers/utils/discoveryCache.js +59 -0
- package/dist/plus/integrations/providers/utils/discoveryCache.js.map +1 -0
- package/dist/plus/integrations/reads/cursors.d.ts +8 -0
- package/dist/plus/integrations/reads/cursors.js +5 -3
- package/dist/plus/integrations/reads/cursors.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +0 -1
- package/dist/plus/integrations/reads/drains.js +4 -4
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/filters.d.ts +8 -0
- package/dist/plus/integrations/reads/filters.js +2 -1
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts +9 -1
- package/dist/plus/integrations/reads/issueTracker.js +20 -3
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +11 -0
- package/dist/plus/integrations/reads/warnings.js +14 -1
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/docs/integrations.md +4 -0
- package/package.json +2 -2
|
@@ -70,6 +70,7 @@ var TrelloIntegration = class extends IssuesIntegration {
|
|
|
70
70
|
}
|
|
71
71
|
async getProviderIssuesForProjectWithTruncation(session, project, options) {
|
|
72
72
|
if (!isIssueResourceDescriptor(project)) return void 0;
|
|
73
|
+
if (options?.state != null && options.state !== "open") throw new IntegrationReadUnavailableError(metadata.name, `issue state '${options.state}' is not supported`);
|
|
73
74
|
const appKey = this.requireAppKey(session);
|
|
74
75
|
const api = await this.getProvidersApi();
|
|
75
76
|
const tokenWithInfo = toTokenWithInfo(this.id, session);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trello.js","names":[],"sources":["../../../../../plus/integrations/src/providers/trello.ts"],"sourcesContent":["import type { Account } from '@gitlens/git/models/author.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 type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport { IntegrationReadUnavailableError } from '../errors.js';\nimport type { IssuesForProjectOptions, ProjectIssuesDrain } from '../models/issueReads.js';\nimport { IssuesIntegration } from '../models/issuesIntegration.js';\nimport { fromProviderIssue, providersMetadata, toIssueShape } from './models.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Trello];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\n\n/** A Trello board, surfaced as both a \"resource\" (org analogue) and a \"project\" for issue reads. */\nexport interface TrelloBoardDescriptor extends IssueResourceDescriptor {}\n\nexport class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Trello> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\n\toverride get id(): IssuesCloudHostIntegrationId.Trello {\n\t\treturn IssuesCloudHostIntegrationId.Trello;\n\t}\n\tprotected override get key(): 'trello' {\n\t\treturn 'trello';\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\n\t/** The Trello client requires the app key paired with the token; a session without one can't read. */\n\tprivate appKeyFor(session: ProviderAuthenticationSession): string | undefined {\n\t\treturn session.appKey;\n\t}\n\n\t/**\n\t * Resolves the app key or throws — a session that authenticated but carries no app key can't read Trello,\n\t * and must be surfaced as a broken read (warning + fetchFailed) rather than an empty account.\n\t */\n\tprivate requireAppKey(session: ProviderAuthenticationSession): string {\n\t\tconst appKey = this.appKeyFor(session);\n\t\tif (appKey == null) {\n\t\t\tthrow new IntegrationReadUnavailableError(metadata.name, 'missing app key (session has no appKey)');\n\t\t}\n\t\treturn appKey;\n\t}\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst user = await api.getTrelloCurrentUser(toTokenWithInfo(this.id, session), appKey);\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,\n\t\t\tusername: user.username,\n\t\t\temail: user.email,\n\t\t\tavatarUrl: user.avatarUrl ?? undefined,\n\t\t};\n\t}\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<TrelloBoardDescriptor[] | undefined> {\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst boards = await api.getTrelloBoardsForCurrentUser(toTokenWithInfo(this.id, session), appKey);\n\t\treturn boards?.map(b => ({ key: b.id, id: b.id, name: b.name }));\n\t}\n\n\tprotected override getProviderProjectsForResources(\n\t\t_session: ProviderAuthenticationSession,\n\t\tresources: ResourceDescriptor[],\n\t): Promise<TrelloBoardDescriptor[] | undefined> {\n\t\t// Trello boards are both the resource and the project scope, so projects are the boards themselves.\n\t\treturn Promise.resolve(\n\t\t\tresources.filter(isIssueResourceDescriptor).map(r => ({ key: r.key, id: r.id, name: r.name })),\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\t// A non-issue descriptor genuinely has nothing to read (empty), but a missing app key is a broken read.\n\t\tif (!isIssueResourceDescriptor(project)) return undefined;\n\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\n\t\t// Enrich issues with their board list names (card status), mirroring gitkraken.dev's Trello reads.\n\t\tconst lists = await api.getTrelloListsForBoard(tokenWithInfo, appKey, project.id);\n\t\tconst trelloBoardListsById = lists?.reduce<Record<string, { name: string }>>((acc, list) => {\n\t\t\tacc[list.id] = { name: list.name };\n\t\t\treturn acc;\n\t\t}, {});\n\t\tconst boardProject = {\n\t\t\tid: project.id,\n\t\t\tname: project.name,\n\t\t\tresourceId: project.id,\n\t\t\tresourceName: project.name,\n\t\t};\n\n\t\tconst result = await api.getTrelloIssuesForBoard(tokenWithInfo, appKey, project.id, {\n\t\t\tassigneeLogins: options?.user != null ? [options.user] : undefined,\n\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t\tsort: options?.sort,\n\t\t});\n\n\t\tconst values = result.values.flatMap(issue => {\n\t\t\tconst mapped = toIssueShape(issue, this);\n\t\t\treturn mapped == null ? [] : [{ ...mapped, project: boardProject } satisfies IssueShape];\n\t\t});\n\n\t\t// Trello's search caps results and reports the cap through `metadata.completeness` (partial/unknown),\n\t\t// never a cursor. Surface that as terminal truncation; there is no next page to fetch, so retrying the\n\t\t// same read cannot recover the omitted cards (D11).\n\t\tconst truncated = result.metadata != null && result.metadata.completeness !== 'complete';\n\t\treturn truncated\n\t\t\t? { values: values, truncated: true, recovery: 'none', metadata: result.metadata }\n\t\t\t: { values: values, truncated: false, metadata: result.metadata };\n\t}\n\n\tprotected override searchProviderMyIssues(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resources?: ResourceDescriptor[],\n\t\t_cancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\t// Trello has no cross-board \"issues assigned to me\" endpoint; issues are read per board via\n\t\t// getIssuesForProject. Callers that need them enumerate boards first.\n\t\treturn Promise.resolve(undefined);\n\t}\n\n\tprotected override getProviderLinkedIssueOrPullRequest(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t\t_id: { id: string; key: string },\n\t\t_type: undefined | IssueOrPullRequestType,\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\treturn Promise.resolve(undefined);\n\t}\n\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tif (!isIssueResourceDescriptor(resource)) return undefined;\n\n\t\tconst appKey = this.requireAppKey(session);\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst lists = await api.getTrelloListsForBoard(tokenWithInfo, appKey, resource.id);\n\t\tconst trelloBoardListsById = lists?.reduce<Record<string, { name: string }>>((acc, list) => {\n\t\t\tacc[list.id] = { name: list.name };\n\t\t\treturn acc;\n\t\t}, {});\n\n\t\t// Branch-association round-trips use the encoded entity identifier's stable Trello card id. Resolve that\n\t\t// directly so large boards don't require a capped whole-board scan; keep a numeric idShort fallback so\n\t\t// legacy/manual callers that still pass the board-local display number continue to resolve.\n\t\tlet issue = await api.getTrelloCard(tokenWithInfo, appKey, id, {\n\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t});\n\t\tif (issue == null && /^[0-9]+$/.test(id)) {\n\t\t\tissue = (\n\t\t\t\tawait api.getTrelloIssuesForBoard(tokenWithInfo, appKey, resource.id, {\n\t\t\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t\t\t})\n\t\t\t).values.find(issue => issue.number === id);\n\t\t}\n\n\t\treturn issue != null\n\t\t\t? fromProviderIssue(issue, this, {\n\t\t\t\t\tproject: {\n\t\t\t\t\t\tid: resource.id,\n\t\t\t\t\t\tname: resource.name,\n\t\t\t\t\t\tresourceId: resource.id,\n\t\t\t\t\t\tresourceName: resource.name,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t: undefined;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"trello.js","names":[],"sources":["../../../../../plus/integrations/src/providers/trello.ts"],"sourcesContent":["import type { Account } from '@gitlens/git/models/author.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 type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport { IntegrationReadUnavailableError } from '../errors.js';\nimport type { IssuesForProjectOptions, ProjectIssuesDrain } from '../models/issueReads.js';\nimport { IssuesIntegration } from '../models/issuesIntegration.js';\nimport { fromProviderIssue, providersMetadata, toIssueShape } from './models.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Trello];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\n\n/** A Trello board, surfaced as both a \"resource\" (org analogue) and a \"project\" for issue reads. */\nexport interface TrelloBoardDescriptor extends IssueResourceDescriptor {}\n\nexport class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Trello> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\n\toverride get id(): IssuesCloudHostIntegrationId.Trello {\n\t\treturn IssuesCloudHostIntegrationId.Trello;\n\t}\n\tprotected override get key(): 'trello' {\n\t\treturn 'trello';\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\n\t/** The Trello client requires the app key paired with the token; a session without one can't read. */\n\tprivate appKeyFor(session: ProviderAuthenticationSession): string | undefined {\n\t\treturn session.appKey;\n\t}\n\n\t/**\n\t * Resolves the app key or throws — a session that authenticated but carries no app key can't read Trello,\n\t * and must be surfaced as a broken read (warning + fetchFailed) rather than an empty account.\n\t */\n\tprivate requireAppKey(session: ProviderAuthenticationSession): string {\n\t\tconst appKey = this.appKeyFor(session);\n\t\tif (appKey == null) {\n\t\t\tthrow new IntegrationReadUnavailableError(metadata.name, 'missing app key (session has no appKey)');\n\t\t}\n\t\treturn appKey;\n\t}\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst user = await api.getTrelloCurrentUser(toTokenWithInfo(this.id, session), appKey);\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,\n\t\t\tusername: user.username,\n\t\t\temail: user.email,\n\t\t\tavatarUrl: user.avatarUrl ?? undefined,\n\t\t};\n\t}\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<TrelloBoardDescriptor[] | undefined> {\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst boards = await api.getTrelloBoardsForCurrentUser(toTokenWithInfo(this.id, session), appKey);\n\t\treturn boards?.map(b => ({ key: b.id, id: b.id, name: b.name }));\n\t}\n\n\tprotected override getProviderProjectsForResources(\n\t\t_session: ProviderAuthenticationSession,\n\t\tresources: ResourceDescriptor[],\n\t): Promise<TrelloBoardDescriptor[] | undefined> {\n\t\t// Trello boards are both the resource and the project scope, so projects are the boards themselves.\n\t\treturn Promise.resolve(\n\t\t\tresources.filter(isIssueResourceDescriptor).map(r => ({ key: r.key, id: r.id, name: r.name })),\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\t// A non-issue descriptor genuinely has nothing to read (empty), but a missing app key is a broken read.\n\t\tif (!isIssueResourceDescriptor(project)) return undefined;\n\n\t\t// The board read always excludes archived cards, and a card has no other state, so only open cards can be\n\t\t// read. Refused rather than ignored: answering `closed` with the open cards would publish them as closed.\n\t\tif (options?.state != null && options.state !== 'open') {\n\t\t\tthrow new IntegrationReadUnavailableError(metadata.name, `issue state '${options.state}' is not supported`);\n\t\t}\n\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\n\t\t// Enrich issues with their board list names (card status), mirroring gitkraken.dev's Trello reads.\n\t\tconst lists = await api.getTrelloListsForBoard(tokenWithInfo, appKey, project.id);\n\t\tconst trelloBoardListsById = lists?.reduce<Record<string, { name: string }>>((acc, list) => {\n\t\t\tacc[list.id] = { name: list.name };\n\t\t\treturn acc;\n\t\t}, {});\n\t\tconst boardProject = {\n\t\t\tid: project.id,\n\t\t\tname: project.name,\n\t\t\tresourceId: project.id,\n\t\t\tresourceName: project.name,\n\t\t};\n\n\t\tconst result = await api.getTrelloIssuesForBoard(tokenWithInfo, appKey, project.id, {\n\t\t\tassigneeLogins: options?.user != null ? [options.user] : undefined,\n\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t\tsort: options?.sort,\n\t\t});\n\n\t\tconst values = result.values.flatMap(issue => {\n\t\t\tconst mapped = toIssueShape(issue, this);\n\t\t\treturn mapped == null ? [] : [{ ...mapped, project: boardProject } satisfies IssueShape];\n\t\t});\n\n\t\t// Trello's search caps results and reports the cap through `metadata.completeness` (partial/unknown),\n\t\t// never a cursor. Surface that as terminal truncation; there is no next page to fetch, so retrying the\n\t\t// same read cannot recover the omitted cards (D11).\n\t\tconst truncated = result.metadata != null && result.metadata.completeness !== 'complete';\n\t\treturn truncated\n\t\t\t? { values: values, truncated: true, recovery: 'none', metadata: result.metadata }\n\t\t\t: { values: values, truncated: false, metadata: result.metadata };\n\t}\n\n\tprotected override searchProviderMyIssues(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resources?: ResourceDescriptor[],\n\t\t_cancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\t// Trello has no cross-board \"issues assigned to me\" endpoint; issues are read per board via\n\t\t// getIssuesForProject. Callers that need them enumerate boards first.\n\t\treturn Promise.resolve(undefined);\n\t}\n\n\tprotected override getProviderLinkedIssueOrPullRequest(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t\t_id: { id: string; key: string },\n\t\t_type: undefined | IssueOrPullRequestType,\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\treturn Promise.resolve(undefined);\n\t}\n\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tif (!isIssueResourceDescriptor(resource)) return undefined;\n\n\t\tconst appKey = this.requireAppKey(session);\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst lists = await api.getTrelloListsForBoard(tokenWithInfo, appKey, resource.id);\n\t\tconst trelloBoardListsById = lists?.reduce<Record<string, { name: string }>>((acc, list) => {\n\t\t\tacc[list.id] = { name: list.name };\n\t\t\treturn acc;\n\t\t}, {});\n\n\t\t// Branch-association round-trips use the encoded entity identifier's stable Trello card id. Resolve that\n\t\t// directly so large boards don't require a capped whole-board scan; keep a numeric idShort fallback so\n\t\t// legacy/manual callers that still pass the board-local display number continue to resolve.\n\t\tlet issue = await api.getTrelloCard(tokenWithInfo, appKey, id, {\n\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t});\n\t\tif (issue == null && /^[0-9]+$/.test(id)) {\n\t\t\tissue = (\n\t\t\t\tawait api.getTrelloIssuesForBoard(tokenWithInfo, appKey, resource.id, {\n\t\t\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t\t\t})\n\t\t\t).values.find(issue => issue.number === id);\n\t\t}\n\n\t\treturn issue != null\n\t\t\t? fromProviderIssue(issue, this, {\n\t\t\t\t\tproject: {\n\t\t\t\t\t\tid: resource.id,\n\t\t\t\t\t\tname: resource.name,\n\t\t\t\t\t\tresourceId: resource.id,\n\t\t\t\t\t\tresourceName: resource.name,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t: undefined;\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region ../plus/integrations/src/providers/utils/discoveryCache.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* How long a tracker's discovered project set (Jira sites and projects, Linear teams) is trusted before it is read
|
|
4
|
+
* again. Projects are created and deleted while a session runs, and a stale set costs more than a re-read: a created
|
|
5
|
+
* project never appears, and a deleted one keeps being searched, which Jira refuses with a `400` (#5907).
|
|
6
|
+
*/
|
|
7
|
+
export declare const discoveryCacheTtl: number;
|
|
8
|
+
/**
|
|
9
|
+
* A per-key cache of discovery results that expires entries after {@link discoveryCacheTtl} and can be dropped
|
|
10
|
+
* wholesale when the connection is re-synced.
|
|
11
|
+
*
|
|
12
|
+
* {@link clear} bumps a generation, and {@link set} refuses a value read under an older one, so a read that was
|
|
13
|
+
* already in flight when the cache was cleared cannot write the set it read before the clear back into it.
|
|
14
|
+
*/
|
|
15
|
+
export declare class DiscoveryCache<V> {
|
|
16
|
+
private readonly ttl;
|
|
17
|
+
private readonly entries;
|
|
18
|
+
private _generation;
|
|
19
|
+
constructor(ttl?: number);
|
|
20
|
+
/** Pass to {@link set} to have a value read now refused if the cache is cleared before it is stored. */
|
|
21
|
+
get generation(): number;
|
|
22
|
+
get(key: string): V | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Stores `value` unless the cache was cleared since `generation` was taken. `storedAt` backdates an entry seeded
|
|
25
|
+
* from persisted storage, so it expires on the schedule of the read that produced it rather than of the seeding.
|
|
26
|
+
* Returns whether the value was stored.
|
|
27
|
+
*/
|
|
28
|
+
set(key: string, value: V, options?: {
|
|
29
|
+
generation?: number;
|
|
30
|
+
storedAt?: number;
|
|
31
|
+
}): boolean;
|
|
32
|
+
delete(key: string): void;
|
|
33
|
+
clear(): void;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//# sourceMappingURL=discoveryCache.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//#region ../plus/integrations/src/providers/utils/discoveryCache.ts
|
|
2
|
+
/**
|
|
3
|
+
* How long a tracker's discovered project set (Jira sites and projects, Linear teams) is trusted before it is read
|
|
4
|
+
* again. Projects are created and deleted while a session runs, and a stale set costs more than a re-read: a created
|
|
5
|
+
* project never appears, and a deleted one keeps being searched, which Jira refuses with a `400` (#5907).
|
|
6
|
+
*/
|
|
7
|
+
const discoveryCacheTtl = 3e5;
|
|
8
|
+
/**
|
|
9
|
+
* A per-key cache of discovery results that expires entries after {@link discoveryCacheTtl} and can be dropped
|
|
10
|
+
* wholesale when the connection is re-synced.
|
|
11
|
+
*
|
|
12
|
+
* {@link clear} bumps a generation, and {@link set} refuses a value read under an older one, so a read that was
|
|
13
|
+
* already in flight when the cache was cleared cannot write the set it read before the clear back into it.
|
|
14
|
+
*/
|
|
15
|
+
var DiscoveryCache = class {
|
|
16
|
+
ttl;
|
|
17
|
+
entries = /* @__PURE__ */ new Map();
|
|
18
|
+
_generation = 0;
|
|
19
|
+
constructor(ttl = discoveryCacheTtl) {
|
|
20
|
+
this.ttl = ttl;
|
|
21
|
+
}
|
|
22
|
+
/** Pass to {@link set} to have a value read now refused if the cache is cleared before it is stored. */
|
|
23
|
+
get generation() {
|
|
24
|
+
return this._generation;
|
|
25
|
+
}
|
|
26
|
+
get(key) {
|
|
27
|
+
const entry = this.entries.get(key);
|
|
28
|
+
if (entry == null) return void 0;
|
|
29
|
+
if (Date.now() - entry.storedAt >= this.ttl) {
|
|
30
|
+
this.entries.delete(key);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
return entry.value;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Stores `value` unless the cache was cleared since `generation` was taken. `storedAt` backdates an entry seeded
|
|
37
|
+
* from persisted storage, so it expires on the schedule of the read that produced it rather than of the seeding.
|
|
38
|
+
* Returns whether the value was stored.
|
|
39
|
+
*/
|
|
40
|
+
set(key, value, options) {
|
|
41
|
+
if (options?.generation != null && options.generation !== this._generation) return false;
|
|
42
|
+
this.entries.set(key, {
|
|
43
|
+
value,
|
|
44
|
+
storedAt: options?.storedAt ?? Date.now()
|
|
45
|
+
});
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
delete(key) {
|
|
49
|
+
this.entries.delete(key);
|
|
50
|
+
}
|
|
51
|
+
clear() {
|
|
52
|
+
this._generation++;
|
|
53
|
+
this.entries.clear();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
export { DiscoveryCache, discoveryCacheTtl };
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=discoveryCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discoveryCache.js","names":[],"sources":["../../../../../../plus/integrations/src/providers/utils/discoveryCache.ts"],"sourcesContent":["/**\n * How long a tracker's discovered project set (Jira sites and projects, Linear teams) is trusted before it is read\n * again. Projects are created and deleted while a session runs, and a stale set costs more than a re-read: a created\n * project never appears, and a deleted one keeps being searched, which Jira refuses with a `400` (#5907).\n */\nexport const discoveryCacheTtl = 5 * 60 * 1000;\n\n/**\n * A per-key cache of discovery results that expires entries after {@link discoveryCacheTtl} and can be dropped\n * wholesale when the connection is re-synced.\n *\n * {@link clear} bumps a generation, and {@link set} refuses a value read under an older one, so a read that was\n * already in flight when the cache was cleared cannot write the set it read before the clear back into it.\n */\nexport class DiscoveryCache<V> {\n\tprivate readonly entries = new Map<string, { value: V; storedAt: number }>();\n\tprivate _generation = 0;\n\n\tconstructor(private readonly ttl: number = discoveryCacheTtl) {}\n\n\t/** Pass to {@link set} to have a value read now refused if the cache is cleared before it is stored. */\n\tget generation(): number {\n\t\treturn this._generation;\n\t}\n\n\tget(key: string): V | undefined {\n\t\tconst entry = this.entries.get(key);\n\t\tif (entry == null) return undefined;\n\t\tif (Date.now() - entry.storedAt >= this.ttl) {\n\t\t\tthis.entries.delete(key);\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn entry.value;\n\t}\n\n\t/**\n\t * Stores `value` unless the cache was cleared since `generation` was taken. `storedAt` backdates an entry seeded\n\t * from persisted storage, so it expires on the schedule of the read that produced it rather than of the seeding.\n\t * Returns whether the value was stored.\n\t */\n\tset(key: string, value: V, options?: { generation?: number; storedAt?: number }): boolean {\n\t\tif (options?.generation != null && options.generation !== this._generation) return false;\n\n\t\tthis.entries.set(key, { value: value, storedAt: options?.storedAt ?? Date.now() });\n\t\treturn true;\n\t}\n\n\tdelete(key: string): void {\n\t\tthis.entries.delete(key);\n\t}\n\n\tclear(): void {\n\t\tthis._generation++;\n\t\tthis.entries.clear();\n\t}\n}\n"],"mappings":";;;;;;AAKA,MAAa,oBAAoB;;;;;;;;AASjC,IAAa,iBAAb,MAA+B;CAID;CAH7B,0BAA2B,IAAI,IAA4C;CAC3E,cAAsB;CAEtB,YAAY,MAA+B,mBAAmB;EAAjC,KAAA,MAAA;CAAkC;;CAG/D,IAAI,aAAqB;EACxB,OAAO,KAAK;CACb;CAEA,IAAI,KAA4B;EAC/B,MAAM,QAAQ,KAAK,QAAQ,IAAI,GAAG;EAClC,IAAI,SAAS,MAAM,OAAO,KAAA;EAC1B,IAAI,KAAK,IAAI,IAAI,MAAM,YAAY,KAAK,KAAK;GAC5C,KAAK,QAAQ,OAAO,GAAG;GACvB;EACD;EAEA,OAAO,MAAM;CACd;;;;;;CAOA,IAAI,KAAa,OAAU,SAA+D;EACzF,IAAI,SAAS,cAAc,QAAQ,QAAQ,eAAe,KAAK,aAAa,OAAO;EAEnF,KAAK,QAAQ,IAAI,KAAK;GAAS;GAAO,UAAU,SAAS,YAAY,KAAK,IAAI;EAAE,CAAC;EACjF,OAAO;CACR;CAEA,OAAO,KAAmB;EACzB,KAAK,QAAQ,OAAO,GAAG;CACxB;CAEA,QAAc;EACb,KAAK;EACL,KAAK,QAAQ,MAAM;CACpB;AACD"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IssueStateFilter } from "../../../git/models/issue.js";
|
|
1
2
|
import { IntegrationIds } from "../constants.js";
|
|
2
3
|
//#region ../plus/integrations/src/reads/cursors.d.ts
|
|
3
4
|
/**
|
|
@@ -21,6 +22,12 @@ export interface IssueTrackerPageCursor {
|
|
|
21
22
|
retryPages?: number[];
|
|
22
23
|
retryProjects?: string[];
|
|
23
24
|
completedProjects?: string[];
|
|
25
|
+
/**
|
|
26
|
+
* The `state` the read that minted this cursor asked for, omitted for open. `completedProjects` records what was
|
|
27
|
+
* already drained UNDER that state, so resuming it under another would skip projects whose other-state issues
|
|
28
|
+
* were never read.
|
|
29
|
+
*/
|
|
30
|
+
state?: 'closed' | 'all';
|
|
24
31
|
}
|
|
25
32
|
export declare function parseIssueTrackerPageCursor(cursor: string | undefined): IssueTrackerPageCursor | undefined;
|
|
26
33
|
export declare function toIssueTrackerPageCursor(options: {
|
|
@@ -30,6 +37,7 @@ export declare function toIssueTrackerPageCursor(options: {
|
|
|
30
37
|
retryPages: readonly number[];
|
|
31
38
|
retryProjects: readonly string[];
|
|
32
39
|
completedProjects?: readonly string[];
|
|
40
|
+
state?: IssueStateFilter;
|
|
33
41
|
}): string | undefined;
|
|
34
42
|
/** Identifies one org slice of the fan-out. Connection and domain are part of the key: two accounts, or two
|
|
35
43
|
* self-managed hosts, can each have an org of the same name. */
|
|
@@ -29,7 +29,8 @@ function parseIssueTrackerPageCursor(cursor) {
|
|
|
29
29
|
...typeof parsed.nextPage === "number" && Number.isSafeInteger(parsed.nextPage) && parsed.nextPage > 0 ? { nextPage: parsed.nextPage } : {},
|
|
30
30
|
...retryPages != null ? { retryPages } : {},
|
|
31
31
|
...retryProjects != null ? { retryProjects } : {},
|
|
32
|
-
...completedProjects != null ? { completedProjects } : {}
|
|
32
|
+
...completedProjects != null ? { completedProjects } : {},
|
|
33
|
+
...parsed.state === "closed" || parsed.state === "all" ? { state: parsed.state } : {}
|
|
33
34
|
};
|
|
34
35
|
} catch {
|
|
35
36
|
return;
|
|
@@ -40,7 +41,7 @@ function toIssueTrackerPageCursor(options) {
|
|
|
40
41
|
const retryProjects = [...new Set(options.retryProjects)].sort();
|
|
41
42
|
const completedProjects = retryPages.length > 0 || retryProjects.length > 0 || options.nextPage != null ? [...new Set(options.completedProjects ?? [])].sort() : [];
|
|
42
43
|
if (retryPages.length === 0 && retryProjects.length === 0 && options.nextPage == null) return;
|
|
43
|
-
if (retryPages.length === 0 && retryProjects.length === 0 && completedProjects.length === 0 && options.nextPage != null && options.unpaged !== true) return toPageCursor(options.nextPage);
|
|
44
|
+
if (retryPages.length === 0 && retryProjects.length === 0 && completedProjects.length === 0 && options.nextPage != null && options.unpaged !== true && (options.state == null || options.state === "open")) return toPageCursor(options.nextPage);
|
|
44
45
|
return JSON.stringify({
|
|
45
46
|
type: "issue-tracker-page",
|
|
46
47
|
currentPage: options.currentPage,
|
|
@@ -48,7 +49,8 @@ function toIssueTrackerPageCursor(options) {
|
|
|
48
49
|
...options.nextPage != null ? { nextPage: options.nextPage } : {},
|
|
49
50
|
...retryPages.length > 0 ? { retryPages } : {},
|
|
50
51
|
...retryProjects.length > 0 ? { retryProjects } : {},
|
|
51
|
-
...completedProjects.length > 0 ? { completedProjects } : {}
|
|
52
|
+
...completedProjects.length > 0 ? { completedProjects } : {},
|
|
53
|
+
...options.state === "closed" || options.state === "all" ? { state: options.state } : {}
|
|
52
54
|
});
|
|
53
55
|
}
|
|
54
56
|
/** Whether a bundle entry addresses the same org slice as `org`, comparing domains by host. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cursors.js","names":[],"sources":["../../../../../plus/integrations/src/reads/cursors.ts"],"sourcesContent":["import type { IntegrationIds } from '../constants.js';\nimport { toPageCursor } from '../providers/utils/providerPaging.js';\nimport { hostFromDomain } from '../utils/domain.utils.js';\nimport { pageToCursor } from './paging.js';\n\n/**\n * The COMPOSITE cursors, for the two reads a single provider cursor can't address.\n *\n * A plain paged read threads the provider's own opaque continuation. These two fan out instead — the\n * issue-tracker read over a window of projects, the broaden read over a set of orgs — so one round produces\n * several provider positions plus the bookkeeping needed to resume: which scopes failed and should be retried,\n * and which are already drained and must NOT be re-read (a cursor-only provider handed a fresh page-1 request\n * answers with its first page again, duplicating items).\n *\n * Both encodings are opaque to consumers and are parsed defensively: a cursor is caller-supplied data that may\n * be stale, truncated, or from another read entirely, so anything unrecognized degrades to \"no cursor\" (which\n * falls back to page paging) rather than throwing or being forwarded to a provider as a foreign string.\n */\n\n// ---------------------------------------------------------------------------------------------------------\n// Issue-tracker page cursor (Jira/Linear/Trello: resource → project, paginated by a window of PROJECTS)\n// ---------------------------------------------------------------------------------------------------------\n\nexport interface IssueTrackerPageCursor {\n\ttype: 'issue-tracker-page';\n\tcurrentPage: number;\n\tunpaged?: boolean;\n\tnextPage?: number;\n\tretryPages?: number[];\n\tretryProjects?: string[];\n\tcompletedProjects?: string[];\n}\n\n/** Positive safe integers only, deduped; `undefined` when nothing survives (so the field is omitted). */\nfunction positiveIntegers(values: unknown): number[] | undefined {\n\tif (!Array.isArray(values)) return undefined;\n\n\tconst valid = values.filter(\n\t\t(value): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value > 0,\n\t);\n\treturn valid.length > 0 ? [...new Set(valid)] : undefined;\n}\n\n/** Non-empty strings only, deduped; `undefined` when nothing survives (so the field is omitted). */\nfunction nonEmptyStrings(values: unknown): string[] | undefined {\n\tif (!Array.isArray(values)) return undefined;\n\n\tconst valid = values.filter((value): value is string => typeof value === 'string' && value.length > 0);\n\treturn valid.length > 0 ? [...new Set(valid)] : undefined;\n}\n\nexport function parseIssueTrackerPageCursor(cursor: string | undefined): IssueTrackerPageCursor | undefined {\n\tif (cursor == null) return undefined;\n\n\ttry {\n\t\tconst parsed = JSON.parse(cursor) as Partial<IssueTrackerPageCursor>;\n\t\tif (\n\t\t\tparsed.type !== 'issue-tracker-page' ||\n\t\t\ttypeof parsed.currentPage !== 'number' ||\n\t\t\t!Number.isSafeInteger(parsed.currentPage) ||\n\t\t\tparsed.currentPage < 1\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst retryPages = positiveIntegers(parsed.retryPages);\n\t\tconst retryProjects = nonEmptyStrings(parsed.retryProjects);\n\t\tconst completedProjects = nonEmptyStrings(parsed.completedProjects);\n\t\treturn {\n\t\t\ttype: 'issue-tracker-page',\n\t\t\tcurrentPage: parsed.currentPage,\n\t\t\t...(parsed.unpaged === true ? { unpaged: true } : {}),\n\t\t\t...(typeof parsed.nextPage === 'number' && Number.isSafeInteger(parsed.nextPage) && parsed.nextPage > 0\n\t\t\t\t? { nextPage: parsed.nextPage }\n\t\t\t\t: {}),\n\t\t\t...(retryPages != null ? { retryPages: retryPages } : {}),\n\t\t\t...(retryProjects != null ? { retryProjects: retryProjects } : {}),\n\t\t\t...(completedProjects != null ? { completedProjects: completedProjects } : {}),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function toIssueTrackerPageCursor(options: {\n\tcurrentPage: number;\n\tunpaged?: boolean;\n\tnextPage?: number;\n\tretryPages: readonly number[];\n\tretryProjects: readonly string[];\n\tcompletedProjects?: readonly string[];\n}): string | undefined {\n\tconst retryPages = [...new Set(options.retryPages)].sort((a, b) => a - b);\n\tconst retryProjects = [...new Set(options.retryProjects)].sort();\n\tconst completedProjects =\n\t\tretryPages.length > 0 || retryProjects.length > 0 || options.nextPage != null\n\t\t\t? [...new Set(options.completedProjects ?? [])].sort()\n\t\t\t: [];\n\tif (retryPages.length === 0 && retryProjects.length === 0 && options.nextPage == null) {\n\t\treturn undefined;\n\t}\n\t// Nothing to carry beyond the next window: emit the plain page cursor instead of a composite one, so an\n\t// ordinary forward read round-trips through the same encoding every other paged read uses.\n\tif (\n\t\tretryPages.length === 0 &&\n\t\tretryProjects.length === 0 &&\n\t\tcompletedProjects.length === 0 &&\n\t\toptions.nextPage != null &&\n\t\toptions.unpaged !== true\n\t) {\n\t\treturn toPageCursor(options.nextPage);\n\t}\n\treturn JSON.stringify({\n\t\ttype: 'issue-tracker-page',\n\t\tcurrentPage: options.currentPage,\n\t\t...(options.unpaged === true ? { unpaged: true } : {}),\n\t\t...(options.nextPage != null ? { nextPage: options.nextPage } : {}),\n\t\t...(retryPages.length > 0 ? { retryPages: retryPages } : {}),\n\t\t...(retryProjects.length > 0 ? { retryProjects: retryProjects } : {}),\n\t\t...(completedProjects.length > 0 ? { completedProjects: completedProjects } : {}),\n\t} satisfies IssueTrackerPageCursor);\n}\n\n// ---------------------------------------------------------------------------------------------------------\n// Broaden-issues cursor bundle (one provider position per org in the fan-out)\n// ---------------------------------------------------------------------------------------------------------\n\n/** Identifies one org slice of the fan-out. Connection and domain are part of the key: two accounts, or two\n * self-managed hosts, can each have an org of the same name. */\nexport interface BroadenIssuesOrgKey {\n\tproviderId: IntegrationIds;\n\tname: string;\n\tconnectionId?: string;\n\tdomain?: string;\n}\n\n/** One org's position in the bundle: either the provider's own cursor, or a page to retry after a failure. */\nexport interface BroadenIssuesOrgCursor {\n\tproviderId: IntegrationIds;\n\torg: string;\n\tconnectionId?: string;\n\tdomain?: string;\n\tcursor?: string;\n\tretryPage?: number;\n}\n\nexport type BroadenIssuesExhaustedOrg = Omit<BroadenIssuesOrgCursor, 'cursor' | 'retryPage'>;\n\n/** Whether a bundle entry addresses the same org slice as `org`, comparing domains by host. */\nfunction matchesOrg(\n\tentry: { providerId?: IntegrationIds; org?: string; connectionId?: string; domain?: string },\n\torg: BroadenIssuesOrgKey,\n): boolean {\n\treturn (\n\t\tentry.providerId === org.providerId &&\n\t\tentry.org === org.name &&\n\t\tentry.connectionId === org.connectionId &&\n\t\t(hostFromDomain(entry.domain) ?? entry.domain) === (hostFromDomain(org.domain) ?? org.domain)\n\t);\n}\n\n/**\n * The cursor to send for one org in this round: its entry in the bundle, or a synthesized page cursor.\n *\n * A single-org fan-out has no bundle to key into — its cursor IS the provider's, threaded straight through.\n */\nexport function getBroadenIssuesCursor(\n\tcursor: string | undefined,\n\torg: BroadenIssuesOrgKey,\n\tpage: number,\n\torgCount: number,\n): string | undefined {\n\tif (orgCount === 1) return cursor ?? pageToCursor(page);\n\n\tif (cursor != null) {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(cursor) as { cursors?: BroadenIssuesOrgCursor[] };\n\t\t\tconst match = parsed.cursors?.find(c => matchesOrg(c, org));\n\t\t\tif (match?.cursor != null) return match.cursor;\n\t\t\tif (typeof match?.retryPage === 'number' && Number.isSafeInteger(match.retryPage) && match.retryPage > 0) {\n\t\t\t\treturn toPageCursor(match.retryPage);\n\t\t\t}\n\t\t} catch {}\n\t}\n\n\treturn pageToCursor(page);\n}\n\n/**\n * Whether a prior round already drained this org (multi-org fan-out only). Once an org runs out of pages while\n * another org keeps paging, the bundle records it as exhausted so the next round skips it instead of re-issuing\n * a page-1 read — which cursor-only providers (having no page-number cursor to honor) would answer with their\n * first page again, duplicating results.\n */\nexport function isBroadenIssuesOrgExhausted(\n\tcursor: string | undefined,\n\torg: BroadenIssuesOrgKey,\n\torgCount: number,\n): boolean {\n\tif (orgCount === 1 || cursor == null) return false;\n\n\ttry {\n\t\tconst parsed = JSON.parse(cursor) as { exhausted?: BroadenIssuesExhaustedOrg[] };\n\t\treturn parsed.exhausted?.some(e => matchesOrg(e, org)) ?? false;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Encodes the round's per-org positions. A single-org fan-out emits that org's cursor directly, unwrapped. */\nexport function toBroadenIssuesCursor(\n\tcursors: BroadenIssuesOrgCursor[],\n\texhausted: BroadenIssuesExhaustedOrg[],\n\torgCount: number,\n): string | undefined {\n\tif (cursors.length === 0) return undefined;\n\tif (orgCount === 1) {\n\t\treturn cursors[0].cursor ?? (cursors[0].retryPage != null ? toPageCursor(cursors[0].retryPage) : undefined);\n\t}\n\n\t// Carry the exhausted orgs alongside the still-active cursors so the next round can skip them (see\n\t// isBroadenIssuesOrgExhausted). Only meaningful while at least one org still has more to read.\n\treturn JSON.stringify({ cursors: cursors, exhausted: exhausted });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cursors.js","names":[],"sources":["../../../../../plus/integrations/src/reads/cursors.ts"],"sourcesContent":["import type { IssueStateFilter } from '@gitlens/git/models/issue.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { toPageCursor } from '../providers/utils/providerPaging.js';\nimport { hostFromDomain } from '../utils/domain.utils.js';\nimport { pageToCursor } from './paging.js';\n\n/**\n * The COMPOSITE cursors, for the two reads a single provider cursor can't address.\n *\n * A plain paged read threads the provider's own opaque continuation. These two fan out instead — the\n * issue-tracker read over a window of projects, the broaden read over a set of orgs — so one round produces\n * several provider positions plus the bookkeeping needed to resume: which scopes failed and should be retried,\n * and which are already drained and must NOT be re-read (a cursor-only provider handed a fresh page-1 request\n * answers with its first page again, duplicating items).\n *\n * Both encodings are opaque to consumers and are parsed defensively: a cursor is caller-supplied data that may\n * be stale, truncated, or from another read entirely, so anything unrecognized degrades to \"no cursor\" (which\n * falls back to page paging) rather than throwing or being forwarded to a provider as a foreign string.\n */\n\n// ---------------------------------------------------------------------------------------------------------\n// Issue-tracker page cursor (Jira/Linear/Trello: resource → project, paginated by a window of PROJECTS)\n// ---------------------------------------------------------------------------------------------------------\n\nexport interface IssueTrackerPageCursor {\n\ttype: 'issue-tracker-page';\n\tcurrentPage: number;\n\tunpaged?: boolean;\n\tnextPage?: number;\n\tretryPages?: number[];\n\tretryProjects?: string[];\n\tcompletedProjects?: string[];\n\t/**\n\t * The `state` the read that minted this cursor asked for, omitted for open. `completedProjects` records what was\n\t * already drained UNDER that state, so resuming it under another would skip projects whose other-state issues\n\t * were never read.\n\t */\n\tstate?: 'closed' | 'all';\n}\n\n/** Positive safe integers only, deduped; `undefined` when nothing survives (so the field is omitted). */\nfunction positiveIntegers(values: unknown): number[] | undefined {\n\tif (!Array.isArray(values)) return undefined;\n\n\tconst valid = values.filter(\n\t\t(value): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value > 0,\n\t);\n\treturn valid.length > 0 ? [...new Set(valid)] : undefined;\n}\n\n/** Non-empty strings only, deduped; `undefined` when nothing survives (so the field is omitted). */\nfunction nonEmptyStrings(values: unknown): string[] | undefined {\n\tif (!Array.isArray(values)) return undefined;\n\n\tconst valid = values.filter((value): value is string => typeof value === 'string' && value.length > 0);\n\treturn valid.length > 0 ? [...new Set(valid)] : undefined;\n}\n\nexport function parseIssueTrackerPageCursor(cursor: string | undefined): IssueTrackerPageCursor | undefined {\n\tif (cursor == null) return undefined;\n\n\ttry {\n\t\tconst parsed = JSON.parse(cursor) as Partial<IssueTrackerPageCursor>;\n\t\tif (\n\t\t\tparsed.type !== 'issue-tracker-page' ||\n\t\t\ttypeof parsed.currentPage !== 'number' ||\n\t\t\t!Number.isSafeInteger(parsed.currentPage) ||\n\t\t\tparsed.currentPage < 1\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst retryPages = positiveIntegers(parsed.retryPages);\n\t\tconst retryProjects = nonEmptyStrings(parsed.retryProjects);\n\t\tconst completedProjects = nonEmptyStrings(parsed.completedProjects);\n\t\treturn {\n\t\t\ttype: 'issue-tracker-page',\n\t\t\tcurrentPage: parsed.currentPage,\n\t\t\t...(parsed.unpaged === true ? { unpaged: true } : {}),\n\t\t\t...(typeof parsed.nextPage === 'number' && Number.isSafeInteger(parsed.nextPage) && parsed.nextPage > 0\n\t\t\t\t? { nextPage: parsed.nextPage }\n\t\t\t\t: {}),\n\t\t\t...(retryPages != null ? { retryPages: retryPages } : {}),\n\t\t\t...(retryProjects != null ? { retryProjects: retryProjects } : {}),\n\t\t\t...(completedProjects != null ? { completedProjects: completedProjects } : {}),\n\t\t\t...(parsed.state === 'closed' || parsed.state === 'all' ? { state: parsed.state } : {}),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function toIssueTrackerPageCursor(options: {\n\tcurrentPage: number;\n\tunpaged?: boolean;\n\tnextPage?: number;\n\tretryPages: readonly number[];\n\tretryProjects: readonly string[];\n\tcompletedProjects?: readonly string[];\n\tstate?: IssueStateFilter;\n}): string | undefined {\n\tconst retryPages = [...new Set(options.retryPages)].sort((a, b) => a - b);\n\tconst retryProjects = [...new Set(options.retryProjects)].sort();\n\tconst completedProjects =\n\t\tretryPages.length > 0 || retryProjects.length > 0 || options.nextPage != null\n\t\t\t? [...new Set(options.completedProjects ?? [])].sort()\n\t\t\t: [];\n\tif (retryPages.length === 0 && retryProjects.length === 0 && options.nextPage == null) {\n\t\treturn undefined;\n\t}\n\t// Nothing to carry beyond the next window: emit the plain page cursor instead of a composite one, so an\n\t// ordinary forward read round-trips through the same encoding every other paged read uses.\n\tif (\n\t\tretryPages.length === 0 &&\n\t\tretryProjects.length === 0 &&\n\t\tcompletedProjects.length === 0 &&\n\t\toptions.nextPage != null &&\n\t\toptions.unpaged !== true &&\n\t\t// A page cursor carries no state, so a narrowed read keeps the composite one: resumed under another state, a\n\t\t// plain cursor would start at its window and skip the earlier projects' issues of that state.\n\t\t(options.state == null || options.state === 'open')\n\t) {\n\t\treturn toPageCursor(options.nextPage);\n\t}\n\treturn JSON.stringify({\n\t\ttype: 'issue-tracker-page',\n\t\tcurrentPage: options.currentPage,\n\t\t...(options.unpaged === true ? { unpaged: true } : {}),\n\t\t...(options.nextPage != null ? { nextPage: options.nextPage } : {}),\n\t\t...(retryPages.length > 0 ? { retryPages: retryPages } : {}),\n\t\t...(retryProjects.length > 0 ? { retryProjects: retryProjects } : {}),\n\t\t...(completedProjects.length > 0 ? { completedProjects: completedProjects } : {}),\n\t\t...(options.state === 'closed' || options.state === 'all' ? { state: options.state } : {}),\n\t} satisfies IssueTrackerPageCursor);\n}\n\n// ---------------------------------------------------------------------------------------------------------\n// Broaden-issues cursor bundle (one provider position per org in the fan-out)\n// ---------------------------------------------------------------------------------------------------------\n\n/** Identifies one org slice of the fan-out. Connection and domain are part of the key: two accounts, or two\n * self-managed hosts, can each have an org of the same name. */\nexport interface BroadenIssuesOrgKey {\n\tproviderId: IntegrationIds;\n\tname: string;\n\tconnectionId?: string;\n\tdomain?: string;\n}\n\n/** One org's position in the bundle: either the provider's own cursor, or a page to retry after a failure. */\nexport interface BroadenIssuesOrgCursor {\n\tproviderId: IntegrationIds;\n\torg: string;\n\tconnectionId?: string;\n\tdomain?: string;\n\tcursor?: string;\n\tretryPage?: number;\n}\n\nexport type BroadenIssuesExhaustedOrg = Omit<BroadenIssuesOrgCursor, 'cursor' | 'retryPage'>;\n\n/** Whether a bundle entry addresses the same org slice as `org`, comparing domains by host. */\nfunction matchesOrg(\n\tentry: { providerId?: IntegrationIds; org?: string; connectionId?: string; domain?: string },\n\torg: BroadenIssuesOrgKey,\n): boolean {\n\treturn (\n\t\tentry.providerId === org.providerId &&\n\t\tentry.org === org.name &&\n\t\tentry.connectionId === org.connectionId &&\n\t\t(hostFromDomain(entry.domain) ?? entry.domain) === (hostFromDomain(org.domain) ?? org.domain)\n\t);\n}\n\n/**\n * The cursor to send for one org in this round: its entry in the bundle, or a synthesized page cursor.\n *\n * A single-org fan-out has no bundle to key into — its cursor IS the provider's, threaded straight through.\n */\nexport function getBroadenIssuesCursor(\n\tcursor: string | undefined,\n\torg: BroadenIssuesOrgKey,\n\tpage: number,\n\torgCount: number,\n): string | undefined {\n\tif (orgCount === 1) return cursor ?? pageToCursor(page);\n\n\tif (cursor != null) {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(cursor) as { cursors?: BroadenIssuesOrgCursor[] };\n\t\t\tconst match = parsed.cursors?.find(c => matchesOrg(c, org));\n\t\t\tif (match?.cursor != null) return match.cursor;\n\t\t\tif (typeof match?.retryPage === 'number' && Number.isSafeInteger(match.retryPage) && match.retryPage > 0) {\n\t\t\t\treturn toPageCursor(match.retryPage);\n\t\t\t}\n\t\t} catch {}\n\t}\n\n\treturn pageToCursor(page);\n}\n\n/**\n * Whether a prior round already drained this org (multi-org fan-out only). Once an org runs out of pages while\n * another org keeps paging, the bundle records it as exhausted so the next round skips it instead of re-issuing\n * a page-1 read — which cursor-only providers (having no page-number cursor to honor) would answer with their\n * first page again, duplicating results.\n */\nexport function isBroadenIssuesOrgExhausted(\n\tcursor: string | undefined,\n\torg: BroadenIssuesOrgKey,\n\torgCount: number,\n): boolean {\n\tif (orgCount === 1 || cursor == null) return false;\n\n\ttry {\n\t\tconst parsed = JSON.parse(cursor) as { exhausted?: BroadenIssuesExhaustedOrg[] };\n\t\treturn parsed.exhausted?.some(e => matchesOrg(e, org)) ?? false;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Encodes the round's per-org positions. A single-org fan-out emits that org's cursor directly, unwrapped. */\nexport function toBroadenIssuesCursor(\n\tcursors: BroadenIssuesOrgCursor[],\n\texhausted: BroadenIssuesExhaustedOrg[],\n\torgCount: number,\n): string | undefined {\n\tif (cursors.length === 0) return undefined;\n\tif (orgCount === 1) {\n\t\treturn cursors[0].cursor ?? (cursors[0].retryPage != null ? toPageCursor(cursors[0].retryPage) : undefined);\n\t}\n\n\t// Carry the exhausted orgs alongside the still-active cursors so the next round can skip them (see\n\t// isBroadenIssuesOrgExhausted). Only meaningful while at least one org still has more to read.\n\treturn JSON.stringify({ cursors: cursors, exhausted: exhausted });\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { PullRequestFilter, PullRequestStateFilter } from "../../../git/models/pullRequest.js";
|
|
2
2
|
import { IntegrationIds } from "../constants.js";
|
|
3
|
-
import "../providerFilters.js";
|
|
4
3
|
import { ProviderWarning } from "../results.js";
|
|
5
4
|
import { GitHostIntegration } from "../models/gitHostIntegration.js";
|
|
6
5
|
import { IntegrationResult } from "../models/integration.js";
|
|
@@ -3,7 +3,7 @@ import { appendDedupedWarning, reconcileOmissionsWithFailure, toProviderWarning
|
|
|
3
3
|
import { mergeAssessmentInto } from "../collectionMetadata.js";
|
|
4
4
|
import { isIssuesHostIntegrationId } from "../utils/integration.utils.js";
|
|
5
5
|
import { getProviderPullRequestIdentity } from "../providers/models.js";
|
|
6
|
-
import { noConnectionWarning, truncationWarning } from "./warnings.js";
|
|
6
|
+
import { cappedBesideBudgetWarning, noConnectionWarning, truncationWarning } from "./warnings.js";
|
|
7
7
|
//#region ../plus/integrations/src/reads/drains.ts
|
|
8
8
|
/**
|
|
9
9
|
* The all-pages reads: a provider read run repeatedly until it runs out of pages, as opposed to the single-page
|
|
@@ -48,7 +48,6 @@ async function drainPullRequests(integration, id, domain, repos, state, filters,
|
|
|
48
48
|
let page = 0;
|
|
49
49
|
let fetchFailed = false;
|
|
50
50
|
let truncated = false;
|
|
51
|
-
let providerTruncated = false;
|
|
52
51
|
let unexplainedTruncation = false;
|
|
53
52
|
const accountWide = repos.length === 0;
|
|
54
53
|
const includeReviewRequested = accountWide && (accountWideOptions?.includeReviewRequested ?? false);
|
|
@@ -101,7 +100,6 @@ async function drainPullRequests(integration, id, domain, repos, state, filters,
|
|
|
101
100
|
fetchFailed = fetchFailed || assessment.fetchFailed;
|
|
102
101
|
const pageTruncated = value.truncated === true || value.paging?.truncated === true || assessment.truncated;
|
|
103
102
|
truncated = truncated || pageTruncated;
|
|
104
|
-
providerTruncated = providerTruncated || pageTruncated;
|
|
105
103
|
unexplainedTruncation = unexplainedTruncation || pageTruncated && !assessment.truncated;
|
|
106
104
|
if (!(value.paging?.more ?? false)) {
|
|
107
105
|
if (unexplainedTruncation) appendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, "Pull request", fetchFailed ? "interrupted" : "exhausted"));
|
|
@@ -117,7 +115,9 @@ async function drainPullRequests(integration, id, domain, repos, state, filters,
|
|
|
117
115
|
const nextCursor = value.paging?.cursor;
|
|
118
116
|
const continuable = nextCursor != null && nextCursor !== "{}" && !seenCursors.has(nextCursor);
|
|
119
117
|
if (!continuable || page >= maxPages) {
|
|
120
|
-
|
|
118
|
+
const budgetStop = !fetchFailed && continuable;
|
|
119
|
+
appendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, "Pull request", fetchFailed ? "interrupted" : budgetStop ? "page-budget" : "exhausted"));
|
|
120
|
+
if (budgetStop && unexplainedTruncation) appendDedupedWarning(warnings, cappedBesideBudgetWarning(id, domain, connectionId, "Pull request"));
|
|
121
121
|
reconcileOmissionsWithFailure(warnings, fetchFailed);
|
|
122
122
|
return {
|
|
123
123
|
items,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drains.js","names":[],"sources":["../../../../../plus/integrations/src/reads/drains.ts"],"sourcesContent":["import type { PullRequestStateFilter } from '@gitlens/git/models/pullRequest.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { supportedOrderedCloudIntegrationIds } from '../constants.js';\nimport type { ProviderSweepTarget, PullRequestSweepOptions } from '../manager.js';\nimport type { GitHostIntegration } from '../models/gitHostIntegration.js';\nimport type { IntegrationResult } from '../models/integration.js';\nimport type { PullRequestFilter } from '../providerFilters.js';\nimport type { ProviderPullRequest, ProviderReposInput, ProviderRepository } from '../providers/models.js';\nimport { getProviderPullRequestIdentity } from '../providers/models.js';\nimport type { ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, reconcileOmissionsWithFailure, toProviderWarning } from '../results.js';\nimport { isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport { noConnectionWarning, truncationWarning } from './warnings.js';\n\n/**\n * The all-pages reads: a provider read run repeatedly until it runs out of pages, as opposed to the single-page\n * reads the facade's paged surface exposes. `sweepPullRequests` and `broadenIssues` are built on these.\n *\n * A drain's job is to keep going where a single page would stop, so what distinguishes it is its failure\n * bookkeeping rather than its loop: it must return the prefix it did read plus enough signal for the caller to\n * know the result is not authoritative — `truncated` when pages remain unread (a page backstop, or a provider\n * claiming another page while handing back no usable cursor), `fetchFailed` when a page or scope failed\n * outright. Discarding the prefix on a late failure, or returning it as if complete, are the two mistakes these\n * exist to prevent.\n */\n\n/**\n * Runs a result-returning read and captures failure as a neutral {@link ProviderWarning} rather than\n * letting it throw or silently vanish. Handles both a returned `{ error }` (the read cores' contract)\n * and a hard throw; a soft warning (`{ value, error }`) yields the value *and* a warning.\n */\nexport async function runCaptured<T>(\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tfn: () => Promise<IntegrationResult<T>>,\n\toptions?: { warnOnMissingSession?: boolean },\n): Promise<{ value?: T; warning?: ProviderWarning }> {\n\ttry {\n\t\tconst result = await fn();\n\t\tif (result == null) {\n\t\t\t// The read core returns undefined only when it couldn't resolve a session. For a per-connection\n\t\t\t// or explicit-domain read that means the requested target is gone or its authentication is invalid,\n\t\t\t// which must not be reported as an empty account. The untargeted primary path legitimately yields\n\t\t\t// nothing when the provider isn't connected, so leave it as an empty result.\n\t\t\treturn connectionId != null || options?.warnOnMissingSession\n\t\t\t\t? { warning: noConnectionWarning(id, domain, connectionId) }\n\t\t\t\t: {};\n\t\t}\n\t\tif (result.error != null) {\n\t\t\treturn { value: result.value, warning: toProviderWarning(id, domain, connectionId, result.error) };\n\t\t}\n\t\treturn { value: result.value };\n\t} catch (ex) {\n\t\treturn { warning: toProviderWarning(id, domain, connectionId, ex) };\n\t}\n}\n\n/**\n * Drains every page of the user's pull requests for one git-host integration, threading the opaque\n * next-cursor the provider returns (so it works for both page- and cursor-based hosts). Stops at\n * `maxPages` (marking `truncated`) or on a hard read failure (marking `fetchFailed`), keeping the\n * pages fetched so far. A soft warning (`{ value, error }`) is recorded but the drain continues.\n */\nexport async function drainPullRequests(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\trepos: ProviderReposInput,\n\tstate: PullRequestStateFilter[] | undefined,\n\tfilters: PullRequestFilter[] | undefined,\n\t/**\n\t * Breadth opt-ins that only the account-wide read can express. Grouped rather than passed positionally\n\t * because they are same-typed neighbors a transposition would silently swap, and picked from the sweep\n\t * options rather than restated so a third opt-in is declared once.\n\t */\n\taccountWideOptions: Pick<PullRequestSweepOptions, 'includeReviewRequested' | 'includeReviews'> | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n\tattributeUnavailableProvider: boolean,\n): Promise<{\n\titems: ProviderPullRequest[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n\tfailedProvider: boolean;\n}> {\n\tconst items: ProviderPullRequest[] = [];\n\tconst itemIndexByIdentity = new Map<string, number>();\n\tconst warnings: ProviderWarning[] = [];\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\t// SDK metadata failures across pages mean the collection is incomplete even when no page threw; carry\n\t// this through the terminal returns instead of resetting it to false at the last page.\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\t// A page the provider capped or couldn't vouch for. Decides the CAUSE the terminal warning reports: a cap\n\t// outranks a budget stop, because raising a budget cannot un-cap a page.\n\tlet providerTruncated = false;\n\t// The subset of that which no other warning already explains — decides whether this drain raises one of\n\t// its own on an otherwise clean exit.\n\tlet unexplainedTruncation = false;\n\n\t// With no repos this is an account-wide \"my PRs\" sweep. The repo-scoped core rejects an empty `repos`\n\t// input, so read the provider-native account-wide core instead.\n\tconst accountWide = repos.length === 0;\n\t// The breadth opt-ins are only expressible on the account-wide core, so gate them here — the single place\n\t// that already knows which core it is about to call.\n\tconst includeReviewRequested = accountWide && (accountWideOptions?.includeReviewRequested ?? false);\n\tconst includeReviews = accountWide && (accountWideOptions?.includeReviews ?? false);\n\t/** Every cursor already followed, so a provider that cycles them can't keep the drain walking in circles. */\n\tconst seenCursors = new Set<string>();\n\n\tfor (;;) {\n\t\tpage++;\n\t\t// Snapshot the mutable loop cursor so the read closure doesn't capture a later-reassigned value.\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\taccountWide\n\t\t\t\t? integration.getMyPullRequestsForUserResult(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstate: state,\n\t\t\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\t\t\tincludeReviewRequested: includeReviewRequested,\n\t\t\t\t\t\t\tfilters: filters,\n\t\t\t\t\t\t\t// Lite by default; the full projection (reviews) only when a caller opts in.\n\t\t\t\t\t\t\tsummary: !includeReviews,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t)\n\t\t\t\t: integration.getMyPullRequestsForReposResult(\n\t\t\t\t\t\trepos,\n\t\t\t\t\t\t{ state: state, filters: filters, cursor: pageCursor },\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t),\n\t\t);\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// An implicit sweep may silently skip a provider that has no session before it yields data.\n\t\t\t// Once a page has been returned, however, losing that session leaves an unread tail and must\n\t\t\t// be attributed even when the provider wasn't explicitly requested.\n\t\t\tconst sessionLostAfterProgress = warning == null && page > 1;\n\t\t\tconst unavailable = warning == null && (attributeUnavailableProvider || sessionLostAfterProgress);\n\t\t\tif (unavailable) {\n\t\t\t\tappendDedupedWarning(warnings, noConnectionWarning(id, domain, connectionId));\n\t\t\t}\n\t\t\t// `warning` set → a hard read failure (incomplete items); otherwise not connected / no session.\n\t\t\tconst failed = fetchFailed || warning != null || unavailable;\n\t\t\t// A cap seen on an earlier page still left results out, so it is still worth saying — but as part\n\t\t\t// of a read that failed, never as an omission. (`failed` is always true here when anything was\n\t\t\t// latched: reaching this exit past page 1 means a later page was lost.)\n\t\t\tif (unexplainedTruncation) {\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(id, domain, connectionId, 'Pull request', failed ? 'interrupted' : 'exhausted'),\n\t\t\t\t);\n\t\t\t}\n\t\t\t// An earlier page may already have emitted an omission before this one died; it asserts the read\n\t\t\t// succeeded, which is no longer true.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || sessionLostAfterProgress,\n\t\t\t\t// Only a top-level first-page rejection means the provider itself failed. A later-page or\n\t\t\t\t// per-scope failure still yielded a usable provider slice and stays represented separately.\n\t\t\t\tfailedProvider: page === 1 && (warning != null || unavailable),\n\t\t\t};\n\t\t}\n\n\t\t// Composite account-wide queries can surface the same PR on different relationship/state pages\n\t\t// (for example authored + review-requested, or closed + merged). Keep the first stable position but\n\t\t// replace its value with the latest representation so a later, richer merged state wins. Repository\n\t\t// identity is what keys a row (see `getProviderPullRequestIdentity`), falling back to the canonical\n\t\t// URL; a row with neither stays unkeyed and is retained so unrelated incomplete rows are never\n\t\t// collapsed.\n\t\tfor (const pullRequest of value.values) {\n\t\t\tconst identity = getProviderPullRequestIdentity(pullRequest);\n\t\t\tif (identity == null) {\n\t\t\t\titems.push(pullRequest);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst existingIndex = itemIndexByIdentity.get(identity);\n\t\t\tif (existingIndex == null) {\n\t\t\t\titemIndexByIdentity.set(identity, items.length);\n\t\t\t\titems.push(pullRequest);\n\t\t\t} else {\n\t\t\t\titems[existingIndex] = pullRequest;\n\t\t\t}\n\t\t}\n\n\t\t// Assess this page's SDK metadata: append scope-aware warnings (deduped across pages), and remember\n\t\t// whether a structured failure or incompleteness occurred.\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\tconst pageTruncated =\n\t\t\t(value as { truncated?: boolean }).truncated === true ||\n\t\t\tvalue.paging?.truncated === true ||\n\t\t\tassessment.truncated;\n\t\ttruncated = truncated || pageTruncated;\n\t\t// A page the provider itself capped, or one whose completeness it couldn't confirm. Latched from ANY\n\t\t// source, SDK metadata included: no budget of ours un-caps a page, so a later `maxPages` hit must not\n\t\t// claim raising it would help.\n\t\tproviderTruncated = providerTruncated || pageTruncated;\n\t\t// Whether this drain owes its OWN warning for that is a separate question — `mergeAssessmentInto` has\n\t\t// already appended one when the fact came from SDK metadata, and repeating it would be noise.\n\t\tunexplainedTruncation = unexplainedTruncation || (pageTruncated && !assessment.truncated);\n\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\t// A read that can't confirm completeness (single-page provider reads with no `hasNextPage`)\n\t\t\t// sets `paging.truncated`; propagate it (and any top-level `truncated` and SDK incompleteness)\n\t\t\t// so the sweep doesn't claim an all-pages result.\n\t\t\tif (unexplainedTruncation) {\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t\t'Pull request',\n\t\t\t\t\t\tfetchFailed ? 'interrupted' : 'exhausted',\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\t// Resolve the continuation BEFORE deciding why the drain stops. `page-budget` claims the missing items\n\t\t// are reachable, which is only true with a usable cursor in hand — and a provider can report another\n\t\t// page while handing back none (Bitbucket Server does, when it omits `nextPageStart`). Checking the\n\t\t// budget first would label that unreachable tail as merely unfetched, so raising `maxPages` would\n\t\t// return the identical set.\n\t\tconst nextCursor = value.paging?.cursor;\n\t\t// A cursor already used isn't a continuation either — following it refetches a page we have, and a\n\t\t// provider that cycles (A→B→A) would otherwise burn the whole budget and then be reported as merely\n\t\t// out of budget. Tracked as a SET rather than compared one-back, matching the SDK's own `followCursors`:\n\t\t// `drainToRequestedPage` and `collectProviderPagedResult` compare only the previous cursor, which a\n\t\t// cycle slips past. That is tolerable there and not here, because only this drain reports `page-budget`.\n\t\tconst continuable = nextCursor != null && nextCursor !== '{}' && !seenCursors.has(nextCursor);\n\t\tif (!continuable || page >= maxPages) {\n\t\t\t// `providerTruncated` outranks the budget: a page the provider capped stays capped however many\n\t\t\t// pages we are allowed to read, so promising `page-budget` on top of it would be a load-more that\n\t\t\t// cannot deliver the capped part.\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\ttruncationWarning(\n\t\t\t\t\tid,\n\t\t\t\t\tdomain,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\t'Pull request',\n\t\t\t\t\tfetchFailed ? 'interrupted' : continuable && !providerTruncated ? 'page-budget' : 'exhausted',\n\t\t\t\t),\n\t\t\t);\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: true,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\tseenCursors.add(nextCursor);\n\t\tcursor = nextCursor;\n\t}\n}\n\n/**\n * Drains every page of repositories under an org for one git-host integration, threading the opaque\n * next-cursor the provider returns. Stops at `maxPages` (marking `truncated`) or on a hard read failure\n * (marking `fetchFailed`), keeping the pages fetched so far.\n */\nexport async function drainRepositories(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\torg: string,\n\tproject: string | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n): Promise<{\n\trepos: ProviderRepository[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n}> {\n\tconst repos: ProviderRepository[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\n\tfor (;;) {\n\t\tpage++;\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(\n\t\t\tid,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t() =>\n\t\t\t\tintegration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\tproject: project,\n\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\tconnectionId: connectionId,\n\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: true },\n\t\t);\n\t\tif (warning != null) {\n\t\t\t// Deduped, like every other accumulation here: a soft warning (`{ value, error }`) repeats verbatim on\n\t\t\t// each page that hits the same condition, and the array's contract is that no two entries are equal.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\tconst interruptedAfterProgress = page > 1;\n\t\t\tif (interruptedAfterProgress && warning == null) {\n\t\t\t\t// Not a backstop: the read was cut short mid-drain, which is why it also sets `fetchFailed`\n\t\t\t\t// below. A retry may complete it, so this must not claim the succeeded-but-capped omission.\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(id, domain, connectionId, 'Repository', 'interrupted'),\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst failed = fetchFailed || warning != null || interruptedAfterProgress;\n\t\t\t// An SDK omission from an earlier page asserts the read succeeded; this one didn't.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || interruptedAfterProgress,\n\t\t\t};\n\t\t}\n\n\t\trepos.push(...value.values);\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\ttruncated = truncated || value.truncated === true || value.paging?.truncated === true || assessment.truncated;\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t};\n\t\t}\n\n\t\tif (page >= maxPages) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tconst nextCursor = value.paging?.cursor;\n\t\tif (nextCursor == null || nextCursor === '{}') {\n\t\t\t// Provider says there is more but didn't return a usable cursor; stop rather than refetch the same page.\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tcursor = nextCursor;\n\t}\n}\n\nexport function resolvePullRequestSweepTargets(options: PullRequestSweepOptions | undefined): {\n\ttargets: readonly ProviderSweepTarget[];\n\tattributeUnavailableProviders: boolean;\n} {\n\tif (options?.targets != null) {\n\t\tif (options.providerIds != null || options.connectionId != null) {\n\t\t\tthrow new TypeError(\"Pull request sweep 'targets' cannot be combined with 'providerIds' or 'connectionId'\");\n\t\t}\n\n\t\tconst seenProviderIds = new Set<IntegrationIds>();\n\t\tfor (const target of options.targets) {\n\t\t\tif (seenProviderIds.has(target.providerId)) {\n\t\t\t\tthrow new TypeError(\n\t\t\t\t\t`Pull request sweep targets must contain at most one target per provider; duplicate '${target.providerId}'`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tseenProviderIds.add(target.providerId);\n\t\t}\n\n\t\treturn { targets: options.targets, attributeUnavailableProviders: true };\n\t}\n\n\tconst providerIds =\n\t\toptions?.providerIds ?? supportedOrderedCloudIntegrationIds.filter(id => !isIssuesHostIntegrationId(id));\n\tconst connectionId = providerIds.length === 1 ? options?.connectionId : undefined;\n\treturn {\n\t\ttargets: providerIds.map(providerId => ({ providerId: providerId, connectionId: connectionId })),\n\t\tattributeUnavailableProviders: options?.providerIds != null,\n\t};\n}\n\nexport async function getCurrentAccountIdentity(\n\tintegration: GitHostIntegration,\n\tconnectionId: string | undefined,\n): Promise<{ id: string; username?: string } | undefined> {\n\ttry {\n\t\tconst account = await integration.getCurrentAccount({ connectionId: connectionId });\n\t\treturn account != null ? { id: account.id, username: account.username } : undefined;\n\t} catch {\n\t\t// Authorship is optional enrichment; don't turn a successful PR read into a failure if identity lookup fails.\n\t\treturn undefined;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"drains.js","names":[],"sources":["../../../../../plus/integrations/src/reads/drains.ts"],"sourcesContent":["import type { PullRequestStateFilter } from '@gitlens/git/models/pullRequest.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { supportedOrderedCloudIntegrationIds } from '../constants.js';\nimport type { ProviderSweepTarget, PullRequestSweepOptions } from '../manager.js';\nimport type { GitHostIntegration } from '../models/gitHostIntegration.js';\nimport type { IntegrationResult } from '../models/integration.js';\nimport type { PullRequestFilter } from '../providerFilters.js';\nimport type { ProviderPullRequest, ProviderReposInput, ProviderRepository } from '../providers/models.js';\nimport { getProviderPullRequestIdentity } from '../providers/models.js';\nimport type { ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, reconcileOmissionsWithFailure, toProviderWarning } from '../results.js';\nimport { isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport { cappedBesideBudgetWarning, noConnectionWarning, truncationWarning } from './warnings.js';\n\n/**\n * The all-pages reads: a provider read run repeatedly until it runs out of pages, as opposed to the single-page\n * reads the facade's paged surface exposes. `sweepPullRequests` and `broadenIssues` are built on these.\n *\n * A drain's job is to keep going where a single page would stop, so what distinguishes it is its failure\n * bookkeeping rather than its loop: it must return the prefix it did read plus enough signal for the caller to\n * know the result is not authoritative — `truncated` when pages remain unread (a page backstop, or a provider\n * claiming another page while handing back no usable cursor), `fetchFailed` when a page or scope failed\n * outright. Discarding the prefix on a late failure, or returning it as if complete, are the two mistakes these\n * exist to prevent.\n */\n\n/**\n * Runs a result-returning read and captures failure as a neutral {@link ProviderWarning} rather than\n * letting it throw or silently vanish. Handles both a returned `{ error }` (the read cores' contract)\n * and a hard throw; a soft warning (`{ value, error }`) yields the value *and* a warning.\n */\nexport async function runCaptured<T>(\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tfn: () => Promise<IntegrationResult<T>>,\n\toptions?: { warnOnMissingSession?: boolean },\n): Promise<{ value?: T; warning?: ProviderWarning }> {\n\ttry {\n\t\tconst result = await fn();\n\t\tif (result == null) {\n\t\t\t// The read core returns undefined only when it couldn't resolve a session. For a per-connection\n\t\t\t// or explicit-domain read that means the requested target is gone or its authentication is invalid,\n\t\t\t// which must not be reported as an empty account. The untargeted primary path legitimately yields\n\t\t\t// nothing when the provider isn't connected, so leave it as an empty result.\n\t\t\treturn connectionId != null || options?.warnOnMissingSession\n\t\t\t\t? { warning: noConnectionWarning(id, domain, connectionId) }\n\t\t\t\t: {};\n\t\t}\n\t\tif (result.error != null) {\n\t\t\treturn { value: result.value, warning: toProviderWarning(id, domain, connectionId, result.error) };\n\t\t}\n\t\treturn { value: result.value };\n\t} catch (ex) {\n\t\treturn { warning: toProviderWarning(id, domain, connectionId, ex) };\n\t}\n}\n\n/**\n * Drains every page of the user's pull requests for one git-host integration, threading the opaque\n * next-cursor the provider returns (so it works for both page- and cursor-based hosts). Stops at\n * `maxPages` (marking `truncated`) or on a hard read failure (marking `fetchFailed`), keeping the\n * pages fetched so far. A soft warning (`{ value, error }`) is recorded but the drain continues.\n */\nexport async function drainPullRequests(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\trepos: ProviderReposInput,\n\tstate: PullRequestStateFilter[] | undefined,\n\tfilters: PullRequestFilter[] | undefined,\n\t/**\n\t * Breadth opt-ins that only the account-wide read can express. Grouped rather than passed positionally\n\t * because they are same-typed neighbors a transposition would silently swap, and picked from the sweep\n\t * options rather than restated so a third opt-in is declared once.\n\t */\n\taccountWideOptions: Pick<PullRequestSweepOptions, 'includeReviewRequested' | 'includeReviews'> | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n\tattributeUnavailableProvider: boolean,\n): Promise<{\n\titems: ProviderPullRequest[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n\tfailedProvider: boolean;\n}> {\n\tconst items: ProviderPullRequest[] = [];\n\tconst itemIndexByIdentity = new Map<string, number>();\n\tconst warnings: ProviderWarning[] = [];\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\t// SDK metadata failures across pages mean the collection is incomplete even when no page threw; carry\n\t// this through the terminal returns instead of resetting it to false at the last page.\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\t// A page the provider capped or couldn't vouch for, and which no other warning already explains — decides\n\t// whether this drain raises one of its own for that part.\n\tlet unexplainedTruncation = false;\n\n\t// With no repos this is an account-wide \"my PRs\" sweep. The repo-scoped core rejects an empty `repos`\n\t// input, so read the provider-native account-wide core instead.\n\tconst accountWide = repos.length === 0;\n\t// The breadth opt-ins are only expressible on the account-wide core, so gate them here — the single place\n\t// that already knows which core it is about to call.\n\tconst includeReviewRequested = accountWide && (accountWideOptions?.includeReviewRequested ?? false);\n\tconst includeReviews = accountWide && (accountWideOptions?.includeReviews ?? false);\n\t/** Every cursor already followed, so a provider that cycles them can't keep the drain walking in circles. */\n\tconst seenCursors = new Set<string>();\n\n\tfor (;;) {\n\t\tpage++;\n\t\t// Snapshot the mutable loop cursor so the read closure doesn't capture a later-reassigned value.\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\taccountWide\n\t\t\t\t? integration.getMyPullRequestsForUserResult(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstate: state,\n\t\t\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\t\t\tincludeReviewRequested: includeReviewRequested,\n\t\t\t\t\t\t\tfilters: filters,\n\t\t\t\t\t\t\t// Lite by default; the full projection (reviews) only when a caller opts in.\n\t\t\t\t\t\t\tsummary: !includeReviews,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t)\n\t\t\t\t: integration.getMyPullRequestsForReposResult(\n\t\t\t\t\t\trepos,\n\t\t\t\t\t\t{ state: state, filters: filters, cursor: pageCursor },\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t),\n\t\t);\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// An implicit sweep may silently skip a provider that has no session before it yields data.\n\t\t\t// Once a page has been returned, however, losing that session leaves an unread tail and must\n\t\t\t// be attributed even when the provider wasn't explicitly requested.\n\t\t\tconst sessionLostAfterProgress = warning == null && page > 1;\n\t\t\tconst unavailable = warning == null && (attributeUnavailableProvider || sessionLostAfterProgress);\n\t\t\tif (unavailable) {\n\t\t\t\tappendDedupedWarning(warnings, noConnectionWarning(id, domain, connectionId));\n\t\t\t}\n\t\t\t// `warning` set → a hard read failure (incomplete items); otherwise not connected / no session.\n\t\t\tconst failed = fetchFailed || warning != null || unavailable;\n\t\t\t// A cap seen on an earlier page still left results out, so it is still worth saying — but as part\n\t\t\t// of a read that failed, never as an omission. (`failed` is always true here when anything was\n\t\t\t// latched: reaching this exit past page 1 means a later page was lost.)\n\t\t\tif (unexplainedTruncation) {\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(id, domain, connectionId, 'Pull request', failed ? 'interrupted' : 'exhausted'),\n\t\t\t\t);\n\t\t\t}\n\t\t\t// An earlier page may already have emitted an omission before this one died; it asserts the read\n\t\t\t// succeeded, which is no longer true.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || sessionLostAfterProgress,\n\t\t\t\t// Only a top-level first-page rejection means the provider itself failed. A later-page or\n\t\t\t\t// per-scope failure still yielded a usable provider slice and stays represented separately.\n\t\t\t\tfailedProvider: page === 1 && (warning != null || unavailable),\n\t\t\t};\n\t\t}\n\n\t\t// Composite account-wide queries can surface the same PR on different relationship/state pages\n\t\t// (for example authored + review-requested, or closed + merged). Keep the first stable position but\n\t\t// replace its value with the latest representation so a later, richer merged state wins. Repository\n\t\t// identity is what keys a row (see `getProviderPullRequestIdentity`), falling back to the canonical\n\t\t// URL; a row with neither stays unkeyed and is retained so unrelated incomplete rows are never\n\t\t// collapsed.\n\t\tfor (const pullRequest of value.values) {\n\t\t\tconst identity = getProviderPullRequestIdentity(pullRequest);\n\t\t\tif (identity == null) {\n\t\t\t\titems.push(pullRequest);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst existingIndex = itemIndexByIdentity.get(identity);\n\t\t\tif (existingIndex == null) {\n\t\t\t\titemIndexByIdentity.set(identity, items.length);\n\t\t\t\titems.push(pullRequest);\n\t\t\t} else {\n\t\t\t\titems[existingIndex] = pullRequest;\n\t\t\t}\n\t\t}\n\n\t\t// Assess this page's SDK metadata: append scope-aware warnings (deduped across pages), and remember\n\t\t// whether a structured failure or incompleteness occurred.\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\tconst pageTruncated =\n\t\t\t(value as { truncated?: boolean }).truncated === true ||\n\t\t\tvalue.paging?.truncated === true ||\n\t\t\tassessment.truncated;\n\t\ttruncated = truncated || pageTruncated;\n\t\t// Whether this drain owes its OWN warning for a capped page — `mergeAssessmentInto` has already appended\n\t\t// one when the fact came from SDK metadata, and repeating it would be noise.\n\t\tunexplainedTruncation = unexplainedTruncation || (pageTruncated && !assessment.truncated);\n\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\t// A read that can't confirm completeness (single-page provider reads with no `hasNextPage`)\n\t\t\t// sets `paging.truncated`; propagate it (and any top-level `truncated` and SDK incompleteness)\n\t\t\t// so the sweep doesn't claim an all-pages result.\n\t\t\tif (unexplainedTruncation) {\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t\t'Pull request',\n\t\t\t\t\t\tfetchFailed ? 'interrupted' : 'exhausted',\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\t// Resolve the continuation BEFORE deciding why the drain stops. `page-budget` claims the missing items\n\t\t// are reachable, which is only true with a usable cursor in hand — and a provider can report another\n\t\t// page while handing back none (Bitbucket Server does, when it omits `nextPageStart`). Checking the\n\t\t// budget first would label that unreachable tail as merely unfetched, so raising `maxPages` would\n\t\t// return the identical set.\n\t\tconst nextCursor = value.paging?.cursor;\n\t\t// A cursor already used isn't a continuation either — following it refetches a page we have, and a\n\t\t// provider that cycles (A→B→A) would otherwise burn the whole budget and then be reported as merely\n\t\t// out of budget. Tracked as a SET rather than compared one-back, matching the SDK's own `followCursors`:\n\t\t// `drainToRequestedPage` and `collectProviderPagedResult` compare only the previous cursor, which a\n\t\t// cycle slips past. That is tolerable there and not here, because only this drain reports `page-budget`.\n\t\tconst continuable = nextCursor != null && nextCursor !== '{}' && !seenCursors.has(nextCursor);\n\t\tif (!continuable || page >= maxPages) {\n\t\t\t// A usable cursor in hand means `page-budget`, even when an earlier page was capped: a composite\n\t\t\t// read caps its facets independently, so one facet reaching the provider's ceiling leaves its\n\t\t\t// siblings' cursors live, and raising the budget returns more of them. What no budget returns is the\n\t\t\t// capped part, so that is reported BESIDE the budget stop rather than in place of it — unless an SDK\n\t\t\t// omission already says so. Neither warning then misstates the part the other describes.\n\t\t\tconst budgetStop = !fetchFailed && continuable;\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\ttruncationWarning(\n\t\t\t\t\tid,\n\t\t\t\t\tdomain,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\t'Pull request',\n\t\t\t\t\tfetchFailed ? 'interrupted' : budgetStop ? 'page-budget' : 'exhausted',\n\t\t\t\t),\n\t\t\t);\n\t\t\tif (budgetStop && unexplainedTruncation) {\n\t\t\t\tappendDedupedWarning(warnings, cappedBesideBudgetWarning(id, domain, connectionId, 'Pull request'));\n\t\t\t}\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: true,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\tseenCursors.add(nextCursor);\n\t\tcursor = nextCursor;\n\t}\n}\n\n/**\n * Drains every page of repositories under an org for one git-host integration, threading the opaque\n * next-cursor the provider returns. Stops at `maxPages` (marking `truncated`) or on a hard read failure\n * (marking `fetchFailed`), keeping the pages fetched so far.\n */\nexport async function drainRepositories(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\torg: string,\n\tproject: string | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n): Promise<{\n\trepos: ProviderRepository[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n}> {\n\tconst repos: ProviderRepository[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\n\tfor (;;) {\n\t\tpage++;\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(\n\t\t\tid,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t() =>\n\t\t\t\tintegration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\tproject: project,\n\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\tconnectionId: connectionId,\n\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: true },\n\t\t);\n\t\tif (warning != null) {\n\t\t\t// Deduped, like every other accumulation here: a soft warning (`{ value, error }`) repeats verbatim on\n\t\t\t// each page that hits the same condition, and the array's contract is that no two entries are equal.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\tconst interruptedAfterProgress = page > 1;\n\t\t\tif (interruptedAfterProgress && warning == null) {\n\t\t\t\t// Not a backstop: the read was cut short mid-drain, which is why it also sets `fetchFailed`\n\t\t\t\t// below. A retry may complete it, so this must not claim the succeeded-but-capped omission.\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(id, domain, connectionId, 'Repository', 'interrupted'),\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst failed = fetchFailed || warning != null || interruptedAfterProgress;\n\t\t\t// An SDK omission from an earlier page asserts the read succeeded; this one didn't.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || interruptedAfterProgress,\n\t\t\t};\n\t\t}\n\n\t\trepos.push(...value.values);\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\ttruncated = truncated || value.truncated === true || value.paging?.truncated === true || assessment.truncated;\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t};\n\t\t}\n\n\t\tif (page >= maxPages) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tconst nextCursor = value.paging?.cursor;\n\t\tif (nextCursor == null || nextCursor === '{}') {\n\t\t\t// Provider says there is more but didn't return a usable cursor; stop rather than refetch the same page.\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tcursor = nextCursor;\n\t}\n}\n\nexport function resolvePullRequestSweepTargets(options: PullRequestSweepOptions | undefined): {\n\ttargets: readonly ProviderSweepTarget[];\n\tattributeUnavailableProviders: boolean;\n} {\n\tif (options?.targets != null) {\n\t\tif (options.providerIds != null || options.connectionId != null) {\n\t\t\tthrow new TypeError(\"Pull request sweep 'targets' cannot be combined with 'providerIds' or 'connectionId'\");\n\t\t}\n\n\t\tconst seenProviderIds = new Set<IntegrationIds>();\n\t\tfor (const target of options.targets) {\n\t\t\tif (seenProviderIds.has(target.providerId)) {\n\t\t\t\tthrow new TypeError(\n\t\t\t\t\t`Pull request sweep targets must contain at most one target per provider; duplicate '${target.providerId}'`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tseenProviderIds.add(target.providerId);\n\t\t}\n\n\t\treturn { targets: options.targets, attributeUnavailableProviders: true };\n\t}\n\n\tconst providerIds =\n\t\toptions?.providerIds ?? supportedOrderedCloudIntegrationIds.filter(id => !isIssuesHostIntegrationId(id));\n\tconst connectionId = providerIds.length === 1 ? options?.connectionId : undefined;\n\treturn {\n\t\ttargets: providerIds.map(providerId => ({ providerId: providerId, connectionId: connectionId })),\n\t\tattributeUnavailableProviders: options?.providerIds != null,\n\t};\n}\n\nexport async function getCurrentAccountIdentity(\n\tintegration: GitHostIntegration,\n\tconnectionId: string | undefined,\n): Promise<{ id: string; username?: string } | undefined> {\n\ttry {\n\t\tconst account = await integration.getCurrentAccount({ connectionId: connectionId });\n\t\treturn account != null ? { id: account.id, username: account.username } : undefined;\n\t} catch {\n\t\t// Authorship is optional enrichment; don't turn a successful PR read into a failure if identity lookup fails.\n\t\treturn undefined;\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -220,6 +220,14 @@ export type SupportedFilters = {
|
|
|
220
220
|
* `issueSorts`.
|
|
221
221
|
*/
|
|
222
222
|
issueSortsAccountWide: IssueSorting[];
|
|
223
|
+
/**
|
|
224
|
+
* Whether an issue tracker's reads can be asked for a `state` other than open (`closed`, `all`). False means
|
|
225
|
+
* only open issues are readable, and the read refuses the other two rather than serving the open list.
|
|
226
|
+
*
|
|
227
|
+
* Tracker-only, like `issues`: the git hosts' state support belongs to the filtered search, under
|
|
228
|
+
* `issueSearch.states`.
|
|
229
|
+
*/
|
|
230
|
+
issueStates: boolean;
|
|
223
231
|
};
|
|
224
232
|
/**
|
|
225
233
|
* The filters `listPullRequestsPage`/`listIssuesPage` (and the sweeps) accept for a provider, so a caller can
|
|
@@ -357,7 +357,8 @@ function getSupportedFilters(providerId) {
|
|
|
357
357
|
sorts: [...issueSearch?.sorts ?? []]
|
|
358
358
|
},
|
|
359
359
|
issueSorts: [...metadata?.supportedIssueSorts ?? []],
|
|
360
|
-
issueSortsAccountWide: [...metadata?.supportedAccountWideIssueSorts ?? []]
|
|
360
|
+
issueSortsAccountWide: [...metadata?.supportedAccountWideIssueSorts ?? []],
|
|
361
|
+
issueStates: metadata?.supportsIssueStates === true
|
|
361
362
|
};
|
|
362
363
|
}
|
|
363
364
|
//#endregion
|