@gitkraken/core-gitlens 0.9.8 → 0.10.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 +29 -1
- package/dist/git/errors.d.ts +7 -0
- package/dist/git/errors.js +14 -1
- package/dist/git/errors.js.map +1 -1
- package/dist/git/providers/status.d.ts +6 -1
- package/dist/git/run.types.d.ts +9 -4
- package/dist/git-cli/exec/git.js +36 -16
- package/dist/git-cli/exec/git.js.map +1 -1
- package/dist/git-cli/providers/diff.d.ts +5 -2
- package/dist/git-cli/providers/diff.js +21 -9
- package/dist/git-cli/providers/diff.js.map +1 -1
- package/dist/git-cli/providers/patch.js +5 -7
- package/dist/git-cli/providers/patch.js.map +1 -1
- package/dist/git-cli/providers/status.d.ts +0 -1
- package/dist/git-cli/providers/status.js +119 -142
- package/dist/git-cli/providers/status.js.map +1 -1
- package/dist/plus/git-github/providers/github/status.d.ts +0 -1
- package/dist/plus/git-github/providers/github/status.js.map +1 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +7 -0
- package/dist/plus/integrations/collectionMetadata.js +11 -1
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/models/issueReads.d.ts +5 -0
- package/dist/plus/integrations/models/issuesIntegration.d.ts +37 -1
- package/dist/plus/integrations/models/issuesIntegration.js +118 -2
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/providers/jira-server.d.ts +8 -1
- package/dist/plus/integrations/providers/jira-server.js +90 -138
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +11 -1
- package/dist/plus/integrations/providers/jira.js +88 -122
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.d.ts +22 -1
- package/dist/plus/integrations/providers/linear.js +84 -35
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +5 -9
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +24 -3
- package/dist/plus/integrations/providers/providersApi.js +56 -4
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/utils/jiraIssueDrains.d.ts +86 -0
- package/dist/plus/integrations/providers/utils/jiraIssueDrains.js +139 -0
- package/dist/plus/integrations/providers/utils/jiraIssueDrains.js.map +1 -0
- package/dist/plus/integrations/reads/issueTracker.js +20 -9
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/utils/cancellation.d.ts +4 -1
- package/dist/utils/cancellation.js +4 -2
- package/dist/utils/cancellation.js.map +1 -1
- package/dist/utils/env/node/exec.d.ts +5 -1
- package/dist/utils/env/node/exec.js +8 -5
- package/dist/utils/env/node/exec.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ProviderReference } from "../../../../git/models/remoteProvider.js";
|
|
2
|
+
import { IssueShape } from "../../../../git/models/issue.js";
|
|
3
|
+
import { IssueFilter } from "../../providerFilters.js";
|
|
4
|
+
import { ProviderIssue } from "../models.js";
|
|
5
|
+
import { CollectionMetadata, CollectionScopeFailure } from "@gitkraken/provider-apis";
|
|
6
|
+
//#region ../plus/integrations/src/providers/utils/jiraIssueDrains.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* The issue drains Jira Cloud and Jira Server share. Their reads differ only in how a page is addressed (a site's
|
|
9
|
+
* `resourceId` vs an instance's `baseUrl`) and in which identity a user field takes, so the page fetch and the
|
|
10
|
+
* identities come in as arguments and the bookkeeping is written once.
|
|
11
|
+
*/
|
|
12
|
+
export type JiraUserScope = {
|
|
13
|
+
authorLogin?: string;
|
|
14
|
+
assigneeLogins?: string[];
|
|
15
|
+
mentionLogin?: string;
|
|
16
|
+
};
|
|
17
|
+
/** One page of a Jira issue search, whether it names one project or several. */
|
|
18
|
+
export type JiraIssuePage = {
|
|
19
|
+
data: ProviderIssue[];
|
|
20
|
+
hasMore: boolean;
|
|
21
|
+
nextCursor: string | undefined;
|
|
22
|
+
} | undefined;
|
|
23
|
+
export type JiraIssuePageFetcher = (scope: JiraUserScope, cursor: string | undefined) => Promise<JiraIssuePage>;
|
|
24
|
+
/**
|
|
25
|
+
* Three states rather than a boolean: the backstop is recoverable by narrowing the scope, a page error or a stalled
|
|
26
|
+
* cursor is not, and `ProjectIssuesDrain` makes the caller say which.
|
|
27
|
+
*/
|
|
28
|
+
export type JiraIssuesDrain = {
|
|
29
|
+
issues: ProviderIssue[];
|
|
30
|
+
status: 'complete' | 'backstop' | 'incomplete';
|
|
31
|
+
metadata?: CollectionMetadata;
|
|
32
|
+
};
|
|
33
|
+
export type JiraUserScopedRead = {
|
|
34
|
+
issues: ProviderIssue[];
|
|
35
|
+
truncated: boolean;
|
|
36
|
+
metadata?: CollectionMetadata;
|
|
37
|
+
};
|
|
38
|
+
export type JiraDrainOptions = {
|
|
39
|
+
maxPages: number;
|
|
40
|
+
/** Where a failure that stops the drain is recorded. */
|
|
41
|
+
failureScope: CollectionScopeFailure['scope'];
|
|
42
|
+
/** Names the provider in the failures this drain synthesizes. */
|
|
43
|
+
providerName: string;
|
|
44
|
+
/**
|
|
45
|
+
* A failure before anything was read that means "no issues here" rather than a failed read, e.g. a project this
|
|
46
|
+
* connection can no longer see (#5907). Returning true serves the drain as complete and empty.
|
|
47
|
+
*/
|
|
48
|
+
isEmptyFailure?: (ex: unknown) => boolean;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Follows a search's cursor up to `maxPages`. A page failure after the first page leaves the already-drained prefix
|
|
52
|
+
* intact and records the failure instead of re-throwing and discarding the prefix; if nothing was fetched yet, the
|
|
53
|
+
* throw propagates so the caller sees a hard error rather than an empty partial success.
|
|
54
|
+
*/
|
|
55
|
+
export declare function drainJiraIssues(fetchPage: JiraIssuePageFetcher, scope: JiraUserScope, options: JiraDrainOptions): Promise<JiraIssuesDrain>;
|
|
56
|
+
/**
|
|
57
|
+
* Runs one drain per requested relationship and keeps whatever succeeded. Throws only when every relationship failed,
|
|
58
|
+
* so the caller sees a hard error rather than an empty success.
|
|
59
|
+
*
|
|
60
|
+
* `identities` are the values each kind of clause takes, which the providers resolve differently: `userField` scopes
|
|
61
|
+
* `assignee`/`creator`, which are user fields, and `mention` scopes `comment ~ "..."`, a free-text search over comment
|
|
62
|
+
* bodies, which only a name can match.
|
|
63
|
+
*/
|
|
64
|
+
export declare function readUserScopedJiraIssues(fetchPage: JiraIssuePageFetcher, requestedFilters: readonly IssueFilter[] | undefined, identities: {
|
|
65
|
+
userField: string;
|
|
66
|
+
mention: string;
|
|
67
|
+
}, options: JiraDrainOptions): Promise<JiraUserScopedRead>;
|
|
68
|
+
/**
|
|
69
|
+
* Each relationship is its own search, so an issue matching two of them arrives twice. Every caller is a tracker
|
|
70
|
+
* project read, so the rows carry the `project` projection.
|
|
71
|
+
*/
|
|
72
|
+
export declare function toUniqueJiraIssueShapes(issues: readonly ProviderIssue[], provider: ProviderReference): IssueShape[];
|
|
73
|
+
/**
|
|
74
|
+
* Reads the current user's issues of several projects with one search per relationship, split back to the projects
|
|
75
|
+
* in `projectIds` order by each issue's `project.id`. Undefined when the search did not complete or an issue belongs
|
|
76
|
+
* to no requested project, so the caller reads them one by one: a search's pages are global to its project set, and
|
|
77
|
+
* an incomplete one says nothing about which of its projects it covered. Its failures are therefore never
|
|
78
|
+
* published, and the waste is bounded by one project's page budget, paid only by a search that needed more than
|
|
79
|
+
* that or failed partway.
|
|
80
|
+
*/
|
|
81
|
+
export declare function searchJiraProjectIssues(fetchPage: JiraIssuePageFetcher, projectIds: readonly string[], filters: readonly IssueFilter[] | undefined, identities: {
|
|
82
|
+
userField: string;
|
|
83
|
+
mention: string;
|
|
84
|
+
}, options: JiraDrainOptions, provider: ProviderReference): Promise<IssueShape[][] | undefined>;
|
|
85
|
+
//#endregion
|
|
86
|
+
//# sourceMappingURL=jiraIssueDrains.d.ts.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Logger as _logger } from "../../../../utils/logger.js";
|
|
2
|
+
import { throwIfCallerContractError, toCollectionScopeFailure } from "../../collectionMetadata.js";
|
|
3
|
+
import "../../providerFilters.js";
|
|
4
|
+
import { mergeCollectionMetadata } from "./providerPaging.js";
|
|
5
|
+
import { toIssueShape } from "../models.js";
|
|
6
|
+
import { splitProjectIssuesSearch } from "../../models/issuesIntegration.js";
|
|
7
|
+
//#region ../plus/integrations/src/providers/utils/jiraIssueDrains.ts
|
|
8
|
+
/**
|
|
9
|
+
* Follows a search's cursor up to `maxPages`. A page failure after the first page leaves the already-drained prefix
|
|
10
|
+
* intact and records the failure instead of re-throwing and discarding the prefix; if nothing was fetched yet, the
|
|
11
|
+
* throw propagates so the caller sees a hard error rather than an empty partial success.
|
|
12
|
+
*/
|
|
13
|
+
async function drainJiraIssues(fetchPage, scope, options) {
|
|
14
|
+
const issues = [];
|
|
15
|
+
let cursor;
|
|
16
|
+
let status = "complete";
|
|
17
|
+
let metadata;
|
|
18
|
+
const stopIncomplete = (ex) => {
|
|
19
|
+
status = "incomplete";
|
|
20
|
+
metadata = mergeCollectionMetadata(metadata, {
|
|
21
|
+
completeness: "partial",
|
|
22
|
+
failures: [toCollectionScopeFailure(options.failureScope, ex)]
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
for (let i = 0; i < options.maxPages; i++) {
|
|
26
|
+
let result;
|
|
27
|
+
try {
|
|
28
|
+
result = await fetchPage(scope, cursor);
|
|
29
|
+
} catch (ex) {
|
|
30
|
+
if (issues.length === 0) {
|
|
31
|
+
if (options.isEmptyFailure?.(ex)) return {
|
|
32
|
+
issues: [],
|
|
33
|
+
status: "complete"
|
|
34
|
+
};
|
|
35
|
+
throw ex;
|
|
36
|
+
}
|
|
37
|
+
stopIncomplete(ex);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
if (result == null) {
|
|
41
|
+
if (cursor == null) break;
|
|
42
|
+
stopIncomplete(/* @__PURE__ */ new Error(`${options.providerName} returned no page after advertising a continuation`));
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
issues.push(...result.data);
|
|
46
|
+
if (!result.hasMore) break;
|
|
47
|
+
if (result.nextCursor == null || result.nextCursor === cursor) {
|
|
48
|
+
stopIncomplete(/* @__PURE__ */ new Error(`${options.providerName} returned no advancing issue continuation`));
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
cursor = result.nextCursor;
|
|
52
|
+
if (i === options.maxPages - 1) status = "backstop";
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
issues,
|
|
56
|
+
status,
|
|
57
|
+
metadata
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Runs one drain per requested relationship and keeps whatever succeeded. Throws only when every relationship failed,
|
|
62
|
+
* so the caller sees a hard error rather than an empty success.
|
|
63
|
+
*
|
|
64
|
+
* `identities` are the values each kind of clause takes, which the providers resolve differently: `userField` scopes
|
|
65
|
+
* `assignee`/`creator`, which are user fields, and `mention` scopes `comment ~ "..."`, a free-text search over comment
|
|
66
|
+
* bodies, which only a name can match.
|
|
67
|
+
*/
|
|
68
|
+
async function readUserScopedJiraIssues(fetchPage, requestedFilters, identities, options) {
|
|
69
|
+
const filters = requestedFilters?.length ? requestedFilters : ["assignee"];
|
|
70
|
+
const settled = await Promise.allSettled(filters.map((filter) => drainJiraIssues(fetchPage, {
|
|
71
|
+
authorLogin: filter === "author" ? identities.userField : void 0,
|
|
72
|
+
assigneeLogins: filter === "assignee" ? [identities.userField] : void 0,
|
|
73
|
+
mentionLogin: filter === "mention" ? identities.mention : void 0
|
|
74
|
+
}, options)));
|
|
75
|
+
if (settled.every((r) => r.status === "rejected")) {
|
|
76
|
+
for (let i = 1; i < settled.length; i++) {
|
|
77
|
+
const outcome = settled[i];
|
|
78
|
+
if (outcome.status === "rejected") _logger.error(outcome.reason, `readUserScopedJiraIssues: filter '${filters[i]}' failed`);
|
|
79
|
+
}
|
|
80
|
+
throw settled[0].status === "rejected" ? settled[0].reason : /* @__PURE__ */ new Error(`${options.providerName} issue read failed`);
|
|
81
|
+
}
|
|
82
|
+
let truncated = false;
|
|
83
|
+
let metadata;
|
|
84
|
+
const issues = [];
|
|
85
|
+
for (let i = 0; i < settled.length; i++) {
|
|
86
|
+
const outcome = settled[i];
|
|
87
|
+
const filter = filters[i];
|
|
88
|
+
if (outcome.status !== "fulfilled") {
|
|
89
|
+
throwIfCallerContractError(outcome.reason);
|
|
90
|
+
truncated = true;
|
|
91
|
+
const failure = toCollectionScopeFailure(options.failureScope, outcome.reason);
|
|
92
|
+
metadata = mergeCollectionMetadata(metadata, {
|
|
93
|
+
completeness: "partial",
|
|
94
|
+
failures: [{
|
|
95
|
+
...failure,
|
|
96
|
+
message: `Issue filter '${filter}' could not be read${failure.message != null ? `: ${failure.message}` : ""}`
|
|
97
|
+
}]
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (outcome.value.status !== "complete") truncated = true;
|
|
102
|
+
if (outcome.value.metadata != null) metadata = mergeCollectionMetadata(metadata, outcome.value.metadata);
|
|
103
|
+
issues.push(...outcome.value.issues);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
issues,
|
|
107
|
+
truncated,
|
|
108
|
+
metadata
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Each relationship is its own search, so an issue matching two of them arrives twice. Every caller is a tracker
|
|
113
|
+
* project read, so the rows carry the `project` projection.
|
|
114
|
+
*/
|
|
115
|
+
function toUniqueJiraIssueShapes(issues, provider) {
|
|
116
|
+
const resultsById = /* @__PURE__ */ new Map();
|
|
117
|
+
for (const issue of issues) {
|
|
118
|
+
const shape = toIssueShape(issue, provider, { projection: "project" });
|
|
119
|
+
if (shape != null && !resultsById.has(shape.id)) resultsById.set(shape.id, shape);
|
|
120
|
+
}
|
|
121
|
+
return [...resultsById.values()];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Reads the current user's issues of several projects with one search per relationship, split back to the projects
|
|
125
|
+
* in `projectIds` order by each issue's `project.id`. Undefined when the search did not complete or an issue belongs
|
|
126
|
+
* to no requested project, so the caller reads them one by one: a search's pages are global to its project set, and
|
|
127
|
+
* an incomplete one says nothing about which of its projects it covered. Its failures are therefore never
|
|
128
|
+
* published, and the waste is bounded by one project's page budget, paid only by a search that needed more than
|
|
129
|
+
* that or failed partway.
|
|
130
|
+
*/
|
|
131
|
+
async function searchJiraProjectIssues(fetchPage, projectIds, filters, identities, options, provider) {
|
|
132
|
+
const read = await readUserScopedJiraIssues(fetchPage, filters, identities, options);
|
|
133
|
+
if (read.truncated) return void 0;
|
|
134
|
+
return splitProjectIssuesSearch(projectIds, read.issues, (issue) => issue.project?.id ?? void 0)?.map((issues) => toUniqueJiraIssueShapes(issues, provider));
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { drainJiraIssues, readUserScopedJiraIssues, searchJiraProjectIssues, toUniqueJiraIssueShapes };
|
|
138
|
+
|
|
139
|
+
//# sourceMappingURL=jiraIssueDrains.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jiraIssueDrains.js","names":[],"sources":["../../../../../../plus/integrations/src/providers/utils/jiraIssueDrains.ts"],"sourcesContent":["import type { CollectionMetadata, CollectionScopeFailure } from '@gitkraken/provider-apis';\nimport type { IssueShape } from '@gitlens/git/models/issue.js';\nimport type { ProviderReference } from '@gitlens/git/models/remoteProvider.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport { throwIfCallerContractError, toCollectionScopeFailure } from '../../collectionMetadata.js';\nimport { splitProjectIssuesSearch } from '../../models/issuesIntegration.js';\nimport { IssueFilter } from '../../providerFilters.js';\nimport type { ProviderIssue } from '../models.js';\nimport { toIssueShape } from '../models.js';\nimport { mergeCollectionMetadata } from './providerPaging.js';\n\n/**\n * The issue drains Jira Cloud and Jira Server share. Their reads differ only in how a page is addressed (a site's\n * `resourceId` vs an instance's `baseUrl`) and in which identity a user field takes, so the page fetch and the\n * identities come in as arguments and the bookkeeping is written once.\n */\n\nexport type JiraUserScope = { authorLogin?: string; assigneeLogins?: string[]; mentionLogin?: string };\n\n/** One page of a Jira issue search, whether it names one project or several. */\nexport type JiraIssuePage = { data: ProviderIssue[]; hasMore: boolean; nextCursor: string | undefined } | undefined;\nexport type JiraIssuePageFetcher = (scope: JiraUserScope, cursor: string | undefined) => Promise<JiraIssuePage>;\n\n/**\n * Three states rather than a boolean: the backstop is recoverable by narrowing the scope, a page error or a stalled\n * cursor is not, and `ProjectIssuesDrain` makes the caller say which.\n */\nexport type JiraIssuesDrain = {\n\tissues: ProviderIssue[];\n\tstatus: 'complete' | 'backstop' | 'incomplete';\n\tmetadata?: CollectionMetadata;\n};\n\nexport type JiraUserScopedRead = { issues: ProviderIssue[]; truncated: boolean; metadata?: CollectionMetadata };\n\nexport type JiraDrainOptions = {\n\tmaxPages: number;\n\t/** Where a failure that stops the drain is recorded. */\n\tfailureScope: CollectionScopeFailure['scope'];\n\t/** Names the provider in the failures this drain synthesizes. */\n\tproviderName: string;\n\t/**\n\t * A failure before anything was read that means \"no issues here\" rather than a failed read, e.g. a project this\n\t * connection can no longer see (#5907). Returning true serves the drain as complete and empty.\n\t */\n\tisEmptyFailure?: (ex: unknown) => boolean;\n};\n\n/**\n * Follows a search's cursor up to `maxPages`. A page failure after the first page leaves the already-drained prefix\n * intact and records the failure instead of re-throwing and discarding the prefix; if nothing was fetched yet, the\n * throw propagates so the caller sees a hard error rather than an empty partial success.\n */\nexport async function drainJiraIssues(\n\tfetchPage: JiraIssuePageFetcher,\n\tscope: JiraUserScope,\n\toptions: JiraDrainOptions,\n): Promise<JiraIssuesDrain> {\n\tconst issues: ProviderIssue[] = [];\n\tlet cursor: string | undefined;\n\tlet status: JiraIssuesDrain['status'] = 'complete';\n\tlet metadata: CollectionMetadata | undefined;\n\tconst stopIncomplete = (ex: unknown): void => {\n\t\tstatus = 'incomplete';\n\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\tcompleteness: 'partial',\n\t\t\tfailures: [toCollectionScopeFailure(options.failureScope, ex)],\n\t\t});\n\t};\n\tfor (let i = 0; i < options.maxPages; i++) {\n\t\tlet result: JiraIssuePage;\n\t\ttry {\n\t\t\tresult = await fetchPage(scope, cursor);\n\t\t} catch (ex) {\n\t\t\tif (issues.length === 0) {\n\t\t\t\tif (options.isEmptyFailure?.(ex)) return { issues: [], status: 'complete' };\n\n\t\t\t\tthrow ex;\n\t\t\t}\n\n\t\t\tstopIncomplete(ex);\n\t\t\tbreak;\n\t\t}\n\t\tif (result == null) {\n\t\t\tif (cursor == null) break;\n\n\t\t\tstopIncomplete(new Error(`${options.providerName} returned no page after advertising a continuation`));\n\t\t\tbreak;\n\t\t}\n\n\t\tissues.push(...result.data);\n\t\tif (!result.hasMore) break;\n\n\t\t// The provider claims more pages but gave no advancing cursor: we can't continue, so the drain\n\t\t// is incomplete — flag it rather than silently stopping (matches drainPullRequests/Repositories).\n\t\tif (result.nextCursor == null || result.nextCursor === cursor) {\n\t\t\tstopIncomplete(new Error(`${options.providerName} returned no advancing issue continuation`));\n\t\t\tbreak;\n\t\t}\n\n\t\tcursor = result.nextCursor;\n\t\t// Pages remain but this was the last allowed iteration: the backstop stopped the drain, which a\n\t\t// narrower scope would avoid.\n\t\tif (i === options.maxPages - 1) {\n\t\t\tstatus = 'backstop';\n\t\t}\n\t}\n\treturn { issues: issues, status: status, metadata: metadata };\n}\n\n/**\n * Runs one drain per requested relationship and keeps whatever succeeded. Throws only when every relationship failed,\n * so the caller sees a hard error rather than an empty success.\n *\n * `identities` are the values each kind of clause takes, which the providers resolve differently: `userField` scopes\n * `assignee`/`creator`, which are user fields, and `mention` scopes `comment ~ \"...\"`, a free-text search over comment\n * bodies, which only a name can match.\n */\nexport async function readUserScopedJiraIssues(\n\tfetchPage: JiraIssuePageFetcher,\n\trequestedFilters: readonly IssueFilter[] | undefined,\n\tidentities: { userField: string; mention: string },\n\toptions: JiraDrainOptions,\n): Promise<JiraUserScopedRead> {\n\t// A resolved user always scopes the read. Default to the assignee filter (\"my issues\") when no explicit filters\n\t// are given — otherwise a caller that scopes by user but omits filters would fall through to an unscoped fetch\n\t// and get every issue in the project instead of the user's (#5438).\n\tconst filters = requestedFilters?.length ? requestedFilters : [IssueFilter.Assignee];\n\tconst settled = await Promise.allSettled(\n\t\tfilters.map(filter =>\n\t\t\tdrainJiraIssues(\n\t\t\t\tfetchPage,\n\t\t\t\t{\n\t\t\t\t\tauthorLogin: filter === IssueFilter.Author ? identities.userField : undefined,\n\t\t\t\t\tassigneeLogins: filter === IssueFilter.Assignee ? [identities.userField] : undefined,\n\t\t\t\t\tmentionLogin: filter === IssueFilter.Mention ? identities.mention : undefined,\n\t\t\t\t},\n\t\t\t\toptions,\n\t\t\t),\n\t\t),\n\t);\n\n\t// If every filter branch rejected, the read failed outright — propagate the first rejection instead\n\t// of returning an empty list, which the facade (getIssuesForProjectResult → runCaptured) would\n\t// otherwise surface as a successful \"no issues\" rather than a warning + fetchFailed. The first\n\t// reason is re-thrown as-is (not wrapped in an AggregateError) so the facade can still classify it\n\t// by type (auth/rate-limit) — wrapping would collapse every failure to a generic 'other'. The\n\t// remaining reasons would otherwise be discarded, so log them here to keep them diagnosable.\n\tif (settled.every(r => r.status === 'rejected')) {\n\t\tfor (let i = 1; i < settled.length; i++) {\n\t\t\tconst outcome = settled[i];\n\t\t\tif (outcome.status === 'rejected') {\n\t\t\t\tLogger.error(outcome.reason, `readUserScopedJiraIssues: filter '${filters[i]}' failed`);\n\t\t\t}\n\t\t}\n\t\tthrow settled[0].status === 'rejected'\n\t\t\t? settled[0].reason\n\t\t\t: new Error(`${options.providerName} issue read failed`);\n\t}\n\n\tlet truncated = false;\n\tlet metadata: CollectionMetadata | undefined;\n\tconst issues: ProviderIssue[] = [];\n\tfor (let i = 0; i < settled.length; i++) {\n\t\tconst outcome = settled[i];\n\t\tconst filter = filters[i];\n\t\t// A rejected filter branch (with at least one sibling succeeding) means these issues are incomplete:\n\t\t// keep the sibling results but record a structured failure so the facade can warn on the specific\n\t\t// filter (auth/rate-limit) instead of just a generic truncation flag.\n\t\tif (outcome.status !== 'fulfilled') {\n\t\t\t// Identical for every filter branch, so degrading it would report one failure per branch for a\n\t\t\t// single invalid call — see `throwIfCallerContractError`.\n\t\t\tthrowIfCallerContractError(outcome.reason);\n\n\t\t\ttruncated = true;\n\t\t\tconst failure = toCollectionScopeFailure(options.failureScope, outcome.reason);\n\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\tcompleteness: 'partial',\n\t\t\t\tfailures: [\n\t\t\t\t\t{\n\t\t\t\t\t\t...failure,\n\t\t\t\t\t\tmessage: `Issue filter '${filter}' could not be read${\n\t\t\t\t\t\t\tfailure.message != null ? `: ${failure.message}` : ''\n\t\t\t\t\t\t}`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (outcome.value.status !== 'complete') {\n\t\t\ttruncated = true;\n\t\t}\n\t\tif (outcome.value.metadata != null) {\n\t\t\tmetadata = mergeCollectionMetadata(metadata, outcome.value.metadata);\n\t\t}\n\t\tissues.push(...outcome.value.issues);\n\t}\n\treturn { issues: issues, truncated: truncated, metadata: metadata };\n}\n\n/**\n * Each relationship is its own search, so an issue matching two of them arrives twice. Every caller is a tracker\n * project read, so the rows carry the `project` projection.\n */\nexport function toUniqueJiraIssueShapes(issues: readonly ProviderIssue[], provider: ProviderReference): IssueShape[] {\n\tconst resultsById = new Map<string, IssueShape>();\n\tfor (const issue of issues) {\n\t\tconst shape = toIssueShape(issue, provider, { projection: 'project' });\n\t\tif (shape != null && !resultsById.has(shape.id)) {\n\t\t\tresultsById.set(shape.id, shape);\n\t\t}\n\t}\n\treturn [...resultsById.values()];\n}\n\n/**\n * Reads the current user's issues of several projects with one search per relationship, split back to the projects\n * in `projectIds` order by each issue's `project.id`. Undefined when the search did not complete or an issue belongs\n * to no requested project, so the caller reads them one by one: a search's pages are global to its project set, and\n * an incomplete one says nothing about which of its projects it covered. Its failures are therefore never\n * published, and the waste is bounded by one project's page budget, paid only by a search that needed more than\n * that or failed partway.\n */\nexport async function searchJiraProjectIssues(\n\tfetchPage: JiraIssuePageFetcher,\n\tprojectIds: readonly string[],\n\tfilters: readonly IssueFilter[] | undefined,\n\tidentities: { userField: string; mention: string },\n\toptions: JiraDrainOptions,\n\tprovider: ProviderReference,\n): Promise<IssueShape[][] | undefined> {\n\tconst read = await readUserScopedJiraIssues(fetchPage, filters, identities, options);\n\tif (read.truncated) return undefined;\n\n\tconst split = splitProjectIssuesSearch(projectIds, read.issues, issue => issue.project?.id ?? undefined);\n\treturn split?.map(issues => toUniqueJiraIssueShapes(issues, provider));\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mapBounded } from "../../../utils/promise.js";
|
|
2
|
+
import { toError } from "../errors.js";
|
|
2
3
|
import "../constants.js";
|
|
3
4
|
import { appendDedupedWarning, reconcileOmissionsWithFailure } from "../results.js";
|
|
4
5
|
import { assessCollectionMetadata, mergeAssessmentInto } from "../collectionMetadata.js";
|
|
@@ -207,16 +208,26 @@ async function listIssueTrackerIssuesPage(ctx, options) {
|
|
|
207
208
|
warnings.push(unmergeableIssueSortWarning(options.providerId, domain, options.connectionId, ordering.unmergeable, scopedProjectsWithUsers.length > 1 ? "projects" : "filters"));
|
|
208
209
|
return emptyPage(true);
|
|
209
210
|
}
|
|
210
|
-
|
|
211
|
+
let projectReads;
|
|
212
|
+
try {
|
|
213
|
+
projectReads = await integration.getIssuesForProjectsWithTruncationResult(scopedProjects.map((project) => ({
|
|
214
|
+
project,
|
|
215
|
+
options: {
|
|
216
|
+
user: userForProject(project),
|
|
217
|
+
userId: userIdForProject(project),
|
|
218
|
+
filters: options.filters,
|
|
219
|
+
sort,
|
|
220
|
+
state: options.state
|
|
221
|
+
}
|
|
222
|
+
})), options.connectionId);
|
|
223
|
+
} catch (ex) {
|
|
224
|
+
const error = toError(ex);
|
|
225
|
+
projectReads = scopedProjects.map(() => ({ error }));
|
|
226
|
+
}
|
|
227
|
+
const perProject = await Promise.all(scopedProjects.map(async (project, index) => ({
|
|
211
228
|
project,
|
|
212
|
-
...await runCaptured(options.providerId, domain, options.connectionId, () =>
|
|
213
|
-
|
|
214
|
-
userId: userIdForProject(project),
|
|
215
|
-
filters: options.filters,
|
|
216
|
-
sort,
|
|
217
|
-
state: options.state
|
|
218
|
-
}, options.connectionId))
|
|
219
|
-
}));
|
|
229
|
+
...await runCaptured(options.providerId, domain, options.connectionId, () => Promise.resolve(projectReads[index]))
|
|
230
|
+
})));
|
|
220
231
|
let fetchFailed = projectDiscoveryFailed || accountLookupFailed;
|
|
221
232
|
let truncationRecovery = projectDiscoveryTruncated ? "none" : void 0;
|
|
222
233
|
let drainMetadata;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issueTracker.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issueTracker.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport type { IssueShape, IssueSorting, IssueStateFilter } from '@gitlens/git/models/issue.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport { assessCollectionMetadata, mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { providerFanOutConcurrency } from '../constants.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { IssueFilter, providersMetadata } from '../providers/models.js';\nimport { mergeCollectionMetadata, parsePageCursor } from '../providers/utils/providerPaging.js';\nimport type { ProviderPagedResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, reconcileOmissionsWithFailure } from '../results.js';\nimport { isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from './cursors.js';\nimport { runCaptured } from './drains.js';\nimport { projectKey, resourceIdForProject, resourceLabel, resourceMatchesOrg } from './hierarchy.utils.js';\nimport { resolveIssueSort, toIssueOrdering } from './ordering.js';\nimport {\n\tincompleteReadWarning,\n\tissueTrackerOnlySurfaceWarning,\n\totherWarning,\n\tunmergeableIssueSortWarning,\n\tunsupportedIssueSortWarning,\n\tunsupportedWarning,\n} from './warnings.js';\n\nconst issueStateFilters: readonly IssueStateFilter[] = ['open', 'closed', 'all'];\n\nexport async function listIssueTrackerIssuesPage(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\torg?: string;\n\t\tproject?: string;\n\t\tfilters?: IssueFilter[];\n\t\t/** Broadens the read to every assignee. Scopes to user-assigned issues when omitted. */\n\t\tincludeAllAssignees?: boolean;\n\t\t/**\n\t\t * How to order the issues, as `field:direction`. Omitted orders most-recently-updated-first where the\n\t\t * tracker can express it.\n\t\t *\n\t\t * Validated against `getSupportedFilters().issues`' sibling `issueSorts` — a tracker reports there, not\n\t\t * under the account-wide table, because resource -> project IS its only issue surface. A key it can't\n\t\t * express refuses the read rather than serving a differently-ordered list.\n\t\t *\n\t\t * One page can span SEVERAL projects, whose issues are merged here, so a key no normalized issue carries\n\t\t * (`priority`, `dueDate`, `resolved`) is refused for a multi-project page even though the tracker orders by\n\t\t * it perfectly well within one project.\n\t\t *\n\t\t * Unlike the git-host reads this one is safe to change mid-pagination: its cursor windows PROJECTS and\n\t\t * drains each to exhaustion, so which projects a round covers doesn't depend on how their issues are\n\t\t * ordered. Only the order within a page changes.\n\t\t */\n\t\tsort?: IssueSorting;\n\t\t/**\n\t\t * Which issue states to read. Omitted reads open issues only.\n\t\t *\n\t\t * Applied server-side by every project read, so it narrows each project's drain rather than the merged page.\n\t\t * Only a tracker declaring `ProviderMetadata.supportsIssueStates` can express anything but `'open'`; asking\n\t\t * another for `'closed'`/`'all'` refuses the read rather than serving its open issues.\n\t\t */\n\t\tstate?: IssueStateFilter;\n\t\tforceSync?: boolean;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\titemsPerPage?: number;\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed tracker host. Used to select the instance when the requested connection has no\n\t\t * configured domain; it must come from the trusted authentication configuration, never from repository\n\t\t * or remote data. Ignored for the cloud trackers.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<IssueShape>> {\n\t// Pagination is opt-in: only window the projects when the caller actually asked to page. A caller that\n\t// passes none of page/cursor/itemsPerPage keeps the \"aggregate every matched project\" contract, so an\n\t// existing consumer that doesn't inspect `hasMore` never silently loses projects past a default window.\n\tconst compositeCursor = parseIssueTrackerPageCursor(options.cursor);\n\tconst paginated =\n\t\tcompositeCursor?.unpaged !== true &&\n\t\t(options.page != null || options.cursor != null || options.itemsPerPage != null);\n\t// A composite cursor can carry retries from earlier project windows alongside the next untouched window.\n\t// `currentPage` remains the caller-facing position; `nextPage` is the window this round should advance.\n\tconst page = Math.max(\n\t\t1,\n\t\tMath.trunc(compositeCursor?.currentPage ?? parsePageCursor(options.cursor) ?? options.page ?? 1),\n\t);\n\tconst projectsPerPage = Math.max(1, Math.trunc(options.itemsPerPage ?? 20));\n\n\tconst items: IssueShape[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tconst emptyPage = (\n\t\tfetchFailed?: boolean,\n\t\ttruncated?: boolean,\n\t\tretry?: {\n\t\t\tnextPage?: number;\n\t\t\tpages?: readonly number[];\n\t\t\tprojects?: readonly string[];\n\t\t\tcompletedProjects?: readonly string[];\n\t\t},\n\t): ProviderPagedResult<IssueShape> => {\n\t\tconst cursor =\n\t\t\tretry != null\n\t\t\t\t? toIssueTrackerPageCursor({\n\t\t\t\t\t\tcurrentPage: page + 1,\n\t\t\t\t\t\tunpaged: !paginated,\n\t\t\t\t\t\tnextPage: retry.nextPage,\n\t\t\t\t\t\tretryPages: retry.pages ?? [],\n\t\t\t\t\t\tretryProjects: retry.projects ?? [],\n\t\t\t\t\t\tcompletedProjects: retry.completedProjects ?? [],\n\t\t\t\t\t\tstate: options.state,\n\t\t\t\t\t})\n\t\t\t\t: undefined;\n\t\treturn {\n\t\t\titems: items,\n\t\t\twarnings: warnings,\n\t\t\tpage: { currentPage: page, itemsPerPage: items.length, truncated: truncated || undefined },\n\t\t\t// Retry-only state is deliberately manual: advertising it as forward progress would make a\n\t\t\t// persistent provider failure spin forever in a normal `while (hasMore)` consumer. An untouched\n\t\t\t// project window remains normal forward progress even when this window failed completely.\n\t\t\thasMore: retry?.nextPage != null && cursor != null,\n\t\t\tcursor: cursor,\n\t\t\tfetchFailed: fetchFailed || undefined,\n\t\t};\n\t};\n\tconst normalWindowPage = paginated\n\t\t? (compositeCursor?.nextPage ?? (compositeCursor == null ? page : undefined))\n\t\t: undefined;\n\tconst priorRetryPages = compositeCursor?.retryPages ?? [];\n\tconst priorRetryProjects = compositeCursor?.retryProjects ?? [];\n\tconst priorCompletedProjects = compositeCursor?.completedProjects ?? [];\n\tconst trackCompletedProjects =\n\t\tpriorCompletedProjects.length > 0 || priorRetryPages.length > 0 || compositeCursor?.completedProjects != null;\n\tconst retryPagesForDiscoveryFailure = (): number[] => [\n\t\t...priorRetryPages,\n\t\t...(normalWindowPage != null ? [normalWindowPage] : compositeCursor == null ? [1] : []),\n\t];\n\tconst discoveryFailureRetry = (): {\n\t\tpages: readonly number[];\n\t\tprojects: readonly string[];\n\t\tcompletedProjects: readonly string[];\n\t} => ({\n\t\tpages: retryPagesForDiscoveryFailure(),\n\t\tprojects: priorRetryProjects,\n\t\tcompletedProjects: priorCompletedProjects,\n\t});\n\n\tif (!isIssuesHostIntegrationId(options.providerId)) {\n\t\twarnings.push(\n\t\t\tissueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, 'Issue-tracker project reads'),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connectionId or domain that no longer resolves is a broken target, not an empty account.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\twarnings.push(...early.warnings);\n\t\treturn emptyPage(early.fetchFailed);\n\t}\n\tif (!isIssuesIntegration(integration)) {\n\t\twarnings.push(\n\t\t\tissueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, 'Issue-tracker project reads'),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\t// Before any upstream request: the discovery fan-outs below (resources, projects, per-resource accounts) are\n\t// three round trips, and an order this tracker can't express refuses the read whatever they return. Placed after\n\t// `domain` only because every warning carries it.\n\tconst resolvedSort = resolveIssueSort(providersMetadata[options.providerId]?.supportedIssueSorts, options.sort);\n\tif (resolvedSort.rejection != null) {\n\t\twarnings.push(\n\t\t\tunsupportedIssueSortWarning(options.providerId, domain, options.connectionId, resolvedSort.rejection),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst sort = resolvedSort.sort;\n\n\t// Refused before any request for the same reason as the sort: a state the tracker can't express would otherwise\n\t// be dropped by its project reads, and their open issues published as the requested state. A value outside the\n\t// vocabulary is refused too, since `toProviderIssueStates` would read it as omitted, which means open.\n\tif (\n\t\toptions.state != null &&\n\t\t(!issueStateFilters.includes(options.state) ||\n\t\t\t(options.state !== 'open' && !providersMetadata[options.providerId]?.supportsIssueStates))\n\t) {\n\t\t// A known state this tracker can't express is an unsupported capability; a value outside the vocabulary is\n\t\t// caller input the caller fixes in the request.\n\t\twarnings.push(\n\t\t\tissueStateFilters.includes(options.state)\n\t\t\t\t? unsupportedWarning(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t`Issue state '${options.state}' is not supported by '${options.providerId}'.`,\n\t\t\t\t\t)\n\t\t\t\t: otherWarning(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t`Unknown issue state; expected one of ${issueStateFilters.join(', ')}.`,\n\t\t\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\t// A cursor resumed under another state than it was minted for skips what it already covered, whose other-state\n\t// issues were never read. A narrowed read only mints composite cursors, which record their state, so any other\n\t// cursor came from an open read.\n\tconst cursorState =\n\t\tcompositeCursor != null ? (compositeCursor.state ?? 'open') : options.cursor != null ? 'open' : undefined;\n\tif (cursorState != null && cursorState !== (options.state ?? 'open')) {\n\t\twarnings.push(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'The cursor belongs to a read of another issue state; start again without it.',\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);\n\n\tconst { value: resources, warning: resourcesWarning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() => integration.getResourcesForUserResult(options.connectionId),\n\t);\n\tif (resourcesWarning != null) {\n\t\twarnings.push(resourcesWarning);\n\t}\n\tif (resources == null || resources.length === 0) {\n\t\tconst fetchFailed = resourcesWarning != null && resources == null;\n\t\treturn emptyPage(fetchFailed, false, fetchFailed ? discoveryFailureRetry() : undefined);\n\t}\n\n\tconst scopedResources =\n\t\toptions.org != null ? resources.filter(r => resourceMatchesOrg(r, options.org!)) : resources;\n\tif (scopedResources.length === 0) {\n\t\treturn emptyPage();\n\t}\n\n\tconst { value: projectsResult, warning: projectsWarning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() => integration.getProjectsForResourcesWithMetadataResult(scopedResources, options.connectionId),\n\t);\n\tif (projectsWarning != null) {\n\t\twarnings.push(projectsWarning);\n\t}\n\t// Partial project discovery: continue with the resources that succeeded, but surface per-resource\n\t// failures as warnings and remember to mark the page fetchFailed so the caller knows some issues may be\n\t// missing. `projectDiscoveryFailed`/`projectDiscoveryTruncated` are OR-ed into the page's\n\t// fetchFailed/truncated at every return below (a truncated-but-not-failed discovery, e.g. a paging\n\t// backstop, still means the project set is incomplete).\n\tconst projectDiscoveryAssessment = mergeAssessmentInto(\n\t\twarnings,\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\tprojectsResult?.metadata,\n\t);\n\tconst projectDiscoveryFailed = projectDiscoveryAssessment.fetchFailed;\n\tconst projectDiscoveryTruncated = projectDiscoveryAssessment.truncated;\n\tconst projects = projectsResult?.values;\n\tif (projects == null || projects.length === 0) {\n\t\tconst fetchFailed = (projectsWarning != null && projectsResult == null) || projectDiscoveryFailed;\n\t\treturn emptyPage(\n\t\t\tfetchFailed,\n\t\t\tprojectDiscoveryTruncated,\n\t\t\tfetchFailed || projectDiscoveryTruncated ? discoveryFailureRetry() : undefined,\n\t\t);\n\t}\n\n\tconst matchedProjects =\n\t\toptions.project != null ? projects.filter(p => resourceMatchesOrg(p, options.project!)) : projects;\n\n\t// Validate the requested filters against what this provider supports (e.g. Linear/Trello support only\n\t// Assignee). An unsupported filter must not silently degrade — Linear/Trello ignore the requested type\n\t// and apply Assignee regardless — so warn + fetchFailed instead of returning a differently-scoped set.\n\tif (options.filters?.length) {\n\t\tconst supported = providersMetadata[options.providerId]?.supportedIssueFilters;\n\t\tconst allSupported = supported != null && options.filters.every(f => supported.includes(f));\n\t\tif (!allSupported) {\n\t\t\twarnings.push(\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\t`One or more requested issue filters are not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn emptyPage(true);\n\t\t}\n\t}\n\n\t// `includeAllAssignees` drops the user scope, but a user-relative filter (Author/Mention) is meaningless\n\t// without a user. Passing both to the provider degrades silently: Jira, seeing no user, falls through to\n\t// an unscoped project fetch and returns EVERY issue instead of the requested author's/mentions. Reject\n\t// the incompatible combination up front rather than publishing a differently-scoped set as the result.\n\tif (options.includeAllAssignees === true && options.filters?.some(f => f !== IssueFilter.Assignee)) {\n\t\twarnings.push(\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`\\`includeAllAssignees\\` cannot be combined with an author/mention filter for '${options.providerId}' (those filters require a user scope).`,\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\t// Scope to the current user's assigned issues unless the caller broadens to all assignees. Resolve the\n\t// handle from each resource's own account (multi-account safe), capturing any error so its kind\n\t// (e.g. auth) is preserved rather than collapsed to a generic warning.\n\tlet usersByResourceId: Map<string, string> | undefined;\n\t// The same accounts keyed by the same resource, holding the provider's stable id rather than the handle. A\n\t// separate map rather than a widened value because the two are resolved and consumed independently: a\n\t// provider may carry an account with no id, and `scopedProjectsWithUsers` gates on the HANDLE alone, so\n\t// folding them together would drop a resource whose id happened to be missing.\n\tlet userIdsByResourceId: Map<string, string> | undefined;\n\tlet accountLookupFailed = false;\n\tif (options.includeAllAssignees !== true) {\n\t\tusersByResourceId = new Map<string, string>();\n\t\tuserIdsByResourceId = new Map<string, string>();\n\t\tconst accounts = await mapBounded(scopedResources, providerFanOutConcurrency, async resource => ({\n\t\t\tresource: resource,\n\t\t\t...(await runCaptured(options.providerId, domain, options.connectionId, () =>\n\t\t\t\tintegration.getAccountForResourceResult(resource, options.connectionId),\n\t\t\t)),\n\t\t}));\n\n\t\tfor (const { resource, value: account, warning: accountWarning } of accounts) {\n\t\t\tconst user = account?.username ?? account?.name ?? undefined;\n\t\t\tif (user != null) {\n\t\t\t\tconst resourceKey = resourceIdForProject(resource) ?? resource.key;\n\t\t\t\tusersByResourceId.set(resourceKey, user);\n\t\t\t\t// Empty-checked, not just null-checked: an account shape declares `id` as a plain string, and a\n\t\t\t\t// provider with nothing to put there sets `''` rather than omitting it — which would otherwise\n\t\t\t\t// reach a user-field query as an empty identity and match nothing.\n\t\t\t\tif (account?.id != null && account.id !== '') {\n\t\t\t\t\tuserIdsByResourceId.set(resourceKey, account.id);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Deduped: a captured `accountWarning` carries only the provider error, with no resource in its\n\t\t\t// message, so one expired token across N resources would otherwise repeat verbatim N times. The\n\t\t\t// synthesized fallback names the resource, so those stay distinct on their own.\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\taccountWarning ??\n\t\t\t\t\totherWarning(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t`Could not resolve the current user for '${resourceLabel(resource)}'; skipping that resource to avoid returning issues assigned to others.`,\n\t\t\t\t\t),\n\t\t\t);\n\t\t\taccountLookupFailed = true;\n\t\t}\n\t}\n\n\tconst fallbackUserForUnscopedProject =\n\t\tscopedResources.length === 1 && usersByResourceId?.size === 1\n\t\t\t? usersByResourceId.values().next().value\n\t\t\t: undefined;\n\tconst userForProject = (project: ResourceDescriptor): string | undefined => {\n\t\tconst resourceId = resourceIdForProject(project);\n\t\tif (resourceId != null) {\n\t\t\tconst user = usersByResourceId?.get(resourceId);\n\t\t\tif (user != null) {\n\t\t\t\treturn user;\n\t\t\t}\n\t\t}\n\n\t\t// Some providers/tests return project descriptors without their parent resource id. When we have only\n\t\t// one scoped resource, re-use that sole resolved user rather than silently dropping every project.\n\t\treturn fallbackUserForUnscopedProject;\n\t};\n\t// Deliberately NOT falling back the way `userForProject` does. That fallback exists so a project with no\n\t// parent resource id still gets scoped rather than dropped, and the handle is what the drop is judged on;\n\t// the id is optional everywhere it is read, so a miss degrades to the handle instead of widening the read.\n\tconst userIdForProject = (project: ResourceDescriptor): string | undefined => {\n\t\tconst resourceId = resourceIdForProject(project);\n\t\treturn resourceId != null ? userIdsByResourceId?.get(resourceId) : undefined;\n\t};\n\n\tconst retryProjectKeys = new Set<string>();\n\tconst completedProjectKeys = new Set(priorCompletedProjects);\n\tfor (const retryProject of priorRetryProjects) {\n\t\tcompletedProjectKeys.delete(retryProject);\n\t}\n\tconst scopedProjectsWithUsers =\n\t\tusersByResourceId != null\n\t\t\t? matchedProjects.filter(project => {\n\t\t\t\t\tif (userForProject(project) != null) return true;\n\n\t\t\t\t\tconst key = projectKey(project);\n\t\t\t\t\tretryProjectKeys.add(key);\n\t\t\t\t\tcompletedProjectKeys.delete(key);\n\t\t\t\t\treturn false;\n\t\t\t\t})\n\t\t\t: matchedProjects;\n\n\t// Select the untouched window plus any earlier windows/projects that explicitly need retrying. Keys\n\t// include the parent resource so two trackers can reuse the same project id without collapsing.\n\tconst projectsByRetryKey = new Map(scopedProjectsWithUsers.map(project => [projectKey(project), project]));\n\tconst scopedProjectsByKey = new Map<string, ResourceDescriptor>();\n\tconst addScopedProject = (project: ResourceDescriptor): void => {\n\t\tconst key = projectKey(project);\n\t\tif (completedProjectKeys.has(key)) return;\n\n\t\tscopedProjectsByKey.set(key, project);\n\t};\n\tfor (const retryProject of priorRetryProjects) {\n\t\tconst project = projectsByRetryKey.get(retryProject);\n\t\tif (project != null) {\n\t\t\taddScopedProject(project);\n\t\t} else if (projectDiscoveryFailed || projectDiscoveryTruncated) {\n\t\t\tretryProjectKeys.add(retryProject);\n\t\t}\n\t}\n\tconst windowPages = new Set<number>(priorRetryPages);\n\tif (normalWindowPage != null) {\n\t\twindowPages.add(normalWindowPage);\n\t}\n\tif (!paginated && (projectDiscoveryFailed || projectDiscoveryTruncated)) {\n\t\twindowPages.add(1);\n\t}\n\tif (paginated) {\n\t\tfor (const windowPage of windowPages) {\n\t\t\tconst windowStart = (windowPage - 1) * projectsPerPage;\n\t\t\tfor (const project of scopedProjectsWithUsers.slice(windowStart, windowStart + projectsPerPage)) {\n\t\t\t\taddScopedProject(project);\n\t\t\t}\n\t\t}\n\t} else if (compositeCursor == null || priorRetryPages.length > 0) {\n\t\t// An unpaged retry-only cursor should re-read only the explicitly failed projects already added\n\t\t// above. Discovery retries still rescan the aggregate set so newly recovered projects are included.\n\t\tfor (const project of scopedProjectsWithUsers) {\n\t\t\taddScopedProject(project);\n\t\t}\n\t}\n\tconst scopedProjects = [...scopedProjectsByKey.values()];\n\tconst furthestWindowPage = windowPages.size > 0 ? Math.max(...windowPages) : normalWindowPage;\n\tconst normalWindowEnd =\n\t\tfurthestWindowPage != null ? furthestWindowPage * projectsPerPage : scopedProjectsWithUsers.length;\n\tconst nextPage =\n\t\tpaginated && furthestWindowPage != null && scopedProjectsWithUsers.length > normalWindowEnd\n\t\t\t? furthestWindowPage + 1\n\t\t\t: undefined;\n\tconst retryWindowPages = (): number[] => {\n\t\tif (!projectDiscoveryFailed && !projectDiscoveryTruncated && retryProjectKeys.size === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst pages = new Set(windowPages);\n\t\tif (pages.size === 0) {\n\t\t\t// A terminal retry has no untouched next window to identify its origin. Keep a manual window\n\t\t\t// marker so a later partial discovery can reconcile against the projects already emitted.\n\t\t\tpages.add(paginated ? Math.max(1, page - 1) : 1);\n\t\t}\n\t\treturn [...pages];\n\t};\n\tif (scopedProjects.length === 0) {\n\t\t// The discovered projects didn't intersect the requested filter/window, or every matching resource\n\t\t// failed user resolution. If discovery or account lookup was partial, the empty result is not a\n\t\t// proven-empty account — carry `fetchFailed` so the caller knows issues may be missing.\n\t\treturn emptyPage(projectDiscoveryFailed || accountLookupFailed, projectDiscoveryTruncated, {\n\t\t\tnextPage: nextPage,\n\t\t\tpages: retryWindowPages(),\n\t\t\tprojects: [...retryProjectKeys],\n\t\t\tcompletedProjects: [...completedProjectKeys],\n\t\t});\n\t}\n\n\t// A read spanning several projects is a merge, so it can only honor a key a normalized issue carries. Bound here\n\t// rather than next to the key's validation above, because the project count is only known now — while still\n\t// before any project read, so a refusal costs no request. Refused rather than served as concatenated\n\t// per-project runs, which would look ordered without being so.\n\t//\n\t// Counted over every project the READ covers, not the `scopedProjects` window this page happens to hold: the\n\t// window is `itemsPerPage` projects wide, so counting it would refuse a 20-project page and then serve the\n\t// final 1-project page of the same paginated call — the same key answered two ways, with the earlier pages'\n\t// issues missing from a read that reported success.\n\t//\n\t// Projects are not the only thing a tracker read merges: a user-scoped read asking for several relationships\n\t// issues one provider query PER relationship and unions them within each project (Jira runs one JQL drain per\n\t// filter and folds them into one map, in filter order), so a single-project page of two filters is just as\n\t// concatenated as a two-project one. Counting only projects published those runs under the requested key.\n\t// Not gated on the provider: the two trackers that don't fan out (Linear, Trello) declare `Assignee` as their\n\t// only supported filter, so a multi-filter read is already refused above and can never reach this.\n\tconst mergesFilters = options.includeAllAssignees !== true && (options.filters?.length ?? 0) > 1;\n\tconst ordering = toIssueOrdering(sort, scopedProjectsWithUsers.length > 1 || mergesFilters);\n\tif (ordering.unmergeable != null) {\n\t\twarnings.push(\n\t\t\tunmergeableIssueSortWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tordering.unmergeable,\n\t\t\t\tscopedProjectsWithUsers.length > 1 ? 'projects' : 'filters',\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst perProject = await mapBounded(scopedProjects, providerFanOutConcurrency, async project => ({\n\t\tproject: project,\n\t\t...(await runCaptured(options.providerId, domain, options.connectionId, () =>\n\t\t\tintegration.getIssuesForProjectWithTruncationResult(\n\t\t\t\tproject,\n\t\t\t\t{\n\t\t\t\t\tuser: userForProject(project),\n\t\t\t\t\tuserId: userIdForProject(project),\n\t\t\t\t\tfilters: options.filters,\n\t\t\t\t\tsort: sort,\n\t\t\t\t\tstate: options.state,\n\t\t\t\t},\n\t\t\t\toptions.connectionId,\n\t\t\t),\n\t\t)),\n\t}));\n\n\t// Partial project discovery means some projects' issues are missing from this page; propagate it so the\n\t// page reports fetchFailed even when every discovered project's own read succeeded.\n\tlet fetchFailed = projectDiscoveryFailed || accountLookupFailed;\n\tlet truncationRecovery: 'narrow-scope' | 'none' | undefined = projectDiscoveryTruncated ? 'none' : undefined;\n\tlet drainMetadata: CollectionMetadata | undefined;\n\tfor (const { project, value: result, warning } of perProject) {\n\t\tconst key = projectKey(project);\n\t\tconst retryProject = result == null;\n\t\tif (warning != null) {\n\t\t\t// Deduped: the per-project warning is built from the provider error alone and names no project, so a\n\t\t\t// single failing token repeats verbatim once per project in the window.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t\tfetchFailed = true;\n\t\t\ttruncationRecovery = 'none';\n\t\t}\n\t\t// A thrown/unsupported read (e.g. Linear not-implemented) surfaces as a warning with no value;\n\t\t// mark the aggregate as fetchFailed so an empty result isn't mistaken for \"no issues\".\n\t\tif (result == null) {\n\t\t\tfetchFailed = true;\n\t\t}\n\t\tif (result != null) {\n\t\t\titems.push(...result.values);\n\t\t\tif (result.truncated) {\n\t\t\t\tconst recovery = result.recovery;\n\t\t\t\ttruncationRecovery = truncationRecovery == null || truncationRecovery === recovery ? recovery : 'none';\n\t\t\t}\n\t\t\tif (result.metadata != null) {\n\t\t\t\tdrainMetadata = mergeCollectionMetadata(drainMetadata, result.metadata);\n\t\t\t}\n\t\t}\n\t\tif (retryProject) {\n\t\t\tretryProjectKeys.add(key);\n\t\t\tcompletedProjectKeys.delete(key);\n\t\t} else {\n\t\t\t// A usable partial/truncated project is emitted once and remains structurally incomplete.\n\t\t\t// Re-running the same project cursor would normally return the same prefix and duplicate it\n\t\t\t// across facade pages; only a project that returned no value is safe to retry automatically.\n\t\t\tcompletedProjectKeys.add(key);\n\t\t}\n\t}\n\n\t// Each provider query arrived ordered by the tracker; the union of them — across projects, and across the\n\t// per-relationship queries a multi-filter read fans out into — is not, and the union is what this read\n\t// publishes. A no-op when nothing merged, since the tracker already ordered that single run.\n\tconst orderedItems = ordering.order(items);\n\n\tconst projectReadReportedTruncation = truncationRecovery != null;\n\tconst drainAssessment = assessCollectionMetadata(options.providerId, domain, options.connectionId, drainMetadata);\n\tfor (const warning of drainAssessment.warnings) {\n\t\t// A project result already supplies the recovery semantics. Replace metadata's unstructured fallback with\n\t\t// the structured warning below, while retaining specific failures and omissions.\n\t\tif (projectReadReportedTruncation && !drainAssessment.fetchFailed && warning.omission == null) continue;\n\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\tfetchFailed = fetchFailed || drainAssessment.fetchFailed;\n\tif (drainAssessment.truncated) {\n\t\ttruncationRecovery = 'none';\n\t}\n\n\tconst projectTruncated = truncationRecovery != null;\n\tconst scopeTooLarge = !fetchFailed && truncationRecovery === 'narrow-scope';\n\tif (scopeTooLarge || (!fetchFailed && projectTruncated && !warnings.some(warning => warning.omission != null))) {\n\t\tappendDedupedWarning(\n\t\t\twarnings,\n\t\t\tincompleteReadWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tscopeTooLarge\n\t\t\t\t\t? 'Some projects hold more issues than one read can return; narrow the scope to read the rest.'\n\t\t\t\t\t: 'Some issues were omitted; the provider returned an incomplete result.',\n\t\t\t\tscopeTooLarge ? 'scope-too-large' : 'exhausted',\n\t\t\t),\n\t\t);\n\t}\n\t// A metadata omission from an earlier project asserts the read succeeded; a later one may since have failed.\n\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\n\tconst retryPages = retryWindowPages();\n\tconst cursor = toIssueTrackerPageCursor({\n\t\tcurrentPage: page + 1,\n\t\tunpaged: !paginated,\n\t\tnextPage: nextPage,\n\t\tretryPages: retryPages,\n\t\tretryProjects: [...retryProjectKeys],\n\t\tcompletedProjects:\n\t\t\t(trackCompletedProjects ||\n\t\t\t\tprojectDiscoveryFailed ||\n\t\t\t\tprojectDiscoveryTruncated ||\n\t\t\t\tretryProjectKeys.size > 0) &&\n\t\t\t(retryPages.length > 0 || nextPage != null)\n\t\t\t\t? [...completedProjectKeys]\n\t\t\t\t: [],\n\t\tstate: options.state,\n\t});\n\treturn {\n\t\titems: orderedItems,\n\t\twarnings: warnings,\n\t\tpage: { currentPage: page, itemsPerPage: orderedItems.length, truncated: projectTruncated || undefined },\n\t\t// Failed-project retries alone are manual. Only an untouched project window is automatic progress.\n\t\thasMore: nextPage != null && cursor != null,\n\t\tcursor: cursor,\n\t\tfetchFailed: fetchFailed || undefined,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"issueTracker.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issueTracker.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport type { IssueShape, IssueSorting, IssueStateFilter } from '@gitlens/git/models/issue.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport { assessCollectionMetadata, mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { providerFanOutConcurrency } from '../constants.js';\nimport { toError } from '../errors.js';\nimport type { IntegrationResult } from '../models/integration.js';\nimport type { ProjectIssuesDrain } from '../models/issueReads.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { IssueFilter, providersMetadata } from '../providers/models.js';\nimport { mergeCollectionMetadata, parsePageCursor } from '../providers/utils/providerPaging.js';\nimport type { ProviderPagedResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, reconcileOmissionsWithFailure } from '../results.js';\nimport { isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from './cursors.js';\nimport { runCaptured } from './drains.js';\nimport { projectKey, resourceIdForProject, resourceLabel, resourceMatchesOrg } from './hierarchy.utils.js';\nimport { resolveIssueSort, toIssueOrdering } from './ordering.js';\nimport {\n\tincompleteReadWarning,\n\tissueTrackerOnlySurfaceWarning,\n\totherWarning,\n\tunmergeableIssueSortWarning,\n\tunsupportedIssueSortWarning,\n\tunsupportedWarning,\n} from './warnings.js';\n\nconst issueStateFilters: readonly IssueStateFilter[] = ['open', 'closed', 'all'];\n\nexport async function listIssueTrackerIssuesPage(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\torg?: string;\n\t\tproject?: string;\n\t\tfilters?: IssueFilter[];\n\t\t/** Broadens the read to every assignee. Scopes to user-assigned issues when omitted. */\n\t\tincludeAllAssignees?: boolean;\n\t\t/**\n\t\t * How to order the issues, as `field:direction`. Omitted orders most-recently-updated-first where the\n\t\t * tracker can express it.\n\t\t *\n\t\t * Validated against `getSupportedFilters().issues`' sibling `issueSorts` — a tracker reports there, not\n\t\t * under the account-wide table, because resource -> project IS its only issue surface. A key it can't\n\t\t * express refuses the read rather than serving a differently-ordered list.\n\t\t *\n\t\t * One page can span SEVERAL projects, whose issues are merged here, so a key no normalized issue carries\n\t\t * (`priority`, `dueDate`, `resolved`) is refused for a multi-project page even though the tracker orders by\n\t\t * it perfectly well within one project.\n\t\t *\n\t\t * Unlike the git-host reads this one is safe to change mid-pagination: its cursor windows PROJECTS and\n\t\t * drains each to exhaustion, so which projects a round covers doesn't depend on how their issues are\n\t\t * ordered. Only the order within a page changes.\n\t\t */\n\t\tsort?: IssueSorting;\n\t\t/**\n\t\t * Which issue states to read. Omitted reads open issues only.\n\t\t *\n\t\t * Applied server-side by every project read, so it narrows each project's drain rather than the merged page.\n\t\t * Only a tracker declaring `ProviderMetadata.supportsIssueStates` can express anything but `'open'`; asking\n\t\t * another for `'closed'`/`'all'` refuses the read rather than serving its open issues.\n\t\t */\n\t\tstate?: IssueStateFilter;\n\t\tforceSync?: boolean;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\titemsPerPage?: number;\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed tracker host. Used to select the instance when the requested connection has no\n\t\t * configured domain; it must come from the trusted authentication configuration, never from repository\n\t\t * or remote data. Ignored for the cloud trackers.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<IssueShape>> {\n\t// Pagination is opt-in: only window the projects when the caller actually asked to page. A caller that\n\t// passes none of page/cursor/itemsPerPage keeps the \"aggregate every matched project\" contract, so an\n\t// existing consumer that doesn't inspect `hasMore` never silently loses projects past a default window.\n\tconst compositeCursor = parseIssueTrackerPageCursor(options.cursor);\n\tconst paginated =\n\t\tcompositeCursor?.unpaged !== true &&\n\t\t(options.page != null || options.cursor != null || options.itemsPerPage != null);\n\t// A composite cursor can carry retries from earlier project windows alongside the next untouched window.\n\t// `currentPage` remains the caller-facing position; `nextPage` is the window this round should advance.\n\tconst page = Math.max(\n\t\t1,\n\t\tMath.trunc(compositeCursor?.currentPage ?? parsePageCursor(options.cursor) ?? options.page ?? 1),\n\t);\n\tconst projectsPerPage = Math.max(1, Math.trunc(options.itemsPerPage ?? 20));\n\n\tconst items: IssueShape[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tconst emptyPage = (\n\t\tfetchFailed?: boolean,\n\t\ttruncated?: boolean,\n\t\tretry?: {\n\t\t\tnextPage?: number;\n\t\t\tpages?: readonly number[];\n\t\t\tprojects?: readonly string[];\n\t\t\tcompletedProjects?: readonly string[];\n\t\t},\n\t): ProviderPagedResult<IssueShape> => {\n\t\tconst cursor =\n\t\t\tretry != null\n\t\t\t\t? toIssueTrackerPageCursor({\n\t\t\t\t\t\tcurrentPage: page + 1,\n\t\t\t\t\t\tunpaged: !paginated,\n\t\t\t\t\t\tnextPage: retry.nextPage,\n\t\t\t\t\t\tretryPages: retry.pages ?? [],\n\t\t\t\t\t\tretryProjects: retry.projects ?? [],\n\t\t\t\t\t\tcompletedProjects: retry.completedProjects ?? [],\n\t\t\t\t\t\tstate: options.state,\n\t\t\t\t\t})\n\t\t\t\t: undefined;\n\t\treturn {\n\t\t\titems: items,\n\t\t\twarnings: warnings,\n\t\t\tpage: { currentPage: page, itemsPerPage: items.length, truncated: truncated || undefined },\n\t\t\t// Retry-only state is deliberately manual: advertising it as forward progress would make a\n\t\t\t// persistent provider failure spin forever in a normal `while (hasMore)` consumer. An untouched\n\t\t\t// project window remains normal forward progress even when this window failed completely.\n\t\t\thasMore: retry?.nextPage != null && cursor != null,\n\t\t\tcursor: cursor,\n\t\t\tfetchFailed: fetchFailed || undefined,\n\t\t};\n\t};\n\tconst normalWindowPage = paginated\n\t\t? (compositeCursor?.nextPage ?? (compositeCursor == null ? page : undefined))\n\t\t: undefined;\n\tconst priorRetryPages = compositeCursor?.retryPages ?? [];\n\tconst priorRetryProjects = compositeCursor?.retryProjects ?? [];\n\tconst priorCompletedProjects = compositeCursor?.completedProjects ?? [];\n\tconst trackCompletedProjects =\n\t\tpriorCompletedProjects.length > 0 || priorRetryPages.length > 0 || compositeCursor?.completedProjects != null;\n\tconst retryPagesForDiscoveryFailure = (): number[] => [\n\t\t...priorRetryPages,\n\t\t...(normalWindowPage != null ? [normalWindowPage] : compositeCursor == null ? [1] : []),\n\t];\n\tconst discoveryFailureRetry = (): {\n\t\tpages: readonly number[];\n\t\tprojects: readonly string[];\n\t\tcompletedProjects: readonly string[];\n\t} => ({\n\t\tpages: retryPagesForDiscoveryFailure(),\n\t\tprojects: priorRetryProjects,\n\t\tcompletedProjects: priorCompletedProjects,\n\t});\n\n\tif (!isIssuesHostIntegrationId(options.providerId)) {\n\t\twarnings.push(\n\t\t\tissueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, 'Issue-tracker project reads'),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connectionId or domain that no longer resolves is a broken target, not an empty account.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\twarnings.push(...early.warnings);\n\t\treturn emptyPage(early.fetchFailed);\n\t}\n\tif (!isIssuesIntegration(integration)) {\n\t\twarnings.push(\n\t\t\tissueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, 'Issue-tracker project reads'),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\t// Before any upstream request: the discovery fan-outs below (resources, projects, per-resource accounts) are\n\t// three round trips, and an order this tracker can't express refuses the read whatever they return. Placed after\n\t// `domain` only because every warning carries it.\n\tconst resolvedSort = resolveIssueSort(providersMetadata[options.providerId]?.supportedIssueSorts, options.sort);\n\tif (resolvedSort.rejection != null) {\n\t\twarnings.push(\n\t\t\tunsupportedIssueSortWarning(options.providerId, domain, options.connectionId, resolvedSort.rejection),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst sort = resolvedSort.sort;\n\n\t// Refused before any request for the same reason as the sort: a state the tracker can't express would otherwise\n\t// be dropped by its project reads, and their open issues published as the requested state. A value outside the\n\t// vocabulary is refused too, since `toProviderIssueStates` would read it as omitted, which means open.\n\tif (\n\t\toptions.state != null &&\n\t\t(!issueStateFilters.includes(options.state) ||\n\t\t\t(options.state !== 'open' && !providersMetadata[options.providerId]?.supportsIssueStates))\n\t) {\n\t\t// A known state this tracker can't express is an unsupported capability; a value outside the vocabulary is\n\t\t// caller input the caller fixes in the request.\n\t\twarnings.push(\n\t\t\tissueStateFilters.includes(options.state)\n\t\t\t\t? unsupportedWarning(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t`Issue state '${options.state}' is not supported by '${options.providerId}'.`,\n\t\t\t\t\t)\n\t\t\t\t: otherWarning(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t`Unknown issue state; expected one of ${issueStateFilters.join(', ')}.`,\n\t\t\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\t// A cursor resumed under another state than it was minted for skips what it already covered, whose other-state\n\t// issues were never read. A narrowed read only mints composite cursors, which record their state, so any other\n\t// cursor came from an open read.\n\tconst cursorState =\n\t\tcompositeCursor != null ? (compositeCursor.state ?? 'open') : options.cursor != null ? 'open' : undefined;\n\tif (cursorState != null && cursorState !== (options.state ?? 'open')) {\n\t\twarnings.push(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'The cursor belongs to a read of another issue state; start again without it.',\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);\n\n\tconst { value: resources, warning: resourcesWarning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() => integration.getResourcesForUserResult(options.connectionId),\n\t);\n\tif (resourcesWarning != null) {\n\t\twarnings.push(resourcesWarning);\n\t}\n\tif (resources == null || resources.length === 0) {\n\t\tconst fetchFailed = resourcesWarning != null && resources == null;\n\t\treturn emptyPage(fetchFailed, false, fetchFailed ? discoveryFailureRetry() : undefined);\n\t}\n\n\tconst scopedResources =\n\t\toptions.org != null ? resources.filter(r => resourceMatchesOrg(r, options.org!)) : resources;\n\tif (scopedResources.length === 0) {\n\t\treturn emptyPage();\n\t}\n\n\tconst { value: projectsResult, warning: projectsWarning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() => integration.getProjectsForResourcesWithMetadataResult(scopedResources, options.connectionId),\n\t);\n\tif (projectsWarning != null) {\n\t\twarnings.push(projectsWarning);\n\t}\n\t// Partial project discovery: continue with the resources that succeeded, but surface per-resource\n\t// failures as warnings and remember to mark the page fetchFailed so the caller knows some issues may be\n\t// missing. `projectDiscoveryFailed`/`projectDiscoveryTruncated` are OR-ed into the page's\n\t// fetchFailed/truncated at every return below (a truncated-but-not-failed discovery, e.g. a paging\n\t// backstop, still means the project set is incomplete).\n\tconst projectDiscoveryAssessment = mergeAssessmentInto(\n\t\twarnings,\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\tprojectsResult?.metadata,\n\t);\n\tconst projectDiscoveryFailed = projectDiscoveryAssessment.fetchFailed;\n\tconst projectDiscoveryTruncated = projectDiscoveryAssessment.truncated;\n\tconst projects = projectsResult?.values;\n\tif (projects == null || projects.length === 0) {\n\t\tconst fetchFailed = (projectsWarning != null && projectsResult == null) || projectDiscoveryFailed;\n\t\treturn emptyPage(\n\t\t\tfetchFailed,\n\t\t\tprojectDiscoveryTruncated,\n\t\t\tfetchFailed || projectDiscoveryTruncated ? discoveryFailureRetry() : undefined,\n\t\t);\n\t}\n\n\tconst matchedProjects =\n\t\toptions.project != null ? projects.filter(p => resourceMatchesOrg(p, options.project!)) : projects;\n\n\t// Validate the requested filters against what this provider supports (e.g. Linear/Trello support only\n\t// Assignee). An unsupported filter must not silently degrade — Linear/Trello ignore the requested type\n\t// and apply Assignee regardless — so warn + fetchFailed instead of returning a differently-scoped set.\n\tif (options.filters?.length) {\n\t\tconst supported = providersMetadata[options.providerId]?.supportedIssueFilters;\n\t\tconst allSupported = supported != null && options.filters.every(f => supported.includes(f));\n\t\tif (!allSupported) {\n\t\t\twarnings.push(\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\t`One or more requested issue filters are not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn emptyPage(true);\n\t\t}\n\t}\n\n\t// `includeAllAssignees` drops the user scope, but a user-relative filter (Author/Mention) is meaningless\n\t// without a user. Passing both to the provider degrades silently: Jira, seeing no user, falls through to\n\t// an unscoped project fetch and returns EVERY issue instead of the requested author's/mentions. Reject\n\t// the incompatible combination up front rather than publishing a differently-scoped set as the result.\n\tif (options.includeAllAssignees === true && options.filters?.some(f => f !== IssueFilter.Assignee)) {\n\t\twarnings.push(\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`\\`includeAllAssignees\\` cannot be combined with an author/mention filter for '${options.providerId}' (those filters require a user scope).`,\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\t// Scope to the current user's assigned issues unless the caller broadens to all assignees. Resolve the\n\t// handle from each resource's own account (multi-account safe), capturing any error so its kind\n\t// (e.g. auth) is preserved rather than collapsed to a generic warning.\n\tlet usersByResourceId: Map<string, string> | undefined;\n\t// The same accounts keyed by the same resource, holding the provider's stable id rather than the handle. A\n\t// separate map rather than a widened value because the two are resolved and consumed independently: a\n\t// provider may carry an account with no id, and `scopedProjectsWithUsers` gates on the HANDLE alone, so\n\t// folding them together would drop a resource whose id happened to be missing.\n\tlet userIdsByResourceId: Map<string, string> | undefined;\n\tlet accountLookupFailed = false;\n\tif (options.includeAllAssignees !== true) {\n\t\tusersByResourceId = new Map<string, string>();\n\t\tuserIdsByResourceId = new Map<string, string>();\n\t\tconst accounts = await mapBounded(scopedResources, providerFanOutConcurrency, async resource => ({\n\t\t\tresource: resource,\n\t\t\t...(await runCaptured(options.providerId, domain, options.connectionId, () =>\n\t\t\t\tintegration.getAccountForResourceResult(resource, options.connectionId),\n\t\t\t)),\n\t\t}));\n\n\t\tfor (const { resource, value: account, warning: accountWarning } of accounts) {\n\t\t\tconst user = account?.username ?? account?.name ?? undefined;\n\t\t\tif (user != null) {\n\t\t\t\tconst resourceKey = resourceIdForProject(resource) ?? resource.key;\n\t\t\t\tusersByResourceId.set(resourceKey, user);\n\t\t\t\t// Empty-checked, not just null-checked: an account shape declares `id` as a plain string, and a\n\t\t\t\t// provider with nothing to put there sets `''` rather than omitting it — which would otherwise\n\t\t\t\t// reach a user-field query as an empty identity and match nothing.\n\t\t\t\tif (account?.id != null && account.id !== '') {\n\t\t\t\t\tuserIdsByResourceId.set(resourceKey, account.id);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Deduped: a captured `accountWarning` carries only the provider error, with no resource in its\n\t\t\t// message, so one expired token across N resources would otherwise repeat verbatim N times. The\n\t\t\t// synthesized fallback names the resource, so those stay distinct on their own.\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\taccountWarning ??\n\t\t\t\t\totherWarning(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t`Could not resolve the current user for '${resourceLabel(resource)}'; skipping that resource to avoid returning issues assigned to others.`,\n\t\t\t\t\t),\n\t\t\t);\n\t\t\taccountLookupFailed = true;\n\t\t}\n\t}\n\n\tconst fallbackUserForUnscopedProject =\n\t\tscopedResources.length === 1 && usersByResourceId?.size === 1\n\t\t\t? usersByResourceId.values().next().value\n\t\t\t: undefined;\n\tconst userForProject = (project: ResourceDescriptor): string | undefined => {\n\t\tconst resourceId = resourceIdForProject(project);\n\t\tif (resourceId != null) {\n\t\t\tconst user = usersByResourceId?.get(resourceId);\n\t\t\tif (user != null) {\n\t\t\t\treturn user;\n\t\t\t}\n\t\t}\n\n\t\t// Some providers/tests return project descriptors without their parent resource id. When we have only\n\t\t// one scoped resource, re-use that sole resolved user rather than silently dropping every project.\n\t\treturn fallbackUserForUnscopedProject;\n\t};\n\t// Deliberately NOT falling back the way `userForProject` does. That fallback exists so a project with no\n\t// parent resource id still gets scoped rather than dropped, and the handle is what the drop is judged on;\n\t// the id is optional everywhere it is read, so a miss degrades to the handle instead of widening the read.\n\tconst userIdForProject = (project: ResourceDescriptor): string | undefined => {\n\t\tconst resourceId = resourceIdForProject(project);\n\t\treturn resourceId != null ? userIdsByResourceId?.get(resourceId) : undefined;\n\t};\n\n\tconst retryProjectKeys = new Set<string>();\n\tconst completedProjectKeys = new Set(priorCompletedProjects);\n\tfor (const retryProject of priorRetryProjects) {\n\t\tcompletedProjectKeys.delete(retryProject);\n\t}\n\tconst scopedProjectsWithUsers =\n\t\tusersByResourceId != null\n\t\t\t? matchedProjects.filter(project => {\n\t\t\t\t\tif (userForProject(project) != null) return true;\n\n\t\t\t\t\tconst key = projectKey(project);\n\t\t\t\t\tretryProjectKeys.add(key);\n\t\t\t\t\tcompletedProjectKeys.delete(key);\n\t\t\t\t\treturn false;\n\t\t\t\t})\n\t\t\t: matchedProjects;\n\n\t// Select the untouched window plus any earlier windows/projects that explicitly need retrying. Keys\n\t// include the parent resource so two trackers can reuse the same project id without collapsing.\n\tconst projectsByRetryKey = new Map(scopedProjectsWithUsers.map(project => [projectKey(project), project]));\n\tconst scopedProjectsByKey = new Map<string, ResourceDescriptor>();\n\tconst addScopedProject = (project: ResourceDescriptor): void => {\n\t\tconst key = projectKey(project);\n\t\tif (completedProjectKeys.has(key)) return;\n\n\t\tscopedProjectsByKey.set(key, project);\n\t};\n\tfor (const retryProject of priorRetryProjects) {\n\t\tconst project = projectsByRetryKey.get(retryProject);\n\t\tif (project != null) {\n\t\t\taddScopedProject(project);\n\t\t} else if (projectDiscoveryFailed || projectDiscoveryTruncated) {\n\t\t\tretryProjectKeys.add(retryProject);\n\t\t}\n\t}\n\tconst windowPages = new Set<number>(priorRetryPages);\n\tif (normalWindowPage != null) {\n\t\twindowPages.add(normalWindowPage);\n\t}\n\tif (!paginated && (projectDiscoveryFailed || projectDiscoveryTruncated)) {\n\t\twindowPages.add(1);\n\t}\n\tif (paginated) {\n\t\tfor (const windowPage of windowPages) {\n\t\t\tconst windowStart = (windowPage - 1) * projectsPerPage;\n\t\t\tfor (const project of scopedProjectsWithUsers.slice(windowStart, windowStart + projectsPerPage)) {\n\t\t\t\taddScopedProject(project);\n\t\t\t}\n\t\t}\n\t} else if (compositeCursor == null || priorRetryPages.length > 0) {\n\t\t// An unpaged retry-only cursor should re-read only the explicitly failed projects already added\n\t\t// above. Discovery retries still rescan the aggregate set so newly recovered projects are included.\n\t\tfor (const project of scopedProjectsWithUsers) {\n\t\t\taddScopedProject(project);\n\t\t}\n\t}\n\tconst scopedProjects = [...scopedProjectsByKey.values()];\n\tconst furthestWindowPage = windowPages.size > 0 ? Math.max(...windowPages) : normalWindowPage;\n\tconst normalWindowEnd =\n\t\tfurthestWindowPage != null ? furthestWindowPage * projectsPerPage : scopedProjectsWithUsers.length;\n\tconst nextPage =\n\t\tpaginated && furthestWindowPage != null && scopedProjectsWithUsers.length > normalWindowEnd\n\t\t\t? furthestWindowPage + 1\n\t\t\t: undefined;\n\tconst retryWindowPages = (): number[] => {\n\t\tif (!projectDiscoveryFailed && !projectDiscoveryTruncated && retryProjectKeys.size === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst pages = new Set(windowPages);\n\t\tif (pages.size === 0) {\n\t\t\t// A terminal retry has no untouched next window to identify its origin. Keep a manual window\n\t\t\t// marker so a later partial discovery can reconcile against the projects already emitted.\n\t\t\tpages.add(paginated ? Math.max(1, page - 1) : 1);\n\t\t}\n\t\treturn [...pages];\n\t};\n\tif (scopedProjects.length === 0) {\n\t\t// The discovered projects didn't intersect the requested filter/window, or every matching resource\n\t\t// failed user resolution. If discovery or account lookup was partial, the empty result is not a\n\t\t// proven-empty account — carry `fetchFailed` so the caller knows issues may be missing.\n\t\treturn emptyPage(projectDiscoveryFailed || accountLookupFailed, projectDiscoveryTruncated, {\n\t\t\tnextPage: nextPage,\n\t\t\tpages: retryWindowPages(),\n\t\t\tprojects: [...retryProjectKeys],\n\t\t\tcompletedProjects: [...completedProjectKeys],\n\t\t});\n\t}\n\n\t// A read spanning several projects is a merge, so it can only honor a key a normalized issue carries. Bound here\n\t// rather than next to the key's validation above, because the project count is only known now — while still\n\t// before any project read, so a refusal costs no request. Refused rather than served as concatenated\n\t// per-project runs, which would look ordered without being so.\n\t//\n\t// Counted over every project the READ covers, not the `scopedProjects` window this page happens to hold: the\n\t// window is `itemsPerPage` projects wide, so counting it would refuse a 20-project page and then serve the\n\t// final 1-project page of the same paginated call — the same key answered two ways, with the earlier pages'\n\t// issues missing from a read that reported success.\n\t//\n\t// Projects are not the only thing a tracker read merges: a user-scoped read asking for several relationships\n\t// issues one provider query PER relationship and unions them within each project (Jira runs one JQL drain per\n\t// filter and folds them into one map, in filter order), so a single-project page of two filters is just as\n\t// concatenated as a two-project one. Counting only projects published those runs under the requested key.\n\t// Not gated on the provider: the two trackers that don't fan out (Linear, Trello) declare `Assignee` as their\n\t// only supported filter, so a multi-filter read is already refused above and can never reach this.\n\tconst mergesFilters = options.includeAllAssignees !== true && (options.filters?.length ?? 0) > 1;\n\tconst ordering = toIssueOrdering(sort, scopedProjectsWithUsers.length > 1 || mergesFilters);\n\tif (ordering.unmergeable != null) {\n\t\twarnings.push(\n\t\t\tunmergeableIssueSortWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tordering.unmergeable,\n\t\t\t\tscopedProjectsWithUsers.length > 1 ? 'projects' : 'filters',\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\t// One call for the whole window, so a tracker that can search several projects at once (Jira) does, instead\n\t// of paying one drain per project. Results still come back per project, which the accounting below needs.\n\tlet projectReads: IntegrationResult<ProjectIssuesDrain | undefined>[];\n\ttry {\n\t\tprojectReads = await integration.getIssuesForProjectsWithTruncationResult(\n\t\t\tscopedProjects.map(project => ({\n\t\t\t\tproject: project,\n\t\t\t\toptions: {\n\t\t\t\t\tuser: userForProject(project),\n\t\t\t\t\tuserId: userIdForProject(project),\n\t\t\t\t\tfilters: options.filters,\n\t\t\t\t\tsort: sort,\n\t\t\t\t\tstate: options.state,\n\t\t\t\t},\n\t\t\t})),\n\t\t\toptions.connectionId,\n\t\t);\n\t} catch (ex) {\n\t\tconst error = toError(ex);\n\t\tprojectReads = scopedProjects.map(() => ({ error: error }));\n\t}\n\tconst perProject = await Promise.all(\n\t\tscopedProjects.map(async (project, index) => ({\n\t\t\tproject: project,\n\t\t\t...(await runCaptured(options.providerId, domain, options.connectionId, () =>\n\t\t\t\tPromise.resolve(projectReads[index]),\n\t\t\t)),\n\t\t})),\n\t);\n\n\t// Partial project discovery means some projects' issues are missing from this page; propagate it so the\n\t// page reports fetchFailed even when every discovered project's own read succeeded.\n\tlet fetchFailed = projectDiscoveryFailed || accountLookupFailed;\n\tlet truncationRecovery: 'narrow-scope' | 'none' | undefined = projectDiscoveryTruncated ? 'none' : undefined;\n\tlet drainMetadata: CollectionMetadata | undefined;\n\tfor (const { project, value: result, warning } of perProject) {\n\t\tconst key = projectKey(project);\n\t\tconst retryProject = result == null;\n\t\tif (warning != null) {\n\t\t\t// Deduped: the per-project warning is built from the provider error alone and names no project, so a\n\t\t\t// single failing token repeats verbatim once per project in the window.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t\tfetchFailed = true;\n\t\t\ttruncationRecovery = 'none';\n\t\t}\n\t\t// A thrown/unsupported read (e.g. Linear not-implemented) surfaces as a warning with no value;\n\t\t// mark the aggregate as fetchFailed so an empty result isn't mistaken for \"no issues\".\n\t\tif (result == null) {\n\t\t\tfetchFailed = true;\n\t\t}\n\t\tif (result != null) {\n\t\t\titems.push(...result.values);\n\t\t\tif (result.truncated) {\n\t\t\t\tconst recovery = result.recovery;\n\t\t\t\ttruncationRecovery = truncationRecovery == null || truncationRecovery === recovery ? recovery : 'none';\n\t\t\t}\n\t\t\tif (result.metadata != null) {\n\t\t\t\tdrainMetadata = mergeCollectionMetadata(drainMetadata, result.metadata);\n\t\t\t}\n\t\t}\n\t\tif (retryProject) {\n\t\t\tretryProjectKeys.add(key);\n\t\t\tcompletedProjectKeys.delete(key);\n\t\t} else {\n\t\t\t// A usable partial/truncated project is emitted once and remains structurally incomplete.\n\t\t\t// Re-running the same project cursor would normally return the same prefix and duplicate it\n\t\t\t// across facade pages; only a project that returned no value is safe to retry automatically.\n\t\t\tcompletedProjectKeys.add(key);\n\t\t}\n\t}\n\n\t// Each provider query arrived ordered by the tracker; the union of them — across projects, and across the\n\t// per-relationship queries a multi-filter read fans out into — is not, and the union is what this read\n\t// publishes. A no-op when nothing merged, since the tracker already ordered that single run.\n\tconst orderedItems = ordering.order(items);\n\n\tconst projectReadReportedTruncation = truncationRecovery != null;\n\tconst drainAssessment = assessCollectionMetadata(options.providerId, domain, options.connectionId, drainMetadata);\n\tfor (const warning of drainAssessment.warnings) {\n\t\t// A project result already supplies the recovery semantics. Replace metadata's unstructured fallback with\n\t\t// the structured warning below, while retaining specific failures and omissions.\n\t\tif (projectReadReportedTruncation && !drainAssessment.fetchFailed && warning.omission == null) continue;\n\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\tfetchFailed = fetchFailed || drainAssessment.fetchFailed;\n\tif (drainAssessment.truncated) {\n\t\ttruncationRecovery = 'none';\n\t}\n\n\tconst projectTruncated = truncationRecovery != null;\n\tconst scopeTooLarge = !fetchFailed && truncationRecovery === 'narrow-scope';\n\tif (scopeTooLarge || (!fetchFailed && projectTruncated && !warnings.some(warning => warning.omission != null))) {\n\t\tappendDedupedWarning(\n\t\t\twarnings,\n\t\t\tincompleteReadWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tscopeTooLarge\n\t\t\t\t\t? 'Some projects hold more issues than one read can return; narrow the scope to read the rest.'\n\t\t\t\t\t: 'Some issues were omitted; the provider returned an incomplete result.',\n\t\t\t\tscopeTooLarge ? 'scope-too-large' : 'exhausted',\n\t\t\t),\n\t\t);\n\t}\n\t// A metadata omission from an earlier project asserts the read succeeded; a later one may since have failed.\n\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\n\tconst retryPages = retryWindowPages();\n\tconst cursor = toIssueTrackerPageCursor({\n\t\tcurrentPage: page + 1,\n\t\tunpaged: !paginated,\n\t\tnextPage: nextPage,\n\t\tretryPages: retryPages,\n\t\tretryProjects: [...retryProjectKeys],\n\t\tcompletedProjects:\n\t\t\t(trackCompletedProjects ||\n\t\t\t\tprojectDiscoveryFailed ||\n\t\t\t\tprojectDiscoveryTruncated ||\n\t\t\t\tretryProjectKeys.size > 0) &&\n\t\t\t(retryPages.length > 0 || nextPage != null)\n\t\t\t\t? [...completedProjectKeys]\n\t\t\t\t: [],\n\t\tstate: options.state,\n\t});\n\treturn {\n\t\titems: orderedItems,\n\t\twarnings: warnings,\n\t\tpage: { currentPage: page, itemsPerPage: orderedItems.length, truncated: projectTruncated || undefined },\n\t\t// Failed-project retries alone are manual. Only an untouched project window is automatic progress.\n\t\thasMore: nextPage != null && cursor != null,\n\t\tcursor: cursor,\n\t\tfetchFailed: fetchFailed || undefined,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
//#region ../utils/src/cancellation.d.ts
|
|
2
|
+
/** Why an operation was cancelled: a caller abort, a timeout, or a termination whose cause is unknown. */
|
|
3
|
+
export type CancellationReason = 'aborted' | 'timeout' | 'unknown';
|
|
2
4
|
/**
|
|
3
5
|
* A standalone CancellationError for use in the library.
|
|
4
6
|
* Uses a symbol brand so that both this class and the extension's
|
|
@@ -7,8 +9,9 @@
|
|
|
7
9
|
*/
|
|
8
10
|
export declare class CancellationError extends Error {
|
|
9
11
|
readonly original?: Error | undefined;
|
|
12
|
+
readonly reason?: CancellationReason | undefined;
|
|
10
13
|
private readonly _cancellationErrorBrand;
|
|
11
|
-
constructor(original?: Error | undefined);
|
|
14
|
+
constructor(original?: Error | undefined, reason?: CancellationReason | undefined);
|
|
12
15
|
}
|
|
13
16
|
export declare function isCancellationError(ex: unknown): ex is CancellationError;
|
|
14
17
|
/**
|
|
@@ -9,10 +9,12 @@ const cancellationErrorBrand = Symbol.for("CancellationError");
|
|
|
9
9
|
*/
|
|
10
10
|
var CancellationError = class extends Error {
|
|
11
11
|
original;
|
|
12
|
+
reason;
|
|
12
13
|
_cancellationErrorBrand = true;
|
|
13
|
-
constructor(original) {
|
|
14
|
+
constructor(original, reason) {
|
|
14
15
|
super();
|
|
15
16
|
this.original = original;
|
|
17
|
+
this.reason = reason;
|
|
16
18
|
Object.defineProperty(this, cancellationErrorBrand, {
|
|
17
19
|
value: true,
|
|
18
20
|
writable: true,
|
|
@@ -61,7 +63,7 @@ function raceWithSignal(promise, signal) {
|
|
|
61
63
|
function raceWithTimeout(promise, ms, abortOnTimeout) {
|
|
62
64
|
return new Promise((resolve, reject) => {
|
|
63
65
|
const timer = setTimeout(() => {
|
|
64
|
-
const error = new CancellationError(/* @__PURE__ */ new Error(`Timed out after ${ms}ms`));
|
|
66
|
+
const error = new CancellationError(/* @__PURE__ */ new Error(`Timed out after ${ms}ms`), "timeout");
|
|
65
67
|
abortOnTimeout?.abort(error);
|
|
66
68
|
reject(error);
|
|
67
69
|
}, ms);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cancellation.js","names":[],"sources":["../../../utils/src/cancellation.ts"],"sourcesContent":["import { getScopedCounter } from './counter.js';\n\nconst cancellationErrorBrand = Symbol.for('CancellationError');\n\n/**\n * A standalone CancellationError for use in the library.\n * Uses a symbol brand so that both this class and the extension's\n * CancellationError (which extends vscode.CancellationError) are\n * recognized by `isCancellationError`.\n */\nexport class CancellationError extends Error {\n\t// TS-only nominal brand (not read anywhere) — without it, `CancellationError` has the same\n\t// structural shape as any other `Error` subtype, so `isCancellationError`'s negative narrowing\n\t// (`if (isCancellationError(ex)) throw ex;`) collapses an unrelated error union to `never` instead\n\t// of leaving it narrowed. The runtime brand stays a symbol (below) for the cross-realm check.\n\tprivate readonly _cancellationErrorBrand: true = true;\n\n\tconstructor(public readonly original?: Error) {\n\t\tsuper();\n\t\t// A symbol-keyed computed class field can't be typed for `isolatedDeclarations` (only well-known\n\t\t// symbols like `Symbol.iterator` qualify), so the brand is set here with the same runtime shape\n\t\t// (writable/enumerable/configurable) a plain field assignment would produce.\n\t\tObject.defineProperty(this, cancellationErrorBrand, {\n\t\t\tvalue: true,\n\t\t\twritable: true,\n\t\t\tenumerable: true,\n\t\t\tconfigurable: true,\n\t\t});\n\t\tthis.name = 'CancellationError';\n\n\t\tif (this.original) {\n\t\t\tif (this.original.message.startsWith('Operation cancelled')) {\n\t\t\t\tthis.message = this.original.message;\n\t\t\t} else {\n\t\t\t\tthis.message = `Operation cancelled; ${this.original.message}`;\n\t\t\t}\n\t\t} else {\n\t\t\tthis.message = 'Operation cancelled';\n\t\t}\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n}\n\nexport function isCancellationError(ex: unknown): ex is CancellationError {\n\treturn (\n\t\t(ex instanceof Error && (cancellationErrorBrand in ex || ex.name === 'CancellationError')) ||\n\t\t(ex instanceof DOMException && ex.name === 'AbortError')\n\t);\n}\n\n/**\n * Races a promise against an `AbortSignal`. If the signal fires before\n * the promise settles, the returned promise rejects with `CancellationError`.\n * If the promise settles first, the signal listener is cleaned up.\n */\nexport function raceWithSignal<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {\n\tif (signal.aborted) return Promise.reject(new CancellationError());\n\n\treturn new Promise<T>((resolve, reject) => {\n\t\tconst onAbort = () => reject(new CancellationError());\n\t\tsignal.addEventListener('abort', onAbort, { once: true });\n\t\tpromise.then(\n\t\t\tv => {\n\t\t\t\tsignal.removeEventListener('abort', onAbort);\n\t\t\t\tresolve(v);\n\t\t\t},\n\t\t\t(e: unknown) => {\n\t\t\t\tsignal.removeEventListener('abort', onAbort);\n\t\t\t\treject(e instanceof Error ? e : new Error(String(e)));\n\t\t\t},\n\t\t);\n\t});\n}\n\n/**\n * Races a promise against a timeout. If `ms` elapses before the promise settles, the returned promise\n * rejects with `CancellationError`. If the promise settles first, the timer is cleared. Parallels\n * `raceWithSignal`, with a timer standing in for the signal.\n *\n * When `abortOnTimeout` is provided, it is aborted on timeout — so a caller can link the underlying operation to\n * it (e.g. via `AbortSignal.any`) and have it torn down instead of orphaned. Abort is best-effort (a truly-stuck\n * op may ignore it); the returned promise rejects on timeout regardless of whether the underlying op settles.\n */\nexport function raceWithTimeout<T>(promise: Promise<T>, ms: number, abortOnTimeout?: AbortController): Promise<T> {\n\treturn new Promise<T>((resolve, reject) => {\n\t\tconst timer = setTimeout(() => {\n\t\t\tconst error = new CancellationError(new Error(`Timed out after ${ms}ms`));\n\t\t\tabortOnTimeout?.abort(error);\n\t\t\treject(error);\n\t\t}, ms);\n\t\t// Don't let the timeout timer keep a (Node) process alive on its own — a still-pending promise shouldn't\n\t\t// hold the event loop open just for its backstop (no-op in the browser, where setTimeout returns a number).\n\t\tif (typeof timer !== 'number') {\n\t\t\ttimer.unref();\n\t\t}\n\t\tpromise.then(\n\t\t\tv => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tresolve(v);\n\t\t\t},\n\t\t\t(e: unknown) => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\treject(e instanceof Error ? e : new Error(String(e)));\n\t\t\t},\n\t\t);\n\t});\n}\n\n/**\n * Aggregates multiple caller `AbortSignal`s into a single signal that\n * only fires when **all** callers have cancelled (the inverse of `AbortSignal.any()`).\n *\n * Designed for shared-promise caching: each caller registers their signal,\n * and the underlying operation receives the aggregate signal. Individual callers\n * can cancel early (via `raceWithSignal`), but the operation continues as long\n * as at least one caller is still active.\n */\nexport class AbortAggregate {\n\tprivate readonly controller = new AbortController();\n\tprivate activeCount = 0;\n\t/** One listener-remover per registration (NOT keyed by signal), so adding the same signal twice is safe. */\n\tprivate readonly cleanups = new Set<() => void>();\n\n\t/** The aggregate signal — only fires when all callers have cancelled. */\n\tget signal(): AbortSignal {\n\t\treturn this.controller.signal;\n\t}\n\n\t/**\n\t * Register a caller.\n\t * - If `cancellation` is provided, the caller is auto-removed when it fires.\n\t * - If `cancellation` is `undefined`, the caller is \"permanent\" — the aggregate\n\t * can never fire while it's active. Call the returned cleanup on promise settle.\n\t * - The same signal instance may safely be added more than once — each registration is tracked and cleaned up\n\t * independently, so no listener leaks.\n\t * @returns A cleanup function to unregister the caller.\n\t */\n\tadd(cancellation?: AbortSignal): () => void {\n\t\tthis.activeCount++;\n\n\t\tif (cancellation == null) {\n\t\t\treturn () => {\n\t\t\t\tthis.activeCount--;\n\t\t\t\tthis.checkAbort();\n\t\t\t};\n\t\t}\n\n\t\tif (cancellation.aborted) {\n\t\t\tthis.activeCount--;\n\t\t\tthis.checkAbort();\n\t\t\treturn () => {};\n\t\t}\n\n\t\t// Track this registration's listener removal individually (a Set of removers, NOT keyed by the signal) so\n\t\t// adding the same signal twice can't overwrite the other's cleanup. `settled` keeps both exit paths (abort\n\t\t// vs. manual cleanup) idempotent and preserves the accounting: only `onAbort` decrements `activeCount`.\n\t\tlet settled = false;\n\t\tlet remove: () => void;\n\t\tconst onAbort = () => {\n\t\t\tif (settled) return;\n\n\t\t\tsettled = true;\n\t\t\tthis.cleanups.delete(remove);\n\t\t\tthis.activeCount--;\n\t\t\tthis.checkAbort();\n\t\t};\n\t\tremove = () => cancellation.removeEventListener('abort', onAbort);\n\t\tcancellation.addEventListener('abort', onAbort, { once: true });\n\t\tthis.cleanups.add(remove);\n\n\t\treturn () => {\n\t\t\tif (settled) return;\n\n\t\t\tsettled = true;\n\t\t\tthis.cleanups.delete(remove);\n\t\t\tremove();\n\t\t};\n\t}\n\n\tprivate checkAbort(): void {\n\t\tif (this.activeCount <= 0 && !this.controller.signal.aborted) {\n\t\t\tthis.controller.abort();\n\t\t}\n\t}\n\n\t/** Remove all abort listeners. Call when the cached promise settles. */\n\tdispose(): void {\n\t\tfor (const remove of this.cleanups) {\n\t\t\tremove();\n\t\t}\n\t\tthis.cleanups.clear();\n\t}\n}\n\nconst signalIds = new WeakMap<AbortSignal, number>();\nconst signalIdCounter = getScopedCounter();\n\n/**\n * Returns a stable, unique numeric ID for the given AbortSignal.\n * Used to differentiate concurrent git commands that have different\n * cancellation signals, so one caller aborting doesn't cancel another.\n */\nexport function getAbortSignalId(signal: AbortSignal | undefined): string {\n\tif (signal == null) return '';\n\n\tlet id = signalIds.get(signal);\n\tif (id == null) {\n\t\tid = signalIdCounter.next();\n\t\tsignalIds.set(signal, id);\n\t}\n\treturn String(id);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cancellation.js","names":[],"sources":["../../../utils/src/cancellation.ts"],"sourcesContent":["import { getScopedCounter } from './counter.js';\n\nconst cancellationErrorBrand = Symbol.for('CancellationError');\n\n/** Why an operation was cancelled: a caller abort, a timeout, or a termination whose cause is unknown. */\nexport type CancellationReason = 'aborted' | 'timeout' | 'unknown';\n\n/**\n * A standalone CancellationError for use in the library.\n * Uses a symbol brand so that both this class and the extension's\n * CancellationError (which extends vscode.CancellationError) are\n * recognized by `isCancellationError`.\n */\nexport class CancellationError extends Error {\n\t// TS-only nominal brand (not read anywhere) — without it, `CancellationError` has the same\n\t// structural shape as any other `Error` subtype, so `isCancellationError`'s negative narrowing\n\t// (`if (isCancellationError(ex)) throw ex;`) collapses an unrelated error union to `never` instead\n\t// of leaving it narrowed. The runtime brand stays a symbol (below) for the cross-realm check.\n\tprivate readonly _cancellationErrorBrand: true = true;\n\n\tconstructor(\n\t\tpublic readonly original?: Error,\n\t\tpublic readonly reason?: CancellationReason,\n\t) {\n\t\tsuper();\n\t\t// A symbol-keyed computed class field can't be typed for `isolatedDeclarations` (only well-known\n\t\t// symbols like `Symbol.iterator` qualify), so the brand is set here with the same runtime shape\n\t\t// (writable/enumerable/configurable) a plain field assignment would produce.\n\t\tObject.defineProperty(this, cancellationErrorBrand, {\n\t\t\tvalue: true,\n\t\t\twritable: true,\n\t\t\tenumerable: true,\n\t\t\tconfigurable: true,\n\t\t});\n\t\tthis.name = 'CancellationError';\n\n\t\tif (this.original) {\n\t\t\tif (this.original.message.startsWith('Operation cancelled')) {\n\t\t\t\tthis.message = this.original.message;\n\t\t\t} else {\n\t\t\t\tthis.message = `Operation cancelled; ${this.original.message}`;\n\t\t\t}\n\t\t} else {\n\t\t\tthis.message = 'Operation cancelled';\n\t\t}\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n}\n\nexport function isCancellationError(ex: unknown): ex is CancellationError {\n\treturn (\n\t\t(ex instanceof Error && (cancellationErrorBrand in ex || ex.name === 'CancellationError')) ||\n\t\t(ex instanceof DOMException && ex.name === 'AbortError')\n\t);\n}\n\n/**\n * Races a promise against an `AbortSignal`. If the signal fires before\n * the promise settles, the returned promise rejects with `CancellationError`.\n * If the promise settles first, the signal listener is cleaned up.\n */\nexport function raceWithSignal<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {\n\tif (signal.aborted) return Promise.reject(new CancellationError());\n\n\treturn new Promise<T>((resolve, reject) => {\n\t\tconst onAbort = () => reject(new CancellationError());\n\t\tsignal.addEventListener('abort', onAbort, { once: true });\n\t\tpromise.then(\n\t\t\tv => {\n\t\t\t\tsignal.removeEventListener('abort', onAbort);\n\t\t\t\tresolve(v);\n\t\t\t},\n\t\t\t(e: unknown) => {\n\t\t\t\tsignal.removeEventListener('abort', onAbort);\n\t\t\t\treject(e instanceof Error ? e : new Error(String(e)));\n\t\t\t},\n\t\t);\n\t});\n}\n\n/**\n * Races a promise against a timeout. If `ms` elapses before the promise settles, the returned promise\n * rejects with `CancellationError`. If the promise settles first, the timer is cleared. Parallels\n * `raceWithSignal`, with a timer standing in for the signal.\n *\n * When `abortOnTimeout` is provided, it is aborted on timeout — so a caller can link the underlying operation to\n * it (e.g. via `AbortSignal.any`) and have it torn down instead of orphaned. Abort is best-effort (a truly-stuck\n * op may ignore it); the returned promise rejects on timeout regardless of whether the underlying op settles.\n */\nexport function raceWithTimeout<T>(promise: Promise<T>, ms: number, abortOnTimeout?: AbortController): Promise<T> {\n\treturn new Promise<T>((resolve, reject) => {\n\t\tconst timer = setTimeout(() => {\n\t\t\tconst error = new CancellationError(new Error(`Timed out after ${ms}ms`), 'timeout');\n\t\t\tabortOnTimeout?.abort(error);\n\t\t\treject(error);\n\t\t}, ms);\n\t\t// Don't let the timeout timer keep a (Node) process alive on its own — a still-pending promise shouldn't\n\t\t// hold the event loop open just for its backstop (no-op in the browser, where setTimeout returns a number).\n\t\tif (typeof timer !== 'number') {\n\t\t\ttimer.unref();\n\t\t}\n\t\tpromise.then(\n\t\t\tv => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tresolve(v);\n\t\t\t},\n\t\t\t(e: unknown) => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\treject(e instanceof Error ? e : new Error(String(e)));\n\t\t\t},\n\t\t);\n\t});\n}\n\n/**\n * Aggregates multiple caller `AbortSignal`s into a single signal that\n * only fires when **all** callers have cancelled (the inverse of `AbortSignal.any()`).\n *\n * Designed for shared-promise caching: each caller registers their signal,\n * and the underlying operation receives the aggregate signal. Individual callers\n * can cancel early (via `raceWithSignal`), but the operation continues as long\n * as at least one caller is still active.\n */\nexport class AbortAggregate {\n\tprivate readonly controller = new AbortController();\n\tprivate activeCount = 0;\n\t/** One listener-remover per registration (NOT keyed by signal), so adding the same signal twice is safe. */\n\tprivate readonly cleanups = new Set<() => void>();\n\n\t/** The aggregate signal — only fires when all callers have cancelled. */\n\tget signal(): AbortSignal {\n\t\treturn this.controller.signal;\n\t}\n\n\t/**\n\t * Register a caller.\n\t * - If `cancellation` is provided, the caller is auto-removed when it fires.\n\t * - If `cancellation` is `undefined`, the caller is \"permanent\" — the aggregate\n\t * can never fire while it's active. Call the returned cleanup on promise settle.\n\t * - The same signal instance may safely be added more than once — each registration is tracked and cleaned up\n\t * independently, so no listener leaks.\n\t * @returns A cleanup function to unregister the caller.\n\t */\n\tadd(cancellation?: AbortSignal): () => void {\n\t\tthis.activeCount++;\n\n\t\tif (cancellation == null) {\n\t\t\treturn () => {\n\t\t\t\tthis.activeCount--;\n\t\t\t\tthis.checkAbort();\n\t\t\t};\n\t\t}\n\n\t\tif (cancellation.aborted) {\n\t\t\tthis.activeCount--;\n\t\t\tthis.checkAbort();\n\t\t\treturn () => {};\n\t\t}\n\n\t\t// Track this registration's listener removal individually (a Set of removers, NOT keyed by the signal) so\n\t\t// adding the same signal twice can't overwrite the other's cleanup. `settled` keeps both exit paths (abort\n\t\t// vs. manual cleanup) idempotent and preserves the accounting: only `onAbort` decrements `activeCount`.\n\t\tlet settled = false;\n\t\tlet remove: () => void;\n\t\tconst onAbort = () => {\n\t\t\tif (settled) return;\n\n\t\t\tsettled = true;\n\t\t\tthis.cleanups.delete(remove);\n\t\t\tthis.activeCount--;\n\t\t\tthis.checkAbort();\n\t\t};\n\t\tremove = () => cancellation.removeEventListener('abort', onAbort);\n\t\tcancellation.addEventListener('abort', onAbort, { once: true });\n\t\tthis.cleanups.add(remove);\n\n\t\treturn () => {\n\t\t\tif (settled) return;\n\n\t\t\tsettled = true;\n\t\t\tthis.cleanups.delete(remove);\n\t\t\tremove();\n\t\t};\n\t}\n\n\tprivate checkAbort(): void {\n\t\tif (this.activeCount <= 0 && !this.controller.signal.aborted) {\n\t\t\tthis.controller.abort();\n\t\t}\n\t}\n\n\t/** Remove all abort listeners. Call when the cached promise settles. */\n\tdispose(): void {\n\t\tfor (const remove of this.cleanups) {\n\t\t\tremove();\n\t\t}\n\t\tthis.cleanups.clear();\n\t}\n}\n\nconst signalIds = new WeakMap<AbortSignal, number>();\nconst signalIdCounter = getScopedCounter();\n\n/**\n * Returns a stable, unique numeric ID for the given AbortSignal.\n * Used to differentiate concurrent git commands that have different\n * cancellation signals, so one caller aborting doesn't cancel another.\n */\nexport function getAbortSignalId(signal: AbortSignal | undefined): string {\n\tif (signal == null) return '';\n\n\tlet id = signalIds.get(signal);\n\tif (id == null) {\n\t\tid = signalIdCounter.next();\n\t\tsignalIds.set(signal, id);\n\t}\n\treturn String(id);\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CancellationReason } from "../../cancellation.js";
|
|
1
2
|
import { Writable } from "stream";
|
|
2
3
|
//#region ../utils/src/env/node/exec.d.ts
|
|
3
4
|
interface RunErrorContext {
|
|
@@ -17,7 +18,8 @@ export declare class RunError extends Error {
|
|
|
17
18
|
constructor(context: RunErrorContext, stdout: string, stderr: string);
|
|
18
19
|
}
|
|
19
20
|
export declare class CancelledRunError extends RunError {
|
|
20
|
-
|
|
21
|
+
readonly reason: CancellationReason;
|
|
22
|
+
constructor(cmd: string, killed: boolean, code?: number | string | undefined, signal?: NodeJS.Signals, reason?: CancellationReason);
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* Finds the executable and parameters to run on Windows. This method
|
|
@@ -94,6 +96,8 @@ export interface RunExitResult {
|
|
|
94
96
|
readonly exitCode?: number;
|
|
95
97
|
/** Set when the process was terminated by a signal instead of exiting; {@link exitCode} is then absent. */
|
|
96
98
|
readonly signal?: NodeJS.Signals;
|
|
99
|
+
/** Whether Node itself killed the process (its spawn `timeout`), as opposed to an external signal. */
|
|
100
|
+
readonly killed?: boolean;
|
|
97
101
|
}
|
|
98
102
|
export interface RunResult<T extends string | Buffer> extends RunExitResult {
|
|
99
103
|
stdout: T;
|