@gitkraken/core-gitlens 0.5.101 → 0.5.103
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 +25 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +16 -10
- package/dist/plus/integrations/collectionMetadata.d.ts.map +1 -1
- package/dist/plus/integrations/collectionMetadata.js +64 -16
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/index.d.ts +1 -1
- package/dist/plus/integrations/index.d.ts.map +1 -1
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.js +2 -1
- package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts.map +1 -1
- package/dist/plus/integrations/reads/drains.js +59 -20
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.d.ts.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.js +34 -17
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +8 -2
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/issues.d.ts.map +1 -1
- package/dist/plus/integrations/reads/issues.js +13 -3
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.d.ts.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.js +7 -1
- package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +43 -4
- package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
- package/dist/plus/integrations/reads/warnings.js +47 -5
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +115 -1
- package/dist/plus/integrations/results.d.ts.map +1 -1
- package/dist/plus/integrations/results.js +78 -3
- package/dist/plus/integrations/results.js.map +1 -1
- package/docs/integrations.md +81 -7
- package/package.json +1 -1
- package/src/plus/integrations/collectionMetadata.ts +67 -21
- package/src/plus/integrations/index.ts +4 -0
- package/src/plus/integrations/providers/utils/providerPaging.ts +2 -1
- package/src/plus/integrations/reads/drains.ts +84 -20
- package/src/plus/integrations/reads/hierarchy.ts +43 -32
- package/src/plus/integrations/reads/issueTracker.ts +8 -2
- package/src/plus/integrations/reads/issues.ts +21 -4
- package/src/plus/integrations/reads/pullRequests.ts +15 -1
- package/src/plus/integrations/reads/warnings.ts +86 -10
- package/src/plus/integrations/results.ts +173 -4
|
@@ -9,7 +9,7 @@ import type { PullRequestFilter } from '../providerFilters.js';
|
|
|
9
9
|
import type { ProviderPullRequest, ProviderReposInput, ProviderRepository } from '../providers/models.js';
|
|
10
10
|
import { getProviderPullRequestIdentity } from '../providers/models.js';
|
|
11
11
|
import type { ProviderWarning } from '../results.js';
|
|
12
|
-
import { appendDedupedWarning, toProviderWarning } from '../results.js';
|
|
12
|
+
import { appendDedupedWarning, reconcileOmissionsWithFailure, toProviderWarning } from '../results.js';
|
|
13
13
|
import { isIssuesHostIntegrationId } from '../utils/integration.utils.js';
|
|
14
14
|
import { noConnectionWarning, truncationWarning } from './warnings.js';
|
|
15
15
|
|
|
@@ -90,10 +90,18 @@ export async function drainPullRequests(
|
|
|
90
90
|
// this through the terminal returns instead of resetting it to false at the last page.
|
|
91
91
|
let fetchFailed = false;
|
|
92
92
|
let truncated = false;
|
|
93
|
+
// A page the provider capped or couldn't vouch for. Decides the CAUSE the terminal warning reports: a cap
|
|
94
|
+
// outranks a budget stop, because raising a budget cannot un-cap a page.
|
|
95
|
+
let providerTruncated = false;
|
|
96
|
+
// The subset of that which no other warning already explains — decides whether this drain raises one of
|
|
97
|
+
// its own on an otherwise clean exit.
|
|
98
|
+
let unexplainedTruncation = false;
|
|
93
99
|
|
|
94
100
|
// With no repos this is an account-wide "my PRs" sweep. The repo-scoped core rejects an empty `repos`
|
|
95
101
|
// input, so read the provider-native account-wide core instead.
|
|
96
102
|
const accountWide = repos.length === 0;
|
|
103
|
+
/** Every cursor already followed, so a provider that cycles them can't keep the drain walking in circles. */
|
|
104
|
+
const seenCursors = new Set<string>();
|
|
97
105
|
|
|
98
106
|
for (;;) {
|
|
99
107
|
page++;
|
|
@@ -130,10 +138,23 @@ export async function drainPullRequests(
|
|
|
130
138
|
appendDedupedWarning(warnings, noConnectionWarning(id, domain, connectionId));
|
|
131
139
|
}
|
|
132
140
|
// `warning` set → a hard read failure (incomplete items); otherwise not connected / no session.
|
|
141
|
+
const failed = fetchFailed || warning != null || unavailable;
|
|
142
|
+
// A cap seen on an earlier page still left results out, so it is still worth saying — but as part
|
|
143
|
+
// of a read that failed, never as an omission. (`failed` is always true here when anything was
|
|
144
|
+
// latched: reaching this exit past page 1 means a later page was lost.)
|
|
145
|
+
if (unexplainedTruncation) {
|
|
146
|
+
appendDedupedWarning(
|
|
147
|
+
warnings,
|
|
148
|
+
truncationWarning(id, domain, connectionId, 'Pull request', failed ? 'interrupted' : 'exhausted'),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
// An earlier page may already have emitted an omission before this one died; it asserts the read
|
|
152
|
+
// succeeded, which is no longer true.
|
|
153
|
+
reconcileOmissionsWithFailure(warnings, failed);
|
|
133
154
|
return {
|
|
134
155
|
items: items,
|
|
135
156
|
warnings: warnings,
|
|
136
|
-
fetchFailed:
|
|
157
|
+
fetchFailed: failed,
|
|
137
158
|
truncated: truncated || sessionLostAfterProgress,
|
|
138
159
|
// Only a top-level first-page rejection means the provider itself failed. A later-page or
|
|
139
160
|
// per-scope failure still yielded a usable provider slice and stays represented separately.
|
|
@@ -171,14 +192,31 @@ export async function drainPullRequests(
|
|
|
171
192
|
value.paging?.truncated === true ||
|
|
172
193
|
assessment.truncated;
|
|
173
194
|
truncated = truncated || pageTruncated;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
195
|
+
// A page the provider itself capped, or one whose completeness it couldn't confirm. Latched from ANY
|
|
196
|
+
// source, SDK metadata included: no budget of ours un-caps a page, so a later `maxPages` hit must not
|
|
197
|
+
// claim raising it would help.
|
|
198
|
+
providerTruncated = providerTruncated || pageTruncated;
|
|
199
|
+
// Whether this drain owes its OWN warning for that is a separate question — `mergeAssessmentInto` has
|
|
200
|
+
// already appended one when the fact came from SDK metadata, and repeating it would be noise.
|
|
201
|
+
unexplainedTruncation = unexplainedTruncation || (pageTruncated && !assessment.truncated);
|
|
177
202
|
|
|
178
203
|
if (!(value.paging?.more ?? false)) {
|
|
179
204
|
// A read that can't confirm completeness (single-page provider reads with no `hasNextPage`)
|
|
180
205
|
// sets `paging.truncated`; propagate it (and any top-level `truncated` and SDK incompleteness)
|
|
181
206
|
// so the sweep doesn't claim an all-pages result.
|
|
207
|
+
if (unexplainedTruncation) {
|
|
208
|
+
appendDedupedWarning(
|
|
209
|
+
warnings,
|
|
210
|
+
truncationWarning(
|
|
211
|
+
id,
|
|
212
|
+
domain,
|
|
213
|
+
connectionId,
|
|
214
|
+
'Pull request',
|
|
215
|
+
fetchFailed ? 'interrupted' : 'exhausted',
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
reconcileOmissionsWithFailure(warnings, fetchFailed);
|
|
182
220
|
return {
|
|
183
221
|
items: items,
|
|
184
222
|
warnings: warnings,
|
|
@@ -187,21 +225,34 @@ export async function drainPullRequests(
|
|
|
187
225
|
failedProvider: false,
|
|
188
226
|
};
|
|
189
227
|
}
|
|
190
|
-
if (page >= maxPages) {
|
|
191
|
-
appendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, 'Pull request'));
|
|
192
|
-
return {
|
|
193
|
-
items: items,
|
|
194
|
-
warnings: warnings,
|
|
195
|
-
fetchFailed: fetchFailed,
|
|
196
|
-
truncated: true,
|
|
197
|
-
failedProvider: false,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
228
|
|
|
229
|
+
// Resolve the continuation BEFORE deciding why the drain stops. `page-budget` claims the missing items
|
|
230
|
+
// are reachable, which is only true with a usable cursor in hand — and a provider can report another
|
|
231
|
+
// page while handing back none (Bitbucket Server does, when it omits `nextPageStart`). Checking the
|
|
232
|
+
// budget first would label that unreachable tail as merely unfetched, so raising `maxPages` would
|
|
233
|
+
// return the identical set.
|
|
201
234
|
const nextCursor = value.paging?.cursor;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
235
|
+
// A cursor already used isn't a continuation either — following it refetches a page we have, and a
|
|
236
|
+
// provider that cycles (A→B→A) would otherwise burn the whole budget and then be reported as merely
|
|
237
|
+
// out of budget. Tracked as a SET rather than compared one-back, matching the SDK's own `followCursors`:
|
|
238
|
+
// `drainToRequestedPage` and `collectProviderPagedResult` compare only the previous cursor, which a
|
|
239
|
+
// cycle slips past. That is tolerable there and not here, because only this drain reports `page-budget`.
|
|
240
|
+
const continuable = nextCursor != null && nextCursor !== '{}' && !seenCursors.has(nextCursor);
|
|
241
|
+
if (!continuable || page >= maxPages) {
|
|
242
|
+
// `providerTruncated` outranks the budget: a page the provider capped stays capped however many
|
|
243
|
+
// pages we are allowed to read, so promising `page-budget` on top of it would be a load-more that
|
|
244
|
+
// cannot deliver the capped part.
|
|
245
|
+
appendDedupedWarning(
|
|
246
|
+
warnings,
|
|
247
|
+
truncationWarning(
|
|
248
|
+
id,
|
|
249
|
+
domain,
|
|
250
|
+
connectionId,
|
|
251
|
+
'Pull request',
|
|
252
|
+
fetchFailed ? 'interrupted' : continuable && !providerTruncated ? 'page-budget' : 'exhausted',
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
reconcileOmissionsWithFailure(warnings, fetchFailed);
|
|
205
256
|
return {
|
|
206
257
|
items: items,
|
|
207
258
|
warnings: warnings,
|
|
@@ -211,6 +262,7 @@ export async function drainPullRequests(
|
|
|
211
262
|
};
|
|
212
263
|
}
|
|
213
264
|
|
|
265
|
+
seenCursors.add(nextCursor);
|
|
214
266
|
cursor = nextCursor;
|
|
215
267
|
}
|
|
216
268
|
}
|
|
@@ -262,12 +314,20 @@ export async function drainRepositories(
|
|
|
262
314
|
if (value == null) {
|
|
263
315
|
const interruptedAfterProgress = page > 1;
|
|
264
316
|
if (interruptedAfterProgress && warning == null) {
|
|
265
|
-
|
|
317
|
+
// Not a backstop: the read was cut short mid-drain, which is why it also sets `fetchFailed`
|
|
318
|
+
// below. A retry may complete it, so this must not claim the succeeded-but-capped omission.
|
|
319
|
+
appendDedupedWarning(
|
|
320
|
+
warnings,
|
|
321
|
+
truncationWarning(id, domain, connectionId, 'Repository', 'interrupted'),
|
|
322
|
+
);
|
|
266
323
|
}
|
|
324
|
+
const failed = fetchFailed || warning != null || interruptedAfterProgress;
|
|
325
|
+
// An SDK omission from an earlier page asserts the read succeeded; this one didn't.
|
|
326
|
+
reconcileOmissionsWithFailure(warnings, failed);
|
|
267
327
|
return {
|
|
268
328
|
repos: repos,
|
|
269
329
|
warnings: warnings,
|
|
270
|
-
fetchFailed:
|
|
330
|
+
fetchFailed: failed,
|
|
271
331
|
truncated: truncated || interruptedAfterProgress,
|
|
272
332
|
};
|
|
273
333
|
}
|
|
@@ -277,6 +337,7 @@ export async function drainRepositories(
|
|
|
277
337
|
fetchFailed = fetchFailed || assessment.fetchFailed;
|
|
278
338
|
truncated = truncated || value.truncated === true || value.paging?.truncated === true || assessment.truncated;
|
|
279
339
|
if (!(value.paging?.more ?? false)) {
|
|
340
|
+
reconcileOmissionsWithFailure(warnings, fetchFailed);
|
|
280
341
|
return {
|
|
281
342
|
repos: repos,
|
|
282
343
|
warnings: warnings,
|
|
@@ -284,13 +345,16 @@ export async function drainRepositories(
|
|
|
284
345
|
truncated: truncated,
|
|
285
346
|
};
|
|
286
347
|
}
|
|
348
|
+
|
|
287
349
|
if (page >= maxPages) {
|
|
350
|
+
reconcileOmissionsWithFailure(warnings, fetchFailed);
|
|
288
351
|
return { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };
|
|
289
352
|
}
|
|
290
353
|
|
|
291
354
|
const nextCursor = value.paging?.cursor;
|
|
292
355
|
if (nextCursor == null || nextCursor === '{}') {
|
|
293
356
|
// Provider says there is more but didn't return a usable cursor; stop rather than refetch the same page.
|
|
357
|
+
reconcileOmissionsWithFailure(warnings, fetchFailed);
|
|
294
358
|
return { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };
|
|
295
359
|
}
|
|
296
360
|
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '../constants.js';
|
|
11
11
|
import type { ListOrgsOptions, ListProjectsOptions } from '../manager.js';
|
|
12
12
|
import { isIssuesIntegration } from '../models/issuesIntegration.js';
|
|
13
|
+
import type { ProviderHierarchyResult } from '../providers/models.js';
|
|
13
14
|
import { toProviderRepositoryShape } from '../providers/models.js';
|
|
14
15
|
import type {
|
|
15
16
|
ProviderOrganization,
|
|
@@ -35,7 +36,46 @@ import {
|
|
|
35
36
|
withProviderContext,
|
|
36
37
|
} from './hierarchy.utils.js';
|
|
37
38
|
import { pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo } from './paging.js';
|
|
38
|
-
import { gitHostOnlySurfaceWarning, otherWarning } from './warnings.js';
|
|
39
|
+
import { gitHostOnlySurfaceWarning, otherWarning, truncationWarning } from './warnings.js';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Folds a flat hierarchy result's incompleteness into `warnings`, returning whether it leaves the read
|
|
43
|
+
* non-authoritative. The single funnel for both hierarchy reads, so neither can drift from the other on any of
|
|
44
|
+
* the four decisions it makes:
|
|
45
|
+
*
|
|
46
|
+
* - The metadata is assessed FIRST, so the truncation warning can tell whether it already explained this gap.
|
|
47
|
+
* Ordered deliberately: emitting first meant an expired credential produced BOTH a typed `auth` warning and
|
|
48
|
+
* an unclassifiable one for the same cause, and a consumer routing on classification showed the second as
|
|
49
|
+
* "this provider failed to load", with a remedy that doesn't exist, next to the reconnect prompt that was the
|
|
50
|
+
* actual fix.
|
|
51
|
+
* - The read states the truncation itself only when the metadata reported nothing — the rule
|
|
52
|
+
* `assessCollectionMetadata` applies to its own generic fallback. Whatever the metadata reported already names
|
|
53
|
+
* this provider and says something stronger, so restating it costs a false verdict and adds no information.
|
|
54
|
+
* - `'exhausted'`, never `'page-budget'`: `truncated` is one boolean here, so a drain that stopped at its page
|
|
55
|
+
* budget is indistinguishable from one whose cursor stalled, and `IncompleteReadCause` makes `'exhausted'`
|
|
56
|
+
* the default whenever a raisable budget is not demonstrably the cause.
|
|
57
|
+
* - A truncation counts as non-authoritative even though the omission above asserts the read succeeded. Unlike
|
|
58
|
+
* paged repository reads, a flattened hierarchy result has no page object on which to carry incompleteness,
|
|
59
|
+
* so `fetchFailed` is the only signal a consumer has that the list is short.
|
|
60
|
+
*/
|
|
61
|
+
function mergeHierarchyIncompleteness(
|
|
62
|
+
warnings: ProviderWarning[],
|
|
63
|
+
id: IntegrationIds,
|
|
64
|
+
domain: string | undefined,
|
|
65
|
+
connectionId: string | undefined,
|
|
66
|
+
result: Pick<ProviderHierarchyResult<unknown>, 'truncated' | 'metadata'>,
|
|
67
|
+
readKind: 'Organization' | 'Project',
|
|
68
|
+
): boolean {
|
|
69
|
+
const assessment = mergeAssessmentInto(warnings, id, domain, connectionId, result.metadata);
|
|
70
|
+
if (!result.truncated) return assessment.fetchFailed || assessment.truncated;
|
|
71
|
+
|
|
72
|
+
if (!assessment.reported) {
|
|
73
|
+
// Deduped like every other `truncationWarning` emission (see `drains.ts`), not plain-pushed: no caller can
|
|
74
|
+
// collide with it today, but nothing about this funnel guarantees a third read won't.
|
|
75
|
+
appendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, readKind, 'exhausted'));
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
39
79
|
|
|
40
80
|
export async function listOrgs(
|
|
41
81
|
ctx: ProviderReadContext,
|
|
@@ -103,22 +143,8 @@ export async function listOrgs(
|
|
|
103
143
|
);
|
|
104
144
|
if (value != null) {
|
|
105
145
|
items.push(...value.values.map(org => withProviderContext(id, org)));
|
|
106
|
-
if (value.truncated) {
|
|
107
|
-
warnings.push(
|
|
108
|
-
otherWarning(
|
|
109
|
-
id,
|
|
110
|
-
domain,
|
|
111
|
-
connectionId,
|
|
112
|
-
'Organization listing was truncated before the upstream results were exhausted.',
|
|
113
|
-
),
|
|
114
|
-
);
|
|
115
|
-
// `ProviderResult` has no page object on which to carry truncation. Mark the flat
|
|
116
|
-
// hierarchy result incomplete so consumers don't treat omitted orgs as authoritative.
|
|
117
|
-
fetchFailed = true;
|
|
118
|
-
}
|
|
119
146
|
|
|
120
|
-
|
|
121
|
-
if (assessment.fetchFailed || assessment.truncated) {
|
|
147
|
+
if (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, value, 'Organization')) {
|
|
122
148
|
fetchFailed = true;
|
|
123
149
|
}
|
|
124
150
|
}
|
|
@@ -200,22 +226,7 @@ export async function listProjects(
|
|
|
200
226
|
if (projects != null) {
|
|
201
227
|
items.push(...projects.values.map(project => withProviderContext(id, project)));
|
|
202
228
|
|
|
203
|
-
if (projects
|
|
204
|
-
warnings.push(
|
|
205
|
-
otherWarning(
|
|
206
|
-
id,
|
|
207
|
-
domain,
|
|
208
|
-
connectionId,
|
|
209
|
-
'Project listing was truncated before the upstream results were exhausted.',
|
|
210
|
-
),
|
|
211
|
-
);
|
|
212
|
-
// Unlike paged repository reads, this flattened hierarchy result has no continuation
|
|
213
|
-
// or page metadata. `fetchFailed` is its structural non-authoritative signal.
|
|
214
|
-
fetchFailed = true;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const assessment = mergeAssessmentInto(warnings, id, domain, connectionId, projects.metadata);
|
|
218
|
-
if (assessment.fetchFailed || assessment.truncated) {
|
|
229
|
+
if (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, projects, 'Project')) {
|
|
219
230
|
fetchFailed = true;
|
|
220
231
|
}
|
|
221
232
|
}
|
|
@@ -9,12 +9,13 @@ import { isIssuesIntegration } from '../models/issuesIntegration.js';
|
|
|
9
9
|
import { IssueFilter, providersMetadata } from '../providers/models.js';
|
|
10
10
|
import { mergeCollectionMetadata, parsePageCursor } from '../providers/utils/providerPaging.js';
|
|
11
11
|
import type { ProviderPagedResult, ProviderWarning } from '../results.js';
|
|
12
|
+
import { reconcileOmissionsWithFailure } from '../results.js';
|
|
12
13
|
import { isIssuesHostIntegrationId } from '../utils/integration.utils.js';
|
|
13
14
|
import type { ProviderReadContext } from './context.js';
|
|
14
15
|
import { parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from './cursors.js';
|
|
15
16
|
import { runCaptured } from './drains.js';
|
|
16
17
|
import { projectKey, resourceIdForProject, resourceLabel, resourceMatchesOrg } from './hierarchy.utils.js';
|
|
17
|
-
import { issueTrackerOnlySurfaceWarning, otherWarning } from './warnings.js';
|
|
18
|
+
import { incompleteReadWarning, issueTrackerOnlySurfaceWarning, otherWarning } from './warnings.js';
|
|
18
19
|
|
|
19
20
|
export async function listIssueTrackerIssuesPage(
|
|
20
21
|
ctx: ProviderReadContext,
|
|
@@ -431,14 +432,19 @@ export async function listIssueTrackerIssuesPage(
|
|
|
431
432
|
// the caller sees the truncation, but only when no warning already explains it (avoid duplicate noise).
|
|
432
433
|
if (projectTruncated && warnings.length === 0) {
|
|
433
434
|
warnings.push(
|
|
434
|
-
|
|
435
|
+
incompleteReadWarning(
|
|
435
436
|
options.providerId,
|
|
436
437
|
domain,
|
|
437
438
|
options.connectionId,
|
|
438
439
|
'Some issues were omitted; the provider returned an incomplete result.',
|
|
440
|
+
// `exhausted`, not `page-budget`: the per-project drain's backstop is an internal constant
|
|
441
|
+
// (`maxPagesPerRequest`), not an option this read exposes, so no caller can raise it.
|
|
442
|
+
fetchFailed ? 'interrupted' : 'exhausted',
|
|
439
443
|
),
|
|
440
444
|
);
|
|
441
445
|
}
|
|
446
|
+
// A metadata omission from an earlier project asserts the read succeeded; a later one may since have failed.
|
|
447
|
+
reconcileOmissionsWithFailure(warnings, fetchFailed);
|
|
442
448
|
|
|
443
449
|
const retryPages = retryWindowPages();
|
|
444
450
|
const cursor = toIssueTrackerPageCursor({
|
|
@@ -6,7 +6,7 @@ import type { IssueFilter, ProviderReposInput } from '../providers/models.js';
|
|
|
6
6
|
import { PagingMode, providersMetadata } from '../providers/models.js';
|
|
7
7
|
import { mergeCollectionMetadata } from '../providers/utils/providerPaging.js';
|
|
8
8
|
import type { ProviderPagedResult, ProviderWarning } from '../results.js';
|
|
9
|
-
import { appendDedupedWarning } from '../results.js';
|
|
9
|
+
import { appendDedupedWarning, reconcileOmissionsWithFailure } from '../results.js';
|
|
10
10
|
import {
|
|
11
11
|
isGitHostIntegration,
|
|
12
12
|
isIssuesHostIntegrationId,
|
|
@@ -269,14 +269,19 @@ export async function listIssuesPage(
|
|
|
269
269
|
const truncated = continuation.truncated || assessment.truncated;
|
|
270
270
|
if (truncated && warnings.length === 0) {
|
|
271
271
|
warnings.push(
|
|
272
|
-
|
|
272
|
+
truncationWarning(
|
|
273
273
|
options.providerId,
|
|
274
274
|
domain,
|
|
275
275
|
options.connectionId,
|
|
276
|
-
|
|
276
|
+
'Account-wide issue search',
|
|
277
|
+
// `exhausted`: this composite read exposes no budget the caller can raise, so nothing it
|
|
278
|
+
// could call would return the withheld items.
|
|
279
|
+
assessment.fetchFailed || pageFetchFailed ? 'interrupted' : 'exhausted',
|
|
277
280
|
),
|
|
278
281
|
);
|
|
279
282
|
}
|
|
283
|
+
// A metadata omission from an earlier page asserts the read succeeded; a later page may since have failed.
|
|
284
|
+
reconcileOmissionsWithFailure(warnings, assessment.fetchFailed || pageFetchFailed);
|
|
280
285
|
return {
|
|
281
286
|
items: items,
|
|
282
287
|
warnings: warnings,
|
|
@@ -385,8 +390,20 @@ export async function listIssuesPage(
|
|
|
385
390
|
// page isn't published as complete. Metadata incompleteness is an independent source of the same signal.
|
|
386
391
|
const truncated = continuation.truncated || assessment.truncated;
|
|
387
392
|
if (truncated && warnings.length === 0) {
|
|
388
|
-
warnings.push(
|
|
393
|
+
warnings.push(
|
|
394
|
+
truncationWarning(
|
|
395
|
+
options.providerId,
|
|
396
|
+
domain,
|
|
397
|
+
options.connectionId,
|
|
398
|
+
'Issue',
|
|
399
|
+
// `exhausted`, never `page-budget`: a paged read has no budget the caller can raise, and
|
|
400
|
+
// ordinary continuation is already expressed by `hasMore`/`cursor`.
|
|
401
|
+
assessment.fetchFailed || pageFetchFailed ? 'interrupted' : 'exhausted',
|
|
402
|
+
),
|
|
403
|
+
);
|
|
389
404
|
}
|
|
405
|
+
// A metadata omission from an earlier page asserts the read succeeded; a later page may since have failed.
|
|
406
|
+
reconcileOmissionsWithFailure(warnings, assessment.fetchFailed || pageFetchFailed);
|
|
390
407
|
return {
|
|
391
408
|
items: items,
|
|
392
409
|
warnings: warnings,
|
|
@@ -4,6 +4,7 @@ import type { IntegrationIds } from '../constants.js';
|
|
|
4
4
|
import type { ProviderPullRequest, ProviderReposInput, PullRequestFilter } from '../providers/models.js';
|
|
5
5
|
import { fromProviderPullRequest, PagingMode, providersMetadata } from '../providers/models.js';
|
|
6
6
|
import type { ProviderPagedResult } from '../results.js';
|
|
7
|
+
import { reconcileOmissionsWithFailure } from '../results.js';
|
|
7
8
|
import {
|
|
8
9
|
isGitHostIntegration,
|
|
9
10
|
isIssuesHostIntegrationId,
|
|
@@ -226,8 +227,21 @@ export async function listPullRequestsPage(
|
|
|
226
227
|
// the page may be incomplete. Metadata incompleteness is an independent source of the same signal.
|
|
227
228
|
const truncated = continuation.truncated || assessment.truncated;
|
|
228
229
|
if (truncated && warnings.length === 0) {
|
|
229
|
-
|
|
230
|
+
// `exhausted`, never `page-budget`: a paged read has no budget the caller can raise. Ordinary
|
|
231
|
+
// continuation is already expressed by `hasMore`/`cursor`, so reaching here means this page itself
|
|
232
|
+
// couldn't be completed — raising anything would return the same page.
|
|
233
|
+
warnings.push(
|
|
234
|
+
truncationWarning(
|
|
235
|
+
options.providerId,
|
|
236
|
+
domain,
|
|
237
|
+
options.connectionId,
|
|
238
|
+
'Pull request',
|
|
239
|
+
assessment.fetchFailed || pageFetchFailed ? 'interrupted' : 'exhausted',
|
|
240
|
+
),
|
|
241
|
+
);
|
|
230
242
|
}
|
|
243
|
+
// A metadata omission from an earlier page asserts the read succeeded; a later page may since have failed.
|
|
244
|
+
reconcileOmissionsWithFailure(warnings, assessment.fetchFailed || pageFetchFailed);
|
|
231
245
|
const currentAccountId = items.some(pr => pr.author != null)
|
|
232
246
|
? await getCurrentAccountId(integration, options.connectionId)
|
|
233
247
|
: undefined;
|
|
@@ -93,22 +93,98 @@ export function issueTrackerOnlySurfaceWarning(
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
96
|
+
* Why a read the facade drove itself returned less than everything. The two questions a consumer cannot
|
|
97
|
+
* answer from `truncated` or from the message, kept together because they are decided together:
|
|
98
|
+
*
|
|
99
|
+
* - `interrupted`: the read did NOT succeed — the session went away, a later page failed. It leaves an unread
|
|
100
|
+
* tail like the others, but it is a failure: it sets `fetchFailed`, and a retry is exactly the right move.
|
|
101
|
+
* Gets no omission at all, since the omission asserts the opposite.
|
|
102
|
+
* - `page-budget`: the drain stopped at its own `maxPages`, with a usable cursor still in hand. The items ARE
|
|
103
|
+
* reachable and were simply not fetched, so re-running with a higher budget returns them.
|
|
104
|
+
* - `exhausted`: everything else that succeeded and came back short — the provider capped the page it served,
|
|
105
|
+
* advertised another page without a usable cursor (or with one it had already handed out), or a read could
|
|
106
|
+
* not confirm it had drained everything. The default of the three: choose it whenever a raisable budget is
|
|
107
|
+
* not demonstrably what stopped the read, so a consumer is never offered a fetch that cannot deliver.
|
|
99
108
|
*/
|
|
109
|
+
export type IncompleteReadCause = 'interrupted' | 'page-budget' | 'exhausted';
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Builds the warning for a read that returned less than everything, carrying the two facts a consumer cannot
|
|
113
|
+
* recover on its own: did the request succeed, and would anything fetch the rest?
|
|
114
|
+
*
|
|
115
|
+
* Every incompleteness warning the facade raises on its own terms goes through here, so no read can drift
|
|
116
|
+
* into claiming success on a failure or offering a "load more" that cannot deliver. The omission it attaches
|
|
117
|
+
* is the same shape `assessCollectionMetadata` derives from SDK metadata, so a consumer never has to care
|
|
118
|
+
* which layer noticed the gap.
|
|
119
|
+
*
|
|
120
|
+
* No `scope`: this applies to the whole read, not to one repository or project the way an SDK-attributed
|
|
121
|
+
* omission can, so a consumer acting on `recovery` re-runs the read itself.
|
|
122
|
+
*/
|
|
123
|
+
export function incompleteReadWarning(
|
|
124
|
+
id: IntegrationIds,
|
|
125
|
+
domain: string | undefined,
|
|
126
|
+
connectionId: string | undefined,
|
|
127
|
+
message: string,
|
|
128
|
+
cause: IncompleteReadCause,
|
|
129
|
+
): ProviderWarning {
|
|
130
|
+
const warning = otherWarning(id, domain, connectionId, message);
|
|
131
|
+
if (cause === 'interrupted') return warning;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
...warning,
|
|
135
|
+
omission: {
|
|
136
|
+
kind: 'pagination-incomplete',
|
|
137
|
+
recovery: cause === 'page-budget' ? 'page-budget' : 'none',
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The read surfaces {@link truncationWarning} phrases for. `'Account-wide issue search'` names the composite
|
|
144
|
+
* read that spans several provider searches rather than one surface.
|
|
145
|
+
*
|
|
146
|
+
* Deliberately not exported: every caller passes a literal, and publishing it would invite a consumer to switch
|
|
147
|
+
* on a set that exists only to word a message.
|
|
148
|
+
*/
|
|
149
|
+
type TruncatedReadKind =
|
|
150
|
+
| 'Pull request'
|
|
151
|
+
| 'Issue'
|
|
152
|
+
| 'Repository'
|
|
153
|
+
| 'Organization'
|
|
154
|
+
| 'Project'
|
|
155
|
+
| 'Account-wide issue search';
|
|
156
|
+
|
|
157
|
+
/** {@link incompleteReadWarning} for a paged or drained read, phrased per surface. */
|
|
100
158
|
export function truncationWarning(
|
|
101
159
|
id: IntegrationIds,
|
|
102
160
|
domain: string | undefined,
|
|
103
161
|
connectionId: string | undefined,
|
|
104
|
-
readKind:
|
|
162
|
+
readKind: TruncatedReadKind,
|
|
163
|
+
/**
|
|
164
|
+
* Deliberately REQUIRED and not defaulted: a default would make one of these the silent fallback at any
|
|
165
|
+
* call site that forgot it, and both of the claims it carries — "the request succeeded" and "more can be
|
|
166
|
+
* fetched" — are ones this warning must never make by accident.
|
|
167
|
+
*/
|
|
168
|
+
cause: IncompleteReadCause,
|
|
105
169
|
): ProviderWarning {
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
170
|
+
return incompleteReadWarning(id, domain, connectionId, truncationMessage(id, readKind, cause), cause);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function truncationMessage(id: IntegrationIds, readKind: TruncatedReadKind, cause: IncompleteReadCause): string {
|
|
174
|
+
switch (cause) {
|
|
175
|
+
case 'interrupted':
|
|
176
|
+
// Deliberately does not name a mechanism. This fires both when a page was lost mid-drain and when a
|
|
177
|
+
// drain that latched an earlier scope failure later stopped for its own reasons; "was interrupted"
|
|
178
|
+
// would be false in the second. What is true in both is that something failed and results are gone.
|
|
179
|
+
return `${readKind} read for '${id}' did not complete; some results are missing and the read reported a failure.`;
|
|
180
|
+
case 'page-budget':
|
|
181
|
+
return `${readKind} read for '${id}' stopped at its page budget; more results can be read by raising it.`;
|
|
182
|
+
case 'exhausted':
|
|
183
|
+
return `${readKind} read for '${id}' was truncated and cannot be continued; results may be incomplete.`;
|
|
184
|
+
}
|
|
185
|
+
// No `default`: `IncompleteReadCause` is declared in this file, so `noImplicitReturns` already fails the
|
|
186
|
+
// build here if a cause is added without its own wording. (`collectionOmissionMessage`'s `satisfies never`
|
|
187
|
+
// is not the same case — that union is the SDK's, and can widen under a dependency bump.)
|
|
112
188
|
}
|
|
113
189
|
|
|
114
190
|
/** Warning for an account-wide issue read whose requested filters the provider can't express server-side. */
|