@gitkraken/core-gitlens 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/CHANGELOG.md +78 -1
  2. package/dist/git/cache.d.ts +84 -29
  3. package/dist/git/cache.js +188 -54
  4. package/dist/git/cache.js.map +1 -1
  5. package/dist/git/features.d.ts +1 -1
  6. package/dist/git/features.js +5 -0
  7. package/dist/git/features.js.map +1 -1
  8. package/dist/git/models/branch.d.ts +20 -2
  9. package/dist/git/models/branch.js +28 -6
  10. package/dist/git/models/branch.js.map +1 -1
  11. package/dist/git/models/reference.d.ts +3 -0
  12. package/dist/git/models/status.js +2 -2
  13. package/dist/git/models/worktree.d.ts +4 -1
  14. package/dist/git/models/worktree.js +4 -2
  15. package/dist/git/models/worktree.js.map +1 -1
  16. package/dist/git/parsers/diffParser.js +1 -1
  17. package/dist/git/providers/branches.d.ts +73 -8
  18. package/dist/git/providers/operations.d.ts +3 -2
  19. package/dist/git/providers/pausedOperations.d.ts +6 -0
  20. package/dist/git/providers/provider.d.ts +45 -2
  21. package/dist/git/providers/revision.d.ts +17 -3
  22. package/dist/git/providers/status.d.ts +8 -2
  23. package/dist/git/remotes/azure-devops.js +61 -53
  24. package/dist/git/remotes/azure-devops.js.map +1 -1
  25. package/dist/git/run.types.d.ts +12 -0
  26. package/dist/git/service.d.ts +8 -5
  27. package/dist/git/service.js +16 -8
  28. package/dist/git/service.js.map +1 -1
  29. package/dist/git/utils/branch.utils.d.ts +12 -2
  30. package/dist/git/utils/branch.utils.js +49 -5
  31. package/dist/git/utils/branch.utils.js.map +1 -1
  32. package/dist/git/utils/reference.utils.d.ts +1 -0
  33. package/dist/git/utils/reference.utils.js.map +1 -1
  34. package/dist/git/watching/changeEvent.d.ts +13 -2
  35. package/dist/git/watching/changeEvent.js +17 -3
  36. package/dist/git/watching/changeEvent.js.map +1 -1
  37. package/dist/git/watching/gitDirPoller.d.ts +111 -0
  38. package/dist/git/watching/gitDirPoller.js +266 -0
  39. package/dist/git/watching/gitDirPoller.js.map +1 -0
  40. package/dist/git/watching/provider.d.ts +16 -0
  41. package/dist/git/watching/watchGroup.d.ts +40 -4
  42. package/dist/git/watching/watchGroup.js +143 -17
  43. package/dist/git/watching/watchGroup.js.map +1 -1
  44. package/dist/git/watching/watchService.d.ts +70 -1
  45. package/dist/git/watching/watchService.js +158 -13
  46. package/dist/git/watching/watchService.js.map +1 -1
  47. package/dist/git/watching/watchSession.d.ts +44 -0
  48. package/dist/git/watching/watchSession.js +91 -6
  49. package/dist/git/watching/watchSession.js.map +1 -1
  50. package/dist/git/watching/watcherPatterns.d.ts +21 -0
  51. package/dist/git/watching/watcherPatterns.js +67 -1
  52. package/dist/git/watching/watcherPatterns.js.map +1 -1
  53. package/dist/git/watching/worktreeSummary.d.ts +34 -7
  54. package/dist/git/watching/worktreeSummary.js +39 -12
  55. package/dist/git/watching/worktreeSummary.js.map +1 -1
  56. package/dist/git-cli/cliGitProvider.d.ts +40 -2
  57. package/dist/git-cli/cliGitProvider.js +182 -18
  58. package/dist/git-cli/cliGitProvider.js.map +1 -1
  59. package/dist/git-cli/exec/commandClassifier.d.ts +15 -0
  60. package/dist/git-cli/exec/commandClassifier.js +48 -2
  61. package/dist/git-cli/exec/commandClassifier.js.map +1 -1
  62. package/dist/git-cli/exec/git.d.ts +20 -3
  63. package/dist/git-cli/exec/git.js +63 -19
  64. package/dist/git-cli/exec/git.js.map +1 -1
  65. package/dist/git-cli/exec/gitQueue.d.ts +2 -0
  66. package/dist/git-cli/exec/gitQueue.js +2 -1
  67. package/dist/git-cli/exec/gitQueue.js.map +1 -1
  68. package/dist/git-cli/exec/indexLock.d.ts +39 -0
  69. package/dist/git-cli/exec/indexLock.js +160 -0
  70. package/dist/git-cli/exec/indexLock.js.map +1 -0
  71. package/dist/git-cli/parsers/blameParser.js +2 -2
  72. package/dist/git-cli/parsers/refParser.js +4 -2
  73. package/dist/git-cli/parsers/refParser.js.map +1 -1
  74. package/dist/git-cli/parsers/statusParser.d.ts +6 -0
  75. package/dist/git-cli/parsers/statusParser.js +33 -3
  76. package/dist/git-cli/parsers/statusParser.js.map +1 -1
  77. package/dist/git-cli/parsers/worktreeParser.js +42 -2
  78. package/dist/git-cli/parsers/worktreeParser.js.map +1 -1
  79. package/dist/git-cli/providers/branchRemote.d.ts +14 -0
  80. package/dist/git-cli/providers/branchRemote.js +36 -0
  81. package/dist/git-cli/providers/branchRemote.js.map +1 -0
  82. package/dist/git-cli/providers/branches.d.ts +109 -10
  83. package/dist/git-cli/providers/branches.js +630 -134
  84. package/dist/git-cli/providers/branches.js.map +1 -1
  85. package/dist/git-cli/providers/commits.js +2 -2
  86. package/dist/git-cli/providers/config.d.ts +9 -5
  87. package/dist/git-cli/providers/config.js +58 -25
  88. package/dist/git-cli/providers/config.js.map +1 -1
  89. package/dist/git-cli/providers/contributors.js +1 -1
  90. package/dist/git-cli/providers/graph.js +9 -6
  91. package/dist/git-cli/providers/graph.js.map +1 -1
  92. package/dist/git-cli/providers/operations.d.ts +17 -0
  93. package/dist/git-cli/providers/operations.js +172 -111
  94. package/dist/git-cli/providers/operations.js.map +1 -1
  95. package/dist/git-cli/providers/pausedOperations.d.ts +8 -0
  96. package/dist/git-cli/providers/pausedOperations.js +59 -39
  97. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  98. package/dist/git-cli/providers/refs.js +11 -8
  99. package/dist/git-cli/providers/refs.js.map +1 -1
  100. package/dist/git-cli/providers/remotes.js +19 -10
  101. package/dist/git-cli/providers/remotes.js.map +1 -1
  102. package/dist/git-cli/providers/revision.d.ts +6 -2
  103. package/dist/git-cli/providers/revision.js +38 -12
  104. package/dist/git-cli/providers/revision.js.map +1 -1
  105. package/dist/git-cli/providers/staging.js +39 -14
  106. package/dist/git-cli/providers/staging.js.map +1 -1
  107. package/dist/git-cli/providers/stash.d.ts +2 -0
  108. package/dist/git-cli/providers/stash.js +33 -18
  109. package/dist/git-cli/providers/stash.js.map +1 -1
  110. package/dist/git-cli/providers/status.d.ts +3 -1
  111. package/dist/git-cli/providers/status.js +40 -22
  112. package/dist/git-cli/providers/status.js.map +1 -1
  113. package/dist/git-cli/providers/tags.js +6 -4
  114. package/dist/git-cli/providers/tags.js.map +1 -1
  115. package/dist/git-cli/providers/worktrees.d.ts +1 -0
  116. package/dist/git-cli/providers/worktrees.js +60 -31
  117. package/dist/git-cli/providers/worktrees.js.map +1 -1
  118. package/dist/git-cli/service.d.ts +6 -0
  119. package/dist/git-cli/service.js +5 -2
  120. package/dist/git-cli/service.js.map +1 -1
  121. package/dist/plus/git-github/providers/github/revision.d.ts +6 -2
  122. package/dist/plus/git-github/providers/github/revision.js +15 -10
  123. package/dist/plus/git-github/providers/github/revision.js.map +1 -1
  124. package/dist/plus/git-github/providers/github/status.d.ts +3 -1
  125. package/dist/plus/git-github/providers/github/status.js +2 -2
  126. package/dist/plus/git-github/providers/github/status.js.map +1 -1
  127. package/dist/plus/integrations/collectionMetadata.d.ts +13 -2
  128. package/dist/plus/integrations/collectionMetadata.js +14 -2
  129. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  130. package/dist/plus/integrations/index.d.ts +2 -2
  131. package/dist/plus/integrations/index.js.map +1 -1
  132. package/dist/plus/integrations/integrationService.d.ts +1 -0
  133. package/dist/plus/integrations/integrationService.js +1 -1
  134. package/dist/plus/integrations/integrationService.js.map +1 -1
  135. package/dist/plus/integrations/manager.d.ts +1 -0
  136. package/dist/plus/integrations/models/gitHostIntegration.d.ts +41 -5
  137. package/dist/plus/integrations/models/gitHostIntegration.js +106 -29
  138. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  139. package/dist/plus/integrations/models/issueReads.d.ts +17 -4
  140. package/dist/plus/integrations/providers/apiClients.d.ts +2 -0
  141. package/dist/plus/integrations/providers/apiClients.js +7 -1
  142. package/dist/plus/integrations/providers/apiClients.js.map +1 -1
  143. package/dist/plus/integrations/providers/azure/azure.d.ts +5 -4
  144. package/dist/plus/integrations/providers/azure/azure.js +36 -76
  145. package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
  146. package/dist/plus/integrations/providers/azure/azure.utils.js +20 -17
  147. package/dist/plus/integrations/providers/azure/azure.utils.js.map +1 -1
  148. package/dist/plus/integrations/providers/azure/models.d.ts +2 -2
  149. package/dist/plus/integrations/providers/azure/models.js +4 -4
  150. package/dist/plus/integrations/providers/azure/models.js.map +1 -1
  151. package/dist/plus/integrations/providers/azure/workItemStateCategories.d.ts +21 -12
  152. package/dist/plus/integrations/providers/azure/workItemStateCategories.js +74 -17
  153. package/dist/plus/integrations/providers/azure/workItemStateCategories.js.map +1 -1
  154. package/dist/plus/integrations/providers/azureDevOps.d.ts +44 -0
  155. package/dist/plus/integrations/providers/azureDevOps.js +108 -17
  156. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  157. package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +45 -1
  158. package/dist/plus/integrations/providers/azureDevOpsServer.js +121 -6
  159. package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
  160. package/dist/plus/integrations/providers/jira-server.js +18 -14
  161. package/dist/plus/integrations/providers/jira-server.js.map +1 -1
  162. package/dist/plus/integrations/providers/jira.js +18 -14
  163. package/dist/plus/integrations/providers/jira.js.map +1 -1
  164. package/dist/plus/integrations/providers/models.d.ts +1 -2
  165. package/dist/plus/integrations/providers/models.js +9 -2
  166. package/dist/plus/integrations/providers/models.js.map +1 -1
  167. package/dist/plus/integrations/providers/providersApi.d.ts +7 -17
  168. package/dist/plus/integrations/providers/providersApi.js +85 -78
  169. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  170. package/dist/plus/integrations/providers/utils/jiraIssueDrains.d.ts +22 -0
  171. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js +69 -9
  172. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js.map +1 -1
  173. package/dist/plus/integrations/providers/utils/providerPaging.js +8 -2
  174. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  175. package/dist/plus/integrations/reads/filters.d.ts +3 -3
  176. package/dist/plus/integrations/reads/filters.js.map +1 -1
  177. package/dist/plus/integrations/reads/issueBatch.d.ts +2 -1
  178. package/dist/plus/integrations/reads/issueBatch.js +1 -1
  179. package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
  180. package/dist/plus/integrations/reads/issueTracker.js +23 -14
  181. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  182. package/dist/plus/integrations/reads/issues.d.ts +2 -1
  183. package/dist/plus/integrations/reads/issues.js +18 -3
  184. package/dist/plus/integrations/reads/issues.js.map +1 -1
  185. package/dist/plus/integrations/reads/pullRequestBatch.js +1 -1
  186. package/dist/plus/integrations/reads/resolveRepository.js +12 -3
  187. package/dist/plus/integrations/reads/resolveRepository.js.map +1 -1
  188. package/dist/plus/integrations/reads/warnings.d.ts +11 -4
  189. package/dist/plus/integrations/reads/warnings.js +15 -5
  190. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  191. package/dist/plus/integrations/results.d.ts +36 -2
  192. package/dist/plus/integrations/results.js +33 -13
  193. package/dist/plus/integrations/results.js.map +1 -1
  194. package/dist/utils/promiseCache.d.ts +10 -0
  195. package/dist/utils/promiseCache.js +17 -2
  196. package/dist/utils/promiseCache.js.map +1 -1
  197. package/docs/integrations.md +52 -20
  198. package/package.json +2 -2
@@ -1,5 +1,6 @@
1
1
  import { ProviderReference } from "../../../../git/models/remoteProvider.js";
2
2
  import { IssueShape } from "../../../../git/models/issue.js";
3
+ import { ProviderWarningUnreadableItems } from "../../results.js";
3
4
  import { IssueFilter } from "../../providerFilters.js";
4
5
  import { JiraIssuePage, ProviderIssue } from "../models.js";
5
6
  import { CollectionMetadata, CollectionScopeFailure } from "@gitkraken/provider-apis";
@@ -52,8 +53,29 @@ export declare function withFailureScope(metadata: CollectionMetadata, scope: Co
52
53
  * reported one. provider-apis reports the issues a page dropped per page, naming that page's keys, so the failures
53
54
  * of a drain over many unreadable pages never dedupe and would publish one warning per page. The first page's
54
55
  * failure is kept, naming its keys, and says how many more pages dropped issues too.
56
+ *
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.
55
61
  */
56
62
  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;
57
79
  /**
58
80
  * Follows a search's cursor up to `maxPages`. A page failure after the first page leaves the already-drained prefix
59
81
  * intact and records the failure instead of re-throwing and discarding the prefix; if nothing was fetched yet, the
@@ -1,5 +1,5 @@
1
1
  import { Logger as _logger } from "../../../../utils/logger.js";
2
- import { collectionScopeKey } from "../../results.js";
2
+ import { collectionScopeKey, mergeUnreadableItems } from "../../results.js";
3
3
  import { isIncompleteCollection, throwIfCallerContractError, toCollectionScopeFailure } from "../../collectionMetadata.js";
4
4
  import "../../providerFilters.js";
5
5
  import { mergeCollectionMetadata } from "./providerPaging.js";
@@ -26,6 +26,11 @@ function withFailureScope(metadata, scope) {
26
26
  * reported one. provider-apis reports the issues a page dropped per page, naming that page's keys, so the failures
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
+ *
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.
29
34
  */
30
35
  function summarizePageMetadata(pages) {
31
36
  let summary;
@@ -33,24 +38,78 @@ function summarizePageMetadata(pages) {
33
38
  for (const { failures: pageFailures, ...page } of pages) {
34
39
  summary = mergeCollectionMetadata(summary, page);
35
40
  for (const failure of pageFailures ?? []) {
36
- const key = `${collectionScopeKey(failure.scope)}\0${failure.kind}`;
41
+ const unreadableItems = parseSkippedJiraIssues(failure.message);
42
+ const key = `${collectionScopeKey(failure.scope)}\0${failure.kind}\0${unreadableItems != null}`;
37
43
  const group = failures.get(key);
38
44
  if (group == null) failures.set(key, {
39
45
  failure,
40
- pages: 1
46
+ pages: 1,
47
+ unreadableItems
41
48
  });
42
- else group.pages++;
49
+ else {
50
+ group.pages++;
51
+ group.unreadableItems = mergeUnreadableItems(group.unreadableItems, unreadableItems);
52
+ }
43
53
  }
44
54
  }
45
55
  if (failures.size === 0) return summary;
46
56
  return mergeCollectionMetadata(summary, {
47
57
  completeness: "partial",
48
- failures: Array.from(failures.values(), ({ failure, pages: count }) => count === 1 ? failure : {
58
+ failures: Array.from(failures.values(), ({ failure, pages: count, unreadableItems }) => ({
49
59
  ...failure,
50
- message: `${failure.message != null ? `${failure.message} ` : ""}Issues on ${count - 1} more ${count === 2 ? "page" : "pages"} could not be read either.`
51
- })
60
+ ...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 } : {}
62
+ }))
52
63
  });
53
64
  }
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
+ /**
100
+ * Says a drain of `scope` stopped at its page backstop with pages left: an omission, not a failure, since every request
101
+ * answered, so it retracts no other omission of the read. A clean drain leaves it to its caller (`'backstop'`), which
102
+ * knows whether a narrower scope would avoid it.
103
+ */
104
+ function backstopOmission(scope) {
105
+ return {
106
+ completeness: "partial",
107
+ omissions: [{
108
+ kind: "pagination-incomplete",
109
+ scope
110
+ }]
111
+ };
112
+ }
54
113
  /**
55
114
  * Follows a search's cursor up to `maxPages`. A page failure after the first page leaves the already-drained prefix
56
115
  * intact and records the failure instead of re-throwing and discarding the prefix; if nothing was fetched yet, the
@@ -109,7 +168,7 @@ async function drainJiraIssues(fetchPage, scope, options) {
109
168
  cursor = result.nextCursor;
110
169
  if (i === options.maxPages - 1) {
111
170
  if (status === "complete") status = "backstop";
112
- else stopIncomplete(/* @__PURE__ */ new Error(`${options.providerName} issue drain stopped at the ${options.maxPages}-page backstop`));
171
+ else metadata = mergeCollectionMetadata(metadata, backstopOmission(options.failureScope));
113
172
  }
114
173
  }
115
174
  return {
@@ -160,6 +219,7 @@ async function readUserScopedJiraIssues(fetchPage, requestedFilters, identities,
160
219
  continue;
161
220
  }
162
221
  if (outcome.value.status !== "complete") truncated = true;
222
+ if (outcome.value.status === "backstop") metadata = mergeCollectionMetadata(metadata, backstopOmission(options.failureScope));
163
223
  if (outcome.value.metadata != null) metadata = mergeCollectionMetadata(metadata, outcome.value.metadata);
164
224
  issues.push(...outcome.value.issues);
165
225
  }
@@ -196,6 +256,6 @@ async function searchJiraProjectIssues(fetchPage, projectIds, filters, identitie
196
256
  return splitProjectIssuesSearch(projectIds, read.issues, (issue) => issue.project?.id ?? void 0)?.map((issues) => toUniqueJiraIssueShapes(issues, provider));
197
257
  }
198
258
  //#endregion
199
- export { drainJiraIssues, readUserScopedJiraIssues, searchJiraProjectIssues, summarizePageMetadata, toUniqueJiraIssueShapes, withFailureScope };
259
+ export { drainJiraIssues, parseSkippedJiraIssues, readUserScopedJiraIssues, searchJiraProjectIssues, summarizePageMetadata, toUniqueJiraIssueShapes, withFailureScope };
200
260
 
201
261
  //# 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 {\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 } 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 */\nexport function summarizePageMetadata(pages: readonly CollectionMetadata[]): CollectionMetadata | undefined {\n\tlet summary: CollectionMetadata | undefined;\n\tconst failures = new Map<string, { failure: CollectionScopeFailure; pages: number }>();\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}`;\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 });\n\t\t\t} else {\n\t\t\t\tgroup.pages++;\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 }) =>\n\t\t\tcount === 1\n\t\t\t\t? failure\n\t\t\t\t: {\n\t\t\t\t\t\t...failure,\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),\n\t});\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\tstopIncomplete(\n\t\t\t\t\tnew Error(`${options.providerName} issue drain stopped at the ${options.maxPages}-page backstop`),\n\t\t\t\t);\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\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 } 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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"}
@@ -3,7 +3,7 @@ import { chunk } from "../../../../utils/array.js";
3
3
  import { isCancellationError } from "../../../../utils/cancellation.js";
4
4
  import { mapSettledBounded } from "../../../../utils/promise.js";
5
5
  import "../../constants.js";
6
- import { collectionScopeKey } from "../../results.js";
6
+ import { collectionScopeKey, mergeUnreadableItems } from "../../results.js";
7
7
  import { throwIfCallerContractError, toCollectionScopeFailure } from "../../collectionMetadata.js";
8
8
  //#region ../plus/integrations/src/providers/utils/providerPaging.ts
9
9
  /**
@@ -177,7 +177,13 @@ function mergeCollectionMetadata(base, next) {
177
177
  if (base == null) return next;
178
178
  if (next == null) return base;
179
179
  const completeness = completenessRank[next.completeness] > completenessRank[base.completeness] ? next.completeness : base.completeness;
180
- const failures = [...uniqueBy([...base.failures ?? [], ...next.failures ?? []], collectionFailureKey, (original) => original)];
180
+ const failures = [...uniqueBy([...base.failures ?? [], ...next.failures ?? []], collectionFailureKey, (original, current) => {
181
+ const unreadableItems = mergeUnreadableItems(original.unreadableItems, current.unreadableItems);
182
+ return unreadableItems === original.unreadableItems ? original : {
183
+ ...original,
184
+ unreadableItems
185
+ };
186
+ })];
181
187
  const omissions = [...uniqueBy([...base.omissions ?? [], ...next.omissions ?? []], collectionOmissionKey, (original, current) => (current.totalCount ?? -1) > (original.totalCount ?? -1) ? current : void 0)];
182
188
  return {
183
189
  completeness,
@@ -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 { throwIfCallerContractError, toCollectionScopeFailure } from '../../collectionMetadata.js';\nimport { providerFanOutConcurrency } from '../../constants.js';\nimport { collectionScopeKey } 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.\n\tconst failures = [\n\t\t...uniqueBy([...(base.failures ?? []), ...(next.failures ?? [])], collectionFailureKey, original => original),\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":";;;;;;;;;;;;;AAqBA,SAAgB,aAAa,MAAsB;CAClD,OAAO,KAAK,UAAU;EAAE,OAAO;EAAM,MAAM;CAAO,CAAC;AACpD;;AAGA,SAAgB,gBAAgB,QAAgD;CAC/E,IAAI,UAAU,QAAQ,WAAW,MAAM,OAAO,KAAA;CAE9C,IAAI;EACH,MAAM,SAAS,KAAK,MAAM,MAAM;EAChC,IAAI,OAAO,SAAS,UAAU,OAAO,OAAO,UAAU,UAAU,OAAO,OAAO;CAC/E,QAAQ,CAAC;AAGV;;AAGA,SAAgB,wBAA2B,SAA0C;CACpF,IAAI,QAAQ,MAAK,WAAU,OAAO,WAAW,WAAW,GAAG;CAE3D,MAAM,WAAW,QAAQ,MAAM,WAA4C,OAAO,WAAW,UAAU;CACvG,IAAI,YAAY,MAAM,MAAM,SAAS;AACtC;;;;;;;AAQA,SAAgB,yBACf,MACA,SAOsC;CACtC,MAAM,UAAU,KAAK,OAAO,QAAQ,WAAW,CAAC,CAAC,MAAM,GAAG,MAAM,QAAQ,UAAU,CAAC,IAAI,QAAQ,UAAU,CAAC,CAAC;CAC3G,OAAO;EACN,QAAQ,QAAQ,MAAM,GAAG,QAAQ,KAAK,CAAC,CAAC,IAAI,QAAQ,GAAG;EACvD,WAAW,QAAQ,QAAQ,QAAQ,SAAS,QAAQ;CACrD;AACD;;;;;;;;;;AAWA,eAAsB,0BACrB,SACA,OACA,SAG4F;CAC5F,MAAM,cAAc,MAAM,SAAS,MAAM,MACxC,KAAK,WAAW,cACb,KAAK,MAAM,QAAQ,KAAI,YAAW;EAClC,OAAO,QAAQ,EAAE,CAAC;EAClB,MAAM,QAAQ,EAAE,CAAC;EACT;EACR,SAAS,QAAQ,EAAE,CAAC;CACrB,EAAE,IACD,CAAC,CACL;CAEA,IAAI,WAAiE,CAAC;CACtE,IAAI,YAAY,SAAS,GACxB,IAAI;EACH,WACE,MAAM,QAAQ,WAAW,KAC1B,YAAY,WAAW;GACtB,QAAQ;GACR,wBAAQ,IAAI,MAAM,qCAAqC;EACxD,EAAE;CACJ,SAAS,IAAI;EACZ,WAAW,YAAY,WAAW;GAAE,QAAQ;GAAY,QAAQ;EAAG,EAAE;CACtE;CAGD,IAAI,SAAS;CACb,OAAO,MAAM,KAAI,SAAQ;EACxB,IAAI,KAAK,WAAW,YAAY,OAAO;EAEvC,MAAM,QAAQ,SAAS,MAAM,QAAQ,SAAS,KAAK,MAAM,QAAQ,MAAM;EACvE,UAAU,KAAK,MAAM,QAAQ;EAE7B,MAAM,UAAU,MAAM,MAAM,MAAkC,EAAE,WAAW,UAAU;EACrF,IAAI,WAAW,MAAM,OAAO;EAE5B,OAAO;GACN,QAAQ;GACR,OAAO;IACN,cAAc,MAAM,SAAQ,MAAM,EAAE,WAAW,eAAe,EAAE,SAAS,OAAO,CAAC,EAAE,KAAK,IAAI,CAAC,CAAE;IAC/F,WAAW,KAAK,MAAM;GACvB;EACD;CACD,CAAC;AACF;;;;;;;AAQA,eAAsB,2BACrB,aACA,WACA,MACA,KACsD;CACtD,MAAM,+BAAe,IAAI,IAAsB;CAC/C,KAAK,MAAM,CAAC,OAAO,MAAM,YAAY,QAAQ,GAAG;EAC/C,MAAM,MAAM,GAAG,EAAE,MAAM,GAAG,EAAE;EAC5B,IAAI,UAAU,aAAa,IAAI,GAAG;EAClC,IAAI,WAAW,MAAM;GACpB,UAAU,CAAC;GACX,aAAa,IAAI,KAAK,OAAO;EAC9B;EACA,QAAQ,KAAK,KAAK;CACnB;CAEA,MAAM,WAAW,CAAC,GAAG,aAAa,OAAO,CAAC,CAAC,CAAC,SAAQ,YACnD,MAAM,SAAS,SAAS,CAAC,CAAC,KAAI,kBAAiB,EAAE,SAAS,aAAa,EAAE,CAC1E;CACA,MAAM,UAAU,MAAM,kBAAkB,UAAA,IAAqC,MAAK;EACjF,MAAM,EAAE,OAAO,SAAS,YAAY,EAAE,QAAQ;EAC9C,OAAO,KACN,OACA,MACA,EAAE,QAAQ,KAAI,MAAK,YAAY,EAAE,CAAC,MAAM,CACzC;CACD,CAAC;CAED,MAAM,QAAQ,IAAI,MAAgD,YAAY,MAAM;CACpF,KAAK,MAAM,CAAC,GAAG,WAAW,QAAQ,QAAQ,GACzC,KAAK,MAAM,CAAC,GAAG,UAAU,SAAS,EAAE,CAAC,QAAQ,QAAQ,GAAG;EACvD,IAAI,OAAO,WAAW,YAAY;GACjC,MAAM,SAAS;GACf;EACD;EAEA,MAAM,OAAO,OAAO,MAAM;EAC1B,IAAI;GACH,MAAM,SAAS;IAAE,QAAQ;IAAa,OAAO,QAAQ,OAAO,IAAI,IAAI,IAAI,KAAA;GAAU;EACnF,SAAS,IAAI;GACZ,MAAM,SAAS;IAAE,QAAQ;IAAY,QAAQ;GAAG;EACjD;CACD;CAED,OAAO;AACR;AAEA,SAAgB,0BAA6B,SAA2C;CACvF,wBAAwB,OAAO;CAG/B,OADkB,QAAQ,QAAQ,WAAkD,OAAO,WAAW,WACvF,CAAC,CAAC,SAAQ,WAAU,OAAO,KAAK;AAChD;AAEA,eAAsB,mBAAsB,UAAwC;CACnF,OAAO,0BAA0B,MAAM,QAAQ,WAAW,QAAQ,CAAC;AACpE;;;AAIA,MAAM,mBAA2D;CAAE,SAAS;CAAG,SAAS;CAAG,UAAU;AAAE;;AAGvG,SAAS,qBAAqB,SAAyC;CACtE,OAAO;EAAC,QAAQ;EAAM,mBAAmB,QAAQ,KAAK;EAAG,QAAQ,WAAW;CAAE,CAAC,CAAC,KAAK,GAAG;AACzF;;;;;;;;;;;AAYA,SAAS,sBAAsB,UAAsC;CACpE,OAAO,CAAC,SAAS,MAAM,mBAAmB,SAAS,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG;AACpE;;;;;;;;AASA,SAAgB,wBACf,MACA,MACiC;CACjC,IAAI,QAAQ,MAAM,OAAO;CACzB,IAAI,QAAQ,MAAM,OAAO;CAEzB,MAAM,eACL,iBAAiB,KAAK,gBAAgB,iBAAiB,KAAK,gBACzD,KAAK,eACL,KAAK;CAGT,MAAM,WAAW,CAChB,GAAG,SAAS,CAAC,GAAI,KAAK,YAAY,CAAC,GAAI,GAAI,KAAK,YAAY,CAAC,CAAE,GAAG,uBAAsB,aAAY,QAAQ,CAC7G;CAGA,MAAM,YAAY,CACjB,GAAG,SACF,CAAC,GAAI,KAAK,aAAa,CAAC,GAAI,GAAI,KAAK,aAAa,CAAC,CAAE,GACrD,wBACC,UAAU,aAAc,QAAQ,cAAc,OAAO,SAAS,cAAc,MAAM,UAAU,KAAA,CAC9F,CACD;CAEA,OAAO;EACQ;EACd,GAAI,SAAS,SAAS,EAAY,SAAS,IAAI,CAAC;EAChD,GAAI,UAAU,SAAS,EAAa,UAAU,IAAI,CAAC;CACpD;AACD;;;;;;;;;;AAWA,eAAsB,2BACrB,OACA,WAAW,IACX,OACsC;CACtC,MAAM,SAA2B,CAAC;CAClC,IAAI;CACJ,IAAI;CACJ,IAAI,YAAY;CAIhB,MAAM,SAAS,UAA4E;EAC1F,MAAM,iBAAiB,OAAO,YAAY;EAC1C,OAAO;GACE;GACR,GAAG;GACH,GAAI,aAAa,OAAO,cAAc,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;GACpE,GAAI,kBAAkB,OAAO,EAAE,UAAU,eAAe,IAAI,CAAC;EAC9D;CACD;CAEA,KAAK,IAAI,OAAO,GAAG,OAAO,UAAU,QAAQ;EAC3C,IAAI;EACJ,IAAI;GACH,SAAS,MAAM,MAAM,MAAM;EAC5B,SAAS,IAAI;GACZ,IAAI,oBAAoB,EAAE,GAAG,MAAM;GAKnC,2BAA2B,EAAE;GAK7B,IAAI,SAAS,MAAM,MAAM;GACzB,OAAO,MAAM;IACZ,WAAW;IACX,UAAU,wBAAwB,UAAU;KAC3C,cAAc;KACd,UAAU,CAAC,yBAAyB,OAAO,EAAE,CAAC;IAC/C,CAAC;GACF,CAAC;EACF;EACA,IAAI,UAAU,MAAM;GACnB,IAAI,SAAS,GAAG,OAAO,MAAM;GAE7B,OAAO,MAAM;IACZ,WAAW;IACX,GAAI,SAAS,OACV,EACA,UAAU,wBAAwB,UAAU;KAC3C,cAAc;KACd,UAAU,CACT,yBACC,uBACA,IAAI,MAAM,4DAA4D,CACvE,CACD;IACD,CAAC,EACF,IACC,KAAA;GACJ,CAAC;EACF;EAEA,OAAO,KAAK,GAAG,OAAO,MAAM;EAC5B,WAAW,wBAAwB,UAAU,OAAO,QAAQ;EAC5D,cAAc,OAAO,QAAQ,cAAc;EAE3C,IAAI,CAAC,OAAO,QAAQ,MAAM,OAAO,MAAM;EAEvC,IAAI,OAAO,OAAO,WAAW,QAC5B,OAAO,MAAM,EAAE,WAAW,KAAK,CAAC;EAGjC,SAAS,OAAO,OAAO;EACvB,IAAI,UAAU,QAAQ,WAAW,MAChC,OAAO,MAAM,EAAE,WAAW,KAAK,CAAC;EAEjC,IAAI,SAAS,WAAW,GACvB,OAAO,MAAM;GAAE,QAAQ,OAAO;GAAQ,WAAW;EAAK,CAAC;CAEzD;CAEA,OAAO,MAAM;AACd"}
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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"}
@@ -221,11 +221,11 @@ export type SupportedFilters = {
221
221
  */
222
222
  issueSortsAccountWide: IssueSorting[];
223
223
  /**
224
- * Whether an issue tracker's reads can be asked for a `state` other than open (`closed`, `all`). False means
224
+ * Whether issue list reads can be asked for a `state` other than open (`closed`, `all`). False means
225
225
  * only open issues are readable, and the read refuses the other two rather than serving the open list.
226
226
  *
227
- * Tracker-only, like `issues`: the git hosts' state support belongs to the filtered search, under
228
- * `issueSearch.states`.
227
+ * Includes Azure DevOps Services and Server list reads. Filtered search reports its independent state
228
+ * capability under `issueSearch.states`.
229
229
  */
230
230
  issueStates: boolean;
231
231
  };