process-graph 1.0.5 → 1.1.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/README.md +85 -17
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +92 -0
- package/dist/cli.d.ts +512 -0
- package/dist/cli.js +3 -0
- package/dist/control-aDk9560d.js +294 -0
- package/dist/detail-panel-B1ZmMuJT.js +341 -0
- package/dist/dist-BpCBmnMl.js +582 -0
- package/dist/dist-HLlW8vgT.js +1388 -0
- package/dist/dist-IjrgWhHQ.js +3 -0
- package/dist/errors-BZe5WzCn.js +19 -0
- package/dist/exit-codes-0ultCqdS.js +23 -0
- package/dist/graphql-C1j5wQFz.d.ts +631 -0
- package/dist/index-CCfe0csl.d.ts +353 -0
- package/dist/index-CmZOswkx.d.ts +512 -0
- package/dist/index-CoA6vXqe.d.ts +404 -0
- package/dist/index-CptUHq6F.d.ts +58 -0
- package/dist/index-CrM5qWUA.d.ts +110 -0
- package/dist/index-DhbvDgK4.d.ts +1582 -0
- package/dist/index-DricKcyl.d.ts +421 -0
- package/dist/index.d.ts +111 -4
- package/dist/index.js +929 -5
- package/dist/layout-CCM-KBdp.js +307 -0
- package/dist/layout.d.ts +8 -0
- package/dist/layout.js +2 -0
- package/dist/node-AchI1T-K.js +40 -0
- package/dist/open-CyRXJ4bt.js +29 -0
- package/dist/parse-CuipC_2K.js +41 -0
- package/dist/queries-CCexJAE2.js +2202 -0
- package/dist/react-flow/style.css +658 -0
- package/dist/react-flow.d.ts +173 -0
- package/dist/react-flow.js +450 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +107 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/router-D4gtTlXm.js +188 -0
- package/dist/schema-CVnS6HiX.js +234 -0
- package/dist/selection-BW_xXWOy.js +314 -0
- package/dist/source/github/node.d.ts +16 -0
- package/dist/source/github/node.js +2 -0
- package/dist/source/github.d.ts +49 -0
- package/dist/source/github.js +201 -0
- package/dist/source-DRDpEA8Q.js +1316 -0
- package/dist/status-Bm1CGu9C.js +11 -0
- package/dist/store/node.d.ts +24 -0
- package/dist/store/node.js +2 -0
- package/dist/store/wasm/worker.d.ts +16 -0
- package/dist/store/wasm/worker.js +98 -0
- package/dist/store/wasm.d.ts +9 -0
- package/dist/store/wasm.js +11 -0
- package/dist/store-BcR70CaO.js +1287 -0
- package/dist/store.d.ts +1328 -0
- package/dist/store.js +238 -0
- package/dist/ui/style.css +351 -0
- package/dist/ui.d.ts +235 -0
- package/dist/ui.js +2 -0
- package/dist/validate-DF3mQZww.js +125 -0
- package/package.json +148 -13
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { c as deriveStatuses, i as readySet, n as criticalPath, r as impact, s as explainStatus, t as blockedBy } from "./queries-CCexJAE2.js";
|
|
2
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
+
//#region ../../ui/graph/react/dist/analysis.js
|
|
4
|
+
/**
|
|
5
|
+
* The status of every element in a run, derived by `deriveStatuses`.
|
|
6
|
+
* @param run - The model, overlay and facts.
|
|
7
|
+
* @returns Each element's status, in model order.
|
|
8
|
+
* @throws CycleError when the model has an illegal cycle, so an error boundary shows it.
|
|
9
|
+
*/
|
|
10
|
+
const useStatuses = (run) => {
|
|
11
|
+
const { model, overlay, facts } = run;
|
|
12
|
+
return useMemo(() => deriveStatuses(model, overlay, facts), [
|
|
13
|
+
model,
|
|
14
|
+
overlay,
|
|
15
|
+
facts
|
|
16
|
+
]);
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The work that can start now, by `readySet`.
|
|
20
|
+
* @param run - The model, overlay and facts.
|
|
21
|
+
* @returns The ready elements, in model order.
|
|
22
|
+
*/
|
|
23
|
+
const useReadySet = (run) => {
|
|
24
|
+
const { model, overlay, facts } = run;
|
|
25
|
+
return useMemo(() => readySet(model, overlay, facts), [
|
|
26
|
+
model,
|
|
27
|
+
overlay,
|
|
28
|
+
facts
|
|
29
|
+
]);
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Why an element has its status, by `explainStatus`.
|
|
33
|
+
* @param run - The model, overlay and facts.
|
|
34
|
+
* @param id - The element asked about, or `undefined` when nothing is.
|
|
35
|
+
* @returns The explanation, or `undefined` when `id` is.
|
|
36
|
+
* @throws UnknownNodeError when `id` is not an element of the model.
|
|
37
|
+
*/
|
|
38
|
+
const useWhyStatus = (run, id) => {
|
|
39
|
+
const { model, overlay, facts } = run;
|
|
40
|
+
return useMemo(() => id === void 0 ? void 0 : explainStatus(model, overlay, facts, id), [
|
|
41
|
+
model,
|
|
42
|
+
overlay,
|
|
43
|
+
facts,
|
|
44
|
+
id
|
|
45
|
+
]);
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The unfinished work holding an element back: the whole chain and the blockers at its end.
|
|
49
|
+
* @param run - The model, overlay and facts.
|
|
50
|
+
* @param id - The element asked about, or `undefined` when nothing is.
|
|
51
|
+
* @returns The chain and its blockers; both empty when `id` is `undefined`.
|
|
52
|
+
* @throws UnknownNodeError when `id` is not an element of the model.
|
|
53
|
+
*/
|
|
54
|
+
const useBlockedChain = (run, id) => {
|
|
55
|
+
const explanation = useWhyStatus(run, id);
|
|
56
|
+
return useMemo(() => {
|
|
57
|
+
if (explanation === void 0) return {
|
|
58
|
+
chain: [],
|
|
59
|
+
blockers: []
|
|
60
|
+
};
|
|
61
|
+
const [, ...behind] = explanation.chain;
|
|
62
|
+
return {
|
|
63
|
+
chain: explanation.chain,
|
|
64
|
+
blockers: behind.filter((link) => link.waitingOn.length === 0).map((link) => link.element)
|
|
65
|
+
};
|
|
66
|
+
}, [explanation]);
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* What every blocked element is blocked by, by `blockedBy`, for a view that shows each element's blockers beside it.
|
|
70
|
+
* @param run - The model, overlay and facts.
|
|
71
|
+
* @returns The open work at the end of each blocked element's chain, keyed by the blocked element, in model order. Elements that are not blocked are absent.
|
|
72
|
+
*/
|
|
73
|
+
const useBlockers = (run) => {
|
|
74
|
+
const statuses = useStatuses(run);
|
|
75
|
+
const { model, overlay, facts } = run;
|
|
76
|
+
return useMemo(() => new Map([...statuses].filter(([, status]) => status === "blocked").map(([id]) => [id, blockedBy(model, overlay, facts, id)])), [
|
|
77
|
+
statuses,
|
|
78
|
+
model,
|
|
79
|
+
overlay,
|
|
80
|
+
facts
|
|
81
|
+
]);
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* The live work that transitively depends on an element, by `impact`.
|
|
85
|
+
* @param run - The model, overlay and facts.
|
|
86
|
+
* @param id - The element asked about, or `undefined` when nothing is.
|
|
87
|
+
* @returns The impacted elements; empty when `id` is `undefined`.
|
|
88
|
+
* @throws UnknownNodeError when `id` is not an element of the model.
|
|
89
|
+
*/
|
|
90
|
+
const useImpact = (run, id) => {
|
|
91
|
+
const { model, overlay, facts } = run;
|
|
92
|
+
return useMemo(() => id === void 0 ? [] : impact(model, overlay, facts, id), [
|
|
93
|
+
model,
|
|
94
|
+
overlay,
|
|
95
|
+
facts,
|
|
96
|
+
id
|
|
97
|
+
]);
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Every element weighs one step, so the critical path is the longest chain of work still ahead.
|
|
101
|
+
* @returns One.
|
|
102
|
+
*/
|
|
103
|
+
const stepWeight = () => 1;
|
|
104
|
+
/**
|
|
105
|
+
* The heaviest chain through the work still ahead, by `criticalPath`.
|
|
106
|
+
* @param run - The model, overlay and facts.
|
|
107
|
+
* @param weight - The cost of an element, finite and not negative. Keep it stable between renders. Defaults to `stepWeight`.
|
|
108
|
+
* @returns The path in dependency order and its total weight; empty when nothing is live.
|
|
109
|
+
* @throws RangeError when `weight` returns a negative or non-finite number.
|
|
110
|
+
*/
|
|
111
|
+
const useCriticalPath = (run, weight = stepWeight) => {
|
|
112
|
+
const { model, overlay, facts } = run;
|
|
113
|
+
return useMemo(() => criticalPath(model, overlay, facts, weight), [
|
|
114
|
+
model,
|
|
115
|
+
overlay,
|
|
116
|
+
facts,
|
|
117
|
+
weight
|
|
118
|
+
]);
|
|
119
|
+
};
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region ../../ui/graph/react/dist/filter.js
|
|
122
|
+
/** The filter that shows every element. */
|
|
123
|
+
const emptyFilter = {
|
|
124
|
+
statuses: /* @__PURE__ */ new Set(),
|
|
125
|
+
actors: /* @__PURE__ */ new Set(),
|
|
126
|
+
sources: /* @__PURE__ */ new Set()
|
|
127
|
+
};
|
|
128
|
+
/** The separator between a source-qualified identifier's collection and its item, as in `github:org/repo#12`. */
|
|
129
|
+
const itemSeparator = "#";
|
|
130
|
+
/**
|
|
131
|
+
* The collection a source-qualified identifier belongs to: the identifier up to its item separator, so `github:org/repo#12` is in `github:org/repo`. An identifier with no item separator is its own collection.
|
|
132
|
+
* @param source - A source-qualified identifier.
|
|
133
|
+
* @returns The identifier's collection.
|
|
134
|
+
*/
|
|
135
|
+
const sourceScope = (source) => {
|
|
136
|
+
const at = source.lastIndexOf(itemSeparator);
|
|
137
|
+
return at === -1 ? source : source.slice(0, at);
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* The actors an element's work belongs to: the actor of an activity or decision, and both ends of a message.
|
|
141
|
+
* @param element - The element.
|
|
142
|
+
* @returns Its actors' participant ids.
|
|
143
|
+
*/
|
|
144
|
+
const actorsOf = (element) => {
|
|
145
|
+
if (element.kind === "message") return [element.from, element.to];
|
|
146
|
+
return (element.kind === "activity" || element.kind === "decision") && element.actor !== void 0 ? [element.actor] : [];
|
|
147
|
+
};
|
|
148
|
+
const passes = (allowed, values) => allowed.size === 0 || values.some((value) => allowed.has(value));
|
|
149
|
+
/**
|
|
150
|
+
* Whether an element passes a filter.
|
|
151
|
+
* @param element - The element.
|
|
152
|
+
* @param status - The element's status in the run.
|
|
153
|
+
* @param filter - The filter.
|
|
154
|
+
* @returns True when the element is shown.
|
|
155
|
+
*/
|
|
156
|
+
const matchesFilter = (element, status, filter) => passes(filter.statuses, [status]) && passes(filter.actors, actorsOf(element)) && passes(filter.sources, element.kind === "activity" && element.source !== void 0 ? [sourceScope(element.source)] : []);
|
|
157
|
+
/**
|
|
158
|
+
* The elements a filter shows.
|
|
159
|
+
* @param model - The model.
|
|
160
|
+
* @param statuses - Each element's status, from `useStatuses`.
|
|
161
|
+
* @param filter - The filter.
|
|
162
|
+
* @returns The shown elements' ids, in model order.
|
|
163
|
+
*/
|
|
164
|
+
const filterElements = (model, statuses, filter) => model.elements.flatMap((element) => {
|
|
165
|
+
const status = statuses.get(element.id);
|
|
166
|
+
return status !== void 0 && matchesFilter(element, status, filter) ? [element.id] : [];
|
|
167
|
+
});
|
|
168
|
+
/**
|
|
169
|
+
* The actors and sources a model's elements can be filtered by.
|
|
170
|
+
* @param model - The model.
|
|
171
|
+
* @returns The options, each labelled for display.
|
|
172
|
+
*/
|
|
173
|
+
const filterOptions = (model) => {
|
|
174
|
+
const acting = new Set(model.elements.flatMap(actorsOf));
|
|
175
|
+
const sources = new Set(model.elements.flatMap((element) => element.kind === "activity" && element.source !== void 0 ? [sourceScope(element.source)] : []));
|
|
176
|
+
return {
|
|
177
|
+
actors: model.participants.filter((participant) => acting.has(participant.id)).map((participant) => ({
|
|
178
|
+
value: participant.id,
|
|
179
|
+
label: participant.label
|
|
180
|
+
})),
|
|
181
|
+
sources: [...sources].map((source) => ({
|
|
182
|
+
value: source,
|
|
183
|
+
label: source
|
|
184
|
+
}))
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
const toggled = (set, value) => {
|
|
188
|
+
const next = new Set(set);
|
|
189
|
+
if (!next.delete(value)) next.add(value);
|
|
190
|
+
return next;
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Filter state for a view: which statuses, actors and sources to show.
|
|
194
|
+
* @param initial - The filter to start with. Defaults to `emptyFilter`.
|
|
195
|
+
* @returns The filter and its operations, each stable between renders.
|
|
196
|
+
*/
|
|
197
|
+
const useElementFilter = (initial = emptyFilter) => {
|
|
198
|
+
const [filter, setFilter] = useState(initial);
|
|
199
|
+
const toggleStatus = useCallback((status) => {
|
|
200
|
+
setFilter((current) => ({
|
|
201
|
+
...current,
|
|
202
|
+
statuses: toggled(current.statuses, status)
|
|
203
|
+
}));
|
|
204
|
+
}, []);
|
|
205
|
+
const toggleActor = useCallback((actor) => {
|
|
206
|
+
setFilter((current) => ({
|
|
207
|
+
...current,
|
|
208
|
+
actors: toggled(current.actors, actor)
|
|
209
|
+
}));
|
|
210
|
+
}, []);
|
|
211
|
+
const toggleSource = useCallback((source) => {
|
|
212
|
+
setFilter((current) => ({
|
|
213
|
+
...current,
|
|
214
|
+
sources: toggled(current.sources, source)
|
|
215
|
+
}));
|
|
216
|
+
}, []);
|
|
217
|
+
const clear = useCallback(() => {
|
|
218
|
+
setFilter(emptyFilter);
|
|
219
|
+
}, []);
|
|
220
|
+
return useMemo(() => ({
|
|
221
|
+
filter,
|
|
222
|
+
toggleStatus,
|
|
223
|
+
toggleActor,
|
|
224
|
+
toggleSource,
|
|
225
|
+
clear
|
|
226
|
+
}), [
|
|
227
|
+
filter,
|
|
228
|
+
toggleStatus,
|
|
229
|
+
toggleActor,
|
|
230
|
+
toggleSource,
|
|
231
|
+
clear
|
|
232
|
+
]);
|
|
233
|
+
};
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region ../../ui/graph/react/dist/selection.js
|
|
236
|
+
/** What each navigation key does, given the items in order and the index of the selected one (-1 when none is). */
|
|
237
|
+
const moves = {
|
|
238
|
+
ArrowDown: (count, at) => (at + 1) % count,
|
|
239
|
+
ArrowRight: (count, at) => (at + 1) % count,
|
|
240
|
+
ArrowUp: (count, at) => (at <= 0 ? count : at) - 1,
|
|
241
|
+
ArrowLeft: (count, at) => (at <= 0 ? count : at) - 1,
|
|
242
|
+
Home: () => 0,
|
|
243
|
+
End: (count) => count - 1,
|
|
244
|
+
Escape: () => void 0
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* Selection state with keyboard navigation over the elements of a view, following the listbox pattern of the WAI-ARIA Authoring Practices: the arrow keys move the selection to the next or previous element in `order`, wrapping at either end; Home and End move to the first and last; Escape clears it. Only one item is a tab stop: the selected one, or the first when none is. A keyboard move also focuses the item it selects.
|
|
248
|
+
* @param order - The navigable elements, in the order the keys move through them: the elements on show, after filtering.
|
|
249
|
+
* @param options - The initial selection or the owner's selection, a listener for changes and how to focus an item.
|
|
250
|
+
* @returns The selection, a setter, and the container and item props.
|
|
251
|
+
*/
|
|
252
|
+
const useGraphSelection = (order, options = {}) => {
|
|
253
|
+
const { initial, onChange, focus } = options;
|
|
254
|
+
const controlled = "selected" in options;
|
|
255
|
+
const [kept, setKept] = useState(initial);
|
|
256
|
+
const chosen = controlled ? options.selected : kept;
|
|
257
|
+
const items = useRef(/* @__PURE__ */ new Map());
|
|
258
|
+
const select = useCallback((id) => {
|
|
259
|
+
if (!controlled) setKept(id);
|
|
260
|
+
onChange?.(id);
|
|
261
|
+
}, [controlled, onChange]);
|
|
262
|
+
const selected = chosen !== void 0 && order.includes(chosen) ? chosen : void 0;
|
|
263
|
+
const stop = selected ?? order[0];
|
|
264
|
+
const onKeyDown = useCallback((event) => {
|
|
265
|
+
const move = moves[event.key];
|
|
266
|
+
if (move === void 0 || order.length === 0) return;
|
|
267
|
+
event.preventDefault();
|
|
268
|
+
const index = move(order.length, selected === void 0 ? -1 : order.indexOf(selected));
|
|
269
|
+
const next = index === void 0 ? void 0 : order[index];
|
|
270
|
+
select(next);
|
|
271
|
+
if (next === void 0) return;
|
|
272
|
+
if (focus === void 0) items.current.get(next)?.focus();
|
|
273
|
+
else focus(next);
|
|
274
|
+
}, [
|
|
275
|
+
order,
|
|
276
|
+
selected,
|
|
277
|
+
select,
|
|
278
|
+
focus
|
|
279
|
+
]);
|
|
280
|
+
const itemProps = useCallback((id) => ({
|
|
281
|
+
role: "option",
|
|
282
|
+
ref: (element) => {
|
|
283
|
+
if (element !== null) items.current.set(id, element);
|
|
284
|
+
return () => {
|
|
285
|
+
items.current.delete(id);
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
tabIndex: id === stop ? 0 : -1,
|
|
289
|
+
"aria-selected": id === selected,
|
|
290
|
+
onClick: () => {
|
|
291
|
+
select(id);
|
|
292
|
+
}
|
|
293
|
+
}), [
|
|
294
|
+
selected,
|
|
295
|
+
stop,
|
|
296
|
+
select
|
|
297
|
+
]);
|
|
298
|
+
return useMemo(() => ({
|
|
299
|
+
selected,
|
|
300
|
+
select,
|
|
301
|
+
containerProps: {
|
|
302
|
+
role: "listbox",
|
|
303
|
+
onKeyDown
|
|
304
|
+
},
|
|
305
|
+
itemProps
|
|
306
|
+
}), [
|
|
307
|
+
selected,
|
|
308
|
+
select,
|
|
309
|
+
onKeyDown,
|
|
310
|
+
itemProps
|
|
311
|
+
]);
|
|
312
|
+
};
|
|
313
|
+
//#endregion
|
|
314
|
+
export { filterOptions as a, useElementFilter as c, useBlockers as d, useCriticalPath as f, useWhyStatus as g, useStatuses as h, filterElements as i, stepWeight as l, useReadySet as m, actorsOf as n, matchesFilter as o, useImpact as p, emptyFilter as r, sourceScope as s, useGraphSelection as t, useBlockedChain as u };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { t as GitHubTokenPort } from "../../graphql-C1j5wQFz.js";
|
|
2
|
+
//#region ../../adapters/graph/source-github-issues/dist/node.d.ts
|
|
3
|
+
/** Where `githubTokenFromEnvironment` looks. Both are ports, so a test supplies its own. */
|
|
4
|
+
export interface EnvironmentTokenOptions {
|
|
5
|
+
/** The environment to read `GITHUB_TOKEN` from; `process.env` when absent. */
|
|
6
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
7
|
+
/** Runs `gh auth token`; the GitHub CLI when absent. */
|
|
8
|
+
readonly runGh?: () => Promise<string>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A token port for Node that uses `GITHUB_TOKEN` when it is set and not empty, and otherwise the token the GitHub CLI holds (`gh auth token`), asked for once and reused. The same token serves every owner. The token is never logged, and an error from the CLI is reported without its output.
|
|
12
|
+
* @param options - The environment and the CLI runner, for tests.
|
|
13
|
+
* @returns The token port.
|
|
14
|
+
*/
|
|
15
|
+
export declare const githubTokenFromEnvironment: (options?: EnvironmentTokenOptions) => GitHubTokenPort;
|
|
16
|
+
//#endregion
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { $ as scopeRootsInputSchema, A as ScopeEntry, B as badRequestReasons, C as Gap, D as Relation, E as OrderField, F as SourceParticipant, G as orderDirections, H as filterStates, I as SourceTransition, J as relationKinds, K as orderFields, L as SyncFilter, M as ScopeListingInput, N as ScopeRootsInput, O as RelationKind, P as SourceDescription, Q as scopeListingSchema, R as SyncInput, S as FilterState, T as OrderDirection, U as gapReasons, V as boundActivitySchema, W as gapSchema, X as scopeChildrenInputSchema, Y as relationSchema, Z as scopeEntrySchema, _ as GraphSourceFailure, a as FakePage, b as BadRequestReason, c as assembleSync, d as GraphSourceRouter, et as sourceDescriptionSchema, f as GraphSourceScopes, g as GraphSourceError, h as createGraphSourceRouter, i as FakeGraphSourceOptions, it as transitionSchema, j as ScopeListing, k as ScopeChildrenInput, l as ChildrenRequest, m as PageRequest, n as GraphqlFetch, nt as syncInputSchema, o as createFakeGraphSource, p as ListingRequest, q as participantSchema, r as githubGraphqlEndpoint, rt as syncPageSchema, s as AssembledSync, t as GitHubTokenPort, tt as syncFilterSchema, u as GraphSourcePager, v as GraphSourceClient, w as GapReason, x as BoundActivity, y as graphSourceContract, z as SyncPage } from "../graphql-C1j5wQFz.js";
|
|
2
|
+
import { a as GitHubIssuesSourceOptions, i as storedTokenPort, n as StoredTokenPortOptions, o as createGitHubIssuesSource, r as selectStoredToken, t as StoredGitHubToken } from "../index-CptUHq6F.js";
|
|
3
|
+
//#region ../../core/graph/source-contract/dist/paginate.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Pages through a cursor-linked sequence on demand: each `next()` reads one page, starting after `first`, and the sequence ends after the page whose `next` is absent. Nothing is read ahead of the consumer, so a consumer that stops early stops the reads.
|
|
6
|
+
* @param first - The cursor to start after, or `undefined` for the first page.
|
|
7
|
+
* @param read - Reads the page after a cursor.
|
|
8
|
+
* @returns The pages, in order.
|
|
9
|
+
*/
|
|
10
|
+
export declare const paginate: <Page extends {
|
|
11
|
+
readonly next?: string;
|
|
12
|
+
}>(first: string | undefined, read: (after: string | undefined) => Promise<Page>) => AsyncIterable<Page>;
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region ../../adapters/graph/source-github-issues/dist/identifiers.d.ts
|
|
15
|
+
/** The prefix of every identifier this adapter emits and every scope it accepts. */
|
|
16
|
+
export declare const githubSource = "github";
|
|
17
|
+
/** What one scope identifier selects. */
|
|
18
|
+
type Scope = {
|
|
19
|
+
readonly kind: "owner";
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly owner: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: "repository";
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly owner: string;
|
|
26
|
+
readonly name: string;
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: "project";
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly owner: string;
|
|
31
|
+
readonly number: number;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Parses a scope identifier: `github:owner` selects every repository the account owns, `github:owner/repo` one repository, and `github:owner/projects/number` the issues on an organisation's or user's Projects v2 board. A repository name cannot hold a slash, so the three forms never overlap.
|
|
35
|
+
* @param id - The scope identifier.
|
|
36
|
+
* @returns The parsed scope.
|
|
37
|
+
* @throws GraphSourceError with `bad-request` and `invalid-scope` when it is none of the forms.
|
|
38
|
+
*/
|
|
39
|
+
export declare const parseScope: (id: string) => Scope;
|
|
40
|
+
/** The scope identifier of an owner: `github:owner`. */
|
|
41
|
+
export declare const ownerScopeId: (login: string) => string;
|
|
42
|
+
/** The identifier of an issue: `github:owner/repo#number`, which is also its element identifier. */
|
|
43
|
+
export declare const issueId: (owner: string, name: string, number: number) => string;
|
|
44
|
+
/** The identifier of a repository participant: `github:owner/repo`. */
|
|
45
|
+
export declare const repositoryId: (owner: string, name: string) => string;
|
|
46
|
+
/** The identifier of an account participant, organisation or user: `github:@login`. The `@` keeps it apart from the `github:owner` scope syntax. */
|
|
47
|
+
export declare const accountId: (login: string) => string;
|
|
48
|
+
//#endregion
|
|
49
|
+
export { type AssembledSync, type BadRequestReason, type BoundActivity, type ChildrenRequest, type FakeGraphSourceOptions, type FakePage, type FilterState, type Gap, type GapReason, type GitHubIssuesSourceOptions, type GitHubTokenPort, type GraphSourceClient, GraphSourceError, type GraphSourceFailure, type GraphSourcePager, type GraphSourceRouter, type GraphSourceScopes, type GraphqlFetch, type ListingRequest, type OrderDirection, type OrderField, type PageRequest, type Relation, type RelationKind, type Scope, type ScopeChildrenInput, type ScopeEntry, type ScopeListing, type ScopeListingInput, type ScopeRootsInput, type SourceDescription, type SourceParticipant, type SourceTransition, type StoredGitHubToken, type StoredTokenPortOptions, type SyncFilter, type SyncInput, type SyncPage, assembleSync, badRequestReasons, boundActivitySchema, createFakeGraphSource, createGitHubIssuesSource, createGraphSourceRouter, filterStates, gapReasons, gapSchema, githubGraphqlEndpoint, graphSourceContract, orderDirections, orderFields, participantSchema, relationKinds, relationSchema, scopeChildrenInputSchema, scopeEntrySchema, scopeListingSchema, scopeRootsInputSchema, selectStoredToken, sourceDescriptionSchema, storedTokenPort, syncFilterSchema, syncInputSchema, syncPageSchema, transitionSchema };
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { A as syncPageSchema, C as scopeChildrenInputSchema, D as sourceDescriptionSchema, E as scopeRootsInputSchema, O as syncFilterSchema, S as relationSchema, T as scopeListingSchema, _ as gapSchema, a as ownerScopeId, b as participantSchema, c as githubGraphqlEndpoint, d as paginate, f as graphSourceContract, g as gapReasons, h as filterStates, i as issueId, j as transitionSchema, k as syncInputSchema, l as assembleSync, m as boundActivitySchema, n as accountId, o as parseScope, p as badRequestReasons, r as githubSource, s as repositoryId, t as createGitHubIssuesSource, u as createGraphSourceRouter, v as orderDirections, w as scopeEntrySchema, x as relationKinds, y as orderFields } from "../source-DRDpEA8Q.js";
|
|
2
|
+
import { t as GraphSourceError } from "../errors-BZe5WzCn.js";
|
|
3
|
+
//#region ../../core/graph/source-contract/dist/fake.js
|
|
4
|
+
const gapIndex = -1;
|
|
5
|
+
const cursorPattern = /^(?<scope>[^@;]+)@(?<index>-1|0|[1-9]\d*)(?:;(?<filter>.+))?$/u;
|
|
6
|
+
/** A filter as one string with its fields in a fixed order, so the same filter always gives the same cursor and two filters compare by value. */
|
|
7
|
+
const filterKey = (filter) => filter === void 0 ? void 0 : encodeURIComponent(JSON.stringify([
|
|
8
|
+
filter.states ?? null,
|
|
9
|
+
filter.labels ?? null,
|
|
10
|
+
filter.order === void 0 ? null : [filter.order.field, filter.order.direction]
|
|
11
|
+
]));
|
|
12
|
+
const encodeCursor = (position, filter) => `${encodeURIComponent(position.scope)}@${String(position.index)}${filter === void 0 ? "" : `;${filter}`}`;
|
|
13
|
+
/**
|
|
14
|
+
* Applies a filter to a page as though the source had listed only the activities it keeps: an activity is kept when its run state is one of `states` and it carries one of `labels`. The fake's activities carry no labels, so a filter naming labels keeps none. What a dropped activity was part of (transitions, relations, run state) is dropped with it. `order` does not change the fake's fixed page order.
|
|
15
|
+
*/
|
|
16
|
+
const filtered = (page, filter) => {
|
|
17
|
+
if (filter === void 0) return page;
|
|
18
|
+
const { states, labels } = filter;
|
|
19
|
+
const kept = new Set(page.activities.filter((activity) => {
|
|
20
|
+
const state = page.overlay[activity.id];
|
|
21
|
+
return (states === void 0 || state !== void 0 && states.some((wanted) => wanted === state)) && labels === void 0;
|
|
22
|
+
}).map((activity) => activity.id));
|
|
23
|
+
const dropped = new Set(page.activities.map((activity) => activity.id).filter((id) => !kept.has(id)));
|
|
24
|
+
return {
|
|
25
|
+
...page,
|
|
26
|
+
activities: page.activities.filter((activity) => kept.has(activity.id)),
|
|
27
|
+
transitions: page.transitions.filter((transition) => !dropped.has(transition.source) && !dropped.has(transition.target)),
|
|
28
|
+
relations: page.relations.filter((relation) => !dropped.has(relation.from) && !dropped.has(relation.to)),
|
|
29
|
+
overlay: Object.fromEntries(Object.entries(page.overlay).filter(([id]) => !dropped.has(id)))
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
/** The cursor of a listing page: which listing (`roots`, or the encoded parent scope, which always holds an encoded colon) and the offset of the next entry. */
|
|
33
|
+
const listingCursorPattern = /^(?<listing>[^@]+)@(?<offset>[1-9]\d*)$/u;
|
|
34
|
+
const rootsListing = "roots";
|
|
35
|
+
const samePosition = (a, b) => a.scope === b.scope && a.index === b.index;
|
|
36
|
+
/**
|
|
37
|
+
* The reference in-memory `GraphSourcePager`: a fixed set of pages per scope, served in scope order. A scope that is well formed but unknown yields one page holding a `not-found` gap, so a sync over several scopes still returns the rest. Cursors encode the scope identifier, page index and filter, so they are stable, resume in any instance built from the same pages, and are refused under a different filter. A filter is applied to each page as described at `filtered`.
|
|
38
|
+
*
|
|
39
|
+
* Its scope listing serves `roots` and `children` from the options, `listingPageSize` entries a page.
|
|
40
|
+
* @param options - The source prefix and the pages of each scope.
|
|
41
|
+
* @returns A pager for `createGraphSourceRouter`.
|
|
42
|
+
*/
|
|
43
|
+
const createFakeGraphSource = (options) => {
|
|
44
|
+
const { source, scopes } = options;
|
|
45
|
+
const prefix = `${source}:`;
|
|
46
|
+
const positionsOf = (scope) => [...new Set(scope)].flatMap((id) => {
|
|
47
|
+
if (!id.startsWith(prefix) || id.length === prefix.length) throw new GraphSourceError({
|
|
48
|
+
kind: "bad-request",
|
|
49
|
+
message: `"${id}" is not a ${source} scope identifier`,
|
|
50
|
+
reason: "invalid-scope",
|
|
51
|
+
value: id
|
|
52
|
+
});
|
|
53
|
+
const pages = scopes[id];
|
|
54
|
+
if (pages === void 0) return [{
|
|
55
|
+
scope: id,
|
|
56
|
+
index: gapIndex
|
|
57
|
+
}];
|
|
58
|
+
return pages.length === 0 ? [{
|
|
59
|
+
scope: id,
|
|
60
|
+
index: 0
|
|
61
|
+
}] : pages.map((_, index) => ({
|
|
62
|
+
scope: id,
|
|
63
|
+
index
|
|
64
|
+
}));
|
|
65
|
+
});
|
|
66
|
+
const startOf = (positions, cursor, filter) => {
|
|
67
|
+
if (cursor === void 0) return 0;
|
|
68
|
+
const groups = cursorPattern.exec(cursor)?.groups;
|
|
69
|
+
const after = groups?.scope === void 0 || groups.index === void 0 || groups.filter !== filter ? void 0 : {
|
|
70
|
+
scope: decodeURIComponent(groups.scope),
|
|
71
|
+
index: Number(groups.index)
|
|
72
|
+
};
|
|
73
|
+
const found = after === void 0 ? -1 : positions.findIndex((position) => samePosition(position, after));
|
|
74
|
+
if (found === -1 || found === positions.length - 1) throw new GraphSourceError({
|
|
75
|
+
kind: "bad-request",
|
|
76
|
+
message: `"${cursor}" is not a cursor for this scope`,
|
|
77
|
+
reason: "invalid-cursor",
|
|
78
|
+
value: cursor
|
|
79
|
+
});
|
|
80
|
+
return found + 1;
|
|
81
|
+
};
|
|
82
|
+
const pageAt = (position) => {
|
|
83
|
+
const page = scopes[position.scope]?.[position.index];
|
|
84
|
+
if (page !== void 0) return page;
|
|
85
|
+
return {
|
|
86
|
+
participants: [],
|
|
87
|
+
activities: [],
|
|
88
|
+
transitions: [],
|
|
89
|
+
relations: [],
|
|
90
|
+
overlay: {},
|
|
91
|
+
gaps: position.index === gapIndex ? [{
|
|
92
|
+
target: position.scope,
|
|
93
|
+
reason: "not-found",
|
|
94
|
+
message: `${position.scope} does not exist`
|
|
95
|
+
}] : []
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
const read = ({ scope, cursor, signal, filter }) => {
|
|
99
|
+
signal?.throwIfAborted();
|
|
100
|
+
const key = filterKey(filter);
|
|
101
|
+
const positions = positionsOf(scope);
|
|
102
|
+
const start = startOf(positions, cursor, key);
|
|
103
|
+
const position = positions[start];
|
|
104
|
+
if (position === void 0) throw new Error("unreachable: startOf returns an index within the positions");
|
|
105
|
+
const page = filtered(pageAt(position), filter);
|
|
106
|
+
return start === positions.length - 1 ? page : {
|
|
107
|
+
...page,
|
|
108
|
+
next: encodeCursor(position, key)
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
const listingPageSize = options.listingPageSize;
|
|
112
|
+
if (listingPageSize !== void 0 && (!Number.isInteger(listingPageSize) || listingPageSize < 1)) throw new RangeError(`listingPageSize must be a positive integer, not ${String(listingPageSize)}`);
|
|
113
|
+
const roots = options.roots ?? [];
|
|
114
|
+
const children = options.children ?? {};
|
|
115
|
+
/** One page of `entries` from the cursor's offset, after checking the cursor belongs to `listing`. */
|
|
116
|
+
const listingPage = (listing, entries, { cursor, signal }) => {
|
|
117
|
+
signal?.throwIfAborted();
|
|
118
|
+
const size = listingPageSize ?? Math.max(entries.length, 1);
|
|
119
|
+
const groups = cursor === void 0 ? void 0 : listingCursorPattern.exec(cursor)?.groups;
|
|
120
|
+
const offset = cursor === void 0 ? 0 : groups?.listing === listing && groups.offset !== void 0 ? Number(groups.offset) : void 0;
|
|
121
|
+
if (offset === void 0 || offset % size !== 0 || offset > 0 && offset >= entries.length) throw new GraphSourceError({
|
|
122
|
+
kind: "bad-request",
|
|
123
|
+
message: `"${String(cursor)}" is not a cursor for this listing`,
|
|
124
|
+
reason: "invalid-cursor",
|
|
125
|
+
value: String(cursor)
|
|
126
|
+
});
|
|
127
|
+
const end = offset + size;
|
|
128
|
+
const page = {
|
|
129
|
+
entries: entries.slice(offset, end),
|
|
130
|
+
gaps: []
|
|
131
|
+
};
|
|
132
|
+
return end < entries.length ? {
|
|
133
|
+
...page,
|
|
134
|
+
next: `${listing}@${String(end)}`
|
|
135
|
+
} : page;
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
source,
|
|
139
|
+
page: async (request) => Promise.resolve(read(request)),
|
|
140
|
+
scopes: {
|
|
141
|
+
roots: async (request) => Promise.resolve(request).then(() => listingPage(rootsListing, roots, request)),
|
|
142
|
+
children: async (request) => Promise.resolve(request).then(() => {
|
|
143
|
+
const { scope } = request;
|
|
144
|
+
if (!scope.startsWith(prefix) || scope.length === prefix.length) throw new GraphSourceError({
|
|
145
|
+
kind: "bad-request",
|
|
146
|
+
message: `"${scope}" is not a ${source} scope identifier`,
|
|
147
|
+
reason: "invalid-scope",
|
|
148
|
+
value: scope
|
|
149
|
+
});
|
|
150
|
+
if (!(Object.hasOwn(children, scope) || Object.hasOwn(scopes, scope) || roots.some((entry) => entry.scope === scope)) && request.cursor === void 0) {
|
|
151
|
+
request.signal?.throwIfAborted();
|
|
152
|
+
return {
|
|
153
|
+
entries: [],
|
|
154
|
+
gaps: [{
|
|
155
|
+
target: scope,
|
|
156
|
+
reason: "not-found",
|
|
157
|
+
message: `${scope} does not exist`
|
|
158
|
+
}]
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return listingPage(encodeURIComponent(scope), children[scope] ?? [], request);
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region ../../adapters/graph/source-github-issues/dist/tokens.js
|
|
168
|
+
/** Orders tokens most recently used first, a token never used last. ISO 8601 instants in UTC order as text. */
|
|
169
|
+
const byRecency = (left, right) => (right.lastUsedAt ?? "").localeCompare(left.lastUsedAt ?? "");
|
|
170
|
+
/**
|
|
171
|
+
* Picks the stored token for a request about one owner. A token scoped to that owner wins over one scoped to any owner; among owner-scoped tokens the one naming the fewest owners wins, being the most specific, then the most recently used; among tokens for any owner the most recently used wins. A request about the token's own holder (`owner` undefined) can only use a token for any owner. Logins match without regard to case, as GitHub's do.
|
|
172
|
+
* @param tokens - The stored tokens.
|
|
173
|
+
* @param owner - The login the request is about, or `undefined` for the token's own holder.
|
|
174
|
+
* @returns The token to use, or `undefined` when none fits.
|
|
175
|
+
*/
|
|
176
|
+
const selectStoredToken = (tokens, owner) => {
|
|
177
|
+
const login = owner?.toLowerCase();
|
|
178
|
+
const [best] = tokens.flatMap((candidate) => candidate.owners !== "any" && login !== void 0 && candidate.owners.some((named) => named.toLowerCase() === login) ? [{
|
|
179
|
+
candidate,
|
|
180
|
+
breadth: candidate.owners.length
|
|
181
|
+
}] : []).sort((left, right) => left.breadth - right.breadth || byRecency(left.candidate, right.candidate));
|
|
182
|
+
if (best !== void 0) return best.candidate;
|
|
183
|
+
return tokens.filter((candidate) => candidate.owners === "any").sort(byRecency).at(0);
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* A token port over tokens a host keeps, choosing one per request with `selectStoredToken`.
|
|
187
|
+
* @param options - How to read the stored tokens, and how to report one being used.
|
|
188
|
+
* @returns The token port.
|
|
189
|
+
* @throws GraphSourceError with `unauthorised`, from the port, when no stored token fits the owner; the message names the owner and never a token.
|
|
190
|
+
*/
|
|
191
|
+
const storedTokenPort = (options) => async (owner, signal) => {
|
|
192
|
+
const chosen = selectStoredToken(await options.list(signal), owner);
|
|
193
|
+
if (chosen === void 0) throw new GraphSourceError({
|
|
194
|
+
kind: "unauthorised",
|
|
195
|
+
message: owner === void 0 ? "no stored GitHub token serves any owner, which a request about the token's holder needs" : `no stored GitHub token is for ${owner} or for any owner`
|
|
196
|
+
});
|
|
197
|
+
options.used?.(chosen);
|
|
198
|
+
return chosen.token;
|
|
199
|
+
};
|
|
200
|
+
//#endregion
|
|
201
|
+
export { GraphSourceError, accountId, assembleSync, badRequestReasons, boundActivitySchema, createFakeGraphSource, createGitHubIssuesSource, createGraphSourceRouter, filterStates, gapReasons, gapSchema, githubGraphqlEndpoint, githubSource, graphSourceContract, issueId, orderDirections, orderFields, ownerScopeId, paginate, parseScope, participantSchema, relationKinds, relationSchema, repositoryId, scopeChildrenInputSchema, scopeEntrySchema, scopeListingSchema, scopeRootsInputSchema, selectStoredToken, sourceDescriptionSchema, storedTokenPort, syncFilterSchema, syncInputSchema, syncPageSchema, transitionSchema };
|