@kud/gh 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/inbox.d.ts CHANGED
@@ -6,13 +6,39 @@
6
6
  * title, url, draft, timestamp, repo.
7
7
  *
8
8
  * This is a cost axis, not a taste one. Measured against GitHub's own
9
- * `rateLimit { cost }`: the full query costs **111 points** of a 5000/hour
10
- * budget, at a nodeCount of ~25,550 — because `statusCheckRollup.contexts`
9
+ * `rateLimit { cost }`: the full query costs **73 points** of a 5000/hour
10
+ * budget, at a nodeCount of ~16,870 — because `statusCheckRollup.contexts`
11
11
  * appears in five PR fragments and `reviewThreads(first: 50)` multiplies
12
12
  * beneath each one. A caller that renders a title and a link was paying for
13
- * every check run on every open PR. At 111 a poller gets 45 refreshes an hour.
13
+ * every check run on every open PR.
14
14
  */
15
+ /**
16
+ * How many of your own open PRs to ask for.
17
+ *
18
+ * This is the single biggest lever on the query's cost, because connections
19
+ * MULTIPLY: the health and conversation fragments hang ~80 nodes off each PR
20
+ * (`reviewThreads(first: 50)`, `statusCheckRollup.contexts(first: 20)`,
21
+ * `labels(first: 10)`), so the outer number is a multiplier on all of them.
22
+ * GitHub scores a call by the nodes it could return, not by how many calls you
23
+ * make — which is why splitting the query across requests costs the same, and
24
+ * this saves a lot.
25
+ *
26
+ * It was 100, against ~25,550 total nodes and a measured 111 points. 100 × 80 is
27
+ * 8,000 of those nodes from this one search, and it was 100 for no reason beyond
28
+ * the search API's own maximum — every other search here already asks for 20-30.
29
+ * At 30 this search costs 2,400 nodes instead of 8,000, roughly halving the whole
30
+ * query.
31
+ *
32
+ * `issueCount` is fetched alongside so the cap can never drop rows in silence: a
33
+ * scalar costs nothing, and a host that knows the true total can say what it is
34
+ * not showing.
35
+ */
36
+ export declare const MY_PRS_LIMIT = 30;
15
37
  export type InboxShape = "full" | "minimal";
38
+ /** One search in the inbox, addressable by the alias it answers under. */
39
+ export type InboxSource = "myPRs" | "reviewRequests" | "reviewed" | "assigned" | "repoIssues" | "authoredIssues" | "repoPRs" | "recentlyDone";
40
+ /** Every source, in the order a whole-inbox query asks for them. */
41
+ export declare const INBOX_SOURCES: readonly InboxSource[];
16
42
  /** A repo-scoped or account-wide inbox. */
17
43
  export type InboxQueryOptions = {
18
44
  /** `owner/name`. Scopes every search to one repository. */
@@ -26,6 +52,15 @@ export type InboxQueryOptions = {
26
52
  * falls through to a wrong answer rather than an absent one.
27
53
  */
28
54
  shape?: InboxShape;
55
+ /**
56
+ * Which sources to ask for. Defaults to all of them.
57
+ *
58
+ * Every query is still a complete, valid document — `rateLimit` and `viewer`
59
+ * ride along with any subset — so a caller can split the inbox across
60
+ * requests without the parts becoming fragments that only mean something
61
+ * reassembled.
62
+ */
63
+ sources?: readonly InboxSource[];
29
64
  };
30
65
  /**
31
66
  * Build the inbox query. Without `repo`, it is account-wide.
@@ -36,5 +71,52 @@ export type InboxQueryOptions = {
36
71
  * render empty and read as "nothing to do here". So `repo:` REPLACES
37
72
  * `user:@me` rather than joining it.
38
73
  */
39
- export declare const buildInboxQuery: ({ repo, doneWithinDays, shape, }?: InboxQueryOptions) => string;
74
+ export declare const buildInboxQuery: (options?: InboxQueryOptions) => string;
75
+ /**
76
+ * How many sources to put in one request.
77
+ *
78
+ * GitHub's proxy answers a request, not a query — so the ceiling here is WALL
79
+ * CLOCK, not cost. Measured on 2026-08-28 against an account-wide inbox: the
80
+ * eight-source query returns HTTP 502 on roughly two runs in three, after
81
+ * 10–30s; the same eight sources asked one at a time return 200 every time, for
82
+ * the same 73 points and the same ~16,870 nodes. Adding sources one at a time
83
+ * shows a clean gradient — five is reliable, six starts failing, eight mostly
84
+ * fails.
85
+ *
86
+ * The cost is identical either way, which is the whole reason this is safe:
87
+ * GraphQL scores the nodes a query COULD return, so splitting one document into
88
+ * four buys reliability at no extra budget. It is also why the same query is
89
+ * fine repo-scoped — `repo:` narrows what the search index has to walk, while
90
+ * `author:@me` across an account does not.
91
+ *
92
+ * Two rather than five, because the gradient is a probability and not a cliff:
93
+ * the point is headroom, and four requests in parallel cost the same wall clock
94
+ * as one.
95
+ */
96
+ export declare const INBOX_SOURCES_PER_QUERY = 2;
97
+ /**
98
+ * Build the inbox as several independent queries, to be issued in parallel and
99
+ * merged with `mergeInboxData`.
100
+ *
101
+ * Prefer this to `buildInboxQuery` for an account-wide inbox. Each query is a
102
+ * complete document carrying its own `rateLimit` and `viewer`, so a partial
103
+ * failure is one source missing rather than a malformed whole.
104
+ */
105
+ export declare const buildInboxQueries: (options?: InboxQueryOptions & {
106
+ sourcesPerQuery?: number;
107
+ }) => string[];
108
+ /**
109
+ * Reassemble what `buildInboxQueries` split.
110
+ *
111
+ * The source aliases are disjoint, so they merge by assignment. `rateLimit` and
112
+ * `viewer` are not: every part carries its own.
113
+ *
114
+ * `cost` and `nodeCount` SUM, because each part really was charged separately
115
+ * and a host reporting one part's cost as the inbox's would understate it by
116
+ * four. `remaining` takes the LOWEST reading — the parts run concurrently and
117
+ * the budget only falls, so the smallest is the closest to now. `resetAt` rides
118
+ * along with it, since a window and its expiry have to describe the same
119
+ * moment.
120
+ */
121
+ export declare const mergeInboxData: (parts: any[]) => any;
40
122
  //# sourceMappingURL=inbox.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../src/inbox.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,CAAA;AAE3C,2CAA2C;AAC3C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,UAAU,CAAA;CACnB,CAAA;AA6DD;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAAI,mCAI7B,iBAAsB,WAwGxB,CAAA"}
1
+ {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../src/inbox.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;GAaG;AACH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,YAAY,KAAK,CAAA;AAE9B,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,CAAA;AAE3C,0EAA0E;AAC1E,MAAM,MAAM,WAAW,GACnB,OAAO,GACP,gBAAgB,GAChB,UAAU,GACV,UAAU,GACV,YAAY,GACZ,gBAAgB,GAChB,SAAS,GACT,cAAc,CAAA;AAElB,oEAAoE;AACpE,eAAO,MAAM,aAAa,EAAE,SAAS,WAAW,EAS/C,CAAA;AAED,2CAA2C;AAC3C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,SAAS,WAAW,EAAE,CAAA;CACjC,CAAA;AA8MD;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAAI,UAAS,iBAAsB,WAqB9D,CAAA;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAC5B,UAAS,iBAAiB,GAAG;IAAE,eAAe,CAAC,EAAE,MAAM,CAAA;CAAO,KAC7D,MAAM,EAYR,CAAA;AASD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,GAAG,EAAE,KAAG,GA2B7C,CAAA"}
package/dist/inbox.js CHANGED
@@ -1,10 +1,57 @@
1
- // The inbox query — one GraphQL round-trip for "what is on my plate": my PRs,
2
- // review requests, reviews I have given, assigned work, issues and PRs on my
3
- // repos, and what I closed recently.
1
+ // The inbox query — "what is on my plate": my PRs, review requests, reviews I
2
+ // have given, assigned work, issues and PRs on my repos, and what I closed
3
+ // recently.
4
4
  //
5
5
  // Query-only by design. Assembling the result into sections is the surface's
6
6
  // own vocabulary — cockpit splits home from work, a web dashboard may group by
7
7
  // project — so nothing here decides what a section is.
8
+ /**
9
+ * How much of each row to ask for.
10
+ *
11
+ * `full` carries health, conversation and labels — everything `computeHealth`
12
+ * and a conversation line need. `minimal` carries identity only: type, number,
13
+ * title, url, draft, timestamp, repo.
14
+ *
15
+ * This is a cost axis, not a taste one. Measured against GitHub's own
16
+ * `rateLimit { cost }`: the full query costs **73 points** of a 5000/hour
17
+ * budget, at a nodeCount of ~16,870 — because `statusCheckRollup.contexts`
18
+ * appears in five PR fragments and `reviewThreads(first: 50)` multiplies
19
+ * beneath each one. A caller that renders a title and a link was paying for
20
+ * every check run on every open PR.
21
+ */
22
+ /**
23
+ * How many of your own open PRs to ask for.
24
+ *
25
+ * This is the single biggest lever on the query's cost, because connections
26
+ * MULTIPLY: the health and conversation fragments hang ~80 nodes off each PR
27
+ * (`reviewThreads(first: 50)`, `statusCheckRollup.contexts(first: 20)`,
28
+ * `labels(first: 10)`), so the outer number is a multiplier on all of them.
29
+ * GitHub scores a call by the nodes it could return, not by how many calls you
30
+ * make — which is why splitting the query across requests costs the same, and
31
+ * this saves a lot.
32
+ *
33
+ * It was 100, against ~25,550 total nodes and a measured 111 points. 100 × 80 is
34
+ * 8,000 of those nodes from this one search, and it was 100 for no reason beyond
35
+ * the search API's own maximum — every other search here already asks for 20-30.
36
+ * At 30 this search costs 2,400 nodes instead of 8,000, roughly halving the whole
37
+ * query.
38
+ *
39
+ * `issueCount` is fetched alongside so the cap can never drop rows in silence: a
40
+ * scalar costs nothing, and a host that knows the true total can say what it is
41
+ * not showing.
42
+ */
43
+ export const MY_PRS_LIMIT = 30;
44
+ /** Every source, in the order a whole-inbox query asks for them. */
45
+ export const INBOX_SOURCES = [
46
+ "myPRs",
47
+ "reviewRequests",
48
+ "reviewed",
49
+ "assigned",
50
+ "repoIssues",
51
+ "authoredIssues",
52
+ "repoPRs",
53
+ "recentlyDone",
54
+ ];
8
55
  // computeHealth's precedence needs reviewDecision, mergeable and the check
9
56
  // rollup. A fragment that omits them does NOT degrade gracefully — it falls
10
57
  // straight through to whatever token is left, so a PR you already sent back
@@ -58,41 +105,15 @@ const ISSUE_LABELS = `
58
105
  * the server booted and quietly stop reporting anything closed since.
59
106
  */
60
107
  const sinceDay = (days) => new Date(Date.now() - days * 86400 * 1000).toISOString().slice(0, 10);
61
- /**
62
- * Build the inbox query. Without `repo`, it is account-wide.
63
- *
64
- * The two qualifiers do NOT compose. `user:@me` means "in repos I own", and
65
- * GitHub ANDs search qualifiers — so `user:@me repo:someone-else/x` matches
66
- * nothing, and a scoped inbox on a repo you contribute to but do not own would
67
- * render empty and read as "nothing to do here". So `repo:` REPLACES
68
- * `user:@me` rather than joining it.
108
+ /*
109
+ * One entry per source, keyed by the alias it answers under, so a caller can
110
+ * ask for any subset without this file growing a second copy of the query text.
111
+ * Each value is the whole `alias: search(…) { … }` selection, indented as it
112
+ * appears in the document.
69
113
  */
70
- export const buildInboxQuery = ({ repo, doneWithinDays = 14, shape = "full", } = {}) => {
71
- const scope = repo ? `repo:${repo} ` : "";
72
- const owned = repo ? "" : "user:@me ";
73
- const doneSince = sinceDay(doneWithinDays);
74
- /* Interpolated as empty strings rather than branching the query text, so the
75
- two shapes cannot drift into two separately-maintained queries. */
76
- const full = shape === "full";
77
- const health = full ? PR_HEALTH : "";
78
- const conversation = full ? PR_CONVERSATION : "";
79
- const issueConversation = full ? ISSUE_CONVERSATION : "";
80
- const issueLabels = full ? ISSUE_LABELS : "";
81
- /*
82
- * `rateLimit` is free — it does not count against itself — and it is the only
83
- * authoritative source for what this query costs. Every estimate made about
84
- * that on 2026-08-14 was wrong, one of them by 25x, because GraphQL cost is
85
- * node-count based and nested connections multiply: `reviewThreads(first: 50)`
86
- * beneath `search(first: 100)` is 5,000 nodes from two lines of query text. A
87
- * package handing out a 111-point query should hand out the means to see it.
88
- *
89
- * (GraphQL has no `/* *\/` comments, only `#`, so this note lives out here.)
90
- */
91
- return `
92
- {
93
- rateLimit { cost nodeCount remaining resetAt }
94
- viewer { login }
95
- myPRs: search(query: "${scope}is:pr is:open author:@me", type: ISSUE, first: 100) {
114
+ const SOURCES = {
115
+ myPRs: ({ scope, health, conversation }) => ` myPRs: search(query: "${scope}is:pr is:open author:@me", type: ISSUE, first: ${MY_PRS_LIMIT}) {
116
+ issueCount
96
117
  nodes { __typename ... on PullRequest {
97
118
  number title createdAt url headRefName isDraft
98
119
  repository { nameWithOwner }
@@ -100,8 +121,8 @@ export const buildInboxQuery = ({ repo, doneWithinDays = 14, shape = "full", } =
100
121
  ${health}
101
122
  ${conversation}
102
123
  }}
103
- }
104
- reviewRequests: search(query: "${scope}is:pr is:open review-requested:@me", type: ISSUE, first: 20) {
124
+ }`,
125
+ reviewRequests: ({ scope, health, conversation }) => ` reviewRequests: search(query: "${scope}is:pr is:open review-requested:@me", type: ISSUE, first: 20) {
105
126
  nodes { __typename ... on PullRequest {
106
127
  number title createdAt url headRefName isDraft
107
128
  repository { nameWithOwner }
@@ -109,8 +130,8 @@ export const buildInboxQuery = ({ repo, doneWithinDays = 14, shape = "full", } =
109
130
  ${health}
110
131
  ${conversation}
111
132
  }}
112
- }
113
- reviewed: search(query: "${scope}is:pr is:open reviewed-by:@me -author:@me -review-requested:@me", type: ISSUE, first: 20) {
133
+ }`,
134
+ reviewed: ({ scope, health, conversation }) => ` reviewed: search(query: "${scope}is:pr is:open reviewed-by:@me -author:@me -review-requested:@me", type: ISSUE, first: 20) {
114
135
  nodes { __typename ... on PullRequest {
115
136
  number title createdAt url headRefName isDraft
116
137
  repository { nameWithOwner }
@@ -118,8 +139,8 @@ export const buildInboxQuery = ({ repo, doneWithinDays = 14, shape = "full", } =
118
139
  ${health}
119
140
  ${conversation}
120
141
  }}
121
- }
122
- assigned: search(query: "${scope}is:open assignee:@me", type: ISSUE, first: 30) {
142
+ }`,
143
+ assigned: ({ scope, health, conversation, issueConversation, issueLabels, }) => ` assigned: search(query: "${scope}is:open assignee:@me", type: ISSUE, first: 30) {
123
144
  nodes {
124
145
  __typename
125
146
  ... on Issue {
@@ -133,8 +154,8 @@ export const buildInboxQuery = ({ repo, doneWithinDays = 14, shape = "full", } =
133
154
  ${conversation}
134
155
  }
135
156
  }
136
- }
137
- repoIssues: search(query: "${scope}${owned}is:issue is:open archived:false", type: ISSUE, first: 30) {
157
+ }`,
158
+ repoIssues: ({ scope, owned, issueConversation, issueLabels, }) => ` repoIssues: search(query: "${scope}${owned}is:issue is:open archived:false", type: ISSUE, first: 30) {
138
159
  nodes { __typename ... on Issue {
139
160
  number title createdAt url
140
161
  repository { nameWithOwner }
@@ -142,8 +163,8 @@ export const buildInboxQuery = ({ repo, doneWithinDays = 14, shape = "full", } =
142
163
  ${issueConversation}
143
164
  ${issueLabels}
144
165
  }}
145
- }
146
- authoredIssues: search(query: "${scope}is:issue is:open author:@me archived:false", type: ISSUE, first: 30) {
166
+ }`,
167
+ authoredIssues: ({ scope, issueConversation, issueLabels, }) => ` authoredIssues: search(query: "${scope}is:issue is:open author:@me archived:false", type: ISSUE, first: 30) {
147
168
  nodes { ... on Issue {
148
169
  number title createdAt url
149
170
  repository { nameWithOwner }
@@ -151,8 +172,8 @@ export const buildInboxQuery = ({ repo, doneWithinDays = 14, shape = "full", } =
151
172
  ${issueConversation}
152
173
  ${issueLabels}
153
174
  }}
154
- }
155
- repoPRs: search(query: "${scope}${owned}is:pr is:open -author:@me archived:false", type: ISSUE, first: 30) {
175
+ }`,
176
+ repoPRs: ({ scope, owned, health, conversation, }) => ` repoPRs: search(query: "${scope}${owned}is:pr is:open -author:@me archived:false", type: ISSUE, first: 30) {
156
177
  nodes { __typename ... on PullRequest {
157
178
  number title createdAt url headRefName isDraft
158
179
  repository { nameWithOwner }
@@ -160,14 +181,130 @@ export const buildInboxQuery = ({ repo, doneWithinDays = 14, shape = "full", } =
160
181
  ${health}
161
182
  ${conversation}
162
183
  }}
163
- }
164
- recentlyDone: search(query: "${scope}is:pr author:@me -is:open closed:>=${doneSince}", type: ISSUE, first: 30) {
184
+ }`,
185
+ recentlyDone: ({ scope, doneSince, }) => ` recentlyDone: search(query: "${scope}is:pr author:@me -is:open closed:>=${doneSince}", type: ISSUE, first: 30) {
165
186
  nodes { __typename ... on PullRequest {
166
187
  number title state isDraft createdAt mergedAt closedAt url
167
188
  repository { nameWithOwner }
168
189
  }}
169
- }
190
+ }`,
191
+ };
192
+ const selectionsFor = ({ repo, doneWithinDays = 14, shape = "full", }) => {
193
+ /* Interpolated as empty strings rather than branching the query text, so the
194
+ two shapes cannot drift into two separately-maintained queries. */
195
+ const full = shape === "full";
196
+ return {
197
+ scope: repo ? `repo:${repo} ` : "",
198
+ owned: repo ? "" : "user:@me ",
199
+ doneSince: sinceDay(doneWithinDays),
200
+ health: full ? PR_HEALTH : "",
201
+ conversation: full ? PR_CONVERSATION : "",
202
+ issueConversation: full ? ISSUE_CONVERSATION : "",
203
+ issueLabels: full ? ISSUE_LABELS : "",
204
+ };
205
+ };
206
+ /**
207
+ * Build the inbox query. Without `repo`, it is account-wide.
208
+ *
209
+ * The two qualifiers do NOT compose. `user:@me` means "in repos I own", and
210
+ * GitHub ANDs search qualifiers — so `user:@me repo:someone-else/x` matches
211
+ * nothing, and a scoped inbox on a repo you contribute to but do not own would
212
+ * render empty and read as "nothing to do here". So `repo:` REPLACES
213
+ * `user:@me` rather than joining it.
214
+ */
215
+ export const buildInboxQuery = (options = {}) => {
216
+ const selections = selectionsFor(options);
217
+ const sources = options.sources ?? INBOX_SOURCES;
218
+ /*
219
+ * `rateLimit` is free — it does not count against itself — and it is the only
220
+ * authoritative source for what this query costs. Every estimate made about
221
+ * that on 2026-08-14 was wrong, one of them by 25x, because GraphQL cost is
222
+ * node-count based and nested connections multiply: `reviewThreads(first: 50)`
223
+ * beneath `search(first: 100)` is 5,000 nodes from two lines of query text. A
224
+ * package handing out a 73-point query should hand out the means to see it.
225
+ *
226
+ * (GraphQL has no block comments, only `#`, so this note lives out here.)
227
+ */
228
+ return `
229
+ {
230
+ rateLimit { cost nodeCount remaining resetAt }
231
+ viewer { login }
232
+ ${sources.map((source) => SOURCES[source](selections)).join("\n")}
170
233
  }
171
234
  `;
172
235
  };
236
+ /**
237
+ * How many sources to put in one request.
238
+ *
239
+ * GitHub's proxy answers a request, not a query — so the ceiling here is WALL
240
+ * CLOCK, not cost. Measured on 2026-08-28 against an account-wide inbox: the
241
+ * eight-source query returns HTTP 502 on roughly two runs in three, after
242
+ * 10–30s; the same eight sources asked one at a time return 200 every time, for
243
+ * the same 73 points and the same ~16,870 nodes. Adding sources one at a time
244
+ * shows a clean gradient — five is reliable, six starts failing, eight mostly
245
+ * fails.
246
+ *
247
+ * The cost is identical either way, which is the whole reason this is safe:
248
+ * GraphQL scores the nodes a query COULD return, so splitting one document into
249
+ * four buys reliability at no extra budget. It is also why the same query is
250
+ * fine repo-scoped — `repo:` narrows what the search index has to walk, while
251
+ * `author:@me` across an account does not.
252
+ *
253
+ * Two rather than five, because the gradient is a probability and not a cliff:
254
+ * the point is headroom, and four requests in parallel cost the same wall clock
255
+ * as one.
256
+ */
257
+ export const INBOX_SOURCES_PER_QUERY = 2;
258
+ /**
259
+ * Build the inbox as several independent queries, to be issued in parallel and
260
+ * merged with `mergeInboxData`.
261
+ *
262
+ * Prefer this to `buildInboxQuery` for an account-wide inbox. Each query is a
263
+ * complete document carrying its own `rateLimit` and `viewer`, so a partial
264
+ * failure is one source missing rather than a malformed whole.
265
+ */
266
+ export const buildInboxQueries = (options = {}) => {
267
+ const { sourcesPerQuery = INBOX_SOURCES_PER_QUERY, ...queryOptions } = options;
268
+ const sources = options.sources ?? INBOX_SOURCES;
269
+ const size = Math.max(1, Math.trunc(sourcesPerQuery));
270
+ const chunks = [];
271
+ for (let i = 0; i < sources.length; i += size)
272
+ chunks.push(sources.slice(i, i + size));
273
+ return chunks.map((chunk) => buildInboxQuery({ ...queryOptions, sources: chunk }));
274
+ };
275
+ /**
276
+ * Reassemble what `buildInboxQueries` split.
277
+ *
278
+ * The source aliases are disjoint, so they merge by assignment. `rateLimit` and
279
+ * `viewer` are not: every part carries its own.
280
+ *
281
+ * `cost` and `nodeCount` SUM, because each part really was charged separately
282
+ * and a host reporting one part's cost as the inbox's would understate it by
283
+ * four. `remaining` takes the LOWEST reading — the parts run concurrently and
284
+ * the budget only falls, so the smallest is the closest to now. `resetAt` rides
285
+ * along with it, since a window and its expiry have to describe the same
286
+ * moment.
287
+ */
288
+ export const mergeInboxData = (parts) => {
289
+ const answered = parts.filter(Boolean);
290
+ if (answered.length === 0)
291
+ return undefined;
292
+ const limits = answered
293
+ .map((part) => part.rateLimit)
294
+ .filter(Boolean);
295
+ const scarcest = limits.reduce((lowest, limit) => !lowest || limit.remaining < lowest.remaining ? limit : lowest, undefined);
296
+ return {
297
+ ...Object.assign({}, ...answered),
298
+ viewer: answered.find((part) => part.viewer)?.viewer,
299
+ ...(scarcest
300
+ ? {
301
+ rateLimit: {
302
+ ...scarcest,
303
+ cost: limits.reduce((total, l) => total + (l.cost ?? 0), 0),
304
+ nodeCount: limits.reduce((total, l) => total + (l.nodeCount ?? 0), 0),
305
+ },
306
+ }
307
+ : {}),
308
+ };
309
+ };
173
310
  //# sourceMappingURL=inbox.js.map
package/dist/inbox.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"inbox.js","sourceRoot":"","sources":["../src/inbox.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,6EAA6E;AAC7E,qCAAqC;AACrC,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,uDAAuD;AAiCvD,2EAA2E;AAC3E,4EAA4E;AAC5E,4EAA4E;AAC5E,mEAAmE;AACnE,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,8EAA8E;AAC9E,8EAA8E;AAC9E,yBAAyB;AACzB,EAAE;AACF,2EAA2E;AAC3E,8EAA8E;AAC9E,2EAA2E;AAC3E,qBAAqB;AACrB,MAAM,SAAS,GAAG;;;;;YAKN,CAAA;AAEZ,6EAA6E;AAC7E,gFAAgF;AAChF,8EAA8E;AAC9E,2DAA2D;AAC3D,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,4EAA4E;AAC5E,gFAAgF;AAChF,8EAA8E;AAC9E,+DAA+D;AAC/D,MAAM,eAAe,GAAG;;;;;;;8DAOsC,CAAA;AAE9D,MAAM,kBAAkB,GAAG;uFAC4D,CAAA;AAEvF,+EAA+E;AAC/E,qEAAqE;AACrE,MAAM,YAAY,GAAG;2CACsB,CAAA;AAE3C;;;;;GAKG;AACH,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,EAAE,CAChC,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AAEvE;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,EAC9B,IAAI,EACJ,cAAc,GAAG,EAAE,EACnB,KAAK,GAAG,MAAM,MACO,EAAE,EAAE,EAAE;IAC3B,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;IACzC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAA;IACrC,MAAM,SAAS,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAA;IAE1C;yEACqE;IACrE,MAAM,IAAI,GAAG,KAAK,KAAK,MAAM,CAAA;IAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAA;IACpC,MAAM,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,CAAA;IAChD,MAAM,iBAAiB,GAAG,IAAI,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAA;IACxD,MAAM,WAAW,GAAG,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAA;IAE5C;;;;;;;;;OASG;IACH,OAAO;;;;0BAIiB,KAAK;;;;;QAKvB,MAAM;QACN,YAAY;;;mCAGe,KAAK;;;;;QAKhC,MAAM;QACN,YAAY;;;6BAGS,KAAK;;;;;QAK1B,MAAM;QACN,YAAY;;;6BAGS,KAAK;;;;;UAKxB,iBAAiB;UACjB,WAAW;;;;UAIX,MAAM;UACN,YAAY;;;;+BAIS,KAAK,GAAG,KAAK;;;;;QAKpC,iBAAiB;QACjB,WAAW;;;mCAGgB,KAAK;;;;;QAKhC,iBAAiB;QACjB,WAAW;;;4BAGS,KAAK,GAAG,KAAK;;;;;QAKjC,MAAM;QACN,YAAY;;;iCAGa,KAAK,sCAAsC,SAAS;;;;;;;CAOpF,CAAA;AACD,CAAC,CAAA"}
1
+ {"version":3,"file":"inbox.js","sourceRoot":"","sources":["../src/inbox.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "Surface-agnostic GitHub core — gh CLI primitives, PR review comments, health, and webhook logic.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",