@gitkraken/core-gitlens 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +7 -0
- package/dist/plus/integrations/collectionMetadata.js +10 -1
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/errors.d.ts +5 -0
- package/dist/plus/integrations/errors.js +10 -1
- package/dist/plus/integrations/errors.js.map +1 -1
- package/dist/plus/integrations/index.d.ts +2 -2
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +6 -2
- package/dist/plus/integrations/models/gitHostIntegration.js +37 -7
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/issueReads.d.ts +1 -1
- package/dist/plus/integrations/models/issuesIntegration.d.ts +33 -3
- package/dist/plus/integrations/models/issuesIntegration.js +76 -5
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +9 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.js +17 -9
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.js +3 -2
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira-server.d.ts +3 -0
- package/dist/plus/integrations/providers/jira-server.js +15 -7
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +8 -0
- package/dist/plus/integrations/providers/jira.js +88 -58
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.d.ts +4 -0
- package/dist/plus/integrations/providers/linear.js +23 -2
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/providerErrors.d.ts +24 -0
- package/dist/plus/integrations/providers/providerErrors.js +41 -4
- package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +1 -1
- package/dist/plus/integrations/providers/providersApi.js +15 -4
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/trello.d.ts +1 -0
- package/dist/plus/integrations/providers/trello.js +3 -0
- package/dist/plus/integrations/providers/trello.js.map +1 -1
- package/dist/plus/integrations/reads/cursors.js +23 -5
- package/dist/plus/integrations/reads/cursors.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +16 -0
- package/dist/plus/integrations/reads/drains.js +19 -1
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.js +5 -3
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.utils.d.ts +3 -2
- package/dist/plus/integrations/reads/hierarchy.utils.js +4 -7
- package/dist/plus/integrations/reads/hierarchy.utils.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +71 -7
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/selectionConfirmation.d.ts +69 -0
- package/dist/plus/integrations/reads/selectionConfirmation.js +79 -0
- package/dist/plus/integrations/reads/selectionConfirmation.js.map +1 -0
- package/dist/plus/integrations/reads/warnings.d.ts +9 -3
- package/dist/plus/integrations/reads/warnings.js +19 -3
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +30 -3
- package/dist/plus/integrations/results.js +2 -1
- package/dist/plus/integrations/results.js.map +1 -1
- package/docs/integrations.md +59 -6
- package/package.json +1 -1
|
@@ -28,6 +28,7 @@ export declare class TrelloIntegration extends IssuesIntegration<IssuesCloudHost
|
|
|
28
28
|
* account-wide, so the answer is lent briefly to the per-board lookup a tracker read makes right after it.
|
|
29
29
|
*/
|
|
30
30
|
protected getProviderCurrentAccount(session: ProviderAuthenticationSession): Promise<Account | undefined>;
|
|
31
|
+
protected getProviderUncachedAccountForResource(session: ProviderAuthenticationSession, _resource: ResourceDescriptor): Promise<Account | undefined>;
|
|
31
32
|
protected getProviderAccountForResource(session: ProviderAuthenticationSession, _resource: ResourceDescriptor): Promise<Account | undefined>;
|
|
32
33
|
invalidateDiscoveryCaches(): void;
|
|
33
34
|
private readonly _recentAccounts;
|
|
@@ -49,6 +49,9 @@ var TrelloIntegration = class extends IssuesIntegration {
|
|
|
49
49
|
if (account != null) this._recentAccounts.set(session.accessToken, Promise.resolve(account));
|
|
50
50
|
return account;
|
|
51
51
|
}
|
|
52
|
+
getProviderUncachedAccountForResource(session, _resource) {
|
|
53
|
+
return this.fetchAccount(session, this.requireAppKey(session));
|
|
54
|
+
}
|
|
52
55
|
async getProviderAccountForResource(session, _resource) {
|
|
53
56
|
const appKey = this.requireAppKey(session);
|
|
54
57
|
return this._recentAccounts.getOrCreate(session.accessToken, () => this.fetchAccount(session, appKey), { evictWhen: (account) => account == null });
|
|
@@ -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 { PromiseCache } from '@gitlens/utils/promiseCache.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { 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/** Long enough to span one tracker read, short enough that a profile change shows on the next. */\nconst recentAccountTTL = 60 * 1000;\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\t/**\n\t * Always asks Trello: the host's current-account cache decides when this runs. The member lookup is\n\t * account-wide, so the answer is lent briefly to the per-board lookup a tracker read makes right after it.\n\t */\n\tprotected override async getProviderCurrentAccount(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<Account | undefined> {\n\t\tconst account = await this.fetchAccount(session);\n\t\tif (account != null) {\n\t\t\tthis._recentAccounts.set(session.accessToken, Promise.resolve(account));\n\t\t}\n\t\treturn account;\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\t\treturn this._recentAccounts.getOrCreate(session.accessToken, () => this.fetchAccount(session, appKey), {\n\t\t\tevictWhen: account => account == null,\n\t\t});\n\t}\n\n\toverride invalidateDiscoveryCaches(): void {\n\t\tthis._recentAccounts.clear();\n\t}\n\n\tprivate readonly _recentAccounts = new PromiseCache<string, Account | undefined>({\n\t\tcapacity: 10,\n\t\tcreateTTL: recentAccountTTL,\n\t});\n\n\tprivate async fetchAccount(\n\t\tsession: ProviderAuthenticationSession,\n\t\tappKey: string = this.requireAppKey(session),\n\t): Promise<Account | undefined> {\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, { projection: 'project' });\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\tprojection: 'point',\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 { PromiseCache } from '@gitlens/utils/promiseCache.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { 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/** Long enough to span one tracker read, short enough that a profile change shows on the next. */\nconst recentAccountTTL = 60 * 1000;\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\t/**\n\t * Always asks Trello: the host's current-account cache decides when this runs. The member lookup is\n\t * account-wide, so the answer is lent briefly to the per-board lookup a tracker read makes right after it.\n\t */\n\tprotected override async getProviderCurrentAccount(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<Account | undefined> {\n\t\tconst account = await this.fetchAccount(session);\n\t\tif (account != null) {\n\t\t\tthis._recentAccounts.set(session.accessToken, Promise.resolve(account));\n\t\t}\n\t\treturn account;\n\t}\n\n\tprotected override getProviderUncachedAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\treturn this.fetchAccount(session, this.requireAppKey(session));\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\t\treturn this._recentAccounts.getOrCreate(session.accessToken, () => this.fetchAccount(session, appKey), {\n\t\t\tevictWhen: account => account == null,\n\t\t});\n\t}\n\n\toverride invalidateDiscoveryCaches(): void {\n\t\tthis._recentAccounts.clear();\n\t}\n\n\tprivate readonly _recentAccounts = new PromiseCache<string, Account | undefined>({\n\t\tcapacity: 10,\n\t\tcreateTTL: recentAccountTTL,\n\t});\n\n\tprivate async fetchAccount(\n\t\tsession: ProviderAuthenticationSession,\n\t\tappKey: string = this.requireAppKey(session),\n\t): Promise<Account | undefined> {\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, { projection: 'project' });\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\tprojection: 'point',\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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"}
|
|
@@ -8,20 +8,38 @@ function positiveIntegers(values) {
|
|
|
8
8
|
const valid = values.filter((value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0);
|
|
9
9
|
return valid.length > 0 ? [...new Set(valid)] : void 0;
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Non-empty project keys only, upgraded then deduped, so a legacy and a current key for one project merge;
|
|
13
|
+
* `undefined` when nothing survives (so the field is omitted).
|
|
14
|
+
*/
|
|
15
|
+
function projectKeys(values) {
|
|
13
16
|
if (!Array.isArray(values)) return void 0;
|
|
14
|
-
const valid = values.filter((value) => typeof value === "string" && value.length > 0);
|
|
17
|
+
const valid = values.filter((value) => typeof value === "string" && value.length > 0).map(upgradeProjectKey);
|
|
15
18
|
return valid.length > 0 ? [...new Set(valid)] : void 0;
|
|
16
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Reads a `projectKey` (`[resourceId, id || key]`) from a cursor, converting one minted when it also carried the
|
|
22
|
+
* project key (`[resourceId, id, key]`) so that cursor still resumes its retrying and completed projects.
|
|
23
|
+
*/
|
|
24
|
+
function upgradeProjectKey(key) {
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(key);
|
|
28
|
+
} catch {
|
|
29
|
+
return key;
|
|
30
|
+
}
|
|
31
|
+
if (!Array.isArray(parsed) || parsed.length !== 3 || !parsed.every((part) => typeof part === "string")) return key;
|
|
32
|
+
const [resourceId, id, legacyKey] = parsed;
|
|
33
|
+
return JSON.stringify([resourceId, id || legacyKey]);
|
|
34
|
+
}
|
|
17
35
|
function parseIssueTrackerPageCursor(cursor) {
|
|
18
36
|
if (cursor == null) return void 0;
|
|
19
37
|
try {
|
|
20
38
|
const parsed = JSON.parse(cursor);
|
|
21
39
|
if (parsed.type !== "issue-tracker-page" || typeof parsed.currentPage !== "number" || !Number.isSafeInteger(parsed.currentPage) || parsed.currentPage < 1) return;
|
|
22
40
|
const retryPages = positiveIntegers(parsed.retryPages);
|
|
23
|
-
const retryProjects =
|
|
24
|
-
const completedProjects =
|
|
41
|
+
const retryProjects = projectKeys(parsed.retryProjects);
|
|
42
|
+
const completedProjects = projectKeys(parsed.completedProjects);
|
|
25
43
|
return {
|
|
26
44
|
type: "issue-tracker-page",
|
|
27
45
|
currentPage: parsed.currentPage,
|
|
@@ -1 +1 @@
|
|
|
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
|
+
{"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/**\n * Non-empty project keys only, upgraded then deduped, so a legacy and a current key for one project merge;\n * `undefined` when nothing survives (so the field is omitted).\n */\nfunction projectKeys(values: unknown): string[] | undefined {\n\tif (!Array.isArray(values)) return undefined;\n\n\tconst valid = values\n\t\t.filter((value): value is string => typeof value === 'string' && value.length > 0)\n\t\t.map(upgradeProjectKey);\n\treturn valid.length > 0 ? [...new Set(valid)] : undefined;\n}\n\n/**\n * Reads a `projectKey` (`[resourceId, id || key]`) from a cursor, converting one minted when it also carried the\n * project key (`[resourceId, id, key]`) so that cursor still resumes its retrying and completed projects.\n */\nfunction upgradeProjectKey(key: string): string {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(key);\n\t} catch {\n\t\treturn key;\n\t}\n\tif (!Array.isArray(parsed) || parsed.length !== 3 || !parsed.every(part => typeof part === 'string')) return key;\n\n\tconst [resourceId, id, legacyKey] = parsed as [string, string, string];\n\treturn JSON.stringify([resourceId, id || legacyKey]);\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 = projectKeys(parsed.retryProjects);\n\t\tconst completedProjects = projectKeys(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,10 +1,13 @@
|
|
|
1
|
+
import { ResourceDescriptor } from "../../../git/models/resourceDescriptor.js";
|
|
1
2
|
import { PullRequest, PullRequestFilter, PullRequestStateFilter } from "../../../git/models/pullRequest.js";
|
|
2
3
|
import { IntegrationIds } from "../constants.js";
|
|
3
4
|
import { ProviderWarning } from "../results.js";
|
|
4
5
|
import { ProviderPullRequest, ProviderReposInput, ProviderRepository } from "../providers/models.js";
|
|
5
6
|
import { GitHostIntegration } from "../models/gitHostIntegration.js";
|
|
7
|
+
import { IssuesIntegration } from "../models/issuesIntegration.js";
|
|
6
8
|
import { IntegrationResult } from "../models/integration.js";
|
|
7
9
|
import { ProviderSweepTarget, PullRequestSweepOptions } from "../manager.js";
|
|
10
|
+
import { CollectionMetadata } from "@gitkraken/provider-apis";
|
|
8
11
|
//#region ../plus/integrations/src/reads/drains.d.ts
|
|
9
12
|
/**
|
|
10
13
|
* The all-pages reads: a provider read run repeatedly until it runs out of pages, as opposed to the single-page
|
|
@@ -28,6 +31,19 @@ export declare function runCaptured<T>(id: IntegrationIds, domain: string | unde
|
|
|
28
31
|
value?: T;
|
|
29
32
|
warning?: ProviderWarning;
|
|
30
33
|
}>;
|
|
34
|
+
/**
|
|
35
|
+
* {@link runCaptured} over an issue tracker's resource (site, organization, board) discovery. A read that resolved a
|
|
36
|
+
* session yet answered with no data and no error carries `completeness: 'unknown'` metadata, so it reads as an
|
|
37
|
+
* incomplete discovery rather than as an account with no resources, which `runCaptured` alone can't tell apart from
|
|
38
|
+
* an untargeted read with no session.
|
|
39
|
+
*/
|
|
40
|
+
export declare function discoverResources(integration: IssuesIntegration, id: IntegrationIds, domain: string | undefined, connectionId: string | undefined, options?: {
|
|
41
|
+
warnOnMissingSession?: boolean;
|
|
42
|
+
}): Promise<{
|
|
43
|
+
value?: ResourceDescriptor[];
|
|
44
|
+
warning?: ProviderWarning;
|
|
45
|
+
metadata?: CollectionMetadata;
|
|
46
|
+
}>;
|
|
31
47
|
/**
|
|
32
48
|
* Drains every page of the user's pull requests for one git-host integration, threading the opaque
|
|
33
49
|
* next-cursor the provider returns (so it works for both page- and cursor-based hosts). Stops at
|
|
@@ -36,6 +36,24 @@ async function runCaptured(id, domain, connectionId, fn, options) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
+
* {@link runCaptured} over an issue tracker's resource (site, organization, board) discovery. A read that resolved a
|
|
40
|
+
* session yet answered with no data and no error carries `completeness: 'unknown'` metadata, so it reads as an
|
|
41
|
+
* incomplete discovery rather than as an account with no resources, which `runCaptured` alone can't tell apart from
|
|
42
|
+
* an untargeted read with no session.
|
|
43
|
+
*/
|
|
44
|
+
async function discoverResources(integration, id, domain, connectionId, options) {
|
|
45
|
+
let noData = false;
|
|
46
|
+
const captured = await runCaptured(id, domain, connectionId, async () => {
|
|
47
|
+
const result = await integration.getResourcesForUserResult(connectionId);
|
|
48
|
+
noData = result != null && result.error == null && result.value == null;
|
|
49
|
+
return result;
|
|
50
|
+
}, options);
|
|
51
|
+
return noData ? {
|
|
52
|
+
...captured,
|
|
53
|
+
metadata: { completeness: "unknown" }
|
|
54
|
+
} : captured;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
39
57
|
* Drains every page of the user's pull requests for one git-host integration, threading the opaque
|
|
40
58
|
* next-cursor the provider returns (so it works for both page- and cursor-based hosts). Stops at
|
|
41
59
|
* `maxPages` (marking `truncated`) or on a hard read failure (marking `fetchFailed`), keeping the
|
|
@@ -258,6 +276,6 @@ async function getPullRequestViewers(integration, connectionId, pullRequests) {
|
|
|
258
276
|
}
|
|
259
277
|
}
|
|
260
278
|
//#endregion
|
|
261
|
-
export { drainPullRequests, drainRepositories, getCurrentAccountIdentity, getPullRequestViewers, resolvePullRequestSweepTargets, runCaptured };
|
|
279
|
+
export { discoverResources, drainPullRequests, drainRepositories, getCurrentAccountIdentity, getPullRequestViewers, resolvePullRequestSweepTargets, runCaptured };
|
|
262
280
|
|
|
263
281
|
//# sourceMappingURL=drains.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drains.js","names":[],"sources":["../../../../../plus/integrations/src/reads/drains.ts"],"sourcesContent":["import type { PullRequest, PullRequestStateFilter } from '@gitlens/git/models/pullRequest.js';\nimport { defaultPullRequestSort } 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 {\n\tcappedBesideBudgetWarning,\n\tnoConnectionWarning,\n\tpullRequestSearchCapResultWarning,\n\ttruncationWarning,\n} 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 | PullRequest)[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n\tfailedProvider: boolean;\n}> {\n\tconst items: (ProviderPullRequest | PullRequest)[] = [];\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\t// The largest match count reported on a truncated page, from which the drain derives the provider's cap.\n\tlet truncatedTotalCount: number | undefined;\n\tconst capWarning = (): ProviderWarning | undefined =>\n\t\tpullRequestSearchCapResultWarning(id, domain, connectionId, truncatedTotalCount, defaultPullRequestSort);\n\tconst appendTruncationWarnings = (cause: 'interrupted' | 'exhausted'): void => {\n\t\tconst cap = capWarning();\n\t\tif (cap != null) {\n\t\t\tappendDedupedWarning(warnings, cap);\n\t\t}\n\t\t// A cap explains the missing tail of a read that succeeded; a failed read still owes its own warning.\n\t\tif (unexplainedTruncation && (cap == null || cause === 'interrupted')) {\n\t\t\tappendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, 'Pull request', cause));\n\t\t}\n\t};\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\tappendTruncationWarnings(failed ? 'interrupted' : 'exhausted');\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\t\t// Only the account-wide read reports a search match count here; the repo-scoped one reports the item\n\t\t// total, which no cap is derived from. Latched even when SDK metadata explains the page: a composite page\n\t\t// reports a failed or cursorless sibling facet there, which says nothing about the cap another reached.\n\t\tif (accountWide && pageTruncated && value.paging?.totalCount != null) {\n\t\t\ttruncatedTotalCount = Math.max(truncatedTotalCount ?? 0, value.paging.totalCount);\n\t\t}\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\tappendTruncationWarnings(fetchFailed ? 'interrupted' : 'exhausted');\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\tconst cap = capWarning();\n\t\t\tif (cap != null) {\n\t\t\t\tappendDedupedWarning(warnings, cap);\n\t\t\t} else if (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\n/**\n * The current account to map each raw row against, in order (see {@link GitHostIntegration.getPullRequestViewers}).\n * Rows with no author can't be matched, so a read of only those resolves nothing; a failed lookup leaves every row's\n * authorship unknown rather than failing the read.\n */\nexport async function getPullRequestViewers(\n\tintegration: GitHostIntegration,\n\tconnectionId: string | undefined,\n\tpullRequests: readonly (ProviderPullRequest | PullRequest)[],\n): Promise<({ id: string; username?: string } | undefined)[]> {\n\tif (!pullRequests.some(pr => pr.author != null)) return pullRequests.map(() => undefined);\n\n\ttry {\n\t\treturn await integration.getPullRequestViewers(pullRequests, connectionId);\n\t} catch {\n\t\treturn pullRequests.map(() => 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 { CollectionMetadata } from '@gitkraken/provider-apis';\nimport type { PullRequest, PullRequestStateFilter } from '@gitlens/git/models/pullRequest.js';\nimport { defaultPullRequestSort } from '@gitlens/git/models/pullRequest.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.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 { IssuesIntegration } from '../models/issuesIntegration.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 {\n\tcappedBesideBudgetWarning,\n\tnoConnectionWarning,\n\tpullRequestSearchCapResultWarning,\n\ttruncationWarning,\n} 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 * {@link runCaptured} over an issue tracker's resource (site, organization, board) discovery. A read that resolved a\n * session yet answered with no data and no error carries `completeness: 'unknown'` metadata, so it reads as an\n * incomplete discovery rather than as an account with no resources, which `runCaptured` alone can't tell apart from\n * an untargeted read with no session.\n */\nexport async function discoverResources(\n\tintegration: IssuesIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\toptions?: { warnOnMissingSession?: boolean },\n): Promise<{ value?: ResourceDescriptor[]; warning?: ProviderWarning; metadata?: CollectionMetadata }> {\n\tlet noData = false;\n\tconst captured = await runCaptured(\n\t\tid,\n\t\tdomain,\n\t\tconnectionId,\n\t\tasync () => {\n\t\t\tconst result = await integration.getResourcesForUserResult(connectionId);\n\t\t\tnoData = result != null && result.error == null && result.value == null;\n\t\t\treturn result;\n\t\t},\n\t\toptions,\n\t);\n\treturn noData ? { ...captured, metadata: { completeness: 'unknown' } } : captured;\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 | PullRequest)[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n\tfailedProvider: boolean;\n}> {\n\tconst items: (ProviderPullRequest | PullRequest)[] = [];\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\t// The largest match count reported on a truncated page, from which the drain derives the provider's cap.\n\tlet truncatedTotalCount: number | undefined;\n\tconst capWarning = (): ProviderWarning | undefined =>\n\t\tpullRequestSearchCapResultWarning(id, domain, connectionId, truncatedTotalCount, defaultPullRequestSort);\n\tconst appendTruncationWarnings = (cause: 'interrupted' | 'exhausted'): void => {\n\t\tconst cap = capWarning();\n\t\tif (cap != null) {\n\t\t\tappendDedupedWarning(warnings, cap);\n\t\t}\n\t\t// A cap explains the missing tail of a read that succeeded; a failed read still owes its own warning.\n\t\tif (unexplainedTruncation && (cap == null || cause === 'interrupted')) {\n\t\t\tappendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, 'Pull request', cause));\n\t\t}\n\t};\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\tappendTruncationWarnings(failed ? 'interrupted' : 'exhausted');\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\t\t// Only the account-wide read reports a search match count here; the repo-scoped one reports the item\n\t\t// total, which no cap is derived from. Latched even when SDK metadata explains the page: a composite page\n\t\t// reports a failed or cursorless sibling facet there, which says nothing about the cap another reached.\n\t\tif (accountWide && pageTruncated && value.paging?.totalCount != null) {\n\t\t\ttruncatedTotalCount = Math.max(truncatedTotalCount ?? 0, value.paging.totalCount);\n\t\t}\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\tappendTruncationWarnings(fetchFailed ? 'interrupted' : 'exhausted');\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\tconst cap = capWarning();\n\t\t\tif (cap != null) {\n\t\t\t\tappendDedupedWarning(warnings, cap);\n\t\t\t} else if (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\n/**\n * The current account to map each raw row against, in order (see {@link GitHostIntegration.getPullRequestViewers}).\n * Rows with no author can't be matched, so a read of only those resolves nothing; a failed lookup leaves every row's\n * authorship unknown rather than failing the read.\n */\nexport async function getPullRequestViewers(\n\tintegration: GitHostIntegration,\n\tconnectionId: string | undefined,\n\tpullRequests: readonly (ProviderPullRequest | PullRequest)[],\n): Promise<({ id: string; username?: string } | undefined)[]> {\n\tif (!pullRequests.some(pr => pr.author != null)) return pullRequests.map(() => undefined);\n\n\ttry {\n\t\treturn await integration.getPullRequestViewers(pullRequests, connectionId);\n\t} catch {\n\t\treturn pullRequests.map(() => undefined);\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@ import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionFo
|
|
|
6
6
|
import { toProviderRepositoryShape } from "../providers/models.js";
|
|
7
7
|
import { pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo } from "./paging.js";
|
|
8
8
|
import { gitHostOnlySurfaceWarning, truncationWarning, unsupportedWarning } from "./warnings.js";
|
|
9
|
-
import { runCaptured } from "./drains.js";
|
|
9
|
+
import { discoverResources, runCaptured } from "./drains.js";
|
|
10
10
|
import { isIssuesIntegration } from "../models/issuesIntegration.js";
|
|
11
11
|
import { assertHierarchyReadTarget, mergeHierarchyResults, projectToOrg, resourceMatchesOrg, resourceToOrg, withProviderContext } from "./hierarchy.utils.js";
|
|
12
12
|
//#region ../plus/integrations/src/reads/hierarchy.ts
|
|
@@ -58,12 +58,13 @@ async function listOrgs(ctx, options) {
|
|
|
58
58
|
const domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);
|
|
59
59
|
const warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);
|
|
60
60
|
if (isIssuesIntegration(integration)) {
|
|
61
|
-
const { value: resources, warning } = await
|
|
61
|
+
const { value: resources, warning, metadata } = await discoverResources(integration, id, domain, connectionId, { warnOnMissingSession });
|
|
62
62
|
if (resources != null) items.push(...resources.map((r) => resourceToOrg(id, r)));
|
|
63
63
|
if (warning != null) {
|
|
64
64
|
warnings.push(warning);
|
|
65
65
|
if (resources == null) fetchFailed = true;
|
|
66
66
|
}
|
|
67
|
+
if (mergeAssessmentInto(warnings, id, domain, connectionId, metadata).truncated) fetchFailed = true;
|
|
67
68
|
} else if (!integration.supportsOrganizationDiscovery) {
|
|
68
69
|
fetchFailed = true;
|
|
69
70
|
warnings.push(unsupportedWarning(id, domain, connectionId, `Organization discovery is not supported by '${id}'.`));
|
|
@@ -133,11 +134,12 @@ async function listProjects(ctx, options) {
|
|
|
133
134
|
fetchFailed
|
|
134
135
|
};
|
|
135
136
|
}
|
|
136
|
-
const { value: resources, warning: resourcesWarning } = await
|
|
137
|
+
const { value: resources, warning: resourcesWarning, metadata: resourcesMetadata } = await discoverResources(integration, id, domain, connectionId, { warnOnMissingSession });
|
|
137
138
|
if (resourcesWarning != null) {
|
|
138
139
|
warnings.push(resourcesWarning);
|
|
139
140
|
if (resources == null) fetchFailed = true;
|
|
140
141
|
}
|
|
142
|
+
if (mergeAssessmentInto(warnings, id, domain, connectionId, resourcesMetadata).truncated) fetchFailed = true;
|
|
141
143
|
const scopedResources = org != null ? resources?.filter((resource) => resourceMatchesOrg(resource, org)) : resources;
|
|
142
144
|
if (scopedResources != null && scopedResources.length !== 0) {
|
|
143
145
|
const { value: projects, warning: projectsWarning } = await runCaptured(id, domain, connectionId, () => integration.getProjectsForResourcesWithMetadataResult(scopedResources, connectionId));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hierarchy.js","names":[],"sources":["../../../../../plus/integrations/src/reads/hierarchy.ts"],"sourcesContent":["import { mapBounded } from '@gitlens/utils/promise.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tproviderFanOutConcurrency,\n\tsupportedOrderedCloudIntegrationIds,\n\tsupportedOrderedCloudIssuesIntegrationIds,\n} from '../constants.js';\nimport type { ListOrgsOptions, ListProjectsOptions } from '../manager.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport type { ProviderHierarchyResult } from '../providers/models.js';\nimport { toProviderRepositoryShape } from '../providers/models.js';\nimport type {\n\tProviderOrganization,\n\tProviderPagedResult,\n\tProviderRepositoryShape,\n\tProviderResult,\n\tProviderWarning,\n} from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tassertHierarchyReadTarget,\n\tmergeHierarchyResults,\n\tprojectToOrg,\n\tresourceMatchesOrg,\n\tresourceToOrg,\n\twithProviderContext,\n} from './hierarchy.utils.js';\nimport { pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo } from './paging.js';\nimport { gitHostOnlySurfaceWarning, truncationWarning, unsupportedWarning } from './warnings.js';\n\n/**\n * Folds a flat hierarchy result's incompleteness into `warnings`, returning whether it leaves the read\n * non-authoritative. The single funnel for both hierarchy reads, so neither can drift from the other on any of\n * the four decisions it makes:\n *\n * - The metadata is assessed FIRST, so the truncation warning can tell whether it already explained this gap.\n * Ordered deliberately: emitting first meant an expired credential produced BOTH a typed `auth` warning and\n * an unclassifiable one for the same cause, and a consumer routing on classification showed the second as\n * \"this provider failed to load\", with a remedy that doesn't exist, next to the reconnect prompt that was the\n * actual fix.\n * - The read states the truncation itself only when the metadata reported nothing — the rule\n * `assessCollectionMetadata` applies to its own generic fallback. Whatever the metadata reported already names\n * this provider and says something stronger, so restating it costs a false verdict and adds no information.\n * - `'exhausted'`, never `'page-budget'`: `truncated` is one boolean here, so a drain that stopped at its page\n * budget is indistinguishable from one whose cursor stalled, and `IncompleteReadCause` makes `'exhausted'`\n * the default whenever a raisable budget is not demonstrably the cause.\n * - A truncation counts as non-authoritative even though the omission above asserts the read succeeded. Unlike\n * paged repository reads, a flattened hierarchy result has no page object on which to carry incompleteness,\n * so `fetchFailed` is the only signal a consumer has that the list is short.\n */\nfunction mergeHierarchyIncompleteness(\n\twarnings: ProviderWarning[],\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tresult: Pick<ProviderHierarchyResult<unknown>, 'truncated' | 'metadata'>,\n\treadKind: 'Organization' | 'Project',\n): boolean {\n\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, result.metadata);\n\tif (!result.truncated) return assessment.fetchFailed || assessment.truncated;\n\n\tif (!assessment.reported) {\n\t\t// Deduped like every other `truncationWarning` emission (see `drains.ts`), not plain-pushed: no caller can\n\t\t// collide with it today, but nothing about this funnel guarantees a third read won't.\n\t\tappendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, readKind, 'exhausted'));\n\t}\n\treturn true;\n}\n\nexport async function listOrgs(\n\tctx: ProviderReadContext,\n\toptions?: ListOrgsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listOrgs', options);\n\tconst ids = options?.providerId != null ? [options.providerId] : supportedOrderedCloudIntegrationIds;\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A specifically requested connection that can't be resolved is a broken connection, not a\n\t\t\t// provider with no orgs — surface it (warning + fetchFailed) instead of dropping the id\n\t\t\t// silently, so a caller can tell it apart from an account that genuinely has no orgs.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\t\tif (isIssuesIntegration(integration)) {\n\t\t\t// Issue trackers expose \"resources\" (Jira sites, Linear orgs, …) as their org analogue.\n\t\t\tconst { value: resources, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getResourcesForUserResult(connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (resources != null) {\n\t\t\t\titems.push(...resources.map(r => resourceToOrg(id, r)));\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\t// A warning with no value is a hard read failure, not an empty account.\n\t\t\t\tif (resources == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (!integration.supportsOrganizationDiscovery) {\n\t\t\t// The provider registers no org-discovery hook. Report it as\n\t\t\t// explicitly unsupported rather than contributing a silent empty list that a caller can't\n\t\t\t// tell apart from \"this account has no orgs\".\n\t\t\tfetchFailed = true;\n\t\t\twarnings.push(\n\t\t\t\tunsupportedWarning(id, domain, connectionId, `Organization discovery is not supported by '${id}'.`),\n\t\t\t);\n\t\t} else {\n\t\t\tconst { value, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getOrganizationsForUserResult(connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (value != null) {\n\t\t\t\titems.push(...value.values.map(org => withProviderContext(id, org)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, value, 'Organization')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (value == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\nexport async function listProjects(\n\tctx: ProviderReadContext,\n\toptions?: ListProjectsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listProjects', options);\n\tconst ids =\n\t\toptions?.providerId != null\n\t\t\t? [options.providerId]\n\t\t\t: [\n\t\t\t\t\t...supportedOrderedCloudIssuesIntegrationIds,\n\t\t\t\t\tGitCloudHostIntegrationId.AzureDevOps,\n\t\t\t\t\tGitSelfManagedHostIntegrationId.AzureDevOpsServer,\n\t\t\t\t];\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A requested connection that can't be resolved is a broken connection, not a provider with\n\t\t\t// no projects — surface it (warning + fetchFailed) instead of dropping the id silently.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst org = options?.org;\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\n\t\t// Git hosts with a project tier (Azure DevOps) read projects through their own hierarchy hook,\n\t\t// scoped to `org` when given. Non-Azure git hosts have no project tier and return undefined.\n\t\tif (!isIssuesIntegration(integration)) {\n\t\t\t// Check the capability first, like listOrgs/listRepos/listIssuesPage do. Without it, a host\n\t\t\t// with no project tier returns `undefined` from a successful read, which `runCaptured` can't\n\t\t\t// tell apart from an unresolvable session — turning a healthy provider into a `no-connection`\n\t\t\t// warning + `fetchFailed` (and, in Kepler, a spurious reconnect prompt).\n\t\t\tif (!integration.supportsProjectDiscovery) {\n\t\t\t\treturn { items: items, warnings: warnings, fetchFailed: false };\n\t\t\t}\n\n\t\t\tconst { value: projects, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getProjectsForOrgResult(org, connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(...projects.values.map(project => withProviderContext(id, project)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, projects, 'Project')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t\t}\n\n\t\tconst { value: resources, warning: resourcesWarning } = await runCaptured(\n\t\t\tid,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t() => integration.getResourcesForUserResult(connectionId),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\t\tif (resourcesWarning != null) {\n\t\t\twarnings.push(resourcesWarning);\n\t\t\tif (resources == null) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\n\t\t// By any identifier, unlike the issue reads' `selectorMatches`: this lists what the caller can then select, so a\n\t\t// match through a name shows the user those projects rather than silently reading another backlog.\n\t\tconst scopedResources =\n\t\t\torg != null ? resources?.filter(resource => resourceMatchesOrg(resource, org)) : resources;\n\t\tif (scopedResources != null && scopedResources.length !== 0) {\n\t\t\tconst { value: projects, warning: projectsWarning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\t\tintegration.getProjectsForResourcesWithMetadataResult(scopedResources, connectionId),\n\t\t\t);\n\t\t\tif (projectsWarning != null) {\n\t\t\t\twarnings.push(projectsWarning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(...projects.values.map(project => projectToOrg(id, project, scopedResources)));\n\t\t\t}\n\t\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, projects?.metadata);\n\t\t\tif (assessment.fetchFailed || assessment.truncated) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\nexport async function listRepos(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\torg?: string;\n\t\tproject?: string;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\titemsPerPage?: number;\n\t\tconnectionId?: string;\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<ProviderRepositoryShape>> {\n\t// Truncated as well as floored, like every other paged read here: `page` is reported back as\n\t// `page.currentPage` and bounds the cursor walk below, and neither can be fractional.\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account —\n\t\t// surface a no-connection warning + fetchFailed rather than a silent empty page.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(page, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tconst accountWide = options.org == null;\n\tconst supported = accountWide\n\t\t? integration.supportsUserRepositoryDiscovery\n\t\t: integration.supportsRepositoryDiscovery;\n\tif (!supported) {\n\t\t// No matching repo-discovery hook — org-scoped (e.g. Bitbucket Data Center) or account-wide (e.g.\n\t\t// Bitbucket/Azure, whose repos can only be walked per workspace/org). Report unsupported rather than\n\t\t// a silent empty page indistinguishable from \"no repos\"; for the account-wide case the caller should\n\t\t// fan out per org from listOrgs instead.\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tunsupportedWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\taccountWide\n\t\t\t\t\t\t? `Account-wide repository discovery is not supported by '${options.providerId}'; list repositories per org instead.`\n\t\t\t\t\t\t: `Repository discovery is not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst org = options.org;\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\tconst readPage = (cursor: string | undefined) =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\torg == null\n\t\t\t\t\t? integration.getRepositoriesForUserResult({ cursor: cursor, connectionId: options.connectionId })\n\t\t\t\t\t: integration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\t\t\tproject: options.project,\n\t\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\t\tconnectionId: options.connectionId,\n\t\t\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\n\t/**\n\t * Builds one page's result from what the provider returned. Shared by the initial read and by every round of\n\t * the cursor walk below, so a walked page N is described by exactly the same rules as a directly-requested one\n\t * (position, continuation, truncation) rather than by a second copy of them.\n\t */\n\tconst toResult = (\n\t\t{ value, warning }: Awaited<ReturnType<typeof readPage>>,\n\t\trequestedPage: number,\n\t\tsuppliedCursor: string | undefined,\n\t): ProviderPagedResult<ProviderRepositoryShape> => {\n\t\tconst items = value?.values ?? [];\n\t\tconst warnings = warning != null ? [warning] : [];\n\t\t// The repos read core is cursor-only and can't accept a page size, so don't echo the requested\n\t\t// `itemsPerPage` as if it were applied — report what the provider returned (its own pageSize when\n\t\t// available, else the actual item count).\n\t\tconst paged = toProviderPageInfo(items.length, value?.paging);\n\t\t// Convert the SDK collection metadata into scope-aware warnings + failure/truncation flags, appending\n\t\t// them to any captured thrown-error warning without discarding the partial result's items.\n\t\tconst assessment = mergeAssessmentInto(\n\t\t\twarnings,\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\tvalue?.metadata,\n\t\t);\n\t\t// Whether this read honors a page NUMBER, decided from what the provider actually reported rather than\n\t\t// guessed from the absence of a cursor. A numbered-page repos host reports its position (`paging.page`)\n\t\t// and/or its successor (`paging.nextPage`) — Bitbucket's workspace walk consumes `page` as a real 1-based\n\t\t// page and reports `nextPage` without echoing `currentPage`, so both signals have to count. Every wired\n\t\t// cursor-based repos read (GitHub's org/user walks, GitLab's user walk, Bitbucket's cursor read) reports\n\t\t// neither. Keying off `paged.cursor == null` instead read the paging layer's \"reported another page,\n\t\t// handed back no continuation\" sentinel as evidence of a numbered host, which is exactly backwards.\n\t\tconst pageAdvanceable = value?.paging?.page != null || value?.paging?.nextPage != null;\n\t\t// Numbered-page hosts that don't echo `currentPage` may still be advanced by the requested `page` (initial\n\t\t// read) or by the cursor the caller threaded back. A cursor-only host ignores an UNTHREADED `page`\n\t\t// request, so its page-less first page is page 1.\n\t\tconst currentPage = resolveCurrentPage({\n\t\t\tproviderPage: paged.page.currentPage,\n\t\t\trequestedPage: requestedPage,\n\t\t\tsuppliedCursor: suppliedCursor,\n\t\t\tpageAdvanceable: pageAdvanceable,\n\t\t});\n\t\t// Never advertise `hasMore` without a continuation the caller can act on — the same contract\n\t\t// `listPullRequestsPage`, `listIssuesPage` and `broadenIssues` hold, via the same helper. A provider that\n\t\t// reports another page but hands back neither `endCursor` nor `nextPage` (surfaced as the `'{}'` sentinel,\n\t\t// which `toProviderPageInfo` drops) is terminal-but-incomplete: `hasMore: false` + `page.truncated`.\n\t\t// Synthesizing a page-number cursor for a cursor-only host instead — which is what this read used to do\n\t\t// unconditionally — handed back a continuation the provider ignores, so it answered with its FIRST page\n\t\t// again while still reporting `hasMore: true`. A consumer draining until `hasMore` clears (Kepler's repo\n\t\t// drain does exactly that) then looped to its own page cap, accumulating a duplicate copy of every repo\n\t\t// per round, and never saw a truncation signal.\n\t\tconst continuation = resolveContinuation(paged, pageAdvanceable ? currentPage + 1 : undefined);\n\t\t// The org-hierarchy read can also stop at a defensive backstop with more repos unlisted and no cursor to\n\t\t// resume (top-level `truncated`, or `paging.truncated` on a single-page read). Metadata incompleteness and\n\t\t// a demoted continuation are two further, independent sources of the same signal.\n\t\tconst truncated =\n\t\t\t(value?.truncated ?? false) ||\n\t\t\t(value?.paging?.truncated ?? false) ||\n\t\t\tassessment.truncated ||\n\t\t\tcontinuation.truncated;\n\t\treturn {\n\t\t\t// Normalize the raw provider-apis repos to the GitLens-owned shape at the surface boundary.\n\t\t\titems: items.map(toProviderRepositoryShape),\n\t\t\twarnings: warnings,\n\t\t\tpage: { ...paged.page, currentPage: currentPage, truncated: truncated || undefined },\n\t\t\thasMore: continuation.hasMore,\n\t\t\tcursor: continuation.cursor,\n\t\t\tfetchFailed: assessment.fetchFailed || (warning != null && value == null) || undefined,\n\t\t};\n\t};\n\n\tconst first = await readPage(\n\t\toptions.cursor ??\n\t\t\t(integration.id === GitSelfManagedHostIntegrationId.BitbucketServer ? undefined : pageToCursor(page)),\n\t);\n\tconst result = toResult(first, page, options.cursor);\n\t// A numbered-page host honored the synthesized cursor, so this IS page N; a cursor-only host ignored it and\n\t// answered with page 1, which the walk below advances. Decided from the provider's own paging metadata rather\n\t// than assumed, hence after the read.\n\tconst pageAdvanceable = first.value?.paging?.page != null || first.value?.paging?.nextPage != null;\n\tif (options.cursor != null || page === 1 || pageAdvanceable) return result;\n\n\t// Walk the provider's real continuations to the requested page. Only the last page's items are kept — pages\n\t// 1..N returned as \"page N\" would duplicate items for a normal paged consumer — while warnings, truncation,\n\t// and failure accumulate across the whole walked prefix, since an earlier round's problem still means this\n\t// result isn't authoritative. The provider read is re-issued per round, but the integration is resolved once:\n\t// re-entering this function per page would repeat its auth resolution N times for page N.\n\tconst walkedWarnings = [...result.warnings];\n\tlet walkedFetchFailed = result.fetchFailed === true;\n\tlet walkedTruncated = result.page.truncated === true;\n\tlet previous = result;\n\tfor (let currentPage = 2; currentPage <= page; currentPage++) {\n\t\tif (!previous.hasMore || previous.cursor == null) {\n\t\t\t// The requested page lies past the provider's terminal cursor: an EMPTY page N, never the last\n\t\t\t// available page relabeled (see ProviderPageInfo.currentPage).\n\t\t\treturn {\n\t\t\t\titems: [],\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { currentPage: page, itemsPerPage: 0, truncated: walkedTruncated || undefined },\n\t\t\t\thasMore: false,\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tconst suppliedCursor = previous.cursor;\n\t\tconst requested = toResult(await readPage(suppliedCursor), currentPage, suppliedCursor);\n\t\tfor (const warning of requested.warnings) {\n\t\t\tappendDedupedWarning(walkedWarnings, warning);\n\t\t}\n\t\twalkedFetchFailed ||= requested.fetchFailed === true;\n\t\twalkedTruncated ||= requested.page.truncated === true;\n\t\tif (currentPage === page) {\n\t\t\treturn {\n\t\t\t\t...requested,\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { ...requested.page, truncated: walkedTruncated || undefined },\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tprevious = requested;\n\t}\n\n\treturn result;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hierarchy.js","names":[],"sources":["../../../../../plus/integrations/src/reads/hierarchy.ts"],"sourcesContent":["import { mapBounded } from '@gitlens/utils/promise.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tproviderFanOutConcurrency,\n\tsupportedOrderedCloudIntegrationIds,\n\tsupportedOrderedCloudIssuesIntegrationIds,\n} from '../constants.js';\nimport type { ListOrgsOptions, ListProjectsOptions } from '../manager.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport type { ProviderHierarchyResult } from '../providers/models.js';\nimport { toProviderRepositoryShape } from '../providers/models.js';\nimport type {\n\tProviderOrganization,\n\tProviderPagedResult,\n\tProviderRepositoryShape,\n\tProviderResult,\n\tProviderWarning,\n} from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { discoverResources, runCaptured } from './drains.js';\nimport {\n\tassertHierarchyReadTarget,\n\tmergeHierarchyResults,\n\tprojectToOrg,\n\tresourceMatchesOrg,\n\tresourceToOrg,\n\twithProviderContext,\n} from './hierarchy.utils.js';\nimport { pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo } from './paging.js';\nimport { gitHostOnlySurfaceWarning, truncationWarning, unsupportedWarning } from './warnings.js';\n\n/**\n * Folds a flat hierarchy result's incompleteness into `warnings`, returning whether it leaves the read\n * non-authoritative. The single funnel for both hierarchy reads, so neither can drift from the other on any of\n * the four decisions it makes:\n *\n * - The metadata is assessed FIRST, so the truncation warning can tell whether it already explained this gap.\n * Ordered deliberately: emitting first meant an expired credential produced BOTH a typed `auth` warning and\n * an unclassifiable one for the same cause, and a consumer routing on classification showed the second as\n * \"this provider failed to load\", with a remedy that doesn't exist, next to the reconnect prompt that was the\n * actual fix.\n * - The read states the truncation itself only when the metadata reported nothing — the rule\n * `assessCollectionMetadata` applies to its own generic fallback. Whatever the metadata reported already names\n * this provider and says something stronger, so restating it costs a false verdict and adds no information.\n * - `'exhausted'`, never `'page-budget'`: `truncated` is one boolean here, so a drain that stopped at its page\n * budget is indistinguishable from one whose cursor stalled, and `IncompleteReadCause` makes `'exhausted'`\n * the default whenever a raisable budget is not demonstrably the cause.\n * - A truncation counts as non-authoritative even though the omission above asserts the read succeeded. Unlike\n * paged repository reads, a flattened hierarchy result has no page object on which to carry incompleteness,\n * so `fetchFailed` is the only signal a consumer has that the list is short.\n */\nfunction mergeHierarchyIncompleteness(\n\twarnings: ProviderWarning[],\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tresult: Pick<ProviderHierarchyResult<unknown>, 'truncated' | 'metadata'>,\n\treadKind: 'Organization' | 'Project',\n): boolean {\n\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, result.metadata);\n\tif (!result.truncated) return assessment.fetchFailed || assessment.truncated;\n\n\tif (!assessment.reported) {\n\t\t// Deduped like every other `truncationWarning` emission (see `drains.ts`), not plain-pushed: no caller can\n\t\t// collide with it today, but nothing about this funnel guarantees a third read won't.\n\t\tappendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, readKind, 'exhausted'));\n\t}\n\treturn true;\n}\n\nexport async function listOrgs(\n\tctx: ProviderReadContext,\n\toptions?: ListOrgsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listOrgs', options);\n\tconst ids = options?.providerId != null ? [options.providerId] : supportedOrderedCloudIntegrationIds;\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A specifically requested connection that can't be resolved is a broken connection, not a\n\t\t\t// provider with no orgs — surface it (warning + fetchFailed) instead of dropping the id\n\t\t\t// silently, so a caller can tell it apart from an account that genuinely has no orgs.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\t\tif (isIssuesIntegration(integration)) {\n\t\t\t// Issue trackers expose \"resources\" (Jira sites, Linear orgs, …) as their org analogue.\n\t\t\tconst {\n\t\t\t\tvalue: resources,\n\t\t\t\twarning,\n\t\t\t\tmetadata,\n\t\t\t} = await discoverResources(integration, id, domain, connectionId, {\n\t\t\t\twarnOnMissingSession: warnOnMissingSession,\n\t\t\t});\n\t\t\tif (resources != null) {\n\t\t\t\titems.push(...resources.map(r => resourceToOrg(id, r)));\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\t// A warning with no value is a hard read failure, not an empty account.\n\t\t\t\tif (resources == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (mergeAssessmentInto(warnings, id, domain, connectionId, metadata).truncated) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t} else if (!integration.supportsOrganizationDiscovery) {\n\t\t\t// The provider registers no org-discovery hook. Report it as\n\t\t\t// explicitly unsupported rather than contributing a silent empty list that a caller can't\n\t\t\t// tell apart from \"this account has no orgs\".\n\t\t\tfetchFailed = true;\n\t\t\twarnings.push(\n\t\t\t\tunsupportedWarning(id, domain, connectionId, `Organization discovery is not supported by '${id}'.`),\n\t\t\t);\n\t\t} else {\n\t\t\tconst { value, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getOrganizationsForUserResult(connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (value != null) {\n\t\t\t\titems.push(...value.values.map(org => withProviderContext(id, org)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, value, 'Organization')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (value == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\nexport async function listProjects(\n\tctx: ProviderReadContext,\n\toptions?: ListProjectsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listProjects', options);\n\tconst ids =\n\t\toptions?.providerId != null\n\t\t\t? [options.providerId]\n\t\t\t: [\n\t\t\t\t\t...supportedOrderedCloudIssuesIntegrationIds,\n\t\t\t\t\tGitCloudHostIntegrationId.AzureDevOps,\n\t\t\t\t\tGitSelfManagedHostIntegrationId.AzureDevOpsServer,\n\t\t\t\t];\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A requested connection that can't be resolved is a broken connection, not a provider with\n\t\t\t// no projects — surface it (warning + fetchFailed) instead of dropping the id silently.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst org = options?.org;\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\n\t\t// Git hosts with a project tier (Azure DevOps) read projects through their own hierarchy hook,\n\t\t// scoped to `org` when given. Non-Azure git hosts have no project tier and return undefined.\n\t\tif (!isIssuesIntegration(integration)) {\n\t\t\t// Check the capability first, like listOrgs/listRepos/listIssuesPage do. Without it, a host\n\t\t\t// with no project tier returns `undefined` from a successful read, which `runCaptured` can't\n\t\t\t// tell apart from an unresolvable session — turning a healthy provider into a `no-connection`\n\t\t\t// warning + `fetchFailed` (and, in Kepler, a spurious reconnect prompt).\n\t\t\tif (!integration.supportsProjectDiscovery) {\n\t\t\t\treturn { items: items, warnings: warnings, fetchFailed: false };\n\t\t\t}\n\n\t\t\tconst { value: projects, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getProjectsForOrgResult(org, connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(...projects.values.map(project => withProviderContext(id, project)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, projects, 'Project')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t\t}\n\n\t\tconst {\n\t\t\tvalue: resources,\n\t\t\twarning: resourcesWarning,\n\t\t\tmetadata: resourcesMetadata,\n\t\t} = await discoverResources(integration, id, domain, connectionId, {\n\t\t\twarnOnMissingSession: warnOnMissingSession,\n\t\t});\n\t\tif (resourcesWarning != null) {\n\t\t\twarnings.push(resourcesWarning);\n\t\t\tif (resources == null) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\t\tif (mergeAssessmentInto(warnings, id, domain, connectionId, resourcesMetadata).truncated) {\n\t\t\tfetchFailed = true;\n\t\t}\n\n\t\t// By any identifier, unlike the issue reads' `selectorMatches`: this lists what the caller can then select, so a\n\t\t// match through a name shows the user those projects rather than silently reading another backlog.\n\t\tconst scopedResources =\n\t\t\torg != null ? resources?.filter(resource => resourceMatchesOrg(resource, org)) : resources;\n\t\tif (scopedResources != null && scopedResources.length !== 0) {\n\t\t\tconst { value: projects, warning: projectsWarning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\t\tintegration.getProjectsForResourcesWithMetadataResult(scopedResources, connectionId),\n\t\t\t);\n\t\t\tif (projectsWarning != null) {\n\t\t\t\twarnings.push(projectsWarning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(...projects.values.map(project => projectToOrg(id, project, scopedResources)));\n\t\t\t}\n\t\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, projects?.metadata);\n\t\t\tif (assessment.fetchFailed || assessment.truncated) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\nexport async function listRepos(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\torg?: string;\n\t\tproject?: string;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\titemsPerPage?: number;\n\t\tconnectionId?: string;\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<ProviderRepositoryShape>> {\n\t// Truncated as well as floored, like every other paged read here: `page` is reported back as\n\t// `page.currentPage` and bounds the cursor walk below, and neither can be fractional.\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account —\n\t\t// surface a no-connection warning + fetchFailed rather than a silent empty page.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(page, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tconst accountWide = options.org == null;\n\tconst supported = accountWide\n\t\t? integration.supportsUserRepositoryDiscovery\n\t\t: integration.supportsRepositoryDiscovery;\n\tif (!supported) {\n\t\t// No matching repo-discovery hook — org-scoped (e.g. Bitbucket Data Center) or account-wide (e.g.\n\t\t// Bitbucket/Azure, whose repos can only be walked per workspace/org). Report unsupported rather than\n\t\t// a silent empty page indistinguishable from \"no repos\"; for the account-wide case the caller should\n\t\t// fan out per org from listOrgs instead.\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tunsupportedWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\taccountWide\n\t\t\t\t\t\t? `Account-wide repository discovery is not supported by '${options.providerId}'; list repositories per org instead.`\n\t\t\t\t\t\t: `Repository discovery is not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst org = options.org;\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\tconst readPage = (cursor: string | undefined) =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\torg == null\n\t\t\t\t\t? integration.getRepositoriesForUserResult({ cursor: cursor, connectionId: options.connectionId })\n\t\t\t\t\t: integration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\t\t\tproject: options.project,\n\t\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\t\tconnectionId: options.connectionId,\n\t\t\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\n\t/**\n\t * Builds one page's result from what the provider returned. Shared by the initial read and by every round of\n\t * the cursor walk below, so a walked page N is described by exactly the same rules as a directly-requested one\n\t * (position, continuation, truncation) rather than by a second copy of them.\n\t */\n\tconst toResult = (\n\t\t{ value, warning }: Awaited<ReturnType<typeof readPage>>,\n\t\trequestedPage: number,\n\t\tsuppliedCursor: string | undefined,\n\t): ProviderPagedResult<ProviderRepositoryShape> => {\n\t\tconst items = value?.values ?? [];\n\t\tconst warnings = warning != null ? [warning] : [];\n\t\t// The repos read core is cursor-only and can't accept a page size, so don't echo the requested\n\t\t// `itemsPerPage` as if it were applied — report what the provider returned (its own pageSize when\n\t\t// available, else the actual item count).\n\t\tconst paged = toProviderPageInfo(items.length, value?.paging);\n\t\t// Convert the SDK collection metadata into scope-aware warnings + failure/truncation flags, appending\n\t\t// them to any captured thrown-error warning without discarding the partial result's items.\n\t\tconst assessment = mergeAssessmentInto(\n\t\t\twarnings,\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\tvalue?.metadata,\n\t\t);\n\t\t// Whether this read honors a page NUMBER, decided from what the provider actually reported rather than\n\t\t// guessed from the absence of a cursor. A numbered-page repos host reports its position (`paging.page`)\n\t\t// and/or its successor (`paging.nextPage`) — Bitbucket's workspace walk consumes `page` as a real 1-based\n\t\t// page and reports `nextPage` without echoing `currentPage`, so both signals have to count. Every wired\n\t\t// cursor-based repos read (GitHub's org/user walks, GitLab's user walk, Bitbucket's cursor read) reports\n\t\t// neither. Keying off `paged.cursor == null` instead read the paging layer's \"reported another page,\n\t\t// handed back no continuation\" sentinel as evidence of a numbered host, which is exactly backwards.\n\t\tconst pageAdvanceable = value?.paging?.page != null || value?.paging?.nextPage != null;\n\t\t// Numbered-page hosts that don't echo `currentPage` may still be advanced by the requested `page` (initial\n\t\t// read) or by the cursor the caller threaded back. A cursor-only host ignores an UNTHREADED `page`\n\t\t// request, so its page-less first page is page 1.\n\t\tconst currentPage = resolveCurrentPage({\n\t\t\tproviderPage: paged.page.currentPage,\n\t\t\trequestedPage: requestedPage,\n\t\t\tsuppliedCursor: suppliedCursor,\n\t\t\tpageAdvanceable: pageAdvanceable,\n\t\t});\n\t\t// Never advertise `hasMore` without a continuation the caller can act on — the same contract\n\t\t// `listPullRequestsPage`, `listIssuesPage` and `broadenIssues` hold, via the same helper. A provider that\n\t\t// reports another page but hands back neither `endCursor` nor `nextPage` (surfaced as the `'{}'` sentinel,\n\t\t// which `toProviderPageInfo` drops) is terminal-but-incomplete: `hasMore: false` + `page.truncated`.\n\t\t// Synthesizing a page-number cursor for a cursor-only host instead — which is what this read used to do\n\t\t// unconditionally — handed back a continuation the provider ignores, so it answered with its FIRST page\n\t\t// again while still reporting `hasMore: true`. A consumer draining until `hasMore` clears (Kepler's repo\n\t\t// drain does exactly that) then looped to its own page cap, accumulating a duplicate copy of every repo\n\t\t// per round, and never saw a truncation signal.\n\t\tconst continuation = resolveContinuation(paged, pageAdvanceable ? currentPage + 1 : undefined);\n\t\t// The org-hierarchy read can also stop at a defensive backstop with more repos unlisted and no cursor to\n\t\t// resume (top-level `truncated`, or `paging.truncated` on a single-page read). Metadata incompleteness and\n\t\t// a demoted continuation are two further, independent sources of the same signal.\n\t\tconst truncated =\n\t\t\t(value?.truncated ?? false) ||\n\t\t\t(value?.paging?.truncated ?? false) ||\n\t\t\tassessment.truncated ||\n\t\t\tcontinuation.truncated;\n\t\treturn {\n\t\t\t// Normalize the raw provider-apis repos to the GitLens-owned shape at the surface boundary.\n\t\t\titems: items.map(toProviderRepositoryShape),\n\t\t\twarnings: warnings,\n\t\t\tpage: { ...paged.page, currentPage: currentPage, truncated: truncated || undefined },\n\t\t\thasMore: continuation.hasMore,\n\t\t\tcursor: continuation.cursor,\n\t\t\tfetchFailed: assessment.fetchFailed || (warning != null && value == null) || undefined,\n\t\t};\n\t};\n\n\tconst first = await readPage(\n\t\toptions.cursor ??\n\t\t\t(integration.id === GitSelfManagedHostIntegrationId.BitbucketServer ? undefined : pageToCursor(page)),\n\t);\n\tconst result = toResult(first, page, options.cursor);\n\t// A numbered-page host honored the synthesized cursor, so this IS page N; a cursor-only host ignored it and\n\t// answered with page 1, which the walk below advances. Decided from the provider's own paging metadata rather\n\t// than assumed, hence after the read.\n\tconst pageAdvanceable = first.value?.paging?.page != null || first.value?.paging?.nextPage != null;\n\tif (options.cursor != null || page === 1 || pageAdvanceable) return result;\n\n\t// Walk the provider's real continuations to the requested page. Only the last page's items are kept — pages\n\t// 1..N returned as \"page N\" would duplicate items for a normal paged consumer — while warnings, truncation,\n\t// and failure accumulate across the whole walked prefix, since an earlier round's problem still means this\n\t// result isn't authoritative. The provider read is re-issued per round, but the integration is resolved once:\n\t// re-entering this function per page would repeat its auth resolution N times for page N.\n\tconst walkedWarnings = [...result.warnings];\n\tlet walkedFetchFailed = result.fetchFailed === true;\n\tlet walkedTruncated = result.page.truncated === true;\n\tlet previous = result;\n\tfor (let currentPage = 2; currentPage <= page; currentPage++) {\n\t\tif (!previous.hasMore || previous.cursor == null) {\n\t\t\t// The requested page lies past the provider's terminal cursor: an EMPTY page N, never the last\n\t\t\t// available page relabeled (see ProviderPageInfo.currentPage).\n\t\t\treturn {\n\t\t\t\titems: [],\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { currentPage: page, itemsPerPage: 0, truncated: walkedTruncated || undefined },\n\t\t\t\thasMore: false,\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tconst suppliedCursor = previous.cursor;\n\t\tconst requested = toResult(await readPage(suppliedCursor), currentPage, suppliedCursor);\n\t\tfor (const warning of requested.warnings) {\n\t\t\tappendDedupedWarning(walkedWarnings, warning);\n\t\t}\n\t\twalkedFetchFailed ||= requested.fetchFailed === true;\n\t\twalkedTruncated ||= requested.page.truncated === true;\n\t\tif (currentPage === page) {\n\t\t\treturn {\n\t\t\t\t...requested,\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { ...requested.page, truncated: walkedTruncated || undefined },\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tprevious = requested;\n\t}\n\n\treturn result;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,SAAS,6BACR,UACA,IACA,QACA,cACA,QACA,UACU;CACV,MAAM,aAAa,oBAAoB,UAAU,IAAI,QAAQ,cAAc,OAAO,QAAQ;CAC1F,IAAI,CAAC,OAAO,WAAW,OAAO,WAAW,eAAe,WAAW;CAEnE,IAAI,CAAC,WAAW,UAGf,qBAAqB,UAAU,kBAAkB,IAAI,QAAQ,cAAc,UAAU,WAAW,CAAC;CAElG,OAAO;AACR;AAEA,eAAsB,SACrB,KACA,SACgD;CAChD,0BAA0B,YAAY,OAAO;CAC7C,MAAM,MAAM,SAAS,cAAc,OAAO,CAAC,QAAQ,UAAU,IAAI;CACjE,MAAM,iBAAiB,IAAI,WAAW;CACtC,MAAM,eAAe,iBAAiB,SAAS,eAAe,KAAA;CAC9D,MAAM,kBAAkB,iBAAiB,SAAS,SAAS,KAAA;CAE3D,MAAM,UAAU,MAAM,WAAW,KAAA,GAAgC,OAAM,OAAM;EAC5E,MAAM,cAAc,MAAM,IAAI,sBAAsB,IAAI,cAAc,eAAe;EACrF,IAAI,eAAe,MAAM;GAIxB,MAAM,QAAQ,IAAI,8BAA8B,IAAI,cAAc,eAAe;GACjF,OAAO;IACN,OAAO,CAAC;IACR,UAAU,MAAM;IAChB,aAAa,MAAM;GACpB;EACD;EAEA,MAAM,QAAgC,CAAC;EACvC,MAAM,WAA8B,CAAC;EACrC,IAAI,cAAc;EAClB,MAAM,SAAS,IAAI,cAAc,aAAa,IAAI,cAAc,eAAe;EAC/E,MAAM,uBAAuB,8BAA8B,IAAI,eAAe;EAC9E,IAAI,oBAAoB,WAAW,GAAG;GAErC,MAAM,EACL,OAAO,WACP,SACA,aACG,MAAM,kBAAkB,aAAa,IAAI,QAAQ,cAAc,EAC5C,qBACvB,CAAC;GACD,IAAI,aAAa,MAChB,MAAM,KAAK,GAAG,UAAU,KAAI,MAAK,cAAc,IAAI,CAAC,CAAC,CAAC;GAEvD,IAAI,WAAW,MAAM;IACpB,SAAS,KAAK,OAAO;IAErB,IAAI,aAAa,MAChB,cAAc;GAEhB;GACA,IAAI,oBAAoB,UAAU,IAAI,QAAQ,cAAc,QAAQ,CAAC,CAAC,WACrE,cAAc;EAEhB,OAAO,IAAI,CAAC,YAAY,+BAA+B;GAItD,cAAc;GACd,SAAS,KACR,mBAAmB,IAAI,QAAQ,cAAc,+CAA+C,GAAG,GAAG,CACnG;EACD,OAAO;GACN,MAAM,EAAE,OAAO,YAAY,MAAM,YAChC,IACA,QACA,oBACM,YAAY,8BAA8B,YAAY,GAC5D,EAAwB,qBAAqB,CAC9C;GACA,IAAI,SAAS,MAAM;IAClB,MAAM,KAAK,GAAG,MAAM,OAAO,KAAI,QAAO,oBAAoB,IAAI,GAAG,CAAC,CAAC;IAEnE,IAAI,6BAA6B,UAAU,IAAI,QAAQ,cAAc,OAAO,cAAc,GACzF,cAAc;GAEhB;GACA,IAAI,WAAW,MAAM;IACpB,SAAS,KAAK,OAAO;IACrB,IAAI,SAAS,MACZ,cAAc;GAEhB;EACD;EAEA,OAAO;GAAS;GAAiB;GAAuB;EAAY;CACrE,CAAC;CAED,OAAO,sBAAsB,OAAO;AACrC;AAEA,eAAsB,aACrB,KACA,SACgD;CAChD,0BAA0B,gBAAgB,OAAO;CACjD,MAAM,MACL,SAAS,cAAc,OACpB,CAAC,QAAQ,UAAU,IACnB;EACA,GAAG;;;CAGJ;CACH,MAAM,iBAAiB,IAAI,WAAW;CACtC,MAAM,eAAe,iBAAiB,SAAS,eAAe,KAAA;CAC9D,MAAM,kBAAkB,iBAAiB,SAAS,SAAS,KAAA;CAE3D,MAAM,UAAU,MAAM,WAAW,KAAA,GAAgC,OAAM,OAAM;EAC5E,MAAM,cAAc,MAAM,IAAI,sBAAsB,IAAI,cAAc,eAAe;EACrF,IAAI,eAAe,MAAM;GAGxB,MAAM,QAAQ,IAAI,8BAA8B,IAAI,cAAc,eAAe;GACjF,OAAO;IACN,OAAO,CAAC;IACR,UAAU,MAAM;IAChB,aAAa,MAAM;GACpB;EACD;EAEA,MAAM,QAAgC,CAAC;EACvC,MAAM,WAA8B,CAAC;EACrC,IAAI,cAAc;EAClB,MAAM,SAAS,IAAI,cAAc,aAAa,IAAI,cAAc,eAAe;EAC/E,MAAM,MAAM,SAAS;EACrB,MAAM,uBAAuB,8BAA8B,IAAI,eAAe;EAI9E,IAAI,CAAC,oBAAoB,WAAW,GAAG;GAKtC,IAAI,CAAC,YAAY,0BAChB,OAAO;IAAS;IAAiB;IAAU,aAAa;GAAM;GAG/D,MAAM,EAAE,OAAO,UAAU,YAAY,MAAM,YAC1C,IACA,QACA,oBACM,YAAY,wBAAwB,KAAK,YAAY,GAC3D,EAAwB,qBAAqB,CAC9C;GACA,IAAI,WAAW,MAAM;IACpB,SAAS,KAAK,OAAO;IACrB,IAAI,YAAY,MACf,cAAc;GAEhB;GACA,IAAI,YAAY,MAAM;IACrB,MAAM,KAAK,GAAG,SAAS,OAAO,KAAI,YAAW,oBAAoB,IAAI,OAAO,CAAC,CAAC;IAE9E,IAAI,6BAA6B,UAAU,IAAI,QAAQ,cAAc,UAAU,SAAS,GACvF,cAAc;GAEhB;GACA,OAAO;IAAS;IAAiB;IAAuB;GAAY;EACrE;EAEA,MAAM,EACL,OAAO,WACP,SAAS,kBACT,UAAU,sBACP,MAAM,kBAAkB,aAAa,IAAI,QAAQ,cAAc,EAC5C,qBACvB,CAAC;EACD,IAAI,oBAAoB,MAAM;GAC7B,SAAS,KAAK,gBAAgB;GAC9B,IAAI,aAAa,MAChB,cAAc;EAEhB;EACA,IAAI,oBAAoB,UAAU,IAAI,QAAQ,cAAc,iBAAiB,CAAC,CAAC,WAC9E,cAAc;EAKf,MAAM,kBACL,OAAO,OAAO,WAAW,QAAO,aAAY,mBAAmB,UAAU,GAAG,CAAC,IAAI;EAClF,IAAI,mBAAmB,QAAQ,gBAAgB,WAAW,GAAG;GAC5D,MAAM,EAAE,OAAO,UAAU,SAAS,oBAAoB,MAAM,YAAY,IAAI,QAAQ,oBACnF,YAAY,0CAA0C,iBAAiB,YAAY,CACpF;GACA,IAAI,mBAAmB,MAAM;IAC5B,SAAS,KAAK,eAAe;IAC7B,IAAI,YAAY,MACf,cAAc;GAEhB;GACA,IAAI,YAAY,MACf,MAAM,KAAK,GAAG,SAAS,OAAO,KAAI,YAAW,aAAa,IAAI,SAAS,eAAe,CAAC,CAAC;GAEzF,MAAM,aAAa,oBAAoB,UAAU,IAAI,QAAQ,cAAc,UAAU,QAAQ;GAC7F,IAAI,WAAW,eAAe,WAAW,WACxC,cAAc;EAEhB;EAEA,OAAO;GAAS;GAAiB;GAAuB;EAAY;CACrE,CAAC;CAED,OAAO,sBAAsB,OAAO;AACrC;AAEA,eAAsB,UACrB,KACA,SAUwD;CAGxD,MAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,QAAQ,CAAC,CAAC;CACtD,IAAI,0BAA0B,QAAQ,UAAU,GAC/C,OAAO,YACN,MACA,CAAC,0BAA0B,QAAQ,YAAY,KAAA,GAAW,QAAQ,cAAc,sBAAsB,CAAC,GACvG,IACD;CAGD,MAAM,cAAc,MAAM,IAAI,sBAAsB,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CAC5G,IAAI,eAAe,MAAM;EAGxB,MAAM,QAAQ,IAAI,8BAA8B,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;EACxG,OAAO,YAAY,MAAM,MAAM,UAAU,MAAM,WAAW;CAC3D;CACA,IAAI,CAAC,qBAAqB,WAAW,GACpC,OAAO,YACN,MACA,CAAC,0BAA0B,QAAQ,YAAY,KAAA,GAAW,QAAQ,cAAc,sBAAsB,CAAC,GACvG,IACD;CAGD,MAAM,SAAS,IAAI,cAAc,aAAa,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CAEtG,MAAM,cAAc,QAAQ,OAAO;CAInC,IAAI,EAHc,cACf,YAAY,kCACZ,YAAY,8BAMd,OAAO,YACN,MACA,CACC,mBACC,QAAQ,YACR,QACA,QAAQ,cACR,cACG,0DAA0D,QAAQ,WAAW,yCAC7E,6CAA6C,QAAQ,WAAW,GACpE,CACD,GACA,IACD;CAGD,MAAM,MAAM,QAAQ;CACpB,MAAM,uBAAuB,8BAA8B,QAAQ,YAAY,QAAQ,MAAM;CAC7F,MAAM,YAAY,WACjB,YACC,QAAQ,YACR,QACA,QAAQ,oBAEP,OAAO,OACJ,YAAY,6BAA6B;EAAU;EAAQ,cAAc,QAAQ;CAAa,CAAC,IAC/F,YAAY,4BAA4B,KAAK;EAC7C,SAAS,QAAQ;EACT;EACR,cAAc,QAAQ;CACvB,CAAC,GACJ,EAAwB,qBAAqB,CAC9C;;;;;;CAOD,MAAM,YACL,EAAE,OAAO,WACT,eACA,mBACkD;EAClD,MAAM,QAAQ,OAAO,UAAU,CAAC;EAChC,MAAM,WAAW,WAAW,OAAO,CAAC,OAAO,IAAI,CAAC;EAIhD,MAAM,QAAQ,mBAAmB,MAAM,QAAQ,OAAO,MAAM;EAG5D,MAAM,aAAa,oBAClB,UACA,QAAQ,YACR,QACA,QAAQ,cACR,OAAO,QACR;EAQA,MAAM,kBAAkB,OAAO,QAAQ,QAAQ,QAAQ,OAAO,QAAQ,YAAY;EAIlF,MAAM,cAAc,mBAAmB;GACtC,cAAc,MAAM,KAAK;GACV;GACC;GACC;EAClB,CAAC;EAUD,MAAM,eAAe,oBAAoB,OAAO,kBAAkB,cAAc,IAAI,KAAA,CAAS;EAI7F,MAAM,aACJ,OAAO,aAAa,WACpB,OAAO,QAAQ,aAAa,UAC7B,WAAW,aACX,aAAa;EACd,OAAO;GAEN,OAAO,MAAM,IAAI,yBAAyB;GAChC;GACV,MAAM;IAAE,GAAG,MAAM;IAAmB;IAAa,WAAW,aAAa,KAAA;GAAU;GACnF,SAAS,aAAa;GACtB,QAAQ,aAAa;GACrB,aAAa,WAAW,eAAgB,WAAW,QAAQ,SAAS,QAAS,KAAA;EAC9E;CACD;CAEA,MAAM,QAAQ,MAAM,SACnB,QAAQ,WACN,YAAY,OAAA,qBAAyD,KAAA,IAAY,aAAa,IAAI,EACrG;CACA,MAAM,SAAS,SAAS,OAAO,MAAM,QAAQ,MAAM;CAInD,MAAM,kBAAkB,MAAM,OAAO,QAAQ,QAAQ,QAAQ,MAAM,OAAO,QAAQ,YAAY;CAC9F,IAAI,QAAQ,UAAU,QAAQ,SAAS,KAAK,iBAAiB,OAAO;CAOpE,MAAM,iBAAiB,CAAC,GAAG,OAAO,QAAQ;CAC1C,IAAI,oBAAoB,OAAO,gBAAgB;CAC/C,IAAI,kBAAkB,OAAO,KAAK,cAAc;CAChD,IAAI,WAAW;CACf,KAAK,IAAI,cAAc,GAAG,eAAe,MAAM,eAAe;EAC7D,IAAI,CAAC,SAAS,WAAW,SAAS,UAAU,MAG3C,OAAO;GACN,OAAO,CAAC;GACR,UAAU;GACV,MAAM;IAAE,aAAa;IAAM,cAAc;IAAG,WAAW,mBAAmB,KAAA;GAAU;GACpF,SAAS;GACT,aAAa,qBAAqB,KAAA;EACnC;EAGD,MAAM,iBAAiB,SAAS;EAChC,MAAM,YAAY,SAAS,MAAM,SAAS,cAAc,GAAG,aAAa,cAAc;EACtF,KAAK,MAAM,WAAW,UAAU,UAC/B,qBAAqB,gBAAgB,OAAO;EAE7C,sBAAsB,UAAU,gBAAgB;EAChD,oBAAoB,UAAU,KAAK,cAAc;EACjD,IAAI,gBAAgB,MACnB,OAAO;GACN,GAAG;GACH,UAAU;GACV,MAAM;IAAE,GAAG,UAAU;IAAM,WAAW,mBAAmB,KAAA;GAAU;GACnE,aAAa,qBAAqB,KAAA;EACnC;EAGD,WAAW;CACZ;CAEA,OAAO;AACR"}
|