|
2 | 2 |
|
3 | 3 | import { ApiError, ValidationError } from "../../lib/errors.js"; |
4 | 4 | import { LIST_MAX_LIMIT } from "../../lib/list-command.js"; |
5 | | -import { distributeFetchBudget, type FetchResult } from "../../lib/org-list.js"; |
| 5 | +import { |
| 6 | + type FetchGroupsOptions, |
| 7 | + type FetchResult, |
| 8 | + fetchGroupsWithBudget, |
| 9 | + type ListFetchPage, |
| 10 | +} from "../../lib/org-list.js"; |
6 | 11 |
|
7 | | -export type AlertRuleFetchPage<TRule> = { |
| 12 | +/** Alert rules with the cursor metadata used by shared list fetching. */ |
| 13 | +export type AlertRuleFetchPage<TRule> = ListFetchPage & { |
| 14 | + /** Rules returned for this group. */ |
8 | 15 | rules: TRule[]; |
9 | | - hasMore?: boolean; |
10 | | - nextCursor?: string; |
11 | 16 | }; |
12 | 17 |
|
13 | | -type FetchPageOptions = { limit: number; startCursor?: string }; |
14 | | -type BudgetOptions<TGroup, TRule, TPage extends AlertRuleFetchPage<TRule>> = { |
15 | | - limit: number; |
16 | | - startCursors?: Map<string, string>; |
17 | | - getGroupKey: (group: TGroup) => string; |
18 | | - fetchGroup: ( |
19 | | - group: TGroup, |
20 | | - options: FetchPageOptions |
21 | | - ) => Promise<FetchResult<TPage>>; |
22 | | - onProgress: (fetched: number) => void; |
23 | | -}; |
| 18 | +type BudgetOptions< |
| 19 | + TGroup, |
| 20 | + TRule, |
| 21 | + TPage extends AlertRuleFetchPage<TRule>, |
| 22 | +> = Omit<FetchGroupsOptions<TGroup, TRule, TPage>, "getItems">; |
24 | 23 |
|
25 | 24 | export function assertAlertListLimit(limit: number): void { |
26 | 25 | if (limit < 1) { |
@@ -70,80 +69,17 @@ export function buildAlertListFailureErrors<TKey extends string, TFailure>( |
70 | 69 | }); |
71 | 70 | } |
72 | 71 |
|
73 | | -function countFetched<TRule, TPage extends AlertRuleFetchPage<TRule>>( |
74 | | - results: FetchResult<TPage>[] |
75 | | -): number { |
76 | | - return results.reduce( |
77 | | - (total, result) => total + (result.success ? result.data.rules.length : 0), |
78 | | - 0 |
79 | | - ); |
80 | | -} |
81 | | - |
82 | | -function hasMore<TRule, TPage extends AlertRuleFetchPage<TRule>>( |
83 | | - results: FetchResult<TPage>[] |
84 | | -): boolean { |
85 | | - return results.some((result) => result.success && result.data.hasMore); |
86 | | -} |
87 | | - |
88 | | -export async function fetchAlertRulesWithBudget< |
| 72 | +/** Preserve the alert page shape while sharing budget and cursor handling. */ |
| 73 | +export function fetchAlertRulesWithBudget< |
89 | 74 | TGroup, |
90 | 75 | TRule, |
91 | 76 | TPage extends AlertRuleFetchPage<TRule>, |
92 | 77 | >( |
93 | 78 | groups: TGroup[], |
94 | 79 | options: BudgetOptions<TGroup, TRule, TPage> |
95 | 80 | ): Promise<{ results: FetchResult<TPage>[]; hasMore: boolean }> { |
96 | | - const { limit, startCursors, getGroupKey, fetchGroup, onProgress } = options; |
97 | | - const quotas = distributeFetchBudget(limit, groups.length, { |
98 | | - minimumPerGroup: true, |
99 | | - }); |
100 | | - const phase1 = await Promise.all( |
101 | | - groups.map((group, index) => |
102 | | - fetchGroup(group, { |
103 | | - limit: quotas[index] ?? 1, |
104 | | - startCursor: startCursors?.get(getGroupKey(group)), |
105 | | - }) |
106 | | - ) |
107 | | - ); |
108 | | - |
109 | | - let totalFetched = countFetched(phase1); |
110 | | - onProgress(totalFetched); |
111 | | - |
112 | | - const surplus = limit - totalFetched; |
113 | | - if (surplus <= 0) { |
114 | | - return { results: phase1, hasMore: hasMore(phase1) }; |
115 | | - } |
116 | | - |
117 | | - const expandable = phase1.flatMap((result, index) => { |
118 | | - const group = groups[index]; |
119 | | - if (!(group && result.success && result.data.nextCursor)) { |
120 | | - return []; |
121 | | - } |
122 | | - return [{ group, index, cursor: result.data.nextCursor }]; |
| 81 | + return fetchGroupsWithBudget<TGroup, TRule, TPage>(groups, { |
| 82 | + ...options, |
| 83 | + getItems: (page) => page.rules, |
123 | 84 | }); |
124 | | - const extraQuotas = distributeFetchBudget(surplus, expandable.length); |
125 | | - const requests = expandable.flatMap((request, index) => { |
126 | | - const extraLimit = extraQuotas[index] ?? 0; |
127 | | - return extraLimit > 0 ? [{ ...request, limit: extraLimit }] : []; |
128 | | - }); |
129 | | - |
130 | | - const phase2 = await Promise.all( |
131 | | - requests.map(({ group, limit: requestLimit, cursor }) => |
132 | | - fetchGroup(group, { limit: requestLimit, startCursor: cursor }) |
133 | | - ) |
134 | | - ); |
135 | | - for (let index = 0; index < requests.length; index++) { |
136 | | - const request = requests[index]; |
137 | | - const p1 = request ? phase1[request.index] : undefined; |
138 | | - const p2 = phase2[index]; |
139 | | - if (p1?.success && p2?.success) { |
140 | | - p1.data.rules.push(...p2.data.rules); |
141 | | - p1.data.hasMore = p2.data.hasMore; |
142 | | - p1.data.nextCursor = p2.data.nextCursor; |
143 | | - } |
144 | | - } |
145 | | - |
146 | | - totalFetched = countFetched(phase1); |
147 | | - onProgress(totalFetched); |
148 | | - return { results: phase1, hasMore: hasMore(phase1) }; |
149 | 85 | } |
0 commit comments