@gitkraken/core-gitlens 0.15.1 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/dist/git/models/issue.d.ts +2 -2
  3. package/dist/git/models/issue.js.map +1 -1
  4. package/dist/git/models/issueOrPullRequest.d.ts +2 -1
  5. package/dist/git/models/pullRequest.d.ts +1 -0
  6. package/dist/git/models/pullRequest.js.map +1 -1
  7. package/dist/git/utils/revision.utils.d.ts +3 -0
  8. package/dist/git/utils/revision.utils.js +5 -1
  9. package/dist/git/utils/revision.utils.js.map +1 -1
  10. package/dist/plus/git-github/api/github.d.ts +4 -0
  11. package/dist/plus/git-github/api/github.js +31 -4
  12. package/dist/plus/git-github/api/github.js.map +1 -1
  13. package/dist/plus/git-github/api/types.d.ts +4 -0
  14. package/dist/plus/git-github/providers/github/revision.js +12 -1
  15. package/dist/plus/git-github/providers/github/revision.js.map +1 -1
  16. package/dist/plus/integrations/collectionMetadata.d.ts +22 -7
  17. package/dist/plus/integrations/collectionMetadata.js +49 -6
  18. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  19. package/dist/plus/integrations/models/integration.d.ts +1 -1
  20. package/dist/plus/integrations/models/integration.js.map +1 -1
  21. package/dist/plus/integrations/providers/azure/azure.d.ts +3 -2
  22. package/dist/plus/integrations/providers/azure/azure.js +35 -6
  23. package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
  24. package/dist/plus/integrations/providers/azure/models.d.ts +3 -1
  25. package/dist/plus/integrations/providers/azure/models.js +8 -5
  26. package/dist/plus/integrations/providers/azure/models.js.map +1 -1
  27. package/dist/plus/integrations/providers/azure/search.d.ts +6 -4
  28. package/dist/plus/integrations/providers/azure/search.js +29 -10
  29. package/dist/plus/integrations/providers/azure/search.js.map +1 -1
  30. package/dist/plus/integrations/providers/azureDevOpsServer.js +2 -2
  31. package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
  32. package/dist/plus/integrations/providers/issueSorts.d.ts +1 -0
  33. package/dist/plus/integrations/providers/models.js +11 -8
  34. package/dist/plus/integrations/providers/models.js.map +1 -1
  35. package/dist/plus/integrations/providers/providersApi.js +5 -4
  36. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  37. package/dist/plus/integrations/providers/utils/jiraIssueDrains.d.ts +4 -21
  38. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js +13 -48
  39. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js.map +1 -1
  40. package/dist/plus/integrations/providers/utils/providerPaging.d.ts +20 -6
  41. package/dist/plus/integrations/providers/utils/providerPaging.js +50 -13
  42. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  43. package/dist/plus/integrations/reads/drains.d.ts +1 -0
  44. package/dist/plus/integrations/reads/issues.js +1 -1
  45. package/dist/plus/integrations/reads/issues.js.map +1 -1
  46. package/dist/plus/integrations/reads/paging.js +1 -1
  47. package/dist/plus/integrations/reads/paging.js.map +1 -1
  48. package/dist/plus/integrations/results.d.ts +12 -7
  49. package/dist/plus/integrations/results.js +3 -4
  50. package/dist/plus/integrations/results.js.map +1 -1
  51. package/docs/integrations.md +9 -5
  52. package/package.json +1 -1
@@ -1,6 +1,5 @@
1
1
  import { ProviderReference } from "../../../../git/models/remoteProvider.js";
2
2
  import { IssueShape } from "../../../../git/models/issue.js";
3
- import { ProviderWarningUnreadableItems } from "../../results.js";
4
3
  import { IssueFilter } from "../../providerFilters.js";
5
4
  import { JiraIssuePage, ProviderIssue } from "../models.js";
6
5
  import { CollectionMetadata, CollectionScopeFailure } from "@gitkraken/provider-apis";
@@ -54,28 +53,12 @@ export declare function withFailureScope(metadata: CollectionMetadata, scope: Co
54
53
  * of a drain over many unreadable pages never dedupe and would publish one warning per page. The first page's
55
54
  * failure is kept, naming its keys, and says how many more pages dropped issues too.
56
55
  *
57
- * A failure that names the issues a page dropped because it could not read them (GKDEV-3652) is marked as such, with
58
- * the keys every page of its group named (see {@link parseSkippedJiraIssues}), and is published as an
59
- * `'unreadable-items'` warning rather than a failed read. `pages` holds only what the SDK's search pages reported, never
60
- * a failure synthesized here: only those can carry that form.
56
+ * A failure that reports the issues a page dropped because it could not read them (GKDEV-3652) carries
57
+ * `skippedItems`, which the summary keeps for the whole group (the keys of every page of it, their totals added), and
58
+ * is published as an `'unreadable-items'` warning rather than a failed read. `pages` holds only what the SDK's search
59
+ * pages reported, never a failure synthesized here: only those can carry that field.
61
60
  */
62
61
  export declare function summarizePageMetadata(pages: readonly CollectionMetadata[]): CollectionMetadata | undefined;
63
- /**
64
- * The keys of the issues a Jira search page dropped, read from the message of the failure that reports them, since
65
- * provider-apis names them nowhere else: `Skipped Jira issues that could not be read: KEY-1 (reason), KEY-2 (reason)
66
- * and N more.`, with at most ten named and a position (`result 3`) in place of a key the issue lacked.
67
- *
68
- * A reason is the error's own text, so the entries can't be told apart by their parentheses: every place an entry can
69
- * open is taken as one instead. That can name a key that only appeared in a reason, never miss a skipped one, which is
70
- * the safe side: a consumer relies only on an issue being absent from `keys`.
71
- *
72
- * Undefined for a message that doesn't open that way, which is then no report of dropped issues: a failure of another
73
- * kind must not pass for one, since a consumer takes the rest of its scope as read. Never throws. A list that names
74
- * only some of its issues, doesn't open with a named entry, holds an unnamed one or lacks its closing period is
75
- * reported incomplete. A change of separator can't be told from a reason that holds one, so the test reading a page
76
- * through the real SDK is what pins the form.
77
- */
78
- export declare function parseSkippedJiraIssues(message: string | undefined): ProviderWarningUnreadableItems | undefined;
79
62
  /**
80
63
  * Follows a search's cursor up to `maxPages`. A page failure after the first page leaves the already-drained prefix
81
64
  * intact and records the failure instead of re-throwing and discarding the prefix; if nothing was fetched yet, the
@@ -1,8 +1,8 @@
1
1
  import { Logger as _logger } from "../../../../utils/logger.js";
2
- import { collectionScopeKey, mergeUnreadableItems } from "../../results.js";
2
+ import { collectionScopeKey } from "../../results.js";
3
3
  import { isIncompleteCollection, throwIfCallerContractError, toCollectionScopeFailure } from "../../collectionMetadata.js";
4
4
  import "../../providerFilters.js";
5
- import { mergeCollectionMetadata } from "./providerPaging.js";
5
+ import { mergeCollectionMetadata, mergeSkippedItems } from "./providerPaging.js";
6
6
  import { toIssueShape } from "../models.js";
7
7
  import { splitProjectIssuesSearch } from "../../models/issuesIntegration.js";
8
8
  //#region ../plus/integrations/src/providers/utils/jiraIssueDrains.ts
@@ -27,10 +27,10 @@ function withFailureScope(metadata, scope) {
27
27
  * of a drain over many unreadable pages never dedupe and would publish one warning per page. The first page's
28
28
  * failure is kept, naming its keys, and says how many more pages dropped issues too.
29
29
  *
30
- * A failure that names the issues a page dropped because it could not read them (GKDEV-3652) is marked as such, with
31
- * the keys every page of its group named (see {@link parseSkippedJiraIssues}), and is published as an
32
- * `'unreadable-items'` warning rather than a failed read. `pages` holds only what the SDK's search pages reported, never
33
- * a failure synthesized here: only those can carry that form.
30
+ * A failure that reports the issues a page dropped because it could not read them (GKDEV-3652) carries
31
+ * `skippedItems`, which the summary keeps for the whole group (the keys of every page of it, their totals added), and
32
+ * is published as an `'unreadable-items'` warning rather than a failed read. `pages` holds only what the SDK's search
33
+ * pages reported, never a failure synthesized here: only those can carry that field.
34
34
  */
35
35
  function summarizePageMetadata(pages) {
36
36
  let summary;
@@ -38,64 +38,29 @@ function summarizePageMetadata(pages) {
38
38
  for (const { failures: pageFailures, ...page } of pages) {
39
39
  summary = mergeCollectionMetadata(summary, page);
40
40
  for (const failure of pageFailures ?? []) {
41
- const unreadableItems = parseSkippedJiraIssues(failure.message);
42
- const key = `${collectionScopeKey(failure.scope)}\0${failure.kind}\0${unreadableItems != null}`;
41
+ const key = `${collectionScopeKey(failure.scope)}\0${failure.kind}\0${failure.skippedItems != null}`;
43
42
  const group = failures.get(key);
44
43
  if (group == null) failures.set(key, {
45
44
  failure,
46
45
  pages: 1,
47
- unreadableItems
46
+ skippedItems: failure.skippedItems
48
47
  });
49
48
  else {
50
49
  group.pages++;
51
- group.unreadableItems = mergeUnreadableItems(group.unreadableItems, unreadableItems);
50
+ if (failure.skippedItems != null && group.skippedItems != null) group.skippedItems = mergeSkippedItems(group.skippedItems, failure.skippedItems, "disjoint");
52
51
  }
53
52
  }
54
53
  }
55
54
  if (failures.size === 0) return summary;
56
55
  return mergeCollectionMetadata(summary, {
57
56
  completeness: "partial",
58
- failures: Array.from(failures.values(), ({ failure, pages: count, unreadableItems }) => ({
57
+ failures: Array.from(failures.values(), ({ failure, pages: count, skippedItems }) => ({
59
58
  ...failure,
60
59
  ...count === 1 ? {} : { message: `${failure.message != null ? `${failure.message} ` : ""}Issues on ${count - 1} more ${count === 2 ? "page" : "pages"} could not be read either.` },
61
- ...unreadableItems != null ? { unreadableItems } : {}
60
+ ...skippedItems != null ? { skippedItems } : {}
62
61
  }))
63
62
  });
64
63
  }
65
- const skippedJiraIssuesPrefix = "Skipped Jira issues that could not be read: ";
66
- /** Where provider-apis names a skipped issue: its key, or its position, opening its entry at the start or after `, `. */
67
- const skippedJiraIssueEntry = /(?:^|, )(result \d+|[^\s(),]+) \(/g;
68
- const moreSkippedJiraIssues = / and \d+ more\.$/;
69
- /** An entry with no name: provider-apis names one by its key or position, so this one can't be accounted for. */
70
- const unnamedSkippedJiraIssue = /(?:^|, ) \(/;
71
- /**
72
- * The keys of the issues a Jira search page dropped, read from the message of the failure that reports them, since
73
- * provider-apis names them nowhere else: `Skipped Jira issues that could not be read: KEY-1 (reason), KEY-2 (reason)
74
- * and N more.`, with at most ten named and a position (`result 3`) in place of a key the issue lacked.
75
- *
76
- * A reason is the error's own text, so the entries can't be told apart by their parentheses: every place an entry can
77
- * open is taken as one instead. That can name a key that only appeared in a reason, never miss a skipped one, which is
78
- * the safe side: a consumer relies only on an issue being absent from `keys`.
79
- *
80
- * Undefined for a message that doesn't open that way, which is then no report of dropped issues: a failure of another
81
- * kind must not pass for one, since a consumer takes the rest of its scope as read. Never throws. A list that names
82
- * only some of its issues, doesn't open with a named entry, holds an unnamed one or lacks its closing period is
83
- * reported incomplete. A change of separator can't be told from a reason that holds one, so the test reading a page
84
- * through the real SDK is what pins the form.
85
- */
86
- function parseSkippedJiraIssues(message) {
87
- if (!message?.startsWith(skippedJiraIssuesPrefix)) return void 0;
88
- const list = message.slice(44);
89
- const entries = [...list.matchAll(skippedJiraIssueEntry)];
90
- let complete = entries[0]?.index === 0 && !list.startsWith(", ") && !unnamedSkippedJiraIssue.test(list) && list.endsWith(".") && !moreSkippedJiraIssues.test(list);
91
- const keys = [];
92
- for (const [, name] of entries) if (name.startsWith("result ")) complete = false;
93
- else if (!keys.includes(name)) keys.push(name);
94
- return {
95
- keys,
96
- complete
97
- };
98
- }
99
64
  /**
100
65
  * Says a drain of `scope` stopped at its page backstop with pages left: an omission, not a failure, since every request
101
66
  * answered, so it retracts no other omission of the read. A clean drain leaves it to its caller (`'backstop'`), which
@@ -220,7 +185,7 @@ async function readUserScopedJiraIssues(fetchPage, requestedFilters, identities,
220
185
  }
221
186
  if (outcome.value.status !== "complete") truncated = true;
222
187
  if (outcome.value.status === "backstop") metadata = mergeCollectionMetadata(metadata, backstopOmission(options.failureScope));
223
- if (outcome.value.metadata != null) metadata = mergeCollectionMetadata(metadata, outcome.value.metadata);
188
+ if (outcome.value.metadata != null) metadata = mergeCollectionMetadata(metadata, outcome.value.metadata, "overlapping");
224
189
  issues.push(...outcome.value.issues);
225
190
  }
226
191
  return {
@@ -256,6 +221,6 @@ async function searchJiraProjectIssues(fetchPage, projectIds, filters, identitie
256
221
  return splitProjectIssuesSearch(projectIds, read.issues, (issue) => issue.project?.id ?? void 0)?.map((issues) => toUniqueJiraIssueShapes(issues, provider));
257
222
  }
258
223
  //#endregion
259
- export { drainJiraIssues, parseSkippedJiraIssues, readUserScopedJiraIssues, searchJiraProjectIssues, summarizePageMetadata, toUniqueJiraIssueShapes, withFailureScope };
224
+ export { drainJiraIssues, readUserScopedJiraIssues, searchJiraProjectIssues, summarizePageMetadata, toUniqueJiraIssueShapes, withFailureScope };
260
225
 
261
226
  //# sourceMappingURL=jiraIssueDrains.js.map
@@ -1 +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 type { ProviderScopeFailure } from '../../collectionMetadata.js';\nimport {\n\tisIncompleteCollection,\n\tthrowIfCallerContractError,\n\ttoCollectionScopeFailure,\n} from '../../collectionMetadata.js';\nimport { splitProjectIssuesSearch } from '../../models/issuesIntegration.js';\nimport { IssueFilter } from '../../providerFilters.js';\nimport type { ProviderWarningUnreadableItems } from '../../results.js';\nimport { collectionScopeKey, mergeUnreadableItems } from '../../results.js';\nimport type { JiraIssuePage, 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\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, a stalled\n * cursor or a page that dropped issues 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, or that a page reported, 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 * A search page's metadata with its failures recorded under `scope`. provider-apis scopes a page failure to what it\n * searched (a Cloud site, or nothing below the provider on Data Center, which would publish it as a failure of the\n * whole connection); the caller knows what it read, e.g. one project. Omissions keep their own scope.\n */\nexport function withFailureScope(\n\tmetadata: CollectionMetadata,\n\tscope: CollectionScopeFailure['scope'],\n): CollectionMetadata {\n\tif (metadata.failures == null) return metadata;\n\n\treturn { ...metadata, failures: metadata.failures.map(failure => ({ ...failure, scope: scope })) };\n}\n\n/**\n * Folds what a drain's pages reported into one metadata, with one failure per scope and kind however many pages\n * reported one. provider-apis reports the issues a page dropped per page, naming that page's keys, so the failures\n * of a drain over many unreadable pages never dedupe and would publish one warning per page. The first page's\n * failure is kept, naming its keys, and says how many more pages dropped issues too.\n *\n * A failure that names the issues a page dropped because it could not read them (GKDEV-3652) is marked as such, with\n * the keys every page of its group named (see {@link parseSkippedJiraIssues}), and is published as an\n * `'unreadable-items'` warning rather than a failed read. `pages` holds only what the SDK's search pages reported, never\n * a failure synthesized here: only those can carry that form.\n */\nexport function summarizePageMetadata(pages: readonly CollectionMetadata[]): CollectionMetadata | undefined {\n\tlet summary: CollectionMetadata | undefined;\n\tconst failures = new Map<\n\t\tstring,\n\t\t{ failure: CollectionScopeFailure; pages: number; unreadableItems: ProviderWarningUnreadableItems | undefined }\n\t>();\n\tfor (const { failures: pageFailures, ...page } of pages) {\n\t\tsummary = mergeCollectionMetadata(summary, page);\n\t\tfor (const failure of pageFailures ?? []) {\n\t\t\tconst unreadableItems = parseSkippedJiraIssues(failure.message);\n\t\t\tconst key = `${collectionScopeKey(failure.scope)}\\0${failure.kind}\\0${unreadableItems != null}`;\n\t\t\tconst group = failures.get(key);\n\t\t\tif (group == null) {\n\t\t\t\tfailures.set(key, { failure: failure, pages: 1, unreadableItems: unreadableItems });\n\t\t\t} else {\n\t\t\t\tgroup.pages++;\n\t\t\t\tgroup.unreadableItems = mergeUnreadableItems(group.unreadableItems, unreadableItems);\n\t\t\t}\n\t\t}\n\t}\n\tif (failures.size === 0) return summary;\n\n\treturn mergeCollectionMetadata(summary, {\n\t\tcompleteness: 'partial',\n\t\tfailures: Array.from(failures.values(), ({ failure, pages: count, unreadableItems }): ProviderScopeFailure => ({\n\t\t\t...failure,\n\t\t\t...(count === 1\n\t\t\t\t? {}\n\t\t\t\t: {\n\t\t\t\t\t\tmessage: `${failure.message != null ? `${failure.message} ` : ''}Issues on ${count - 1} more ${count === 2 ? 'page' : 'pages'} could not be read either.`,\n\t\t\t\t\t}),\n\t\t\t...(unreadableItems != null ? { unreadableItems: unreadableItems } : {}),\n\t\t})),\n\t});\n}\n\nconst skippedJiraIssuesPrefix = 'Skipped Jira issues that could not be read: ';\n/** Where provider-apis names a skipped issue: its key, or its position, opening its entry at the start or after `, `. */\nconst skippedJiraIssueEntry = /(?:^|, )(result \\d+|[^\\s(),]+) \\(/g;\nconst moreSkippedJiraIssues = / and \\d+ more\\.$/;\n/** An entry with no name: provider-apis names one by its key or position, so this one can't be accounted for. */\nconst unnamedSkippedJiraIssue = /(?:^|, ) \\(/;\n\n/**\n * The keys of the issues a Jira search page dropped, read from the message of the failure that reports them, since\n * provider-apis names them nowhere else: `Skipped Jira issues that could not be read: KEY-1 (reason), KEY-2 (reason)\n * and N more.`, with at most ten named and a position (`result 3`) in place of a key the issue lacked.\n *\n * A reason is the error's own text, so the entries can't be told apart by their parentheses: every place an entry can\n * open is taken as one instead. That can name a key that only appeared in a reason, never miss a skipped one, which is\n * the safe side: a consumer relies only on an issue being absent from `keys`.\n *\n * Undefined for a message that doesn't open that way, which is then no report of dropped issues: a failure of another\n * kind must not pass for one, since a consumer takes the rest of its scope as read. Never throws. A list that names\n * only some of its issues, doesn't open with a named entry, holds an unnamed one or lacks its closing period is\n * reported incomplete. A change of separator can't be told from a reason that holds one, so the test reading a page\n * through the real SDK is what pins the form.\n */\nexport function parseSkippedJiraIssues(message: string | undefined): ProviderWarningUnreadableItems | undefined {\n\tif (!message?.startsWith(skippedJiraIssuesPrefix)) return undefined;\n\n\tconst list = message.slice(skippedJiraIssuesPrefix.length);\n\tconst entries = [...list.matchAll(skippedJiraIssueEntry)];\n\tlet complete =\n\t\tentries[0]?.index === 0 &&\n\t\t!list.startsWith(', ') &&\n\t\t!unnamedSkippedJiraIssue.test(list) &&\n\t\tlist.endsWith('.') &&\n\t\t!moreSkippedJiraIssues.test(list);\n\tconst keys: string[] = [];\n\tfor (const [, name] of entries) {\n\t\tif (name.startsWith('result ')) {\n\t\t\tcomplete = false;\n\t\t} else if (!keys.includes(name)) {\n\t\t\tkeys.push(name);\n\t\t}\n\t}\n\treturn { keys: keys, complete: complete };\n}\n\n/**\n * Says a drain of `scope` stopped at its page backstop with pages left: an omission, not a failure, since every request\n * answered, so it retracts no other omission of the read. A clean drain leaves it to its caller (`'backstop'`), which\n * knows whether a narrower scope would avoid it.\n */\nfunction backstopOmission(scope: CollectionScopeFailure['scope']): CollectionMetadata {\n\treturn { completeness: 'partial', omissions: [{ kind: 'pagination-incomplete', scope: scope }] };\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 *\n * A page that dropped issues it could not read (its metadata says so) makes the drain `incomplete` without stopping\n * it: the issues it did read and the pages after it are kept, and its failures are recorded under `failureScope`,\n * which names what this drain reads (e.g. one project) more narrowly than the site the SDK searched.\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 pages: CollectionMetadata[] = [];\n\tconst recordPage = (pageMetadata: CollectionMetadata | undefined): void => {\n\t\tif (pageMetadata == null || (!isIncompleteCollection(pageMetadata) && !pageMetadata.failures?.length)) return;\n\n\t\tstatus = 'incomplete';\n\t\tpages.push(withFailureScope(pageMetadata, options.failureScope));\n\t};\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\t// A page that read no issue but reported dropping some (every issue on it was unreadable) is not\n\t\t\t// \"nothing fetched\": its failure is kept rather than lost to the throw or to an empty success.\n\t\t\tconst nothingRead = issues.length === 0 && pages.length === 0;\n\t\t\tif (nothingRead) {\n\t\t\t\tif (options.isEmptyFailure?.(ex)) return { issues: [], status: 'complete' };\n\n\t\t\t\tthrow ex;\n\t\t\t}\n\t\t\t// The project is gone, so it has nothing more to report; what its pages dropped already is.\n\t\t\tif (issues.length === 0 && options.isEmptyFailure?.(ex)) break;\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\trecordPage(result.metadata);\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. A drain already incomplete stays so (narrowing can't recover a dropped\n\t\t// issue), so its metadata says the backstop also stopped it rather than only naming the dropped issues.\n\t\tif (i === options.maxPages - 1) {\n\t\t\tif (status === 'complete') {\n\t\t\t\tstatus = 'backstop';\n\t\t\t} else {\n\t\t\t\tmetadata = mergeCollectionMetadata(metadata, backstopOmission(options.failureScope));\n\t\t\t}\n\t\t}\n\t}\n\treturn {\n\t\tissues: issues,\n\t\tstatus: status,\n\t\tmetadata: mergeCollectionMetadata(summarizePageMetadata(pages), metadata),\n\t};\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\t// A backstop leaves nothing in the drain's metadata, so merged with a sibling's failure it would be told only by\n\t\t// `truncated`, which that failure sets too: a skipped issue would then stand for a read that also stopped short.\n\t\tif (outcome.value.status === 'backstop') {\n\t\t\tmetadata = mergeCollectionMetadata(metadata, backstopOmission(options.failureScope));\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 (including a page\n * that dropped an issue it could not read) or an issue belongs to no requested project, so the caller reads them one\n * by one: a search's pages are global to its project set, and an incomplete one says nothing about which of its\n * projects it covered. Its failures are therefore never published, and the waste is bounded by one project's page\n * budget, paid by a search that needed more than that or did not read cleanly. An issue that stays unreadable makes\n * every such search pay it, as Linear's does for an incomplete search.\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
+ {"version":3,"file":"jiraIssueDrains.js","names":[],"sources":["../../../../../../plus/integrations/src/providers/utils/jiraIssueDrains.ts"],"sourcesContent":["import type { CollectionMetadata, CollectionScopeFailure, CollectionSkippedItems } 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 {\n\tisIncompleteCollection,\n\tthrowIfCallerContractError,\n\ttoCollectionScopeFailure,\n} from '../../collectionMetadata.js';\nimport { splitProjectIssuesSearch } from '../../models/issuesIntegration.js';\nimport { IssueFilter } from '../../providerFilters.js';\nimport { collectionScopeKey } from '../../results.js';\nimport type { JiraIssuePage, ProviderIssue } from '../models.js';\nimport { toIssueShape } from '../models.js';\nimport { mergeCollectionMetadata, mergeSkippedItems } 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\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, a stalled\n * cursor or a page that dropped issues 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, or that a page reported, 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 * A search page's metadata with its failures recorded under `scope`. provider-apis scopes a page failure to what it\n * searched (a Cloud site, or nothing below the provider on Data Center, which would publish it as a failure of the\n * whole connection); the caller knows what it read, e.g. one project. Omissions keep their own scope.\n */\nexport function withFailureScope(\n\tmetadata: CollectionMetadata,\n\tscope: CollectionScopeFailure['scope'],\n): CollectionMetadata {\n\tif (metadata.failures == null) return metadata;\n\n\treturn { ...metadata, failures: metadata.failures.map(failure => ({ ...failure, scope: scope })) };\n}\n\n/**\n * Folds what a drain's pages reported into one metadata, with one failure per scope and kind however many pages\n * reported one. provider-apis reports the issues a page dropped per page, naming that page's keys, so the failures\n * of a drain over many unreadable pages never dedupe and would publish one warning per page. The first page's\n * failure is kept, naming its keys, and says how many more pages dropped issues too.\n *\n * A failure that reports the issues a page dropped because it could not read them (GKDEV-3652) carries\n * `skippedItems`, which the summary keeps for the whole group (the keys of every page of it, their totals added), and\n * is published as an `'unreadable-items'` warning rather than a failed read. `pages` holds only what the SDK's search\n * pages reported, never a failure synthesized here: only those can carry that field.\n */\nexport function summarizePageMetadata(pages: readonly CollectionMetadata[]): CollectionMetadata | undefined {\n\tlet summary: CollectionMetadata | undefined;\n\tconst failures = new Map<\n\t\tstring,\n\t\t{ failure: CollectionScopeFailure; pages: number; skippedItems: CollectionSkippedItems | undefined }\n\t>();\n\tfor (const { failures: pageFailures, ...page } of pages) {\n\t\tsummary = mergeCollectionMetadata(summary, page);\n\t\tfor (const failure of pageFailures ?? []) {\n\t\t\tconst key = `${collectionScopeKey(failure.scope)}\\0${failure.kind}\\0${failure.skippedItems != null}`;\n\t\t\tconst group = failures.get(key);\n\t\t\tif (group == null) {\n\t\t\t\tfailures.set(key, {\n\t\t\t\t\tfailure: failure,\n\t\t\t\t\tpages: 1,\n\t\t\t\t\tskippedItems: failure.skippedItems,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tgroup.pages++;\n\t\t\t\tif (failure.skippedItems != null && group.skippedItems != null) {\n\t\t\t\t\tgroup.skippedItems = mergeSkippedItems(group.skippedItems, failure.skippedItems, 'disjoint');\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (failures.size === 0) return summary;\n\n\treturn mergeCollectionMetadata(summary, {\n\t\tcompleteness: 'partial',\n\t\tfailures: Array.from(failures.values(), ({ failure, pages: count, skippedItems }): CollectionScopeFailure => ({\n\t\t\t...failure,\n\t\t\t...(count === 1\n\t\t\t\t? {}\n\t\t\t\t: {\n\t\t\t\t\t\tmessage: `${failure.message != null ? `${failure.message} ` : ''}Issues on ${count - 1} more ${count === 2 ? 'page' : 'pages'} could not be read either.`,\n\t\t\t\t\t}),\n\t\t\t...(skippedItems != null ? { skippedItems: skippedItems } : {}),\n\t\t})),\n\t});\n}\n\n/**\n * Says a drain of `scope` stopped at its page backstop with pages left: an omission, not a failure, since every request\n * answered, so it retracts no other omission of the read. A clean drain leaves it to its caller (`'backstop'`), which\n * knows whether a narrower scope would avoid it.\n */\nfunction backstopOmission(scope: CollectionScopeFailure['scope']): CollectionMetadata {\n\treturn { completeness: 'partial', omissions: [{ kind: 'pagination-incomplete', scope: scope }] };\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 *\n * A page that dropped issues it could not read (its metadata says so) makes the drain `incomplete` without stopping\n * it: the issues it did read and the pages after it are kept, and its failures are recorded under `failureScope`,\n * which names what this drain reads (e.g. one project) more narrowly than the site the SDK searched.\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 pages: CollectionMetadata[] = [];\n\tconst recordPage = (pageMetadata: CollectionMetadata | undefined): void => {\n\t\tif (pageMetadata == null || (!isIncompleteCollection(pageMetadata) && !pageMetadata.failures?.length)) return;\n\n\t\tstatus = 'incomplete';\n\t\tpages.push(withFailureScope(pageMetadata, options.failureScope));\n\t};\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\t// A page that read no issue but reported dropping some (every issue on it was unreadable) is not\n\t\t\t// \"nothing fetched\": its failure is kept rather than lost to the throw or to an empty success.\n\t\t\tconst nothingRead = issues.length === 0 && pages.length === 0;\n\t\t\tif (nothingRead) {\n\t\t\t\tif (options.isEmptyFailure?.(ex)) return { issues: [], status: 'complete' };\n\n\t\t\t\tthrow ex;\n\t\t\t}\n\t\t\t// The project is gone, so it has nothing more to report; what its pages dropped already is.\n\t\t\tif (issues.length === 0 && options.isEmptyFailure?.(ex)) break;\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\trecordPage(result.metadata);\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. A drain already incomplete stays so (narrowing can't recover a dropped\n\t\t// issue), so its metadata says the backstop also stopped it rather than only naming the dropped issues.\n\t\tif (i === options.maxPages - 1) {\n\t\t\tif (status === 'complete') {\n\t\t\t\tstatus = 'backstop';\n\t\t\t} else {\n\t\t\t\tmetadata = mergeCollectionMetadata(metadata, backstopOmission(options.failureScope));\n\t\t\t}\n\t\t}\n\t}\n\treturn {\n\t\tissues: issues,\n\t\tstatus: status,\n\t\tmetadata: mergeCollectionMetadata(summarizePageMetadata(pages), metadata),\n\t};\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\t// A backstop leaves nothing in the drain's metadata, so merged with a sibling's failure it would be told only by\n\t\t// `truncated`, which that failure sets too: a skipped issue would then stand for a read that also stopped short.\n\t\tif (outcome.value.status === 'backstop') {\n\t\t\tmetadata = mergeCollectionMetadata(metadata, backstopOmission(options.failureScope));\n\t\t}\n\t\tif (outcome.value.metadata != null) {\n\t\t\t// The relationships' searches overlap: an issue one reaches, another may too.\n\t\t\tmetadata = mergeCollectionMetadata(metadata, outcome.value.metadata, 'overlapping');\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 (including a page\n * that dropped an issue it could not read) or an issue belongs to no requested project, so the caller reads them one\n * by one: a search's pages are global to its project set, and an incomplete one says nothing about which of its\n * projects it covered. Its failures are therefore never published, and the waste is bounded by one project's page\n * budget, paid by a search that needed more than that or did not read cleanly. An issue that stays unreadable makes\n * every such search pay it, as Linear's does for an incomplete search.\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,6 +1,6 @@
1
1
  import { PullRequestShape } from "../../../../git/models/pullRequest.js";
2
2
  import { ProviderApiPagedResult, ProviderHierarchyResult } from "../models.js";
3
- import { CollectionMetadata, CollectionScopeFailure } from "@gitkraken/provider-apis";
3
+ import { CollectionMetadata, CollectionScopeFailure, CollectionSkippedItems } from "@gitkraken/provider-apis";
4
4
  //#region ../plus/integrations/src/providers/utils/providerPaging.d.ts
5
5
  /**
6
6
  * Encodes an opaque numeric paging token as the `{ value, type: 'page' }` cursor the paging layer uses.
@@ -66,14 +66,28 @@ export declare function readEtagFieldsByRepository<Node, Fields>(coordinates: re
66
66
  }[], chunkSize: number, read: (owner: string, repo: string, numbers: number[]) => Promise<(Node | undefined)[]>, map: (node: Node) => Fields): Promise<PromiseSettledResult<Fields | undefined>[]>;
67
67
  export declare function flatSettledResultsOrThrow<T>(results: PromiseSettledResult<T[]>[]): T[];
68
68
  export declare function flatSettledOrThrow<T>(promises: Promise<T[]>[]): Promise<T[]>;
69
+ /**
70
+ * The items two reports of skipped items left out together: the union of their keys in reported order, complete only
71
+ * when both are, and a total that depends on whether the reads can overlap. Reads of the same scope that each search for
72
+ * something else (an issue the assignee and the author relationships both reach) are `'overlapping'`: which issues they
73
+ * share isn't known, so the total is the larger of the two and never less than the keys named, a lower bound. The pages
74
+ * of one drain are `'disjoint'`, so their totals add (a key repeated across pages that shifted counts once per page).
75
+ * Both are normalized first ({@link normalizeSkippedItems}).
76
+ */
77
+ export declare function mergeSkippedItems(existing: CollectionSkippedItems, next: CollectionSkippedItems, reads: 'disjoint' | 'overlapping'): CollectionSkippedItems;
69
78
  /**
70
79
  * Merges SDK collection metadata across drained pages. Completeness follows {@link completenessRank};
71
- * failures are deduplicated by kind, scope IDs, and message, and omissions are collapsed per kind and scope
72
- * keeping the highest reported total ({@link collectionOmissionKey}). Both preserve first-reported order.
73
- * Returns `undefined` when no page supplied metadata, so metadata-free providers and test doubles keep
74
- * behaving as before.
80
+ * failures are deduplicated by kind and scope IDs for those that report `skippedItems` ({@link collectionFailureKey}),
81
+ * and by kind, scope IDs and message for the rest, and omissions are collapsed per kind and scope
82
+ * keeping the highest reported total ({@link collectionOmissionKey}). Both preserve first-reported order. Two failures
83
+ * that report skipped items merge them instead of the first winning ({@link mergeSkippedItems}), and the merged failure
84
+ * keeps the message of the one that names more keys (the first when they name as many), so the text covers at least
85
+ * what the larger report does, whichever came first. `reads` says how the items add up: `'overlapping'`, the default,
86
+ * suits any reads that may reach the same item (a user's relationships), and a caller merging the successive pages of
87
+ * one drain says `'disjoint'`. Returns `undefined` when no page supplied metadata, so metadata-free providers and test
88
+ * doubles keep behaving as before.
75
89
  */
76
- export declare function mergeCollectionMetadata(base: CollectionMetadata | undefined, next: CollectionMetadata | undefined): CollectionMetadata | undefined;
90
+ export declare function mergeCollectionMetadata(base: CollectionMetadata | undefined, next: CollectionMetadata | undefined, reads?: 'disjoint' | 'overlapping'): CollectionMetadata | undefined;
77
91
  /**
78
92
  * Drains a provider paged fetcher into a single result while preserving enough metadata to
79
93
  * signal whether the defensive backstop interrupted the drain, and merging SDK collection metadata
@@ -4,7 +4,7 @@ import { isCancellationError } from "../../../../utils/cancellation.js";
4
4
  import { mapSettledBounded } from "../../../../utils/promise.js";
5
5
  import "../../constants.js";
6
6
  import { collectionScopeKey, mergeUnreadableItems } from "../../results.js";
7
- import { throwIfCallerContractError, toCollectionScopeFailure } from "../../collectionMetadata.js";
7
+ import { normalizeSkippedItems, throwIfCallerContractError, toCollectionScopeFailure } from "../../collectionMetadata.js";
8
8
  //#region ../plus/integrations/src/providers/utils/providerPaging.ts
9
9
  /**
10
10
  * Encodes an opaque numeric paging token as the `{ value, type: 'page' }` cursor the paging layer uses.
@@ -145,15 +145,43 @@ const completenessRank = {
145
145
  unknown: 1,
146
146
  complete: 0
147
147
  };
148
- /** A stable key for deduplicating structurally-identical scope failures accumulated across drained pages. */
148
+ /**
149
+ * A stable key for deduplicating structurally-identical scope failures accumulated across drained pages: kind, scope IDs
150
+ * and message. A failure that reports skipped items is keyed by kind and scope alone, never by message: it is one
151
+ * account of what a scope skipped, however the reads that reported it worded it or split the issues over their pages
152
+ * (the message is display prose, and names a different handful of issues and pages in each). And it never shares a key
153
+ * with one that doesn't: that one says nothing of which items were read, however alike their messages.
154
+ */
149
155
  function collectionFailureKey(failure) {
150
- return [
156
+ return failure.skippedItems != null ? [
157
+ "skipped",
158
+ failure.kind,
159
+ collectionScopeKey(failure.scope)
160
+ ].join(" ") : [
151
161
  failure.kind,
152
162
  collectionScopeKey(failure.scope),
153
163
  failure.message ?? ""
154
164
  ].join(" ");
155
165
  }
156
166
  /**
167
+ * The items two reports of skipped items left out together: the union of their keys in reported order, complete only
168
+ * when both are, and a total that depends on whether the reads can overlap. Reads of the same scope that each search for
169
+ * something else (an issue the assignee and the author relationships both reach) are `'overlapping'`: which issues they
170
+ * share isn't known, so the total is the larger of the two and never less than the keys named, a lower bound. The pages
171
+ * of one drain are `'disjoint'`, so their totals add (a key repeated across pages that shifted counts once per page).
172
+ * Both are normalized first ({@link normalizeSkippedItems}).
173
+ */
174
+ function mergeSkippedItems(existing, next, reads) {
175
+ const a = normalizeSkippedItems(existing);
176
+ const b = normalizeSkippedItems(next);
177
+ const { keys, complete } = mergeUnreadableItems(a, b);
178
+ return {
179
+ keys,
180
+ total: reads === "disjoint" ? a.total + b.total : Math.max(a.total, b.total, keys.length),
181
+ complete
182
+ };
183
+ }
184
+ /**
157
185
  * A stable key for collapsing omissions accumulated across drained pages, matching the SDK's own
158
186
  * `dedupeOmissions`: kind plus scope IDs, deliberately WITHOUT `limit`/`totalCount`.
159
187
  *
@@ -168,20 +196,29 @@ function collectionOmissionKey(omission) {
168
196
  }
169
197
  /**
170
198
  * Merges SDK collection metadata across drained pages. Completeness follows {@link completenessRank};
171
- * failures are deduplicated by kind, scope IDs, and message, and omissions are collapsed per kind and scope
172
- * keeping the highest reported total ({@link collectionOmissionKey}). Both preserve first-reported order.
173
- * Returns `undefined` when no page supplied metadata, so metadata-free providers and test doubles keep
174
- * behaving as before.
199
+ * failures are deduplicated by kind and scope IDs for those that report `skippedItems` ({@link collectionFailureKey}),
200
+ * and by kind, scope IDs and message for the rest, and omissions are collapsed per kind and scope
201
+ * keeping the highest reported total ({@link collectionOmissionKey}). Both preserve first-reported order. Two failures
202
+ * that report skipped items merge them instead of the first winning ({@link mergeSkippedItems}), and the merged failure
203
+ * keeps the message of the one that names more keys (the first when they name as many), so the text covers at least
204
+ * what the larger report does, whichever came first. `reads` says how the items add up: `'overlapping'`, the default,
205
+ * suits any reads that may reach the same item (a user's relationships), and a caller merging the successive pages of
206
+ * one drain says `'disjoint'`. Returns `undefined` when no page supplied metadata, so metadata-free providers and test
207
+ * doubles keep behaving as before.
175
208
  */
176
- function mergeCollectionMetadata(base, next) {
209
+ function mergeCollectionMetadata(base, next, reads = "overlapping") {
177
210
  if (base == null) return next;
178
211
  if (next == null) return base;
179
212
  const completeness = completenessRank[next.completeness] > completenessRank[base.completeness] ? next.completeness : base.completeness;
180
213
  const failures = [...uniqueBy([...base.failures ?? [], ...next.failures ?? []], collectionFailureKey, (original, current) => {
181
- const unreadableItems = mergeUnreadableItems(original.unreadableItems, current.unreadableItems);
182
- return unreadableItems === original.unreadableItems ? original : {
214
+ if (original.skippedItems == null) return void 0;
215
+ const reported = current.skippedItems ?? original.skippedItems;
216
+ const named = (skipped) => normalizeSkippedItems(skipped).keys.length;
217
+ const fuller = named(reported) > named(original.skippedItems) ? current : original;
218
+ return {
183
219
  ...original,
184
- unreadableItems
220
+ ...fuller.message != null ? { message: fuller.message } : {},
221
+ skippedItems: mergeSkippedItems(original.skippedItems, reported, reads)
185
222
  };
186
223
  })];
187
224
  const omissions = [...uniqueBy([...base.omissions ?? [], ...next.omissions ?? []], collectionOmissionKey, (original, current) => (current.totalCount ?? -1) > (original.totalCount ?? -1) ? current : void 0)];
@@ -241,7 +278,7 @@ async function collectProviderPagedResult(fetch, maxPages = 20, scope) {
241
278
  });
242
279
  }
243
280
  values.push(...result.values);
244
- metadata = mergeCollectionMetadata(metadata, result.metadata);
281
+ metadata = mergeCollectionMetadata(metadata, result.metadata, "disjoint");
245
282
  truncated ||= result.paging?.truncated === true;
246
283
  if (!result.paging?.more) return build();
247
284
  if (result.paging.cursor === cursor) return build({ truncated: true });
@@ -255,6 +292,6 @@ async function collectProviderPagedResult(fetch, maxPages = 20, scope) {
255
292
  return build();
256
293
  }
257
294
  //#endregion
258
- export { collectProviderPagedResult, flatSettledOrThrow, flatSettledResultsOrThrow, mergeCollectionMetadata, parsePageCursor, readEtagFieldsByRepository, resolveBranchPullRequests, selectBranchPullRequests, throwIfAllSettledFailed, toPageCursor };
295
+ export { collectProviderPagedResult, flatSettledOrThrow, flatSettledResultsOrThrow, mergeCollectionMetadata, mergeSkippedItems, parsePageCursor, readEtagFieldsByRepository, resolveBranchPullRequests, selectBranchPullRequests, throwIfAllSettledFailed, toPageCursor };
259
296
 
260
297
  //# sourceMappingURL=providerPaging.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"providerPaging.js","names":[],"sources":["../../../../../../plus/integrations/src/providers/utils/providerPaging.ts"],"sourcesContent":["import type {\n\tCollectionCompleteness,\n\tCollectionMetadata,\n\tCollectionOmission,\n\tCollectionScopeFailure,\n} from '@gitkraken/provider-apis';\nimport type { PullRequestShape } from '@gitlens/git/models/pullRequest.js';\nimport { chunk } from '@gitlens/utils/array.js';\nimport { isCancellationError } from '@gitlens/utils/cancellation.js';\nimport { uniqueBy } from '@gitlens/utils/iterable.js';\nimport { mapSettledBounded } from '@gitlens/utils/promise.js';\nimport type { ProviderScopeFailure } from '../../collectionMetadata.js';\nimport { throwIfCallerContractError, toCollectionScopeFailure } from '../../collectionMetadata.js';\nimport { providerFanOutConcurrency } from '../../constants.js';\nimport { collectionScopeKey, mergeUnreadableItems } from '../../results.js';\nimport type { ProviderApiPagedResult, ProviderHierarchyResult } from '../models.js';\n\n/**\n * Encodes an opaque numeric paging token as the `{ value, type: 'page' }` cursor the paging layer uses.\n * The value is whatever the consumer round-trips unchanged: a 1-based page number for numbered-page reads,\n * or a provider offset (e.g. Bitbucket Server's `nextPageStart`) that is never reinterpreted here.\n */\nexport function toPageCursor(page: number): string {\n\treturn JSON.stringify({ value: page, type: 'page' });\n}\n\n/** Extracts the numeric paging token from a `{ value, type: 'page' }` cursor; undefined when absent/malformed. */\nexport function parsePageCursor(cursor: string | undefined): number | undefined {\n\tif (cursor == null || cursor === '{}') return undefined;\n\n\ttry {\n\t\tconst parsed = JSON.parse(cursor) as { value?: unknown; type?: unknown };\n\t\tif (parsed.type === 'page' && typeof parsed.value === 'number') return parsed.value;\n\t} catch {}\n\n\treturn undefined;\n}\n\n/** Preserves successful sibling scopes, but doesn't turn an all-scope provider failure into an empty success. */\nexport function throwIfAllSettledFailed<T>(results: PromiseSettledResult<T>[]): void {\n\tif (results.some(result => result.status === 'fulfilled')) return;\n\n\tconst rejected = results.find((result): result is PromiseRejectedResult => result.status === 'rejected');\n\tif (rejected != null) throw rejected.reason;\n}\n\n/**\n * The answer a pull-requests-by-branch read gives for one branch: the rows `matchesHead` accepts, newest first, at\n * most `limit` of them, each passed through `map`. Rows are filtered before they're mapped, so a non-matching row\n * can't fail the answer. `more` says the host holds rows it didn't return; the filter can't see those, so they make\n * the answer `truncated` however few rows passed it.\n */\nexport function selectBranchPullRequests<T, R>(\n\trows: readonly T[],\n\toptions: {\n\t\tmatchesHead: (row: T) => boolean;\n\t\tupdatedAt: (row: T) => number;\n\t\tmap: (row: T) => R;\n\t\tlimit: number;\n\t\tmore: boolean;\n\t},\n): { values: R[]; truncated: boolean } {\n\tconst matched = rows.filter(options.matchesHead).sort((a, b) => options.updatedAt(b) - options.updatedAt(a));\n\treturn {\n\t\tvalues: matched.slice(0, options.limit).map(options.map),\n\t\ttruncated: options.more || matched.length > options.limit,\n\t};\n}\n\n/**\n * The second step of a pull-requests-by-branch read on a host whose branch query can't return the batch read's\n * rows: resolves each target's matched numbers through `resolve` (the host's `getProviderPullRequestsBatch`), so a\n * pull request comes back identical whichever read found it.\n *\n * One `resolve` call for every target's numbers, so the host's own fan-out bounds the concurrency. A number it\n * reports absent (deleted between the two steps) is dropped; one it couldn't check rejects the whole target, since\n * a partial list would read as a complete one. The first step's order is kept.\n */\nexport async function resolveBranchPullRequests(\n\ttargets: readonly { owner: string; repo: string; project?: string }[],\n\tfound: readonly PromiseSettledResult<{ numbers: number[]; truncated: boolean }>[],\n\tresolve: (\n\t\tcoordinates: { owner: string; repo: string; number: number; project?: string }[],\n\t) => Promise<PromiseSettledResult<PullRequestShape | undefined>[] | undefined>,\n): Promise<PromiseSettledResult<{ pullRequests: PullRequestShape[]; truncated: boolean }>[]> {\n\tconst coordinates = found.flatMap((slot, i) =>\n\t\tslot.status === 'fulfilled'\n\t\t\t? slot.value.numbers.map(number => ({\n\t\t\t\t\towner: targets[i].owner,\n\t\t\t\t\trepo: targets[i].repo,\n\t\t\t\t\tnumber: number,\n\t\t\t\t\tproject: targets[i].project,\n\t\t\t\t}))\n\t\t\t: [],\n\t);\n\n\tlet resolved: PromiseSettledResult<PullRequestShape | undefined>[] = [];\n\tif (coordinates.length > 0) {\n\t\ttry {\n\t\t\tresolved =\n\t\t\t\t(await resolve(coordinates)) ??\n\t\t\t\tcoordinates.map(() => ({\n\t\t\t\t\tstatus: 'rejected',\n\t\t\t\t\treason: new Error('Pull requests could not be resolved'),\n\t\t\t\t}));\n\t\t} catch (ex) {\n\t\t\tresolved = coordinates.map(() => ({ status: 'rejected', reason: ex }));\n\t\t}\n\t}\n\n\tlet offset = 0;\n\treturn found.map(slot => {\n\t\tif (slot.status === 'rejected') return slot;\n\n\t\tconst slots = resolved.slice(offset, offset + slot.value.numbers.length);\n\t\toffset += slot.value.numbers.length;\n\n\t\tconst failure = slots.find((s): s is PromiseRejectedResult => s.status === 'rejected');\n\t\tif (failure != null) return failure;\n\n\t\treturn {\n\t\t\tstatus: 'fulfilled',\n\t\t\tvalue: {\n\t\t\t\tpullRequests: slots.flatMap(s => (s.status === 'fulfilled' && s.value != null ? [s.value] : [])),\n\t\t\t\ttruncated: slot.value.truncated,\n\t\t\t},\n\t\t};\n\t});\n}\n\n/**\n * Runs a cheap etag read over `coordinates` with one request per repository and chunk of `chunkSize` numbers, with\n * bounded concurrency, and maps the answers back to the coordinates positionally (`read` answers one entry per\n * number, in order, `undefined` for a proven absence). Settled per target: a request that throws rejects only its\n * own slots, and a slot whose conversion throws rejects only itself.\n */\nexport async function readEtagFieldsByRepository<Node, Fields>(\n\tcoordinates: readonly { owner: string; repo: string; number: number }[],\n\tchunkSize: number,\n\tread: (owner: string, repo: string, numbers: number[]) => Promise<(Node | undefined)[]>,\n\tmap: (node: Node) => Fields,\n): Promise<PromiseSettledResult<Fields | undefined>[]> {\n\tconst byRepository = new Map<string, number[]>();\n\tfor (const [index, c] of coordinates.entries()) {\n\t\tconst key = `${c.owner}/${c.repo}`;\n\t\tlet indices = byRepository.get(key);\n\t\tif (indices == null) {\n\t\t\tindices = [];\n\t\t\tbyRepository.set(key, indices);\n\t\t}\n\t\tindices.push(index);\n\t}\n\n\tconst requests = [...byRepository.values()].flatMap(indices =>\n\t\tchunk(indices, chunkSize).map(chunkIndices => ({ indices: chunkIndices })),\n\t);\n\tconst answers = await mapSettledBounded(requests, providerFanOutConcurrency, r => {\n\t\tconst { owner, repo } = coordinates[r.indices[0]];\n\t\treturn read(\n\t\t\towner,\n\t\t\trepo,\n\t\t\tr.indices.map(i => coordinates[i].number),\n\t\t);\n\t});\n\n\tconst slots = new Array<PromiseSettledResult<Fields | undefined>>(coordinates.length);\n\tfor (const [i, answer] of answers.entries()) {\n\t\tfor (const [j, index] of requests[i].indices.entries()) {\n\t\t\tif (answer.status === 'rejected') {\n\t\t\t\tslots[index] = answer;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst node = answer.value[j];\n\t\t\ttry {\n\t\t\t\tslots[index] = { status: 'fulfilled', value: node != null ? map(node) : undefined };\n\t\t\t} catch (ex) {\n\t\t\t\tslots[index] = { status: 'rejected', reason: ex };\n\t\t\t}\n\t\t}\n\t}\n\treturn slots;\n}\n\nexport function flatSettledResultsOrThrow<T>(results: PromiseSettledResult<T[]>[]): T[] {\n\tthrowIfAllSettledFailed(results);\n\n\tconst fulfilled = results.filter((result): result is PromiseFulfilledResult<T[]> => result.status === 'fulfilled');\n\treturn fulfilled.flatMap(result => result.value);\n}\n\nexport async function flatSettledOrThrow<T>(promises: Promise<T[]>[]): Promise<T[]> {\n\treturn flatSettledResultsOrThrow(await Promise.allSettled(promises));\n}\n\n/** Precedence for merged completeness: any known omission (`partial`) wins, then inability to confirm\n * (`unknown`), and only an all-`complete` set of pages stays `complete`. */\nconst completenessRank: Record<CollectionCompleteness, number> = { partial: 2, unknown: 1, complete: 0 };\n\n/** A stable key for deduplicating structurally-identical scope failures accumulated across drained pages. */\nfunction collectionFailureKey(failure: CollectionScopeFailure): string {\n\treturn [failure.kind, collectionScopeKey(failure.scope), failure.message ?? ''].join(' ');\n}\n\n/**\n * A stable key for collapsing omissions accumulated across drained pages, matching the SDK's own\n * `dedupeOmissions`: kind plus scope IDs, deliberately WITHOUT `limit`/`totalCount`.\n *\n * Those counts are a re-measurement, not an identity. GitHub recomputes the match total on every request, so\n * one repository drained over several pages reports the same cap with a drifting total; keying on the total\n * would emit a near-identical warning per page (\"matched 1393…\", \"matched 1402…\") that\n * `appendDedupedWarning` cannot collapse, since the messages genuinely differ. Independently-scoped omissions\n * still stay distinct, which is the case that carries information.\n */\nfunction collectionOmissionKey(omission: CollectionOmission): string {\n\treturn [omission.kind, collectionScopeKey(omission.scope)].join(' ');\n}\n\n/**\n * Merges SDK collection metadata across drained pages. Completeness follows {@link completenessRank};\n * failures are deduplicated by kind, scope IDs, and message, and omissions are collapsed per kind and scope\n * keeping the highest reported total ({@link collectionOmissionKey}). Both preserve first-reported order.\n * Returns `undefined` when no page supplied metadata, so metadata-free providers and test doubles keep\n * behaving as before.\n */\nexport function mergeCollectionMetadata(\n\tbase: CollectionMetadata | undefined,\n\tnext: CollectionMetadata | undefined,\n): CollectionMetadata | undefined {\n\tif (base == null) return next;\n\tif (next == null) return base;\n\n\tconst completeness =\n\t\tcompletenessRank[next.completeness] > completenessRank[base.completeness]\n\t\t\t? next.completeness\n\t\t\t: base.completeness;\n\n\t// First occurrence wins: a repeated failure carries no new information, since the message is part of its key. Except\n\t// for the items it says it left out, which two reads reporting the same text can differ in.\n\tconst failures = [\n\t\t...uniqueBy<string, ProviderScopeFailure>(\n\t\t\t[...(base.failures ?? []), ...(next.failures ?? [])],\n\t\t\tcollectionFailureKey,\n\t\t\t(original, current) => {\n\t\t\t\tconst unreadableItems = mergeUnreadableItems(original.unreadableItems, current.unreadableItems);\n\t\t\t\treturn unreadableItems === original.unreadableItems\n\t\t\t\t\t? original\n\t\t\t\t\t: { ...original, unreadableItems: unreadableItems };\n\t\t\t},\n\t\t),\n\t];\n\t// Highest total wins, so re-measuring one cap across pages collapses to a single omission holding the\n\t// largest figure reported for it — the same rule the SDK applies in `createCollectionMetadata`.\n\tconst omissions = [\n\t\t...uniqueBy(\n\t\t\t[...(base.omissions ?? []), ...(next.omissions ?? [])],\n\t\t\tcollectionOmissionKey,\n\t\t\t(original, current) => ((current.totalCount ?? -1) > (original.totalCount ?? -1) ? current : undefined),\n\t\t),\n\t];\n\n\treturn {\n\t\tcompleteness: completeness,\n\t\t...(failures.length ? { failures: failures } : {}),\n\t\t...(omissions.length ? { omissions: omissions } : {}),\n\t};\n}\n\n/**\n * Drains a provider paged fetcher into a single result while preserving enough metadata to\n * signal whether the defensive backstop interrupted the drain, and merging SDK collection metadata\n * ({@link mergeCollectionMetadata}) across the fetched pages.\n *\n * The local `truncated` flag and SDK `metadata` are distinct facts: a page-drain backstop must remain visible\n * even if every fetched page reported `complete`, and SDK incompleteness is preserved even when the drain\n * finished within its page budget.\n */\nexport async function collectProviderPagedResult<T>(\n\tfetch: (cursor: string | undefined) => Promise<ProviderApiPagedResult<T> | undefined>,\n\tmaxPages = 20,\n\tscope?: CollectionScopeFailure['scope'],\n): Promise<ProviderHierarchyResult<T>> {\n\tconst values: NonNullable<T>[] = [];\n\tlet cursor: string | undefined;\n\tlet metadata: CollectionMetadata | undefined;\n\tlet truncated = false;\n\n\t// Omit `metadata` entirely when no page supplied it, so a metadata-free drain stays deep-equal to its\n\t// pre-metadata shape (and consumers never see an explicit `undefined`).\n\tconst build = (extra?: Partial<ProviderHierarchyResult<T>>): ProviderHierarchyResult<T> => {\n\t\tconst mergedMetadata = extra?.metadata ?? metadata;\n\t\treturn {\n\t\t\tvalues: values,\n\t\t\t...extra,\n\t\t\t...(truncated || extra?.truncated === true ? { truncated: true } : {}),\n\t\t\t...(mergedMetadata != null ? { metadata: mergedMetadata } : {}),\n\t\t};\n\t};\n\n\tfor (let page = 0; page < maxPages; page++) {\n\t\tlet result: ProviderApiPagedResult<T> | undefined;\n\t\ttry {\n\t\t\tresult = await fetch(cursor);\n\t\t} catch (ex) {\n\t\t\tif (isCancellationError(ex)) throw ex;\n\n\t\t\t// A caller-contract error is not a fact about this scope, so it is never recorded as one — see\n\t\t\t// `throwIfCallerContractError`. Checked alongside cancellation because both are errors that a\n\t\t\t// per-scope failure would misdescribe.\n\t\t\tthrowIfCallerContractError(ex);\n\n\t\t\t// When the caller supplied a scope, preserve the items already fetched from that scope and record the\n\t\t\t// failure in collection metadata rather than re-throwing and discarding the prefix. Callers without a\n\t\t\t// scope keep the legacy throw behavior.\n\t\t\tif (scope == null) throw ex;\n\t\t\treturn build({\n\t\t\t\ttruncated: true,\n\t\t\t\tmetadata: mergeCollectionMetadata(metadata, {\n\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\tfailures: [toCollectionScopeFailure(scope, ex)],\n\t\t\t\t}),\n\t\t\t});\n\t\t}\n\t\tif (result == null) {\n\t\t\tif (page === 0) return build();\n\n\t\t\treturn build({\n\t\t\t\ttruncated: true,\n\t\t\t\t...(scope != null\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tmetadata: mergeCollectionMetadata(metadata, {\n\t\t\t\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\t\t\t\tfailures: [\n\t\t\t\t\t\t\t\t\ttoCollectionScopeFailure(\n\t\t\t\t\t\t\t\t\t\tscope,\n\t\t\t\t\t\t\t\t\t\tnew Error('Provider returned no page after advertising a continuation'),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined),\n\t\t\t});\n\t\t}\n\n\t\tvalues.push(...result.values);\n\t\tmetadata = mergeCollectionMetadata(metadata, result.metadata);\n\t\ttruncated ||= result.paging?.truncated === true;\n\n\t\tif (!result.paging?.more) return build();\n\n\t\tif (result.paging.cursor === cursor) {\n\t\t\treturn build({ truncated: true });\n\t\t}\n\n\t\tcursor = result.paging.cursor;\n\t\tif (cursor == null || cursor === '{}') {\n\t\t\treturn build({ truncated: true });\n\t\t}\n\t\tif (page === maxPages - 1) {\n\t\t\treturn build({ paging: result.paging, truncated: true });\n\t\t}\n\t}\n\n\treturn build();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"providerPaging.js","names":[],"sources":["../../../../../../plus/integrations/src/providers/utils/providerPaging.ts"],"sourcesContent":["import type {\n\tCollectionCompleteness,\n\tCollectionMetadata,\n\tCollectionOmission,\n\tCollectionScopeFailure,\n\tCollectionSkippedItems,\n} from '@gitkraken/provider-apis';\nimport type { PullRequestShape } from '@gitlens/git/models/pullRequest.js';\nimport { chunk } from '@gitlens/utils/array.js';\nimport { isCancellationError } from '@gitlens/utils/cancellation.js';\nimport { uniqueBy } from '@gitlens/utils/iterable.js';\nimport { mapSettledBounded } from '@gitlens/utils/promise.js';\nimport type { ProviderScopeFailure } from '../../collectionMetadata.js';\nimport {\n\tnormalizeSkippedItems,\n\tthrowIfCallerContractError,\n\ttoCollectionScopeFailure,\n} from '../../collectionMetadata.js';\nimport { providerFanOutConcurrency } from '../../constants.js';\nimport { collectionScopeKey, mergeUnreadableItems } from '../../results.js';\nimport type { ProviderApiPagedResult, ProviderHierarchyResult } from '../models.js';\n\n/**\n * Encodes an opaque numeric paging token as the `{ value, type: 'page' }` cursor the paging layer uses.\n * The value is whatever the consumer round-trips unchanged: a 1-based page number for numbered-page reads,\n * or a provider offset (e.g. Bitbucket Server's `nextPageStart`) that is never reinterpreted here.\n */\nexport function toPageCursor(page: number): string {\n\treturn JSON.stringify({ value: page, type: 'page' });\n}\n\n/** Extracts the numeric paging token from a `{ value, type: 'page' }` cursor; undefined when absent/malformed. */\nexport function parsePageCursor(cursor: string | undefined): number | undefined {\n\tif (cursor == null || cursor === '{}') return undefined;\n\n\ttry {\n\t\tconst parsed = JSON.parse(cursor) as { value?: unknown; type?: unknown };\n\t\tif (parsed.type === 'page' && typeof parsed.value === 'number') return parsed.value;\n\t} catch {}\n\n\treturn undefined;\n}\n\n/** Preserves successful sibling scopes, but doesn't turn an all-scope provider failure into an empty success. */\nexport function throwIfAllSettledFailed<T>(results: PromiseSettledResult<T>[]): void {\n\tif (results.some(result => result.status === 'fulfilled')) return;\n\n\tconst rejected = results.find((result): result is PromiseRejectedResult => result.status === 'rejected');\n\tif (rejected != null) throw rejected.reason;\n}\n\n/**\n * The answer a pull-requests-by-branch read gives for one branch: the rows `matchesHead` accepts, newest first, at\n * most `limit` of them, each passed through `map`. Rows are filtered before they're mapped, so a non-matching row\n * can't fail the answer. `more` says the host holds rows it didn't return; the filter can't see those, so they make\n * the answer `truncated` however few rows passed it.\n */\nexport function selectBranchPullRequests<T, R>(\n\trows: readonly T[],\n\toptions: {\n\t\tmatchesHead: (row: T) => boolean;\n\t\tupdatedAt: (row: T) => number;\n\t\tmap: (row: T) => R;\n\t\tlimit: number;\n\t\tmore: boolean;\n\t},\n): { values: R[]; truncated: boolean } {\n\tconst matched = rows.filter(options.matchesHead).sort((a, b) => options.updatedAt(b) - options.updatedAt(a));\n\treturn {\n\t\tvalues: matched.slice(0, options.limit).map(options.map),\n\t\ttruncated: options.more || matched.length > options.limit,\n\t};\n}\n\n/**\n * The second step of a pull-requests-by-branch read on a host whose branch query can't return the batch read's\n * rows: resolves each target's matched numbers through `resolve` (the host's `getProviderPullRequestsBatch`), so a\n * pull request comes back identical whichever read found it.\n *\n * One `resolve` call for every target's numbers, so the host's own fan-out bounds the concurrency. A number it\n * reports absent (deleted between the two steps) is dropped; one it couldn't check rejects the whole target, since\n * a partial list would read as a complete one. The first step's order is kept.\n */\nexport async function resolveBranchPullRequests(\n\ttargets: readonly { owner: string; repo: string; project?: string }[],\n\tfound: readonly PromiseSettledResult<{ numbers: number[]; truncated: boolean }>[],\n\tresolve: (\n\t\tcoordinates: { owner: string; repo: string; number: number; project?: string }[],\n\t) => Promise<PromiseSettledResult<PullRequestShape | undefined>[] | undefined>,\n): Promise<PromiseSettledResult<{ pullRequests: PullRequestShape[]; truncated: boolean }>[]> {\n\tconst coordinates = found.flatMap((slot, i) =>\n\t\tslot.status === 'fulfilled'\n\t\t\t? slot.value.numbers.map(number => ({\n\t\t\t\t\towner: targets[i].owner,\n\t\t\t\t\trepo: targets[i].repo,\n\t\t\t\t\tnumber: number,\n\t\t\t\t\tproject: targets[i].project,\n\t\t\t\t}))\n\t\t\t: [],\n\t);\n\n\tlet resolved: PromiseSettledResult<PullRequestShape | undefined>[] = [];\n\tif (coordinates.length > 0) {\n\t\ttry {\n\t\t\tresolved =\n\t\t\t\t(await resolve(coordinates)) ??\n\t\t\t\tcoordinates.map(() => ({\n\t\t\t\t\tstatus: 'rejected',\n\t\t\t\t\treason: new Error('Pull requests could not be resolved'),\n\t\t\t\t}));\n\t\t} catch (ex) {\n\t\t\tresolved = coordinates.map(() => ({ status: 'rejected', reason: ex }));\n\t\t}\n\t}\n\n\tlet offset = 0;\n\treturn found.map(slot => {\n\t\tif (slot.status === 'rejected') return slot;\n\n\t\tconst slots = resolved.slice(offset, offset + slot.value.numbers.length);\n\t\toffset += slot.value.numbers.length;\n\n\t\tconst failure = slots.find((s): s is PromiseRejectedResult => s.status === 'rejected');\n\t\tif (failure != null) return failure;\n\n\t\treturn {\n\t\t\tstatus: 'fulfilled',\n\t\t\tvalue: {\n\t\t\t\tpullRequests: slots.flatMap(s => (s.status === 'fulfilled' && s.value != null ? [s.value] : [])),\n\t\t\t\ttruncated: slot.value.truncated,\n\t\t\t},\n\t\t};\n\t});\n}\n\n/**\n * Runs a cheap etag read over `coordinates` with one request per repository and chunk of `chunkSize` numbers, with\n * bounded concurrency, and maps the answers back to the coordinates positionally (`read` answers one entry per\n * number, in order, `undefined` for a proven absence). Settled per target: a request that throws rejects only its\n * own slots, and a slot whose conversion throws rejects only itself.\n */\nexport async function readEtagFieldsByRepository<Node, Fields>(\n\tcoordinates: readonly { owner: string; repo: string; number: number }[],\n\tchunkSize: number,\n\tread: (owner: string, repo: string, numbers: number[]) => Promise<(Node | undefined)[]>,\n\tmap: (node: Node) => Fields,\n): Promise<PromiseSettledResult<Fields | undefined>[]> {\n\tconst byRepository = new Map<string, number[]>();\n\tfor (const [index, c] of coordinates.entries()) {\n\t\tconst key = `${c.owner}/${c.repo}`;\n\t\tlet indices = byRepository.get(key);\n\t\tif (indices == null) {\n\t\t\tindices = [];\n\t\t\tbyRepository.set(key, indices);\n\t\t}\n\t\tindices.push(index);\n\t}\n\n\tconst requests = [...byRepository.values()].flatMap(indices =>\n\t\tchunk(indices, chunkSize).map(chunkIndices => ({ indices: chunkIndices })),\n\t);\n\tconst answers = await mapSettledBounded(requests, providerFanOutConcurrency, r => {\n\t\tconst { owner, repo } = coordinates[r.indices[0]];\n\t\treturn read(\n\t\t\towner,\n\t\t\trepo,\n\t\t\tr.indices.map(i => coordinates[i].number),\n\t\t);\n\t});\n\n\tconst slots = new Array<PromiseSettledResult<Fields | undefined>>(coordinates.length);\n\tfor (const [i, answer] of answers.entries()) {\n\t\tfor (const [j, index] of requests[i].indices.entries()) {\n\t\t\tif (answer.status === 'rejected') {\n\t\t\t\tslots[index] = answer;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst node = answer.value[j];\n\t\t\ttry {\n\t\t\t\tslots[index] = { status: 'fulfilled', value: node != null ? map(node) : undefined };\n\t\t\t} catch (ex) {\n\t\t\t\tslots[index] = { status: 'rejected', reason: ex };\n\t\t\t}\n\t\t}\n\t}\n\treturn slots;\n}\n\nexport function flatSettledResultsOrThrow<T>(results: PromiseSettledResult<T[]>[]): T[] {\n\tthrowIfAllSettledFailed(results);\n\n\tconst fulfilled = results.filter((result): result is PromiseFulfilledResult<T[]> => result.status === 'fulfilled');\n\treturn fulfilled.flatMap(result => result.value);\n}\n\nexport async function flatSettledOrThrow<T>(promises: Promise<T[]>[]): Promise<T[]> {\n\treturn flatSettledResultsOrThrow(await Promise.allSettled(promises));\n}\n\n/** Precedence for merged completeness: any known omission (`partial`) wins, then inability to confirm\n * (`unknown`), and only an all-`complete` set of pages stays `complete`. */\nconst completenessRank: Record<CollectionCompleteness, number> = { partial: 2, unknown: 1, complete: 0 };\n\n/**\n * A stable key for deduplicating structurally-identical scope failures accumulated across drained pages: kind, scope IDs\n * and message. A failure that reports skipped items is keyed by kind and scope alone, never by message: it is one\n * account of what a scope skipped, however the reads that reported it worded it or split the issues over their pages\n * (the message is display prose, and names a different handful of issues and pages in each). And it never shares a key\n * with one that doesn't: that one says nothing of which items were read, however alike their messages.\n */\nfunction collectionFailureKey(failure: CollectionScopeFailure): string {\n\treturn failure.skippedItems != null\n\t\t? ['skipped', failure.kind, collectionScopeKey(failure.scope)].join(' ')\n\t\t: [failure.kind, collectionScopeKey(failure.scope), failure.message ?? ''].join(' ');\n}\n\n/**\n * The items two reports of skipped items left out together: the union of their keys in reported order, complete only\n * when both are, and a total that depends on whether the reads can overlap. Reads of the same scope that each search for\n * something else (an issue the assignee and the author relationships both reach) are `'overlapping'`: which issues they\n * share isn't known, so the total is the larger of the two and never less than the keys named, a lower bound. The pages\n * of one drain are `'disjoint'`, so their totals add (a key repeated across pages that shifted counts once per page).\n * Both are normalized first ({@link normalizeSkippedItems}).\n */\nexport function mergeSkippedItems(\n\texisting: CollectionSkippedItems,\n\tnext: CollectionSkippedItems,\n\treads: 'disjoint' | 'overlapping',\n): CollectionSkippedItems {\n\tconst a = normalizeSkippedItems(existing);\n\tconst b = normalizeSkippedItems(next);\n\tconst { keys, complete } = mergeUnreadableItems(a, b);\n\treturn {\n\t\tkeys: keys,\n\t\ttotal: reads === 'disjoint' ? a.total + b.total : Math.max(a.total, b.total, keys.length),\n\t\tcomplete: complete,\n\t};\n}\n\n/**\n * A stable key for collapsing omissions accumulated across drained pages, matching the SDK's own\n * `dedupeOmissions`: kind plus scope IDs, deliberately WITHOUT `limit`/`totalCount`.\n *\n * Those counts are a re-measurement, not an identity. GitHub recomputes the match total on every request, so\n * one repository drained over several pages reports the same cap with a drifting total; keying on the total\n * would emit a near-identical warning per page (\"matched 1393…\", \"matched 1402…\") that\n * `appendDedupedWarning` cannot collapse, since the messages genuinely differ. Independently-scoped omissions\n * still stay distinct, which is the case that carries information.\n */\nfunction collectionOmissionKey(omission: CollectionOmission): string {\n\treturn [omission.kind, collectionScopeKey(omission.scope)].join(' ');\n}\n\n/**\n * Merges SDK collection metadata across drained pages. Completeness follows {@link completenessRank};\n * failures are deduplicated by kind and scope IDs for those that report `skippedItems` ({@link collectionFailureKey}),\n * and by kind, scope IDs and message for the rest, and omissions are collapsed per kind and scope\n * keeping the highest reported total ({@link collectionOmissionKey}). Both preserve first-reported order. Two failures\n * that report skipped items merge them instead of the first winning ({@link mergeSkippedItems}), and the merged failure\n * keeps the message of the one that names more keys (the first when they name as many), so the text covers at least\n * what the larger report does, whichever came first. `reads` says how the items add up: `'overlapping'`, the default,\n * suits any reads that may reach the same item (a user's relationships), and a caller merging the successive pages of\n * one drain says `'disjoint'`. Returns `undefined` when no page supplied metadata, so metadata-free providers and test\n * doubles keep behaving as before.\n */\nexport function mergeCollectionMetadata(\n\tbase: CollectionMetadata | undefined,\n\tnext: CollectionMetadata | undefined,\n\treads: 'disjoint' | 'overlapping' = 'overlapping',\n): CollectionMetadata | undefined {\n\tif (base == null) return next;\n\tif (next == null) return base;\n\n\tconst completeness =\n\t\tcompletenessRank[next.completeness] > completenessRank[base.completeness]\n\t\t\t? next.completeness\n\t\t\t: base.completeness;\n\n\t// First occurrence wins: a repeated failure carries no new information, since what identifies it is what it says.\n\t// Except for one that reports skipped items, which is keyed without its message and so merges what each report\n\t// skipped; the key guarantees `current` reports skipped items too.\n\tconst failures = [\n\t\t...uniqueBy<string, ProviderScopeFailure>(\n\t\t\t[...(base.failures ?? []), ...(next.failures ?? [])],\n\t\t\tcollectionFailureKey,\n\t\t\t(original, current) => {\n\t\t\t\tif (original.skippedItems == null) return undefined;\n\n\t\t\t\tconst reported = current.skippedItems ?? original.skippedItems;\n\t\t\t\tconst named = (skipped: CollectionSkippedItems) => normalizeSkippedItems(skipped).keys.length;\n\t\t\t\tconst fuller = named(reported) > named(original.skippedItems) ? current : original;\n\t\t\t\treturn {\n\t\t\t\t\t...original,\n\t\t\t\t\t...(fuller.message != null ? { message: fuller.message } : {}),\n\t\t\t\t\tskippedItems: mergeSkippedItems(original.skippedItems, reported, reads),\n\t\t\t\t};\n\t\t\t},\n\t\t),\n\t];\n\t// Highest total wins, so re-measuring one cap across pages collapses to a single omission holding the\n\t// largest figure reported for it — the same rule the SDK applies in `createCollectionMetadata`.\n\tconst omissions = [\n\t\t...uniqueBy(\n\t\t\t[...(base.omissions ?? []), ...(next.omissions ?? [])],\n\t\t\tcollectionOmissionKey,\n\t\t\t(original, current) => ((current.totalCount ?? -1) > (original.totalCount ?? -1) ? current : undefined),\n\t\t),\n\t];\n\n\treturn {\n\t\tcompleteness: completeness,\n\t\t...(failures.length ? { failures: failures } : {}),\n\t\t...(omissions.length ? { omissions: omissions } : {}),\n\t};\n}\n\n/**\n * Drains a provider paged fetcher into a single result while preserving enough metadata to\n * signal whether the defensive backstop interrupted the drain, and merging SDK collection metadata\n * ({@link mergeCollectionMetadata}) across the fetched pages.\n *\n * The local `truncated` flag and SDK `metadata` are distinct facts: a page-drain backstop must remain visible\n * even if every fetched page reported `complete`, and SDK incompleteness is preserved even when the drain\n * finished within its page budget.\n */\nexport async function collectProviderPagedResult<T>(\n\tfetch: (cursor: string | undefined) => Promise<ProviderApiPagedResult<T> | undefined>,\n\tmaxPages = 20,\n\tscope?: CollectionScopeFailure['scope'],\n): Promise<ProviderHierarchyResult<T>> {\n\tconst values: NonNullable<T>[] = [];\n\tlet cursor: string | undefined;\n\tlet metadata: CollectionMetadata | undefined;\n\tlet truncated = false;\n\n\t// Omit `metadata` entirely when no page supplied it, so a metadata-free drain stays deep-equal to its\n\t// pre-metadata shape (and consumers never see an explicit `undefined`).\n\tconst build = (extra?: Partial<ProviderHierarchyResult<T>>): ProviderHierarchyResult<T> => {\n\t\tconst mergedMetadata = extra?.metadata ?? metadata;\n\t\treturn {\n\t\t\tvalues: values,\n\t\t\t...extra,\n\t\t\t...(truncated || extra?.truncated === true ? { truncated: true } : {}),\n\t\t\t...(mergedMetadata != null ? { metadata: mergedMetadata } : {}),\n\t\t};\n\t};\n\n\tfor (let page = 0; page < maxPages; page++) {\n\t\tlet result: ProviderApiPagedResult<T> | undefined;\n\t\ttry {\n\t\t\tresult = await fetch(cursor);\n\t\t} catch (ex) {\n\t\t\tif (isCancellationError(ex)) throw ex;\n\n\t\t\t// A caller-contract error is not a fact about this scope, so it is never recorded as one — see\n\t\t\t// `throwIfCallerContractError`. Checked alongside cancellation because both are errors that a\n\t\t\t// per-scope failure would misdescribe.\n\t\t\tthrowIfCallerContractError(ex);\n\n\t\t\t// When the caller supplied a scope, preserve the items already fetched from that scope and record the\n\t\t\t// failure in collection metadata rather than re-throwing and discarding the prefix. Callers without a\n\t\t\t// scope keep the legacy throw behavior.\n\t\t\tif (scope == null) throw ex;\n\t\t\treturn build({\n\t\t\t\ttruncated: true,\n\t\t\t\tmetadata: mergeCollectionMetadata(metadata, {\n\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\tfailures: [toCollectionScopeFailure(scope, ex)],\n\t\t\t\t}),\n\t\t\t});\n\t\t}\n\t\tif (result == null) {\n\t\t\tif (page === 0) return build();\n\n\t\t\treturn build({\n\t\t\t\ttruncated: true,\n\t\t\t\t...(scope != null\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tmetadata: mergeCollectionMetadata(metadata, {\n\t\t\t\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\t\t\t\tfailures: [\n\t\t\t\t\t\t\t\t\ttoCollectionScopeFailure(\n\t\t\t\t\t\t\t\t\t\tscope,\n\t\t\t\t\t\t\t\t\t\tnew Error('Provider returned no page after advertising a continuation'),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined),\n\t\t\t});\n\t\t}\n\n\t\tvalues.push(...result.values);\n\t\tmetadata = mergeCollectionMetadata(metadata, result.metadata, 'disjoint');\n\t\ttruncated ||= result.paging?.truncated === true;\n\n\t\tif (!result.paging?.more) return build();\n\n\t\tif (result.paging.cursor === cursor) {\n\t\t\treturn build({ truncated: true });\n\t\t}\n\n\t\tcursor = result.paging.cursor;\n\t\tif (cursor == null || cursor === '{}') {\n\t\t\treturn build({ truncated: true });\n\t\t}\n\t\tif (page === maxPages - 1) {\n\t\t\treturn build({ paging: result.paging, truncated: true });\n\t\t}\n\t}\n\n\treturn build();\n}\n"],"mappings":"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"}
@@ -2,6 +2,7 @@ import { ResourceDescriptor } from "../../../git/models/resourceDescriptor.js";
2
2
  import { PullRequest, PullRequestFilter, PullRequestStateFilter } from "../../../git/models/pullRequest.js";
3
3
  import { IntegrationIds } from "../constants.js";
4
4
  import { ProviderWarning } from "../results.js";
5
+ import "../providerFilters.js";
5
6
  import { ProviderPullRequest, ProviderReposInput, ProviderRepository } from "../providers/models.js";
6
7
  import { GitHostIntegration } from "../models/gitHostIntegration.js";
7
8
  import { IssuesIntegration } from "../models/issuesIntegration.js";
@@ -101,7 +101,7 @@ async function readAccountWideIssuesPage({ options, integration, domain, warnOnM
101
101
  warnings,
102
102
  readPage: readAccountWidePage,
103
103
  fold: (p) => {
104
- allMetadata = mergeCollectionMetadata(allMetadata, p.metadata);
104
+ allMetadata = mergeCollectionMetadata(allMetadata, p.metadata, "disjoint");
105
105
  }
106
106
  });
107
107
  const { value, currentPage, requestedPageMissing } = drained;