@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 +86 -4
- package/dist/inbox.d.ts.map +1 -1
- package/dist/inbox.js +189 -52
- package/dist/inbox.js.map +1 -1
- package/package.json +1 -1
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 **
|
|
10
|
-
* budget, at a nodeCount of ~
|
|
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.
|
|
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: (
|
|
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
|
package/dist/inbox.d.ts.map
CHANGED
|
@@ -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;
|
|
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 —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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,
|
|
1
|
+
{"version":3,"file":"inbox.js","sourceRoot":"","sources":["../src/inbox.ts"],"names":[],"mappings":"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"}
|